Add more rclone flags, fix minor issues

This commit is contained in:
Mukhtar Akere
2025-08-23 06:00:07 +01:00
parent b0a698f15e
commit f8667938b6
27 changed files with 256 additions and 123 deletions

View File

@@ -7,10 +7,10 @@ import (
"github.com/sirrobot01/decypharr/internal/logger"
"github.com/sirrobot01/decypharr/pkg/qbit"
"github.com/sirrobot01/decypharr/pkg/server"
"github.com/sirrobot01/decypharr/pkg/store"
"github.com/sirrobot01/decypharr/pkg/version"
"github.com/sirrobot01/decypharr/pkg/web"
"github.com/sirrobot01/decypharr/pkg/webdav"
"github.com/sirrobot01/decypharr/pkg/wire"
"net/http"
"os"
"runtime"
@@ -77,7 +77,7 @@ func Start(ctx context.Context) error {
reset := func() {
// Reset the store and services
qb.Reset()
store.Reset()
wire.Reset()
// refresh GC
runtime.GC()
}
@@ -151,7 +151,7 @@ func startServices(ctx context.Context, cancelSvc context.CancelFunc, wd *webdav
// Start rclone RC server if enabled
safeGo(func() error {
rcManager := store.Get().RcloneManager()
rcManager := wire.Get().RcloneManager()
if rcManager == nil {
return nil
}
@@ -160,7 +160,7 @@ func startServices(ctx context.Context, cancelSvc context.CancelFunc, wd *webdav
if cfg := config.Get(); cfg.Repair.Enabled {
safeGo(func() error {
repair := store.Get().Repair()
repair := wire.Get().Repair()
if repair != nil {
if err := repair.Start(ctx); err != nil {
_log.Error().Err(err).Msg("repair failed")
@@ -171,7 +171,7 @@ func startServices(ctx context.Context, cancelSvc context.CancelFunc, wd *webdav
}
safeGo(func() error {
store.Get().StartWorkers(ctx)
wire.Get().StartWorkers(ctx)
return nil
})

View File

@@ -91,6 +91,7 @@ type Rclone struct {
// Global mount folder where all providers will be mounted as subfolders
Enabled bool `json:"enabled,omitempty"`
MountPath string `json:"mount_path,omitempty"`
RcPort string `json:"rc_port,omitempty"`
// Cache settings
CacheDir string `json:"cache_dir,omitempty"`
@@ -98,6 +99,7 @@ type Rclone struct {
// VFS settings
VfsCacheMode string `json:"vfs_cache_mode,omitempty"` // off, minimal, writes, full
VfsCacheMaxAge string `json:"vfs_cache_max_age,omitempty"` // Maximum age of objects in the cache (default 1h)
VfsDiskSpaceTotal string `json:"vfs_disk_space_total,omitempty"` // Total disk space available for the cache (default off)
VfsCacheMaxSize string `json:"vfs_cache_max_size,omitempty"` // Maximum size of the cache (default off)
VfsCachePollInterval string `json:"vfs_cache_poll_interval,omitempty"` // How often to poll for changes (default 1m)
VfsReadChunkSize string `json:"vfs_read_chunk_size,omitempty"` // Read chunk size (default 128M)
@@ -106,6 +108,13 @@ type Rclone struct {
VfsPollInterval string `json:"vfs_poll_interval,omitempty"` // How often to rclone cleans the cache (default 1m)
BufferSize string `json:"buffer_size,omitempty"` // Buffer size for reading files (default 16M)
VfsCacheMinFreeSpace string `json:"vfs_cache_min_free_space,omitempty"`
VfsFastFingerprint bool `json:"vfs_fast_fingerprint,omitempty"`
VfsReadChunkStreams int `json:"vfs_read_chunk_streams,omitempty"`
AsyncRead *bool `json:"async_read,omitempty"` // Use async read for files
Transfers int `json:"transfers,omitempty"` // Number of transfers to use (default 4)
UseMmap bool `json:"use_mmap,omitempty"`
// File system settings
UID uint32 `json:"uid,omitempty"` // User ID for mounted files
GID uint32 `json:"gid,omitempty"` // Group ID for mounted files
@@ -417,6 +426,11 @@ func (c *Config) setDefaults() {
// Rclone defaults
if c.Rclone.Enabled {
c.Rclone.RcPort = cmp.Or(c.Rclone.RcPort, "5572")
if c.Rclone.AsyncRead == nil {
_asyncTrue := true
c.Rclone.AsyncRead = &_asyncTrue
}
c.Rclone.VfsCacheMode = cmp.Or(c.Rclone.VfsCacheMode, "off")
if c.Rclone.UID == 0 {
c.Rclone.UID = uint32(os.Getuid())
@@ -429,6 +443,9 @@ func (c *Config) setDefaults() {
c.Rclone.GID = uint32(os.Getgid())
}
}
if c.Rclone.Transfers == 0 {
c.Rclone.Transfers = 4 // Default number of transfers
}
if c.Rclone.VfsCacheMode != "off" {
c.Rclone.VfsCachePollInterval = cmp.Or(c.Rclone.VfsCachePollInterval, "1m") // Clean cache every minute
}

View File

@@ -958,8 +958,7 @@ func (r *RealDebrid) GetProfile() (*types.Profile, error) {
}
func (r *RealDebrid) GetAvailableSlots() (int, error) {
url := fmt.Sprintf("%s/torrents/activeCount", r.Host)
req, _ := http.NewRequest(http.MethodGet, url, nil)
req, _ := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/torrents/activeCount", r.Host), nil)
resp, err := r.client.MakeRequest(req)
if err != nil {
return 0, nil

View File

@@ -7,7 +7,7 @@ import (
"github.com/go-chi/chi/v5"
"github.com/sirrobot01/decypharr/internal/config"
"github.com/sirrobot01/decypharr/pkg/arr"
"github.com/sirrobot01/decypharr/pkg/store"
"github.com/sirrobot01/decypharr/pkg/wire"
"golang.org/x/crypto/bcrypt"
"net/http"
"net/url"
@@ -130,7 +130,7 @@ func (q *QBit) authContext(next http.Handler) http.Handler {
cfg := config.Get()
host, token, err := decodeAuthHeader(r.Header.Get("Authorization"))
category := getCategory(r.Context())
arrs := store.Get().Arr()
arrs := wire.Get().Arr()
// Check if arr exists
a := arrs.Get(category)
if a == nil {

View File

@@ -4,7 +4,7 @@ import (
"github.com/rs/zerolog"
"github.com/sirrobot01/decypharr/internal/config"
"github.com/sirrobot01/decypharr/internal/logger"
"github.com/sirrobot01/decypharr/pkg/store"
"github.com/sirrobot01/decypharr/pkg/wire"
)
type QBit struct {
@@ -12,7 +12,7 @@ type QBit struct {
Password string
DownloadFolder string
Categories []string
storage *store.TorrentStorage
storage *wire.TorrentStorage
logger zerolog.Logger
Tags []string
}
@@ -25,7 +25,7 @@ func New() *QBit {
Password: cfg.Password,
DownloadFolder: cfg.DownloadFolder,
Categories: cfg.Categories,
storage: store.Get().Torrents(),
storage: wire.Get().Torrents(),
logger: logger.New("qbit"),
}
}

View File

@@ -5,7 +5,7 @@ import (
"fmt"
"github.com/sirrobot01/decypharr/internal/utils"
"github.com/sirrobot01/decypharr/pkg/arr"
"github.com/sirrobot01/decypharr/pkg/store"
"github.com/sirrobot01/decypharr/pkg/wire"
"io"
"mime/multipart"
"strings"
@@ -18,9 +18,9 @@ func (q *QBit) addMagnet(ctx context.Context, url string, arr *arr.Arr, debrid s
if err != nil {
return fmt.Errorf("error parsing magnet link: %w", err)
}
_store := store.Get()
_store := wire.Get()
importReq := store.NewImportRequest(debrid, q.DownloadFolder, magnet, arr, action, false, "", store.ImportTypeQBitTorrent)
importReq := wire.NewImportRequest(debrid, q.DownloadFolder, magnet, arr, action, false, "", wire.ImportTypeQBitTorrent)
err = _store.AddTorrent(ctx, importReq)
if err != nil {
@@ -37,8 +37,8 @@ func (q *QBit) addTorrent(ctx context.Context, fileHeader *multipart.FileHeader,
if err != nil {
return fmt.Errorf("error reading file: %s \n %w", fileHeader.Filename, err)
}
_store := store.Get()
importReq := store.NewImportRequest(debrid, q.DownloadFolder, magnet, arr, action, false, "", store.ImportTypeQBitTorrent)
_store := wire.Get()
importReq := wire.NewImportRequest(debrid, q.DownloadFolder, magnet, arr, action, false, "", wire.ImportTypeQBitTorrent)
err = _store.AddTorrent(ctx, importReq)
if err != nil {
return fmt.Errorf("failed to process torrent: %w", err)
@@ -46,19 +46,19 @@ func (q *QBit) addTorrent(ctx context.Context, fileHeader *multipart.FileHeader,
return nil
}
func (q *QBit) ResumeTorrent(t *store.Torrent) bool {
func (q *QBit) ResumeTorrent(t *wire.Torrent) bool {
return true
}
func (q *QBit) PauseTorrent(t *store.Torrent) bool {
func (q *QBit) PauseTorrent(t *wire.Torrent) bool {
return true
}
func (q *QBit) RefreshTorrent(t *store.Torrent) bool {
func (q *QBit) RefreshTorrent(t *wire.Torrent) bool {
return true
}
func (q *QBit) GetTorrentProperties(t *store.Torrent) *TorrentProperties {
func (q *QBit) GetTorrentProperties(t *wire.Torrent) *TorrentProperties {
return &TorrentProperties{
AdditionDate: t.AddedOn,
Comment: "Debrid Blackhole <https://github.com/sirrobot01/decypharr>",
@@ -83,7 +83,7 @@ func (q *QBit) GetTorrentProperties(t *store.Torrent) *TorrentProperties {
}
}
func (q *QBit) setTorrentTags(t *store.Torrent, tags []string) bool {
func (q *QBit) setTorrentTags(t *wire.Torrent, tags []string) bool {
torrentTags := strings.Split(t.Tags, ",")
for _, tag := range tags {
if tag == "" {
@@ -101,7 +101,7 @@ func (q *QBit) setTorrentTags(t *store.Torrent, tags []string) bool {
return true
}
func (q *QBit) removeTorrentTags(t *store.Torrent, tags []string) bool {
func (q *QBit) removeTorrentTags(t *wire.Torrent, tags []string) bool {
torrentTags := strings.Split(t.Tags, ",")
newTorrentTags := utils.RemoveItem(torrentTags, tags...)
q.Tags = utils.RemoveItem(q.Tags, tags...)

View File

@@ -4,19 +4,18 @@ import (
"fmt"
"os"
"os/exec"
"path/filepath"
"time"
"github.com/sirrobot01/decypharr/internal/config"
)
// Mount creates a mount using the rclone RC API with retry logic
func (m *Manager) Mount(provider, webdavURL string) error {
return m.mountWithRetry(provider, webdavURL, 3)
func (m *Manager) Mount(mountPath, provider, webdavURL string) error {
return m.mountWithRetry(mountPath, provider, webdavURL, 3)
}
// mountWithRetry attempts to mount with retry logic
func (m *Manager) mountWithRetry(provider, webdavURL string, maxRetries int) error {
func (m *Manager) mountWithRetry(mountPath, provider, webdavURL string, maxRetries int) error {
if !m.IsReady() {
if err := m.WaitForReady(30 * time.Second); err != nil {
return fmt.Errorf("rclone RC server not ready: %w", err)
@@ -34,7 +33,7 @@ func (m *Manager) mountWithRetry(provider, webdavURL string, maxRetries int) err
time.Sleep(wait)
}
if err := m.performMount(provider, webdavURL); err != nil {
if err := m.performMount(mountPath, provider, webdavURL); err != nil {
m.logger.Error().
Err(err).
Str("provider", provider).
@@ -49,9 +48,8 @@ func (m *Manager) mountWithRetry(provider, webdavURL string, maxRetries int) err
}
// performMount performs a single mount attempt
func (m *Manager) performMount(provider, webdavURL string) error {
func (m *Manager) performMount(mountPath, provider, webdavURL string) error {
cfg := config.Get()
mountPath := filepath.Join(cfg.Rclone.MountPath, provider)
// Create mount directory
if err := os.MkdirAll(mountPath, 0755); err != nil {
@@ -94,6 +92,18 @@ func (m *Manager) performMount(provider, webdavURL string) error {
"VolumeName": fmt.Sprintf("decypharr-%s", provider),
}
if cfg.Rclone.AsyncRead != nil {
mountOpt["AsyncRead"] = *cfg.Rclone.AsyncRead
}
if cfg.Rclone.UseMmap {
mountOpt["UseMmap"] = cfg.Rclone.UseMmap
}
if cfg.Rclone.Transfers != 0 {
mountOpt["Transfers"] = cfg.Rclone.Transfers
}
configOpts := make(map[string]interface{})
if cfg.Rclone.BufferSize != "" {
@@ -127,6 +137,19 @@ func (m *Manager) performMount(provider, webdavURL string) error {
if cfg.Rclone.VfsReadAhead != "" {
vfsOpt["ReadAhead"] = cfg.Rclone.VfsReadAhead
}
if cfg.Rclone.VfsCacheMinFreeSpace != "" {
vfsOpt["CacheMinFreeSpace"] = cfg.Rclone.VfsCacheMinFreeSpace
}
if cfg.Rclone.VfsFastFingerprint {
vfsOpt["FastFingerprint"] = cfg.Rclone.VfsFastFingerprint
}
if cfg.Rclone.VfsReadChunkStreams != 0 {
vfsOpt["ReadChunkStreams"] = cfg.Rclone.VfsReadChunkStreams
}
if cfg.Rclone.NoChecksum {
vfsOpt["NoChecksum"] = cfg.Rclone.NoChecksum
}

View File

@@ -74,7 +74,7 @@ func (m *Manager) RecoverMount(provider string) error {
time.Sleep(1 * time.Second)
// Try to remount
if err := m.Mount(provider, mountInfo.WebDAVURL); err != nil {
if err := m.Mount(mountInfo.LocalPath, provider, mountInfo.WebDAVURL); err != nil {
return fmt.Errorf("failed to recover mount for %s: %w", provider, err)
}

View File

@@ -6,11 +6,13 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"path/filepath"
"slices"
"strings"
"sync"
"time"
@@ -261,50 +263,35 @@ func (m *Manager) Stop() error {
case err := <-done:
if err != nil && !errors.Is(err, context.Canceled) && !WasHardTerminated(err) {
m.logger.Warn().Err(err).Msg("Rclone process exited with error")
} else {
m.logger.Info().Msg("Rclone process exited gracefully")
}
case <-time.After(10 * time.Second):
case <-time.After(2 * time.Second):
m.logger.Warn().Msg("Timeout waiting for rclone to exit, force killing")
if err := m.cmd.Process.Kill(); err != nil {
m.logger.Error().Err(err).Msg("Failed to force kill rclone process")
return err
// Check if the process already finished
if !strings.Contains(err.Error(), "process already finished") {
m.logger.Error().Err(err).Msg("Failed to force kill rclone process")
return err
}
m.logger.Info().Msg("Process already finished during kill attempt")
}
// Wait a bit more for the kill to take effect
// Still wait for the Wait() to complete to clean up the process
select {
case <-done:
m.logger.Info().Msg("Rclone process killed successfully")
m.logger.Info().Msg("Rclone process cleanup completed")
case <-time.After(5 * time.Second):
m.logger.Error().Msg("Process may still be running after kill")
m.logger.Error().Msg("Process cleanup timeout")
}
}
}
// Clean up any remaining mount directories
cfg := config.Get()
if cfg.Rclone.MountPath != "" {
m.cleanupMountDirectories(cfg.Rclone.MountPath)
}
m.serverStarted = false
m.logger.Info().Msg("Rclone RC server stopped")
return nil
}
// cleanupMountDirectories removes empty mount directories
func (m *Manager) cleanupMountDirectories(_ string) {
m.mountsMutex.RLock()
defer m.mountsMutex.RUnlock()
for _, mount := range m.mounts {
if mount.LocalPath != "" {
// Try to remove the directory if it's empty
if err := os.Remove(mount.LocalPath); err == nil {
m.logger.Debug().Str("path", mount.LocalPath).Msg("Removed empty mount directory")
}
// Don't log errors here as the directory might not be empty, which is fine
}
}
}
// waitForServer waits for the RC server to become available
func (m *Manager) waitForServer() {
maxAttempts := 30
@@ -352,7 +339,12 @@ func (m *Manager) makeRequest(req RCRequest, close bool) (*http.Response, error)
if resp.StatusCode != http.StatusOK {
// Read the response body to get more details
defer resp.Body.Close()
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
m.logger.Debug().Err(err).Msg("Failed to close response body")
}
}(resp.Body)
var errorResp RCResponse
if err := json.NewDecoder(resp.Body).Decode(&errorResp); err != nil {
return nil, fmt.Errorf("request failed with status %s, but could not decode error response: %w", resp.Status, err)

View File

@@ -65,7 +65,7 @@ func (m *Mount) Mount(ctx context.Context) error {
Str("mount_path", m.LocalPath).
Msg("Creating mount via RC")
if err := m.rcManager.Mount(m.Provider, m.WebDAVURL); err != nil {
if err := m.rcManager.Mount(m.LocalPath, m.Provider, m.WebDAVURL); err != nil {
m.logger.Error().Str("provider", m.Provider).Msg("Mount operation failed")
return fmt.Errorf("mount failed for %s", m.Provider)
}

View File

@@ -5,14 +5,14 @@ import (
"github.com/go-chi/chi/v5"
"github.com/sirrobot01/decypharr/internal/request"
debridTypes "github.com/sirrobot01/decypharr/pkg/debrid/types"
"github.com/sirrobot01/decypharr/pkg/store"
"github.com/sirrobot01/decypharr/pkg/wire"
"net/http"
"runtime"
)
func (s *Server) handleIngests(w http.ResponseWriter, r *http.Request) {
ingests := make([]debridTypes.IngestData, 0)
_store := store.Get()
_store := wire.Get()
debrids := _store.Debrid()
if debrids == nil {
http.Error(w, "Debrid service is not enabled", http.StatusInternalServerError)
@@ -42,7 +42,7 @@ func (s *Server) handleIngestsByDebrid(w http.ResponseWriter, r *http.Request) {
return
}
_store := store.Get()
_store := wire.Get()
debrids := _store.Debrid()
if debrids == nil {
@@ -92,7 +92,7 @@ func (s *Server) handleStats(w http.ResponseWriter, r *http.Request) {
"go_version": runtime.Version(),
}
debrids := store.Get().Debrid()
debrids := wire.Get().Debrid()
if debrids == nil {
request.JSONResponse(w, stats, http.StatusOK)
return
@@ -121,7 +121,7 @@ func (s *Server) handleStats(w http.ResponseWriter, r *http.Request) {
}
debridStat.Library = libraryStat
// Get detailed account information
accounts := client.Accounts().All()
accountDetails := make([]map[string]any, 0)
@@ -135,7 +135,7 @@ func (s *Server) handleStats(w http.ResponseWriter, r *http.Request) {
} else {
maskedToken = "****"
}
accountDetail := map[string]any{
"order": account.Order,
"disabled": account.Disabled,
@@ -153,7 +153,7 @@ func (s *Server) handleStats(w http.ResponseWriter, r *http.Request) {
stats["debrids"] = debridStats
// Add rclone stats if available
if rcManager := store.Get().RcloneManager(); rcManager != nil && rcManager.IsReady() {
if rcManager := wire.Get().RcloneManager(); rcManager != nil && rcManager.IsReady() {
rcStats, err := rcManager.GetStats()
if err != nil {
s.logger.Error().Err(err).Msg("Failed to get rclone stats")

View File

@@ -3,7 +3,7 @@ package server
import (
"cmp"
"encoding/json"
"github.com/sirrobot01/decypharr/pkg/store"
"github.com/sirrobot01/decypharr/pkg/wire"
"net/http"
)
@@ -38,7 +38,7 @@ func (s *Server) handleTautulli(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Invalid ID", http.StatusBadRequest)
return
}
repair := store.Get().Repair()
repair := wire.Get().Repair()
mediaId := cmp.Or(payload.TmdbID, payload.TvdbID)

View File

@@ -2,7 +2,7 @@ package web
import (
"fmt"
"github.com/sirrobot01/decypharr/pkg/store"
"github.com/sirrobot01/decypharr/pkg/wire"
"golang.org/x/crypto/bcrypt"
"net/http"
"strings"
@@ -18,7 +18,7 @@ import (
)
func (wb *Web) handleGetArrs(w http.ResponseWriter, r *http.Request) {
_store := store.Get()
_store := wire.Get()
request.JSONResponse(w, _store.Arr().GetAll(), http.StatusOK)
}
@@ -28,9 +28,9 @@ func (wb *Web) handleAddContent(w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
_store := store.Get()
_store := wire.Get()
results := make([]*store.ImportRequest, 0)
results := make([]*wire.ImportRequest, 0)
errs := make([]string, 0)
arrName := r.FormValue("arr")
@@ -66,7 +66,7 @@ func (wb *Web) handleAddContent(w http.ResponseWriter, r *http.Request) {
continue
}
importReq := store.NewImportRequest(debridName, downloadFolder, magnet, _arr, action, downloadUncached, callbackUrl, store.ImportTypeAPI)
importReq := wire.NewImportRequest(debridName, downloadFolder, magnet, _arr, action, downloadUncached, callbackUrl, wire.ImportTypeAPI)
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 +91,7 @@ func (wb *Web) handleAddContent(w http.ResponseWriter, r *http.Request) {
continue
}
importReq := store.NewImportRequest(debridName, downloadFolder, magnet, _arr, action, downloadUncached, callbackUrl, store.ImportTypeAPI)
importReq := wire.NewImportRequest(debridName, downloadFolder, magnet, _arr, action, downloadUncached, callbackUrl, wire.ImportTypeAPI)
err = _store.AddTorrent(ctx, importReq)
if err != nil {
wb.logger.Error().Err(err).Str("file", fileHeader.Filename).Msg("Failed to add torrent")
@@ -103,8 +103,8 @@ func (wb *Web) handleAddContent(w http.ResponseWriter, r *http.Request) {
}
request.JSONResponse(w, struct {
Results []*store.ImportRequest `json:"results"`
Errors []string `json:"errors,omitempty"`
Results []*wire.ImportRequest `json:"results"`
Errors []string `json:"errors,omitempty"`
}{
Results: results,
Errors: errs,
@@ -118,7 +118,7 @@ func (wb *Web) handleRepairMedia(w http.ResponseWriter, r *http.Request) {
return
}
_store := store.Get()
_store := wire.Get()
var arrs []string
@@ -186,7 +186,7 @@ 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 := store.Get().Arr()
arrStorage := wire.Get().Arr()
// Add existing Arrs from storage
for _, a := range arrStorage.GetAll() {
@@ -276,7 +276,7 @@ func (wb *Web) handleUpdateConfig(w http.ResponseWriter, r *http.Request) {
}
// Update Arrs through the service
storage := store.Get()
storage := wire.Get()
arrStorage := storage.Arr()
newConfigArrs := make([]config.Arr, 0)
@@ -330,7 +330,7 @@ func (wb *Web) handleUpdateConfig(w http.ResponseWriter, r *http.Request) {
}
func (wb *Web) handleGetRepairJobs(w http.ResponseWriter, r *http.Request) {
_store := store.Get()
_store := wire.Get()
request.JSONResponse(w, _store.Repair().GetJobs(), http.StatusOK)
}
@@ -340,7 +340,7 @@ func (wb *Web) handleProcessRepairJob(w http.ResponseWriter, r *http.Request) {
http.Error(w, "No job ID provided", http.StatusBadRequest)
return
}
_store := store.Get()
_store := wire.Get()
if err := _store.Repair().ProcessJob(id); err != nil {
wb.logger.Error().Err(err).Msg("Failed to process repair job")
}
@@ -361,7 +361,7 @@ func (wb *Web) handleDeleteRepairJob(w http.ResponseWriter, r *http.Request) {
return
}
_store := store.Get()
_store := wire.Get()
_store.Repair().DeleteJobs(req.IDs)
w.WriteHeader(http.StatusOK)
}
@@ -372,7 +372,7 @@ func (wb *Web) handleStopRepairJob(w http.ResponseWriter, r *http.Request) {
http.Error(w, "No job ID provided", http.StatusBadRequest)
return
}
_store := store.Get()
_store := wire.Get()
if err := _store.Repair().StopJob(id); err != nil {
wb.logger.Error().Err(err).Msg("Failed to stop repair job")
http.Error(w, "Failed to stop job: "+err.Error(), http.StatusInternalServerError)

File diff suppressed because one or more lines are too long

View File

@@ -149,10 +149,11 @@ class ConfigManager {
if (!rcloneConfig) return;
const fields = [
'enabled', 'mount_path', 'cache_dir', 'vfs_cache_mode', 'vfs_cache_max_size', 'vfs_cache_max_age',
'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',
'uid', 'gid', 'vfs_read_ahead', 'attr_timeout', 'dir_cache_time', 'poll_interval', 'umask',
'no_modtime', 'no_checksum', 'log_level'
'no_modtime', 'no_checksum', 'log_level', 'vfs_cache_min_free_space', 'vfs_fast_fingerprint', 'vfs_read_chunk_streams',
'async_read', 'use_mmap'
];
fields.forEach(field => {
@@ -273,7 +274,6 @@ class ConfigManager {
<div class="form-control flex-1">
<label class="label" for="debrid[${index}].download_api_keys">
<span class="label-text font-medium">Download API Keys</span>
<span class="badge badge-ghost badge-sm">Optional</span>
</label>
<div class="password-toggle-container">
<textarea class="textarea textarea-bordered has-toggle font-mono h-full min-h-[200px]"
@@ -290,7 +290,7 @@ class ConfigManager {
</div>
</div>
<div class="space-y-4">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div class="form-control">
<label class="label" for="debrid[${index}].folder">
<span class="label-text font-medium">Mount/Rclone Folder</span>
@@ -302,17 +302,6 @@ class ConfigManager {
<span class="label-text-alt">Path where debrid files are mounted</span>
</div>
</div>
<div class="form-control">
<label class="label" for="debrid[${index}].rate_limit">
<span class="label-text font-medium">Rate Limit</span>
</label>
<input type="text" class="input input-bordered"
name="debrid[${index}].rate_limit" id="debrid[${index}].rate_limit"
placeholder="250/minute" value="250/minute">
<div class="label">
<span class="label-text-alt">API rate limit for this service</span>
</div>
</div>
<div class="form-control">
<label class="label" for="debrid[${index}].rclone_mount_path">
<span class="label-text font-medium">Custom Rclone Mount Path</span>
@@ -324,8 +313,21 @@ class ConfigManager {
<div class="label">
<span class="label-text-alt">Custom mount path for this debrid service. If empty, uses global rclone mount path.</span>
</div>
</div>
</div>
</div>
<div class="grid grid-cols-2 lg:grid-cols-3 gap-3">
<div class="form-control">
<label class="label" for="debrid[${index}].rate_limit">
<span class="label-text font-medium">Rate Limit</span>
</label>
<input type="text" class="input input-bordered"
name="debrid[${index}].rate_limit" id="debrid[${index}].rate_limit"
placeholder="250/minute" value="250/minute">
<div class="label">
<span class="label-text-alt">API rate limit for this service</span>
</div>
</div>
<div class="form-control">
<label class="label" for="debrid[${index}].proxy">
<span class="label-text font-medium">Proxy</span>
@@ -337,6 +339,17 @@ class ConfigManager {
<span class="label-text-alt">This proxy is used for this debrid account</span>
</div>
</div>
<div class="form-control">
<label class="label" for="debrid[${index}].minimum_free_slot">
<span class="label-text font-medium">Minimum Free Slot</span>
</label>
<input type="number" class="input input-bordered"
name="debrid[${index}].minimum_free_slot" id="debrid[${index}].minimum_free_slot"
placeholder="1" value="1">
<div class="label">
<span class="label-text-alt">Minimum free slot for this debrid</span>
</div>
</div>
</div>
</div>
@@ -1102,6 +1115,7 @@ class ConfigManager {
api_key: document.querySelector(`[name="debrid[${i}].api_key"]`).value,
folder: document.querySelector(`[name="debrid[${i}].folder"]`).value,
rate_limit: document.querySelector(`[name="debrid[${i}].rate_limit"]`).value,
minimum_free_slot: parseInt(document.querySelector(`[name="debrid[${i}].minimum_free_slot"]`).value) || 0,
rclone_mount_path: document.querySelector(`[name="debrid[${i}].rclone_mount_path"]`).value,
proxy: document.querySelector(`[name="debrid[${i}].proxy"]`).value,
download_uncached: document.querySelector(`[name="debrid[${i}].download_uncached"]`).checked,
@@ -1231,15 +1245,22 @@ class ConfigManager {
return {
enabled: getElementValue('enabled', false),
rc_port: getElementValue('rc_port', "5572"),
mount_path: getElementValue('mount_path'),
buffer_size: getElementValue('buffer_size'),
cache_dir: getElementValue('cache_dir'),
transfers: getElementValue('transfers', 8),
vfs_cache_mode: getElementValue('vfs_cache_mode', 'off'),
vfs_cache_max_age: getElementValue('vfs_cache_max_age', '1h'),
vfs_cache_max_size: getElementValue('vfs_cache_max_size'),
vfs_cache_poll_interval: getElementValue('vfs_cache_poll_interval', '1m'),
vfs_read_chunk_size: getElementValue('vfs_read_chunk_size', '128M'),
vfs_read_chunk_size_limit: getElementValue('vfs_read_chunk_size_limit', 'off'),
vfs_cache_min_free_space: getElementValue('vfs_cache_min_free_space', ''),
vfs_fast_fingerprint: getElementValue('vfs_fast_fingerprint', false),
vfs_read_chunk_streams: getElementValue('vfs_read_chunk_streams', 0),
use_mmap: getElementValue('use_mmap', false),
async_read: getElementValue('async_read', true),
uid: getElementValue('uid', 0),
gid: getElementValue('gid', 0),
umask: getElementValue('umask', ''),

View File

@@ -466,6 +466,13 @@
</div>
</div>
<div class="form-control">
<label class="label" for="rclone.rc_port">
<span class="label-text font-medium">RC Port</span>
</label>
<input type="text" class="input input-bordered" name="rclone.rc_port" id="rclone.rc_port">
</div>
<div class="form-control">
<label class="label" for="rclone.log_level">
<span class="label-text font-medium">Log Level</span>
@@ -524,6 +531,15 @@
<span class="label-text-alt">How long the kernel caches the attributes (size, modification time, etc.)</span>
</div>
</div>
<div class="form-control">
<label class="label" for="rclone.transfers">
<span class="label-text font-medium">Transfers</span>
</label>
<input type="number" class="input input-bordered" name="rclone.transfers" id="rclone.transfers" placeholder="8" min="1">
<div class="label">
<span class="label-text-alt">Number of file transfers to run in parallel</span>
</div>
</div>
</div>
</div>
</div>
@@ -627,6 +643,36 @@
<span class="label-text-alt">How often VFS cache dir gets cleaned</span>
</div>
</div>
<div class="form-control">
<label class="label" for="rclone.vfs_cache_min_free_space">
<span class="label-text font-medium">VFS Cache Min Free Space</span>
</label>
<input type="text" class="input input-bordered" name="rclone.vfs_cache_min_free_space" id="rclone.vfs_cache_min_free_space" placeholder="1G">
<div class="label">
<span class="label-text-alt">Target minimum free space on the disk containing the cache</span>
</div>
</div>
<div class="form-control">
<label class="label" for="rclone.vfs_disk_space_total">
<span class="label-text font-medium">VFS Disk Space Total</span>
</label>
<input type="text" class="input input-bordered" name="rclone.vfs_disk_space_total" id="rclone.vfs_disk_space_total" placeholder="1G">
<div class="label">
<span class="label-text-alt">Specify the total space of disk</span>
</div>
</div>
<div class="form-control">
<label class="label" for="rclone.vfs_read_chunk_streams">
<span class="label-text font-medium">VFS Read Chunk Streams</span>
</label>
<input type="number" class="input input-bordered" name="rclone.vfs_read_chunk_streams" id="rclone.vfs_read_chunk_streams" placeholder="4" min="0">
<div class="label">
<span class="label-text-alt">The number of parallel streams to read at once</span>
</div>
</div>
</div>
</div>
</div>
@@ -636,7 +682,7 @@
<h3 class="text-lg font-semibold mb-4 flex items-center">
<i class="bi bi-gear mr-2"></i>Advanced Settings
</h3>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div class="grid grid-cols-2 lg:grid-cols-3 gap-4">
<div class="form-control">
<label class="label cursor-pointer justify-start gap-3">
<input type="checkbox" class="checkbox" name="rclone.no_modtime" id="rclone.no_modtime">
@@ -656,6 +702,36 @@
</div>
</label>
</div>
<div class="form-control">
<label class="label cursor-pointer justify-start gap-3" for="rclone.async_read">
<input type="checkbox" class="checkbox" name="rclone.async_read" id="rclone.async_read">
<div>
<span class="label-text font-medium">Async Read</span>
<div class="label-text-alt">Use asynchronous reads</div>
</div>
</label>
</div>
<div class="form-control">
<label class="label cursor-pointer justify-start gap-3" for="rclone.vfs_fast_fingerprint">
<input type="checkbox" class="checkbox" name="rclone.vfs_fast_fingerprint" id="rclone.vfs_fast_fingerprint">
<div>
<span class="label-text font-medium">VFS Fast Fingerprint</span>
<div class="label-text-alt">Use fast (less accurate) fingerprints for change detection</div>
</div>
</label>
</div>
<div class="form-control">
<label class="label cursor-pointer justify-start gap-3" for="rclone.use_mmap">
<input type="checkbox" class="checkbox" name="rclone.use_mmap" id="rclone.use_mmap">
<div>
<span class="label-text font-medium">Use Mmap</span>
<div class="label-text-alt">Use fast (less accurate) fingerprints for change detection</div>
</div>
</label>
</div>
</div>
</div>
</div>

View File

@@ -6,7 +6,7 @@ import (
"github.com/gorilla/sessions"
"github.com/rs/zerolog"
"github.com/sirrobot01/decypharr/internal/logger"
"github.com/sirrobot01/decypharr/pkg/store"
"github.com/sirrobot01/decypharr/pkg/wire"
"html/template"
"os"
)
@@ -60,7 +60,7 @@ type Web struct {
logger zerolog.Logger
cookie *sessions.CookieStore
templates *template.Template
torrents *store.TorrentStorage
torrents *wire.TorrentStorage
}
func New() *Web {
@@ -86,6 +86,6 @@ func New() *Web {
logger: logger.New("ui"),
templates: templates,
cookie: cookieStore,
torrents: store.Get().Torrents(),
torrents: wire.Get().Torrents(),
}
}

View File

@@ -7,7 +7,7 @@ import (
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/sirrobot01/decypharr/internal/config"
"github.com/sirrobot01/decypharr/pkg/store"
"github.com/sirrobot01/decypharr/pkg/wire"
"html/template"
"net/http"
"net/url"
@@ -97,7 +97,7 @@ func New() *WebDav {
Handlers: make([]*Handler, 0),
URLBase: urlBase,
}
for name, c := range store.Get().Debrid().Caches() {
for name, c := range wire.Get().Debrid().Caches() {
h := NewHandler(name, urlBase, c, c.Logger())
w.Handlers = append(w.Handlers, h)
}

View File

@@ -1,4 +1,4 @@
package store
package wire
import (
"fmt"
@@ -310,7 +310,6 @@ func (s *Store) getTorrentPath(rclonePath string, debridTorrent *types.Torrent)
for {
torrentPath, err := debridTorrent.GetMountFolder(rclonePath)
if err == nil {
s.logger.Debug().Msgf("Found torrent path: %s", torrentPath)
return torrentPath, err
}
time.Sleep(100 * time.Millisecond)

View File

@@ -1,4 +1,4 @@
package store
package wire
import (
"os"

View File

@@ -1,4 +1,4 @@
package store
package wire
import (
"context"
@@ -29,7 +29,6 @@ func (s *Store) addToQueue(importReq *ImportRequest) error {
func (s *Store) StartQueueWorkers(ctx context.Context) error {
// This function is responsible for starting the scheduled tasks
if ctx == nil {
ctx = context.Background()
}
@@ -45,7 +44,7 @@ func (s *Store) StartQueueWorkers(ctx context.Context) error {
}), gocron.WithContext(ctx)); err != nil {
s.logger.Error().Err(err).Msg("Failed to create slots tracking job")
} else {
s.logger.Trace().Msgf("Download link refresh job scheduled for every %s", "30s")
s.logger.Trace().Msgf("Slots tracking job scheduled for every %s", "30s")
}
}
@@ -86,13 +85,17 @@ func (s *Store) trackAvailableSlots(ctx context.Context) {
availableSlots[name] = slots
}
if len(availableSlots) == 0 {
s.logger.Debug().Msg("No debrid clients available or no slots found")
return // No debrid clients or slots available, nothing to process
}
if s.importsQueue.Size() <= 0 {
// Queue is empty, no need to process
return
}
for name, slots := range availableSlots {
s.logger.Debug().Msgf("Available slots for %s: %d", name, slots)
// If slots are available, process the next import request from the queue
for slots > 0 {

View File

@@ -1,4 +1,4 @@
package store
package wire
import (
"bytes"

View File

@@ -1,4 +1,4 @@
package store
package wire
import (
"cmp"
@@ -86,7 +86,10 @@ func Reset() {
}
if instance.rcloneManager != nil {
instance.rcloneManager.Stop()
err := instance.rcloneManager.Stop()
if err != nil {
instance.logger.Error().Err(err).Msg("Failed to stop rclone manager")
}
}
if instance.importsQueue != nil {

View File

@@ -1,4 +1,4 @@
package store
package wire
import (
"cmp"

View File

@@ -1,4 +1,4 @@
package store
package wire
import (
"encoding/json"

View File

@@ -1,4 +1,4 @@
package store
package wire
import (
"fmt"

View File

@@ -1,4 +1,4 @@
package store
package wire
import "context"