- Add support for per-file deletion

- Per-file repair instead of per-torrent
- Fix issues with LoadLocation
- Fix other minor bug fixes woth torbox
This commit is contained in:
Mukhtar Akere
2025-05-27 19:31:19 +01:00
parent 09202b88e9
commit 7f25599b60
11 changed files with 268 additions and 103 deletions
+45 -7
View File
@@ -23,6 +23,7 @@ import (
"github.com/sirrobot01/decypharr/internal/logger"
"github.com/sirrobot01/decypharr/internal/utils"
"github.com/sirrobot01/decypharr/pkg/debrid/types"
_ "time/tzdata"
)
type WebDavFolderNaming string
@@ -109,9 +110,16 @@ type Cache struct {
func New(dc config.Debrid, client types.Client) *Cache {
cfg := config.Get()
cet, _ := time.LoadLocation("CET")
cetSc, _ := gocron.NewScheduler(gocron.WithLocation(cet))
scheduler, _ := gocron.NewScheduler(gocron.WithLocation(time.Local))
cetSc, err := gocron.NewScheduler(gocron.WithLocation(time.UTC))
if err != nil {
// If we can't create a CET scheduler, fallback to local time
cetSc, _ = gocron.NewScheduler(gocron.WithLocation(time.Local))
}
scheduler, err := gocron.NewScheduler(gocron.WithLocation(time.Local))
if err != nil {
// If we can't create a local scheduler, fallback to CET
scheduler = cetSc
}
autoExpiresLinksAfter, err := time.ParseDuration(dc.AutoExpireLinksAfter)
if autoExpiresLinksAfter == 0 || err != nil {
@@ -307,10 +315,10 @@ func (c *Cache) load(ctx context.Context) (map[string]CachedTorrent, error) {
}
isComplete := true
if len(ct.Files) != 0 {
if len(ct.GetFiles()) != 0 {
// Check if all files are valid, if not, delete the file.json and remove from cache.
fs := make(map[string]types.File, len(ct.Files))
for _, f := range ct.Files {
fs := make(map[string]types.File, len(ct.GetFiles()))
for _, f := range ct.GetFiles() {
if f.Link == "" {
isComplete = false
break
@@ -756,7 +764,7 @@ func (c *Cache) deleteTorrent(id string, removeFromDebrid bool) bool {
newFiles := map[string]types.File{}
newId := ""
for _, file := range t.Files {
for _, file := range t.GetFiles() {
if file.TorrentId != "" && file.TorrentId != id {
if newId == "" && file.TorrentId != "" {
newId = file.TorrentId
@@ -815,6 +823,36 @@ func (c *Cache) OnRemove(torrentId string) {
}
}
// RemoveFile removes a file from the torrent cache
// TODO sends a re-insert that removes the file from debrid
func (c *Cache) RemoveFile(torrentId string, filename string) error {
c.logger.Debug().Str("torrent_id", torrentId).Msgf("Removing file %s", filename)
torrent, ok := c.torrents.getByID(torrentId)
if !ok {
return fmt.Errorf("torrent %s not found", torrentId)
}
file, ok := torrent.GetFile(filename)
if !ok {
return fmt.Errorf("file %s not found in torrent %s", filename, torrentId)
}
file.Deleted = true
torrent.Files[filename] = file
// If the torrent has no files left, delete it
if len(torrent.GetFiles()) == 0 {
c.logger.Debug().Msgf("Torrent %s has no files left, deleting it", torrentId)
if err := c.DeleteTorrent(torrentId); err != nil {
return fmt.Errorf("failed to delete torrent %s: %w", torrentId, err)
}
return nil
}
c.setTorrent(torrent, func(torrent CachedTorrent) {
c.listingDebouncer.Call(true)
}) // Update the torrent in the cache
return nil
}
func (c *Cache) GetLogger() zerolog.Logger {
return c.logger
}
+17 -5
View File
@@ -103,7 +103,10 @@ func (c *Cache) fetchDownloadLink(torrentName, filename, fileLink string) (strin
if ct == nil {
return "", fmt.Errorf("torrent not found")
}
file := ct.Files[filename]
file, ok := ct.GetFile(filename)
if !ok {
return "", fmt.Errorf("file %s not found in torrent %s", filename, torrentName)
}
if file.Link == "" {
// file link is empty, refresh the torrent to get restricted links
@@ -111,7 +114,10 @@ func (c *Cache) fetchDownloadLink(torrentName, filename, fileLink string) (strin
if ct == nil {
return "", fmt.Errorf("failed to refresh torrent")
} else {
file = ct.Files[filename]
file, ok = ct.GetFile(filename)
if !ok {
return "", fmt.Errorf("file %s not found in refreshed torrent %s", filename, torrentName)
}
}
}
@@ -123,7 +129,10 @@ func (c *Cache) fetchDownloadLink(torrentName, filename, fileLink string) (strin
return "", fmt.Errorf("failed to reinsert torrent. %w", err)
}
ct = newCt
file = ct.Files[filename]
file, ok = ct.GetFile(filename)
if !ok {
return "", fmt.Errorf("file %s not found in reinserted torrent %s", filename, torrentName)
}
}
c.logger.Trace().Msgf("Getting download link for %s(%s)", filename, file.Link)
@@ -135,7 +144,10 @@ func (c *Cache) fetchDownloadLink(torrentName, filename, fileLink string) (strin
return "", fmt.Errorf("failed to reinsert torrent: %w", err)
}
ct = newCt
file = ct.Files[filename]
file, ok = ct.GetFile(filename)
if !ok {
return "", fmt.Errorf("file %s not found in reinserted torrent %s", filename, torrentName)
}
// Retry getting the download link
downloadLink, err = c.client.GetDownloadLink(ct.Torrent, &file)
if err != nil {
@@ -165,7 +177,7 @@ func (c *Cache) GenerateDownloadLinks(t CachedTorrent) {
c.logger.Error().Err(err).Str("torrent", t.Name).Msg("Failed to generate download links")
return
}
for _, file := range t.Files {
for _, file := range t.GetFiles() {
if file.DownloadLink != nil {
c.updateDownloadLink(file.DownloadLink)
}
+1 -1
View File
@@ -19,7 +19,7 @@ func mergeFiles(torrents ...CachedTorrent) map[string]types.File {
})
for _, torrent := range torrents {
for _, file := range torrent.Files {
for _, file := range torrent.GetFiles() {
merged[file.Name] = file
}
}
+12 -17
View File
@@ -59,11 +59,9 @@ func (c *Cache) markAsSuccessfullyReinserted(torrentId string) {
}
}
func (c *Cache) IsTorrentBroken(t *CachedTorrent, filenames []string) bool {
// Check torrent files
isBroken := false
func (c *Cache) GetBrokenFiles(t *CachedTorrent, filenames []string) []string {
files := make(map[string]types.File)
brokenFiles := make([]string, 0)
if len(filenames) > 0 {
for name, f := range t.Files {
if utils.Contains(filenames, name) {
@@ -73,8 +71,6 @@ func (c *Cache) IsTorrentBroken(t *CachedTorrent, filenames []string) bool {
} else {
files = t.Files
}
// Check empty links
for _, f := range files {
// Check if file is missing
if f.Link == "" {
@@ -83,14 +79,14 @@ func (c *Cache) IsTorrentBroken(t *CachedTorrent, filenames []string) bool {
t = newT
} else {
c.logger.Error().Str("torrentId", t.Torrent.Id).Msg("Failed to refresh torrent")
return true
return filenames // Return original filenames if refresh fails(torrent is somehow botched)
}
}
}
if t.Torrent == nil {
c.logger.Error().Str("torrentId", t.Torrent.Id).Msg("Failed to refresh torrent")
return true
return filenames // Return original filenames if refresh fails(torrent is somehow botched)
}
files = t.Files
@@ -98,29 +94,28 @@ func (c *Cache) IsTorrentBroken(t *CachedTorrent, filenames []string) bool {
for _, f := range files {
// Check if file link is still missing
if f.Link == "" {
isBroken = true
break
brokenFiles = append(brokenFiles, f.Name)
} else {
// Check if file.Link not in the downloadLink Cache
if err := c.client.CheckLink(f.Link); err != nil {
if errors.Is(err, request.HosterUnavailableError) {
isBroken = true
break
brokenFiles = append(brokenFiles, f.Name)
}
}
}
}
// Try to reinsert the torrent if it's broken
if isBroken && t.Torrent != nil {
if len(brokenFiles) > 0 && t.Torrent != nil {
// Check if the torrent is already in progress
if _, err := c.reInsertTorrent(t); err != nil {
c.logger.Error().Err(err).Str("torrentId", t.Torrent.Id).Msg("Failed to reinsert torrent")
return true
return brokenFiles // Return broken files if reinsert fails
}
return false
return nil // Return nil if the torrent was successfully reinserted
}
return isBroken
return brokenFiles
}
func (c *Cache) repairWorker(ctx context.Context) {
@@ -223,7 +218,7 @@ func (c *Cache) reInsertTorrent(ct *CachedTorrent) (*CachedTorrent, error) {
if err != nil {
addedOn = time.Now()
}
for _, f := range newTorrent.Files {
for _, f := range newTorrent.GetFiles() {
if f.Link == "" {
c.markAsFailedToReinsert(oldID)
return ct, fmt.Errorf("failed to reinsert torrent: empty link")
+1 -1
View File
@@ -234,7 +234,7 @@ func (tb *Torbox) GetTorrent(torrentId string) (*types.Torrent, error) {
Id: strconv.Itoa(f.Id),
Name: fileName,
Size: f.Size,
Path: fileName,
Path: f.Name,
}
t.Files[fileName] = file
}
+20 -9
View File
@@ -29,6 +29,7 @@ type Torrent struct {
Seeders int `json:"seeders"`
Links []string `json:"links"`
MountPath string `json:"mount_path"`
DeletedFiles []string `json:"deleted_files"`
Debrid string `json:"debrid"`
@@ -75,6 +76,24 @@ func (t *Torrent) GetMountFolder(rClonePath string) (string, error) {
return "", fmt.Errorf("no path found")
}
func (t *Torrent) GetFile(filename string) (File, bool) {
f, ok := t.Files[filename]
if !ok {
return File{}, false
}
return f, !f.Deleted
}
func (t *Torrent) GetFiles() []File {
files := make([]File, 0, len(t.Files))
for _, f := range t.Files {
if !f.Deleted {
files = append(files, f)
}
}
return files
}
type File struct {
TorrentId string `json:"torrent_id"`
Id string `json:"id"`
@@ -85,6 +104,7 @@ type File struct {
DownloadLink *DownloadLink `json:"-"`
AccountId string `json:"account_id"`
Generated time.Time `json:"generated"`
Deleted bool `json:"deleted"`
}
func (t *Torrent) Cleanup(remove bool) {
@@ -96,15 +116,6 @@ func (t *Torrent) Cleanup(remove bool) {
}
}
func (t *Torrent) GetFile(id string) *File {
for _, f := range t.Files {
if f.Id == id {
return &f
}
}
return nil
}
type Account struct {
ID string `json:"id"`
Disabled bool `json:"disabled"`