Fixes
- Be conservative about the number of goroutines - Minor fixes - Add Webdav to ui - Add more configs to UI
This commit is contained in:
@@ -3,7 +3,6 @@ package decypharr
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/config"
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/logger"
|
||||
"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/worker"
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"sync"
|
||||
"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 {
|
||||
|
||||
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() {
|
||||
wg.Wait()
|
||||
close(errChan)
|
||||
|
||||
@@ -25,13 +25,8 @@ type Debrid struct {
|
||||
CheckCached bool `json:"check_cached"`
|
||||
RateLimit string `json:"rate_limit"` // 200/minute or 10/second
|
||||
|
||||
// Webdav
|
||||
UseWebdav bool `json:"use_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"`
|
||||
UseWebDav bool `json:"use_webdav"`
|
||||
WebDav
|
||||
}
|
||||
|
||||
type Proxy struct {
|
||||
@@ -136,6 +131,10 @@ func (c *Config) loadConfig() error {
|
||||
c.Debrids = append(c.Debrids, c.Debrid)
|
||||
}
|
||||
|
||||
for i, debrid := range c.Debrids {
|
||||
c.Debrids[i] = c.GetDebridWebDav(debrid)
|
||||
}
|
||||
|
||||
if len(c.AllowedExt) == 0 {
|
||||
c.AllowedExt = getDefaultExtensions()
|
||||
}
|
||||
@@ -313,17 +312,22 @@ func (c *Config) NeedsSetup() bool {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
if d.TorrentRefreshWorkers == 0 {
|
||||
d.TorrentRefreshWorkers = cmp.Or(c.WebDav.Workers, 30) // 30 workers
|
||||
if d.Workers == 0 {
|
||||
d.Workers = cmp.Or(c.WebDav.Workers, 30) // 30 workers
|
||||
}
|
||||
if d.WebDavFolderNaming == "" {
|
||||
d.WebDavFolderNaming = cmp.Or(c.WebDav.FolderNaming, "original_no_ext")
|
||||
if d.FolderNaming == "" {
|
||||
d.FolderNaming = cmp.Or(c.WebDav.FolderNaming, "original_no_ext")
|
||||
}
|
||||
if d.AutoExpireLinksAfter == "" {
|
||||
d.AutoExpireLinksAfter = cmp.Or(c.WebDav.AutoExpireLinksAfter, "24h")
|
||||
|
||||
@@ -39,7 +39,7 @@ type PropfindResponse struct {
|
||||
|
||||
type CachedTorrent struct {
|
||||
*types.Torrent
|
||||
LastRead time.Time `json:"last_read"`
|
||||
AddedOn time.Time `json:"added_on"`
|
||||
IsComplete bool `json:"is_complete"`
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ type Cache struct {
|
||||
|
||||
func NewCache(dc config.Debrid, client types.Client) *Cache {
|
||||
cfg := config.GetConfig()
|
||||
torrentRefreshInterval, err := time.ParseDuration(dc.TorrentRefreshInterval)
|
||||
torrentRefreshInterval, err := time.ParseDuration(dc.TorrentsRefreshInterval)
|
||||
if err != nil {
|
||||
torrentRefreshInterval = time.Second * 15
|
||||
}
|
||||
@@ -109,7 +109,7 @@ func NewCache(dc config.Debrid, client types.Client) *Cache {
|
||||
torrentRefreshInterval: torrentRefreshInterval,
|
||||
downloadLinksRefreshInterval: downloadLinksRefreshInterval,
|
||||
PropfindResp: xsync.NewMapOf[string, PropfindResponse](),
|
||||
folderNaming: WebDavFolderNaming(dc.WebDavFolderNaming),
|
||||
folderNaming: WebDavFolderNaming(dc.FolderNaming),
|
||||
autoExpiresLinksAfter: autoExpiresLinksAfter,
|
||||
repairsInProgress: xsync.NewMapOf[string, bool](),
|
||||
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)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
for _, file := range files {
|
||||
if file.IsDir() || filepath.Ext(file.Name()) != ".json" {
|
||||
continue
|
||||
@@ -232,7 +233,11 @@ func (c *Cache) load() (map[string]*CachedTorrent, error) {
|
||||
linkStore[f.Link] = true
|
||||
}
|
||||
}
|
||||
|
||||
addedOn, err := time.Parse(time.RFC3339, ct.Added)
|
||||
if err != nil {
|
||||
addedOn = now
|
||||
}
|
||||
ct.AddedOn = addedOn
|
||||
ct.IsComplete = true
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
addedOn, err := time.Parse(time.RFC3339, t.Added)
|
||||
if err != nil {
|
||||
addedOn = time.Now()
|
||||
}
|
||||
ct := &CachedTorrent{
|
||||
Torrent: t,
|
||||
LastRead: time.Now(),
|
||||
IsComplete: len(t.Files) > 0,
|
||||
AddedOn: addedOn,
|
||||
}
|
||||
c.setTorrent(ct)
|
||||
|
||||
@@ -487,25 +497,27 @@ func (c *Cache) GetDownloadLink(torrentId, filename, fileLink string) string {
|
||||
downloadLink, err := c.client.GetDownloadLink(ct.Torrent, &file)
|
||||
if err != nil {
|
||||
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??
|
||||
c.logger.Debug().Err(err).Msgf("Hoster is unavailable. Triggering repair for %s", ct.Name)
|
||||
if err := c.repairTorrent(ct); err != nil {
|
||||
c.logger.Error().Err(err).Msgf("Failed to trigger repair for %s", ct.Name)
|
||||
return ""
|
||||
}
|
||||
// Generate download link for the file then
|
||||
f := ct.Files[filename]
|
||||
downloadLink, _ = c.client.GetDownloadLink(ct.Torrent, &f)
|
||||
f.DownloadLink = downloadLink
|
||||
file.Generated = time.Now()
|
||||
ct.Files[filename] = f
|
||||
c.updateDownloadLink(file.Link, downloadLink)
|
||||
|
||||
go func() {
|
||||
go c.setTorrent(ct)
|
||||
}()
|
||||
|
||||
return downloadLink // Gets download link in the next pass
|
||||
//c.logger.Debug().Err(err).Msgf("Hoster is unavailable. Triggering repair for %s", ct.Name)
|
||||
//if err := c.repairTorrent(ct); err != nil {
|
||||
// c.logger.Error().Err(err).Msgf("Failed to trigger repair for %s", ct.Name)
|
||||
// return ""
|
||||
//}
|
||||
//// Generate download link for the file then
|
||||
//f := ct.Files[filename]
|
||||
//downloadLink, _ = c.client.GetDownloadLink(ct.Torrent, &f)
|
||||
//f.DownloadLink = downloadLink
|
||||
//file.Generated = time.Now()
|
||||
//ct.Files[filename] = f
|
||||
//c.updateDownloadLink(file.Link, downloadLink)
|
||||
//
|
||||
//go func() {
|
||||
// go c.setTorrent(ct)
|
||||
//}()
|
||||
//
|
||||
//return downloadLink // Gets download link in the next pass
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
addedOn, err := time.Parse(time.RFC3339, t.Added)
|
||||
if err != nil {
|
||||
addedOn = time.Now()
|
||||
}
|
||||
ct := &CachedTorrent{
|
||||
Torrent: t,
|
||||
LastRead: time.Now(),
|
||||
IsComplete: len(t.Files) > 0,
|
||||
AddedOn: addedOn,
|
||||
}
|
||||
c.setTorrent(ct)
|
||||
c.refreshListings()
|
||||
|
||||
@@ -18,10 +18,9 @@ func NewEngine() *Engine {
|
||||
caches := make(map[string]*Cache)
|
||||
|
||||
for _, dc := range cfg.Debrids {
|
||||
dc = cfg.GetDebridWebDav(dc)
|
||||
client := createDebridClient(dc)
|
||||
logger := client.GetLogger()
|
||||
if dc.UseWebdav {
|
||||
if dc.UseWebDav {
|
||||
caches[dc.Name] = NewCache(dc, client)
|
||||
logger.Info().Msg("Debrid Service started with WebDAV")
|
||||
} else {
|
||||
|
||||
@@ -38,25 +38,25 @@ func (c *Cache) refreshListings() {
|
||||
} else {
|
||||
return
|
||||
}
|
||||
// Copy the current torrents to avoid concurrent issues
|
||||
torrents := make([]string, 0, c.torrentsNames.Size())
|
||||
// COpy the torrents to a string|time map
|
||||
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 {
|
||||
torrentsTime[key] = value.AddedOn
|
||||
torrents = append(torrents, key)
|
||||
return true
|
||||
})
|
||||
|
||||
sort.Slice(torrents, func(i, j int) bool {
|
||||
return torrents[i] < torrents[j]
|
||||
})
|
||||
// Sort the torrents by name
|
||||
sort.Strings(torrents)
|
||||
|
||||
files := make([]os.FileInfo, 0, len(torrents))
|
||||
now := time.Now()
|
||||
for _, t := range torrents {
|
||||
files = append(files, &fileInfo{
|
||||
name: t,
|
||||
size: 0,
|
||||
mode: 0755 | os.ModeDir,
|
||||
modTime: now,
|
||||
modTime: torrentsTime[t],
|
||||
isDir: true,
|
||||
})
|
||||
}
|
||||
@@ -219,10 +219,13 @@ func (c *Cache) refreshTorrent(t *CachedTorrent) *CachedTorrent {
|
||||
if len(t.Files) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
addedOn, err := time.Parse(time.RFC3339, _torrent.Added)
|
||||
if err != nil {
|
||||
addedOn = time.Now()
|
||||
}
|
||||
ct := &CachedTorrent{
|
||||
Torrent: _torrent,
|
||||
LastRead: time.Now(),
|
||||
AddedOn: addedOn,
|
||||
IsComplete: len(t.Files) > 0,
|
||||
}
|
||||
c.setTorrent(ct)
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type RealDebrid struct {
|
||||
@@ -178,6 +179,7 @@ func (r *RealDebrid) UpdateTorrent(t *types.Torrent) error {
|
||||
t.Links = data.Links
|
||||
t.MountPath = r.MountPath
|
||||
t.Debrid = r.Name
|
||||
t.Added = data.Added
|
||||
t.Files = getTorrentFiles(t, data, false) // Get selected files
|
||||
return nil
|
||||
}
|
||||
@@ -422,6 +424,7 @@ func (r *RealDebrid) getTorrents(offset int, limit int) (int, []*types.Torrent,
|
||||
InfoHash: t.Hash,
|
||||
Debrid: r.Name,
|
||||
MountPath: r.MountPath,
|
||||
Added: t.Added.Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
return totalItems, torrents, nil
|
||||
|
||||
@@ -18,7 +18,7 @@ func (r *Repair) clean(job *Job) error {
|
||||
mu := sync.Mutex{}
|
||||
|
||||
// Limit concurrent goroutines
|
||||
g.SetLimit(runtime.NumCPU() * 4)
|
||||
g.SetLimit(10)
|
||||
|
||||
for _, a := range job.Arrs {
|
||||
a := a // Capture range variable
|
||||
|
||||
@@ -218,6 +218,8 @@ func (r *Repair) repair(job *Job) error {
|
||||
// Create a new error group with context
|
||||
g, ctx := errgroup.WithContext(context.Background())
|
||||
|
||||
g.SetLimit(4)
|
||||
|
||||
// Use a mutex to protect concurrent access to brokenItems
|
||||
var mu sync.Mutex
|
||||
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())
|
||||
|
||||
// Limit concurrent goroutines
|
||||
g.SetLimit(runtime.NumCPU() * 4)
|
||||
g.SetLimit(10)
|
||||
|
||||
// Mutex for brokenItems
|
||||
var mu sync.Mutex
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/goccy/go-json"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/config"
|
||||
"github.com/sirrobot01/debrid-blackhole/internal/logger"
|
||||
@@ -13,6 +14,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
@@ -41,6 +43,7 @@ func (s *Server) Start(ctx context.Context) error {
|
||||
|
||||
// Register logs
|
||||
s.router.Get("/logs", s.getLogs)
|
||||
s.router.Get("/stats", s.getStats)
|
||||
port := fmt.Sprintf(":%s", cfg.QBitTorrent.Port)
|
||||
s.logger.Info().Msgf("Starting server on %s", port)
|
||||
srv := &http.Server{
|
||||
@@ -102,3 +105,29 @@ func (s *Server) getLogs(w http.ResponseWriter, r *http.Request) {
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<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>
|
||||
<option value="info">Info</option>
|
||||
<option value="debug">Debug</option>
|
||||
@@ -86,13 +86,13 @@
|
||||
</div>
|
||||
<!-- Debrid Configuration -->
|
||||
<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>
|
||||
|
||||
<!-- QBitTorrent Configuration -->
|
||||
<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="col-md-6 mb-3">
|
||||
<label class="form-label">Username</label>
|
||||
@@ -114,12 +114,16 @@
|
||||
<label class="form-label">Refresh Interval (seconds)</label>
|
||||
<input type="number" class="form-control" name="qbit.refresh_interval">
|
||||
</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>
|
||||
|
||||
<!-- Arr Configurations -->
|
||||
<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>
|
||||
|
||||
@@ -141,6 +145,10 @@
|
||||
<input type="checkbox" disabled class="form-check-input" name="repair.enabled" id="repairEnabled">
|
||||
<label class="form-check-label" for="repairEnabled">Enable Repair</label>
|
||||
</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">
|
||||
<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>
|
||||
@@ -159,7 +167,7 @@
|
||||
// Templates for dynamic elements
|
||||
const debridTemplate = (index) => `
|
||||
<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">
|
||||
<label class="form-label">Name</label>
|
||||
<input type="text" disabled class="form-control" name="debrid[${index}].name" required>
|
||||
@@ -191,6 +199,47 @@
|
||||
</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>
|
||||
`;
|
||||
|
||||
@@ -360,16 +409,37 @@
|
||||
container.insertAdjacentHTML('beforeend', debridTemplate(debridCount));
|
||||
|
||||
if (data) {
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
const input = container.querySelector(`[name="debrid[${debridCount}].${key}"]`);
|
||||
if (input) {
|
||||
if (input.type === 'checkbox') {
|
||||
input.checked = value;
|
||||
} else {
|
||||
input.value = value;
|
||||
}
|
||||
|
||||
if (data.use_webdav) {
|
||||
let _webCfg = container.querySelector(`.webdav-${debridCount}`);
|
||||
if (_webCfg) {
|
||||
_webCfg.classList.remove('d-none');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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++;
|
||||
|
||||
@@ -117,6 +117,18 @@
|
||||
background-color: rgba(128, 128, 128, 0.2);
|
||||
}
|
||||
</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>
|
||||
<body>
|
||||
<div class="toast-container position-fixed bottom-0 end-0 p-3">
|
||||
@@ -149,7 +161,12 @@
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<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>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
|
||||
@@ -31,6 +31,8 @@ type File struct {
|
||||
fileId string
|
||||
torrentId string
|
||||
|
||||
modTime time.Time
|
||||
|
||||
size int64
|
||||
offset int64
|
||||
isDir bool
|
||||
@@ -176,7 +178,7 @@ func (f *File) Stat() (os.FileInfo, error) {
|
||||
name: f.name,
|
||||
size: 0,
|
||||
mode: 0755 | os.ModeDir,
|
||||
modTime: time.Now(),
|
||||
modTime: f.modTime,
|
||||
isDir: true,
|
||||
}, nil
|
||||
}
|
||||
@@ -185,7 +187,7 @@ func (f *File) Stat() (os.FileInfo, error) {
|
||||
name: f.name,
|
||||
size: f.size,
|
||||
mode: 0644,
|
||||
modTime: time.Now(),
|
||||
modTime: f.modTime,
|
||||
isDir: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -116,6 +116,8 @@ func (h *Handler) OpenFile(ctx context.Context, name string, flag int, perm os.F
|
||||
metadataOnly = true
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
// Fast path optimization with a map lookup instead of string comparisons
|
||||
switch name {
|
||||
case rootDir:
|
||||
@@ -125,6 +127,7 @@ func (h *Handler) OpenFile(ctx context.Context, name string, flag int, perm os.F
|
||||
children: h.getParentFiles(),
|
||||
name: "/",
|
||||
metadataOnly: metadataOnly,
|
||||
modTime: now,
|
||||
}, nil
|
||||
case path.Join(rootDir, "version.txt"):
|
||||
return &File{
|
||||
@@ -134,6 +137,7 @@ func (h *Handler) OpenFile(ctx context.Context, name string, flag int, perm os.F
|
||||
name: "version.txt",
|
||||
size: int64(len("v1.0.0")),
|
||||
metadataOnly: metadataOnly,
|
||||
modTime: now,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -152,6 +156,7 @@ func (h *Handler) OpenFile(ctx context.Context, name string, flag int, perm os.F
|
||||
name: folderName,
|
||||
size: 0,
|
||||
metadataOnly: metadataOnly,
|
||||
modTime: now,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -177,6 +182,7 @@ func (h *Handler) OpenFile(ctx context.Context, name string, flag int, perm os.F
|
||||
name: cachedTorrent.Name,
|
||||
size: cachedTorrent.Size,
|
||||
metadataOnly: metadataOnly,
|
||||
modTime: cachedTorrent.AddedOn,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -192,6 +198,7 @@ func (h *Handler) OpenFile(ctx context.Context, name string, flag int, perm os.F
|
||||
size: file.Size,
|
||||
link: file.Link,
|
||||
metadataOnly: metadataOnly,
|
||||
modTime: cachedTorrent.AddedOn,
|
||||
}
|
||||
return fi, nil
|
||||
}
|
||||
@@ -457,7 +464,12 @@ func (h *Handler) serveDirectory(w http.ResponseWriter, r *http.Request, file we
|
||||
}
|
||||
|
||||
// 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 {
|
||||
h.logger.Error().Err(err).Msg("Failed to parse directory template")
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
|
||||
@@ -73,6 +73,8 @@ const directoryTemplate = `
|
||||
display: block;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
padding-left: 50px; /* Make room for the number */
|
||||
}
|
||||
a:hover {
|
||||
background-color: #f7f9fa;
|
||||
@@ -85,23 +87,40 @@ const directoryTemplate = `
|
||||
.parent-dir {
|
||||
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>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Index of {{.Path}}</h1>
|
||||
<ul>
|
||||
{{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}}
|
||||
{{range .Children}}
|
||||
{{range $index, $file := .Children}}
|
||||
<li>
|
||||
<a href="{{$.Path}}/{{.Name}}">
|
||||
{{.Name}}{{if .IsDir}}/{{end}}
|
||||
<a href="{{$.Path}}/{{$file.Name}}">
|
||||
<span class="file-number">{{add $index 1}}.</span>
|
||||
<span class="file-name">{{$file.Name}}{{if $file.IsDir}}/{{end}}</span>
|
||||
<span class="file-info">
|
||||
{{if not .IsDir}}
|
||||
{{.Size}} bytes
|
||||
{{if not $file.IsDir}}
|
||||
{{$file.Size}} bytes
|
||||
{{end}}
|
||||
{{.ModTime.Format "2006-01-02 15:04:05"}}
|
||||
{{$file.ModTime.Format "2006-01-02 15:04:05"}}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
Reference in New Issue
Block a user