Add more rclone flags, fix minor issues
This commit is contained in:
@@ -7,10 +7,10 @@ import (
|
|||||||
"github.com/sirrobot01/decypharr/internal/logger"
|
"github.com/sirrobot01/decypharr/internal/logger"
|
||||||
"github.com/sirrobot01/decypharr/pkg/qbit"
|
"github.com/sirrobot01/decypharr/pkg/qbit"
|
||||||
"github.com/sirrobot01/decypharr/pkg/server"
|
"github.com/sirrobot01/decypharr/pkg/server"
|
||||||
"github.com/sirrobot01/decypharr/pkg/store"
|
|
||||||
"github.com/sirrobot01/decypharr/pkg/version"
|
"github.com/sirrobot01/decypharr/pkg/version"
|
||||||
"github.com/sirrobot01/decypharr/pkg/web"
|
"github.com/sirrobot01/decypharr/pkg/web"
|
||||||
"github.com/sirrobot01/decypharr/pkg/webdav"
|
"github.com/sirrobot01/decypharr/pkg/webdav"
|
||||||
|
"github.com/sirrobot01/decypharr/pkg/wire"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
@@ -77,7 +77,7 @@ func Start(ctx context.Context) error {
|
|||||||
reset := func() {
|
reset := func() {
|
||||||
// Reset the store and services
|
// Reset the store and services
|
||||||
qb.Reset()
|
qb.Reset()
|
||||||
store.Reset()
|
wire.Reset()
|
||||||
// refresh GC
|
// refresh GC
|
||||||
runtime.GC()
|
runtime.GC()
|
||||||
}
|
}
|
||||||
@@ -151,7 +151,7 @@ func startServices(ctx context.Context, cancelSvc context.CancelFunc, wd *webdav
|
|||||||
|
|
||||||
// Start rclone RC server if enabled
|
// Start rclone RC server if enabled
|
||||||
safeGo(func() error {
|
safeGo(func() error {
|
||||||
rcManager := store.Get().RcloneManager()
|
rcManager := wire.Get().RcloneManager()
|
||||||
if rcManager == nil {
|
if rcManager == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -160,7 +160,7 @@ func startServices(ctx context.Context, cancelSvc context.CancelFunc, wd *webdav
|
|||||||
|
|
||||||
if cfg := config.Get(); cfg.Repair.Enabled {
|
if cfg := config.Get(); cfg.Repair.Enabled {
|
||||||
safeGo(func() error {
|
safeGo(func() error {
|
||||||
repair := store.Get().Repair()
|
repair := wire.Get().Repair()
|
||||||
if repair != nil {
|
if repair != nil {
|
||||||
if err := repair.Start(ctx); err != nil {
|
if err := repair.Start(ctx); err != nil {
|
||||||
_log.Error().Err(err).Msg("repair failed")
|
_log.Error().Err(err).Msg("repair failed")
|
||||||
@@ -171,7 +171,7 @@ func startServices(ctx context.Context, cancelSvc context.CancelFunc, wd *webdav
|
|||||||
}
|
}
|
||||||
|
|
||||||
safeGo(func() error {
|
safeGo(func() error {
|
||||||
store.Get().StartWorkers(ctx)
|
wire.Get().StartWorkers(ctx)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ type Rclone struct {
|
|||||||
// Global mount folder where all providers will be mounted as subfolders
|
// Global mount folder where all providers will be mounted as subfolders
|
||||||
Enabled bool `json:"enabled,omitempty"`
|
Enabled bool `json:"enabled,omitempty"`
|
||||||
MountPath string `json:"mount_path,omitempty"`
|
MountPath string `json:"mount_path,omitempty"`
|
||||||
|
RcPort string `json:"rc_port,omitempty"`
|
||||||
|
|
||||||
// Cache settings
|
// Cache settings
|
||||||
CacheDir string `json:"cache_dir,omitempty"`
|
CacheDir string `json:"cache_dir,omitempty"`
|
||||||
@@ -98,6 +99,7 @@ type Rclone struct {
|
|||||||
// VFS settings
|
// VFS settings
|
||||||
VfsCacheMode string `json:"vfs_cache_mode,omitempty"` // off, minimal, writes, full
|
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)
|
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)
|
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)
|
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)
|
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)
|
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)
|
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
|
// File system settings
|
||||||
UID uint32 `json:"uid,omitempty"` // User ID for mounted files
|
UID uint32 `json:"uid,omitempty"` // User ID for mounted files
|
||||||
GID uint32 `json:"gid,omitempty"` // Group ID for mounted files
|
GID uint32 `json:"gid,omitempty"` // Group ID for mounted files
|
||||||
@@ -417,6 +426,11 @@ func (c *Config) setDefaults() {
|
|||||||
|
|
||||||
// Rclone defaults
|
// Rclone defaults
|
||||||
if c.Rclone.Enabled {
|
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")
|
c.Rclone.VfsCacheMode = cmp.Or(c.Rclone.VfsCacheMode, "off")
|
||||||
if c.Rclone.UID == 0 {
|
if c.Rclone.UID == 0 {
|
||||||
c.Rclone.UID = uint32(os.Getuid())
|
c.Rclone.UID = uint32(os.Getuid())
|
||||||
@@ -429,6 +443,9 @@ func (c *Config) setDefaults() {
|
|||||||
c.Rclone.GID = uint32(os.Getgid())
|
c.Rclone.GID = uint32(os.Getgid())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if c.Rclone.Transfers == 0 {
|
||||||
|
c.Rclone.Transfers = 4 // Default number of transfers
|
||||||
|
}
|
||||||
if c.Rclone.VfsCacheMode != "off" {
|
if c.Rclone.VfsCacheMode != "off" {
|
||||||
c.Rclone.VfsCachePollInterval = cmp.Or(c.Rclone.VfsCachePollInterval, "1m") // Clean cache every minute
|
c.Rclone.VfsCachePollInterval = cmp.Or(c.Rclone.VfsCachePollInterval, "1m") // Clean cache every minute
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -958,8 +958,7 @@ func (r *RealDebrid) GetProfile() (*types.Profile, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *RealDebrid) GetAvailableSlots() (int, error) {
|
func (r *RealDebrid) GetAvailableSlots() (int, error) {
|
||||||
url := fmt.Sprintf("%s/torrents/activeCount", r.Host)
|
req, _ := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/torrents/activeCount", r.Host), nil)
|
||||||
req, _ := http.NewRequest(http.MethodGet, url, nil)
|
|
||||||
resp, err := r.client.MakeRequest(req)
|
resp, err := r.client.MakeRequest(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, nil
|
return 0, nil
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/sirrobot01/decypharr/internal/config"
|
"github.com/sirrobot01/decypharr/internal/config"
|
||||||
"github.com/sirrobot01/decypharr/pkg/arr"
|
"github.com/sirrobot01/decypharr/pkg/arr"
|
||||||
"github.com/sirrobot01/decypharr/pkg/store"
|
"github.com/sirrobot01/decypharr/pkg/wire"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -130,7 +130,7 @@ func (q *QBit) authContext(next http.Handler) http.Handler {
|
|||||||
cfg := config.Get()
|
cfg := config.Get()
|
||||||
host, token, err := decodeAuthHeader(r.Header.Get("Authorization"))
|
host, token, err := decodeAuthHeader(r.Header.Get("Authorization"))
|
||||||
category := getCategory(r.Context())
|
category := getCategory(r.Context())
|
||||||
arrs := store.Get().Arr()
|
arrs := wire.Get().Arr()
|
||||||
// Check if arr exists
|
// Check if arr exists
|
||||||
a := arrs.Get(category)
|
a := arrs.Get(category)
|
||||||
if a == nil {
|
if a == nil {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/sirrobot01/decypharr/internal/config"
|
"github.com/sirrobot01/decypharr/internal/config"
|
||||||
"github.com/sirrobot01/decypharr/internal/logger"
|
"github.com/sirrobot01/decypharr/internal/logger"
|
||||||
"github.com/sirrobot01/decypharr/pkg/store"
|
"github.com/sirrobot01/decypharr/pkg/wire"
|
||||||
)
|
)
|
||||||
|
|
||||||
type QBit struct {
|
type QBit struct {
|
||||||
@@ -12,7 +12,7 @@ type QBit struct {
|
|||||||
Password string
|
Password string
|
||||||
DownloadFolder string
|
DownloadFolder string
|
||||||
Categories []string
|
Categories []string
|
||||||
storage *store.TorrentStorage
|
storage *wire.TorrentStorage
|
||||||
logger zerolog.Logger
|
logger zerolog.Logger
|
||||||
Tags []string
|
Tags []string
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,7 @@ func New() *QBit {
|
|||||||
Password: cfg.Password,
|
Password: cfg.Password,
|
||||||
DownloadFolder: cfg.DownloadFolder,
|
DownloadFolder: cfg.DownloadFolder,
|
||||||
Categories: cfg.Categories,
|
Categories: cfg.Categories,
|
||||||
storage: store.Get().Torrents(),
|
storage: wire.Get().Torrents(),
|
||||||
logger: logger.New("qbit"),
|
logger: logger.New("qbit"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"github.com/sirrobot01/decypharr/internal/utils"
|
"github.com/sirrobot01/decypharr/internal/utils"
|
||||||
"github.com/sirrobot01/decypharr/pkg/arr"
|
"github.com/sirrobot01/decypharr/pkg/arr"
|
||||||
"github.com/sirrobot01/decypharr/pkg/store"
|
"github.com/sirrobot01/decypharr/pkg/wire"
|
||||||
"io"
|
"io"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -18,9 +18,9 @@ func (q *QBit) addMagnet(ctx context.Context, url string, arr *arr.Arr, debrid s
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error parsing magnet link: %w", err)
|
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)
|
err = _store.AddTorrent(ctx, importReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -37,8 +37,8 @@ func (q *QBit) addTorrent(ctx context.Context, fileHeader *multipart.FileHeader,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error reading file: %s \n %w", fileHeader.Filename, err)
|
return fmt.Errorf("error reading file: %s \n %w", fileHeader.Filename, 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)
|
err = _store.AddTorrent(ctx, importReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to process torrent: %w", err)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *QBit) ResumeTorrent(t *store.Torrent) bool {
|
func (q *QBit) ResumeTorrent(t *wire.Torrent) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *QBit) PauseTorrent(t *store.Torrent) bool {
|
func (q *QBit) PauseTorrent(t *wire.Torrent) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *QBit) RefreshTorrent(t *store.Torrent) bool {
|
func (q *QBit) RefreshTorrent(t *wire.Torrent) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *QBit) GetTorrentProperties(t *store.Torrent) *TorrentProperties {
|
func (q *QBit) GetTorrentProperties(t *wire.Torrent) *TorrentProperties {
|
||||||
return &TorrentProperties{
|
return &TorrentProperties{
|
||||||
AdditionDate: t.AddedOn,
|
AdditionDate: t.AddedOn,
|
||||||
Comment: "Debrid Blackhole <https://github.com/sirrobot01/decypharr>",
|
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, ",")
|
torrentTags := strings.Split(t.Tags, ",")
|
||||||
for _, tag := range tags {
|
for _, tag := range tags {
|
||||||
if tag == "" {
|
if tag == "" {
|
||||||
@@ -101,7 +101,7 @@ func (q *QBit) setTorrentTags(t *store.Torrent, tags []string) bool {
|
|||||||
return true
|
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, ",")
|
torrentTags := strings.Split(t.Tags, ",")
|
||||||
newTorrentTags := utils.RemoveItem(torrentTags, tags...)
|
newTorrentTags := utils.RemoveItem(torrentTags, tags...)
|
||||||
q.Tags = utils.RemoveItem(q.Tags, tags...)
|
q.Tags = utils.RemoveItem(q.Tags, tags...)
|
||||||
|
|||||||
@@ -4,19 +4,18 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sirrobot01/decypharr/internal/config"
|
"github.com/sirrobot01/decypharr/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Mount creates a mount using the rclone RC API with retry logic
|
// Mount creates a mount using the rclone RC API with retry logic
|
||||||
func (m *Manager) Mount(provider, webdavURL string) error {
|
func (m *Manager) Mount(mountPath, provider, webdavURL string) error {
|
||||||
return m.mountWithRetry(provider, webdavURL, 3)
|
return m.mountWithRetry(mountPath, provider, webdavURL, 3)
|
||||||
}
|
}
|
||||||
|
|
||||||
// mountWithRetry attempts to mount with retry logic
|
// 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 !m.IsReady() {
|
||||||
if err := m.WaitForReady(30 * time.Second); err != nil {
|
if err := m.WaitForReady(30 * time.Second); err != nil {
|
||||||
return fmt.Errorf("rclone RC server not ready: %w", err)
|
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)
|
time.Sleep(wait)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.performMount(provider, webdavURL); err != nil {
|
if err := m.performMount(mountPath, provider, webdavURL); err != nil {
|
||||||
m.logger.Error().
|
m.logger.Error().
|
||||||
Err(err).
|
Err(err).
|
||||||
Str("provider", provider).
|
Str("provider", provider).
|
||||||
@@ -49,9 +48,8 @@ func (m *Manager) mountWithRetry(provider, webdavURL string, maxRetries int) err
|
|||||||
}
|
}
|
||||||
|
|
||||||
// performMount performs a single mount attempt
|
// 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()
|
cfg := config.Get()
|
||||||
mountPath := filepath.Join(cfg.Rclone.MountPath, provider)
|
|
||||||
|
|
||||||
// Create mount directory
|
// Create mount directory
|
||||||
if err := os.MkdirAll(mountPath, 0755); err != nil {
|
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),
|
"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{})
|
configOpts := make(map[string]interface{})
|
||||||
|
|
||||||
if cfg.Rclone.BufferSize != "" {
|
if cfg.Rclone.BufferSize != "" {
|
||||||
@@ -127,6 +137,19 @@ func (m *Manager) performMount(provider, webdavURL string) error {
|
|||||||
if cfg.Rclone.VfsReadAhead != "" {
|
if cfg.Rclone.VfsReadAhead != "" {
|
||||||
vfsOpt["ReadAhead"] = 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 {
|
if cfg.Rclone.NoChecksum {
|
||||||
vfsOpt["NoChecksum"] = cfg.Rclone.NoChecksum
|
vfsOpt["NoChecksum"] = cfg.Rclone.NoChecksum
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ func (m *Manager) RecoverMount(provider string) error {
|
|||||||
time.Sleep(1 * time.Second)
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
// Try to remount
|
// 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)
|
return fmt.Errorf("failed to recover mount for %s: %w", provider, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -261,50 +263,35 @@ func (m *Manager) Stop() error {
|
|||||||
case err := <-done:
|
case err := <-done:
|
||||||
if err != nil && !errors.Is(err, context.Canceled) && !WasHardTerminated(err) {
|
if err != nil && !errors.Is(err, context.Canceled) && !WasHardTerminated(err) {
|
||||||
m.logger.Warn().Err(err).Msg("Rclone process exited with error")
|
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")
|
m.logger.Warn().Msg("Timeout waiting for rclone to exit, force killing")
|
||||||
if err := m.cmd.Process.Kill(); err != nil {
|
if err := m.cmd.Process.Kill(); err != nil {
|
||||||
m.logger.Error().Err(err).Msg("Failed to force kill rclone process")
|
// Check if the process already finished
|
||||||
return err
|
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 {
|
select {
|
||||||
case <-done:
|
case <-done:
|
||||||
m.logger.Info().Msg("Rclone process killed successfully")
|
m.logger.Info().Msg("Rclone process cleanup completed")
|
||||||
case <-time.After(5 * time.Second):
|
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.serverStarted = false
|
||||||
m.logger.Info().Msg("Rclone RC server stopped")
|
m.logger.Info().Msg("Rclone RC server stopped")
|
||||||
return nil
|
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
|
// waitForServer waits for the RC server to become available
|
||||||
func (m *Manager) waitForServer() {
|
func (m *Manager) waitForServer() {
|
||||||
maxAttempts := 30
|
maxAttempts := 30
|
||||||
@@ -352,7 +339,12 @@ func (m *Manager) makeRequest(req RCRequest, close bool) (*http.Response, error)
|
|||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
// Read the response body to get more details
|
// 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
|
var errorResp RCResponse
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&errorResp); err != nil {
|
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)
|
return nil, fmt.Errorf("request failed with status %s, but could not decode error response: %w", resp.Status, err)
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ func (m *Mount) Mount(ctx context.Context) error {
|
|||||||
Str("mount_path", m.LocalPath).
|
Str("mount_path", m.LocalPath).
|
||||||
Msg("Creating mount via RC")
|
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")
|
m.logger.Error().Str("provider", m.Provider).Msg("Mount operation failed")
|
||||||
return fmt.Errorf("mount failed for %s", m.Provider)
|
return fmt.Errorf("mount failed for %s", m.Provider)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ import (
|
|||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/sirrobot01/decypharr/internal/request"
|
"github.com/sirrobot01/decypharr/internal/request"
|
||||||
debridTypes "github.com/sirrobot01/decypharr/pkg/debrid/types"
|
debridTypes "github.com/sirrobot01/decypharr/pkg/debrid/types"
|
||||||
"github.com/sirrobot01/decypharr/pkg/store"
|
"github.com/sirrobot01/decypharr/pkg/wire"
|
||||||
"net/http"
|
"net/http"
|
||||||
"runtime"
|
"runtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Server) handleIngests(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleIngests(w http.ResponseWriter, r *http.Request) {
|
||||||
ingests := make([]debridTypes.IngestData, 0)
|
ingests := make([]debridTypes.IngestData, 0)
|
||||||
_store := store.Get()
|
_store := wire.Get()
|
||||||
debrids := _store.Debrid()
|
debrids := _store.Debrid()
|
||||||
if debrids == nil {
|
if debrids == nil {
|
||||||
http.Error(w, "Debrid service is not enabled", http.StatusInternalServerError)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_store := store.Get()
|
_store := wire.Get()
|
||||||
debrids := _store.Debrid()
|
debrids := _store.Debrid()
|
||||||
|
|
||||||
if debrids == nil {
|
if debrids == nil {
|
||||||
@@ -92,7 +92,7 @@ func (s *Server) handleStats(w http.ResponseWriter, r *http.Request) {
|
|||||||
"go_version": runtime.Version(),
|
"go_version": runtime.Version(),
|
||||||
}
|
}
|
||||||
|
|
||||||
debrids := store.Get().Debrid()
|
debrids := wire.Get().Debrid()
|
||||||
if debrids == nil {
|
if debrids == nil {
|
||||||
request.JSONResponse(w, stats, http.StatusOK)
|
request.JSONResponse(w, stats, http.StatusOK)
|
||||||
return
|
return
|
||||||
@@ -153,7 +153,7 @@ func (s *Server) handleStats(w http.ResponseWriter, r *http.Request) {
|
|||||||
stats["debrids"] = debridStats
|
stats["debrids"] = debridStats
|
||||||
|
|
||||||
// Add rclone stats if available
|
// 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()
|
rcStats, err := rcManager.GetStats()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error().Err(err).Msg("Failed to get rclone stats")
|
s.logger.Error().Err(err).Msg("Failed to get rclone stats")
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package server
|
|||||||
import (
|
import (
|
||||||
"cmp"
|
"cmp"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/sirrobot01/decypharr/pkg/store"
|
"github.com/sirrobot01/decypharr/pkg/wire"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ func (s *Server) handleTautulli(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, "Invalid ID", http.StatusBadRequest)
|
http.Error(w, "Invalid ID", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
repair := store.Get().Repair()
|
repair := wire.Get().Repair()
|
||||||
|
|
||||||
mediaId := cmp.Or(payload.TmdbID, payload.TvdbID)
|
mediaId := cmp.Or(payload.TmdbID, payload.TvdbID)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package web
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/sirrobot01/decypharr/pkg/store"
|
"github.com/sirrobot01/decypharr/pkg/wire"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -18,7 +18,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (wb *Web) handleGetArrs(w http.ResponseWriter, r *http.Request) {
|
func (wb *Web) handleGetArrs(w http.ResponseWriter, r *http.Request) {
|
||||||
_store := store.Get()
|
_store := wire.Get()
|
||||||
request.JSONResponse(w, _store.Arr().GetAll(), http.StatusOK)
|
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)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_store := store.Get()
|
_store := wire.Get()
|
||||||
|
|
||||||
results := make([]*store.ImportRequest, 0)
|
results := make([]*wire.ImportRequest, 0)
|
||||||
errs := make([]string, 0)
|
errs := make([]string, 0)
|
||||||
|
|
||||||
arrName := r.FormValue("arr")
|
arrName := r.FormValue("arr")
|
||||||
@@ -66,7 +66,7 @@ func (wb *Web) handleAddContent(w http.ResponseWriter, r *http.Request) {
|
|||||||
continue
|
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 {
|
if err := _store.AddTorrent(ctx, importReq); err != nil {
|
||||||
wb.logger.Error().Err(err).Str("url", url).Msg("Failed to add torrent")
|
wb.logger.Error().Err(err).Str("url", url).Msg("Failed to add torrent")
|
||||||
errs = append(errs, fmt.Sprintf("URL %s: %v", url, err))
|
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
|
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)
|
err = _store.AddTorrent(ctx, importReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
wb.logger.Error().Err(err).Str("file", fileHeader.Filename).Msg("Failed to add torrent")
|
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 {
|
request.JSONResponse(w, struct {
|
||||||
Results []*store.ImportRequest `json:"results"`
|
Results []*wire.ImportRequest `json:"results"`
|
||||||
Errors []string `json:"errors,omitempty"`
|
Errors []string `json:"errors,omitempty"`
|
||||||
}{
|
}{
|
||||||
Results: results,
|
Results: results,
|
||||||
Errors: errs,
|
Errors: errs,
|
||||||
@@ -118,7 +118,7 @@ func (wb *Web) handleRepairMedia(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_store := store.Get()
|
_store := wire.Get()
|
||||||
|
|
||||||
var arrs []string
|
var arrs []string
|
||||||
|
|
||||||
@@ -186,7 +186,7 @@ func (wb *Web) handleGetConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
// Merge config arrs, with arr Storage
|
// Merge config arrs, with arr Storage
|
||||||
unique := map[string]config.Arr{}
|
unique := map[string]config.Arr{}
|
||||||
cfg := config.Get()
|
cfg := config.Get()
|
||||||
arrStorage := store.Get().Arr()
|
arrStorage := wire.Get().Arr()
|
||||||
|
|
||||||
// Add existing Arrs from storage
|
// Add existing Arrs from storage
|
||||||
for _, a := range arrStorage.GetAll() {
|
for _, a := range arrStorage.GetAll() {
|
||||||
@@ -276,7 +276,7 @@ func (wb *Web) handleUpdateConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update Arrs through the service
|
// Update Arrs through the service
|
||||||
storage := store.Get()
|
storage := wire.Get()
|
||||||
arrStorage := storage.Arr()
|
arrStorage := storage.Arr()
|
||||||
|
|
||||||
newConfigArrs := make([]config.Arr, 0)
|
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) {
|
func (wb *Web) handleGetRepairJobs(w http.ResponseWriter, r *http.Request) {
|
||||||
_store := store.Get()
|
_store := wire.Get()
|
||||||
request.JSONResponse(w, _store.Repair().GetJobs(), http.StatusOK)
|
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)
|
http.Error(w, "No job ID provided", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_store := store.Get()
|
_store := wire.Get()
|
||||||
if err := _store.Repair().ProcessJob(id); err != nil {
|
if err := _store.Repair().ProcessJob(id); err != nil {
|
||||||
wb.logger.Error().Err(err).Msg("Failed to process repair job")
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_store := store.Get()
|
_store := wire.Get()
|
||||||
_store.Repair().DeleteJobs(req.IDs)
|
_store.Repair().DeleteJobs(req.IDs)
|
||||||
w.WriteHeader(http.StatusOK)
|
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)
|
http.Error(w, "No job ID provided", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_store := store.Get()
|
_store := wire.Get()
|
||||||
if err := _store.Repair().StopJob(id); err != nil {
|
if err := _store.Repair().StopJob(id); err != nil {
|
||||||
wb.logger.Error().Err(err).Msg("Failed to stop repair job")
|
wb.logger.Error().Err(err).Msg("Failed to stop repair job")
|
||||||
http.Error(w, "Failed to stop job: "+err.Error(), http.StatusInternalServerError)
|
http.Error(w, "Failed to stop job: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -149,10 +149,11 @@ class ConfigManager {
|
|||||||
if (!rcloneConfig) return;
|
if (!rcloneConfig) return;
|
||||||
|
|
||||||
const fields = [
|
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',
|
'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',
|
'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 => {
|
fields.forEach(field => {
|
||||||
@@ -273,7 +274,6 @@ class ConfigManager {
|
|||||||
<div class="form-control flex-1">
|
<div class="form-control flex-1">
|
||||||
<label class="label" for="debrid[${index}].download_api_keys">
|
<label class="label" for="debrid[${index}].download_api_keys">
|
||||||
<span class="label-text font-medium">Download API Keys</span>
|
<span class="label-text font-medium">Download API Keys</span>
|
||||||
<span class="badge badge-ghost badge-sm">Optional</span>
|
|
||||||
</label>
|
</label>
|
||||||
<div class="password-toggle-container">
|
<div class="password-toggle-container">
|
||||||
<textarea class="textarea textarea-bordered has-toggle font-mono h-full min-h-[200px]"
|
<textarea class="textarea textarea-bordered has-toggle font-mono h-full min-h-[200px]"
|
||||||
@@ -290,7 +290,7 @@ class ConfigManager {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-4">
|
<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">
|
<div class="form-control">
|
||||||
<label class="label" for="debrid[${index}].folder">
|
<label class="label" for="debrid[${index}].folder">
|
||||||
<span class="label-text font-medium">Mount/Rclone Folder</span>
|
<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>
|
<span class="label-text-alt">Path where debrid files are mounted</span>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="form-control">
|
||||||
<label class="label" for="debrid[${index}].rclone_mount_path">
|
<label class="label" for="debrid[${index}].rclone_mount_path">
|
||||||
<span class="label-text font-medium">Custom Rclone Mount Path</span>
|
<span class="label-text font-medium">Custom Rclone Mount Path</span>
|
||||||
@@ -324,8 +313,21 @@ class ConfigManager {
|
|||||||
<div class="label">
|
<div class="label">
|
||||||
<span class="label-text-alt">Custom mount path for this debrid service. If empty, uses global rclone mount path.</span>
|
<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>
|
||||||
|
<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">
|
<div class="form-control">
|
||||||
<label class="label" for="debrid[${index}].proxy">
|
<label class="label" for="debrid[${index}].proxy">
|
||||||
<span class="label-text font-medium">Proxy</span>
|
<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>
|
<span class="label-text-alt">This proxy is used for this debrid account</span>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -1102,6 +1115,7 @@ class ConfigManager {
|
|||||||
api_key: document.querySelector(`[name="debrid[${i}].api_key"]`).value,
|
api_key: document.querySelector(`[name="debrid[${i}].api_key"]`).value,
|
||||||
folder: document.querySelector(`[name="debrid[${i}].folder"]`).value,
|
folder: document.querySelector(`[name="debrid[${i}].folder"]`).value,
|
||||||
rate_limit: document.querySelector(`[name="debrid[${i}].rate_limit"]`).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,
|
rclone_mount_path: document.querySelector(`[name="debrid[${i}].rclone_mount_path"]`).value,
|
||||||
proxy: document.querySelector(`[name="debrid[${i}].proxy"]`).value,
|
proxy: document.querySelector(`[name="debrid[${i}].proxy"]`).value,
|
||||||
download_uncached: document.querySelector(`[name="debrid[${i}].download_uncached"]`).checked,
|
download_uncached: document.querySelector(`[name="debrid[${i}].download_uncached"]`).checked,
|
||||||
@@ -1231,15 +1245,22 @@ class ConfigManager {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
enabled: getElementValue('enabled', false),
|
enabled: getElementValue('enabled', false),
|
||||||
|
rc_port: getElementValue('rc_port', "5572"),
|
||||||
mount_path: getElementValue('mount_path'),
|
mount_path: getElementValue('mount_path'),
|
||||||
buffer_size: getElementValue('buffer_size'),
|
buffer_size: getElementValue('buffer_size'),
|
||||||
cache_dir: getElementValue('cache_dir'),
|
cache_dir: getElementValue('cache_dir'),
|
||||||
|
transfers: getElementValue('transfers', 8),
|
||||||
vfs_cache_mode: getElementValue('vfs_cache_mode', 'off'),
|
vfs_cache_mode: getElementValue('vfs_cache_mode', 'off'),
|
||||||
vfs_cache_max_age: getElementValue('vfs_cache_max_age', '1h'),
|
vfs_cache_max_age: getElementValue('vfs_cache_max_age', '1h'),
|
||||||
vfs_cache_max_size: getElementValue('vfs_cache_max_size'),
|
vfs_cache_max_size: getElementValue('vfs_cache_max_size'),
|
||||||
vfs_cache_poll_interval: getElementValue('vfs_cache_poll_interval', '1m'),
|
vfs_cache_poll_interval: getElementValue('vfs_cache_poll_interval', '1m'),
|
||||||
vfs_read_chunk_size: getElementValue('vfs_read_chunk_size', '128M'),
|
vfs_read_chunk_size: getElementValue('vfs_read_chunk_size', '128M'),
|
||||||
vfs_read_chunk_size_limit: getElementValue('vfs_read_chunk_size_limit', 'off'),
|
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),
|
uid: getElementValue('uid', 0),
|
||||||
gid: getElementValue('gid', 0),
|
gid: getElementValue('gid', 0),
|
||||||
umask: getElementValue('umask', ''),
|
umask: getElementValue('umask', ''),
|
||||||
|
|||||||
@@ -466,6 +466,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="form-control">
|
||||||
<label class="label" for="rclone.log_level">
|
<label class="label" for="rclone.log_level">
|
||||||
<span class="label-text font-medium">Log Level</span>
|
<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>
|
<span class="label-text-alt">How long the kernel caches the attributes (size, modification time, etc.)</span>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -627,6 +643,36 @@
|
|||||||
<span class="label-text-alt">How often VFS cache dir gets cleaned</span>
|
<span class="label-text-alt">How often VFS cache dir gets cleaned</span>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -636,7 +682,7 @@
|
|||||||
<h3 class="text-lg font-semibold mb-4 flex items-center">
|
<h3 class="text-lg font-semibold mb-4 flex items-center">
|
||||||
<i class="bi bi-gear mr-2"></i>Advanced Settings
|
<i class="bi bi-gear mr-2"></i>Advanced Settings
|
||||||
</h3>
|
</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">
|
<div class="form-control">
|
||||||
<label class="label cursor-pointer justify-start gap-3">
|
<label class="label cursor-pointer justify-start gap-3">
|
||||||
<input type="checkbox" class="checkbox" name="rclone.no_modtime" id="rclone.no_modtime">
|
<input type="checkbox" class="checkbox" name="rclone.no_modtime" id="rclone.no_modtime">
|
||||||
@@ -656,6 +702,36 @@
|
|||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"github.com/gorilla/sessions"
|
"github.com/gorilla/sessions"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/sirrobot01/decypharr/internal/logger"
|
"github.com/sirrobot01/decypharr/internal/logger"
|
||||||
"github.com/sirrobot01/decypharr/pkg/store"
|
"github.com/sirrobot01/decypharr/pkg/wire"
|
||||||
"html/template"
|
"html/template"
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
@@ -60,7 +60,7 @@ type Web struct {
|
|||||||
logger zerolog.Logger
|
logger zerolog.Logger
|
||||||
cookie *sessions.CookieStore
|
cookie *sessions.CookieStore
|
||||||
templates *template.Template
|
templates *template.Template
|
||||||
torrents *store.TorrentStorage
|
torrents *wire.TorrentStorage
|
||||||
}
|
}
|
||||||
|
|
||||||
func New() *Web {
|
func New() *Web {
|
||||||
@@ -86,6 +86,6 @@ func New() *Web {
|
|||||||
logger: logger.New("ui"),
|
logger: logger.New("ui"),
|
||||||
templates: templates,
|
templates: templates,
|
||||||
cookie: cookieStore,
|
cookie: cookieStore,
|
||||||
torrents: store.Get().Torrents(),
|
torrents: wire.Get().Torrents(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/go-chi/chi/v5/middleware"
|
"github.com/go-chi/chi/v5/middleware"
|
||||||
"github.com/sirrobot01/decypharr/internal/config"
|
"github.com/sirrobot01/decypharr/internal/config"
|
||||||
"github.com/sirrobot01/decypharr/pkg/store"
|
"github.com/sirrobot01/decypharr/pkg/wire"
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -97,7 +97,7 @@ func New() *WebDav {
|
|||||||
Handlers: make([]*Handler, 0),
|
Handlers: make([]*Handler, 0),
|
||||||
URLBase: urlBase,
|
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())
|
h := NewHandler(name, urlBase, c, c.Logger())
|
||||||
w.Handlers = append(w.Handlers, h)
|
w.Handlers = append(w.Handlers, h)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package store
|
package wire
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -310,7 +310,6 @@ func (s *Store) getTorrentPath(rclonePath string, debridTorrent *types.Torrent)
|
|||||||
for {
|
for {
|
||||||
torrentPath, err := debridTorrent.GetMountFolder(rclonePath)
|
torrentPath, err := debridTorrent.GetMountFolder(rclonePath)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
s.logger.Debug().Msgf("Found torrent path: %s", torrentPath)
|
|
||||||
return torrentPath, err
|
return torrentPath, err
|
||||||
}
|
}
|
||||||
time.Sleep(100 * time.Millisecond)
|
time.Sleep(100 * time.Millisecond)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package store
|
package wire
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package store
|
package wire
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -29,7 +29,6 @@ func (s *Store) addToQueue(importReq *ImportRequest) error {
|
|||||||
|
|
||||||
func (s *Store) StartQueueWorkers(ctx context.Context) error {
|
func (s *Store) StartQueueWorkers(ctx context.Context) error {
|
||||||
// This function is responsible for starting the scheduled tasks
|
// This function is responsible for starting the scheduled tasks
|
||||||
|
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
}
|
}
|
||||||
@@ -45,7 +44,7 @@ func (s *Store) StartQueueWorkers(ctx context.Context) error {
|
|||||||
}), gocron.WithContext(ctx)); err != nil {
|
}), gocron.WithContext(ctx)); err != nil {
|
||||||
s.logger.Error().Err(err).Msg("Failed to create slots tracking job")
|
s.logger.Error().Err(err).Msg("Failed to create slots tracking job")
|
||||||
} else {
|
} 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
|
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 {
|
if s.importsQueue.Size() <= 0 {
|
||||||
// Queue is empty, no need to process
|
// Queue is empty, no need to process
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, slots := range availableSlots {
|
for name, slots := range availableSlots {
|
||||||
|
|
||||||
s.logger.Debug().Msgf("Available slots for %s: %d", name, slots)
|
s.logger.Debug().Msgf("Available slots for %s: %d", name, slots)
|
||||||
// If slots are available, process the next import request from the queue
|
// If slots are available, process the next import request from the queue
|
||||||
for slots > 0 {
|
for slots > 0 {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package store
|
package wire
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package store
|
package wire
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"cmp"
|
"cmp"
|
||||||
@@ -86,7 +86,10 @@ func Reset() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if instance.rcloneManager != nil {
|
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 {
|
if instance.importsQueue != nil {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package store
|
package wire
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"cmp"
|
"cmp"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package store
|
package wire
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package store
|
package wire
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package store
|
package wire
|
||||||
|
|
||||||
import "context"
|
import "context"
|
||||||
|
|
||||||
Reference in New Issue
Block a user