From 2b2a682218346c3cd1e817052022b6a6ad6a1b2f Mon Sep 17 00:00:00 2001 From: Mukhtar Akere Date: Sun, 9 Mar 2025 03:56:34 +0100 Subject: [PATCH] - Fix ARR flaky bug - Refined download uncached options - Deprecate qbittorent log level - Skip Repair for specified arr --- CHANGELOG.md | 7 +- README.md | 4 +- doc/config.full.json | 20 +++-- internal/config/config.go | 11 +-- pkg/arr/arr.go | 30 ++++--- pkg/arr/content.go | 112 ++++++++++++++++++-------- pkg/arr/types.go | 15 ++-- pkg/debrid/alldebrid/alldebrid.go | 10 +-- pkg/debrid/debrid.go | 13 +-- pkg/debrid/debrid_link/debrid_link.go | 9 +-- pkg/debrid/engine/service.go | 2 +- pkg/debrid/realdebrid/realdebrid.go | 8 +- pkg/debrid/torbox/torbox.go | 10 +-- pkg/debrid/torrent/torrent.go | 25 +----- pkg/qbit/downloader.go | 2 +- pkg/qbit/http.go | 2 +- pkg/qbit/import.go | 36 +++++---- pkg/qbit/qbit.go | 2 +- pkg/qbit/torrent.go | 4 +- pkg/repair/repair.go | 53 +++++++++--- pkg/server/server.go | 2 +- pkg/server/webhook.go | 2 +- pkg/web/{ui.go => server.go} | 21 +++-- pkg/web/web/config.html | 28 +++---- pkg/web/web/download.html | 27 +++++-- pkg/web/web/repair.html | 2 +- 26 files changed, 276 insertions(+), 181 deletions(-) rename pkg/web/{ui.go => server.go} (95%) diff --git a/CHANGELOG.md b/CHANGELOG.md index d12d947..71ad6e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -158,4 +158,9 @@ - Discord Notifications - Minor bug fixes - Add Tautulli support - - playback_failed event triggers a repair \ No newline at end of file + - playback_failed event triggers a repair +- Miscellaneous improvements + - Add an option to skip the repair worker for a specific arr + - Arr specific uncached downloading option + - Option to download uncached torrents from UI +- Remove QbitTorrent Log level(Use the global log level) \ No newline at end of file diff --git a/README.md b/README.md index 363b6e9..7a9eb79 100644 --- a/README.md +++ b/README.md @@ -140,14 +140,14 @@ This is the default config file. You can create a `config.json` file in the root "port": "8282", "download_folder": "/mnt/symlinks/", "categories": ["sonarr", "radarr"], - "log_level": "info" }, "repair": { "enabled": false, "interval": "12h", "run_on_start": false }, - "use_auth": false + "use_auth": false, + "log_level": "info" } ``` diff --git a/doc/config.full.json b/doc/config.full.json index fc927d9..b3107e5 100644 --- a/doc/config.full.json +++ b/doc/config.full.json @@ -51,20 +51,30 @@ "download_folder": "/mnt/symlinks/", "categories": ["sonarr", "radarr"], "refresh_interval": 5, - "log_level": "info" }, "arrs": [ { "name": "sonarr", - "host": "http://host:8989", + "host": "http://radarr:8989", "token": "arr_key", - "cleanup": true + "cleanup": true, + "skip_repair": true, + "download_uncached": false }, { "name": "radarr", - "host": "http://host:7878", + "host": "http://radarr:7878", "token": "arr_key", - "cleanup": false + "cleanup": false, + "download_uncached": false + }, + { + "name": "lidarr", + "host": "http://lidarr:7878", + "token": "arr_key", + "cleanup": false, + "skip_repair": true, + "download_uncached": false } ], "repair": { diff --git a/internal/config/config.go b/internal/config/config.go index 206bec8..70f4372 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -38,17 +38,18 @@ type QBitTorrent struct { Username string `json:"username"` Password string `json:"password"` Port string `json:"port"` - LogLevel string `json:"log_level"` DownloadFolder string `json:"download_folder"` Categories []string `json:"categories"` RefreshInterval int `json:"refresh_interval"` } type Arr struct { - Name string `json:"name"` - Host string `json:"host"` - Token string `json:"token"` - Cleanup bool `json:"cleanup"` + Name string `json:"name"` + Host string `json:"host"` + Token string `json:"token"` + Cleanup bool `json:"cleanup"` + SkipRepair bool `json:"skip_repair"` + DownloadUncached bool `json:"download_uncached"` } type Repair struct { diff --git a/pkg/arr/arr.go b/pkg/arr/arr.go index 94e1d18..808b8b7 100644 --- a/pkg/arr/arr.go +++ b/pkg/arr/arr.go @@ -25,21 +25,25 @@ const ( ) type Arr struct { - Name string `json:"name"` - Host string `json:"host"` - Token string `json:"token"` - Type Type `json:"type"` - Cleanup bool `json:"cleanup"` - client *http.Client + Name string `json:"name"` + Host string `json:"host"` + Token string `json:"token"` + Type Type `json:"type"` + Cleanup bool `json:"cleanup"` + SkipRepair bool `json:"skip_repair"` + DownloadUncached bool `json:"download_uncached"` + client *http.Client } -func New(name, host, token string, cleanup bool) *Arr { +func New(name, host, token string, cleanup, skipRepair, downloadUncached bool) *Arr { return &Arr{ - Name: name, - Host: host, - Token: strings.TrimSpace(token), - Type: InferType(host, name), - Cleanup: cleanup, + Name: name, + Host: host, + Token: strings.TrimSpace(token), + Type: InferType(host, name), + Cleanup: cleanup, + SkipRepair: skipRepair, + DownloadUncached: downloadUncached, client: &http.Client{ Transport: &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, @@ -142,7 +146,7 @@ func NewStorage() *Storage { arrs := make(map[string]*Arr) for _, a := range config.GetConfig().Arrs { name := a.Name - arrs[name] = New(name, a.Host, a.Token, a.Cleanup) + arrs[name] = New(name, a.Host, a.Token, a.Cleanup, a.SkipRepair, a.DownloadUncached) } return &Storage{ Arrs: arrs, diff --git a/pkg/arr/content.go b/pkg/arr/content.go index 3bd314f..9f2e192 100644 --- a/pkg/arr/content.go +++ b/pkg/arr/content.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "strconv" + "strings" ) type episode struct { @@ -12,6 +13,17 @@ type episode struct { EpisodeFileID int `json:"episodeFileId"` } +type sonarrSearch struct { + Name string `json:"name"` + SeasonNumber int `json:"seasonNumber"` + SeriesId int `json:"episodeIds"` +} + +type radarrSearch struct { + Name string `json:"name"` + MovieIds []int `json:"movieIds"` +} + func (a *Arr) GetMedia(mediaId string) ([]Content, error) { // Get series if a.Type == Radarr { @@ -80,9 +92,10 @@ func (a *Arr) GetMedia(mediaId string) ([]Content, error) { continue } files = append(files, ContentFile{ - FileId: file.Id, - Path: file.Path, - Id: eId, + FileId: file.Id, + Path: file.Path, + Id: eId, + SeasonNumber: file.SeasonNumber, }) } if len(files) == 0 { @@ -132,29 +145,64 @@ func GetMovies(a *Arr, tvId string) ([]Content, error) { return contents, nil } -func (a *Arr) search(ids []int) error { - var payload interface{} - switch a.Type { - case Sonarr: - payload = struct { - Name string `json:"name"` - EpisodeIds []int `json:"episodeIds"` - }{ - Name: "EpisodeSearch", - EpisodeIds: ids, - } - case Radarr: - payload = struct { - Name string `json:"name"` - MovieIds []int `json:"movieIds"` - }{ - Name: "MoviesSearch", - MovieIds: ids, - } - default: - return fmt.Errorf("unknown arr type: %s", a.Type) +// searchSonarr searches for missing files in the arr +// map ids are series id and season number +func (a *Arr) searchSonarr(files []ContentFile) error { + ids := make(map[string]any) + for _, f := range files { + // Join series id and season number + id := fmt.Sprintf("%d-%d", f.Id, f.SeasonNumber) + ids[id] = nil } + errs := make(chan error, len(ids)) + for id := range ids { + go func() { + parts := strings.Split(id, "-") + if len(parts) != 2 { + return + } + seriesId, err := strconv.Atoi(parts[0]) + if err != nil { + return + } + seasonNumber, err := strconv.Atoi(parts[1]) + if err != nil { + return + } + payload := sonarrSearch{ + Name: "SeasonSearch", + SeasonNumber: seasonNumber, + SeriesId: seriesId, + } + resp, err := a.Request(http.MethodPost, "api/v3/command", payload) + if err != nil { + errs <- fmt.Errorf("failed to automatic search: %v", err) + return + } + if statusOk := strconv.Itoa(resp.StatusCode)[0] == '2'; !statusOk { + errs <- fmt.Errorf("failed to automatic search. Status Code: %s", resp.Status) + return + } + }() + } + for range ids { + err := <-errs + if err != nil { + return err + } + } + return nil +} +func (a *Arr) searchRadarr(files []ContentFile) error { + ids := make([]int, 0) + for _, f := range files { + ids = append(ids, f.Id) + } + payload := radarrSearch{ + Name: "MoviesSearch", + MovieIds: ids, + } resp, err := a.Request(http.MethodPost, "api/v3/command", payload) if err != nil { return fmt.Errorf("failed to automatic search: %v", err) @@ -166,16 +214,14 @@ func (a *Arr) search(ids []int) error { } func (a *Arr) SearchMissing(files []ContentFile) error { - - ids := make([]int, 0) - for _, f := range files { - ids = append(ids, f.Id) + switch a.Type { + case Sonarr: + return a.searchSonarr(files) + case Radarr: + return a.searchRadarr(files) + default: + return fmt.Errorf("unknown arr type: %s", a.Type) } - - if len(ids) == 0 { - return nil - } - return a.search(ids) } func (a *Arr) DeleteFiles(files []ContentFile) error { diff --git a/pkg/arr/types.go b/pkg/arr/types.go index 0d77195..a951f3c 100644 --- a/pkg/arr/types.go +++ b/pkg/arr/types.go @@ -14,13 +14,14 @@ type Movie struct { } type ContentFile struct { - Name string `json:"name"` - Path string `json:"path"` - Id int `json:"id"` - FileId int `json:"fileId"` - TargetPath string `json:"targetPath"` - IsSymlink bool `json:"isSymlink"` - IsBroken bool `json:"isBroken"` + Name string `json:"name"` + Path string `json:"path"` + Id int `json:"id"` + FileId int `json:"fileId"` + TargetPath string `json:"targetPath"` + IsSymlink bool `json:"isSymlink"` + IsBroken bool `json:"isBroken"` + SeasonNumber int `json:"seasonNumber"` } type Content struct { diff --git a/pkg/debrid/alldebrid/alldebrid.go b/pkg/debrid/alldebrid/alldebrid.go index 9c46c2c..190b5cb 100644 --- a/pkg/debrid/alldebrid/alldebrid.go +++ b/pkg/debrid/alldebrid/alldebrid.go @@ -135,9 +135,8 @@ func flattenFiles(files []MagnetFile, parentPath string, index *int) []torrent.F 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) +func (ad *AllDebrid) GetTorrent(t *torrent.Torrent) (*torrent.Torrent, error) { + url := fmt.Sprintf("%s/magnet/status?id=%s", ad.Host, t.Id) req, _ := http.NewRequest(http.MethodGet, url, nil) resp, err := ad.client.MakeRequest(req) if err != nil { @@ -152,7 +151,6 @@ func (ad *AllDebrid) GetTorrent(id string) (*torrent.Torrent, error) { data := res.Data.Magnets status := getAlldebridStatus(data.StatusCode) name := data.Filename - t.Id = id t.Name = name t.Status = status t.Filename = name @@ -176,7 +174,7 @@ func (ad *AllDebrid) GetTorrent(id string) (*torrent.Torrent, error) { func (ad *AllDebrid) CheckStatus(torrent *torrent.Torrent, isSymlink bool) (*torrent.Torrent, error) { for { - tb, err := ad.GetTorrent(torrent.Id) + tb, err := ad.GetTorrent(torrent) torrent = tb @@ -194,7 +192,7 @@ func (ad *AllDebrid) CheckStatus(torrent *torrent.Torrent, isSymlink bool) (*tor } break } else if slices.Contains(ad.GetDownloadingStatus(), status) { - if !ad.DownloadUncached { + if !ad.DownloadUncached && !torrent.DownloadUncached { return torrent, fmt.Errorf("torrent: %s not cached", torrent.Name) } // Break out of the loop if the torrent is downloading. diff --git a/pkg/debrid/debrid.go b/pkg/debrid/debrid.go index f1afc90..cabda74 100644 --- a/pkg/debrid/debrid.go +++ b/pkg/debrid/debrid.go @@ -47,13 +47,14 @@ func createDebrid(dc config.Debrid, cache *cache.Cache) engine.Service { } } -func ProcessTorrent(d *engine.Engine, magnet *utils.Magnet, a *arr.Arr, isSymlink bool) (*torrent.Torrent, error) { +func ProcessTorrent(d *engine.Engine, magnet *utils.Magnet, a *arr.Arr, isSymlink, downloadUncached bool) (*torrent.Torrent, error) { debridTorrent := &torrent.Torrent{ - InfoHash: magnet.InfoHash, - Magnet: magnet, - Name: magnet.Name, - Arr: a, - Size: magnet.Size, + InfoHash: magnet.InfoHash, + Magnet: magnet, + Name: magnet.Name, + Arr: a, + Size: magnet.Size, + DownloadUncached: cmp.Or(downloadUncached, a.DownloadUncached), } errs := make([]error, 0) diff --git a/pkg/debrid/debrid_link/debrid_link.go b/pkg/debrid/debrid_link/debrid_link.go index e2c399a..059eaf5 100644 --- a/pkg/debrid/debrid_link/debrid_link.go +++ b/pkg/debrid/debrid_link/debrid_link.go @@ -97,9 +97,8 @@ func (dl *DebridLink) IsAvailable(infohashes []string) map[string]bool { 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) +func (dl *DebridLink) GetTorrent(t *torrent.Torrent) (*torrent.Torrent, error) { + url := fmt.Sprintf("%s/seedbox/list?ids=%s", dl.Host, t.Id) req, _ := http.NewRequest(http.MethodGet, url, nil) resp, err := dl.client.MakeRequest(req) if err != nil { @@ -204,7 +203,7 @@ func (dl *DebridLink) SubmitMagnet(t *torrent.Torrent) (*torrent.Torrent, error) func (dl *DebridLink) CheckStatus(torrent *torrent.Torrent, isSymlink bool) (*torrent.Torrent, error) { for { - t, err := dl.GetTorrent(torrent.Id) + t, err := dl.GetTorrent(torrent) torrent = t if err != nil || torrent == nil { return torrent, err @@ -218,7 +217,7 @@ func (dl *DebridLink) CheckStatus(torrent *torrent.Torrent, isSymlink bool) (*to } break } else if slices.Contains(dl.GetDownloadingStatus(), status) { - if !dl.DownloadUncached { + if !dl.DownloadUncached && !torrent.DownloadUncached { return torrent, fmt.Errorf("torrent: %s not cached", torrent.Name) } // Break out of the loop if the torrent is downloading. diff --git a/pkg/debrid/engine/service.go b/pkg/debrid/engine/service.go index 05819f0..a670b79 100644 --- a/pkg/debrid/engine/service.go +++ b/pkg/debrid/engine/service.go @@ -13,7 +13,7 @@ type Service interface { DeleteTorrent(tr *torrent.Torrent) IsAvailable(infohashes []string) map[string]bool GetCheckCached() bool - GetTorrent(id string) (*torrent.Torrent, error) + GetTorrent(torrent *torrent.Torrent) (*torrent.Torrent, error) GetTorrents() ([]*torrent.Torrent, error) GetName() string GetLogger() zerolog.Logger diff --git a/pkg/debrid/realdebrid/realdebrid.go b/pkg/debrid/realdebrid/realdebrid.go index 2298b6f..7a64ff7 100644 --- a/pkg/debrid/realdebrid/realdebrid.go +++ b/pkg/debrid/realdebrid/realdebrid.go @@ -160,9 +160,8 @@ func (r *RealDebrid) SubmitMagnet(t *torrent.Torrent) (*torrent.Torrent, error) 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) +func (r *RealDebrid) GetTorrent(t *torrent.Torrent) (*torrent.Torrent, error) { + url := fmt.Sprintf("%s/torrents/info/%s", r.Host, t.Id) req, _ := http.NewRequest(http.MethodGet, url, nil) resp, err := r.client.MakeRequest(req) if err != nil { @@ -174,7 +173,6 @@ func (r *RealDebrid) GetTorrent(id string) (*torrent.Torrent, error) { return t, err } name := utils.RemoveInvalidChars(data.OriginalFilename) - t.Id = id t.Name = name t.Bytes = data.Bytes t.Folder = name @@ -251,7 +249,7 @@ func (r *RealDebrid) CheckStatus(t *torrent.Torrent, isSymlink bool) (*torrent.T } break } else if slices.Contains(r.GetDownloadingStatus(), status) { - if !r.DownloadUncached { + if !r.DownloadUncached && !t.DownloadUncached { return t, fmt.Errorf("torrent: %s not cached", t.Name) } // Break out of the loop if the torrent is downloading. diff --git a/pkg/debrid/torbox/torbox.go b/pkg/debrid/torbox/torbox.go index e2ebc9b..d95c7f2 100644 --- a/pkg/debrid/torbox/torbox.go +++ b/pkg/debrid/torbox/torbox.go @@ -149,9 +149,8 @@ func getTorboxStatus(status string, finished bool) string { } } -func (tb *Torbox) GetTorrent(id string) (*torrent.Torrent, error) { - t := &torrent.Torrent{} - url := fmt.Sprintf("%s/api/torrents/mylist/?id=%s", tb.Host, id) +func (tb *Torbox) GetTorrent(t *torrent.Torrent) (*torrent.Torrent, error) { + url := fmt.Sprintf("%s/api/torrents/mylist/?id=%s", tb.Host, t.Id) req, _ := http.NewRequest(http.MethodGet, url, nil) resp, err := tb.client.MakeRequest(req) if err != nil { @@ -164,7 +163,6 @@ func (tb *Torbox) GetTorrent(id string) (*torrent.Torrent, error) { } data := res.Data name := data.Name - t.Id = id t.Name = name t.Bytes = data.Size t.Folder = name @@ -215,7 +213,7 @@ func (tb *Torbox) GetTorrent(id string) (*torrent.Torrent, error) { func (tb *Torbox) CheckStatus(torrent *torrent.Torrent, isSymlink bool) (*torrent.Torrent, error) { for { - t, err := tb.GetTorrent(torrent.Id) + t, err := tb.GetTorrent(torrent) torrent = t @@ -233,7 +231,7 @@ func (tb *Torbox) CheckStatus(torrent *torrent.Torrent, isSymlink bool) (*torren } break } else if slices.Contains(tb.GetDownloadingStatus(), status) { - if !tb.DownloadUncached { + if !tb.DownloadUncached && !torrent.DownloadUncached { return torrent, fmt.Errorf("torrent: %s not cached", torrent.Name) } // Break out of the loop if the torrent is downloading. diff --git a/pkg/debrid/torrent/torrent.go b/pkg/debrid/torrent/torrent.go index 282151b..9f48e87 100644 --- a/pkg/debrid/torrent/torrent.go +++ b/pkg/debrid/torrent/torrent.go @@ -11,24 +11,6 @@ import ( "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"` @@ -51,9 +33,10 @@ type Torrent struct { Debrid string `json:"debrid"` - Arr *arr.Arr `json:"arr"` - Mu sync.Mutex `json:"-"` - SizeDownloaded int64 `json:"-"` // This is used for local download + Arr *arr.Arr `json:"arr"` + Mu sync.Mutex `json:"-"` + SizeDownloaded int64 `json:"-"` // This is used for local download + DownloadUncached bool `json:"-"` } type DownloadLinks struct { diff --git a/pkg/qbit/downloader.go b/pkg/qbit/downloader.go index af6ef9e..c9ae894 100644 --- a/pkg/qbit/downloader.go +++ b/pkg/qbit/downloader.go @@ -187,7 +187,7 @@ func (q *QBit) getTorrentPath(rclonePath string, debridTorrent *debrid.Torrent) q.logger.Debug().Msgf("Found torrent path: %s", torrentPath) return torrentPath, err } - time.Sleep(10 * time.Millisecond) + time.Sleep(100 * time.Millisecond) } } diff --git a/pkg/qbit/http.go b/pkg/qbit/http.go index ea5b6a7..bd57c36 100644 --- a/pkg/qbit/http.go +++ b/pkg/qbit/http.go @@ -59,7 +59,7 @@ func (q *QBit) authContext(next http.Handler) http.Handler { // Check if arr exists a := svc.Arr.Get(category) if a == nil { - a = arr.New(category, "", "", false) + a = arr.New(category, "", "", false, false, false) } if err == nil { host = strings.TrimSpace(host) diff --git a/pkg/qbit/import.go b/pkg/qbit/import.go index 11661f5..a46f21f 100644 --- a/pkg/qbit/import.go +++ b/pkg/qbit/import.go @@ -12,14 +12,15 @@ import ( ) type ImportRequest struct { - ID string `json:"id"` - Path string `json:"path"` - URI string `json:"uri"` - Arr *arr.Arr `json:"arr"` - IsSymlink bool `json:"isSymlink"` - SeriesId int `json:"series"` - Seasons []int `json:"seasons"` - Episodes []string `json:"episodes"` + ID string `json:"id"` + Path string `json:"path"` + URI string `json:"uri"` + Arr *arr.Arr `json:"arr"` + IsSymlink bool `json:"isSymlink"` + SeriesId int `json:"series"` + Seasons []int `json:"seasons"` + Episodes []string `json:"episodes"` + DownloadUncached bool `json:"downloadUncached"` Failed bool `json:"failed"` FailedAt time.Time `json:"failedAt"` @@ -40,15 +41,16 @@ type ManualImportResponseSchema struct { Id int `json:"id"` } -func NewImportRequest(uri string, arr *arr.Arr, isSymlink bool) *ImportRequest { +func NewImportRequest(uri string, arr *arr.Arr, isSymlink, downloadUncached bool) *ImportRequest { return &ImportRequest{ - ID: uuid.NewString(), - URI: uri, - Arr: arr, - Failed: false, - Completed: false, - Async: false, - IsSymlink: isSymlink, + ID: uuid.NewString(), + URI: uri, + Arr: arr, + Failed: false, + Completed: false, + Async: false, + IsSymlink: isSymlink, + DownloadUncached: downloadUncached, } } @@ -72,7 +74,7 @@ func (i *ImportRequest) Process(q *QBit) (err error) { return fmt.Errorf("error parsing magnet link: %w", err) } torrent := CreateTorrentFromMagnet(magnet, i.Arr.Name, "manual") - debridTorrent, err := debrid.ProcessTorrent(svc.Debrid, magnet, i.Arr, i.IsSymlink) + debridTorrent, err := debrid.ProcessTorrent(svc.Debrid, magnet, i.Arr, i.IsSymlink, i.DownloadUncached) if err != nil || debridTorrent == nil { if debridTorrent != nil { dbClient := service.GetDebrid().GetByName(debridTorrent.Debrid) diff --git a/pkg/qbit/qbit.go b/pkg/qbit/qbit.go index 7493f29..c7102dd 100644 --- a/pkg/qbit/qbit.go +++ b/pkg/qbit/qbit.go @@ -33,7 +33,7 @@ func New() *QBit { DownloadFolder: cfg.DownloadFolder, Categories: cfg.Categories, Storage: NewTorrentStorage(filepath.Join(_cfg.Path, "torrents.json")), - logger: logger.NewLogger("qbit", cfg.LogLevel, os.Stdout), + logger: logger.NewLogger("qbit", _cfg.LogLevel, os.Stdout), RefreshInterval: refreshInterval, } } diff --git a/pkg/qbit/torrent.go b/pkg/qbit/torrent.go index 74a52f1..fd09410 100644 --- a/pkg/qbit/torrent.go +++ b/pkg/qbit/torrent.go @@ -56,7 +56,7 @@ func (q *QBit) Process(ctx context.Context, magnet *utils.Magnet, category strin return fmt.Errorf("arr not found in context") } isSymlink := ctx.Value("isSymlink").(bool) - debridTorrent, err := db.ProcessTorrent(svc.Debrid, magnet, a, isSymlink) + debridTorrent, err := db.ProcessTorrent(svc.Debrid, magnet, a, isSymlink, false) if err != nil || debridTorrent == nil { if debridTorrent != nil { dbClient := service.GetDebrid().GetByName(debridTorrent.Debrid) @@ -185,7 +185,7 @@ func (q *QBit) UpdateTorrent(t *Torrent, debridTorrent *debrid.Torrent) *Torrent } _db := service.GetDebrid().GetByName(debridTorrent.Debrid) if debridTorrent.Status != "downloaded" { - debridTorrent, _ = _db.GetTorrent(t.ID) + debridTorrent, _ = _db.GetTorrent(debridTorrent) } t = q.UpdateTorrentMin(t, debridTorrent) t.ContentPath = t.TorrentPath + string(os.PathSeparator) diff --git a/pkg/repair/repair.go b/pkg/repair/repair.go index c11e5b7..7f41663 100644 --- a/pkg/repair/repair.go +++ b/pkg/repair/repair.go @@ -12,6 +12,7 @@ import ( "github.com/sirrobot01/debrid-blackhole/pkg/arr" "github.com/sirrobot01/debrid-blackhole/pkg/debrid/engine" "golang.org/x/sync/errgroup" + "net" "net/http" "net/url" "os" @@ -75,13 +76,13 @@ type Job struct { ID string `json:"id"` Arrs []*arr.Arr `json:"arrs"` MediaIDs []string `json:"media_ids"` - OneOff bool `json:"one_off"` StartedAt time.Time `json:"created_at"` BrokenItems map[string][]arr.ContentFile `json:"broken_items"` Status JobStatus `json:"status"` CompletedAt time.Time `json:"finished_at"` FailedAt time.Time `json:"failed_at"` AutoProcess bool `json:"auto_process"` + Recurrent bool `json:"recurrent"` Error string `json:"error"` } @@ -106,10 +107,14 @@ func (j *Job) discordContext() string { } func (r *Repair) getArrs(arrNames []string) []*arr.Arr { + checkSkip := true // This is useful when user triggers repair with specific arrs arrs := make([]*arr.Arr, 0) if len(arrNames) == 0 { + // No specific arrs, get all + // Also check if any arrs are set to skip repair arrs = r.arrs.GetAll() } else { + checkSkip = false for _, name := range arrNames { a := r.arrs.Get(name) if a == nil || a.Host == "" || a.Token == "" { @@ -118,7 +123,17 @@ func (r *Repair) getArrs(arrNames []string) []*arr.Arr { arrs = append(arrs, a) } } - return arrs + if !checkSkip { + return arrs + } + filtered := make([]*arr.Arr, 0) + for _, a := range arrs { + if a.SkipRepair { + continue + } + filtered = append(filtered, a) + } + return filtered } func jobKey(arrNames []string, mediaIDs []string) string { @@ -133,7 +148,7 @@ func (r *Repair) reset(j *Job) { j.FailedAt = time.Time{} j.BrokenItems = nil j.Error = "" - if j.Arrs == nil { + if j.Recurrent || j.Arrs == nil { j.Arrs = r.getArrs([]string{}) // Get new arrs } } @@ -166,13 +181,17 @@ func (r *Repair) preRunChecks() error { return nil } -func (r *Repair) AddJob(arrsNames []string, mediaIDs []string, autoProcess bool) error { +func (r *Repair) AddJob(arrsNames []string, mediaIDs []string, autoProcess, recurrent bool) error { key := jobKey(arrsNames, mediaIDs) job, ok := r.Jobs[key] + if job != nil && job.Status == JobStarted { + return fmt.Errorf("job already running") + } if !ok { job = r.newJob(arrsNames, mediaIDs) } job.AutoProcess = autoProcess + job.Recurrent = recurrent r.reset(job) r.Jobs[key] = job go r.saveToFile() @@ -290,7 +309,7 @@ func (r *Repair) Start(ctx context.Context) error { if r.runOnStart { r.logger.Info().Msgf("Running initial repair") go func() { - if err := r.AddJob([]string{}, []string{}, r.autoProcess); err != nil { + if err := r.AddJob([]string{}, []string{}, r.autoProcess, true); err != nil { r.logger.Error().Err(err).Msg("Error running initial repair") } }() @@ -308,7 +327,7 @@ func (r *Repair) Start(ctx context.Context) error { return nil case t := <-ticker.C: r.logger.Info().Msgf("Running repair at %v", t.Format("15:04:05")) - if err := r.AddJob([]string{}, []string{}, r.autoProcess); err != nil { + if err := r.AddJob([]string{}, []string{}, r.autoProcess, true); err != nil { r.logger.Error().Err(err).Msg("Error running repair") } @@ -483,6 +502,16 @@ func (r *Repair) getZurgBrokenFiles(media arr.Content) []arr.ContentFile { uniqueParents[parent] = append(uniqueParents[parent], file) } } + client := &http.Client{ + Timeout: 0, + Transport: &http.Transport{ + TLSHandshakeTimeout: 60 * time.Second, + DialContext: (&net.Dialer{ + Timeout: 20 * time.Second, + KeepAlive: 30 * time.Second, + }).DialContext, + }, + } // Access zurg url + symlink folder + first file(encoded) for parent, f := range uniqueParents { r.logger.Debug().Msgf("Checking %s", parent) @@ -496,25 +525,25 @@ func (r *Repair) getZurgBrokenFiles(media arr.Content) []arr.ContentFile { continue } - resp, err := http.Get(fullURL) + resp, err := client.Get(fullURL) if err != nil { r.logger.Debug().Err(err).Msgf("Failed to reach %s", fullURL) brokenFiles = append(brokenFiles, f...) continue } - err = resp.Body.Close() - if err != nil { - return nil - } + if resp.StatusCode != http.StatusOK { r.logger.Debug().Msgf("Failed to get download url for %s", fullURL) resp.Body.Close() brokenFiles = append(brokenFiles, f...) continue } + downloadUrl := resp.Request.URL.String() + resp.Body.Close() + if downloadUrl != "" { - r.logger.Debug().Msgf("Found download url: %s", downloadUrl) + r.logger.Trace().Msgf("Found download url: %s", downloadUrl) } else { r.logger.Debug().Msgf("Failed to get download url for %s", fullURL) brokenFiles = append(brokenFiles, f...) diff --git a/pkg/server/server.go b/pkg/server/server.go index 414a1d4..4a9f72a 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -23,7 +23,7 @@ type Server struct { func New() *Server { cfg := config.GetConfig() - l := logger.NewLogger("http", cfg.QBitTorrent.LogLevel, os.Stdout) + l := logger.NewLogger("http", cfg.LogLevel, os.Stdout) r := chi.NewRouter() r.Use(middleware.Recoverer) r.Handle("/static/*", http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) diff --git a/pkg/server/webhook.go b/pkg/server/webhook.go index cb3c620..3d1bf13 100644 --- a/pkg/server/webhook.go +++ b/pkg/server/webhook.go @@ -47,7 +47,7 @@ func (s *Server) handleTautulli(w http.ResponseWriter, r *http.Request) { http.Error(w, "Repair service is not enabled", http.StatusInternalServerError) return } - if err := repair.AddJob([]string{}, []string{mediaId}, payload.AutoProcess); err != nil { + if err := repair.AddJob([]string{}, []string{mediaId}, payload.AutoProcess, false); err != nil { http.Error(w, "Failed to add job: "+err.Error(), http.StatusInternalServerError) return } diff --git a/pkg/web/ui.go b/pkg/web/server.go similarity index 95% rename from pkg/web/ui.go rename to pkg/web/server.go index ec7a96e..a5dfea7 100644 --- a/pkg/web/ui.go +++ b/pkg/web/server.go @@ -307,10 +307,11 @@ func (ui *Handler) handleAddContent(w http.ResponseWriter, r *http.Request) { arrName := r.FormValue("arr") notSymlink := r.FormValue("notSymlink") == "true" + downloadUncached := r.FormValue("downloadUncached") == "true" _arr := svc.Arr.Get(arrName) if _arr == nil { - _arr = arr.New(arrName, "", "", false) + _arr = arr.New(arrName, "", "", false, false, false) } // Handle URLs @@ -323,7 +324,7 @@ func (ui *Handler) handleAddContent(w http.ResponseWriter, r *http.Request) { } for _, url := range urlList { - importReq := qbit.NewImportRequest(url, _arr, !notSymlink) + importReq := qbit.NewImportRequest(url, _arr, !notSymlink, downloadUncached) err := importReq.Process(ui.qbit) if err != nil { errs = append(errs, fmt.Sprintf("URL %s: %v", url, err)) @@ -348,7 +349,7 @@ func (ui *Handler) handleAddContent(w http.ResponseWriter, r *http.Request) { continue } - importReq := qbit.NewImportRequest(magnet.Link, _arr, !notSymlink) + importReq := qbit.NewImportRequest(magnet.Link, _arr, !notSymlink, downloadUncached) err = importReq.Process(ui.qbit) if err != nil { errs = append(errs, fmt.Sprintf("File %s: %v", fileHeader.Filename, err)) @@ -384,7 +385,7 @@ func (ui *Handler) handleRepairMedia(w http.ResponseWriter, r *http.Request) { if req.Async { go func() { - if err := svc.Repair.AddJob([]string{req.ArrName}, req.MediaIds, req.AutoProcess); err != nil { + if err := svc.Repair.AddJob([]string{req.ArrName}, req.MediaIds, req.AutoProcess, false); err != nil { ui.logger.Error().Err(err).Msg("Failed to repair media") } }() @@ -392,10 +393,9 @@ func (ui *Handler) handleRepairMedia(w http.ResponseWriter, r *http.Request) { return } - if err := svc.Repair.AddJob([]string{req.ArrName}, req.MediaIds, req.AutoProcess); err != nil { + if err := svc.Repair.AddJob([]string{req.ArrName}, req.MediaIds, req.AutoProcess, false); err != nil { http.Error(w, fmt.Sprintf("Failed to repair: %v", err), http.StatusInternalServerError) return - } request.JSONResponse(w, "Repair completed", http.StatusOK) @@ -437,7 +437,14 @@ func (ui *Handler) handleGetConfig(w http.ResponseWriter, r *http.Request) { arrCfgs := make([]config.Arr, 0) svc := service.GetService() for _, a := range svc.Arr.GetAll() { - arrCfgs = append(arrCfgs, config.Arr{Host: a.Host, Name: a.Name, Token: a.Token, Cleanup: a.Cleanup}) + arrCfgs = append(arrCfgs, config.Arr{ + Host: a.Host, + Name: a.Name, + Token: a.Token, + Cleanup: a.Cleanup, + SkipRepair: a.SkipRepair, + DownloadUncached: a.DownloadUncached, + }) } cfg.Arrs = arrCfgs request.JSONResponse(w, cfg, http.StatusOK) diff --git a/pkg/web/web/config.html b/pkg/web/web/config.html index 331a5f5..8627757 100644 --- a/pkg/web/web/config.html +++ b/pkg/web/web/config.html @@ -12,7 +12,7 @@
- @@ -114,18 +114,6 @@
-
-
- - -
-
@@ -225,10 +213,22 @@
- +
+
+
+ + +
+
+
+
+ + +
+
`; diff --git a/pkg/web/web/download.html b/pkg/web/web/download.html index ded44b6..62dadf2 100644 --- a/pkg/web/web/download.html +++ b/pkg/web/web/download.html @@ -22,13 +22,21 @@ -
-
- - +
+
+
+ + +
+
+
+ + +
+
+ +