- Delete empty files selected torrents

- Add more info to UI
- Add a global file download limit for local downloads
This commit is contained in:
Mukhtar Akere
2025-04-27 01:16:24 +01:00
parent e8112a4647
commit a3e64cc269
11 changed files with 88 additions and 46 deletions

View File

@@ -39,6 +39,7 @@ type QBitTorrent struct {
Categories []string `json:"categories,omitempty"`
RefreshInterval int `json:"refresh_interval,omitempty"`
SkipPreCache bool `json:"skip_pre_cache,omitempty"`
MaxDownloads int `json:"max_downloads,omitempty"`
}
type Arr struct {

View File

@@ -232,14 +232,12 @@ func (ad *AllDebrid) CheckStatus(torrent *types.Torrent, isSymlink bool) (*types
break
} else if slices.Contains(ad.GetDownloadingStatus(), status) {
if !torrent.DownloadUncached {
_ = ad.DeleteTorrent(torrent.Id)
return torrent, fmt.Errorf("torrent: %s not cached", torrent.Name)
}
// Break out of the loop if the torrent is downloading.
// This is necessary to prevent infinite loop since we moved to sync downloading and async processing
return torrent, nil
} else {
_ = ad.DeleteTorrent(torrent.Id)
return torrent, fmt.Errorf("torrent: %s has error", torrent.Name)
}

View File

@@ -76,7 +76,12 @@ func ProcessTorrent(d *Engine, magnet *utils.Magnet, a *arr.Arr, isSymlink, over
}
logger.Info().Msgf("Torrent: %s(id=%s) submitted to %s", dbt.Name, dbt.Id, db.GetName())
d.LastUsed = index
return db.CheckStatus(dbt, isSymlink)
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)
}
return torrent, err
}
err := fmt.Errorf("failed to process torrent")
for _, e := range errs {

View File

@@ -167,14 +167,14 @@ func (c *Cache) reInsertTorrent(ct *CachedTorrent) (*CachedTorrent, error) {
}
torrent.DownloadUncached = false // Set to false, avoid re-downloading
torrent, err = c.client.CheckStatus(torrent, true)
if err != nil && torrent != nil {
if err != nil {
if torrent != nil && torrent.Id != "" {
// Delete the torrent if it was not downloaded
_ = c.client.DeleteTorrent(torrent.Id)
}
c.failedToReinsert.Store(oldID, struct{}{})
return ct, fmt.Errorf("failed to check status: %w", err)
}
if torrent == nil {
c.failedToReinsert.Store(oldID, struct{}{})
return ct, fmt.Errorf("failed to check status: empty torrent")
}
// Update the torrent in the cache
addedOn, err := time.Parse(time.RFC3339, torrent.Added)

View File

@@ -227,14 +227,12 @@ func (dl *DebridLink) CheckStatus(torrent *types.Torrent, isSymlink bool) (*type
break
} else if slices.Contains(dl.GetDownloadingStatus(), status) {
if !torrent.DownloadUncached {
_ = dl.DeleteTorrent(torrent.Id)
return torrent, fmt.Errorf("torrent: %s not cached", torrent.Name)
}
// Break out of the loop if the torrent is downloading.
// This is necessary to prevent infinite loop since we moved to sync downloading and async processing
return torrent, nil
} else {
_ = dl.DeleteTorrent(torrent.Id)
return torrent, fmt.Errorf("torrent: %s has error", torrent.Name)
}

View File

@@ -354,12 +354,10 @@ func (r *RealDebrid) CheckStatus(t *types.Torrent, isSymlink bool) (*types.Torre
break
} else if slices.Contains(r.GetDownloadingStatus(), status) {
if !t.DownloadUncached {
_ = r.DeleteTorrent(t.Id)
return t, fmt.Errorf("torrent: %s not cached", t.Name)
}
return t, nil
} else {
_ = r.DeleteTorrent(t.Id)
return t, fmt.Errorf("torrent: %s has error: %s", t.Name, status)
}
@@ -482,7 +480,7 @@ func (r *RealDebrid) _getDownloadLink(file *types.File) (*types.DownloadLink, er
}
var data UnrestrictResponse
if err = json.Unmarshal(b, &data); err != nil {
return nil, err
return nil, fmt.Errorf("realdebrid API error: Error unmarshalling response: %w", err)
}
if data.Download == "" {
return nil, fmt.Errorf("realdebrid API error: download link not found")

View File

@@ -11,11 +11,13 @@ import (
"github.com/sirrobot01/decypharr/internal/request"
"github.com/sirrobot01/decypharr/internal/utils"
"github.com/sirrobot01/decypharr/pkg/debrid/types"
"github.com/sirrobot01/decypharr/pkg/version"
"mime/multipart"
"net/http"
gourl "net/url"
"path"
"path/filepath"
"runtime"
"slices"
"strconv"
"strings"
@@ -41,6 +43,7 @@ func New(dc config.Debrid) *Torbox {
headers := map[string]string{
"Authorization": fmt.Sprintf("Bearer %s", dc.APIKey),
"User-Agent": fmt.Sprintf("Decypharr/%s (%s; %s)", version.GetInfo(), runtime.GOOS, runtime.GOARCH),
}
_log := logger.New(dc.Name)
client := request.New(
@@ -259,14 +262,12 @@ func (tb *Torbox) CheckStatus(torrent *types.Torrent, isSymlink bool) (*types.To
break
} else if slices.Contains(tb.GetDownloadingStatus(), status) {
if !torrent.DownloadUncached {
_ = tb.DeleteTorrent(torrent.Id)
return torrent, fmt.Errorf("torrent: %s not cached", torrent.Name)
}
// Break out of the loop if the torrent is downloading.
// This is necessary to prevent infinite loop since we moved to sync downloading and async processing
return torrent, nil
} else {
_ = tb.DeleteTorrent(torrent.Id)
return torrent, fmt.Errorf("torrent: %s has error", torrent.Name)
}

View File

@@ -66,7 +66,7 @@ func (q *QBit) ProcessManualFile(torrent *Torrent) (string, error) {
func (q *QBit) downloadFiles(torrent *Torrent, parent string) {
debridTorrent := torrent.DebridTorrent
var wg sync.WaitGroup
semaphore := make(chan struct{}, 5)
totalSize := int64(0)
for _, file := range debridTorrent.Files {
totalSize += file.Size
@@ -92,8 +92,8 @@ func (q *QBit) downloadFiles(torrent *Torrent, parent string) {
q.UpdateTorrentMin(torrent, debridTorrent)
}
client := &grab.Client{
UserAgent: "qBitTorrent",
HTTPClient: request.New(request.WithTimeout(0)),
UserAgent: "Decypharr[QBitTorrent]",
HTTPClient: request.New(request.WithTimeout(60 * time.Second)),
}
for _, file := range debridTorrent.Files {
if file.DownloadLink == nil {
@@ -101,10 +101,10 @@ func (q *QBit) downloadFiles(torrent *Torrent, parent string) {
continue
}
wg.Add(1)
semaphore <- struct{}{}
q.downloadSemaphore <- struct{}{}
go func(file debrid.File) {
defer wg.Done()
defer func() { <-semaphore }()
defer func() { <-q.downloadSemaphore }()
filename := file.Link
err := Download(

View File

@@ -20,6 +20,8 @@ type QBit struct {
Tags []string
RefreshInterval int
SkipPreCache bool
downloadSemaphore chan struct{}
}
func New() *QBit {
@@ -37,5 +39,6 @@ func New() *QBit {
logger: logger.New("qbit"),
RefreshInterval: refreshInterval,
SkipPreCache: cfg.SkipPreCache,
downloadSemaphore: make(chan struct{}, cmp.Or(cfg.MaxDownloads, 5)),
}
}

View File

@@ -76,6 +76,10 @@ func (q *QBit) ProcessFiles(torrent *Torrent, debridTorrent *debrid.Torrent, arr
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)
}
q.logger.Error().Msgf("Error checking status: %v", err)
q.MarkAsFailed(torrent)
if err := arr.Refresh(); err != nil {

View File

@@ -193,18 +193,25 @@
<div class="setup-step d-none" id="step3">
<div class="section mb-5">
<div class="row">
<div class="col-md-6 mb-3">
<div class="col-md-4 mb-3">
<label class="form-label" for="qbit.download_folder">Symlink/Download Folder</label>
<input type="text" class="form-control" name="qbit.download_folder" id="qbit.download_folder">
<small class="form-text text-muted">Folder where the downloaded files will be stored</small>
</div>
<div class="col-md-6 mb-3">
<div class="col-md-3 mb-3">
<label class="form-label" for="qbit.refresh_interval">Refresh Interval (seconds)</label>
<input type="number" class="form-control" name="qbit.refresh_interval" id="qbit.refresh_interval">
</div>
<div class="col-md-5 mb-3">
<label class="form-label" for="qbit.max_downloads">Maximum Downloads Limit</label>
<input type="number" class="form-control" name="qbit.max_downloads" id="qbit.max_downloads">
<small class="form-text text-muted">Maximum number of simultaneous local downloads across all torrents</small>
</div>
<div class="col mb-3">
<div class="form-check me-3 d-inline-block">
<input type="checkbox" class="form-check-input" name="qbit.skip_pre_cache" id="qbit.skip_pre_cache">
<label class="form-check-label" for="qbit.skip_pre_cache">Disable Pre-Cache On Download (unchecking this caches a tiny part of your file to speed up import)</label>
<label class="form-check-label" for="qbit.skip_pre_cache">Disable Pre-Cache On Download</label>
<small class="form-text text-muted">Unchecking this caches a tiny part of your file to speed up import</small>
</div>
</div>
</div>
@@ -248,39 +255,45 @@
</div>
<div id="repairCol" class="d-none">
<div class="row">
<div class="col-md-3 mb-3">
<div class="col-md-4 mb-3">
<label class="form-label" for="repair.interval">Interval</label>
<input type="text" class="form-control" name="repair.interval" id="repair.interval" placeholder="e.g., 24h">
<small class="form-text text-muted">Interval for the repair process(e.g., 24h, 1d, 03:00)</small>
</div>
<div class="col-md-4 mb-3">
<div class="col-md-5 mb-3">
<label class="form-label" for="repair.zurg_url">Zurg URL</label>
<input type="text" class="form-control" name="repair.zurg_url" id="repair.zurg_url" placeholder="http://zurg:9999">
<small class="form-text text-muted">Speeds up the repair process by using Zurg</small>
</div>
</div>
<div class="row">
<div class="col-md-2 mb-3">
<div class="col-md-3 mb-3">
<div class="form-check">
<input type="checkbox" class="form-check-input" name="repair.use_webdav" id="repair.use_webdav">
<label class="form-check-label" for="repair.use_webdav">Use Webdav</label>
</div>
<small class="form-text text-muted">Use Internal Webdav for repair(make sure webdav is enabled in the debrid section</small>
</div>
<div class="col-md-2 mb-3">
<div class="col-md-3 mb-3">
<div class="form-check">
<input type="checkbox" class="form-check-input" name="repair.run_on_start" id="repair.run_on_start">
<label class="form-check-label" for="repair.run_on_start">Run on Start</label>
</div>
<small class="form-text text-muted">Run repair on startup</small>
</div>
<div class="col-md-3 mb-3">
<div class="form-check">
<input type="checkbox" class="form-check-input" name="repair.reinsert" id="repair.reinsert">
<label class="form-check-label" for="repair.reinsert">Re-Insert Nerfed Release</label>
</div>
<small class="form-text text-muted">Tries to autofix broken releases by re-inserting the torrent</small>
</div>
<div class="col mb-3">
<div class="col-md-3 mb-3">
<div class="form-check">
<input type="checkbox" class="form-check-input" name="repair.auto_process" id="repair.auto_process">
<label class="form-check-label" for="repair.auto_process">Auto Process(Scheduled jobs will be auto-processed)</label>
<label class="form-check-label" for="repair.auto_process">Auto Process</label>
</div>
<small class="form-text text-muted">Automatically process the repair job(delete broken symlinks and searches the arr again)</small>
</div>
</div>
</div>
@@ -321,28 +334,40 @@
<div class="col-md-6 mb-3">
<label class="form-label" for="debrid[${index}].api_key" >API Key</label>
<input type="password" class="form-control" name="debrid[${index}].api_key" id="debrid[${index}].api_key" required>
<small class="form-text text-muted">API Key for the debrid service</small>
</div>
<div class="col-md-6 mb-3">
<label class="form-label" for="debrid[${index}].folder" >Mount Folder</label>
<label class="form-label" for="debrid[${index}].folder">Mount/Rclone Folder</label>
<input type="text" class="form-control" name="debrid[${index}].folder" id="debrid[${index}].folder" placeholder="e.g. /mnt/remote/realdebrid" required>
<small class="form-text text-muted">Path to where you've mounted the debrid files. Usually your rclone path</small>
</div>
<div class="col-md-6 mb-3">
<label class="form-label" for="debrid[${index}].rate_limit" >Rate Limit</label>
<input type="text" class="form-control" name="debrid[${index}].rate_limit" id="debrid[${index}].rate_limit" placeholder="e.g., 200/minute" value="250/minute">
<small class="form-text text-muted">Rate limit for the debrid service. Confirm your debrid service rate limit</small>
</div>
<div class="col-12">
<div class="form-check me-3 d-inline-block">
</div>
<div class="row">
<div class="col-md-4">
<div class="form-check me-3">
<input type="checkbox" class="form-check-input" name="debrid[${index}].download_uncached" id="debrid[${index}].download_uncached">
<label class="form-check-label" for="debrid[${index}].download_uncached">Download Uncached</label>
</div>
<div class="form-check me-3 d-inline-block">
<small class="form-text text-muted">Download uncached files from the debrid service</small>
</div>
<div class="col-md-4">
<div class="form-check me-3">
<input type="checkbox" class="form-check-input" name="debrid[${index}].check_cached" id="debrid[${index}].check_cached">
<label class="form-check-label" for="debrid[${index}].check_cached" >Check Cached</label>
<label class="form-check-label" for="debrid[${index}].check_cached" disabled>Check Cached</label>
</div>
<div class="form-check d-inline-block">
<small class="form-text text-muted">Check if the file is cached before downloading(Disabled)</small>
</div>
<div class="col-md-4">
<div class="form-check me-3">
<input type="checkbox" class="form-check-input useWebdav" name="debrid[${index}].use_webdav" id="debrid[${index}].use_webdav">
<label class="form-check-label" for="debrid[${index}].use_webdav" >Use WebDav</label>
<label class="form-check-label" for="debrid[${index}].use_webdav">Enable WebDav Server</label>
</div>
<small class="form-text text-muted">Create an internal webdav for this debrid</small>
</div>
</div>
<div class="row mt-3 webdav d-none">
@@ -350,14 +375,17 @@
<div class="col-md-3 mb-3">
<label class="form-label" for="debrid[${index}].torrents_refresh_interval">Torrents Refresh Interval</label>
<input type="text" class="form-control webdav-field" name="debrid[${index}].torrents_refresh_interval" id="debrid[${index}].torrents_refresh_interval" placeholder="15s" value="15s">
<small class="form-text text-muted">How often to refresh the torrents list from debrid(instant when using webdav)</small>
</div>
<div class="col-md-3 mb-3">
<label class="form-label" for="debrid[${index}].download_links_refresh_interval">Download Links Refresh Interval</label>
<input type="text" class="form-control webdav-field" name="debrid[${index}].download_links_refresh_interval" id="debrid[${index}].download_links_refresh_interval" placeholder="40m" value="40m">
<small class="form-text text-muted">How often to refresh the download links list from debrid</small>
</div>
<div class="col-md-3 mb-3">
<label class="form-label" for="debrid[${index}].auto_expire_links_after">Expire Links After</label>
<input type="text" class="form-control webdav-field" name="debrid[${index}].auto_expire_links_after" id="debrid[${index}].auto_expire_links_after" placeholder="3d" value="3d">
<small class="form-text text-muted">How long to keep the links in the webdav before expiring</small>
</div>
<div class="col-md-3 mb-3">
<label class="form-label" for="debrid[${index}].folder_naming">Folder Naming Structure</label>
@@ -369,22 +397,27 @@
<option value="id">Use ID</option>
<option value="infohash">Use Infohash</option>
</select>
<small class="form-text text-muted">How to name each torrent directory in the webdav</small>
</div>
<div class="col-md-3 mb-3">
<label class="form-label" for="debrid[${index}].workers">Number of Workers</label>
<input type="text" class="form-control webdav-field" name="debrid[${index}].workers" id="debrid[${index}].workers" placeholder="e.g., 50" value="50">
<small class="form-text text-muted">Number of workers to use for the webdav server(when refreshing)</small>
</div>
<div class="col-md-3 mb-3">
<label class="form-label" for="debrid[${index}].rc_url">Rclone RC URL</label>
<input type="text" class="form-control webdav-field" name="debrid[${index}].rc_url" id="debrid[${index}].rc_url" placeholder="e.g., http://localhost:9990">
<small class="form-text text-muted">Rclone RC URL for the webdav server(speeds up import significantly)</small>
</div>
<div class="col-md-3 mb-3">
<label class="form-label" for="debrid[${index}].rc_user">Rclone RC User</label>
<input type="text" class="form-control webdav-field" name="debrid[${index}].rc_user" id="debrid[${index}].rc_user">
<small class="form-text text-muted">Rclone RC User for the webdav server</small>
</div>
<div class="col-md-3 mb-3">
<label class="form-label" for="debrid[${index}].rc_pass">Rclone RC Password</label>
<input type="password" class="form-control webdav-field" name="debrid[${index}].rc_pass" id="debrid[${index}].rc_pass">
<small class="form-text text-muted">Rclone RC Password for the webdav server</small>
</div>
</div>
</div>
@@ -736,6 +769,7 @@
qbittorrent: {
download_folder: document.querySelector('[name="qbit.download_folder"]').value,
refresh_interval: parseInt(document.querySelector('[name="qbit.refresh_interval"]').value || '0', 10),
max_downloads: parseInt(document.querySelector('[name="qbit.max_downloads"]').value || '0', 5),
skip_pre_cache: document.querySelector('[name="qbit.skip_pre_cache"]').checked
},
arrs: [],