From a3a24124a8c7734ec448c7e4cc6fe3cb2185e84d Mon Sep 17 00:00:00 2001 From: Ben Sarmiento Date: Mon, 20 May 2024 20:43:19 +0200 Subject: [PATCH] Introduce components --- README.md | 4 +- go.mod | 1 + go.sum | 2 + internal/torrent/delete.go | 24 +--- internal/torrent/file_types.go | 18 +++ internal/torrent/fixer.go | 4 +- internal/torrent/fsm.go | 31 +++++ internal/torrent/manager.go | 101 +++++++++++----- internal/torrent/refresh.go | 113 ++++++++---------- internal/torrent/repair.go | 43 +++---- .../torrent/{types.go => torrent_types.go} | 83 ++++--------- 11 files changed, 222 insertions(+), 202 deletions(-) create mode 100644 internal/torrent/file_types.go create mode 100644 internal/torrent/fsm.go rename internal/torrent/{types.go => torrent_types.go} (54%) diff --git a/README.md b/README.md index 427ac43..c28a575 100644 --- a/README.md +++ b/README.md @@ -6,14 +6,14 @@ A self-hosted Real-Debrid webdav server written from scratch. Together with [rcl ## Download -### Latest version: v0.9.3-hotfix.9 +### Latest version: v0.10.0 [Download the binary](https://github.com/debridmediamanager/zurg-testing/releases) or use docker ```sh docker pull ghcr.io/debridmediamanager/zurg-testing:latest # or -docker pull ghcr.io/debridmediamanager/zurg-testing:v0.9.3-hotfix.9 +docker pull ghcr.io/debridmediamanager/zurg-testing:v0.10.0 ``` ## How to run zurg in 5 steps for Plex with Docker diff --git a/go.mod b/go.mod index 25114e8..c46af5d 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( ) require ( + github.com/looplab/fsm v1.0.1 // indirect github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect diff --git a/go.sum b/go.sum index c875902..3f974a7 100644 --- a/go.sum +++ b/go.sum @@ -11,6 +11,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/looplab/fsm v1.0.1 h1:OEW0ORrIx095N/6lgoGkFkotqH6s7vaFPsgjLAaF5QU= +github.com/looplab/fsm v1.0.1/go.mod h1:PmD3fFvQEIsjMEfvZdrCDZ6y8VwKTwWNjlpEr6IKPO4= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= diff --git a/internal/torrent/delete.go b/internal/torrent/delete.go index 6656f61..ff34bdf 100644 --- a/internal/torrent/delete.go +++ b/internal/torrent/delete.go @@ -12,16 +12,7 @@ func (t *TorrentManager) CheckDeletedStatus(torrent *Torrent) bool { if len(deletedIDs) == torrent.SelectedFiles.Count() && len(deletedIDs) > 0 { return true } else if len(deletedIDs) > 0 { - t.saveTorrentChangesToDisk(torrent, func(info *Torrent) { - info.SelectedFiles.IterCb(func(_ string, file *File) { - for _, deletedID := range deletedIDs { - if file.ID == deletedID { - file.IsDeleted = true - break - } - } - }) - }) + t.writeTorrentToFile(torrent) } return false } @@ -29,15 +20,12 @@ func (t *TorrentManager) CheckDeletedStatus(torrent *Torrent) bool { func (t *TorrentManager) Delete(accessKey string, deleteInRD bool) { allTorrents, _ := t.DirectoryMap.Get(INT_ALL) if deleteInRD { - infoCache, _ := t.DirectoryMap.Get(INT_INFO_CACHE) if torrent, ok := allTorrents.Get(accessKey); ok { - torrent.DownloadedIDs.Union(torrent.InProgressIDs).Each(func(id string) bool { - t.log.Debugf("Deleting torrent %s (id=%s) in RD", accessKey, id) - t.api.DeleteTorrent(id) - infoCache.Remove(id) - t.deleteTorrentFile(id) - return false - }) + for torrentID := range torrent.Components { + t.log.Debugf("Deleting torrent %s (id=%s) in RD", accessKey, torrentID) + t.api.DeleteTorrent(torrentID) + t.deleteInfoFile(torrentID) + } } } t.log.Infof("Removing torrent %s from zurg database (not real-debrid)", accessKey) diff --git a/internal/torrent/file_types.go b/internal/torrent/file_types.go new file mode 100644 index 0000000..6fac6d5 --- /dev/null +++ b/internal/torrent/file_types.go @@ -0,0 +1,18 @@ +package torrent + +import ( + "github.com/debridmediamanager/zurg/pkg/realdebrid" + "github.com/looplab/fsm" +) + +type File struct { + realdebrid.File + Link string `json:"Link"` + Ended string `json:"Ended"` + + IsBroken bool `json:"IsBroken"` + IsDeleted bool `json:"IsDeleted"` + State *fsm.FSM `json:"-"` + + Rename string `json:"Rename"` +} diff --git a/internal/torrent/fixer.go b/internal/torrent/fixer.go index 30d2b53..e727738 100644 --- a/internal/torrent/fixer.go +++ b/internal/torrent/fixer.go @@ -60,11 +60,9 @@ func (t *TorrentManager) processFixers(instances []realdebrid.Torrent) { } } - infoCache, _ := t.DirectoryMap.Get(INT_INFO_CACHE) for _, id := range toDelete { t.api.DeleteTorrent(id) - infoCache.Remove(id) - t.deleteTorrentFile(id) + t.deleteInfoFile(id) } for _, torrent := range toRedownload { diff --git a/internal/torrent/fsm.go b/internal/torrent/fsm.go new file mode 100644 index 0000000..5536fdc --- /dev/null +++ b/internal/torrent/fsm.go @@ -0,0 +1,31 @@ +package torrent + +import ( + "github.com/looplab/fsm" +) + +func NewFileState() *fsm.FSM { + return fsm.NewFSM( + "ok", + fsm.Events{ + {Name: "break", Src: []string{"ok"}, Dst: "broken"}, + {Name: "repair", Src: []string{"broken"}, Dst: "under_repair"}, + {Name: "repair_done", Src: []string{"under_repair"}, Dst: "ok"}, + {Name: "delete", Src: []string{"ok", "broken", "under_repair"}, Dst: "deleted"}, + }, + fsm.Callbacks{}, + ) +} + +func NewTorrentState() *fsm.FSM { + return fsm.NewFSM( + "ok", + fsm.Events{ + {Name: "break", Src: []string{"ok"}, Dst: "broken"}, + {Name: "repair", Src: []string{"broken"}, Dst: "under_repair"}, + {Name: "repair_done", Src: []string{"under_repair"}, Dst: "ok"}, + {Name: "delete", Src: []string{"ok", "broken", "under_repair"}, Dst: "deleted"}, + }, + fsm.Callbacks{}, + ) +} diff --git a/internal/torrent/manager.go b/internal/torrent/manager.go index 9b3b676..8d63537 100644 --- a/internal/torrent/manager.go +++ b/internal/torrent/manager.go @@ -18,8 +18,7 @@ import ( ) const ( - INT_ALL = "int__all__" - INT_INFO_CACHE = "int__info__" + INT_ALL = "int__all__" ) type TorrentManager struct { @@ -41,7 +40,6 @@ type TorrentManager struct { latestState *LibraryState allAccessKeys mapset.Set[string] - allIDs mapset.Set[string] fixers cmap.ConcurrentMap[string, string] // trigger -> [command, id] repairTrigger chan *Torrent @@ -55,7 +53,7 @@ type TorrentManager struct { // and store them in-memory and cached in files func NewTorrentManager(cfg config.ConfigInterface, api *realdebrid.RealDebrid, workerPool *ants.Pool, log *logutil.Logger) *TorrentManager { t := &TorrentManager{ - requiredVersion: "0.9.3-hotfix.10", + requiredVersion: "0.10.0", Config: cfg, api: api, @@ -73,7 +71,6 @@ func NewTorrentManager(cfg config.ConfigInterface, api *realdebrid.RealDebrid, w latestState: &LibraryState{}, allAccessKeys: mapset.NewSet[string](), - allIDs: mapset.NewSet[string](), } t.fixers = t.readFixersFromFile() @@ -140,8 +137,10 @@ func (t *TorrentManager) GetPath(file *File) string { return filename } -func (t *TorrentManager) writeTorrentToFile(instanceID string, torrent *Torrent) { - filePath := "data/" + instanceID + ".json" +/// torrent functions + +func (t *TorrentManager) writeTorrentToFile(torrent *Torrent) { + filePath := "data/" + torrent.Hash + ".json" file, err := os.Create(filePath) if err != nil { t.log.Warnf("Cannot create file %s: %v", filePath, err) @@ -162,11 +161,11 @@ func (t *TorrentManager) writeTorrentToFile(instanceID string, torrent *Torrent) return } - t.log.Debugf("Saved torrent %s to file", instanceID) + t.log.Debugf("Saved torrent %s to file", torrent.Hash) } -func (t *TorrentManager) readTorrentFromFile(torrentID string) *Torrent { - filePath := "data/" + torrentID + ".json" +func (t *TorrentManager) readTorrentFromFile(hash string) *Torrent { + filePath := "data/" + hash + ".json" file, err := os.Open(filePath) if err != nil { if os.IsNotExist(err) { @@ -183,7 +182,7 @@ func (t *TorrentManager) readTorrentFromFile(torrentID string) *Torrent { if err := json.Unmarshal(jsonData, &torrent); err != nil { return nil } - if torrent.DownloadedIDs.Union(torrent.InProgressIDs).IsEmpty() { + if len(torrent.Components) == 0 { t.log.Fatal("Torrent has no downloaded or in progress ids") } if torrent.Version != t.requiredVersion { @@ -192,14 +191,72 @@ func (t *TorrentManager) readTorrentFromFile(torrentID string) *Torrent { return torrent } -func (t *TorrentManager) deleteTorrentFile(torrentID string) { - filePath := "data/" + torrentID + ".json" +func (t *TorrentManager) deleteTorrentFile(hash string) { + filePath := "data/" + hash + ".json" err := os.Remove(filePath) if err != nil { t.log.Warnf("Cannot delete file %s: %v", filePath, err) } } +/// end torrent functions + +/// info functions + +func (t *TorrentManager) writeInfoToFile(info *realdebrid.TorrentInfo) { + filePath := "data/" + info.ID + ".info" + file, err := os.Create(filePath) + if err != nil { + t.log.Warnf("Cannot create info file %s: %v", filePath, err) + return + } + defer file.Close() + + jsonData, err := json.Marshal(info) + if err != nil { + t.log.Warnf("Cannot marshal torrent info: %v", err) + return + } + + if _, err := file.Write(jsonData); err != nil { + t.log.Warnf("Cannot write to info file %s: %v", filePath, err) + return + } + + t.log.Debugf("Saved torrent %s to info file", info.ID) +} + +func (t *TorrentManager) readInfoFromFile(torrentID string) *realdebrid.TorrentInfo { + filePath := "data/" + torrentID + ".info" + file, err := os.Open(filePath) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return nil + } + defer file.Close() + jsonData, err := io.ReadAll(file) + if err != nil { + return nil + } + var info *realdebrid.TorrentInfo + if err := json.Unmarshal(jsonData, &info); err != nil { + return nil + } + return info +} + +func (t *TorrentManager) deleteInfoFile(torrentID string) { + filePath := "data/" + torrentID + ".info" + err := os.Remove(filePath) + if err != nil { + t.log.Warnf("Cannot delete info file %s: %v", filePath, err) + } +} + +/// end info functions + func (t *TorrentManager) mountDownloads() { if !t.Config.EnableDownloadMount() { return @@ -246,26 +303,10 @@ func (t *TorrentManager) StartDownloadsJob() { func (t *TorrentManager) initializeDirectories() { // create internal directories - t.DirectoryMap.Set(INT_ALL, cmap.New[*Torrent]()) // key is GetAccessKey() - t.DirectoryMap.Set(INT_INFO_CACHE, cmap.New[*Torrent]()) // key is Torrent ID + t.DirectoryMap.Set(INT_ALL, cmap.New[*Torrent]()) // key is GetAccessKey() // create directory maps for _, directory := range t.Config.GetDirectories() { t.DirectoryMap.Set(directory, cmap.New[*Torrent]()) // t.RootNode.AddChild(fs.NewFileNode(directory, true)) } } - -func (t *TorrentManager) saveTorrentChangesToDisk(torrent *Torrent, cb func(*Torrent)) { - infoCache, _ := t.DirectoryMap.Get(INT_INFO_CACHE) - torrent.DownloadedIDs.Union(torrent.InProgressIDs).Each(func(id string) bool { - info, exists := infoCache.Get(id) - if !exists { - return false - } - if cb != nil { - cb(info) - } - t.writeTorrentToFile(id, info) - return false - }) -} diff --git a/internal/torrent/refresh.go b/internal/torrent/refresh.go index fdadd27..447940f 100644 --- a/internal/torrent/refresh.go +++ b/internal/torrent/refresh.go @@ -21,7 +21,7 @@ func (t *TorrentManager) refreshTorrents(isInitialRun bool) []string { return nil } t.log.Infof("Fetched %d torrents", len(instances)) - infoChan := make(chan *Torrent, len(instances)) + torChan := make(chan *Torrent, len(instances)) var wg sync.WaitGroup for i := range instances { @@ -29,60 +29,56 @@ func (t *TorrentManager) refreshTorrents(isInitialRun bool) []string { wg.Add(1) _ = t.workerPool.Submit(func() { defer wg.Done() - infoChan <- t.getMoreInfo(instances[idx]) + torChan <- t.getMoreInfo(instances[idx]) }) } wg.Wait() - close(infoChan) + close(torChan) t.log.Infof("Fetched info for %d torrents", len(instances)) var updatedPaths []string noInfoCount := 0 allTorrents, _ := t.DirectoryMap.Get(INT_ALL) freshAccessKeys := mapset.NewSet[string]() - deletedIDs := t.allIDs.Clone() - for info := range infoChan { - if info == nil { + for torrent := range torChan { + if torrent == nil { noInfoCount++ continue } - infoID, _ := info.DownloadedIDs.Clone().Pop() - deletedIDs.Remove(infoID) - accessKey := t.GetKey(info) + // there's only 1 component torrent at this point, let's get it + var tInfo *realdebrid.TorrentInfo + for _, tInfo = range torrent.Components { + break + } + accessKey := t.GetKey(torrent) freshAccessKeys.Add(accessKey) // update allTorrents + isNewID := false mainTorrent, exists := allTorrents.Get(accessKey) if !exists { - allTorrents.Set(accessKey, info) - } else { - if !mainTorrent.DownloadedIDs.Contains(infoID) { - merged := t.mergeToMain(mainTorrent, info) - allTorrents.Set(accessKey, merged) - } + allTorrents.Set(accessKey, torrent) + mainTorrent = torrent + isNewID = true + } else if _, ok := mainTorrent.Components[tInfo.ID]; !ok { + merged := t.mergeToMain(mainTorrent, torrent) + allTorrents.Set(accessKey, merged) + mainTorrent = merged + isNewID = true } - // check for newly finished torrents for assigning to directories - isDone := info.DownloadedIDs.Cardinality() > 0 && info.InProgressIDs.IsEmpty() - if isDone && !t.allIDs.Contains(infoID) { - var directories []string - mainTor, _ := allTorrents.Get(accessKey) - t.assignedDirectoryCb(mainTor, func(directory string) { + if isNewID && tInfo.Progress == 100 { + // assign to directory + t.assignedDirectoryCb(mainTorrent, func(directory string) { listing, _ := t.DirectoryMap.Get(directory) - listing.Set(accessKey, mainTor) + listing.Set(accessKey, mainTorrent) updatedPaths = append(updatedPaths, fmt.Sprintf("%s/%s", directory, accessKey)) - // this is just for the logs - if directory != config.ALL_TORRENTS { - directories = append(directories, directory) - } }) - t.allIDs.Add(infoID) } } - t.allIDs.RemoveAll(deletedIDs.ToSlice()...) t.log.Infof("Compiled into %d torrents, %d were missing info", allTorrents.Count(), noInfoCount) // removed torrents @@ -138,24 +134,14 @@ func (t *TorrentManager) StartRefreshJob() { // getMoreInfo gets original name, size and files for a torrent func (t *TorrentManager) getMoreInfo(rdTorrent realdebrid.Torrent) *Torrent { - infoCache, _ := t.DirectoryMap.Get(INT_INFO_CACHE) - - if cachedTor, exists := infoCache.Get(rdTorrent.ID); exists && - cachedTor.SelectedFiles.Count() == len(rdTorrent.Links) { - - return cachedTor - - } else if diskTor := t.readTorrentFromFile(rdTorrent.ID); diskTor != nil && !diskTor.AllInProgress() { - - infoCache.Set(rdTorrent.ID, diskTor) - t.ResetSelectedFiles(diskTor) - return diskTor - } - - info, err := t.api.GetTorrentInfo(rdTorrent.ID) - if err != nil { - t.log.Warnf("Cannot get info for id=%s: %v", rdTorrent.ID, err) - return nil + info := t.readInfoFromFile(rdTorrent.ID) + if info == nil { + var err error + info, err = t.api.GetTorrentInfo(rdTorrent.ID) + if err != nil { + t.log.Warnf("Cannot get info for id=%s: %v", rdTorrent.ID, err) + return nil + } } torrent := Torrent{ @@ -204,15 +190,10 @@ func (t *TorrentManager) getMoreInfo(rdTorrent realdebrid.Torrent) *Torrent { torrent.SelectedFiles.Set(filename, file) } } - torrent.DownloadedIDs = mapset.NewSet[string]() - torrent.InProgressIDs = mapset.NewSet[string]() - if rdTorrent.Progress == 100 { - torrent.DownloadedIDs.Add(info.ID) - // save to cache if it's not in progress anymore - infoCache.Set(rdTorrent.ID, &torrent) - t.saveTorrentChangesToDisk(&torrent, nil) - } else { - torrent.InProgressIDs.Add(info.ID) + torrent.Components = map[string]*realdebrid.TorrentInfo{rdTorrent.ID: info} + + if info.Progress == 100 { + t.writeInfoToFile(info) } return &torrent @@ -247,6 +228,14 @@ func (t *TorrentManager) mergeToMain(existing, toMerge *Torrent) *Torrent { older = toMerge } + mergedComponents := map[string]*realdebrid.TorrentInfo{} + for k, v := range older.Components { + mergedComponents[k] = v + } + for k, v := range newer.Components { + mergedComponents[k] = v + } + // build the main torrent mainTorrent := Torrent{ Name: newer.Name, @@ -255,8 +244,7 @@ func (t *TorrentManager) mergeToMain(existing, toMerge *Torrent) *Torrent { Hash: newer.Hash, Added: newer.Added, - DownloadedIDs: newer.DownloadedIDs.Union(older.DownloadedIDs), - InProgressIDs: newer.InProgressIDs.Union(older.InProgressIDs), + Components: mergedComponents, UnassignedLinks: newer.UnassignedLinks.Union(older.UnassignedLinks), UnrepairableReason: newer.UnrepairableReason, } @@ -268,12 +256,6 @@ func (t *TorrentManager) mergeToMain(existing, toMerge *Torrent) *Torrent { mainTorrent.UnrepairableReason = older.UnrepairableReason } - // update in progress ids - mainTorrent.DownloadedIDs.Each(func(id string) bool { - mainTorrent.InProgressIDs.Remove(id) - return false - }) - // the link can have the following values // 1. https://*** - the file is available // 3. empty - the file is not available @@ -310,7 +292,10 @@ func (t *TorrentManager) mergeToMain(existing, toMerge *Torrent) *Torrent { } func (t *TorrentManager) assignedDirectoryCb(tor *Torrent, cb func(string)) { - torrentIDs := tor.DownloadedIDs.Union(tor.InProgressIDs).ToSlice() + torrentIDs := []string{} + for id := range tor.Components { + torrentIDs = append(torrentIDs, id) + } // get filenames needed for directory conditions var filenames []string var fileSizes []int64 diff --git a/internal/torrent/repair.go b/internal/torrent/repair.go index c203f5f..bdea00a 100644 --- a/internal/torrent/repair.go +++ b/internal/torrent/repair.go @@ -154,7 +154,11 @@ func (t *TorrentManager) Repair(torrent *Torrent, wg *sync.WaitGroup) { } func (t *TorrentManager) repair(torrent *Torrent) { - t.log.Infof("Started repair process for torrent %s (ids=%v)", t.GetKey(torrent), torrent.DownloadedIDs.Union(torrent.InProgressIDs).ToSlice()) + torrentIDs := []string{} + for id := range torrent.Components { + torrentIDs = append(torrentIDs, id) + } + t.log.Infof("Started repair process for torrent %s (ids=%v)", t.GetKey(torrent), torrentIDs) // handle torrents with incomplete links for selected files // torrent can be rare'ed by RD, so we need to check for that @@ -171,27 +175,14 @@ func (t *TorrentManager) repair(torrent *Torrent) { // first step: redownload the whole torrent info, err := t.redownloadTorrent(torrent, "") // reinsert the torrent, passing "" if info != nil && info.Progress != 100 { - torrent.InProgressIDs.Add(info.ID) - t.saveTorrentChangesToDisk(torrent, nil) t.log.Infof("Torrent %s (files=%s) is still in progress after redownloading but it should be repaired once done", t.GetKey(torrent), brokenFileIDs) return } else if info != nil && info.Progress == 100 && !t.isStillBroken(info, brokenFiles) { - selectedFiles := getSelectedFiles(info) - torrent.SelectedFiles.IterCb(func(_ string, oldFile *File) { - for _, newFile := range selectedFiles { - if oldFile.Bytes == newFile.Bytes { - oldFile.Link = newFile.Link - oldFile.IsBroken = false - break - } - } - }) - torrent.DownloadedIDs.Add(info.ID) - t.saveTorrentChangesToDisk(torrent, nil) t.log.Infof("Successfully repaired torrent %s (files=%s) by redownloading", t.GetKey(torrent), brokenFileIDs) return } - t.log.Warnf("Cannot repair torrent %s by redownloading (error=%s)", t.GetKey(torrent), err.Error()) + + t.log.Warnf("Cannot repair torrent %s by redownloading all files (error=%s)", t.GetKey(torrent), err.Error()) if torrent.UnrepairableReason != "" { t.log.Debugf("Torrent %s has been marked as unfixable during redownload (%s), ending repair process early", t.GetKey(torrent), torrent.UnrepairableReason) @@ -209,7 +200,10 @@ func (t *TorrentManager) repair(torrent *Torrent) { } else if len(brokenFiles) > 1 { t.log.Infof("Repairing by downloading 2 batches of the %d broken files of torrent %s", len(brokenFiles), t.GetKey(torrent)) - oldTorrentIDs := torrent.DownloadedIDs.Union(torrent.InProgressIDs).ToSlice() + oldTorrentIDs := []string{} + for id := range torrent.Components { + oldTorrentIDs = append(torrentIDs, id) + } newlyDownloadedIds := make([]string, 0) group := make([]*File, 0) @@ -270,7 +264,7 @@ func (t *TorrentManager) assignUnassignedLinks(torrent *Torrent) bool { assigned := false torrent.SelectedFiles.IterCb(func(_ string, file *File) { // base it on size because why not? - if file.Bytes == unrestrict.Filesize || strings.HasSuffix(strings.ToLower(file.Path), strings.ToLower(unrestrict.Filename)) { + if (unrestrict.Filesize > 1_000_000 && file.Bytes == unrestrict.Filesize) || strings.HasSuffix(strings.ToLower(file.Path), strings.ToLower(unrestrict.Filename)) { file.Link = link file.IsBroken = false assigned = true @@ -329,9 +323,8 @@ func (t *TorrentManager) assignUnassignedLinks(torrent *Torrent) bool { // empty/reset the unassigned links as we have assigned them already if torrent.UnassignedLinks.Cardinality() > 0 { - t.saveTorrentChangesToDisk(torrent, func(info *Torrent) { - info.UnassignedLinks = mapset.NewSet[string]() - }) + torrent.UnassignedLinks = mapset.NewSet[string]() + t.writeTorrentToFile(torrent) } return true @@ -343,7 +336,9 @@ func (t *TorrentManager) redownloadTorrent(torrent *Torrent, selection string) ( oldTorrentIDs := make([]string, 0) if selection == "" { // only delete the old torrent if we are redownloading all files - oldTorrentIDs = torrent.DownloadedIDs.Union(torrent.InProgressIDs).ToSlice() + for id := range torrent.Components { + oldTorrentIDs = append(oldTorrentIDs, id) + } tmpSelection := "" torrent.SelectedFiles.IterCb(func(_ string, file *File) { tmpSelection += fmt.Sprintf("%d,", file.ID) // select all files @@ -488,9 +483,7 @@ func (t *TorrentManager) markAsUnplayable(torrent *Torrent, reason string) { func (t *TorrentManager) markAsUnfixable(torrent *Torrent, reason string) { t.log.Warnf("Marking torrent %s as unfixable - %s", t.GetKey(torrent), reason) torrent.UnrepairableReason = reason - t.saveTorrentChangesToDisk(torrent, func(t *Torrent) { - t.UnrepairableReason = reason - }) + t.writeTorrentToFile(torrent) } // getBrokenFiles returns the files that are not http links and not deleted diff --git a/internal/torrent/types.go b/internal/torrent/torrent_types.go similarity index 54% rename from internal/torrent/types.go rename to internal/torrent/torrent_types.go index 17f5f36..0ae240d 100644 --- a/internal/torrent/types.go +++ b/internal/torrent/torrent_types.go @@ -3,38 +3,37 @@ package torrent import ( stdjson "encoding/json" "strings" - "time" "github.com/debridmediamanager/zurg/pkg/realdebrid" mapset "github.com/deckarep/golang-set/v2" jsoniter "github.com/json-iterator/go" + "github.com/looplab/fsm" cmap "github.com/orcaman/concurrent-map/v2" ) var json = jsoniter.ConfigCompatibleWithStandardLibrary type Torrent struct { - Name string `json:"Name"` // immutable - OriginalName string `json:"OriginalName"` // immutable - Hash string `json:"Hash"` // immutable - Added string `json:"Added"` // immutable - DownloadedIDs mapset.Set[string] `json:"DownloadedIDs"` // immutable - InProgressIDs mapset.Set[string] `json:"InProgressIDs"` // immutable - UnassignedLinks mapset.Set[string] `json:"UnassignedLinks"` // immutable + Name string `json:"Name"` + OriginalName string `json:"OriginalName"` + Hash string `json:"Hash"` + Added string `json:"Added"` + Components map[string]*realdebrid.TorrentInfo `json:"Components"` + + UnassignedLinks mapset.Set[string] `json:"UnassignedLinks"` // when links are not complete, we cannot assign them to a file so we store them here until it's fixed Rename string `json:"Rename"` // modified over time SelectedFiles cmap.ConcurrentMap[string, *File] `json:"-"` // modified over time UnrepairableReason string `json:"Unfixable"` // modified over time - Version string `json:"Version"` // only used for files + State *fsm.FSM `json:"-"` + Version string `json:"Version"` // only used for files } func (t *Torrent) MarshalJSON() ([]byte, error) { type Alias Torrent temp := &struct { SelectedFilesJson stdjson.RawMessage `json:"SelectedFiles"` - DownloadedIDsJson stdjson.RawMessage `json:"DownloadedIDs"` - InProgressIDsJson stdjson.RawMessage `json:"InProgressIDs"` UnassignedLinksJson stdjson.RawMessage `json:"UnassignedLinks"` *Alias }{ @@ -47,20 +46,6 @@ func (t *Torrent) MarshalJSON() ([]byte, error) { } temp.SelectedFilesJson = selectedFilesJson - if t.DownloadedIDs.IsEmpty() { - temp.DownloadedIDsJson = []byte(`""`) - } else { - downloadedIDsStr := `"` + strings.Join(t.DownloadedIDs.ToSlice(), ",") + `"` - temp.DownloadedIDsJson = []byte(downloadedIDsStr) - } - - if t.InProgressIDs.IsEmpty() { - temp.InProgressIDsJson = []byte(`""`) - } else { - inProgressIDsStr := `"` + strings.Join(t.InProgressIDs.ToSlice(), ",") + `"` - temp.InProgressIDsJson = []byte(inProgressIDsStr) - } - if t.UnassignedLinks.IsEmpty() { temp.UnassignedLinksJson = []byte(`""`) } else { @@ -75,8 +60,6 @@ func (t *Torrent) UnmarshalJSON(data []byte) error { type Alias Torrent temp := &struct { SelectedFilesJson stdjson.RawMessage `json:"SelectedFiles"` - DownloadedIDsJson stdjson.RawMessage `json:"DownloadedIDs"` - InProgressIDsJson stdjson.RawMessage `json:"InProgressIDs"` UnassignedLinksJson stdjson.RawMessage `json:"UnassignedLinks"` *Alias }{ @@ -93,20 +76,6 @@ func (t *Torrent) UnmarshalJSON(data []byte) error { } } - if len(temp.DownloadedIDsJson) > 2 { - downloadedIDs := strings.Split(strings.ReplaceAll(string(temp.DownloadedIDsJson), `"`, ""), ",") - t.DownloadedIDs = mapset.NewSet[string](downloadedIDs...) - } else { - t.DownloadedIDs = mapset.NewSet[string]() - } - - if len(temp.InProgressIDsJson) > 2 { - inProgressIDs := strings.Split(strings.ReplaceAll(string(temp.InProgressIDsJson), `"`, ""), ",") - t.InProgressIDs = mapset.NewSet[string](inProgressIDs...) - } else { - t.InProgressIDs = mapset.NewSet[string]() - } - if len(temp.UnassignedLinksJson) > 2 { unassignedLinks := strings.Split(strings.ReplaceAll(string(temp.UnassignedLinksJson), `"`, ""), ",") t.UnassignedLinks = mapset.NewSet[string](unassignedLinks...) @@ -118,11 +87,21 @@ func (t *Torrent) UnmarshalJSON(data []byte) error { } func (t *Torrent) AnyInProgress() bool { - return !t.InProgressIDs.IsEmpty() + for _, info := range t.Components { + if info.Progress != 100 { + return true + } + } + return false } func (t *Torrent) AllInProgress() bool { - return t.DownloadedIDs.IsEmpty() && !t.InProgressIDs.IsEmpty() + for _, info := range t.Components { + if info.Progress == 100 { + return false + } + } + return true } func (t *Torrent) ComputeTotalSize() int64 { @@ -133,6 +112,7 @@ func (t *Torrent) ComputeTotalSize() int64 { return totalSize } +// used for showing only the biggest file in directory func (t *Torrent) ComputeBiggestFileSize() int64 { biggestSize := int64(0) t.SelectedFiles.IterCb(func(key string, value *File) { @@ -142,20 +122,3 @@ func (t *Torrent) ComputeBiggestFileSize() int64 { }) return biggestSize } - -func (t *Torrent) OlderThanDuration(duration time.Duration) bool { - added, err := time.Parse(time.RFC3339, t.Added) - if err != nil { - return false - } - return time.Since(added) > duration -} - -type File struct { - realdebrid.File - Ended string `json:"Ended"` - Link string `json:"Link"` - IsBroken bool `json:"IsBroken"` - IsDeleted bool `json:"IsDeleted"` - Rename string `json:"Rename"` -}