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
### 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

1
go.mod
View File

@@ -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

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/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=

View File

@@ -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)

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 {
t.api.DeleteTorrent(id)
infoCache.Remove(id)
t.deleteTorrentFile(id)
t.deleteInfoFile(id)
}
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 (
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
})
}

View File

@@ -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

View File

@@ -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

View File

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