diff --git a/CHANGELOG.md b/CHANGELOG.md index d75099a..1d398f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -136,4 +136,12 @@ - Fixes - Fix Alldebrid struggling to find the correct file - Minor bug fixes or speed-gains -- A new cleanup worker to clean up ARR queues \ No newline at end of file +- A new cleanup worker to clean up ARR queues + + +#### 0.4.2 + +- Hotfixes + - Fix saving torrents error + - Fix bugs with the UI +- Speed improvements \ No newline at end of file diff --git a/pkg/debrid/engine.go b/pkg/debrid/engine.go deleted file mode 100644 index 57845ff..0000000 --- a/pkg/debrid/engine.go +++ /dev/null @@ -1 +0,0 @@ -package debrid diff --git a/pkg/downloader/grab.go b/pkg/downloader/grab.go deleted file mode 100644 index 987a696..0000000 --- a/pkg/downloader/grab.go +++ /dev/null @@ -1,2 +0,0 @@ -package downloader - diff --git a/pkg/qbit/misc.go b/pkg/qbit/misc.go index 3416d05..2d1a5e6 100644 --- a/pkg/qbit/misc.go +++ b/pkg/qbit/misc.go @@ -3,31 +3,9 @@ package qbit import ( "github.com/google/uuid" "github.com/sirrobot01/debrid-blackhole/internal/utils" - debrid "github.com/sirrobot01/debrid-blackhole/pkg/debrid/torrent" - "os" - "path/filepath" "strings" - "sync" - "time" ) -func checkFileLoop(wg *sync.WaitGroup, dir string, file debrid.File, ready chan<- debrid.File) { - defer wg.Done() - ticker := time.NewTicker(1 * time.Second) // Check every second - defer ticker.Stop() - path := filepath.Join(dir, file.Path) - for { - select { - case <-ticker.C: - _, err := os.Stat(path) - if !os.IsNotExist(err) { - ready <- file - return - } - } - } -} - func CreateTorrentFromMagnet(magnet *utils.Magnet, category, source string) *Torrent { torrent := &Torrent{ ID: uuid.NewString(), diff --git a/pkg/qbit/storage.go b/pkg/qbit/storage.go index db93b5e..2feb84f 100644 --- a/pkg/qbit/storage.go +++ b/pkg/qbit/storage.go @@ -51,14 +51,24 @@ func (ts *TorrentStorage) Add(torrent *Torrent) { ts.mu.Lock() defer ts.mu.Unlock() ts.torrents[keyPair(torrent.Hash, torrent.Category)] = torrent - _ = ts.saveToFile() + go func() { + err := ts.saveToFile() + if err != nil { + fmt.Println(err) + } + }() } func (ts *TorrentStorage) AddOrUpdate(torrent *Torrent) { ts.mu.Lock() defer ts.mu.Unlock() ts.torrents[keyPair(torrent.Hash, torrent.Category)] = torrent - _ = ts.saveToFile() + go func() { + err := ts.saveToFile() + if err != nil { + fmt.Println(err) + } + }() } func (ts *TorrentStorage) Get(hash, category string) *Torrent { @@ -108,7 +118,12 @@ func (ts *TorrentStorage) Update(torrent *Torrent) { ts.mu.Lock() defer ts.mu.Unlock() ts.torrents[keyPair(torrent.Hash, torrent.Category)] = torrent - _ = ts.saveToFile() + go func() { + err := ts.saveToFile() + if err != nil { + fmt.Println(err) + } + }() } func (ts *TorrentStorage) Delete(hash, category string) { @@ -126,6 +141,9 @@ func (ts *TorrentStorage) Delete(hash, category string) { } } } + if torrent == nil { + return + } delete(ts.torrents, key) // Delete the torrent folder if torrent.ContentPath != "" { @@ -134,7 +152,12 @@ func (ts *TorrentStorage) Delete(hash, category string) { return } } - _ = ts.saveToFile() + go func() { + err := ts.saveToFile() + if err != nil { + fmt.Println(err) + } + }() } func (ts *TorrentStorage) DeleteMultiple(hashes []string) { @@ -147,7 +170,12 @@ func (ts *TorrentStorage) DeleteMultiple(hashes []string) { } } } - _ = ts.saveToFile() + go func() { + err := ts.saveToFile() + if err != nil { + fmt.Println(err) + } + }() } func (ts *TorrentStorage) Save() error { diff --git a/pkg/qbit/torrent.go b/pkg/qbit/torrent.go index 4f8c1d9..78c4ab3 100644 --- a/pkg/qbit/torrent.go +++ b/pkg/qbit/torrent.go @@ -107,7 +107,9 @@ func (q *QBit) ProcessFiles(torrent *Torrent, debridTorrent *debrid.Torrent, arr } torrent.TorrentPath = torrentSymlinkPath q.UpdateTorrent(torrent, debridTorrent) - _ = arr.Refresh() + if err := arr.Refresh(); err != nil { + q.logger.Error().Msgf("Error refreshing arr: %v", err) + } } func (q *QBit) MarkAsFailed(t *Torrent) *Torrent { @@ -180,7 +182,7 @@ func (q *QBit) UpdateTorrent(t *Torrent, debridTorrent *debrid.Torrent) *Torrent return t } - ticker := time.NewTicker(2 * time.Second) + ticker := time.NewTicker(100 * time.Millisecond) defer ticker.Stop() for { diff --git a/pkg/rclone/rclone.go b/pkg/rclone/rclone.go deleted file mode 100644 index c5b7ccd..0000000 --- a/pkg/rclone/rclone.go +++ /dev/null @@ -1,283 +0,0 @@ -package rclone - -import ( - "bufio" - "context" - "fmt" - "github.com/rs/zerolog" - "github.com/sirrobot01/debrid-blackhole/internal/config" - "github.com/sirrobot01/debrid-blackhole/internal/logger" - "github.com/sirrobot01/debrid-blackhole/pkg/webdav" - "os" - "os/exec" - "path/filepath" - "strings" - "sync" - "time" -) - -type Remote struct { - Type string `json:"type"` - Name string `json:"name"` - Url string `json:"url"` - MountPoint string `json:"mount_point"` - Flags map[string]string `json:"flags"` -} - -func (rc *Rclone) Config() string { - var content string - - for _, remote := range rc.Remotes { - content += fmt.Sprintf("[%s]\n", remote.Name) - content += fmt.Sprintf("type = %s\n", remote.Type) - content += fmt.Sprintf("url = %s\n", remote.Url) - content += fmt.Sprintf("vendor = other\n") - - for key, value := range remote.Flags { - content += fmt.Sprintf("%s = %s\n", key, value) - } - content += "\n\n" - } - - return content -} - -type Rclone struct { - Remotes map[string]Remote `json:"remotes"` - logger zerolog.Logger - cmd *exec.Cmd - configPath string -} - -func New(webdav *webdav.WebDav) (*Rclone, error) { - // Check if rclone is installed - cfg := config.GetConfig() - configPath := fmt.Sprintf("%s/rclone.conf", cfg.Path) - - if _, err := exec.LookPath("rclone"); err != nil { - return nil, fmt.Errorf("rclone is not installed: %w", err) - } - remotes := make(map[string]Remote) - for _, handler := range webdav.Handlers { - url := fmt.Sprintf("http://localhost:%s/webdav/%s/", cfg.QBitTorrent.Port, strings.ToLower(handler.Name)) - rmt := Remote{ - Type: "webdav", - Name: handler.Name, - Url: url, - MountPoint: filepath.Join("/mnt/rclone/", handler.Name), - Flags: map[string]string{}, - } - remotes[handler.Name] = rmt - } - - rc := &Rclone{ - logger: logger.NewLogger("rclone", "info", os.Stdout), - Remotes: remotes, - configPath: configPath, - } - if err := rc.WriteConfig(); err != nil { - return nil, err - } - return rc, nil -} - -func (rc *Rclone) WriteConfig() error { - - // Create config directory if it doesn't exist - configDir := filepath.Dir(rc.configPath) - if err := os.MkdirAll(configDir, 0755); err != nil { - return fmt.Errorf("failed to create config directory: %w", err) - } - - // Write the config file - if err := os.WriteFile(rc.configPath, []byte(rc.Config()), 0600); err != nil { - return fmt.Errorf("failed to write config file: %w", err) - } - - rc.logger.Info().Msgf("Wrote rclone config with %d remotes to %s", len(rc.Remotes), rc.configPath) - return nil -} - -func (rc *Rclone) Start(ctx context.Context) error { - var wg sync.WaitGroup - errChan := make(chan error) - for _, remote := range rc.Remotes { - wg.Add(1) - go func(remote Remote) { - defer wg.Done() - if err := rc.Mount(ctx, &remote); err != nil { - rc.logger.Error().Err(err).Msgf("failed to mount %s", remote.Name) - select { - case errChan <- err: - default: - } - } - }(remote) - } - return <-errChan -} - -func (rc *Rclone) testConnection(ctx context.Context, remote *Remote) error { - testArgs := []string{ - "ls", - "--config", rc.configPath, - "--log-level", "DEBUG", - remote.Name + ":", - } - - cmd := exec.CommandContext(ctx, "rclone", testArgs...) - output, err := cmd.CombinedOutput() - if err != nil { - rc.logger.Error().Err(err).Str("output", string(output)).Msg("Connection test failed") - return fmt.Errorf("connection test failed: %w", err) - } - - rc.logger.Info().Msg("Connection test successful") - return nil -} - -func (rc *Rclone) Mount(ctx context.Context, remote *Remote) error { - // Ensure the mount point directory exists - if err := os.MkdirAll(remote.MountPoint, 0755); err != nil { - rc.logger.Info().Err(err).Msgf("failed to create mount point directory: %s", remote.MountPoint) - return err - } - - //if err := rc.testConnection(ctx, remote); err != nil { - // return err - //} - - // Basic arguments - args := []string{ - "mount", - remote.Name + ":", - remote.MountPoint, - "--config", rc.configPath, - "--vfs-cache-mode", "full", - "--log-level", "DEBUG", // Keep this, remove -vv - "--allow-other", // Keep this - "--allow-root", // Add this - "--default-permissions", // Add this - "--vfs-cache-max-age", "24h", - "--timeout", "1m", - "--transfers", "4", - "--buffer-size", "32M", - } - - // Add any additional flags - for key, value := range remote.Flags { - args = append(args, "--"+key, value) - } - - // Create command - rc.cmd = exec.CommandContext(ctx, "rclone", args...) - - // Set up pipes for stdout and stderr - stdout, err := rc.cmd.StdoutPipe() - if err != nil { - return err - } - - stderr, err := rc.cmd.StderrPipe() - if err != nil { - return err - } - - // Start the command - if err := rc.cmd.Start(); err != nil { - return err - } - - // Channel to signal mount success - mountReady := make(chan bool) - mountError := make(chan error) - - // Monitor stdout - go func() { - scanner := bufio.NewScanner(stdout) - for scanner.Scan() { - text := scanner.Text() - rc.logger.Info().Msg("stdout: " + text) - if strings.Contains(text, "Mount succeeded") { - mountReady <- true - return - } - } - }() - - // Monitor stderr - go func() { - scanner := bufio.NewScanner(stderr) - for scanner.Scan() { - text := scanner.Text() - rc.logger.Info().Msg("stderr: " + text) - if strings.Contains(text, "error") { - mountError <- fmt.Errorf("mount error: %s", text) - return - } - } - }() - - // Wait for mount with timeout - select { - case <-mountReady: - rc.logger.Info().Msgf("Successfully mounted %s at %s", remote.Name, remote.MountPoint) - return nil - case err := <-mountError: - err = rc.cmd.Process.Kill() - if err != nil { - return err - } - return err - case <-ctx.Done(): - err := rc.cmd.Process.Kill() - if err != nil { - return err - } - return ctx.Err() - case <-time.After(30 * time.Second): - err := rc.cmd.Process.Kill() - if err != nil { - return err - } - return fmt.Errorf("mount timeout after 30 seconds") - } -} - -func (rc *Rclone) Unmount(ctx context.Context, remote *Remote) error { - if rc.cmd != nil && rc.cmd.Process != nil { - // First try graceful shutdown - if err := rc.cmd.Process.Signal(os.Interrupt); err != nil { - rc.logger.Warn().Err(err).Msg("failed to send interrupt signal") - } - - // Wait for a bit to allow graceful shutdown - done := make(chan error) - go func() { - done <- rc.cmd.Wait() - }() - - select { - case err := <-done: - if err != nil { - rc.logger.Warn().Err(err).Msg("process exited with error") - } - case <-time.After(5 * time.Second): - // Force kill if it doesn't shut down gracefully - if err := rc.cmd.Process.Kill(); err != nil { - rc.logger.Error().Err(err).Msg("failed to kill process") - return err - } - } - } - - // Use fusermount to ensure the mountpoint is unmounted - cmd := exec.CommandContext(ctx, "fusermount", "-u", remote.MountPoint) - if err := cmd.Run(); err != nil { - rc.logger.Warn().Err(err).Msg("fusermount unmount failed") - // Don't return error here as the process might already be dead - } - - rc.logger.Info().Msgf("Successfully unmounted %s", remote.MountPoint) - return nil -} diff --git a/pkg/repair/repair.go b/pkg/repair/repair.go index a37d0e7..040983e 100644 --- a/pkg/repair/repair.go +++ b/pkg/repair/repair.go @@ -178,13 +178,13 @@ func (r *Repair) RepairArr(a *arr.Arr, tmdbId string) error { r.logger.Info().Msgf("Starting repair for %s", a.Name) media, err := a.GetMedia(tmdbId) if err != nil { - r.logger.Info().Msgf("Failed to get %s media: %v", a.Type, err) + r.logger.Info().Msgf("Failed to get %s media: %v", a.Name, err) return err } - r.logger.Info().Msgf("Found %d %s media", len(media), a.Type) + r.logger.Info().Msgf("Found %d %s media", len(media), a.Name) if len(media) == 0 { - r.logger.Info().Msgf("No %s media found", a.Type) + r.logger.Info().Msgf("No %s media found", a.Name) return nil } // Check first media to confirm mounts are accessible diff --git a/pkg/web/web/download.html b/pkg/web/web/download.html index 4e87007..ded44b6 100644 --- a/pkg/web/web/download.html +++ b/pkg/web/web/download.html @@ -114,10 +114,9 @@ } } 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'); } finally { diff --git a/pkg/worker/worker.go b/pkg/worker/worker.go index dd60a8d..8bc9e51 100644 --- a/pkg/worker/worker.go +++ b/pkg/worker/worker.go @@ -53,7 +53,6 @@ func arrRefreshWorker(ctx context.Context, cfg *config.Config) { _logger.Debug().Msg("Refresh Worker started") refreshCtx := context.WithValue(ctx, "worker", "refresh") refreshTicker := time.NewTicker(time.Duration(cfg.QBitTorrent.RefreshInterval) * time.Second) - var refreshMutex sync.Mutex for { select { @@ -61,14 +60,7 @@ func arrRefreshWorker(ctx context.Context, cfg *config.Config) { _logger.Debug().Msg("Refresh Worker stopped") return case <-refreshTicker.C: - if refreshMutex.TryLock() { - go func() { - defer refreshMutex.Unlock() - refreshArrs() - }() - } else { - _logger.Debug().Msg("Previous refresh still running, skipping this cycle") - } + refreshArrs() } } } @@ -111,9 +103,11 @@ func cleanUpQueuesWorker(ctx context.Context, cfg *config.Config) { func refreshArrs() { arrs := service.GetService().Arr - for _, arr := range arrs.GetAll() { - err := arr.Refresh() + for _, a := range arrs.GetAll() { + err := a.Refresh() if err != nil { + _logger := getLogger() + _logger.Debug().Err(err).Msgf("Error refreshing %s", a.Name) return } }