From 3efda45304b26371392e72374fd66c22998bf26a Mon Sep 17 00:00:00 2001 From: Mukhtar Akere Date: Sun, 8 Jun 2025 19:06:17 +0100 Subject: [PATCH] - IMplement multi-download api tokens - Move things around a bit --- internal/config/config.go | 10 +- pkg/debrid/providers/alldebrid/alldebrid.go | 93 +++---- .../providers/debrid_link/debrid_link.go | 137 ++++++----- pkg/debrid/providers/realdebrid/realdebrid.go | 220 +++++++---------- pkg/debrid/providers/torbox/torbox.go | 84 +++---- pkg/debrid/store/cache.go | 39 ++- pkg/debrid/store/download_link.go | 162 ++++-------- pkg/debrid/store/refresh.go | 20 +- pkg/debrid/store/repair.go | 2 +- pkg/debrid/types/account.go | 230 ++++++++++++++++++ pkg/debrid/types/client.go | 8 +- pkg/debrid/types/error.go | 30 +++ pkg/debrid/types/torrent.go | 37 ++- 13 files changed, 607 insertions(+), 465 deletions(-) create mode 100644 pkg/debrid/types/account.go create mode 100644 pkg/debrid/types/error.go diff --git a/internal/config/config.go b/internal/config/config.go index dbe079e..d84401d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -277,9 +277,15 @@ func (c *Config) updateDebrid(d Debrid) Debrid { workers := runtime.NumCPU() * 50 perDebrid := workers / len(c.Debrids) - if len(d.DownloadAPIKeys) == 0 { - d.DownloadAPIKeys = append(d.DownloadAPIKeys, d.APIKey) + var downloadKeys []string + + if len(d.DownloadAPIKeys) > 0 { + downloadKeys = d.DownloadAPIKeys + } else { + // If no download API keys are specified, use the main API key + downloadKeys = []string{d.APIKey} } + d.DownloadAPIKeys = downloadKeys if !d.UseWebDav { return d diff --git a/pkg/debrid/providers/alldebrid/alldebrid.go b/pkg/debrid/providers/alldebrid/alldebrid.go index a8abaa6..e635992 100644 --- a/pkg/debrid/providers/alldebrid/alldebrid.go +++ b/pkg/debrid/providers/alldebrid/alldebrid.go @@ -18,12 +18,13 @@ import ( ) type AllDebrid struct { - name string - Host string `json:"host"` - APIKey string - accounts map[string]types.Account - DownloadUncached bool - client *request.Client + name string + Host string `json:"host"` + APIKey string + accounts *types.Accounts + autoExpiresLinksAfter time.Duration + DownloadUncached bool + client *request.Client MountPath string logger zerolog.Logger @@ -50,27 +51,23 @@ func New(dc config.Debrid) (*AllDebrid, error) { request.WithProxy(dc.Proxy), ) - accounts := make(map[string]types.Account) - for idx, key := range dc.DownloadAPIKeys { - id := strconv.Itoa(idx) - accounts[id] = types.Account{ - Name: key, - ID: id, - Token: key, - } + autoExpiresLinksAfter, err := time.ParseDuration(dc.AutoExpireLinksAfter) + if autoExpiresLinksAfter == 0 || err != nil { + autoExpiresLinksAfter = 48 * time.Hour } return &AllDebrid{ - name: "alldebrid", - Host: "http://api.alldebrid.com/v4.1", - APIKey: dc.APIKey, - accounts: accounts, - DownloadUncached: dc.DownloadUncached, - client: client, - MountPath: dc.Folder, - logger: logger.New(dc.Name), - checkCached: dc.CheckCached, - addSamples: dc.AddSamples, - minimumFreeSlot: dc.MinimumFreeSlot, + name: "alldebrid", + Host: "http://api.alldebrid.com/v4.1", + APIKey: dc.APIKey, + accounts: types.NewAccounts(dc), + DownloadUncached: dc.DownloadUncached, + autoExpiresLinksAfter: autoExpiresLinksAfter, + client: client, + MountPath: dc.Folder, + logger: logger.New(dc.Name), + checkCached: dc.CheckCached, + addSamples: dc.AddSamples, + minimumFreeSlot: dc.MinimumFreeSlot, }, nil } @@ -273,8 +270,8 @@ func (ad *AllDebrid) CheckStatus(torrent *types.Torrent, isSymlink bool) (*types if status == "downloaded" { ad.logger.Info().Msgf("Torrent: %s downloaded", torrent.Name) if !isSymlink { - err = ad.GenerateDownloadLinks(torrent) - if err != nil { + + if err = ad.GetFileDownloadLinks(torrent); err != nil { return torrent, err } } @@ -304,8 +301,9 @@ func (ad *AllDebrid) DeleteTorrent(torrentId string) error { return nil } -func (ad *AllDebrid) GenerateDownloadLinks(t *types.Torrent) error { +func (ad *AllDebrid) GetFileDownloadLinks(t *types.Torrent) error { filesCh := make(chan types.File, len(t.Files)) + linksCh := make(chan *types.DownloadLink, len(t.Files)) errCh := make(chan error, len(t.Files)) var wg sync.WaitGroup @@ -318,17 +316,19 @@ func (ad *AllDebrid) GenerateDownloadLinks(t *types.Torrent) error { errCh <- err return } - file.DownloadLink = link if link != nil { errCh <- fmt.Errorf("download link is empty") return } + linksCh <- link + file.DownloadLink = link filesCh <- file }(file) } go func() { wg.Wait() close(filesCh) + close(linksCh) close(errCh) }() files := make(map[string]types.File, len(t.Files)) @@ -336,10 +336,22 @@ func (ad *AllDebrid) GenerateDownloadLinks(t *types.Torrent) error { files[file.Name] = file } + // Collect download links + links := make(map[string]*types.DownloadLink, len(t.Files)) + + for link := range linksCh { + if link == nil { + continue + } + links[link.Link] = link + } + // Update the files with download links + ad.accounts.SetDownloadLinks(links) + // Check for errors for err := range errCh { if err != nil { - return err // Return the first error encountered + return err } } @@ -369,21 +381,18 @@ func (ad *AllDebrid) GetDownloadLink(t *types.Torrent, file *types.File) (*types if link == "" { return nil, fmt.Errorf("download link is empty") } + now := time.Now() return &types.DownloadLink{ Link: file.Link, DownloadLink: link, Id: data.Data.Id, Size: file.Size, Filename: file.Name, - Generated: time.Now(), - AccountId: "0", + Generated: now, + ExpiresAt: now.Add(ad.autoExpiresLinksAfter), }, nil } -func (ad *AllDebrid) GetCheckCached() bool { - return ad.checkCached -} - func (ad *AllDebrid) GetTorrents() ([]*types.Torrent, error) { url := fmt.Sprintf("%s/magnet/status?status=ready", ad.Host) req, _ := http.NewRequest(http.MethodGet, url, nil) @@ -417,7 +426,7 @@ func (ad *AllDebrid) GetTorrents() ([]*types.Torrent, error) { return torrents, nil } -func (ad *AllDebrid) GetDownloads() (map[string]types.DownloadLink, error) { +func (ad *AllDebrid) GetDownloadLinks() (map[string]*types.DownloadLink, error) { return nil, nil } @@ -437,12 +446,6 @@ func (ad *AllDebrid) GetMountPath() string { return ad.MountPath } -func (ad *AllDebrid) DisableAccount(accountId string) { -} - -func (ad *AllDebrid) ResetActiveDownloadKeys() { - -} func (ad *AllDebrid) DeleteDownloadLink(linkId string) error { return nil } @@ -452,3 +455,7 @@ func (ad *AllDebrid) GetAvailableSlots() (int, error) { //TODO: Implement the logic to check available slots for AllDebrid return 0, fmt.Errorf("GetAvailableSlots not implemented for AllDebrid") } + +func (ad *AllDebrid) Accounts() *types.Accounts { + return ad.accounts +} diff --git a/pkg/debrid/providers/debrid_link/debrid_link.go b/pkg/debrid/providers/debrid_link/debrid_link.go index ffa3311..a109495 100644 --- a/pkg/debrid/providers/debrid_link/debrid_link.go +++ b/pkg/debrid/providers/debrid_link/debrid_link.go @@ -10,7 +10,6 @@ import ( "github.com/sirrobot01/decypharr/internal/request" "github.com/sirrobot01/decypharr/internal/utils" "github.com/sirrobot01/decypharr/pkg/debrid/types" - "strconv" "time" "net/http" @@ -21,10 +20,12 @@ type DebridLink struct { name string Host string `json:"host"` APIKey string - accounts map[string]types.Account + accounts *types.Accounts DownloadUncached bool client *request.Client + autoExpiresLinksAfter time.Duration + MountPath string logger zerolog.Logger checkCached bool @@ -46,26 +47,22 @@ func New(dc config.Debrid) (*DebridLink, error) { request.WithProxy(dc.Proxy), ) - accounts := make(map[string]types.Account) - for idx, key := range dc.DownloadAPIKeys { - id := strconv.Itoa(idx) - accounts[id] = types.Account{ - Name: key, - ID: id, - Token: key, - } + autoExpiresLinksAfter, err := time.ParseDuration(dc.AutoExpireLinksAfter) + if autoExpiresLinksAfter == 0 || err != nil { + autoExpiresLinksAfter = 48 * time.Hour } return &DebridLink{ - name: "debridlink", - Host: "https://debrid-link.com/api/v2", - APIKey: dc.APIKey, - accounts: accounts, - DownloadUncached: dc.DownloadUncached, - client: client, - MountPath: dc.Folder, - logger: logger.New(dc.Name), - checkCached: dc.CheckCached, - addSamples: dc.AddSamples, + name: "debridlink", + Host: "https://debrid-link.com/api/v2", + APIKey: dc.APIKey, + accounts: types.NewAccounts(dc), + DownloadUncached: dc.DownloadUncached, + autoExpiresLinksAfter: autoExpiresLinksAfter, + client: client, + MountPath: dc.Folder, + logger: logger.New(dc.Name), + checkCached: dc.CheckCached, + addSamples: dc.AddSamples, }, nil } @@ -177,14 +174,7 @@ func (dl *DebridLink) GetTorrent(torrentId string) (*types.Torrent, error) { Name: f.Name, Size: f.Size, Path: f.Name, - DownloadLink: &types.DownloadLink{ - Filename: f.Name, - Link: f.DownloadURL, - DownloadLink: f.DownloadURL, - Generated: time.Now(), - AccountId: "0", - }, - Link: f.DownloadURL, + Link: f.DownloadURL, } torrent.Files[file.Name] = file } @@ -233,6 +223,8 @@ func (dl *DebridLink) UpdateTorrent(t *types.Torrent) error { t.OriginalFilename = name t.Added = time.Unix(data.Created, 0).Format(time.RFC3339) cfg := config.Get() + links := make(map[string]*types.DownloadLink) + now := time.Now() for _, f := range data.Files { if !cfg.IsSizeAllowed(f.Size) { continue @@ -243,17 +235,21 @@ func (dl *DebridLink) UpdateTorrent(t *types.Torrent) error { Name: f.Name, Size: f.Size, Path: f.Name, - DownloadLink: &types.DownloadLink{ - Filename: f.Name, - Link: f.DownloadURL, - DownloadLink: f.DownloadURL, - Generated: time.Now(), - AccountId: "0", - }, - Link: f.DownloadURL, + Link: f.DownloadURL, } + link := &types.DownloadLink{ + Filename: f.Name, + Link: f.DownloadURL, + DownloadLink: f.DownloadURL, + Generated: now, + ExpiresAt: now.Add(dl.autoExpiresLinksAfter), + } + links[file.Link] = link + file.DownloadLink = link t.Files[f.Name] = file } + + dl.accounts.SetDownloadLinks(links) return nil } @@ -290,6 +286,9 @@ func (dl *DebridLink) SubmitMagnet(t *types.Torrent) (*types.Torrent, error) { t.MountPath = dl.MountPath t.Debrid = dl.name t.Added = time.Unix(data.Created, 0).Format(time.RFC3339) + + links := make(map[string]*types.DownloadLink) + now := time.Now() for _, f := range data.Files { file := types.File{ TorrentId: t.Id, @@ -298,18 +297,22 @@ func (dl *DebridLink) SubmitMagnet(t *types.Torrent) (*types.Torrent, error) { Size: f.Size, Path: f.Name, Link: f.DownloadURL, - DownloadLink: &types.DownloadLink{ - Filename: f.Name, - Link: f.DownloadURL, - DownloadLink: f.DownloadURL, - Generated: time.Now(), - AccountId: "0", - }, - Generated: time.Now(), + Generated: now, } + link := &types.DownloadLink{ + Filename: f.Name, + Link: f.DownloadURL, + DownloadLink: f.DownloadURL, + Generated: now, + ExpiresAt: now.Add(dl.autoExpiresLinksAfter), + } + links[file.Link] = link + file.DownloadLink = link t.Files[f.Name] = file } + dl.accounts.SetDownloadLinks(links) + return t, nil } @@ -322,8 +325,8 @@ func (dl *DebridLink) CheckStatus(torrent *types.Torrent, isSymlink bool) (*type status := torrent.Status if status == "downloaded" { dl.logger.Info().Msgf("Torrent: %s downloaded", torrent.Name) - err = dl.GenerateDownloadLinks(torrent) - if err != nil { + + if err = dl.GetFileDownloadLinks(torrent); err != nil { return torrent, err } break @@ -352,27 +355,23 @@ func (dl *DebridLink) DeleteTorrent(torrentId string) error { return nil } -func (dl *DebridLink) GenerateDownloadLinks(t *types.Torrent) error { +func (dl *DebridLink) GetFileDownloadLinks(t *types.Torrent) error { // Download links are already generated return nil } -func (dl *DebridLink) GetDownloads() (map[string]types.DownloadLink, error) { +func (dl *DebridLink) GetDownloadLinks() (map[string]*types.DownloadLink, error) { return nil, nil } func (dl *DebridLink) GetDownloadLink(t *types.Torrent, file *types.File) (*types.DownloadLink, error) { - return file.DownloadLink, nil + return dl.accounts.GetDownloadLink(file.Link) } func (dl *DebridLink) GetDownloadingStatus() []string { return []string{"downloading"} } -func (dl *DebridLink) GetCheckCached() bool { - return dl.checkCached -} - func (dl *DebridLink) GetDownloadUncached() bool { return dl.DownloadUncached } @@ -411,6 +410,7 @@ func (dl *DebridLink) getTorrents(page, perPage int) ([]*types.Torrent, error) { } data := *res.Value + links := make(map[string]*types.DownloadLink) if len(data) == 0 { return torrents, nil @@ -433,6 +433,7 @@ func (dl *DebridLink) getTorrents(page, perPage int) ([]*types.Torrent, error) { Added: time.Unix(t.Created, 0).Format(time.RFC3339), } cfg := config.Get() + now := time.Now() for _, f := range t.Files { if !cfg.IsSizeAllowed(f.Size) { continue @@ -443,19 +444,23 @@ func (dl *DebridLink) getTorrents(page, perPage int) ([]*types.Torrent, error) { Name: f.Name, Size: f.Size, Path: f.Name, - DownloadLink: &types.DownloadLink{ - Filename: f.Name, - Link: f.DownloadURL, - DownloadLink: f.DownloadURL, - Generated: time.Now(), - AccountId: "0", - }, - Link: f.DownloadURL, + Link: f.DownloadURL, } + link := &types.DownloadLink{ + Filename: f.Name, + Link: f.DownloadURL, + DownloadLink: f.DownloadURL, + Generated: now, + ExpiresAt: now.Add(dl.autoExpiresLinksAfter), + } + links[file.Link] = link + file.DownloadLink = link torrent.Files[f.Name] = file } torrents = append(torrents, torrent) } + dl.accounts.SetDownloadLinks(links) + return torrents, nil } @@ -467,12 +472,6 @@ func (dl *DebridLink) GetMountPath() string { return dl.MountPath } -func (dl *DebridLink) DisableAccount(accountId string) { -} - -func (dl *DebridLink) ResetActiveDownloadKeys() { -} - func (dl *DebridLink) DeleteDownloadLink(linkId string) error { return nil } @@ -481,3 +480,7 @@ func (dl *DebridLink) GetAvailableSlots() (int, error) { //TODO: Implement the logic to check available slots for DebridLink return 0, fmt.Errorf("GetAvailableSlots not implemented for DebridLink") } + +func (dl *DebridLink) Accounts() *types.Accounts { + return dl.accounts +} diff --git a/pkg/debrid/providers/realdebrid/realdebrid.go b/pkg/debrid/providers/realdebrid/realdebrid.go index 297a075..ff102fa 100644 --- a/pkg/debrid/providers/realdebrid/realdebrid.go +++ b/pkg/debrid/providers/realdebrid/realdebrid.go @@ -10,7 +10,6 @@ import ( "net/http" gourl "net/url" "path/filepath" - "sort" "strconv" "strings" "sync" @@ -28,14 +27,13 @@ type RealDebrid struct { name string Host string `json:"host"` - APIKey string - currentDownloadKey string - accounts map[string]types.Account - accountsMutex sync.RWMutex + APIKey string + accounts *types.Accounts - DownloadUncached bool - client *request.Client - downloadClient *request.Client + DownloadUncached bool + client *request.Client + downloadClient *request.Client + autoExpiresLinksAfter time.Duration MountPath string logger zerolog.Logger @@ -57,28 +55,19 @@ func New(dc config.Debrid) (*RealDebrid, error) { } _log := logger.New(dc.Name) - accounts := make(map[string]types.Account) - currentDownloadKey := dc.DownloadAPIKeys[0] - for idx, key := range dc.DownloadAPIKeys { - id := strconv.Itoa(idx) - accounts[id] = types.Account{ - Name: key, - ID: id, - Token: key, - } - } - - downloadHeaders := map[string]string{ - "Authorization": fmt.Sprintf("Bearer %s", currentDownloadKey), + autoExpiresLinksAfter, err := time.ParseDuration(dc.AutoExpireLinksAfter) + if autoExpiresLinksAfter == 0 || err != nil { + autoExpiresLinksAfter = 48 * time.Hour } r := &RealDebrid{ - name: "realdebrid", - Host: "https://api.real-debrid.com/rest/1.0", - APIKey: dc.APIKey, - accounts: accounts, - DownloadUncached: dc.DownloadUncached, - UnpackRar: dc.UnpackRar, + name: "realdebrid", + Host: "https://api.real-debrid.com/rest/1.0", + APIKey: dc.APIKey, + accounts: types.NewAccounts(dc), + DownloadUncached: dc.DownloadUncached, + autoExpiresLinksAfter: autoExpiresLinksAfter, + UnpackRar: dc.UnpackRar, client: request.New( request.WithHeaders(headers), request.WithRateLimiter(rl), @@ -88,19 +77,17 @@ func New(dc config.Debrid) (*RealDebrid, error) { request.WithProxy(dc.Proxy), ), downloadClient: request.New( - request.WithHeaders(downloadHeaders), request.WithLogger(_log), request.WithMaxRetries(10), request.WithRetryableStatus(429, 447, 502), request.WithProxy(dc.Proxy), ), - currentDownloadKey: currentDownloadKey, - MountPath: dc.Folder, - logger: logger.New(dc.Name), - rarSemaphore: make(chan struct{}, 2), - checkCached: dc.CheckCached, - addSamples: dc.AddSamples, - minimumFreeSlot: dc.MinimumFreeSlot, + MountPath: dc.Folder, + logger: logger.New(dc.Name), + rarSemaphore: make(chan struct{}, 2), + checkCached: dc.CheckCached, + addSamples: dc.AddSamples, + minimumFreeSlot: dc.MinimumFreeSlot, } if _, err := r.GetProfile(); err != nil { @@ -182,7 +169,6 @@ func (r *RealDebrid) handleRarArchive(t *types.Torrent, data torrentInfo, select ByteRange: nil, Path: t.Name + ".rar", Link: data.Links[0], - AccountId: selectedFiles[0].AccountId, Generated: time.Now(), } files[file.Name] = file @@ -219,19 +205,14 @@ func (r *RealDebrid) handleRarArchive(t *types.Torrent, data torrentInfo, select fileMap[safeName] = &selectedFiles[i] } + now := time.Now() + for _, rarFile := range rarFiles { if file, exists := fileMap[rarFile.Name()]; exists { file.IsRar = true file.ByteRange = rarFile.ByteRange() file.Link = data.Links[0] - file.DownloadLink = &types.DownloadLink{ - Link: data.Links[0], - DownloadLink: dlLink, - Filename: file.Name, - Size: file.Size, - Generated: time.Now(), - } - + file.Generated = now files[file.Name] = *file } else if !rarFile.IsDirectory { r.logger.Warn().Msgf("RAR file %s not found in torrent files", rarFile.Name()) @@ -545,8 +526,7 @@ func (r *RealDebrid) CheckStatus(t *types.Torrent, isSymlink bool) (*types.Torre r.logger.Info().Msgf("Torrent: %s downloaded to RD", t.Name) if !isSymlink { - err = r.GenerateDownloadLinks(t) - if err != nil { + if err = r.GetFileDownloadLinks(t); err != nil { return t, err } } @@ -574,9 +554,10 @@ func (r *RealDebrid) DeleteTorrent(torrentId string) error { return nil } -func (r *RealDebrid) GenerateDownloadLinks(t *types.Torrent) error { +func (r *RealDebrid) GetFileDownloadLinks(t *types.Torrent) error { filesCh := make(chan types.File, len(t.Files)) errCh := make(chan error, len(t.Files)) + linksCh := make(chan *types.DownloadLink) var wg sync.WaitGroup wg.Add(len(t.Files)) @@ -589,7 +570,11 @@ func (r *RealDebrid) GenerateDownloadLinks(t *types.Torrent) error { errCh <- err return } - + if link == nil { + errCh <- fmt.Errorf("realdebrid API error: download link not found for file %s", file.Name) + return + } + linksCh <- link file.DownloadLink = link filesCh <- file }(f) @@ -598,6 +583,7 @@ func (r *RealDebrid) GenerateDownloadLinks(t *types.Torrent) error { go func() { wg.Wait() close(filesCh) + close(linksCh) close(errCh) }() @@ -607,6 +593,18 @@ func (r *RealDebrid) GenerateDownloadLinks(t *types.Torrent) error { files[file.Name] = file } + // Collect download links + links := make(map[string]*types.DownloadLink) + for link := range linksCh { + if link == nil { + continue + } + links[link.Link] = link + } + + // Add links to cache + r.accounts.SetDownloadLinks(links) + // Check for errors for err := range errCh { if err != nil { @@ -636,8 +634,12 @@ func (r *RealDebrid) CheckLink(link string) error { func (r *RealDebrid) _getDownloadLink(file *types.File) (*types.DownloadLink, error) { url := fmt.Sprintf("%s/unrestrict/link/", r.Host) + _link := file.Link + if strings.HasPrefix(_link, "https://real-debrid.com/d/") { + _link = file.Link[0:39] + } payload := gourl.Values{ - "link": {file.Link}, + "link": {_link}, } req, _ := http.NewRequest(http.MethodPost, url, strings.NewReader(payload.Encode())) resp, err := r.downloadClient.Do(req) @@ -684,32 +686,31 @@ func (r *RealDebrid) _getDownloadLink(file *types.File) (*types.DownloadLink, er if data.Download == "" { return nil, fmt.Errorf("realdebrid API error: download link not found") } + now := time.Now() return &types.DownloadLink{ Filename: data.Filename, Size: data.Filesize, Link: data.Link, DownloadLink: data.Download, - Generated: time.Now(), + Generated: now, + ExpiresAt: now.Add(r.autoExpiresLinksAfter), }, nil } func (r *RealDebrid) GetDownloadLink(t *types.Torrent, file *types.File) (*types.DownloadLink, error) { - if r.currentDownloadKey == "" { - // If no download key is set, use the first one - accounts := r.getActiveAccounts() - if len(accounts) < 1 { - // No active download keys. It's likely that the key has reached bandwidth limit - return nil, fmt.Errorf("no active download keys") - } - r.currentDownloadKey = accounts[0].Token - } + accounts := r.accounts.All() - r.downloadClient.SetHeader("Authorization", fmt.Sprintf("Bearer %s", r.currentDownloadKey)) - downloadLink, err := r._getDownloadLink(file) - retries := 0 - if err != nil { + for _, account := range accounts { + r.downloadClient.SetHeader("Authorization", fmt.Sprintf("Bearer %s", account.Token)) + downloadLink, err := r._getDownloadLink(file) + + if err == nil { + return downloadLink, nil + } + + retries := 0 if errors.Is(err, utils.TrafficExceededError) { // Retries generating retries = 5 @@ -717,25 +718,22 @@ func (r *RealDebrid) GetDownloadLink(t *types.Torrent, file *types.File) (*types // 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 { - return downloadLink, nil + backOff := 1 * time.Second + for retries > 0 { + downloadLink, err = r._getDownloadLink(file) + if err == nil { + return downloadLink, nil + } + if !errors.Is(err, utils.TrafficExceededError) { + return nil, err + } + // Add a delay before retrying + time.Sleep(backOff) + backOff *= 2 // Exponential backoff + retries-- } - if !errors.Is(err, utils.TrafficExceededError) { - return nil, err - } - // Add a delay before retrying - time.Sleep(backOff) - backOff *= 2 // Exponential backoff } - return downloadLink, nil -} - -func (r *RealDebrid) GetCheckCached() bool { - return r.checkCached + return nil, fmt.Errorf("realdebrid API error: download link not found") } func (r *RealDebrid) getTorrents(offset int, limit int) (int, []*types.Torrent, error) { @@ -824,18 +822,19 @@ func (r *RealDebrid) GetTorrents() ([]*types.Torrent, error) { return allTorrents, nil } -func (r *RealDebrid) GetDownloads() (map[string]types.DownloadLink, error) { - links := make(map[string]types.DownloadLink) +func (r *RealDebrid) GetDownloadLinks() (map[string]*types.DownloadLink, error) { + links := make(map[string]*types.DownloadLink) offset := 0 limit := 1000 - accounts := r.getActiveAccounts() + accounts := r.accounts.All() if len(accounts) < 1 { // No active download keys. It's likely that the key has reached bandwidth limit - return nil, fmt.Errorf("no active download keys") + return links, fmt.Errorf("no active download keys") } - r.downloadClient.SetHeader("Authorization", fmt.Sprintf("Bearer %s", accounts[0].Token)) + activeAccount := accounts[0] + r.downloadClient.SetHeader("Authorization", fmt.Sprintf("Bearer %s", activeAccount.Token)) for { dl, err := r._getDownloads(offset, limit) if err != nil { @@ -850,11 +849,12 @@ func (r *RealDebrid) GetDownloads() (map[string]types.DownloadLink, error) { // This is ordered by date, so we can skip the rest continue } - links[d.Link] = d + links[d.Link] = &d } offset += len(dl) } + return links, nil } @@ -880,6 +880,7 @@ func (r *RealDebrid) _getDownloads(offset int, limit int) ([]types.DownloadLink, Link: d.Link, DownloadLink: d.Download, Generated: d.Generated, + ExpiresAt: d.Generated.Add(r.autoExpiresLinksAfter), Id: d.Id, }) @@ -899,49 +900,6 @@ func (r *RealDebrid) GetMountPath() string { return r.MountPath } -func (r *RealDebrid) DisableAccount(accountId string) { - 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.accounts[accountId]; ok { - value.Disabled = true - r.accounts[accountId] = value - r.logger.Info().Msgf("Disabled account Index: %s", value.ID) - } -} - -func (r *RealDebrid) ResetActiveDownloadKeys() { - r.accountsMutex.Lock() - defer r.accountsMutex.Unlock() - for key, value := range r.accounts { - value.Disabled = false - r.accounts[key] = value - } -} - -func (r *RealDebrid) getActiveAccounts() []types.Account { - r.accountsMutex.RLock() - defer r.accountsMutex.RUnlock() - accounts := make([]types.Account, 0) - - for _, value := range r.accounts { - if value.Disabled { - continue - } - accounts = append(accounts, value) - } - - // Sort accounts by ID - sort.Slice(accounts, func(i, j int) bool { - return accounts[i].ID < accounts[j].ID - }) - return accounts -} - func (r *RealDebrid) DeleteDownloadLink(linkId string) error { url := fmt.Sprintf("%s/downloads/delete/%s", r.Host, linkId) req, _ := http.NewRequest(http.MethodDelete, url, nil) @@ -991,3 +949,7 @@ func (r *RealDebrid) GetAvailableSlots() (int, error) { } return data.TotalSlots - data.ActiveSlots - r.minimumFreeSlot, nil // Ensure we maintain minimum active pots } + +func (r *RealDebrid) Accounts() *types.Accounts { + return r.accounts +} diff --git a/pkg/debrid/providers/torbox/torbox.go b/pkg/debrid/providers/torbox/torbox.go index 337d598..8d7e2ef 100644 --- a/pkg/debrid/providers/torbox/torbox.go +++ b/pkg/debrid/providers/torbox/torbox.go @@ -24,10 +24,12 @@ import ( ) type Torbox struct { - name string - Host string `json:"host"` - APIKey string - accounts map[string]types.Account + name string + Host string `json:"host"` + APIKey string + accounts *types.Accounts + autoExpiresLinksAfter time.Duration + DownloadUncached bool client *request.Client @@ -55,28 +57,23 @@ func New(dc config.Debrid) (*Torbox, error) { request.WithLogger(_log), request.WithProxy(dc.Proxy), ) - - accounts := make(map[string]types.Account) - for idx, key := range dc.DownloadAPIKeys { - id := strconv.Itoa(idx) - accounts[id] = types.Account{ - Name: key, - ID: id, - Token: key, - } + autoExpiresLinksAfter, err := time.ParseDuration(dc.AutoExpireLinksAfter) + if autoExpiresLinksAfter == 0 || err != nil { + autoExpiresLinksAfter = 48 * time.Hour } return &Torbox{ - name: "torbox", - Host: "https://api.torbox.app/v1", - APIKey: dc.APIKey, - accounts: accounts, - DownloadUncached: dc.DownloadUncached, - client: client, - MountPath: dc.Folder, - logger: _log, - checkCached: dc.CheckCached, - addSamples: dc.AddSamples, + name: "torbox", + Host: "https://api.torbox.app/v1", + APIKey: dc.APIKey, + accounts: types.NewAccounts(dc), + DownloadUncached: dc.DownloadUncached, + autoExpiresLinksAfter: autoExpiresLinksAfter, + client: client, + MountPath: dc.Folder, + logger: _log, + checkCached: dc.CheckCached, + addSamples: dc.AddSamples, }, nil } @@ -326,8 +323,7 @@ func (tb *Torbox) CheckStatus(torrent *types.Torrent, isSymlink bool) (*types.To if status == "downloaded" { tb.logger.Info().Msgf("Torrent: %s downloaded", torrent.Name) if !isSymlink { - err = tb.GenerateDownloadLinks(torrent) - if err != nil { + if err = tb.GetFileDownloadLinks(torrent); err != nil { return torrent, err } } @@ -359,8 +355,9 @@ func (tb *Torbox) DeleteTorrent(torrentId string) error { return nil } -func (tb *Torbox) GenerateDownloadLinks(t *types.Torrent) error { +func (tb *Torbox) GetFileDownloadLinks(t *types.Torrent) error { filesCh := make(chan types.File, len(t.Files)) + linkCh := make(chan *types.DownloadLink) errCh := make(chan error, len(t.Files)) var wg sync.WaitGroup @@ -373,13 +370,17 @@ func (tb *Torbox) GenerateDownloadLinks(t *types.Torrent) error { errCh <- err return } - file.DownloadLink = link + if link != nil { + linkCh <- link + file.DownloadLink = link + } filesCh <- file }() } go func() { wg.Wait() close(filesCh) + close(linkCh) close(errCh) }() @@ -389,6 +390,13 @@ func (tb *Torbox) GenerateDownloadLinks(t *types.Torrent) error { files[file.Name] = file } + // Collect download links + for link := range linkCh { + if link != nil { + tb.accounts.SetDownloadLink(link.Link, link) + } + } + // Check for errors for err := range errCh { if err != nil { @@ -423,12 +431,13 @@ func (tb *Torbox) GetDownloadLink(t *types.Torrent, file *types.File) (*types.Do if link == "" { return nil, fmt.Errorf("error getting download links") } + now := time.Now() return &types.DownloadLink{ Link: file.Link, DownloadLink: link, Id: file.Id, - AccountId: "0", - Generated: time.Now(), + Generated: now, + ExpiresAt: now.Add(tb.autoExpiresLinksAfter), }, nil } @@ -436,10 +445,6 @@ func (tb *Torbox) GetDownloadingStatus() []string { return []string{"downloading"} } -func (tb *Torbox) GetCheckCached() bool { - return tb.checkCached -} - func (tb *Torbox) GetTorrents() ([]*types.Torrent, error) { return nil, nil } @@ -448,7 +453,7 @@ func (tb *Torbox) GetDownloadUncached() bool { return tb.DownloadUncached } -func (tb *Torbox) GetDownloads() (map[string]types.DownloadLink, error) { +func (tb *Torbox) GetDownloadLinks() (map[string]*types.DownloadLink, error) { return nil, nil } @@ -460,13 +465,6 @@ func (tb *Torbox) GetMountPath() string { return tb.MountPath } -func (tb *Torbox) DisableAccount(accountId string) { -} - -func (tb *Torbox) ResetActiveDownloadKeys() { - -} - func (tb *Torbox) DeleteDownloadLink(linkId string) error { return nil } @@ -475,3 +473,7 @@ func (tb *Torbox) GetAvailableSlots() (int, error) { //TODO: Implement the logic to check available slots for Torbox return 0, fmt.Errorf("not implemented") } + +func (tb *Torbox) Accounts() *types.Accounts { + return tb.accounts +} diff --git a/pkg/debrid/store/cache.go b/pkg/debrid/store/cache.go index 5edb3f5..321091a 100644 --- a/pkg/debrid/store/cache.go +++ b/pkg/debrid/store/cache.go @@ -73,7 +73,6 @@ type Cache struct { logger zerolog.Logger torrents *torrentCache - downloadLinks *downloadLinkCache invalidDownloadLinks sync.Map folderNaming WebDavFolderNaming @@ -90,10 +89,9 @@ type Cache struct { ready chan struct{} // config - workers int - torrentRefreshInterval string - downloadLinksRefreshInterval string - autoExpiresLinksAfterDuration time.Duration + workers int + torrentRefreshInterval string + downloadLinksRefreshInterval string // refresh mutex downloadLinksRefreshMu sync.RWMutex // for refreshing download links @@ -121,10 +119,6 @@ func NewDebridCache(dc config.Debrid, client types.Client) *Cache { scheduler = cetSc } - autoExpiresLinksAfter, err := time.ParseDuration(dc.AutoExpireLinksAfter) - if autoExpiresLinksAfter == 0 || err != nil { - autoExpiresLinksAfter = 48 * time.Hour - } var customFolders []string dirFilters := map[string][]directoryFilter{} for name, value := range dc.Directories { @@ -147,18 +141,16 @@ func NewDebridCache(dc config.Debrid, client types.Client) *Cache { c := &Cache{ dir: filepath.Join(cfg.Path, "cache", dc.Name), // path to save cache files - torrents: newTorrentCache(dirFilters), - client: client, - logger: _log, - workers: dc.Workers, - downloadLinks: newDownloadLinkCache(), - torrentRefreshInterval: dc.TorrentsRefreshInterval, - downloadLinksRefreshInterval: dc.DownloadLinksRefreshInterval, - folderNaming: WebDavFolderNaming(dc.FolderNaming), - autoExpiresLinksAfterDuration: autoExpiresLinksAfter, - saveSemaphore: make(chan struct{}, 50), - cetScheduler: cetSc, - scheduler: scheduler, + torrents: newTorrentCache(dirFilters), + client: client, + logger: _log, + workers: dc.Workers, + torrentRefreshInterval: dc.TorrentsRefreshInterval, + downloadLinksRefreshInterval: dc.DownloadLinksRefreshInterval, + folderNaming: WebDavFolderNaming(dc.FolderNaming), + saveSemaphore: make(chan struct{}, 50), + cetScheduler: cetSc, + scheduler: scheduler, config: dc, customFolders: customFolders, @@ -202,9 +194,6 @@ func (c *Cache) Reset() { // 1. Reset torrent storage c.torrents.reset() - // 2. Reset download-link cache - c.downloadLinks.reset() - // 3. Clear any sync.Maps c.invalidDownloadLinks = sync.Map{} c.repairRequest = sync.Map{} @@ -714,7 +703,7 @@ func (c *Cache) Add(t *types.Torrent) error { c.setTorrent(ct, func(tor CachedTorrent) { c.RefreshListings(true) }) - go c.GenerateDownloadLinks(ct) + go c.GetFileDownloadLinks(ct) return nil } diff --git a/pkg/debrid/store/download_link.go b/pkg/debrid/store/download_link.go index 951cb1d..19c57d6 100644 --- a/pkg/debrid/store/download_link.go +++ b/pkg/debrid/store/download_link.go @@ -5,58 +5,8 @@ import ( "fmt" "github.com/sirrobot01/decypharr/internal/utils" "github.com/sirrobot01/decypharr/pkg/debrid/types" - - "sync" - "time" ) -type linkCache struct { - Id string - link string - accountId string - expiresAt time.Time -} - -type downloadLinkCache struct { - data map[string]linkCache - mu sync.Mutex -} - -func newDownloadLinkCache() *downloadLinkCache { - return &downloadLinkCache{ - data: make(map[string]linkCache), - } -} - -func (c *downloadLinkCache) reset() { - c.mu.Lock() - c.data = make(map[string]linkCache) - c.mu.Unlock() -} - -func (c *downloadLinkCache) Load(key string) (linkCache, bool) { - c.mu.Lock() - defer c.mu.Unlock() - dl, ok := c.data[key] - return dl, ok -} -func (c *downloadLinkCache) Store(key string, value linkCache) { - c.mu.Lock() - defer c.mu.Unlock() - c.data[key] = value -} -func (c *downloadLinkCache) Delete(key string) { - c.mu.Lock() - defer c.mu.Unlock() - delete(c.data, key) -} - -func (c *downloadLinkCache) Len() int { - c.mu.Lock() - defer c.mu.Unlock() - return len(c.data) -} - type downloadLinkRequest struct { result string err error @@ -82,8 +32,10 @@ func (r *downloadLinkRequest) Wait() (string, error) { func (c *Cache) GetDownloadLink(torrentName, filename, fileLink string) (string, error) { // Check link cache - if dl := c.checkDownloadLink(fileLink); dl != "" { + if dl, err := c.checkDownloadLink(fileLink); dl != "" && err == nil { return dl, nil + } else { + c.logger.Trace().Msgf("Download link check failed: %v", err) } if req, inFlight := c.downloadLinkRequests.Load(fileLink); inFlight { @@ -96,34 +48,36 @@ func (c *Cache) GetDownloadLink(torrentName, filename, fileLink string) (string, 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) + dl, err := c.fetchDownloadLink(torrentName, filename, fileLink) + if err != nil { + req.Complete("", err) + c.downloadLinkRequests.Delete(fileLink) + return "", err + } + req.Complete(dl.DownloadLink, err) c.downloadLinkRequests.Delete(fileLink) - - return downloadLink, err + return dl.DownloadLink, err } -func (c *Cache) fetchDownloadLink(torrentName, filename, fileLink string) (string, error) { +func (c *Cache) fetchDownloadLink(torrentName, filename, fileLink string) (*types.DownloadLink, error) { ct := c.GetTorrentByName(torrentName) if ct == nil { - return "", fmt.Errorf("torrent not found") + return nil, fmt.Errorf("torrent not found") } file, ok := ct.GetFile(filename) if !ok { - return "", fmt.Errorf("file %s not found in torrent %s", filename, torrentName) + return nil, fmt.Errorf("file %s not found in torrent %s", filename, torrentName) } if file.Link == "" { // file link is empty, refresh the torrent to get restricted links ct = c.refreshTorrent(file.TorrentId) // Refresh the torrent from the debrid if ct == nil { - return "", fmt.Errorf("failed to refresh torrent") + return nil, fmt.Errorf("failed to refresh torrent") } else { file, ok = ct.GetFile(filename) if !ok { - return "", fmt.Errorf("file %s not found in refreshed torrent %s", filename, torrentName) + return nil, fmt.Errorf("file %s not found in refreshed torrent %s", filename, torrentName) } } } @@ -133,12 +87,12 @@ func (c *Cache) fetchDownloadLink(torrentName, filename, fileLink string) (strin // Try to reinsert the torrent? newCt, err := c.reInsertTorrent(ct) if err != nil { - return "", fmt.Errorf("failed to reinsert torrent. %w", err) + return nil, fmt.Errorf("failed to reinsert torrent. %w", err) } ct = newCt file, ok = ct.GetFile(filename) if !ok { - return "", fmt.Errorf("file %s not found in reinserted torrent %s", filename, torrentName) + return nil, fmt.Errorf("file %s not found in reinserted torrent %s", filename, torrentName) } } @@ -148,93 +102,71 @@ func (c *Cache) fetchDownloadLink(torrentName, filename, fileLink string) (strin if errors.Is(err, utils.HosterUnavailableError) { newCt, err := c.reInsertTorrent(ct) if err != nil { - return "", fmt.Errorf("failed to reinsert torrent: %w", err) + return nil, fmt.Errorf("failed to reinsert torrent: %w", err) } ct = newCt file, ok = ct.GetFile(filename) if !ok { - return "", fmt.Errorf("file %s not found in reinserted torrent %s", filename, torrentName) + return nil, fmt.Errorf("file %s not found in reinserted torrent %s", filename, torrentName) } // Retry getting the download link downloadLink, err = c.client.GetDownloadLink(ct.Torrent, &file) if err != nil { - return "", err + return nil, err } if downloadLink == nil { - return "", fmt.Errorf("download link is empty for") + return nil, fmt.Errorf("download link is empty for") } - c.updateDownloadLink(downloadLink) - return "", nil + return nil, nil } else if errors.Is(err, utils.TrafficExceededError) { // This is likely a fair usage limit error - return "", err + return nil, err } else { - return "", fmt.Errorf("failed to get download link: %w", err) + return nil, fmt.Errorf("failed to get download link: %w", err) } } if downloadLink == nil { - return "", fmt.Errorf("download link is empty") + return nil, fmt.Errorf("download link is empty") } - c.updateDownloadLink(downloadLink) - return downloadLink.DownloadLink, nil + + // Set link to cache + go c.client.Accounts().SetDownloadLink(fileLink, downloadLink) + return downloadLink, nil } -func (c *Cache) GenerateDownloadLinks(t CachedTorrent) { - if err := c.client.GenerateDownloadLinks(t.Torrent); err != nil { +func (c *Cache) GetFileDownloadLinks(t CachedTorrent) { + if err := c.client.GetFileDownloadLinks(t.Torrent); err != nil { c.logger.Error().Err(err).Str("torrent", t.Name).Msg("Failed to generate download links") return } - for _, file := range t.GetFiles() { - if file.DownloadLink != nil { - c.updateDownloadLink(file.DownloadLink) - } - } - c.setTorrent(t, nil) } -func (c *Cache) updateDownloadLink(dl *types.DownloadLink) { - c.downloadLinks.Store(dl.Link, linkCache{ - Id: dl.Id, - link: dl.DownloadLink, - expiresAt: time.Now().Add(c.autoExpiresLinksAfterDuration), - accountId: dl.AccountId, - }) -} +func (c *Cache) checkDownloadLink(link string) (string, error) { -func (c *Cache) checkDownloadLink(link string) string { - if dl, ok := c.downloadLinks.Load(link); ok { - if dl.expiresAt.After(time.Now()) && !c.IsDownloadLinkInvalid(dl.link) { - return dl.link - } + dl, err := c.client.Accounts().GetDownloadLink(link) + if err != nil { + return "", err } - return "" + if !c.downloadLinkIsInvalid(dl.DownloadLink) { + return dl.DownloadLink, nil + } + return "", fmt.Errorf("download link not found for %s", link) } func (c *Cache) MarkDownloadLinkAsInvalid(link, downloadLink, reason string) { c.invalidDownloadLinks.Store(downloadLink, reason) // Remove the download api key from active if reason == "bandwidth_exceeded" { - if dl, ok := c.downloadLinks.Load(link); ok { - if dl.accountId != "" && dl.link == downloadLink { - c.client.DisableAccount(dl.accountId) - } - } - } - c.removeDownloadLink(link) -} - -func (c *Cache) removeDownloadLink(link string) { - if dl, ok := c.downloadLinks.Load(link); ok { - // Delete dl from cache - c.downloadLinks.Delete(link) - // Delete dl from debrid - if dl.Id != "" { - _ = c.client.DeleteDownloadLink(dl.Id) + // Disable the account + _, account, err := c.client.Accounts().GetDownloadLinkWithAccount(link) + if err != nil { + return } + c.client.Accounts().Disable(account) } } -func (c *Cache) IsDownloadLinkInvalid(downloadLink string) bool { +func (c *Cache) downloadLinkIsInvalid(downloadLink string) bool { if reason, ok := c.invalidDownloadLinks.Load(downloadLink); ok { c.logger.Debug().Msgf("Download link %s is invalid: %s", downloadLink, reason) return true @@ -252,5 +184,5 @@ func (c *Cache) GetDownloadByteRange(torrentName, filename string) (*[2]int64, e } func (c *Cache) GetTotalActiveDownloadLinks() int { - return c.downloadLinks.Len() + return c.client.Accounts().GetLinksCount() } diff --git a/pkg/debrid/store/refresh.go b/pkg/debrid/store/refresh.go index f9e5a5d..0dd8c59 100644 --- a/pkg/debrid/store/refresh.go +++ b/pkg/debrid/store/refresh.go @@ -241,24 +241,14 @@ func (c *Cache) refreshDownloadLinks(ctx context.Context) { } defer c.downloadLinksRefreshMu.Unlock() - downloadLinks, err := c.client.GetDownloads() + links, err := c.client.GetDownloadLinks() if err != nil { c.logger.Error().Err(err).Msg("Failed to get download links") return } - for k, v := range downloadLinks { - // if link is generated in the last 24 hours, add it to cache - timeSince := time.Since(v.Generated) - if timeSince < c.autoExpiresLinksAfterDuration { - c.downloadLinks.Store(k, linkCache{ - Id: v.Id, - accountId: v.AccountId, - link: v.DownloadLink, - expiresAt: v.Generated.Add(c.autoExpiresLinksAfterDuration - timeSince), - }) - } else { - c.downloadLinks.Delete(k) - } - } + + c.client.Accounts().SetDownloadLinks(links) + + c.logger.Debug().Msgf("Refreshed download %d links", c.client.Accounts().GetLinksCount()) } diff --git a/pkg/debrid/store/repair.go b/pkg/debrid/store/repair.go index 8fbdb04..201109e 100644 --- a/pkg/debrid/store/repair.go +++ b/pkg/debrid/store/repair.go @@ -252,5 +252,5 @@ func (c *Cache) reInsertTorrent(ct *CachedTorrent) (*CachedTorrent, error) { func (c *Cache) resetInvalidLinks() { c.invalidDownloadLinks = sync.Map{} - c.client.ResetActiveDownloadKeys() // Reset the active download keys + c.client.Accounts().Reset() // Reset the active download keys } diff --git a/pkg/debrid/types/account.go b/pkg/debrid/types/account.go new file mode 100644 index 0000000..9187bca --- /dev/null +++ b/pkg/debrid/types/account.go @@ -0,0 +1,230 @@ +package types + +import ( + "github.com/sirrobot01/decypharr/internal/config" + "sync" + "time" +) + +type Accounts struct { + current *Account + accounts []*Account + mu sync.RWMutex +} + +func NewAccounts(debridConf config.Debrid) *Accounts { + accounts := make([]*Account, 0) + for idx, token := range debridConf.DownloadAPIKeys { + if token == "" { + continue + } + account := newAccount(token, idx) + accounts = append(accounts, account) + } + + var current *Account + if len(accounts) > 0 { + current = accounts[0] + } + return &Accounts{ + accounts: accounts, + current: current, + } +} + +type Account struct { + Order int + Disabled bool + Token string + links map[string]*DownloadLink + mu sync.RWMutex +} + +func (a *Accounts) All() []*Account { + a.mu.RLock() + defer a.mu.RUnlock() + activeAccounts := make([]*Account, 0) + for _, acc := range a.accounts { + if !acc.Disabled { + activeAccounts = append(activeAccounts, acc) + } + } + return activeAccounts +} + +func (a *Accounts) Current() *Account { + a.mu.RLock() + if a.current != nil { + current := a.current + a.mu.RUnlock() + return current + } + a.mu.RUnlock() + + a.mu.Lock() + defer a.mu.Unlock() + + // Double-check after acquiring write lock + if a.current != nil { + return a.current + } + + activeAccounts := make([]*Account, 0) + for _, acc := range a.accounts { + if !acc.Disabled { + activeAccounts = append(activeAccounts, acc) + } + } + + if len(activeAccounts) > 0 { + a.current = activeAccounts[0] + } + return a.current +} + +func (a *Accounts) Disable(account *Account) { + a.mu.Lock() + defer a.mu.Unlock() + account.disable() + + if a.current == account { + var newCurrent *Account + for _, acc := range a.accounts { + if !acc.Disabled { + newCurrent = acc + break + } + } + a.current = newCurrent + } +} + +func (a *Accounts) Reset() { + a.mu.Lock() + defer a.mu.Unlock() + for _, acc := range a.accounts { + acc.resetDownloadLinks() + acc.Disabled = false + } + if len(a.accounts) > 0 { + a.current = a.accounts[0] + } else { + a.current = nil + } +} + +func (a *Accounts) GetDownloadLink(fileLink string) (*DownloadLink, error) { + if a.Current() == nil { + return nil, NoActiveAccountsError + } + dl, ok := a.Current().getLink(fileLink) + if !ok { + return nil, NoDownloadLinkError + } + if dl.ExpiresAt.IsZero() || dl.ExpiresAt.Before(time.Now()) { + return nil, DownloadLinkExpiredError + } + if dl.DownloadLink == "" { + return nil, EmptyDownloadLinkError + } + return dl, nil +} + +func (a *Accounts) GetDownloadLinkWithAccount(fileLink string) (*DownloadLink, *Account, error) { + currentAccount := a.Current() + if currentAccount == nil { + return nil, nil, NoActiveAccountsError + } + dl, ok := currentAccount.getLink(fileLink) + if !ok { + return nil, nil, NoDownloadLinkError + } + if dl.ExpiresAt.IsZero() || dl.ExpiresAt.Before(time.Now()) { + return nil, currentAccount, DownloadLinkExpiredError + } + if dl.DownloadLink == "" { + return nil, currentAccount, EmptyDownloadLinkError + } + return dl, currentAccount, nil +} + +func (a *Accounts) SetDownloadLink(fileLink string, dl *DownloadLink) { + if a.Current() == nil { + return + } + a.Current().setLink(fileLink, dl) +} + +func (a *Accounts) DeleteDownloadLink(fileLink string) { + if a.Current() == nil { + return + } + a.Current().deleteLink(fileLink) +} + +func (a *Accounts) GetLinksCount() int { + if a.Current() == nil { + return 0 + } + return a.Current().LinksCount() +} + +func (a *Accounts) SetDownloadLinks(links map[string]*DownloadLink) { + if a.Current() == nil { + return + } + a.Current().setLinks(links) +} + +func newAccount(token string, index int) *Account { + return &Account{ + Token: token, + Order: index, + links: make(map[string]*DownloadLink), + } +} + +func (a *Account) getLink(fileLink string) (*DownloadLink, bool) { + a.mu.RLock() + defer a.mu.RUnlock() + dl, ok := a.links[fileLink[0:39]] + return dl, ok +} +func (a *Account) setLink(fileLink string, dl *DownloadLink) { + a.mu.Lock() + defer a.mu.Unlock() + a.links[fileLink[0:39]] = dl +} +func (a *Account) deleteLink(fileLink string) { + a.mu.Lock() + defer a.mu.Unlock() + + delete(a.links, fileLink[0:39]) +} +func (a *Account) resetDownloadLinks() { + a.mu.Lock() + defer a.mu.Unlock() + a.links = make(map[string]*DownloadLink) +} +func (a *Account) LinksCount() int { + a.mu.RLock() + defer a.mu.RUnlock() + return len(a.links) +} + +func (a *Account) disable() { + a.Disabled = true +} + +func (a *Account) setLinks(links map[string]*DownloadLink) { + a.mu.Lock() + defer a.mu.Unlock() + now := time.Now() + for _, dl := range links { + if !dl.ExpiresAt.IsZero() && dl.ExpiresAt.Before(now) { + // Expired, continue + continue + } + a.links[dl.Link[0:39]] = dl + } +} diff --git a/pkg/debrid/types/client.go b/pkg/debrid/types/client.go index 61b4f2b..8dfef25 100644 --- a/pkg/debrid/types/client.go +++ b/pkg/debrid/types/client.go @@ -7,11 +7,10 @@ import ( type Client interface { SubmitMagnet(tr *Torrent) (*Torrent, error) CheckStatus(tr *Torrent, isSymlink bool) (*Torrent, error) - GenerateDownloadLinks(tr *Torrent) error + GetFileDownloadLinks(tr *Torrent) error GetDownloadLink(tr *Torrent, file *File) (*DownloadLink, error) DeleteTorrent(torrentId string) error IsAvailable(infohashes []string) map[string]bool - GetCheckCached() bool GetDownloadUncached() bool UpdateTorrent(torrent *Torrent) error GetTorrent(torrentId string) (*Torrent, error) @@ -19,11 +18,10 @@ type Client interface { Name() string Logger() zerolog.Logger GetDownloadingStatus() []string - GetDownloads() (map[string]DownloadLink, error) + GetDownloadLinks() (map[string]*DownloadLink, error) CheckLink(link string) error GetMountPath() string - DisableAccount(string) - ResetActiveDownloadKeys() + Accounts() *Accounts // Returns the active download account/token DeleteDownloadLink(linkId string) error GetProfile() (*Profile, error) GetAvailableSlots() (int, error) diff --git a/pkg/debrid/types/error.go b/pkg/debrid/types/error.go new file mode 100644 index 0000000..c8cf016 --- /dev/null +++ b/pkg/debrid/types/error.go @@ -0,0 +1,30 @@ +package types + +type Error struct { + Message string `json:"message"` + Code string `json:"code"` +} + +func (e *Error) Error() string { + return e.Message +} + +var NoActiveAccountsError = &Error{ + Message: "No active accounts", + Code: "no_active_accounts", +} + +var NoDownloadLinkError = &Error{ + Message: "No download link found", + Code: "no_download_link", +} + +var DownloadLinkExpiredError = &Error{ + Message: "Download link expired", + Code: "download_link_expired", +} + +var EmptyDownloadLinkError = &Error{ + Message: "Download link is empty", + Code: "empty_download_link", +} diff --git a/pkg/debrid/types/torrent.go b/pkg/debrid/types/torrent.go index 0975e0d..60f8bc7 100644 --- a/pkg/debrid/types/torrent.go +++ b/pkg/debrid/types/torrent.go @@ -42,20 +42,6 @@ type Torrent struct { sync.Mutex } -type DownloadLink struct { - Filename string `json:"filename"` - Link string `json:"link"` - DownloadLink string `json:"download_link"` - Generated time.Time `json:"generated"` - Size int64 `json:"size"` - Id string `json:"id"` - AccountId string `json:"account_id"` -} - -func (d *DownloadLink) String() string { - return d.DownloadLink -} - func (t *Torrent) GetSymlinkFolder(parent string) string { return filepath.Join(parent, t.Arr.Name, t.Folder) } @@ -106,10 +92,10 @@ type File struct { ByteRange *[2]int64 `json:"byte_range,omitempty"` Path string `json:"path"` Link string `json:"link"` - DownloadLink *DownloadLink `json:"-"` AccountId string `json:"account_id"` Generated time.Time `json:"generated"` Deleted bool `json:"deleted"` + DownloadLink *DownloadLink `json:"-"` } func (t *Torrent) Cleanup(remove bool) { @@ -121,13 +107,6 @@ func (t *Torrent) Cleanup(remove bool) { } } -type Account struct { - ID string `json:"id"` - Disabled bool `json:"disabled"` - Name string `json:"name"` - Token string `json:"token"` -} - type IngestData struct { Debrid string `json:"debrid"` Name string `json:"name"` @@ -149,3 +128,17 @@ type Profile struct { BadTorrents int `json:"bad_torrents"` ActiveLinks int `json:"active_links"` } + +type DownloadLink struct { + Filename string `json:"filename"` + Link string `json:"link"` + DownloadLink string `json:"download_link"` + Generated time.Time `json:"generated"` + Size int64 `json:"size"` + Id string `json:"id"` + ExpiresAt time.Time +} + +func (d *DownloadLink) String() string { + return d.DownloadLink +}