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" }} -
| - - | -Name | -Size | -Progress | -Speed | -Category | -Debrid | -State | -Actions | -
|---|