Introduce components
This commit is contained in:
@@ -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
1
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
|
||||
|
||||
2
go.sum
2
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=
|
||||
|
||||
@@ -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)
|
||||
|
||||
18
internal/torrent/file_types.go
Normal file
18
internal/torrent/file_types.go
Normal 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"`
|
||||
}
|
||||
@@ -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
31
internal/torrent/fsm.go
Normal 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{},
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
Reference in New Issue
Block a user