diff --git a/.air.toml b/.air.toml index 76593c7..20d240e 100644 --- a/.air.toml +++ b/.air.toml @@ -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 = [] diff --git a/Dockerfile b/Dockerfile index ba11eb1..34a6212 100644 --- a/Dockerfile +++ b/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 @@ -31,10 +31,10 @@ RUN --mount=type=cache,target=/go/pkg/mod \ # Stage 2: Create directory structure FROM alpine:3.19 as dirsetup -RUN mkdir -p /data/logs && \ - chmod 777 /data/logs && \ - touch /data/logs/decypharr.log && \ - chmod 666 /data/logs/decypharr.log +RUN mkdir -p /app/logs && \ + chmod 777 /app/logs && \ + touch /app/logs/decypharr.log && \ + chmod 666 /app/logs/decypharr.log # Stage 3: Final image FROM gcr.io/distroless/static-debian12:nonroot @@ -47,18 +47,19 @@ 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 +COPY --from=dirsetup --chown=nonroot:nonroot /app /app + # Metadata -ENV LOG_PATH=/data/logs +ENV LOG_PATH=/app/logs EXPOSE 8181 8282 -VOLUME ["/data", "/app"] +VOLUME ["/app"] USER nonroot:nonroot -HEALTHCHECK CMD ["/healthcheck"] +HEALTHCHECK CMD ["/usr/bin/healthcheck"] -CMD ["/blackhole", "--config", "/data"] \ No newline at end of file +CMD ["/usr/bin/blackhole", "--config", "/app"] \ No newline at end of file diff --git a/README.md b/README.md index ce8f416..16f88d1 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ services: user: "1000:1000" volumes: - /mnt/:/mnt - - ~/plex/configs/blackhole/:/data # Path to the config file. config.json + - ~/plex/configs/blackhole/:/app # config.json must be in this directory environment: - PUID=1000 - PGID=1000 @@ -78,7 +78,7 @@ services: Download the binary from the releases page and run it with the config file. ```bash -./blackhole --config /path/to/ +./blackhole --config /app ``` ### Usage @@ -104,7 +104,7 @@ Download the binary from the releases page and run it with the config file. #### Basic Sample Config -This is the default config file. You can create a `config.json` file in the root directory of the project or mount it to /data in the docker-compose file. +This is the default config file. You can create a `config.json` file in the root directory of the project or mount it to /app in the docker-compose file. ```json { "debrids": [ @@ -130,7 +130,8 @@ This is the default config file. You can create a `config.json` file in the root "enabled": false, "interval": "12h", "run_on_start": false - } + }, + "use_auth": false } ``` @@ -145,7 +146,7 @@ Full config are [here](doc/config.full.json) - The `log_level` key is used to set the log level of the application. The default value is `info`. log level can be set to `debug`, `info`, `warn`, `error` - The `max_cache_size` key is used to set the maximum number of infohashes that can be stored in the availability cache. This is used to prevent round trip to the debrid provider when using the proxy/Qbittorrent. The default value is `1000` - The `allowed_file_types` key is an array of allowed file types that can be downloaded. By default, all movie, tv show and music file types are allowed -- `use_auth` is used to enable basic authentication for the UI. +- The `use_auth` is used to enable basic authentication for the UI. The default value is `false` ##### Debrid Config - The `debrids` key is an array of debrid providers diff --git a/cmd/main.go b/cmd/decypharr/main.go similarity index 59% rename from cmd/main.go rename to cmd/decypharr/main.go index e54251d..b46dc35 100644 --- a/cmd/main.go +++ b/cmd/decypharr/main.go @@ -1,60 +1,71 @@ -package cmd +package decypharr 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) } }() diff --git a/common/regex.go b/common/regex.go index e39ba49..09a959b 100644 --- a/common/regex.go +++ b/common/regex.go @@ -37,7 +37,7 @@ func RemoveInvalidChars(value string) string { } func RemoveExtension(value string) string { - re := regexp.MustCompile(VIDEOMATCH + "|" + SAMPLEMATCH + "|" + MUSICMATCH) + re := regexp.MustCompile(VIDEOMATCH + "|" + MUSICMATCH) // Find the last index of the matched extension loc := re.FindStringIndex(value) diff --git a/go.mod b/go.mod index b4379bc..e522873 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index e4fff34..5395410 100644 --- a/go.sum +++ b/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= diff --git a/internal/config/config.go b/internal/config/config.go index 57a6926..41b5337 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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 @@ -197,21 +197,20 @@ func validateConfig(config *Config) error { return nil } -func SetConfigPath(path string) { - // Backward compatibility - // Check if the path is not a dir - if fi, err := os.Stat(path); err == nil && !fi.IsDir() { - // Get the directory of the file - path = filepath.Dir(path) - } +func SetConfigPath(path string) error { configPath = path + return nil } 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 diff --git a/internal/logger/logger.go b/internal/logger/logger.go index 03d2ff5..1a9e8fc 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -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,14 +17,13 @@ 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)) + if _, err := os.Stat(logsDir); os.IsNotExist(err) { + if err := os.MkdirAll(logsDir, 0755); err != nil { + panic(fmt.Sprintf("Failed to create logs directory: %v", err)) + } } return filepath.Join(logsDir, "decypharr.log") @@ -33,7 +33,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 +87,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 } diff --git a/internal/request/request.go b/internal/request/request.go index fd1cb6a..6326590 100644 --- a/internal/request/request.go +++ b/internal/request/request.go @@ -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 >= 200 && res.StatusCode < 300 + 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 } diff --git a/main.go b/main.go index e140e95..0c7ed3b 100644 --- a/main.go +++ b/main.go @@ -3,20 +3,22 @@ package main import ( "context" "flag" - "github.com/sirrobot01/debrid-blackhole/cmd" + "github.com/sirrobot01/debrid-blackhole/cmd/decypharr" "github.com/sirrobot01/debrid-blackhole/internal/config" "log" ) func main() { var configPath string - flag.StringVar(&configPath, "config", "config.json", "path to the config file") + flag.StringVar(&configPath, "config", "/data", "path to the data folder") flag.Parse() - config.SetConfigPath(configPath) + if err := config.SetConfigPath(configPath); err != nil { + log.Fatal(err) + } config.GetConfig() ctx := context.Background() - if err := cmd.Start(ctx); err != nil { + if err := decypharr.Start(ctx); err != nil { log.Fatal(err) } diff --git a/pkg/debrid/alldebrid.go b/pkg/debrid/alldebrid.go deleted file mode 100644 index 749a8e4..0000000 --- a/pkg/debrid/alldebrid.go +++ /dev/null @@ -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, - }, - } -} diff --git a/pkg/debrid/alldebrid/alldebrid.go b/pkg/debrid/alldebrid/alldebrid.go index b7b7e12..fbb7bdd 100644 --- a/pkg/debrid/alldebrid/alldebrid.go +++ b/pkg/debrid/alldebrid/alldebrid.go @@ -29,10 +29,6 @@ type AllDebrid struct { CheckCached bool } -func (ad *AllDebrid) GetMountPath() string { - return ad.MountPath -} - func (ad *AllDebrid) GetName() string { return ad.Name } @@ -77,7 +73,6 @@ func (ad *AllDebrid) SubmitMagnet(torrent *torrent.Torrent) (*torrent.Torrent, e } 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 @@ -170,12 +165,6 @@ func (ad *AllDebrid) GetTorrent(id string) (*torrent.Torrent, error) { 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 diff --git a/pkg/debrid/debrid.go b/pkg/debrid/debrid.go index 1c66c98..40eeba1 100644 --- a/pkg/debrid/debrid.go +++ b/pkg/debrid/debrid.go @@ -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,15 +75,14 @@ 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 == "" { errs = append(errs, err) continue } - logger.Info().Msgf("Torrent: %s submitted to %s", dbt.Name, db.GetName()) - d.lastUsed = index + logger.Info().Msgf("Torrent: %s(id=%s) submitted to %s", dbt.Name, dbt.Id, db.GetName()) + d.LastUsed = index return db.CheckStatus(dbt, isSymlink) } err := fmt.Errorf("failed to process torrent") diff --git a/pkg/debrid/debrid_link.go b/pkg/debrid/debrid_link.go deleted file mode 100644 index 6d9227d..0000000 --- a/pkg/debrid/debrid_link.go +++ /dev/null @@ -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, - }, - } -} diff --git a/pkg/debrid/debrid_link/debrid_link.go b/pkg/debrid/debrid_link/debrid_link.go index 91bdee8..b6d813a 100644 --- a/pkg/debrid/debrid_link/debrid_link.go +++ b/pkg/debrid/debrid_link/debrid_link.go @@ -11,7 +11,6 @@ import ( "github.com/sirrobot01/debrid-blackhole/internal/request" "github.com/sirrobot01/debrid-blackhole/pkg/debrid/torrent" - "log" "net/http" "os" "strings" @@ -29,10 +28,6 @@ type DebridLink struct { CheckCached bool } -func (dl *DebridLink) GetMountPath() string { - return dl.MountPath -} - func (dl *DebridLink) GetName() string { return dl.Name } @@ -176,7 +171,6 @@ func (dl *DebridLink) SubmitMagnet(t *torrent.Torrent) (*torrent.Torrent, error) } 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 diff --git a/pkg/debrid/engine/service.go b/pkg/debrid/engine/service.go index 4a4163e..8bf4c7e 100644 --- a/pkg/debrid/engine/service.go +++ b/pkg/debrid/engine/service.go @@ -12,7 +12,6 @@ type Service interface { 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) diff --git a/pkg/debrid/realdebrid.go b/pkg/debrid/realdebrid.go deleted file mode 100644 index fa0a4a4..0000000 --- a/pkg/debrid/realdebrid.go +++ /dev/null @@ -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, - }, - } -} diff --git a/pkg/debrid/realdebrid/realdebrid.go b/pkg/debrid/realdebrid/realdebrid.go index 054ea83..066fbc4 100644 --- a/pkg/debrid/realdebrid/realdebrid.go +++ b/pkg/debrid/realdebrid/realdebrid.go @@ -31,10 +31,6 @@ type RealDebrid struct { CheckCached bool } -func (r *RealDebrid) GetMountPath() string { - return r.MountPath -} - func (r *RealDebrid) GetName() string { return r.Name } @@ -156,9 +152,9 @@ func (r *RealDebrid) SubmitMagnet(t *torrent.Torrent) (*torrent.Torrent, error) 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 - + t.Debrid = r.Name + t.MountPath = r.MountPath return t, nil } @@ -218,6 +214,8 @@ func (r *RealDebrid) CheckStatus(t *torrent.Torrent, isSymlink bool) (*torrent.T t.Seeders = data.Seeders t.Links = data.Links t.Status = status + t.Debrid = r.Name + t.MountPath = r.MountPath downloadingStatus := []string{"downloading", "magnet_conversion", "queued", "compressing", "uploading"} if status == "waiting_files_selection" { files := GetTorrentFiles(data, true) // Validate files to be selected diff --git a/pkg/debrid/service.go b/pkg/debrid/service.go deleted file mode 100644 index de7d0d9..0000000 --- a/pkg/debrid/service.go +++ /dev/null @@ -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 -} diff --git a/pkg/debrid/torbox.go b/pkg/debrid/torbox.go deleted file mode 100644 index 04ba760..0000000 --- a/pkg/debrid/torbox.go +++ /dev/null @@ -1,313 +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" - "mime/multipart" - "net/http" - gourl "net/url" - "os" - "path" - "path/filepath" - "slices" - "strconv" - "strings" -) - -type Torbox struct { - BaseDebrid -} - -func (r *Torbox) GetMountPath() string { - return r.MountPath -} - -func (r *Torbox) GetName() string { - return r.Name -} - -func (r *Torbox) GetLogger() zerolog.Logger { - return r.logger -} - -func (r *Torbox) 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/api/torrents/checkcached?hash=%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 res types.TorBoxAvailableResponse - err = json.Unmarshal(resp, &res) - if err != nil { - r.logger.Info().Msgf("Error marshalling availability: %v", err) - return result - } - if res.Data == nil { - return result - } - - for h, cache := range *res.Data { - if cache.Size > 0 { - result[strings.ToUpper(h)] = true - } - } - } - r.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) - payload := &bytes.Buffer{} - writer := multipart.NewWriter(payload) - _ = writer.WriteField("magnet", torrent.Magnet.Link) - err := writer.Close() - if err != nil { - return nil, err - } - req, _ := http.NewRequest(http.MethodPost, url, payload) - req.Header.Set("Content-Type", writer.FormDataContentType()) - resp, err := r.client.MakeRequest(req) - if err != nil { - return nil, err - } - var data types.TorBoxAddMagnetResponse - err = json.Unmarshal(resp, &data) - if err != nil { - return nil, err - } - if data.Data == nil { - return nil, fmt.Errorf("error adding torrent") - } - dt := *data.Data - torrentId := strconv.Itoa(dt.Id) - log.Printf("Torrent: %s added with id: %s", torrent.Name, torrentId) - torrent.Id = torrentId - - return torrent, nil -} - -func getTorboxStatus(status string, finished bool) string { - if finished { - return "downloaded" - } - downloading := []string{"completed", "cached", "paused", "downloading", "uploading", - "checkingResumeData", "metaDL", "pausedUP", "queuedUP", "checkingUP", - "forcedUP", "allocating", "downloading", "metaDL", "pausedDL", - "queuedDL", "checkingDL", "forcedDL", "checkingResumeData", "moving"} - switch { - case slices.Contains(downloading, status): - return "downloading" - default: - return "error" - } -} - -func (r *Torbox) GetTorrent(id string) (*Torrent, error) { - torrent := &Torrent{} - url := fmt.Sprintf("%s/api/torrents/mylist/?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.TorboxInfoResponse - err = json.Unmarshal(resp, &res) - if err != nil { - return torrent, 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) - cfg := config.GetConfig() - for _, f := range data.Files { - fileName := filepath.Base(f.Name) - if common.RegexMatch(common.SAMPLEMATCH, fileName) { - // Skip sample files - continue - } - if !cfg.IsAllowedFile(fileName) { - continue - } - - if !cfg.IsSizeAllowed(f.Size) { - continue - } - file := TorrentFile{ - Id: strconv.Itoa(f.Id), - Name: fileName, - Size: f.Size, - Path: fileName, - } - files = append(files, file) - } - var cleanPath string - if len(files) > 0 { - cleanPath = path.Clean(data.Files[0].Name) - } else { - cleanPath = path.Clean(data.Name) - } - - torrent.OriginalFilename = strings.Split(cleanPath, "/")[0] - torrent.Files = files - torrent.Debrid = r - return torrent, nil -} - -func (r *Torbox) 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 *Torbox) DeleteTorrent(torrent *Torrent) { - url := fmt.Sprintf("%s/api/torrents/controltorrent/%s", r.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) - if err == nil { - r.logger.Info().Msgf("Torrent: %s deleted", torrent.Name) - } else { - r.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) - query := gourl.Values{} - query.Add("torrent_id", torrent.Id) - query.Add("token", r.APIKey) - query.Add("file_id", file.Id) - url += "?" + query.Encode() - req, _ := http.NewRequest(http.MethodGet, url, nil) - resp, err := r.client.MakeRequest(req) - if err != nil { - return err - } - var data types.TorBoxDownloadLinksResponse - if err = json.Unmarshal(resp, &data); err != nil { - return err - } - if data.Data == nil { - return fmt.Errorf("error getting download links") - } - idx := 0 - link := *data.Data - - dl := TorrentDownloadLinks{ - Link: link, - Filename: torrent.Files[idx].Name, - DownloadLink: link, - } - downloadLinks = append(downloadLinks, dl) - } - torrent.DownloadLinks = downloadLinks - return nil -} - -func (r *Torbox) GetCheckCached() bool { - return r.CheckCached -} - -func NewTorbox(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, - }, - } -} diff --git a/pkg/debrid/torbox/torbox.go b/pkg/debrid/torbox/torbox.go index 8e2ae1f..4291a15 100644 --- a/pkg/debrid/torbox/torbox.go +++ b/pkg/debrid/torbox/torbox.go @@ -11,7 +11,6 @@ import ( "github.com/sirrobot01/debrid-blackhole/internal/request" "github.com/sirrobot01/debrid-blackhole/pkg/debrid/torrent" - "log" "mime/multipart" "net/http" gourl "net/url" @@ -35,10 +34,6 @@ type Torbox struct { CheckCached bool } -func (tb *Torbox) GetMountPath() string { - return tb.MountPath -} - func (tb *Torbox) GetName() string { return tb.Name } @@ -130,8 +125,9 @@ func (tb *Torbox) SubmitMagnet(torrent *torrent.Torrent) (*torrent.Torrent, erro } dt := *data.Data torrentId := strconv.Itoa(dt.Id) - log.Printf("Torrent: %s added with id: %s", torrent.Name, torrentId) torrent.Id = torrentId + torrent.MountPath = tb.MountPath + torrent.Debrid = tb.Name return torrent, nil } diff --git a/pkg/debrid/torrent.go b/pkg/debrid/torrent.go deleted file mode 100644 index 8b58c87..0000000 --- a/pkg/debrid/torrent.go +++ /dev/null @@ -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 - } - } -} diff --git a/pkg/debrid/types/alldebrid.go b/pkg/debrid/types/alldebrid.go deleted file mode 100644 index fbf86e1..0000000 --- a/pkg/debrid/types/alldebrid.go +++ /dev/null @@ -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"` -} diff --git a/pkg/debrid/types/debrid_link.go b/pkg/debrid/types/debrid_link.go deleted file mode 100644 index 30aebee..0000000 --- a/pkg/debrid/types/debrid_link.go +++ /dev/null @@ -1,45 +0,0 @@ -package types - -type DebridLinkAPIResponse[T any] struct { - Success bool `json:"success"` - Value *T `json:"value"` // Use pointer to allow nil -} - -type DebridLinkAvailableResponse DebridLinkAPIResponse[map[string]map[string]struct { - Name string `json:"name"` - HashString string `json:"hashString"` - Files []struct { - Name string `json:"name"` - Size int `json:"size"` - } `json:"files"` -}] - -type debridLinkTorrentInfo struct { - ID string `json:"id"` - Name string `json:"name"` - HashString string `json:"hashString"` - UploadRatio float64 `json:"uploadRatio"` - ServerID string `json:"serverId"` - Wait bool `json:"wait"` - PeersConnected int `json:"peersConnected"` - Status int `json:"status"` - TotalSize int64 `json:"totalSize"` - Files []struct { - ID string `json:"id"` - Name string `json:"name"` - DownloadURL string `json:"downloadUrl"` - Size int64 `json:"size"` - DownloadPercent int `json:"downloadPercent"` - } `json:"files"` - Trackers []struct { - Announce string `json:"announce"` - } `json:"trackers"` - Created int64 `json:"created"` - DownloadPercent float64 `json:"downloadPercent"` - DownloadSpeed int64 `json:"downloadSpeed"` - UploadSpeed int64 `json:"uploadSpeed"` -} - -type DebridLinkTorrentInfo DebridLinkAPIResponse[[]debridLinkTorrentInfo] - -type DebridLinkSubmitTorrentInfo DebridLinkAPIResponse[debridLinkTorrentInfo] diff --git a/pkg/debrid/types/realdebrid.go b/pkg/debrid/types/realdebrid.go deleted file mode 100644 index 5577862..0000000 --- a/pkg/debrid/types/realdebrid.go +++ /dev/null @@ -1,107 +0,0 @@ -package types - -import ( - "encoding/json" - "fmt" -) - -type RealDebridAvailabilityResponse map[string]Hoster - -func (r *RealDebridAvailabilityResponse) UnmarshalJSON(data []byte) error { - // First, try to unmarshal as an object - var objectData map[string]Hoster - err := json.Unmarshal(data, &objectData) - if err == nil { - *r = objectData - return nil - } - - // If that fails, try to unmarshal as an array - var arrayData []map[string]Hoster - err = json.Unmarshal(data, &arrayData) - if err != nil { - return fmt.Errorf("failed to unmarshal as both object and array: %v", err) - } - - // If it's an array, use the first element - if len(arrayData) > 0 { - *r = arrayData[0] - return nil - } - - // If it's an empty array, initialize as an empty map - *r = make(map[string]Hoster) - return nil -} - -type Hoster struct { - Rd []map[string]FileVariant `json:"rd"` -} - -func (h *Hoster) UnmarshalJSON(data []byte) error { - // Attempt to unmarshal into the expected structure (an object with an "rd" key) - type Alias Hoster - var obj Alias - if err := json.Unmarshal(data, &obj); err == nil { - *h = Hoster(obj) - return nil - } - - // If unmarshalling into an object fails, check if it's an empty array - var arr []interface{} - if err := json.Unmarshal(data, &arr); err == nil && len(arr) == 0 { - // It's an empty array; initialize with no entries - *h = Hoster{Rd: nil} - return nil - } - - // If both attempts fail, return an error - return fmt.Errorf("hoster: cannot unmarshal JSON data: %s", string(data)) -} - -type FileVariant struct { - Filename string `json:"filename"` - Filesize int `json:"filesize"` -} - -type RealDebridAddMagnetSchema struct { - Id string `json:"id"` - Uri string `json:"uri"` -} - -type RealDebridTorrentInfo struct { - ID string `json:"id"` - Filename string `json:"filename"` - OriginalFilename string `json:"original_filename"` - Hash string `json:"hash"` - Bytes int64 `json:"bytes"` - OriginalBytes int64 `json:"original_bytes"` - Host string `json:"host"` - Split int `json:"split"` - Progress float64 `json:"progress"` - Status string `json:"status"` - Added string `json:"added"` - Files []struct { - ID int `json:"id"` - Path string `json:"path"` - Bytes int64 `json:"bytes"` - Selected int `json:"selected"` - } `json:"files"` - Links []string `json:"links"` - Ended string `json:"ended,omitempty"` - Speed int64 `json:"speed,omitempty"` - Seeders int `json:"seeders,omitempty"` -} - -type RealDebridUnrestrictResponse struct { - Id string `json:"id"` - Filename string `json:"filename"` - MimeType string `json:"mimeType"` - Filesize int `json:"filesize"` - Link string `json:"link"` - Host string `json:"host"` - Chunks int `json:"chunks"` - Crc int `json:"crc"` - Download string `json:"download"` - Streamable int `json:"streamable"` -} diff --git a/pkg/debrid/types/torbox.go b/pkg/debrid/types/torbox.go deleted file mode 100644 index 8289532..0000000 --- a/pkg/debrid/types/torbox.go +++ /dev/null @@ -1,75 +0,0 @@ -package types - -import "time" - -type TorboxAPIResponse[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 { - Name string `json:"name"` - Size int `json:"size"` - Hash string `json:"hash"` -}] - -type TorBoxAddMagnetResponse TorboxAPIResponse[struct { - Id int `json:"torrent_id"` - Hash string `json:"hash"` -}] - -type torboxInfo struct { - Id int `json:"id"` - AuthId string `json:"auth_id"` - Server int `json:"server"` - Hash string `json:"hash"` - Name string `json:"name"` - Magnet interface{} `json:"magnet"` - Size int64 `json:"size"` - Active bool `json:"active"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - DownloadState string `json:"download_state"` - Seeds int `json:"seeds"` - Peers int `json:"peers"` - Ratio float64 `json:"ratio"` - Progress float64 `json:"progress"` - DownloadSpeed int64 `json:"download_speed"` - UploadSpeed int `json:"upload_speed"` - Eta int `json:"eta"` - TorrentFile bool `json:"torrent_file"` - ExpiresAt interface{} `json:"expires_at"` - DownloadPresent bool `json:"download_present"` - Files []struct { - Id int `json:"id"` - Md5 interface{} `json:"md5"` - Hash string `json:"hash"` - Name string `json:"name"` - Size int64 `json:"size"` - Zipped bool `json:"zipped"` - S3Path string `json:"s3_path"` - Infected bool `json:"infected"` - Mimetype string `json:"mimetype"` - ShortName string `json:"short_name"` - AbsolutePath string `json:"absolute_path"` - } `json:"files"` - DownloadPath string `json:"download_path"` - InactiveCheck int `json:"inactive_check"` - Availability int `json:"availability"` - DownloadFinished bool `json:"download_finished"` - Tracker interface{} `json:"tracker"` - TotalUploaded int `json:"total_uploaded"` - TotalDownloaded int `json:"total_downloaded"` - Cached bool `json:"cached"` - Owner string `json:"owner"` - SeedTorrent bool `json:"seed_torrent"` - AllowZipped bool `json:"allow_zipped"` - LongTermSeeding bool `json:"long_term_seeding"` - TrackerMessage interface{} `json:"tracker_message"` -} - -type TorboxInfoResponse TorboxAPIResponse[torboxInfo] - -type TorBoxDownloadLinksResponse TorboxAPIResponse[string] diff --git a/pkg/downloader/grab.go b/pkg/downloader/grab.go new file mode 100644 index 0000000..987a696 --- /dev/null +++ b/pkg/downloader/grab.go @@ -0,0 +1,2 @@ +package downloader + diff --git a/pkg/downloaders/fasthttp.go b/pkg/downloaders/fasthttp.go deleted file mode 100644 index 4eca3c6..0000000 --- a/pkg/downloaders/fasthttp.go +++ /dev/null @@ -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 -} diff --git a/pkg/downloaders/grab.go b/pkg/downloaders/grab.go deleted file mode 100644 index c26a1e9..0000000 --- a/pkg/downloaders/grab.go +++ /dev/null @@ -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() -} diff --git a/pkg/downloaders/http.go b/pkg/downloaders/http.go deleted file mode 100644 index 3358518..0000000 --- a/pkg/downloaders/http.go +++ /dev/null @@ -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 -} diff --git a/pkg/proxy/proxy.go b/pkg/proxy/proxy.go index f2afa9e..e830319 100644 --- a/pkg/proxy/proxy.go +++ b/pkg/proxy/proxy.go @@ -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 { diff --git a/pkg/qbit/downloader.go b/pkg/qbit/downloader.go index fce3d15..710a5c7 100644 --- a/pkg/qbit/downloader.go +++ b/pkg/qbit/downloader.go @@ -1,27 +1,64 @@ package qbit import ( + "crypto/tls" "fmt" + "github.com/cavaliergopher/grab/v3" "github.com/sirrobot01/debrid-blackhole/common" debrid "github.com/sirrobot01/debrid-blackhole/pkg/debrid/torrent" - "github.com/sirrobot01/debrid-blackhole/pkg/downloaders" + "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)) - torrentPath := common.RemoveExtension(debridTorrent.OriginalFilename) - parent := common.RemoveInvalidChars(filepath.Join(q.DownloadFolder, debridTorrent.Arr.Name, torrentPath)) - err := os.MkdirAll(parent, os.ModePerm) + torrentPath := common.RemoveInvalidChars(filepath.Join(q.DownloadFolder, debridTorrent.Arr.Name, common.RemoveExtension(debridTorrent.OriginalFilename))) + err := os.MkdirAll(torrentPath, os.ModePerm) if err != nil { // add previous error to the error and return - return "", fmt.Errorf("failed to create directory: %s: %v", parent, err) + return "", fmt.Errorf("failed to create directory: %s: %v", torrentPath, err) } - q.downloadFiles(torrent, parent) + q.downloadFiles(torrent, torrentPath) return torrentPath, nil } @@ -37,7 +74,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 +90,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) @@ -66,7 +113,7 @@ func (q *QBit) downloadFiles(torrent *Torrent, parent string) { defer func() { <-semaphore }() filename := link.Filename - err := downloaders.NormalGrab( + err := Download( client, link.DownloadLink, filepath.Join(parent, filename), @@ -92,25 +139,26 @@ func (q *QBit) ProcessSymlink(torrent *Torrent) (string, error) { if len(files) == 0 { return "", fmt.Errorf("no video files found") } - q.logger.Info().Msgf("Checking %d files...", len(files)) + q.logger.Info().Msgf("Checking symlinks for %d files...", len(files)) rCloneBase := debridTorrent.MountPath torrentPath, err := q.getTorrentPath(rCloneBase, debridTorrent) // /MyTVShow/ + // This returns filename.ext for alldebrid instead of the parent folder filename/ + torrentFolder := torrentPath if err != nil { return "", fmt.Errorf("failed to get torrent path: %v", err) } - // Fix for alldebrid - newTorrentPath := torrentPath - if newTorrentPath == "" { - // Alldebrid at times doesn't return the parent folder for single file torrents - newTorrentPath = common.RemoveExtension(debridTorrent.Name) // MyTVShow + // Check if the torrent path is a file + torrentRclonePath := filepath.Join(rCloneBase, torrentPath) // leave it as is + if debridTorrent.Debrid == "alldebrid" && len(files) == 1 { + // Alldebrid hotfix for single file torrents + torrentFolder = common.RemoveExtension(torrentFolder) + torrentRclonePath = rCloneBase // /mnt/rclone/magnets/ // Remove the filename since it's in the root folder } - torrentSymlinkPath := filepath.Join(q.DownloadFolder, debridTorrent.Arr.Name, newTorrentPath) // /mnt/symlinks/{category}/MyTVShow/ + torrentSymlinkPath := filepath.Join(q.DownloadFolder, debridTorrent.Arr.Name, torrentFolder) // /mnt/symlinks/{category}/MyTVShow/ err = os.MkdirAll(torrentSymlinkPath, os.ModePerm) if err != nil { return "", fmt.Errorf("failed to create directory: %s: %v", torrentSymlinkPath, err) } - torrentRclonePath := filepath.Join(rCloneBase, torrentPath) // leave it as is - q.logger.Debug().Msgf("Debrid torrent path: %s\nSymlink Path: %s", torrentRclonePath, torrentSymlinkPath) for _, file := range files { wg.Add(1) go checkFileLoop(&wg, torrentRclonePath, file, ready) @@ -125,12 +173,11 @@ func (q *QBit) ProcessSymlink(torrent *Torrent) (string, error) { q.logger.Info().Msgf("File is ready: %s", f.Path) q.createSymLink(torrentSymlinkPath, torrentRclonePath, f) } - return torrentPath, nil + return torrentSymlinkPath, nil } func (q *QBit) getTorrentPath(rclonePath string, debridTorrent *debrid.Torrent) (string, error) { for { - q.logger.Debug().Msgf("Checking for torrent path: %s", rclonePath) torrentPath, err := debridTorrent.GetMountFolder(rclonePath) if err == nil { q.logger.Debug().Msgf("Found torrent path: %s", torrentPath) diff --git a/pkg/qbit/import.go b/pkg/qbit/import.go index 11661f5..6bbc56d 100644 --- a/pkg/qbit/import.go +++ b/pkg/qbit/import.go @@ -74,6 +74,7 @@ func (i *ImportRequest) Process(q *QBit) (err error) { torrent := CreateTorrentFromMagnet(magnet, i.Arr.Name, "manual") debridTorrent, err := debrid.ProcessTorrent(svc.Debrid, magnet, i.Arr, i.IsSymlink) if err != nil || debridTorrent == nil { + fmt.Println("Error deleting torrent: ", err) if debridTorrent != nil { dbClient := service.GetDebrid().GetByName(debridTorrent.Debrid) go dbClient.DeleteTorrent(debridTorrent) diff --git a/pkg/qbit/main.go b/pkg/qbit/main.go deleted file mode 100644 index d979978..0000000 --- a/pkg/qbit/main.go +++ /dev/null @@ -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 -} diff --git a/pkg/qbit/qbit.go b/pkg/qbit/qbit.go index 53b3af3..4674c23 100644 --- a/pkg/qbit/qbit.go +++ b/pkg/qbit/qbit.go @@ -6,6 +6,7 @@ import ( "github.com/sirrobot01/debrid-blackhole/internal/config" "github.com/sirrobot01/debrid-blackhole/internal/logger" "os" + "path/filepath" ) type QBit struct { @@ -22,7 +23,8 @@ type QBit struct { } func New() *QBit { - cfg := config.GetConfig().QBitTorrent + _cfg := config.GetConfig() + cfg := _cfg.QBitTorrent port := cmp.Or(cfg.Port, os.Getenv("QBIT_PORT"), "8282") refreshInterval := cmp.Or(cfg.RefreshInterval, 10) return &QBit{ @@ -31,7 +33,7 @@ func New() *QBit { Port: port, DownloadFolder: cfg.DownloadFolder, Categories: cfg.Categories, - Storage: NewTorrentStorage(cmp.Or(os.Getenv("TORRENT_FILE"), "/data/qbit_torrents.json")), + Storage: NewTorrentStorage(filepath.Join(_cfg.Path, "torrents.json")), logger: logger.NewLogger("qbit", cfg.LogLevel, os.Stdout), RefreshInterval: refreshInterval, } diff --git a/pkg/qbit/routes.go b/pkg/qbit/routes.go index b82991a..c992571 100644 --- a/pkg/qbit/routes.go +++ b/pkg/qbit/routes.go @@ -2,15 +2,11 @@ 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) diff --git a/pkg/qbit/server/import.go b/pkg/qbit/server/import.go deleted file mode 100644 index 73231fb..0000000 --- a/pkg/qbit/server/import.go +++ /dev/null @@ -1,88 +0,0 @@ -package server - -import ( - "fmt" - "github.com/sirrobot01/debrid-blackhole/internal/utils" - "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 { - ID string `json:"id"` - Path string `json:"path"` - URI string `json:"uri"` - Arr *arr.Arr `json:"arr"` - IsSymlink bool `json:"isSymlink"` - SeriesId int `json:"series"` - Seasons []int `json:"seasons"` - Episodes []string `json:"episodes"` - - Failed bool `json:"failed"` - FailedAt time.Time `json:"failedAt"` - Reason string `json:"reason"` - Completed bool `json:"completed"` - CompletedAt time.Time `json:"completedAt"` - Async bool `json:"async"` -} - -type ManualImportResponseSchema struct { - Priority string `json:"priority"` - Status string `json:"status"` - Result string `json:"result"` - Queued time.Time `json:"queued"` - Trigger string `json:"trigger"` - SendUpdatesToClient bool `json:"sendUpdatesToClient"` - UpdateScheduledTask bool `json:"updateScheduledTask"` - Id int `json:"id"` -} - -func NewImportRequest(uri string, arr *arr.Arr, isSymlink bool) *ImportRequest { - return &ImportRequest{ - ID: uuid.NewString(), - URI: uri, - Arr: arr, - Failed: false, - Completed: false, - Async: false, - IsSymlink: isSymlink, - } -} - -func (i *ImportRequest) Fail(reason string) { - i.Failed = true - i.FailedAt = time.Now() - i.Reason = reason -} - -func (i *ImportRequest) Complete() { - i.Completed = true - i.CompletedAt = time.Now() -} - -func (i *ImportRequest) Process(q *shared.QBit) (err error) { - // Use this for now. - // This sends the torrent to the arr - 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) - if err != nil || debridTorrent == nil { - if debridTorrent != nil { - go debridTorrent.Delete() - } - if err == nil { - err = fmt.Errorf("failed to process torrent") - } - return err - } - torrent = q.UpdateTorrentMin(torrent, debridTorrent) - q.Storage.AddOrUpdate(torrent) - go q.ProcessFiles(torrent, debridTorrent, i.Arr, i.IsSymlink) - return nil -} diff --git a/pkg/qbit/server/qbit_handlers.go b/pkg/qbit/server/qbit_handlers.go deleted file mode 100644 index dd53304..0000000 --- a/pkg/qbit/server/qbit_handlers.go +++ /dev/null @@ -1,382 +0,0 @@ -package server - -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" - "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 { - return "", "", nil - } - encodedToken := encodedTokens[1] - - bytes, err := base64.StdEncoding.DecodeString(encodedToken) - if err != nil { - return "", "", err - } - - bearer := string(bytes) - - colonIndex := strings.LastIndex(bearer, ":") - host := bearer[:colonIndex] - token := bearer[colonIndex+1:] - - return host, token, nil -} - -func (q *QbitHandler) 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 == "" { - // Get from form - _ = r.ParseForm() - category = r.Form.Get("category") - if category == "" { - // Get from multipart form - _ = r.ParseMultipartForm(32 << 20) - category = r.FormValue("category") - } - } - ctx := r.Context() - ctx = context.WithValue(r.Context(), "category", strings.TrimSpace(category)) - next.ServeHTTP(w, r.WithContext(ctx)) - }) -} - -func (q *QbitHandler) 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) - a := &arr.Arr{ - Name: category, - } - if err == nil { - a.Host = strings.TrimSpace(host) - a.Token = strings.TrimSpace(token) - } - q.qbit.Arrs.AddOrUpdate(a) - ctx := context.WithValue(r.Context(), "arr", a) - next.ServeHTTP(w, r.WithContext(ctx)) - }) -} - -func HashesCtx(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _hashes := chi.URLParam(r, "hashes") - var hashes []string - if _hashes != "" { - hashes = strings.Split(_hashes, "|") - } - if hashes == nil { - // Get hashes from form - _ = r.ParseForm() - hashes = r.Form["hashes"] - } - for i, hash := range hashes { - hashes[i] = strings.TrimSpace(hash) - } - ctx := context.WithValue(r.Context(), "hashes", hashes) - next.ServeHTTP(w, r.WithContext(ctx)) - }) -} - -func (q *QbitHandler) handleLogin(w http.ResponseWriter, r *http.Request) { - _, _ = w.Write([]byte("Ok.")) -} - -func (q *QbitHandler) handleVersion(w http.ResponseWriter, r *http.Request) { - _, _ = w.Write([]byte("v4.3.2")) -} - -func (q *QbitHandler) 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() - - preferences.WebUiUsername = q.qbit.Username - preferences.SavePath = q.qbit.DownloadFolder - preferences.TempPath = filepath.Join(q.qbit.DownloadFolder, "temp") - - request.JSONResponse(w, preferences, http.StatusOK) -} - -func (q *QbitHandler) handleBuildInfo(w http.ResponseWriter, r *http.Request) { - res := shared.BuildInfo{ - Bitness: 64, - Boost: "1.75.0", - Libtorrent: "1.2.11.0", - Openssl: "1.1.1i", - Qt: "5.15.2", - Zlib: "1.2.11", - } - request.JSONResponse(w, res, http.StatusOK) -} - -func (q *QbitHandler) shutdown(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) -} - -func (q *QbitHandler) 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) - request.JSONResponse(w, torrents, http.StatusOK) -} - -func (q *QbitHandler) handleTorrentsAdd(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - // Parse form based on content type - contentType := r.Header.Get("Content-Type") - if strings.Contains(contentType, "multipart/form-data") { - if err := r.ParseMultipartForm(32 << 20); err != nil { - q.logger.Info().Msgf("Error parsing multipart form: %v", err) - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - } else if strings.Contains(contentType, "application/x-www-form-urlencoded") { - if err := r.ParseForm(); err != nil { - q.logger.Info().Msgf("Error parsing form: %v", err) - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - } else { - http.Error(w, "Invalid content type", http.StatusBadRequest) - return - } - - isSymlink := strings.ToLower(r.FormValue("sequentialDownload")) != "true" - category := r.FormValue("category") - atleastOne := false - ctx = context.WithValue(ctx, "isSymlink", isSymlink) - - // Handle magnet URLs - if urls := r.FormValue("urls"); urls != "" { - var urlList []string - for _, u := range strings.Split(urls, "\n") { - urlList = append(urlList, strings.TrimSpace(u)) - } - for _, url := range urlList { - if err := q.qbit.AddMagnet(ctx, url, category); err != nil { - q.logger.Info().Msgf("Error adding magnet: %v", err) - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - atleastOne = true - } - } - - // Handle torrent files - 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 { - q.logger.Info().Msgf("Error adding torrent: %v", err) - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - atleastOne = true - } - } - } - - if !atleastOne { - http.Error(w, "No valid URLs or torrents provided", http.StatusBadRequest) - return - } - - w.WriteHeader(http.StatusOK) -} - -func (q *QbitHandler) handleTorrentsDelete(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - hashes, _ := ctx.Value("hashes").([]string) - if len(hashes) == 0 { - http.Error(w, "No hashes provided", http.StatusBadRequest) - return - } - for _, hash := range hashes { - q.qbit.Storage.Delete(hash) - } - - w.WriteHeader(http.StatusOK) -} - -func (q *QbitHandler) 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) - if torrent == nil { - continue - } - go q.qbit.PauseTorrent(torrent) - } - - w.WriteHeader(http.StatusOK) -} - -func (q *QbitHandler) 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) - if torrent == nil { - continue - } - go q.qbit.ResumeTorrent(torrent) - } - - w.WriteHeader(http.StatusOK) -} - -func (q *QbitHandler) 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) - if torrent == nil { - continue - } - go q.qbit.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{ - Name: cat, - SavePath: path, - } - } - request.JSONResponse(w, categories, http.StatusOK) -} - -func (q *QbitHandler) handleCreateCategory(w http.ResponseWriter, r *http.Request) { - err := r.ParseForm() - if err != nil { - http.Error(w, "Failed to parse form data", http.StatusBadRequest) - return - } - - name := r.Form.Get("category") - if name == "" { - http.Error(w, "No name provided", http.StatusBadRequest) - return - } - - q.qbit.Categories = append(q.qbit.Categories, name) - - request.JSONResponse(w, nil, http.StatusOK) -} - -func (q *QbitHandler) handleTorrentProperties(w http.ResponseWriter, r *http.Request) { - hash := r.URL.Query().Get("hash") - torrent := q.qbit.Storage.Get(hash) - properties := q.qbit.GetTorrentProperties(torrent) - request.JSONResponse(w, properties, http.StatusOK) -} - -func (q *QbitHandler) handleTorrentFiles(w http.ResponseWriter, r *http.Request) { - hash := r.URL.Query().Get("hash") - torrent := q.qbit.Storage.Get(hash) - if torrent == nil { - return - } - files := q.qbit.GetTorrentFiles(torrent) - request.JSONResponse(w, files, http.StatusOK) -} - -func (q *QbitHandler) 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) - for _, torrent := range torrents { - torrent.Category = category - q.qbit.Storage.AddOrUpdate(torrent) - } - request.JSONResponse(w, nil, http.StatusOK) -} - -func (q *QbitHandler) handleAddTorrentTags(w http.ResponseWriter, r *http.Request) { - err := r.ParseForm() - if err != nil { - http.Error(w, "Failed to parse form data", http.StatusBadRequest) - return - } - ctx := r.Context() - hashes, _ := ctx.Value("hashes").([]string) - tags := strings.Split(r.FormValue("tags"), ",") - for i, tag := range tags { - tags[i] = strings.TrimSpace(tag) - } - torrents := q.qbit.Storage.GetAll("", "", hashes) - for _, t := range torrents { - q.qbit.SetTorrentTags(t, tags) - } - request.JSONResponse(w, nil, http.StatusOK) -} - -func (q *QbitHandler) handleRemoveTorrentTags(w http.ResponseWriter, r *http.Request) { - err := r.ParseForm() - if err != nil { - http.Error(w, "Failed to parse form data", http.StatusBadRequest) - return - } - ctx := r.Context() - hashes, _ := ctx.Value("hashes").([]string) - tags := strings.Split(r.FormValue("tags"), ",") - for i, tag := range tags { - tags[i] = strings.TrimSpace(tag) - } - torrents := q.qbit.Storage.GetAll("", "", hashes) - for _, torrent := range torrents { - q.qbit.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 *QbitHandler) handleCreateTags(w http.ResponseWriter, r *http.Request) { - err := r.ParseForm() - if err != nil { - http.Error(w, "Failed to parse form data", http.StatusBadRequest) - return - } - tags := strings.Split(r.FormValue("tags"), ",") - for i, tag := range tags { - tags[i] = strings.TrimSpace(tag) - } - q.qbit.AddTags(tags) - request.JSONResponse(w, nil, http.StatusOK) -} diff --git a/pkg/qbit/server/qbit_routes.go b/pkg/qbit/server/qbit_routes.go deleted file mode 100644 index 3082505..0000000 --- a/pkg/qbit/server/qbit_routes.go +++ /dev/null @@ -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 -} diff --git a/pkg/qbit/server/server.go b/pkg/qbit/server/server.go deleted file mode 100644 index 41b5a2e..0000000 --- a/pkg/qbit/server/server.go +++ /dev/null @@ -1,110 +0,0 @@ -package server - -import ( - "context" - "errors" - "fmt" - "github.com/go-chi/chi/v5" - "github.com/go-chi/chi/v5/middleware" - "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" - "os/signal" - "syscall" -) - -type Server struct { - qbit *shared.QBit - logger zerolog.Logger -} - -func NewServer(deb *debrid.DebridService, arrs *arr.Storage, _repair *repair.Repair) *Server { - cfg := config.GetConfig() - l := logger.NewLogger("QBit", cfg.QBitTorrent.LogLevel, os.Stdout) - q := shared.NewQBit(deb, l, arrs, _repair) - return &Server{ - qbit: q, - 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, - } - - // 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) - srv := &http.Server{ - Addr: port, - Handler: r, - } - - ctx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM) - defer stop() - - go func() { - if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { - s.logger.Info().Msgf("Error starting server: %v", err) - stop() - } - }() - - <-ctx.Done() - s.logger.Info().Msg("Shutting down gracefully...") - return srv.Shutdown(context.Background()) -} - -func (s *Server) GetLogs(w http.ResponseWriter, r *http.Request) { - logFile := logger.GetLogPath() - - // Open and read the file - file, err := os.Open(logFile) - if err != nil { - http.Error(w, "Error reading log file", http.StatusInternalServerError) - return - } - defer func(file *os.File) { - err := file.Close() - if err != nil { - s.logger.Debug().Err(err).Msg("Error closing log file") - } - }(file) - - // Set headers - w.Header().Set("Content-Type", "text/plain; charset=utf-8") - w.Header().Set("Content-Disposition", "inline; filename=application.log") - w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") - w.Header().Set("Pragma", "no-cache") - w.Header().Set("Expires", "0") - - // Stream the file - _, err = io.Copy(w, file) - if err != nil { - s.logger.Debug().Err(err).Msg("Error streaming log file") - http.Error(w, "Error streaming log file", http.StatusInternalServerError) - return - } -} diff --git a/pkg/qbit/server/templates/config.html b/pkg/qbit/server/templates/config.html deleted file mode 100644 index c208fb0..0000000 --- a/pkg/qbit/server/templates/config.html +++ /dev/null @@ -1,393 +0,0 @@ -{{ define "config" }} -
-
-
-

Configuration

-
-
-
-
-
General Configuration
-
-
-
- - -
-
- -
- - -
-
-
- -
- -
-
-
-
-
- - - Minimum file size to download (0 for no limit) -
-
- -
-
- - - Maximum file size to download (0 for no limit) -
-
-
-
- -
-
Debrid Configuration
-
-
- - -
-
QBitTorrent Configuration
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- - -
-
-
-
- - -
-
Arr Configurations
-
-
- - -
-
Repair Configuration
-
-
- - -
-
-
- - -
-
- - -
-
-
-
-
-
-
-
- -{{ end }} \ No newline at end of file diff --git a/pkg/qbit/server/templates/download.html b/pkg/qbit/server/templates/download.html deleted file mode 100644 index 4e87007..0000000 --- a/pkg/qbit/server/templates/download.html +++ /dev/null @@ -1,142 +0,0 @@ -{{ define "download" }} -
-
-
-

Add New Download

-
-
-
-
- - -
- -
- -
- -
- -
- - -
- -
-
- - -
-
- - -
-
-
-
- - -{{ end }} \ No newline at end of file diff --git a/pkg/qbit/server/templates/index.html b/pkg/qbit/server/templates/index.html deleted file mode 100644 index fa04115..0000000 --- a/pkg/qbit/server/templates/index.html +++ /dev/null @@ -1,245 +0,0 @@ -{{ define "index" }} -
-
-
-

Active Torrents

-
- - - - -
-
-
-
- - - - - - - - - - - - - - - - -
- - NameSizeProgressSpeedCategoryDebridStateActions
-
-
-
-
- -{{ end }} \ No newline at end of file diff --git a/pkg/qbit/server/templates/layout.html b/pkg/qbit/server/templates/layout.html deleted file mode 100644 index f63be29..0000000 --- a/pkg/qbit/server/templates/layout.html +++ /dev/null @@ -1,196 +0,0 @@ -{{ define "layout" }} - - - - - - DecyphArr - {{.Title}} - - - - - - - -
- -
- - - {{ if eq .Page "index" }} - {{ template "index" . }} - {{ else if eq .Page "download" }} - {{ template "download" . }} - {{ else if eq .Page "repair" }} - {{ template "repair" . }} - {{ else if eq .Page "config" }} - {{ template "config" . }} - {{ else if eq .Page "login" }} - {{ template "login" . }} - {{ else if eq .Page "setup" }} - {{ template "setup" . }} - {{ else }} - {{ end }} - - - - - - - -{{ end }} \ No newline at end of file diff --git a/pkg/qbit/server/templates/login.html b/pkg/qbit/server/templates/login.html deleted file mode 100644 index 1625f3b..0000000 --- a/pkg/qbit/server/templates/login.html +++ /dev/null @@ -1,131 +0,0 @@ -{{ define "login" }} -
-
-
-
-
-

Login

-
-
-
-
- - -
-
- - -
-
- -
-
-
-
-
-
-
- - -{{ end }} - -{{ define "setup" }} -
-
-
-
-
-

First Time Setup

-
-
-
-
- - -
-
- - -
-
- - -
-
- -
-
-
-
-
-
-
- - -{{ end }} \ No newline at end of file diff --git a/pkg/qbit/server/templates/repair.html b/pkg/qbit/server/templates/repair.html deleted file mode 100644 index 2c678da..0000000 --- a/pkg/qbit/server/templates/repair.html +++ /dev/null @@ -1,94 +0,0 @@ -{{ define "repair" }} -
-
-
-

Repair Media

-
-
-
-
- - -
- -
- - - Enter TV DB ids for Sonarr, TM DB ids for Radarr -
- -
-
- - -
-
- - -
-
-
-
- -{{ end }} \ No newline at end of file diff --git a/pkg/qbit/server/templates/setup.html b/pkg/qbit/server/templates/setup.html deleted file mode 100644 index a7f666e..0000000 --- a/pkg/qbit/server/templates/setup.html +++ /dev/null @@ -1,32 +0,0 @@ -{{ define "setup" }} -
-
-
-
-
-

First Time Setup

-
-
-
-
- - -
-
- - -
-
- - -
-
- -
-
-
-
-
-
-
-{{ end }} \ No newline at end of file diff --git a/pkg/qbit/server/ui_auth_handlers.go b/pkg/qbit/server/ui_auth_handlers.go deleted file mode 100644 index cfcad82..0000000 --- a/pkg/qbit/server/ui_auth_handlers.go +++ /dev/null @@ -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 -} diff --git a/pkg/qbit/server/ui_handlers.go b/pkg/qbit/server/ui_handlers.go deleted file mode 100644 index a9b6f13..0000000 --- a/pkg/qbit/server/ui_handlers.go +++ /dev/null @@ -1,392 +0,0 @@ -package server - -import ( - "embed" - "encoding/json" - "fmt" - "github.com/gorilla/sessions" - "github.com/sirrobot01/debrid-blackhole/internal/config" - "github.com/sirrobot01/debrid-blackhole/internal/request" - "github.com/sirrobot01/debrid-blackhole/internal/utils" - "golang.org/x/crypto/bcrypt" - "html/template" - "net/http" - "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" -) - -type AddRequest struct { - Url string `json:"url"` - Arr string `json:"arr"` - File string `json:"file"` - NotSymlink bool `json:"notSymlink"` - Content string `json:"content"` - Seasons []string `json:"seasons"` - Episodes []string `json:"episodes"` -} - -type ArrResponse struct { - Name string `json:"name"` - Url string `json:"url"` -} - -type ContentResponse struct { - ID string `json:"id"` - Title string `json:"title"` - Type string `json:"type"` - ArrID string `json:"arr"` -} - -type RepairRequest struct { - ArrName string `json:"arr"` - MediaIds []string `json:"mediaIds"` - Async bool `json:"async"` -} - -//go:embed templates/* -var content embed.FS - -type UIHandler struct { - qbit *shared.QBit - logger zerolog.Logger - debug bool -} - -var ( - store = sessions.NewCookieStore([]byte("your-secret-key")) // Change this to a secure key - templates *template.Template -) - -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", - )) - - store.Options = &sessions.Options{ - Path: "/", - MaxAge: 86400 * 7, - HttpOnly: false, - } -} - -func (u *UIHandler) LoginHandler(w http.ResponseWriter, r *http.Request) { - if r.Method == "GET" { - data := map[string]interface{}{ - "Page": "login", - "Title": "Login", - } - if err := templates.ExecuteTemplate(w, "layout", data); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } - return - } - - var credentials struct { - Username string `json:"username"` - Password string `json:"password"` - } - - if err := json.NewDecoder(r.Body).Decode(&credentials); err != nil { - http.Error(w, "Invalid request", http.StatusBadRequest) - return - } - - if u.verifyAuth(credentials.Username, credentials.Password) { - session, _ := store.Get(r, "auth-session") - session.Values["authenticated"] = true - session.Values["username"] = credentials.Username - session.Save(r, w) - http.Redirect(w, r, "/", http.StatusSeeOther) - return - } - - http.Error(w, "Invalid credentials", http.StatusUnauthorized) -} - -func (u *UIHandler) 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) - http.Redirect(w, r, "/login", http.StatusSeeOther) -} - -func (u *UIHandler) SetupHandler(w http.ResponseWriter, r *http.Request) { - cfg := config.GetConfig() - authCfg := cfg.GetAuth() - - if !cfg.NeedsSetup() { - http.Redirect(w, r, "/", http.StatusSeeOther) - return - } - - if r.Method == "GET" { - data := map[string]interface{}{ - "Page": "setup", - "Title": "Setup", - } - if err := templates.ExecuteTemplate(w, "layout", data); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } - return - } - - // Handle POST (setup attempt) - username := r.FormValue("username") - password := r.FormValue("password") - confirmPassword := r.FormValue("confirmPassword") - - if password != confirmPassword { - http.Error(w, "Passwords do not match", http.StatusBadRequest) - return - } - - // Hash the password - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) - if err != nil { - http.Error(w, "Error processing password", http.StatusInternalServerError) - return - } - - // Set the credentials - authCfg.Username = username - authCfg.Password = string(hashedPassword) - - if err := cfg.SaveAuth(authCfg); err != nil { - http.Error(w, "Error saving credentials", http.StatusInternalServerError) - return - } - - // Create a session - session, _ := store.Get(r, "auth-session") - session.Values["authenticated"] = true - session.Values["username"] = username - session.Save(r, w) - - http.Redirect(w, r, "/", http.StatusSeeOther) -} - -func (u *UIHandler) IndexHandler(w http.ResponseWriter, r *http.Request) { - data := map[string]interface{}{ - "Page": "index", - "Title": "Torrents", - } - if err := templates.ExecuteTemplate(w, "layout", data); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } -} - -func (u *UIHandler) DownloadHandler(w http.ResponseWriter, r *http.Request) { - data := map[string]interface{}{ - "Page": "download", - "Title": "Download", - } - if err := templates.ExecuteTemplate(w, "layout", data); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } -} - -func (u *UIHandler) RepairHandler(w http.ResponseWriter, r *http.Request) { - data := map[string]interface{}{ - "Page": "repair", - "Title": "Repair", - } - if err := templates.ExecuteTemplate(w, "layout", data); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } -} - -func (u *UIHandler) ConfigHandler(w http.ResponseWriter, r *http.Request) { - data := map[string]interface{}{ - "Page": "config", - "Title": "Config", - } - if err := templates.ExecuteTemplate(w, "layout", data); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } -} - -func (u *UIHandler) handleGetArrs(w http.ResponseWriter, r *http.Request) { - request.JSONResponse(w, u.qbit.Arrs.GetAll(), http.StatusOK) -} - -func (u *UIHandler) handleAddContent(w http.ResponseWriter, r *http.Request) { - if err := r.ParseMultipartForm(32 << 20); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - results := make([]*ImportRequest, 0) - errs := make([]string, 0) - - arrName := r.FormValue("arr") - notSymlink := r.FormValue("notSymlink") == "true" - - _arr := u.qbit.Arrs.Get(arrName) - if _arr == nil { - _arr = arr.NewArr(arrName, "", "", arr.Sonarr) - } - - // Handle URLs - if urls := r.FormValue("urls"); urls != "" { - var urlList []string - for _, u := range strings.Split(urls, "\n") { - if trimmed := strings.TrimSpace(u); trimmed != "" { - urlList = append(urlList, trimmed) - } - } - - for _, url := range urlList { - importReq := NewImportRequest(url, _arr, !notSymlink) - err := importReq.Process(u.qbit) - if err != nil { - errs = append(errs, fmt.Sprintf("URL %s: %v", url, err)) - continue - } - results = append(results, importReq) - } - } - - // Handle torrent/magnet files - if files := r.MultipartForm.File["files"]; len(files) > 0 { - for _, fileHeader := range files { - file, err := fileHeader.Open() - if err != nil { - errs = append(errs, fmt.Sprintf("Failed to open file %s: %v", fileHeader.Filename, err)) - continue - } - - magnet, err := utils.GetMagnetFromFile(file, fileHeader.Filename) - if err != nil { - errs = append(errs, fmt.Sprintf("Failed to parse torrent file %s: %v", fileHeader.Filename, err)) - continue - } - - importReq := NewImportRequest(magnet.Link, _arr, !notSymlink) - err = importReq.Process(u.qbit) - if err != nil { - errs = append(errs, fmt.Sprintf("File %s: %v", fileHeader.Filename, err)) - continue - } - results = append(results, importReq) - } - } - - request.JSONResponse(w, struct { - Results []*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) { - 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) - if _arr == nil { - http.Error(w, "No Arrs found to repair", http.StatusNotFound) - return - } - - 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") - } - }() - request.JSONResponse(w, "Repair process started", http.StatusOK) - return - } - - if err := u.qbit.Repair.Repair([]*arr.Arr{_arr}, req.MediaIds); err != nil { - http.Error(w, fmt.Sprintf("Failed to repair: %v", err), http.StatusInternalServerError) - return - - } - - request.JSONResponse(w, "Repair completed", http.StatusOK) -} - -func (u *UIHandler) 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 (u *UIHandler) 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) - w.WriteHeader(http.StatusOK) -} - -func (u *UIHandler) handleGetConfig(w http.ResponseWriter, r *http.Request) { - cfg := config.GetConfig() - arrCfgs := make([]config.Arr, 0) - for _, a := range u.qbit.Arrs.GetAll() { - arrCfgs = append(arrCfgs, config.Arr{Host: a.Host, Name: a.Name, Token: a.Token}) - } - cfg.Arrs = arrCfgs - request.JSONResponse(w, cfg, http.StatusOK) -} diff --git a/pkg/qbit/server/ui_routes.go b/pkg/qbit/server/ui_routes.go deleted file mode 100644 index fffe35d..0000000 --- a/pkg/qbit/server/ui_routes.go +++ /dev/null @@ -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 -} diff --git a/pkg/qbit/shared/downloader.go b/pkg/qbit/shared/downloader.go deleted file mode 100644 index 14800c0..0000000 --- a/pkg/qbit/shared/downloader.go +++ /dev/null @@ -1,153 +0,0 @@ -package shared - -import ( - "fmt" - "github.com/sirrobot01/debrid-blackhole/common" - "github.com/sirrobot01/debrid-blackhole/pkg/debrid" - "github.com/sirrobot01/debrid-blackhole/pkg/downloaders" - "os" - "path/filepath" - "sync" - "time" -) - -func (q *QBit) ProcessManualFile(torrent *Torrent) (string, error) { - debridTorrent := torrent.DebridTorrent - q.logger.Info().Msgf("Downloading %d files...", len(debridTorrent.DownloadLinks)) - torrentPath := common.RemoveExtension(debridTorrent.OriginalFilename) - parent := common.RemoveInvalidChars(filepath.Join(q.DownloadFolder, debridTorrent.Arr.Name, torrentPath)) - err := os.MkdirAll(parent, os.ModePerm) - if err != nil { - // add previous error to the error and return - return "", fmt.Errorf("failed to create directory: %s: %v", parent, err) - } - q.downloadFiles(torrent, parent) - return torrentPath, nil -} - -func (q *QBit) downloadFiles(torrent *Torrent, parent string) { - debridTorrent := torrent.DebridTorrent - var wg sync.WaitGroup - semaphore := make(chan struct{}, 5) - totalSize := int64(0) - for _, file := range debridTorrent.Files { - totalSize += file.Size - } - debridTorrent.Mu.Lock() - 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() - torrent.Mu.Lock() - defer torrent.Mu.Unlock() - - // Update total downloaded bytes - debridTorrent.SizeDownloaded += downloaded - debridTorrent.Speed = speed - - // Calculate overall progress - if totalSize > 0 { - debridTorrent.Progress = float64(debridTorrent.SizeDownloaded) / float64(totalSize) * 100 - } - q.UpdateTorrentMin(torrent, debridTorrent) - } - for _, link := range debridTorrent.DownloadLinks { - if link.DownloadLink == "" { - q.logger.Info().Msgf("No download link found for %s", link.Filename) - continue - } - wg.Add(1) - semaphore <- struct{}{} - go func(link debrid.TorrentDownloadLinks) { - defer wg.Done() - defer func() { <-semaphore }() - filename := link.Filename - - err := downloaders.NormalGrab( - client, - link.DownloadLink, - filepath.Join(parent, filename), - progressCallback, - ) - - if err != nil { - q.logger.Error().Msgf("Failed to download %s: %v", filename, err) - } else { - q.logger.Info().Msgf("Downloaded %s", filename) - } - }(link) - } - wg.Wait() - q.logger.Info().Msgf("Downloaded all files for %s", debridTorrent.Name) -} - -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)) - if len(files) == 0 { - return "", fmt.Errorf("no video files found") - } - q.logger.Info().Msgf("Checking %d files...", len(files)) - rCloneBase := debridTorrent.Debrid.GetMountPath() - torrentPath, err := q.getTorrentPath(rCloneBase, debridTorrent) // /MyTVShow/ - if err != nil { - return "", fmt.Errorf("failed to get torrent path: %v", err) - } - // Fix for alldebrid - newTorrentPath := torrentPath - if newTorrentPath == "" { - // Alldebrid at times doesn't return the parent folder for single file torrents - newTorrentPath = common.RemoveExtension(debridTorrent.Name) // MyTVShow - } - torrentSymlinkPath := filepath.Join(q.DownloadFolder, debridTorrent.Arr.Name, newTorrentPath) // /mnt/symlinks/{category}/MyTVShow/ - err = os.MkdirAll(torrentSymlinkPath, os.ModePerm) - if err != nil { - return "", fmt.Errorf("failed to create directory: %s: %v", torrentSymlinkPath, err) - } - torrentRclonePath := filepath.Join(rCloneBase, torrentPath) // leave it as is - q.logger.Debug().Msgf("Debrid torrent path: %s\nSymlink Path: %s", torrentRclonePath, torrentSymlinkPath) - for _, file := range files { - wg.Add(1) - go checkFileLoop(&wg, torrentRclonePath, file, ready) - } - - go func() { - wg.Wait() - close(ready) - }() - - for f := range ready { - q.logger.Info().Msgf("File is ready: %s", f.Path) - q.createSymLink(torrentSymlinkPath, torrentRclonePath, f) - } - return torrentPath, nil -} - -func (q *QBit) getTorrentPath(rclonePath string, debridTorrent *debrid.Torrent) (string, error) { - for { - q.logger.Debug().Msgf("Checking for torrent path: %s", rclonePath) - torrentPath, err := debridTorrent.GetMountFolder(rclonePath) - if err == nil { - q.logger.Debug().Msgf("Found torrent path: %s", torrentPath) - return torrentPath, err - } - time.Sleep(time.Second) - } -} - -func (q *QBit) createSymLink(path string, torrentMountPath string, file debrid.TorrentFile) { - - // Combine the directory and filename to form a full path - fullPath := filepath.Join(path, file.Name) // /mnt/symlinks/{category}/MyTVShow/MyTVShow.S01E01.720p.mkv - // Create a symbolic link if file doesn't exist - torrentFilePath := filepath.Join(torrentMountPath, file.Path) // debridFolder/MyTVShow/MyTVShow.S01E01.720p.mkv - err := os.Symlink(torrentFilePath, fullPath) - if err != nil { - q.logger.Info().Msgf("Failed to create symlink: %s: %v", fullPath, err) - } -} diff --git a/pkg/qbit/shared/qbit.go b/pkg/qbit/shared/qbit.go deleted file mode 100644 index 21fa59c..0000000 --- a/pkg/qbit/shared/qbit.go +++ /dev/null @@ -1,46 +0,0 @@ -package shared - -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" - "os" -) - -type QBit struct { - Username string `json:"username"` - Password string `json:"password"` - 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 { - cfg := config.GetConfig().QBitTorrent - port := cmp.Or(cfg.Port, os.Getenv("QBIT_PORT"), "8282") - refreshInterval := cmp.Or(cfg.RefreshInterval, 10) - return &QBit{ - Username: cfg.Username, - Password: cfg.Password, - 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, - RefreshInterval: refreshInterval, - } -} diff --git a/pkg/qbit/shared/storage.go b/pkg/qbit/shared/storage.go deleted file mode 100644 index 16ce342..0000000 --- a/pkg/qbit/shared/storage.go +++ /dev/null @@ -1,151 +0,0 @@ -package shared - -import ( - "encoding/json" - "os" - "sync" -) - -type TorrentStorage struct { - torrents map[string]*Torrent - mu sync.RWMutex - order []string - filename string // Added to store the filename for persistence -} - -func loadTorrentsFromJSON(filename string) (map[string]*Torrent, error) { - data, err := os.ReadFile(filename) - if err != nil { - return nil, err - } - torrents := make(map[string]*Torrent) - if err := json.Unmarshal(data, &torrents); err != nil { - return nil, err - } - return torrents, nil -} - -func NewTorrentStorage(filename string) *TorrentStorage { - // Open the JSON file and read the data - torrents, err := loadTorrentsFromJSON(filename) - if err != nil { - torrents = make(map[string]*Torrent) - } - order := make([]string, 0, len(torrents)) - for id := range torrents { - order = append(order, id) - } - // Create a new TorrentStorage - return &TorrentStorage{ - torrents: torrents, - order: order, - filename: filename, - } -} - -func (ts *TorrentStorage) Add(torrent *Torrent) { - ts.mu.Lock() - defer ts.mu.Unlock() - ts.torrents[torrent.Hash] = torrent - ts.order = append(ts.order, torrent.Hash) - _ = ts.saveToFile() -} - -func (ts *TorrentStorage) AddOrUpdate(torrent *Torrent) { - ts.mu.Lock() - defer ts.mu.Unlock() - if _, exists := ts.torrents[torrent.Hash]; !exists { - ts.order = append(ts.order, torrent.Hash) - } - ts.torrents[torrent.Hash] = torrent - _ = ts.saveToFile() -} - -func (ts *TorrentStorage) GetByID(id string) *Torrent { - ts.mu.RLock() - defer ts.mu.RUnlock() - for _, torrent := range ts.torrents { - if torrent.ID == id { - return torrent - } - } - return nil -} - -func (ts *TorrentStorage) Get(hash string) *Torrent { - ts.mu.RLock() - defer ts.mu.RUnlock() - return ts.torrents[hash] -} - -func (ts *TorrentStorage) GetAll(category string, filter string, hashes []string) []*Torrent { - ts.mu.RLock() - defer ts.mu.RUnlock() - torrents := make([]*Torrent, 0) - for _, id := range ts.order { - torrent := ts.torrents[id] - if category != "" && torrent.Category != category { - continue - } - if filter != "" && torrent.State != filter { - continue - } - torrents = append(torrents, torrent) - } - if len(hashes) > 0 { - filtered := make([]*Torrent, 0, len(torrents)) - for _, hash := range hashes { - if torrent := ts.torrents[hash]; torrent != nil { - filtered = append(filtered, torrent) - } - } - torrents = filtered - } - return torrents -} - -func (ts *TorrentStorage) Update(torrent *Torrent) { - ts.mu.Lock() - defer ts.mu.Unlock() - ts.torrents[torrent.Hash] = torrent - _ = ts.saveToFile() -} - -func (ts *TorrentStorage) Delete(hash string) { - ts.mu.Lock() - defer ts.mu.Unlock() - torrent, exists := ts.torrents[hash] - if !exists { - return - } - delete(ts.torrents, hash) - for i, id := range ts.order { - if id == hash { - ts.order = append(ts.order[:i], ts.order[i+1:]...) - break - } - } - // Delete the torrent folder - if torrent.ContentPath != "" { - err := os.RemoveAll(torrent.ContentPath) - if err != nil { - return - } - } - _ = ts.saveToFile() -} - -func (ts *TorrentStorage) Save() error { - ts.mu.RLock() - defer ts.mu.RUnlock() - return ts.saveToFile() -} - -// saveToFile is a helper function to write the current state to the JSON file -func (ts *TorrentStorage) saveToFile() error { - data, err := json.MarshalIndent(ts.torrents, "", " ") - if err != nil { - return err - } - return os.WriteFile(ts.filename, data, 0644) -} diff --git a/pkg/qbit/shared/torrent.go b/pkg/qbit/shared/torrent.go deleted file mode 100644 index cb56113..0000000 --- a/pkg/qbit/shared/torrent.go +++ /dev/null @@ -1,321 +0,0 @@ -package shared - -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" - "io" - "mime/multipart" - "os" - "path/filepath" - "slices" - "strings" - "time" -) - -// All torrent related helpers goes here - -func (q *QBit) AddMagnet(ctx context.Context, url, category string) error { - magnet, err := utils.GetMagnetFromUrl(url) - if err != nil { - return fmt.Errorf("error parsing magnet link: %w", err) - } - err = q.Process(ctx, magnet, category) - if err != nil { - return fmt.Errorf("failed to process torrent: %w", err) - } - return nil -} - -func (q *QBit) AddTorrent(ctx context.Context, fileHeader *multipart.FileHeader, category string) error { - file, _ := fileHeader.Open() - defer file.Close() - var reader io.Reader = file - magnet, err := utils.GetMagnetFromFile(reader, fileHeader.Filename) - if err != nil { - return fmt.Errorf("error reading file: %s \n %w", fileHeader.Filename, err) - } - err = q.Process(ctx, magnet, category) - if err != nil { - return fmt.Errorf("failed to process torrent: %w", err) - } - return nil -} - -func (q *QBit) Process(ctx context.Context, magnet *utils.Magnet, category string) error { - torrent := q.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) - if err != nil || debridTorrent == nil { - if debridTorrent != nil { - go debridTorrent.Delete() - } - if err == nil { - err = fmt.Errorf("failed to process torrent") - } - return err - } - torrent = q.UpdateTorrentMin(torrent, debridTorrent) - q.Storage.AddOrUpdate(torrent) - go q.ProcessFiles(torrent, debridTorrent, a, isSymlink) // We can send async for file processing not to delay the response - 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) { - for debridTorrent.Status != "downloaded" { - progress := debridTorrent.Progress - q.logger.Debug().Msgf("%s -> (%s) Download Progress: %.2f%%", debridTorrent.Debrid.GetName(), debridTorrent.Name, progress) - time.Sleep(10 * time.Second) - dbT, err := debridTorrent.Debrid.CheckStatus(debridTorrent, isSymlink) - if err != nil { - q.logger.Error().Msgf("Error checking status: %v", err) - go debridTorrent.Delete() - q.MarkAsFailed(torrent) - _ = arr.Refresh() - return - } - debridTorrent = dbT - torrent = q.UpdateTorrentMin(torrent, debridTorrent) - } - var ( - torrentPath string - err error - ) - debridTorrent.Arr = arr - if isSymlink { - torrentPath, err = q.ProcessSymlink(torrent) - } else { - torrentPath, err = q.ProcessManualFile(torrent) - } - if err != nil { - q.MarkAsFailed(torrent) - go debridTorrent.Delete() - q.logger.Info().Msgf("Error: %v", err) - return - } - torrent.TorrentPath = filepath.Base(torrentPath) - q.UpdateTorrent(torrent, debridTorrent) - _ = arr.Refresh() -} - -func (q *QBit) MarkAsFailed(t *Torrent) *Torrent { - t.State = "error" - q.Storage.AddOrUpdate(t) - return t -} - -func (q *QBit) UpdateTorrentMin(t *Torrent, debridTorrent *debrid.Torrent) *Torrent { - if debridTorrent == nil { - return t - } - - addedOn, err := time.Parse(time.RFC3339, debridTorrent.Added) - if err != nil { - addedOn = time.Now() - } - totalSize := debridTorrent.Bytes - progress := cmp.Or(debridTorrent.Progress, 100) - progress = progress / 100.0 - sizeCompleted := int64(float64(totalSize) * progress) - - var speed int64 - if debridTorrent.Speed != 0 { - speed = debridTorrent.Speed - } - var eta int - if speed != 0 { - eta = int((totalSize - sizeCompleted) / speed) - } - t.ID = debridTorrent.Id - t.Name = debridTorrent.Name - t.AddedOn = addedOn.Unix() - t.DebridTorrent = debridTorrent - t.Debrid = debridTorrent.Debrid.GetName() - t.Size = totalSize - t.Completed = sizeCompleted - t.Downloaded = sizeCompleted - t.DownloadedSession = sizeCompleted - t.Uploaded = sizeCompleted - t.UploadedSession = sizeCompleted - t.AmountLeft = totalSize - sizeCompleted - t.Progress = progress - t.Eta = eta - t.Dlspeed = speed - t.Upspeed = speed - t.SavePath = filepath.Join(q.DownloadFolder, t.Category) + string(os.PathSeparator) - t.ContentPath = filepath.Join(t.SavePath, t.Name) + string(os.PathSeparator) - return t -} - -func (q *QBit) UpdateTorrent(t *Torrent, debridTorrent *debrid.Torrent) *Torrent { - db := debridTorrent.Debrid - rcLoneMount := db.GetMountPath() - if debridTorrent == nil && 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()) - return t - } - if debridTorrent.Status != "downloaded" { - debridTorrent, _ = db.GetTorrent(t.ID) - } - - if t.TorrentPath == "" { - tPath, _ := debridTorrent.GetMountFolder(rcLoneMount) - t.TorrentPath = filepath.Base(tPath) - } - savePath := filepath.Join(q.DownloadFolder, t.Category) + string(os.PathSeparator) - torrentPath := filepath.Join(savePath, t.TorrentPath) + string(os.PathSeparator) - t = q.UpdateTorrentMin(t, debridTorrent) - t.ContentPath = torrentPath - - if t.IsReady() { - t.State = "pausedUP" - q.Storage.Update(t) - return t - } - - ticker := time.NewTicker(2 * time.Second) - defer ticker.Stop() - - for { - select { - case <-ticker.C: - if t.IsReady() { - t.State = "pausedUP" - q.Storage.Update(t) - return t - } - updatedT := q.UpdateTorrent(t, debridTorrent) - t = updatedT - - case <-time.After(10 * time.Minute): // Add a timeout - return t - } - } -} - -func (q *QBit) ResumeTorrent(t *Torrent) bool { - return true -} - -func (q *QBit) PauseTorrent(t *Torrent) bool { - return true -} - -func (q *QBit) RefreshTorrent(t *Torrent) bool { - return true -} - -func (q *QBit) GetTorrentProperties(t *Torrent) *TorrentProperties { - return &TorrentProperties{ - AdditionDate: t.AddedOn, - Comment: "Debrid Blackhole ", - CreatedBy: "Debrid Blackhole ", - CreationDate: t.AddedOn, - DlLimit: -1, - UpLimit: -1, - DlSpeed: t.Dlspeed, - UpSpeed: t.Upspeed, - TotalSize: t.Size, - TotalUploaded: t.Uploaded, - TotalDownloaded: t.Downloaded, - TotalUploadedSession: t.UploadedSession, - TotalDownloadedSession: t.DownloadedSession, - LastSeen: time.Now().Unix(), - NbConnectionsLimit: 100, - Peers: 0, - PeersTotal: 2, - SeedingTime: 1, - Seeds: 100, - ShareRatio: 100, - } -} - -func (q *QBit) GetTorrentFiles(t *Torrent) []*TorrentFile { - files := make([]*TorrentFile, 0) - if t.DebridTorrent == nil { - return files - } - for _, file := range t.DebridTorrent.Files { - files = append(files, &TorrentFile{ - Name: file.Path, - Size: file.Size, - }) - } - return files -} - -func (q *QBit) SetTorrentTags(t *Torrent, tags []string) bool { - torrentTags := strings.Split(t.Tags, ",") - for _, tag := range tags { - if tag == "" { - continue - } - if !slices.Contains(torrentTags, tag) { - torrentTags = append(torrentTags, tag) - } - if !slices.Contains(q.Tags, tag) { - q.Tags = append(q.Tags, tag) - } - } - t.Tags = strings.Join(torrentTags, ",") - q.Storage.Update(t) - return true -} - -func (q *QBit) RemoveTorrentTags(t *Torrent, tags []string) bool { - torrentTags := strings.Split(t.Tags, ",") - newTorrentTags := utils.RemoveItem(torrentTags, tags...) - q.Tags = utils.RemoveItem(q.Tags, tags...) - t.Tags = strings.Join(newTorrentTags, ",") - q.Storage.Update(t) - return true -} - -func (q *QBit) AddTags(tags []string) bool { - for _, tag := range tags { - if tag == "" { - continue - } - if !slices.Contains(q.Tags, tag) { - q.Tags = append(q.Tags, tag) - } - } - return true -} - -func (q *QBit) RemoveTags(tags []string) bool { - q.Tags = utils.RemoveItem(q.Tags, tags...) - return true -} diff --git a/pkg/qbit/shared/types.go b/pkg/qbit/shared/types.go deleted file mode 100644 index 0a27953..0000000 --- a/pkg/qbit/shared/types.go +++ /dev/null @@ -1,430 +0,0 @@ -package shared - -import ( - "github.com/sirrobot01/debrid-blackhole/pkg/debrid" - "sync" -) - -type BuildInfo struct { - Libtorrent string `json:"libtorrent"` - Bitness int `json:"bitness"` - Boost string `json:"boost"` - Openssl string `json:"openssl"` - Qt string `json:"qt"` - Zlib string `json:"zlib"` -} - -type AppPreferences struct { - AddTrackers string `json:"add_trackers"` - AddTrackersEnabled bool `json:"add_trackers_enabled"` - AltDlLimit int `json:"alt_dl_limit"` - AltUpLimit int `json:"alt_up_limit"` - AlternativeWebuiEnabled bool `json:"alternative_webui_enabled"` - AlternativeWebuiPath string `json:"alternative_webui_path"` - AnnounceIp string `json:"announce_ip"` - AnnounceToAllTiers bool `json:"announce_to_all_tiers"` - AnnounceToAllTrackers bool `json:"announce_to_all_trackers"` - AnonymousMode bool `json:"anonymous_mode"` - AsyncIoThreads int `json:"async_io_threads"` - AutoDeleteMode int `json:"auto_delete_mode"` - AutoTmmEnabled bool `json:"auto_tmm_enabled"` - AutorunEnabled bool `json:"autorun_enabled"` - AutorunProgram string `json:"autorun_program"` - BannedIPs string `json:"banned_IPs"` - BittorrentProtocol int `json:"bittorrent_protocol"` - BypassAuthSubnetWhitelist string `json:"bypass_auth_subnet_whitelist"` - BypassAuthSubnetWhitelistEnabled bool `json:"bypass_auth_subnet_whitelist_enabled"` - BypassLocalAuth bool `json:"bypass_local_auth"` - CategoryChangedTmmEnabled bool `json:"category_changed_tmm_enabled"` - CheckingMemoryUse int `json:"checking_memory_use"` - CreateSubfolderEnabled bool `json:"create_subfolder_enabled"` - CurrentInterfaceAddress string `json:"current_interface_address"` - CurrentNetworkInterface string `json:"current_network_interface"` - Dht bool `json:"dht"` - DiskCache int `json:"disk_cache"` - DiskCacheTtl int `json:"disk_cache_ttl"` - DlLimit int `json:"dl_limit"` - DontCountSlowTorrents bool `json:"dont_count_slow_torrents"` - DyndnsDomain string `json:"dyndns_domain"` - DyndnsEnabled bool `json:"dyndns_enabled"` - DyndnsPassword string `json:"dyndns_password"` - DyndnsService int `json:"dyndns_service"` - DyndnsUsername string `json:"dyndns_username"` - EmbeddedTrackerPort int `json:"embedded_tracker_port"` - EnableCoalesceReadWrite bool `json:"enable_coalesce_read_write"` - EnableEmbeddedTracker bool `json:"enable_embedded_tracker"` - EnableMultiConnectionsFromSameIp bool `json:"enable_multi_connections_from_same_ip"` - EnableOsCache bool `json:"enable_os_cache"` - EnablePieceExtentAffinity bool `json:"enable_piece_extent_affinity"` - EnableSuperSeeding bool `json:"enable_super_seeding"` - EnableUploadSuggestions bool `json:"enable_upload_suggestions"` - Encryption int `json:"encryption"` - ExportDir string `json:"export_dir"` - ExportDirFin string `json:"export_dir_fin"` - FilePoolSize int `json:"file_pool_size"` - IncompleteFilesExt bool `json:"incomplete_files_ext"` - IpFilterEnabled bool `json:"ip_filter_enabled"` - IpFilterPath string `json:"ip_filter_path"` - IpFilterTrackers bool `json:"ip_filter_trackers"` - LimitLanPeers bool `json:"limit_lan_peers"` - LimitTcpOverhead bool `json:"limit_tcp_overhead"` - LimitUtpRate bool `json:"limit_utp_rate"` - ListenPort int `json:"listen_port"` - Locale string `json:"locale"` - Lsd bool `json:"lsd"` - MailNotificationAuthEnabled bool `json:"mail_notification_auth_enabled"` - MailNotificationEmail string `json:"mail_notification_email"` - MailNotificationEnabled bool `json:"mail_notification_enabled"` - MailNotificationPassword string `json:"mail_notification_password"` - MailNotificationSender string `json:"mail_notification_sender"` - MailNotificationSmtp string `json:"mail_notification_smtp"` - MailNotificationSslEnabled bool `json:"mail_notification_ssl_enabled"` - MailNotificationUsername string `json:"mail_notification_username"` - MaxActiveDownloads int `json:"max_active_downloads"` - MaxActiveTorrents int `json:"max_active_torrents"` - MaxActiveUploads int `json:"max_active_uploads"` - MaxConnec int `json:"max_connec"` - MaxConnecPerTorrent int `json:"max_connec_per_torrent"` - MaxRatio int `json:"max_ratio"` - MaxRatioAct int `json:"max_ratio_act"` - MaxRatioEnabled bool `json:"max_ratio_enabled"` - MaxSeedingTime int `json:"max_seeding_time"` - MaxSeedingTimeEnabled bool `json:"max_seeding_time_enabled"` - MaxUploads int `json:"max_uploads"` - MaxUploadsPerTorrent int `json:"max_uploads_per_torrent"` - OutgoingPortsMax int `json:"outgoing_ports_max"` - OutgoingPortsMin int `json:"outgoing_ports_min"` - Pex bool `json:"pex"` - PreallocateAll bool `json:"preallocate_all"` - ProxyAuthEnabled bool `json:"proxy_auth_enabled"` - ProxyIp string `json:"proxy_ip"` - ProxyPassword string `json:"proxy_password"` - ProxyPeerConnections bool `json:"proxy_peer_connections"` - ProxyPort int `json:"proxy_port"` - ProxyTorrentsOnly bool `json:"proxy_torrents_only"` - ProxyType int `json:"proxy_type"` - ProxyUsername string `json:"proxy_username"` - QueueingEnabled bool `json:"queueing_enabled"` - RandomPort bool `json:"random_port"` - RecheckCompletedTorrents bool `json:"recheck_completed_torrents"` - ResolvePeerCountries bool `json:"resolve_peer_countries"` - RssAutoDownloadingEnabled bool `json:"rss_auto_downloading_enabled"` - RssMaxArticlesPerFeed int `json:"rss_max_articles_per_feed"` - RssProcessingEnabled bool `json:"rss_processing_enabled"` - RssRefreshInterval int `json:"rss_refresh_interval"` - SavePath string `json:"save_path"` - SavePathChangedTmmEnabled bool `json:"save_path_changed_tmm_enabled"` - SaveResumeDataInterval int `json:"save_resume_data_interval"` - ScanDirs ScanDirs `json:"scan_dirs"` - ScheduleFromHour int `json:"schedule_from_hour"` - ScheduleFromMin int `json:"schedule_from_min"` - ScheduleToHour int `json:"schedule_to_hour"` - ScheduleToMin int `json:"schedule_to_min"` - SchedulerDays int `json:"scheduler_days"` - SchedulerEnabled bool `json:"scheduler_enabled"` - SendBufferLowWatermark int `json:"send_buffer_low_watermark"` - SendBufferWatermark int `json:"send_buffer_watermark"` - SendBufferWatermarkFactor int `json:"send_buffer_watermark_factor"` - SlowTorrentDlRateThreshold int `json:"slow_torrent_dl_rate_threshold"` - SlowTorrentInactiveTimer int `json:"slow_torrent_inactive_timer"` - SlowTorrentUlRateThreshold int `json:"slow_torrent_ul_rate_threshold"` - SocketBacklogSize int `json:"socket_backlog_size"` - StartPausedEnabled bool `json:"start_paused_enabled"` - StopTrackerTimeout int `json:"stop_tracker_timeout"` - TempPath string `json:"temp_path"` - TempPathEnabled bool `json:"temp_path_enabled"` - TorrentChangedTmmEnabled bool `json:"torrent_changed_tmm_enabled"` - UpLimit int `json:"up_limit"` - UploadChokingAlgorithm int `json:"upload_choking_algorithm"` - UploadSlotsBehavior int `json:"upload_slots_behavior"` - Upnp bool `json:"upnp"` - UpnpLeaseDuration int `json:"upnp_lease_duration"` - UseHttps bool `json:"use_https"` - UtpTcpMixedMode int `json:"utp_tcp_mixed_mode"` - WebUiAddress string `json:"web_ui_address"` - WebUiBanDuration int `json:"web_ui_ban_duration"` - WebUiClickjackingProtectionEnabled bool `json:"web_ui_clickjacking_protection_enabled"` - WebUiCsrfProtectionEnabled bool `json:"web_ui_csrf_protection_enabled"` - WebUiDomainList string `json:"web_ui_domain_list"` - WebUiHostHeaderValidationEnabled bool `json:"web_ui_host_header_validation_enabled"` - WebUiHttpsCertPath string `json:"web_ui_https_cert_path"` - WebUiHttpsKeyPath string `json:"web_ui_https_key_path"` - WebUiMaxAuthFailCount int `json:"web_ui_max_auth_fail_count"` - WebUiPort int `json:"web_ui_port"` - WebUiSecureCookieEnabled bool `json:"web_ui_secure_cookie_enabled"` - WebUiSessionTimeout int `json:"web_ui_session_timeout"` - WebUiUpnp bool `json:"web_ui_upnp"` - WebUiUsername string `json:"web_ui_username"` - WebUiPassword string `json:"web_ui_password"` - SSLKey string `json:"ssl_key"` - SSLCert string `json:"ssl_cert"` - RSSDownloadRepack string `json:"rss_download_repack_proper_episodes"` - RSSSmartEpisodeFilters string `json:"rss_smart_episode_filters"` - WebUiUseCustomHttpHeaders bool `json:"web_ui_use_custom_http_headers"` - WebUiUseCustomHttpHeadersEnabled bool `json:"web_ui_use_custom_http_headers_enabled"` -} - -type ScanDirs struct{} - -type TorrentCategory struct { - Name string `json:"name"` - SavePath string `json:"savePath"` -} - -type Torrent struct { - ID string `json:"-"` - DebridTorrent *debrid.Torrent `json:"-"` - Debrid string `json:"debrid"` - TorrentPath string `json:"-"` - - AddedOn int64 `json:"added_on,omitempty"` - AmountLeft int64 `json:"amount_left"` - AutoTmm bool `json:"auto_tmm"` - Availability float64 `json:"availability,omitempty"` - Category string `json:"category,omitempty"` - Completed int64 `json:"completed"` - CompletionOn int `json:"completion_on,omitempty"` - ContentPath string `json:"content_path"` - DlLimit int `json:"dl_limit"` - Dlspeed int64 `json:"dlspeed"` - Downloaded int64 `json:"downloaded"` - DownloadedSession int64 `json:"downloaded_session"` - Eta int `json:"eta"` - FlPiecePrio bool `json:"f_l_piece_prio,omitempty"` - ForceStart bool `json:"force_start,omitempty"` - Hash string `json:"hash"` - LastActivity int64 `json:"last_activity,omitempty"` - MagnetUri string `json:"magnet_uri,omitempty"` - MaxRatio int `json:"max_ratio,omitempty"` - MaxSeedingTime int `json:"max_seeding_time,omitempty"` - Name string `json:"name,omitempty"` - NumComplete int `json:"num_complete,omitempty"` - NumIncomplete int `json:"num_incomplete,omitempty"` - NumLeechs int `json:"num_leechs,omitempty"` - NumSeeds int `json:"num_seeds,omitempty"` - Priority int `json:"priority,omitempty"` - Progress float64 `json:"progress"` - Ratio int `json:"ratio,omitempty"` - RatioLimit int `json:"ratio_limit,omitempty"` - SavePath string `json:"save_path"` - SeedingTimeLimit int `json:"seeding_time_limit,omitempty"` - SeenComplete int64 `json:"seen_complete,omitempty"` - SeqDl bool `json:"seq_dl"` - Size int64 `json:"size,omitempty"` - State string `json:"state,omitempty"` - SuperSeeding bool `json:"super_seeding"` - Tags string `json:"tags,omitempty"` - TimeActive int `json:"time_active,omitempty"` - TotalSize int64 `json:"total_size,omitempty"` - Tracker string `json:"tracker,omitempty"` - UpLimit int64 `json:"up_limit,omitempty"` - Uploaded int64 `json:"uploaded,omitempty"` - UploadedSession int64 `json:"uploaded_session,omitempty"` - Upspeed int64 `json:"upspeed,omitempty"` - Source string `json:"source,omitempty"` - - Mu sync.Mutex `json:"-"` -} - -func (t *Torrent) IsReady() bool { - return t.AmountLeft <= 0 && t.TorrentPath != "" -} - -type TorrentProperties struct { - AdditionDate int64 `json:"addition_date,omitempty"` - Comment string `json:"comment,omitempty"` - CompletionDate int64 `json:"completion_date,omitempty"` - CreatedBy string `json:"created_by,omitempty"` - CreationDate int64 `json:"creation_date,omitempty"` - DlLimit int `json:"dl_limit,omitempty"` - DlSpeed int64 `json:"dl_speed,omitempty"` - DlSpeedAvg int `json:"dl_speed_avg,omitempty"` - Eta int `json:"eta,omitempty"` - LastSeen int64 `json:"last_seen,omitempty"` - NbConnections int `json:"nb_connections,omitempty"` - NbConnectionsLimit int `json:"nb_connections_limit,omitempty"` - Peers int `json:"peers,omitempty"` - PeersTotal int `json:"peers_total,omitempty"` - PieceSize int64 `json:"piece_size,omitempty"` - PiecesHave int64 `json:"pieces_have,omitempty"` - PiecesNum int64 `json:"pieces_num,omitempty"` - Reannounce int `json:"reannounce,omitempty"` - SavePath string `json:"save_path,omitempty"` - SeedingTime int `json:"seeding_time,omitempty"` - Seeds int `json:"seeds,omitempty"` - SeedsTotal int `json:"seeds_total,omitempty"` - ShareRatio int `json:"share_ratio,omitempty"` - TimeElapsed int64 `json:"time_elapsed,omitempty"` - TotalDownloaded int64 `json:"total_downloaded,omitempty"` - TotalDownloadedSession int64 `json:"total_downloaded_session,omitempty"` - TotalSize int64 `json:"total_size,omitempty"` - TotalUploaded int64 `json:"total_uploaded,omitempty"` - TotalUploadedSession int64 `json:"total_uploaded_session,omitempty"` - TotalWasted int64 `json:"total_wasted,omitempty"` - UpLimit int `json:"up_limit,omitempty"` - UpSpeed int64 `json:"up_speed,omitempty"` - UpSpeedAvg int `json:"up_speed_avg,omitempty"` -} - -type TorrentFile struct { - Index int `json:"index,omitempty"` - Name string `json:"name,omitempty"` - Size int64 `json:"size,omitempty"` - Progress int `json:"progress,omitempty"` - Priority int `json:"priority,omitempty"` - IsSeed bool `json:"is_seed,omitempty"` - PieceRange []int `json:"piece_range,omitempty"` - Availability float64 `json:"availability,omitempty"` -} - -func NewAppPreferences() *AppPreferences { - preferences := &AppPreferences{ - AddTrackers: "", - AddTrackersEnabled: false, - AltDlLimit: 10240, - AltUpLimit: 10240, - AlternativeWebuiEnabled: false, - AlternativeWebuiPath: "", - AnnounceIp: "", - AnnounceToAllTiers: true, - AnnounceToAllTrackers: false, - AnonymousMode: false, - AsyncIoThreads: 4, - AutoDeleteMode: 0, - AutoTmmEnabled: false, - AutorunEnabled: false, - AutorunProgram: "", - BannedIPs: "", - BittorrentProtocol: 0, - BypassAuthSubnetWhitelist: "", - BypassAuthSubnetWhitelistEnabled: false, - BypassLocalAuth: false, - CategoryChangedTmmEnabled: false, - CheckingMemoryUse: 32, - CreateSubfolderEnabled: true, - CurrentInterfaceAddress: "", - CurrentNetworkInterface: "", - Dht: true, - DiskCache: -1, - DiskCacheTtl: 60, - DlLimit: 0, - DontCountSlowTorrents: false, - DyndnsDomain: "changeme.dyndns.org", - DyndnsEnabled: false, - DyndnsPassword: "", - DyndnsService: 0, - DyndnsUsername: "", - EmbeddedTrackerPort: 9000, - EnableCoalesceReadWrite: true, - EnableEmbeddedTracker: false, - EnableMultiConnectionsFromSameIp: false, - EnableOsCache: true, - EnablePieceExtentAffinity: false, - EnableSuperSeeding: false, - EnableUploadSuggestions: false, - Encryption: 0, - ExportDir: "", - ExportDirFin: "", - FilePoolSize: 40, - IncompleteFilesExt: false, - IpFilterEnabled: false, - IpFilterPath: "", - IpFilterTrackers: false, - LimitLanPeers: true, - LimitTcpOverhead: false, - LimitUtpRate: true, - ListenPort: 31193, - Locale: "en", - Lsd: true, - MailNotificationAuthEnabled: false, - MailNotificationEmail: "", - MailNotificationEnabled: false, - MailNotificationPassword: "", - MailNotificationSender: "qBittorrentNotification@example.com", - MailNotificationSmtp: "smtp.changeme.com", - MailNotificationSslEnabled: false, - MailNotificationUsername: "", - MaxActiveDownloads: 3, - MaxActiveTorrents: 5, - MaxActiveUploads: 3, - MaxConnec: 500, - MaxConnecPerTorrent: 100, - MaxRatio: -1, - MaxRatioAct: 0, - MaxRatioEnabled: false, - MaxSeedingTime: -1, - MaxSeedingTimeEnabled: false, - MaxUploads: -1, - MaxUploadsPerTorrent: -1, - OutgoingPortsMax: 0, - OutgoingPortsMin: 0, - Pex: true, - PreallocateAll: false, - ProxyAuthEnabled: false, - ProxyIp: "0.0.0.0", - ProxyPassword: "", - ProxyPeerConnections: false, - ProxyPort: 8080, - ProxyTorrentsOnly: false, - ProxyType: 0, - ProxyUsername: "", - QueueingEnabled: false, - RandomPort: false, - RecheckCompletedTorrents: false, - ResolvePeerCountries: true, - RssAutoDownloadingEnabled: false, - RssMaxArticlesPerFeed: 50, - RssProcessingEnabled: false, - RssRefreshInterval: 30, - SavePathChangedTmmEnabled: false, - SaveResumeDataInterval: 60, - ScanDirs: ScanDirs{}, - ScheduleFromHour: 8, - ScheduleFromMin: 0, - ScheduleToHour: 20, - ScheduleToMin: 0, - SchedulerDays: 0, - SchedulerEnabled: false, - SendBufferLowWatermark: 10, - SendBufferWatermark: 500, - SendBufferWatermarkFactor: 50, - SlowTorrentDlRateThreshold: 2, - SlowTorrentInactiveTimer: 60, - SlowTorrentUlRateThreshold: 2, - SocketBacklogSize: 30, - StartPausedEnabled: false, - StopTrackerTimeout: 1, - TempPathEnabled: false, - TorrentChangedTmmEnabled: true, - UpLimit: 0, - UploadChokingAlgorithm: 1, - UploadSlotsBehavior: 0, - Upnp: true, - UpnpLeaseDuration: 0, - UseHttps: false, - UtpTcpMixedMode: 0, - WebUiAddress: "*", - WebUiBanDuration: 3600, - WebUiClickjackingProtectionEnabled: true, - WebUiCsrfProtectionEnabled: true, - WebUiDomainList: "*", - WebUiHostHeaderValidationEnabled: true, - WebUiHttpsCertPath: "", - WebUiHttpsKeyPath: "", - WebUiMaxAuthFailCount: 5, - WebUiPort: 8080, - WebUiSecureCookieEnabled: true, - WebUiSessionTimeout: 3600, - WebUiUpnp: false, - - // Fields in the struct but not in the JSON (set to zero values): - WebUiPassword: "", - SSLKey: "", - SSLCert: "", - RSSDownloadRepack: "", - RSSSmartEpisodeFilters: "", - WebUiUseCustomHttpHeaders: false, - WebUiUseCustomHttpHeadersEnabled: false, - } - return preferences -} diff --git a/pkg/qbit/shared/utils.go b/pkg/qbit/shared/utils.go deleted file mode 100644 index 0a864e1..0000000 --- a/pkg/qbit/shared/utils.go +++ /dev/null @@ -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 - } - } - } -} diff --git a/pkg/qbit/shared/worker.go b/pkg/qbit/shared/worker.go deleted file mode 100644 index c2b710f..0000000 --- a/pkg/qbit/shared/worker.go +++ /dev/null @@ -1,37 +0,0 @@ -package shared - -import ( - "context" - "time" -) - -func (q *QBit) StartWorker(ctx context.Context) { - q.logger.Info().Msg("Qbit Worker started") - q.StartRefreshWorker(ctx) -} - -func (q *QBit) StartRefreshWorker(ctx context.Context) { - refreshCtx := context.WithValue(ctx, "worker", "refresh") - refreshTicker := time.NewTicker(time.Duration(q.RefreshInterval) * time.Second) - for { - select { - case <-refreshCtx.Done(): - q.logger.Info().Msg("Qbit Refresh Worker stopped") - return - case <-refreshTicker.C: - torrents := q.Storage.GetAll("", "", nil) - if len(torrents) > 0 { - q.RefreshArrs() - } - } - } -} - -func (q *QBit) RefreshArrs() { - for _, arr := range q.Arrs.GetAll() { - err := arr.Refresh() - if err != nil { - return - } - } -} diff --git a/pkg/qbit/torrent.go b/pkg/qbit/torrent.go index 9650378..4f8c1d9 100644 --- a/pkg/qbit/torrent.go +++ b/pkg/qbit/torrent.go @@ -90,14 +90,14 @@ func (q *QBit) ProcessFiles(torrent *Torrent, debridTorrent *debrid.Torrent, arr torrent = q.UpdateTorrentMin(torrent, debridTorrent) } var ( - torrentPath string - err error + torrentSymlinkPath string + err error ) debridTorrent.Arr = arr if isSymlink { - torrentPath, err = q.ProcessSymlink(torrent) + torrentSymlinkPath, err = q.ProcessSymlink(torrent) // /mnt/symlinks/{category}/MyTVShow/ } else { - torrentPath, err = q.ProcessManualFile(torrent) + torrentSymlinkPath, err = q.ProcessManualFile(torrent) } if err != nil { q.MarkAsFailed(torrent) @@ -105,7 +105,7 @@ func (q *QBit) ProcessFiles(torrent *Torrent, debridTorrent *debrid.Torrent, arr q.logger.Info().Msgf("Error: %v", err) return } - torrent.TorrentPath = filepath.Base(torrentPath) + torrent.TorrentPath = torrentSymlinkPath q.UpdateTorrent(torrent, debridTorrent) _ = arr.Refresh() } @@ -161,7 +161,6 @@ func (q *QBit) UpdateTorrentMin(t *Torrent, debridTorrent *debrid.Torrent) *Torr func (q *QBit) UpdateTorrent(t *Torrent, debridTorrent *debrid.Torrent) *Torrent { _db := service.GetDebrid().GetByName(debridTorrent.Debrid) - rcLoneMount := _db.GetMountPath() if debridTorrent == nil && t.ID != "" { debridTorrent, _ = _db.GetTorrent(t.ID) } @@ -172,15 +171,8 @@ func (q *QBit) UpdateTorrent(t *Torrent, debridTorrent *debrid.Torrent) *Torrent if debridTorrent.Status != "downloaded" { debridTorrent, _ = _db.GetTorrent(t.ID) } - - if t.TorrentPath == "" { - tPath, _ := debridTorrent.GetMountFolder(rcLoneMount) - t.TorrentPath = filepath.Base(tPath) - } - savePath := filepath.Join(q.DownloadFolder, t.Category) + string(os.PathSeparator) - torrentPath := filepath.Join(savePath, t.TorrentPath) + string(os.PathSeparator) t = q.UpdateTorrentMin(t, debridTorrent) - t.ContentPath = torrentPath + t.ContentPath = t.TorrentPath + string(os.PathSeparator) if t.IsReady() { t.State = "pausedUP" diff --git a/pkg/repair/repair.go b/pkg/repair/repair.go index 9be58da..a37d0e7 100644 --- a/pkg/repair/repair.go +++ b/pkg/repair/repair.go @@ -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, diff --git a/pkg/service/service.go b/pkg/service/service.go index acc36bd..7eb5315 100644 --- a/pkg/service/service.go +++ b/pkg/service/service.go @@ -3,17 +3,15 @@ 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 + Repair *repair.Repair + Arr *arr.Storage + Debrid *engine.Engine } var ( @@ -26,10 +24,9 @@ func New() *Service { arrs := arr.NewStorage() deb := debrid.New() instance = &Service{ - Repair: repair.New(deb, arrs), - Arr: arrs, - Debrid: deb, - DebridCache: cache.NewManager(deb), + Repair: repair.New(deb, arrs), + Arr: arrs, + Debrid: deb, } }) return instance diff --git a/pkg/web/routes.go b/pkg/web/routes.go index df6a141..67889bc 100644 --- a/pkg/web/routes.go +++ b/pkg/web/routes.go @@ -2,17 +2,12 @@ package web import ( "github.com/go-chi/chi/v5" - "github.com/go-chi/chi/v5/middleware" "net/http" ) func (ui *Handler) Routes() http.Handler { r := chi.NewRouter() - if ui.logger.GetLevel().String() == "debug" { - r.Use(middleware.Logger) - } - r.Get("/login", ui.LoginHandler) r.Post("/login", ui.LoginHandler) r.Get("/setup", ui.SetupHandler) diff --git a/pkg/web/ui.go b/pkg/web/ui.go index 740de4d..43bb34a 100644 --- a/pkg/web/ui.go +++ b/pkg/web/ui.go @@ -100,6 +100,11 @@ func (ui *Handler) authMiddleware(next http.Handler) http.Handler { return } + if !cfg.UseAuth { + next.ServeHTTP(w, r) + return + } + // Skip auth check for setup page if r.URL.Path == "/setup" { next.ServeHTTP(w, r)