- IMplement multi-download api tokens

- Move things around a bit
This commit is contained in:
Mukhtar Akere
2025-06-08 19:06:17 +01:00
parent 5bf1dab5e6
commit 3efda45304
13 changed files with 607 additions and 465 deletions

View File

@@ -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
}

View File

@@ -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()
}

View File

@@ -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())
}

View File

@@ -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
}