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 (
|
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
@@ -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
@@ -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()
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user