- Be conservative about the number of goroutines
- Minor fixes
- Add Webdav to ui
- Add more configs to UI
This commit is contained in:
Mukhtar Akere
2025-03-28 00:25:02 +01:00
parent 4ae5de99e8
commit f9bc7ad914
14 changed files with 252 additions and 96 deletions
-20
View File
@@ -3,7 +3,6 @@ package decypharr
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/rs/zerolog"
"github.com/sirrobot01/debrid-blackhole/internal/config" "github.com/sirrobot01/debrid-blackhole/internal/config"
"github.com/sirrobot01/debrid-blackhole/internal/logger" "github.com/sirrobot01/debrid-blackhole/internal/logger"
"github.com/sirrobot01/debrid-blackhole/pkg/proxy" "github.com/sirrobot01/debrid-blackhole/pkg/proxy"
@@ -15,26 +14,12 @@ import (
"github.com/sirrobot01/debrid-blackhole/pkg/webdav" "github.com/sirrobot01/debrid-blackhole/pkg/webdav"
"github.com/sirrobot01/debrid-blackhole/pkg/worker" "github.com/sirrobot01/debrid-blackhole/pkg/worker"
"os" "os"
"runtime"
"runtime/debug" "runtime/debug"
"strconv" "strconv"
"sync" "sync"
"syscall" "syscall"
"time"
) )
func monitorGoroutines(interval time.Duration, _log zerolog.Logger) {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
_log.Debug().Msgf("Current goroutines: %d", runtime.NumGoroutine())
}
}
}
func Start(ctx context.Context) error { func Start(ctx context.Context) error {
if umaskStr := os.Getenv("UMASK"); umaskStr != "" { if umaskStr := os.Getenv("UMASK"); umaskStr != "" {
@@ -121,11 +106,6 @@ func Start(ctx context.Context) error {
}) })
} }
safeGo(func() error {
monitorGoroutines(1*time.Minute, _log)
return nil
})
go func() { go func() {
wg.Wait() wg.Wait()
close(errChan) close(errChan)
+18 -14
View File
@@ -25,13 +25,8 @@ type Debrid struct {
CheckCached bool `json:"check_cached"` CheckCached bool `json:"check_cached"`
RateLimit string `json:"rate_limit"` // 200/minute or 10/second RateLimit string `json:"rate_limit"` // 200/minute or 10/second
// Webdav UseWebDav bool `json:"use_webdav"`
UseWebdav bool `json:"use_webdav"` WebDav
TorrentRefreshInterval string `json:"torrent_refresh_interval"`
DownloadLinksRefreshInterval string `json:"downloads_refresh_interval"`
TorrentRefreshWorkers int `json:"torrent_refresh_workers"`
WebDavFolderNaming string `json:"webdav_folder_naming"`
AutoExpireLinksAfter string `json:"auto_expire_links_after"`
} }
type Proxy struct { type Proxy struct {
@@ -136,6 +131,10 @@ func (c *Config) loadConfig() error {
c.Debrids = append(c.Debrids, c.Debrid) c.Debrids = append(c.Debrids, c.Debrid)
} }
for i, debrid := range c.Debrids {
c.Debrids[i] = c.GetDebridWebDav(debrid)
}
if len(c.AllowedExt) == 0 { if len(c.AllowedExt) == 0 {
c.AllowedExt = getDefaultExtensions() c.AllowedExt = getDefaultExtensions()
} }
@@ -313,17 +312,22 @@ func (c *Config) NeedsSetup() bool {
} }
func (c *Config) GetDebridWebDav(d Debrid) Debrid { func (c *Config) GetDebridWebDav(d Debrid) Debrid {
if d.TorrentRefreshInterval == "" {
d.TorrentRefreshInterval = cmp.Or(c.WebDav.TorrentsRefreshInterval, "15s") // 15 seconds if !d.UseWebDav {
return d
} }
if d.DownloadLinksRefreshInterval == "" {
if d.TorrentsRefreshInterval == "" {
d.TorrentsRefreshInterval = cmp.Or(c.WebDav.TorrentsRefreshInterval, "15s") // 15 seconds
}
if d.WebDav.DownloadLinksRefreshInterval == "" {
d.DownloadLinksRefreshInterval = cmp.Or(c.WebDav.DownloadLinksRefreshInterval, "40m") // 40 minutes d.DownloadLinksRefreshInterval = cmp.Or(c.WebDav.DownloadLinksRefreshInterval, "40m") // 40 minutes
} }
if d.TorrentRefreshWorkers == 0 { if d.Workers == 0 {
d.TorrentRefreshWorkers = cmp.Or(c.WebDav.Workers, 30) // 30 workers d.Workers = cmp.Or(c.WebDav.Workers, 30) // 30 workers
} }
if d.WebDavFolderNaming == "" { if d.FolderNaming == "" {
d.WebDavFolderNaming = cmp.Or(c.WebDav.FolderNaming, "original_no_ext") d.FolderNaming = cmp.Or(c.WebDav.FolderNaming, "original_no_ext")
} }
if d.AutoExpireLinksAfter == "" { if d.AutoExpireLinksAfter == "" {
d.AutoExpireLinksAfter = cmp.Or(c.WebDav.AutoExpireLinksAfter, "24h") d.AutoExpireLinksAfter = cmp.Or(c.WebDav.AutoExpireLinksAfter, "24h")
+40 -24
View File
@@ -39,7 +39,7 @@ type PropfindResponse struct {
type CachedTorrent struct { type CachedTorrent struct {
*types.Torrent *types.Torrent
LastRead time.Time `json:"last_read"` AddedOn time.Time `json:"added_on"`
IsComplete bool `json:"is_complete"` IsComplete bool `json:"is_complete"`
} }
@@ -86,7 +86,7 @@ type Cache struct {
func NewCache(dc config.Debrid, client types.Client) *Cache { func NewCache(dc config.Debrid, client types.Client) *Cache {
cfg := config.GetConfig() cfg := config.GetConfig()
torrentRefreshInterval, err := time.ParseDuration(dc.TorrentRefreshInterval) torrentRefreshInterval, err := time.ParseDuration(dc.TorrentsRefreshInterval)
if err != nil { if err != nil {
torrentRefreshInterval = time.Second * 15 torrentRefreshInterval = time.Second * 15
} }
@@ -109,7 +109,7 @@ func NewCache(dc config.Debrid, client types.Client) *Cache {
torrentRefreshInterval: torrentRefreshInterval, torrentRefreshInterval: torrentRefreshInterval,
downloadLinksRefreshInterval: downloadLinksRefreshInterval, downloadLinksRefreshInterval: downloadLinksRefreshInterval,
PropfindResp: xsync.NewMapOf[string, PropfindResponse](), PropfindResp: xsync.NewMapOf[string, PropfindResponse](),
folderNaming: WebDavFolderNaming(dc.WebDavFolderNaming), folderNaming: WebDavFolderNaming(dc.FolderNaming),
autoExpiresLinksAfter: autoExpiresLinksAfter, autoExpiresLinksAfter: autoExpiresLinksAfter,
repairsInProgress: xsync.NewMapOf[string, bool](), repairsInProgress: xsync.NewMapOf[string, bool](),
saveSemaphore: make(chan struct{}, 10), saveSemaphore: make(chan struct{}, 10),
@@ -201,6 +201,7 @@ func (c *Cache) load() (map[string]*CachedTorrent, error) {
return torrents, fmt.Errorf("failed to read cache directory: %w", err) return torrents, fmt.Errorf("failed to read cache directory: %w", err)
} }
now := time.Now()
for _, file := range files { for _, file := range files {
if file.IsDir() || filepath.Ext(file.Name()) != ".json" { if file.IsDir() || filepath.Ext(file.Name()) != ".json" {
continue continue
@@ -232,7 +233,11 @@ func (c *Cache) load() (map[string]*CachedTorrent, error) {
linkStore[f.Link] = true linkStore[f.Link] = true
} }
} }
addedOn, err := time.Parse(time.RFC3339, ct.Added)
if err != nil {
addedOn = now
}
ct.AddedOn = addedOn
ct.IsComplete = true ct.IsComplete = true
torrents[ct.Id] = &ct torrents[ct.Id] = &ct
} }
@@ -447,10 +452,15 @@ func (c *Cache) ProcessTorrent(t *types.Torrent, refreshRclone bool) error {
return fmt.Errorf("failed to update torrent: %w", err) return fmt.Errorf("failed to update torrent: %w", err)
} }
} }
addedOn, err := time.Parse(time.RFC3339, t.Added)
if err != nil {
addedOn = time.Now()
}
ct := &CachedTorrent{ ct := &CachedTorrent{
Torrent: t, Torrent: t,
LastRead: time.Now(),
IsComplete: len(t.Files) > 0, IsComplete: len(t.Files) > 0,
AddedOn: addedOn,
} }
c.setTorrent(ct) c.setTorrent(ct)
@@ -487,25 +497,27 @@ func (c *Cache) GetDownloadLink(torrentId, filename, fileLink string) string {
downloadLink, err := c.client.GetDownloadLink(ct.Torrent, &file) downloadLink, err := c.client.GetDownloadLink(ct.Torrent, &file)
if err != nil { if err != nil {
if errors.Is(err, request.HosterUnavailableError) { if errors.Is(err, request.HosterUnavailableError) {
// This code is commented iut due to the fact that if a torrent link is uncached, it's likely that we can't redownload it again
// Do not attempt to repair the torrent if the hoster is unavailable
// Check link here?? // Check link here??
c.logger.Debug().Err(err).Msgf("Hoster is unavailable. Triggering repair for %s", ct.Name) //c.logger.Debug().Err(err).Msgf("Hoster is unavailable. Triggering repair for %s", ct.Name)
if err := c.repairTorrent(ct); err != nil { //if err := c.repairTorrent(ct); err != nil {
c.logger.Error().Err(err).Msgf("Failed to trigger repair for %s", ct.Name) // c.logger.Error().Err(err).Msgf("Failed to trigger repair for %s", ct.Name)
return "" // return ""
} //}
// Generate download link for the file then //// Generate download link for the file then
f := ct.Files[filename] //f := ct.Files[filename]
downloadLink, _ = c.client.GetDownloadLink(ct.Torrent, &f) //downloadLink, _ = c.client.GetDownloadLink(ct.Torrent, &f)
f.DownloadLink = downloadLink //f.DownloadLink = downloadLink
file.Generated = time.Now() //file.Generated = time.Now()
ct.Files[filename] = f //ct.Files[filename] = f
c.updateDownloadLink(file.Link, downloadLink) //c.updateDownloadLink(file.Link, downloadLink)
//
go func() { //go func() {
go c.setTorrent(ct) // go c.setTorrent(ct)
}() //}()
//
return downloadLink // Gets download link in the next pass //return downloadLink // Gets download link in the next pass
} }
c.logger.Debug().Err(err).Msgf("Failed to get download link for :%s", file.Link) c.logger.Debug().Err(err).Msgf("Failed to get download link for :%s", file.Link)
@@ -537,10 +549,14 @@ func (c *Cache) AddTorrent(t *types.Torrent) error {
return fmt.Errorf("failed to update torrent: %w", err) return fmt.Errorf("failed to update torrent: %w", err)
} }
} }
addedOn, err := time.Parse(time.RFC3339, t.Added)
if err != nil {
addedOn = time.Now()
}
ct := &CachedTorrent{ ct := &CachedTorrent{
Torrent: t, Torrent: t,
LastRead: time.Now(),
IsComplete: len(t.Files) > 0, IsComplete: len(t.Files) > 0,
AddedOn: addedOn,
} }
c.setTorrent(ct) c.setTorrent(ct)
c.refreshListings() c.refreshListings()
+1 -2
View File
@@ -18,10 +18,9 @@ func NewEngine() *Engine {
caches := make(map[string]*Cache) caches := make(map[string]*Cache)
for _, dc := range cfg.Debrids { for _, dc := range cfg.Debrids {
dc = cfg.GetDebridWebDav(dc)
client := createDebridClient(dc) client := createDebridClient(dc)
logger := client.GetLogger() logger := client.GetLogger()
if dc.UseWebdav { if dc.UseWebDav {
caches[dc.Name] = NewCache(dc, client) caches[dc.Name] = NewCache(dc, client)
logger.Info().Msg("Debrid Service started with WebDAV") logger.Info().Msg("Debrid Service started with WebDAV")
} else { } else {
+12 -9
View File
@@ -38,25 +38,25 @@ func (c *Cache) refreshListings() {
} else { } else {
return return
} }
// Copy the current torrents to avoid concurrent issues // COpy the torrents to a string|time map
torrents := make([]string, 0, c.torrentsNames.Size()) torrentsTime := make(map[string]time.Time, c.torrents.Size())
torrents := make([]string, 0, c.torrents.Size())
c.torrentsNames.Range(func(key string, value *CachedTorrent) bool { c.torrentsNames.Range(func(key string, value *CachedTorrent) bool {
torrentsTime[key] = value.AddedOn
torrents = append(torrents, key) torrents = append(torrents, key)
return true return true
}) })
sort.Slice(torrents, func(i, j int) bool { // Sort the torrents by name
return torrents[i] < torrents[j] sort.Strings(torrents)
})
files := make([]os.FileInfo, 0, len(torrents)) files := make([]os.FileInfo, 0, len(torrents))
now := time.Now()
for _, t := range torrents { for _, t := range torrents {
files = append(files, &fileInfo{ files = append(files, &fileInfo{
name: t, name: t,
size: 0, size: 0,
mode: 0755 | os.ModeDir, mode: 0755 | os.ModeDir,
modTime: now, modTime: torrentsTime[t],
isDir: true, isDir: true,
}) })
} }
@@ -219,10 +219,13 @@ func (c *Cache) refreshTorrent(t *CachedTorrent) *CachedTorrent {
if len(t.Files) == 0 { if len(t.Files) == 0 {
return nil return nil
} }
addedOn, err := time.Parse(time.RFC3339, _torrent.Added)
if err != nil {
addedOn = time.Now()
}
ct := &CachedTorrent{ ct := &CachedTorrent{
Torrent: _torrent, Torrent: _torrent,
LastRead: time.Now(), AddedOn: addedOn,
IsComplete: len(t.Files) > 0, IsComplete: len(t.Files) > 0,
} }
c.setTorrent(ct) c.setTorrent(ct)
+3
View File
@@ -17,6 +17,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"time"
) )
type RealDebrid struct { type RealDebrid struct {
@@ -178,6 +179,7 @@ func (r *RealDebrid) UpdateTorrent(t *types.Torrent) error {
t.Links = data.Links t.Links = data.Links
t.MountPath = r.MountPath t.MountPath = r.MountPath
t.Debrid = r.Name t.Debrid = r.Name
t.Added = data.Added
t.Files = getTorrentFiles(t, data, false) // Get selected files t.Files = getTorrentFiles(t, data, false) // Get selected files
return nil return nil
} }
@@ -422,6 +424,7 @@ func (r *RealDebrid) getTorrents(offset int, limit int) (int, []*types.Torrent,
InfoHash: t.Hash, InfoHash: t.Hash,
Debrid: r.Name, Debrid: r.Name,
MountPath: r.MountPath, MountPath: r.MountPath,
Added: t.Added.Format(time.RFC3339),
}) })
} }
return totalItems, torrents, nil return totalItems, torrents, nil
+1 -1
View File
@@ -18,7 +18,7 @@ func (r *Repair) clean(job *Job) error {
mu := sync.Mutex{} mu := sync.Mutex{}
// Limit concurrent goroutines // Limit concurrent goroutines
g.SetLimit(runtime.NumCPU() * 4) g.SetLimit(10)
for _, a := range job.Arrs { for _, a := range job.Arrs {
a := a // Capture range variable a := a // Capture range variable
+3 -1
View File
@@ -218,6 +218,8 @@ func (r *Repair) repair(job *Job) error {
// Create a new error group with context // Create a new error group with context
g, ctx := errgroup.WithContext(context.Background()) g, ctx := errgroup.WithContext(context.Background())
g.SetLimit(4)
// Use a mutex to protect concurrent access to brokenItems // Use a mutex to protect concurrent access to brokenItems
var mu sync.Mutex var mu sync.Mutex
brokenItems := map[string][]arr.ContentFile{} brokenItems := map[string][]arr.ContentFile{}
@@ -397,7 +399,7 @@ func (r *Repair) repairArr(j *Job, _arr string, tmdbId string) ([]arr.ContentFil
g, ctx := errgroup.WithContext(context.Background()) g, ctx := errgroup.WithContext(context.Background())
// Limit concurrent goroutines // Limit concurrent goroutines
g.SetLimit(runtime.NumCPU() * 4) g.SetLimit(10)
// Mutex for brokenItems // Mutex for brokenItems
var mu sync.Mutex var mu sync.Mutex
+29
View File
@@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"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/goccy/go-json"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/sirrobot01/debrid-blackhole/internal/config" "github.com/sirrobot01/debrid-blackhole/internal/config"
"github.com/sirrobot01/debrid-blackhole/internal/logger" "github.com/sirrobot01/debrid-blackhole/internal/logger"
@@ -13,6 +14,7 @@ import (
"net/http" "net/http"
"os" "os"
"os/signal" "os/signal"
"runtime"
"syscall" "syscall"
) )
@@ -41,6 +43,7 @@ func (s *Server) Start(ctx context.Context) error {
// Register logs // Register logs
s.router.Get("/logs", s.getLogs) s.router.Get("/logs", s.getLogs)
s.router.Get("/stats", s.getStats)
port := fmt.Sprintf(":%s", cfg.QBitTorrent.Port) port := fmt.Sprintf(":%s", cfg.QBitTorrent.Port)
s.logger.Info().Msgf("Starting server on %s", port) s.logger.Info().Msgf("Starting server on %s", port)
srv := &http.Server{ srv := &http.Server{
@@ -102,3 +105,29 @@ func (s *Server) getLogs(w http.ResponseWriter, r *http.Request) {
return return
} }
} }
func (s *Server) getStats(w http.ResponseWriter, r *http.Request) {
var memStats runtime.MemStats
runtime.ReadMemStats(&memStats)
stats := map[string]interface{}{
// Memory stats
"heap_alloc_mb": fmt.Sprintf("%.2fMB", float64(memStats.HeapAlloc)/1024/1024),
"total_alloc_mb": fmt.Sprintf("%.2fMB", float64(memStats.TotalAlloc)/1024/1024),
"sys_mb": fmt.Sprintf("%.2fMB", float64(memStats.Sys)/1024/1024),
// GC stats
"gc_cycles": memStats.NumGC,
// Goroutine stats
"goroutines": runtime.NumGoroutine(),
// System info
"num_cpu": runtime.NumCPU(),
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(stats); err != nil {
s.logger.Error().Err(err).Msg("Failed to encode stats")
}
}
+84 -14
View File
@@ -11,7 +11,7 @@
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<div class="form-group"> <div class="form-group">
<label for="qbitDebug">Log Level</label> <label for="log-level">Log Level</label>
<select class="form-select" name="log_level" id="log-level" disabled> <select class="form-select" name="log_level" id="log-level" disabled>
<option value="info">Info</option> <option value="info">Info</option>
<option value="debug">Debug</option> <option value="debug">Debug</option>
@@ -86,13 +86,13 @@
</div> </div>
<!-- Debrid Configuration --> <!-- Debrid Configuration -->
<div class="section mb-5"> <div class="section mb-5">
<h5 class="border-bottom pb-2">Debrid Configuration</h5> <h5 class="border-bottom pb-2">Debrids</h5>
<div id="debridConfigs"></div> <div id="debridConfigs"></div>
</div> </div>
<!-- QBitTorrent Configuration --> <!-- QBitTorrent Configuration -->
<div class="section mb-5"> <div class="section mb-5">
<h5 class="border-bottom pb-2">QBitTorrent Configuration</h5> <h5 class="border-bottom pb-2">QBitTorrent</h5>
<div class="row"> <div class="row">
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label class="form-label">Username</label> <label class="form-label">Username</label>
@@ -114,12 +114,16 @@
<label class="form-label">Refresh Interval (seconds)</label> <label class="form-label">Refresh Interval (seconds)</label>
<input type="number" class="form-control" name="qbit.refresh_interval"> <input type="number" class="form-control" name="qbit.refresh_interval">
</div> </div>
<div class="col-md-6 mb-3">
<input type="checkbox" disabled class="form-check-input" name="qbit.skip_pre_cache">
<label class="form-check-label">Skip Pre-Cache On Download(This caches a tiny part of your file to speed up import)</label>
</div>
</div> </div>
</div> </div>
<!-- Arr Configurations --> <!-- Arr Configurations -->
<div class="section mb-5"> <div class="section mb-5">
<h5 class="border-bottom pb-2">Arr Configurations</h5> <h5 class="border-bottom pb-2">Arrs</h5>
<div id="arrConfigs"></div> <div id="arrConfigs"></div>
</div> </div>
@@ -141,6 +145,10 @@
<input type="checkbox" disabled class="form-check-input" name="repair.enabled" id="repairEnabled"> <input type="checkbox" disabled class="form-check-input" name="repair.enabled" id="repairEnabled">
<label class="form-check-label" for="repairEnabled">Enable Repair</label> <label class="form-check-label" for="repairEnabled">Enable Repair</label>
</div> </div>
<div class="form-check me-3 d-inline-block">
<input type="checkbox" disabled class="form-check-input" name="repair.use_webdav" id="repairUseWebdav">
<label class="form-check-label" for="repairUseWebdav">Use Webdav</label>
</div>
<div class="form-check me-3 d-inline-block"> <div class="form-check me-3 d-inline-block">
<input type="checkbox" disabled class="form-check-input" name="repair.run_on_start" id="repairOnStart"> <input type="checkbox" disabled class="form-check-input" name="repair.run_on_start" id="repairOnStart">
<label class="form-check-label" for="repairOnStart">Run on Start</label> <label class="form-check-label" for="repairOnStart">Run on Start</label>
@@ -159,7 +167,7 @@
// Templates for dynamic elements // Templates for dynamic elements
const debridTemplate = (index) => ` const debridTemplate = (index) => `
<div class="config-item position-relative mb-3 p-3 border rounded"> <div class="config-item position-relative mb-3 p-3 border rounded">
<div class="row"> <div class="row mb-2">
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label class="form-label">Name</label> <label class="form-label">Name</label>
<input type="text" disabled class="form-control" name="debrid[${index}].name" required> <input type="text" disabled class="form-control" name="debrid[${index}].name" required>
@@ -191,6 +199,47 @@
</div> </div>
</div> </div>
</div> </div>
<div class="row mt-3 webdav-${index} d-none">
<h6 class="pb-2">Webdav</h6>
<div class="col-md-3 mb-3">
<label class="form-label">Torrents Refresh Interval</label>
<input type="text" disabled class="form-control" name="debrid[${index}].torrents_refresh_interval" placeholder="15s" required>
</div>
<div class="col-md-3 mb-3">
<label class="form-label">Download Links Refresh Interval</label>
<input type="text" disabled class="form-control" name="debrid[${index}].download_links_refresh_interval" placeholder="24h" required>
</div>
<div class="col-md-3 mb-3">
<label class="form-label">Expire Links After</label>
<input type="text" disabled class="form-control" name="debrid[${index}].auto_expire_links_after" placeholder="24h" required>
</div>
<div class="col-md-3 mb-3">
<label class="form-label">Folder Naming Structure</label>
<select class="form-select" name="debrid[${index}].folder_naming" disabled>
<option value="filename">File name</option>
<option value="filename_no_ext">File name with No Ext</option>
<option value="original">Original name</option>
<option value="original_no_ext">Original name with No Ext</option>
<option value="id">Use ID</option>
</select>
</div>
<div class="col-md-3 mb-3">
<label class="form-label">Number of Workers</label>
<input type="text" disabled class="form-control" name="debrid[${index}].workers" required placeholder="e.g., 20">
</div>
<div class="col-md-3 mb-3">
<label class="form-label">Rclone RC URL</label>
<input type="text" disabled class="form-control" name="debrid[${index}].rc_url" placeholder="e.g., http://localhost:9990">
</div>
<div class="col-md-3 mb-3">
<label class="form-label">Rclone RC User</label>
<input type="text" disabled class="form-control" name="debrid[${index}].rc_user">
</div>
<div class="col-md-3 mb-3">
<label class="form-label">Rclone RC Password</label>
<input type="password" disabled class="form-control" name="debrid[${index}].rc_pass">
</div>
</div>
</div> </div>
`; `;
@@ -360,16 +409,37 @@
container.insertAdjacentHTML('beforeend', debridTemplate(debridCount)); container.insertAdjacentHTML('beforeend', debridTemplate(debridCount));
if (data) { if (data) {
Object.entries(data).forEach(([key, value]) => {
const input = container.querySelector(`[name="debrid[${debridCount}].${key}"]`); if (data.use_webdav) {
if (input) { let _webCfg = container.querySelector(`.webdav-${debridCount}`);
if (input.type === 'checkbox') { if (_webCfg) {
input.checked = value; _webCfg.classList.remove('d-none');
} else {
input.value = value;
}
} }
}); }
function setFieldValues(obj, prefix) {
Object.entries(obj).forEach(([key, value]) => {
const fieldName = prefix ? `${prefix}.${key}` : key;
// If value is an object and not null, recursively process nested fields
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
setFieldValues(value, fieldName);
} else {
// Handle leaf values (actual form fields)
const input = container.querySelector(`[name="debrid[${debridCount}].${fieldName}"]`);
if (input) {
if (input.type === 'checkbox') {
input.checked = value;
} else {
input.value = value;
}
}
}
});
}
// Start processing with the root object
setFieldValues(data, '');
} }
debridCount++; debridCount++;
+18 -1
View File
@@ -117,6 +117,18 @@
background-color: rgba(128, 128, 128, 0.2); background-color: rgba(128, 128, 128, 0.2);
} }
</style> </style>
<script>
(function() {
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
document.documentElement.setAttribute('data-bs-theme', savedTheme);
} else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.setAttribute('data-bs-theme', 'dark');
} else {
document.documentElement.setAttribute('data-bs-theme', 'light');
}
})();
</script>
</head> </head>
<body> <body>
<div class="toast-container position-fixed bottom-0 end-0 p-3"> <div class="toast-container position-fixed bottom-0 end-0 p-3">
@@ -149,7 +161,12 @@
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link {{if eq .Page "config"}}active{{end}}" href="/config"> <a class="nav-link {{if eq .Page "config"}}active{{end}}" href="/config">
<i class="bi bi-gear me-1"></i>Config <i class="bi bi-gear me-1"></i>Settings
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/webdav" target="_blank">
<i class="bi bi-cloud me-1"></i>WebDAV
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
+4 -2
View File
@@ -31,6 +31,8 @@ type File struct {
fileId string fileId string
torrentId string torrentId string
modTime time.Time
size int64 size int64
offset int64 offset int64
isDir bool isDir bool
@@ -176,7 +178,7 @@ func (f *File) Stat() (os.FileInfo, error) {
name: f.name, name: f.name,
size: 0, size: 0,
mode: 0755 | os.ModeDir, mode: 0755 | os.ModeDir,
modTime: time.Now(), modTime: f.modTime,
isDir: true, isDir: true,
}, nil }, nil
} }
@@ -185,7 +187,7 @@ func (f *File) Stat() (os.FileInfo, error) {
name: f.name, name: f.name,
size: f.size, size: f.size,
mode: 0644, mode: 0644,
modTime: time.Now(), modTime: f.modTime,
isDir: false, isDir: false,
}, nil }, nil
} }
+13 -1
View File
@@ -116,6 +116,8 @@ func (h *Handler) OpenFile(ctx context.Context, name string, flag int, perm os.F
metadataOnly = true metadataOnly = true
} }
now := time.Now()
// Fast path optimization with a map lookup instead of string comparisons // Fast path optimization with a map lookup instead of string comparisons
switch name { switch name {
case rootDir: case rootDir:
@@ -125,6 +127,7 @@ func (h *Handler) OpenFile(ctx context.Context, name string, flag int, perm os.F
children: h.getParentFiles(), children: h.getParentFiles(),
name: "/", name: "/",
metadataOnly: metadataOnly, metadataOnly: metadataOnly,
modTime: now,
}, nil }, nil
case path.Join(rootDir, "version.txt"): case path.Join(rootDir, "version.txt"):
return &File{ return &File{
@@ -134,6 +137,7 @@ func (h *Handler) OpenFile(ctx context.Context, name string, flag int, perm os.F
name: "version.txt", name: "version.txt",
size: int64(len("v1.0.0")), size: int64(len("v1.0.0")),
metadataOnly: metadataOnly, metadataOnly: metadataOnly,
modTime: now,
}, nil }, nil
} }
@@ -152,6 +156,7 @@ func (h *Handler) OpenFile(ctx context.Context, name string, flag int, perm os.F
name: folderName, name: folderName,
size: 0, size: 0,
metadataOnly: metadataOnly, metadataOnly: metadataOnly,
modTime: now,
}, nil }, nil
} }
@@ -177,6 +182,7 @@ func (h *Handler) OpenFile(ctx context.Context, name string, flag int, perm os.F
name: cachedTorrent.Name, name: cachedTorrent.Name,
size: cachedTorrent.Size, size: cachedTorrent.Size,
metadataOnly: metadataOnly, metadataOnly: metadataOnly,
modTime: cachedTorrent.AddedOn,
}, nil }, nil
} }
@@ -192,6 +198,7 @@ func (h *Handler) OpenFile(ctx context.Context, name string, flag int, perm os.F
size: file.Size, size: file.Size,
link: file.Link, link: file.Link,
metadataOnly: metadataOnly, metadataOnly: metadataOnly,
modTime: cachedTorrent.AddedOn,
} }
return fi, nil return fi, nil
} }
@@ -457,7 +464,12 @@ func (h *Handler) serveDirectory(w http.ResponseWriter, r *http.Request, file we
} }
// Parse and execute template // Parse and execute template
tmpl, err := template.New("directory").Parse(directoryTemplate) funcMap := template.FuncMap{
"add": func(a, b int) int {
return a + b
},
}
tmpl, err := template.New("directory").Funcs(funcMap).Parse(directoryTemplate)
if err != nil { if err != nil {
h.logger.Error().Err(err).Msg("Failed to parse directory template") h.logger.Error().Err(err).Msg("Failed to parse directory template")
http.Error(w, "Internal Server Error", http.StatusInternalServerError) http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+26 -7
View File
@@ -73,6 +73,8 @@ const directoryTemplate = `
display: block; display: block;
border: 1px solid #eee; border: 1px solid #eee;
border-radius: 4px; border-radius: 4px;
position: relative;
padding-left: 50px; /* Make room for the number */
} }
a:hover { a:hover {
background-color: #f7f9fa; background-color: #f7f9fa;
@@ -85,23 +87,40 @@ const directoryTemplate = `
.parent-dir { .parent-dir {
background-color: #f8f9fa; background-color: #f8f9fa;
} }
.file-number {
position: absolute;
left: 10px;
top: 10px;
width: 30px;
color: #777;
font-weight: bold;
text-align: right;
}
.file-name {
display: inline-block;
max-width: 70%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style> </style>
</head> </head>
<body> <body>
<h1>Index of {{.Path}}</h1> <h1>Index of {{.Path}}</h1>
<ul> <ul>
{{if .ShowParent}} {{if .ShowParent}}
<li><a href="{{.ParentPath}}" class="parent-dir">Parent Directory</a></li> <li><a href="{{.ParentPath}}" class="parent-dir"><span class="file-number"></span>Parent Directory</a></li>
{{end}} {{end}}
{{range .Children}} {{range $index, $file := .Children}}
<li> <li>
<a href="{{$.Path}}/{{.Name}}"> <a href="{{$.Path}}/{{$file.Name}}">
{{.Name}}{{if .IsDir}}/{{end}} <span class="file-number">{{add $index 1}}.</span>
<span class="file-name">{{$file.Name}}{{if $file.IsDir}}/{{end}}</span>
<span class="file-info"> <span class="file-info">
{{if not .IsDir}} {{if not $file.IsDir}}
{{.Size}} bytes {{$file.Size}} bytes
{{end}} {{end}}
{{.ModTime.Format "2006-01-02 15:04:05"}} {{$file.ModTime.Format "2006-01-02 15:04:05"}}
</span> </span>
</a> </a>
</li> </li>