- Fix alldebrid bug with webdav(for nested files)
- Add support for re-inserting broken files - Other minor bug fixes
This commit is contained in:
@@ -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"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user