Changelog 0.6.0
This commit is contained in:
@@ -170,6 +170,12 @@ func (as *Storage) GetAll() []*Arr {
|
||||
return arrs
|
||||
}
|
||||
|
||||
func (as *Storage) Clear() {
|
||||
as.mu.Lock()
|
||||
defer as.mu.Unlock()
|
||||
as.Arrs = make(map[string]*Arr)
|
||||
}
|
||||
|
||||
func (a *Arr) Refresh() error {
|
||||
payload := struct {
|
||||
Name string `json:"name"`
|
||||
|
||||
@@ -57,7 +57,7 @@ func New(dc config.Debrid) *AllDebrid {
|
||||
}
|
||||
return &AllDebrid{
|
||||
Name: "alldebrid",
|
||||
Host: dc.Host,
|
||||
Host: "http://api.alldebrid.com/v4.1",
|
||||
APIKey: dc.APIKey,
|
||||
DownloadKeys: accounts,
|
||||
DownloadUncached: dc.DownloadUncached,
|
||||
|
||||
+18
-23
@@ -15,7 +15,6 @@ import (
|
||||
"github.com/sirrobot01/decypharr/pkg/debrid/types"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -111,10 +110,6 @@ func New(dc config.Debrid, client types.Client) *Cache {
|
||||
if err != nil {
|
||||
autoExpiresLinksAfter = time.Hour * 24
|
||||
}
|
||||
workers := runtime.NumCPU() * 50
|
||||
if dc.Workers > 0 {
|
||||
workers = dc.Workers
|
||||
}
|
||||
return &Cache{
|
||||
dir: filepath.Join(cfg.Path, "cache", dc.Name), // path to save cache files
|
||||
torrents: xsync.NewMapOf[string, *CachedTorrent](),
|
||||
@@ -122,7 +117,7 @@ func New(dc config.Debrid, client types.Client) *Cache {
|
||||
invalidDownloadLinks: xsync.NewMapOf[string, string](),
|
||||
client: client,
|
||||
logger: logger.New(fmt.Sprintf("%s-webdav", client.GetName())),
|
||||
workers: workers,
|
||||
workers: dc.Workers,
|
||||
downloadLinks: xsync.NewMapOf[string, downloadLinkCache](),
|
||||
torrentRefreshInterval: torrentRefreshInterval,
|
||||
downloadLinksRefreshInterval: downloadLinksRefreshInterval,
|
||||
@@ -211,13 +206,13 @@ func (c *Cache) load() (map[string]*CachedTorrent, error) {
|
||||
filePath := filepath.Join(c.dir, fileName)
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
c.logger.Debug().Err(err).Msgf("Failed to read file: %s", filePath)
|
||||
c.logger.Error().Err(err).Msgf("Failed to read file: %s", filePath)
|
||||
continue
|
||||
}
|
||||
|
||||
var ct CachedTorrent
|
||||
if err := json.Unmarshal(data, &ct); err != nil {
|
||||
c.logger.Debug().Err(err).Msgf("Failed to unmarshal file: %s", filePath)
|
||||
c.logger.Error().Err(err).Msgf("Failed to unmarshal file: %s", filePath)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -271,7 +266,7 @@ func (c *Cache) Sync() error {
|
||||
defer c.logger.Info().Msg("WebDav server sync complete")
|
||||
cachedTorrents, err := c.load()
|
||||
if err != nil {
|
||||
c.logger.Debug().Err(err).Msg("Failed to load cache")
|
||||
c.logger.Error().Err(err).Msg("Failed to load cache")
|
||||
}
|
||||
|
||||
torrents, err := c.client.GetTorrents()
|
||||
@@ -465,7 +460,7 @@ func (c *Cache) SaveTorrents() {
|
||||
func (c *Cache) SaveTorrent(ct *CachedTorrent) {
|
||||
marshaled, err := json.MarshalIndent(ct, "", " ")
|
||||
if err != nil {
|
||||
c.logger.Debug().Err(err).Msgf("Failed to marshal torrent: %s", ct.Id)
|
||||
c.logger.Error().Err(err).Msgf("Failed to marshal torrent: %s", ct.Id)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -500,7 +495,7 @@ func (c *Cache) saveTorrent(id string, data []byte) {
|
||||
|
||||
f, err := os.Create(tmpFile)
|
||||
if err != nil {
|
||||
c.logger.Debug().Err(err).Msgf("Failed to create file: %s", tmpFile)
|
||||
c.logger.Error().Err(err).Msgf("Failed to create file: %s", tmpFile)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -517,12 +512,12 @@ func (c *Cache) saveTorrent(id string, data []byte) {
|
||||
|
||||
w := bufio.NewWriter(f)
|
||||
if _, err := w.Write(data); err != nil {
|
||||
c.logger.Debug().Err(err).Msgf("Failed to write data: %s", tmpFile)
|
||||
c.logger.Error().Err(err).Msgf("Failed to write data: %s", tmpFile)
|
||||
return
|
||||
}
|
||||
|
||||
if err := w.Flush(); err != nil {
|
||||
c.logger.Debug().Err(err).Msgf("Failed to flush data: %s", tmpFile)
|
||||
c.logger.Error().Err(err).Msgf("Failed to flush data: %s", tmpFile)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -531,7 +526,7 @@ func (c *Cache) saveTorrent(id string, data []byte) {
|
||||
fileClosed = true
|
||||
|
||||
if err := os.Rename(tmpFile, filePath); err != nil {
|
||||
c.logger.Debug().Err(err).Msgf("Failed to rename file: %s", tmpFile)
|
||||
c.logger.Error().Err(err).Msgf("Failed to rename file: %s", tmpFile)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -559,7 +554,7 @@ func (c *Cache) ProcessTorrent(t *types.Torrent, refreshRclone bool) error {
|
||||
c.logger.Debug().Msgf("Torrent %s is still not complete. Triggering a reinsert(disabled)", t.Id)
|
||||
//ct, err := c.reInsertTorrent(t)
|
||||
//if err != nil {
|
||||
// c.logger.Debug().Err(err).Msgf("Failed to reinsert torrent %s", t.Id)
|
||||
// c.logger.Error().Err(err).Msgf("Failed to reinsert torrent %s", t.Id)
|
||||
// return err
|
||||
//}
|
||||
//c.logger.Debug().Msgf("Reinserted torrent %s", ct.Id)
|
||||
@@ -610,9 +605,9 @@ func (c *Cache) GetDownloadLink(torrentId, filename, fileLink string) string {
|
||||
if file.Link == "" {
|
||||
c.logger.Debug().Msgf("File link is empty for %s. Release is probably nerfed", filename)
|
||||
// Try to reinsert the torrent?
|
||||
ct, err := c.reInsertTorrent(ct.Torrent)
|
||||
ct, err := c.reInsertTorrent(ct)
|
||||
if err != nil {
|
||||
c.logger.Debug().Err(err).Msgf("Failed to reinsert torrent %s", ct.Name)
|
||||
c.logger.Error().Err(err).Msgf("Failed to reinsert torrent %s", ct.Name)
|
||||
return ""
|
||||
}
|
||||
file = ct.Files[filename]
|
||||
@@ -623,10 +618,10 @@ 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) {
|
||||
c.logger.Debug().Err(err).Msgf("Hoster is unavailable. Triggering repair for %s", ct.Name)
|
||||
ct, err := c.reInsertTorrent(ct.Torrent)
|
||||
c.logger.Error().Err(err).Msgf("Hoster is unavailable. Triggering repair for %s", ct.Name)
|
||||
ct, err := c.reInsertTorrent(ct)
|
||||
if err != nil {
|
||||
c.logger.Debug().Err(err).Msgf("Failed to reinsert torrent %s", ct.Name)
|
||||
c.logger.Error().Err(err).Msgf("Failed to reinsert torrent %s", ct.Name)
|
||||
return ""
|
||||
}
|
||||
c.logger.Debug().Msgf("Reinserted torrent %s", ct.Name)
|
||||
@@ -634,7 +629,7 @@ func (c *Cache) GetDownloadLink(torrentId, filename, fileLink string) string {
|
||||
// Retry getting the download link
|
||||
downloadLink, err = c.client.GetDownloadLink(ct.Torrent, &file)
|
||||
if err != nil {
|
||||
c.logger.Debug().Err(err).Msgf("Failed to get download link for %s", file.Link)
|
||||
c.logger.Error().Err(err).Msgf("Failed to get download link for %s", file.Link)
|
||||
return ""
|
||||
}
|
||||
if downloadLink == nil {
|
||||
@@ -645,9 +640,9 @@ func (c *Cache) GetDownloadLink(torrentId, filename, fileLink string) string {
|
||||
return downloadLink.DownloadLink
|
||||
} else if errors.Is(err, request.TrafficExceededError) {
|
||||
// This is likely a fair usage limit error
|
||||
c.logger.Debug().Err(err).Msgf("Traffic exceeded for %s", ct.Name)
|
||||
c.logger.Error().Err(err).Msgf("Traffic exceeded for %s", ct.Name)
|
||||
} else {
|
||||
c.logger.Debug().Err(err).Msgf("Failed to get download link for %s", file.Link)
|
||||
c.logger.Error().Err(err).Msgf("Failed to get download link for %s", file.Link)
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,14 @@ package debrid
|
||||
import (
|
||||
"github.com/sirrobot01/decypharr/internal/config"
|
||||
"github.com/sirrobot01/decypharr/pkg/debrid/types"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Engine struct {
|
||||
Clients map[string]types.Client
|
||||
clientsMu sync.Mutex
|
||||
Caches map[string]*Cache
|
||||
CacheMu sync.Mutex
|
||||
LastUsed string
|
||||
}
|
||||
|
||||
@@ -37,16 +40,9 @@ func NewEngine() *Engine {
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *Engine) Get() types.Client {
|
||||
if d.LastUsed == "" {
|
||||
for _, c := range d.Clients {
|
||||
return c
|
||||
}
|
||||
}
|
||||
return d.Clients[d.LastUsed]
|
||||
}
|
||||
|
||||
func (d *Engine) GetByName(name string) types.Client {
|
||||
func (d *Engine) GetClient(name string) types.Client {
|
||||
d.clientsMu.Lock()
|
||||
defer d.clientsMu.Unlock()
|
||||
return d.Clients[name]
|
||||
}
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ func (c *Cache) refreshTorrents() {
|
||||
// Get all torrents from the debrid service
|
||||
debTorrents, err := c.client.GetTorrents()
|
||||
if err != nil {
|
||||
c.logger.Debug().Err(err).Msg("Failed to get torrents")
|
||||
c.logger.Error().Err(err).Msg("Failed to get torrents")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ func (c *Cache) refreshTorrents() {
|
||||
default:
|
||||
}
|
||||
if err := c.ProcessTorrent(t, true); err != nil {
|
||||
c.logger.Debug().Err(err).Msgf("Failed to process new torrent %s", t.Id)
|
||||
c.logger.Error().Err(err).Msgf("Failed to process new torrent %s", t.Id)
|
||||
errChan <- err
|
||||
}
|
||||
}
|
||||
@@ -208,7 +208,7 @@ func (c *Cache) refreshDownloadLinks() {
|
||||
|
||||
downloadLinks, err := c.client.GetDownloads()
|
||||
if err != nil {
|
||||
c.logger.Debug().Err(err).Msg("Failed to get download links")
|
||||
c.logger.Error().Err(err).Msg("Failed to get download links")
|
||||
}
|
||||
for k, v := range downloadLinks {
|
||||
// if link is generated in the last 24 hours, add it to cache
|
||||
@@ -225,6 +225,6 @@ func (c *Cache) refreshDownloadLinks() {
|
||||
}
|
||||
}
|
||||
|
||||
c.logger.Debug().Msgf("Refreshed %d download links", len(downloadLinks))
|
||||
c.logger.Trace().Msgf("Refreshed %d download links", len(downloadLinks))
|
||||
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ func (c *Cache) repairWorker() {
|
||||
case RepairTypeReinsert:
|
||||
c.logger.Debug().Str("torrentId", torrentId).Msg("Reinserting torrent")
|
||||
var err error
|
||||
cachedTorrent, err = c.reInsertTorrent(cachedTorrent.Torrent)
|
||||
cachedTorrent, err = c.reInsertTorrent(cachedTorrent)
|
||||
if err != nil {
|
||||
c.logger.Error().Err(err).Str("torrentId", cachedTorrent.Id).Msg("Failed to reinsert torrent")
|
||||
continue
|
||||
@@ -96,10 +96,11 @@ func (c *Cache) repairWorker() {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) reInsertTorrent(torrent *types.Torrent) (*CachedTorrent, error) {
|
||||
func (c *Cache) reInsertTorrent(ct *CachedTorrent) (*CachedTorrent, error) {
|
||||
// Check if Magnet is not empty, if empty, reconstruct the magnet
|
||||
torrent := ct.Torrent
|
||||
if _, ok := c.repairsInProgress.Load(torrent.Id); ok {
|
||||
return nil, fmt.Errorf("repair already in progress for torrent %s", torrent.Id)
|
||||
return ct, fmt.Errorf("repair already in progress for torrent %s", torrent.Id)
|
||||
}
|
||||
|
||||
if torrent.Magnet == nil {
|
||||
@@ -152,7 +153,7 @@ func (c *Cache) reInsertTorrent(torrent *types.Torrent) (*CachedTorrent, error)
|
||||
if err != nil {
|
||||
addedOn = time.Now()
|
||||
}
|
||||
ct := &CachedTorrent{
|
||||
ct = &CachedTorrent{
|
||||
Torrent: torrent,
|
||||
IsComplete: len(torrent.Files) > 0,
|
||||
AddedOn: addedOn,
|
||||
|
||||
@@ -299,7 +299,7 @@ func New(dc config.Debrid) *DebridLink {
|
||||
}
|
||||
return &DebridLink{
|
||||
Name: "debridlink",
|
||||
Host: dc.Host,
|
||||
Host: "https://debrid-link.com/api/v2",
|
||||
APIKey: dc.APIKey,
|
||||
DownloadKeys: accounts,
|
||||
DownloadUncached: dc.DownloadUncached,
|
||||
|
||||
@@ -84,7 +84,7 @@ func New(dc config.Debrid) *RealDebrid {
|
||||
|
||||
return &RealDebrid{
|
||||
Name: "realdebrid",
|
||||
Host: dc.Host,
|
||||
Host: "https://api.real-debrid.com/rest/1.0",
|
||||
APIKey: dc.APIKey,
|
||||
DownloadKeys: accounts,
|
||||
DownloadUncached: dc.DownloadUncached,
|
||||
|
||||
@@ -62,7 +62,7 @@ func New(dc config.Debrid) *Torbox {
|
||||
|
||||
return &Torbox{
|
||||
Name: "torbox",
|
||||
Host: dc.Host,
|
||||
Host: "https://api.torbox.app/v1",
|
||||
APIKey: dc.APIKey,
|
||||
DownloadKeys: accounts,
|
||||
DownloadUncached: dc.DownloadUncached,
|
||||
|
||||
@@ -58,7 +58,7 @@ func (t *Torrent) GetSymlinkFolder(parent string) string {
|
||||
}
|
||||
|
||||
func (t *Torrent) GetMountFolder(rClonePath string) (string, error) {
|
||||
_log := logger.GetDefaultLogger()
|
||||
_log := logger.Default()
|
||||
possiblePaths := []string{
|
||||
t.OriginalFilename,
|
||||
t.Filename,
|
||||
|
||||
+16
-7
@@ -226,26 +226,34 @@ func (q *QBit) preCacheFile(name string, filePaths []string) error {
|
||||
}
|
||||
|
||||
for _, filePath := range filePaths {
|
||||
func(f string) {
|
||||
err := func(f string) error {
|
||||
|
||||
file, err := os.Open(f)
|
||||
if err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Pre-cache the file header (first 256KB) using 16KB chunks.
|
||||
q.readSmallChunks(file, 0, 256*1024, 16*1024)
|
||||
q.readSmallChunks(file, 1024*1024, 64*1024, 16*1024)
|
||||
if err := q.readSmallChunks(file, 0, 256*1024, 16*1024); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := q.readSmallChunks(file, 1024*1024, 64*1024, 16*1024); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (q *QBit) readSmallChunks(file *os.File, startPos int64, totalToRead int, chunkSize int) {
|
||||
func (q *QBit) readSmallChunks(file *os.File, startPos int64, totalToRead int, chunkSize int) error {
|
||||
_, err := file.Seek(startPos, 0)
|
||||
if err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
buf := make([]byte, chunkSize)
|
||||
@@ -262,9 +270,10 @@ func (q *QBit) readSmallChunks(file *os.File, startPos int64, totalToRead int, c
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
bytesRemaining -= n
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
+1
-1
@@ -231,7 +231,7 @@ func (q *QBit) handleTorrentsDelete(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
category := ctx.Value("category").(string)
|
||||
for _, hash := range hashes {
|
||||
q.Storage.Delete(hash, category)
|
||||
q.Storage.Delete(hash, category, false)
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
+1
-1
@@ -73,7 +73,7 @@ func (i *ImportRequest) Process(q *QBit) (err error) {
|
||||
debridTorrent, err := debrid.ProcessTorrent(svc.Debrid, i.Magnet, i.Arr, i.IsSymlink, i.DownloadUncached)
|
||||
if err != nil || debridTorrent == nil {
|
||||
if debridTorrent != nil {
|
||||
dbClient := service.GetDebrid().GetByName(debridTorrent.Debrid)
|
||||
dbClient := service.GetDebrid().GetClient(debridTorrent.Debrid)
|
||||
go func() {
|
||||
_ = dbClient.DeleteTorrent(debridTorrent.Id)
|
||||
}()
|
||||
|
||||
+1
-2
@@ -1,14 +1,13 @@
|
||||
package qbit
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/sirrobot01/decypharr/internal/utils"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func createTorrentFromMagnet(magnet *utils.Magnet, category, source string) *Torrent {
|
||||
torrent := &Torrent{
|
||||
ID: uuid.NewString(),
|
||||
ID: "",
|
||||
Hash: strings.ToLower(magnet.InfoHash),
|
||||
Name: magnet.Name,
|
||||
Size: magnet.Size,
|
||||
|
||||
+44
-3
@@ -3,6 +3,7 @@ package qbit
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/goccy/go-json"
|
||||
"github.com/sirrobot01/decypharr/pkg/service"
|
||||
"os"
|
||||
"sort"
|
||||
"sync"
|
||||
@@ -166,7 +167,7 @@ func (ts *TorrentStorage) Update(torrent *Torrent) {
|
||||
}()
|
||||
}
|
||||
|
||||
func (ts *TorrentStorage) Delete(hash, category string) {
|
||||
func (ts *TorrentStorage) Delete(hash, category string, removeFromDebrid bool) {
|
||||
ts.mu.Lock()
|
||||
defer ts.mu.Unlock()
|
||||
key := keyPair(hash, category)
|
||||
@@ -181,10 +182,22 @@ func (ts *TorrentStorage) Delete(hash, category string) {
|
||||
}
|
||||
}
|
||||
}
|
||||
delete(ts.torrents, key)
|
||||
|
||||
if torrent == nil {
|
||||
return
|
||||
}
|
||||
if removeFromDebrid && torrent.ID != "" && torrent.Debrid != "" {
|
||||
dbClient := service.GetDebrid().GetClient(torrent.Debrid)
|
||||
if dbClient != nil {
|
||||
err := dbClient.DeleteTorrent(torrent.ID)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delete(ts.torrents, key)
|
||||
|
||||
// Delete the torrent folder
|
||||
if torrent.ContentPath != "" {
|
||||
err := os.RemoveAll(torrent.ContentPath)
|
||||
@@ -200,13 +213,27 @@ func (ts *TorrentStorage) Delete(hash, category string) {
|
||||
}()
|
||||
}
|
||||
|
||||
func (ts *TorrentStorage) DeleteMultiple(hashes []string) {
|
||||
func (ts *TorrentStorage) DeleteMultiple(hashes []string, removeFromDebrid bool) {
|
||||
ts.mu.Lock()
|
||||
defer ts.mu.Unlock()
|
||||
toDelete := make(map[string]string)
|
||||
|
||||
for _, hash := range hashes {
|
||||
for key, torrent := range ts.torrents {
|
||||
if torrent == nil {
|
||||
continue
|
||||
}
|
||||
if torrent.Hash == hash {
|
||||
if removeFromDebrid && torrent.ID != "" && torrent.Debrid != "" {
|
||||
toDelete[torrent.ID] = torrent.Debrid
|
||||
}
|
||||
delete(ts.torrents, key)
|
||||
if torrent.ContentPath != "" {
|
||||
err := os.RemoveAll(torrent.ContentPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -216,6 +243,20 @@ func (ts *TorrentStorage) DeleteMultiple(hashes []string) {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
for id, debrid := range toDelete {
|
||||
dbClient := service.GetDebrid().GetClient(debrid)
|
||||
if dbClient == nil {
|
||||
continue
|
||||
}
|
||||
fmt.Println("Deleting torrent from debrid:", id)
|
||||
err := dbClient.DeleteTorrent(id)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (ts *TorrentStorage) Save() error {
|
||||
|
||||
+3
-3
@@ -59,7 +59,7 @@ func (q *QBit) Process(ctx context.Context, magnet *utils.Magnet, category strin
|
||||
debridTorrent, err := db.ProcessTorrent(svc.Debrid, magnet, a, isSymlink, false)
|
||||
if err != nil || debridTorrent == nil {
|
||||
if debridTorrent != nil {
|
||||
dbClient := service.GetDebrid().GetByName(debridTorrent.Debrid)
|
||||
dbClient := service.GetDebrid().GetClient(debridTorrent.Debrid)
|
||||
go func() {
|
||||
_ = dbClient.DeleteTorrent(debridTorrent.Id)
|
||||
}()
|
||||
@@ -77,7 +77,7 @@ func (q *QBit) Process(ctx context.Context, magnet *utils.Magnet, category strin
|
||||
|
||||
func (q *QBit) ProcessFiles(torrent *Torrent, debridTorrent *debrid.Torrent, arr *arr.Arr, isSymlink bool) {
|
||||
svc := service.GetService()
|
||||
client := svc.Debrid.GetByName(debridTorrent.Debrid)
|
||||
client := svc.Debrid.GetClient(debridTorrent.Debrid)
|
||||
for debridTorrent.Status != "downloaded" {
|
||||
q.logger.Debug().Msgf("%s <- (%s) Download Progress: %.2f%%", debridTorrent.Debrid, debridTorrent.Name, debridTorrent.Progress)
|
||||
dbT, err := client.CheckStatus(debridTorrent, isSymlink)
|
||||
@@ -216,7 +216,7 @@ func (q *QBit) UpdateTorrent(t *Torrent, debridTorrent *debrid.Torrent) *Torrent
|
||||
if debridTorrent == nil {
|
||||
return t
|
||||
}
|
||||
_db := service.GetDebrid().GetByName(debridTorrent.Debrid)
|
||||
_db := service.GetDebrid().GetClient(debridTorrent.Debrid)
|
||||
if debridTorrent.Status != "downloaded" {
|
||||
_ = _db.UpdateTorrent(debridTorrent)
|
||||
}
|
||||
|
||||
@@ -217,7 +217,7 @@ func (r *Repair) preRunChecks() error {
|
||||
}
|
||||
resp, err := http.Get(fmt.Sprint(r.ZurgURL, "/http/version.txt"))
|
||||
if err != nil {
|
||||
r.logger.Debug().Err(err).Msgf("Precheck failed: Failed to reach zurg at %s", r.ZurgURL)
|
||||
r.logger.Error().Err(err).Msgf("Precheck failed: Failed to reach zurg at %s", r.ZurgURL)
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
@@ -525,7 +525,7 @@ func (r *Repair) getZurgBrokenFiles(media arr.Content) []arr.ContentFile {
|
||||
|
||||
resp, err := client.Get(fullURL)
|
||||
if err != nil {
|
||||
r.logger.Debug().Err(err).Msgf("Failed to reach %s", fullURL)
|
||||
r.logger.Error().Err(err).Msgf("Failed to reach %s", fullURL)
|
||||
brokenFiles = append(brokenFiles, f...)
|
||||
continue
|
||||
}
|
||||
@@ -737,7 +737,7 @@ func (r *Repair) saveToFile() {
|
||||
// Save jobs to file
|
||||
data, err := json.Marshal(r.Jobs)
|
||||
if err != nil {
|
||||
r.logger.Debug().Err(err).Msg("Failed to marshal jobs")
|
||||
r.logger.Error().Err(err).Msg("Failed to marshal jobs")
|
||||
}
|
||||
_ = os.WriteFile(r.filename, data, 0644)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -44,7 +45,8 @@ 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)
|
||||
p := cmp.Or(cfg.QBitTorrent.Port, "8282")
|
||||
port := fmt.Sprintf(":%s", p)
|
||||
s.logger.Info().Msgf("Server started on %s", port)
|
||||
srv := &http.Server{
|
||||
Addr: port,
|
||||
@@ -86,7 +88,7 @@ func (s *Server) getLogs(w http.ResponseWriter, r *http.Request) {
|
||||
defer func(file *os.File) {
|
||||
err := file.Close()
|
||||
if err != nil {
|
||||
s.logger.Debug().Err(err).Msg("Error closing log file")
|
||||
s.logger.Error().Err(err).Msg("Error closing log file")
|
||||
}
|
||||
}(file)
|
||||
|
||||
@@ -100,7 +102,7 @@ func (s *Server) getLogs(w http.ResponseWriter, r *http.Request) {
|
||||
// Stream the file
|
||||
_, err = io.Copy(w, file)
|
||||
if err != nil {
|
||||
s.logger.Debug().Err(err).Msg("Error streaming log file")
|
||||
s.logger.Error().Err(err).Msg("Error streaming log file")
|
||||
http.Error(w, "Error streaming log file", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ var (
|
||||
)
|
||||
|
||||
func New() *Service {
|
||||
once = sync.Once{}
|
||||
once.Do(func() {
|
||||
arrs := arr.NewStorage()
|
||||
deb := debrid.NewEngine()
|
||||
|
||||
+3
-2
@@ -10,8 +10,8 @@ func (ui *Handler) Routes() http.Handler {
|
||||
|
||||
r.Get("/login", ui.LoginHandler)
|
||||
r.Post("/login", ui.LoginHandler)
|
||||
r.Get("/setup", ui.SetupHandler)
|
||||
r.Post("/setup", ui.SetupHandler)
|
||||
r.Get("/auth", ui.SetupHandler)
|
||||
r.Post("/auth", ui.SetupHandler)
|
||||
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(ui.authMiddleware)
|
||||
@@ -30,6 +30,7 @@ func (ui *Handler) Routes() http.Handler {
|
||||
r.Delete("/torrents/{category}/{hash}", ui.handleDeleteTorrent)
|
||||
r.Delete("/torrents/", ui.handleDeleteTorrents)
|
||||
r.Get("/config", ui.handleGetConfig)
|
||||
r.Post("/config", ui.handleUpdateConfig)
|
||||
r.Get("/version", ui.handleGetVersion)
|
||||
})
|
||||
})
|
||||
|
||||
+90
-16
@@ -15,6 +15,7 @@ import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/rs/zerolog"
|
||||
@@ -22,6 +23,13 @@ import (
|
||||
"github.com/sirrobot01/decypharr/pkg/version"
|
||||
)
|
||||
|
||||
var restartFunc func()
|
||||
|
||||
// SetRestartFunc allows setting a callback to restart services
|
||||
func SetRestartFunc(fn func()) {
|
||||
restartFunc = fn
|
||||
}
|
||||
|
||||
type AddRequest struct {
|
||||
Url string `json:"url"`
|
||||
Arr string `json:"arr"`
|
||||
@@ -51,7 +59,7 @@ type RepairRequest struct {
|
||||
AutoProcess bool `json:"autoProcess"`
|
||||
}
|
||||
|
||||
//go:embed web/*
|
||||
//go:embed templates/*
|
||||
var content embed.FS
|
||||
|
||||
type Handler struct {
|
||||
@@ -67,20 +75,20 @@ func New(qbit *qbit.QBit) *Handler {
|
||||
}
|
||||
|
||||
var (
|
||||
store = sessions.NewCookieStore([]byte("your-secret-key")) // Change this to a secure key
|
||||
store = sessions.NewCookieStore([]byte("your-secret-key"))
|
||||
templates *template.Template
|
||||
)
|
||||
|
||||
func init() {
|
||||
templates = template.Must(template.ParseFS(
|
||||
content,
|
||||
"web/layout.html",
|
||||
"web/index.html",
|
||||
"web/download.html",
|
||||
"web/repair.html",
|
||||
"web/config.html",
|
||||
"web/login.html",
|
||||
"web/setup.html",
|
||||
"templates/layout.html",
|
||||
"templates/index.html",
|
||||
"templates/download.html",
|
||||
"templates/repair.html",
|
||||
"templates/config.html",
|
||||
"templates/login.html",
|
||||
"templates/setup.html",
|
||||
))
|
||||
|
||||
store.Options = &sessions.Options{
|
||||
@@ -94,8 +102,8 @@ func (ui *Handler) authMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Check if setup is needed
|
||||
cfg := config.Get()
|
||||
if cfg.NeedsSetup() && r.URL.Path != "/setup" {
|
||||
http.Redirect(w, r, "/setup", http.StatusSeeOther)
|
||||
if cfg.NeedsAuth() && r.URL.Path != "/auth" {
|
||||
http.Redirect(w, r, "/auth", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -105,7 +113,7 @@ func (ui *Handler) authMiddleware(next http.Handler) http.Handler {
|
||||
}
|
||||
|
||||
// Skip auth check for setup page
|
||||
if r.URL.Path == "/setup" {
|
||||
if r.URL.Path == "/auth" {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
@@ -195,8 +203,8 @@ func (ui *Handler) SetupHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if r.Method == "GET" {
|
||||
data := map[string]interface{}{
|
||||
"Page": "setup",
|
||||
"Title": "Setup",
|
||||
"Page": "auth",
|
||||
"Title": "Auth Setup",
|
||||
}
|
||||
_ = templates.ExecuteTemplate(w, "layout", data)
|
||||
return
|
||||
@@ -404,22 +412,24 @@ func (ui *Handler) handleGetTorrents(w http.ResponseWriter, r *http.Request) {
|
||||
func (ui *Handler) handleDeleteTorrent(w http.ResponseWriter, r *http.Request) {
|
||||
hash := chi.URLParam(r, "hash")
|
||||
category := r.URL.Query().Get("category")
|
||||
removeFromDebrid := r.URL.Query().Get("removeFromDebrid") == "true"
|
||||
if hash == "" {
|
||||
http.Error(w, "No hash provided", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
ui.qbit.Storage.Delete(hash, category)
|
||||
ui.qbit.Storage.Delete(hash, category, removeFromDebrid)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (ui *Handler) handleDeleteTorrents(w http.ResponseWriter, r *http.Request) {
|
||||
hashesStr := r.URL.Query().Get("hashes")
|
||||
removeFromDebrid := r.URL.Query().Get("removeFromDebrid") == "true"
|
||||
if hashesStr == "" {
|
||||
http.Error(w, "No hashes provided", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
hashes := strings.Split(hashesStr, ",")
|
||||
ui.qbit.Storage.DeleteMultiple(hashes)
|
||||
ui.qbit.Storage.DeleteMultiple(hashes, removeFromDebrid)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
@@ -441,6 +451,70 @@ func (ui *Handler) handleGetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
request.JSONResponse(w, cfg, http.StatusOK)
|
||||
}
|
||||
|
||||
func (ui *Handler) handleUpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
// Decode the JSON body
|
||||
var updatedConfig config.Config
|
||||
if err := json.NewDecoder(r.Body).Decode(&updatedConfig); err != nil {
|
||||
ui.logger.Error().Err(err).Msg("Failed to decode config update request")
|
||||
http.Error(w, "Invalid request body: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the current configuration
|
||||
currentConfig := config.Get()
|
||||
|
||||
// Update fields that can be changed
|
||||
currentConfig.LogLevel = updatedConfig.LogLevel
|
||||
currentConfig.MinFileSize = updatedConfig.MinFileSize
|
||||
currentConfig.MaxFileSize = updatedConfig.MaxFileSize
|
||||
currentConfig.AllowedExt = updatedConfig.AllowedExt
|
||||
currentConfig.DiscordWebhook = updatedConfig.DiscordWebhook
|
||||
|
||||
// Update QBitTorrent config
|
||||
currentConfig.QBitTorrent = updatedConfig.QBitTorrent
|
||||
|
||||
// Update Repair config
|
||||
currentConfig.Repair = updatedConfig.Repair
|
||||
|
||||
// Update Debrids
|
||||
if len(updatedConfig.Debrids) > 0 {
|
||||
currentConfig.Debrids = updatedConfig.Debrids
|
||||
// Clear legacy single debrid if using array
|
||||
|
||||
}
|
||||
|
||||
// Update Arrs through the service
|
||||
svc := service.GetService()
|
||||
svc.Arr.Clear() // Clear existing arrs
|
||||
|
||||
for _, a := range updatedConfig.Arrs {
|
||||
svc.Arr.AddOrUpdate(&arr.Arr{
|
||||
Name: a.Name,
|
||||
Host: a.Host,
|
||||
Token: a.Token,
|
||||
Cleanup: a.Cleanup,
|
||||
SkipRepair: a.SkipRepair,
|
||||
DownloadUncached: a.DownloadUncached,
|
||||
})
|
||||
}
|
||||
if err := currentConfig.Save(); err != nil {
|
||||
http.Error(w, "Error saving config: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if restartFunc != nil {
|
||||
go func() {
|
||||
// Small delay to ensure the response is sent
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
restartFunc()
|
||||
}()
|
||||
}
|
||||
|
||||
// Return success
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "success"})
|
||||
}
|
||||
|
||||
func (ui *Handler) handleGetRepairJobs(w http.ResponseWriter, r *http.Request) {
|
||||
svc := service.GetService()
|
||||
request.JSONResponse(w, svc.Repair.GetJobs(), http.StatusOK)
|
||||
|
||||
@@ -0,0 +1,626 @@
|
||||
{{ define "config" }}
|
||||
<div class="container mt-4">
|
||||
<form id="configForm">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center mb-2">
|
||||
<h4 class="mb-0"><i class="bi bi-gear me-2"></i>Configuration</h4>
|
||||
<button type="submit" class="btn btn-primary px-4">
|
||||
<i class="bi bi-save"></i> Save
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="section mb-5">
|
||||
<h5 class="border-bottom pb-2">General Configuration</h5>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="log-level">Log Level</label>
|
||||
<select class="form-select" name="log_level" id="log-level">
|
||||
<option value="info">Info</option>
|
||||
<option value="debug">Debug</option>
|
||||
<option value="warn">Warning</option>
|
||||
<option value="error">Error</option>
|
||||
<option value="trace">Trace</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Register Magnet Link Button -->
|
||||
<div class="col-md-6">
|
||||
<label>
|
||||
<!-- Empty label to keep the button aligned -->
|
||||
</label>
|
||||
<div class="btn btn-primary w-100" onclick="registerMagnetLinkHandler()" id="registerMagnetLink">
|
||||
Open Magnet Links in Decypharr
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 mt-3">
|
||||
<div class="form-group">
|
||||
<label for="discordWebhookUrl">Discord Webhook URL</label>
|
||||
<div class="input-group">
|
||||
<textarea
|
||||
class="form-control"
|
||||
id="discordWebhookUrl"
|
||||
name="discord_webhook_url"
|
||||
placeholder="https://discord..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 mt-3">
|
||||
<div class="form-group">
|
||||
<label for="allowedExtensions">Allowed File Extensions</label>
|
||||
<div class="input-group">
|
||||
<textarea
|
||||
class="form-control"
|
||||
id="allowedExtensions"
|
||||
name="allowed_file_types"
|
||||
placeholder="mkv, mp4, avi, etc.">
|
||||
</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 mt-3">
|
||||
<div class="form-group">
|
||||
<label for="minFileSize">Minimum File Size</label>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
id="minFileSize"
|
||||
name="min_file_size"
|
||||
placeholder="e.g., 10MB, 1GB">
|
||||
<small class="form-text text-muted">Minimum file size to download (0 for no limit)</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mt-3">
|
||||
<div class="form-group">
|
||||
<label for="maxFileSize">Maximum File Size</label>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
id="maxFileSize"
|
||||
name="max_file_size"
|
||||
placeholder="e.g., 50GB, 100MB">
|
||||
<small class="form-text text-muted">Maximum file size to download (0 for no limit)</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Debrid Configuration -->
|
||||
<div class="section mb-5">
|
||||
<h5 class="border-bottom pb-2">Debrids</h5>
|
||||
<div id="debridConfigs"></div>
|
||||
<div class="mb-3">
|
||||
<button type="button" id="addDebridBtn" class="btn btn-secondary">
|
||||
<i class="bi bi-plus"></i> Add New Debrid
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- QBitTorrent Configuration -->
|
||||
<div class="section mb-5">
|
||||
<h5 class="border-bottom pb-2">QBitTorrent</h5>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label" for="qbit.username">Username</label>
|
||||
<input type="text" class="form-control" name="qbit.username" id="qbit.username">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label" for="qbit.password">Password</label>
|
||||
<input type="password" class="form-control" name="qbit.password" id="qbit.password">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label" for="qbit.port">Port</label>
|
||||
<input type="text" class="form-control" name="qbit.port" id="qbit.port" placeholder="e.g., 8080">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label" for="qbit.download_folder">Symlink/Download Folder</label>
|
||||
<input type="text" class="form-control" name="qbit.download_folder" id="qbit.download_folder">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label" for="qbit.refresh_interval">Refresh Interval (seconds)</label>
|
||||
<input type="number" class="form-control" name="qbit.refresh_interval" id="qbit.refresh_interval">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<input type="checkbox" class="form-check-input" name="qbit.skip_pre_cache" id="qbit.skip_pre_cache">
|
||||
<label class="form-check-label" for="qbit.skip_pre_cache">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>
|
||||
<div id="arrConfigs"></div>
|
||||
<div class="mb-3">
|
||||
<button type="button" id="addArrBtn" class="btn btn-secondary">
|
||||
<i class="bi bi-plus"></i> Add New Arr
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Repair Configuration -->
|
||||
<div class="section mb-5">
|
||||
<h5 class="border-bottom pb-2">Repair Configuration</h5>
|
||||
<div class="row">
|
||||
<div class="col-md-3 mb-3">
|
||||
<label class="form-label">Interval</label>
|
||||
<input type="text" class="form-control" name="repair.interval" placeholder="e.g., 24h">
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label" >Zurg URL</label>
|
||||
<input type="text" class="form-control" name="repair.zurg_url" id="repair.zurg_url" placeholder="http://zurg:9999">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="form-check me-3 d-inline-block">
|
||||
<input type="checkbox" class="form-check-input" name="repair.enabled" id="repair.enabled">
|
||||
<label class="form-check-label" for="repair.enabled">Enable Repair</label>
|
||||
</div>
|
||||
<div class="form-check me-3 d-inline-block">
|
||||
<input type="checkbox" class="form-check-input" name="repair.use_webdav" id="repair.use_webdav">
|
||||
<label class="form-check-label" for="repair.use_webdav">Use Webdav</label>
|
||||
</div>
|
||||
<div class="form-check me-3 d-inline-block">
|
||||
<input type="checkbox" class="form-check-input" name="repair.run_on_start" id="repair.run_on_start">
|
||||
<label class="form-check-label" for="repair.run_on_start">Run on Start</label>
|
||||
</div>
|
||||
<div class="form-check d-inline-block">
|
||||
<input type="checkbox" class="form-check-input" name="repair.auto_process" id="repair.auto_process">
|
||||
<label class="form-check-label" for="repair.auto_process">Auto Process(Scheduled jobs will be processed automatically)</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-end mt-4 mb-3">
|
||||
<button type="submit" class="btn btn-primary px-4">
|
||||
<i class="bi bi-save"></i> Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<script>
|
||||
// Templates for dynamic elements
|
||||
const debridTemplate = (index) => `
|
||||
<div class="config-item position-relative mb-3 p-3 border rounded">
|
||||
<button type="button" class="btn btn-sm btn-danger position-absolute top-0 end-0 m-2"
|
||||
onclick="if(confirm('Are you sure you want to delete this debrid?')) this.closest('.config-item').remove();"
|
||||
title="Delete this debrid">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label" for="debrid[${index}].name" >Name</label>
|
||||
<input type="text" class="form-control" name="debrid[${index}].name" id="debrid[${index}].name" required>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label" for="debrid[${index}].api_key" >API Key</label>
|
||||
<input type="password" class="form-control" name="debrid[${index}].api_key" id="debrid[${index}].api_key" required>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label" for="debrid[${index}].folder" >Mount Folder</label>
|
||||
<input type="text" class="form-control" name="debrid[${index}].folder" id="debrid[${index}].folder">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label" for="debrid[${index}].rate_limit" >Rate Limit</label>
|
||||
<input type="text" class="form-control" name="debrid[${index}].rate_limit" id="debrid[${index}].rate_limit" placeholder="e.g., 200/minute">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="form-check me-3 d-inline-block">
|
||||
<input type="checkbox" class="form-check-input" name="debrid[${index}].download_uncached" id="debrid[${index}].download_uncached">
|
||||
<label class="form-check-label" for="debrid[${index}].download_uncached" >Download Uncached</label>
|
||||
</div>
|
||||
<div class="form-check me-3 d-inline-block">
|
||||
<input type="checkbox" class="form-check-input" name="debrid[${index}].check_cached" id="debrid[${index}].check_cached">
|
||||
<label class="form-check-label" for="debrid[${index}].check_cached" >Check Cached</label>
|
||||
</div>
|
||||
<div class="form-check d-inline-block">
|
||||
<input type="checkbox" class="form-check-input useWebdav" name="debrid[${index}].use_webdav" id="debrid[${index}].use_webdav">
|
||||
<label class="form-check-label" for="debrid[${index}].use_webdav" >Use WebDav</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3 webdav d-none">
|
||||
<h6 class="pb-2">Webdav</h6>
|
||||
<div class="col-md-3 mb-3">
|
||||
<label class="form-label" for="debrid[${index}].torrents_refresh_interval">Torrents Refresh Interval</label>
|
||||
<input type="text" class="form-control" name="debrid[${index}].torrents_refresh_interval" id="debrid[${index}].torrents_refresh_interval" placeholder="15s" required>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<label class="form-label" for="debrid[${index}].download_links_refresh_interval">Download Links Refresh Interval</label>
|
||||
<input type="text" class="form-control" name="debrid[${index}].download_links_refresh_interval" id="debrid[${index}].download_links_refresh_interval" placeholder="24h" required>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<label class="form-label" for="debrid[${index}].auto_expire_links_after">Expire Links After</label>
|
||||
<input type="text" class="form-control" name="debrid[${index}].auto_expire_links_after" id="debrid[${index}].auto_expire_links_after" placeholder="24h" required>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<label class="form-label" for="debrid[${index}].folder_naming">Folder Naming Structure</label>
|
||||
<select class="form-select" name="debrid[${index}].folder_naming" id="debrid[${index}].folder_naming">
|
||||
<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" for="debrid[${index}].workers">Number of Workers</label>
|
||||
<input type="text" class="form-control" name="debrid[${index}].workers" id="debrid[${index}].workers" required placeholder="e.g., 20">
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<label class="form-label" for="debrid[${index}].rc_url">Rclone RC URL</label>
|
||||
<input type="text" class="form-control" name="debrid[${index}].rc_url" id="debrid[${index}].rc_url" placeholder="e.g., http://localhost:9990">
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<label class="form-label" for="debrid[${index}].rc_user">Rclone RC User</label>
|
||||
<input type="text" class="form-control" name="debrid[${index}].rc_user" id="debrid[${index}].rc_user">
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<label class="form-label" for="debrid[${index}].rc_pass">Rclone RC Password</label>
|
||||
<input type="password" class="form-control" name="debrid[${index}].rc_pass" id="debrid[${index}].rc_pass">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const arrTemplate = (index) => `
|
||||
<div class="config-item position-relative mb-3 p-3 border rounded">
|
||||
<button type="button" class="btn btn-sm btn-danger position-absolute top-0 end-0 m-2"
|
||||
onclick="if(confirm('Are you sure you want to delete this arr?')) this.closest('.config-item').remove();"
|
||||
title="Delete this arr">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-3">
|
||||
<label for="arr[${index}].name" class="form-label">Name</label>
|
||||
<input type="text" class="form-control" name="arr[${index}].name" id="arr[${index}].name" required>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label for="arr[${index}].host" class="form-label">Host</label>
|
||||
<input type="text" class="form-control" name="arr[${index}].host" id="arr[${index}].host" required>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label for"arr[${index}].token" class="form-label">API Token</label>
|
||||
<input type="password" class="form-control" name="arr[${index}].token" id="arr[${index}].token" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-2 mb-3">
|
||||
<div class="form-check">
|
||||
<label for="arr[${index}].cleanup" class="form-check-label">Cleanup Queue</label>
|
||||
<input type="checkbox" class="form-check-input" name="arr[${index}].cleanup" id="arr[${index}].cleanup">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2 mb-3">
|
||||
<div class="form-check">
|
||||
<label for="arr[${index}].skip_repair" class="form-check-label">Skip Repair</label>
|
||||
<input type="checkbox" class="form-check-input" name="arr[${index}].skip_repair" id="arr[${index}].skip_repair">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2 mb-3">
|
||||
<div class="form-check">
|
||||
<label for="arr[${index}].download_uncached" class="form-check-label">Download Uncached</label>
|
||||
<input type="checkbox" class="form-check-input" name="arr[${index}].download_uncached" id="arr[${index}].download_uncached">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Main functionality
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
let debridCount = 0;
|
||||
let arrCount = 0;
|
||||
|
||||
// Load existing configuration
|
||||
fetch('/internal/config')
|
||||
.then(response => response.json())
|
||||
.then(config => {
|
||||
// Load Debrid configs
|
||||
config.debrids?.forEach(debrid => {
|
||||
addDebridConfig(debrid);
|
||||
});
|
||||
|
||||
// Load QBitTorrent config
|
||||
if (config.qbittorrent) {
|
||||
Object.entries(config.qbittorrent).forEach(([key, value]) => {
|
||||
const input = document.querySelector(`[name="qbit.${key}"]`);
|
||||
if (input) {
|
||||
if (input.type === 'checkbox') {
|
||||
input.checked = value;
|
||||
} else {
|
||||
input.value = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Load Arr configs
|
||||
config.arrs?.forEach(arr => {
|
||||
addArrConfig(arr);
|
||||
});
|
||||
|
||||
// Load Repair config
|
||||
if (config.repair) {
|
||||
Object.entries(config.repair).forEach(([key, value]) => {
|
||||
const input = document.querySelector(`[name="repair.${key}"]`);
|
||||
if (input) {
|
||||
if (input.type === 'checkbox') {
|
||||
input.checked = value;
|
||||
} else {
|
||||
input.value = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Load general config
|
||||
|
||||
const logLevel = document.getElementById('log-level');
|
||||
logLevel.value = config.log_level;
|
||||
if (config.allowed_file_types && Array.isArray(config.allowed_file_types)) {
|
||||
document.querySelector('[name="allowed_file_types"]').value = config.allowed_file_types.join(', ');
|
||||
}
|
||||
if (config.min_file_size) {
|
||||
document.querySelector('[name="min_file_size"]').value = config.min_file_size;
|
||||
}
|
||||
if (config.max_file_size) {
|
||||
document.querySelector('[name="max_file_size"]').value = config.max_file_size;
|
||||
}
|
||||
if (config.discord_webhook_url) {
|
||||
document.querySelector('[name="discord_webhook_url"]').value = config.discord_webhook_url;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading configuration:', error);
|
||||
createToast(`Error loading configuration: ${error.message}`, 'error');
|
||||
});
|
||||
|
||||
// Handle form submission
|
||||
document.getElementById('configForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
await saveConfig(e);
|
||||
});
|
||||
|
||||
document.getElementById('addDebridBtn').addEventListener('click', () => {
|
||||
addDebridConfig();
|
||||
});
|
||||
document.getElementById('addArrBtn').addEventListener('click', () => {
|
||||
addArrConfig();
|
||||
});
|
||||
|
||||
$(document).on('change', '.useWebdav', function() {
|
||||
const webdavConfig = $(this).closest('.config-item').find(`.webdav`);
|
||||
if (webdavConfig.length === 0) return;
|
||||
if (this.checked) {
|
||||
webdavConfig.removeClass('d-none');
|
||||
} else {
|
||||
webdavConfig.addClass('d-none');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
// In your JavaScript for the config page:
|
||||
async function saveConfig(e) {
|
||||
const submitButton = e.target.querySelector('button[type="submit"]');
|
||||
submitButton.disabled = true;
|
||||
submitButton.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Saving...';
|
||||
// Show a spinner or loading overlay
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'position-fixed top-0 start-0 w-100 h-100 d-flex justify-content-center align-items-center';
|
||||
overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';
|
||||
overlay.style.zIndex = '9999';
|
||||
overlay.innerHTML = `
|
||||
<div class="card p-4 text-center">
|
||||
<div class="spinner-border mb-3" role="status"></div>
|
||||
<h5>Applying configuration changes...</h5>
|
||||
<p class="text-muted">This may take a few seconds</p>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
try {
|
||||
const config = collectFormData();
|
||||
// Save config logic
|
||||
const response = await fetch('/internal/config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(config)
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error(await response.text());
|
||||
|
||||
createToast('Configuration saved successfully! Services are restarting...', 'success');
|
||||
|
||||
// Wait a moment before reloading to allow the server to restart
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1500);
|
||||
} catch (error) {
|
||||
createToast(`Error saving configuration: ${error.message}`, 'error');
|
||||
console.error('Error saving configuration:', error);
|
||||
overlay.remove(); // Remove overlay on error
|
||||
} finally {
|
||||
// Re-enable the button
|
||||
submitButton.disabled = false;
|
||||
submitButton.innerHTML = '<i class="bi bi-save"></i> Save';
|
||||
}
|
||||
}
|
||||
|
||||
function addDebridConfig(data = {}) {
|
||||
const container = document.getElementById('debridConfigs');
|
||||
container.insertAdjacentHTML('beforeend', debridTemplate(debridCount));
|
||||
|
||||
// Add a delete button to the new debrid
|
||||
const newDebrid = container.lastElementChild;
|
||||
addDeleteButton(newDebrid, `Delete this debrid`);
|
||||
|
||||
if (data) {
|
||||
|
||||
if (data.use_webdav) {
|
||||
let _webCfg = newDebrid.querySelector(`.webdav`);
|
||||
if (_webCfg) {
|
||||
_webCfg.classList.remove('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
debridCount++;
|
||||
}
|
||||
|
||||
function addArrConfig(data = {}) {
|
||||
const container = document.getElementById('arrConfigs');
|
||||
container.insertAdjacentHTML('beforeend', arrTemplate(arrCount));
|
||||
|
||||
// Add a delete button to the new arr
|
||||
const newArr = container.lastElementChild;
|
||||
addDeleteButton(newArr, `Delete this arr`);
|
||||
|
||||
if (data) {
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
const input = container.querySelector(`[name="arr[${arrCount}].${key}"]`);
|
||||
if (input) {
|
||||
if (input.type === 'checkbox') {
|
||||
input.checked = value;
|
||||
} else {
|
||||
input.value = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
arrCount++;
|
||||
}
|
||||
|
||||
function addDeleteButton(element, tooltip) {
|
||||
const deleteBtn = document.createElement('button');
|
||||
deleteBtn.type = 'button';
|
||||
deleteBtn.className = 'btn btn-sm btn-danger position-absolute top-0 end-0 m-2';
|
||||
deleteBtn.innerHTML = '<i class="bi bi-trash"></i>';
|
||||
deleteBtn.title = tooltip;
|
||||
|
||||
deleteBtn.addEventListener('click', function() {
|
||||
if (confirm('Are you sure you want to delete this item?')) {
|
||||
element.remove();
|
||||
}
|
||||
});
|
||||
element.appendChild(deleteBtn);
|
||||
}
|
||||
|
||||
function collectFormData() {
|
||||
const formEl = document.getElementById('configForm');
|
||||
|
||||
// Create the config object
|
||||
const config = {
|
||||
log_level: document.getElementById('log-level').value,
|
||||
discord_webhook_url: document.getElementById('discordWebhookUrl').value,
|
||||
allowed_file_types: document.getElementById('allowedExtensions').value.split(',').map(ext => ext.trim()).filter(Boolean),
|
||||
min_file_size: document.getElementById('minFileSize').value,
|
||||
max_file_size: document.getElementById('maxFileSize').value,
|
||||
debrids: [],
|
||||
qbittorrent: {
|
||||
username: document.querySelector('[name="qbit.username"]').value,
|
||||
password: document.querySelector('[name="qbit.password"]').value,
|
||||
port: document.querySelector('[name="qbit.port"]').value,
|
||||
download_folder: document.querySelector('[name="qbit.download_folder"]').value,
|
||||
refresh_interval: parseInt(document.querySelector('[name="qbit.refresh_interval"]').value || '0', 10)
|
||||
},
|
||||
arrs: [],
|
||||
repair: {
|
||||
enabled: document.querySelector('[name="repair.enabled"]').checked,
|
||||
interval: document.querySelector('[name="repair.interval"]').value,
|
||||
run_on_start: document.querySelector('[name="repair.run_on_start"]').checked,
|
||||
zurg_url: document.querySelector('[name="repair.zurg_url"]').value,
|
||||
use_webdav: document.querySelector('[name="repair.use_webdav"]').checked,
|
||||
auto_process: document.querySelector('[name="repair.auto_process"]').checked
|
||||
}
|
||||
};
|
||||
|
||||
// Collect all debrids
|
||||
for (let i = 0; i < debridCount; i++) {
|
||||
const nameEl = document.querySelector(`[name="debrid[${i}].name"]`);
|
||||
if (!nameEl) continue;
|
||||
|
||||
const debrid = {
|
||||
name: nameEl.value,
|
||||
api_key: document.querySelector(`[name="debrid[${i}].api_key"]`).value,
|
||||
folder: document.querySelector(`[name="debrid[${i}].folder"]`).value,
|
||||
rate_limit: document.querySelector(`[name="debrid[${i}].rate_limit"]`).value,
|
||||
download_uncached: document.querySelector(`[name="debrid[${i}].download_uncached"]`).checked,
|
||||
check_cached: document.querySelector(`[name="debrid[${i}].check_cached"]`).checked,
|
||||
use_webdav: document.querySelector(`[name="debrid[${i}].use_webdav"]`).checked,
|
||||
torrents_refresh_interval: document.querySelector(`[name="debrid[${i}].torrents_refresh_interval"]`).value,
|
||||
download_links_refresh_interval: document.querySelector(`[name="debrid[${i}].download_links_refresh_interval"]`).value,
|
||||
auto_expire_links_after: document.querySelector(`[name="debrid[${i}].auto_expire_links_after"]`).value,
|
||||
folder_naming: document.querySelector(`[name="debrid[${i}].folder_naming"]`).value,
|
||||
workers: parseInt(document.querySelector(`[name="debrid[${i}].workers"]`).value),
|
||||
rc_url: document.querySelector(`[name="debrid[${i}].rc_url"]`).value,
|
||||
rc_user: document.querySelector(`[name="debrid[${i}].rc_user"]`).value,
|
||||
rc_pass: document.querySelector(`[name="debrid[${i}].rc_pass"]`).value
|
||||
};
|
||||
|
||||
if (debrid.name && debrid.api_key) {
|
||||
config.debrids.push(debrid);
|
||||
}
|
||||
}
|
||||
|
||||
// Collect all arrs
|
||||
for (let i = 0; i < arrCount; i++) {
|
||||
const nameEl = document.querySelector(`[name="arr[${i}].name"]`);
|
||||
if (!nameEl) continue;
|
||||
|
||||
const arr = {
|
||||
name: nameEl.value,
|
||||
host: document.querySelector(`[name="arr[${i}].host"]`).value,
|
||||
token: document.querySelector(`[name="arr[${i}].token"]`).value,
|
||||
cleanup: document.querySelector(`[name="arr[${i}].cleanup"]`).checked,
|
||||
skip_repair: document.querySelector(`[name="arr[${i}].skip_repair"]`).checked,
|
||||
download_uncached: document.querySelector(`[name="arr[${i}].download_uncached"]`).checked
|
||||
};
|
||||
|
||||
if (arr.name && arr.host) {
|
||||
config.arrs.push(arr);
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
});
|
||||
// Register magnet link handler
|
||||
function registerMagnetLinkHandler() {
|
||||
if ('registerProtocolHandler' in navigator) {
|
||||
try {
|
||||
navigator.registerProtocolHandler(
|
||||
'magnet',
|
||||
`${window.location.origin}/download?magnet=%s`,
|
||||
'DecyphArr'
|
||||
);
|
||||
localStorage.setItem('magnetHandler', 'true');
|
||||
document.getElementById('registerMagnetLink').innerText = '✅ DecyphArr Can Open Magnet Links';
|
||||
document.getElementById('registerMagnetLink').classList.add('bg-white', 'text-black');
|
||||
console.log('Registered magnet link handler successfully.');
|
||||
} catch (error) {
|
||||
console.error('Failed to register magnet link handler:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var magnetHandler = localStorage.getItem('magnetHandler');
|
||||
if (magnetHandler === 'true') {
|
||||
document.getElementById('registerMagnetLink').innerText = '✅ DecyphArr Can Open Magnet Links';
|
||||
document.getElementById('registerMagnetLink').classList.add('bg-white', 'text-black');
|
||||
}
|
||||
</script>
|
||||
{{ end }}
|
||||
@@ -127,8 +127,8 @@
|
||||
}
|
||||
} else {
|
||||
createToast(`Successfully added ${result.results.length} torrents!`);
|
||||
//document.getElementById('magnetURI').value = '';
|
||||
//document.getElementById('torrentFiles').value = '';
|
||||
document.getElementById('magnetURI').value = '';
|
||||
document.getElementById('torrentFiles').value = '';
|
||||
}
|
||||
} catch (error) {
|
||||
createToast(`Error adding downloads: ${error.message}`, 'error');
|
||||
@@ -110,9 +110,14 @@
|
||||
<td>${torrent.debrid || 'None'}</td>
|
||||
<td><span class="badge ${getStateColor(torrent.state)}">${torrent.state}</span></td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="deleteTorrent('${torrent.hash}', '${torrent.category}')">
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="deleteTorrent('${torrent.hash}', '${torrent.category}', false)">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
${torrent.debrid && torrent.id ? `
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="deleteTorrent('${torrent.hash}', '${torrent.category}', true)">
|
||||
<i class="bi bi-trash"></i> Remove from Debrid
|
||||
</button>
|
||||
` : ''}
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
@@ -247,11 +252,11 @@
|
||||
return result;
|
||||
}
|
||||
|
||||
async function deleteTorrent(hash, category) {
|
||||
async function deleteTorrent(hash, category, removeFromDebrid = false) {
|
||||
if (!confirm('Are you sure you want to delete this torrent?')) return;
|
||||
|
||||
try {
|
||||
await fetch(`/internal/torrents/${category}/${hash}`, {
|
||||
await fetch(`/internal/torrents/${category}/${hash}?removeFromDebrid=${removeFromDebrid}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
await loadTorrents();
|
||||
@@ -197,8 +197,8 @@
|
||||
{{ template "config" . }}
|
||||
{{ else if eq .Page "login" }}
|
||||
{{ template "login" . }}
|
||||
{{ else if eq .Page "setup" }}
|
||||
{{ template "setup" . }}
|
||||
{{ else if eq .Page "auth" }}
|
||||
{{ template "auth" . }}
|
||||
{{ else }}
|
||||
{{ end }}
|
||||
|
||||
@@ -309,7 +309,7 @@
|
||||
const channelBadge = document.getElementById('channel-badge');
|
||||
|
||||
// Add url to version badge
|
||||
versionBadge.innerHTML = `<a href="https://github.com/sirrobot01/decypharr/releases/tag/${data.version}" target="_blank" class="text-white">${data.version}</a>`;
|
||||
versionBadge.innerHTML = `<a href="https://github.com/sirrobot01/debrid-blackhole/releases/tag/${data.version}" target="_blank" class="text-white">${data.version}</a>`;
|
||||
channelBadge.textContent = data.channel.charAt(0).toUpperCase() + data.channel.slice(1);
|
||||
|
||||
if (data.channel === 'beta') {
|
||||
@@ -57,7 +57,7 @@
|
||||
</script>
|
||||
{{ end }}
|
||||
|
||||
{{ define "setup" }}
|
||||
{{ define "auth" }}
|
||||
<div class="container mt-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 col-lg-4">
|
||||
@@ -66,7 +66,7 @@
|
||||
<h4 class="mb-0 text-center">First Time Setup</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="setupForm">
|
||||
<form id="authForm">
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Choose Username</label>
|
||||
<input type="text" class="form-control" id="username" name="username" required>
|
||||
@@ -90,7 +90,7 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('setupForm').addEventListener('submit', async (e) => {
|
||||
document.getElementById('authForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const password = document.getElementById('password').value;
|
||||
@@ -108,7 +108,7 @@
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/setup', {
|
||||
const response = await fetch('/auth', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
@@ -1,4 +1,4 @@
|
||||
{{ define "setup" }}
|
||||
{{ define "auth" }}
|
||||
<div class="container mt-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 col-lg-4">
|
||||
@@ -7,7 +7,7 @@
|
||||
<h4 class="mb-0 text-center">First Time Setup</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="setupForm" method="POST" action="/setup">
|
||||
<form id="authForm" method="POST" action="/auth">
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Choose Username</label>
|
||||
<input type="text" class="form-control" id="username" name="username" required>
|
||||
@@ -1,495 +0,0 @@
|
||||
{{ define "config" }}
|
||||
<div class="container mt-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4 class="mb-0"><i class="bi bi-gear me-2"></i>Configuration</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="configForm">
|
||||
<div class="section mb-5">
|
||||
<h5 class="border-bottom pb-2">General Configuration</h5>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<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>
|
||||
<option value="warn">Warning</option>
|
||||
<option value="error">Error</option>
|
||||
<option value="trace">Trace</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Register Magnet Link Button -->
|
||||
<div class="col-md-6">
|
||||
<label>
|
||||
<!-- Empty label to keep the button aligned -->
|
||||
</label>
|
||||
<div class="btn btn-primary w-100" onclick="registerMagnetLinkHandler()" id="registerMagnetLink">
|
||||
Open Magnet Links in Decypharr
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 mt-3">
|
||||
<div class="form-group">
|
||||
<label for="discordWebhookUrl">Discord Webhook URL</label>
|
||||
<div class="input-group">
|
||||
<textarea type="text"
|
||||
class="form-control"
|
||||
id="discordWebhookUrl"
|
||||
name="discord_webhook_url"
|
||||
disabled
|
||||
placeholder="https://discord..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 mt-3">
|
||||
<div class="form-group">
|
||||
<label for="allowedExtensions">Allowed File Extensions</label>
|
||||
<div class="input-group">
|
||||
<textarea
|
||||
class="form-control"
|
||||
id="allowedExtensions"
|
||||
name="allowed_file_types"
|
||||
disabled
|
||||
placeholder="mkv, mp4, avi, etc.">
|
||||
</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 mt-3">
|
||||
<div class="form-group">
|
||||
<label for="minFileSize">Minimum File Size</label>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
id="minFileSize"
|
||||
name="min_file_size"
|
||||
disabled
|
||||
placeholder="e.g., 10MB, 1GB">
|
||||
<small class="form-text text-muted">Minimum file size to download (0 for no limit)</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mt-3">
|
||||
<div class="form-group">
|
||||
<label for="maxFileSize">Maximum File Size</label>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
id="maxFileSize"
|
||||
name="max_file_size"
|
||||
disabled
|
||||
placeholder="e.g., 50GB, 100MB">
|
||||
<small class="form-text text-muted">Maximum file size to download (0 for no limit)</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Debrid Configuration -->
|
||||
<div class="section mb-5">
|
||||
<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</h5>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Username</label>
|
||||
<input type="text" disabled class="form-control" name="qbit.username">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Password</label>
|
||||
<input type="password" disabled class="form-control" name="qbit.password">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Port</label>
|
||||
<input type="text" disabled class="form-control" name="qbit.port">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Symlink/Download Folder</label>
|
||||
<input type="text" disabled class="form-control" name="qbit.download_folder">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<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">Arrs</h5>
|
||||
<div id="arrConfigs"></div>
|
||||
</div>
|
||||
|
||||
<!-- Repair Configuration -->
|
||||
<div class="section mb-5">
|
||||
<h5 class="border-bottom pb-2">Repair Configuration</h5>
|
||||
<div class="row">
|
||||
<div class="col-md-3 mb-3">
|
||||
<label class="form-label">Interval</label>
|
||||
<input type="text" disabled class="form-control" name="repair.interval" placeholder="e.g., 24h">
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label">Zurg URL</label>
|
||||
<input type="text" disabled class="form-control" name="repair.zurg_url" placeholder="http://zurg:9999">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="form-check me-3 d-inline-block">
|
||||
<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>
|
||||
</div>
|
||||
<div class="form-check d-inline-block">
|
||||
<input type="checkbox" disabled class="form-check-input" name="repair.auto_process" id="autoProcess">
|
||||
<label class="form-check-label" for="autoProcess">Auto Process(Scheduled jobs will be processed automatically)</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
// Templates for dynamic elements
|
||||
const debridTemplate = (index) => `
|
||||
<div class="config-item position-relative mb-3 p-3 border rounded">
|
||||
<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>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Host</label>
|
||||
<input type="text" disabled class="form-control" name="debrid[${index}].host" required>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">API Key</label>
|
||||
<input type="password" disabled class="form-control" name="debrid[${index}].api_key" required>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Mount Folder</label>
|
||||
<input type="text" disabled class="form-control" name="debrid[${index}].folder">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Rate Limit</label>
|
||||
<input type="text" disabled class="form-control" name="debrid[${index}].rate_limit" placeholder="e.g., 200/minute">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="form-check me-3 d-inline-block">
|
||||
<input type="checkbox" disabled class="form-check-input" name="debrid[${index}].download_uncached">
|
||||
<label class="form-check-label">Download Uncached</label>
|
||||
</div>
|
||||
<div class="form-check d-inline-block">
|
||||
<input type="checkbox" disabled class="form-check-input" name="debrid[${index}].check_cached">
|
||||
<label class="form-check-label">Check Cached</label>
|
||||
</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>
|
||||
`;
|
||||
|
||||
const arrTemplate = (index) => `
|
||||
<div class="config-item position-relative mb-3 p-3 border rounded">
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label">Name</label>
|
||||
<input type="text" disabled class="form-control" name="arr[${index}].name" required>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label">Host</label>
|
||||
<input type="text" disabled class="form-control" name="arr[${index}].host" required>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label">API Token</label>
|
||||
<input type="password" disabled class="form-control" name="arr[${index}].token" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-2 mb-3">
|
||||
<div class="form-check">
|
||||
<label class="form-check-label">Cleanup Queue</label>
|
||||
<input type="checkbox" disabled class="form-check-input" name="arr[${index}].cleanup">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2 mb-3">
|
||||
<div class="form-check">
|
||||
<label class="form-check-label">Skip Repair</label>
|
||||
<input type="checkbox" disabled class="form-check-input" name="arr[${index}].skip_repair">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2 mb-3">
|
||||
<div class="form-check">
|
||||
<label class="form-check-label">Download Uncached</label>
|
||||
<input type="checkbox" disabled class="form-check-input" name="arr[${index}].download_uncached">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Main functionality
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
let debridCount = 0;
|
||||
let arrCount = 0;
|
||||
|
||||
// Load existing configuration
|
||||
fetch('/internal/config')
|
||||
.then(response => response.json())
|
||||
.then(config => {
|
||||
// Load Debrid configs
|
||||
config.debrids?.forEach(debrid => {
|
||||
addDebridConfig(debrid);
|
||||
});
|
||||
|
||||
// Load QBitTorrent config
|
||||
if (config.qbittorrent) {
|
||||
Object.entries(config.qbittorrent).forEach(([key, value]) => {
|
||||
const input = document.querySelector(`[name="qbit.${key}"]`);
|
||||
if (input) {
|
||||
if (input.type === 'checkbox') {
|
||||
input.checked = value;
|
||||
} else {
|
||||
input.value = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Load Arr configs
|
||||
config.arrs?.forEach(arr => {
|
||||
addArrConfig(arr);
|
||||
});
|
||||
|
||||
// Load Repair config
|
||||
if (config.repair) {
|
||||
Object.entries(config.repair).forEach(([key, value]) => {
|
||||
const input = document.querySelector(`[name="repair.${key}"]`);
|
||||
if (input) {
|
||||
if (input.type === 'checkbox') {
|
||||
input.checked = value;
|
||||
} else {
|
||||
input.value = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Load general config
|
||||
|
||||
const logLevel = document.getElementById('log-level');
|
||||
logLevel.value = config.log_level;
|
||||
if (config.allowed_file_types && Array.isArray(config.allowed_file_types)) {
|
||||
document.querySelector('[name="allowed_file_types"]').value = config.allowed_file_types.join(', ');
|
||||
}
|
||||
if (config.min_file_size) {
|
||||
document.querySelector('[name="min_file_size"]').value = config.min_file_size;
|
||||
}
|
||||
if (config.max_file_size) {
|
||||
document.querySelector('[name="max_file_size"]').value = config.max_file_size;
|
||||
}
|
||||
if (config.discord_webhook_url) {
|
||||
document.querySelector('[name="discord_webhook_url"]').value = config.discord_webhook_url;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// Handle form submission
|
||||
document.getElementById('configForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.target);
|
||||
const config = {
|
||||
debrids: [],
|
||||
qbittorrent: {},
|
||||
arrs: [],
|
||||
repair: {}
|
||||
};
|
||||
|
||||
// Process form data
|
||||
for (let [key, value] of formData.entries()) {
|
||||
if (key.startsWith('debrid[')) {
|
||||
const match = key.match(/debrid\[(\d+)\]\.(.+)/);
|
||||
if (match) {
|
||||
const [_, index, field] = match;
|
||||
if (!config.debrids[index]) config.debrids[index] = {};
|
||||
config.debrids[index][field] = value;
|
||||
}
|
||||
} else if (key.startsWith('qbit.')) {
|
||||
config.qbittorrent[key.replace('qbit.', '')] = value;
|
||||
} else if (key.startsWith('arr[')) {
|
||||
const match = key.match(/arr\[(\d+)\]\.(.+)/);
|
||||
if (match) {
|
||||
const [_, index, field] = match;
|
||||
if (!config.arrs[index]) config.arrs[index] = {};
|
||||
config.arrs[index][field] = value;
|
||||
}
|
||||
} else if (key.startsWith('repair.')) {
|
||||
config.repair[key.replace('repair.', '')] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up arrays (remove empty entries)
|
||||
config.debrids = config.debrids.filter(Boolean);
|
||||
config.arrs = config.arrs.filter(Boolean);
|
||||
|
||||
try {
|
||||
const response = await fetch('/internal/config', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(config)
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error(await response.text());
|
||||
|
||||
createToast('Configuration saved successfully!');
|
||||
} catch (error) {
|
||||
createToast(`Error saving configuration: ${error.message}`, 'error');
|
||||
}
|
||||
});
|
||||
|
||||
// Helper functions
|
||||
function addDebridConfig(data = {}) {
|
||||
const container = document.getElementById('debridConfigs');
|
||||
container.insertAdjacentHTML('beforeend', debridTemplate(debridCount));
|
||||
|
||||
if (data) {
|
||||
|
||||
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++;
|
||||
}
|
||||
|
||||
function addArrConfig(data = {}) {
|
||||
const container = document.getElementById('arrConfigs');
|
||||
container.insertAdjacentHTML('beforeend', arrTemplate(arrCount));
|
||||
|
||||
if (data) {
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
const input = container.querySelector(`[name="arr[${arrCount}].${key}"]`);
|
||||
if (input) {
|
||||
if (input.type === 'checkbox') {
|
||||
input.checked = value;
|
||||
} else {
|
||||
input.value = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
arrCount++;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Register magnet link handler
|
||||
function registerMagnetLinkHandler() {
|
||||
if ('registerProtocolHandler' in navigator) {
|
||||
try {
|
||||
navigator.registerProtocolHandler(
|
||||
'magnet',
|
||||
`${window.location.origin}/download?magnet=%s`,
|
||||
'DecyphArr'
|
||||
);
|
||||
localStorage.setItem('magnetHandler', 'true');
|
||||
document.getElementById('registerMagnetLink').innerText = '✅ DecyphArr Can Open Magnet Links';
|
||||
document.getElementById('registerMagnetLink').classList.add('bg-white', 'text-black');
|
||||
console.log('Registered magnet link handler successfully.');
|
||||
} catch (error) {
|
||||
console.error('Failed to register magnet link handler:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var magnetHandler = localStorage.getItem('magnetHandler');
|
||||
if (magnetHandler === 'true') {
|
||||
document.getElementById('registerMagnetLink').innerText = '✅ DecyphArr Can Open Magnet Links';
|
||||
document.getElementById('registerMagnetLink').classList.add('bg-white', 'text-black');
|
||||
}
|
||||
</script>
|
||||
{{ end }}
|
||||
@@ -240,7 +240,6 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.WithValue(r.Context(), "metadataOnly", true)
|
||||
r = r.WithContext(ctx)
|
||||
cleanPath := path.Clean(r.URL.Path)
|
||||
r.Header.Set("Depth", "1")
|
||||
if r.Header.Get("Depth") == "" {
|
||||
r.Header.Set("Depth", "1")
|
||||
}
|
||||
@@ -257,7 +256,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// - If the path is exactly the parent folder (which changes frequently),
|
||||
// use a short TTL.
|
||||
// - Otherwise, for deeper (torrent folder) paths, use a longer TTL.
|
||||
ttl := 30 * time.Minute
|
||||
ttl := 1 * time.Minute
|
||||
if h.isParentPath(r.URL.Path) {
|
||||
ttl = 30 * time.Second
|
||||
}
|
||||
@@ -312,7 +311,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "GET" {
|
||||
f, err := h.OpenFile(r.Context(), r.URL.Path, os.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
h.logger.Debug().Err(err).Str("path", r.URL.Path).Msg("Failed to open file")
|
||||
h.logger.Error().Err(err).Str("path", r.URL.Path).Msg("Failed to open file")
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -12,20 +12,16 @@ import (
|
||||
|
||||
var (
|
||||
_logInstance zerolog.Logger
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
func getLogger() zerolog.Logger {
|
||||
|
||||
once.Do(func() {
|
||||
_logInstance = logger.New("worker")
|
||||
})
|
||||
return _logInstance
|
||||
}
|
||||
|
||||
func Start(ctx context.Context) error {
|
||||
cfg := config.Get()
|
||||
// Start Arr Refresh Worker
|
||||
_logInstance = logger.New("worker")
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
@@ -71,7 +67,7 @@ func cleanUpQueues() {
|
||||
}
|
||||
_logger.Trace().Msgf("Cleaning up queue for %s", a.Name)
|
||||
if err := a.CleanupQueue(); err != nil {
|
||||
_logger.Debug().Err(err).Msg("Error cleaning up queue")
|
||||
_logger.Error().Err(err).Msg("Error cleaning up queue")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user