- Rewrite arr storage to fix issues with repair
- Fix issues with restarts taking longer than expected
- Add bw_limit to rclone config
- Add support for skipping multi-season
- Other minor bug fixes
This commit is contained in:
Mukhtar Akere
2025-10-13 17:02:50 +01:00
parent ab485adfc8
commit 726f97e13c
17 changed files with 252 additions and 193 deletions

View File

@@ -106,6 +106,7 @@ type Rclone struct {
VfsReadChunkSizeLimit string `json:"vfs_read_chunk_size_limit,omitempty"` // Max chunk size (default off)
VfsReadAhead string `json:"vfs_read_ahead,omitempty"` // read ahead size
BufferSize string `json:"buffer_size,omitempty"` // Buffer size for reading files (default 16M)
BwLimit string `json:"bw_limit,omitempty"` // Bandwidth limit (default off)
VfsCacheMinFreeSpace string `json:"vfs_cache_min_free_space,omitempty"`
VfsFastFingerprint bool `json:"vfs_fast_fingerprint,omitempty"`

View File

@@ -2,6 +2,7 @@ package arr
import (
"bytes"
"cmp"
"context"
"crypto/tls"
"encoding/json"
@@ -33,6 +34,7 @@ const (
Radarr Type = "radarr"
Lidarr Type = "lidarr"
Readarr Type = "readarr"
Others Type = "others"
)
type Arr struct {
@@ -113,6 +115,7 @@ func (a *Arr) Validate() error {
if a.Token == "" || a.Host == "" {
return fmt.Errorf("arr not configured")
}
if request.ValidateURL(a.Host) != nil {
return fmt.Errorf("invalid arr host URL")
}
@@ -151,7 +154,7 @@ func InferType(host, name string) Type {
case strings.Contains(host, "readarr") || strings.Contains(name, "readarr"):
return Readarr
default:
return ""
return Others
}
}
@@ -162,7 +165,11 @@ func NewStorage() *Storage {
continue // Skip if host or token is not set
}
name := a.Name
arrs[name] = New(name, a.Host, a.Token, a.Cleanup, a.SkipRepair, a.DownloadUncached, a.SelectedDebrid, a.Source)
as := New(name, a.Host, a.Token, a.Cleanup, a.SkipRepair, a.DownloadUncached, a.SelectedDebrid, a.Source)
if request.ValidateURL(as.Host) != nil {
continue
}
arrs[a.Name] = as
}
return &Storage{
Arrs: arrs,
@@ -176,6 +183,11 @@ func (s *Storage) AddOrUpdate(arr *Arr) {
if arr.Host == "" || arr.Token == "" || arr.Name == "" {
return
}
// Check the host URL
if request.ValidateURL(arr.Host) != nil {
return
}
s.Arrs[arr.Name] = arr
}
@@ -195,6 +207,87 @@ func (s *Storage) GetAll() []*Arr {
return arrs
}
func (s *Storage) SyncToConfig() []config.Arr {
s.mu.Lock()
defer s.mu.Unlock()
cfg := config.Get()
arrConfigs := make(map[string]config.Arr)
for _, a := range cfg.Arrs {
if a.Host == "" || a.Token == "" {
continue // Skip empty arrs
}
arrConfigs[a.Name] = a
}
for name, arr := range s.Arrs {
exists, ok := arrConfigs[name]
if ok {
// Update existing arr config
// Check if the host URL is valid
if request.ValidateURL(arr.Host) == nil {
exists.Host = arr.Host
}
exists.Token = cmp.Or(exists.Token, arr.Token)
exists.Cleanup = arr.Cleanup
exists.SkipRepair = arr.SkipRepair
exists.DownloadUncached = arr.DownloadUncached
exists.SelectedDebrid = arr.SelectedDebrid
arrConfigs[name] = exists
} else {
// Add new arr config
arrConfigs[name] = config.Arr{
Name: arr.Name,
Host: arr.Host,
Token: arr.Token,
Cleanup: arr.Cleanup,
SkipRepair: arr.SkipRepair,
DownloadUncached: arr.DownloadUncached,
SelectedDebrid: arr.SelectedDebrid,
Source: arr.Source,
}
}
}
// Convert map to slice
arrs := make([]config.Arr, 0, len(arrConfigs))
for _, a := range arrConfigs {
arrs = append(arrs, a)
}
return arrs
}
func (s *Storage) SyncFromConfig(arrs []config.Arr) {
s.mu.Lock()
defer s.mu.Unlock()
arrConfigs := make(map[string]*Arr)
for _, a := range arrs {
arrConfigs[a.Name] = New(a.Name, a.Host, a.Token, a.Cleanup, a.SkipRepair, a.DownloadUncached, a.SelectedDebrid, a.Source)
}
// Add or update arrs from config
for name, arr := range s.Arrs {
if ac, ok := arrConfigs[name]; ok {
// Update existing arr
// is the host URL valid?
if request.ValidateURL(ac.Host) == nil {
ac.Host = arr.Host
}
ac.Token = cmp.Or(ac.Token, arr.Token)
ac.Cleanup = arr.Cleanup
ac.SkipRepair = arr.SkipRepair
ac.DownloadUncached = arr.DownloadUncached
ac.SelectedDebrid = arr.SelectedDebrid
ac.Source = arr.Source
arrConfigs[name] = ac
} else {
arrConfigs[name] = arr
}
}
// Replace the arrs map
s.Arrs = arrConfigs
}
func (s *Storage) StartWorker(ctx context.Context) error {
ticker := time.NewTicker(10 * time.Second)

View File

@@ -231,14 +231,12 @@ func (c *Cache) Reset() {
}
}
if err := c.scheduler.StopJobs(); err != nil {
c.logger.Error().Err(err).Msg("Failed to stop scheduler jobs")
}
if err := c.scheduler.Shutdown(); err != nil {
c.logger.Error().Err(err).Msg("Failed to stop scheduler")
}
go func() {
// Shutdown the scheduler (this will stop all jobs)
if err := c.scheduler.Shutdown(); err != nil {
c.logger.Error().Err(err).Msg("Failed to stop scheduler")
}
}()
// Stop the listing debouncer
c.listingDebouncer.Stop()

View File

@@ -161,11 +161,8 @@ func (q *QBit) authenticate(category, username, password string) (*arr.Arr, erro
return nil, fmt.Errorf("unauthorized: invalid credentials")
}
}
if arrValidated {
// Only update the arr if arr validation was successful
a.Source = "auto"
arrs.AddOrUpdate(a)
}
a.Source = "auto"
arrs.AddOrUpdate(a)
return a, nil
}

View File

@@ -3,13 +3,14 @@ package qbit
import (
"context"
"fmt"
"github.com/sirrobot01/decypharr/internal/utils"
"github.com/sirrobot01/decypharr/pkg/arr"
"github.com/sirrobot01/decypharr/pkg/wire"
"io"
"mime/multipart"
"strings"
"time"
"github.com/sirrobot01/decypharr/internal/utils"
"github.com/sirrobot01/decypharr/pkg/arr"
"github.com/sirrobot01/decypharr/pkg/wire"
)
// All torrent-related helpers goes here
@@ -20,7 +21,7 @@ func (q *QBit) addMagnet(ctx context.Context, url string, arr *arr.Arr, debrid s
}
_store := wire.Get()
importReq := wire.NewImportRequest(debrid, q.DownloadFolder, magnet, arr, action, false, "", wire.ImportTypeQBitTorrent)
importReq := wire.NewImportRequest(debrid, q.DownloadFolder, magnet, arr, action, false, "", wire.ImportTypeQBitTorrent, false)
err = _store.AddTorrent(ctx, importReq)
if err != nil {
@@ -38,7 +39,7 @@ func (q *QBit) addTorrent(ctx context.Context, fileHeader *multipart.FileHeader,
return fmt.Errorf("error reading file: %s \n %w", fileHeader.Filename, err)
}
_store := wire.Get()
importReq := wire.NewImportRequest(debrid, q.DownloadFolder, magnet, arr, action, false, "", wire.ImportTypeQBitTorrent)
importReq := wire.NewImportRequest(debrid, q.DownloadFolder, magnet, arr, action, false, "", wire.ImportTypeQBitTorrent, false)
err = _store.AddTorrent(ctx, importReq)
if err != nil {
return fmt.Errorf("failed to process torrent: %w", err)

View File

@@ -111,6 +111,10 @@ func (m *Manager) performMount(mountPath, provider, webdavURL string) error {
configOpts["BufferSize"] = cfg.Rclone.BufferSize
}
if cfg.Rclone.BwLimit != "" {
configOpts["BwLimit"] = cfg.Rclone.BwLimit
}
if len(configOpts) > 0 {
// Only add _config if there are options to set
mountArgs["_config"] = configOpts

View File

@@ -5,16 +5,6 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/go-co-op/gocron/v2"
"github.com/google/uuid"
"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/arr"
"github.com/sirrobot01/decypharr/pkg/debrid"
"golang.org/x/sync/errgroup"
"net"
"net/http"
"net/url"
@@ -25,6 +15,17 @@ import (
"strings"
"sync"
"time"
"github.com/go-co-op/gocron/v2"
"github.com/google/uuid"
"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/arr"
"github.com/sirrobot01/decypharr/pkg/debrid"
"golang.org/x/sync/errgroup"
)
type Repair struct {
@@ -105,10 +106,6 @@ func New(arrs *arr.Storage, engine *debrid.Storage) *Repair {
func (r *Repair) Reset() {
// Stop scheduler
if r.scheduler != nil {
if err := r.scheduler.StopJobs(); err != nil {
r.logger.Error().Err(err).Msg("Error stopping scheduler")
}
if err := r.scheduler.Shutdown(); err != nil {
r.logger.Error().Err(err).Msg("Error shutting down scheduler")
}

View File

@@ -2,13 +2,15 @@ package web
import (
"fmt"
"github.com/sirrobot01/decypharr/pkg/wire"
"golang.org/x/crypto/bcrypt"
"net/http"
"strings"
"time"
"github.com/sirrobot01/decypharr/pkg/wire"
"golang.org/x/crypto/bcrypt"
"encoding/json"
"github.com/go-chi/chi/v5"
"github.com/sirrobot01/decypharr/internal/config"
"github.com/sirrobot01/decypharr/internal/request"
@@ -18,8 +20,8 @@ import (
)
func (wb *Web) handleGetArrs(w http.ResponseWriter, r *http.Request) {
_store := wire.Get()
request.JSONResponse(w, _store.Arr().GetAll(), http.StatusOK)
arrStorage := wire.Get().Arr()
request.JSONResponse(w, arrStorage.GetAll(), http.StatusOK)
}
func (wb *Web) handleAddContent(w http.ResponseWriter, r *http.Request) {
@@ -41,6 +43,7 @@ func (wb *Web) handleAddContent(w http.ResponseWriter, r *http.Request) {
if downloadFolder == "" {
downloadFolder = config.Get().QBitTorrent.DownloadFolder
}
skipMultiSeason := r.FormValue("skipMultiSeason") == "true"
downloadUncached := r.FormValue("downloadUncached") == "true"
@@ -66,7 +69,7 @@ func (wb *Web) handleAddContent(w http.ResponseWriter, r *http.Request) {
continue
}
importReq := wire.NewImportRequest(debridName, downloadFolder, magnet, _arr, action, downloadUncached, callbackUrl, wire.ImportTypeAPI)
importReq := wire.NewImportRequest(debridName, downloadFolder, magnet, _arr, action, downloadUncached, callbackUrl, wire.ImportTypeAPI, skipMultiSeason)
if err := _store.AddTorrent(ctx, importReq); err != nil {
wb.logger.Error().Err(err).Str("url", url).Msg("Failed to add torrent")
errs = append(errs, fmt.Sprintf("URL %s: %v", url, err))
@@ -91,7 +94,7 @@ func (wb *Web) handleAddContent(w http.ResponseWriter, r *http.Request) {
continue
}
importReq := wire.NewImportRequest(debridName, downloadFolder, magnet, _arr, action, downloadUncached, callbackUrl, wire.ImportTypeAPI)
importReq := wire.NewImportRequest(debridName, downloadFolder, magnet, _arr, action, downloadUncached, callbackUrl, wire.ImportTypeAPI, skipMultiSeason)
err = _store.AddTorrent(ctx, importReq)
if err != nil {
wb.logger.Error().Err(err).Str("file", fileHeader.Filename).Msg("Failed to add torrent")
@@ -183,38 +186,9 @@ func (wb *Web) handleDeleteTorrents(w http.ResponseWriter, r *http.Request) {
}
func (wb *Web) handleGetConfig(w http.ResponseWriter, r *http.Request) {
// Merge config arrs, with arr Storage
unique := map[string]config.Arr{}
cfg := config.Get()
arrStorage := wire.Get().Arr()
// Add existing Arrs from storage
for _, a := range arrStorage.GetAll() {
if _, ok := unique[a.Name]; !ok {
// Only add if not already in the unique map
unique[a.Name] = config.Arr{
Name: a.Name,
Host: a.Host,
Token: a.Token,
Cleanup: a.Cleanup,
SkipRepair: a.SkipRepair,
DownloadUncached: a.DownloadUncached,
SelectedDebrid: a.SelectedDebrid,
Source: a.Source,
}
}
}
for _, a := range cfg.Arrs {
if a.Host == "" || a.Token == "" {
continue // Skip empty arrs
}
unique[a.Name] = a
}
cfg.Arrs = make([]config.Arr, 0, len(unique))
for _, a := range unique {
cfg.Arrs = append(cfg.Arrs, a)
}
cfg := config.Get()
cfg.Arrs = arrStorage.SyncToConfig()
// Create response with API token info
type ConfigResponse struct {
@@ -271,10 +245,7 @@ func (wb *Web) handleUpdateConfig(w http.ResponseWriter, r *http.Request) {
currentConfig.Rclone = updatedConfig.Rclone
// Update Debrids
if len(updatedConfig.Debrids) > 0 {
currentConfig.Debrids = updatedConfig.Debrids
// Clear legacy single debrid if using array
}
currentConfig.Debrids = updatedConfig.Debrids
// Update Arrs through the service
storage := wire.Get()
@@ -290,28 +261,8 @@ func (wb *Web) handleUpdateConfig(w http.ResponseWriter, r *http.Request) {
}
currentConfig.Arrs = newConfigArrs
// Add config arr into the config
for _, a := range currentConfig.Arrs {
if a.Host == "" || a.Token == "" {
continue // Skip empty arrs
}
existingArr := arrStorage.Get(a.Name)
if existingArr != nil {
// Update existing Arr
existingArr.Host = a.Host
existingArr.Token = a.Token
existingArr.Cleanup = a.Cleanup
existingArr.SkipRepair = a.SkipRepair
existingArr.DownloadUncached = a.DownloadUncached
existingArr.SelectedDebrid = a.SelectedDebrid
existingArr.Source = a.Source
arrStorage.AddOrUpdate(existingArr)
} else {
// Create new Arr if it doesn't exist
newArr := arr.New(a.Name, a.Host, a.Token, a.Cleanup, a.SkipRepair, a.DownloadUncached, a.SelectedDebrid, a.Source)
arrStorage.AddOrUpdate(newArr)
}
}
// Sync arrStorage with the new arrs
arrStorage.SyncFromConfig(currentConfig.Arrs)
if err := currentConfig.Save(); err != nil {
http.Error(w, "Error saving config: "+err.Error(), http.StatusInternalServerError)

File diff suppressed because one or more lines are too long

View File

@@ -150,7 +150,7 @@ class ConfigManager {
const fields = [
'enabled', 'rc_port', 'mount_path', 'cache_dir', 'transfers', 'vfs_cache_mode', 'vfs_cache_max_size', 'vfs_cache_max_age',
'vfs_cache_poll_interval', 'vfs_read_chunk_size', 'vfs_read_chunk_size_limit', 'buffer_size',
'vfs_cache_poll_interval', 'vfs_read_chunk_size', 'vfs_read_chunk_size_limit', 'buffer_size', 'bw_limit',
'uid', 'gid', 'vfs_read_ahead', 'attr_timeout', 'dir_cache_time', 'poll_interval', 'umask',
'no_modtime', 'no_checksum', 'log_level', 'vfs_cache_min_free_space', 'vfs_fast_fingerprint', 'vfs_read_chunk_streams',
'async_read', 'use_mmap'
@@ -1245,6 +1245,7 @@ class ConfigManager {
rc_port: getElementValue('rc_port', "5572"),
mount_path: getElementValue('mount_path'),
buffer_size: getElementValue('buffer_size'),
bw_limit: getElementValue('bw_limit'),
cache_dir: getElementValue('cache_dir'),
transfers: getElementValue('transfers', 8),
vfs_cache_mode: getElementValue('vfs_cache_mode', 'off'),

View File

@@ -13,8 +13,8 @@ func (wb *Web) Routes() http.Handler {
// Static assets - always public
staticFS, _ := fs.Sub(assetsEmbed, "assets/build")
imagesFS, _ := fs.Sub(imagesEmbed, "assets/images")
r.Handle("/assets/*", http.StripPrefix("/assets/", http.FileServer(http.FS(staticFS))))
r.Handle("/images/*", http.StripPrefix("/images/", http.FileServer(http.FS(imagesFS))))
r.Handle("/assets/*", http.StripPrefix(wb.urlBase+"assets/", http.FileServer(http.FS(staticFS))))
r.Handle("/images/*", http.StripPrefix(wb.urlBase+"images/", http.FileServer(http.FS(imagesFS))))
// Public routes - no auth needed
r.Get("/version", wb.handleGetVersion)

View File

@@ -471,7 +471,7 @@
<h3 class="text-lg font-semibold mb-4 flex items-center">
<i class="bi bi-folder mr-2"></i>Mount Configuration
</h3>
<div class="grid grid-cols-3 gap-4">
<div class="grid grid-cols-1 lg:grid-cols-4 gap-4">
<div class="form-control">
<label class="label" for="rclone.mount_path">
<span class="label-text font-medium">Global Mount Path</span>
@@ -533,11 +533,20 @@
<label class="label" for="rclone.buffer_size">
<span class="label-text font-medium">Buffer Size</span>
</label>
<input type="text" class="input input-bordered" name="rclone.buffer_size" id="rclone.buffer_size" placeholder="10M" min="0">
<input type="text" class="input input-bordered" name="rclone.buffer_size" id="rclone.buffer_size" placeholder="10M">
<div class="label">
<span class="label-text-alt">Buffer Size(This caches to memory, be wary!!)</span>
</div>
</div>
<div class="form-control">
<label class="label" for="rclone.bw_limit">
<span class="label-text font-medium">Bandwidth Limit</span>
</label>
<input type="text" class="input input-bordered" name="rclone.bw_limit" id="rclone.bw_limit" placeholder="100M">
<div class="label">
<span class="label-text-alt">Bandwidth limit (e.g., 100M, 1G, leave empty for unlimited)</span>
</div>
</div>
<div class="form-control">
<label class="label" for="rclone.attr_timeout">
<span class="label-text font-medium">Attribute Caching Timeout</span>

View File

@@ -5,16 +5,18 @@
<i class="bi bi-exclamation-triangle text-xl"></i>
<div>
<h3 class="font-bold">Configuration Required</h3>
<div class="text-sm">Your configuration is incomplete. Please complete the setup in the <a href="{{.URLBase}}settings" class="link link-hover font-semibold">Settings page</a>.</div>
<div class="text-sm">Your configuration is incomplete. Please complete the setup in the <a
href="{{.URLBase}}settings" class="link link-hover font-semibold">Settings page</a>.
</div>
</div>
</div>
{{ end }}
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<form id="downloadForm" enctype="multipart/form-data" class="space-y-3">
<div class="space-y-2">
<div class="form-control">
<form id="downloadForm" enctype="multipart/form-data" class="space-y-6">
<div class="flex gap-4">
<div class="form-control flex-1">
<label class="label" for="magnetURI">
<span class="label-text font-semibold">
<i class="bi bi-magnet mr-2 text-primary"></i>Torrent Links
@@ -27,9 +29,7 @@
placeholder="Paste your magnet links or torrent URLs here, one per line..."></textarea>
</div>
<div class="divider">OR</div>
<div class="form-control">
<div class="form-control flex-1">
<label class="label">
<span class="label-text font-semibold">
<i class="bi bi-file-earmark-arrow-up mr-2 text-secondary"></i>Upload Torrent Files
@@ -50,86 +50,84 @@
</div>
</div>
<div class="divider"></div>
<div class="divider">Download Settings</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-3">
<div class="space-y-2">
<h3 class="text-lg font-semibold flex items-center">
<i class="bi bi-gear mr-2 text-info"></i>Download Settings
</h3>
<div class="form-control">
<label class="label" for="downloadAction">
<span class="label-text">Post Download Action</span>
</label>
<select class="select select-bordered" id="downloadAction" name="downloadAction">
<option value="symlink" selected>Create Symlink</option>
<option value="download">Download Files</option>
<option value="none">No Action</option>
</select>
<div class="label">
<span class="label-text-alt">How to handle files after download completion</span>
</div>
</div>
<div class="form-control">
<label class="label" for="downloadFolder">
<span class="label-text">Download Folder</span>
</label>
<input type="text"
class="input input-bordered"
id="downloadFolder"
name="downloadFolder"
placeholder="/downloads/torrents">
<div class="label">
<span class="label-text-alt">Leave empty to use default qBittorrent folder</span>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-3 space-y-4">
<div class="form-control">
<label class="label" for="downloadAction">
<span class="label-text">Post Download Action</span>
</label>
<select class="select select-bordered" id="downloadAction" name="downloadAction">
<option value="symlink" selected>Create Symlink</option>
<option value="download">Download Files</option>
<option value="none">No Action</option>
</select>
<div class="label">
<span class="label-text-alt">How to handle files after download completion</span>
</div>
</div>
<div class="space-y-2">
<h3 class="text-lg font-semibold flex items-center">
<i class="bi bi-tags mr-2 text-warning"></i>Categorization
</h3>
<div class="form-control">
<label class="label" for="arr">
<span class="label-text">Arr Category</span>
</label>
<input type="text"
class="input input-bordered"
id="arr"
name="arr"
placeholder="sonarr, radarr, etc.">
<div class="label">
<span class="label-text-alt">Optional: Specify which Arr service should handle this</span>
</div>
<div class="form-control">
<label class="label" for="downloadFolder">
<span class="label-text">Download Folder</span>
</label>
<input type="text"
class="input input-bordered"
id="downloadFolder"
name="downloadFolder"
placeholder="/downloads/torrents">
<div class="label">
<span class="label-text-alt">Leave empty to use default qBittorrent folder</span>
</div>
{{ if .HasMultiDebrid }}
<div class="form-control">
<label class="label" for="debrid">
<span class="label-text">Debrid Service</span>
</label>
<select class="select select-bordered" id="debrid" name="debrid">
{{ range $index, $debrid := .Debrids }}
<option value="{{ $debrid }}" {{ if eq $index 0 }}selected{{end}}>
{{ $debrid }}
</option>
{{ end }}
</select>
<div class="label">
<span class="label-text-alt">Choose which debrid service to use</span>
</div>
</div>
{{ end }}
</div>
<div class="form-control">
<label class="label" for="arr">
<span class="label-text">Arr Category</span>
</label>
<input type="text"
class="input input-bordered"
id="arr"
name="arr"
placeholder="sonarr, radarr, etc.">
<div class="label">
<span class="label-text-alt">Optional: Specify which Arr service should handle this</span>
</div>
</div>
{{ if .HasMultiDebrid }}
<div class="form-control">
<label class="label" for="debrid">
<span class="label-text">Debrid Service</span>
</label>
<select class="select select-bordered" id="debrid" name="debrid">
{{ range $index, $debrid := .Debrids }}
<option value="{{ $debrid }}" {{ if eq $index 0 }}selected{{end}}>
{{ $debrid }}
</option>
{{ end }}
</select>
<div class="label">
<span class="label-text-alt">Choose which debrid service to use</span>
</div>
</div>
{{ end }}
<div class="form-control">
<label class="label cursor-pointer justify-start gap-3">
<input type="checkbox" class="checkbox" name="downloadUncached" id="downloadUncached">
<div>
<span class="label-text font-medium">Download Uncached Content</span>
<div class="label-text-alt">Allow downloading of content not cached by debrid service</div>
<div class="label-text-alt">Allow downloading of content not cached by debrid service
</div>
</div>
</label>
</div>
<div class="form-control">
<label class="label cursor-pointer justify-start gap-3">
<input type="checkbox" class="checkbox" name="skipMultiSeason" id="skipMultiSeason">
<div>
<span class="label-text font-medium">Skip Multi-Season Checker</span>
<div class="label-text-alt">Skip the multi-season episode checker for TV shows</div>
</div>
</label>
</div>

View File

@@ -61,6 +61,7 @@ type Web struct {
cookie *sessions.CookieStore
templates *template.Template
torrents *wire.TorrentStorage
urlBase string
}
func New() *Web {
@@ -87,5 +88,6 @@ func New() *Web {
templates: templates,
cookie: cookieStore,
torrents: wire.Get().Torrents(),
urlBase: cfg.URLBase,
}
}

View File

@@ -6,16 +6,17 @@ import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"sync"
"time"
"github.com/google/uuid"
"github.com/sirrobot01/decypharr/internal/config"
"github.com/sirrobot01/decypharr/internal/request"
"github.com/sirrobot01/decypharr/internal/utils"
"github.com/sirrobot01/decypharr/pkg/arr"
debridTypes "github.com/sirrobot01/decypharr/pkg/debrid/types"
"net/http"
"net/url"
"sync"
"time"
)
type ImportType string
@@ -34,6 +35,7 @@ type ImportRequest struct {
Action string `json:"action"`
DownloadUncached bool `json:"downloadUncached"`
CallBackUrl string `json:"callBackUrl"`
SkipMultiSeason bool `json:"skip_multi_season"`
Status string `json:"status"`
CompletedAt time.Time `json:"completedAt,omitempty"`
@@ -43,7 +45,7 @@ type ImportRequest struct {
Async bool `json:"async"`
}
func NewImportRequest(debrid string, downloadFolder string, magnet *utils.Magnet, arr *arr.Arr, action string, downloadUncached bool, callBackUrl string, importType ImportType) *ImportRequest {
func NewImportRequest(debrid string, downloadFolder string, magnet *utils.Magnet, arr *arr.Arr, action string, downloadUncached bool, callBackUrl string, importType ImportType, skipMultiSeason bool) *ImportRequest {
cfg := config.Get()
callBackUrl = cmp.Or(callBackUrl, cfg.CallbackURL)
return &ImportRequest{
@@ -57,6 +59,7 @@ func NewImportRequest(debrid string, downloadFolder string, magnet *utils.Magnet
DownloadUncached: downloadUncached,
CallBackUrl: callBackUrl,
Type: importType,
SkipMultiSeason: skipMultiSeason,
}
}

View File

@@ -3,6 +3,9 @@ package wire
import (
"cmp"
"context"
"sync"
"time"
"github.com/go-co-op/gocron/v2"
"github.com/rs/zerolog"
"github.com/sirrobot01/decypharr/internal/config"
@@ -11,8 +14,6 @@ import (
"github.com/sirrobot01/decypharr/pkg/debrid"
"github.com/sirrobot01/decypharr/pkg/rclone"
"github.com/sirrobot01/decypharr/pkg/repair"
"sync"
"time"
)
type Store struct {
@@ -101,7 +102,6 @@ func Reset() {
}
if instance.scheduler != nil {
_ = instance.scheduler.StopJobs()
_ = instance.scheduler.Shutdown()
}
}

View File

@@ -18,7 +18,6 @@ import (
func (s *Store) AddTorrent(ctx context.Context, importReq *ImportRequest) error {
torrent := createTorrentFromMagnet(importReq)
debridTorrent, err := debridTypes.Process(ctx, s.debrid, importReq.SelectedDebrid, importReq.Magnet, importReq.Arr, importReq.Action, importReq.DownloadUncached)
if err != nil {
var httpErr *utils.HTTPError
if ok := errors.As(err, &httpErr); ok {
@@ -131,11 +130,16 @@ func (s *Store) processFiles(torrent *Torrent, debridTorrent *types.Torrent, imp
}
// Check for multi-season torrent support
isMultiSeason, seasons, err := s.detectMultiSeason(debridTorrent)
if err != nil {
s.logger.Warn().Msgf("Error detecting multi-season for %s: %v", debridTorrent.Name, err)
// Continue with normal processing if detection fails
isMultiSeason = false
var isMultiSeason bool
var seasons []SeasonInfo
var err error
if !importReq.SkipMultiSeason {
isMultiSeason, seasons, err = s.detectMultiSeason(debridTorrent)
if err != nil {
s.logger.Warn().Msgf("Error detecting multi-season for %s: %v", debridTorrent.Name, err)
// Continue with normal processing if detection fails
isMultiSeason = false
}
}
switch importReq.Action {