package torrent import ( "fmt" "path/filepath" "strings" "sync" "time" "github.com/debridmediamanager/zurg/internal/config" "github.com/debridmediamanager/zurg/pkg/realdebrid" mapset "github.com/deckarep/golang-set/v2" cmap "github.com/orcaman/concurrent-map/v2" ) func (t *TorrentManager) RefreshTorrents() []string { instances, _, err := t.Api.GetTorrents(0, false) if err != nil { t.log.Warnf("Cannot get torrents: %v", err) return nil } infoChan := make(chan *Torrent, len(instances)) var wg sync.WaitGroup for i := range instances { idx := i _ = t.workerPool.Submit(func() { defer wg.Done() infoChan <- t.getMoreInfo(instances[idx]) }) wg.Add(1) } wg.Wait() close(infoChan) t.log.Debugf("Fetched info for %d torrents", len(instances)) freshKeys := mapset.NewSet[string]() allTorrents, _ := t.DirectoryMap.Get(INT_ALL) noInfoCount := 0 for info := range infoChan { if info == nil { noInfoCount++ continue } if !info.AnyInProgress() { freshKeys.Add(t.GetKey(info)) } if torrent, exists := allTorrents.Get(t.GetKey(info)); !exists { allTorrents.Set(t.GetKey(info), info) } else if !info.DownloadedIDs.Difference(torrent.DownloadedIDs).IsEmpty() { mainTorrent := t.mergeToMain(torrent, info) allTorrents.Set(t.GetKey(info), &mainTorrent) } } t.log.Infof("Compiled into %d torrents, %d were missing info", allTorrents.Count(), noInfoCount) var updatedPaths []string // torrents yet to be assigned in a directory freshKeys.Difference(t.allAccessKeys).Each(func(accessKey string) bool { // assign to directories tor, ok := allTorrents.Get(accessKey) if !ok { return false } var directories []string t.assignedDirectoryCb(tor, func(directory string) { torrents, _ := t.DirectoryMap.Get(directory) torrents.Set(accessKey, tor) 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.log.Debugf("Added %s to %v", accessKey, directories) t.allAccessKeys.Add(accessKey) return false }) // removed torrents t.allAccessKeys.Difference(freshKeys).Each(func(accessKey string) bool { t.Delete(accessKey, false) return false }) return updatedPaths } // startRefreshJob periodically refreshes the torrents func (t *TorrentManager) startRefreshJob() { t.log.Info("Starting periodic refresh") for { <-time.After(time.Duration(t.Config.GetRefreshEverySeconds()) * time.Second) checksum := t.getCurrentState() if t.latestState.equal(checksum) { continue } t.SetNewLatestState(checksum) t.log.Infof("Detected changes! Refreshing %d torrents", checksum.TotalCount) updatedPaths := t.RefreshTorrents() t.log.Info("Finished refreshing torrents") t.TriggerHookOnLibraryUpdate(updatedPaths) if t.Config.EnableRepair() { t.RepairAll() } else { t.log.Info("Repair is disabled, skipping repair check") } } } // 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 torrentFromCache, exists := infoCache.Get(rdTorrent.ID); exists && !torrentFromCache.AnyInProgress() && torrentFromCache.SelectedFiles.Count() == len(rdTorrent.Links) { return torrentFromCache } torrentFromFile := t.readTorrentFromFile(rdTorrent.ID) if torrentFromFile != nil && !torrentFromFile.AnyInProgress() && torrentFromFile.SelectedFiles.Count() == len(rdTorrent.Links) { infoCache.Set(rdTorrent.ID, torrentFromFile) return torrentFromFile } 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{ Name: info.Name, OriginalName: info.OriginalName, LatestAdded: info.Added, Hash: info.Hash, } // SelectedFiles is a subset of Files with only the selected ones // it also has a Link field, which can be empty // if it is empty, it means the file is no longer available // Files+Links together are the same as SelectedFiles var selectedFiles []*File // if some Links are empty, we need to repair it for _, file := range info.Files { if file.Selected == 0 { continue } selectedFiles = append(selectedFiles, &File{ File: file, Ended: info.Ended, Link: "", // no link yet }) } if len(selectedFiles) == len(info.Links) { // all links are still intact! good! for i, file := range selectedFiles { file.Link = info.Links[i] } torrent.UnassignedLinks = mapset.NewSet[string]() } else { torrent.UnassignedLinks = mapset.NewSet[string](info.Links...) } torrent.SelectedFiles = cmap.New[*File]() for _, file := range selectedFiles { // todo better handling of duplicate filenames if torrent.SelectedFiles.Has(filepath.Base(file.Path)) { oldName := filepath.Base(file.Path) ext := filepath.Ext(oldName) filename := strings.TrimSuffix(oldName, ext) newName := fmt.Sprintf("%s (%d)%s", filename, file.ID, ext) torrent.SelectedFiles.Set(newName, file) } else { torrent.SelectedFiles.Set(filepath.Base(file.Path), file) } } torrent.DownloadedIDs = mapset.NewSet[string]() torrent.InProgressIDs = mapset.NewSet[string]() if info.Progress == 100 { torrent.DownloadedIDs.Add(info.ID) } else { torrent.InProgressIDs.Add(info.ID) } t.writeTorrentToFile(rdTorrent.ID, &torrent, true) infoCache.Set(rdTorrent.ID, &torrent) return &torrent } func (t *TorrentManager) mergeToMain(existing, toMerge *Torrent) Torrent { mainTorrent := Torrent{ Name: existing.Name, OriginalName: existing.OriginalName, Rename: existing.Rename, Hash: existing.Hash, DownloadedIDs: mapset.NewSet[string](), InProgressIDs: mapset.NewSet[string](), Unfixable: existing.Unfixable || toMerge.Unfixable, UnassignedLinks: existing.UnassignedLinks.Union(toMerge.UnassignedLinks), } // this function triggers only when we have a new DownloadedID toMerge.DownloadedIDs.Difference(existing.DownloadedIDs).Each(func(id string) bool { mainTorrent.DownloadedIDs.Add(id) mainTorrent.InProgressIDs.Remove(id) return false }) // the link can have the following values // 1. https://*** - the file is available // 2. repair - the file is available but we need to repair it // 3. repairing - the file is being repaired // 4. unselect - the file is deleted mainTorrent.SelectedFiles = existing.SelectedFiles toMerge.SelectedFiles.IterCb(func(filepath string, fileToMerge *File) { // see if it already exists in the main torrent if originalFile, ok := mainTorrent.SelectedFiles.Get(filepath); !ok || fileToMerge.Link == "unselect" { // if it doesn't exist in the main torrent, add it mainTorrent.SelectedFiles.Set(filepath, fileToMerge) } else if originalFile.Link != "unselect" { // if it exists, compare the LatestAdded property and the link if existing.LatestAdded < toMerge.LatestAdded && strings.HasPrefix(fileToMerge.Link, "http") { // if torrentToMerge is more recent and its file has a link, update the main torrent's file mainTorrent.SelectedFiles.Set(filepath, fileToMerge) } // else do nothing, the main torrent's file is more recent or has a valid link } }) if existing.LatestAdded < toMerge.LatestAdded { mainTorrent.LatestAdded = toMerge.LatestAdded } else { mainTorrent.LatestAdded = existing.LatestAdded } return mainTorrent }