chore:
- 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:
+14
-63
@@ -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
@@ -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'),
|
||||
|
||||
+2
-2
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user