From eefe8a390183704fb749cfd775bcd9bedd79b682 Mon Sep 17 00:00:00 2001 From: Mukhtar Akere Date: Sun, 24 Aug 2025 21:54:26 +0100 Subject: [PATCH] Hotfix for download link generation and account switching --- internal/request/request.go | 73 ++++++------ pkg/debrid/providers/alldebrid/alldebrid.go | 20 ++-- .../providers/debridlink/debrid_link.go | 13 +- pkg/debrid/providers/realdebrid/realdebrid.go | 87 +++++++------- pkg/debrid/providers/torbox/torbox.go | 20 ++-- pkg/debrid/store/cache.go | 41 ++++++- pkg/debrid/store/download_link.go | 27 +---- pkg/debrid/store/refresh.go | 6 +- pkg/debrid/types/account.go | 111 +++++++++++------- pkg/debrid/types/client.go | 4 +- pkg/debrid/types/torrent.go | 14 ++- pkg/server/debug.go | 1 + pkg/web/templates/stats.html | 5 + pkg/webdav/file.go | 22 +--- 14 files changed, 242 insertions(+), 202 deletions(-) diff --git a/internal/request/request.go b/internal/request/request.go index 6fa1f43..ab14c7c 100644 --- a/internal/request/request.go +++ b/internal/request/request.go @@ -298,40 +298,7 @@ func New(options ...ClientOption) *Client { } // Configure proxy if needed - if client.proxy != "" { - if strings.HasPrefix(client.proxy, "socks5://") { - // Handle SOCKS5 proxy - socksURL, err := url.Parse(client.proxy) - if err != nil { - client.logger.Error().Msgf("Failed to parse SOCKS5 proxy URL: %v", err) - } else { - auth := &proxy.Auth{} - if socksURL.User != nil { - auth.User = socksURL.User.Username() - password, _ := socksURL.User.Password() - auth.Password = password - } - - dialer, err := proxy.SOCKS5("tcp", socksURL.Host, auth, proxy.Direct) - if err != nil { - client.logger.Error().Msgf("Failed to create SOCKS5 dialer: %v", err) - } else { - transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { - return dialer.Dial(network, addr) - } - } - } - } else { - proxyURL, err := url.Parse(client.proxy) - if err != nil { - client.logger.Error().Msgf("Failed to parse proxy URL: %v", err) - } else { - transport.Proxy = http.ProxyURL(proxyURL) - } - } - } else { - transport.Proxy = http.ProxyFromEnvironment - } + SetProxy(transport, client.proxy) // Set the transport to the client client.client.Transport = transport @@ -417,3 +384,41 @@ func isRetryableError(err error) bool { // Not a retryable error return false } + +func SetProxy(transport *http.Transport, proxyURL string) { + if proxyURL != "" { + if strings.HasPrefix(proxyURL, "socks5://") { + // Handle SOCKS5 proxy + socksURL, err := url.Parse(proxyURL) + if err != nil { + return + } else { + auth := &proxy.Auth{} + if socksURL.User != nil { + auth.User = socksURL.User.Username() + password, _ := socksURL.User.Password() + auth.Password = password + } + + dialer, err := proxy.SOCKS5("tcp", socksURL.Host, auth, proxy.Direct) + if err != nil { + return + } else { + transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { + return dialer.Dial(network, addr) + } + } + } + } else { + _proxy, err := url.Parse(proxyURL) + if err != nil { + return + } else { + transport.Proxy = http.ProxyURL(_proxy) + } + } + } else { + transport.Proxy = http.ProxyFromEnvironment + } + return +} diff --git a/pkg/debrid/providers/alldebrid/alldebrid.go b/pkg/debrid/providers/alldebrid/alldebrid.go index fb074d6..af3ab04 100644 --- a/pkg/debrid/providers/alldebrid/alldebrid.go +++ b/pkg/debrid/providers/alldebrid/alldebrid.go @@ -301,7 +301,7 @@ func (ad *AllDebrid) GetFileDownloadLinks(t *types.Torrent) error { for _, file := range t.Files { go func(file types.File) { defer wg.Done() - link, err := ad.GetDownloadLink(t, &file) + link, _, err := ad.GetDownloadLink(t, &file) if err != nil { errCh <- err return @@ -336,7 +336,7 @@ func (ad *AllDebrid) GetFileDownloadLinks(t *types.Torrent) error { links[link.Link] = link } // Update the files with download links - ad.accounts.SetDownloadLinks(links) + ad.accounts.SetDownloadLinks(nil, links) // Check for errors for err := range errCh { @@ -349,7 +349,7 @@ func (ad *AllDebrid) GetFileDownloadLinks(t *types.Torrent) error { return nil } -func (ad *AllDebrid) GetDownloadLink(t *types.Torrent, file *types.File) (*types.DownloadLink, error) { +func (ad *AllDebrid) GetDownloadLink(t *types.Torrent, file *types.File) (*types.DownloadLink, *types.Account, error) { url := fmt.Sprintf("%s/link/unlock", ad.Host) query := gourl.Values{} query.Add("link", file.Link) @@ -357,19 +357,19 @@ func (ad *AllDebrid) GetDownloadLink(t *types.Torrent, file *types.File) (*types req, _ := http.NewRequest(http.MethodGet, url, nil) resp, err := ad.client.MakeRequest(req) if err != nil { - return nil, err + return nil, nil, err } var data DownloadLink if err = json.Unmarshal(resp, &data); err != nil { - return nil, err + return nil, nil, err } if data.Error != nil { - return nil, fmt.Errorf("error getting download link: %s", data.Error.Message) + return nil, nil, fmt.Errorf("error getting download link: %s", data.Error.Message) } link := data.Data.Link if link == "" { - return nil, fmt.Errorf("download link is empty") + return nil, nil, fmt.Errorf("download link is empty") } now := time.Now() return &types.DownloadLink{ @@ -380,7 +380,7 @@ func (ad *AllDebrid) GetDownloadLink(t *types.Torrent, file *types.File) (*types Filename: file.Name, Generated: now, ExpiresAt: now.Add(ad.autoExpiresLinksAfter), - }, nil + }, nil, nil } func (ad *AllDebrid) GetTorrents() ([]*types.Torrent, error) { @@ -416,8 +416,8 @@ func (ad *AllDebrid) GetTorrents() ([]*types.Torrent, error) { return torrents, nil } -func (ad *AllDebrid) GetDownloadLinks() (map[string]*types.DownloadLink, error) { - return nil, nil +func (ad *AllDebrid) RefreshDownloadLinks() error { + return nil } func (ad *AllDebrid) GetDownloadingStatus() []string { diff --git a/pkg/debrid/providers/debridlink/debrid_link.go b/pkg/debrid/providers/debridlink/debrid_link.go index 4826506..afebd81 100644 --- a/pkg/debrid/providers/debridlink/debrid_link.go +++ b/pkg/debrid/providers/debridlink/debrid_link.go @@ -247,7 +247,7 @@ func (dl *DebridLink) UpdateTorrent(t *types.Torrent) error { t.Files[f.Name] = file } - dl.accounts.SetDownloadLinks(links) + dl.accounts.SetDownloadLinks(nil, links) return nil } @@ -308,8 +308,7 @@ func (dl *DebridLink) SubmitMagnet(t *types.Torrent) (*types.Torrent, error) { file.DownloadLink = link t.Files[f.Name] = file } - - dl.accounts.SetDownloadLinks(links) + dl.accounts.SetDownloadLinks(nil, links) return t, nil } @@ -353,11 +352,11 @@ func (dl *DebridLink) GetFileDownloadLinks(t *types.Torrent) error { return nil } -func (dl *DebridLink) GetDownloadLinks() (map[string]*types.DownloadLink, error) { - return nil, nil +func (dl *DebridLink) RefreshDownloadLinks() error { + return nil } -func (dl *DebridLink) GetDownloadLink(t *types.Torrent, file *types.File) (*types.DownloadLink, error) { +func (dl *DebridLink) GetDownloadLink(t *types.Torrent, file *types.File) (*types.DownloadLink, *types.Account, error) { return dl.accounts.GetDownloadLink(file.Link) } @@ -452,7 +451,7 @@ func (dl *DebridLink) getTorrents(page, perPage int) ([]*types.Torrent, error) { } torrents = append(torrents, torrent) } - dl.accounts.SetDownloadLinks(links) + dl.accounts.SetDownloadLinks(nil, links) return torrents, nil } diff --git a/pkg/debrid/providers/realdebrid/realdebrid.go b/pkg/debrid/providers/realdebrid/realdebrid.go index a9a249f..47b88c7 100644 --- a/pkg/debrid/providers/realdebrid/realdebrid.go +++ b/pkg/debrid/providers/realdebrid/realdebrid.go @@ -195,7 +195,7 @@ func (r *RealDebrid) handleRarArchive(t *types.Torrent, data torrentInfo, select r.logger.Info().Msgf("RAR file detected, unpacking: %s", t.Name) linkFile := &types.File{TorrentId: t.Id, Link: data.Links[0]} - downloadLinkObj, err := r.GetDownloadLink(t, linkFile) + downloadLinkObj, account, err := r.GetDownloadLink(t, linkFile) if err != nil { r.logger.Debug().Err(err).Msgf("Error getting download link for RAR file: %s. Falling back to single file representation.", t.Name) @@ -244,6 +244,7 @@ func (r *RealDebrid) handleRarArchive(t *types.Torrent, data torrentInfo, select return r.handleRarFallback(t, data) } r.logger.Info().Msgf("Unpacked RAR archive for torrent: %s with %d files", t.Name, len(files)) + r.accounts.SetDownloadLink(account, downloadLinkObj) return files, nil } @@ -588,7 +589,7 @@ func (r *RealDebrid) GetFileDownloadLinks(t *types.Torrent) error { go func(file types.File) { defer wg.Done() - link, err := r.GetDownloadLink(t, &file) + link, account, err := r.GetDownloadLink(t, &file) if err != nil { mu.Lock() if firstErr == nil { @@ -607,6 +608,7 @@ func (r *RealDebrid) GetFileDownloadLinks(t *types.Torrent) error { } file.DownloadLink = link + r.accounts.SetDownloadLink(account, link) mu.Lock() files[file.Name] = file @@ -622,7 +624,6 @@ func (r *RealDebrid) GetFileDownloadLinks(t *types.Torrent) error { } // Add links to cache - r.accounts.SetDownloadLinks(links) t.Files = files return nil } @@ -643,7 +644,7 @@ func (r *RealDebrid) CheckLink(link string) error { return nil } -func (r *RealDebrid) _getDownloadLink(file *types.File) (*types.DownloadLink, error) { +func (r *RealDebrid) getDownloadLink(account *types.Account, file *types.File) (*types.DownloadLink, error) { url := fmt.Sprintf("%s/unrestrict/link/", r.Host) _link := file.Link if strings.HasPrefix(file.Link, "https://real-debrid.com/d/") && len(file.Link) > 39 { @@ -653,6 +654,7 @@ func (r *RealDebrid) _getDownloadLink(file *types.File) (*types.DownloadLink, er "link": {_link}, } req, _ := http.NewRequest(http.MethodPost, url, strings.NewReader(payload.Encode())) + r.downloadClient.SetHeader("Authorization", fmt.Sprintf("Bearer %s", account.Token)) resp, err := r.downloadClient.Do(req) if err != nil { @@ -709,16 +711,14 @@ func (r *RealDebrid) _getDownloadLink(file *types.File) (*types.DownloadLink, er } -func (r *RealDebrid) GetDownloadLink(t *types.Torrent, file *types.File) (*types.DownloadLink, error) { +func (r *RealDebrid) GetDownloadLink(t *types.Torrent, file *types.File) (*types.DownloadLink, *types.Account, error) { accounts := r.accounts.Active() - for _, account := range accounts { - r.downloadClient.SetHeader("Authorization", fmt.Sprintf("Bearer %s", account.Token)) - downloadLink, err := r._getDownloadLink(file) + downloadLink, err := r.getDownloadLink(account, file) if err == nil { - return downloadLink, nil + return downloadLink, account, nil } retries := 0 @@ -727,16 +727,16 @@ func (r *RealDebrid) GetDownloadLink(t *types.Torrent, file *types.File) (*types retries = 5 } else { // If the error is not traffic exceeded, return the error - return nil, err + return nil, account, err } backOff := 1 * time.Second for retries > 0 { - downloadLink, err = r._getDownloadLink(file) + downloadLink, err = r.getDownloadLink(account, file) if err == nil { - return downloadLink, nil + return downloadLink, account, nil } if !errors.Is(err, utils.TrafficExceededError) { - return nil, err + return nil, account, err } // Add a delay before retrying time.Sleep(backOff) @@ -744,7 +744,7 @@ func (r *RealDebrid) GetDownloadLink(t *types.Torrent, file *types.File) (*types retries-- } } - return nil, fmt.Errorf("realdebrid API error: download link not found") + return nil, nil, fmt.Errorf("realdebrid API error: download link not found") } func (r *RealDebrid) getTorrents(offset int, limit int) (int, []*types.Torrent, error) { @@ -841,48 +841,47 @@ func (r *RealDebrid) GetTorrents() ([]*types.Torrent, error) { return allTorrents, nil } -func (r *RealDebrid) GetDownloadLinks() (map[string]*types.DownloadLink, error) { - links := make(map[string]*types.DownloadLink) - offset := 0 - limit := 1000 +func (r *RealDebrid) RefreshDownloadLinks() error { + accounts := r.accounts.All() - accounts := r.accounts.Active() - - if len(accounts) < 1 { - // No active download keys. It's likely that the key has reached bandwidth limit - return links, fmt.Errorf("no active download keys") - } - activeAccount := accounts[0] - r.downloadClient.SetHeader("Authorization", fmt.Sprintf("Bearer %s", activeAccount.Token)) - for { - dl, err := r._getDownloads(offset, limit) - if err != nil { - break + for _, account := range accounts { + if account == nil || account.Token == "" { + continue } - if len(dl) == 0 { - break - } - - for _, d := range dl { - if _, exists := links[d.Link]; exists { - // This is ordered by date, so we can skip the rest - continue + offset := 0 + limit := 1000 + links := make(map[string]*types.DownloadLink) + for { + dl, err := r.getDownloadLinks(account, offset, limit) + if err != nil { + break } - links[d.Link] = &d + if len(dl) == 0 { + break + } + + for _, d := range dl { + if _, exists := links[d.Link]; exists { + // This is ordered by date, so we can skip the rest + continue + } + links[d.Link] = &d + } + + offset += len(dl) } - - offset += len(dl) + r.accounts.SetDownloadLinks(account, links) } - - return links, nil + return nil } -func (r *RealDebrid) _getDownloads(offset int, limit int) ([]types.DownloadLink, error) { +func (r *RealDebrid) getDownloadLinks(account *types.Account, offset int, limit int) ([]types.DownloadLink, error) { url := fmt.Sprintf("%s/downloads?limit=%d", r.Host, limit) if offset > 0 { url = fmt.Sprintf("%s&offset=%d", url, offset) } req, _ := http.NewRequest(http.MethodGet, url, nil) + r.downloadClient.SetHeader("Authorization", fmt.Sprintf("Bearer %s", account.Token)) resp, err := r.downloadClient.MakeRequest(req) if err != nil { return nil, err diff --git a/pkg/debrid/providers/torbox/torbox.go b/pkg/debrid/providers/torbox/torbox.go index bdaac13..bd850f6 100644 --- a/pkg/debrid/providers/torbox/torbox.go +++ b/pkg/debrid/providers/torbox/torbox.go @@ -411,7 +411,7 @@ func (tb *Torbox) GetFileDownloadLinks(t *types.Torrent) error { for _, file := range t.Files { go func() { defer wg.Done() - link, err := tb.GetDownloadLink(t, &file) + link, _, err := tb.GetDownloadLink(t, &file) if err != nil { errCh <- err return @@ -439,7 +439,7 @@ func (tb *Torbox) GetFileDownloadLinks(t *types.Torrent) error { // Collect download links for link := range linkCh { if link != nil { - tb.accounts.SetDownloadLink(link.Link, link) + tb.accounts.SetDownloadLink(nil, link) } } @@ -454,7 +454,7 @@ func (tb *Torbox) GetFileDownloadLinks(t *types.Torrent) error { return nil } -func (tb *Torbox) GetDownloadLink(t *types.Torrent, file *types.File) (*types.DownloadLink, error) { +func (tb *Torbox) GetDownloadLink(t *types.Torrent, file *types.File) (*types.DownloadLink, *types.Account, error) { url := fmt.Sprintf("%s/api/torrents/requestdl/", tb.Host) query := gourl.Values{} query.Add("torrent_id", t.Id) @@ -470,7 +470,7 @@ func (tb *Torbox) GetDownloadLink(t *types.Torrent, file *types.File) (*types.Do Str("torrent_id", t.Id). Str("file_id", file.Id). Msg("Failed to make request to Torbox API") - return nil, err + return nil, nil, err } var data DownloadLinksResponse @@ -480,7 +480,7 @@ func (tb *Torbox) GetDownloadLink(t *types.Torrent, file *types.File) (*types.Do Str("torrent_id", t.Id). Str("file_id", file.Id). Msg("Failed to unmarshal Torbox API response") - return nil, err + return nil, nil, err } if data.Data == nil { @@ -491,7 +491,7 @@ func (tb *Torbox) GetDownloadLink(t *types.Torrent, file *types.File) (*types.Do Interface("error", data.Error). Str("detail", data.Detail). Msg("Torbox API returned no data") - return nil, fmt.Errorf("error getting download links") + return nil, nil, fmt.Errorf("error getting download links") } link := *data.Data @@ -500,7 +500,7 @@ func (tb *Torbox) GetDownloadLink(t *types.Torrent, file *types.File) (*types.Do Str("torrent_id", t.Id). Str("file_id", file.Id). Msg("Torbox API returned empty download link") - return nil, fmt.Errorf("error getting download links") + return nil, nil, fmt.Errorf("error getting download links") } now := time.Now() @@ -512,7 +512,7 @@ func (tb *Torbox) GetDownloadLink(t *types.Torrent, file *types.File) (*types.Do ExpiresAt: now.Add(tb.autoExpiresLinksAfter), } - return downloadLink, nil + return downloadLink, nil, nil } func (tb *Torbox) GetDownloadingStatus() []string { @@ -607,8 +607,8 @@ func (tb *Torbox) GetDownloadUncached() bool { return tb.DownloadUncached } -func (tb *Torbox) GetDownloadLinks() (map[string]*types.DownloadLink, error) { - return nil, nil +func (tb *Torbox) RefreshDownloadLinks() error { + return nil } func (tb *Torbox) CheckLink(link string) error { diff --git a/pkg/debrid/store/cache.go b/pkg/debrid/store/cache.go index 8f61f12..a135579 100644 --- a/pkg/debrid/store/cache.go +++ b/pkg/debrid/store/cache.go @@ -4,9 +4,12 @@ import ( "bufio" "cmp" "context" + "crypto/tls" "errors" "fmt" + "github.com/sirrobot01/decypharr/internal/request" "github.com/sirrobot01/decypharr/pkg/rclone" + "net/http" "os" "path" "path/filepath" @@ -40,6 +43,20 @@ const ( WebdavUseHash WebDavFolderNaming = "infohash" ) +var streamingTransport = &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + MaxIdleConns: 200, + MaxIdleConnsPerHost: 100, + MaxConnsPerHost: 200, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ResponseHeaderTimeout: 60 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + DisableKeepAlives: true, + ForceAttemptHTTP2: false, + TLSNextProto: make(map[string]func(string, *tls.Conn) http.RoundTripper), +} + type CachedTorrent struct { *types.Torrent AddedOn time.Time `json:"added_on"` @@ -81,9 +98,8 @@ type Cache struct { listingDebouncer *utils.Debouncer[bool] // monitors - repairRequest sync.Map - failedToReinsert sync.Map - downloadLinkRequests sync.Map + repairRequest sync.Map + failedToReinsert sync.Map // repair repairChan chan RepairRequest @@ -108,6 +124,7 @@ type Cache struct { config config.Debrid customFolders []string mounter *rclone.Mount + httpClient *http.Client } func NewDebridCache(dc config.Debrid, client types.Client, mounter *rclone.Mount) *Cache { @@ -153,6 +170,16 @@ func NewDebridCache(dc config.Debrid, client types.Client, mounter *rclone.Mount } _log := logger.New(fmt.Sprintf("%s-webdav", client.Name())) + if dc.Proxy != "" { + + } + transport := streamingTransport + request.SetProxy(transport, dc.Proxy) + httpClient := &http.Client{ + Transport: transport, + Timeout: 0, + } + c := &Cache{ dir: filepath.Join(cfg.Path, "cache", dc.Name), // path to save cache files @@ -171,7 +198,8 @@ func NewDebridCache(dc config.Debrid, client types.Client, mounter *rclone.Mount customFolders: customFolders, mounter: mounter, - ready: make(chan struct{}), + ready: make(chan struct{}), + httpClient: httpClient, } c.listingDebouncer = utils.NewDebouncer[bool](100*time.Millisecond, func(refreshRclone bool) { @@ -225,7 +253,6 @@ func (c *Cache) Reset() { c.invalidDownloadLinks = sync.Map{} c.repairRequest = sync.Map{} c.failedToReinsert = sync.Map{} - c.downloadLinkRequests = sync.Map{} // 5. Rebuild the listing debouncer c.listingDebouncer = utils.NewDebouncer[bool]( @@ -904,3 +931,7 @@ func (c *Cache) Logger() zerolog.Logger { func (c *Cache) GetConfig() config.Debrid { return c.config } + +func (c *Cache) Download(req *http.Request) (*http.Response, error) { + return c.httpClient.Do(req) +} diff --git a/pkg/debrid/store/download_link.go b/pkg/debrid/store/download_link.go index b18e9a3..50f8de5 100644 --- a/pkg/debrid/store/download_link.go +++ b/pkg/debrid/store/download_link.go @@ -36,31 +36,15 @@ func (c *Cache) GetDownloadLink(torrentName, filename, fileLink string) (string, 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) - dl, err := c.fetchDownloadLink(torrentName, filename, fileLink) if err != nil { - req.Complete("", err) - c.downloadLinkRequests.Delete(fileLink) return "", err } if dl == nil || dl.DownloadLink == "" { err = fmt.Errorf("download link is empty for %s in torrent %s", filename, torrentName) - req.Complete("", err) - c.downloadLinkRequests.Delete(fileLink) return "", err } - req.Complete(dl.DownloadLink, err) - c.downloadLinkRequests.Delete(fileLink) return dl.DownloadLink, err } @@ -102,10 +86,11 @@ func (c *Cache) fetchDownloadLink(torrentName, filename, fileLink string) (*type } c.logger.Trace().Msgf("Getting download link for %s(%s)", filename, file.Link) - downloadLink, err := c.client.GetDownloadLink(ct.Torrent, &file) + downloadLink, account, err := c.client.GetDownloadLink(ct.Torrent, &file) if err != nil { if errors.Is(err, utils.HosterUnavailableError) { c.logger.Trace(). + Str("account", account.Username). Str("filename", filename). Str("torrent_id", ct.Id). Msg("Hoster unavailable, attempting to reinsert torrent") @@ -120,7 +105,7 @@ func (c *Cache) fetchDownloadLink(torrentName, filename, fileLink string) (*type 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) + downloadLink, account, err = c.client.GetDownloadLink(ct.Torrent, &file) if err != nil { return nil, fmt.Errorf("retry failed to get download link: %w", err) } @@ -140,7 +125,7 @@ func (c *Cache) fetchDownloadLink(torrentName, filename, fileLink string) (*type } // Set link to cache - go c.client.Accounts().SetDownloadLink(fileLink, downloadLink) + go c.client.Accounts().SetDownloadLink(account, downloadLink) return downloadLink, nil } @@ -153,7 +138,7 @@ func (c *Cache) GetFileDownloadLinks(t CachedTorrent) { func (c *Cache) checkDownloadLink(link string) (string, error) { - dl, err := c.client.Accounts().GetDownloadLink(link) + dl, _, err := c.client.Accounts().GetDownloadLink(link) if err != nil { return "", err } @@ -168,7 +153,7 @@ func (c *Cache) MarkDownloadLinkAsInvalid(link, downloadLink, reason string) { // Remove the download api key from active if reason == "bandwidth_exceeded" { // Disable the account - _, account, err := c.client.Accounts().GetDownloadLinkWithAccount(link) + account, err := c.client.Accounts().GetAccountFromLink(link) if err != nil { return } diff --git a/pkg/debrid/store/refresh.go b/pkg/debrid/store/refresh.go index 505ed88..c2f3dc4 100644 --- a/pkg/debrid/store/refresh.go +++ b/pkg/debrid/store/refresh.go @@ -243,14 +243,10 @@ func (c *Cache) refreshDownloadLinks(ctx context.Context) { } defer c.downloadLinksRefreshMu.Unlock() - links, err := c.client.GetDownloadLinks() - - if err != nil { + if err := c.client.RefreshDownloadLinks(); err != nil { c.logger.Error().Err(err).Msg("Failed to get download links") return } - c.client.Accounts().SetDownloadLinks(links) - c.logger.Debug().Msgf("Refreshed download %d links", c.client.Accounts().GetLinksCount()) } diff --git a/pkg/debrid/types/account.go b/pkg/debrid/types/account.go index 53453d7..b095bd8 100644 --- a/pkg/debrid/types/account.go +++ b/pkg/debrid/types/account.go @@ -4,11 +4,11 @@ import ( "github.com/sirrobot01/decypharr/internal/config" "slices" "sync" - "time" + "sync/atomic" ) type Accounts struct { - current *Account + current atomic.Value accounts sync.Map // map[string]*Account // key is token } @@ -27,7 +27,7 @@ func NewAccounts(debridConf config.Debrid) *Accounts { current = account } } - a.current = current + a.setCurrent(current) return a } @@ -35,6 +35,7 @@ type Account struct { Debrid string // e.g., "realdebrid", "torbox", etc. Order int Disabled bool + InUse bool Token string `json:"token"` links map[string]*DownloadLink mu sync.RWMutex @@ -75,25 +76,53 @@ func (a *Accounts) All() []*Account { return allAccounts } +func (a *Accounts) getCurrent() *Account { + if acc := a.current.Load(); acc != nil { + if current, ok := acc.(*Account); ok { + return current + } + } + return nil +} + func (a *Accounts) Current() *Account { - if a.current != nil && !a.current.Disabled { - current := a.current + current := a.getCurrent() + if current != nil && !current.Disabled { return current } activeAccounts := a.Active() if len(activeAccounts) == 0 { - return a.current + return current } - a.current = activeAccounts[0] + current = activeAccounts[0] + a.setCurrent(current) + return current +} - return a.current +func (a *Accounts) setCurrent(account *Account) { + if account == nil { + return + } + // Set every account InUse to false + a.accounts.Range(func(key, value interface{}) bool { + acc, ok := value.(*Account) + if ok { + acc.InUse = false + a.accounts.Store(key, acc) + } + return true + }) + account.InUse = true + a.current.Store(account) } func (a *Accounts) Disable(account *Account) { account.Disabled = true a.accounts.Store(account.Token, account) - if a.current.Equals(account) { + current := a.getCurrent() + + if current.Equals(account) { var newCurrent *Account a.accounts.Range(func(key, value interface{}) bool { @@ -104,66 +133,67 @@ func (a *Accounts) Disable(account *Account) { } return true // Continue the loop }) - a.current = newCurrent + a.setCurrent(newCurrent) } } func (a *Accounts) Reset() { + var current *Account a.accounts.Range(func(key, value interface{}) bool { acc, ok := value.(*Account) if ok { acc.resetDownloadLinks() acc.Disabled = false a.accounts.Store(key, acc) - if a.current == nil { - a.current = acc + if current == nil { + current = acc } } return true }) + a.setCurrent(current) } -func (a *Accounts) GetDownloadLink(fileLink string) (*DownloadLink, error) { - if a.Current() == nil { - return nil, NoActiveAccountsError +func (a *Accounts) GetDownloadLink(fileLink string) (*DownloadLink, *Account, error) { + current := a.Current() + if current == nil { + return nil, nil, NoActiveAccountsError } - dl, ok := a.Current().getLink(fileLink) + dl, ok := current.getLink(fileLink) if !ok { - return nil, NoDownloadLinkError + return nil, current, NoDownloadLinkError } - if dl.ExpiresAt.IsZero() || dl.ExpiresAt.Before(time.Now()) { - return nil, DownloadLinkExpiredError + if err := dl.Valid(); err != nil { + return nil, current, err } - if dl.DownloadLink == "" { - return nil, EmptyDownloadLinkError - } - return dl, nil + return dl, current, nil } -func (a *Accounts) GetDownloadLinkWithAccount(fileLink string) (*DownloadLink, *Account, error) { +func (a *Accounts) GetAccountFromLink(fileLink string) (*Account, error) { currentAccount := a.Current() if currentAccount == nil { - return nil, nil, NoActiveAccountsError + return 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 + return nil, NoDownloadLinkError } if dl.DownloadLink == "" { - return nil, currentAccount, EmptyDownloadLinkError + return currentAccount, EmptyDownloadLinkError } - return dl, currentAccount, nil + return currentAccount, nil } -func (a *Accounts) SetDownloadLink(fileLink string, dl *DownloadLink) { - if a.Current() == nil { +// SetDownloadLink sets the download link for the current account +func (a *Accounts) SetDownloadLink(account *Account, dl *DownloadLink) { + if dl == nil { return } - a.Current().setLink(fileLink, dl) + if account == nil { + account = a.getCurrent() + } + account.setLink(dl.Link, dl) } func (a *Accounts) DeleteDownloadLink(fileLink string) { @@ -180,11 +210,12 @@ func (a *Accounts) GetLinksCount() int { return a.Current().LinksCount() } -func (a *Accounts) SetDownloadLinks(links map[string]*DownloadLink) { - if a.Current() == nil { - return +func (a *Accounts) SetDownloadLinks(account *Account, links map[string]*DownloadLink) { + if account == nil { + account = a.Current() } - a.Current().setLinks(links) + account.setLinks(links) + a.accounts.Store(account.Token, account) } func (a *Accounts) Update(account *Account) { @@ -241,10 +272,8 @@ func (a *Account) LinksCount() int { 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 + if err := dl.Valid(); err != nil { continue } a.links[a.sliceFileLink(dl.Link)] = dl diff --git a/pkg/debrid/types/client.go b/pkg/debrid/types/client.go index 8eaa6c6..6aeff7f 100644 --- a/pkg/debrid/types/client.go +++ b/pkg/debrid/types/client.go @@ -8,7 +8,7 @@ type Client interface { SubmitMagnet(tr *Torrent) (*Torrent, error) CheckStatus(tr *Torrent) (*Torrent, error) GetFileDownloadLinks(tr *Torrent) error - GetDownloadLink(tr *Torrent, file *File) (*DownloadLink, error) + GetDownloadLink(tr *Torrent, file *File) (*DownloadLink, *Account, error) DeleteTorrent(torrentId string) error IsAvailable(infohashes []string) map[string]bool GetDownloadUncached() bool @@ -18,7 +18,7 @@ type Client interface { Name() string Logger() zerolog.Logger GetDownloadingStatus() []string - GetDownloadLinks() (map[string]*DownloadLink, error) + RefreshDownloadLinks() error CheckLink(link string) error GetMountPath() string Accounts() *Accounts // Returns the active download account/token diff --git a/pkg/debrid/types/torrent.go b/pkg/debrid/types/torrent.go index 093c9ec..b5de18e 100644 --- a/pkg/debrid/types/torrent.go +++ b/pkg/debrid/types/torrent.go @@ -179,6 +179,16 @@ type DownloadLink struct { ExpiresAt time.Time } -func (d *DownloadLink) String() string { - return d.DownloadLink +func (dl *DownloadLink) Valid() error { + if dl.DownloadLink == "" { + return EmptyDownloadLinkError + } + if dl.ExpiresAt.IsZero() || dl.ExpiresAt.Before(time.Now()) { + return DownloadLinkExpiredError + } + return nil +} + +func (dl *DownloadLink) String() string { + return dl.DownloadLink } diff --git a/pkg/server/debug.go b/pkg/server/debug.go index 2ed14c7..c889463 100644 --- a/pkg/server/debug.go +++ b/pkg/server/debug.go @@ -136,6 +136,7 @@ func (s *Server) handleStats(w http.ResponseWriter, r *http.Request) { accountDetail := map[string]any{ "order": account.Order, "disabled": account.Disabled, + "in_use": account.InUse, "token_masked": maskedToken, "username": account.Username, "traffic_used": account.TrafficUsed, diff --git a/pkg/web/templates/stats.html b/pkg/web/templates/stats.html index 2e66108..03042d1 100644 --- a/pkg/web/templates/stats.html +++ b/pkg/web/templates/stats.html @@ -371,6 +371,10 @@ const statusBadge = account.disabled ? 'Disabled' : 'Active'; + + const inUseBadge = account.in_use ? + 'In Use' : + ''; html += `
@@ -380,6 +384,7 @@
Account #${account.order + 1}
${statusBadge} + ${inUseBadge}

${account.username || 'No username'}

diff --git a/pkg/webdav/file.go b/pkg/webdav/file.go index 14f0dc3..66b2c13 100644 --- a/pkg/webdav/file.go +++ b/pkg/webdav/file.go @@ -1,7 +1,6 @@ package webdav import ( - "crypto/tls" "fmt" "io" "net/http" @@ -12,25 +11,6 @@ import ( "github.com/sirrobot01/decypharr/pkg/debrid/store" ) -var streamingTransport = &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - MaxIdleConns: 200, - MaxIdleConnsPerHost: 100, - MaxConnsPerHost: 200, - IdleConnTimeout: 90 * time.Second, - TLSHandshakeTimeout: 10 * time.Second, - ResponseHeaderTimeout: 60 * time.Second, - ExpectContinueTimeout: 1 * time.Second, - DisableKeepAlives: true, - ForceAttemptHTTP2: false, - TLSNextProto: make(map[string]func(string, *tls.Conn) http.RoundTripper), -} - -var sharedClient = &http.Client{ - Transport: streamingTransport, - Timeout: 0, -} - type streamError struct { Err error StatusCode int @@ -176,7 +156,7 @@ func (f *File) streamWithRetry(w http.ResponseWriter, r *http.Request, retryCoun return &streamError{Err: fmt.Errorf("invalid range"), StatusCode: http.StatusRequestedRangeNotSatisfiable} } - resp, err := sharedClient.Do(upstreamReq) + resp, err := f.cache.Download(upstreamReq) if err != nil { return &streamError{Err: err, StatusCode: http.StatusServiceUnavailable} }