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

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

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

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

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
}

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

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

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

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,

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

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

View File

@@ -5,7 +5,7 @@ import (
"github.com/cavaliergopher/grab/v3"
"github.com/sirrobot01/decypharr/internal/request"
"github.com/sirrobot01/decypharr/internal/utils"
debrid "github.com/sirrobot01/decypharr/pkg/debrid/types"
debridTypes "github.com/sirrobot01/decypharr/pkg/debrid/types"
"io"
"os"
"path/filepath"
@@ -102,7 +102,7 @@ func (q *QBit) downloadFiles(torrent *Torrent, parent string) {
}
wg.Add(1)
q.downloadSemaphore <- struct{}{}
go func(file debrid.File) {
go func(file debridTypes.File) {
defer wg.Done()
defer func() { <-q.downloadSemaphore }()
filename := file.Link
@@ -149,7 +149,7 @@ func (q *QBit) ProcessSymlink(torrent *Torrent) (string, error) {
return q.createSymlinks(debridTorrent, torrentRclonePath, torrentFolder) // verify cos we're using external webdav
}
func (q *QBit) createSymlinksWebdav(debridTorrent *debrid.Torrent, rclonePath, torrentFolder string) (string, error) {
func (q *QBit) createSymlinksWebdav(debridTorrent *debridTypes.Torrent, rclonePath, torrentFolder string) (string, error) {
files := debridTorrent.Files
symlinkPath := filepath.Join(q.DownloadFolder, debridTorrent.Arr.Name, torrentFolder) // /mnt/symlinks/{category}/MyTVShow/
err := os.MkdirAll(symlinkPath, os.ModePerm)
@@ -157,7 +157,7 @@ func (q *QBit) createSymlinksWebdav(debridTorrent *debrid.Torrent, rclonePath, t
return "", fmt.Errorf("failed to create directory: %s: %v", symlinkPath, err)
}
remainingFiles := make(map[string]debrid.File)
remainingFiles := make(map[string]debridTypes.File)
for _, file := range files {
remainingFiles[utils.EscapePath(file.Name)] = file
}
@@ -214,7 +214,7 @@ func (q *QBit) createSymlinksWebdav(debridTorrent *debrid.Torrent, rclonePath, t
return symlinkPath, nil
}
func (q *QBit) createSymlinks(debridTorrent *debrid.Torrent, rclonePath, torrentFolder string) (string, error) {
func (q *QBit) createSymlinks(debridTorrent *debridTypes.Torrent, rclonePath, torrentFolder string) (string, error) {
files := debridTorrent.Files
symlinkPath := filepath.Join(q.DownloadFolder, debridTorrent.Arr.Name, torrentFolder) // /mnt/symlinks/{category}/MyTVShow/
err := os.MkdirAll(symlinkPath, os.ModePerm)
@@ -222,7 +222,7 @@ func (q *QBit) createSymlinks(debridTorrent *debrid.Torrent, rclonePath, torrent
return "", fmt.Errorf("failed to create directory: %s: %v", symlinkPath, err)
}
remainingFiles := make(map[string]debrid.File)
remainingFiles := make(map[string]debridTypes.File)
for _, file := range files {
remainingFiles[file.Path] = file
}
@@ -278,7 +278,7 @@ func (q *QBit) createSymlinks(debridTorrent *debrid.Torrent, rclonePath, torrent
return symlinkPath, nil
}
func (q *QBit) getTorrentPath(rclonePath string, debridTorrent *debrid.Torrent) (string, error) {
func (q *QBit) getTorrentPath(rclonePath string, debridTorrent *debridTypes.Torrent) (string, error) {
for {
torrentPath, err := debridTorrent.GetMountFolder(rclonePath)
if err == nil {

View File

@@ -7,14 +7,13 @@ import (
"github.com/sirrobot01/decypharr/internal/request"
"github.com/sirrobot01/decypharr/internal/utils"
"github.com/sirrobot01/decypharr/pkg/arr"
db "github.com/sirrobot01/decypharr/pkg/debrid/debrid"
debrid "github.com/sirrobot01/decypharr/pkg/debrid/types"
"github.com/sirrobot01/decypharr/pkg/debrid/debrid"
debridTypes "github.com/sirrobot01/decypharr/pkg/debrid/types"
"github.com/sirrobot01/decypharr/pkg/service"
"io"
"mime/multipart"
"os"
"path/filepath"
"slices"
"strings"
"time"
)
@@ -56,7 +55,7 @@ func (q *QBit) Process(ctx context.Context, magnet *utils.Magnet, category strin
return fmt.Errorf("arr not found in context")
}
isSymlink := ctx.Value("isSymlink").(bool)
debridTorrent, err := db.ProcessTorrent(svc.Debrid, magnet, a, isSymlink, false)
debridTorrent, err := debrid.ProcessTorrent(svc.Debrid, magnet, a, isSymlink, false)
if err != nil || debridTorrent == nil {
if err == nil {
err = fmt.Errorf("failed to process torrent")
@@ -69,22 +68,27 @@ func (q *QBit) Process(ctx context.Context, magnet *utils.Magnet, category strin
return nil
}
func (q *QBit) ProcessFiles(torrent *Torrent, debridTorrent *debrid.Torrent, arr *arr.Arr, isSymlink bool) {
func (q *QBit) ProcessFiles(torrent *Torrent, debridTorrent *debridTypes.Torrent, arr *arr.Arr, isSymlink bool) {
svc := service.GetService()
client := svc.Debrid.GetClient(debridTorrent.Debrid)
downloadingStatuses := client.GetDownloadingStatus()
for debridTorrent.Status != "downloaded" {
q.logger.Debug().Msgf("%s <- (%s) Download Progress: %.2f%%", debridTorrent.Debrid, debridTorrent.Name, debridTorrent.Progress)
dbT, err := client.CheckStatus(debridTorrent, isSymlink)
if err != nil {
if dbT != nil && dbT.Id != "" {
// Delete the torrent if it was not downloaded
_ = client.DeleteTorrent(dbT.Id)
go func() {
_ = client.DeleteTorrent(dbT.Id)
}()
}
q.logger.Error().Msgf("Error checking status: %v", err)
q.MarkAsFailed(torrent)
if err := arr.Refresh(); err != nil {
q.logger.Error().Msgf("Error refreshing arr: %v", err)
}
go func() {
if err := arr.Refresh(); err != nil {
q.logger.Error().Msgf("Error refreshing arr: %v", err)
}
}()
return
}
@@ -92,22 +96,24 @@ func (q *QBit) ProcessFiles(torrent *Torrent, debridTorrent *debrid.Torrent, arr
torrent = q.UpdateTorrentMin(torrent, debridTorrent)
// Exit the loop for downloading statuses to prevent memory buildup
if !slices.Contains(client.GetDownloadingStatus(), debridTorrent.Status) {
if debridTorrent.Status == "downloaded" || !utils.Contains(downloadingStatuses, debridTorrent.Status) {
break
}
if !utils.Contains(client.GetDownloadingStatus(), debridTorrent.Status) {
break
}
time.Sleep(time.Duration(q.RefreshInterval) * time.Second)
}
var (
torrentSymlinkPath string
err error
)
var torrentSymlinkPath string
var err error
debridTorrent.Arr = arr
// Check if debrid supports webdav by checking cache
if isSymlink {
cache, ok := svc.Debrid.Caches[debridTorrent.Debrid]
if ok {
cache, useWebdav := svc.Debrid.Caches[debridTorrent.Debrid]
if useWebdav {
q.logger.Info().Msgf("Using internal webdav for %s", debridTorrent.Debrid)
timer := time.Now()
// Use webdav to download the file
if err := cache.AddTorrent(debridTorrent); err != nil {
@@ -118,7 +124,6 @@ func (q *QBit) ProcessFiles(torrent *Torrent, debridTorrent *debrid.Torrent, arr
rclonePath := filepath.Join(debridTorrent.MountPath, cache.GetTorrentFolder(debridTorrent)) // /mnt/remote/realdebrid/MyTVShow
torrentFolderNoExt := utils.RemoveExtension(debridTorrent.Name)
timer := time.Now()
torrentSymlinkPath, err = q.createSymlinksWebdav(debridTorrent, rclonePath, torrentFolderNoExt) // /mnt/symlinks/{category}/MyTVShow/
q.logger.Debug().Msgf("Adding %s took %s", debridTorrent.Name, time.Since(timer))
@@ -160,7 +165,7 @@ func (q *QBit) MarkAsFailed(t *Torrent) *Torrent {
return t
}
func (q *QBit) UpdateTorrentMin(t *Torrent, debridTorrent *debrid.Torrent) *Torrent {
func (q *QBit) UpdateTorrentMin(t *Torrent, debridTorrent *debridTypes.Torrent) *Torrent {
if debridTorrent == nil {
return t
}
@@ -202,7 +207,7 @@ func (q *QBit) UpdateTorrentMin(t *Torrent, debridTorrent *debrid.Torrent) *Torr
return t
}
func (q *QBit) UpdateTorrent(t *Torrent, debridTorrent *debrid.Torrent) *Torrent {
func (q *QBit) UpdateTorrent(t *Torrent, debridTorrent *debridTypes.Torrent) *Torrent {
if debridTorrent == nil {
return t
}
@@ -298,10 +303,10 @@ func (q *QBit) SetTorrentTags(t *Torrent, tags []string) bool {
if tag == "" {
continue
}
if !slices.Contains(torrentTags, tag) {
if !utils.Contains(torrentTags, tag) {
torrentTags = append(torrentTags, tag)
}
if !slices.Contains(q.Tags, tag) {
if !utils.Contains(q.Tags, tag) {
q.Tags = append(q.Tags, tag)
}
}
@@ -324,7 +329,7 @@ func (q *QBit) AddTags(tags []string) bool {
if tag == "" {
continue
}
if !slices.Contains(q.Tags, tag) {
if !utils.Contains(q.Tags, tag) {
q.Tags = append(q.Tags, tag)
}
}

View File

@@ -5,73 +5,8 @@ import (
"github.com/sirrobot01/decypharr/pkg/arr"
"os"
"path/filepath"
"strconv"
"strings"
"time"
)
func parseSchedule(schedule string) (time.Duration, error) {
if schedule == "" {
return time.Hour, nil // default 60m
}
// Check if it's a time-of-day format (HH:MM)
if strings.Contains(schedule, ":") {
return parseTimeOfDay(schedule)
}
// Otherwise treat as duration interval
return parseDurationInterval(schedule)
}
func parseTimeOfDay(schedule string) (time.Duration, error) {
now := time.Now()
scheduledTime, err := time.Parse("15:04", schedule)
if err != nil {
return 0, fmt.Errorf("invalid time format: %s. Use HH:MM in 24-hour format", schedule)
}
// Convert scheduled time to today
scheduleToday := time.Date(
now.Year(), now.Month(), now.Day(),
scheduledTime.Hour(), scheduledTime.Minute(), 0, 0,
now.Location(),
)
if scheduleToday.Before(now) {
scheduleToday = scheduleToday.Add(24 * time.Hour)
}
return scheduleToday.Sub(now), nil
}
func parseDurationInterval(interval string) (time.Duration, error) {
if len(interval) < 2 {
return 0, fmt.Errorf("invalid interval format: %s", interval)
}
numStr := interval[:len(interval)-1]
unit := interval[len(interval)-1]
num, err := strconv.Atoi(numStr)
if err != nil {
return 0, fmt.Errorf("invalid number in interval: %s", numStr)
}
switch unit {
case 'm':
return time.Duration(num) * time.Minute, nil
case 'h':
return time.Duration(num) * time.Hour, nil
case 'd':
return time.Duration(num) * 24 * time.Hour, nil
case 's':
return time.Duration(num) * time.Second, nil
default:
return 0, fmt.Errorf("invalid unit in interval: %c", unit)
}
}
func fileIsSymlinked(file string) bool {
info, err := os.Lstat(file)
if err != nil {

View File

@@ -18,7 +18,7 @@ func (ui *Handler) setupMiddleware(next http.Handler) http.Handler {
// strip inco from URL
if inco := r.URL.Query().Get("inco"); inco != "" && needsAuth == nil && r.URL.Path == "/config" {
// redirect to the same URL without the inco parameter
http.Redirect(w, r, fmt.Sprintf("/config"), http.StatusSeeOther)
http.Redirect(w, r, "/config", http.StatusSeeOther)
}
next.ServeHTTP(w, r)
})

View File

@@ -165,7 +165,7 @@ func (h *Handler) OpenFile(ctx context.Context, name string, flag int, perm os.F
_path := strings.TrimPrefix(name, rootDir)
parts := strings.Split(strings.TrimPrefix(_path, string(os.PathSeparator)), string(os.PathSeparator))
if len(parts) >= 2 && (slices.Contains(h.getParentItems(), parts[0])) {
if len(parts) >= 2 && (utils.Contains(h.getParentItems(), parts[0])) {
torrentName := parts[1]
cachedTorrent := h.cache.GetTorrentByName(torrentName)