Introduce components

This commit is contained in:
Ben Sarmiento
2024-05-20 20:43:19 +02:00
parent ab81eb5f39
commit a3a24124a8
11 changed files with 222 additions and 202 deletions

View File

@@ -6,14 +6,14 @@ A self-hosted Real-Debrid webdav server written from scratch. Together with [rcl
## Download ## 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 [Download the binary](https://github.com/debridmediamanager/zurg-testing/releases) or use docker
```sh ```sh
docker pull ghcr.io/debridmediamanager/zurg-testing:latest docker pull ghcr.io/debridmediamanager/zurg-testing:latest
# or # 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 ## How to run zurg in 5 steps for Plex with Docker

1
go.mod
View File

@@ -19,6 +19,7 @@ require (
) )
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/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect

2
go.sum
View File

@@ -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/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 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 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 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=

View File

@@ -12,16 +12,7 @@ func (t *TorrentManager) CheckDeletedStatus(torrent *Torrent) bool {
if len(deletedIDs) == torrent.SelectedFiles.Count() && len(deletedIDs) > 0 { if len(deletedIDs) == torrent.SelectedFiles.Count() && len(deletedIDs) > 0 {
return true return true
} else if len(deletedIDs) > 0 { } else if len(deletedIDs) > 0 {
t.saveTorrentChangesToDisk(torrent, func(info *Torrent) { t.writeTorrentToFile(torrent)
info.SelectedFiles.IterCb(func(_ string, file *File) {
for _, deletedID := range deletedIDs {
if file.ID == deletedID {
file.IsDeleted = true
break
}
}
})
})
} }
return false return false
} }
@@ -29,15 +20,12 @@ func (t *TorrentManager) CheckDeletedStatus(torrent *Torrent) bool {
func (t *TorrentManager) Delete(accessKey string, deleteInRD bool) { func (t *TorrentManager) Delete(accessKey string, deleteInRD bool) {
allTorrents, _ := t.DirectoryMap.Get(INT_ALL) allTorrents, _ := t.DirectoryMap.Get(INT_ALL)
if deleteInRD { if deleteInRD {
infoCache, _ := t.DirectoryMap.Get(INT_INFO_CACHE)
if torrent, ok := allTorrents.Get(accessKey); ok { if torrent, ok := allTorrents.Get(accessKey); ok {
torrent.DownloadedIDs.Union(torrent.InProgressIDs).Each(func(id string) bool { for torrentID := range torrent.Components {
t.log.Debugf("Deleting torrent %s (id=%s) in RD", accessKey, id) t.log.Debugf("Deleting torrent %s (id=%s) in RD", accessKey, torrentID)
t.api.DeleteTorrent(id) t.api.DeleteTorrent(torrentID)
infoCache.Remove(id) t.deleteInfoFile(torrentID)
t.deleteTorrentFile(id) }
return false
})
} }
} }
t.log.Infof("Removing torrent %s from zurg database (not real-debrid)", accessKey) t.log.Infof("Removing torrent %s from zurg database (not real-debrid)", accessKey)

View File

@@ -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"`
}

View File

@@ -60,11 +60,9 @@ func (t *TorrentManager) processFixers(instances []realdebrid.Torrent) {
} }
} }
infoCache, _ := t.DirectoryMap.Get(INT_INFO_CACHE)
for _, id := range toDelete { for _, id := range toDelete {
t.api.DeleteTorrent(id) t.api.DeleteTorrent(id)
infoCache.Remove(id) t.deleteInfoFile(id)
t.deleteTorrentFile(id)
} }
for _, torrent := range toRedownload { for _, torrent := range toRedownload {

31
internal/torrent/fsm.go Normal file
View File

@@ -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{},
)
}

View File

@@ -18,8 +18,7 @@ import (
) )
const ( const (
INT_ALL = "int__all__" INT_ALL = "int__all__"
INT_INFO_CACHE = "int__info__"
) )
type TorrentManager struct { type TorrentManager struct {
@@ -41,7 +40,6 @@ type TorrentManager struct {
latestState *LibraryState latestState *LibraryState
allAccessKeys mapset.Set[string] allAccessKeys mapset.Set[string]
allIDs mapset.Set[string]
fixers cmap.ConcurrentMap[string, string] // trigger -> [command, id] fixers cmap.ConcurrentMap[string, string] // trigger -> [command, id]
repairTrigger chan *Torrent repairTrigger chan *Torrent
@@ -55,7 +53,7 @@ type TorrentManager struct {
// and store them in-memory and cached in files // and store them in-memory and cached in files
func NewTorrentManager(cfg config.ConfigInterface, api *realdebrid.RealDebrid, workerPool *ants.Pool, log *logutil.Logger) *TorrentManager { func NewTorrentManager(cfg config.ConfigInterface, api *realdebrid.RealDebrid, workerPool *ants.Pool, log *logutil.Logger) *TorrentManager {
t := &TorrentManager{ t := &TorrentManager{
requiredVersion: "0.9.3-hotfix.10", requiredVersion: "0.10.0",
Config: cfg, Config: cfg,
api: api, api: api,
@@ -73,7 +71,6 @@ func NewTorrentManager(cfg config.ConfigInterface, api *realdebrid.RealDebrid, w
latestState: &LibraryState{}, latestState: &LibraryState{},
allAccessKeys: mapset.NewSet[string](), allAccessKeys: mapset.NewSet[string](),
allIDs: mapset.NewSet[string](),
} }
t.fixers = t.readFixersFromFile() t.fixers = t.readFixersFromFile()
@@ -140,8 +137,10 @@ func (t *TorrentManager) GetPath(file *File) string {
return filename return filename
} }
func (t *TorrentManager) writeTorrentToFile(instanceID string, torrent *Torrent) { /// torrent functions
filePath := "data/" + instanceID + ".json"
func (t *TorrentManager) writeTorrentToFile(torrent *Torrent) {
filePath := "data/" + torrent.Hash + ".json"
file, err := os.Create(filePath) file, err := os.Create(filePath)
if err != nil { if err != nil {
t.log.Warnf("Cannot create file %s: %v", filePath, err) t.log.Warnf("Cannot create file %s: %v", filePath, err)
@@ -162,11 +161,11 @@ func (t *TorrentManager) writeTorrentToFile(instanceID string, torrent *Torrent)
return 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 { func (t *TorrentManager) readTorrentFromFile(hash string) *Torrent {
filePath := "data/" + torrentID + ".json" filePath := "data/" + hash + ".json"
file, err := os.Open(filePath) file, err := os.Open(filePath)
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
@@ -183,7 +182,7 @@ func (t *TorrentManager) readTorrentFromFile(torrentID string) *Torrent {
if err := json.Unmarshal(jsonData, &torrent); err != nil { if err := json.Unmarshal(jsonData, &torrent); err != nil {
return 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") t.log.Fatal("Torrent has no downloaded or in progress ids")
} }
if torrent.Version != t.requiredVersion { if torrent.Version != t.requiredVersion {
@@ -192,14 +191,72 @@ func (t *TorrentManager) readTorrentFromFile(torrentID string) *Torrent {
return torrent return torrent
} }
func (t *TorrentManager) deleteTorrentFile(torrentID string) { func (t *TorrentManager) deleteTorrentFile(hash string) {
filePath := "data/" + torrentID + ".json" filePath := "data/" + hash + ".json"
err := os.Remove(filePath) err := os.Remove(filePath)
if err != nil { if err != nil {
t.log.Warnf("Cannot delete file %s: %v", filePath, err) 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() { func (t *TorrentManager) mountDownloads() {
if !t.Config.EnableDownloadMount() { if !t.Config.EnableDownloadMount() {
return return
@@ -246,26 +303,10 @@ func (t *TorrentManager) StartDownloadsJob() {
func (t *TorrentManager) initializeDirectories() { func (t *TorrentManager) initializeDirectories() {
// create internal directories // create internal directories
t.DirectoryMap.Set(INT_ALL, cmap.New[*Torrent]()) // key is GetAccessKey() t.DirectoryMap.Set(INT_ALL, cmap.New[*Torrent]()) // key is GetAccessKey()
t.DirectoryMap.Set(INT_INFO_CACHE, cmap.New[*Torrent]()) // key is Torrent ID
// create directory maps // create directory maps
for _, directory := range t.Config.GetDirectories() { for _, directory := range t.Config.GetDirectories() {
t.DirectoryMap.Set(directory, cmap.New[*Torrent]()) t.DirectoryMap.Set(directory, cmap.New[*Torrent]())
// t.RootNode.AddChild(fs.NewFileNode(directory, true)) // 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
})
}

View File

@@ -21,7 +21,7 @@ func (t *TorrentManager) refreshTorrents(isInitialRun bool) []string {
return nil return nil
} }
t.log.Infof("Fetched %d torrents", len(instances)) t.log.Infof("Fetched %d torrents", len(instances))
infoChan := make(chan *Torrent, len(instances)) torChan := make(chan *Torrent, len(instances))
var wg sync.WaitGroup var wg sync.WaitGroup
for i := range instances { for i := range instances {
@@ -29,60 +29,56 @@ func (t *TorrentManager) refreshTorrents(isInitialRun bool) []string {
wg.Add(1) wg.Add(1)
_ = t.workerPool.Submit(func() { _ = t.workerPool.Submit(func() {
defer wg.Done() defer wg.Done()
infoChan <- t.getMoreInfo(instances[idx]) torChan <- t.getMoreInfo(instances[idx])
}) })
} }
wg.Wait() wg.Wait()
close(infoChan) close(torChan)
t.log.Infof("Fetched info for %d torrents", len(instances)) t.log.Infof("Fetched info for %d torrents", len(instances))
var updatedPaths []string var updatedPaths []string
noInfoCount := 0 noInfoCount := 0
allTorrents, _ := t.DirectoryMap.Get(INT_ALL) allTorrents, _ := t.DirectoryMap.Get(INT_ALL)
freshAccessKeys := mapset.NewSet[string]() freshAccessKeys := mapset.NewSet[string]()
deletedIDs := t.allIDs.Clone() for torrent := range torChan {
for info := range infoChan { if torrent == nil {
if info == nil {
noInfoCount++ noInfoCount++
continue continue
} }
infoID, _ := info.DownloadedIDs.Clone().Pop() // there's only 1 component torrent at this point, let's get it
deletedIDs.Remove(infoID) var tInfo *realdebrid.TorrentInfo
accessKey := t.GetKey(info) for _, tInfo = range torrent.Components {
break
}
accessKey := t.GetKey(torrent)
freshAccessKeys.Add(accessKey) freshAccessKeys.Add(accessKey)
// update allTorrents // update allTorrents
isNewID := false
mainTorrent, exists := allTorrents.Get(accessKey) mainTorrent, exists := allTorrents.Get(accessKey)
if !exists { if !exists {
allTorrents.Set(accessKey, info) allTorrents.Set(accessKey, torrent)
} else { mainTorrent = torrent
if !mainTorrent.DownloadedIDs.Contains(infoID) { isNewID = true
merged := t.mergeToMain(mainTorrent, info) } else if _, ok := mainTorrent.Components[tInfo.ID]; !ok {
allTorrents.Set(accessKey, merged) merged := t.mergeToMain(mainTorrent, torrent)
} allTorrents.Set(accessKey, merged)
mainTorrent = merged
isNewID = true
} }
// check for newly finished torrents for assigning to directories if isNewID && tInfo.Progress == 100 {
isDone := info.DownloadedIDs.Cardinality() > 0 && info.InProgressIDs.IsEmpty() // assign to directory
if isDone && !t.allIDs.Contains(infoID) { t.assignedDirectoryCb(mainTorrent, func(directory string) {
var directories []string
mainTor, _ := allTorrents.Get(accessKey)
t.assignedDirectoryCb(mainTor, func(directory string) {
listing, _ := t.DirectoryMap.Get(directory) listing, _ := t.DirectoryMap.Get(directory)
listing.Set(accessKey, mainTor) listing.Set(accessKey, mainTorrent)
updatedPaths = append(updatedPaths, fmt.Sprintf("%s/%s", directory, accessKey)) 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) t.log.Infof("Compiled into %d torrents, %d were missing info", allTorrents.Count(), noInfoCount)
// removed torrents // removed torrents
@@ -138,24 +134,14 @@ func (t *TorrentManager) StartRefreshJob() {
// getMoreInfo gets original name, size and files for a torrent // getMoreInfo gets original name, size and files for a torrent
func (t *TorrentManager) getMoreInfo(rdTorrent realdebrid.Torrent) *Torrent { func (t *TorrentManager) getMoreInfo(rdTorrent realdebrid.Torrent) *Torrent {
infoCache, _ := t.DirectoryMap.Get(INT_INFO_CACHE) info := t.readInfoFromFile(rdTorrent.ID)
if info == nil {
if cachedTor, exists := infoCache.Get(rdTorrent.ID); exists && var err error
cachedTor.SelectedFiles.Count() == len(rdTorrent.Links) { info, err = t.api.GetTorrentInfo(rdTorrent.ID)
if err != nil {
return cachedTor t.log.Warnf("Cannot get info for id=%s: %v", rdTorrent.ID, err)
return nil
} 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
} }
torrent := Torrent{ torrent := Torrent{
@@ -204,15 +190,10 @@ func (t *TorrentManager) getMoreInfo(rdTorrent realdebrid.Torrent) *Torrent {
torrent.SelectedFiles.Set(filename, file) torrent.SelectedFiles.Set(filename, file)
} }
} }
torrent.DownloadedIDs = mapset.NewSet[string]() torrent.Components = map[string]*realdebrid.TorrentInfo{rdTorrent.ID: info}
torrent.InProgressIDs = mapset.NewSet[string]()
if rdTorrent.Progress == 100 { if info.Progress == 100 {
torrent.DownloadedIDs.Add(info.ID) t.writeInfoToFile(info)
// 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)
} }
return &torrent return &torrent
@@ -247,6 +228,14 @@ func (t *TorrentManager) mergeToMain(existing, toMerge *Torrent) *Torrent {
older = toMerge 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 // build the main torrent
mainTorrent := Torrent{ mainTorrent := Torrent{
Name: newer.Name, Name: newer.Name,
@@ -255,8 +244,7 @@ func (t *TorrentManager) mergeToMain(existing, toMerge *Torrent) *Torrent {
Hash: newer.Hash, Hash: newer.Hash,
Added: newer.Added, Added: newer.Added,
DownloadedIDs: newer.DownloadedIDs.Union(older.DownloadedIDs), Components: mergedComponents,
InProgressIDs: newer.InProgressIDs.Union(older.InProgressIDs),
UnassignedLinks: newer.UnassignedLinks.Union(older.UnassignedLinks), UnassignedLinks: newer.UnassignedLinks.Union(older.UnassignedLinks),
UnrepairableReason: newer.UnrepairableReason, UnrepairableReason: newer.UnrepairableReason,
} }
@@ -268,12 +256,6 @@ func (t *TorrentManager) mergeToMain(existing, toMerge *Torrent) *Torrent {
mainTorrent.UnrepairableReason = older.UnrepairableReason 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 // the link can have the following values
// 1. https://*** - the file is available // 1. https://*** - the file is available
// 3. empty - the file is not 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)) { 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 // get filenames needed for directory conditions
var filenames []string var filenames []string
var fileSizes []int64 var fileSizes []int64

View File

@@ -154,7 +154,11 @@ func (t *TorrentManager) Repair(torrent *Torrent, wg *sync.WaitGroup) {
} }
func (t *TorrentManager) repair(torrent *Torrent) { 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 // handle torrents with incomplete links for selected files
// torrent can be rare'ed by RD, so we need to check for that // 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 // first step: redownload the whole torrent
info, err := t.redownloadTorrent(torrent, "") // reinsert the torrent, passing "" info, err := t.redownloadTorrent(torrent, "") // reinsert the torrent, passing ""
if info != nil && info.Progress != 100 { 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) 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 return
} else if info != nil && info.Progress == 100 && !t.isStillBroken(info, brokenFiles) { } 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) t.log.Infof("Successfully repaired torrent %s (files=%s) by redownloading", t.GetKey(torrent), brokenFileIDs)
return 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 != "" { 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) 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 { } 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)) 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) newlyDownloadedIds := make([]string, 0)
group := make([]*File, 0) group := make([]*File, 0)
@@ -270,7 +264,7 @@ func (t *TorrentManager) assignUnassignedLinks(torrent *Torrent) bool {
assigned := false assigned := false
torrent.SelectedFiles.IterCb(func(_ string, file *File) { torrent.SelectedFiles.IterCb(func(_ string, file *File) {
// base it on size because why not? // 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.Link = link
file.IsBroken = false file.IsBroken = false
assigned = true assigned = true
@@ -329,9 +323,8 @@ func (t *TorrentManager) assignUnassignedLinks(torrent *Torrent) bool {
// empty/reset the unassigned links as we have assigned them already // empty/reset the unassigned links as we have assigned them already
if torrent.UnassignedLinks.Cardinality() > 0 { if torrent.UnassignedLinks.Cardinality() > 0 {
t.saveTorrentChangesToDisk(torrent, func(info *Torrent) { torrent.UnassignedLinks = mapset.NewSet[string]()
info.UnassignedLinks = mapset.NewSet[string]() t.writeTorrentToFile(torrent)
})
} }
return true return true
@@ -343,7 +336,9 @@ func (t *TorrentManager) redownloadTorrent(torrent *Torrent, selection string) (
oldTorrentIDs := make([]string, 0) oldTorrentIDs := make([]string, 0)
if selection == "" { if selection == "" {
// only delete the old torrent if we are redownloading all files // 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 := "" tmpSelection := ""
torrent.SelectedFiles.IterCb(func(_ string, file *File) { torrent.SelectedFiles.IterCb(func(_ string, file *File) {
tmpSelection += fmt.Sprintf("%d,", file.ID) // select all files 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) { func (t *TorrentManager) markAsUnfixable(torrent *Torrent, reason string) {
t.log.Warnf("Marking torrent %s as unfixable - %s", t.GetKey(torrent), reason) t.log.Warnf("Marking torrent %s as unfixable - %s", t.GetKey(torrent), reason)
torrent.UnrepairableReason = reason torrent.UnrepairableReason = reason
t.saveTorrentChangesToDisk(torrent, func(t *Torrent) { t.writeTorrentToFile(torrent)
t.UnrepairableReason = reason
})
} }
// getBrokenFiles returns the files that are not http links and not deleted // getBrokenFiles returns the files that are not http links and not deleted

View File

@@ -3,38 +3,37 @@ package torrent
import ( import (
stdjson "encoding/json" stdjson "encoding/json"
"strings" "strings"
"time"
"github.com/debridmediamanager/zurg/pkg/realdebrid" "github.com/debridmediamanager/zurg/pkg/realdebrid"
mapset "github.com/deckarep/golang-set/v2" mapset "github.com/deckarep/golang-set/v2"
jsoniter "github.com/json-iterator/go" jsoniter "github.com/json-iterator/go"
"github.com/looplab/fsm"
cmap "github.com/orcaman/concurrent-map/v2" cmap "github.com/orcaman/concurrent-map/v2"
) )
var json = jsoniter.ConfigCompatibleWithStandardLibrary var json = jsoniter.ConfigCompatibleWithStandardLibrary
type Torrent struct { type Torrent struct {
Name string `json:"Name"` // immutable Name string `json:"Name"`
OriginalName string `json:"OriginalName"` // immutable OriginalName string `json:"OriginalName"`
Hash string `json:"Hash"` // immutable Hash string `json:"Hash"`
Added string `json:"Added"` // immutable Added string `json:"Added"`
DownloadedIDs mapset.Set[string] `json:"DownloadedIDs"` // immutable Components map[string]*realdebrid.TorrentInfo `json:"Components"`
InProgressIDs mapset.Set[string] `json:"InProgressIDs"` // immutable
UnassignedLinks mapset.Set[string] `json:"UnassignedLinks"` // immutable 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 Rename string `json:"Rename"` // modified over time
SelectedFiles cmap.ConcurrentMap[string, *File] `json:"-"` // modified over time SelectedFiles cmap.ConcurrentMap[string, *File] `json:"-"` // modified over time
UnrepairableReason string `json:"Unfixable"` // 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) { func (t *Torrent) MarshalJSON() ([]byte, error) {
type Alias Torrent type Alias Torrent
temp := &struct { temp := &struct {
SelectedFilesJson stdjson.RawMessage `json:"SelectedFiles"` SelectedFilesJson stdjson.RawMessage `json:"SelectedFiles"`
DownloadedIDsJson stdjson.RawMessage `json:"DownloadedIDs"`
InProgressIDsJson stdjson.RawMessage `json:"InProgressIDs"`
UnassignedLinksJson stdjson.RawMessage `json:"UnassignedLinks"` UnassignedLinksJson stdjson.RawMessage `json:"UnassignedLinks"`
*Alias *Alias
}{ }{
@@ -47,20 +46,6 @@ func (t *Torrent) MarshalJSON() ([]byte, error) {
} }
temp.SelectedFilesJson = selectedFilesJson 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() { if t.UnassignedLinks.IsEmpty() {
temp.UnassignedLinksJson = []byte(`""`) temp.UnassignedLinksJson = []byte(`""`)
} else { } else {
@@ -75,8 +60,6 @@ func (t *Torrent) UnmarshalJSON(data []byte) error {
type Alias Torrent type Alias Torrent
temp := &struct { temp := &struct {
SelectedFilesJson stdjson.RawMessage `json:"SelectedFiles"` SelectedFilesJson stdjson.RawMessage `json:"SelectedFiles"`
DownloadedIDsJson stdjson.RawMessage `json:"DownloadedIDs"`
InProgressIDsJson stdjson.RawMessage `json:"InProgressIDs"`
UnassignedLinksJson stdjson.RawMessage `json:"UnassignedLinks"` UnassignedLinksJson stdjson.RawMessage `json:"UnassignedLinks"`
*Alias *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 { if len(temp.UnassignedLinksJson) > 2 {
unassignedLinks := strings.Split(strings.ReplaceAll(string(temp.UnassignedLinksJson), `"`, ""), ",") unassignedLinks := strings.Split(strings.ReplaceAll(string(temp.UnassignedLinksJson), `"`, ""), ",")
t.UnassignedLinks = mapset.NewSet[string](unassignedLinks...) t.UnassignedLinks = mapset.NewSet[string](unassignedLinks...)
@@ -118,11 +87,21 @@ func (t *Torrent) UnmarshalJSON(data []byte) error {
} }
func (t *Torrent) AnyInProgress() bool { 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 { 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 { func (t *Torrent) ComputeTotalSize() int64 {
@@ -133,6 +112,7 @@ func (t *Torrent) ComputeTotalSize() int64 {
return totalSize return totalSize
} }
// used for showing only the biggest file in directory
func (t *Torrent) ComputeBiggestFileSize() int64 { func (t *Torrent) ComputeBiggestFileSize() int64 {
biggestSize := int64(0) biggestSize := int64(0)
t.SelectedFiles.IterCb(func(key string, value *File) { t.SelectedFiles.IterCb(func(key string, value *File) {
@@ -142,20 +122,3 @@ func (t *Torrent) ComputeBiggestFileSize() int64 {
}) })
return biggestSize 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"`
}