From 14341d30bc9de24bcfa8453a1aed8f18f209bf98 Mon Sep 17 00:00:00 2001 From: Mukhtar Akere Date: Thu, 13 Feb 2025 02:08:18 +0100 Subject: [PATCH 1/4] More cleanup, more refractor, more energy, more passion, more footwork --- .air.toml | 4 +- Dockerfile | 14 +- cmd/main.go | 53 ++- go.mod | 6 +- go.sum | 16 +- internal/config/config.go | 10 +- internal/logger/logger.go | 12 +- internal/request/request.go | 20 +- pkg/debrid/alldebrid.go | 271 ------------ pkg/debrid/alldebrid/alldebrid.go | 306 +++++++++++++ pkg/debrid/alldebrid/types.go | 75 ++++ pkg/debrid/cache/cache.go | 360 ++++++++++++++++ pkg/debrid/debrid.go | 143 +------ pkg/debrid/debrid_link.go | 280 ------------ pkg/debrid/debrid_link/debrid_link.go | 298 +++++++++++++ .../debrid_link.go => debrid_link/types.go} | 10 +- pkg/debrid/engine.go | 1 + pkg/debrid/engine/engine.go | 26 ++ pkg/debrid/engine/service.go | 21 + pkg/debrid/realdebrid.go | 303 ------------- pkg/debrid/realdebrid/realdebrid.go | 402 ++++++++++++++++++ .../realdebrid.go => realdebrid/types.go} | 27 +- pkg/debrid/service.go | 22 - pkg/debrid/{ => torbox}/torbox.go | 211 +++++---- .../{types/torbox.go => torbox/types.go} | 12 +- pkg/debrid/torrent.go | 122 ------ pkg/debrid/torrent/torrent.go | 135 ++++++ pkg/debrid/types/alldebrid.go | 75 ---- pkg/downloader/grab.go | 2 + pkg/downloaders/fasthttp.go | 59 --- pkg/downloaders/grab.go | 57 --- pkg/downloaders/http.go | 44 -- pkg/proxy/proxy.go | 12 +- pkg/qbit/{shared => }/downloader.go | 66 ++- pkg/qbit/{server/qbit_handlers.go => http.go} | 124 +++--- pkg/qbit/{server => }/import.go | 14 +- pkg/qbit/main.go | 18 - pkg/qbit/misc.go | 50 +++ pkg/qbit/{shared => }/qbit.go | 18 +- pkg/qbit/routes.go | 47 ++ pkg/qbit/server/qbit_routes.go | 48 --- pkg/qbit/server/ui_auth_handlers.go | 50 --- pkg/qbit/server/ui_routes.go | 33 -- pkg/qbit/shared/utils.go | 26 -- pkg/qbit/{shared => }/storage.go | 2 +- pkg/qbit/{shared => }/torrent.go | 57 +-- pkg/qbit/{shared => }/types.go | 12 +- pkg/qbit/{shared => }/worker.go | 6 +- pkg/repair/repair.go | 10 +- pkg/{qbit => }/server/server.go | 53 +-- pkg/service/service.go | 59 +++ pkg/web/routes.go | 34 ++ pkg/{qbit/server/ui_handlers.go => web/ui.go} | 194 +++++---- .../server/templates => web/web}/config.html | 0 .../templates => web/web}/download.html | 0 .../server/templates => web/web}/index.html | 0 .../server/templates => web/web}/layout.html | 0 .../server/templates => web/web}/login.html | 0 .../server/templates => web/web}/repair.html | 0 .../server/templates => web/web}/setup.html | 0 60 files changed, 2369 insertions(+), 1961 deletions(-) delete mode 100644 pkg/debrid/alldebrid.go create mode 100644 pkg/debrid/alldebrid/alldebrid.go create mode 100644 pkg/debrid/alldebrid/types.go create mode 100644 pkg/debrid/cache/cache.go delete mode 100644 pkg/debrid/debrid_link.go create mode 100644 pkg/debrid/debrid_link/debrid_link.go rename pkg/debrid/{types/debrid_link.go => debrid_link/types.go} (80%) create mode 100644 pkg/debrid/engine.go create mode 100644 pkg/debrid/engine/engine.go create mode 100644 pkg/debrid/engine/service.go delete mode 100644 pkg/debrid/realdebrid.go create mode 100644 pkg/debrid/realdebrid/realdebrid.go rename pkg/debrid/{types/realdebrid.go => realdebrid/types.go} (80%) delete mode 100644 pkg/debrid/service.go rename pkg/debrid/{ => torbox}/torbox.go (51%) rename pkg/debrid/{types/torbox.go => torbox/types.go} (89%) delete mode 100644 pkg/debrid/torrent.go create mode 100644 pkg/debrid/torrent/torrent.go delete mode 100644 pkg/debrid/types/alldebrid.go create mode 100644 pkg/downloader/grab.go delete mode 100644 pkg/downloaders/fasthttp.go delete mode 100644 pkg/downloaders/grab.go delete mode 100644 pkg/downloaders/http.go rename pkg/qbit/{shared => }/downloader.go (78%) rename pkg/qbit/{server/qbit_handlers.go => http.go} (67%) rename pkg/qbit/{server => }/import.go (84%) delete mode 100644 pkg/qbit/main.go create mode 100644 pkg/qbit/misc.go rename pkg/qbit/{shared => }/qbit.go (63%) create mode 100644 pkg/qbit/routes.go delete mode 100644 pkg/qbit/server/qbit_routes.go delete mode 100644 pkg/qbit/server/ui_auth_handlers.go delete mode 100644 pkg/qbit/server/ui_routes.go delete mode 100644 pkg/qbit/shared/utils.go rename pkg/qbit/{shared => }/storage.go (99%) rename pkg/qbit/{shared => }/torrent.go (86%) rename pkg/qbit/{shared => }/types.go (98%) rename pkg/qbit/{shared => }/worker.go (83%) rename pkg/{qbit => }/server/server.go (65%) create mode 100644 pkg/service/service.go create mode 100644 pkg/web/routes.go rename pkg/{qbit/server/ui_handlers.go => web/ui.go} (65%) rename pkg/{qbit/server/templates => web/web}/config.html (100%) rename pkg/{qbit/server/templates => web/web}/download.html (100%) rename pkg/{qbit/server/templates => web/web}/index.html (100%) rename pkg/{qbit/server/templates => web/web}/layout.html (100%) rename pkg/{qbit/server/templates => web/web}/login.html (100%) rename pkg/{qbit/server/templates => web/web}/repair.html (100%) rename pkg/{qbit/server/templates => web/web}/setup.html (100%) 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..fb462c6 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 @@ -47,8 +47,8 @@ LABEL org.opencontainers.image.authors = "sirrobot01" LABEL org.opencontainers.image.documentation = "https://github.com/sirrobot01/debrid-blackhole/blob/main/README.md" # Copy binaries -COPY --from=builder --chown=nonroot:nonroot /blackhole /blackhole -COPY --from=builder --chown=nonroot:nonroot /healthcheck /healthcheck +COPY --from=builder --chown=nonroot:nonroot /blackhole /usr/bin/blackhole +COPY --from=builder --chown=nonroot:nonroot /healthcheck /usr/bin/healthcheck # Copy pre-made directory structure COPY --from=dirsetup --chown=nonroot:nonroot /data /data @@ -59,6 +59,6 @@ EXPOSE 8181 8282 VOLUME ["/data", "/app"] USER nonroot:nonroot -HEALTHCHECK CMD ["/healthcheck"] +HEALTHCHECK CMD ["/usr/bin/healthcheck"] -CMD ["/blackhole", "--config", "/data"] \ No newline at end of file +CMD ["/usr/bin/blackhole", "--config", "/data/config.json"] \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go index e54251d..fe7b3b8 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -4,57 +4,68 @@ import ( "context" "github.com/sirrobot01/debrid-blackhole/internal/config" "github.com/sirrobot01/debrid-blackhole/internal/logger" - "github.com/sirrobot01/debrid-blackhole/pkg/arr" - "github.com/sirrobot01/debrid-blackhole/pkg/debrid" "github.com/sirrobot01/debrid-blackhole/pkg/proxy" "github.com/sirrobot01/debrid-blackhole/pkg/qbit" - "github.com/sirrobot01/debrid-blackhole/pkg/repair" + "github.com/sirrobot01/debrid-blackhole/pkg/server" + "github.com/sirrobot01/debrid-blackhole/pkg/service" "github.com/sirrobot01/debrid-blackhole/pkg/version" + "github.com/sirrobot01/debrid-blackhole/pkg/web" "log" "sync" ) func Start(ctx context.Context) error { cfg := config.GetConfig() + var wg sync.WaitGroup + errChan := make(chan error) _log := logger.GetLogger(cfg.LogLevel) + _log.Info().Msgf("Version: %s", version.GetInfo().String()) _log.Debug().Msgf("Config Loaded: %s", cfg.JsonFile()) _log.Debug().Msgf("Default Log Level: %s", cfg.LogLevel) - _log.Info().Msgf("Version: %s", version.GetInfo().String()) + svc := service.New() + _qbit := qbit.New() + _proxy := proxy.NewProxy() + srv := server.New() + webRoutes := web.New(_qbit).Routes() + qbitRoutes := _qbit.Routes() - deb := debrid.NewDebrid() - arrs := arr.NewStorage() - _repair := repair.NewRepair(deb.Get(), arrs) - - var wg sync.WaitGroup - errChan := make(chan error, 2) + // Register routes + srv.Mount("/", webRoutes) + srv.Mount("/api/v2", qbitRoutes) if cfg.Proxy.Enabled { wg.Add(1) go func() { defer wg.Done() - if err := proxy.NewProxy(deb).Start(ctx); err != nil { - errChan <- err - } - }() - } - if cfg.QBitTorrent.Port != "" { - wg.Add(1) - go func() { - defer wg.Done() - if err := qbit.Start(ctx, deb, arrs, _repair); err != nil { + if err := _proxy.Start(ctx); err != nil { errChan <- err } }() } + wg.Add(1) + go func() { + defer wg.Done() + if err := srv.Start(ctx); err != nil { + errChan <- err + } + + }() + + wg.Add(1) + go func() { + defer wg.Done() + _qbit.StartWorker(ctx) + }() + if cfg.Repair.Enabled { wg.Add(1) go func() { defer wg.Done() - if err := _repair.Start(ctx); err != nil { + if err := svc.Repair.Start(ctx); err != nil { log.Printf("Error during repair: %v", err) } }() 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..3464958 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 @@ -211,7 +211,11 @@ func GetConfig() *Config { once.Do(func() { instance = &Config{} // Initialize instance first if err := instance.loadConfig(); err != nil { - panic(err) + _, err := fmt.Fprintf(os.Stderr, "Configuration Error: %v\n", err) + if err != nil { + return + } + os.Exit(1) } }) return instance diff --git a/internal/logger/logger.go b/internal/logger/logger.go index 03d2ff5..77cb840 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,11 +17,8 @@ var ( ) func GetLogPath() string { - logsDir := os.Getenv("LOG_PATH") - if logsDir == "" { - // Create the logs directory if it doesn't exist - logsDir = "logs" - } + cfg := config.GetConfig() + logsDir := filepath.Join(cfg.Path, "logs") if err := os.MkdirAll(logsDir, 0755); err != nil { panic(fmt.Sprintf("Failed to create logs directory: %v", err)) @@ -33,7 +31,7 @@ func NewLogger(prefix string, level string, output *os.File) zerolog.Logger { rotatingLogFile := &lumberjack.Logger{ Filename: GetLogPath(), - MaxSize: 10, + MaxSize: 2, MaxBackups: 2, MaxAge: 28, Compress: true, @@ -87,7 +85,7 @@ func NewLogger(prefix string, level string, output *os.File) zerolog.Logger { func GetLogger(level string) zerolog.Logger { once.Do(func() { - logger = NewLogger("Decypharr", level, os.Stdout) + logger = NewLogger("decypharr", level, os.Stdout) }) return logger } diff --git a/internal/request/request.go b/internal/request/request.go index fd1cb6a..a310aa2 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 == http.StatusOK || res.StatusCode == http.StatusCreated + if !statusOk { + // Add status code error to the body + b = append(b, []byte(fmt.Sprintf("\nstatus code: %d", res.StatusCode))...) + return nil, fmt.Errorf(string(b)) + } + return b, nil } 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 new file mode 100644 index 0000000..b7b7e12 --- /dev/null +++ b/pkg/debrid/alldebrid/alldebrid.go @@ -0,0 +1,306 @@ +package alldebrid + +import ( + "encoding/json" + "fmt" + "github.com/rs/zerolog" + "github.com/sirrobot01/debrid-blackhole/common" + "github.com/sirrobot01/debrid-blackhole/internal/config" + "github.com/sirrobot01/debrid-blackhole/internal/logger" + "github.com/sirrobot01/debrid-blackhole/internal/request" + "github.com/sirrobot01/debrid-blackhole/pkg/debrid/torrent" + + "net/http" + gourl "net/url" + "os" + "path/filepath" + "strconv" +) + +type AllDebrid struct { + Name string + Host string `json:"host"` + APIKey string + DownloadUncached bool + client *request.RLHTTPClient + cache *common.Cache + MountPath string + logger zerolog.Logger + CheckCached bool +} + +func (ad *AllDebrid) GetMountPath() string { + return ad.MountPath +} + +func (ad *AllDebrid) GetName() string { + return ad.Name +} + +func (ad *AllDebrid) GetLogger() zerolog.Logger { + return ad.logger +} + +func (ad *AllDebrid) IsAvailable(infohashes []string) map[string]bool { + // Check if the infohashes are available in the local cache + hashes, result := torrent.GetLocalCache(infohashes, ad.cache) + + if len(hashes) == 0 { + // Either all the infohashes are locally cached or none are + ad.cache.AddMultiple(result) + return result + } + + // Divide hashes into groups of 100 + // AllDebrid does not support checking cached infohashes + return result +} + +func (ad *AllDebrid) SubmitMagnet(torrent *torrent.Torrent) (*torrent.Torrent, error) { + url := fmt.Sprintf("%s/magnet/upload", ad.Host) + query := gourl.Values{} + query.Add("magnets[]", torrent.Magnet.Link) + url += "?" + query.Encode() + req, _ := http.NewRequest(http.MethodGet, url, nil) + resp, err := ad.client.MakeRequest(req) + if err != nil { + return nil, err + } + var data UploadMagnetResponse + err = json.Unmarshal(resp, &data) + if err != nil { + return nil, err + } + magnets := data.Data.Magnets + if len(magnets) == 0 { + return nil, fmt.Errorf("error adding torrent") + } + magnet := magnets[0] + torrentId := strconv.Itoa(magnet.ID) + ad.logger.Info().Msgf("Torrent: %s added with id: %s", torrent.Name, torrentId) + torrent.Id = torrentId + + return torrent, nil +} + +func getAlldebridStatus(statusCode int) string { + switch { + case statusCode == 4: + return "downloaded" + case statusCode >= 0 && statusCode <= 3: + return "downloading" + default: + return "error" + } +} + +func flattenFiles(files []MagnetFile, parentPath string, index *int) []torrent.File { + result := make([]torrent.File, 0) + + cfg := config.GetConfig() + + for _, f := range files { + currentPath := f.Name + if parentPath != "" { + currentPath = filepath.Join(parentPath, f.Name) + } + + if f.Elements != nil { + // This is a folder, recurse into it + result = append(result, flattenFiles(f.Elements, currentPath, index)...) + } else { + // This is a file + fileName := filepath.Base(f.Name) + if common.RegexMatch(common.SAMPLEMATCH, fileName) { + continue + } + if !cfg.IsAllowedFile(fileName) { + continue + } + + if !cfg.IsSizeAllowed(f.Size) { + continue + } + + *index++ + file := torrent.File{ + Id: strconv.Itoa(*index), + Name: fileName, + Size: f.Size, + Path: currentPath, + } + result = append(result, file) + } + } + + return result +} + +func (ad *AllDebrid) GetTorrent(id string) (*torrent.Torrent, error) { + t := &torrent.Torrent{} + url := fmt.Sprintf("%s/magnet/status?id=%s", ad.Host, id) + req, _ := http.NewRequest(http.MethodGet, url, nil) + resp, err := ad.client.MakeRequest(req) + if err != nil { + return t, err + } + var res TorrentInfoResponse + err = json.Unmarshal(resp, &res) + if err != nil { + ad.logger.Info().Msgf("Error unmarshalling torrent info: %s", err) + return t, err + } + data := res.Data.Magnets + status := getAlldebridStatus(data.StatusCode) + name := data.Filename + t.Id = id + t.Name = name + t.Status = status + t.Filename = name + t.OriginalFilename = name + t.Folder = name + t.MountPath = ad.MountPath + t.Debrid = ad.Name + t.DownloadLinks = make(map[string]torrent.DownloadLinks) + if status == "downloaded" { + t.Bytes = data.Size + + t.Progress = float64((data.Downloaded / data.Size) * 100) + t.Speed = data.DownloadSpeed + t.Seeders = data.Seeders + index := -1 + files := flattenFiles(data.Files, "", &index) + parentFolder := data.Filename + if data.NbLinks == 1 { + // All debrid doesn't return the parent folder for single file torrents + parentFolder = "" + } + t.OriginalFilename = parentFolder + t.Files = files + } + return t, nil +} + +func (ad *AllDebrid) CheckStatus(torrent *torrent.Torrent, isSymlink bool) (*torrent.Torrent, error) { + for { + tb, err := ad.GetTorrent(torrent.Id) + + torrent = tb + + if err != nil || tb == nil { + return tb, err + } + status := torrent.Status + if status == "downloaded" { + ad.logger.Info().Msgf("Torrent: %s downloaded", torrent.Name) + if !isSymlink { + err = ad.GetDownloadLinks(torrent) + if err != nil { + return torrent, err + } + } + break + } else if status == "downloading" { + if !ad.DownloadUncached { + go ad.DeleteTorrent(torrent) + return torrent, fmt.Errorf("torrent: %s not cached", torrent.Name) + } + // Break out of the loop if the torrent is downloading. + // This is necessary to prevent infinite loop since we moved to sync downloading and async processing + break + } else { + return torrent, fmt.Errorf("torrent: %s has error", torrent.Name) + } + + } + return torrent, nil +} + +func (ad *AllDebrid) DeleteTorrent(torrent *torrent.Torrent) { + url := fmt.Sprintf("%s/magnet/delete?id=%s", ad.Host, torrent.Id) + req, _ := http.NewRequest(http.MethodGet, url, nil) + _, err := ad.client.MakeRequest(req) + if err == nil { + ad.logger.Info().Msgf("Torrent: %s deleted", torrent.Name) + } else { + ad.logger.Info().Msgf("Error deleting torrent: %s", err) + } +} + +func (ad *AllDebrid) GetDownloadLinks(t *torrent.Torrent) error { + downloadLinks := make(map[string]torrent.DownloadLinks) + for _, file := range t.Files { + url := fmt.Sprintf("%s/link/unlock", ad.Host) + query := gourl.Values{} + query.Add("link", file.Link) + url += "?" + query.Encode() + req, _ := http.NewRequest(http.MethodGet, url, nil) + resp, err := ad.client.MakeRequest(req) + if err != nil { + return err + } + var data DownloadLink + if err = json.Unmarshal(resp, &data); err != nil { + return err + } + link := data.Data.Link + + dl := torrent.DownloadLinks{ + Link: file.Link, + Filename: data.Data.Filename, + DownloadLink: link, + } + downloadLinks[file.Id] = dl + } + t.DownloadLinks = downloadLinks + return nil +} + +func (ad *AllDebrid) GetDownloadLink(t *torrent.Torrent, file *torrent.File) *torrent.DownloadLinks { + url := fmt.Sprintf("%s/link/unlock", ad.Host) + query := gourl.Values{} + query.Add("link", file.Link) + url += "?" + query.Encode() + req, _ := http.NewRequest(http.MethodGet, url, nil) + resp, err := ad.client.MakeRequest(req) + if err != nil { + return nil + } + var data DownloadLink + if err = json.Unmarshal(resp, &data); err != nil { + return nil + } + link := data.Data.Link + return &torrent.DownloadLinks{ + DownloadLink: link, + Link: file.Link, + Filename: data.Data.Filename, + } +} + +func (ad *AllDebrid) GetCheckCached() bool { + return ad.CheckCached +} + +func (ad *AllDebrid) GetTorrents() ([]*torrent.Torrent, error) { + return nil, fmt.Errorf("not implemented") +} + +func New(dc config.Debrid, cache *common.Cache) *AllDebrid { + rl := request.ParseRateLimit(dc.RateLimit) + headers := map[string]string{ + "Authorization": fmt.Sprintf("Bearer %s", dc.APIKey), + } + client := request.NewRLHTTPClient(rl, headers) + return &AllDebrid{ + Name: "alldebrid", + Host: dc.Host, + APIKey: dc.APIKey, + DownloadUncached: dc.DownloadUncached, + client: client, + cache: cache, + MountPath: dc.Folder, + logger: logger.NewLogger(dc.Name, config.GetConfig().LogLevel, os.Stdout), + CheckCached: dc.CheckCached, + } +} diff --git a/pkg/debrid/alldebrid/types.go b/pkg/debrid/alldebrid/types.go new file mode 100644 index 0000000..aa489c0 --- /dev/null +++ b/pkg/debrid/alldebrid/types.go @@ -0,0 +1,75 @@ +package alldebrid + +type errorResponse struct { + Code string `json:"code"` + Message string `json:"message"` +} + +type MagnetFile struct { + Name string `json:"n"` + Size int64 `json:"s"` + Link string `json:"l"` + Elements []MagnetFile `json:"e"` +} +type magnetInfo struct { + Id int `json:"id"` + Filename string `json:"filename"` + Size int64 `json:"size"` + Hash string `json:"hash"` + Status string `json:"status"` + StatusCode int `json:"statusCode"` + UploadDate int `json:"uploadDate"` + Downloaded int64 `json:"downloaded"` + Uploaded int64 `json:"uploaded"` + DownloadSpeed int64 `json:"downloadSpeed"` + UploadSpeed int64 `json:"uploadSpeed"` + Seeders int `json:"seeders"` + CompletionDate int `json:"completionDate"` + Type string `json:"type"` + Notified bool `json:"notified"` + Version int `json:"version"` + NbLinks int `json:"nbLinks"` + Files []MagnetFile `json:"files"` +} + +type TorrentInfoResponse struct { + Status string `json:"status"` + Data struct { + Magnets magnetInfo `json:"magnets"` + } `json:"data"` + Error *errorResponse `json:"error"` +} + +type UploadMagnetResponse struct { + Status string `json:"status"` + Data struct { + Magnets []struct { + Magnet string `json:"magnet"` + Hash string `json:"hash"` + Name string `json:"name"` + FilenameOriginal string `json:"filename_original"` + Size int64 `json:"size"` + Ready bool `json:"ready"` + ID int `json:"id"` + } `json:"magnets"` + } + Error *errorResponse `json:"error"` +} + +type DownloadLink struct { + Status string `json:"status"` + Data struct { + Link string `json:"link"` + Host string `json:"host"` + Filename string `json:"filename"` + Streaming []interface{} `json:"streaming"` + Paws bool `json:"paws"` + Filesize int `json:"filesize"` + Id string `json:"id"` + Path []struct { + Name string `json:"n"` + Size int `json:"s"` + } `json:"path"` + } `json:"data"` + Error *errorResponse `json:"error"` +} diff --git a/pkg/debrid/cache/cache.go b/pkg/debrid/cache/cache.go new file mode 100644 index 0000000..348a421 --- /dev/null +++ b/pkg/debrid/cache/cache.go @@ -0,0 +1,360 @@ +package cache + +import ( + "bufio" + "encoding/json" + "fmt" + "github.com/rs/zerolog" + "github.com/sirrobot01/debrid-blackhole/internal/logger" + "os" + "path/filepath" + "runtime" + "sync" + "time" + + "github.com/sirrobot01/debrid-blackhole/internal/config" + "github.com/sirrobot01/debrid-blackhole/pkg/debrid/engine" + "github.com/sirrobot01/debrid-blackhole/pkg/debrid/torrent" +) + +type DownloadLinkCache struct { + Link string `json:"download_link"` +} + +type CachedTorrent struct { + *torrent.Torrent + LastRead time.Time `json:"last_read"` + IsComplete bool `json:"is_complete"` + DownloadLinks map[string]DownloadLinkCache `json:"download_links"` +} + +var ( + _logInstance zerolog.Logger + once sync.Once +) + +func getLogger() zerolog.Logger { + once.Do(func() { + _logInstance = logger.NewLogger("cache", "info", os.Stdout) + }) + return _logInstance +} + +type Cache struct { + dir string + client engine.Service + torrents *sync.Map // key: torrent.Id, value: *CachedTorrent + torrentsNames *sync.Map // key: torrent.Name, value: torrent.Id + LastUpdated time.Time `json:"last_updated"` +} + +type Manager struct { + caches map[string]*Cache +} + +func NewManager(debridService *engine.Engine) *Manager { + cfg := config.GetConfig() + cm := &Manager{ + caches: make(map[string]*Cache), + } + for _, debrid := range debridService.GetDebrids() { + c := New(debrid, cfg.Path) + cm.caches[debrid.GetName()] = c + } + return cm +} + +func (m *Manager) GetCaches() map[string]*Cache { + return m.caches +} + +func (m *Manager) GetCache(debridName string) *Cache { + return m.caches[debridName] +} + +func New(debridService engine.Service, basePath string) *Cache { + return &Cache{ + dir: filepath.Join(basePath, "cache", debridService.GetName(), "torrents"), + torrents: &sync.Map{}, + torrentsNames: &sync.Map{}, + client: debridService, + } +} + +func (c *Cache) Start() error { + _logger := getLogger() + _logger.Info().Msg("Starting cache for: " + c.client.GetName()) + if err := c.Load(); err != nil { + return fmt.Errorf("failed to load cache: %v", err) + } + if err := c.Sync(); err != nil { + return fmt.Errorf("failed to sync cache: %v", err) + } + return nil +} + +func (c *Cache) Load() error { + _logger := getLogger() + + if err := os.MkdirAll(c.dir, 0755); err != nil { + return fmt.Errorf("failed to create cache directory: %w", err) + } + + files, err := os.ReadDir(c.dir) + if err != nil { + return fmt.Errorf("failed to read cache directory: %w", err) + } + + for _, file := range files { + if file.IsDir() || filepath.Ext(file.Name()) != ".json" { + continue + } + + filePath := filepath.Join(c.dir, file.Name()) + data, err := os.ReadFile(filePath) + if err != nil { + _logger.Debug().Err(err).Msgf("Failed to read file: %s", filePath) + continue + } + + var ct CachedTorrent + if err := json.Unmarshal(data, &ct); err != nil { + _logger.Debug().Err(err).Msgf("Failed to unmarshal file: %s", filePath) + continue + } + if len(ct.Files) > 0 { + c.torrents.Store(ct.Torrent.Id, &ct) + c.torrentsNames.Store(ct.Torrent.Name, ct.Torrent.Id) + } + } + + return nil +} + +func (c *Cache) GetTorrent(id string) *CachedTorrent { + if value, ok := c.torrents.Load(id); ok { + return value.(*CachedTorrent) + } + return nil +} + +func (c *Cache) GetTorrentByName(name string) *CachedTorrent { + if id, ok := c.torrentsNames.Load(name); ok { + return c.GetTorrent(id.(string)) + } + return nil +} + +func (c *Cache) SaveTorrent(ct *CachedTorrent) error { + data, err := json.MarshalIndent(ct, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal torrent: %w", err) + } + + fileName := ct.Torrent.Id + ".json" + filePath := filepath.Join(c.dir, fileName) + tmpFile := filePath + ".tmp" + + f, err := os.Create(tmpFile) + if err != nil { + return fmt.Errorf("failed to create temp file: %w", err) + } + defer f.Close() + + w := bufio.NewWriter(f) + if _, err := w.Write(data); err != nil { + return fmt.Errorf("failed to write data: %w", err) + } + + if err := w.Flush(); err != nil { + return fmt.Errorf("failed to flush data: %w", err) + } + + return os.Rename(tmpFile, filePath) +} + +func (c *Cache) SaveAll() error { + const batchSize = 100 + var wg sync.WaitGroup + _logger := getLogger() + + tasks := make(chan *CachedTorrent, batchSize) + + for i := 0; i < runtime.NumCPU(); i++ { + wg.Add(1) + go func() { + defer wg.Done() + for ct := range tasks { + if err := c.SaveTorrent(ct); err != nil { + _logger.Error().Err(err).Msg("failed to save torrent") + } + } + }() + } + + c.torrents.Range(func(_, value interface{}) bool { + tasks <- value.(*CachedTorrent) + return true + }) + + close(tasks) + wg.Wait() + c.LastUpdated = time.Now() + return nil +} + +func (c *Cache) Sync() error { + _logger := getLogger() + torrents, err := c.client.GetTorrents() + if err != nil { + return fmt.Errorf("failed to sync torrents: %v", err) + } + + workers := runtime.NumCPU() * 200 + workChan := make(chan *torrent.Torrent, len(torrents)) + errChan := make(chan error, len(torrents)) + + var wg sync.WaitGroup + + for i := 0; i < workers; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for t := range workChan { + if err := c.processTorrent(t); err != nil { + errChan <- err + } + } + }() + } + + for _, t := range torrents { + workChan <- t + } + close(workChan) + + wg.Wait() + close(errChan) + + for err := range errChan { + _logger.Error().Err(err).Msg("sync error") + } + + _logger.Info().Msgf("Synced %d torrents", len(torrents)) + return nil +} + +func (c *Cache) processTorrent(t *torrent.Torrent) error { + if existing, ok := c.torrents.Load(t.Id); ok { + ct := existing.(*CachedTorrent) + if ct.IsComplete { + return nil + } + } + c.AddTorrent(t) + return nil +} + +func (c *Cache) AddTorrent(t *torrent.Torrent) { + _logger := getLogger() + + if len(t.Files) == 0 { + tNew, err := c.client.GetTorrent(t.Id) + _logger.Debug().Msgf("Getting torrent files for %s", t.Id) + if err != nil { + _logger.Debug().Msgf("Failed to get torrent files for %s: %v", t.Id, err) + return + } + t = tNew + } + + if len(t.Files) == 0 { + _logger.Debug().Msgf("No files found for %s", t.Id) + return + } + + ct := &CachedTorrent{ + Torrent: t, + LastRead: time.Now(), + IsComplete: len(t.Files) > 0, + DownloadLinks: make(map[string]DownloadLinkCache), + } + + c.torrents.Store(t.Id, ct) + c.torrentsNames.Store(t.Name, t.Id) + + go func() { + if err := c.SaveTorrent(ct); err != nil { + _logger.Debug().Err(err).Msgf("Failed to save torrent %s", t.Id) + } + }() +} + +func (c *Cache) RefreshTorrent(torrentId string) *CachedTorrent { + _logger := getLogger() + + t, err := c.client.GetTorrent(torrentId) + if err != nil { + _logger.Debug().Msgf("Failed to get torrent files for %s: %v", torrentId, err) + return nil + } + if len(t.Files) == 0 { + return nil + } + + ct := &CachedTorrent{ + Torrent: t, + LastRead: time.Now(), + IsComplete: len(t.Files) > 0, + DownloadLinks: make(map[string]DownloadLinkCache), + } + + c.torrents.Store(t.Id, ct) + c.torrentsNames.Store(t.Name, t.Id) + + go func() { + if err := c.SaveTorrent(ct); err != nil { + _logger.Debug().Err(err).Msgf("Failed to save torrent %s", t.Id) + } + }() + + return ct +} + +func (c *Cache) GetFileDownloadLink(t *CachedTorrent, file *torrent.File) (string, error) { + _logger := getLogger() + + if linkCache, ok := t.DownloadLinks[file.Id]; ok { + return linkCache.Link, nil + } + + if file.Link == "" { + t = c.RefreshTorrent(t.Id) + if t == nil { + return "", fmt.Errorf("torrent not found") + } + file = t.Torrent.GetFile(file.Id) + } + + _logger.Debug().Msgf("Getting download link for %s", t.Name) + link := c.client.GetDownloadLink(t.Torrent, file) + if link == nil { + return "", fmt.Errorf("download link not found") + } + + t.DownloadLinks[file.Id] = DownloadLinkCache{ + Link: link.DownloadLink, + } + + go func() { + if err := c.SaveTorrent(t); err != nil { + _logger.Debug().Err(err).Msgf("Failed to save torrent %s", t.Id) + } + }() + + return link.DownloadLink, nil +} + +func (c *Cache) GetTorrents() *sync.Map { + return c.torrents +} diff --git a/pkg/debrid/debrid.go b/pkg/debrid/debrid.go index 1c66c98..fb382f8 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,7 +75,6 @@ func ProcessTorrent(d *DebridService, magnet *utils.Magnet, a *arr.Arr, isSymlin dbt, err := db.SubmitMagnet(debridTorrent) if dbt != nil { - dbt.Debrid = db dbt.Arr = a } if err != nil || dbt == nil || dbt.Id == "" { @@ -187,7 +82,7 @@ func ProcessTorrent(d *DebridService, magnet *utils.Magnet, a *arr.Arr, isSymlin continue } logger.Info().Msgf("Torrent: %s submitted to %s", dbt.Name, db.GetName()) - d.lastUsed = index + d.LastUsed = index return db.CheckStatus(dbt, isSymlink) } err := fmt.Errorf("failed to process torrent") 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 new file mode 100644 index 0000000..91bdee8 --- /dev/null +++ b/pkg/debrid/debrid_link/debrid_link.go @@ -0,0 +1,298 @@ +package debrid_link + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/rs/zerolog" + "github.com/sirrobot01/debrid-blackhole/common" + "github.com/sirrobot01/debrid-blackhole/internal/config" + "github.com/sirrobot01/debrid-blackhole/internal/logger" + "github.com/sirrobot01/debrid-blackhole/internal/request" + "github.com/sirrobot01/debrid-blackhole/pkg/debrid/torrent" + + "log" + "net/http" + "os" + "strings" +) + +type DebridLink struct { + Name string + Host string `json:"host"` + APIKey string + DownloadUncached bool + client *request.RLHTTPClient + cache *common.Cache + MountPath string + logger zerolog.Logger + CheckCached bool +} + +func (dl *DebridLink) GetMountPath() string { + return dl.MountPath +} + +func (dl *DebridLink) GetName() string { + return dl.Name +} + +func (dl *DebridLink) GetLogger() zerolog.Logger { + return dl.logger +} + +func (dl *DebridLink) IsAvailable(infohashes []string) map[string]bool { + // Check if the infohashes are available in the local cache + hashes, result := torrent.GetLocalCache(infohashes, dl.cache) + + if len(hashes) == 0 { + // Either all the infohashes are locally cached or none are + dl.cache.AddMultiple(result) + return result + } + + // Divide hashes into groups of 100 + for i := 0; i < len(hashes); i += 100 { + end := i + 100 + if end > len(hashes) { + end = len(hashes) + } + + // Filter out empty strings + validHashes := make([]string, 0, end-i) + for _, hash := range hashes[i:end] { + if hash != "" { + validHashes = append(validHashes, hash) + } + } + + // If no valid hashes in this batch, continue to the next batch + if len(validHashes) == 0 { + continue + } + + hashStr := strings.Join(validHashes, ",") + url := fmt.Sprintf("%s/seedbox/cached/%s", dl.Host, hashStr) + req, _ := http.NewRequest(http.MethodGet, url, nil) + resp, err := dl.client.MakeRequest(req) + if err != nil { + dl.logger.Info().Msgf("Error checking availability: %v", err) + return result + } + var data AvailableResponse + err = json.Unmarshal(resp, &data) + if err != nil { + dl.logger.Info().Msgf("Error marshalling availability: %v", err) + return result + } + if data.Value == nil { + return result + } + value := *data.Value + for _, h := range hashes[i:end] { + _, exists := value[h] + if exists { + result[h] = true + } + } + } + dl.cache.AddMultiple(result) // Add the results to the cache + return result +} + +func (dl *DebridLink) GetTorrent(id string) (*torrent.Torrent, error) { + t := &torrent.Torrent{} + url := fmt.Sprintf("%s/seedbox/list?ids=%s", dl.Host, id) + req, _ := http.NewRequest(http.MethodGet, url, nil) + resp, err := dl.client.MakeRequest(req) + if err != nil { + return t, err + } + var res TorrentInfo + err = json.Unmarshal(resp, &res) + if err != nil { + return t, err + } + if res.Success == false { + return t, fmt.Errorf("error getting torrent") + } + if res.Value == nil { + return t, fmt.Errorf("torrent not found") + } + dt := *res.Value + + if len(dt) == 0 { + return t, fmt.Errorf("torrent not found") + } + data := dt[0] + status := "downloading" + if data.Status == 100 { + status = "downloaded" + } + name := common.RemoveInvalidChars(data.Name) + t.Id = data.ID + t.Name = name + t.Bytes = data.TotalSize + t.Folder = name + t.Progress = data.DownloadPercent + t.Status = status + t.Speed = data.DownloadSpeed + t.Seeders = data.PeersConnected + t.Filename = name + t.OriginalFilename = name + files := make([]torrent.File, len(data.Files)) + cfg := config.GetConfig() + for i, f := range data.Files { + if !cfg.IsSizeAllowed(f.Size) { + continue + } + files[i] = torrent.File{ + Id: f.ID, + Name: f.Name, + Size: f.Size, + Path: f.Name, + } + } + t.Files = files + return t, nil +} + +func (dl *DebridLink) SubmitMagnet(t *torrent.Torrent) (*torrent.Torrent, error) { + url := fmt.Sprintf("%s/seedbox/add", dl.Host) + payload := map[string]string{"url": t.Magnet.Link} + jsonPayload, _ := json.Marshal(payload) + req, _ := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(jsonPayload)) + resp, err := dl.client.MakeRequest(req) + if err != nil { + return nil, err + } + var res SubmitTorrentInfo + err = json.Unmarshal(resp, &res) + if err != nil { + return nil, err + } + if res.Success == false || res.Value == nil { + return nil, fmt.Errorf("error adding torrent") + } + data := *res.Value + status := "downloading" + log.Printf("Torrent: %s added with id: %s", t.Name, data.ID) + name := common.RemoveInvalidChars(data.Name) + t.Id = data.ID + t.Name = name + t.Bytes = data.TotalSize + t.Folder = name + t.Progress = data.DownloadPercent + t.Status = status + t.Speed = data.DownloadSpeed + t.Seeders = data.PeersConnected + t.Filename = name + t.OriginalFilename = name + t.MountPath = dl.MountPath + t.Debrid = dl.Name + t.DownloadLinks = make(map[string]torrent.DownloadLinks) + files := make([]torrent.File, len(data.Files)) + for i, f := range data.Files { + files[i] = torrent.File{ + Id: f.ID, + Name: f.Name, + Size: f.Size, + Path: f.Name, + Link: f.DownloadURL, + } + } + t.Files = files + + return t, nil +} + +func (dl *DebridLink) CheckStatus(torrent *torrent.Torrent, isSymlink bool) (*torrent.Torrent, error) { + for { + t, err := dl.GetTorrent(torrent.Id) + torrent = t + if err != nil || torrent == nil { + return torrent, err + } + status := torrent.Status + if status == "downloaded" { + dl.logger.Info().Msgf("Torrent: %s downloaded", torrent.Name) + err = dl.GetDownloadLinks(torrent) + if err != nil { + return torrent, err + } + break + } else if status == "downloading" { + if !dl.DownloadUncached { + go dl.DeleteTorrent(torrent) + return torrent, fmt.Errorf("torrent: %s not cached", torrent.Name) + } + // Break out of the loop if the torrent is downloading. + // This is necessary to prevent infinite loop since we moved to sync downloading and async processing + break + } else { + return torrent, fmt.Errorf("torrent: %s has error", torrent.Name) + } + + } + return torrent, nil +} + +func (dl *DebridLink) DeleteTorrent(torrent *torrent.Torrent) { + url := fmt.Sprintf("%s/seedbox/%s/remove", dl.Host, torrent.Id) + req, _ := http.NewRequest(http.MethodDelete, url, nil) + _, err := dl.client.MakeRequest(req) + if err == nil { + dl.logger.Info().Msgf("Torrent: %s deleted", torrent.Name) + } else { + dl.logger.Info().Msgf("Error deleting torrent: %s", err) + } +} + +func (dl *DebridLink) GetDownloadLinks(t *torrent.Torrent) error { + downloadLinks := make(map[string]torrent.DownloadLinks) + for _, f := range t.Files { + dl := torrent.DownloadLinks{ + Link: f.Link, + Filename: f.Name, + } + downloadLinks[f.Id] = dl + } + t.DownloadLinks = downloadLinks + return nil +} + +func (dl *DebridLink) GetDownloadLink(t *torrent.Torrent, file *torrent.File) *torrent.DownloadLinks { + dlLink, ok := t.DownloadLinks[file.Id] + if !ok { + return nil + } + return &dlLink +} + +func (dl *DebridLink) GetCheckCached() bool { + return dl.CheckCached +} + +func New(dc config.Debrid, cache *common.Cache) *DebridLink { + rl := request.ParseRateLimit(dc.RateLimit) + headers := map[string]string{ + "Authorization": fmt.Sprintf("Bearer %s", dc.APIKey), + "Content-Type": "application/json", + } + client := request.NewRLHTTPClient(rl, headers) + return &DebridLink{ + Name: "debridlink", + Host: dc.Host, + APIKey: dc.APIKey, + DownloadUncached: dc.DownloadUncached, + client: client, + cache: cache, + MountPath: dc.Folder, + logger: logger.NewLogger(dc.Name, config.GetConfig().LogLevel, os.Stdout), + CheckCached: dc.CheckCached, + } +} + +func (dl *DebridLink) GetTorrents() ([]*torrent.Torrent, error) { + return nil, fmt.Errorf("not implemented") +} diff --git a/pkg/debrid/types/debrid_link.go b/pkg/debrid/debrid_link/types.go similarity index 80% rename from pkg/debrid/types/debrid_link.go rename to pkg/debrid/debrid_link/types.go index 30aebee..156e918 100644 --- a/pkg/debrid/types/debrid_link.go +++ b/pkg/debrid/debrid_link/types.go @@ -1,11 +1,11 @@ -package types +package debrid_link -type DebridLinkAPIResponse[T any] struct { +type APIResponse[T any] struct { Success bool `json:"success"` Value *T `json:"value"` // Use pointer to allow nil } -type DebridLinkAvailableResponse DebridLinkAPIResponse[map[string]map[string]struct { +type AvailableResponse APIResponse[map[string]map[string]struct { Name string `json:"name"` HashString string `json:"hashString"` Files []struct { @@ -40,6 +40,6 @@ type debridLinkTorrentInfo struct { UploadSpeed int64 `json:"uploadSpeed"` } -type DebridLinkTorrentInfo DebridLinkAPIResponse[[]debridLinkTorrentInfo] +type TorrentInfo APIResponse[[]debridLinkTorrentInfo] -type DebridLinkSubmitTorrentInfo DebridLinkAPIResponse[debridLinkTorrentInfo] +type SubmitTorrentInfo APIResponse[debridLinkTorrentInfo] diff --git a/pkg/debrid/engine.go b/pkg/debrid/engine.go new file mode 100644 index 0000000..57845ff --- /dev/null +++ b/pkg/debrid/engine.go @@ -0,0 +1 @@ +package debrid diff --git a/pkg/debrid/engine/engine.go b/pkg/debrid/engine/engine.go new file mode 100644 index 0000000..7d0d303 --- /dev/null +++ b/pkg/debrid/engine/engine.go @@ -0,0 +1,26 @@ +package engine + +type Engine struct { + Debrids []Service + LastUsed int +} + +func (d *Engine) Get() Service { + if d.LastUsed == 0 { + return d.Debrids[0] + } + return d.Debrids[d.LastUsed] +} + +func (d *Engine) GetByName(name string) Service { + for _, deb := range d.Debrids { + if deb.GetName() == name { + return deb + } + } + return nil +} + +func (d *Engine) GetDebrids() []Service { + return d.Debrids +} diff --git a/pkg/debrid/engine/service.go b/pkg/debrid/engine/service.go new file mode 100644 index 0000000..4a4163e --- /dev/null +++ b/pkg/debrid/engine/service.go @@ -0,0 +1,21 @@ +package engine + +import ( + "github.com/rs/zerolog" + "github.com/sirrobot01/debrid-blackhole/pkg/debrid/torrent" +) + +type Service interface { + SubmitMagnet(tr *torrent.Torrent) (*torrent.Torrent, error) + CheckStatus(tr *torrent.Torrent, isSymlink bool) (*torrent.Torrent, error) + GetDownloadLinks(tr *torrent.Torrent) error + GetDownloadLink(tr *torrent.Torrent, file *torrent.File) *torrent.DownloadLinks + DeleteTorrent(tr *torrent.Torrent) + IsAvailable(infohashes []string) map[string]bool + GetMountPath() string + GetCheckCached() bool + GetTorrent(id string) (*torrent.Torrent, error) + GetTorrents() ([]*torrent.Torrent, error) + GetName() string + GetLogger() zerolog.Logger +} 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 new file mode 100644 index 0000000..054ea83 --- /dev/null +++ b/pkg/debrid/realdebrid/realdebrid.go @@ -0,0 +1,402 @@ +package realdebrid + +import ( + "encoding/json" + "fmt" + "github.com/rs/zerolog" + "github.com/sirrobot01/debrid-blackhole/common" + "github.com/sirrobot01/debrid-blackhole/internal/config" + "github.com/sirrobot01/debrid-blackhole/internal/logger" + "github.com/sirrobot01/debrid-blackhole/internal/request" + "github.com/sirrobot01/debrid-blackhole/internal/utils" + "github.com/sirrobot01/debrid-blackhole/pkg/debrid/torrent" + "net/http" + gourl "net/url" + "os" + "path/filepath" + "slices" + "strconv" + "strings" +) + +type RealDebrid struct { + Name string + Host string `json:"host"` + APIKey string + DownloadUncached bool + client *request.RLHTTPClient + cache *common.Cache + MountPath string + logger zerolog.Logger + CheckCached bool +} + +func (r *RealDebrid) GetMountPath() string { + return r.MountPath +} + +func (r *RealDebrid) GetName() string { + return r.Name +} + +func (r *RealDebrid) GetLogger() zerolog.Logger { + return r.logger +} + +// GetTorrentFiles returns a list of torrent files from the torrent info +// validate is used to determine if the files should be validated +// if validate is false, selected files will be returned +func GetTorrentFiles(data TorrentInfo, validate bool) []torrent.File { + files := make([]torrent.File, 0) + cfg := config.GetConfig() + idx := 0 + for _, f := range data.Files { + + name := filepath.Base(f.Path) + + if validate { + if utils.RegexMatch(utils.SAMPLEMATCH, name) { + // Skip sample files + continue + } + if !cfg.IsAllowedFile(name) { + continue + } + if !cfg.IsSizeAllowed(f.Bytes) { + continue + } + } else { + if f.Selected == 0 { + continue + } + } + + fileId := f.ID + _link := "" + if len(data.Links) > idx { + _link = data.Links[idx] + } + file := torrent.File{ + Name: name, + Path: name, + Size: f.Bytes, + Id: strconv.Itoa(fileId), + Link: _link, + } + files = append(files, file) + idx++ + } + return files +} + +func (r *RealDebrid) IsAvailable(infohashes []string) map[string]bool { + // Check if the infohashes are available in the local cache + hashes, result := torrent.GetLocalCache(infohashes, r.cache) + + if len(hashes) == 0 { + // Either all the infohashes are locally cached or none are + r.cache.AddMultiple(result) + return result + } + + // Divide hashes into groups of 100 + for i := 0; i < len(hashes); i += 200 { + end := i + 200 + if end > len(hashes) { + end = len(hashes) + } + + // Filter out empty strings + validHashes := make([]string, 0, end-i) + for _, hash := range hashes[i:end] { + if hash != "" { + validHashes = append(validHashes, hash) + } + } + + // If no valid hashes in this batch, continue to the next batch + if len(validHashes) == 0 { + continue + } + + hashStr := strings.Join(validHashes, "/") + url := fmt.Sprintf("%s/torrents/instantAvailability/%s", r.Host, hashStr) + req, _ := http.NewRequest(http.MethodGet, url, nil) + resp, err := r.client.MakeRequest(req) + if err != nil { + r.logger.Info().Msgf("Error checking availability: %v", err) + return result + } + var data AvailabilityResponse + err = json.Unmarshal(resp, &data) + if err != nil { + r.logger.Info().Msgf("Error marshalling availability: %v", err) + return result + } + for _, h := range hashes[i:end] { + hosters, exists := data[strings.ToLower(h)] + if exists && len(hosters.Rd) > 0 { + result[h] = true + } + } + } + r.cache.AddMultiple(result) // Add the results to the cache + return result +} + +func (r *RealDebrid) SubmitMagnet(t *torrent.Torrent) (*torrent.Torrent, error) { + url := fmt.Sprintf("%s/torrents/addMagnet", r.Host) + payload := gourl.Values{ + "magnet": {t.Magnet.Link}, + } + var data AddMagnetSchema + req, _ := http.NewRequest(http.MethodPost, url, strings.NewReader(payload.Encode())) + resp, err := r.client.MakeRequest(req) + if err != nil { + return nil, err + } + err = json.Unmarshal(resp, &data) + r.logger.Info().Msgf("Torrent: %s added with id: %s", t.Name, data.Id) + t.Id = data.Id + + return t, nil +} + +func (r *RealDebrid) GetTorrent(id string) (*torrent.Torrent, error) { + t := &torrent.Torrent{} + url := fmt.Sprintf("%s/torrents/info/%s", r.Host, id) + req, _ := http.NewRequest(http.MethodGet, url, nil) + resp, err := r.client.MakeRequest(req) + if err != nil { + return t, err + } + var data TorrentInfo + err = json.Unmarshal(resp, &data) + if err != nil { + return t, err + } + name := common.RemoveInvalidChars(data.OriginalFilename) + t.Id = id + t.Name = name + t.Bytes = data.Bytes + t.Folder = name + t.Progress = data.Progress + t.Status = data.Status + t.Speed = data.Speed + t.Seeders = data.Seeders + t.Filename = data.Filename + t.OriginalFilename = data.OriginalFilename + t.Links = data.Links + t.MountPath = r.MountPath + t.Debrid = r.Name + t.DownloadLinks = make(map[string]torrent.DownloadLinks) + files := GetTorrentFiles(data, false) // Get selected files + t.Files = files + return t, nil +} + +func (r *RealDebrid) CheckStatus(t *torrent.Torrent, isSymlink bool) (*torrent.Torrent, error) { + url := fmt.Sprintf("%s/torrents/info/%s", r.Host, t.Id) + req, _ := http.NewRequest(http.MethodGet, url, nil) + for { + resp, err := r.client.MakeRequest(req) + if err != nil { + r.logger.Info().Msgf("ERROR Checking file: %v", err) + return t, err + } + var data TorrentInfo + err = json.Unmarshal(resp, &data) + status := data.Status + name := common.RemoveInvalidChars(data.OriginalFilename) + t.Name = name // Important because some magnet changes the name + t.Folder = name + t.Filename = data.Filename + t.OriginalFilename = data.OriginalFilename + t.Bytes = data.Bytes + t.Progress = data.Progress + t.Speed = data.Speed + t.Seeders = data.Seeders + t.Links = data.Links + t.Status = status + downloadingStatus := []string{"downloading", "magnet_conversion", "queued", "compressing", "uploading"} + if status == "waiting_files_selection" { + files := GetTorrentFiles(data, true) // Validate files to be selected + t.Files = files + if len(files) == 0 { + return t, fmt.Errorf("no video files found") + } + filesId := make([]string, 0) + for _, f := range files { + filesId = append(filesId, f.Id) + } + p := gourl.Values{ + "files": {strings.Join(filesId, ",")}, + } + payload := strings.NewReader(p.Encode()) + req, _ := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/torrents/selectFiles/%s", r.Host, t.Id), payload) + _, err = r.client.MakeRequest(req) + if err != nil { + return t, err + } + } else if status == "downloaded" { + files := GetTorrentFiles(data, false) // Get selected files + t.Files = files + r.logger.Info().Msgf("Torrent: %s downloaded to RD", t.Name) + if !isSymlink { + err = r.GetDownloadLinks(t) + if err != nil { + return t, err + } + } + break + } else if slices.Contains(downloadingStatus, status) { + if !r.DownloadUncached { + go r.DeleteTorrent(t) + return t, fmt.Errorf("torrent: %s not cached", t.Name) + } + // Break out of the loop if the torrent is downloading. + // This is necessary to prevent infinite loop since we moved to sync downloading and async processing + break + } else { + return t, fmt.Errorf("torrent: %s has error: %s", t.Name, status) + } + + } + return t, nil +} + +func (r *RealDebrid) DeleteTorrent(torrent *torrent.Torrent) { + url := fmt.Sprintf("%s/torrents/delete/%s", r.Host, torrent.Id) + req, _ := http.NewRequest(http.MethodDelete, url, nil) + _, err := r.client.MakeRequest(req) + if err == nil { + r.logger.Info().Msgf("Torrent: %s deleted", torrent.Name) + } else { + r.logger.Info().Msgf("Error deleting torrent: %s", err) + } +} + +func (r *RealDebrid) GetDownloadLinks(t *torrent.Torrent) error { + url := fmt.Sprintf("%s/unrestrict/link/", r.Host) + downloadLinks := make(map[string]torrent.DownloadLinks) + for _, f := range t.Files { + dlLink := t.DownloadLinks[f.Id] + if f.Link == "" || dlLink.DownloadLink != "" { + continue + } + payload := gourl.Values{ + "link": {f.Link}, + } + req, _ := http.NewRequest(http.MethodPost, url, strings.NewReader(payload.Encode())) + resp, err := r.client.MakeRequest(req) + if err != nil { + return err + } + var data UnrestrictResponse + if err = json.Unmarshal(resp, &data); err != nil { + return err + } + download := torrent.DownloadLinks{ + Link: data.Link, + Filename: data.Filename, + DownloadLink: data.Download, + } + downloadLinks[f.Id] = download + } + t.DownloadLinks = downloadLinks + return nil +} + +func (r *RealDebrid) GetDownloadLink(t *torrent.Torrent, file *torrent.File) *torrent.DownloadLinks { + url := fmt.Sprintf("%s/unrestrict/link/", r.Host) + payload := gourl.Values{ + "link": {file.Link}, + } + req, _ := http.NewRequest(http.MethodPost, url, strings.NewReader(payload.Encode())) + resp, err := r.client.MakeRequest(req) + if err != nil { + return nil + } + var data UnrestrictResponse + if err = json.Unmarshal(resp, &data); err != nil { + return nil + } + return &torrent.DownloadLinks{ + Link: data.Link, + Filename: data.Filename, + DownloadLink: data.Download, + } +} + +func (r *RealDebrid) GetCheckCached() bool { + return r.CheckCached +} + +func (r *RealDebrid) getTorrents(offset int, limit int) ([]*torrent.Torrent, error) { + url := fmt.Sprintf("%s/torrents?limit=%d", r.Host, limit) + if offset > 0 { + url = fmt.Sprintf("%s&offset=%d", url, offset) + } + req, _ := http.NewRequest(http.MethodGet, url, nil) + resp, err := r.client.MakeRequest(req) + if err != nil { + return nil, err + } + var data []TorrentsResponse + if err = json.Unmarshal(resp, &data); err != nil { + return nil, err + } + torrents := make([]*torrent.Torrent, 0) + for _, t := range data { + + torrents = append(torrents, &torrent.Torrent{ + Id: t.Id, + Name: t.Filename, + Bytes: t.Bytes, + Progress: t.Progress, + Status: t.Status, + Filename: t.Filename, + OriginalFilename: t.Filename, + Links: t.Links, + }) + } + return torrents, nil +} + +func (r *RealDebrid) GetTorrents() ([]*torrent.Torrent, error) { + torrents := make([]*torrent.Torrent, 0) + offset := 0 + limit := 5000 + for { + ts, err := r.getTorrents(offset, limit) + if err != nil { + break + } + if len(ts) == 0 { + break + } + torrents = append(torrents, ts...) + offset = len(torrents) + } + return torrents, nil + +} + +func New(dc config.Debrid, cache *common.Cache) *RealDebrid { + rl := request.ParseRateLimit(dc.RateLimit) + headers := map[string]string{ + "Authorization": fmt.Sprintf("Bearer %s", dc.APIKey), + } + client := request.NewRLHTTPClient(rl, headers) + return &RealDebrid{ + Name: "realdebrid", + Host: dc.Host, + APIKey: dc.APIKey, + DownloadUncached: dc.DownloadUncached, + client: client, + cache: cache, + MountPath: dc.Folder, + logger: logger.NewLogger(dc.Name, config.GetConfig().LogLevel, os.Stdout), + CheckCached: dc.CheckCached, + } +} diff --git a/pkg/debrid/types/realdebrid.go b/pkg/debrid/realdebrid/types.go similarity index 80% rename from pkg/debrid/types/realdebrid.go rename to pkg/debrid/realdebrid/types.go index 5577862..48fdc6c 100644 --- a/pkg/debrid/types/realdebrid.go +++ b/pkg/debrid/realdebrid/types.go @@ -1,13 +1,14 @@ -package types +package realdebrid import ( "encoding/json" "fmt" + "time" ) -type RealDebridAvailabilityResponse map[string]Hoster +type AvailabilityResponse map[string]Hoster -func (r *RealDebridAvailabilityResponse) UnmarshalJSON(data []byte) error { +func (r *AvailabilityResponse) UnmarshalJSON(data []byte) error { // First, try to unmarshal as an object var objectData map[string]Hoster err := json.Unmarshal(data, &objectData) @@ -64,12 +65,12 @@ type FileVariant struct { Filesize int `json:"filesize"` } -type RealDebridAddMagnetSchema struct { +type AddMagnetSchema struct { Id string `json:"id"` Uri string `json:"uri"` } -type RealDebridTorrentInfo struct { +type TorrentInfo struct { ID string `json:"id"` Filename string `json:"filename"` OriginalFilename string `json:"original_filename"` @@ -93,7 +94,7 @@ type RealDebridTorrentInfo struct { Seeders int `json:"seeders,omitempty"` } -type RealDebridUnrestrictResponse struct { +type UnrestrictResponse struct { Id string `json:"id"` Filename string `json:"filename"` MimeType string `json:"mimeType"` @@ -105,3 +106,17 @@ type RealDebridUnrestrictResponse struct { Download string `json:"download"` Streamable int `json:"streamable"` } + +type TorrentsResponse struct { + Id string `json:"id"` + Filename string `json:"filename"` + Hash string `json:"hash"` + Bytes int64 `json:"bytes"` + Host string `json:"host"` + Split int64 `json:"split"` + Progress float64 `json:"progress"` + Status string `json:"status"` + Added time.Time `json:"added"` + Links []string `json:"links"` + Ended time.Time `json:"ended"` +} 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/torbox.go similarity index 51% rename from pkg/debrid/torbox.go rename to pkg/debrid/torbox/torbox.go index 04ba760..8e2ae1f 100644 --- a/pkg/debrid/torbox.go +++ b/pkg/debrid/torbox/torbox.go @@ -1,4 +1,4 @@ -package debrid +package torbox import ( "bytes" @@ -9,7 +9,8 @@ import ( "github.com/sirrobot01/debrid-blackhole/internal/config" "github.com/sirrobot01/debrid-blackhole/internal/logger" "github.com/sirrobot01/debrid-blackhole/internal/request" - "github.com/sirrobot01/debrid-blackhole/pkg/debrid/types" + "github.com/sirrobot01/debrid-blackhole/pkg/debrid/torrent" + "log" "mime/multipart" "net/http" @@ -23,28 +24,36 @@ import ( ) type Torbox struct { - BaseDebrid + Name string + Host string `json:"host"` + APIKey string + DownloadUncached bool + client *request.RLHTTPClient + cache *common.Cache + MountPath string + logger zerolog.Logger + CheckCached bool } -func (r *Torbox) GetMountPath() string { - return r.MountPath +func (tb *Torbox) GetMountPath() string { + return tb.MountPath } -func (r *Torbox) GetName() string { - return r.Name +func (tb *Torbox) GetName() string { + return tb.Name } -func (r *Torbox) GetLogger() zerolog.Logger { - return r.logger +func (tb *Torbox) GetLogger() zerolog.Logger { + return tb.logger } -func (r *Torbox) IsAvailable(infohashes []string) map[string]bool { +func (tb *Torbox) IsAvailable(infohashes []string) map[string]bool { // Check if the infohashes are available in the local cache - hashes, result := GetLocalCache(infohashes, r.cache) + hashes, result := torrent.GetLocalCache(infohashes, tb.cache) if len(hashes) == 0 { // Either all the infohashes are locally cached or none are - r.cache.AddMultiple(result) + tb.cache.AddMultiple(result) return result } @@ -69,17 +78,17 @@ func (r *Torbox) IsAvailable(infohashes []string) map[string]bool { } hashStr := strings.Join(validHashes, ",") - url := fmt.Sprintf("%s/api/torrents/checkcached?hash=%s", r.Host, hashStr) + url := fmt.Sprintf("%s/api/torrents/checkcached?hash=%s", tb.Host, hashStr) req, _ := http.NewRequest(http.MethodGet, url, nil) - resp, err := r.client.MakeRequest(req) + resp, err := tb.client.MakeRequest(req) if err != nil { - r.logger.Info().Msgf("Error checking availability: %v", err) + tb.logger.Info().Msgf("Error checking availability: %v", err) return result } - var res types.TorBoxAvailableResponse + var res AvailableResponse err = json.Unmarshal(resp, &res) if err != nil { - r.logger.Info().Msgf("Error marshalling availability: %v", err) + tb.logger.Info().Msgf("Error marshalling availability: %v", err) return result } if res.Data == nil { @@ -92,12 +101,12 @@ func (r *Torbox) IsAvailable(infohashes []string) map[string]bool { } } } - r.cache.AddMultiple(result) // Add the results to the cache + tb.cache.AddMultiple(result) // Add the results to the cache return result } -func (r *Torbox) SubmitMagnet(torrent *Torrent) (*Torrent, error) { - url := fmt.Sprintf("%s/api/torrents/createtorrent", r.Host) +func (tb *Torbox) SubmitMagnet(torrent *torrent.Torrent) (*torrent.Torrent, error) { + url := fmt.Sprintf("%s/api/torrents/createtorrent", tb.Host) payload := &bytes.Buffer{} writer := multipart.NewWriter(payload) _ = writer.WriteField("magnet", torrent.Magnet.Link) @@ -107,11 +116,11 @@ func (r *Torbox) SubmitMagnet(torrent *Torrent) (*Torrent, error) { } req, _ := http.NewRequest(http.MethodPost, url, payload) req.Header.Set("Content-Type", writer.FormDataContentType()) - resp, err := r.client.MakeRequest(req) + resp, err := tb.client.MakeRequest(req) if err != nil { return nil, err } - var data types.TorBoxAddMagnetResponse + var data AddMagnetResponse err = json.Unmarshal(resp, &data) if err != nil { return nil, err @@ -143,32 +152,35 @@ func getTorboxStatus(status string, finished bool) string { } } -func (r *Torbox) GetTorrent(id string) (*Torrent, error) { - torrent := &Torrent{} - url := fmt.Sprintf("%s/api/torrents/mylist/?id=%s", r.Host, id) +func (tb *Torbox) GetTorrent(id string) (*torrent.Torrent, error) { + t := &torrent.Torrent{} + url := fmt.Sprintf("%s/api/torrents/mylist/?id=%s", tb.Host, id) req, _ := http.NewRequest(http.MethodGet, url, nil) - resp, err := r.client.MakeRequest(req) + resp, err := tb.client.MakeRequest(req) if err != nil { - return torrent, err + return t, err } - var res types.TorboxInfoResponse + var res InfoResponse err = json.Unmarshal(resp, &res) if err != nil { - return torrent, err + return t, err } data := res.Data name := data.Name - torrent.Id = id - torrent.Name = name - torrent.Bytes = data.Size - torrent.Folder = name - torrent.Progress = data.Progress * 100 - torrent.Status = getTorboxStatus(data.DownloadState, data.DownloadFinished) - torrent.Speed = data.DownloadSpeed - torrent.Seeders = data.Seeds - torrent.Filename = name - torrent.OriginalFilename = name - files := make([]TorrentFile, 0) + t.Id = id + t.Name = name + t.Bytes = data.Size + t.Folder = name + t.Progress = data.Progress * 100 + t.Status = getTorboxStatus(data.DownloadState, data.DownloadFinished) + t.Speed = data.DownloadSpeed + t.Seeders = data.Seeds + t.Filename = name + t.OriginalFilename = name + t.MountPath = tb.MountPath + t.Debrid = tb.Name + t.DownloadLinks = make(map[string]torrent.DownloadLinks) + files := make([]torrent.File, 0) cfg := config.GetConfig() for _, f := range data.Files { fileName := filepath.Base(f.Name) @@ -183,7 +195,7 @@ func (r *Torbox) GetTorrent(id string) (*Torrent, error) { if !cfg.IsSizeAllowed(f.Size) { continue } - file := TorrentFile{ + file := torrent.File{ Id: strconv.Itoa(f.Id), Name: fileName, Size: f.Size, @@ -198,34 +210,34 @@ func (r *Torbox) GetTorrent(id string) (*Torrent, error) { cleanPath = path.Clean(data.Name) } - torrent.OriginalFilename = strings.Split(cleanPath, "/")[0] - torrent.Files = files - torrent.Debrid = r - return torrent, nil + t.OriginalFilename = strings.Split(cleanPath, "/")[0] + t.Files = files + //t.Debrid = tb + return t, nil } -func (r *Torbox) CheckStatus(torrent *Torrent, isSymlink bool) (*Torrent, error) { +func (tb *Torbox) CheckStatus(torrent *torrent.Torrent, isSymlink bool) (*torrent.Torrent, error) { for { - tb, err := r.GetTorrent(torrent.Id) + t, err := tb.GetTorrent(torrent.Id) - torrent = tb + torrent = t - if err != nil || tb == nil { - return tb, err + if err != nil || t == nil { + return t, err } status := torrent.Status if status == "downloaded" { - r.logger.Info().Msgf("Torrent: %s downloaded", torrent.Name) + tb.logger.Info().Msgf("Torrent: %s downloaded", torrent.Name) if !isSymlink { - err = r.GetDownloadLinks(torrent) + err = tb.GetDownloadLinks(torrent) if err != nil { return torrent, err } } break } else if status == "downloading" { - if !r.DownloadUncached { - go torrent.Delete() + if !tb.DownloadUncached { + go tb.DeleteTorrent(torrent) return torrent, fmt.Errorf("torrent: %s not cached", torrent.Name) } // Break out of the loop if the torrent is downloading. @@ -239,34 +251,34 @@ func (r *Torbox) CheckStatus(torrent *Torrent, isSymlink bool) (*Torrent, error) return torrent, nil } -func (r *Torbox) DeleteTorrent(torrent *Torrent) { - url := fmt.Sprintf("%s/api/torrents/controltorrent/%s", r.Host, torrent.Id) +func (tb *Torbox) DeleteTorrent(torrent *torrent.Torrent) { + url := fmt.Sprintf("%s/api/torrents/controltorrent/%s", tb.Host, torrent.Id) payload := map[string]string{"torrent_id": torrent.Id, "action": "Delete"} jsonPayload, _ := json.Marshal(payload) req, _ := http.NewRequest(http.MethodDelete, url, bytes.NewBuffer(jsonPayload)) - _, err := r.client.MakeRequest(req) + _, err := tb.client.MakeRequest(req) if err == nil { - r.logger.Info().Msgf("Torrent: %s deleted", torrent.Name) + tb.logger.Info().Msgf("Torrent: %s deleted", torrent.Name) } else { - r.logger.Info().Msgf("Error deleting torrent: %s", err) + tb.logger.Info().Msgf("Error deleting torrent: %s", err) } } -func (r *Torbox) GetDownloadLinks(torrent *Torrent) error { - downloadLinks := make([]TorrentDownloadLinks, 0) - for _, file := range torrent.Files { - url := fmt.Sprintf("%s/api/torrents/requestdl/", r.Host) +func (tb *Torbox) GetDownloadLinks(t *torrent.Torrent) error { + downloadLinks := make(map[string]torrent.DownloadLinks) + for _, file := range t.Files { + url := fmt.Sprintf("%s/api/torrents/requestdl/", tb.Host) query := gourl.Values{} - query.Add("torrent_id", torrent.Id) - query.Add("token", r.APIKey) + query.Add("torrent_id", t.Id) + query.Add("token", tb.APIKey) query.Add("file_id", file.Id) url += "?" + query.Encode() req, _ := http.NewRequest(http.MethodGet, url, nil) - resp, err := r.client.MakeRequest(req) + resp, err := tb.client.MakeRequest(req) if err != nil { return err } - var data types.TorBoxDownloadLinksResponse + var data DownloadLinksResponse if err = json.Unmarshal(resp, &data); err != nil { return err } @@ -276,38 +288,67 @@ func (r *Torbox) GetDownloadLinks(torrent *Torrent) error { idx := 0 link := *data.Data - dl := TorrentDownloadLinks{ + dl := torrent.DownloadLinks{ Link: link, - Filename: torrent.Files[idx].Name, + Filename: t.Files[idx].Name, DownloadLink: link, } - downloadLinks = append(downloadLinks, dl) + downloadLinks[file.Id] = dl } - torrent.DownloadLinks = downloadLinks + t.DownloadLinks = downloadLinks return nil } -func (r *Torbox) GetCheckCached() bool { - return r.CheckCached +func (tb *Torbox) GetDownloadLink(t *torrent.Torrent, file *torrent.File) *torrent.DownloadLinks { + url := fmt.Sprintf("%s/api/torrents/requestdl/", tb.Host) + query := gourl.Values{} + query.Add("torrent_id", t.Id) + query.Add("token", tb.APIKey) + query.Add("file_id", file.Id) + url += "?" + query.Encode() + req, _ := http.NewRequest(http.MethodGet, url, nil) + resp, err := tb.client.MakeRequest(req) + if err != nil { + return nil + } + var data DownloadLinksResponse + if err = json.Unmarshal(resp, &data); err != nil { + return nil + } + if data.Data == nil { + return nil + } + link := *data.Data + return &torrent.DownloadLinks{ + Link: file.Link, + Filename: file.Name, + DownloadLink: link, + } } -func NewTorbox(dc config.Debrid, cache *common.Cache) *Torbox { +func (tb *Torbox) GetCheckCached() bool { + return tb.CheckCached +} + +func (tb *Torbox) GetTorrents() ([]*torrent.Torrent, error) { + return nil, fmt.Errorf("not implemented") +} + +func New(dc config.Debrid, cache *common.Cache) *Torbox { rl := request.ParseRateLimit(dc.RateLimit) headers := map[string]string{ "Authorization": fmt.Sprintf("Bearer %s", dc.APIKey), } client := request.NewRLHTTPClient(rl, headers) return &Torbox{ - BaseDebrid: BaseDebrid{ - Name: "torbox", - Host: dc.Host, - APIKey: dc.APIKey, - DownloadUncached: dc.DownloadUncached, - client: client, - cache: cache, - MountPath: dc.Folder, - logger: logger.NewLogger(dc.Name, config.GetConfig().LogLevel, os.Stdout), - CheckCached: dc.CheckCached, - }, + Name: "torbox", + Host: dc.Host, + APIKey: dc.APIKey, + DownloadUncached: dc.DownloadUncached, + client: client, + cache: cache, + MountPath: dc.Folder, + logger: logger.NewLogger(dc.Name, config.GetConfig().LogLevel, os.Stdout), + CheckCached: dc.CheckCached, } } diff --git a/pkg/debrid/types/torbox.go b/pkg/debrid/torbox/types.go similarity index 89% rename from pkg/debrid/types/torbox.go rename to pkg/debrid/torbox/types.go index 8289532..0716345 100644 --- a/pkg/debrid/types/torbox.go +++ b/pkg/debrid/torbox/types.go @@ -1,21 +1,21 @@ -package types +package torbox import "time" -type TorboxAPIResponse[T any] struct { +type APIResponse[T any] struct { Success bool `json:"success"` Error any `json:"error"` Detail string `json:"detail"` Data *T `json:"data"` // Use pointer to allow nil } -type TorBoxAvailableResponse TorboxAPIResponse[map[string]struct { +type AvailableResponse APIResponse[map[string]struct { Name string `json:"name"` Size int `json:"size"` Hash string `json:"hash"` }] -type TorBoxAddMagnetResponse TorboxAPIResponse[struct { +type AddMagnetResponse APIResponse[struct { Id int `json:"torrent_id"` Hash string `json:"hash"` }] @@ -70,6 +70,6 @@ type torboxInfo struct { TrackerMessage interface{} `json:"tracker_message"` } -type TorboxInfoResponse TorboxAPIResponse[torboxInfo] +type InfoResponse APIResponse[torboxInfo] -type TorBoxDownloadLinksResponse TorboxAPIResponse[string] +type DownloadLinksResponse APIResponse[string] 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/torrent/torrent.go b/pkg/debrid/torrent/torrent.go new file mode 100644 index 0000000..7fe6a64 --- /dev/null +++ b/pkg/debrid/torrent/torrent.go @@ -0,0 +1,135 @@ +package torrent + +import ( + "fmt" + "github.com/sirrobot01/debrid-blackhole/common" + "github.com/sirrobot01/debrid-blackhole/internal/utils" + "github.com/sirrobot01/debrid-blackhole/pkg/arr" + "os" + "path/filepath" + "sync" +) + +type Arr struct { + Name string `json:"name"` + Token string `json:"-"` + Host string `json:"host"` +} + +type ArrHistorySchema struct { + Page int `json:"page"` + PageSize int `json:"pageSize"` + SortKey string `json:"sortKey"` + SortDirection string `json:"sortDirection"` + TotalRecords int `json:"totalRecords"` + Records []struct { + ID int `json:"id"` + DownloadID string `json:"downloadId"` + } `json:"records"` +} + +type Torrent struct { + Id string `json:"id"` + InfoHash string `json:"info_hash"` + Name string `json:"name"` + Folder string `json:"folder"` + Filename string `json:"filename"` + OriginalFilename string `json:"original_filename"` + Size int64 `json:"size"` + Bytes int64 `json:"bytes"` // Size of only the files that are downloaded + Magnet *utils.Magnet `json:"magnet"` + Files []File `json:"files"` + Status string `json:"status"` + Added string `json:"added"` + Progress float64 `json:"progress"` + Speed int64 `json:"speed"` + Seeders int `json:"seeders"` + Links []string `json:"links"` + DownloadLinks map[string]DownloadLinks `json:"download_links"` + MountPath string `json:"mount_path"` + + Debrid string `json:"debrid"` + + Arr *arr.Arr `json:"arr"` + Mu sync.Mutex `json:"-"` + SizeDownloaded int64 `json:"-"` // This is used for local download +} + +type DownloadLinks struct { + Filename string `json:"filename"` + Link string `json:"link"` + DownloadLink string `json:"download_link"` +} + +func (t *Torrent) GetSymlinkFolder(parent string) string { + return filepath.Join(parent, t.Arr.Name, t.Folder) +} + +func (t *Torrent) GetMountFolder(rClonePath string) (string, error) { + possiblePaths := []string{ + t.OriginalFilename, + t.Filename, + common.RemoveExtension(t.OriginalFilename), + } + + for _, path := range possiblePaths { + _, err := os.Stat(filepath.Join(rClonePath, path)) + if !os.IsNotExist(err) { + return path, nil + } + } + return "", fmt.Errorf("no path found") +} + +type File struct { + Id string `json:"id"` + Name string `json:"name"` + Size int64 `json:"size"` + Path string `json:"path"` + Link string `json:"link"` +} + +func (t *Torrent) Cleanup(remove bool) { + if remove { + err := os.Remove(t.Filename) + if err != nil { + return + } + } +} + +func (t *Torrent) GetFile(id string) *File { + for _, f := range t.Files { + if f.Id == id { + return &f + } + } + return nil +} + +func GetLocalCache(infohashes []string, cache *common.Cache) ([]string, map[string]bool) { + result := make(map[string]bool) + hashes := make([]string, 0) + + if len(infohashes) == 0 { + return hashes, result + } + if len(infohashes) == 1 { + if cache.Exists(infohashes[0]) { + return hashes, map[string]bool{infohashes[0]: true} + } + return infohashes, result + } + + cachedHashes := cache.GetMultiple(infohashes) + for _, h := range infohashes { + _, exists := cachedHashes[h] + if !exists { + hashes = append(hashes, h) + } else { + result[h] = true + } + } + + return infohashes, result +} 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/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/shared/downloader.go b/pkg/qbit/downloader.go similarity index 78% rename from pkg/qbit/shared/downloader.go rename to pkg/qbit/downloader.go index 14800c0..0a4c8c3 100644 --- a/pkg/qbit/shared/downloader.go +++ b/pkg/qbit/downloader.go @@ -1,16 +1,54 @@ -package shared +package qbit import ( + "crypto/tls" "fmt" + "github.com/cavaliergopher/grab/v3" "github.com/sirrobot01/debrid-blackhole/common" - "github.com/sirrobot01/debrid-blackhole/pkg/debrid" - "github.com/sirrobot01/debrid-blackhole/pkg/downloaders" + debrid "github.com/sirrobot01/debrid-blackhole/pkg/debrid/torrent" + "net/http" "os" "path/filepath" "sync" "time" ) +func Download(client *grab.Client, url, filename string, progressCallback func(int64, int64)) error { + req, err := grab.NewRequest(filename, url) + if err != nil { + return err + } + resp := client.Do(req) + + t := time.NewTicker(time.Second) + defer t.Stop() + + var lastReported int64 +Loop: + for { + select { + case <-t.C: + current := resp.BytesComplete() + speed := int64(resp.BytesPerSecond()) + if current != lastReported { + if progressCallback != nil { + progressCallback(current-lastReported, speed) + } + lastReported = current + } + case <-resp.Done: + break Loop + } + } + + // Report final bytes + if progressCallback != nil { + progressCallback(resp.BytesComplete()-lastReported, 0) + } + + return resp.Err() +} + func (q *QBit) ProcessManualFile(torrent *Torrent) (string, error) { debridTorrent := torrent.DebridTorrent q.logger.Info().Msgf("Downloading %d files...", len(debridTorrent.DownloadLinks)) @@ -37,7 +75,6 @@ func (q *QBit) downloadFiles(torrent *Torrent, parent string) { debridTorrent.SizeDownloaded = 0 // Reset downloaded bytes debridTorrent.Progress = 0 // Reset progress debridTorrent.Mu.Unlock() - client := downloaders.GetGrabClient() progressCallback := func(downloaded int64, speed int64) { debridTorrent.Mu.Lock() defer debridTorrent.Mu.Unlock() @@ -54,6 +91,17 @@ func (q *QBit) downloadFiles(torrent *Torrent, parent string) { } q.UpdateTorrentMin(torrent, debridTorrent) } + + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + Proxy: http.ProxyFromEnvironment, + } + client := &grab.Client{ + UserAgent: "qBitTorrent", + HTTPClient: &http.Client{ + Transport: tr, + }, + } for _, link := range debridTorrent.DownloadLinks { if link.DownloadLink == "" { q.logger.Info().Msgf("No download link found for %s", link.Filename) @@ -61,12 +109,12 @@ func (q *QBit) downloadFiles(torrent *Torrent, parent string) { } wg.Add(1) semaphore <- struct{}{} - go func(link debrid.TorrentDownloadLinks) { + go func(link debrid.DownloadLinks) { defer wg.Done() defer func() { <-semaphore }() filename := link.Filename - err := downloaders.NormalGrab( + err := Download( client, link.DownloadLink, filepath.Join(parent, filename), @@ -88,12 +136,12 @@ func (q *QBit) ProcessSymlink(torrent *Torrent) (string, error) { debridTorrent := torrent.DebridTorrent var wg sync.WaitGroup files := debridTorrent.Files - ready := make(chan debrid.TorrentFile, len(files)) + ready := make(chan debrid.File, len(files)) if len(files) == 0 { return "", fmt.Errorf("no video files found") } q.logger.Info().Msgf("Checking %d files...", len(files)) - rCloneBase := debridTorrent.Debrid.GetMountPath() + rCloneBase := debridTorrent.MountPath torrentPath, err := q.getTorrentPath(rCloneBase, debridTorrent) // /MyTVShow/ if err != nil { return "", fmt.Errorf("failed to get torrent path: %v", err) @@ -140,7 +188,7 @@ func (q *QBit) getTorrentPath(rclonePath string, debridTorrent *debrid.Torrent) } } -func (q *QBit) createSymLink(path string, torrentMountPath string, file debrid.TorrentFile) { +func (q *QBit) createSymLink(path string, torrentMountPath string, file debrid.File) { // Combine the directory and filename to form a full path fullPath := filepath.Join(path, file.Name) // /mnt/symlinks/{category}/MyTVShow/MyTVShow.S01E01.720p.mkv diff --git a/pkg/qbit/server/qbit_handlers.go b/pkg/qbit/http.go similarity index 67% rename from pkg/qbit/server/qbit_handlers.go rename to pkg/qbit/http.go index dd53304..b5be90c 100644 --- a/pkg/qbit/server/qbit_handlers.go +++ b/pkg/qbit/http.go @@ -1,24 +1,17 @@ -package server +package qbit import ( "context" "encoding/base64" "github.com/go-chi/chi/v5" - "github.com/rs/zerolog" "github.com/sirrobot01/debrid-blackhole/internal/request" "github.com/sirrobot01/debrid-blackhole/pkg/arr" - "github.com/sirrobot01/debrid-blackhole/pkg/qbit/shared" + "github.com/sirrobot01/debrid-blackhole/pkg/service" "net/http" "path/filepath" "strings" ) -type QbitHandler struct { - qbit *shared.QBit - logger zerolog.Logger - debug bool -} - func decodeAuthHeader(header string) (string, string, error) { encodedTokens := strings.Split(header, " ") if len(encodedTokens) != 2 { @@ -40,7 +33,7 @@ func decodeAuthHeader(header string) (string, string, error) { return host, token, nil } -func (q *QbitHandler) CategoryContext(next http.Handler) http.Handler { +func (q *QBit) CategoryContext(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { category := strings.Trim(r.URL.Query().Get("category"), "") if category == "" { @@ -59,7 +52,7 @@ func (q *QbitHandler) CategoryContext(next http.Handler) http.Handler { }) } -func (q *QbitHandler) authContext(next http.Handler) http.Handler { +func (q *QBit) authContext(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { host, token, err := decodeAuthHeader(r.Header.Get("Authorization")) category := r.Context().Value("category").(string) @@ -70,7 +63,8 @@ func (q *QbitHandler) authContext(next http.Handler) http.Handler { a.Host = strings.TrimSpace(host) a.Token = strings.TrimSpace(token) } - q.qbit.Arrs.AddOrUpdate(a) + svc := service.GetService() + svc.Arr.AddOrUpdate(a) ctx := context.WithValue(r.Context(), "arr", a) next.ServeHTTP(w, r.WithContext(ctx)) }) @@ -96,30 +90,30 @@ func HashesCtx(next http.Handler) http.Handler { }) } -func (q *QbitHandler) handleLogin(w http.ResponseWriter, r *http.Request) { +func (q *QBit) handleLogin(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte("Ok.")) } -func (q *QbitHandler) handleVersion(w http.ResponseWriter, r *http.Request) { +func (q *QBit) handleVersion(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte("v4.3.2")) } -func (q *QbitHandler) handleWebAPIVersion(w http.ResponseWriter, r *http.Request) { +func (q *QBit) handleWebAPIVersion(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte("2.7")) } -func (q *QbitHandler) handlePreferences(w http.ResponseWriter, r *http.Request) { - preferences := shared.NewAppPreferences() +func (q *QBit) handlePreferences(w http.ResponseWriter, r *http.Request) { + preferences := NewAppPreferences() - preferences.WebUiUsername = q.qbit.Username - preferences.SavePath = q.qbit.DownloadFolder - preferences.TempPath = filepath.Join(q.qbit.DownloadFolder, "temp") + preferences.WebUiUsername = q.Username + preferences.SavePath = q.DownloadFolder + preferences.TempPath = filepath.Join(q.DownloadFolder, "temp") request.JSONResponse(w, preferences, http.StatusOK) } -func (q *QbitHandler) handleBuildInfo(w http.ResponseWriter, r *http.Request) { - res := shared.BuildInfo{ +func (q *QBit) handleBuildInfo(w http.ResponseWriter, r *http.Request) { + res := BuildInfo{ Bitness: 64, Boost: "1.75.0", Libtorrent: "1.2.11.0", @@ -130,21 +124,21 @@ func (q *QbitHandler) handleBuildInfo(w http.ResponseWriter, r *http.Request) { request.JSONResponse(w, res, http.StatusOK) } -func (q *QbitHandler) shutdown(w http.ResponseWriter, r *http.Request) { +func (q *QBit) handleShutdown(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } -func (q *QbitHandler) handleTorrentsInfo(w http.ResponseWriter, r *http.Request) { +func (q *QBit) handleTorrentsInfo(w http.ResponseWriter, r *http.Request) { //log all url params ctx := r.Context() category := ctx.Value("category").(string) filter := strings.Trim(r.URL.Query().Get("filter"), "") hashes, _ := ctx.Value("hashes").([]string) - torrents := q.qbit.Storage.GetAll(category, filter, hashes) + torrents := q.Storage.GetAll(category, filter, hashes) request.JSONResponse(w, torrents, http.StatusOK) } -func (q *QbitHandler) handleTorrentsAdd(w http.ResponseWriter, r *http.Request) { +func (q *QBit) handleTorrentsAdd(w http.ResponseWriter, r *http.Request) { ctx := r.Context() // Parse form based on content type @@ -178,7 +172,7 @@ func (q *QbitHandler) handleTorrentsAdd(w http.ResponseWriter, r *http.Request) urlList = append(urlList, strings.TrimSpace(u)) } for _, url := range urlList { - if err := q.qbit.AddMagnet(ctx, url, category); err != nil { + if err := q.AddMagnet(ctx, url, category); err != nil { q.logger.Info().Msgf("Error adding magnet: %v", err) http.Error(w, err.Error(), http.StatusBadRequest) return @@ -191,7 +185,7 @@ func (q *QbitHandler) handleTorrentsAdd(w http.ResponseWriter, r *http.Request) if r.MultipartForm != nil && r.MultipartForm.File != nil { if files := r.MultipartForm.File["torrents"]; len(files) > 0 { for _, fileHeader := range files { - if err := q.qbit.AddTorrent(ctx, fileHeader, category); err != nil { + if err := q.AddTorrent(ctx, fileHeader, category); err != nil { q.logger.Info().Msgf("Error adding torrent: %v", err) http.Error(w, err.Error(), http.StatusBadRequest) return @@ -209,7 +203,7 @@ func (q *QbitHandler) handleTorrentsAdd(w http.ResponseWriter, r *http.Request) w.WriteHeader(http.StatusOK) } -func (q *QbitHandler) handleTorrentsDelete(w http.ResponseWriter, r *http.Request) { +func (q *QBit) handleTorrentsDelete(w http.ResponseWriter, r *http.Request) { ctx := r.Context() hashes, _ := ctx.Value("hashes").([]string) if len(hashes) == 0 { @@ -217,59 +211,59 @@ func (q *QbitHandler) handleTorrentsDelete(w http.ResponseWriter, r *http.Reques return } for _, hash := range hashes { - q.qbit.Storage.Delete(hash) + q.Storage.Delete(hash) } w.WriteHeader(http.StatusOK) } -func (q *QbitHandler) handleTorrentsPause(w http.ResponseWriter, r *http.Request) { +func (q *QBit) handleTorrentsPause(w http.ResponseWriter, r *http.Request) { ctx := r.Context() hashes, _ := ctx.Value("hashes").([]string) for _, hash := range hashes { - torrent := q.qbit.Storage.Get(hash) + torrent := q.Storage.Get(hash) if torrent == nil { continue } - go q.qbit.PauseTorrent(torrent) + go q.PauseTorrent(torrent) } w.WriteHeader(http.StatusOK) } -func (q *QbitHandler) handleTorrentsResume(w http.ResponseWriter, r *http.Request) { +func (q *QBit) handleTorrentsResume(w http.ResponseWriter, r *http.Request) { ctx := r.Context() hashes, _ := ctx.Value("hashes").([]string) for _, hash := range hashes { - torrent := q.qbit.Storage.Get(hash) + torrent := q.Storage.Get(hash) if torrent == nil { continue } - go q.qbit.ResumeTorrent(torrent) + go q.ResumeTorrent(torrent) } w.WriteHeader(http.StatusOK) } -func (q *QbitHandler) handleTorrentRecheck(w http.ResponseWriter, r *http.Request) { +func (q *QBit) handleTorrentRecheck(w http.ResponseWriter, r *http.Request) { ctx := r.Context() hashes, _ := ctx.Value("hashes").([]string) for _, hash := range hashes { - torrent := q.qbit.Storage.Get(hash) + torrent := q.Storage.Get(hash) if torrent == nil { continue } - go q.qbit.RefreshTorrent(torrent) + go q.RefreshTorrent(torrent) } w.WriteHeader(http.StatusOK) } -func (q *QbitHandler) handleCategories(w http.ResponseWriter, r *http.Request) { - var categories = map[string]shared.TorrentCategory{} - for _, cat := range q.qbit.Categories { - path := filepath.Join(q.qbit.DownloadFolder, cat) - categories[cat] = shared.TorrentCategory{ +func (q *QBit) handleCategories(w http.ResponseWriter, r *http.Request) { + var categories = map[string]TorrentCategory{} + for _, cat := range q.Categories { + path := filepath.Join(q.DownloadFolder, cat) + categories[cat] = TorrentCategory{ Name: cat, SavePath: path, } @@ -277,7 +271,7 @@ func (q *QbitHandler) handleCategories(w http.ResponseWriter, r *http.Request) { request.JSONResponse(w, categories, http.StatusOK) } -func (q *QbitHandler) handleCreateCategory(w http.ResponseWriter, r *http.Request) { +func (q *QBit) handleCreateCategory(w http.ResponseWriter, r *http.Request) { err := r.ParseForm() if err != nil { http.Error(w, "Failed to parse form data", http.StatusBadRequest) @@ -290,41 +284,41 @@ func (q *QbitHandler) handleCreateCategory(w http.ResponseWriter, r *http.Reques return } - q.qbit.Categories = append(q.qbit.Categories, name) + q.Categories = append(q.Categories, name) request.JSONResponse(w, nil, http.StatusOK) } -func (q *QbitHandler) handleTorrentProperties(w http.ResponseWriter, r *http.Request) { +func (q *QBit) handleTorrentProperties(w http.ResponseWriter, r *http.Request) { hash := r.URL.Query().Get("hash") - torrent := q.qbit.Storage.Get(hash) - properties := q.qbit.GetTorrentProperties(torrent) + torrent := q.Storage.Get(hash) + properties := q.GetTorrentProperties(torrent) request.JSONResponse(w, properties, http.StatusOK) } -func (q *QbitHandler) handleTorrentFiles(w http.ResponseWriter, r *http.Request) { +func (q *QBit) handleTorrentFiles(w http.ResponseWriter, r *http.Request) { hash := r.URL.Query().Get("hash") - torrent := q.qbit.Storage.Get(hash) + torrent := q.Storage.Get(hash) if torrent == nil { return } - files := q.qbit.GetTorrentFiles(torrent) + files := q.GetTorrentFiles(torrent) request.JSONResponse(w, files, http.StatusOK) } -func (q *QbitHandler) handleSetCategory(w http.ResponseWriter, r *http.Request) { +func (q *QBit) handleSetCategory(w http.ResponseWriter, r *http.Request) { ctx := r.Context() category := ctx.Value("category").(string) hashes, _ := ctx.Value("hashes").([]string) - torrents := q.qbit.Storage.GetAll("", "", hashes) + torrents := q.Storage.GetAll("", "", hashes) for _, torrent := range torrents { torrent.Category = category - q.qbit.Storage.AddOrUpdate(torrent) + q.Storage.AddOrUpdate(torrent) } request.JSONResponse(w, nil, http.StatusOK) } -func (q *QbitHandler) handleAddTorrentTags(w http.ResponseWriter, r *http.Request) { +func (q *QBit) handleAddTorrentTags(w http.ResponseWriter, r *http.Request) { err := r.ParseForm() if err != nil { http.Error(w, "Failed to parse form data", http.StatusBadRequest) @@ -336,14 +330,14 @@ func (q *QbitHandler) handleAddTorrentTags(w http.ResponseWriter, r *http.Reques for i, tag := range tags { tags[i] = strings.TrimSpace(tag) } - torrents := q.qbit.Storage.GetAll("", "", hashes) + torrents := q.Storage.GetAll("", "", hashes) for _, t := range torrents { - q.qbit.SetTorrentTags(t, tags) + q.SetTorrentTags(t, tags) } request.JSONResponse(w, nil, http.StatusOK) } -func (q *QbitHandler) handleRemoveTorrentTags(w http.ResponseWriter, r *http.Request) { +func (q *QBit) handleRemoveTorrentTags(w http.ResponseWriter, r *http.Request) { err := r.ParseForm() if err != nil { http.Error(w, "Failed to parse form data", http.StatusBadRequest) @@ -355,19 +349,19 @@ func (q *QbitHandler) handleRemoveTorrentTags(w http.ResponseWriter, r *http.Req for i, tag := range tags { tags[i] = strings.TrimSpace(tag) } - torrents := q.qbit.Storage.GetAll("", "", hashes) + torrents := q.Storage.GetAll("", "", hashes) for _, torrent := range torrents { - q.qbit.RemoveTorrentTags(torrent, tags) + q.RemoveTorrentTags(torrent, tags) } request.JSONResponse(w, nil, http.StatusOK) } -func (q *QbitHandler) handleGetTags(w http.ResponseWriter, r *http.Request) { - request.JSONResponse(w, q.qbit.Tags, http.StatusOK) +func (q *QBit) handleGetTags(w http.ResponseWriter, r *http.Request) { + request.JSONResponse(w, q.Tags, http.StatusOK) } -func (q *QbitHandler) handleCreateTags(w http.ResponseWriter, r *http.Request) { +func (q *QBit) handleCreateTags(w http.ResponseWriter, r *http.Request) { err := r.ParseForm() if err != nil { http.Error(w, "Failed to parse form data", http.StatusBadRequest) @@ -377,6 +371,6 @@ func (q *QbitHandler) handleCreateTags(w http.ResponseWriter, r *http.Request) { for i, tag := range tags { tags[i] = strings.TrimSpace(tag) } - q.qbit.AddTags(tags) + q.AddTags(tags) request.JSONResponse(w, nil, http.StatusOK) } diff --git a/pkg/qbit/server/import.go b/pkg/qbit/import.go similarity index 84% rename from pkg/qbit/server/import.go rename to pkg/qbit/import.go index 73231fb..11661f5 100644 --- a/pkg/qbit/server/import.go +++ b/pkg/qbit/import.go @@ -1,14 +1,14 @@ -package server +package qbit import ( "fmt" "github.com/sirrobot01/debrid-blackhole/internal/utils" + "github.com/sirrobot01/debrid-blackhole/pkg/service" "time" "github.com/google/uuid" "github.com/sirrobot01/debrid-blackhole/pkg/arr" "github.com/sirrobot01/debrid-blackhole/pkg/debrid" - "github.com/sirrobot01/debrid-blackhole/pkg/qbit/shared" ) type ImportRequest struct { @@ -63,18 +63,20 @@ func (i *ImportRequest) Complete() { i.CompletedAt = time.Now() } -func (i *ImportRequest) Process(q *shared.QBit) (err error) { +func (i *ImportRequest) Process(q *QBit) (err error) { // Use this for now. // This sends the torrent to the arr + svc := service.GetService() magnet, err := utils.GetMagnetFromUrl(i.URI) if err != nil { return fmt.Errorf("error parsing magnet link: %w", err) } - torrent := q.CreateTorrentFromMagnet(magnet, i.Arr.Name, "manual") - debridTorrent, err := debrid.ProcessTorrent(q.Debrid, magnet, i.Arr, i.IsSymlink) + torrent := CreateTorrentFromMagnet(magnet, i.Arr.Name, "manual") + debridTorrent, err := debrid.ProcessTorrent(svc.Debrid, magnet, i.Arr, i.IsSymlink) if err != nil || debridTorrent == nil { if debridTorrent != nil { - go debridTorrent.Delete() + dbClient := service.GetDebrid().GetByName(debridTorrent.Debrid) + go dbClient.DeleteTorrent(debridTorrent) } if err == nil { err = fmt.Errorf("failed to process torrent") 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/misc.go b/pkg/qbit/misc.go new file mode 100644 index 0000000..3416d05 --- /dev/null +++ b/pkg/qbit/misc.go @@ -0,0 +1,50 @@ +package qbit + +import ( + "github.com/google/uuid" + "github.com/sirrobot01/debrid-blackhole/internal/utils" + debrid "github.com/sirrobot01/debrid-blackhole/pkg/debrid/torrent" + "os" + "path/filepath" + "strings" + "sync" + "time" +) + +func checkFileLoop(wg *sync.WaitGroup, dir string, file debrid.File, ready chan<- debrid.File) { + defer wg.Done() + ticker := time.NewTicker(1 * time.Second) // Check every second + defer ticker.Stop() + path := filepath.Join(dir, file.Path) + for { + select { + case <-ticker.C: + _, err := os.Stat(path) + if !os.IsNotExist(err) { + ready <- file + return + } + } + } +} + +func CreateTorrentFromMagnet(magnet *utils.Magnet, category, source string) *Torrent { + torrent := &Torrent{ + ID: uuid.NewString(), + Hash: strings.ToLower(magnet.InfoHash), + Name: magnet.Name, + Size: magnet.Size, + Category: category, + Source: source, + State: "downloading", + MagnetUri: magnet.Link, + + Tracker: "udp://tracker.opentrackr.org:1337", + UpLimit: -1, + DlLimit: -1, + AutoTmm: false, + Ratio: 1, + RatioLimit: 1, + } + return torrent +} diff --git a/pkg/qbit/shared/qbit.go b/pkg/qbit/qbit.go similarity index 63% rename from pkg/qbit/shared/qbit.go rename to pkg/qbit/qbit.go index 21fa59c..53b3af3 100644 --- a/pkg/qbit/shared/qbit.go +++ b/pkg/qbit/qbit.go @@ -1,12 +1,10 @@ -package shared +package qbit import ( "cmp" "github.com/rs/zerolog" "github.com/sirrobot01/debrid-blackhole/internal/config" - "github.com/sirrobot01/debrid-blackhole/pkg/arr" - "github.com/sirrobot01/debrid-blackhole/pkg/debrid" - "github.com/sirrobot01/debrid-blackhole/pkg/repair" + "github.com/sirrobot01/debrid-blackhole/internal/logger" "os" ) @@ -16,17 +14,14 @@ type QBit struct { Port string `json:"port"` DownloadFolder string `json:"download_folder"` Categories []string `json:"categories"` - Debrid *debrid.DebridService - Repair *repair.Repair Storage *TorrentStorage debug bool logger zerolog.Logger - Arrs *arr.Storage Tags []string RefreshInterval int } -func NewQBit(deb *debrid.DebridService, logger zerolog.Logger, arrs *arr.Storage, _repair *repair.Repair) *QBit { +func New() *QBit { cfg := config.GetConfig().QBitTorrent port := cmp.Or(cfg.Port, os.Getenv("QBIT_PORT"), "8282") refreshInterval := cmp.Or(cfg.RefreshInterval, 10) @@ -36,11 +31,8 @@ func NewQBit(deb *debrid.DebridService, logger zerolog.Logger, arrs *arr.Storage Port: port, DownloadFolder: cfg.DownloadFolder, Categories: cfg.Categories, - Debrid: deb, - Storage: NewTorrentStorage(cmp.Or(os.Getenv("TORRENT_FILE"), "/data/torrents.json")), - Repair: _repair, - logger: logger, - Arrs: arrs, + Storage: NewTorrentStorage(cmp.Or(os.Getenv("TORRENT_FILE"), "/data/qbit_torrents.json")), + logger: logger.NewLogger("qbit", cfg.LogLevel, os.Stdout), RefreshInterval: refreshInterval, } } diff --git a/pkg/qbit/routes.go b/pkg/qbit/routes.go new file mode 100644 index 0000000..b82991a --- /dev/null +++ b/pkg/qbit/routes.go @@ -0,0 +1,47 @@ +package qbit + +import ( + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" + "net/http" +) + +func (q *QBit) Routes() http.Handler { + r := chi.NewRouter() + if q.logger.GetLevel().String() == "debug" { + r.Use(middleware.Logger) + } + r.Use(q.CategoryContext) + r.Post("/auth/login", q.handleLogin) + + r.Group(func(r chi.Router) { + r.Use(q.authContext) + r.Route("/torrents", func(r chi.Router) { + r.Use(HashesCtx) + r.Get("/info", q.handleTorrentsInfo) + r.Post("/add", q.handleTorrentsAdd) + r.Post("/delete", q.handleTorrentsDelete) + r.Get("/categories", q.handleCategories) + r.Post("/createCategory", q.handleCreateCategory) + r.Post("/setCategory", q.handleSetCategory) + r.Post("/addTags", q.handleAddTorrentTags) + r.Post("/removeTags", q.handleRemoveTorrentTags) + r.Post("/createTags", q.handleCreateTags) + r.Get("/tags", q.handleGetTags) + r.Get("/pause", q.handleTorrentsPause) + r.Get("/resume", q.handleTorrentsResume) + r.Get("/recheck", q.handleTorrentRecheck) + r.Get("/properties", q.handleTorrentProperties) + r.Get("/files", q.handleTorrentFiles) + }) + + r.Route("/app", func(r chi.Router) { + r.Get("/version", q.handleVersion) + r.Get("/webapiVersion", q.handleWebAPIVersion) + r.Get("/preferences", q.handlePreferences) + r.Get("/buildInfo", q.handleBuildInfo) + r.Get("/shutdown", q.handleShutdown) + }) + }) + return r +} 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/ui_auth_handlers.go b/pkg/qbit/server/ui_auth_handlers.go deleted file mode 100644 index cfcad82..0000000 --- a/pkg/qbit/server/ui_auth_handlers.go +++ /dev/null @@ -1,50 +0,0 @@ -package server - -import ( - "github.com/sirrobot01/debrid-blackhole/internal/config" - "golang.org/x/crypto/bcrypt" - "net/http" -) - -func (u *UIHandler) authMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Check if setup is needed - cfg := config.GetConfig() - if cfg.NeedsSetup() && r.URL.Path != "/setup" { - http.Redirect(w, r, "/setup", http.StatusSeeOther) - return - } - - // Skip auth check for setup page - if r.URL.Path == "/setup" { - next.ServeHTTP(w, r) - return - } - - session, _ := store.Get(r, "auth-session") - auth, ok := session.Values["authenticated"].(bool) - - if !ok || !auth { - http.Redirect(w, r, "/login", http.StatusSeeOther) - return - } - - next.ServeHTTP(w, r) - }) -} - -func (u *UIHandler) verifyAuth(username, password string) bool { - // If you're storing hashed password, use bcrypt to compare - if username == "" { - return false - } - auth := config.GetConfig().GetAuth() - if auth == nil { - return false - } - if username != auth.Username { - return false - } - err := bcrypt.CompareHashAndPassword([]byte(auth.Password), []byte(password)) - return err == nil -} diff --git a/pkg/qbit/server/ui_routes.go b/pkg/qbit/server/ui_routes.go deleted file mode 100644 index fffe35d..0000000 --- a/pkg/qbit/server/ui_routes.go +++ /dev/null @@ -1,33 +0,0 @@ -package server - -import ( - "github.com/go-chi/chi/v5" - "net/http" -) - -func (u *UIHandler) Routes(r chi.Router) http.Handler { - r.Get("/login", u.LoginHandler) - r.Post("/login", u.LoginHandler) - r.Get("/setup", u.SetupHandler) - r.Post("/setup", u.SetupHandler) - - r.Group(func(r chi.Router) { - r.Use(u.authMiddleware) - r.Get("/", u.IndexHandler) - r.Get("/download", u.DownloadHandler) - r.Get("/repair", u.RepairHandler) - r.Get("/config", u.ConfigHandler) - r.Route("/internal", func(r chi.Router) { - r.Get("/arrs", u.handleGetArrs) - r.Post("/add", u.handleAddContent) - r.Get("/cached", u.handleCheckCached) - r.Post("/repair", u.handleRepairMedia) - r.Get("/torrents", u.handleGetTorrents) - r.Delete("/torrents/{hash}", u.handleDeleteTorrent) - r.Get("/config", u.handleGetConfig) - r.Get("/version", u.handleGetVersion) - }) - }) - - return r -} diff --git a/pkg/qbit/shared/utils.go b/pkg/qbit/shared/utils.go deleted file mode 100644 index 0a864e1..0000000 --- a/pkg/qbit/shared/utils.go +++ /dev/null @@ -1,26 +0,0 @@ -package shared - -import ( - "github.com/sirrobot01/debrid-blackhole/pkg/debrid" - "os" - "path/filepath" - "sync" - "time" -) - -func checkFileLoop(wg *sync.WaitGroup, dir string, file debrid.TorrentFile, ready chan<- debrid.TorrentFile) { - defer wg.Done() - ticker := time.NewTicker(1 * time.Second) // Check every second - defer ticker.Stop() - path := filepath.Join(dir, file.Path) - for { - select { - case <-ticker.C: - _, err := os.Stat(path) - if !os.IsNotExist(err) { - ready <- file - return - } - } - } -} diff --git a/pkg/qbit/shared/storage.go b/pkg/qbit/storage.go similarity index 99% rename from pkg/qbit/shared/storage.go rename to pkg/qbit/storage.go index 16ce342..06f6445 100644 --- a/pkg/qbit/shared/storage.go +++ b/pkg/qbit/storage.go @@ -1,4 +1,4 @@ -package shared +package qbit import ( "encoding/json" diff --git a/pkg/qbit/shared/torrent.go b/pkg/qbit/torrent.go similarity index 86% rename from pkg/qbit/shared/torrent.go rename to pkg/qbit/torrent.go index cb56113..9650378 100644 --- a/pkg/qbit/shared/torrent.go +++ b/pkg/qbit/torrent.go @@ -1,13 +1,14 @@ -package shared +package qbit import ( "cmp" "context" "fmt" - "github.com/google/uuid" "github.com/sirrobot01/debrid-blackhole/internal/utils" "github.com/sirrobot01/debrid-blackhole/pkg/arr" - "github.com/sirrobot01/debrid-blackhole/pkg/debrid" + db "github.com/sirrobot01/debrid-blackhole/pkg/debrid" + debrid "github.com/sirrobot01/debrid-blackhole/pkg/debrid/torrent" + "github.com/sirrobot01/debrid-blackhole/pkg/service" "io" "mime/multipart" "os" @@ -47,16 +48,18 @@ func (q *QBit) AddTorrent(ctx context.Context, fileHeader *multipart.FileHeader, } func (q *QBit) Process(ctx context.Context, magnet *utils.Magnet, category string) error { - torrent := q.CreateTorrentFromMagnet(magnet, category, "auto") + svc := service.GetService() + torrent := CreateTorrentFromMagnet(magnet, category, "auto") a, ok := ctx.Value("arr").(*arr.Arr) if !ok { return fmt.Errorf("arr not found in context") } isSymlink := ctx.Value("isSymlink").(bool) - debridTorrent, err := debrid.ProcessTorrent(q.Debrid, magnet, a, isSymlink) + debridTorrent, err := db.ProcessTorrent(svc.Debrid, magnet, a, isSymlink) if err != nil || debridTorrent == nil { if debridTorrent != nil { - go debridTorrent.Delete() + dbClient := service.GetDebrid().GetByName(debridTorrent.Debrid) + go dbClient.DeleteTorrent(debridTorrent) } if err == nil { err = fmt.Errorf("failed to process torrent") @@ -69,36 +72,16 @@ func (q *QBit) Process(ctx context.Context, magnet *utils.Magnet, category strin return nil } -func (q *QBit) CreateTorrentFromMagnet(magnet *utils.Magnet, category, source string) *Torrent { - torrent := &Torrent{ - ID: uuid.NewString(), - Hash: strings.ToLower(magnet.InfoHash), - Name: magnet.Name, - Size: magnet.Size, - Category: category, - Source: source, - State: "downloading", - MagnetUri: magnet.Link, - - Tracker: "udp://tracker.opentrackr.org:1337", - UpLimit: -1, - DlLimit: -1, - AutoTmm: false, - Ratio: 1, - RatioLimit: 1, - } - return torrent -} - func (q *QBit) ProcessFiles(torrent *Torrent, debridTorrent *debrid.Torrent, arr *arr.Arr, isSymlink bool) { + debridClient := service.GetDebrid().GetByName(debridTorrent.Debrid) for debridTorrent.Status != "downloaded" { progress := debridTorrent.Progress - q.logger.Debug().Msgf("%s -> (%s) Download Progress: %.2f%%", debridTorrent.Debrid.GetName(), debridTorrent.Name, progress) + q.logger.Debug().Msgf("%s -> (%s) Download Progress: %.2f%%", debridTorrent.Debrid, debridTorrent.Name, progress) time.Sleep(10 * time.Second) - dbT, err := debridTorrent.Debrid.CheckStatus(debridTorrent, isSymlink) + dbT, err := debridClient.CheckStatus(debridTorrent, isSymlink) if err != nil { q.logger.Error().Msgf("Error checking status: %v", err) - go debridTorrent.Delete() + go debridClient.DeleteTorrent(debridTorrent) q.MarkAsFailed(torrent) _ = arr.Refresh() return @@ -118,7 +101,7 @@ func (q *QBit) ProcessFiles(torrent *Torrent, debridTorrent *debrid.Torrent, arr } if err != nil { q.MarkAsFailed(torrent) - go debridTorrent.Delete() + go debridClient.DeleteTorrent(debridTorrent) q.logger.Info().Msgf("Error: %v", err) return } @@ -159,7 +142,7 @@ func (q *QBit) UpdateTorrentMin(t *Torrent, debridTorrent *debrid.Torrent) *Torr t.Name = debridTorrent.Name t.AddedOn = addedOn.Unix() t.DebridTorrent = debridTorrent - t.Debrid = debridTorrent.Debrid.GetName() + t.Debrid = debridTorrent.Debrid t.Size = totalSize t.Completed = sizeCompleted t.Downloaded = sizeCompleted @@ -177,17 +160,17 @@ func (q *QBit) UpdateTorrentMin(t *Torrent, debridTorrent *debrid.Torrent) *Torr } func (q *QBit) UpdateTorrent(t *Torrent, debridTorrent *debrid.Torrent) *Torrent { - db := debridTorrent.Debrid - rcLoneMount := db.GetMountPath() + _db := service.GetDebrid().GetByName(debridTorrent.Debrid) + rcLoneMount := _db.GetMountPath() if debridTorrent == nil && t.ID != "" { - debridTorrent, _ = db.GetTorrent(t.ID) + debridTorrent, _ = _db.GetTorrent(t.ID) } if debridTorrent == nil { - q.logger.Info().Msgf("Torrent with ID %s not found in %s", t.ID, db.GetName()) + q.logger.Info().Msgf("Torrent with ID %s not found in %s", t.ID, _db.GetName()) return t } if debridTorrent.Status != "downloaded" { - debridTorrent, _ = db.GetTorrent(t.ID) + debridTorrent, _ = _db.GetTorrent(t.ID) } if t.TorrentPath == "" { diff --git a/pkg/qbit/shared/types.go b/pkg/qbit/types.go similarity index 98% rename from pkg/qbit/shared/types.go rename to pkg/qbit/types.go index 0a27953..4febf98 100644 --- a/pkg/qbit/shared/types.go +++ b/pkg/qbit/types.go @@ -1,7 +1,7 @@ -package shared +package qbit import ( - "github.com/sirrobot01/debrid-blackhole/pkg/debrid" + "github.com/sirrobot01/debrid-blackhole/pkg/debrid/torrent" "sync" ) @@ -172,10 +172,10 @@ type TorrentCategory struct { } type Torrent struct { - ID string `json:"-"` - DebridTorrent *debrid.Torrent `json:"-"` - Debrid string `json:"debrid"` - TorrentPath string `json:"-"` + ID string `json:"-"` + DebridTorrent *torrent.Torrent `json:"-"` + Debrid string `json:"debrid"` + TorrentPath string `json:"-"` AddedOn int64 `json:"added_on,omitempty"` AmountLeft int64 `json:"amount_left"` diff --git a/pkg/qbit/shared/worker.go b/pkg/qbit/worker.go similarity index 83% rename from pkg/qbit/shared/worker.go rename to pkg/qbit/worker.go index c2b710f..2d517be 100644 --- a/pkg/qbit/shared/worker.go +++ b/pkg/qbit/worker.go @@ -1,7 +1,8 @@ -package shared +package qbit import ( "context" + "github.com/sirrobot01/debrid-blackhole/pkg/service" "time" ) @@ -28,7 +29,8 @@ func (q *QBit) StartRefreshWorker(ctx context.Context) { } func (q *QBit) RefreshArrs() { - for _, arr := range q.Arrs.GetAll() { + arrs := service.GetService().Arr + for _, arr := range arrs.GetAll() { err := arr.Refresh() if err != nil { return diff --git a/pkg/repair/repair.go b/pkg/repair/repair.go index 9be58da..a37d0e7 100644 --- a/pkg/repair/repair.go +++ b/pkg/repair/repair.go @@ -8,7 +8,7 @@ import ( "github.com/sirrobot01/debrid-blackhole/internal/config" "github.com/sirrobot01/debrid-blackhole/internal/logger" "github.com/sirrobot01/debrid-blackhole/pkg/arr" - "github.com/sirrobot01/debrid-blackhole/pkg/debrid" + "github.com/sirrobot01/debrid-blackhole/pkg/debrid/engine" "log" "net/http" "net/url" @@ -24,8 +24,8 @@ import ( type Repair struct { Jobs []Job `json:"jobs"` - deb debrid.Service arrs *arr.Storage + deb engine.Service duration time.Duration runOnStart bool ZurgURL string @@ -33,16 +33,16 @@ type Repair struct { logger zerolog.Logger } -func NewRepair(deb debrid.Service, arrs *arr.Storage) *Repair { +func New(deb *engine.Engine, arrs *arr.Storage) *Repair { cfg := config.GetConfig() duration, err := parseSchedule(cfg.Repair.Interval) if err != nil { duration = time.Hour * 24 } r := &Repair{ - deb: deb, - logger: logger.NewLogger("Repair", cfg.LogLevel, os.Stdout), arrs: arrs, + deb: deb.Get(), + logger: logger.NewLogger("repair", cfg.LogLevel, os.Stdout), duration: duration, runOnStart: cfg.Repair.RunOnStart, ZurgURL: cfg.Repair.ZurgURL, diff --git a/pkg/qbit/server/server.go b/pkg/server/server.go similarity index 65% rename from pkg/qbit/server/server.go rename to pkg/server/server.go index 41b5a2e..424c380 100644 --- a/pkg/qbit/server/server.go +++ b/pkg/server/server.go @@ -9,10 +9,6 @@ import ( "github.com/rs/zerolog" "github.com/sirrobot01/debrid-blackhole/internal/config" "github.com/sirrobot01/debrid-blackhole/internal/logger" - "github.com/sirrobot01/debrid-blackhole/pkg/arr" - "github.com/sirrobot01/debrid-blackhole/pkg/debrid" - "github.com/sirrobot01/debrid-blackhole/pkg/qbit/shared" - "github.com/sirrobot01/debrid-blackhole/pkg/repair" "io" "net/http" "os" @@ -21,45 +17,32 @@ import ( ) type Server struct { - qbit *shared.QBit + router *chi.Mux logger zerolog.Logger } -func NewServer(deb *debrid.DebridService, arrs *arr.Storage, _repair *repair.Repair) *Server { +func New() *Server { cfg := config.GetConfig() - l := logger.NewLogger("QBit", cfg.QBitTorrent.LogLevel, os.Stdout) - q := shared.NewQBit(deb, l, arrs, _repair) + l := logger.NewLogger("http", cfg.QBitTorrent.LogLevel, os.Stdout) + r := chi.NewRouter() + r.Use(middleware.Recoverer) + r.Handle("/static/*", http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) + return &Server{ - qbit: q, + router: r, logger: l, } } func (s *Server) Start(ctx context.Context) error { - r := chi.NewRouter() - r.Use(middleware.Recoverer) - r.Handle("/static/*", http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) - logLevel := s.logger.GetLevel().String() - debug := logLevel == "debug" - q := QbitHandler{qbit: s.qbit, logger: s.logger, debug: debug} - ui := UIHandler{ - qbit: s.qbit, - logger: logger.NewLogger("UI", s.logger.GetLevel().String(), os.Stdout), - debug: debug, - } - + cfg := config.GetConfig() // Register routes - r.Get("/logs", s.GetLogs) - q.Routes(r) - ui.Routes(r) - - go s.qbit.StartWorker(context.Background()) - - s.logger.Info().Msgf("Starting QBit server on :%s", s.qbit.Port) - port := fmt.Sprintf(":%s", s.qbit.Port) + s.router.Get("/logs", s.getLogs) + port := fmt.Sprintf(":%s", cfg.QBitTorrent.Port) + s.logger.Info().Msgf("Starting server on %s", port) srv := &http.Server{ Addr: port, - Handler: r, + Handler: s.router, } ctx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM) @@ -77,7 +60,15 @@ func (s *Server) Start(ctx context.Context) error { return srv.Shutdown(context.Background()) } -func (s *Server) GetLogs(w http.ResponseWriter, r *http.Request) { +func (s *Server) AddRoutes(routes func(r chi.Router) http.Handler) { + routes(s.router) +} + +func (s *Server) Mount(pattern string, handler http.Handler) { + s.router.Mount(pattern, handler) +} + +func (s *Server) getLogs(w http.ResponseWriter, r *http.Request) { logFile := logger.GetLogPath() // Open and read the file diff --git a/pkg/service/service.go b/pkg/service/service.go new file mode 100644 index 0000000..acc36bd --- /dev/null +++ b/pkg/service/service.go @@ -0,0 +1,59 @@ +package service + +import ( + "github.com/sirrobot01/debrid-blackhole/pkg/arr" + "github.com/sirrobot01/debrid-blackhole/pkg/debrid" + "github.com/sirrobot01/debrid-blackhole/pkg/debrid/cache" + "github.com/sirrobot01/debrid-blackhole/pkg/debrid/engine" + "github.com/sirrobot01/debrid-blackhole/pkg/repair" + "sync" +) + +type Service struct { + Repair *repair.Repair + Arr *arr.Storage + Debrid *engine.Engine + DebridCache *cache.Manager +} + +var ( + instance *Service + once sync.Once +) + +func New() *Service { + once.Do(func() { + arrs := arr.NewStorage() + deb := debrid.New() + instance = &Service{ + Repair: repair.New(deb, arrs), + Arr: arrs, + Debrid: deb, + DebridCache: cache.NewManager(deb), + } + }) + return instance +} + +// GetService returns the singleton instance +func GetService() *Service { + if instance == nil { + instance = New() + } + return instance +} + +func Update() *Service { + arrs := arr.NewStorage() + deb := debrid.New() + instance = &Service{ + Repair: repair.New(deb, arrs), + Arr: arrs, + Debrid: deb, + } + return instance +} + +func GetDebrid() *engine.Engine { + return GetService().Debrid +} diff --git a/pkg/web/routes.go b/pkg/web/routes.go new file mode 100644 index 0000000..67889bc --- /dev/null +++ b/pkg/web/routes.go @@ -0,0 +1,34 @@ +package web + +import ( + "github.com/go-chi/chi/v5" + "net/http" +) + +func (ui *Handler) Routes() http.Handler { + r := chi.NewRouter() + + r.Get("/login", ui.LoginHandler) + r.Post("/login", ui.LoginHandler) + r.Get("/setup", ui.SetupHandler) + r.Post("/setup", ui.SetupHandler) + + r.Group(func(r chi.Router) { + r.Use(ui.authMiddleware) + r.Get("/", ui.IndexHandler) + r.Get("/download", ui.DownloadHandler) + r.Get("/repair", ui.RepairHandler) + r.Get("/config", ui.ConfigHandler) + r.Route("/internal", func(r chi.Router) { + r.Get("/arrs", ui.handleGetArrs) + r.Post("/add", ui.handleAddContent) + r.Post("/repair", ui.handleRepairMedia) + r.Get("/torrents", ui.handleGetTorrents) + r.Delete("/torrents/{hash}", ui.handleDeleteTorrent) + r.Get("/config", ui.handleGetConfig) + r.Get("/version", ui.handleGetVersion) + }) + }) + + return r +} diff --git a/pkg/qbit/server/ui_handlers.go b/pkg/web/ui.go similarity index 65% rename from pkg/qbit/server/ui_handlers.go rename to pkg/web/ui.go index a9b6f13..740de4d 100644 --- a/pkg/qbit/server/ui_handlers.go +++ b/pkg/web/ui.go @@ -1,4 +1,4 @@ -package server +package web import ( "embed" @@ -6,18 +6,20 @@ import ( "fmt" "github.com/gorilla/sessions" "github.com/sirrobot01/debrid-blackhole/internal/config" + "github.com/sirrobot01/debrid-blackhole/internal/logger" "github.com/sirrobot01/debrid-blackhole/internal/request" "github.com/sirrobot01/debrid-blackhole/internal/utils" + "github.com/sirrobot01/debrid-blackhole/pkg/qbit" + "github.com/sirrobot01/debrid-blackhole/pkg/service" "golang.org/x/crypto/bcrypt" "html/template" "net/http" + "os" "strings" "github.com/go-chi/chi/v5" "github.com/rs/zerolog" "github.com/sirrobot01/debrid-blackhole/pkg/arr" - "github.com/sirrobot01/debrid-blackhole/pkg/debrid" - "github.com/sirrobot01/debrid-blackhole/pkg/qbit/shared" "github.com/sirrobot01/debrid-blackhole/pkg/version" ) @@ -49,13 +51,20 @@ type RepairRequest struct { Async bool `json:"async"` } -//go:embed templates/* +//go:embed web/* var content embed.FS -type UIHandler struct { - qbit *shared.QBit +type Handler struct { + qbit *qbit.QBit logger zerolog.Logger - debug bool +} + +func New(qbit *qbit.QBit) *Handler { + cfg := config.GetConfig() + return &Handler{ + qbit: qbit, + logger: logger.NewLogger("ui", cfg.LogLevel, os.Stdout), + } } var ( @@ -66,13 +75,13 @@ var ( func init() { templates = template.Must(template.ParseFS( content, - "templates/layout.html", - "templates/index.html", - "templates/download.html", - "templates/repair.html", - "templates/config.html", - "templates/login.html", - "templates/setup.html", + "web/layout.html", + "web/index.html", + "web/download.html", + "web/repair.html", + "web/config.html", + "web/login.html", + "web/setup.html", )) store.Options = &sessions.Options{ @@ -82,7 +91,50 @@ func init() { } } -func (u *UIHandler) LoginHandler(w http.ResponseWriter, r *http.Request) { +func (ui *Handler) authMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Check if setup is needed + cfg := config.GetConfig() + if cfg.NeedsSetup() && r.URL.Path != "/setup" { + http.Redirect(w, r, "/setup", http.StatusSeeOther) + return + } + + // Skip auth check for setup page + if r.URL.Path == "/setup" { + next.ServeHTTP(w, r) + return + } + + session, _ := store.Get(r, "auth-session") + auth, ok := session.Values["authenticated"].(bool) + + if !ok || !auth { + http.Redirect(w, r, "/login", http.StatusSeeOther) + return + } + + next.ServeHTTP(w, r) + }) +} + +func (ui *Handler) verifyAuth(username, password string) bool { + // If you're storing hashed password, use bcrypt to compare + if username == "" { + return false + } + auth := config.GetConfig().GetAuth() + if auth == nil { + return false + } + if username != auth.Username { + return false + } + err := bcrypt.CompareHashAndPassword([]byte(auth.Password), []byte(password)) + return err == nil +} + +func (ui *Handler) LoginHandler(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { data := map[string]interface{}{ "Page": "login", @@ -104,11 +156,14 @@ func (u *UIHandler) LoginHandler(w http.ResponseWriter, r *http.Request) { return } - if u.verifyAuth(credentials.Username, credentials.Password) { + if ui.verifyAuth(credentials.Username, credentials.Password) { session, _ := store.Get(r, "auth-session") session.Values["authenticated"] = true session.Values["username"] = credentials.Username - session.Save(r, w) + if err := session.Save(r, w); err != nil { + http.Error(w, "Error saving session", http.StatusInternalServerError) + return + } http.Redirect(w, r, "/", http.StatusSeeOther) return } @@ -116,15 +171,18 @@ func (u *UIHandler) LoginHandler(w http.ResponseWriter, r *http.Request) { http.Error(w, "Invalid credentials", http.StatusUnauthorized) } -func (u *UIHandler) LogoutHandler(w http.ResponseWriter, r *http.Request) { +func (ui *Handler) LogoutHandler(w http.ResponseWriter, r *http.Request) { session, _ := store.Get(r, "auth-session") session.Values["authenticated"] = false session.Options.MaxAge = -1 - session.Save(r, w) + err := session.Save(r, w) + if err != nil { + return + } http.Redirect(w, r, "/login", http.StatusSeeOther) } -func (u *UIHandler) SetupHandler(w http.ResponseWriter, r *http.Request) { +func (ui *Handler) SetupHandler(w http.ResponseWriter, r *http.Request) { cfg := config.GetConfig() authCfg := cfg.GetAuth() @@ -174,12 +232,15 @@ func (u *UIHandler) SetupHandler(w http.ResponseWriter, r *http.Request) { session, _ := store.Get(r, "auth-session") session.Values["authenticated"] = true session.Values["username"] = username - session.Save(r, w) + if err := session.Save(r, w); err != nil { + http.Error(w, "Error saving session", http.StatusInternalServerError) + return + } http.Redirect(w, r, "/", http.StatusSeeOther) } -func (u *UIHandler) IndexHandler(w http.ResponseWriter, r *http.Request) { +func (ui *Handler) IndexHandler(w http.ResponseWriter, r *http.Request) { data := map[string]interface{}{ "Page": "index", "Title": "Torrents", @@ -190,7 +251,7 @@ func (u *UIHandler) IndexHandler(w http.ResponseWriter, r *http.Request) { } } -func (u *UIHandler) DownloadHandler(w http.ResponseWriter, r *http.Request) { +func (ui *Handler) DownloadHandler(w http.ResponseWriter, r *http.Request) { data := map[string]interface{}{ "Page": "download", "Title": "Download", @@ -201,7 +262,7 @@ func (u *UIHandler) DownloadHandler(w http.ResponseWriter, r *http.Request) { } } -func (u *UIHandler) RepairHandler(w http.ResponseWriter, r *http.Request) { +func (ui *Handler) RepairHandler(w http.ResponseWriter, r *http.Request) { data := map[string]interface{}{ "Page": "repair", "Title": "Repair", @@ -212,7 +273,7 @@ func (u *UIHandler) RepairHandler(w http.ResponseWriter, r *http.Request) { } } -func (u *UIHandler) ConfigHandler(w http.ResponseWriter, r *http.Request) { +func (ui *Handler) ConfigHandler(w http.ResponseWriter, r *http.Request) { data := map[string]interface{}{ "Page": "config", "Title": "Config", @@ -223,23 +284,25 @@ func (u *UIHandler) ConfigHandler(w http.ResponseWriter, r *http.Request) { } } -func (u *UIHandler) handleGetArrs(w http.ResponseWriter, r *http.Request) { - request.JSONResponse(w, u.qbit.Arrs.GetAll(), http.StatusOK) +func (ui *Handler) handleGetArrs(w http.ResponseWriter, r *http.Request) { + svc := service.GetService() + request.JSONResponse(w, svc.Arr.GetAll(), http.StatusOK) } -func (u *UIHandler) handleAddContent(w http.ResponseWriter, r *http.Request) { +func (ui *Handler) handleAddContent(w http.ResponseWriter, r *http.Request) { if err := r.ParseMultipartForm(32 << 20); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } + svc := service.GetService() - results := make([]*ImportRequest, 0) + results := make([]*qbit.ImportRequest, 0) errs := make([]string, 0) arrName := r.FormValue("arr") notSymlink := r.FormValue("notSymlink") == "true" - _arr := u.qbit.Arrs.Get(arrName) + _arr := svc.Arr.Get(arrName) if _arr == nil { _arr = arr.NewArr(arrName, "", "", arr.Sonarr) } @@ -254,8 +317,8 @@ func (u *UIHandler) handleAddContent(w http.ResponseWriter, r *http.Request) { } for _, url := range urlList { - importReq := NewImportRequest(url, _arr, !notSymlink) - err := importReq.Process(u.qbit) + importReq := qbit.NewImportRequest(url, _arr, !notSymlink) + err := importReq.Process(ui.qbit) if err != nil { errs = append(errs, fmt.Sprintf("URL %s: %v", url, err)) continue @@ -279,8 +342,8 @@ func (u *UIHandler) handleAddContent(w http.ResponseWriter, r *http.Request) { continue } - importReq := NewImportRequest(magnet.Link, _arr, !notSymlink) - err = importReq.Process(u.qbit) + importReq := qbit.NewImportRequest(magnet.Link, _arr, !notSymlink) + err = importReq.Process(ui.qbit) if err != nil { errs = append(errs, fmt.Sprintf("File %s: %v", fileHeader.Filename, err)) continue @@ -290,54 +353,24 @@ func (u *UIHandler) handleAddContent(w http.ResponseWriter, r *http.Request) { } request.JSONResponse(w, struct { - Results []*ImportRequest `json:"results"` - Errors []string `json:"errors,omitempty"` + Results []*qbit.ImportRequest `json:"results"` + Errors []string `json:"errors,omitempty"` }{ Results: results, Errors: errs, }, http.StatusOK) } -func (u *UIHandler) handleCheckCached(w http.ResponseWriter, r *http.Request) { - _hashes := r.URL.Query().Get("hash") - if _hashes == "" { - http.Error(w, "No hashes provided", http.StatusBadRequest) - return - } - hashes := strings.Split(_hashes, ",") - if len(hashes) == 0 { - http.Error(w, "No hashes provided", http.StatusBadRequest) - return - } - db := r.URL.Query().Get("debrid") - var deb debrid.Service - if db == "" { - // use the first debrid - deb = u.qbit.Debrid.Get() - } else { - deb = u.qbit.Debrid.GetByName(db) - } - if deb == nil { - http.Error(w, "Invalid debrid", http.StatusBadRequest) - return - } - res := deb.IsAvailable(hashes) - result := make(map[string]bool) - for _, h := range hashes { - _, exists := res[h] - result[h] = exists - } - request.JSONResponse(w, result, http.StatusOK) -} - -func (u *UIHandler) handleRepairMedia(w http.ResponseWriter, r *http.Request) { +func (ui *Handler) handleRepairMedia(w http.ResponseWriter, r *http.Request) { var req RepairRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } - _arr := u.qbit.Arrs.Get(req.ArrName) + svc := service.GetService() + + _arr := svc.Arr.Get(req.ArrName) if _arr == nil { http.Error(w, "No Arrs found to repair", http.StatusNotFound) return @@ -345,15 +378,15 @@ func (u *UIHandler) handleRepairMedia(w http.ResponseWriter, r *http.Request) { if req.Async { go func() { - if err := u.qbit.Repair.Repair([]*arr.Arr{_arr}, req.MediaIds); err != nil { - u.logger.Error().Err(err).Msg("Failed to repair media") + if err := svc.Repair.Repair([]*arr.Arr{_arr}, req.MediaIds); err != nil { + ui.logger.Error().Err(err).Msg("Failed to repair media") } }() request.JSONResponse(w, "Repair process started", http.StatusOK) return } - if err := u.qbit.Repair.Repair([]*arr.Arr{_arr}, req.MediaIds); err != nil { + if err := svc.Repair.Repair([]*arr.Arr{_arr}, req.MediaIds); err != nil { http.Error(w, fmt.Sprintf("Failed to repair: %v", err), http.StatusInternalServerError) return @@ -362,29 +395,30 @@ func (u *UIHandler) handleRepairMedia(w http.ResponseWriter, r *http.Request) { request.JSONResponse(w, "Repair completed", http.StatusOK) } -func (u *UIHandler) handleGetVersion(w http.ResponseWriter, r *http.Request) { +func (ui *Handler) handleGetVersion(w http.ResponseWriter, r *http.Request) { v := version.GetInfo() request.JSONResponse(w, v, http.StatusOK) } -func (u *UIHandler) handleGetTorrents(w http.ResponseWriter, r *http.Request) { - request.JSONResponse(w, u.qbit.Storage.GetAll("", "", nil), http.StatusOK) +func (ui *Handler) handleGetTorrents(w http.ResponseWriter, r *http.Request) { + request.JSONResponse(w, ui.qbit.Storage.GetAll("", "", nil), http.StatusOK) } -func (u *UIHandler) handleDeleteTorrent(w http.ResponseWriter, r *http.Request) { +func (ui *Handler) handleDeleteTorrent(w http.ResponseWriter, r *http.Request) { hash := chi.URLParam(r, "hash") if hash == "" { http.Error(w, "No hash provided", http.StatusBadRequest) return } - u.qbit.Storage.Delete(hash) + ui.qbit.Storage.Delete(hash) w.WriteHeader(http.StatusOK) } -func (u *UIHandler) handleGetConfig(w http.ResponseWriter, r *http.Request) { +func (ui *Handler) handleGetConfig(w http.ResponseWriter, r *http.Request) { cfg := config.GetConfig() arrCfgs := make([]config.Arr, 0) - for _, a := range u.qbit.Arrs.GetAll() { + svc := service.GetService() + for _, a := range svc.Arr.GetAll() { arrCfgs = append(arrCfgs, config.Arr{Host: a.Host, Name: a.Name, Token: a.Token}) } cfg.Arrs = arrCfgs diff --git a/pkg/qbit/server/templates/config.html b/pkg/web/web/config.html similarity index 100% rename from pkg/qbit/server/templates/config.html rename to pkg/web/web/config.html diff --git a/pkg/qbit/server/templates/download.html b/pkg/web/web/download.html similarity index 100% rename from pkg/qbit/server/templates/download.html rename to pkg/web/web/download.html diff --git a/pkg/qbit/server/templates/index.html b/pkg/web/web/index.html similarity index 100% rename from pkg/qbit/server/templates/index.html rename to pkg/web/web/index.html diff --git a/pkg/qbit/server/templates/layout.html b/pkg/web/web/layout.html similarity index 100% rename from pkg/qbit/server/templates/layout.html rename to pkg/web/web/layout.html diff --git a/pkg/qbit/server/templates/login.html b/pkg/web/web/login.html similarity index 100% rename from pkg/qbit/server/templates/login.html rename to pkg/web/web/login.html diff --git a/pkg/qbit/server/templates/repair.html b/pkg/web/web/repair.html similarity index 100% rename from pkg/qbit/server/templates/repair.html rename to pkg/web/web/repair.html diff --git a/pkg/qbit/server/templates/setup.html b/pkg/web/web/setup.html similarity index 100% rename from pkg/qbit/server/templates/setup.html rename to pkg/web/web/setup.html From 6f4f72d7816630785b044563a3de18b1b3a360b7 Mon Sep 17 00:00:00 2001 From: Mukhtar Akere Date: Thu, 13 Feb 2025 02:20:45 +0100 Subject: [PATCH 2/4] hotfix auth checks --- pkg/web/ui.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/web/ui.go b/pkg/web/ui.go index 740de4d..43bb34a 100644 --- a/pkg/web/ui.go +++ b/pkg/web/ui.go @@ -100,6 +100,11 @@ func (ui *Handler) authMiddleware(next http.Handler) http.Handler { return } + if !cfg.UseAuth { + next.ServeHTTP(w, r) + return + } + // Skip auth check for setup page if r.URL.Path == "/setup" { next.ServeHTTP(w, r) From bfd2596367768ff426b7476541db3c2ac5855da3 Mon Sep 17 00:00:00 2001 From: Mukhtar Akere Date: Thu, 13 Feb 2025 05:07:14 +0100 Subject: [PATCH 3/4] fix mounts; backward compatibility --- Dockerfile | 17 +- README.md | 11 +- cmd/{ => decypharr}/main.go | 2 +- common/regex.go | 2 +- internal/config/config.go | 11 +- internal/logger/logger.go | 6 +- internal/request/request.go | 2 +- main.go | 10 +- pkg/debrid/alldebrid/alldebrid.go | 11 - pkg/debrid/cache/cache.go | 360 -------------------------- pkg/debrid/debrid.go | 2 +- pkg/debrid/debrid_link/debrid_link.go | 6 - pkg/debrid/engine/service.go | 1 - pkg/debrid/realdebrid/realdebrid.go | 10 +- pkg/debrid/torbox/torbox.go | 8 +- pkg/qbit/downloader.go | 31 ++- pkg/qbit/import.go | 1 + pkg/qbit/qbit.go | 6 +- pkg/qbit/routes.go | 4 - pkg/qbit/torrent.go | 21 +- pkg/service/service.go | 15 +- 21 files changed, 71 insertions(+), 466 deletions(-) rename cmd/{ => decypharr}/main.go (98%) delete mode 100644 pkg/debrid/cache/cache.go diff --git a/Dockerfile b/Dockerfile index fb462c6..34a6212 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 @@ -51,14 +51,15 @@ 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 ["/usr/bin/healthcheck"] -CMD ["/usr/bin/blackhole", "--config", "/data/config.json"] \ 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 98% rename from cmd/main.go rename to cmd/decypharr/main.go index fe7b3b8..b46dc35 100644 --- a/cmd/main.go +++ b/cmd/decypharr/main.go @@ -1,4 +1,4 @@ -package cmd +package decypharr import ( "context" 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/internal/config/config.go b/internal/config/config.go index 3464958..41b5337 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -197,21 +197,16 @@ 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 { - _, err := fmt.Fprintf(os.Stderr, "Configuration Error: %v\n", err) + _, err := fmt.Fprintf(os.Stderr, "configuration Error: %v\n", err) if err != nil { return } diff --git a/internal/logger/logger.go b/internal/logger/logger.go index 77cb840..1a9e8fc 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -20,8 +20,10 @@ func GetLogPath() string { 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") diff --git a/internal/request/request.go b/internal/request/request.go index a310aa2..6326590 100644 --- a/internal/request/request.go +++ b/internal/request/request.go @@ -105,7 +105,7 @@ func (c *RLHTTPClient) MakeRequest(req *http.Request) ([]byte, error) { if err != nil { return nil, err } - statusOk := res.StatusCode == http.StatusOK || res.StatusCode == http.StatusCreated + 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))...) 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/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/cache/cache.go b/pkg/debrid/cache/cache.go deleted file mode 100644 index 348a421..0000000 --- a/pkg/debrid/cache/cache.go +++ /dev/null @@ -1,360 +0,0 @@ -package cache - -import ( - "bufio" - "encoding/json" - "fmt" - "github.com/rs/zerolog" - "github.com/sirrobot01/debrid-blackhole/internal/logger" - "os" - "path/filepath" - "runtime" - "sync" - "time" - - "github.com/sirrobot01/debrid-blackhole/internal/config" - "github.com/sirrobot01/debrid-blackhole/pkg/debrid/engine" - "github.com/sirrobot01/debrid-blackhole/pkg/debrid/torrent" -) - -type DownloadLinkCache struct { - Link string `json:"download_link"` -} - -type CachedTorrent struct { - *torrent.Torrent - LastRead time.Time `json:"last_read"` - IsComplete bool `json:"is_complete"` - DownloadLinks map[string]DownloadLinkCache `json:"download_links"` -} - -var ( - _logInstance zerolog.Logger - once sync.Once -) - -func getLogger() zerolog.Logger { - once.Do(func() { - _logInstance = logger.NewLogger("cache", "info", os.Stdout) - }) - return _logInstance -} - -type Cache struct { - dir string - client engine.Service - torrents *sync.Map // key: torrent.Id, value: *CachedTorrent - torrentsNames *sync.Map // key: torrent.Name, value: torrent.Id - LastUpdated time.Time `json:"last_updated"` -} - -type Manager struct { - caches map[string]*Cache -} - -func NewManager(debridService *engine.Engine) *Manager { - cfg := config.GetConfig() - cm := &Manager{ - caches: make(map[string]*Cache), - } - for _, debrid := range debridService.GetDebrids() { - c := New(debrid, cfg.Path) - cm.caches[debrid.GetName()] = c - } - return cm -} - -func (m *Manager) GetCaches() map[string]*Cache { - return m.caches -} - -func (m *Manager) GetCache(debridName string) *Cache { - return m.caches[debridName] -} - -func New(debridService engine.Service, basePath string) *Cache { - return &Cache{ - dir: filepath.Join(basePath, "cache", debridService.GetName(), "torrents"), - torrents: &sync.Map{}, - torrentsNames: &sync.Map{}, - client: debridService, - } -} - -func (c *Cache) Start() error { - _logger := getLogger() - _logger.Info().Msg("Starting cache for: " + c.client.GetName()) - if err := c.Load(); err != nil { - return fmt.Errorf("failed to load cache: %v", err) - } - if err := c.Sync(); err != nil { - return fmt.Errorf("failed to sync cache: %v", err) - } - return nil -} - -func (c *Cache) Load() error { - _logger := getLogger() - - if err := os.MkdirAll(c.dir, 0755); err != nil { - return fmt.Errorf("failed to create cache directory: %w", err) - } - - files, err := os.ReadDir(c.dir) - if err != nil { - return fmt.Errorf("failed to read cache directory: %w", err) - } - - for _, file := range files { - if file.IsDir() || filepath.Ext(file.Name()) != ".json" { - continue - } - - filePath := filepath.Join(c.dir, file.Name()) - data, err := os.ReadFile(filePath) - if err != nil { - _logger.Debug().Err(err).Msgf("Failed to read file: %s", filePath) - continue - } - - var ct CachedTorrent - if err := json.Unmarshal(data, &ct); err != nil { - _logger.Debug().Err(err).Msgf("Failed to unmarshal file: %s", filePath) - continue - } - if len(ct.Files) > 0 { - c.torrents.Store(ct.Torrent.Id, &ct) - c.torrentsNames.Store(ct.Torrent.Name, ct.Torrent.Id) - } - } - - return nil -} - -func (c *Cache) GetTorrent(id string) *CachedTorrent { - if value, ok := c.torrents.Load(id); ok { - return value.(*CachedTorrent) - } - return nil -} - -func (c *Cache) GetTorrentByName(name string) *CachedTorrent { - if id, ok := c.torrentsNames.Load(name); ok { - return c.GetTorrent(id.(string)) - } - return nil -} - -func (c *Cache) SaveTorrent(ct *CachedTorrent) error { - data, err := json.MarshalIndent(ct, "", " ") - if err != nil { - return fmt.Errorf("failed to marshal torrent: %w", err) - } - - fileName := ct.Torrent.Id + ".json" - filePath := filepath.Join(c.dir, fileName) - tmpFile := filePath + ".tmp" - - f, err := os.Create(tmpFile) - if err != nil { - return fmt.Errorf("failed to create temp file: %w", err) - } - defer f.Close() - - w := bufio.NewWriter(f) - if _, err := w.Write(data); err != nil { - return fmt.Errorf("failed to write data: %w", err) - } - - if err := w.Flush(); err != nil { - return fmt.Errorf("failed to flush data: %w", err) - } - - return os.Rename(tmpFile, filePath) -} - -func (c *Cache) SaveAll() error { - const batchSize = 100 - var wg sync.WaitGroup - _logger := getLogger() - - tasks := make(chan *CachedTorrent, batchSize) - - for i := 0; i < runtime.NumCPU(); i++ { - wg.Add(1) - go func() { - defer wg.Done() - for ct := range tasks { - if err := c.SaveTorrent(ct); err != nil { - _logger.Error().Err(err).Msg("failed to save torrent") - } - } - }() - } - - c.torrents.Range(func(_, value interface{}) bool { - tasks <- value.(*CachedTorrent) - return true - }) - - close(tasks) - wg.Wait() - c.LastUpdated = time.Now() - return nil -} - -func (c *Cache) Sync() error { - _logger := getLogger() - torrents, err := c.client.GetTorrents() - if err != nil { - return fmt.Errorf("failed to sync torrents: %v", err) - } - - workers := runtime.NumCPU() * 200 - workChan := make(chan *torrent.Torrent, len(torrents)) - errChan := make(chan error, len(torrents)) - - var wg sync.WaitGroup - - for i := 0; i < workers; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for t := range workChan { - if err := c.processTorrent(t); err != nil { - errChan <- err - } - } - }() - } - - for _, t := range torrents { - workChan <- t - } - close(workChan) - - wg.Wait() - close(errChan) - - for err := range errChan { - _logger.Error().Err(err).Msg("sync error") - } - - _logger.Info().Msgf("Synced %d torrents", len(torrents)) - return nil -} - -func (c *Cache) processTorrent(t *torrent.Torrent) error { - if existing, ok := c.torrents.Load(t.Id); ok { - ct := existing.(*CachedTorrent) - if ct.IsComplete { - return nil - } - } - c.AddTorrent(t) - return nil -} - -func (c *Cache) AddTorrent(t *torrent.Torrent) { - _logger := getLogger() - - if len(t.Files) == 0 { - tNew, err := c.client.GetTorrent(t.Id) - _logger.Debug().Msgf("Getting torrent files for %s", t.Id) - if err != nil { - _logger.Debug().Msgf("Failed to get torrent files for %s: %v", t.Id, err) - return - } - t = tNew - } - - if len(t.Files) == 0 { - _logger.Debug().Msgf("No files found for %s", t.Id) - return - } - - ct := &CachedTorrent{ - Torrent: t, - LastRead: time.Now(), - IsComplete: len(t.Files) > 0, - DownloadLinks: make(map[string]DownloadLinkCache), - } - - c.torrents.Store(t.Id, ct) - c.torrentsNames.Store(t.Name, t.Id) - - go func() { - if err := c.SaveTorrent(ct); err != nil { - _logger.Debug().Err(err).Msgf("Failed to save torrent %s", t.Id) - } - }() -} - -func (c *Cache) RefreshTorrent(torrentId string) *CachedTorrent { - _logger := getLogger() - - t, err := c.client.GetTorrent(torrentId) - if err != nil { - _logger.Debug().Msgf("Failed to get torrent files for %s: %v", torrentId, err) - return nil - } - if len(t.Files) == 0 { - return nil - } - - ct := &CachedTorrent{ - Torrent: t, - LastRead: time.Now(), - IsComplete: len(t.Files) > 0, - DownloadLinks: make(map[string]DownloadLinkCache), - } - - c.torrents.Store(t.Id, ct) - c.torrentsNames.Store(t.Name, t.Id) - - go func() { - if err := c.SaveTorrent(ct); err != nil { - _logger.Debug().Err(err).Msgf("Failed to save torrent %s", t.Id) - } - }() - - return ct -} - -func (c *Cache) GetFileDownloadLink(t *CachedTorrent, file *torrent.File) (string, error) { - _logger := getLogger() - - if linkCache, ok := t.DownloadLinks[file.Id]; ok { - return linkCache.Link, nil - } - - if file.Link == "" { - t = c.RefreshTorrent(t.Id) - if t == nil { - return "", fmt.Errorf("torrent not found") - } - file = t.Torrent.GetFile(file.Id) - } - - _logger.Debug().Msgf("Getting download link for %s", t.Name) - link := c.client.GetDownloadLink(t.Torrent, file) - if link == nil { - return "", fmt.Errorf("download link not found") - } - - t.DownloadLinks[file.Id] = DownloadLinkCache{ - Link: link.DownloadLink, - } - - go func() { - if err := c.SaveTorrent(t); err != nil { - _logger.Debug().Err(err).Msgf("Failed to save torrent %s", t.Id) - } - }() - - return link.DownloadLink, nil -} - -func (c *Cache) GetTorrents() *sync.Map { - return c.torrents -} diff --git a/pkg/debrid/debrid.go b/pkg/debrid/debrid.go index fb382f8..40eeba1 100644 --- a/pkg/debrid/debrid.go +++ b/pkg/debrid/debrid.go @@ -81,7 +81,7 @@ func ProcessTorrent(d *engine.Engine, magnet *utils.Magnet, a *arr.Arr, isSymlin errs = append(errs, err) continue } - logger.Info().Msgf("Torrent: %s submitted to %s", dbt.Name, db.GetName()) + logger.Info().Msgf("Torrent: %s(id=%s) submitted to %s", dbt.Name, dbt.Id, db.GetName()) d.LastUsed = index return db.CheckStatus(dbt, isSymlink) } 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/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/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/qbit/downloader.go b/pkg/qbit/downloader.go index 0a4c8c3..710a5c7 100644 --- a/pkg/qbit/downloader.go +++ b/pkg/qbit/downloader.go @@ -52,14 +52,13 @@ Loop: 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 } @@ -140,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) @@ -173,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/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/torrent.go b/pkg/qbit/torrent.go index 9650378..99689a6 100644 --- a/pkg/qbit/torrent.go +++ b/pkg/qbit/torrent.go @@ -90,14 +90,14 @@ func (q *QBit) ProcessFiles(torrent *Torrent, debridTorrent *debrid.Torrent, arr torrent = q.UpdateTorrentMin(torrent, debridTorrent) } var ( - torrentPath string - err error + torrentSymlinkPath string + err error ) debridTorrent.Arr = arr if isSymlink { - torrentPath, err = q.ProcessSymlink(torrent) + torrentSymlinkPath, err = q.ProcessSymlink(torrent) // /mnt/symlinks/{category}/MyTVShow/ } else { - torrentPath, err = q.ProcessManualFile(torrent) + torrentSymlinkPath, err = q.ProcessManualFile(torrent) } if err != nil { q.MarkAsFailed(torrent) @@ -105,7 +105,7 @@ func (q *QBit) ProcessFiles(torrent *Torrent, debridTorrent *debrid.Torrent, arr q.logger.Info().Msgf("Error: %v", err) return } - torrent.TorrentPath = filepath.Base(torrentPath) + torrent.TorrentPath = torrentSymlinkPath q.UpdateTorrent(torrent, debridTorrent) _ = arr.Refresh() } @@ -161,7 +161,6 @@ func (q *QBit) UpdateTorrentMin(t *Torrent, debridTorrent *debrid.Torrent) *Torr func (q *QBit) UpdateTorrent(t *Torrent, debridTorrent *debrid.Torrent) *Torrent { _db := service.GetDebrid().GetByName(debridTorrent.Debrid) - rcLoneMount := _db.GetMountPath() if debridTorrent == nil && t.ID != "" { debridTorrent, _ = _db.GetTorrent(t.ID) } @@ -172,15 +171,9 @@ func (q *QBit) UpdateTorrent(t *Torrent, debridTorrent *debrid.Torrent) *Torrent if debridTorrent.Status != "downloaded" { debridTorrent, _ = _db.GetTorrent(t.ID) } - - if t.TorrentPath == "" { - tPath, _ := debridTorrent.GetMountFolder(rcLoneMount) - t.TorrentPath = filepath.Base(tPath) - } - savePath := filepath.Join(q.DownloadFolder, t.Category) + string(os.PathSeparator) - torrentPath := filepath.Join(savePath, t.TorrentPath) + string(os.PathSeparator) t = q.UpdateTorrentMin(t, debridTorrent) - t.ContentPath = torrentPath + t.ContentPath = t.TorrentPath + string(os.PathSeparator) + t.SavePath = t.ContentPath if t.IsReady() { t.State = "pausedUP" diff --git a/pkg/service/service.go b/pkg/service/service.go index acc36bd..7eb5315 100644 --- a/pkg/service/service.go +++ b/pkg/service/service.go @@ -3,17 +3,15 @@ package service import ( "github.com/sirrobot01/debrid-blackhole/pkg/arr" "github.com/sirrobot01/debrid-blackhole/pkg/debrid" - "github.com/sirrobot01/debrid-blackhole/pkg/debrid/cache" "github.com/sirrobot01/debrid-blackhole/pkg/debrid/engine" "github.com/sirrobot01/debrid-blackhole/pkg/repair" "sync" ) type Service struct { - Repair *repair.Repair - Arr *arr.Storage - Debrid *engine.Engine - DebridCache *cache.Manager + Repair *repair.Repair + Arr *arr.Storage + Debrid *engine.Engine } var ( @@ -26,10 +24,9 @@ func New() *Service { arrs := arr.NewStorage() deb := debrid.New() instance = &Service{ - Repair: repair.New(deb, arrs), - Arr: arrs, - Debrid: deb, - DebridCache: cache.NewManager(deb), + Repair: repair.New(deb, arrs), + Arr: arrs, + Debrid: deb, } }) return instance From 4b2f601df2a41b9ce10d9dbf70a86b130347ed17 Mon Sep 17 00:00:00 2001 From: Mukhtar Akere Date: Thu, 13 Feb 2025 14:19:07 +0100 Subject: [PATCH 4/4] hotfix qbittorent --- pkg/qbit/torrent.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/qbit/torrent.go b/pkg/qbit/torrent.go index 99689a6..4f8c1d9 100644 --- a/pkg/qbit/torrent.go +++ b/pkg/qbit/torrent.go @@ -173,7 +173,6 @@ func (q *QBit) UpdateTorrent(t *Torrent, debridTorrent *debrid.Torrent) *Torrent } t = q.UpdateTorrentMin(t, debridTorrent) t.ContentPath = t.TorrentPath + string(os.PathSeparator) - t.SavePath = t.ContentPath if t.IsReady() { t.State = "pausedUP"