Changelog 0.4.2

This commit is contained in:
Mukhtar Akere
2025-02-28 00:38:31 +01:00
parent e0e71b0f7e
commit 46beac7227
10 changed files with 56 additions and 333 deletions

View File

@@ -136,4 +136,12 @@
- Fixes - Fixes
- Fix Alldebrid struggling to find the correct file - Fix Alldebrid struggling to find the correct file
- Minor bug fixes or speed-gains - Minor bug fixes or speed-gains
- A new cleanup worker to clean up ARR queues - A new cleanup worker to clean up ARR queues
#### 0.4.2
- Hotfixes
- Fix saving torrents error
- Fix bugs with the UI
- Speed improvements

View File

@@ -1 +0,0 @@
package debrid

View File

@@ -1,2 +0,0 @@
package downloader

View File

@@ -3,31 +3,9 @@ package qbit
import ( import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/sirrobot01/debrid-blackhole/internal/utils" "github.com/sirrobot01/debrid-blackhole/internal/utils"
debrid "github.com/sirrobot01/debrid-blackhole/pkg/debrid/torrent"
"os"
"path/filepath"
"strings" "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 { func CreateTorrentFromMagnet(magnet *utils.Magnet, category, source string) *Torrent {
torrent := &Torrent{ torrent := &Torrent{
ID: uuid.NewString(), ID: uuid.NewString(),

View File

@@ -51,14 +51,24 @@ func (ts *TorrentStorage) Add(torrent *Torrent) {
ts.mu.Lock() ts.mu.Lock()
defer ts.mu.Unlock() defer ts.mu.Unlock()
ts.torrents[keyPair(torrent.Hash, torrent.Category)] = torrent 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) { func (ts *TorrentStorage) AddOrUpdate(torrent *Torrent) {
ts.mu.Lock() ts.mu.Lock()
defer ts.mu.Unlock() defer ts.mu.Unlock()
ts.torrents[keyPair(torrent.Hash, torrent.Category)] = torrent 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 { func (ts *TorrentStorage) Get(hash, category string) *Torrent {
@@ -108,7 +118,12 @@ func (ts *TorrentStorage) Update(torrent *Torrent) {
ts.mu.Lock() ts.mu.Lock()
defer ts.mu.Unlock() defer ts.mu.Unlock()
ts.torrents[keyPair(torrent.Hash, torrent.Category)] = torrent 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) { 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(ts.torrents, key)
// Delete the torrent folder // Delete the torrent folder
if torrent.ContentPath != "" { if torrent.ContentPath != "" {
@@ -134,7 +152,12 @@ func (ts *TorrentStorage) Delete(hash, category string) {
return return
} }
} }
_ = ts.saveToFile() go func() {
err := ts.saveToFile()
if err != nil {
fmt.Println(err)
}
}()
} }
func (ts *TorrentStorage) DeleteMultiple(hashes []string) { 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 { func (ts *TorrentStorage) Save() error {

View File

@@ -107,7 +107,9 @@ func (q *QBit) ProcessFiles(torrent *Torrent, debridTorrent *debrid.Torrent, arr
} }
torrent.TorrentPath = torrentSymlinkPath torrent.TorrentPath = torrentSymlinkPath
q.UpdateTorrent(torrent, debridTorrent) 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 { func (q *QBit) MarkAsFailed(t *Torrent) *Torrent {
@@ -180,7 +182,7 @@ func (q *QBit) UpdateTorrent(t *Torrent, debridTorrent *debrid.Torrent) *Torrent
return t return t
} }
ticker := time.NewTicker(2 * time.Second) ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop() defer ticker.Stop()
for { for {

View File

@@ -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
}

View File

@@ -178,13 +178,13 @@ func (r *Repair) RepairArr(a *arr.Arr, tmdbId string) error {
r.logger.Info().Msgf("Starting repair for %s", a.Name) r.logger.Info().Msgf("Starting repair for %s", a.Name)
media, err := a.GetMedia(tmdbId) media, err := a.GetMedia(tmdbId)
if err != nil { 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 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 { 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 return nil
} }
// Check first media to confirm mounts are accessible // Check first media to confirm mounts are accessible

View File

@@ -114,10 +114,9 @@
} }
} else { } else {
createToast(`Successfully added ${result.results.length} torrents!`); 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) { } catch (error) {
createToast(`Error adding downloads: ${error.message}`, 'error'); createToast(`Error adding downloads: ${error.message}`, 'error');
} finally { } finally {

View File

@@ -53,7 +53,6 @@ func arrRefreshWorker(ctx context.Context, cfg *config.Config) {
_logger.Debug().Msg("Refresh Worker started") _logger.Debug().Msg("Refresh Worker started")
refreshCtx := context.WithValue(ctx, "worker", "refresh") refreshCtx := context.WithValue(ctx, "worker", "refresh")
refreshTicker := time.NewTicker(time.Duration(cfg.QBitTorrent.RefreshInterval) * time.Second) refreshTicker := time.NewTicker(time.Duration(cfg.QBitTorrent.RefreshInterval) * time.Second)
var refreshMutex sync.Mutex
for { for {
select { select {
@@ -61,14 +60,7 @@ func arrRefreshWorker(ctx context.Context, cfg *config.Config) {
_logger.Debug().Msg("Refresh Worker stopped") _logger.Debug().Msg("Refresh Worker stopped")
return return
case <-refreshTicker.C: case <-refreshTicker.C:
if refreshMutex.TryLock() { refreshArrs()
go func() {
defer refreshMutex.Unlock()
refreshArrs()
}()
} else {
_logger.Debug().Msg("Previous refresh still running, skipping this cycle")
}
} }
} }
} }
@@ -111,9 +103,11 @@ func cleanUpQueuesWorker(ctx context.Context, cfg *config.Config) {
func refreshArrs() { func refreshArrs() {
arrs := service.GetService().Arr arrs := service.GetService().Arr
for _, arr := range arrs.GetAll() { for _, a := range arrs.GetAll() {
err := arr.Refresh() err := a.Refresh()
if err != nil { if err != nil {
_logger := getLogger()
_logger.Debug().Err(err).Msgf("Error refreshing %s", a.Name)
return return
} }
} }