minor bug fixes; improvements, final-beta-pre-stable

This commit is contained in:
Mukhtar Akere
2025-05-07 18:25:09 +01:00
parent 21354529e7
commit 0deb88e265
16 changed files with 151 additions and 258 deletions
+7 -8
View File
@@ -3,7 +3,6 @@ package alldebrid
import (
"fmt"
"github.com/goccy/go-json"
"github.com/puzpuzpuz/xsync/v3"
"github.com/rs/zerolog"
"github.com/sirrobot01/decypharr/internal/config"
"github.com/sirrobot01/decypharr/internal/logger"
@@ -13,7 +12,6 @@ import (
"net/http"
gourl "net/url"
"path/filepath"
"slices"
"strconv"
"sync"
"time"
@@ -23,7 +21,8 @@ type AllDebrid struct {
Name string
Host string `json:"host"`
APIKey string
DownloadKeys *xsync.MapOf[string, types.Account]
accounts map[string]types.Account
accountsMu sync.RWMutex
DownloadUncached bool
client *request.Client
@@ -46,20 +45,20 @@ func New(dc config.Debrid) *AllDebrid {
request.WithProxy(dc.Proxy),
)
accounts := xsync.NewMapOf[string, types.Account]()
accounts := make(map[string]types.Account)
for idx, key := range dc.DownloadAPIKeys {
id := strconv.Itoa(idx)
accounts.Store(id, types.Account{
accounts[id] = types.Account{
Name: key,
ID: id,
Token: key,
})
}
}
return &AllDebrid{
Name: "alldebrid",
Host: "http://api.alldebrid.com/v4.1",
APIKey: dc.APIKey,
DownloadKeys: accounts,
accounts: accounts,
DownloadUncached: dc.DownloadUncached,
client: client,
MountPath: dc.Folder,
@@ -273,7 +272,7 @@ func (ad *AllDebrid) CheckStatus(torrent *types.Torrent, isSymlink bool) (*types
}
}
break
} else if slices.Contains(ad.GetDownloadingStatus(), status) {
} else if utils.Contains(ad.GetDownloadingStatus(), status) {
if !torrent.DownloadUncached {
return torrent, fmt.Errorf("torrent: %s not cached", torrent.Name)
}
+2 -13
View File
@@ -8,7 +8,6 @@ import (
"fmt"
"os"
"path/filepath"
"slices"
"strconv"
"strings"
"sync"
@@ -49,15 +48,6 @@ type CachedTorrent struct {
DuplicateIds []string `json:"duplicate_ids"`
}
func (ct *CachedTorrent) addDuplicateId(id string) {
if ct.DuplicateIds == nil {
ct.DuplicateIds = make([]string, 0)
}
if !slices.Contains(ct.DuplicateIds, id) {
ct.DuplicateIds = append(ct.DuplicateIds, id)
}
}
type downloadLinkCache struct {
Id string
Link string
@@ -93,9 +83,8 @@ type Cache struct {
folderNaming WebDavFolderNaming
// monitors
repairRequest sync.Map
failedToReinsert sync.Map
downloadLinkRequests sync.Map
repairRequest sync.Map
failedToReinsert sync.Map
// repair
repairChan chan RepairRequest
+42 -26
View File
@@ -10,6 +10,7 @@ import (
"github.com/sirrobot01/decypharr/pkg/debrid/realdebrid"
"github.com/sirrobot01/decypharr/pkg/debrid/torbox"
"github.com/sirrobot01/decypharr/pkg/debrid/types"
"strings"
)
func createDebridClient(dc config.Debrid) types.Client {
@@ -38,54 +39,69 @@ func ProcessTorrent(d *Engine, magnet *utils.Magnet, a *arr.Arr, isSymlink, over
Files: make(map[string]types.File),
}
errs := make([]error, 0)
errs := make([]error, 0, len(d.Clients))
// Override first, arr second, debrid third
if overrideDownloadUncached {
debridTorrent.DownloadUncached = true
} else if a.DownloadUncached != nil {
// Arr cached is set
debridTorrent.DownloadUncached = *a.DownloadUncached
} else {
debridTorrent.DownloadUncached = false
}
for index, db := range d.Clients {
logger := db.GetLogger()
logger.Info().Msgf("Processing debrid: %s", db.GetName())
// Override first, arr second, debrid third
if overrideDownloadUncached {
debridTorrent.DownloadUncached = true
} else if a.DownloadUncached != nil {
// Arr cached is set
debridTorrent.DownloadUncached = *a.DownloadUncached
} else {
if !overrideDownloadUncached && a.DownloadUncached == nil {
debridTorrent.DownloadUncached = db.GetDownloadUncached()
}
logger.Info().Msgf("Torrent Hash: %s", debridTorrent.InfoHash)
if db.GetCheckCached() {
hash, exists := db.IsAvailable([]string{debridTorrent.InfoHash})[debridTorrent.InfoHash]
if !exists || !hash {
logger.Info().Msgf("Torrent: %s is not cached", debridTorrent.Name)
continue
} else {
logger.Info().Msgf("Torrent: %s is cached(or downloading)", debridTorrent.Name)
}
}
//if db.GetCheckCached() {
// hash, exists := db.IsAvailable([]string{debridTorrent.InfoHash})[debridTorrent.InfoHash]
// if !exists || !hash {
// logger.Info().Msgf("Torrent: %s is not cached", debridTorrent.Name)
// continue
// } else {
// logger.Info().Msgf("Torrent: %s is cached(or downloading)", debridTorrent.Name)
// }
//}
dbt, err := db.SubmitMagnet(debridTorrent)
if dbt != nil {
dbt.Arr = a
}
if err != nil || dbt == nil || dbt.Id == "" {
errs = append(errs, err)
continue
}
dbt.Arr = a
logger.Info().Msgf("Torrent: %s(id=%s) submitted to %s", dbt.Name, dbt.Id, db.GetName())
d.LastUsed = index
torrent, err := db.CheckStatus(dbt, isSymlink)
if err != nil && torrent != nil && torrent.Id != "" {
// Delete the torrent if it was not downloaded
_ = db.DeleteTorrent(torrent.Id)
go func(id string) {
_ = db.DeleteTorrent(id)
}(torrent.Id)
}
return torrent, err
}
err := fmt.Errorf("failed to process torrent")
for _, e := range errs {
err = fmt.Errorf("%w\n%w", err, e)
if len(errs) == 0 {
return nil, fmt.Errorf("failed to process torrent: no clients available")
}
return nil, err
var errBuilder strings.Builder
errBuilder.WriteString("failed to process torrent:")
for _, e := range errs {
if e != nil {
errBuilder.WriteString("\n")
errBuilder.WriteString(e.Error())
}
}
return nil, fmt.Errorf(errBuilder.String())
}
-37
View File
@@ -8,51 +8,14 @@ import (
"time"
)
type downloadLinkRequest struct {
result string
err error
done chan struct{}
}
func newDownloadLinkRequest() *downloadLinkRequest {
return &downloadLinkRequest{
done: make(chan struct{}),
}
}
func (r *downloadLinkRequest) Complete(result string, err error) {
r.result = result
r.err = err
close(r.done)
}
func (r *downloadLinkRequest) Wait() (string, error) {
<-r.done
return r.result, r.err
}
func (c *Cache) GetDownloadLink(torrentName, filename, fileLink string) (string, error) {
// Check link cache
if dl := c.checkDownloadLink(fileLink); dl != "" {
return dl, nil
}
if req, inFlight := c.downloadLinkRequests.Load(fileLink); inFlight {
// Wait for the other request to complete and use its result
result := req.(*downloadLinkRequest)
return result.Wait()
}
// Create a new request object
req := newDownloadLinkRequest()
c.downloadLinkRequests.Store(fileLink, req)
downloadLink, err := c.fetchDownloadLink(torrentName, filename, fileLink)
// Complete the request and remove it from the map
req.Complete(downloadLink, err)
c.downloadLinkRequests.Delete(fileLink)
return downloadLink, err
}
+5 -11
View File
@@ -128,23 +128,17 @@ func (c *Cache) refreshTorrents() {
go func() {
defer wg.Done()
for t := range workChan {
select {
default:
if err := c.ProcessTorrent(t); err != nil {
c.logger.Error().Err(err).Msgf("Failed to process new torrent %s", t.Id)
errChan <- err
}
counter++
if err := c.ProcessTorrent(t); err != nil {
c.logger.Error().Err(err).Msgf("Failed to process new torrent %s", t.Id)
errChan <- err
}
counter++
}
}()
}
for _, t := range newTorrents {
select {
default:
workChan <- t
}
workChan <- t
}
close(workChan)
wg.Wait()
+1 -2
View File
@@ -8,7 +8,6 @@ import (
"github.com/sirrobot01/decypharr/internal/request"
"github.com/sirrobot01/decypharr/internal/utils"
"github.com/sirrobot01/decypharr/pkg/debrid/types"
"slices"
"sync"
"time"
)
@@ -43,7 +42,7 @@ func (c *Cache) IsTorrentBroken(t *CachedTorrent, filenames []string) bool {
files := make(map[string]types.File)
if len(filenames) > 0 {
for name, f := range t.Files {
if slices.Contains(filenames, name) {
if utils.Contains(filenames, name) {
files[name] = f
}
}
-26
View File
@@ -10,32 +10,6 @@ import (
"time"
)
// resetPropfindResponse resets the propfind response cache for the specified parent directories.
func (c *Cache) resetPropfindResponse() error {
// Right now, parents are hardcoded
parents := []string{"__all__", "torrents"}
// Reset only the parent directories
// Convert the parents to a keys
// This is a bit hacky, but it works
// Instead of deleting all the keys, we only delete the parent keys, e.g __all__/ or torrents/
keys := make([]string, 0, len(parents))
for _, p := range parents {
// Construct the key
// construct url
url := path.Clean(path.Join("/webdav", c.client.GetName(), p))
key0 := fmt.Sprintf("propfind:%s:0", url)
key1 := fmt.Sprintf("propfind:%s:1", url)
keys = append(keys, key0, key1)
}
// Delete the keys
for _, k := range keys {
c.PropfindResp.Delete(k)
}
c.logger.Trace().Msgf("Reset XML cache for %s", c.client.GetName())
return nil
}
func (c *Cache) refreshParentXml() error {
parents := []string{"__all__", "torrents"}
torrents := c.GetListing()
+8 -8
View File
@@ -4,15 +4,14 @@ import (
"bytes"
"fmt"
"github.com/goccy/go-json"
"github.com/puzpuzpuz/xsync/v3"
"github.com/rs/zerolog"
"github.com/sirrobot01/decypharr/internal/config"
"github.com/sirrobot01/decypharr/internal/logger"
"github.com/sirrobot01/decypharr/internal/request"
"github.com/sirrobot01/decypharr/internal/utils"
"github.com/sirrobot01/decypharr/pkg/debrid/types"
"slices"
"strconv"
"sync"
"time"
"net/http"
@@ -23,7 +22,8 @@ type DebridLink struct {
Name string
Host string `json:"host"`
APIKey string
DownloadKeys *xsync.MapOf[string, types.Account]
accounts map[string]types.Account
accountsMutex sync.RWMutex
DownloadUncached bool
client *request.Client
@@ -286,7 +286,7 @@ func (dl *DebridLink) CheckStatus(torrent *types.Torrent, isSymlink bool) (*type
return torrent, err
}
break
} else if slices.Contains(dl.GetDownloadingStatus(), status) {
} else if utils.Contains(dl.GetDownloadingStatus(), status) {
if !torrent.DownloadUncached {
return torrent, fmt.Errorf("torrent: %s not cached", torrent.Name)
}
@@ -351,20 +351,20 @@ func New(dc config.Debrid) *DebridLink {
request.WithProxy(dc.Proxy),
)
accounts := xsync.NewMapOf[string, types.Account]()
accounts := make(map[string]types.Account)
for idx, key := range dc.DownloadAPIKeys {
id := strconv.Itoa(idx)
accounts.Store(id, types.Account{
accounts[id] = types.Account{
Name: key,
ID: id,
Token: key,
})
}
}
return &DebridLink{
Name: "debridlink",
Host: "https://debrid-link.com/api/v2",
APIKey: dc.APIKey,
DownloadKeys: accounts,
accounts: accounts,
DownloadUncached: dc.DownloadUncached,
client: client,
MountPath: dc.Folder,
+33 -22
View File
@@ -5,7 +5,6 @@ import (
"errors"
"fmt"
"github.com/goccy/go-json"
"github.com/puzpuzpuz/xsync/v3"
"github.com/rs/zerolog"
"github.com/sirrobot01/decypharr/internal/config"
"github.com/sirrobot01/decypharr/internal/logger"
@@ -16,7 +15,6 @@ import (
"net/http"
gourl "net/url"
"path/filepath"
"slices"
"sort"
"strconv"
"strings"
@@ -30,7 +28,8 @@ type RealDebrid struct {
APIKey string
currentDownloadKey string
DownloadKeys *xsync.MapOf[string, types.Account] // index | Account
accounts map[string]types.Account
accountsMutex sync.RWMutex
DownloadUncached bool
client *request.Client
@@ -49,15 +48,15 @@ func New(dc config.Debrid) *RealDebrid {
}
_log := logger.New(dc.Name)
accounts := xsync.NewMapOf[string, types.Account]()
accounts := make(map[string]types.Account)
currentDownloadKey := dc.DownloadAPIKeys[0]
for idx, key := range dc.DownloadAPIKeys {
id := strconv.Itoa(idx)
accounts.Store(id, types.Account{
accounts[id] = types.Account{
Name: key,
ID: id,
Token: key,
})
}
}
downloadHeaders := map[string]string{
@@ -68,7 +67,7 @@ func New(dc config.Debrid) *RealDebrid {
Name: "realdebrid",
Host: "https://api.real-debrid.com/rest/1.0",
APIKey: dc.APIKey,
DownloadKeys: accounts,
accounts: accounts,
DownloadUncached: dc.DownloadUncached,
client: request.New(
request.WithHeaders(headers),
@@ -381,10 +380,13 @@ func (r *RealDebrid) CheckStatus(t *types.Torrent, isSymlink bool) (*types.Torre
}
payload := strings.NewReader(p.Encode())
req, _ := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/torrents/selectFiles/%s", r.Host, t.Id), payload)
_, err = r.client.MakeRequest(req)
res, err := r.client.Do(req)
if err != nil {
return t, err
}
if res.StatusCode != http.StatusNoContent {
return t, fmt.Errorf("realdebrid API error: Status: %d", res.StatusCode)
}
} else if status == "downloaded" {
t.Files = getSelectedFiles(t, data) // Get selected files
r.logger.Info().Msgf("Torrent: %s downloaded to RD", t.Name)
@@ -395,7 +397,7 @@ func (r *RealDebrid) CheckStatus(t *types.Torrent, isSymlink bool) (*types.Torre
}
}
break
} else if slices.Contains(r.GetDownloadingStatus(), status) {
} else if utils.Contains(r.GetDownloadingStatus(), status) {
if !t.DownloadUncached {
return t, fmt.Errorf("torrent: %s not cached", t.Name)
}
@@ -556,12 +558,13 @@ func (r *RealDebrid) GetDownloadLink(t *types.Torrent, file *types.File) (*types
if err != nil {
if errors.Is(err, request.TrafficExceededError) {
// Retries generating
retries = 4
retries = 5
} else {
// If the error is not traffic exceeded, return the error
return nil, err
}
}
backOff := 1 * time.Second
for retries > 0 {
downloadLink, err = r._getDownloadLink(file)
if err == nil {
@@ -571,7 +574,8 @@ func (r *RealDebrid) GetDownloadLink(t *types.Torrent, file *types.File) (*types
return nil, err
}
// Add a delay before retrying
time.Sleep(5 * time.Second)
time.Sleep(backOff)
backOff *= 2 // Exponential backoff
}
return downloadLink, nil
}
@@ -755,35 +759,42 @@ func (r *RealDebrid) GetMountPath() string {
}
func (r *RealDebrid) DisableAccount(accountId string) {
if r.DownloadKeys.Size() == 1 {
r.accountsMutex.Lock()
defer r.accountsMutex.Unlock()
if len(r.accounts) == 1 {
r.logger.Info().Msgf("Cannot disable last account: %s", accountId)
return
}
r.currentDownloadKey = ""
if value, ok := r.DownloadKeys.Load(accountId); ok {
if value, ok := r.accounts[accountId]; ok {
value.Disabled = true
r.DownloadKeys.Store(accountId, value)
r.accounts[accountId] = value
r.logger.Info().Msgf("Disabled account Index: %s", value.ID)
}
}
func (r *RealDebrid) ResetActiveDownloadKeys() {
r.DownloadKeys.Range(func(key string, value types.Account) bool {
r.accountsMutex.Lock()
defer r.accountsMutex.Unlock()
for key, value := range r.accounts {
value.Disabled = false
r.DownloadKeys.Store(key, value)
return true
})
r.accounts[key] = value
}
}
func (r *RealDebrid) getActiveAccounts() []types.Account {
r.accountsMutex.RLock()
defer r.accountsMutex.RUnlock()
accounts := make([]types.Account, 0)
r.DownloadKeys.Range(func(key string, value types.Account) bool {
for _, value := range r.accounts {
if value.Disabled {
return true
continue
}
accounts = append(accounts, value)
return true
})
}
// Sort accounts by ID
sort.Slice(accounts, func(i, j int) bool {
return accounts[i].ID < accounts[j].ID
})
+8 -9
View File
@@ -4,7 +4,6 @@ import (
"bytes"
"fmt"
"github.com/goccy/go-json"
"github.com/puzpuzpuz/xsync/v3"
"github.com/rs/zerolog"
"github.com/sirrobot01/decypharr/internal/config"
"github.com/sirrobot01/decypharr/internal/logger"
@@ -18,7 +17,6 @@ import (
"path"
"path/filepath"
"runtime"
"slices"
"strconv"
"strings"
"sync"
@@ -29,7 +27,8 @@ type Torbox struct {
Name string
Host string `json:"host"`
APIKey string
DownloadKeys *xsync.MapOf[string, types.Account]
accounts map[string]types.Account
accountsMutex sync.RWMutex
DownloadUncached bool
client *request.Client
@@ -53,21 +52,21 @@ func New(dc config.Debrid) *Torbox {
request.WithProxy(dc.Proxy),
)
accounts := xsync.NewMapOf[string, types.Account]()
accounts := make(map[string]types.Account)
for idx, key := range dc.DownloadAPIKeys {
id := strconv.Itoa(idx)
accounts.Store(id, types.Account{
accounts[id] = types.Account{
Name: key,
ID: id,
Token: key,
})
}
}
return &Torbox{
Name: "torbox",
Host: "https://api.torbox.app/v1",
APIKey: dc.APIKey,
DownloadKeys: accounts,
accounts: accounts,
DownloadUncached: dc.DownloadUncached,
client: client,
MountPath: dc.Folder,
@@ -176,7 +175,7 @@ func getTorboxStatus(status string, finished bool) string {
"forcedUP", "allocating", "downloading", "metaDL", "pausedDL",
"queuedDL", "checkingDL", "forcedDL", "checkingResumeData", "moving"}
switch {
case slices.Contains(downloading, status):
case utils.Contains(downloading, status):
return "downloading"
default:
return "error"
@@ -328,7 +327,7 @@ func (tb *Torbox) CheckStatus(torrent *types.Torrent, isSymlink bool) (*types.To
}
}
break
} else if slices.Contains(tb.GetDownloadingStatus(), status) {
} else if utils.Contains(tb.GetDownloadingStatus(), status) {
if !torrent.DownloadUncached {
return torrent, fmt.Errorf("torrent: %s not cached", torrent.Name)
}