finalize experimental

This commit is contained in:
Mukhtar Akere
2025-04-16 11:32:42 +01:00
parent 39945616f3
commit 58fb4e6e14
28 changed files with 528 additions and 358 deletions

View File

@@ -262,15 +262,13 @@ func (ad *AllDebrid) GenerateDownloadLinks(t *types.Torrent) error {
for _, file := range t.Files {
go func(file types.File) {
defer wg.Done()
link, accountId, err := ad.GetDownloadLink(t, &file)
link, err := ad.GetDownloadLink(t, &file)
if err != nil {
errCh <- err
return
}
file.DownloadLink = link
file.Generated = time.Now()
file.AccountId = accountId
if link == "" {
if link != nil {
errCh <- fmt.Errorf("error getting download links %w", err)
return
}
@@ -298,7 +296,7 @@ func (ad *AllDebrid) GenerateDownloadLinks(t *types.Torrent) error {
return nil
}
func (ad *AllDebrid) GetDownloadLink(t *types.Torrent, file *types.File) (string, string, error) {
func (ad *AllDebrid) GetDownloadLink(t *types.Torrent, file *types.File) (*types.DownloadLink, error) {
url := fmt.Sprintf("%s/link/unlock", ad.Host)
query := gourl.Values{}
query.Add("link", file.Link)
@@ -306,17 +304,25 @@ func (ad *AllDebrid) GetDownloadLink(t *types.Torrent, file *types.File) (string
req, _ := http.NewRequest(http.MethodGet, url, nil)
resp, err := ad.client.MakeRequest(req)
if err != nil {
return "", "", err
return nil, err
}
var data DownloadLink
if err = json.Unmarshal(resp, &data); err != nil {
return "", "", err
return nil, err
}
link := data.Data.Link
if link == "" {
return "", "", fmt.Errorf("error getting download links %s", data.Error.Message)
return nil, fmt.Errorf("error getting download links %s", data.Error.Message)
}
return link, "0", nil
return &types.DownloadLink{
Link: file.Link,
DownloadLink: link,
Id: data.Data.Id,
Size: file.Size,
Filename: file.Name,
Generated: time.Now(),
AccountId: "0",
}, nil
}
func (ad *AllDebrid) GetCheckCached() bool {
@@ -355,7 +361,7 @@ func (ad *AllDebrid) GetTorrents() ([]*types.Torrent, error) {
return torrents, nil
}
func (ad *AllDebrid) GetDownloads() (map[string]types.DownloadLinks, error) {
func (ad *AllDebrid) GetDownloads() (map[string]types.DownloadLink, error) {
return nil, nil
}
@@ -381,3 +387,6 @@ func (ad *AllDebrid) DisableAccount(accountId string) {
func (ad *AllDebrid) ResetActiveDownloadKeys() {
}
func (ad *AllDebrid) DeleteDownloadLink(linkId string) error {
return nil
}

View File

@@ -45,6 +45,7 @@ type CachedTorrent struct {
}
type downloadLinkCache struct {
Id string
Link string
AccountId string
ExpiresAt time.Time
@@ -619,7 +620,7 @@ func (c *Cache) GetDownloadLink(torrentId, filename, fileLink string) string {
}
c.logger.Trace().Msgf("Getting download link for %s", filename)
downloadLink, accountId, err := c.client.GetDownloadLink(ct.Torrent, &file)
downloadLink, err := c.client.GetDownloadLink(ct.Torrent, &file)
if err != nil {
if errors.Is(err, request.HosterUnavailableError) {
c.logger.Debug().Err(err).Msgf("Hoster is unavailable. Triggering repair for %s", ct.Name)
@@ -631,41 +632,32 @@ func (c *Cache) GetDownloadLink(torrentId, filename, fileLink string) string {
c.logger.Debug().Msgf("Reinserted torrent %s", ct.Name)
file = ct.Files[filename]
// Retry getting the download link
downloadLink, accountId, err = c.client.GetDownloadLink(ct.Torrent, &file)
downloadLink, err = c.client.GetDownloadLink(ct.Torrent, &file)
if err != nil {
c.logger.Debug().Err(err).Msgf("Failed to get download link for %s", file.Link)
return ""
}
if downloadLink == "" {
if downloadLink == nil {
c.logger.Debug().Msgf("Download link is empty for %s", file.Link)
return ""
}
file.DownloadLink = downloadLink
file.Generated = time.Now()
file.AccountId = accountId
ct.Files[filename] = file
go func() {
c.updateDownloadLink(file.Link, downloadLink, accountId)
c.setTorrent(ct)
}()
return file.DownloadLink
c.updateDownloadLink(downloadLink)
return downloadLink.DownloadLink
} else if errors.Is(err, request.TrafficExceededError) {
// This is likely a fair usage limit error
c.logger.Debug().Err(err).Msgf("Traffic exceeded for %s", ct.Name)
} else {
c.logger.Debug().Err(err).Msgf("Failed to get download link for %s", file.Link)
return ""
}
}
file.DownloadLink = downloadLink
file.Generated = time.Now()
file.AccountId = accountId
ct.Files[filename] = file
go func() {
c.updateDownloadLink(file.Link, downloadLink, file.AccountId)
c.setTorrent(ct)
}()
return file.DownloadLink
if downloadLink == nil {
c.logger.Debug().Msgf("Download link is empty for %s", file.Link)
return ""
}
c.updateDownloadLink(downloadLink)
return downloadLink.DownloadLink
}
func (c *Cache) GenerateDownloadLinks(t *CachedTorrent) {
@@ -673,7 +665,10 @@ func (c *Cache) GenerateDownloadLinks(t *CachedTorrent) {
c.logger.Error().Err(err).Msg("Failed to generate download links")
}
for _, file := range t.Files {
c.updateDownloadLink(file.Link, file.DownloadLink, file.AccountId)
if file.DownloadLink != nil {
c.updateDownloadLink(file.DownloadLink)
}
}
c.SaveTorrent(t)
@@ -701,11 +696,12 @@ func (c *Cache) AddTorrent(t *types.Torrent) error {
}
func (c *Cache) updateDownloadLink(link, downloadLink string, accountId string) {
c.downloadLinks.Store(link, downloadLinkCache{
Link: downloadLink,
func (c *Cache) updateDownloadLink(dl *types.DownloadLink) {
c.downloadLinks.Store(dl.Link, downloadLinkCache{
Id: dl.Id,
Link: dl.DownloadLink,
ExpiresAt: time.Now().Add(c.autoExpiresLinksAfter),
AccountId: accountId,
AccountId: dl.AccountId,
})
}
@@ -728,7 +724,18 @@ func (c *Cache) MarkDownloadLinkAsInvalid(link, downloadLink, reason string) {
}
}
}
c.downloadLinks.Delete(link) // Remove the download link from cache
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)
}
}
}
func (c *Cache) IsDownloadLinkInvalid(downloadLink string) bool {

View File

@@ -8,7 +8,6 @@ import (
"io"
"net/http"
"os"
"slices"
"sort"
"strings"
"sync"
@@ -61,25 +60,17 @@ func (c *Cache) refreshListings() {
// Atomic store of the complete ready-to-use slice
c.listings.Store(files)
_ = c.refreshXml()
if err := c.RefreshRclone(); err != nil {
c.logger.Trace().Err(err).Msg("Failed to refresh rclone") // silent error
}
}
func (c *Cache) refreshTorrents() {
// Use a mutex to prevent concurrent refreshes
if c.torrentsRefreshMu.TryLock() {
defer c.torrentsRefreshMu.Unlock()
} else {
return
}
// Create a copy of the current torrents to avoid concurrent issues
torrents := make(map[string]string, c.torrents.Size()) // a mpa of id and name
c.torrents.Range(func(key string, t *CachedTorrent) bool {
torrents[t.Id] = t.Name
return true
})
// Get new torrents from the debrid service
// Get all torrents from the debrid service
debTorrents, err := c.client.GetTorrents()
if err != nil {
c.logger.Debug().Err(err).Msg("Failed to get torrents")
@@ -91,38 +82,26 @@ func (c *Cache) refreshTorrents() {
return
}
// Get the newly added torrents only
_newTorrents := make([]*types.Torrent, 0)
idStore := make(map[string]struct{}, len(debTorrents))
currentTorrentIds := make(map[string]struct{}, len(debTorrents))
for _, t := range debTorrents {
idStore[t.Id] = struct{}{}
if _, ok := torrents[t.Id]; !ok {
_newTorrents = append(_newTorrents, t)
}
currentTorrentIds[t.Id] = struct{}{}
}
// Check for deleted torrents
deletedTorrents := make([]string, 0)
for id := range torrents {
if _, ok := idStore[id]; !ok {
deletedTorrents = append(deletedTorrents, id)
}
}
// Because of how fast AddTorrent is, a torrent might be added before we check
// So let's disable the deletion of torrents for now
// Deletion now moved to the cleanupWorker
newTorrents := make([]*types.Torrent, 0)
for _, t := range _newTorrents {
if !slices.Contains(deletedTorrents, t.Id) {
for _, t := range debTorrents {
if _, exists := c.torrents.Load(t.Id); !exists {
newTorrents = append(newTorrents, t)
}
}
if len(deletedTorrents) > 0 {
c.DeleteTorrents(deletedTorrents)
}
if len(newTorrents) == 0 {
return
}
c.logger.Info().Msgf("Found %d new torrents", len(newTorrents))
c.logger.Debug().Msgf("Found %d new torrents", len(newTorrents))
workChan := make(chan *types.Torrent, min(100, len(newTorrents)))
errChan := make(chan error, len(newTorrents))
@@ -236,6 +215,8 @@ func (c *Cache) refreshDownloadLinks() {
timeSince := time.Since(v.Generated)
if timeSince < c.autoExpiresLinksAfter {
c.downloadLinks.Store(k, downloadLinkCache{
Id: v.Id,
AccountId: v.AccountId,
Link: v.DownloadLink,
ExpiresAt: v.Generated.Add(c.autoExpiresLinksAfter - timeSince),
})

View File

@@ -4,9 +4,10 @@ import "time"
func (c *Cache) Refresh() error {
// For now, we just want to refresh the listing and download links
//go c.refreshDownloadLinksWorker()
go c.refreshDownloadLinksWorker()
go c.refreshTorrentsWorker()
go c.resetInvalidLinksWorker()
go c.cleanupWorker()
return nil
}
@@ -73,3 +74,36 @@ func (c *Cache) resetInvalidLinksWorker() {
c.resetInvalidLinks()
}
}
func (c *Cache) cleanupWorker() {
// Cleanup every hour
// Removes deleted torrents from the cache
ticker := time.NewTicker(1 * time.Hour)
for range ticker.C {
torrents, err := c.client.GetTorrents()
if err != nil {
c.logger.Error().Err(err).Msg("Failed to get torrents")
continue
}
idStore := make(map[string]struct{})
for _, t := range torrents {
idStore[t.Id] = struct{}{}
}
deletedTorrents := make([]string, 0)
c.torrents.Range(func(key string, _ *CachedTorrent) bool {
if _, exists := idStore[key]; !exists {
deletedTorrents = append(deletedTorrents, key)
}
return true
})
if len(deletedTorrents) > 0 {
c.DeleteTorrents(deletedTorrents)
c.logger.Info().Msgf("Deleted %d torrents", len(deletedTorrents))
}
}
}

View File

@@ -10,6 +10,36 @@ import (
"time"
)
// resetPropfindResponse resets the propfind response cache for the specified parent directories.
func (c *Cache) resetPropfindResponse() error {
// Right now, parents are hardcoded
parents := []string{"__all__", "torrents"}
// Reset only the parent directories
// Convert the parents to a keys
// This is a bit hacky, but it works
// Instead of deleting all the keys, we only delete the parent keys, e.g __all__/ or torrents/
keys := make([]string, 0, len(parents))
for _, p := range parents {
// Construct the key
// construct url
url := path.Clean(path.Join("/webdav", c.client.GetName(), p))
key0 := fmt.Sprintf("propfind:%s:0", url)
key1 := fmt.Sprintf("propfind:%s:1", url)
keys = append(keys, key0, key1)
}
// Delete the keys
for _, k := range keys {
c.PropfindResp.Delete(k)
}
if err := c.RefreshRclone(); err != nil {
c.logger.Trace().Err(err).Msg("Failed to refresh rclone") // silent error
}
c.logger.Trace().Msgf("Reset XML cache for %s", c.client.GetName())
return nil
}
func (c *Cache) refreshXml() error {
parents := []string{"__all__", "torrents"}
torrents := c.GetListing()
@@ -18,6 +48,9 @@ func (c *Cache) refreshXml() error {
return fmt.Errorf("failed to refresh XML for %s: %v", parent, err)
}
}
if err := c.RefreshRclone(); err != nil {
c.logger.Trace().Err(err).Msg("Failed to refresh rclone") // silent error
}
c.logger.Trace().Msgf("Refreshed XML cache for %s", c.client.GetName())
return nil
}

View File

@@ -137,12 +137,18 @@ func (dl *DebridLink) UpdateTorrent(t *types.Torrent) error {
continue
}
file := types.File{
Id: f.ID,
Name: f.Name,
Size: f.Size,
Path: f.Name,
DownloadLink: f.DownloadURL,
Link: f.DownloadURL,
Id: f.ID,
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,
}
t.Files[f.Name] = file
}
@@ -183,13 +189,19 @@ func (dl *DebridLink) SubmitMagnet(t *types.Torrent) (*types.Torrent, error) {
t.Debrid = dl.Name
for _, f := range data.Files {
file := types.File{
Id: f.ID,
Name: f.Name,
Size: f.Size,
Path: f.Name,
Link: f.DownloadURL,
DownloadLink: f.DownloadURL,
Generated: time.Now(),
Id: f.ID,
Name: f.Name,
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(),
}
t.Files[f.Name] = file
}
@@ -241,12 +253,12 @@ func (dl *DebridLink) GenerateDownloadLinks(t *types.Torrent) error {
return nil
}
func (dl *DebridLink) GetDownloads() (map[string]types.DownloadLinks, error) {
func (dl *DebridLink) GetDownloads() (map[string]types.DownloadLink, error) {
return nil, nil
}
func (dl *DebridLink) GetDownloadLink(t *types.Torrent, file *types.File) (string, string, error) {
return file.DownloadLink, "0", nil
func (dl *DebridLink) GetDownloadLink(t *types.Torrent, file *types.File) (*types.DownloadLink, error) {
return file.DownloadLink, nil
}
func (dl *DebridLink) GetDownloadingStatus() []string {
@@ -358,12 +370,18 @@ func (dl *DebridLink) getTorrents(page, perPage int) ([]*types.Torrent, error) {
continue
}
file := types.File{
Id: f.ID,
Name: f.Name,
Size: f.Size,
Path: f.Name,
DownloadLink: f.DownloadURL,
Link: f.DownloadURL,
Id: f.ID,
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,
}
torrent.Files[f.Name] = file
}
@@ -385,3 +403,7 @@ func (dl *DebridLink) DisableAccount(accountId string) {
func (dl *DebridLink) ResetActiveDownloadKeys() {
}
func (dl *DebridLink) DeleteDownloadLink(linkId string) error {
return nil
}

View File

@@ -28,8 +28,9 @@ type RealDebrid struct {
Name string
Host string `json:"host"`
APIKey string
DownloadKeys *xsync.MapOf[string, types.Account] // index | Account
APIKey string
currentDownloadKey string
DownloadKeys *xsync.MapOf[string, types.Account] // index | Account
DownloadUncached bool
client *request.Client
@@ -49,7 +50,7 @@ func New(dc config.Debrid) *RealDebrid {
_log := logger.New(dc.Name)
accounts := xsync.NewMapOf[string, types.Account]()
firstDownloadKey := dc.DownloadAPIKeys[0]
currentDownloadKey := dc.DownloadAPIKeys[0]
for idx, key := range dc.DownloadAPIKeys {
id := strconv.Itoa(idx)
accounts.Store(id, types.Account{
@@ -60,7 +61,7 @@ func New(dc config.Debrid) *RealDebrid {
}
downloadHeaders := map[string]string{
"Authorization": fmt.Sprintf("Bearer %s", firstDownloadKey),
"Authorization": fmt.Sprintf("Bearer %s", currentDownloadKey),
}
downloadClient := request.New(
@@ -70,7 +71,6 @@ func New(dc config.Debrid) *RealDebrid {
request.WithMaxRetries(5),
request.WithRetryableStatus(429, 447),
request.WithProxy(dc.Proxy),
request.WithStatusCooldown(447, 10*time.Second), // 447 is a fair use error
)
client := request.New(
@@ -83,16 +83,17 @@ func New(dc config.Debrid) *RealDebrid {
)
return &RealDebrid{
Name: "realdebrid",
Host: dc.Host,
APIKey: dc.APIKey,
DownloadKeys: accounts,
DownloadUncached: dc.DownloadUncached,
client: client,
downloadClient: downloadClient,
MountPath: dc.Folder,
logger: logger.New(dc.Name),
CheckCached: dc.CheckCached,
Name: "realdebrid",
Host: dc.Host,
APIKey: dc.APIKey,
DownloadKeys: accounts,
DownloadUncached: dc.DownloadUncached,
client: client,
downloadClient: downloadClient,
currentDownloadKey: currentDownloadKey,
MountPath: dc.Folder,
logger: logger.New(dc.Name),
CheckCached: dc.CheckCached,
}
}
@@ -376,14 +377,13 @@ func (r *RealDebrid) GenerateDownloadLinks(t *types.Torrent) error {
go func(file types.File) {
defer wg.Done()
link, accountId, err := r.GetDownloadLink(t, &file)
link, err := r.GetDownloadLink(t, &file)
if err != nil {
errCh <- err
return
}
file.DownloadLink = link
file.AccountId = accountId
filesCh <- file
}(f)
}
@@ -427,95 +427,112 @@ func (r *RealDebrid) CheckLink(link string) error {
return nil
}
func (r *RealDebrid) _getDownloadLink(file *types.File) (string, error) {
func (r *RealDebrid) _getDownloadLink(file *types.File) (*types.DownloadLink, error) {
url := fmt.Sprintf("%s/unrestrict/link/", r.Host)
payload := gourl.Values{
"link": {file.Link},
}
req, _ := http.NewRequest(http.MethodPost, url, strings.NewReader(payload.Encode()))
resp, err := r.downloadClient.Do(req)
if err != nil {
return "", err
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
// Read the response body to get the error message
b, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
return nil, err
}
var data ErrorResponse
if err = json.Unmarshal(b, &data); err != nil {
return "", err
return nil, err
}
switch data.ErrorCode {
case 23:
return "", request.TrafficExceededError
return nil, request.TrafficExceededError
case 24:
return "", request.HosterUnavailableError // Link has been nerfed
return nil, request.HosterUnavailableError // Link has been nerfed
case 19:
return "", request.HosterUnavailableError // File has been removed
return nil, request.HosterUnavailableError // File has been removed
case 36:
return "", request.TrafficExceededError // traffic exceeded
return nil, request.TrafficExceededError // traffic exceeded
case 34:
return "", request.TrafficExceededError // traffic exceeded
return nil, request.TrafficExceededError // traffic exceeded
default:
return "", fmt.Errorf("realdebrid API error: Status: %d || Code: %d", resp.StatusCode, data.ErrorCode)
return nil, fmt.Errorf("realdebrid API error: Status: %d || Code: %d", resp.StatusCode, data.ErrorCode)
}
}
b, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
return nil, err
}
var data UnrestrictResponse
if err = json.Unmarshal(b, &data); err != nil {
return "", err
return nil, err
}
return data.Download, nil
if data.Download == "" {
return nil, fmt.Errorf("realdebrid API error: download link not found")
}
return &types.DownloadLink{
Filename: data.Filename,
Size: data.Filesize,
Link: data.Link,
DownloadLink: data.Download,
Generated: time.Now(),
}, nil
}
func (r *RealDebrid) GetDownloadLink(t *types.Torrent, file *types.File) (string, string, error) {
defer r.downloadClient.SetHeader("Authorization", fmt.Sprintf("Bearer %s", r.APIKey))
var (
downloadLink string
accountId string
err error
)
accounts := r.getActiveAccounts()
if len(accounts) < 1 {
// No active download keys. It's likely that the key has reached bandwidth limit
return "", "", fmt.Errorf("no active download keys")
}
for _, account := range accounts {
r.downloadClient.SetHeader("Authorization", fmt.Sprintf("Bearer %s", account.Token))
downloadLink, err = r._getDownloadLink(file)
if err != nil {
if errors.Is(err, request.TrafficExceededError) {
continue
}
// If the error is not traffic exceeded, skip generating the link with a new key
return "", "", err
} else {
// If we successfully generated a link, break the loop
accountId = account.ID
file.AccountId = accountId
break
}
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
r.DownloadKeys.Range(func(key string, value types.Account) bool {
if !value.Disabled {
r.currentDownloadKey = value.Token
return false
}
return true
})
}
if downloadLink != "" {
// If we successfully generated a link, return it
return downloadLink, accountId, nil
}
// If we reach here, it means all keys are disabled or traffic exceeded
r.downloadClient.SetHeader("Authorization", fmt.Sprintf("Bearer %s", r.currentDownloadKey))
downloadLink, err := r._getDownloadLink(file)
if err != nil {
if errors.Is(err, request.TrafficExceededError) {
return "", "", request.TrafficExceededError
accountsFunc := func() (*types.DownloadLink, error) {
accounts := r.getActiveAccounts()
var err error
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")
}
for _, account := range accounts {
r.downloadClient.SetHeader("Authorization", fmt.Sprintf("Bearer %s", account.Token))
downloadLink, err := r._getDownloadLink(file)
if err != nil {
if errors.Is(err, request.TrafficExceededError) {
continue
}
// If the error is not traffic exceeded, skip generating the link with a new key
return nil, err
} else {
// If we successfully generated a link, break the loop
downloadLink.AccountId = account.ID
return downloadLink, nil
}
}
// If we reach here, it means all keys have been exhausted
if errors.Is(err, request.TrafficExceededError) {
return nil, request.TrafficExceededError
}
return nil, fmt.Errorf("failed to generate download link: %w", err)
}
return "", "", fmt.Errorf("error generating download link: %v", err)
return accountsFunc()
}
return "", "", fmt.Errorf("error generating download link: %v", err)
return downloadLink, nil
}
func (r *RealDebrid) GetCheckCached() bool {
@@ -552,7 +569,7 @@ func (r *RealDebrid) getTorrents(offset int, limit int) (int, []*types.Torrent,
totalItems, _ := strconv.Atoi(resp.Header.Get("X-Total-Count"))
var data []TorrentsResponse
if err = json.Unmarshal(body, &data); err != nil {
return 0, nil, err
return 0, torrents, err
}
filenames := map[string]struct{}{}
for _, t := range data {
@@ -621,8 +638,8 @@ func (r *RealDebrid) GetTorrents() ([]*types.Torrent, error) {
return allTorrents, nil
}
func (r *RealDebrid) GetDownloads() (map[string]types.DownloadLinks, error) {
links := make(map[string]types.DownloadLinks)
func (r *RealDebrid) GetDownloads() (map[string]types.DownloadLink, error) {
links := make(map[string]types.DownloadLink)
offset := 0
limit := 1000
@@ -655,7 +672,7 @@ func (r *RealDebrid) GetDownloads() (map[string]types.DownloadLinks, error) {
return links, nil
}
func (r *RealDebrid) _getDownloads(offset int, limit int) ([]types.DownloadLinks, error) {
func (r *RealDebrid) _getDownloads(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)
@@ -669,9 +686,9 @@ func (r *RealDebrid) _getDownloads(offset int, limit int) ([]types.DownloadLinks
if err = json.Unmarshal(resp, &data); err != nil {
return nil, err
}
links := make([]types.DownloadLinks, 0)
links := make([]types.DownloadLink, 0)
for _, d := range data {
links = append(links, types.DownloadLinks{
links = append(links, types.DownloadLink{
Filename: d.Filename,
Size: d.Filesize,
Link: d.Link,
@@ -697,8 +714,15 @@ func (r *RealDebrid) GetMountPath() string {
}
func (r *RealDebrid) DisableAccount(accountId string) {
if r.DownloadKeys.Size() == 1 {
r.logger.Info().Msgf("Cannot disable last account: %s", accountId)
return
}
if value, ok := r.DownloadKeys.Load(accountId); ok {
value.Disabled = true
if value.Token == r.currentDownloadKey {
r.currentDownloadKey = ""
}
r.DownloadKeys.Store(accountId, value)
r.logger.Info().Msgf("Disabled account Index: %s", value.ID)
}
@@ -726,3 +750,12 @@ func (r *RealDebrid) getActiveAccounts() []types.Account {
})
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)
if _, err := r.downloadClient.MakeRequest(req); err != nil {
return err
}
return nil
}

View File

@@ -20,6 +20,7 @@ import (
"strconv"
"strings"
"sync"
"time"
)
type Torbox struct {
@@ -291,13 +292,12 @@ func (tb *Torbox) GenerateDownloadLinks(t *types.Torrent) error {
for _, file := range t.Files {
go func() {
defer wg.Done()
link, accountId, err := tb.GetDownloadLink(t, &file)
link, err := tb.GetDownloadLink(t, &file)
if err != nil {
errCh <- err
return
}
file.DownloadLink = link
file.AccountId = accountId
filesCh <- file
}()
}
@@ -324,7 +324,7 @@ func (tb *Torbox) GenerateDownloadLinks(t *types.Torrent) error {
return nil
}
func (tb *Torbox) GetDownloadLink(t *types.Torrent, file *types.File) (string, string, error) {
func (tb *Torbox) GetDownloadLink(t *types.Torrent, file *types.File) (*types.DownloadLink, error) {
url := fmt.Sprintf("%s/api/torrents/requestdl/", tb.Host)
query := gourl.Values{}
query.Add("torrent_id", t.Id)
@@ -334,17 +334,26 @@ func (tb *Torbox) GetDownloadLink(t *types.Torrent, file *types.File) (string, s
req, _ := http.NewRequest(http.MethodGet, url, nil)
resp, err := tb.client.MakeRequest(req)
if err != nil {
return "", "", err
return nil, err
}
var data DownloadLinksResponse
if err = json.Unmarshal(resp, &data); err != nil {
return "", "", err
return nil, err
}
if data.Data == nil {
return "", "", fmt.Errorf("error getting download links")
return nil, fmt.Errorf("error getting download links")
}
link := *data.Data
return link, "0", nil
if link == "" {
return nil, fmt.Errorf("error getting download links")
}
return &types.DownloadLink{
Link: file.Link,
DownloadLink: link,
Id: file.Id,
AccountId: "0",
Generated: time.Now(),
}, nil
}
func (tb *Torbox) GetDownloadingStatus() []string {
@@ -363,7 +372,7 @@ func (tb *Torbox) GetDownloadUncached() bool {
return tb.DownloadUncached
}
func (tb *Torbox) GetDownloads() (map[string]types.DownloadLinks, error) {
func (tb *Torbox) GetDownloads() (map[string]types.DownloadLink, error) {
return nil, nil
}
@@ -381,3 +390,7 @@ func (tb *Torbox) DisableAccount(accountId string) {
func (tb *Torbox) ResetActiveDownloadKeys() {
}
func (tb *Torbox) DeleteDownloadLink(linkId string) error {
return nil
}

View File

@@ -8,7 +8,7 @@ type Client interface {
SubmitMagnet(tr *Torrent) (*Torrent, error)
CheckStatus(tr *Torrent, isSymlink bool) (*Torrent, error)
GenerateDownloadLinks(tr *Torrent) error
GetDownloadLink(tr *Torrent, file *File) (string, string, error)
GetDownloadLink(tr *Torrent, file *File) (*DownloadLink, error)
DeleteTorrent(torrentId string) error
IsAvailable(infohashes []string) map[string]bool
GetCheckCached() bool
@@ -18,9 +18,10 @@ type Client interface {
GetName() string
GetLogger() zerolog.Logger
GetDownloadingStatus() []string
GetDownloads() (map[string]DownloadLinks, error)
GetDownloads() (map[string]DownloadLink, error)
CheckLink(link string) error
GetMountPath() string
DisableAccount(string)
ResetActiveDownloadKeys()
DeleteDownloadLink(linkId string) error
}

View File

@@ -39,13 +39,18 @@ type Torrent struct {
DownloadUncached bool `json:"-"`
}
type DownloadLinks struct {
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 {
@@ -72,14 +77,14 @@ func (t *Torrent) GetMountFolder(rClonePath string) (string, error) {
}
type File struct {
Id string `json:"id"`
Name string `json:"name"`
Size int64 `json:"size"`
Path string `json:"path"`
Link string `json:"link"`
DownloadLink string `json:"download_link"`
AccountId string `json:"account_id"`
Generated time.Time `json:"generated"`
Id string `json:"id"`
Name string `json:"name"`
Size int64 `json:"size"`
Path string `json:"path"`
Link string `json:"link"`
DownloadLink *DownloadLink `json:"-"`
AccountId string `json:"account_id"`
Generated time.Time `json:"generated"`
}
func (f *File) IsValid() bool {

View File

@@ -96,7 +96,7 @@ func (q *QBit) downloadFiles(torrent *Torrent, parent string) {
HTTPClient: request.New(request.WithTimeout(0)),
}
for _, file := range debridTorrent.Files {
if file.DownloadLink == "" {
if file.DownloadLink == nil {
q.logger.Info().Msgf("No download link found for %s", file.Name)
continue
}
@@ -109,7 +109,7 @@ func (q *QBit) downloadFiles(torrent *Torrent, parent string) {
err := Download(
client,
file.DownloadLink,
file.DownloadLink.DownloadLink,
filepath.Join(parent, filename),
progressCallback,
)
@@ -186,6 +186,8 @@ func (q *QBit) createSymlinks(debridTorrent *debrid.Torrent, rclonePath, torrent
if err := q.preCacheFile(debridTorrent.Name, filePaths); err != nil {
q.logger.Error().Msgf("Failed to pre-cache file: %s", err)
} else {
q.logger.Debug().Msgf("Pre-cached %d files", len(filePaths))
}
}() // Pre-cache the files in the background
// Pre-cache the first 256KB and 1MB of the file
@@ -222,21 +224,21 @@ func (q *QBit) preCacheFile(name string, filePaths []string) error {
if len(filePaths) == 0 {
return fmt.Errorf("no file paths provided")
}
for _, filePath := range filePaths {
func() {
file, err := os.Open(filePath)
defer func(file *os.File) {
_ = file.Close()
}(file)
func(f string) {
file, err := os.Open(f)
if err != nil {
return
}
defer file.Close()
// Pre-cache the file header (first 256KB) using 16KB chunks.
q.readSmallChunks(file, 0, 256*1024, 16*1024)
q.readSmallChunks(file, 1024*1024, 64*1024, 16*1024)
}()
}(filePath)
}
return nil
}

View File

@@ -604,6 +604,7 @@ func (r *Repair) getWebdavBrokenFiles(media arr.Content) []arr.ContentFile {
torrent := cache.GetTorrentByName(torrentName)
if torrent == nil {
r.logger.Debug().Msgf("No torrent found for %s. Skipping", torrentName)
brokenFiles = append(brokenFiles, f...)
continue
}
files := make([]string, 0)

View File

@@ -144,9 +144,7 @@ func (ui *Handler) LoginHandler(w http.ResponseWriter, r *http.Request) {
"Page": "login",
"Title": "Login",
}
if err := templates.ExecuteTemplate(w, "layout", data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
_ = templates.ExecuteTemplate(w, "layout", data)
return
}
@@ -200,9 +198,7 @@ func (ui *Handler) SetupHandler(w http.ResponseWriter, r *http.Request) {
"Page": "setup",
"Title": "Setup",
}
if err := templates.ExecuteTemplate(w, "layout", data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
_ = templates.ExecuteTemplate(w, "layout", data)
return
}
@@ -249,10 +245,7 @@ func (ui *Handler) IndexHandler(w http.ResponseWriter, r *http.Request) {
"Page": "index",
"Title": "Torrents",
}
if err := templates.ExecuteTemplate(w, "layout", data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
_ = templates.ExecuteTemplate(w, "layout", data)
}
func (ui *Handler) DownloadHandler(w http.ResponseWriter, r *http.Request) {
@@ -260,10 +253,7 @@ func (ui *Handler) DownloadHandler(w http.ResponseWriter, r *http.Request) {
"Page": "download",
"Title": "Download",
}
if err := templates.ExecuteTemplate(w, "layout", data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
_ = templates.ExecuteTemplate(w, "layout", data)
}
func (ui *Handler) RepairHandler(w http.ResponseWriter, r *http.Request) {
@@ -271,10 +261,7 @@ func (ui *Handler) RepairHandler(w http.ResponseWriter, r *http.Request) {
"Page": "repair",
"Title": "Repair",
}
if err := templates.ExecuteTemplate(w, "layout", data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
_ = templates.ExecuteTemplate(w, "layout", data)
}
func (ui *Handler) ConfigHandler(w http.ResponseWriter, r *http.Request) {
@@ -282,10 +269,7 @@ func (ui *Handler) ConfigHandler(w http.ResponseWriter, r *http.Request) {
"Page": "config",
"Title": "Config",
}
if err := templates.ExecuteTemplate(w, "layout", data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
_ = templates.ExecuteTemplate(w, "layout", data)
}
func (ui *Handler) handleGetArrs(w http.ResponseWriter, r *http.Request) {

View File

@@ -7,13 +7,13 @@ import (
"io"
"net/http"
"os"
"strings"
"time"
)
var sharedClient = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
Proxy: http.ProxyFromEnvironment,
MaxIdleConns: 100,
MaxIdleConnsPerHost: 20,
MaxConnsPerHost: 50,
@@ -47,8 +47,6 @@ type File struct {
link string
}
// You can not download this file because you have exceeded your traffic on this hoster
// File interface implementations for File
func (f *File) Close() error {
@@ -67,6 +65,7 @@ func (f *File) getDownloadLink() string {
}
downloadLink := f.cache.GetDownloadLink(f.torrentId, f.name, f.link)
if downloadLink != "" && isValidURL(downloadLink) {
f.downloadLink = downloadLink
return downloadLink
}
return ""
@@ -103,17 +102,27 @@ func (f *File) stream() (*http.Response, error) {
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusPartialContent {
f.downloadLink = ""
closeResp := func() {
_, _ = io.Copy(io.Discard, resp.Body)
resp.Body.Close()
}
if resp.StatusCode == http.StatusServiceUnavailable {
closeResp()
// Read the body to consume the response
f.cache.MarkDownloadLinkAsInvalid(f.link, downloadLink, "bandwidth_exceeded")
// Retry with a different API key if it's available
return f.stream()
b, _ := io.ReadAll(resp.Body)
err := resp.Body.Close()
if err != nil {
return nil, err
}
if strings.Contains(string(b), "You can not download this file because you have exceeded your traffic on this hoster") {
_log.Error().Msgf("Failed to get download link for %s. Download link expired", f.name)
f.cache.MarkDownloadLinkAsInvalid(f.link, downloadLink, "bandwidth_exceeded")
// Retry with a different API key if it's available
return f.stream()
} else {
return resp, fmt.Errorf("link not found")
}
} else if resp.StatusCode == http.StatusNotFound {
closeResp()
@@ -269,10 +278,6 @@ func (f *File) ReadAt(p []byte, off int64) (n int, err error) {
// Read the data
n, err = f.Read(p)
// Don't restore position for Infuse compatibility
// Infuse expects sequential reads after the initial seek
return n, err
}

View File

@@ -95,8 +95,7 @@ func (h *Handler) getParentFiles() []os.FileInfo {
}
if item == "version.txt" {
f.isDir = false
versionInfo := version.GetInfo().String()
f.size = int64(len(versionInfo))
f.size = int64(len(version.GetInfo().String()))
}
rootFiles = append(rootFiles, f)
}
@@ -107,10 +106,7 @@ func (h *Handler) OpenFile(ctx context.Context, name string, flag int, perm os.F
name = path.Clean("/" + name)
rootDir := h.getRootPath()
metadataOnly := false
if ctx.Value("metadataOnly") != nil {
metadataOnly = true
}
metadataOnly := ctx.Value("metadataOnly") != nil
now := time.Now()
@@ -122,7 +118,7 @@ func (h *Handler) OpenFile(ctx context.Context, name string, flag int, perm os.F
isDir: true,
children: h.getParentFiles(),
name: "/",
metadataOnly: metadataOnly,
metadataOnly: true,
modTime: now,
}, nil
case path.Join(rootDir, "version.txt"):
@@ -244,10 +240,16 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), "metadataOnly", true)
r = r.WithContext(ctx)
cleanPath := path.Clean(r.URL.Path)
depth := r.Header.Get("Depth")
if depth == "" {
depth = "1"
r.Header.Set("Depth", "1")
if r.Header.Get("Depth") == "" {
r.Header.Set("Depth", "1")
}
// Reject "infinity" depth
if r.Header.Get("Depth") == "infinity" {
r.Header.Set("Depth", "1")
}
depth := r.Header.Get("Depth")
// Use both path and Depth header to form the cache key.
cacheKey := fmt.Sprintf("propfind:%s:%s", cleanPath, depth)