diff --git a/internal/utils/misc.go b/internal/utils/misc.go index d3dc669..b9e733e 100644 --- a/internal/utils/misc.go +++ b/internal/utils/misc.go @@ -13,3 +13,12 @@ outer: } return result } + +func Contains(slice []string, value string) bool { + for _, item := range slice { + if item == value { + return true + } + } + return false +} diff --git a/pkg/debrid/alldebrid/alldebrid.go b/pkg/debrid/alldebrid/alldebrid.go index 2e67fce..c54f28e 100644 --- a/pkg/debrid/alldebrid/alldebrid.go +++ b/pkg/debrid/alldebrid/alldebrid.go @@ -3,7 +3,6 @@ package alldebrid import ( "fmt" "github.com/goccy/go-json" - "github.com/puzpuzpuz/xsync/v3" "github.com/rs/zerolog" "github.com/sirrobot01/decypharr/internal/config" "github.com/sirrobot01/decypharr/internal/logger" @@ -13,7 +12,6 @@ import ( "net/http" gourl "net/url" "path/filepath" - "slices" "strconv" "sync" "time" @@ -23,7 +21,8 @@ type AllDebrid struct { Name string Host string `json:"host"` APIKey string - DownloadKeys *xsync.MapOf[string, types.Account] + accounts map[string]types.Account + accountsMu sync.RWMutex DownloadUncached bool client *request.Client @@ -46,20 +45,20 @@ func New(dc config.Debrid) *AllDebrid { request.WithProxy(dc.Proxy), ) - accounts := xsync.NewMapOf[string, types.Account]() + accounts := make(map[string]types.Account) for idx, key := range dc.DownloadAPIKeys { id := strconv.Itoa(idx) - accounts.Store(id, types.Account{ + accounts[id] = types.Account{ Name: key, ID: id, Token: key, - }) + } } return &AllDebrid{ Name: "alldebrid", Host: "http://api.alldebrid.com/v4.1", APIKey: dc.APIKey, - DownloadKeys: accounts, + accounts: accounts, DownloadUncached: dc.DownloadUncached, client: client, MountPath: dc.Folder, @@ -273,7 +272,7 @@ func (ad *AllDebrid) CheckStatus(torrent *types.Torrent, isSymlink bool) (*types } } break - } else if slices.Contains(ad.GetDownloadingStatus(), status) { + } else if utils.Contains(ad.GetDownloadingStatus(), status) { if !torrent.DownloadUncached { return torrent, fmt.Errorf("torrent: %s not cached", torrent.Name) } diff --git a/pkg/debrid/debrid/cache.go b/pkg/debrid/debrid/cache.go index 8536463..ca656b7 100644 --- a/pkg/debrid/debrid/cache.go +++ b/pkg/debrid/debrid/cache.go @@ -8,7 +8,6 @@ import ( "fmt" "os" "path/filepath" - "slices" "strconv" "strings" "sync" @@ -49,15 +48,6 @@ type CachedTorrent struct { DuplicateIds []string `json:"duplicate_ids"` } -func (ct *CachedTorrent) addDuplicateId(id string) { - if ct.DuplicateIds == nil { - ct.DuplicateIds = make([]string, 0) - } - if !slices.Contains(ct.DuplicateIds, id) { - ct.DuplicateIds = append(ct.DuplicateIds, id) - } -} - type downloadLinkCache struct { Id string Link string @@ -93,9 +83,8 @@ type Cache struct { folderNaming WebDavFolderNaming // monitors - repairRequest sync.Map - failedToReinsert sync.Map - downloadLinkRequests sync.Map + repairRequest sync.Map + failedToReinsert sync.Map // repair repairChan chan RepairRequest diff --git a/pkg/debrid/debrid/debrid.go b/pkg/debrid/debrid/debrid.go index 09b5e50..9c9bdb0 100644 --- a/pkg/debrid/debrid/debrid.go +++ b/pkg/debrid/debrid/debrid.go @@ -10,6 +10,7 @@ import ( "github.com/sirrobot01/decypharr/pkg/debrid/realdebrid" "github.com/sirrobot01/decypharr/pkg/debrid/torbox" "github.com/sirrobot01/decypharr/pkg/debrid/types" + "strings" ) func createDebridClient(dc config.Debrid) types.Client { @@ -38,54 +39,69 @@ func ProcessTorrent(d *Engine, magnet *utils.Magnet, a *arr.Arr, isSymlink, over Files: make(map[string]types.File), } - errs := make([]error, 0) + errs := make([]error, 0, len(d.Clients)) + + // Override first, arr second, debrid third + + if overrideDownloadUncached { + debridTorrent.DownloadUncached = true + } else if a.DownloadUncached != nil { + // Arr cached is set + debridTorrent.DownloadUncached = *a.DownloadUncached + } else { + debridTorrent.DownloadUncached = false + } for index, db := range d.Clients { logger := db.GetLogger() logger.Info().Msgf("Processing debrid: %s", db.GetName()) - // Override first, arr second, debrid third - - if overrideDownloadUncached { - debridTorrent.DownloadUncached = true - } else if a.DownloadUncached != nil { - // Arr cached is set - debridTorrent.DownloadUncached = *a.DownloadUncached - } else { + if !overrideDownloadUncached && a.DownloadUncached == nil { debridTorrent.DownloadUncached = db.GetDownloadUncached() } logger.Info().Msgf("Torrent Hash: %s", debridTorrent.InfoHash) - if db.GetCheckCached() { - hash, exists := db.IsAvailable([]string{debridTorrent.InfoHash})[debridTorrent.InfoHash] - if !exists || !hash { - logger.Info().Msgf("Torrent: %s is not cached", debridTorrent.Name) - continue - } else { - logger.Info().Msgf("Torrent: %s is cached(or downloading)", debridTorrent.Name) - } - } + + //if db.GetCheckCached() { + // hash, exists := db.IsAvailable([]string{debridTorrent.InfoHash})[debridTorrent.InfoHash] + // if !exists || !hash { + // logger.Info().Msgf("Torrent: %s is not cached", debridTorrent.Name) + // continue + // } else { + // logger.Info().Msgf("Torrent: %s is cached(or downloading)", debridTorrent.Name) + // } + //} dbt, err := db.SubmitMagnet(debridTorrent) - if dbt != nil { - dbt.Arr = a - } if err != nil || dbt == nil || dbt.Id == "" { errs = append(errs, err) continue } + dbt.Arr = a logger.Info().Msgf("Torrent: %s(id=%s) submitted to %s", dbt.Name, dbt.Id, db.GetName()) d.LastUsed = index + torrent, err := db.CheckStatus(dbt, isSymlink) if err != nil && torrent != nil && torrent.Id != "" { // Delete the torrent if it was not downloaded - _ = db.DeleteTorrent(torrent.Id) + go func(id string) { + _ = db.DeleteTorrent(id) + }(torrent.Id) } return torrent, err } - err := fmt.Errorf("failed to process torrent") - for _, e := range errs { - err = fmt.Errorf("%w\n%w", err, e) + if len(errs) == 0 { + return nil, fmt.Errorf("failed to process torrent: no clients available") } - return nil, err + var errBuilder strings.Builder + errBuilder.WriteString("failed to process torrent:") + + for _, e := range errs { + if e != nil { + errBuilder.WriteString("\n") + errBuilder.WriteString(e.Error()) + } + } + + return nil, fmt.Errorf(errBuilder.String()) } diff --git a/pkg/debrid/debrid/download_link.go b/pkg/debrid/debrid/download_link.go index f73e72b..01107af 100644 --- a/pkg/debrid/debrid/download_link.go +++ b/pkg/debrid/debrid/download_link.go @@ -8,51 +8,14 @@ import ( "time" ) -type downloadLinkRequest struct { - result string - err error - done chan struct{} -} - -func newDownloadLinkRequest() *downloadLinkRequest { - return &downloadLinkRequest{ - done: make(chan struct{}), - } -} - -func (r *downloadLinkRequest) Complete(result string, err error) { - r.result = result - r.err = err - close(r.done) -} - -func (r *downloadLinkRequest) Wait() (string, error) { - <-r.done - return r.result, r.err -} - func (c *Cache) GetDownloadLink(torrentName, filename, fileLink string) (string, error) { // Check link cache if dl := c.checkDownloadLink(fileLink); dl != "" { return dl, nil } - if req, inFlight := c.downloadLinkRequests.Load(fileLink); inFlight { - // Wait for the other request to complete and use its result - result := req.(*downloadLinkRequest) - return result.Wait() - } - - // Create a new request object - req := newDownloadLinkRequest() - c.downloadLinkRequests.Store(fileLink, req) - downloadLink, err := c.fetchDownloadLink(torrentName, filename, fileLink) - // Complete the request and remove it from the map - req.Complete(downloadLink, err) - c.downloadLinkRequests.Delete(fileLink) - return downloadLink, err } diff --git a/pkg/debrid/debrid/refresh.go b/pkg/debrid/debrid/refresh.go index 21a25c0..7bb5786 100644 --- a/pkg/debrid/debrid/refresh.go +++ b/pkg/debrid/debrid/refresh.go @@ -128,23 +128,17 @@ func (c *Cache) refreshTorrents() { go func() { defer wg.Done() for t := range workChan { - select { - default: - if err := c.ProcessTorrent(t); err != nil { - c.logger.Error().Err(err).Msgf("Failed to process new torrent %s", t.Id) - errChan <- err - } - counter++ + if err := c.ProcessTorrent(t); err != nil { + c.logger.Error().Err(err).Msgf("Failed to process new torrent %s", t.Id) + errChan <- err } + counter++ } }() } for _, t := range newTorrents { - select { - default: - workChan <- t - } + workChan <- t } close(workChan) wg.Wait() diff --git a/pkg/debrid/debrid/repair.go b/pkg/debrid/debrid/repair.go index 5aa0b71..064d408 100644 --- a/pkg/debrid/debrid/repair.go +++ b/pkg/debrid/debrid/repair.go @@ -8,7 +8,6 @@ import ( "github.com/sirrobot01/decypharr/internal/request" "github.com/sirrobot01/decypharr/internal/utils" "github.com/sirrobot01/decypharr/pkg/debrid/types" - "slices" "sync" "time" ) @@ -43,7 +42,7 @@ func (c *Cache) IsTorrentBroken(t *CachedTorrent, filenames []string) bool { files := make(map[string]types.File) if len(filenames) > 0 { for name, f := range t.Files { - if slices.Contains(filenames, name) { + if utils.Contains(filenames, name) { files[name] = f } } diff --git a/pkg/debrid/debrid/xml.go b/pkg/debrid/debrid/xml.go index 12b63d1..780ea08 100644 --- a/pkg/debrid/debrid/xml.go +++ b/pkg/debrid/debrid/xml.go @@ -10,32 +10,6 @@ import ( "time" ) -// resetPropfindResponse resets the propfind response cache for the specified parent directories. -func (c *Cache) resetPropfindResponse() error { - // Right now, parents are hardcoded - parents := []string{"__all__", "torrents"} - // Reset only the parent directories - // Convert the parents to a keys - // This is a bit hacky, but it works - // Instead of deleting all the keys, we only delete the parent keys, e.g __all__/ or torrents/ - keys := make([]string, 0, len(parents)) - for _, p := range parents { - // Construct the key - // construct url - url := path.Clean(path.Join("/webdav", c.client.GetName(), p)) - key0 := fmt.Sprintf("propfind:%s:0", url) - key1 := fmt.Sprintf("propfind:%s:1", url) - keys = append(keys, key0, key1) - } - - // Delete the keys - for _, k := range keys { - c.PropfindResp.Delete(k) - } - c.logger.Trace().Msgf("Reset XML cache for %s", c.client.GetName()) - return nil -} - func (c *Cache) refreshParentXml() error { parents := []string{"__all__", "torrents"} torrents := c.GetListing() diff --git a/pkg/debrid/debrid_link/debrid_link.go b/pkg/debrid/debrid_link/debrid_link.go index 8bc1036..2205cd9 100644 --- a/pkg/debrid/debrid_link/debrid_link.go +++ b/pkg/debrid/debrid_link/debrid_link.go @@ -4,15 +4,14 @@ import ( "bytes" "fmt" "github.com/goccy/go-json" - "github.com/puzpuzpuz/xsync/v3" "github.com/rs/zerolog" "github.com/sirrobot01/decypharr/internal/config" "github.com/sirrobot01/decypharr/internal/logger" "github.com/sirrobot01/decypharr/internal/request" "github.com/sirrobot01/decypharr/internal/utils" "github.com/sirrobot01/decypharr/pkg/debrid/types" - "slices" "strconv" + "sync" "time" "net/http" @@ -23,7 +22,8 @@ type DebridLink struct { Name string Host string `json:"host"` APIKey string - DownloadKeys *xsync.MapOf[string, types.Account] + accounts map[string]types.Account + accountsMutex sync.RWMutex DownloadUncached bool client *request.Client @@ -286,7 +286,7 @@ func (dl *DebridLink) CheckStatus(torrent *types.Torrent, isSymlink bool) (*type return torrent, err } break - } else if slices.Contains(dl.GetDownloadingStatus(), status) { + } else if utils.Contains(dl.GetDownloadingStatus(), status) { if !torrent.DownloadUncached { return torrent, fmt.Errorf("torrent: %s not cached", torrent.Name) } @@ -351,20 +351,20 @@ func New(dc config.Debrid) *DebridLink { request.WithProxy(dc.Proxy), ) - accounts := xsync.NewMapOf[string, types.Account]() + accounts := make(map[string]types.Account) for idx, key := range dc.DownloadAPIKeys { id := strconv.Itoa(idx) - accounts.Store(id, types.Account{ + accounts[id] = types.Account{ Name: key, ID: id, Token: key, - }) + } } return &DebridLink{ Name: "debridlink", Host: "https://debrid-link.com/api/v2", APIKey: dc.APIKey, - DownloadKeys: accounts, + accounts: accounts, DownloadUncached: dc.DownloadUncached, client: client, MountPath: dc.Folder, diff --git a/pkg/debrid/realdebrid/realdebrid.go b/pkg/debrid/realdebrid/realdebrid.go index 43eaf27..0725d7f 100644 --- a/pkg/debrid/realdebrid/realdebrid.go +++ b/pkg/debrid/realdebrid/realdebrid.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "github.com/goccy/go-json" - "github.com/puzpuzpuz/xsync/v3" "github.com/rs/zerolog" "github.com/sirrobot01/decypharr/internal/config" "github.com/sirrobot01/decypharr/internal/logger" @@ -16,7 +15,6 @@ import ( "net/http" gourl "net/url" "path/filepath" - "slices" "sort" "strconv" "strings" @@ -30,7 +28,8 @@ type RealDebrid struct { APIKey string currentDownloadKey string - DownloadKeys *xsync.MapOf[string, types.Account] // index | Account + accounts map[string]types.Account + accountsMutex sync.RWMutex DownloadUncached bool client *request.Client @@ -49,15 +48,15 @@ func New(dc config.Debrid) *RealDebrid { } _log := logger.New(dc.Name) - accounts := xsync.NewMapOf[string, types.Account]() + accounts := make(map[string]types.Account) currentDownloadKey := dc.DownloadAPIKeys[0] for idx, key := range dc.DownloadAPIKeys { id := strconv.Itoa(idx) - accounts.Store(id, types.Account{ + accounts[id] = types.Account{ Name: key, ID: id, Token: key, - }) + } } downloadHeaders := map[string]string{ @@ -68,7 +67,7 @@ func New(dc config.Debrid) *RealDebrid { Name: "realdebrid", Host: "https://api.real-debrid.com/rest/1.0", APIKey: dc.APIKey, - DownloadKeys: accounts, + accounts: accounts, DownloadUncached: dc.DownloadUncached, client: request.New( request.WithHeaders(headers), @@ -381,10 +380,13 @@ func (r *RealDebrid) CheckStatus(t *types.Torrent, isSymlink bool) (*types.Torre } 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) + res, err := r.client.Do(req) if err != nil { return t, err } + if res.StatusCode != http.StatusNoContent { + return t, fmt.Errorf("realdebrid API error: Status: %d", res.StatusCode) + } } else if status == "downloaded" { t.Files = getSelectedFiles(t, data) // Get selected files r.logger.Info().Msgf("Torrent: %s downloaded to RD", t.Name) @@ -395,7 +397,7 @@ func (r *RealDebrid) CheckStatus(t *types.Torrent, isSymlink bool) (*types.Torre } } break - } else if slices.Contains(r.GetDownloadingStatus(), status) { + } else if utils.Contains(r.GetDownloadingStatus(), status) { if !t.DownloadUncached { return t, fmt.Errorf("torrent: %s not cached", t.Name) } @@ -556,12 +558,13 @@ func (r *RealDebrid) GetDownloadLink(t *types.Torrent, file *types.File) (*types if err != nil { if errors.Is(err, request.TrafficExceededError) { // Retries generating - retries = 4 + retries = 5 } else { // If the error is not traffic exceeded, return the error return nil, err } } + backOff := 1 * time.Second for retries > 0 { downloadLink, err = r._getDownloadLink(file) if err == nil { @@ -571,7 +574,8 @@ func (r *RealDebrid) GetDownloadLink(t *types.Torrent, file *types.File) (*types return nil, err } // Add a delay before retrying - time.Sleep(5 * time.Second) + time.Sleep(backOff) + backOff *= 2 // Exponential backoff } return downloadLink, nil } @@ -755,35 +759,42 @@ func (r *RealDebrid) GetMountPath() string { } func (r *RealDebrid) DisableAccount(accountId string) { - if r.DownloadKeys.Size() == 1 { + r.accountsMutex.Lock() + defer r.accountsMutex.Unlock() + if len(r.accounts) == 1 { r.logger.Info().Msgf("Cannot disable last account: %s", accountId) return } r.currentDownloadKey = "" - if value, ok := r.DownloadKeys.Load(accountId); ok { + if value, ok := r.accounts[accountId]; ok { value.Disabled = true - r.DownloadKeys.Store(accountId, value) + r.accounts[accountId] = value r.logger.Info().Msgf("Disabled account Index: %s", value.ID) } } func (r *RealDebrid) ResetActiveDownloadKeys() { - r.DownloadKeys.Range(func(key string, value types.Account) bool { + r.accountsMutex.Lock() + defer r.accountsMutex.Unlock() + for key, value := range r.accounts { value.Disabled = false - r.DownloadKeys.Store(key, value) - return true - }) + r.accounts[key] = value + } } func (r *RealDebrid) getActiveAccounts() []types.Account { + r.accountsMutex.RLock() + defer r.accountsMutex.RUnlock() accounts := make([]types.Account, 0) - r.DownloadKeys.Range(func(key string, value types.Account) bool { + + for _, value := range r.accounts { if value.Disabled { - return true + continue } accounts = append(accounts, value) - return true - }) + } + + // Sort accounts by ID sort.Slice(accounts, func(i, j int) bool { return accounts[i].ID < accounts[j].ID }) diff --git a/pkg/debrid/torbox/torbox.go b/pkg/debrid/torbox/torbox.go index a39a154..57ee6ba 100644 --- a/pkg/debrid/torbox/torbox.go +++ b/pkg/debrid/torbox/torbox.go @@ -4,7 +4,6 @@ import ( "bytes" "fmt" "github.com/goccy/go-json" - "github.com/puzpuzpuz/xsync/v3" "github.com/rs/zerolog" "github.com/sirrobot01/decypharr/internal/config" "github.com/sirrobot01/decypharr/internal/logger" @@ -18,7 +17,6 @@ import ( "path" "path/filepath" "runtime" - "slices" "strconv" "strings" "sync" @@ -29,7 +27,8 @@ type Torbox struct { Name string Host string `json:"host"` APIKey string - DownloadKeys *xsync.MapOf[string, types.Account] + accounts map[string]types.Account + accountsMutex sync.RWMutex DownloadUncached bool client *request.Client @@ -53,21 +52,21 @@ func New(dc config.Debrid) *Torbox { request.WithProxy(dc.Proxy), ) - accounts := xsync.NewMapOf[string, types.Account]() + accounts := make(map[string]types.Account) for idx, key := range dc.DownloadAPIKeys { id := strconv.Itoa(idx) - accounts.Store(id, types.Account{ + accounts[id] = types.Account{ Name: key, ID: id, Token: key, - }) + } } return &Torbox{ Name: "torbox", Host: "https://api.torbox.app/v1", APIKey: dc.APIKey, - DownloadKeys: accounts, + accounts: accounts, DownloadUncached: dc.DownloadUncached, client: client, MountPath: dc.Folder, @@ -176,7 +175,7 @@ func getTorboxStatus(status string, finished bool) string { "forcedUP", "allocating", "downloading", "metaDL", "pausedDL", "queuedDL", "checkingDL", "forcedDL", "checkingResumeData", "moving"} switch { - case slices.Contains(downloading, status): + case utils.Contains(downloading, status): return "downloading" default: return "error" @@ -328,7 +327,7 @@ func (tb *Torbox) CheckStatus(torrent *types.Torrent, isSymlink bool) (*types.To } } break - } else if slices.Contains(tb.GetDownloadingStatus(), status) { + } else if utils.Contains(tb.GetDownloadingStatus(), status) { if !torrent.DownloadUncached { return torrent, fmt.Errorf("torrent: %s not cached", torrent.Name) } diff --git a/pkg/qbit/downloader.go b/pkg/qbit/downloader.go index d9d085f..5782937 100644 --- a/pkg/qbit/downloader.go +++ b/pkg/qbit/downloader.go @@ -5,7 +5,7 @@ import ( "github.com/cavaliergopher/grab/v3" "github.com/sirrobot01/decypharr/internal/request" "github.com/sirrobot01/decypharr/internal/utils" - debrid "github.com/sirrobot01/decypharr/pkg/debrid/types" + debridTypes "github.com/sirrobot01/decypharr/pkg/debrid/types" "io" "os" "path/filepath" @@ -102,7 +102,7 @@ func (q *QBit) downloadFiles(torrent *Torrent, parent string) { } wg.Add(1) q.downloadSemaphore <- struct{}{} - go func(file debrid.File) { + go func(file debridTypes.File) { defer wg.Done() defer func() { <-q.downloadSemaphore }() filename := file.Link @@ -149,7 +149,7 @@ func (q *QBit) ProcessSymlink(torrent *Torrent) (string, error) { return q.createSymlinks(debridTorrent, torrentRclonePath, torrentFolder) // verify cos we're using external webdav } -func (q *QBit) createSymlinksWebdav(debridTorrent *debrid.Torrent, rclonePath, torrentFolder string) (string, error) { +func (q *QBit) createSymlinksWebdav(debridTorrent *debridTypes.Torrent, rclonePath, torrentFolder string) (string, error) { files := debridTorrent.Files symlinkPath := filepath.Join(q.DownloadFolder, debridTorrent.Arr.Name, torrentFolder) // /mnt/symlinks/{category}/MyTVShow/ err := os.MkdirAll(symlinkPath, os.ModePerm) @@ -157,7 +157,7 @@ func (q *QBit) createSymlinksWebdav(debridTorrent *debrid.Torrent, rclonePath, t return "", fmt.Errorf("failed to create directory: %s: %v", symlinkPath, err) } - remainingFiles := make(map[string]debrid.File) + remainingFiles := make(map[string]debridTypes.File) for _, file := range files { remainingFiles[utils.EscapePath(file.Name)] = file } @@ -214,7 +214,7 @@ func (q *QBit) createSymlinksWebdav(debridTorrent *debrid.Torrent, rclonePath, t return symlinkPath, nil } -func (q *QBit) createSymlinks(debridTorrent *debrid.Torrent, rclonePath, torrentFolder string) (string, error) { +func (q *QBit) createSymlinks(debridTorrent *debridTypes.Torrent, rclonePath, torrentFolder string) (string, error) { files := debridTorrent.Files symlinkPath := filepath.Join(q.DownloadFolder, debridTorrent.Arr.Name, torrentFolder) // /mnt/symlinks/{category}/MyTVShow/ err := os.MkdirAll(symlinkPath, os.ModePerm) @@ -222,7 +222,7 @@ func (q *QBit) createSymlinks(debridTorrent *debrid.Torrent, rclonePath, torrent return "", fmt.Errorf("failed to create directory: %s: %v", symlinkPath, err) } - remainingFiles := make(map[string]debrid.File) + remainingFiles := make(map[string]debridTypes.File) for _, file := range files { remainingFiles[file.Path] = file } @@ -278,7 +278,7 @@ func (q *QBit) createSymlinks(debridTorrent *debrid.Torrent, rclonePath, torrent return symlinkPath, nil } -func (q *QBit) getTorrentPath(rclonePath string, debridTorrent *debrid.Torrent) (string, error) { +func (q *QBit) getTorrentPath(rclonePath string, debridTorrent *debridTypes.Torrent) (string, error) { for { torrentPath, err := debridTorrent.GetMountFolder(rclonePath) if err == nil { diff --git a/pkg/qbit/torrent.go b/pkg/qbit/torrent.go index 844311a..c935398 100644 --- a/pkg/qbit/torrent.go +++ b/pkg/qbit/torrent.go @@ -7,14 +7,13 @@ import ( "github.com/sirrobot01/decypharr/internal/request" "github.com/sirrobot01/decypharr/internal/utils" "github.com/sirrobot01/decypharr/pkg/arr" - db "github.com/sirrobot01/decypharr/pkg/debrid/debrid" - debrid "github.com/sirrobot01/decypharr/pkg/debrid/types" + "github.com/sirrobot01/decypharr/pkg/debrid/debrid" + debridTypes "github.com/sirrobot01/decypharr/pkg/debrid/types" "github.com/sirrobot01/decypharr/pkg/service" "io" "mime/multipart" "os" "path/filepath" - "slices" "strings" "time" ) @@ -56,7 +55,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, false) + debridTorrent, err := debrid.ProcessTorrent(svc.Debrid, magnet, a, isSymlink, false) if err != nil || debridTorrent == nil { if err == nil { err = fmt.Errorf("failed to process torrent") @@ -69,22 +68,27 @@ func (q *QBit) Process(ctx context.Context, magnet *utils.Magnet, category strin return nil } -func (q *QBit) ProcessFiles(torrent *Torrent, debridTorrent *debrid.Torrent, arr *arr.Arr, isSymlink bool) { +func (q *QBit) ProcessFiles(torrent *Torrent, debridTorrent *debridTypes.Torrent, arr *arr.Arr, isSymlink bool) { svc := service.GetService() client := svc.Debrid.GetClient(debridTorrent.Debrid) + downloadingStatuses := client.GetDownloadingStatus() for debridTorrent.Status != "downloaded" { q.logger.Debug().Msgf("%s <- (%s) Download Progress: %.2f%%", debridTorrent.Debrid, debridTorrent.Name, debridTorrent.Progress) dbT, err := client.CheckStatus(debridTorrent, isSymlink) if err != nil { if dbT != nil && dbT.Id != "" { // Delete the torrent if it was not downloaded - _ = client.DeleteTorrent(dbT.Id) + go func() { + _ = client.DeleteTorrent(dbT.Id) + }() } q.logger.Error().Msgf("Error checking status: %v", err) q.MarkAsFailed(torrent) - if err := arr.Refresh(); err != nil { - q.logger.Error().Msgf("Error refreshing arr: %v", err) - } + go func() { + if err := arr.Refresh(); err != nil { + q.logger.Error().Msgf("Error refreshing arr: %v", err) + } + }() return } @@ -92,22 +96,24 @@ func (q *QBit) ProcessFiles(torrent *Torrent, debridTorrent *debrid.Torrent, arr torrent = q.UpdateTorrentMin(torrent, debridTorrent) // Exit the loop for downloading statuses to prevent memory buildup - if !slices.Contains(client.GetDownloadingStatus(), debridTorrent.Status) { + if debridTorrent.Status == "downloaded" || !utils.Contains(downloadingStatuses, debridTorrent.Status) { + break + } + if !utils.Contains(client.GetDownloadingStatus(), debridTorrent.Status) { break } time.Sleep(time.Duration(q.RefreshInterval) * time.Second) } - var ( - torrentSymlinkPath string - err error - ) + var torrentSymlinkPath string + var err error debridTorrent.Arr = arr // Check if debrid supports webdav by checking cache if isSymlink { - cache, ok := svc.Debrid.Caches[debridTorrent.Debrid] - if ok { + cache, useWebdav := svc.Debrid.Caches[debridTorrent.Debrid] + if useWebdav { q.logger.Info().Msgf("Using internal webdav for %s", debridTorrent.Debrid) + timer := time.Now() // Use webdav to download the file if err := cache.AddTorrent(debridTorrent); err != nil { @@ -118,7 +124,6 @@ func (q *QBit) ProcessFiles(torrent *Torrent, debridTorrent *debrid.Torrent, arr rclonePath := filepath.Join(debridTorrent.MountPath, cache.GetTorrentFolder(debridTorrent)) // /mnt/remote/realdebrid/MyTVShow torrentFolderNoExt := utils.RemoveExtension(debridTorrent.Name) - timer := time.Now() torrentSymlinkPath, err = q.createSymlinksWebdav(debridTorrent, rclonePath, torrentFolderNoExt) // /mnt/symlinks/{category}/MyTVShow/ q.logger.Debug().Msgf("Adding %s took %s", debridTorrent.Name, time.Since(timer)) @@ -160,7 +165,7 @@ func (q *QBit) MarkAsFailed(t *Torrent) *Torrent { return t } -func (q *QBit) UpdateTorrentMin(t *Torrent, debridTorrent *debrid.Torrent) *Torrent { +func (q *QBit) UpdateTorrentMin(t *Torrent, debridTorrent *debridTypes.Torrent) *Torrent { if debridTorrent == nil { return t } @@ -202,7 +207,7 @@ func (q *QBit) UpdateTorrentMin(t *Torrent, debridTorrent *debrid.Torrent) *Torr return t } -func (q *QBit) UpdateTorrent(t *Torrent, debridTorrent *debrid.Torrent) *Torrent { +func (q *QBit) UpdateTorrent(t *Torrent, debridTorrent *debridTypes.Torrent) *Torrent { if debridTorrent == nil { return t } @@ -298,10 +303,10 @@ func (q *QBit) SetTorrentTags(t *Torrent, tags []string) bool { if tag == "" { continue } - if !slices.Contains(torrentTags, tag) { + if !utils.Contains(torrentTags, tag) { torrentTags = append(torrentTags, tag) } - if !slices.Contains(q.Tags, tag) { + if !utils.Contains(q.Tags, tag) { q.Tags = append(q.Tags, tag) } } @@ -324,7 +329,7 @@ func (q *QBit) AddTags(tags []string) bool { if tag == "" { continue } - if !slices.Contains(q.Tags, tag) { + if !utils.Contains(q.Tags, tag) { q.Tags = append(q.Tags, tag) } } diff --git a/pkg/repair/misc.go b/pkg/repair/misc.go index a5e77fb..0cce790 100644 --- a/pkg/repair/misc.go +++ b/pkg/repair/misc.go @@ -5,73 +5,8 @@ import ( "github.com/sirrobot01/decypharr/pkg/arr" "os" "path/filepath" - "strconv" - "strings" - "time" ) -func parseSchedule(schedule string) (time.Duration, error) { - if schedule == "" { - return time.Hour, nil // default 60m - } - - // Check if it's a time-of-day format (HH:MM) - if strings.Contains(schedule, ":") { - return parseTimeOfDay(schedule) - } - - // Otherwise treat as duration interval - return parseDurationInterval(schedule) -} - -func parseTimeOfDay(schedule string) (time.Duration, error) { - now := time.Now() - scheduledTime, err := time.Parse("15:04", schedule) - if err != nil { - return 0, fmt.Errorf("invalid time format: %s. Use HH:MM in 24-hour format", schedule) - } - - // Convert scheduled time to today - scheduleToday := time.Date( - now.Year(), now.Month(), now.Day(), - scheduledTime.Hour(), scheduledTime.Minute(), 0, 0, - now.Location(), - ) - - if scheduleToday.Before(now) { - scheduleToday = scheduleToday.Add(24 * time.Hour) - } - - return scheduleToday.Sub(now), nil -} - -func parseDurationInterval(interval string) (time.Duration, error) { - if len(interval) < 2 { - return 0, fmt.Errorf("invalid interval format: %s", interval) - } - - numStr := interval[:len(interval)-1] - unit := interval[len(interval)-1] - - num, err := strconv.Atoi(numStr) - if err != nil { - return 0, fmt.Errorf("invalid number in interval: %s", numStr) - } - - switch unit { - case 'm': - return time.Duration(num) * time.Minute, nil - case 'h': - return time.Duration(num) * time.Hour, nil - case 'd': - return time.Duration(num) * 24 * time.Hour, nil - case 's': - return time.Duration(num) * time.Second, nil - default: - return 0, fmt.Errorf("invalid unit in interval: %c", unit) - } -} - func fileIsSymlinked(file string) bool { info, err := os.Lstat(file) if err != nil { diff --git a/pkg/web/middlewares.go b/pkg/web/middlewares.go index fdb8171..b029d66 100644 --- a/pkg/web/middlewares.go +++ b/pkg/web/middlewares.go @@ -18,7 +18,7 @@ func (ui *Handler) setupMiddleware(next http.Handler) http.Handler { // strip inco from URL if inco := r.URL.Query().Get("inco"); inco != "" && needsAuth == nil && r.URL.Path == "/config" { // redirect to the same URL without the inco parameter - http.Redirect(w, r, fmt.Sprintf("/config"), http.StatusSeeOther) + http.Redirect(w, r, "/config", http.StatusSeeOther) } next.ServeHTTP(w, r) }) diff --git a/pkg/webdav/handler.go b/pkg/webdav/handler.go index d4cbe01..af9ebaf 100644 --- a/pkg/webdav/handler.go +++ b/pkg/webdav/handler.go @@ -165,7 +165,7 @@ func (h *Handler) OpenFile(ctx context.Context, name string, flag int, perm os.F _path := strings.TrimPrefix(name, rootDir) parts := strings.Split(strings.TrimPrefix(_path, string(os.PathSeparator)), string(os.PathSeparator)) - if len(parts) >= 2 && (slices.Contains(h.getParentItems(), parts[0])) { + if len(parts) >= 2 && (utils.Contains(h.getParentItems(), parts[0])) { torrentName := parts[1] cachedTorrent := h.cache.GetTorrentByName(torrentName)