- Fix alldebrid bug with webdav(for nested files)

- Add support for re-inserting broken files
- Other minor bug fixes
This commit is contained in:
Mukhtar Akere
2025-04-18 15:56:52 +01:00
parent f34a371274
commit 52877107c9
9 changed files with 115 additions and 56 deletions

View File

@@ -73,12 +73,12 @@ The documentation includes:
} }
], ],
"qbittorrent": { "qbittorrent": {
"port": "8282",
"download_folder": "/mnt/symlinks/", "download_folder": "/mnt/symlinks/",
"categories": ["sonarr", "radarr"] "categories": ["sonarr", "radarr"]
}, },
"use_auth": false, "use_auth": false,
"log_level": "info" "log_level": "info",
"port": "8282"
} }
``` ```

View File

@@ -58,6 +58,7 @@ type Repair struct {
AutoProcess bool `json:"auto_process,omitempty"` AutoProcess bool `json:"auto_process,omitempty"`
UseWebDav bool `json:"use_webdav,omitempty"` UseWebDav bool `json:"use_webdav,omitempty"`
Workers int `json:"workers,omitempty"` Workers int `json:"workers,omitempty"`
ReInsert bool `json:"reinsert,omitempty"`
} }
type Auth struct { type Auth struct {

View File

@@ -4,6 +4,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/puzpuzpuz/xsync/v3" "github.com/puzpuzpuz/xsync/v3"
"github.com/sirrobot01/decypharr/internal/config"
"github.com/sirrobot01/decypharr/internal/request" "github.com/sirrobot01/decypharr/internal/request"
"github.com/sirrobot01/decypharr/internal/utils" "github.com/sirrobot01/decypharr/internal/utils"
"github.com/sirrobot01/decypharr/pkg/debrid/types" "github.com/sirrobot01/decypharr/pkg/debrid/types"
@@ -53,6 +54,21 @@ func (c *Cache) IsTorrentBroken(t *CachedTorrent, filenames []string) bool {
} }
} }
} }
cfg := config.Get()
// Try to reinsert the torrent if it's broken
if cfg.Repair.ReInsert && isBroken && t.Torrent != nil {
// Check if the torrent is already in progress
if _, inProgress := c.repairsInProgress.Load(t.Torrent.Id); !inProgress {
if err := c.reInsertTorrent(t); err != nil {
c.logger.Error().Err(err).Str("torrentId", t.Torrent.Id).Msg("Failed to reinsert torrent")
return true
} else {
c.logger.Debug().Str("torrentId", t.Torrent.Id).Msg("Reinserted torrent")
return false
}
}
}
return isBroken return isBroken
} }

View File

@@ -99,7 +99,7 @@ func (dl *DebridLink) UpdateTorrent(t *types.Torrent) error {
if err != nil { if err != nil {
return err return err
} }
var res TorrentInfo var res torrentInfo
err = json.Unmarshal(resp, &res) err = json.Unmarshal(resp, &res)
if err != nil { if err != nil {
return err return err
@@ -336,7 +336,7 @@ func (dl *DebridLink) getTorrents(page, perPage int) ([]*types.Torrent, error) {
if err != nil { if err != nil {
return torrents, err return torrents, err
} }
var res TorrentInfo var res torrentInfo
err = json.Unmarshal(resp, &res) err = json.Unmarshal(resp, &res)
if err != nil { if err != nil {
dl.logger.Info().Msgf("Error unmarshalling torrent info: %s", err) dl.logger.Info().Msgf("Error unmarshalling torrent info: %s", err)

View File

@@ -40,6 +40,6 @@ type debridLinkTorrentInfo struct {
UploadSpeed int64 `json:"uploadSpeed"` UploadSpeed int64 `json:"uploadSpeed"`
} }
type TorrentInfo APIResponse[[]debridLinkTorrentInfo] type torrentInfo APIResponse[[]debridLinkTorrentInfo]
type SubmitTorrentInfo APIResponse[debridLinkTorrentInfo] type SubmitTorrentInfo APIResponse[debridLinkTorrentInfo]

View File

@@ -105,7 +105,7 @@ func (r *RealDebrid) GetLogger() zerolog.Logger {
return r.logger return r.logger
} }
func getSelectedFiles(t *types.Torrent, data TorrentInfo) map[string]types.File { func getSelectedFiles(t *types.Torrent, data torrentInfo) map[string]types.File {
selectedFiles := make([]types.File, 0) selectedFiles := make([]types.File, 0)
for _, f := range data.Files { for _, f := range data.Files {
if f.Selected == 1 { if f.Selected == 1 {
@@ -133,7 +133,7 @@ func getSelectedFiles(t *types.Torrent, data TorrentInfo) map[string]types.File
// getTorrentFiles returns a list of torrent files from the torrent info // getTorrentFiles returns a list of torrent files from the torrent info
// validate is used to determine if the files should be validated // validate is used to determine if the files should be validated
// if validate is false, selected files will be returned // if validate is false, selected files will be returned
func getTorrentFiles(t *types.Torrent, data TorrentInfo) map[string]types.File { func getTorrentFiles(t *types.Torrent, data torrentInfo) map[string]types.File {
files := make(map[string]types.File) files := make(map[string]types.File)
cfg := config.Get() cfg := config.Get()
idx := 0 idx := 0
@@ -268,7 +268,7 @@ func (r *RealDebrid) UpdateTorrent(t *types.Torrent) error {
if err != nil { if err != nil {
return err return err
} }
var data TorrentInfo var data torrentInfo
err = json.Unmarshal(resp, &data) err = json.Unmarshal(resp, &data)
if err != nil { if err != nil {
return err return err
@@ -299,7 +299,7 @@ func (r *RealDebrid) CheckStatus(t *types.Torrent, isSymlink bool) (*types.Torre
r.logger.Info().Msgf("ERROR Checking file: %v", err) r.logger.Info().Msgf("ERROR Checking file: %v", err)
return t, err return t, err
} }
var data TorrentInfo var data torrentInfo
if err = json.Unmarshal(resp, &data); err != nil { if err = json.Unmarshal(resp, &data); err != nil {
return t, err return t, err
} }

View File

@@ -70,7 +70,7 @@ type AddMagnetSchema struct {
Uri string `json:"uri"` Uri string `json:"uri"`
} }
type TorrentInfo struct { type torrentInfo struct {
ID string `json:"id"` ID string `json:"id"`
Filename string `json:"filename"` Filename string `json:"filename"`
OriginalFilename string `json:"original_filename"` OriginalFilename string `json:"original_filename"`

View File

@@ -151,10 +151,10 @@ func (q *QBit) ProcessSymlink(torrent *Torrent) (string, error) {
func (q *QBit) createSymlinks(debridTorrent *debrid.Torrent, rclonePath, torrentFolder string) (string, error) { func (q *QBit) createSymlinks(debridTorrent *debrid.Torrent, rclonePath, torrentFolder string) (string, error) {
files := debridTorrent.Files files := debridTorrent.Files
torrentSymlinkPath := filepath.Join(q.DownloadFolder, debridTorrent.Arr.Name, torrentFolder) symlinkPath := filepath.Join(q.DownloadFolder, debridTorrent.Arr.Name, torrentFolder) // /mnt/symlinks/{category}/MyTVShow/
err := os.MkdirAll(torrentSymlinkPath, os.ModePerm) err := os.MkdirAll(symlinkPath, os.ModePerm)
if err != nil { if err != nil {
return "", fmt.Errorf("failed to create directory: %s: %v", torrentSymlinkPath, err) return "", fmt.Errorf("failed to create directory: %s: %v", symlinkPath, err)
} }
pending := make(map[string]debrid.File) pending := make(map[string]debrid.File)
@@ -168,18 +168,33 @@ func (q *QBit) createSymlinks(debridTorrent *debrid.Torrent, rclonePath, torrent
for len(pending) > 0 { for len(pending) > 0 {
<-ticker.C <-ticker.C
for path, file := range pending { for path, file := range pending {
fullFilePath := filepath.Join(rclonePath, file.Path) fullFilePath := filepath.Join(rclonePath, file.Name)
if _, err := os.Stat(fullFilePath); !os.IsNotExist(err) { if _, err := os.Stat(fullFilePath); !os.IsNotExist(err) {
q.logger.Info().Msgf("File is ready: %s", file.Path) q.logger.Info().Msgf("File is ready: %s", file.Name)
_filePath := q.createSymLink(torrentSymlinkPath, rclonePath, file) fileSymlinkPath := filepath.Join(symlinkPath, file.Name)
filePaths = append(filePaths, _filePath) if err := os.Symlink(fullFilePath, fileSymlinkPath); err != nil {
q.logger.Debug().Msgf("Failed to create symlink: %s: %v", fileSymlinkPath, err)
}
filePaths = append(filePaths, fileSymlinkPath)
delete(pending, path) delete(pending, path)
} else if file.Name != file.Path {
// This is likely alldebrid nested files(not using webdav)
fullFilePath = filepath.Join(rclonePath, file.Path)
if _, err := os.Stat(fullFilePath); !os.IsNotExist(err) {
q.logger.Info().Msgf("File is ready: %s", file.Path)
fileSymlinkPath := filepath.Join(symlinkPath, file.Path)
if err := os.Symlink(fullFilePath, fileSymlinkPath); err != nil {
q.logger.Debug().Msgf("Failed to create symlink: %s: %v", fileSymlinkPath, err)
}
filePaths = append(filePaths, fileSymlinkPath)
delete(pending, path)
}
} }
} }
} }
if q.SkipPreCache { if q.SkipPreCache {
return torrentSymlinkPath, nil return symlinkPath, nil
} }
go func() { go func() {
@@ -191,7 +206,7 @@ func (q *QBit) createSymlinks(debridTorrent *debrid.Torrent, rclonePath, torrent
} }
}() // Pre-cache the files in the background }() // Pre-cache the files in the background
// Pre-cache the first 256KB and 1MB of the file // Pre-cache the first 256KB and 1MB of the file
return torrentSymlinkPath, nil return symlinkPath, nil
} }
func (q *QBit) getTorrentPath(rclonePath string, debridTorrent *debrid.Torrent) (string, error) { func (q *QBit) getTorrentPath(rclonePath string, debridTorrent *debrid.Torrent) (string, error) {
@@ -205,18 +220,13 @@ func (q *QBit) getTorrentPath(rclonePath string, debridTorrent *debrid.Torrent)
} }
} }
func (q *QBit) createSymLink(path string, torrentMountPath string, file debrid.File) string { func (q *QBit) createSymLink(torrentFileMountPath, filePath string) string {
err := os.Symlink(torrentFileMountPath, filePath)
// Combine the directory and filename to form a full path
fullPath := filepath.Join(path, file.Name) // /mnt/symlinks/{category}/MyTVShow/MyTVShow.S01E01.720p.mkv
// Create a symbolic link if file doesn't exist
torrentFilePath := filepath.Join(torrentMountPath, file.Path) // debridFolder/MyTVShow/MyTVShow.S01E01.720p.mkv
err := os.Symlink(torrentFilePath, fullPath)
if err != nil { if err != nil {
// It's okay if the symlink already exists // It's okay if the symlink already exists
q.logger.Debug().Msgf("Failed to create symlink: %s: %v", fullPath, err) q.logger.Debug().Msgf("Failed to create symlink: %s: %v", filePath, err)
} }
return torrentFilePath return filePath
} }
func (q *QBit) preCacheFile(name string, filePaths []string) error { func (q *QBit) preCacheFile(name string, filePaths []string) error {

View File

@@ -149,10 +149,12 @@
<label class="form-label" for="qbit.refresh_interval">Refresh Interval (seconds)</label> <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"> <input type="number" class="form-control" name="qbit.refresh_interval" id="qbit.refresh_interval">
</div> </div>
<div class="col-md-6 mb-3"> <div class="col mb-3">
<input type="checkbox" class="form-check-input" name="qbit.skip_pre_cache" id="qbit.skip_pre_cache"> <div class="form-check me-3 d-inline-block">
<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> <input type="checkbox" class="form-check-input" name="qbit.skip_pre_cache" id="qbit.skip_pre_cache">
</div> <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> </div>
</div> </div>
@@ -170,36 +172,54 @@
<!-- Repair Configuration --> <!-- Repair Configuration -->
<div class="section mb-5"> <div class="section mb-5">
<h5 class="border-bottom pb-2">Repair Configuration</h5> <h5 class="border-bottom pb-2">Repair Configuration</h5>
<div class="row"> <div class="row mb-2">
<div class="col-md-3 mb-3"> <div class="col">
<label class="form-label">Interval</label> <div class="form-check me-3 d-inline-block">
<input type="text" class="form-control" name="repair.interval" placeholder="e.g., 24h"> <input type="checkbox" class="form-check-input" name="repair.enabled" id="repair.enabled">
</div> <label class="form-check-label" for="repair.enabled">Enable Repair</label>
<div class="col-md-4 mb-3"> </div>
<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> </div>
<div class="col-12"> <div id="repairCol" class="d-none">
<div class="form-check me-3 d-inline-block"> <div class="row">
<input type="checkbox" class="form-check-input" name="repair.enabled" id="repair.enabled"> <div class="col-md-3 mb-3">
<label class="form-check-label" for="repair.enabled">Enable Repair</label> <label class="form-label" for="repair.interval">Interval</label>
<input type="text" class="form-control" name="repair.interval" id="repair.interval" placeholder="e.g., 24h">
</div>
<div class="col-md-4 mb-3">
<label class="form-label" for="repair.zurg_url">Zurg URL</label>
<input type="text" class="form-control" name="repair.zurg_url" id="repair.zurg_url" placeholder="http://zurg:9999">
</div>
</div> </div>
<div class="form-check me-3 d-inline-block"> <div class="row">
<input type="checkbox" class="form-check-input" name="repair.use_webdav" id="repair.use_webdav"> <div class="col-md-2 mb-3">
<label class="form-check-label" for="repair.use_webdav">Use Webdav</label> <div class="form-check">
</div> <input type="checkbox" class="form-check-input" name="repair.use_webdav" id="repair.use_webdav">
<div class="form-check me-3 d-inline-block"> <label class="form-check-label" for="repair.use_webdav">Use Webdav</label>
<input type="checkbox" class="form-check-input" name="repair.run_on_start" id="repair.run_on_start"> </div>
<label class="form-check-label" for="repair.run_on_start">Run on Start</label> </div>
</div> <div class="col-md-2 mb-3">
<div class="form-check d-inline-block"> <div class="form-check">
<input type="checkbox" class="form-check-input" name="repair.auto_process" id="repair.auto_process"> <input type="checkbox" class="form-check-input" name="repair.run_on_start" id="repair.run_on_start">
<label class="form-check-label" for="repair.auto_process">Auto Process(Scheduled jobs will be processed automatically)</label> <label class="form-check-label" for="repair.run_on_start">Run on Start</label>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="form-check">
<input type="checkbox" class="form-check-input" name="repair.reinsert" id="repair.reinsert">
<label class="form-check-label" for="repair.reinsert">Re-Insert Nerfed Release</label>
</div>
</div>
<div class="col mb-3">
<div class="form-check">
<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 auto-processed)</label>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
<div class="text-end mt-4 mb-3"> <div class="text-end mt-3">
<button type="submit" class="btn btn-primary px-4"> <button type="submit" class="btn btn-primary px-4">
<i class="bi bi-save"></i> Save <i class="bi bi-save"></i> Save
</button> </button>
@@ -372,6 +392,9 @@
// Load Repair config // Load Repair config
if (config.repair) { if (config.repair) {
if (config.repair.enabled) {
document.getElementById('repairCol').classList.remove('d-none');
}
Object.entries(config.repair).forEach(([key, value]) => { Object.entries(config.repair).forEach(([key, value]) => {
const input = document.querySelector(`[name="repair.${key}"]`); const input = document.querySelector(`[name="repair.${key}"]`);
if (input) { if (input) {
@@ -438,6 +461,14 @@
} }
}); });
$(document).on('change', 'input[name="repair.enabled"]', function() {
if (this.checked) {
$('#repairCol').removeClass('d-none');
} else {
$('#repairCol').addClass('d-none');
}
});
// In your JavaScript for the config page: // In your JavaScript for the config page:
@@ -580,6 +611,7 @@
enabled: document.querySelector('[name="repair.enabled"]').checked, enabled: document.querySelector('[name="repair.enabled"]').checked,
interval: document.querySelector('[name="repair.interval"]').value, interval: document.querySelector('[name="repair.interval"]').value,
run_on_start: document.querySelector('[name="repair.run_on_start"]').checked, run_on_start: document.querySelector('[name="repair.run_on_start"]').checked,
reinsert: document.querySelector('[name="repair.reinsert"]').checked,
zurg_url: document.querySelector('[name="repair.zurg_url"]').value, zurg_url: document.querySelector('[name="repair.zurg_url"]').value,
use_webdav: document.querySelector('[name="repair.use_webdav"]').checked, use_webdav: document.querySelector('[name="repair.use_webdav"]').checked,
auto_process: document.querySelector('[name="repair.auto_process"]').checked auto_process: document.querySelector('[name="repair.auto_process"]').checked