More cleanup, more refractor, more energy, more passion, more footwork
This commit is contained in:
@@ -3,9 +3,9 @@ testdata_dir = "testdata"
|
||||
tmp_dir = "tmp"
|
||||
|
||||
[build]
|
||||
args_bin = []
|
||||
args_bin = ["--config", "data/"]
|
||||
bin = "./tmp/main"
|
||||
cmd = "bash -c 'VERSION=$(git describe --tags --always --abbrev=0 2>/dev/null || echo dev) && go build -ldflags \"-X github.com/sirrobot01/debrid-blackhole/pkg/version.Version=$VERSION -X github.com/sirrobot01/debrid-blackhole/pkg/version.Channel=beta\" -o ./tmp/main .'"
|
||||
cmd = "bash -c 'go build -ldflags \"-X github.com/sirrobot01/debrid-blackhole/pkg/version.Version=0.0.4 -X github.com/sirrobot01/debrid-blackhole/pkg/version.Channel=beta\" -o ./tmp/main .'"
|
||||
delay = 1000
|
||||
exclude_dir = ["assets", "tmp", "vendor", "testdata", "data"]
|
||||
exclude_file = []
|
||||
|
||||
14
Dockerfile
14
Dockerfile
@@ -1,10 +1,10 @@
|
||||
# Stage 1: Build binaries
|
||||
FROM --platform=$BUILDPLATFORM golang:1.22-alpine as builder
|
||||
FROM --platform=$BUILDPLATFORM golang:1.23-alpine as builder
|
||||
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ARG VERSION
|
||||
ARG CHANNEL
|
||||
ARG VERSION=0.0.0
|
||||
ARG CHANNEL=dev
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -47,8 +47,8 @@ LABEL org.opencontainers.image.authors = "sirrobot01"
|
||||
LABEL org.opencontainers.image.documentation = "https://github.com/sirrobot01/debrid-blackhole/blob/main/README.md"
|
||||
|
||||
# Copy binaries
|
||||
COPY --from=builder --chown=nonroot:nonroot /blackhole /blackhole
|
||||
COPY --from=builder --chown=nonroot:nonroot /healthcheck /healthcheck
|
||||
COPY --from=builder --chown=nonroot:nonroot /blackhole /usr/bin/blackhole
|
||||
COPY --from=builder --chown=nonroot:nonroot /healthcheck /usr/bin/healthcheck
|
||||
|
||||
# Copy pre-made directory structure
|
||||
COPY --from=dirsetup --chown=nonroot:nonroot /data /data
|
||||
@@ -59,6 +59,6 @@ EXPOSE 8181 8282
|
||||
VOLUME ["/data", "/app"]
|
||||
USER nonroot:nonroot
|
||||
|
||||
HEALTHCHECK CMD ["/healthcheck"]
|
||||
HEALTHCHECK CMD ["/usr/bin/healthcheck"]
|
||||
|
||||
CMD ["/blackhole", "--config", "/data"]
|
||||
CMD ["/usr/bin/blackhole", "--config", "/data/config.json"]
|
||||
53
cmd/main.go
53
cmd/main.go
@@ -4,57 +4,68 @@ import (
|
||||
"context"
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/config"
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/logger"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/arr"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/debrid"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/proxy"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/qbit"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/repair"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/server"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/service"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/version"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/web"
|
||||
"log"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func Start(ctx context.Context) error {
|
||||
cfg := config.GetConfig()
|
||||
var wg sync.WaitGroup
|
||||
errChan := make(chan error)
|
||||
|
||||
_log := logger.GetLogger(cfg.LogLevel)
|
||||
|
||||
_log.Info().Msgf("Version: %s", version.GetInfo().String())
|
||||
_log.Debug().Msgf("Config Loaded: %s", cfg.JsonFile())
|
||||
_log.Debug().Msgf("Default Log Level: %s", cfg.LogLevel)
|
||||
|
||||
_log.Info().Msgf("Version: %s", version.GetInfo().String())
|
||||
svc := service.New()
|
||||
_qbit := qbit.New()
|
||||
_proxy := proxy.NewProxy()
|
||||
srv := server.New()
|
||||
webRoutes := web.New(_qbit).Routes()
|
||||
qbitRoutes := _qbit.Routes()
|
||||
|
||||
deb := debrid.NewDebrid()
|
||||
arrs := arr.NewStorage()
|
||||
_repair := repair.NewRepair(deb.Get(), arrs)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
errChan := make(chan error, 2)
|
||||
// Register routes
|
||||
srv.Mount("/", webRoutes)
|
||||
srv.Mount("/api/v2", qbitRoutes)
|
||||
|
||||
if cfg.Proxy.Enabled {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := proxy.NewProxy(deb).Start(ctx); err != nil {
|
||||
errChan <- err
|
||||
}
|
||||
}()
|
||||
}
|
||||
if cfg.QBitTorrent.Port != "" {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := qbit.Start(ctx, deb, arrs, _repair); err != nil {
|
||||
if err := _proxy.Start(ctx); err != nil {
|
||||
errChan <- err
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := srv.Start(ctx); err != nil {
|
||||
errChan <- err
|
||||
}
|
||||
|
||||
}()
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
_qbit.StartWorker(ctx)
|
||||
}()
|
||||
|
||||
if cfg.Repair.Enabled {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := _repair.Start(ctx); err != nil {
|
||||
if err := svc.Repair.Start(ctx); err != nil {
|
||||
log.Printf("Error during repair: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
6
go.mod
6
go.mod
@@ -11,8 +11,8 @@ require (
|
||||
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2
|
||||
github.com/go-chi/chi/v5 v5.1.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/sessions v1.4.0
|
||||
github.com/rs/zerolog v1.33.0
|
||||
github.com/valyala/fasthttp v1.55.0
|
||||
github.com/valyala/fastjson v1.6.4
|
||||
golang.org/x/crypto v0.33.0
|
||||
golang.org/x/time v0.8.0
|
||||
@@ -22,20 +22,16 @@ require (
|
||||
require (
|
||||
github.com/anacrolix/missinggo v1.3.0 // indirect
|
||||
github.com/anacrolix/missinggo/v2 v2.7.3 // indirect
|
||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
github.com/gorilla/sessions v1.4.0 // indirect
|
||||
github.com/huandu/xstrings v1.3.2 // indirect
|
||||
github.com/klauspost/compress v1.17.11 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
||||
github.com/stretchr/testify v1.10.0 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
|
||||
16
go.sum
16
go.sum
@@ -35,8 +35,6 @@ github.com/anacrolix/tagflag v1.0.0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pm
|
||||
github.com/anacrolix/tagflag v1.1.0/go.mod h1:Scxs9CV10NQatSmbyjqmqmeQNwGzlNe0CMUMIxqHIG8=
|
||||
github.com/anacrolix/torrent v1.55.0 h1:s9yh/YGdPmbN9dTa+0Inh2dLdrLQRvEAj1jdFW/Hdd8=
|
||||
github.com/anacrolix/torrent v1.55.0/go.mod h1:sBdZHBSZNj4de0m+EbYg7vvs/G/STubxu/GzzNbojsE=
|
||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||
github.com/benbjohnson/immutable v0.2.0/go.mod h1:uc6OHo6PN2++n98KHLxW8ef4W42ylHiQSENghE1ezxI=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
@@ -107,6 +105,8 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
@@ -132,8 +132,6 @@ github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVY
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
@@ -210,10 +208,6 @@ github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf
|
||||
github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||
github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8k8=
|
||||
github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRVfcxxoHiOM=
|
||||
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
|
||||
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
|
||||
github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
||||
@@ -223,8 +217,6 @@ go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
@@ -265,14 +257,10 @@ golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
|
||||
|
||||
@@ -113,9 +113,9 @@ func (c *Config) loadConfig() error {
|
||||
// Load the auth file
|
||||
c.Auth = c.GetAuth()
|
||||
|
||||
// Validate the config
|
||||
//Validate the config
|
||||
//if err := validateConfig(c); err != nil {
|
||||
// return nil, err
|
||||
// return err
|
||||
//}
|
||||
|
||||
return nil
|
||||
@@ -211,7 +211,11 @@ func GetConfig() *Config {
|
||||
once.Do(func() {
|
||||
instance = &Config{} // Initialize instance first
|
||||
if err := instance.loadConfig(); err != nil {
|
||||
panic(err)
|
||||
_, err := fmt.Fprintf(os.Stderr, "Configuration Error: %v\n", err)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
})
|
||||
return instance
|
||||
|
||||
@@ -3,6 +3,7 @@ package logger
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/config"
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -16,11 +17,8 @@ var (
|
||||
)
|
||||
|
||||
func GetLogPath() string {
|
||||
logsDir := os.Getenv("LOG_PATH")
|
||||
if logsDir == "" {
|
||||
// Create the logs directory if it doesn't exist
|
||||
logsDir = "logs"
|
||||
}
|
||||
cfg := config.GetConfig()
|
||||
logsDir := filepath.Join(cfg.Path, "logs")
|
||||
|
||||
if err := os.MkdirAll(logsDir, 0755); err != nil {
|
||||
panic(fmt.Sprintf("Failed to create logs directory: %v", err))
|
||||
@@ -33,7 +31,7 @@ func NewLogger(prefix string, level string, output *os.File) zerolog.Logger {
|
||||
|
||||
rotatingLogFile := &lumberjack.Logger{
|
||||
Filename: GetLogPath(),
|
||||
MaxSize: 10,
|
||||
MaxSize: 2,
|
||||
MaxBackups: 2,
|
||||
MaxAge: 28,
|
||||
Compress: true,
|
||||
@@ -87,7 +85,7 @@ func NewLogger(prefix string, level string, output *os.File) zerolog.Logger {
|
||||
|
||||
func GetLogger(level string) zerolog.Logger {
|
||||
once.Do(func() {
|
||||
logger = NewLogger("Decypharr", level, os.Stdout)
|
||||
logger = NewLogger("decypharr", level, os.Stdout)
|
||||
})
|
||||
return logger
|
||||
}
|
||||
|
||||
@@ -93,19 +93,25 @@ func (c *RLHTTPClient) MakeRequest(req *http.Request) ([]byte, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b, _ := io.ReadAll(res.Body)
|
||||
statusOk := strconv.Itoa(res.StatusCode)[0] == '2'
|
||||
if !statusOk {
|
||||
// Add status code error to the body
|
||||
b = append(b, []byte(fmt.Sprintf("\nstatus code: %d", res.StatusCode))...)
|
||||
return nil, fmt.Errorf(string(b))
|
||||
}
|
||||
|
||||
defer func(Body io.ReadCloser) {
|
||||
err := Body.Close()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}(res.Body)
|
||||
|
||||
b, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
statusOk := res.StatusCode == http.StatusOK || res.StatusCode == http.StatusCreated
|
||||
if !statusOk {
|
||||
// Add status code error to the body
|
||||
b = append(b, []byte(fmt.Sprintf("\nstatus code: %d", res.StatusCode))...)
|
||||
return nil, fmt.Errorf(string(b))
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,271 +0,0 @@
|
||||
package debrid
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/sirrobot01/debrid-blackhole/common"
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/config"
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/logger"
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/request"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/debrid/types"
|
||||
"net/http"
|
||||
gourl "net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type AllDebrid struct {
|
||||
BaseDebrid
|
||||
}
|
||||
|
||||
func (r *AllDebrid) GetMountPath() string {
|
||||
return r.MountPath
|
||||
}
|
||||
|
||||
func (r *AllDebrid) GetName() string {
|
||||
return r.Name
|
||||
}
|
||||
|
||||
func (r *AllDebrid) GetLogger() zerolog.Logger {
|
||||
return r.logger
|
||||
}
|
||||
|
||||
func (r *AllDebrid) IsAvailable(infohashes []string) map[string]bool {
|
||||
// Check if the infohashes are available in the local cache
|
||||
hashes, result := GetLocalCache(infohashes, r.cache)
|
||||
|
||||
if len(hashes) == 0 {
|
||||
// Either all the infohashes are locally cached or none are
|
||||
r.cache.AddMultiple(result)
|
||||
return result
|
||||
}
|
||||
|
||||
// Divide hashes into groups of 100
|
||||
// AllDebrid does not support checking cached infohashes
|
||||
return result
|
||||
}
|
||||
|
||||
func (r *AllDebrid) SubmitMagnet(torrent *Torrent) (*Torrent, error) {
|
||||
url := fmt.Sprintf("%s/magnet/upload", r.Host)
|
||||
query := gourl.Values{}
|
||||
query.Add("magnets[]", torrent.Magnet.Link)
|
||||
url += "?" + query.Encode()
|
||||
req, _ := http.NewRequest(http.MethodGet, url, nil)
|
||||
resp, err := r.client.MakeRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var data types.AllDebridUploadMagnetResponse
|
||||
err = json.Unmarshal(resp, &data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
magnets := data.Data.Magnets
|
||||
if len(magnets) == 0 {
|
||||
return nil, fmt.Errorf("error adding torrent")
|
||||
}
|
||||
magnet := magnets[0]
|
||||
torrentId := strconv.Itoa(magnet.ID)
|
||||
r.logger.Info().Msgf("Torrent: %s added with id: %s", torrent.Name, torrentId)
|
||||
torrent.Id = torrentId
|
||||
|
||||
return torrent, nil
|
||||
}
|
||||
|
||||
func getAlldebridStatus(statusCode int) string {
|
||||
switch {
|
||||
case statusCode == 4:
|
||||
return "downloaded"
|
||||
case statusCode >= 0 && statusCode <= 3:
|
||||
return "downloading"
|
||||
default:
|
||||
return "error"
|
||||
}
|
||||
}
|
||||
|
||||
func flattenFiles(files []types.AllDebridMagnetFile, parentPath string, index *int) []TorrentFile {
|
||||
result := make([]TorrentFile, 0)
|
||||
|
||||
cfg := config.GetConfig()
|
||||
|
||||
for _, f := range files {
|
||||
currentPath := f.Name
|
||||
if parentPath != "" {
|
||||
currentPath = filepath.Join(parentPath, f.Name)
|
||||
}
|
||||
|
||||
if f.Elements != nil {
|
||||
// This is a folder, recurse into it
|
||||
result = append(result, flattenFiles(f.Elements, currentPath, index)...)
|
||||
} else {
|
||||
// This is a file
|
||||
fileName := filepath.Base(f.Name)
|
||||
if common.RegexMatch(common.SAMPLEMATCH, fileName) {
|
||||
continue
|
||||
}
|
||||
if !cfg.IsAllowedFile(fileName) {
|
||||
continue
|
||||
}
|
||||
|
||||
if !cfg.IsSizeAllowed(f.Size) {
|
||||
continue
|
||||
}
|
||||
|
||||
*index++
|
||||
file := TorrentFile{
|
||||
Id: strconv.Itoa(*index),
|
||||
Name: fileName,
|
||||
Size: f.Size,
|
||||
Path: currentPath,
|
||||
}
|
||||
result = append(result, file)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (r *AllDebrid) GetTorrent(id string) (*Torrent, error) {
|
||||
torrent := &Torrent{}
|
||||
url := fmt.Sprintf("%s/magnet/status?id=%s", r.Host, id)
|
||||
req, _ := http.NewRequest(http.MethodGet, url, nil)
|
||||
resp, err := r.client.MakeRequest(req)
|
||||
if err != nil {
|
||||
return torrent, err
|
||||
}
|
||||
var res types.AllDebridTorrentInfoResponse
|
||||
err = json.Unmarshal(resp, &res)
|
||||
if err != nil {
|
||||
r.logger.Info().Msgf("Error unmarshalling torrent info: %s", err)
|
||||
return torrent, err
|
||||
}
|
||||
data := res.Data.Magnets
|
||||
status := getAlldebridStatus(data.StatusCode)
|
||||
name := data.Filename
|
||||
torrent.Id = id
|
||||
torrent.Name = name
|
||||
torrent.Status = status
|
||||
torrent.Filename = name
|
||||
torrent.OriginalFilename = name
|
||||
torrent.Folder = name
|
||||
if status == "downloaded" {
|
||||
torrent.Bytes = data.Size
|
||||
|
||||
torrent.Progress = float64((data.Downloaded / data.Size) * 100)
|
||||
torrent.Speed = data.DownloadSpeed
|
||||
torrent.Seeders = data.Seeders
|
||||
index := -1
|
||||
files := flattenFiles(data.Files, "", &index)
|
||||
parentFolder := data.Filename
|
||||
if data.NbLinks == 1 {
|
||||
// All debrid doesn't return the parent folder for single file torrents
|
||||
parentFolder = ""
|
||||
}
|
||||
torrent.OriginalFilename = parentFolder
|
||||
torrent.Files = files
|
||||
}
|
||||
torrent.Debrid = r
|
||||
return torrent, nil
|
||||
}
|
||||
|
||||
func (r *AllDebrid) CheckStatus(torrent *Torrent, isSymlink bool) (*Torrent, error) {
|
||||
for {
|
||||
tb, err := r.GetTorrent(torrent.Id)
|
||||
|
||||
torrent = tb
|
||||
|
||||
if err != nil || tb == nil {
|
||||
return tb, err
|
||||
}
|
||||
status := torrent.Status
|
||||
if status == "downloaded" {
|
||||
r.logger.Info().Msgf("Torrent: %s downloaded", torrent.Name)
|
||||
if !isSymlink {
|
||||
err = r.GetDownloadLinks(torrent)
|
||||
if err != nil {
|
||||
return torrent, err
|
||||
}
|
||||
}
|
||||
break
|
||||
} else if status == "downloading" {
|
||||
if !r.DownloadUncached {
|
||||
go torrent.Delete()
|
||||
return torrent, fmt.Errorf("torrent: %s not cached", torrent.Name)
|
||||
}
|
||||
// Break out of the loop if the torrent is downloading.
|
||||
// This is necessary to prevent infinite loop since we moved to sync downloading and async processing
|
||||
break
|
||||
} else {
|
||||
return torrent, fmt.Errorf("torrent: %s has error", torrent.Name)
|
||||
}
|
||||
|
||||
}
|
||||
return torrent, nil
|
||||
}
|
||||
|
||||
func (r *AllDebrid) DeleteTorrent(torrent *Torrent) {
|
||||
url := fmt.Sprintf("%s/magnet/delete?id=%s", r.Host, torrent.Id)
|
||||
req, _ := http.NewRequest(http.MethodGet, url, nil)
|
||||
_, err := r.client.MakeRequest(req)
|
||||
if err == nil {
|
||||
r.logger.Info().Msgf("Torrent: %s deleted", torrent.Name)
|
||||
} else {
|
||||
r.logger.Info().Msgf("Error deleting torrent: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *AllDebrid) GetDownloadLinks(torrent *Torrent) error {
|
||||
downloadLinks := make([]TorrentDownloadLinks, 0)
|
||||
for _, file := range torrent.Files {
|
||||
url := fmt.Sprintf("%s/link/unlock", r.Host)
|
||||
query := gourl.Values{}
|
||||
query.Add("link", file.Link)
|
||||
url += "?" + query.Encode()
|
||||
req, _ := http.NewRequest(http.MethodGet, url, nil)
|
||||
resp, err := r.client.MakeRequest(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var data types.AllDebridDownloadLink
|
||||
if err = json.Unmarshal(resp, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
link := data.Data.Link
|
||||
|
||||
dl := TorrentDownloadLinks{
|
||||
Link: file.Link,
|
||||
Filename: data.Data.Filename,
|
||||
DownloadLink: link,
|
||||
}
|
||||
downloadLinks = append(downloadLinks, dl)
|
||||
}
|
||||
torrent.DownloadLinks = downloadLinks
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *AllDebrid) GetCheckCached() bool {
|
||||
return r.CheckCached
|
||||
}
|
||||
|
||||
func NewAllDebrid(dc config.Debrid, cache *common.Cache) *AllDebrid {
|
||||
rl := request.ParseRateLimit(dc.RateLimit)
|
||||
headers := map[string]string{
|
||||
"Authorization": fmt.Sprintf("Bearer %s", dc.APIKey),
|
||||
}
|
||||
client := request.NewRLHTTPClient(rl, headers)
|
||||
return &AllDebrid{
|
||||
BaseDebrid: BaseDebrid{
|
||||
Name: "alldebrid",
|
||||
Host: dc.Host,
|
||||
APIKey: dc.APIKey,
|
||||
DownloadUncached: dc.DownloadUncached,
|
||||
client: client,
|
||||
cache: cache,
|
||||
MountPath: dc.Folder,
|
||||
logger: logger.NewLogger(dc.Name, config.GetConfig().LogLevel, os.Stdout),
|
||||
CheckCached: dc.CheckCached,
|
||||
},
|
||||
}
|
||||
}
|
||||
306
pkg/debrid/alldebrid/alldebrid.go
Normal file
306
pkg/debrid/alldebrid/alldebrid.go
Normal file
@@ -0,0 +1,306 @@
|
||||
package alldebrid
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/sirrobot01/debrid-blackhole/common"
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/config"
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/logger"
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/request"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/debrid/torrent"
|
||||
|
||||
"net/http"
|
||||
gourl "net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type AllDebrid struct {
|
||||
Name string
|
||||
Host string `json:"host"`
|
||||
APIKey string
|
||||
DownloadUncached bool
|
||||
client *request.RLHTTPClient
|
||||
cache *common.Cache
|
||||
MountPath string
|
||||
logger zerolog.Logger
|
||||
CheckCached bool
|
||||
}
|
||||
|
||||
func (ad *AllDebrid) GetMountPath() string {
|
||||
return ad.MountPath
|
||||
}
|
||||
|
||||
func (ad *AllDebrid) GetName() string {
|
||||
return ad.Name
|
||||
}
|
||||
|
||||
func (ad *AllDebrid) GetLogger() zerolog.Logger {
|
||||
return ad.logger
|
||||
}
|
||||
|
||||
func (ad *AllDebrid) IsAvailable(infohashes []string) map[string]bool {
|
||||
// Check if the infohashes are available in the local cache
|
||||
hashes, result := torrent.GetLocalCache(infohashes, ad.cache)
|
||||
|
||||
if len(hashes) == 0 {
|
||||
// Either all the infohashes are locally cached or none are
|
||||
ad.cache.AddMultiple(result)
|
||||
return result
|
||||
}
|
||||
|
||||
// Divide hashes into groups of 100
|
||||
// AllDebrid does not support checking cached infohashes
|
||||
return result
|
||||
}
|
||||
|
||||
func (ad *AllDebrid) SubmitMagnet(torrent *torrent.Torrent) (*torrent.Torrent, error) {
|
||||
url := fmt.Sprintf("%s/magnet/upload", ad.Host)
|
||||
query := gourl.Values{}
|
||||
query.Add("magnets[]", torrent.Magnet.Link)
|
||||
url += "?" + query.Encode()
|
||||
req, _ := http.NewRequest(http.MethodGet, url, nil)
|
||||
resp, err := ad.client.MakeRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var data UploadMagnetResponse
|
||||
err = json.Unmarshal(resp, &data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
magnets := data.Data.Magnets
|
||||
if len(magnets) == 0 {
|
||||
return nil, fmt.Errorf("error adding torrent")
|
||||
}
|
||||
magnet := magnets[0]
|
||||
torrentId := strconv.Itoa(magnet.ID)
|
||||
ad.logger.Info().Msgf("Torrent: %s added with id: %s", torrent.Name, torrentId)
|
||||
torrent.Id = torrentId
|
||||
|
||||
return torrent, nil
|
||||
}
|
||||
|
||||
func getAlldebridStatus(statusCode int) string {
|
||||
switch {
|
||||
case statusCode == 4:
|
||||
return "downloaded"
|
||||
case statusCode >= 0 && statusCode <= 3:
|
||||
return "downloading"
|
||||
default:
|
||||
return "error"
|
||||
}
|
||||
}
|
||||
|
||||
func flattenFiles(files []MagnetFile, parentPath string, index *int) []torrent.File {
|
||||
result := make([]torrent.File, 0)
|
||||
|
||||
cfg := config.GetConfig()
|
||||
|
||||
for _, f := range files {
|
||||
currentPath := f.Name
|
||||
if parentPath != "" {
|
||||
currentPath = filepath.Join(parentPath, f.Name)
|
||||
}
|
||||
|
||||
if f.Elements != nil {
|
||||
// This is a folder, recurse into it
|
||||
result = append(result, flattenFiles(f.Elements, currentPath, index)...)
|
||||
} else {
|
||||
// This is a file
|
||||
fileName := filepath.Base(f.Name)
|
||||
if common.RegexMatch(common.SAMPLEMATCH, fileName) {
|
||||
continue
|
||||
}
|
||||
if !cfg.IsAllowedFile(fileName) {
|
||||
continue
|
||||
}
|
||||
|
||||
if !cfg.IsSizeAllowed(f.Size) {
|
||||
continue
|
||||
}
|
||||
|
||||
*index++
|
||||
file := torrent.File{
|
||||
Id: strconv.Itoa(*index),
|
||||
Name: fileName,
|
||||
Size: f.Size,
|
||||
Path: currentPath,
|
||||
}
|
||||
result = append(result, file)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (ad *AllDebrid) GetTorrent(id string) (*torrent.Torrent, error) {
|
||||
t := &torrent.Torrent{}
|
||||
url := fmt.Sprintf("%s/magnet/status?id=%s", ad.Host, id)
|
||||
req, _ := http.NewRequest(http.MethodGet, url, nil)
|
||||
resp, err := ad.client.MakeRequest(req)
|
||||
if err != nil {
|
||||
return t, err
|
||||
}
|
||||
var res TorrentInfoResponse
|
||||
err = json.Unmarshal(resp, &res)
|
||||
if err != nil {
|
||||
ad.logger.Info().Msgf("Error unmarshalling torrent info: %s", err)
|
||||
return t, err
|
||||
}
|
||||
data := res.Data.Magnets
|
||||
status := getAlldebridStatus(data.StatusCode)
|
||||
name := data.Filename
|
||||
t.Id = id
|
||||
t.Name = name
|
||||
t.Status = status
|
||||
t.Filename = name
|
||||
t.OriginalFilename = name
|
||||
t.Folder = name
|
||||
t.MountPath = ad.MountPath
|
||||
t.Debrid = ad.Name
|
||||
t.DownloadLinks = make(map[string]torrent.DownloadLinks)
|
||||
if status == "downloaded" {
|
||||
t.Bytes = data.Size
|
||||
|
||||
t.Progress = float64((data.Downloaded / data.Size) * 100)
|
||||
t.Speed = data.DownloadSpeed
|
||||
t.Seeders = data.Seeders
|
||||
index := -1
|
||||
files := flattenFiles(data.Files, "", &index)
|
||||
parentFolder := data.Filename
|
||||
if data.NbLinks == 1 {
|
||||
// All debrid doesn't return the parent folder for single file torrents
|
||||
parentFolder = ""
|
||||
}
|
||||
t.OriginalFilename = parentFolder
|
||||
t.Files = files
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (ad *AllDebrid) CheckStatus(torrent *torrent.Torrent, isSymlink bool) (*torrent.Torrent, error) {
|
||||
for {
|
||||
tb, err := ad.GetTorrent(torrent.Id)
|
||||
|
||||
torrent = tb
|
||||
|
||||
if err != nil || tb == nil {
|
||||
return tb, err
|
||||
}
|
||||
status := torrent.Status
|
||||
if status == "downloaded" {
|
||||
ad.logger.Info().Msgf("Torrent: %s downloaded", torrent.Name)
|
||||
if !isSymlink {
|
||||
err = ad.GetDownloadLinks(torrent)
|
||||
if err != nil {
|
||||
return torrent, err
|
||||
}
|
||||
}
|
||||
break
|
||||
} else if status == "downloading" {
|
||||
if !ad.DownloadUncached {
|
||||
go ad.DeleteTorrent(torrent)
|
||||
return torrent, fmt.Errorf("torrent: %s not cached", torrent.Name)
|
||||
}
|
||||
// Break out of the loop if the torrent is downloading.
|
||||
// This is necessary to prevent infinite loop since we moved to sync downloading and async processing
|
||||
break
|
||||
} else {
|
||||
return torrent, fmt.Errorf("torrent: %s has error", torrent.Name)
|
||||
}
|
||||
|
||||
}
|
||||
return torrent, nil
|
||||
}
|
||||
|
||||
func (ad *AllDebrid) DeleteTorrent(torrent *torrent.Torrent) {
|
||||
url := fmt.Sprintf("%s/magnet/delete?id=%s", ad.Host, torrent.Id)
|
||||
req, _ := http.NewRequest(http.MethodGet, url, nil)
|
||||
_, err := ad.client.MakeRequest(req)
|
||||
if err == nil {
|
||||
ad.logger.Info().Msgf("Torrent: %s deleted", torrent.Name)
|
||||
} else {
|
||||
ad.logger.Info().Msgf("Error deleting torrent: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (ad *AllDebrid) GetDownloadLinks(t *torrent.Torrent) error {
|
||||
downloadLinks := make(map[string]torrent.DownloadLinks)
|
||||
for _, file := range t.Files {
|
||||
url := fmt.Sprintf("%s/link/unlock", ad.Host)
|
||||
query := gourl.Values{}
|
||||
query.Add("link", file.Link)
|
||||
url += "?" + query.Encode()
|
||||
req, _ := http.NewRequest(http.MethodGet, url, nil)
|
||||
resp, err := ad.client.MakeRequest(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var data DownloadLink
|
||||
if err = json.Unmarshal(resp, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
link := data.Data.Link
|
||||
|
||||
dl := torrent.DownloadLinks{
|
||||
Link: file.Link,
|
||||
Filename: data.Data.Filename,
|
||||
DownloadLink: link,
|
||||
}
|
||||
downloadLinks[file.Id] = dl
|
||||
}
|
||||
t.DownloadLinks = downloadLinks
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ad *AllDebrid) GetDownloadLink(t *torrent.Torrent, file *torrent.File) *torrent.DownloadLinks {
|
||||
url := fmt.Sprintf("%s/link/unlock", ad.Host)
|
||||
query := gourl.Values{}
|
||||
query.Add("link", file.Link)
|
||||
url += "?" + query.Encode()
|
||||
req, _ := http.NewRequest(http.MethodGet, url, nil)
|
||||
resp, err := ad.client.MakeRequest(req)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var data DownloadLink
|
||||
if err = json.Unmarshal(resp, &data); err != nil {
|
||||
return nil
|
||||
}
|
||||
link := data.Data.Link
|
||||
return &torrent.DownloadLinks{
|
||||
DownloadLink: link,
|
||||
Link: file.Link,
|
||||
Filename: data.Data.Filename,
|
||||
}
|
||||
}
|
||||
|
||||
func (ad *AllDebrid) GetCheckCached() bool {
|
||||
return ad.CheckCached
|
||||
}
|
||||
|
||||
func (ad *AllDebrid) GetTorrents() ([]*torrent.Torrent, error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
func New(dc config.Debrid, cache *common.Cache) *AllDebrid {
|
||||
rl := request.ParseRateLimit(dc.RateLimit)
|
||||
headers := map[string]string{
|
||||
"Authorization": fmt.Sprintf("Bearer %s", dc.APIKey),
|
||||
}
|
||||
client := request.NewRLHTTPClient(rl, headers)
|
||||
return &AllDebrid{
|
||||
Name: "alldebrid",
|
||||
Host: dc.Host,
|
||||
APIKey: dc.APIKey,
|
||||
DownloadUncached: dc.DownloadUncached,
|
||||
client: client,
|
||||
cache: cache,
|
||||
MountPath: dc.Folder,
|
||||
logger: logger.NewLogger(dc.Name, config.GetConfig().LogLevel, os.Stdout),
|
||||
CheckCached: dc.CheckCached,
|
||||
}
|
||||
}
|
||||
75
pkg/debrid/alldebrid/types.go
Normal file
75
pkg/debrid/alldebrid/types.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package alldebrid
|
||||
|
||||
type errorResponse struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type MagnetFile struct {
|
||||
Name string `json:"n"`
|
||||
Size int64 `json:"s"`
|
||||
Link string `json:"l"`
|
||||
Elements []MagnetFile `json:"e"`
|
||||
}
|
||||
type magnetInfo struct {
|
||||
Id int `json:"id"`
|
||||
Filename string `json:"filename"`
|
||||
Size int64 `json:"size"`
|
||||
Hash string `json:"hash"`
|
||||
Status string `json:"status"`
|
||||
StatusCode int `json:"statusCode"`
|
||||
UploadDate int `json:"uploadDate"`
|
||||
Downloaded int64 `json:"downloaded"`
|
||||
Uploaded int64 `json:"uploaded"`
|
||||
DownloadSpeed int64 `json:"downloadSpeed"`
|
||||
UploadSpeed int64 `json:"uploadSpeed"`
|
||||
Seeders int `json:"seeders"`
|
||||
CompletionDate int `json:"completionDate"`
|
||||
Type string `json:"type"`
|
||||
Notified bool `json:"notified"`
|
||||
Version int `json:"version"`
|
||||
NbLinks int `json:"nbLinks"`
|
||||
Files []MagnetFile `json:"files"`
|
||||
}
|
||||
|
||||
type TorrentInfoResponse struct {
|
||||
Status string `json:"status"`
|
||||
Data struct {
|
||||
Magnets magnetInfo `json:"magnets"`
|
||||
} `json:"data"`
|
||||
Error *errorResponse `json:"error"`
|
||||
}
|
||||
|
||||
type UploadMagnetResponse struct {
|
||||
Status string `json:"status"`
|
||||
Data struct {
|
||||
Magnets []struct {
|
||||
Magnet string `json:"magnet"`
|
||||
Hash string `json:"hash"`
|
||||
Name string `json:"name"`
|
||||
FilenameOriginal string `json:"filename_original"`
|
||||
Size int64 `json:"size"`
|
||||
Ready bool `json:"ready"`
|
||||
ID int `json:"id"`
|
||||
} `json:"magnets"`
|
||||
}
|
||||
Error *errorResponse `json:"error"`
|
||||
}
|
||||
|
||||
type DownloadLink struct {
|
||||
Status string `json:"status"`
|
||||
Data struct {
|
||||
Link string `json:"link"`
|
||||
Host string `json:"host"`
|
||||
Filename string `json:"filename"`
|
||||
Streaming []interface{} `json:"streaming"`
|
||||
Paws bool `json:"paws"`
|
||||
Filesize int `json:"filesize"`
|
||||
Id string `json:"id"`
|
||||
Path []struct {
|
||||
Name string `json:"n"`
|
||||
Size int `json:"s"`
|
||||
} `json:"path"`
|
||||
} `json:"data"`
|
||||
Error *errorResponse `json:"error"`
|
||||
}
|
||||
360
pkg/debrid/cache/cache.go
vendored
Normal file
360
pkg/debrid/cache/cache.go
vendored
Normal file
@@ -0,0 +1,360 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/logger"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/config"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/debrid/engine"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/debrid/torrent"
|
||||
)
|
||||
|
||||
type DownloadLinkCache struct {
|
||||
Link string `json:"download_link"`
|
||||
}
|
||||
|
||||
type CachedTorrent struct {
|
||||
*torrent.Torrent
|
||||
LastRead time.Time `json:"last_read"`
|
||||
IsComplete bool `json:"is_complete"`
|
||||
DownloadLinks map[string]DownloadLinkCache `json:"download_links"`
|
||||
}
|
||||
|
||||
var (
|
||||
_logInstance zerolog.Logger
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
func getLogger() zerolog.Logger {
|
||||
once.Do(func() {
|
||||
_logInstance = logger.NewLogger("cache", "info", os.Stdout)
|
||||
})
|
||||
return _logInstance
|
||||
}
|
||||
|
||||
type Cache struct {
|
||||
dir string
|
||||
client engine.Service
|
||||
torrents *sync.Map // key: torrent.Id, value: *CachedTorrent
|
||||
torrentsNames *sync.Map // key: torrent.Name, value: torrent.Id
|
||||
LastUpdated time.Time `json:"last_updated"`
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
caches map[string]*Cache
|
||||
}
|
||||
|
||||
func NewManager(debridService *engine.Engine) *Manager {
|
||||
cfg := config.GetConfig()
|
||||
cm := &Manager{
|
||||
caches: make(map[string]*Cache),
|
||||
}
|
||||
for _, debrid := range debridService.GetDebrids() {
|
||||
c := New(debrid, cfg.Path)
|
||||
cm.caches[debrid.GetName()] = c
|
||||
}
|
||||
return cm
|
||||
}
|
||||
|
||||
func (m *Manager) GetCaches() map[string]*Cache {
|
||||
return m.caches
|
||||
}
|
||||
|
||||
func (m *Manager) GetCache(debridName string) *Cache {
|
||||
return m.caches[debridName]
|
||||
}
|
||||
|
||||
func New(debridService engine.Service, basePath string) *Cache {
|
||||
return &Cache{
|
||||
dir: filepath.Join(basePath, "cache", debridService.GetName(), "torrents"),
|
||||
torrents: &sync.Map{},
|
||||
torrentsNames: &sync.Map{},
|
||||
client: debridService,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) Start() error {
|
||||
_logger := getLogger()
|
||||
_logger.Info().Msg("Starting cache for: " + c.client.GetName())
|
||||
if err := c.Load(); err != nil {
|
||||
return fmt.Errorf("failed to load cache: %v", err)
|
||||
}
|
||||
if err := c.Sync(); err != nil {
|
||||
return fmt.Errorf("failed to sync cache: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cache) Load() error {
|
||||
_logger := getLogger()
|
||||
|
||||
if err := os.MkdirAll(c.dir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||
}
|
||||
|
||||
files, err := os.ReadDir(c.dir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read cache directory: %w", err)
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if file.IsDir() || filepath.Ext(file.Name()) != ".json" {
|
||||
continue
|
||||
}
|
||||
|
||||
filePath := filepath.Join(c.dir, file.Name())
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
_logger.Debug().Err(err).Msgf("Failed to read file: %s", filePath)
|
||||
continue
|
||||
}
|
||||
|
||||
var ct CachedTorrent
|
||||
if err := json.Unmarshal(data, &ct); err != nil {
|
||||
_logger.Debug().Err(err).Msgf("Failed to unmarshal file: %s", filePath)
|
||||
continue
|
||||
}
|
||||
if len(ct.Files) > 0 {
|
||||
c.torrents.Store(ct.Torrent.Id, &ct)
|
||||
c.torrentsNames.Store(ct.Torrent.Name, ct.Torrent.Id)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cache) GetTorrent(id string) *CachedTorrent {
|
||||
if value, ok := c.torrents.Load(id); ok {
|
||||
return value.(*CachedTorrent)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cache) GetTorrentByName(name string) *CachedTorrent {
|
||||
if id, ok := c.torrentsNames.Load(name); ok {
|
||||
return c.GetTorrent(id.(string))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cache) SaveTorrent(ct *CachedTorrent) error {
|
||||
data, err := json.MarshalIndent(ct, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal torrent: %w", err)
|
||||
}
|
||||
|
||||
fileName := ct.Torrent.Id + ".json"
|
||||
filePath := filepath.Join(c.dir, fileName)
|
||||
tmpFile := filePath + ".tmp"
|
||||
|
||||
f, err := os.Create(tmpFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temp file: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
w := bufio.NewWriter(f)
|
||||
if _, err := w.Write(data); err != nil {
|
||||
return fmt.Errorf("failed to write data: %w", err)
|
||||
}
|
||||
|
||||
if err := w.Flush(); err != nil {
|
||||
return fmt.Errorf("failed to flush data: %w", err)
|
||||
}
|
||||
|
||||
return os.Rename(tmpFile, filePath)
|
||||
}
|
||||
|
||||
func (c *Cache) SaveAll() error {
|
||||
const batchSize = 100
|
||||
var wg sync.WaitGroup
|
||||
_logger := getLogger()
|
||||
|
||||
tasks := make(chan *CachedTorrent, batchSize)
|
||||
|
||||
for i := 0; i < runtime.NumCPU(); i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for ct := range tasks {
|
||||
if err := c.SaveTorrent(ct); err != nil {
|
||||
_logger.Error().Err(err).Msg("failed to save torrent")
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
c.torrents.Range(func(_, value interface{}) bool {
|
||||
tasks <- value.(*CachedTorrent)
|
||||
return true
|
||||
})
|
||||
|
||||
close(tasks)
|
||||
wg.Wait()
|
||||
c.LastUpdated = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cache) Sync() error {
|
||||
_logger := getLogger()
|
||||
torrents, err := c.client.GetTorrents()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to sync torrents: %v", err)
|
||||
}
|
||||
|
||||
workers := runtime.NumCPU() * 200
|
||||
workChan := make(chan *torrent.Torrent, len(torrents))
|
||||
errChan := make(chan error, len(torrents))
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for i := 0; i < workers; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for t := range workChan {
|
||||
if err := c.processTorrent(t); err != nil {
|
||||
errChan <- err
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
for _, t := range torrents {
|
||||
workChan <- t
|
||||
}
|
||||
close(workChan)
|
||||
|
||||
wg.Wait()
|
||||
close(errChan)
|
||||
|
||||
for err := range errChan {
|
||||
_logger.Error().Err(err).Msg("sync error")
|
||||
}
|
||||
|
||||
_logger.Info().Msgf("Synced %d torrents", len(torrents))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cache) processTorrent(t *torrent.Torrent) error {
|
||||
if existing, ok := c.torrents.Load(t.Id); ok {
|
||||
ct := existing.(*CachedTorrent)
|
||||
if ct.IsComplete {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
c.AddTorrent(t)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cache) AddTorrent(t *torrent.Torrent) {
|
||||
_logger := getLogger()
|
||||
|
||||
if len(t.Files) == 0 {
|
||||
tNew, err := c.client.GetTorrent(t.Id)
|
||||
_logger.Debug().Msgf("Getting torrent files for %s", t.Id)
|
||||
if err != nil {
|
||||
_logger.Debug().Msgf("Failed to get torrent files for %s: %v", t.Id, err)
|
||||
return
|
||||
}
|
||||
t = tNew
|
||||
}
|
||||
|
||||
if len(t.Files) == 0 {
|
||||
_logger.Debug().Msgf("No files found for %s", t.Id)
|
||||
return
|
||||
}
|
||||
|
||||
ct := &CachedTorrent{
|
||||
Torrent: t,
|
||||
LastRead: time.Now(),
|
||||
IsComplete: len(t.Files) > 0,
|
||||
DownloadLinks: make(map[string]DownloadLinkCache),
|
||||
}
|
||||
|
||||
c.torrents.Store(t.Id, ct)
|
||||
c.torrentsNames.Store(t.Name, t.Id)
|
||||
|
||||
go func() {
|
||||
if err := c.SaveTorrent(ct); err != nil {
|
||||
_logger.Debug().Err(err).Msgf("Failed to save torrent %s", t.Id)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (c *Cache) RefreshTorrent(torrentId string) *CachedTorrent {
|
||||
_logger := getLogger()
|
||||
|
||||
t, err := c.client.GetTorrent(torrentId)
|
||||
if err != nil {
|
||||
_logger.Debug().Msgf("Failed to get torrent files for %s: %v", torrentId, err)
|
||||
return nil
|
||||
}
|
||||
if len(t.Files) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
ct := &CachedTorrent{
|
||||
Torrent: t,
|
||||
LastRead: time.Now(),
|
||||
IsComplete: len(t.Files) > 0,
|
||||
DownloadLinks: make(map[string]DownloadLinkCache),
|
||||
}
|
||||
|
||||
c.torrents.Store(t.Id, ct)
|
||||
c.torrentsNames.Store(t.Name, t.Id)
|
||||
|
||||
go func() {
|
||||
if err := c.SaveTorrent(ct); err != nil {
|
||||
_logger.Debug().Err(err).Msgf("Failed to save torrent %s", t.Id)
|
||||
}
|
||||
}()
|
||||
|
||||
return ct
|
||||
}
|
||||
|
||||
func (c *Cache) GetFileDownloadLink(t *CachedTorrent, file *torrent.File) (string, error) {
|
||||
_logger := getLogger()
|
||||
|
||||
if linkCache, ok := t.DownloadLinks[file.Id]; ok {
|
||||
return linkCache.Link, nil
|
||||
}
|
||||
|
||||
if file.Link == "" {
|
||||
t = c.RefreshTorrent(t.Id)
|
||||
if t == nil {
|
||||
return "", fmt.Errorf("torrent not found")
|
||||
}
|
||||
file = t.Torrent.GetFile(file.Id)
|
||||
}
|
||||
|
||||
_logger.Debug().Msgf("Getting download link for %s", t.Name)
|
||||
link := c.client.GetDownloadLink(t.Torrent, file)
|
||||
if link == nil {
|
||||
return "", fmt.Errorf("download link not found")
|
||||
}
|
||||
|
||||
t.DownloadLinks[file.Id] = DownloadLinkCache{
|
||||
Link: link.DownloadLink,
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err := c.SaveTorrent(t); err != nil {
|
||||
_logger.Debug().Err(err).Msgf("Failed to save torrent %s", t.Id)
|
||||
}
|
||||
}()
|
||||
|
||||
return link.DownloadLink, nil
|
||||
}
|
||||
|
||||
func (c *Cache) GetTorrents() *sync.Map {
|
||||
return c.torrents
|
||||
}
|
||||
@@ -3,45 +3,22 @@ package debrid
|
||||
import (
|
||||
"cmp"
|
||||
"fmt"
|
||||
"github.com/anacrolix/torrent/metainfo"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/sirrobot01/debrid-blackhole/common"
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/config"
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/request"
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/utils"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/arr"
|
||||
"path/filepath"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/debrid/alldebrid"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/debrid/debrid_link"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/debrid/engine"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/debrid/realdebrid"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/debrid/torbox"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/debrid/torrent"
|
||||
)
|
||||
|
||||
type BaseDebrid struct {
|
||||
Name string
|
||||
Host string `json:"host"`
|
||||
APIKey string
|
||||
DownloadUncached bool
|
||||
client *request.RLHTTPClient
|
||||
cache *common.Cache
|
||||
MountPath string
|
||||
logger zerolog.Logger
|
||||
CheckCached bool
|
||||
}
|
||||
|
||||
type Service interface {
|
||||
SubmitMagnet(torrent *Torrent) (*Torrent, error)
|
||||
CheckStatus(torrent *Torrent, isSymlink bool) (*Torrent, error)
|
||||
GetDownloadLinks(torrent *Torrent) error
|
||||
DeleteTorrent(torrent *Torrent)
|
||||
IsAvailable(infohashes []string) map[string]bool
|
||||
GetMountPath() string
|
||||
GetCheckCached() bool
|
||||
GetTorrent(id string) (*Torrent, error)
|
||||
GetName() string
|
||||
GetLogger() zerolog.Logger
|
||||
}
|
||||
|
||||
func NewDebrid() *DebridService {
|
||||
func New() *engine.Engine {
|
||||
cfg := config.GetConfig()
|
||||
maxCachedSize := cmp.Or(cfg.MaxCacheSize, 1000)
|
||||
debrids := make([]Service, 0)
|
||||
debrids := make([]engine.Service, 0)
|
||||
// Divide the cache size by the number of debrids
|
||||
maxCacheSize := maxCachedSize / len(cfg.Debrids)
|
||||
|
||||
@@ -51,108 +28,27 @@ func NewDebrid() *DebridService {
|
||||
logger.Info().Msg("Debrid Service started")
|
||||
debrids = append(debrids, d)
|
||||
}
|
||||
d := &DebridService{debrids: debrids, lastUsed: 0}
|
||||
d := &engine.Engine{Debrids: debrids, LastUsed: 0}
|
||||
return d
|
||||
}
|
||||
|
||||
func createDebrid(dc config.Debrid, cache *common.Cache) Service {
|
||||
func createDebrid(dc config.Debrid, cache *common.Cache) engine.Service {
|
||||
switch dc.Name {
|
||||
case "realdebrid":
|
||||
return NewRealDebrid(dc, cache)
|
||||
return realdebrid.New(dc, cache)
|
||||
case "torbox":
|
||||
return NewTorbox(dc, cache)
|
||||
return torbox.New(dc, cache)
|
||||
case "debridlink":
|
||||
return NewDebridLink(dc, cache)
|
||||
return debrid_link.New(dc, cache)
|
||||
case "alldebrid":
|
||||
return NewAllDebrid(dc, cache)
|
||||
return alldebrid.New(dc, cache)
|
||||
default:
|
||||
return NewRealDebrid(dc, cache)
|
||||
return realdebrid.New(dc, cache)
|
||||
}
|
||||
}
|
||||
|
||||
func GetTorrentInfo(filePath string) (*Torrent, error) {
|
||||
// Open and read the .torrent file
|
||||
if filepath.Ext(filePath) == ".torrent" {
|
||||
return getTorrentInfo(filePath)
|
||||
} else {
|
||||
return torrentFromMagnetFile(filePath)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func torrentFromMagnetFile(filePath string) (*Torrent, error) {
|
||||
magnetLink := utils.OpenMagnetFile(filePath)
|
||||
magnet, err := utils.GetMagnetInfo(magnetLink)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
torrent := &Torrent{
|
||||
InfoHash: magnet.InfoHash,
|
||||
Name: magnet.Name,
|
||||
Size: magnet.Size,
|
||||
Magnet: magnet,
|
||||
Filename: filePath,
|
||||
}
|
||||
return torrent, nil
|
||||
}
|
||||
|
||||
func getTorrentInfo(filePath string) (*Torrent, error) {
|
||||
mi, err := metainfo.LoadFromFile(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hash := mi.HashInfoBytes()
|
||||
infoHash := hash.HexString()
|
||||
info, err := mi.UnmarshalInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
infoLength := info.Length
|
||||
magnet := &utils.Magnet{
|
||||
InfoHash: infoHash,
|
||||
Name: info.Name,
|
||||
Size: infoLength,
|
||||
Link: mi.Magnet(&hash, &info).String(),
|
||||
}
|
||||
torrent := &Torrent{
|
||||
InfoHash: infoHash,
|
||||
Name: info.Name,
|
||||
Size: infoLength,
|
||||
Magnet: magnet,
|
||||
Filename: filePath,
|
||||
}
|
||||
return torrent, nil
|
||||
}
|
||||
|
||||
func GetLocalCache(infohashes []string, cache *common.Cache) ([]string, map[string]bool) {
|
||||
result := make(map[string]bool)
|
||||
hashes := make([]string, 0)
|
||||
|
||||
if len(infohashes) == 0 {
|
||||
return hashes, result
|
||||
}
|
||||
if len(infohashes) == 1 {
|
||||
if cache.Exists(infohashes[0]) {
|
||||
return hashes, map[string]bool{infohashes[0]: true}
|
||||
}
|
||||
return infohashes, result
|
||||
}
|
||||
|
||||
cachedHashes := cache.GetMultiple(infohashes)
|
||||
for _, h := range infohashes {
|
||||
_, exists := cachedHashes[h]
|
||||
if !exists {
|
||||
hashes = append(hashes, h)
|
||||
} else {
|
||||
result[h] = true
|
||||
}
|
||||
}
|
||||
|
||||
return infohashes, result
|
||||
}
|
||||
|
||||
func ProcessTorrent(d *DebridService, magnet *utils.Magnet, a *arr.Arr, isSymlink bool) (*Torrent, error) {
|
||||
debridTorrent := &Torrent{
|
||||
func ProcessTorrent(d *engine.Engine, magnet *utils.Magnet, a *arr.Arr, isSymlink bool) (*torrent.Torrent, error) {
|
||||
debridTorrent := &torrent.Torrent{
|
||||
InfoHash: magnet.InfoHash,
|
||||
Magnet: magnet,
|
||||
Name: magnet.Name,
|
||||
@@ -162,7 +58,7 @@ func ProcessTorrent(d *DebridService, magnet *utils.Magnet, a *arr.Arr, isSymlin
|
||||
|
||||
errs := make([]error, 0)
|
||||
|
||||
for index, db := range d.debrids {
|
||||
for index, db := range d.Debrids {
|
||||
logger := db.GetLogger()
|
||||
logger.Info().Msgf("Processing debrid: %s", db.GetName())
|
||||
|
||||
@@ -179,7 +75,6 @@ func ProcessTorrent(d *DebridService, magnet *utils.Magnet, a *arr.Arr, isSymlin
|
||||
|
||||
dbt, err := db.SubmitMagnet(debridTorrent)
|
||||
if dbt != nil {
|
||||
dbt.Debrid = db
|
||||
dbt.Arr = a
|
||||
}
|
||||
if err != nil || dbt == nil || dbt.Id == "" {
|
||||
@@ -187,7 +82,7 @@ func ProcessTorrent(d *DebridService, magnet *utils.Magnet, a *arr.Arr, isSymlin
|
||||
continue
|
||||
}
|
||||
logger.Info().Msgf("Torrent: %s submitted to %s", dbt.Name, db.GetName())
|
||||
d.lastUsed = index
|
||||
d.LastUsed = index
|
||||
return db.CheckStatus(dbt, isSymlink)
|
||||
}
|
||||
err := fmt.Errorf("failed to process torrent")
|
||||
|
||||
@@ -1,280 +0,0 @@
|
||||
package debrid
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/sirrobot01/debrid-blackhole/common"
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/config"
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/logger"
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/request"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/debrid/types"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type DebridLink struct {
|
||||
BaseDebrid
|
||||
}
|
||||
|
||||
func (r *DebridLink) GetMountPath() string {
|
||||
return r.MountPath
|
||||
}
|
||||
|
||||
func (r *DebridLink) GetName() string {
|
||||
return r.Name
|
||||
}
|
||||
|
||||
func (r *DebridLink) GetLogger() zerolog.Logger {
|
||||
return r.logger
|
||||
}
|
||||
|
||||
func (r *DebridLink) IsAvailable(infohashes []string) map[string]bool {
|
||||
// Check if the infohashes are available in the local cache
|
||||
hashes, result := GetLocalCache(infohashes, r.cache)
|
||||
|
||||
if len(hashes) == 0 {
|
||||
// Either all the infohashes are locally cached or none are
|
||||
r.cache.AddMultiple(result)
|
||||
return result
|
||||
}
|
||||
|
||||
// Divide hashes into groups of 100
|
||||
for i := 0; i < len(hashes); i += 100 {
|
||||
end := i + 100
|
||||
if end > len(hashes) {
|
||||
end = len(hashes)
|
||||
}
|
||||
|
||||
// Filter out empty strings
|
||||
validHashes := make([]string, 0, end-i)
|
||||
for _, hash := range hashes[i:end] {
|
||||
if hash != "" {
|
||||
validHashes = append(validHashes, hash)
|
||||
}
|
||||
}
|
||||
|
||||
// If no valid hashes in this batch, continue to the next batch
|
||||
if len(validHashes) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
hashStr := strings.Join(validHashes, ",")
|
||||
url := fmt.Sprintf("%s/seedbox/cached/%s", r.Host, hashStr)
|
||||
req, _ := http.NewRequest(http.MethodGet, url, nil)
|
||||
resp, err := r.client.MakeRequest(req)
|
||||
if err != nil {
|
||||
r.logger.Info().Msgf("Error checking availability: %v", err)
|
||||
return result
|
||||
}
|
||||
var data types.DebridLinkAvailableResponse
|
||||
err = json.Unmarshal(resp, &data)
|
||||
if err != nil {
|
||||
r.logger.Info().Msgf("Error marshalling availability: %v", err)
|
||||
return result
|
||||
}
|
||||
if data.Value == nil {
|
||||
return result
|
||||
}
|
||||
value := *data.Value
|
||||
for _, h := range hashes[i:end] {
|
||||
_, exists := value[h]
|
||||
if exists {
|
||||
result[h] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
r.cache.AddMultiple(result) // Add the results to the cache
|
||||
return result
|
||||
}
|
||||
|
||||
func (r *DebridLink) GetTorrent(id string) (*Torrent, error) {
|
||||
torrent := &Torrent{}
|
||||
url := fmt.Sprintf("%s/seedbox/list?ids=%s", r.Host, id)
|
||||
req, _ := http.NewRequest(http.MethodGet, url, nil)
|
||||
resp, err := r.client.MakeRequest(req)
|
||||
if err != nil {
|
||||
return torrent, err
|
||||
}
|
||||
var res types.DebridLinkTorrentInfo
|
||||
err = json.Unmarshal(resp, &res)
|
||||
if err != nil {
|
||||
return torrent, err
|
||||
}
|
||||
if res.Success == false {
|
||||
return torrent, fmt.Errorf("error getting torrent")
|
||||
}
|
||||
if res.Value == nil {
|
||||
return torrent, fmt.Errorf("torrent not found")
|
||||
}
|
||||
dt := *res.Value
|
||||
|
||||
if len(dt) == 0 {
|
||||
return torrent, fmt.Errorf("torrent not found")
|
||||
}
|
||||
data := dt[0]
|
||||
status := "downloading"
|
||||
if data.Status == 100 {
|
||||
status = "downloaded"
|
||||
}
|
||||
name := common.RemoveInvalidChars(data.Name)
|
||||
torrent.Id = data.ID
|
||||
torrent.Name = name
|
||||
torrent.Bytes = data.TotalSize
|
||||
torrent.Folder = name
|
||||
torrent.Progress = data.DownloadPercent
|
||||
torrent.Status = status
|
||||
torrent.Speed = data.DownloadSpeed
|
||||
torrent.Seeders = data.PeersConnected
|
||||
torrent.Filename = name
|
||||
torrent.OriginalFilename = name
|
||||
files := make([]TorrentFile, len(data.Files))
|
||||
cfg := config.GetConfig()
|
||||
for i, f := range data.Files {
|
||||
if !cfg.IsSizeAllowed(f.Size) {
|
||||
continue
|
||||
}
|
||||
files[i] = TorrentFile{
|
||||
Id: f.ID,
|
||||
Name: f.Name,
|
||||
Size: f.Size,
|
||||
Path: f.Name,
|
||||
}
|
||||
}
|
||||
torrent.Files = files
|
||||
torrent.Debrid = r
|
||||
return torrent, nil
|
||||
}
|
||||
|
||||
func (r *DebridLink) SubmitMagnet(torrent *Torrent) (*Torrent, error) {
|
||||
url := fmt.Sprintf("%s/seedbox/add", r.Host)
|
||||
payload := map[string]string{"url": torrent.Magnet.Link}
|
||||
jsonPayload, _ := json.Marshal(payload)
|
||||
req, _ := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(jsonPayload))
|
||||
resp, err := r.client.MakeRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var res types.DebridLinkSubmitTorrentInfo
|
||||
err = json.Unmarshal(resp, &res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if res.Success == false || res.Value == nil {
|
||||
return nil, fmt.Errorf("error adding torrent")
|
||||
}
|
||||
data := *res.Value
|
||||
status := "downloading"
|
||||
log.Printf("Torrent: %s added with id: %s", torrent.Name, data.ID)
|
||||
name := common.RemoveInvalidChars(data.Name)
|
||||
torrent.Id = data.ID
|
||||
torrent.Name = name
|
||||
torrent.Bytes = data.TotalSize
|
||||
torrent.Folder = name
|
||||
torrent.Progress = data.DownloadPercent
|
||||
torrent.Status = status
|
||||
torrent.Speed = data.DownloadSpeed
|
||||
torrent.Seeders = data.PeersConnected
|
||||
torrent.Filename = name
|
||||
torrent.OriginalFilename = name
|
||||
files := make([]TorrentFile, len(data.Files))
|
||||
for i, f := range data.Files {
|
||||
files[i] = TorrentFile{
|
||||
Id: f.ID,
|
||||
Name: f.Name,
|
||||
Size: f.Size,
|
||||
Path: f.Name,
|
||||
Link: f.DownloadURL,
|
||||
}
|
||||
}
|
||||
torrent.Files = files
|
||||
torrent.Debrid = r
|
||||
|
||||
return torrent, nil
|
||||
}
|
||||
|
||||
func (r *DebridLink) CheckStatus(torrent *Torrent, isSymlink bool) (*Torrent, error) {
|
||||
for {
|
||||
t, err := r.GetTorrent(torrent.Id)
|
||||
torrent = t
|
||||
if err != nil || torrent == nil {
|
||||
return torrent, err
|
||||
}
|
||||
status := torrent.Status
|
||||
if status == "downloaded" {
|
||||
r.logger.Info().Msgf("Torrent: %s downloaded", torrent.Name)
|
||||
if !isSymlink {
|
||||
err = r.GetDownloadLinks(torrent)
|
||||
if err != nil {
|
||||
return torrent, err
|
||||
}
|
||||
}
|
||||
break
|
||||
} else if status == "downloading" {
|
||||
if !r.DownloadUncached {
|
||||
go torrent.Delete()
|
||||
return torrent, fmt.Errorf("torrent: %s not cached", torrent.Name)
|
||||
}
|
||||
// Break out of the loop if the torrent is downloading.
|
||||
// This is necessary to prevent infinite loop since we moved to sync downloading and async processing
|
||||
break
|
||||
} else {
|
||||
return torrent, fmt.Errorf("torrent: %s has error", torrent.Name)
|
||||
}
|
||||
|
||||
}
|
||||
return torrent, nil
|
||||
}
|
||||
|
||||
func (r *DebridLink) DeleteTorrent(torrent *Torrent) {
|
||||
url := fmt.Sprintf("%s/seedbox/%s/remove", r.Host, torrent.Id)
|
||||
req, _ := http.NewRequest(http.MethodDelete, url, nil)
|
||||
_, err := r.client.MakeRequest(req)
|
||||
if err == nil {
|
||||
r.logger.Info().Msgf("Torrent: %s deleted", torrent.Name)
|
||||
} else {
|
||||
r.logger.Info().Msgf("Error deleting torrent: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *DebridLink) GetDownloadLinks(torrent *Torrent) error {
|
||||
downloadLinks := make([]TorrentDownloadLinks, 0)
|
||||
for _, f := range torrent.Files {
|
||||
dl := TorrentDownloadLinks{
|
||||
Link: f.Link,
|
||||
Filename: f.Name,
|
||||
}
|
||||
downloadLinks = append(downloadLinks, dl)
|
||||
}
|
||||
torrent.DownloadLinks = downloadLinks
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *DebridLink) GetCheckCached() bool {
|
||||
return r.CheckCached
|
||||
}
|
||||
|
||||
func NewDebridLink(dc config.Debrid, cache *common.Cache) *DebridLink {
|
||||
rl := request.ParseRateLimit(dc.RateLimit)
|
||||
headers := map[string]string{
|
||||
"Authorization": fmt.Sprintf("Bearer %s", dc.APIKey),
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
client := request.NewRLHTTPClient(rl, headers)
|
||||
return &DebridLink{
|
||||
BaseDebrid: BaseDebrid{
|
||||
Name: "debridlink",
|
||||
Host: dc.Host,
|
||||
APIKey: dc.APIKey,
|
||||
DownloadUncached: dc.DownloadUncached,
|
||||
client: client,
|
||||
cache: cache,
|
||||
MountPath: dc.Folder,
|
||||
logger: logger.NewLogger(dc.Name, config.GetConfig().LogLevel, os.Stdout),
|
||||
CheckCached: dc.CheckCached,
|
||||
},
|
||||
}
|
||||
}
|
||||
298
pkg/debrid/debrid_link/debrid_link.go
Normal file
298
pkg/debrid/debrid_link/debrid_link.go
Normal file
@@ -0,0 +1,298 @@
|
||||
package debrid_link
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/sirrobot01/debrid-blackhole/common"
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/config"
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/logger"
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/request"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/debrid/torrent"
|
||||
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type DebridLink struct {
|
||||
Name string
|
||||
Host string `json:"host"`
|
||||
APIKey string
|
||||
DownloadUncached bool
|
||||
client *request.RLHTTPClient
|
||||
cache *common.Cache
|
||||
MountPath string
|
||||
logger zerolog.Logger
|
||||
CheckCached bool
|
||||
}
|
||||
|
||||
func (dl *DebridLink) GetMountPath() string {
|
||||
return dl.MountPath
|
||||
}
|
||||
|
||||
func (dl *DebridLink) GetName() string {
|
||||
return dl.Name
|
||||
}
|
||||
|
||||
func (dl *DebridLink) GetLogger() zerolog.Logger {
|
||||
return dl.logger
|
||||
}
|
||||
|
||||
func (dl *DebridLink) IsAvailable(infohashes []string) map[string]bool {
|
||||
// Check if the infohashes are available in the local cache
|
||||
hashes, result := torrent.GetLocalCache(infohashes, dl.cache)
|
||||
|
||||
if len(hashes) == 0 {
|
||||
// Either all the infohashes are locally cached or none are
|
||||
dl.cache.AddMultiple(result)
|
||||
return result
|
||||
}
|
||||
|
||||
// Divide hashes into groups of 100
|
||||
for i := 0; i < len(hashes); i += 100 {
|
||||
end := i + 100
|
||||
if end > len(hashes) {
|
||||
end = len(hashes)
|
||||
}
|
||||
|
||||
// Filter out empty strings
|
||||
validHashes := make([]string, 0, end-i)
|
||||
for _, hash := range hashes[i:end] {
|
||||
if hash != "" {
|
||||
validHashes = append(validHashes, hash)
|
||||
}
|
||||
}
|
||||
|
||||
// If no valid hashes in this batch, continue to the next batch
|
||||
if len(validHashes) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
hashStr := strings.Join(validHashes, ",")
|
||||
url := fmt.Sprintf("%s/seedbox/cached/%s", dl.Host, hashStr)
|
||||
req, _ := http.NewRequest(http.MethodGet, url, nil)
|
||||
resp, err := dl.client.MakeRequest(req)
|
||||
if err != nil {
|
||||
dl.logger.Info().Msgf("Error checking availability: %v", err)
|
||||
return result
|
||||
}
|
||||
var data AvailableResponse
|
||||
err = json.Unmarshal(resp, &data)
|
||||
if err != nil {
|
||||
dl.logger.Info().Msgf("Error marshalling availability: %v", err)
|
||||
return result
|
||||
}
|
||||
if data.Value == nil {
|
||||
return result
|
||||
}
|
||||
value := *data.Value
|
||||
for _, h := range hashes[i:end] {
|
||||
_, exists := value[h]
|
||||
if exists {
|
||||
result[h] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
dl.cache.AddMultiple(result) // Add the results to the cache
|
||||
return result
|
||||
}
|
||||
|
||||
func (dl *DebridLink) GetTorrent(id string) (*torrent.Torrent, error) {
|
||||
t := &torrent.Torrent{}
|
||||
url := fmt.Sprintf("%s/seedbox/list?ids=%s", dl.Host, id)
|
||||
req, _ := http.NewRequest(http.MethodGet, url, nil)
|
||||
resp, err := dl.client.MakeRequest(req)
|
||||
if err != nil {
|
||||
return t, err
|
||||
}
|
||||
var res TorrentInfo
|
||||
err = json.Unmarshal(resp, &res)
|
||||
if err != nil {
|
||||
return t, err
|
||||
}
|
||||
if res.Success == false {
|
||||
return t, fmt.Errorf("error getting torrent")
|
||||
}
|
||||
if res.Value == nil {
|
||||
return t, fmt.Errorf("torrent not found")
|
||||
}
|
||||
dt := *res.Value
|
||||
|
||||
if len(dt) == 0 {
|
||||
return t, fmt.Errorf("torrent not found")
|
||||
}
|
||||
data := dt[0]
|
||||
status := "downloading"
|
||||
if data.Status == 100 {
|
||||
status = "downloaded"
|
||||
}
|
||||
name := common.RemoveInvalidChars(data.Name)
|
||||
t.Id = data.ID
|
||||
t.Name = name
|
||||
t.Bytes = data.TotalSize
|
||||
t.Folder = name
|
||||
t.Progress = data.DownloadPercent
|
||||
t.Status = status
|
||||
t.Speed = data.DownloadSpeed
|
||||
t.Seeders = data.PeersConnected
|
||||
t.Filename = name
|
||||
t.OriginalFilename = name
|
||||
files := make([]torrent.File, len(data.Files))
|
||||
cfg := config.GetConfig()
|
||||
for i, f := range data.Files {
|
||||
if !cfg.IsSizeAllowed(f.Size) {
|
||||
continue
|
||||
}
|
||||
files[i] = torrent.File{
|
||||
Id: f.ID,
|
||||
Name: f.Name,
|
||||
Size: f.Size,
|
||||
Path: f.Name,
|
||||
}
|
||||
}
|
||||
t.Files = files
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (dl *DebridLink) SubmitMagnet(t *torrent.Torrent) (*torrent.Torrent, error) {
|
||||
url := fmt.Sprintf("%s/seedbox/add", dl.Host)
|
||||
payload := map[string]string{"url": t.Magnet.Link}
|
||||
jsonPayload, _ := json.Marshal(payload)
|
||||
req, _ := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(jsonPayload))
|
||||
resp, err := dl.client.MakeRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var res SubmitTorrentInfo
|
||||
err = json.Unmarshal(resp, &res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if res.Success == false || res.Value == nil {
|
||||
return nil, fmt.Errorf("error adding torrent")
|
||||
}
|
||||
data := *res.Value
|
||||
status := "downloading"
|
||||
log.Printf("Torrent: %s added with id: %s", t.Name, data.ID)
|
||||
name := common.RemoveInvalidChars(data.Name)
|
||||
t.Id = data.ID
|
||||
t.Name = name
|
||||
t.Bytes = data.TotalSize
|
||||
t.Folder = name
|
||||
t.Progress = data.DownloadPercent
|
||||
t.Status = status
|
||||
t.Speed = data.DownloadSpeed
|
||||
t.Seeders = data.PeersConnected
|
||||
t.Filename = name
|
||||
t.OriginalFilename = name
|
||||
t.MountPath = dl.MountPath
|
||||
t.Debrid = dl.Name
|
||||
t.DownloadLinks = make(map[string]torrent.DownloadLinks)
|
||||
files := make([]torrent.File, len(data.Files))
|
||||
for i, f := range data.Files {
|
||||
files[i] = torrent.File{
|
||||
Id: f.ID,
|
||||
Name: f.Name,
|
||||
Size: f.Size,
|
||||
Path: f.Name,
|
||||
Link: f.DownloadURL,
|
||||
}
|
||||
}
|
||||
t.Files = files
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (dl *DebridLink) CheckStatus(torrent *torrent.Torrent, isSymlink bool) (*torrent.Torrent, error) {
|
||||
for {
|
||||
t, err := dl.GetTorrent(torrent.Id)
|
||||
torrent = t
|
||||
if err != nil || torrent == nil {
|
||||
return torrent, err
|
||||
}
|
||||
status := torrent.Status
|
||||
if status == "downloaded" {
|
||||
dl.logger.Info().Msgf("Torrent: %s downloaded", torrent.Name)
|
||||
err = dl.GetDownloadLinks(torrent)
|
||||
if err != nil {
|
||||
return torrent, err
|
||||
}
|
||||
break
|
||||
} else if status == "downloading" {
|
||||
if !dl.DownloadUncached {
|
||||
go dl.DeleteTorrent(torrent)
|
||||
return torrent, fmt.Errorf("torrent: %s not cached", torrent.Name)
|
||||
}
|
||||
// Break out of the loop if the torrent is downloading.
|
||||
// This is necessary to prevent infinite loop since we moved to sync downloading and async processing
|
||||
break
|
||||
} else {
|
||||
return torrent, fmt.Errorf("torrent: %s has error", torrent.Name)
|
||||
}
|
||||
|
||||
}
|
||||
return torrent, nil
|
||||
}
|
||||
|
||||
func (dl *DebridLink) DeleteTorrent(torrent *torrent.Torrent) {
|
||||
url := fmt.Sprintf("%s/seedbox/%s/remove", dl.Host, torrent.Id)
|
||||
req, _ := http.NewRequest(http.MethodDelete, url, nil)
|
||||
_, err := dl.client.MakeRequest(req)
|
||||
if err == nil {
|
||||
dl.logger.Info().Msgf("Torrent: %s deleted", torrent.Name)
|
||||
} else {
|
||||
dl.logger.Info().Msgf("Error deleting torrent: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (dl *DebridLink) GetDownloadLinks(t *torrent.Torrent) error {
|
||||
downloadLinks := make(map[string]torrent.DownloadLinks)
|
||||
for _, f := range t.Files {
|
||||
dl := torrent.DownloadLinks{
|
||||
Link: f.Link,
|
||||
Filename: f.Name,
|
||||
}
|
||||
downloadLinks[f.Id] = dl
|
||||
}
|
||||
t.DownloadLinks = downloadLinks
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dl *DebridLink) GetDownloadLink(t *torrent.Torrent, file *torrent.File) *torrent.DownloadLinks {
|
||||
dlLink, ok := t.DownloadLinks[file.Id]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return &dlLink
|
||||
}
|
||||
|
||||
func (dl *DebridLink) GetCheckCached() bool {
|
||||
return dl.CheckCached
|
||||
}
|
||||
|
||||
func New(dc config.Debrid, cache *common.Cache) *DebridLink {
|
||||
rl := request.ParseRateLimit(dc.RateLimit)
|
||||
headers := map[string]string{
|
||||
"Authorization": fmt.Sprintf("Bearer %s", dc.APIKey),
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
client := request.NewRLHTTPClient(rl, headers)
|
||||
return &DebridLink{
|
||||
Name: "debridlink",
|
||||
Host: dc.Host,
|
||||
APIKey: dc.APIKey,
|
||||
DownloadUncached: dc.DownloadUncached,
|
||||
client: client,
|
||||
cache: cache,
|
||||
MountPath: dc.Folder,
|
||||
logger: logger.NewLogger(dc.Name, config.GetConfig().LogLevel, os.Stdout),
|
||||
CheckCached: dc.CheckCached,
|
||||
}
|
||||
}
|
||||
|
||||
func (dl *DebridLink) GetTorrents() ([]*torrent.Torrent, error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
package types
|
||||
package debrid_link
|
||||
|
||||
type DebridLinkAPIResponse[T any] struct {
|
||||
type APIResponse[T any] struct {
|
||||
Success bool `json:"success"`
|
||||
Value *T `json:"value"` // Use pointer to allow nil
|
||||
}
|
||||
|
||||
type DebridLinkAvailableResponse DebridLinkAPIResponse[map[string]map[string]struct {
|
||||
type AvailableResponse APIResponse[map[string]map[string]struct {
|
||||
Name string `json:"name"`
|
||||
HashString string `json:"hashString"`
|
||||
Files []struct {
|
||||
@@ -40,6 +40,6 @@ type debridLinkTorrentInfo struct {
|
||||
UploadSpeed int64 `json:"uploadSpeed"`
|
||||
}
|
||||
|
||||
type DebridLinkTorrentInfo DebridLinkAPIResponse[[]debridLinkTorrentInfo]
|
||||
type TorrentInfo APIResponse[[]debridLinkTorrentInfo]
|
||||
|
||||
type DebridLinkSubmitTorrentInfo DebridLinkAPIResponse[debridLinkTorrentInfo]
|
||||
type SubmitTorrentInfo APIResponse[debridLinkTorrentInfo]
|
||||
1
pkg/debrid/engine.go
Normal file
1
pkg/debrid/engine.go
Normal file
@@ -0,0 +1 @@
|
||||
package debrid
|
||||
26
pkg/debrid/engine/engine.go
Normal file
26
pkg/debrid/engine/engine.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package engine
|
||||
|
||||
type Engine struct {
|
||||
Debrids []Service
|
||||
LastUsed int
|
||||
}
|
||||
|
||||
func (d *Engine) Get() Service {
|
||||
if d.LastUsed == 0 {
|
||||
return d.Debrids[0]
|
||||
}
|
||||
return d.Debrids[d.LastUsed]
|
||||
}
|
||||
|
||||
func (d *Engine) GetByName(name string) Service {
|
||||
for _, deb := range d.Debrids {
|
||||
if deb.GetName() == name {
|
||||
return deb
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Engine) GetDebrids() []Service {
|
||||
return d.Debrids
|
||||
}
|
||||
21
pkg/debrid/engine/service.go
Normal file
21
pkg/debrid/engine/service.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/debrid/torrent"
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
SubmitMagnet(tr *torrent.Torrent) (*torrent.Torrent, error)
|
||||
CheckStatus(tr *torrent.Torrent, isSymlink bool) (*torrent.Torrent, error)
|
||||
GetDownloadLinks(tr *torrent.Torrent) error
|
||||
GetDownloadLink(tr *torrent.Torrent, file *torrent.File) *torrent.DownloadLinks
|
||||
DeleteTorrent(tr *torrent.Torrent)
|
||||
IsAvailable(infohashes []string) map[string]bool
|
||||
GetMountPath() string
|
||||
GetCheckCached() bool
|
||||
GetTorrent(id string) (*torrent.Torrent, error)
|
||||
GetTorrents() ([]*torrent.Torrent, error)
|
||||
GetName() string
|
||||
GetLogger() zerolog.Logger
|
||||
}
|
||||
@@ -1,303 +0,0 @@
|
||||
package debrid
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/sirrobot01/debrid-blackhole/common"
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/config"
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/logger"
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/request"
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/utils"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/debrid/types"
|
||||
"net/http"
|
||||
gourl "net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type RealDebrid struct {
|
||||
BaseDebrid
|
||||
}
|
||||
|
||||
func (r *RealDebrid) GetMountPath() string {
|
||||
return r.MountPath
|
||||
}
|
||||
|
||||
func (r *RealDebrid) GetName() string {
|
||||
return r.Name
|
||||
}
|
||||
|
||||
func (r *RealDebrid) GetLogger() zerolog.Logger {
|
||||
return r.logger
|
||||
}
|
||||
|
||||
func GetTorrentFiles(data types.RealDebridTorrentInfo) []TorrentFile {
|
||||
files := make([]TorrentFile, 0)
|
||||
cfg := config.GetConfig()
|
||||
for _, f := range data.Files {
|
||||
name := filepath.Base(f.Path)
|
||||
if utils.RegexMatch(utils.SAMPLEMATCH, name) {
|
||||
// Skip sample files
|
||||
continue
|
||||
}
|
||||
if !cfg.IsAllowedFile(name) {
|
||||
continue
|
||||
}
|
||||
if !cfg.IsSizeAllowed(f.Bytes) {
|
||||
continue
|
||||
}
|
||||
fileId := f.ID
|
||||
file := TorrentFile{
|
||||
Name: name,
|
||||
Path: name,
|
||||
Size: f.Bytes,
|
||||
Id: strconv.Itoa(fileId),
|
||||
}
|
||||
files = append(files, file)
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
func (r *RealDebrid) IsAvailable(infohashes []string) map[string]bool {
|
||||
// Check if the infohashes are available in the local cache
|
||||
hashes, result := GetLocalCache(infohashes, r.cache)
|
||||
|
||||
if len(hashes) == 0 {
|
||||
// Either all the infohashes are locally cached or none are
|
||||
r.cache.AddMultiple(result)
|
||||
return result
|
||||
}
|
||||
|
||||
// Divide hashes into groups of 100
|
||||
for i := 0; i < len(hashes); i += 200 {
|
||||
end := i + 200
|
||||
if end > len(hashes) {
|
||||
end = len(hashes)
|
||||
}
|
||||
|
||||
// Filter out empty strings
|
||||
validHashes := make([]string, 0, end-i)
|
||||
for _, hash := range hashes[i:end] {
|
||||
if hash != "" {
|
||||
validHashes = append(validHashes, hash)
|
||||
}
|
||||
}
|
||||
|
||||
// If no valid hashes in this batch, continue to the next batch
|
||||
if len(validHashes) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
hashStr := strings.Join(validHashes, "/")
|
||||
url := fmt.Sprintf("%s/torrents/instantAvailability/%s", r.Host, hashStr)
|
||||
req, _ := http.NewRequest(http.MethodGet, url, nil)
|
||||
resp, err := r.client.MakeRequest(req)
|
||||
if err != nil {
|
||||
r.logger.Info().Msgf("Error checking availability: %v", err)
|
||||
return result
|
||||
}
|
||||
var data types.RealDebridAvailabilityResponse
|
||||
err = json.Unmarshal(resp, &data)
|
||||
if err != nil {
|
||||
r.logger.Info().Msgf("Error marshalling availability: %v", err)
|
||||
return result
|
||||
}
|
||||
for _, h := range hashes[i:end] {
|
||||
hosters, exists := data[strings.ToLower(h)]
|
||||
if exists && len(hosters.Rd) > 0 {
|
||||
result[h] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
r.cache.AddMultiple(result) // Add the results to the cache
|
||||
return result
|
||||
}
|
||||
|
||||
func (r *RealDebrid) SubmitMagnet(torrent *Torrent) (*Torrent, error) {
|
||||
url := fmt.Sprintf("%s/torrents/addMagnet", r.Host)
|
||||
payload := gourl.Values{
|
||||
"magnet": {torrent.Magnet.Link},
|
||||
}
|
||||
var data types.RealDebridAddMagnetSchema
|
||||
req, _ := http.NewRequest(http.MethodPost, url, strings.NewReader(payload.Encode()))
|
||||
resp, err := r.client.MakeRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = json.Unmarshal(resp, &data)
|
||||
r.logger.Info().Msgf("Torrent: %s added with id: %s", torrent.Name, data.Id)
|
||||
torrent.Id = data.Id
|
||||
|
||||
return torrent, nil
|
||||
}
|
||||
|
||||
func (r *RealDebrid) GetTorrent(id string) (*Torrent, error) {
|
||||
torrent := &Torrent{}
|
||||
url := fmt.Sprintf("%s/torrents/info/%s", r.Host, id)
|
||||
req, _ := http.NewRequest(http.MethodGet, url, nil)
|
||||
resp, err := r.client.MakeRequest(req)
|
||||
if err != nil {
|
||||
return torrent, err
|
||||
}
|
||||
var data types.RealDebridTorrentInfo
|
||||
err = json.Unmarshal(resp, &data)
|
||||
if err != nil {
|
||||
return torrent, err
|
||||
}
|
||||
name := common.RemoveInvalidChars(data.OriginalFilename)
|
||||
torrent.Id = id
|
||||
torrent.Name = name
|
||||
torrent.Bytes = data.Bytes
|
||||
torrent.Folder = name
|
||||
torrent.Progress = data.Progress
|
||||
torrent.Status = data.Status
|
||||
torrent.Speed = data.Speed
|
||||
torrent.Seeders = data.Seeders
|
||||
torrent.Filename = data.Filename
|
||||
torrent.OriginalFilename = data.OriginalFilename
|
||||
torrent.Links = data.Links
|
||||
torrent.Debrid = r
|
||||
files := GetTorrentFiles(data)
|
||||
torrent.Files = files
|
||||
return torrent, nil
|
||||
}
|
||||
|
||||
func (r *RealDebrid) CheckStatus(torrent *Torrent, isSymlink bool) (*Torrent, error) {
|
||||
url := fmt.Sprintf("%s/torrents/info/%s", r.Host, torrent.Id)
|
||||
req, _ := http.NewRequest(http.MethodGet, url, nil)
|
||||
for {
|
||||
resp, err := r.client.MakeRequest(req)
|
||||
if err != nil {
|
||||
r.logger.Info().Msgf("ERROR Checking file: %v", err)
|
||||
return torrent, err
|
||||
}
|
||||
var data types.RealDebridTorrentInfo
|
||||
err = json.Unmarshal(resp, &data)
|
||||
status := data.Status
|
||||
name := common.RemoveInvalidChars(data.OriginalFilename)
|
||||
torrent.Name = name // Important because some magnet changes the name
|
||||
torrent.Folder = name
|
||||
torrent.Filename = data.Filename
|
||||
torrent.OriginalFilename = data.OriginalFilename
|
||||
torrent.Bytes = data.Bytes
|
||||
torrent.Progress = data.Progress
|
||||
torrent.Speed = data.Speed
|
||||
torrent.Seeders = data.Seeders
|
||||
torrent.Links = data.Links
|
||||
torrent.Status = status
|
||||
torrent.Debrid = r
|
||||
downloadingStatus := []string{"downloading", "magnet_conversion", "queued", "compressing", "uploading"}
|
||||
if status == "waiting_files_selection" {
|
||||
files := GetTorrentFiles(data)
|
||||
torrent.Files = files
|
||||
if len(files) == 0 {
|
||||
return torrent, fmt.Errorf("no video files found")
|
||||
}
|
||||
filesId := make([]string, 0)
|
||||
for _, f := range files {
|
||||
filesId = append(filesId, f.Id)
|
||||
}
|
||||
p := gourl.Values{
|
||||
"files": {strings.Join(filesId, ",")},
|
||||
}
|
||||
payload := strings.NewReader(p.Encode())
|
||||
req, _ := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/torrents/selectFiles/%s", r.Host, torrent.Id), payload)
|
||||
_, err = r.client.MakeRequest(req)
|
||||
if err != nil {
|
||||
return torrent, err
|
||||
}
|
||||
} else if status == "downloaded" {
|
||||
files := GetTorrentFiles(data)
|
||||
torrent.Files = files
|
||||
r.logger.Info().Msgf("Torrent: %s downloaded to RD", torrent.Name)
|
||||
if !isSymlink {
|
||||
err = r.GetDownloadLinks(torrent)
|
||||
if err != nil {
|
||||
return torrent, err
|
||||
}
|
||||
}
|
||||
break
|
||||
} else if slices.Contains(downloadingStatus, status) {
|
||||
if !r.DownloadUncached {
|
||||
return torrent, fmt.Errorf("torrent: %s not cached", torrent.Name)
|
||||
}
|
||||
// Break out of the loop if the torrent is downloading.
|
||||
// This is necessary to prevent infinite loop since we moved to sync downloading and async processing
|
||||
break
|
||||
} else {
|
||||
return torrent, fmt.Errorf("torrent: %s has error: %s", torrent.Name, status)
|
||||
}
|
||||
|
||||
}
|
||||
return torrent, nil
|
||||
}
|
||||
|
||||
func (r *RealDebrid) DeleteTorrent(torrent *Torrent) {
|
||||
url := fmt.Sprintf("%s/torrents/delete/%s", r.Host, torrent.Id)
|
||||
req, _ := http.NewRequest(http.MethodDelete, url, nil)
|
||||
_, err := r.client.MakeRequest(req)
|
||||
if err == nil {
|
||||
r.logger.Info().Msgf("Torrent: %s deleted", torrent.Name)
|
||||
} else {
|
||||
r.logger.Info().Msgf("Error deleting torrent: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RealDebrid) GetDownloadLinks(torrent *Torrent) error {
|
||||
url := fmt.Sprintf("%s/unrestrict/link/", r.Host)
|
||||
downloadLinks := make([]TorrentDownloadLinks, 0)
|
||||
for _, link := range torrent.Links {
|
||||
if link == "" {
|
||||
continue
|
||||
}
|
||||
payload := gourl.Values{
|
||||
"link": {link},
|
||||
}
|
||||
req, _ := http.NewRequest(http.MethodPost, url, strings.NewReader(payload.Encode()))
|
||||
resp, err := r.client.MakeRequest(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var data types.RealDebridUnrestrictResponse
|
||||
if err = json.Unmarshal(resp, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
download := TorrentDownloadLinks{
|
||||
Link: data.Link,
|
||||
Filename: data.Filename,
|
||||
DownloadLink: data.Download,
|
||||
}
|
||||
downloadLinks = append(downloadLinks, download)
|
||||
}
|
||||
torrent.DownloadLinks = downloadLinks
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RealDebrid) GetCheckCached() bool {
|
||||
return r.CheckCached
|
||||
}
|
||||
|
||||
func NewRealDebrid(dc config.Debrid, cache *common.Cache) *RealDebrid {
|
||||
rl := request.ParseRateLimit(dc.RateLimit)
|
||||
headers := map[string]string{
|
||||
"Authorization": fmt.Sprintf("Bearer %s", dc.APIKey),
|
||||
}
|
||||
client := request.NewRLHTTPClient(rl, headers)
|
||||
return &RealDebrid{
|
||||
BaseDebrid: BaseDebrid{
|
||||
Name: "realdebrid",
|
||||
Host: dc.Host,
|
||||
APIKey: dc.APIKey,
|
||||
DownloadUncached: dc.DownloadUncached,
|
||||
client: client,
|
||||
cache: cache,
|
||||
MountPath: dc.Folder,
|
||||
logger: logger.NewLogger(dc.Name, config.GetConfig().LogLevel, os.Stdout),
|
||||
CheckCached: dc.CheckCached,
|
||||
},
|
||||
}
|
||||
}
|
||||
402
pkg/debrid/realdebrid/realdebrid.go
Normal file
402
pkg/debrid/realdebrid/realdebrid.go
Normal file
@@ -0,0 +1,402 @@
|
||||
package realdebrid
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/sirrobot01/debrid-blackhole/common"
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/config"
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/logger"
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/request"
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/utils"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/debrid/torrent"
|
||||
"net/http"
|
||||
gourl "net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type RealDebrid struct {
|
||||
Name string
|
||||
Host string `json:"host"`
|
||||
APIKey string
|
||||
DownloadUncached bool
|
||||
client *request.RLHTTPClient
|
||||
cache *common.Cache
|
||||
MountPath string
|
||||
logger zerolog.Logger
|
||||
CheckCached bool
|
||||
}
|
||||
|
||||
func (r *RealDebrid) GetMountPath() string {
|
||||
return r.MountPath
|
||||
}
|
||||
|
||||
func (r *RealDebrid) GetName() string {
|
||||
return r.Name
|
||||
}
|
||||
|
||||
func (r *RealDebrid) GetLogger() zerolog.Logger {
|
||||
return r.logger
|
||||
}
|
||||
|
||||
// GetTorrentFiles returns a list of torrent files from the torrent info
|
||||
// validate is used to determine if the files should be validated
|
||||
// if validate is false, selected files will be returned
|
||||
func GetTorrentFiles(data TorrentInfo, validate bool) []torrent.File {
|
||||
files := make([]torrent.File, 0)
|
||||
cfg := config.GetConfig()
|
||||
idx := 0
|
||||
for _, f := range data.Files {
|
||||
|
||||
name := filepath.Base(f.Path)
|
||||
|
||||
if validate {
|
||||
if utils.RegexMatch(utils.SAMPLEMATCH, name) {
|
||||
// Skip sample files
|
||||
continue
|
||||
}
|
||||
if !cfg.IsAllowedFile(name) {
|
||||
continue
|
||||
}
|
||||
if !cfg.IsSizeAllowed(f.Bytes) {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
if f.Selected == 0 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
fileId := f.ID
|
||||
_link := ""
|
||||
if len(data.Links) > idx {
|
||||
_link = data.Links[idx]
|
||||
}
|
||||
file := torrent.File{
|
||||
Name: name,
|
||||
Path: name,
|
||||
Size: f.Bytes,
|
||||
Id: strconv.Itoa(fileId),
|
||||
Link: _link,
|
||||
}
|
||||
files = append(files, file)
|
||||
idx++
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
func (r *RealDebrid) IsAvailable(infohashes []string) map[string]bool {
|
||||
// Check if the infohashes are available in the local cache
|
||||
hashes, result := torrent.GetLocalCache(infohashes, r.cache)
|
||||
|
||||
if len(hashes) == 0 {
|
||||
// Either all the infohashes are locally cached or none are
|
||||
r.cache.AddMultiple(result)
|
||||
return result
|
||||
}
|
||||
|
||||
// Divide hashes into groups of 100
|
||||
for i := 0; i < len(hashes); i += 200 {
|
||||
end := i + 200
|
||||
if end > len(hashes) {
|
||||
end = len(hashes)
|
||||
}
|
||||
|
||||
// Filter out empty strings
|
||||
validHashes := make([]string, 0, end-i)
|
||||
for _, hash := range hashes[i:end] {
|
||||
if hash != "" {
|
||||
validHashes = append(validHashes, hash)
|
||||
}
|
||||
}
|
||||
|
||||
// If no valid hashes in this batch, continue to the next batch
|
||||
if len(validHashes) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
hashStr := strings.Join(validHashes, "/")
|
||||
url := fmt.Sprintf("%s/torrents/instantAvailability/%s", r.Host, hashStr)
|
||||
req, _ := http.NewRequest(http.MethodGet, url, nil)
|
||||
resp, err := r.client.MakeRequest(req)
|
||||
if err != nil {
|
||||
r.logger.Info().Msgf("Error checking availability: %v", err)
|
||||
return result
|
||||
}
|
||||
var data AvailabilityResponse
|
||||
err = json.Unmarshal(resp, &data)
|
||||
if err != nil {
|
||||
r.logger.Info().Msgf("Error marshalling availability: %v", err)
|
||||
return result
|
||||
}
|
||||
for _, h := range hashes[i:end] {
|
||||
hosters, exists := data[strings.ToLower(h)]
|
||||
if exists && len(hosters.Rd) > 0 {
|
||||
result[h] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
r.cache.AddMultiple(result) // Add the results to the cache
|
||||
return result
|
||||
}
|
||||
|
||||
func (r *RealDebrid) SubmitMagnet(t *torrent.Torrent) (*torrent.Torrent, error) {
|
||||
url := fmt.Sprintf("%s/torrents/addMagnet", r.Host)
|
||||
payload := gourl.Values{
|
||||
"magnet": {t.Magnet.Link},
|
||||
}
|
||||
var data AddMagnetSchema
|
||||
req, _ := http.NewRequest(http.MethodPost, url, strings.NewReader(payload.Encode()))
|
||||
resp, err := r.client.MakeRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = json.Unmarshal(resp, &data)
|
||||
r.logger.Info().Msgf("Torrent: %s added with id: %s", t.Name, data.Id)
|
||||
t.Id = data.Id
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (r *RealDebrid) GetTorrent(id string) (*torrent.Torrent, error) {
|
||||
t := &torrent.Torrent{}
|
||||
url := fmt.Sprintf("%s/torrents/info/%s", r.Host, id)
|
||||
req, _ := http.NewRequest(http.MethodGet, url, nil)
|
||||
resp, err := r.client.MakeRequest(req)
|
||||
if err != nil {
|
||||
return t, err
|
||||
}
|
||||
var data TorrentInfo
|
||||
err = json.Unmarshal(resp, &data)
|
||||
if err != nil {
|
||||
return t, err
|
||||
}
|
||||
name := common.RemoveInvalidChars(data.OriginalFilename)
|
||||
t.Id = id
|
||||
t.Name = name
|
||||
t.Bytes = data.Bytes
|
||||
t.Folder = name
|
||||
t.Progress = data.Progress
|
||||
t.Status = data.Status
|
||||
t.Speed = data.Speed
|
||||
t.Seeders = data.Seeders
|
||||
t.Filename = data.Filename
|
||||
t.OriginalFilename = data.OriginalFilename
|
||||
t.Links = data.Links
|
||||
t.MountPath = r.MountPath
|
||||
t.Debrid = r.Name
|
||||
t.DownloadLinks = make(map[string]torrent.DownloadLinks)
|
||||
files := GetTorrentFiles(data, false) // Get selected files
|
||||
t.Files = files
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (r *RealDebrid) CheckStatus(t *torrent.Torrent, isSymlink bool) (*torrent.Torrent, error) {
|
||||
url := fmt.Sprintf("%s/torrents/info/%s", r.Host, t.Id)
|
||||
req, _ := http.NewRequest(http.MethodGet, url, nil)
|
||||
for {
|
||||
resp, err := r.client.MakeRequest(req)
|
||||
if err != nil {
|
||||
r.logger.Info().Msgf("ERROR Checking file: %v", err)
|
||||
return t, err
|
||||
}
|
||||
var data TorrentInfo
|
||||
err = json.Unmarshal(resp, &data)
|
||||
status := data.Status
|
||||
name := common.RemoveInvalidChars(data.OriginalFilename)
|
||||
t.Name = name // Important because some magnet changes the name
|
||||
t.Folder = name
|
||||
t.Filename = data.Filename
|
||||
t.OriginalFilename = data.OriginalFilename
|
||||
t.Bytes = data.Bytes
|
||||
t.Progress = data.Progress
|
||||
t.Speed = data.Speed
|
||||
t.Seeders = data.Seeders
|
||||
t.Links = data.Links
|
||||
t.Status = status
|
||||
downloadingStatus := []string{"downloading", "magnet_conversion", "queued", "compressing", "uploading"}
|
||||
if status == "waiting_files_selection" {
|
||||
files := GetTorrentFiles(data, true) // Validate files to be selected
|
||||
t.Files = files
|
||||
if len(files) == 0 {
|
||||
return t, fmt.Errorf("no video files found")
|
||||
}
|
||||
filesId := make([]string, 0)
|
||||
for _, f := range files {
|
||||
filesId = append(filesId, f.Id)
|
||||
}
|
||||
p := gourl.Values{
|
||||
"files": {strings.Join(filesId, ",")},
|
||||
}
|
||||
payload := strings.NewReader(p.Encode())
|
||||
req, _ := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/torrents/selectFiles/%s", r.Host, t.Id), payload)
|
||||
_, err = r.client.MakeRequest(req)
|
||||
if err != nil {
|
||||
return t, err
|
||||
}
|
||||
} else if status == "downloaded" {
|
||||
files := GetTorrentFiles(data, false) // Get selected files
|
||||
t.Files = files
|
||||
r.logger.Info().Msgf("Torrent: %s downloaded to RD", t.Name)
|
||||
if !isSymlink {
|
||||
err = r.GetDownloadLinks(t)
|
||||
if err != nil {
|
||||
return t, err
|
||||
}
|
||||
}
|
||||
break
|
||||
} else if slices.Contains(downloadingStatus, status) {
|
||||
if !r.DownloadUncached {
|
||||
go r.DeleteTorrent(t)
|
||||
return t, fmt.Errorf("torrent: %s not cached", t.Name)
|
||||
}
|
||||
// Break out of the loop if the torrent is downloading.
|
||||
// This is necessary to prevent infinite loop since we moved to sync downloading and async processing
|
||||
break
|
||||
} else {
|
||||
return t, fmt.Errorf("torrent: %s has error: %s", t.Name, status)
|
||||
}
|
||||
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (r *RealDebrid) DeleteTorrent(torrent *torrent.Torrent) {
|
||||
url := fmt.Sprintf("%s/torrents/delete/%s", r.Host, torrent.Id)
|
||||
req, _ := http.NewRequest(http.MethodDelete, url, nil)
|
||||
_, err := r.client.MakeRequest(req)
|
||||
if err == nil {
|
||||
r.logger.Info().Msgf("Torrent: %s deleted", torrent.Name)
|
||||
} else {
|
||||
r.logger.Info().Msgf("Error deleting torrent: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RealDebrid) GetDownloadLinks(t *torrent.Torrent) error {
|
||||
url := fmt.Sprintf("%s/unrestrict/link/", r.Host)
|
||||
downloadLinks := make(map[string]torrent.DownloadLinks)
|
||||
for _, f := range t.Files {
|
||||
dlLink := t.DownloadLinks[f.Id]
|
||||
if f.Link == "" || dlLink.DownloadLink != "" {
|
||||
continue
|
||||
}
|
||||
payload := gourl.Values{
|
||||
"link": {f.Link},
|
||||
}
|
||||
req, _ := http.NewRequest(http.MethodPost, url, strings.NewReader(payload.Encode()))
|
||||
resp, err := r.client.MakeRequest(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var data UnrestrictResponse
|
||||
if err = json.Unmarshal(resp, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
download := torrent.DownloadLinks{
|
||||
Link: data.Link,
|
||||
Filename: data.Filename,
|
||||
DownloadLink: data.Download,
|
||||
}
|
||||
downloadLinks[f.Id] = download
|
||||
}
|
||||
t.DownloadLinks = downloadLinks
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RealDebrid) GetDownloadLink(t *torrent.Torrent, file *torrent.File) *torrent.DownloadLinks {
|
||||
url := fmt.Sprintf("%s/unrestrict/link/", r.Host)
|
||||
payload := gourl.Values{
|
||||
"link": {file.Link},
|
||||
}
|
||||
req, _ := http.NewRequest(http.MethodPost, url, strings.NewReader(payload.Encode()))
|
||||
resp, err := r.client.MakeRequest(req)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var data UnrestrictResponse
|
||||
if err = json.Unmarshal(resp, &data); err != nil {
|
||||
return nil
|
||||
}
|
||||
return &torrent.DownloadLinks{
|
||||
Link: data.Link,
|
||||
Filename: data.Filename,
|
||||
DownloadLink: data.Download,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RealDebrid) GetCheckCached() bool {
|
||||
return r.CheckCached
|
||||
}
|
||||
|
||||
func (r *RealDebrid) getTorrents(offset int, limit int) ([]*torrent.Torrent, error) {
|
||||
url := fmt.Sprintf("%s/torrents?limit=%d", r.Host, limit)
|
||||
if offset > 0 {
|
||||
url = fmt.Sprintf("%s&offset=%d", url, offset)
|
||||
}
|
||||
req, _ := http.NewRequest(http.MethodGet, url, nil)
|
||||
resp, err := r.client.MakeRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var data []TorrentsResponse
|
||||
if err = json.Unmarshal(resp, &data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
torrents := make([]*torrent.Torrent, 0)
|
||||
for _, t := range data {
|
||||
|
||||
torrents = append(torrents, &torrent.Torrent{
|
||||
Id: t.Id,
|
||||
Name: t.Filename,
|
||||
Bytes: t.Bytes,
|
||||
Progress: t.Progress,
|
||||
Status: t.Status,
|
||||
Filename: t.Filename,
|
||||
OriginalFilename: t.Filename,
|
||||
Links: t.Links,
|
||||
})
|
||||
}
|
||||
return torrents, nil
|
||||
}
|
||||
|
||||
func (r *RealDebrid) GetTorrents() ([]*torrent.Torrent, error) {
|
||||
torrents := make([]*torrent.Torrent, 0)
|
||||
offset := 0
|
||||
limit := 5000
|
||||
for {
|
||||
ts, err := r.getTorrents(offset, limit)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if len(ts) == 0 {
|
||||
break
|
||||
}
|
||||
torrents = append(torrents, ts...)
|
||||
offset = len(torrents)
|
||||
}
|
||||
return torrents, nil
|
||||
|
||||
}
|
||||
|
||||
func New(dc config.Debrid, cache *common.Cache) *RealDebrid {
|
||||
rl := request.ParseRateLimit(dc.RateLimit)
|
||||
headers := map[string]string{
|
||||
"Authorization": fmt.Sprintf("Bearer %s", dc.APIKey),
|
||||
}
|
||||
client := request.NewRLHTTPClient(rl, headers)
|
||||
return &RealDebrid{
|
||||
Name: "realdebrid",
|
||||
Host: dc.Host,
|
||||
APIKey: dc.APIKey,
|
||||
DownloadUncached: dc.DownloadUncached,
|
||||
client: client,
|
||||
cache: cache,
|
||||
MountPath: dc.Folder,
|
||||
logger: logger.NewLogger(dc.Name, config.GetConfig().LogLevel, os.Stdout),
|
||||
CheckCached: dc.CheckCached,
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
package types
|
||||
package realdebrid
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
type RealDebridAvailabilityResponse map[string]Hoster
|
||||
type AvailabilityResponse map[string]Hoster
|
||||
|
||||
func (r *RealDebridAvailabilityResponse) UnmarshalJSON(data []byte) error {
|
||||
func (r *AvailabilityResponse) UnmarshalJSON(data []byte) error {
|
||||
// First, try to unmarshal as an object
|
||||
var objectData map[string]Hoster
|
||||
err := json.Unmarshal(data, &objectData)
|
||||
@@ -64,12 +65,12 @@ type FileVariant struct {
|
||||
Filesize int `json:"filesize"`
|
||||
}
|
||||
|
||||
type RealDebridAddMagnetSchema struct {
|
||||
type AddMagnetSchema struct {
|
||||
Id string `json:"id"`
|
||||
Uri string `json:"uri"`
|
||||
}
|
||||
|
||||
type RealDebridTorrentInfo struct {
|
||||
type TorrentInfo struct {
|
||||
ID string `json:"id"`
|
||||
Filename string `json:"filename"`
|
||||
OriginalFilename string `json:"original_filename"`
|
||||
@@ -93,7 +94,7 @@ type RealDebridTorrentInfo struct {
|
||||
Seeders int `json:"seeders,omitempty"`
|
||||
}
|
||||
|
||||
type RealDebridUnrestrictResponse struct {
|
||||
type UnrestrictResponse struct {
|
||||
Id string `json:"id"`
|
||||
Filename string `json:"filename"`
|
||||
MimeType string `json:"mimeType"`
|
||||
@@ -105,3 +106,17 @@ type RealDebridUnrestrictResponse struct {
|
||||
Download string `json:"download"`
|
||||
Streamable int `json:"streamable"`
|
||||
}
|
||||
|
||||
type TorrentsResponse struct {
|
||||
Id string `json:"id"`
|
||||
Filename string `json:"filename"`
|
||||
Hash string `json:"hash"`
|
||||
Bytes int64 `json:"bytes"`
|
||||
Host string `json:"host"`
|
||||
Split int64 `json:"split"`
|
||||
Progress float64 `json:"progress"`
|
||||
Status string `json:"status"`
|
||||
Added time.Time `json:"added"`
|
||||
Links []string `json:"links"`
|
||||
Ended time.Time `json:"ended"`
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package debrid
|
||||
|
||||
type DebridService struct {
|
||||
debrids []Service
|
||||
lastUsed int
|
||||
}
|
||||
|
||||
func (d *DebridService) Get() Service {
|
||||
if d.lastUsed == 0 {
|
||||
return d.debrids[0]
|
||||
}
|
||||
return d.debrids[d.lastUsed]
|
||||
}
|
||||
|
||||
func (d *DebridService) GetByName(name string) Service {
|
||||
for _, deb := range d.debrids {
|
||||
if deb.GetName() == name {
|
||||
return deb
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package debrid
|
||||
package torbox
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -9,7 +9,8 @@ import (
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/config"
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/logger"
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/request"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/debrid/types"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/debrid/torrent"
|
||||
|
||||
"log"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
@@ -23,28 +24,36 @@ import (
|
||||
)
|
||||
|
||||
type Torbox struct {
|
||||
BaseDebrid
|
||||
Name string
|
||||
Host string `json:"host"`
|
||||
APIKey string
|
||||
DownloadUncached bool
|
||||
client *request.RLHTTPClient
|
||||
cache *common.Cache
|
||||
MountPath string
|
||||
logger zerolog.Logger
|
||||
CheckCached bool
|
||||
}
|
||||
|
||||
func (r *Torbox) GetMountPath() string {
|
||||
return r.MountPath
|
||||
func (tb *Torbox) GetMountPath() string {
|
||||
return tb.MountPath
|
||||
}
|
||||
|
||||
func (r *Torbox) GetName() string {
|
||||
return r.Name
|
||||
func (tb *Torbox) GetName() string {
|
||||
return tb.Name
|
||||
}
|
||||
|
||||
func (r *Torbox) GetLogger() zerolog.Logger {
|
||||
return r.logger
|
||||
func (tb *Torbox) GetLogger() zerolog.Logger {
|
||||
return tb.logger
|
||||
}
|
||||
|
||||
func (r *Torbox) IsAvailable(infohashes []string) map[string]bool {
|
||||
func (tb *Torbox) IsAvailable(infohashes []string) map[string]bool {
|
||||
// Check if the infohashes are available in the local cache
|
||||
hashes, result := GetLocalCache(infohashes, r.cache)
|
||||
hashes, result := torrent.GetLocalCache(infohashes, tb.cache)
|
||||
|
||||
if len(hashes) == 0 {
|
||||
// Either all the infohashes are locally cached or none are
|
||||
r.cache.AddMultiple(result)
|
||||
tb.cache.AddMultiple(result)
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -69,17 +78,17 @@ func (r *Torbox) IsAvailable(infohashes []string) map[string]bool {
|
||||
}
|
||||
|
||||
hashStr := strings.Join(validHashes, ",")
|
||||
url := fmt.Sprintf("%s/api/torrents/checkcached?hash=%s", r.Host, hashStr)
|
||||
url := fmt.Sprintf("%s/api/torrents/checkcached?hash=%s", tb.Host, hashStr)
|
||||
req, _ := http.NewRequest(http.MethodGet, url, nil)
|
||||
resp, err := r.client.MakeRequest(req)
|
||||
resp, err := tb.client.MakeRequest(req)
|
||||
if err != nil {
|
||||
r.logger.Info().Msgf("Error checking availability: %v", err)
|
||||
tb.logger.Info().Msgf("Error checking availability: %v", err)
|
||||
return result
|
||||
}
|
||||
var res types.TorBoxAvailableResponse
|
||||
var res AvailableResponse
|
||||
err = json.Unmarshal(resp, &res)
|
||||
if err != nil {
|
||||
r.logger.Info().Msgf("Error marshalling availability: %v", err)
|
||||
tb.logger.Info().Msgf("Error marshalling availability: %v", err)
|
||||
return result
|
||||
}
|
||||
if res.Data == nil {
|
||||
@@ -92,12 +101,12 @@ func (r *Torbox) IsAvailable(infohashes []string) map[string]bool {
|
||||
}
|
||||
}
|
||||
}
|
||||
r.cache.AddMultiple(result) // Add the results to the cache
|
||||
tb.cache.AddMultiple(result) // Add the results to the cache
|
||||
return result
|
||||
}
|
||||
|
||||
func (r *Torbox) SubmitMagnet(torrent *Torrent) (*Torrent, error) {
|
||||
url := fmt.Sprintf("%s/api/torrents/createtorrent", r.Host)
|
||||
func (tb *Torbox) SubmitMagnet(torrent *torrent.Torrent) (*torrent.Torrent, error) {
|
||||
url := fmt.Sprintf("%s/api/torrents/createtorrent", tb.Host)
|
||||
payload := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(payload)
|
||||
_ = writer.WriteField("magnet", torrent.Magnet.Link)
|
||||
@@ -107,11 +116,11 @@ func (r *Torbox) SubmitMagnet(torrent *Torrent) (*Torrent, error) {
|
||||
}
|
||||
req, _ := http.NewRequest(http.MethodPost, url, payload)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
resp, err := r.client.MakeRequest(req)
|
||||
resp, err := tb.client.MakeRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var data types.TorBoxAddMagnetResponse
|
||||
var data AddMagnetResponse
|
||||
err = json.Unmarshal(resp, &data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -143,32 +152,35 @@ func getTorboxStatus(status string, finished bool) string {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Torbox) GetTorrent(id string) (*Torrent, error) {
|
||||
torrent := &Torrent{}
|
||||
url := fmt.Sprintf("%s/api/torrents/mylist/?id=%s", r.Host, id)
|
||||
func (tb *Torbox) GetTorrent(id string) (*torrent.Torrent, error) {
|
||||
t := &torrent.Torrent{}
|
||||
url := fmt.Sprintf("%s/api/torrents/mylist/?id=%s", tb.Host, id)
|
||||
req, _ := http.NewRequest(http.MethodGet, url, nil)
|
||||
resp, err := r.client.MakeRequest(req)
|
||||
resp, err := tb.client.MakeRequest(req)
|
||||
if err != nil {
|
||||
return torrent, err
|
||||
return t, err
|
||||
}
|
||||
var res types.TorboxInfoResponse
|
||||
var res InfoResponse
|
||||
err = json.Unmarshal(resp, &res)
|
||||
if err != nil {
|
||||
return torrent, err
|
||||
return t, err
|
||||
}
|
||||
data := res.Data
|
||||
name := data.Name
|
||||
torrent.Id = id
|
||||
torrent.Name = name
|
||||
torrent.Bytes = data.Size
|
||||
torrent.Folder = name
|
||||
torrent.Progress = data.Progress * 100
|
||||
torrent.Status = getTorboxStatus(data.DownloadState, data.DownloadFinished)
|
||||
torrent.Speed = data.DownloadSpeed
|
||||
torrent.Seeders = data.Seeds
|
||||
torrent.Filename = name
|
||||
torrent.OriginalFilename = name
|
||||
files := make([]TorrentFile, 0)
|
||||
t.Id = id
|
||||
t.Name = name
|
||||
t.Bytes = data.Size
|
||||
t.Folder = name
|
||||
t.Progress = data.Progress * 100
|
||||
t.Status = getTorboxStatus(data.DownloadState, data.DownloadFinished)
|
||||
t.Speed = data.DownloadSpeed
|
||||
t.Seeders = data.Seeds
|
||||
t.Filename = name
|
||||
t.OriginalFilename = name
|
||||
t.MountPath = tb.MountPath
|
||||
t.Debrid = tb.Name
|
||||
t.DownloadLinks = make(map[string]torrent.DownloadLinks)
|
||||
files := make([]torrent.File, 0)
|
||||
cfg := config.GetConfig()
|
||||
for _, f := range data.Files {
|
||||
fileName := filepath.Base(f.Name)
|
||||
@@ -183,7 +195,7 @@ func (r *Torbox) GetTorrent(id string) (*Torrent, error) {
|
||||
if !cfg.IsSizeAllowed(f.Size) {
|
||||
continue
|
||||
}
|
||||
file := TorrentFile{
|
||||
file := torrent.File{
|
||||
Id: strconv.Itoa(f.Id),
|
||||
Name: fileName,
|
||||
Size: f.Size,
|
||||
@@ -198,34 +210,34 @@ func (r *Torbox) GetTorrent(id string) (*Torrent, error) {
|
||||
cleanPath = path.Clean(data.Name)
|
||||
}
|
||||
|
||||
torrent.OriginalFilename = strings.Split(cleanPath, "/")[0]
|
||||
torrent.Files = files
|
||||
torrent.Debrid = r
|
||||
return torrent, nil
|
||||
t.OriginalFilename = strings.Split(cleanPath, "/")[0]
|
||||
t.Files = files
|
||||
//t.Debrid = tb
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (r *Torbox) CheckStatus(torrent *Torrent, isSymlink bool) (*Torrent, error) {
|
||||
func (tb *Torbox) CheckStatus(torrent *torrent.Torrent, isSymlink bool) (*torrent.Torrent, error) {
|
||||
for {
|
||||
tb, err := r.GetTorrent(torrent.Id)
|
||||
t, err := tb.GetTorrent(torrent.Id)
|
||||
|
||||
torrent = tb
|
||||
torrent = t
|
||||
|
||||
if err != nil || tb == nil {
|
||||
return tb, err
|
||||
if err != nil || t == nil {
|
||||
return t, err
|
||||
}
|
||||
status := torrent.Status
|
||||
if status == "downloaded" {
|
||||
r.logger.Info().Msgf("Torrent: %s downloaded", torrent.Name)
|
||||
tb.logger.Info().Msgf("Torrent: %s downloaded", torrent.Name)
|
||||
if !isSymlink {
|
||||
err = r.GetDownloadLinks(torrent)
|
||||
err = tb.GetDownloadLinks(torrent)
|
||||
if err != nil {
|
||||
return torrent, err
|
||||
}
|
||||
}
|
||||
break
|
||||
} else if status == "downloading" {
|
||||
if !r.DownloadUncached {
|
||||
go torrent.Delete()
|
||||
if !tb.DownloadUncached {
|
||||
go tb.DeleteTorrent(torrent)
|
||||
return torrent, fmt.Errorf("torrent: %s not cached", torrent.Name)
|
||||
}
|
||||
// Break out of the loop if the torrent is downloading.
|
||||
@@ -239,34 +251,34 @@ func (r *Torbox) CheckStatus(torrent *Torrent, isSymlink bool) (*Torrent, error)
|
||||
return torrent, nil
|
||||
}
|
||||
|
||||
func (r *Torbox) DeleteTorrent(torrent *Torrent) {
|
||||
url := fmt.Sprintf("%s/api/torrents/controltorrent/%s", r.Host, torrent.Id)
|
||||
func (tb *Torbox) DeleteTorrent(torrent *torrent.Torrent) {
|
||||
url := fmt.Sprintf("%s/api/torrents/controltorrent/%s", tb.Host, torrent.Id)
|
||||
payload := map[string]string{"torrent_id": torrent.Id, "action": "Delete"}
|
||||
jsonPayload, _ := json.Marshal(payload)
|
||||
req, _ := http.NewRequest(http.MethodDelete, url, bytes.NewBuffer(jsonPayload))
|
||||
_, err := r.client.MakeRequest(req)
|
||||
_, err := tb.client.MakeRequest(req)
|
||||
if err == nil {
|
||||
r.logger.Info().Msgf("Torrent: %s deleted", torrent.Name)
|
||||
tb.logger.Info().Msgf("Torrent: %s deleted", torrent.Name)
|
||||
} else {
|
||||
r.logger.Info().Msgf("Error deleting torrent: %s", err)
|
||||
tb.logger.Info().Msgf("Error deleting torrent: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Torbox) GetDownloadLinks(torrent *Torrent) error {
|
||||
downloadLinks := make([]TorrentDownloadLinks, 0)
|
||||
for _, file := range torrent.Files {
|
||||
url := fmt.Sprintf("%s/api/torrents/requestdl/", r.Host)
|
||||
func (tb *Torbox) GetDownloadLinks(t *torrent.Torrent) error {
|
||||
downloadLinks := make(map[string]torrent.DownloadLinks)
|
||||
for _, file := range t.Files {
|
||||
url := fmt.Sprintf("%s/api/torrents/requestdl/", tb.Host)
|
||||
query := gourl.Values{}
|
||||
query.Add("torrent_id", torrent.Id)
|
||||
query.Add("token", r.APIKey)
|
||||
query.Add("torrent_id", t.Id)
|
||||
query.Add("token", tb.APIKey)
|
||||
query.Add("file_id", file.Id)
|
||||
url += "?" + query.Encode()
|
||||
req, _ := http.NewRequest(http.MethodGet, url, nil)
|
||||
resp, err := r.client.MakeRequest(req)
|
||||
resp, err := tb.client.MakeRequest(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var data types.TorBoxDownloadLinksResponse
|
||||
var data DownloadLinksResponse
|
||||
if err = json.Unmarshal(resp, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -276,38 +288,67 @@ func (r *Torbox) GetDownloadLinks(torrent *Torrent) error {
|
||||
idx := 0
|
||||
link := *data.Data
|
||||
|
||||
dl := TorrentDownloadLinks{
|
||||
dl := torrent.DownloadLinks{
|
||||
Link: link,
|
||||
Filename: torrent.Files[idx].Name,
|
||||
Filename: t.Files[idx].Name,
|
||||
DownloadLink: link,
|
||||
}
|
||||
downloadLinks = append(downloadLinks, dl)
|
||||
downloadLinks[file.Id] = dl
|
||||
}
|
||||
torrent.DownloadLinks = downloadLinks
|
||||
t.DownloadLinks = downloadLinks
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Torbox) GetCheckCached() bool {
|
||||
return r.CheckCached
|
||||
func (tb *Torbox) GetDownloadLink(t *torrent.Torrent, file *torrent.File) *torrent.DownloadLinks {
|
||||
url := fmt.Sprintf("%s/api/torrents/requestdl/", tb.Host)
|
||||
query := gourl.Values{}
|
||||
query.Add("torrent_id", t.Id)
|
||||
query.Add("token", tb.APIKey)
|
||||
query.Add("file_id", file.Id)
|
||||
url += "?" + query.Encode()
|
||||
req, _ := http.NewRequest(http.MethodGet, url, nil)
|
||||
resp, err := tb.client.MakeRequest(req)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var data DownloadLinksResponse
|
||||
if err = json.Unmarshal(resp, &data); err != nil {
|
||||
return nil
|
||||
}
|
||||
if data.Data == nil {
|
||||
return nil
|
||||
}
|
||||
link := *data.Data
|
||||
return &torrent.DownloadLinks{
|
||||
Link: file.Link,
|
||||
Filename: file.Name,
|
||||
DownloadLink: link,
|
||||
}
|
||||
}
|
||||
|
||||
func NewTorbox(dc config.Debrid, cache *common.Cache) *Torbox {
|
||||
func (tb *Torbox) GetCheckCached() bool {
|
||||
return tb.CheckCached
|
||||
}
|
||||
|
||||
func (tb *Torbox) GetTorrents() ([]*torrent.Torrent, error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
func New(dc config.Debrid, cache *common.Cache) *Torbox {
|
||||
rl := request.ParseRateLimit(dc.RateLimit)
|
||||
headers := map[string]string{
|
||||
"Authorization": fmt.Sprintf("Bearer %s", dc.APIKey),
|
||||
}
|
||||
client := request.NewRLHTTPClient(rl, headers)
|
||||
return &Torbox{
|
||||
BaseDebrid: BaseDebrid{
|
||||
Name: "torbox",
|
||||
Host: dc.Host,
|
||||
APIKey: dc.APIKey,
|
||||
DownloadUncached: dc.DownloadUncached,
|
||||
client: client,
|
||||
cache: cache,
|
||||
MountPath: dc.Folder,
|
||||
logger: logger.NewLogger(dc.Name, config.GetConfig().LogLevel, os.Stdout),
|
||||
CheckCached: dc.CheckCached,
|
||||
},
|
||||
Name: "torbox",
|
||||
Host: dc.Host,
|
||||
APIKey: dc.APIKey,
|
||||
DownloadUncached: dc.DownloadUncached,
|
||||
client: client,
|
||||
cache: cache,
|
||||
MountPath: dc.Folder,
|
||||
logger: logger.NewLogger(dc.Name, config.GetConfig().LogLevel, os.Stdout),
|
||||
CheckCached: dc.CheckCached,
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,21 @@
|
||||
package types
|
||||
package torbox
|
||||
|
||||
import "time"
|
||||
|
||||
type TorboxAPIResponse[T any] struct {
|
||||
type APIResponse[T any] struct {
|
||||
Success bool `json:"success"`
|
||||
Error any `json:"error"`
|
||||
Detail string `json:"detail"`
|
||||
Data *T `json:"data"` // Use pointer to allow nil
|
||||
}
|
||||
|
||||
type TorBoxAvailableResponse TorboxAPIResponse[map[string]struct {
|
||||
type AvailableResponse APIResponse[map[string]struct {
|
||||
Name string `json:"name"`
|
||||
Size int `json:"size"`
|
||||
Hash string `json:"hash"`
|
||||
}]
|
||||
|
||||
type TorBoxAddMagnetResponse TorboxAPIResponse[struct {
|
||||
type AddMagnetResponse APIResponse[struct {
|
||||
Id int `json:"torrent_id"`
|
||||
Hash string `json:"hash"`
|
||||
}]
|
||||
@@ -70,6 +70,6 @@ type torboxInfo struct {
|
||||
TrackerMessage interface{} `json:"tracker_message"`
|
||||
}
|
||||
|
||||
type TorboxInfoResponse TorboxAPIResponse[torboxInfo]
|
||||
type InfoResponse APIResponse[torboxInfo]
|
||||
|
||||
type TorBoxDownloadLinksResponse TorboxAPIResponse[string]
|
||||
type DownloadLinksResponse APIResponse[string]
|
||||
@@ -1,122 +0,0 @@
|
||||
package debrid
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/sirrobot01/debrid-blackhole/common"
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/utils"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/arr"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Arr struct {
|
||||
Name string `json:"name"`
|
||||
Token string `json:"-"`
|
||||
Host string `json:"host"`
|
||||
}
|
||||
|
||||
type ArrHistorySchema struct {
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"pageSize"`
|
||||
SortKey string `json:"sortKey"`
|
||||
SortDirection string `json:"sortDirection"`
|
||||
TotalRecords int `json:"totalRecords"`
|
||||
Records []struct {
|
||||
ID int `json:"id"`
|
||||
DownloadID string `json:"downloadId"`
|
||||
} `json:"records"`
|
||||
}
|
||||
|
||||
type Torrent struct {
|
||||
Id string `json:"id"`
|
||||
InfoHash string `json:"info_hash"`
|
||||
Name string `json:"name"`
|
||||
Folder string `json:"folder"`
|
||||
Filename string `json:"filename"`
|
||||
OriginalFilename string `json:"original_filename"`
|
||||
Size int64 `json:"size"`
|
||||
Bytes int64 `json:"bytes"` // Size of only the files that are downloaded
|
||||
Magnet *utils.Magnet `json:"magnet"`
|
||||
Files []TorrentFile `json:"files"`
|
||||
Status string `json:"status"`
|
||||
Added string `json:"added"`
|
||||
Progress float64 `json:"progress"`
|
||||
Speed int64 `json:"speed"`
|
||||
Seeders int `json:"seeders"`
|
||||
Links []string `json:"links"`
|
||||
DownloadLinks []TorrentDownloadLinks `json:"download_links"`
|
||||
|
||||
Debrid Service `json:"-"`
|
||||
Arr *arr.Arr `json:"arr"`
|
||||
Mu sync.Mutex `json:"-"`
|
||||
SizeDownloaded int64 `json:"-"` // This is used for local download
|
||||
}
|
||||
|
||||
type TorrentDownloadLinks struct {
|
||||
Filename string `json:"filename"`
|
||||
Link string `json:"link"`
|
||||
DownloadLink string `json:"download_link"`
|
||||
}
|
||||
|
||||
func (t *Torrent) GetSymlinkFolder(parent string) string {
|
||||
return filepath.Join(parent, t.Arr.Name, t.Folder)
|
||||
}
|
||||
|
||||
func (t *Torrent) GetMountFolder(rClonePath string) (string, error) {
|
||||
possiblePaths := []string{
|
||||
t.OriginalFilename,
|
||||
t.Filename,
|
||||
common.RemoveExtension(t.OriginalFilename),
|
||||
}
|
||||
|
||||
for _, path := range possiblePaths {
|
||||
_, err := os.Stat(filepath.Join(rClonePath, path))
|
||||
if !os.IsNotExist(err) {
|
||||
return path, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("no path found")
|
||||
}
|
||||
|
||||
func (t *Torrent) Delete() {
|
||||
if t.Debrid == nil {
|
||||
return
|
||||
}
|
||||
t.Debrid.DeleteTorrent(t)
|
||||
|
||||
}
|
||||
|
||||
type TorrentFile struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
Path string `json:"path"`
|
||||
Link string `json:"link"`
|
||||
}
|
||||
|
||||
func getEventId(eventType string) int {
|
||||
switch eventType {
|
||||
case "grabbed":
|
||||
return 1
|
||||
case "seriesFolderDownloaded":
|
||||
return 2
|
||||
case "DownloadFolderImported":
|
||||
return 3
|
||||
case "DownloadFailed":
|
||||
return 4
|
||||
case "DownloadIgnored":
|
||||
return 7
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Torrent) Cleanup(remove bool) {
|
||||
if remove {
|
||||
err := os.Remove(t.Filename)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
135
pkg/debrid/torrent/torrent.go
Normal file
135
pkg/debrid/torrent/torrent.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package torrent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/sirrobot01/debrid-blackhole/common"
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/utils"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/arr"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Arr struct {
|
||||
Name string `json:"name"`
|
||||
Token string `json:"-"`
|
||||
Host string `json:"host"`
|
||||
}
|
||||
|
||||
type ArrHistorySchema struct {
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"pageSize"`
|
||||
SortKey string `json:"sortKey"`
|
||||
SortDirection string `json:"sortDirection"`
|
||||
TotalRecords int `json:"totalRecords"`
|
||||
Records []struct {
|
||||
ID int `json:"id"`
|
||||
DownloadID string `json:"downloadId"`
|
||||
} `json:"records"`
|
||||
}
|
||||
|
||||
type Torrent struct {
|
||||
Id string `json:"id"`
|
||||
InfoHash string `json:"info_hash"`
|
||||
Name string `json:"name"`
|
||||
Folder string `json:"folder"`
|
||||
Filename string `json:"filename"`
|
||||
OriginalFilename string `json:"original_filename"`
|
||||
Size int64 `json:"size"`
|
||||
Bytes int64 `json:"bytes"` // Size of only the files that are downloaded
|
||||
Magnet *utils.Magnet `json:"magnet"`
|
||||
Files []File `json:"files"`
|
||||
Status string `json:"status"`
|
||||
Added string `json:"added"`
|
||||
Progress float64 `json:"progress"`
|
||||
Speed int64 `json:"speed"`
|
||||
Seeders int `json:"seeders"`
|
||||
Links []string `json:"links"`
|
||||
DownloadLinks map[string]DownloadLinks `json:"download_links"`
|
||||
MountPath string `json:"mount_path"`
|
||||
|
||||
Debrid string `json:"debrid"`
|
||||
|
||||
Arr *arr.Arr `json:"arr"`
|
||||
Mu sync.Mutex `json:"-"`
|
||||
SizeDownloaded int64 `json:"-"` // This is used for local download
|
||||
}
|
||||
|
||||
type DownloadLinks struct {
|
||||
Filename string `json:"filename"`
|
||||
Link string `json:"link"`
|
||||
DownloadLink string `json:"download_link"`
|
||||
}
|
||||
|
||||
func (t *Torrent) GetSymlinkFolder(parent string) string {
|
||||
return filepath.Join(parent, t.Arr.Name, t.Folder)
|
||||
}
|
||||
|
||||
func (t *Torrent) GetMountFolder(rClonePath string) (string, error) {
|
||||
possiblePaths := []string{
|
||||
t.OriginalFilename,
|
||||
t.Filename,
|
||||
common.RemoveExtension(t.OriginalFilename),
|
||||
}
|
||||
|
||||
for _, path := range possiblePaths {
|
||||
_, err := os.Stat(filepath.Join(rClonePath, path))
|
||||
if !os.IsNotExist(err) {
|
||||
return path, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("no path found")
|
||||
}
|
||||
|
||||
type File struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
Path string `json:"path"`
|
||||
Link string `json:"link"`
|
||||
}
|
||||
|
||||
func (t *Torrent) Cleanup(remove bool) {
|
||||
if remove {
|
||||
err := os.Remove(t.Filename)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Torrent) GetFile(id string) *File {
|
||||
for _, f := range t.Files {
|
||||
if f.Id == id {
|
||||
return &f
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetLocalCache(infohashes []string, cache *common.Cache) ([]string, map[string]bool) {
|
||||
result := make(map[string]bool)
|
||||
hashes := make([]string, 0)
|
||||
|
||||
if len(infohashes) == 0 {
|
||||
return hashes, result
|
||||
}
|
||||
if len(infohashes) == 1 {
|
||||
if cache.Exists(infohashes[0]) {
|
||||
return hashes, map[string]bool{infohashes[0]: true}
|
||||
}
|
||||
return infohashes, result
|
||||
}
|
||||
|
||||
cachedHashes := cache.GetMultiple(infohashes)
|
||||
for _, h := range infohashes {
|
||||
_, exists := cachedHashes[h]
|
||||
if !exists {
|
||||
hashes = append(hashes, h)
|
||||
} else {
|
||||
result[h] = true
|
||||
}
|
||||
}
|
||||
|
||||
return infohashes, result
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
package types
|
||||
|
||||
type errorResponse struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type AllDebridMagnetFile struct {
|
||||
Name string `json:"n"`
|
||||
Size int64 `json:"s"`
|
||||
Link string `json:"l"`
|
||||
Elements []AllDebridMagnetFile `json:"e"`
|
||||
}
|
||||
type magnetInfo struct {
|
||||
Id int `json:"id"`
|
||||
Filename string `json:"filename"`
|
||||
Size int64 `json:"size"`
|
||||
Hash string `json:"hash"`
|
||||
Status string `json:"status"`
|
||||
StatusCode int `json:"statusCode"`
|
||||
UploadDate int `json:"uploadDate"`
|
||||
Downloaded int64 `json:"downloaded"`
|
||||
Uploaded int64 `json:"uploaded"`
|
||||
DownloadSpeed int64 `json:"downloadSpeed"`
|
||||
UploadSpeed int64 `json:"uploadSpeed"`
|
||||
Seeders int `json:"seeders"`
|
||||
CompletionDate int `json:"completionDate"`
|
||||
Type string `json:"type"`
|
||||
Notified bool `json:"notified"`
|
||||
Version int `json:"version"`
|
||||
NbLinks int `json:"nbLinks"`
|
||||
Files []AllDebridMagnetFile `json:"files"`
|
||||
}
|
||||
|
||||
type AllDebridTorrentInfoResponse struct {
|
||||
Status string `json:"status"`
|
||||
Data struct {
|
||||
Magnets magnetInfo `json:"magnets"`
|
||||
} `json:"data"`
|
||||
Error *errorResponse `json:"error"`
|
||||
}
|
||||
|
||||
type AllDebridUploadMagnetResponse struct {
|
||||
Status string `json:"status"`
|
||||
Data struct {
|
||||
Magnets []struct {
|
||||
Magnet string `json:"magnet"`
|
||||
Hash string `json:"hash"`
|
||||
Name string `json:"name"`
|
||||
FilenameOriginal string `json:"filename_original"`
|
||||
Size int64 `json:"size"`
|
||||
Ready bool `json:"ready"`
|
||||
ID int `json:"id"`
|
||||
} `json:"magnets"`
|
||||
}
|
||||
Error *errorResponse `json:"error"`
|
||||
}
|
||||
|
||||
type AllDebridDownloadLink struct {
|
||||
Status string `json:"status"`
|
||||
Data struct {
|
||||
Link string `json:"link"`
|
||||
Host string `json:"host"`
|
||||
Filename string `json:"filename"`
|
||||
Streaming []interface{} `json:"streaming"`
|
||||
Paws bool `json:"paws"`
|
||||
Filesize int `json:"filesize"`
|
||||
Id string `json:"id"`
|
||||
Path []struct {
|
||||
Name string `json:"n"`
|
||||
Size int `json:"s"`
|
||||
} `json:"path"`
|
||||
} `json:"data"`
|
||||
Error *errorResponse `json:"error"`
|
||||
}
|
||||
2
pkg/downloader/grab.go
Normal file
2
pkg/downloader/grab.go
Normal file
@@ -0,0 +1,2 @@
|
||||
package downloader
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
package downloaders
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"github.com/valyala/fasthttp"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
func GetFastHTTPClient() *fasthttp.Client {
|
||||
return &fasthttp.Client{
|
||||
TLSConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
StreamResponseBody: true,
|
||||
}
|
||||
}
|
||||
|
||||
func NormalFastHTTP(client *fasthttp.Client, url, filename string) error {
|
||||
req := fasthttp.AcquireRequest()
|
||||
resp := fasthttp.AcquireResponse()
|
||||
defer fasthttp.ReleaseRequest(req)
|
||||
defer fasthttp.ReleaseResponse(resp)
|
||||
|
||||
req.SetRequestURI(url)
|
||||
req.Header.SetMethod(fasthttp.MethodGet)
|
||||
|
||||
if err := client.Do(req, resp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check the response status code
|
||||
if resp.StatusCode() != fasthttp.StatusOK {
|
||||
return fmt.Errorf("unexpected status code: %d", resp.StatusCode())
|
||||
}
|
||||
file, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func(file *os.File) {
|
||||
err := file.Close()
|
||||
if err != nil {
|
||||
fmt.Println("Error closing file:", err)
|
||||
return
|
||||
}
|
||||
}(file)
|
||||
bodyStream := resp.BodyStream()
|
||||
if bodyStream == nil {
|
||||
// Write to memory and then to file
|
||||
_, err := file.Write(resp.Body())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if _, err := io.Copy(file, bodyStream); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
package downloaders
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"github.com/cavaliergopher/grab/v3"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func GetGrabClient() *grab.Client {
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
}
|
||||
return &grab.Client{
|
||||
UserAgent: "qBitTorrent",
|
||||
HTTPClient: &http.Client{
|
||||
Transport: tr,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func NormalGrab(client *grab.Client, url, filename string, progressCallback func(int64, int64)) error {
|
||||
req, err := grab.NewRequest(filename, url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp := client.Do(req)
|
||||
|
||||
t := time.NewTicker(time.Second)
|
||||
defer t.Stop()
|
||||
|
||||
var lastReported int64
|
||||
Loop:
|
||||
for {
|
||||
select {
|
||||
case <-t.C:
|
||||
current := resp.BytesComplete()
|
||||
speed := int64(resp.BytesPerSecond())
|
||||
if current != lastReported {
|
||||
if progressCallback != nil {
|
||||
progressCallback(current-lastReported, speed)
|
||||
}
|
||||
lastReported = current
|
||||
}
|
||||
case <-resp.Done:
|
||||
break Loop
|
||||
}
|
||||
}
|
||||
|
||||
// Report final bytes
|
||||
if progressCallback != nil {
|
||||
progressCallback(resp.BytesComplete()-lastReported, 0)
|
||||
}
|
||||
|
||||
return resp.Err()
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package downloaders
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
func GetHTTPClient() *http.Client {
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
return &http.Client{Transport: tr}
|
||||
}
|
||||
|
||||
func NormalHTTP(client *http.Client, url, filename string) error {
|
||||
file, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Send the HTTP GET request
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
fmt.Println("Error downloading file:", err)
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Check server response
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("server returned non-200 status: %d %s", resp.StatusCode, resp.Status)
|
||||
}
|
||||
|
||||
// Write the response body to file
|
||||
_, err = io.Copy(file, resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/config"
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/logger"
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/utils"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/debrid"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/service"
|
||||
"github.com/valyala/fastjson"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -76,11 +76,10 @@ type Proxy struct {
|
||||
username string
|
||||
password string
|
||||
cachedOnly bool
|
||||
debrid debrid.Service
|
||||
logger zerolog.Logger
|
||||
}
|
||||
|
||||
func NewProxy(deb *debrid.DebridService) *Proxy {
|
||||
func NewProxy() *Proxy {
|
||||
cfg := config.GetConfig().Proxy
|
||||
port := cmp.Or(os.Getenv("PORT"), cfg.Port, "8181")
|
||||
return &Proxy{
|
||||
@@ -89,8 +88,7 @@ func NewProxy(deb *debrid.DebridService) *Proxy {
|
||||
username: cfg.Username,
|
||||
password: cfg.Password,
|
||||
cachedOnly: *cfg.CachedOnly,
|
||||
debrid: deb.Get(),
|
||||
logger: logger.NewLogger("Proxy", cfg.LogLevel, os.Stdout),
|
||||
logger: logger.NewLogger("proxy", cfg.LogLevel, os.Stdout),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,6 +226,8 @@ func (p *Proxy) ProcessXMLResponse(resp *http.Response) *http.Response {
|
||||
return resp
|
||||
}
|
||||
|
||||
svc := service.GetService()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
p.logger.Info().Msgf("Error reading response body: %v", err)
|
||||
@@ -262,7 +262,7 @@ func (p *Proxy) ProcessXMLResponse(resp *http.Response) *http.Response {
|
||||
hashes = append(hashes, hash)
|
||||
}
|
||||
}
|
||||
availableHashesMap := p.debrid.IsAvailable(hashes)
|
||||
availableHashesMap := svc.Debrid.Get().IsAvailable(hashes)
|
||||
newItems := make([]Item, 0, len(rss.Channel.Items))
|
||||
|
||||
if len(hashes) > 0 {
|
||||
|
||||
@@ -1,16 +1,54 @@
|
||||
package shared
|
||||
package qbit
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"github.com/cavaliergopher/grab/v3"
|
||||
"github.com/sirrobot01/debrid-blackhole/common"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/debrid"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/downloaders"
|
||||
debrid "github.com/sirrobot01/debrid-blackhole/pkg/debrid/torrent"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Download(client *grab.Client, url, filename string, progressCallback func(int64, int64)) error {
|
||||
req, err := grab.NewRequest(filename, url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp := client.Do(req)
|
||||
|
||||
t := time.NewTicker(time.Second)
|
||||
defer t.Stop()
|
||||
|
||||
var lastReported int64
|
||||
Loop:
|
||||
for {
|
||||
select {
|
||||
case <-t.C:
|
||||
current := resp.BytesComplete()
|
||||
speed := int64(resp.BytesPerSecond())
|
||||
if current != lastReported {
|
||||
if progressCallback != nil {
|
||||
progressCallback(current-lastReported, speed)
|
||||
}
|
||||
lastReported = current
|
||||
}
|
||||
case <-resp.Done:
|
||||
break Loop
|
||||
}
|
||||
}
|
||||
|
||||
// Report final bytes
|
||||
if progressCallback != nil {
|
||||
progressCallback(resp.BytesComplete()-lastReported, 0)
|
||||
}
|
||||
|
||||
return resp.Err()
|
||||
}
|
||||
|
||||
func (q *QBit) ProcessManualFile(torrent *Torrent) (string, error) {
|
||||
debridTorrent := torrent.DebridTorrent
|
||||
q.logger.Info().Msgf("Downloading %d files...", len(debridTorrent.DownloadLinks))
|
||||
@@ -37,7 +75,6 @@ func (q *QBit) downloadFiles(torrent *Torrent, parent string) {
|
||||
debridTorrent.SizeDownloaded = 0 // Reset downloaded bytes
|
||||
debridTorrent.Progress = 0 // Reset progress
|
||||
debridTorrent.Mu.Unlock()
|
||||
client := downloaders.GetGrabClient()
|
||||
progressCallback := func(downloaded int64, speed int64) {
|
||||
debridTorrent.Mu.Lock()
|
||||
defer debridTorrent.Mu.Unlock()
|
||||
@@ -54,6 +91,17 @@ func (q *QBit) downloadFiles(torrent *Torrent, parent string) {
|
||||
}
|
||||
q.UpdateTorrentMin(torrent, debridTorrent)
|
||||
}
|
||||
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
}
|
||||
client := &grab.Client{
|
||||
UserAgent: "qBitTorrent",
|
||||
HTTPClient: &http.Client{
|
||||
Transport: tr,
|
||||
},
|
||||
}
|
||||
for _, link := range debridTorrent.DownloadLinks {
|
||||
if link.DownloadLink == "" {
|
||||
q.logger.Info().Msgf("No download link found for %s", link.Filename)
|
||||
@@ -61,12 +109,12 @@ func (q *QBit) downloadFiles(torrent *Torrent, parent string) {
|
||||
}
|
||||
wg.Add(1)
|
||||
semaphore <- struct{}{}
|
||||
go func(link debrid.TorrentDownloadLinks) {
|
||||
go func(link debrid.DownloadLinks) {
|
||||
defer wg.Done()
|
||||
defer func() { <-semaphore }()
|
||||
filename := link.Filename
|
||||
|
||||
err := downloaders.NormalGrab(
|
||||
err := Download(
|
||||
client,
|
||||
link.DownloadLink,
|
||||
filepath.Join(parent, filename),
|
||||
@@ -88,12 +136,12 @@ func (q *QBit) ProcessSymlink(torrent *Torrent) (string, error) {
|
||||
debridTorrent := torrent.DebridTorrent
|
||||
var wg sync.WaitGroup
|
||||
files := debridTorrent.Files
|
||||
ready := make(chan debrid.TorrentFile, len(files))
|
||||
ready := make(chan debrid.File, len(files))
|
||||
if len(files) == 0 {
|
||||
return "", fmt.Errorf("no video files found")
|
||||
}
|
||||
q.logger.Info().Msgf("Checking %d files...", len(files))
|
||||
rCloneBase := debridTorrent.Debrid.GetMountPath()
|
||||
rCloneBase := debridTorrent.MountPath
|
||||
torrentPath, err := q.getTorrentPath(rCloneBase, debridTorrent) // /MyTVShow/
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get torrent path: %v", err)
|
||||
@@ -140,7 +188,7 @@ func (q *QBit) getTorrentPath(rclonePath string, debridTorrent *debrid.Torrent)
|
||||
}
|
||||
}
|
||||
|
||||
func (q *QBit) createSymLink(path string, torrentMountPath string, file debrid.TorrentFile) {
|
||||
func (q *QBit) createSymLink(path string, torrentMountPath string, file debrid.File) {
|
||||
|
||||
// Combine the directory and filename to form a full path
|
||||
fullPath := filepath.Join(path, file.Name) // /mnt/symlinks/{category}/MyTVShow/MyTVShow.S01E01.720p.mkv
|
||||
@@ -1,24 +1,17 @@
|
||||
package server
|
||||
package qbit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/request"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/arr"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/qbit/shared"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/service"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type QbitHandler struct {
|
||||
qbit *shared.QBit
|
||||
logger zerolog.Logger
|
||||
debug bool
|
||||
}
|
||||
|
||||
func decodeAuthHeader(header string) (string, string, error) {
|
||||
encodedTokens := strings.Split(header, " ")
|
||||
if len(encodedTokens) != 2 {
|
||||
@@ -40,7 +33,7 @@ func decodeAuthHeader(header string) (string, string, error) {
|
||||
return host, token, nil
|
||||
}
|
||||
|
||||
func (q *QbitHandler) CategoryContext(next http.Handler) http.Handler {
|
||||
func (q *QBit) CategoryContext(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
category := strings.Trim(r.URL.Query().Get("category"), "")
|
||||
if category == "" {
|
||||
@@ -59,7 +52,7 @@ func (q *QbitHandler) CategoryContext(next http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
func (q *QbitHandler) authContext(next http.Handler) http.Handler {
|
||||
func (q *QBit) authContext(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
host, token, err := decodeAuthHeader(r.Header.Get("Authorization"))
|
||||
category := r.Context().Value("category").(string)
|
||||
@@ -70,7 +63,8 @@ func (q *QbitHandler) authContext(next http.Handler) http.Handler {
|
||||
a.Host = strings.TrimSpace(host)
|
||||
a.Token = strings.TrimSpace(token)
|
||||
}
|
||||
q.qbit.Arrs.AddOrUpdate(a)
|
||||
svc := service.GetService()
|
||||
svc.Arr.AddOrUpdate(a)
|
||||
ctx := context.WithValue(r.Context(), "arr", a)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
@@ -96,30 +90,30 @@ func HashesCtx(next http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
func (q *QbitHandler) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
func (q *QBit) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte("Ok."))
|
||||
}
|
||||
|
||||
func (q *QbitHandler) handleVersion(w http.ResponseWriter, r *http.Request) {
|
||||
func (q *QBit) handleVersion(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte("v4.3.2"))
|
||||
}
|
||||
|
||||
func (q *QbitHandler) handleWebAPIVersion(w http.ResponseWriter, r *http.Request) {
|
||||
func (q *QBit) handleWebAPIVersion(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte("2.7"))
|
||||
}
|
||||
|
||||
func (q *QbitHandler) handlePreferences(w http.ResponseWriter, r *http.Request) {
|
||||
preferences := shared.NewAppPreferences()
|
||||
func (q *QBit) handlePreferences(w http.ResponseWriter, r *http.Request) {
|
||||
preferences := NewAppPreferences()
|
||||
|
||||
preferences.WebUiUsername = q.qbit.Username
|
||||
preferences.SavePath = q.qbit.DownloadFolder
|
||||
preferences.TempPath = filepath.Join(q.qbit.DownloadFolder, "temp")
|
||||
preferences.WebUiUsername = q.Username
|
||||
preferences.SavePath = q.DownloadFolder
|
||||
preferences.TempPath = filepath.Join(q.DownloadFolder, "temp")
|
||||
|
||||
request.JSONResponse(w, preferences, http.StatusOK)
|
||||
}
|
||||
|
||||
func (q *QbitHandler) handleBuildInfo(w http.ResponseWriter, r *http.Request) {
|
||||
res := shared.BuildInfo{
|
||||
func (q *QBit) handleBuildInfo(w http.ResponseWriter, r *http.Request) {
|
||||
res := BuildInfo{
|
||||
Bitness: 64,
|
||||
Boost: "1.75.0",
|
||||
Libtorrent: "1.2.11.0",
|
||||
@@ -130,21 +124,21 @@ func (q *QbitHandler) handleBuildInfo(w http.ResponseWriter, r *http.Request) {
|
||||
request.JSONResponse(w, res, http.StatusOK)
|
||||
}
|
||||
|
||||
func (q *QbitHandler) shutdown(w http.ResponseWriter, r *http.Request) {
|
||||
func (q *QBit) handleShutdown(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (q *QbitHandler) handleTorrentsInfo(w http.ResponseWriter, r *http.Request) {
|
||||
func (q *QBit) handleTorrentsInfo(w http.ResponseWriter, r *http.Request) {
|
||||
//log all url params
|
||||
ctx := r.Context()
|
||||
category := ctx.Value("category").(string)
|
||||
filter := strings.Trim(r.URL.Query().Get("filter"), "")
|
||||
hashes, _ := ctx.Value("hashes").([]string)
|
||||
torrents := q.qbit.Storage.GetAll(category, filter, hashes)
|
||||
torrents := q.Storage.GetAll(category, filter, hashes)
|
||||
request.JSONResponse(w, torrents, http.StatusOK)
|
||||
}
|
||||
|
||||
func (q *QbitHandler) handleTorrentsAdd(w http.ResponseWriter, r *http.Request) {
|
||||
func (q *QBit) handleTorrentsAdd(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// Parse form based on content type
|
||||
@@ -178,7 +172,7 @@ func (q *QbitHandler) handleTorrentsAdd(w http.ResponseWriter, r *http.Request)
|
||||
urlList = append(urlList, strings.TrimSpace(u))
|
||||
}
|
||||
for _, url := range urlList {
|
||||
if err := q.qbit.AddMagnet(ctx, url, category); err != nil {
|
||||
if err := q.AddMagnet(ctx, url, category); err != nil {
|
||||
q.logger.Info().Msgf("Error adding magnet: %v", err)
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
@@ -191,7 +185,7 @@ func (q *QbitHandler) handleTorrentsAdd(w http.ResponseWriter, r *http.Request)
|
||||
if r.MultipartForm != nil && r.MultipartForm.File != nil {
|
||||
if files := r.MultipartForm.File["torrents"]; len(files) > 0 {
|
||||
for _, fileHeader := range files {
|
||||
if err := q.qbit.AddTorrent(ctx, fileHeader, category); err != nil {
|
||||
if err := q.AddTorrent(ctx, fileHeader, category); err != nil {
|
||||
q.logger.Info().Msgf("Error adding torrent: %v", err)
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
@@ -209,7 +203,7 @@ func (q *QbitHandler) handleTorrentsAdd(w http.ResponseWriter, r *http.Request)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (q *QbitHandler) handleTorrentsDelete(w http.ResponseWriter, r *http.Request) {
|
||||
func (q *QBit) handleTorrentsDelete(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
hashes, _ := ctx.Value("hashes").([]string)
|
||||
if len(hashes) == 0 {
|
||||
@@ -217,59 +211,59 @@ func (q *QbitHandler) handleTorrentsDelete(w http.ResponseWriter, r *http.Reques
|
||||
return
|
||||
}
|
||||
for _, hash := range hashes {
|
||||
q.qbit.Storage.Delete(hash)
|
||||
q.Storage.Delete(hash)
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (q *QbitHandler) handleTorrentsPause(w http.ResponseWriter, r *http.Request) {
|
||||
func (q *QBit) handleTorrentsPause(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
hashes, _ := ctx.Value("hashes").([]string)
|
||||
for _, hash := range hashes {
|
||||
torrent := q.qbit.Storage.Get(hash)
|
||||
torrent := q.Storage.Get(hash)
|
||||
if torrent == nil {
|
||||
continue
|
||||
}
|
||||
go q.qbit.PauseTorrent(torrent)
|
||||
go q.PauseTorrent(torrent)
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (q *QbitHandler) handleTorrentsResume(w http.ResponseWriter, r *http.Request) {
|
||||
func (q *QBit) handleTorrentsResume(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
hashes, _ := ctx.Value("hashes").([]string)
|
||||
for _, hash := range hashes {
|
||||
torrent := q.qbit.Storage.Get(hash)
|
||||
torrent := q.Storage.Get(hash)
|
||||
if torrent == nil {
|
||||
continue
|
||||
}
|
||||
go q.qbit.ResumeTorrent(torrent)
|
||||
go q.ResumeTorrent(torrent)
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (q *QbitHandler) handleTorrentRecheck(w http.ResponseWriter, r *http.Request) {
|
||||
func (q *QBit) handleTorrentRecheck(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
hashes, _ := ctx.Value("hashes").([]string)
|
||||
for _, hash := range hashes {
|
||||
torrent := q.qbit.Storage.Get(hash)
|
||||
torrent := q.Storage.Get(hash)
|
||||
if torrent == nil {
|
||||
continue
|
||||
}
|
||||
go q.qbit.RefreshTorrent(torrent)
|
||||
go q.RefreshTorrent(torrent)
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (q *QbitHandler) handleCategories(w http.ResponseWriter, r *http.Request) {
|
||||
var categories = map[string]shared.TorrentCategory{}
|
||||
for _, cat := range q.qbit.Categories {
|
||||
path := filepath.Join(q.qbit.DownloadFolder, cat)
|
||||
categories[cat] = shared.TorrentCategory{
|
||||
func (q *QBit) handleCategories(w http.ResponseWriter, r *http.Request) {
|
||||
var categories = map[string]TorrentCategory{}
|
||||
for _, cat := range q.Categories {
|
||||
path := filepath.Join(q.DownloadFolder, cat)
|
||||
categories[cat] = TorrentCategory{
|
||||
Name: cat,
|
||||
SavePath: path,
|
||||
}
|
||||
@@ -277,7 +271,7 @@ func (q *QbitHandler) handleCategories(w http.ResponseWriter, r *http.Request) {
|
||||
request.JSONResponse(w, categories, http.StatusOK)
|
||||
}
|
||||
|
||||
func (q *QbitHandler) handleCreateCategory(w http.ResponseWriter, r *http.Request) {
|
||||
func (q *QBit) handleCreateCategory(w http.ResponseWriter, r *http.Request) {
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to parse form data", http.StatusBadRequest)
|
||||
@@ -290,41 +284,41 @@ func (q *QbitHandler) handleCreateCategory(w http.ResponseWriter, r *http.Reques
|
||||
return
|
||||
}
|
||||
|
||||
q.qbit.Categories = append(q.qbit.Categories, name)
|
||||
q.Categories = append(q.Categories, name)
|
||||
|
||||
request.JSONResponse(w, nil, http.StatusOK)
|
||||
}
|
||||
|
||||
func (q *QbitHandler) handleTorrentProperties(w http.ResponseWriter, r *http.Request) {
|
||||
func (q *QBit) handleTorrentProperties(w http.ResponseWriter, r *http.Request) {
|
||||
hash := r.URL.Query().Get("hash")
|
||||
torrent := q.qbit.Storage.Get(hash)
|
||||
properties := q.qbit.GetTorrentProperties(torrent)
|
||||
torrent := q.Storage.Get(hash)
|
||||
properties := q.GetTorrentProperties(torrent)
|
||||
request.JSONResponse(w, properties, http.StatusOK)
|
||||
}
|
||||
|
||||
func (q *QbitHandler) handleTorrentFiles(w http.ResponseWriter, r *http.Request) {
|
||||
func (q *QBit) handleTorrentFiles(w http.ResponseWriter, r *http.Request) {
|
||||
hash := r.URL.Query().Get("hash")
|
||||
torrent := q.qbit.Storage.Get(hash)
|
||||
torrent := q.Storage.Get(hash)
|
||||
if torrent == nil {
|
||||
return
|
||||
}
|
||||
files := q.qbit.GetTorrentFiles(torrent)
|
||||
files := q.GetTorrentFiles(torrent)
|
||||
request.JSONResponse(w, files, http.StatusOK)
|
||||
}
|
||||
|
||||
func (q *QbitHandler) handleSetCategory(w http.ResponseWriter, r *http.Request) {
|
||||
func (q *QBit) handleSetCategory(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
category := ctx.Value("category").(string)
|
||||
hashes, _ := ctx.Value("hashes").([]string)
|
||||
torrents := q.qbit.Storage.GetAll("", "", hashes)
|
||||
torrents := q.Storage.GetAll("", "", hashes)
|
||||
for _, torrent := range torrents {
|
||||
torrent.Category = category
|
||||
q.qbit.Storage.AddOrUpdate(torrent)
|
||||
q.Storage.AddOrUpdate(torrent)
|
||||
}
|
||||
request.JSONResponse(w, nil, http.StatusOK)
|
||||
}
|
||||
|
||||
func (q *QbitHandler) handleAddTorrentTags(w http.ResponseWriter, r *http.Request) {
|
||||
func (q *QBit) handleAddTorrentTags(w http.ResponseWriter, r *http.Request) {
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to parse form data", http.StatusBadRequest)
|
||||
@@ -336,14 +330,14 @@ func (q *QbitHandler) handleAddTorrentTags(w http.ResponseWriter, r *http.Reques
|
||||
for i, tag := range tags {
|
||||
tags[i] = strings.TrimSpace(tag)
|
||||
}
|
||||
torrents := q.qbit.Storage.GetAll("", "", hashes)
|
||||
torrents := q.Storage.GetAll("", "", hashes)
|
||||
for _, t := range torrents {
|
||||
q.qbit.SetTorrentTags(t, tags)
|
||||
q.SetTorrentTags(t, tags)
|
||||
}
|
||||
request.JSONResponse(w, nil, http.StatusOK)
|
||||
}
|
||||
|
||||
func (q *QbitHandler) handleRemoveTorrentTags(w http.ResponseWriter, r *http.Request) {
|
||||
func (q *QBit) handleRemoveTorrentTags(w http.ResponseWriter, r *http.Request) {
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to parse form data", http.StatusBadRequest)
|
||||
@@ -355,19 +349,19 @@ func (q *QbitHandler) handleRemoveTorrentTags(w http.ResponseWriter, r *http.Req
|
||||
for i, tag := range tags {
|
||||
tags[i] = strings.TrimSpace(tag)
|
||||
}
|
||||
torrents := q.qbit.Storage.GetAll("", "", hashes)
|
||||
torrents := q.Storage.GetAll("", "", hashes)
|
||||
for _, torrent := range torrents {
|
||||
q.qbit.RemoveTorrentTags(torrent, tags)
|
||||
q.RemoveTorrentTags(torrent, tags)
|
||||
|
||||
}
|
||||
request.JSONResponse(w, nil, http.StatusOK)
|
||||
}
|
||||
|
||||
func (q *QbitHandler) handleGetTags(w http.ResponseWriter, r *http.Request) {
|
||||
request.JSONResponse(w, q.qbit.Tags, http.StatusOK)
|
||||
func (q *QBit) handleGetTags(w http.ResponseWriter, r *http.Request) {
|
||||
request.JSONResponse(w, q.Tags, http.StatusOK)
|
||||
}
|
||||
|
||||
func (q *QbitHandler) handleCreateTags(w http.ResponseWriter, r *http.Request) {
|
||||
func (q *QBit) handleCreateTags(w http.ResponseWriter, r *http.Request) {
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to parse form data", http.StatusBadRequest)
|
||||
@@ -377,6 +371,6 @@ func (q *QbitHandler) handleCreateTags(w http.ResponseWriter, r *http.Request) {
|
||||
for i, tag := range tags {
|
||||
tags[i] = strings.TrimSpace(tag)
|
||||
}
|
||||
q.qbit.AddTags(tags)
|
||||
q.AddTags(tags)
|
||||
request.JSONResponse(w, nil, http.StatusOK)
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
package server
|
||||
package qbit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/utils"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/service"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/arr"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/debrid"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/qbit/shared"
|
||||
)
|
||||
|
||||
type ImportRequest struct {
|
||||
@@ -63,18 +63,20 @@ func (i *ImportRequest) Complete() {
|
||||
i.CompletedAt = time.Now()
|
||||
}
|
||||
|
||||
func (i *ImportRequest) Process(q *shared.QBit) (err error) {
|
||||
func (i *ImportRequest) Process(q *QBit) (err error) {
|
||||
// Use this for now.
|
||||
// This sends the torrent to the arr
|
||||
svc := service.GetService()
|
||||
magnet, err := utils.GetMagnetFromUrl(i.URI)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing magnet link: %w", err)
|
||||
}
|
||||
torrent := q.CreateTorrentFromMagnet(magnet, i.Arr.Name, "manual")
|
||||
debridTorrent, err := debrid.ProcessTorrent(q.Debrid, magnet, i.Arr, i.IsSymlink)
|
||||
torrent := CreateTorrentFromMagnet(magnet, i.Arr.Name, "manual")
|
||||
debridTorrent, err := debrid.ProcessTorrent(svc.Debrid, magnet, i.Arr, i.IsSymlink)
|
||||
if err != nil || debridTorrent == nil {
|
||||
if debridTorrent != nil {
|
||||
go debridTorrent.Delete()
|
||||
dbClient := service.GetDebrid().GetByName(debridTorrent.Debrid)
|
||||
go dbClient.DeleteTorrent(debridTorrent)
|
||||
}
|
||||
if err == nil {
|
||||
err = fmt.Errorf("failed to process torrent")
|
||||
@@ -1,18 +0,0 @@
|
||||
package qbit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/arr"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/debrid"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/qbit/server"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/repair"
|
||||
)
|
||||
|
||||
func Start(ctx context.Context, deb *debrid.DebridService, arrs *arr.Storage, _repair *repair.Repair) error {
|
||||
srv := server.NewServer(deb, arrs, _repair)
|
||||
if err := srv.Start(ctx); err != nil {
|
||||
return fmt.Errorf("failed to start qbit server: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
50
pkg/qbit/misc.go
Normal file
50
pkg/qbit/misc.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package qbit
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/utils"
|
||||
debrid "github.com/sirrobot01/debrid-blackhole/pkg/debrid/torrent"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
func checkFileLoop(wg *sync.WaitGroup, dir string, file debrid.File, ready chan<- debrid.File) {
|
||||
defer wg.Done()
|
||||
ticker := time.NewTicker(1 * time.Second) // Check every second
|
||||
defer ticker.Stop()
|
||||
path := filepath.Join(dir, file.Path)
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
_, err := os.Stat(path)
|
||||
if !os.IsNotExist(err) {
|
||||
ready <- file
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func CreateTorrentFromMagnet(magnet *utils.Magnet, category, source string) *Torrent {
|
||||
torrent := &Torrent{
|
||||
ID: uuid.NewString(),
|
||||
Hash: strings.ToLower(magnet.InfoHash),
|
||||
Name: magnet.Name,
|
||||
Size: magnet.Size,
|
||||
Category: category,
|
||||
Source: source,
|
||||
State: "downloading",
|
||||
MagnetUri: magnet.Link,
|
||||
|
||||
Tracker: "udp://tracker.opentrackr.org:1337",
|
||||
UpLimit: -1,
|
||||
DlLimit: -1,
|
||||
AutoTmm: false,
|
||||
Ratio: 1,
|
||||
RatioLimit: 1,
|
||||
}
|
||||
return torrent
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
package shared
|
||||
package qbit
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/config"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/arr"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/debrid"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/repair"
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/logger"
|
||||
"os"
|
||||
)
|
||||
|
||||
@@ -16,17 +14,14 @@ type QBit struct {
|
||||
Port string `json:"port"`
|
||||
DownloadFolder string `json:"download_folder"`
|
||||
Categories []string `json:"categories"`
|
||||
Debrid *debrid.DebridService
|
||||
Repair *repair.Repair
|
||||
Storage *TorrentStorage
|
||||
debug bool
|
||||
logger zerolog.Logger
|
||||
Arrs *arr.Storage
|
||||
Tags []string
|
||||
RefreshInterval int
|
||||
}
|
||||
|
||||
func NewQBit(deb *debrid.DebridService, logger zerolog.Logger, arrs *arr.Storage, _repair *repair.Repair) *QBit {
|
||||
func New() *QBit {
|
||||
cfg := config.GetConfig().QBitTorrent
|
||||
port := cmp.Or(cfg.Port, os.Getenv("QBIT_PORT"), "8282")
|
||||
refreshInterval := cmp.Or(cfg.RefreshInterval, 10)
|
||||
@@ -36,11 +31,8 @@ func NewQBit(deb *debrid.DebridService, logger zerolog.Logger, arrs *arr.Storage
|
||||
Port: port,
|
||||
DownloadFolder: cfg.DownloadFolder,
|
||||
Categories: cfg.Categories,
|
||||
Debrid: deb,
|
||||
Storage: NewTorrentStorage(cmp.Or(os.Getenv("TORRENT_FILE"), "/data/torrents.json")),
|
||||
Repair: _repair,
|
||||
logger: logger,
|
||||
Arrs: arrs,
|
||||
Storage: NewTorrentStorage(cmp.Or(os.Getenv("TORRENT_FILE"), "/data/qbit_torrents.json")),
|
||||
logger: logger.NewLogger("qbit", cfg.LogLevel, os.Stdout),
|
||||
RefreshInterval: refreshInterval,
|
||||
}
|
||||
}
|
||||
47
pkg/qbit/routes.go
Normal file
47
pkg/qbit/routes.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package qbit
|
||||
|
||||
import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (q *QBit) Routes() http.Handler {
|
||||
r := chi.NewRouter()
|
||||
if q.logger.GetLevel().String() == "debug" {
|
||||
r.Use(middleware.Logger)
|
||||
}
|
||||
r.Use(q.CategoryContext)
|
||||
r.Post("/auth/login", q.handleLogin)
|
||||
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(q.authContext)
|
||||
r.Route("/torrents", func(r chi.Router) {
|
||||
r.Use(HashesCtx)
|
||||
r.Get("/info", q.handleTorrentsInfo)
|
||||
r.Post("/add", q.handleTorrentsAdd)
|
||||
r.Post("/delete", q.handleTorrentsDelete)
|
||||
r.Get("/categories", q.handleCategories)
|
||||
r.Post("/createCategory", q.handleCreateCategory)
|
||||
r.Post("/setCategory", q.handleSetCategory)
|
||||
r.Post("/addTags", q.handleAddTorrentTags)
|
||||
r.Post("/removeTags", q.handleRemoveTorrentTags)
|
||||
r.Post("/createTags", q.handleCreateTags)
|
||||
r.Get("/tags", q.handleGetTags)
|
||||
r.Get("/pause", q.handleTorrentsPause)
|
||||
r.Get("/resume", q.handleTorrentsResume)
|
||||
r.Get("/recheck", q.handleTorrentRecheck)
|
||||
r.Get("/properties", q.handleTorrentProperties)
|
||||
r.Get("/files", q.handleTorrentFiles)
|
||||
})
|
||||
|
||||
r.Route("/app", func(r chi.Router) {
|
||||
r.Get("/version", q.handleVersion)
|
||||
r.Get("/webapiVersion", q.handleWebAPIVersion)
|
||||
r.Get("/preferences", q.handlePreferences)
|
||||
r.Get("/buildInfo", q.handleBuildInfo)
|
||||
r.Get("/shutdown", q.handleShutdown)
|
||||
})
|
||||
})
|
||||
return r
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (q *QbitHandler) Routes(r chi.Router) http.Handler {
|
||||
r.Route("/api/v2", func(r chi.Router) {
|
||||
//if q.debug {
|
||||
// r.Use(middleware.Logger)
|
||||
//}
|
||||
r.Use(q.CategoryContext)
|
||||
r.Post("/auth/login", q.handleLogin)
|
||||
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(q.authContext)
|
||||
r.Route("/torrents", func(r chi.Router) {
|
||||
r.Use(HashesCtx)
|
||||
r.Get("/info", q.handleTorrentsInfo)
|
||||
r.Post("/add", q.handleTorrentsAdd)
|
||||
r.Post("/delete", q.handleTorrentsDelete)
|
||||
r.Get("/categories", q.handleCategories)
|
||||
r.Post("/createCategory", q.handleCreateCategory)
|
||||
r.Post("/setCategory", q.handleSetCategory)
|
||||
r.Post("/addTags", q.handleAddTorrentTags)
|
||||
r.Post("/removeTags", q.handleRemoveTorrentTags)
|
||||
r.Post("/createTags", q.handleCreateTags)
|
||||
r.Get("/tags", q.handleGetTags)
|
||||
r.Get("/pause", q.handleTorrentsPause)
|
||||
r.Get("/resume", q.handleTorrentsResume)
|
||||
r.Get("/recheck", q.handleTorrentRecheck)
|
||||
r.Get("/properties", q.handleTorrentProperties)
|
||||
r.Get("/files", q.handleTorrentFiles)
|
||||
})
|
||||
|
||||
r.Route("/app", func(r chi.Router) {
|
||||
r.Get("/version", q.handleVersion)
|
||||
r.Get("/webapiVersion", q.handleWebAPIVersion)
|
||||
r.Get("/preferences", q.handlePreferences)
|
||||
r.Get("/buildInfo", q.handleBuildInfo)
|
||||
r.Get("/shutdown", q.shutdown)
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
return r
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/config"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (u *UIHandler) authMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Check if setup is needed
|
||||
cfg := config.GetConfig()
|
||||
if cfg.NeedsSetup() && r.URL.Path != "/setup" {
|
||||
http.Redirect(w, r, "/setup", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
// Skip auth check for setup page
|
||||
if r.URL.Path == "/setup" {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
session, _ := store.Get(r, "auth-session")
|
||||
auth, ok := session.Values["authenticated"].(bool)
|
||||
|
||||
if !ok || !auth {
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func (u *UIHandler) verifyAuth(username, password string) bool {
|
||||
// If you're storing hashed password, use bcrypt to compare
|
||||
if username == "" {
|
||||
return false
|
||||
}
|
||||
auth := config.GetConfig().GetAuth()
|
||||
if auth == nil {
|
||||
return false
|
||||
}
|
||||
if username != auth.Username {
|
||||
return false
|
||||
}
|
||||
err := bcrypt.CompareHashAndPassword([]byte(auth.Password), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (u *UIHandler) Routes(r chi.Router) http.Handler {
|
||||
r.Get("/login", u.LoginHandler)
|
||||
r.Post("/login", u.LoginHandler)
|
||||
r.Get("/setup", u.SetupHandler)
|
||||
r.Post("/setup", u.SetupHandler)
|
||||
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(u.authMiddleware)
|
||||
r.Get("/", u.IndexHandler)
|
||||
r.Get("/download", u.DownloadHandler)
|
||||
r.Get("/repair", u.RepairHandler)
|
||||
r.Get("/config", u.ConfigHandler)
|
||||
r.Route("/internal", func(r chi.Router) {
|
||||
r.Get("/arrs", u.handleGetArrs)
|
||||
r.Post("/add", u.handleAddContent)
|
||||
r.Get("/cached", u.handleCheckCached)
|
||||
r.Post("/repair", u.handleRepairMedia)
|
||||
r.Get("/torrents", u.handleGetTorrents)
|
||||
r.Delete("/torrents/{hash}", u.handleDeleteTorrent)
|
||||
r.Get("/config", u.handleGetConfig)
|
||||
r.Get("/version", u.handleGetVersion)
|
||||
})
|
||||
})
|
||||
|
||||
return r
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package shared
|
||||
|
||||
import (
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/debrid"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
func checkFileLoop(wg *sync.WaitGroup, dir string, file debrid.TorrentFile, ready chan<- debrid.TorrentFile) {
|
||||
defer wg.Done()
|
||||
ticker := time.NewTicker(1 * time.Second) // Check every second
|
||||
defer ticker.Stop()
|
||||
path := filepath.Join(dir, file.Path)
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
_, err := os.Stat(path)
|
||||
if !os.IsNotExist(err) {
|
||||
ready <- file
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package shared
|
||||
package qbit
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -1,13 +1,14 @@
|
||||
package shared
|
||||
package qbit
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/utils"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/arr"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/debrid"
|
||||
db "github.com/sirrobot01/debrid-blackhole/pkg/debrid"
|
||||
debrid "github.com/sirrobot01/debrid-blackhole/pkg/debrid/torrent"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/service"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
@@ -47,16 +48,18 @@ func (q *QBit) AddTorrent(ctx context.Context, fileHeader *multipart.FileHeader,
|
||||
}
|
||||
|
||||
func (q *QBit) Process(ctx context.Context, magnet *utils.Magnet, category string) error {
|
||||
torrent := q.CreateTorrentFromMagnet(magnet, category, "auto")
|
||||
svc := service.GetService()
|
||||
torrent := CreateTorrentFromMagnet(magnet, category, "auto")
|
||||
a, ok := ctx.Value("arr").(*arr.Arr)
|
||||
if !ok {
|
||||
return fmt.Errorf("arr not found in context")
|
||||
}
|
||||
isSymlink := ctx.Value("isSymlink").(bool)
|
||||
debridTorrent, err := debrid.ProcessTorrent(q.Debrid, magnet, a, isSymlink)
|
||||
debridTorrent, err := db.ProcessTorrent(svc.Debrid, magnet, a, isSymlink)
|
||||
if err != nil || debridTorrent == nil {
|
||||
if debridTorrent != nil {
|
||||
go debridTorrent.Delete()
|
||||
dbClient := service.GetDebrid().GetByName(debridTorrent.Debrid)
|
||||
go dbClient.DeleteTorrent(debridTorrent)
|
||||
}
|
||||
if err == nil {
|
||||
err = fmt.Errorf("failed to process torrent")
|
||||
@@ -69,36 +72,16 @@ func (q *QBit) Process(ctx context.Context, magnet *utils.Magnet, category strin
|
||||
return nil
|
||||
}
|
||||
|
||||
func (q *QBit) CreateTorrentFromMagnet(magnet *utils.Magnet, category, source string) *Torrent {
|
||||
torrent := &Torrent{
|
||||
ID: uuid.NewString(),
|
||||
Hash: strings.ToLower(magnet.InfoHash),
|
||||
Name: magnet.Name,
|
||||
Size: magnet.Size,
|
||||
Category: category,
|
||||
Source: source,
|
||||
State: "downloading",
|
||||
MagnetUri: magnet.Link,
|
||||
|
||||
Tracker: "udp://tracker.opentrackr.org:1337",
|
||||
UpLimit: -1,
|
||||
DlLimit: -1,
|
||||
AutoTmm: false,
|
||||
Ratio: 1,
|
||||
RatioLimit: 1,
|
||||
}
|
||||
return torrent
|
||||
}
|
||||
|
||||
func (q *QBit) ProcessFiles(torrent *Torrent, debridTorrent *debrid.Torrent, arr *arr.Arr, isSymlink bool) {
|
||||
debridClient := service.GetDebrid().GetByName(debridTorrent.Debrid)
|
||||
for debridTorrent.Status != "downloaded" {
|
||||
progress := debridTorrent.Progress
|
||||
q.logger.Debug().Msgf("%s -> (%s) Download Progress: %.2f%%", debridTorrent.Debrid.GetName(), debridTorrent.Name, progress)
|
||||
q.logger.Debug().Msgf("%s -> (%s) Download Progress: %.2f%%", debridTorrent.Debrid, debridTorrent.Name, progress)
|
||||
time.Sleep(10 * time.Second)
|
||||
dbT, err := debridTorrent.Debrid.CheckStatus(debridTorrent, isSymlink)
|
||||
dbT, err := debridClient.CheckStatus(debridTorrent, isSymlink)
|
||||
if err != nil {
|
||||
q.logger.Error().Msgf("Error checking status: %v", err)
|
||||
go debridTorrent.Delete()
|
||||
go debridClient.DeleteTorrent(debridTorrent)
|
||||
q.MarkAsFailed(torrent)
|
||||
_ = arr.Refresh()
|
||||
return
|
||||
@@ -118,7 +101,7 @@ func (q *QBit) ProcessFiles(torrent *Torrent, debridTorrent *debrid.Torrent, arr
|
||||
}
|
||||
if err != nil {
|
||||
q.MarkAsFailed(torrent)
|
||||
go debridTorrent.Delete()
|
||||
go debridClient.DeleteTorrent(debridTorrent)
|
||||
q.logger.Info().Msgf("Error: %v", err)
|
||||
return
|
||||
}
|
||||
@@ -159,7 +142,7 @@ func (q *QBit) UpdateTorrentMin(t *Torrent, debridTorrent *debrid.Torrent) *Torr
|
||||
t.Name = debridTorrent.Name
|
||||
t.AddedOn = addedOn.Unix()
|
||||
t.DebridTorrent = debridTorrent
|
||||
t.Debrid = debridTorrent.Debrid.GetName()
|
||||
t.Debrid = debridTorrent.Debrid
|
||||
t.Size = totalSize
|
||||
t.Completed = sizeCompleted
|
||||
t.Downloaded = sizeCompleted
|
||||
@@ -177,17 +160,17 @@ func (q *QBit) UpdateTorrentMin(t *Torrent, debridTorrent *debrid.Torrent) *Torr
|
||||
}
|
||||
|
||||
func (q *QBit) UpdateTorrent(t *Torrent, debridTorrent *debrid.Torrent) *Torrent {
|
||||
db := debridTorrent.Debrid
|
||||
rcLoneMount := db.GetMountPath()
|
||||
_db := service.GetDebrid().GetByName(debridTorrent.Debrid)
|
||||
rcLoneMount := _db.GetMountPath()
|
||||
if debridTorrent == nil && t.ID != "" {
|
||||
debridTorrent, _ = db.GetTorrent(t.ID)
|
||||
debridTorrent, _ = _db.GetTorrent(t.ID)
|
||||
}
|
||||
if debridTorrent == nil {
|
||||
q.logger.Info().Msgf("Torrent with ID %s not found in %s", t.ID, db.GetName())
|
||||
q.logger.Info().Msgf("Torrent with ID %s not found in %s", t.ID, _db.GetName())
|
||||
return t
|
||||
}
|
||||
if debridTorrent.Status != "downloaded" {
|
||||
debridTorrent, _ = db.GetTorrent(t.ID)
|
||||
debridTorrent, _ = _db.GetTorrent(t.ID)
|
||||
}
|
||||
|
||||
if t.TorrentPath == "" {
|
||||
@@ -1,7 +1,7 @@
|
||||
package shared
|
||||
package qbit
|
||||
|
||||
import (
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/debrid"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/debrid/torrent"
|
||||
"sync"
|
||||
)
|
||||
|
||||
@@ -172,10 +172,10 @@ type TorrentCategory struct {
|
||||
}
|
||||
|
||||
type Torrent struct {
|
||||
ID string `json:"-"`
|
||||
DebridTorrent *debrid.Torrent `json:"-"`
|
||||
Debrid string `json:"debrid"`
|
||||
TorrentPath string `json:"-"`
|
||||
ID string `json:"-"`
|
||||
DebridTorrent *torrent.Torrent `json:"-"`
|
||||
Debrid string `json:"debrid"`
|
||||
TorrentPath string `json:"-"`
|
||||
|
||||
AddedOn int64 `json:"added_on,omitempty"`
|
||||
AmountLeft int64 `json:"amount_left"`
|
||||
@@ -1,7 +1,8 @@
|
||||
package shared
|
||||
package qbit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/service"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -28,7 +29,8 @@ func (q *QBit) StartRefreshWorker(ctx context.Context) {
|
||||
}
|
||||
|
||||
func (q *QBit) RefreshArrs() {
|
||||
for _, arr := range q.Arrs.GetAll() {
|
||||
arrs := service.GetService().Arr
|
||||
for _, arr := range arrs.GetAll() {
|
||||
err := arr.Refresh()
|
||||
if err != nil {
|
||||
return
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/config"
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/logger"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/arr"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/debrid"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/debrid/engine"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -24,8 +24,8 @@ import (
|
||||
|
||||
type Repair struct {
|
||||
Jobs []Job `json:"jobs"`
|
||||
deb debrid.Service
|
||||
arrs *arr.Storage
|
||||
deb engine.Service
|
||||
duration time.Duration
|
||||
runOnStart bool
|
||||
ZurgURL string
|
||||
@@ -33,16 +33,16 @@ type Repair struct {
|
||||
logger zerolog.Logger
|
||||
}
|
||||
|
||||
func NewRepair(deb debrid.Service, arrs *arr.Storage) *Repair {
|
||||
func New(deb *engine.Engine, arrs *arr.Storage) *Repair {
|
||||
cfg := config.GetConfig()
|
||||
duration, err := parseSchedule(cfg.Repair.Interval)
|
||||
if err != nil {
|
||||
duration = time.Hour * 24
|
||||
}
|
||||
r := &Repair{
|
||||
deb: deb,
|
||||
logger: logger.NewLogger("Repair", cfg.LogLevel, os.Stdout),
|
||||
arrs: arrs,
|
||||
deb: deb.Get(),
|
||||
logger: logger.NewLogger("repair", cfg.LogLevel, os.Stdout),
|
||||
duration: duration,
|
||||
runOnStart: cfg.Repair.RunOnStart,
|
||||
ZurgURL: cfg.Repair.ZurgURL,
|
||||
|
||||
@@ -9,10 +9,6 @@ import (
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/config"
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/logger"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/arr"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/debrid"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/qbit/shared"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/repair"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -21,45 +17,32 @@ import (
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
qbit *shared.QBit
|
||||
router *chi.Mux
|
||||
logger zerolog.Logger
|
||||
}
|
||||
|
||||
func NewServer(deb *debrid.DebridService, arrs *arr.Storage, _repair *repair.Repair) *Server {
|
||||
func New() *Server {
|
||||
cfg := config.GetConfig()
|
||||
l := logger.NewLogger("QBit", cfg.QBitTorrent.LogLevel, os.Stdout)
|
||||
q := shared.NewQBit(deb, l, arrs, _repair)
|
||||
l := logger.NewLogger("http", cfg.QBitTorrent.LogLevel, os.Stdout)
|
||||
r := chi.NewRouter()
|
||||
r.Use(middleware.Recoverer)
|
||||
r.Handle("/static/*", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
|
||||
|
||||
return &Server{
|
||||
qbit: q,
|
||||
router: r,
|
||||
logger: l,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Start(ctx context.Context) error {
|
||||
r := chi.NewRouter()
|
||||
r.Use(middleware.Recoverer)
|
||||
r.Handle("/static/*", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
|
||||
logLevel := s.logger.GetLevel().String()
|
||||
debug := logLevel == "debug"
|
||||
q := QbitHandler{qbit: s.qbit, logger: s.logger, debug: debug}
|
||||
ui := UIHandler{
|
||||
qbit: s.qbit,
|
||||
logger: logger.NewLogger("UI", s.logger.GetLevel().String(), os.Stdout),
|
||||
debug: debug,
|
||||
}
|
||||
|
||||
cfg := config.GetConfig()
|
||||
// Register routes
|
||||
r.Get("/logs", s.GetLogs)
|
||||
q.Routes(r)
|
||||
ui.Routes(r)
|
||||
|
||||
go s.qbit.StartWorker(context.Background())
|
||||
|
||||
s.logger.Info().Msgf("Starting QBit server on :%s", s.qbit.Port)
|
||||
port := fmt.Sprintf(":%s", s.qbit.Port)
|
||||
s.router.Get("/logs", s.getLogs)
|
||||
port := fmt.Sprintf(":%s", cfg.QBitTorrent.Port)
|
||||
s.logger.Info().Msgf("Starting server on %s", port)
|
||||
srv := &http.Server{
|
||||
Addr: port,
|
||||
Handler: r,
|
||||
Handler: s.router,
|
||||
}
|
||||
|
||||
ctx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
|
||||
@@ -77,7 +60,15 @@ func (s *Server) Start(ctx context.Context) error {
|
||||
return srv.Shutdown(context.Background())
|
||||
}
|
||||
|
||||
func (s *Server) GetLogs(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *Server) AddRoutes(routes func(r chi.Router) http.Handler) {
|
||||
routes(s.router)
|
||||
}
|
||||
|
||||
func (s *Server) Mount(pattern string, handler http.Handler) {
|
||||
s.router.Mount(pattern, handler)
|
||||
}
|
||||
|
||||
func (s *Server) getLogs(w http.ResponseWriter, r *http.Request) {
|
||||
logFile := logger.GetLogPath()
|
||||
|
||||
// Open and read the file
|
||||
59
pkg/service/service.go
Normal file
59
pkg/service/service.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/arr"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/debrid"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/debrid/cache"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/debrid/engine"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/repair"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
Repair *repair.Repair
|
||||
Arr *arr.Storage
|
||||
Debrid *engine.Engine
|
||||
DebridCache *cache.Manager
|
||||
}
|
||||
|
||||
var (
|
||||
instance *Service
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
func New() *Service {
|
||||
once.Do(func() {
|
||||
arrs := arr.NewStorage()
|
||||
deb := debrid.New()
|
||||
instance = &Service{
|
||||
Repair: repair.New(deb, arrs),
|
||||
Arr: arrs,
|
||||
Debrid: deb,
|
||||
DebridCache: cache.NewManager(deb),
|
||||
}
|
||||
})
|
||||
return instance
|
||||
}
|
||||
|
||||
// GetService returns the singleton instance
|
||||
func GetService() *Service {
|
||||
if instance == nil {
|
||||
instance = New()
|
||||
}
|
||||
return instance
|
||||
}
|
||||
|
||||
func Update() *Service {
|
||||
arrs := arr.NewStorage()
|
||||
deb := debrid.New()
|
||||
instance = &Service{
|
||||
Repair: repair.New(deb, arrs),
|
||||
Arr: arrs,
|
||||
Debrid: deb,
|
||||
}
|
||||
return instance
|
||||
}
|
||||
|
||||
func GetDebrid() *engine.Engine {
|
||||
return GetService().Debrid
|
||||
}
|
||||
34
pkg/web/routes.go
Normal file
34
pkg/web/routes.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (ui *Handler) Routes() http.Handler {
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Get("/login", ui.LoginHandler)
|
||||
r.Post("/login", ui.LoginHandler)
|
||||
r.Get("/setup", ui.SetupHandler)
|
||||
r.Post("/setup", ui.SetupHandler)
|
||||
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(ui.authMiddleware)
|
||||
r.Get("/", ui.IndexHandler)
|
||||
r.Get("/download", ui.DownloadHandler)
|
||||
r.Get("/repair", ui.RepairHandler)
|
||||
r.Get("/config", ui.ConfigHandler)
|
||||
r.Route("/internal", func(r chi.Router) {
|
||||
r.Get("/arrs", ui.handleGetArrs)
|
||||
r.Post("/add", ui.handleAddContent)
|
||||
r.Post("/repair", ui.handleRepairMedia)
|
||||
r.Get("/torrents", ui.handleGetTorrents)
|
||||
r.Delete("/torrents/{hash}", ui.handleDeleteTorrent)
|
||||
r.Get("/config", ui.handleGetConfig)
|
||||
r.Get("/version", ui.handleGetVersion)
|
||||
})
|
||||
})
|
||||
|
||||
return r
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package server
|
||||
package web
|
||||
|
||||
import (
|
||||
"embed"
|
||||
@@ -6,18 +6,20 @@ import (
|
||||
"fmt"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/config"
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/logger"
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/request"
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/utils"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/qbit"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/service"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/arr"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/debrid"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/qbit/shared"
|
||||
"github.com/sirrobot01/debrid-blackhole/pkg/version"
|
||||
)
|
||||
|
||||
@@ -49,13 +51,20 @@ type RepairRequest struct {
|
||||
Async bool `json:"async"`
|
||||
}
|
||||
|
||||
//go:embed templates/*
|
||||
//go:embed web/*
|
||||
var content embed.FS
|
||||
|
||||
type UIHandler struct {
|
||||
qbit *shared.QBit
|
||||
type Handler struct {
|
||||
qbit *qbit.QBit
|
||||
logger zerolog.Logger
|
||||
debug bool
|
||||
}
|
||||
|
||||
func New(qbit *qbit.QBit) *Handler {
|
||||
cfg := config.GetConfig()
|
||||
return &Handler{
|
||||
qbit: qbit,
|
||||
logger: logger.NewLogger("ui", cfg.LogLevel, os.Stdout),
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -66,13 +75,13 @@ var (
|
||||
func init() {
|
||||
templates = template.Must(template.ParseFS(
|
||||
content,
|
||||
"templates/layout.html",
|
||||
"templates/index.html",
|
||||
"templates/download.html",
|
||||
"templates/repair.html",
|
||||
"templates/config.html",
|
||||
"templates/login.html",
|
||||
"templates/setup.html",
|
||||
"web/layout.html",
|
||||
"web/index.html",
|
||||
"web/download.html",
|
||||
"web/repair.html",
|
||||
"web/config.html",
|
||||
"web/login.html",
|
||||
"web/setup.html",
|
||||
))
|
||||
|
||||
store.Options = &sessions.Options{
|
||||
@@ -82,7 +91,50 @@ func init() {
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UIHandler) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
func (ui *Handler) authMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Check if setup is needed
|
||||
cfg := config.GetConfig()
|
||||
if cfg.NeedsSetup() && r.URL.Path != "/setup" {
|
||||
http.Redirect(w, r, "/setup", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
// Skip auth check for setup page
|
||||
if r.URL.Path == "/setup" {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
session, _ := store.Get(r, "auth-session")
|
||||
auth, ok := session.Values["authenticated"].(bool)
|
||||
|
||||
if !ok || !auth {
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func (ui *Handler) verifyAuth(username, password string) bool {
|
||||
// If you're storing hashed password, use bcrypt to compare
|
||||
if username == "" {
|
||||
return false
|
||||
}
|
||||
auth := config.GetConfig().GetAuth()
|
||||
if auth == nil {
|
||||
return false
|
||||
}
|
||||
if username != auth.Username {
|
||||
return false
|
||||
}
|
||||
err := bcrypt.CompareHashAndPassword([]byte(auth.Password), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (ui *Handler) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "GET" {
|
||||
data := map[string]interface{}{
|
||||
"Page": "login",
|
||||
@@ -104,11 +156,14 @@ func (u *UIHandler) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if u.verifyAuth(credentials.Username, credentials.Password) {
|
||||
if ui.verifyAuth(credentials.Username, credentials.Password) {
|
||||
session, _ := store.Get(r, "auth-session")
|
||||
session.Values["authenticated"] = true
|
||||
session.Values["username"] = credentials.Username
|
||||
session.Save(r, w)
|
||||
if err := session.Save(r, w); err != nil {
|
||||
http.Error(w, "Error saving session", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
@@ -116,15 +171,18 @@ func (u *UIHandler) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "Invalid credentials", http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
func (u *UIHandler) LogoutHandler(w http.ResponseWriter, r *http.Request) {
|
||||
func (ui *Handler) LogoutHandler(w http.ResponseWriter, r *http.Request) {
|
||||
session, _ := store.Get(r, "auth-session")
|
||||
session.Values["authenticated"] = false
|
||||
session.Options.MaxAge = -1
|
||||
session.Save(r, w)
|
||||
err := session.Save(r, w)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (u *UIHandler) SetupHandler(w http.ResponseWriter, r *http.Request) {
|
||||
func (ui *Handler) SetupHandler(w http.ResponseWriter, r *http.Request) {
|
||||
cfg := config.GetConfig()
|
||||
authCfg := cfg.GetAuth()
|
||||
|
||||
@@ -174,12 +232,15 @@ func (u *UIHandler) SetupHandler(w http.ResponseWriter, r *http.Request) {
|
||||
session, _ := store.Get(r, "auth-session")
|
||||
session.Values["authenticated"] = true
|
||||
session.Values["username"] = username
|
||||
session.Save(r, w)
|
||||
if err := session.Save(r, w); err != nil {
|
||||
http.Error(w, "Error saving session", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (u *UIHandler) IndexHandler(w http.ResponseWriter, r *http.Request) {
|
||||
func (ui *Handler) IndexHandler(w http.ResponseWriter, r *http.Request) {
|
||||
data := map[string]interface{}{
|
||||
"Page": "index",
|
||||
"Title": "Torrents",
|
||||
@@ -190,7 +251,7 @@ func (u *UIHandler) IndexHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UIHandler) DownloadHandler(w http.ResponseWriter, r *http.Request) {
|
||||
func (ui *Handler) DownloadHandler(w http.ResponseWriter, r *http.Request) {
|
||||
data := map[string]interface{}{
|
||||
"Page": "download",
|
||||
"Title": "Download",
|
||||
@@ -201,7 +262,7 @@ func (u *UIHandler) DownloadHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UIHandler) RepairHandler(w http.ResponseWriter, r *http.Request) {
|
||||
func (ui *Handler) RepairHandler(w http.ResponseWriter, r *http.Request) {
|
||||
data := map[string]interface{}{
|
||||
"Page": "repair",
|
||||
"Title": "Repair",
|
||||
@@ -212,7 +273,7 @@ func (u *UIHandler) RepairHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UIHandler) ConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||
func (ui *Handler) ConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||
data := map[string]interface{}{
|
||||
"Page": "config",
|
||||
"Title": "Config",
|
||||
@@ -223,23 +284,25 @@ func (u *UIHandler) ConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UIHandler) handleGetArrs(w http.ResponseWriter, r *http.Request) {
|
||||
request.JSONResponse(w, u.qbit.Arrs.GetAll(), http.StatusOK)
|
||||
func (ui *Handler) handleGetArrs(w http.ResponseWriter, r *http.Request) {
|
||||
svc := service.GetService()
|
||||
request.JSONResponse(w, svc.Arr.GetAll(), http.StatusOK)
|
||||
}
|
||||
|
||||
func (u *UIHandler) handleAddContent(w http.ResponseWriter, r *http.Request) {
|
||||
func (ui *Handler) handleAddContent(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseMultipartForm(32 << 20); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
svc := service.GetService()
|
||||
|
||||
results := make([]*ImportRequest, 0)
|
||||
results := make([]*qbit.ImportRequest, 0)
|
||||
errs := make([]string, 0)
|
||||
|
||||
arrName := r.FormValue("arr")
|
||||
notSymlink := r.FormValue("notSymlink") == "true"
|
||||
|
||||
_arr := u.qbit.Arrs.Get(arrName)
|
||||
_arr := svc.Arr.Get(arrName)
|
||||
if _arr == nil {
|
||||
_arr = arr.NewArr(arrName, "", "", arr.Sonarr)
|
||||
}
|
||||
@@ -254,8 +317,8 @@ func (u *UIHandler) handleAddContent(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
for _, url := range urlList {
|
||||
importReq := NewImportRequest(url, _arr, !notSymlink)
|
||||
err := importReq.Process(u.qbit)
|
||||
importReq := qbit.NewImportRequest(url, _arr, !notSymlink)
|
||||
err := importReq.Process(ui.qbit)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Sprintf("URL %s: %v", url, err))
|
||||
continue
|
||||
@@ -279,8 +342,8 @@ func (u *UIHandler) handleAddContent(w http.ResponseWriter, r *http.Request) {
|
||||
continue
|
||||
}
|
||||
|
||||
importReq := NewImportRequest(magnet.Link, _arr, !notSymlink)
|
||||
err = importReq.Process(u.qbit)
|
||||
importReq := qbit.NewImportRequest(magnet.Link, _arr, !notSymlink)
|
||||
err = importReq.Process(ui.qbit)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Sprintf("File %s: %v", fileHeader.Filename, err))
|
||||
continue
|
||||
@@ -290,54 +353,24 @@ func (u *UIHandler) handleAddContent(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
request.JSONResponse(w, struct {
|
||||
Results []*ImportRequest `json:"results"`
|
||||
Errors []string `json:"errors,omitempty"`
|
||||
Results []*qbit.ImportRequest `json:"results"`
|
||||
Errors []string `json:"errors,omitempty"`
|
||||
}{
|
||||
Results: results,
|
||||
Errors: errs,
|
||||
}, http.StatusOK)
|
||||
}
|
||||
|
||||
func (u *UIHandler) handleCheckCached(w http.ResponseWriter, r *http.Request) {
|
||||
_hashes := r.URL.Query().Get("hash")
|
||||
if _hashes == "" {
|
||||
http.Error(w, "No hashes provided", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
hashes := strings.Split(_hashes, ",")
|
||||
if len(hashes) == 0 {
|
||||
http.Error(w, "No hashes provided", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
db := r.URL.Query().Get("debrid")
|
||||
var deb debrid.Service
|
||||
if db == "" {
|
||||
// use the first debrid
|
||||
deb = u.qbit.Debrid.Get()
|
||||
} else {
|
||||
deb = u.qbit.Debrid.GetByName(db)
|
||||
}
|
||||
if deb == nil {
|
||||
http.Error(w, "Invalid debrid", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
res := deb.IsAvailable(hashes)
|
||||
result := make(map[string]bool)
|
||||
for _, h := range hashes {
|
||||
_, exists := res[h]
|
||||
result[h] = exists
|
||||
}
|
||||
request.JSONResponse(w, result, http.StatusOK)
|
||||
}
|
||||
|
||||
func (u *UIHandler) handleRepairMedia(w http.ResponseWriter, r *http.Request) {
|
||||
func (ui *Handler) handleRepairMedia(w http.ResponseWriter, r *http.Request) {
|
||||
var req RepairRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
_arr := u.qbit.Arrs.Get(req.ArrName)
|
||||
svc := service.GetService()
|
||||
|
||||
_arr := svc.Arr.Get(req.ArrName)
|
||||
if _arr == nil {
|
||||
http.Error(w, "No Arrs found to repair", http.StatusNotFound)
|
||||
return
|
||||
@@ -345,15 +378,15 @@ func (u *UIHandler) handleRepairMedia(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if req.Async {
|
||||
go func() {
|
||||
if err := u.qbit.Repair.Repair([]*arr.Arr{_arr}, req.MediaIds); err != nil {
|
||||
u.logger.Error().Err(err).Msg("Failed to repair media")
|
||||
if err := svc.Repair.Repair([]*arr.Arr{_arr}, req.MediaIds); err != nil {
|
||||
ui.logger.Error().Err(err).Msg("Failed to repair media")
|
||||
}
|
||||
}()
|
||||
request.JSONResponse(w, "Repair process started", http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
if err := u.qbit.Repair.Repair([]*arr.Arr{_arr}, req.MediaIds); err != nil {
|
||||
if err := svc.Repair.Repair([]*arr.Arr{_arr}, req.MediaIds); err != nil {
|
||||
http.Error(w, fmt.Sprintf("Failed to repair: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
|
||||
@@ -362,29 +395,30 @@ func (u *UIHandler) handleRepairMedia(w http.ResponseWriter, r *http.Request) {
|
||||
request.JSONResponse(w, "Repair completed", http.StatusOK)
|
||||
}
|
||||
|
||||
func (u *UIHandler) handleGetVersion(w http.ResponseWriter, r *http.Request) {
|
||||
func (ui *Handler) handleGetVersion(w http.ResponseWriter, r *http.Request) {
|
||||
v := version.GetInfo()
|
||||
request.JSONResponse(w, v, http.StatusOK)
|
||||
}
|
||||
|
||||
func (u *UIHandler) handleGetTorrents(w http.ResponseWriter, r *http.Request) {
|
||||
request.JSONResponse(w, u.qbit.Storage.GetAll("", "", nil), http.StatusOK)
|
||||
func (ui *Handler) handleGetTorrents(w http.ResponseWriter, r *http.Request) {
|
||||
request.JSONResponse(w, ui.qbit.Storage.GetAll("", "", nil), http.StatusOK)
|
||||
}
|
||||
|
||||
func (u *UIHandler) handleDeleteTorrent(w http.ResponseWriter, r *http.Request) {
|
||||
func (ui *Handler) handleDeleteTorrent(w http.ResponseWriter, r *http.Request) {
|
||||
hash := chi.URLParam(r, "hash")
|
||||
if hash == "" {
|
||||
http.Error(w, "No hash provided", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
u.qbit.Storage.Delete(hash)
|
||||
ui.qbit.Storage.Delete(hash)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (u *UIHandler) handleGetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
func (ui *Handler) handleGetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
cfg := config.GetConfig()
|
||||
arrCfgs := make([]config.Arr, 0)
|
||||
for _, a := range u.qbit.Arrs.GetAll() {
|
||||
svc := service.GetService()
|
||||
for _, a := range svc.Arr.GetAll() {
|
||||
arrCfgs = append(arrCfgs, config.Arr{Host: a.Host, Name: a.Name, Token: a.Token})
|
||||
}
|
||||
cfg.Arrs = arrCfgs
|
||||
Reference in New Issue
Block a user