Implement new fsm

This commit is contained in:
Ben Sarmiento
2024-05-21 01:59:53 +02:00
parent d5dd9426ed
commit 2c5e7a1db0
14 changed files with 121 additions and 59 deletions

View File

@@ -1,6 +1,7 @@
package dav
import (
"context"
"fmt"
"github.com/debridmediamanager/zurg/internal/config"
@@ -29,14 +30,17 @@ func HandleDeleteFile(directory, torrentName, fileName string, torMgr *torrent.T
return fmt.Errorf("cannot find torrent %s", torrentName)
}
file, ok := torrent.SelectedFiles.Get(fileName)
if !ok || file.IsDeleted {
if !ok || !file.State.Is("ok") {
return fmt.Errorf("cannot find file %s", fileName)
}
dirCfg := torMgr.Config.(*config.ZurgConfigV1).GetDirectoryConfig(directory)
if dirCfg.OnlyShowTheBiggestFile {
torMgr.Delete(torrentName, true)
} else {
file.IsDeleted = true
err := file.State.Event(context.Background(), "delete")
if err != nil {
return fmt.Errorf("cannot delete file %s: %v", fileName, err)
}
if torMgr.CheckDeletedStatus(torrent) {
torMgr.Delete(torrentName, true)
}

View File

@@ -75,7 +75,7 @@ func ServeFilesListForInfuse(directory, torrentName string, torMgr *torrent.Torr
sort.Strings(filenames)
for _, filename := range filenames {
file, _ := tor.SelectedFiles.Get(filename)
if file.IsDeleted {
if !file.State.Is("ok") {
continue
}
if dirCfg.OnlyShowTheBiggestFile && file.Bytes < biggestFileSize {

View File

@@ -79,7 +79,7 @@ func ServeFilesList(directory, torrentName string, torMgr *torrent.TorrentManage
sort.Strings(filenames)
for _, filename := range filenames {
file, _ := tor.SelectedFiles.Get(filename)
if file.IsDeleted {
if !file.State.Is("ok") {
continue
}
if dirCfg.OnlyShowTheBiggestFile && file.Bytes < biggestFileSize {
@@ -107,7 +107,7 @@ func HandleSingleFile(directory, torrentName, fileName string, torMgr *torrent.T
return nil, fmt.Errorf("cannot find torrent %s", torrentName)
}
file, ok := tor.SelectedFiles.Get(fileName)
if !ok || file.IsDeleted {
if !ok || !file.State.Is("ok") {
return nil, fmt.Errorf("cannot find file %s", fileName)
}

View File

@@ -31,7 +31,7 @@ func HandleRenameFile(directory, torrentName, fileName, newName string, torMgr *
return fmt.Errorf("cannot find torrent %s", torrentName)
}
file, ok := torrent.SelectedFiles.Get(fileName)
if !ok || file.IsDeleted {
if !ok || !file.State.Is("ok") {
return fmt.Errorf("cannot find file %s", fileName)
}
oldName := torMgr.GetPath(file)

View File

@@ -75,7 +75,7 @@ func ServeFilesList(directory, torrentName string, torMgr *torrent.TorrentManage
sort.Strings(filenames)
for _, filename := range filenames {
file, _ := tor.SelectedFiles.Get(filename)
if file.IsDeleted {
if !file.State.Is("ok") {
continue
}
if dirCfg.OnlyShowTheBiggestFile && file.Bytes < biggestFileSize {

View File

@@ -6,7 +6,7 @@ import cmap "github.com/orcaman/concurrent-map/v2"
func (t *TorrentManager) CheckDeletedStatus(torrent *Torrent) bool {
var deletedIDs []int
torrent.SelectedFiles.IterCb(func(_ string, file *File) {
if file.IsDeleted {
if file.State.Is("deleted") {
deletedIDs = append(deletedIDs, file.ID)
}
})

View File

@@ -1,6 +1,8 @@
package torrent
import (
stdjson "encoding/json"
"github.com/debridmediamanager/zurg/pkg/realdebrid"
"github.com/looplab/fsm"
)
@@ -10,9 +12,39 @@ type File struct {
Link string `json:"Link"`
Ended string `json:"Ended"`
IsBroken bool `json:"IsBroken"`
IsDeleted bool `json:"IsDeleted"`
State *fsm.FSM `json:"-"`
State *fsm.FSM `json:"-"`
Rename string `json:"Rename"`
}
func (f *File) MarshalJSON() ([]byte, error) {
type Alias File
temp := &struct {
StateJson stdjson.RawMessage `json:"State"`
*Alias
}{
Alias: (*Alias)(f),
}
temp.StateJson = []byte(`"` + f.State.Current() + `"`)
return json.Marshal(temp)
}
func (f *File) UnmarshalJSON(data []byte) error {
type Alias File
temp := &struct {
StateJson string `json:"State"`
*Alias
}{
Alias: (*Alias)(f),
}
if err := json.Unmarshal(data, &temp); err != nil {
return err
}
f.State = NewFileState(temp.StateJson)
return nil
}

View File

@@ -106,7 +106,7 @@ func (t *TorrentManager) UnrestrictLinkUntilOk(link string) *realdebrid.Download
}
func (t *TorrentManager) UnrestrictFileUntilOk(file *File) *realdebrid.Download {
if file.IsBroken || file.IsDeleted {
if !file.State.Is("ok") {
return nil
}
return t.UnrestrictLinkUntilOk(file.Link)

View File

@@ -159,6 +159,7 @@ func (t *TorrentManager) getMoreInfo(rdTorrent realdebrid.Torrent) *Torrent {
OriginalName: info.OriginalName,
Added: info.Added,
Hash: info.Hash,
State: NewTorrentState("ok"),
}
// SelectedFiles is a subset of Files with only the selected ones
@@ -171,17 +172,17 @@ func (t *TorrentManager) getMoreInfo(rdTorrent realdebrid.Torrent) *Torrent {
continue
}
selectedFiles = append(selectedFiles, &File{
File: file,
Ended: info.Ended,
Link: "", // no link yet, consider it broken
IsBroken: true,
File: file,
Ended: info.Ended,
Link: "", // no link yet, consider it broken
State: NewFileState("broken"),
})
}
if len(selectedFiles) == len(info.Links) {
// all links are still intact! good!
for i, file := range selectedFiles {
file.Link = info.Links[i]
file.IsBroken = false
file.State.SetState("ok")
}
torrent.UnassignedLinks = mapset.NewSet[string]()
} else {
@@ -254,6 +255,8 @@ func (t *TorrentManager) mergeToMain(existing, toMerge *Torrent) *Torrent {
Components: mergedComponents,
UnassignedLinks: newer.UnassignedLinks.Union(older.UnassignedLinks),
UnrepairableReason: newer.UnrepairableReason,
State: older.State,
}
// unrepairable reason
@@ -268,32 +271,17 @@ func (t *TorrentManager) mergeToMain(existing, toMerge *Torrent) *Torrent {
// 3. empty - the file is not available
mainTorrent.SelectedFiles = cmap.New[*File]()
newer.SelectedFiles.IterCb(func(key string, newerFile *File) {
if !newerFile.IsBroken {
mainTorrent.SelectedFiles.Set(key, newerFile)
return
}
olderFile, ok := older.SelectedFiles.Get(key)
if ok && !olderFile.IsBroken {
mainTorrent.SelectedFiles.Set(key, olderFile)
return
}
mainTorrent.SelectedFiles.Set(key, newerFile)
})
inconsistentDeletes := false
older.SelectedFiles.IterCb(func(key string, olderFile *File) {
if !mainTorrent.SelectedFiles.Has(key) {
mainTorrent.SelectedFiles.Set(key, olderFile)
return
}
newerFile, _ := mainTorrent.SelectedFiles.Get(key)
if olderFile.IsDeleted && !newerFile.IsDeleted {
newerFile.IsDeleted = true
inconsistentDeletes = true
} else if olderFile.State.Is("deleted") {
newerFile, _ := mainTorrent.SelectedFiles.Get(key)
newerFile.State.SetState("deleted")
}
})
if inconsistentDeletes {
t.CheckDeletedStatus(&mainTorrent)
}
t.CheckDeletedStatus(&mainTorrent)
return &mainTorrent
}

View File

@@ -1,6 +1,7 @@
package torrent
import (
"context"
"fmt"
"math"
"strings"
@@ -100,7 +101,7 @@ func (t *TorrentManager) repairAll(torrent *Torrent) {
// check 1: for broken files
brokenFileIDs := mapset.NewSet[int]()
torrent.SelectedFiles.IterCb(func(_ string, file *File) {
if file.IsBroken && !file.IsDeleted {
if file.State.Is("broken") {
brokenFileIDs.Add(file.ID)
}
})
@@ -266,7 +267,7 @@ func (t *TorrentManager) assignUnassignedLinks(torrent *Torrent) bool {
// base it on size because why not?
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
file.State.SetState("ok")
assigned = true
assignedCount++
}
@@ -308,9 +309,9 @@ func (t *TorrentManager) assignUnassignedLinks(torrent *Torrent) bool {
Bytes: unassigned.Filesize,
Selected: 0,
},
Ended: torrent.Added,
Link: unassigned.Link,
IsBroken: false,
Ended: torrent.Added,
Link: unassigned.Link,
State: NewFileState("ok"),
}
torrent.SelectedFiles.Set(unassigned.Filename, newFile)
})
@@ -473,6 +474,11 @@ func (t *TorrentManager) canCapacityHandle() bool {
func (t *TorrentManager) markAsUnplayable(torrent *Torrent, reason string) {
t.log.Warnf("Marking torrent %s as unplayable - %s", t.GetKey(torrent), reason)
err := torrent.State.Event(context.Background(), "mark_as_unplayable")
if err != nil {
t.log.Errorf("Failed to mark torrent %s as unplayable: %v", t.GetKey(torrent), err)
return
}
t.DirectoryMap.IterCb(func(directory string, torrents cmap.ConcurrentMap[string, *Torrent]) {
torrents.Remove(t.GetKey(torrent))
})
@@ -491,7 +497,7 @@ func getBrokenFiles(torrent *Torrent) ([]*File, bool) {
var brokenFiles []*File
allBroken := true
torrent.SelectedFiles.IterCb(func(_ string, file *File) {
if file.IsBroken && !file.IsDeleted {
if file.State.Is("broken") {
brokenFiles = append(brokenFiles, file)
} else {
allBroken = false
@@ -509,17 +515,17 @@ func (t *TorrentManager) isStillBroken(info *realdebrid.TorrentInfo, brokenFiles
continue
}
selectedFiles = append(selectedFiles, &File{
File: file,
Ended: info.Ended,
Link: "", // no link yet
IsBroken: true,
File: file,
Ended: info.Ended,
Link: "", // no link yet
State: NewFileState("broken"),
})
}
if len(selectedFiles) == len(info.Links) {
// all links are still intact! good!
for i, file := range selectedFiles {
file.Link = info.Links[i]
file.IsBroken = false
file.State.SetState("ok")
}
} else {
// if we can't assign links, it's still broken

View File

@@ -4,22 +4,32 @@ import (
"github.com/looplab/fsm"
)
func NewFileState() *fsm.FSM {
func NewTorrentState(initial string) *fsm.FSM {
// ok
// broken
// under_repair
// deleted
// unplayable
return fsm.NewFSM(
"ok",
initial,
fsm.Events{
{Name: "break", Src: []string{"ok"}, Dst: "broken"},
{Name: "break", Src: []string{"ok", "unplayable"}, 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"},
{Name: "delete", Src: []string{"ok", "broken", "under_repair", "unplayable"}, Dst: "deleted"},
{Name: "mark_as_unplayable", Src: []string{"ok", "under_repair"}, Dst: "unplayable"},
},
fsm.Callbacks{},
)
}
func NewTorrentState() *fsm.FSM {
func NewFileState(initial string) *fsm.FSM {
// ok
// broken
// under_repair
// deleted
return fsm.NewFSM(
"ok",
initial,
fsm.Events{
{Name: "break", Src: []string{"ok"}, Dst: "broken"},
{Name: "repair", Src: []string{"broken"}, Dst: "under_repair"},

View File

@@ -35,6 +35,7 @@ func (t *Torrent) MarshalJSON() ([]byte, error) {
temp := &struct {
SelectedFilesJson stdjson.RawMessage `json:"SelectedFiles"`
UnassignedLinksJson stdjson.RawMessage `json:"UnassignedLinks"`
StateJson stdjson.RawMessage `json:"State"`
*Alias
}{
Alias: (*Alias)(t),
@@ -53,6 +54,8 @@ func (t *Torrent) MarshalJSON() ([]byte, error) {
temp.UnassignedLinksJson = []byte(unassignedLinksStr)
}
temp.StateJson = []byte(`"` + t.State.Current() + `"`)
return json.Marshal(temp)
}
@@ -61,6 +64,7 @@ func (t *Torrent) UnmarshalJSON(data []byte) error {
temp := &struct {
SelectedFilesJson stdjson.RawMessage `json:"SelectedFiles"`
UnassignedLinksJson stdjson.RawMessage `json:"UnassignedLinks"`
StateJson string `json:"State"`
*Alias
}{
Alias: (*Alias)(t),
@@ -83,6 +87,8 @@ func (t *Torrent) UnmarshalJSON(data []byte) error {
t.UnassignedLinks = mapset.NewSet[string]()
}
t.State = NewFileState(temp.StateJson)
return nil
}

View File

@@ -27,7 +27,7 @@ func CheckFile(directory, torrentName, fileName string, w http.ResponseWriter, r
}
file, ok := torrent.SelectedFiles.Get(fileName)
if !ok || file.IsDeleted {
if !ok || !file.State.Is("ok") {
log.Warnf("Cannot find file %s from path %s", fileName, req.URL.Path)
http.Error(w, "File not found", http.StatusNotFound)
return

View File

@@ -1,6 +1,7 @@
package universal
import (
"context"
"io"
"net/http"
"path/filepath"
@@ -49,20 +50,25 @@ func (dl *Downloader) DownloadFile(
}
file, ok := torrent.SelectedFiles.Get(fileName)
if !ok || file.IsDeleted {
if !ok || !file.State.Is("ok") {
log.Warnf("Cannot find file %s from path %s", fileName, req.URL.Path)
http.Error(resp, "File not found", http.StatusNotFound)
return
}
if file.IsBroken {
if !file.State.Is("ok") {
http.Error(resp, "File is not available", http.StatusNotFound)
return
}
unrestrict := torMgr.UnrestrictFileUntilOk(file)
if unrestrict == nil {
file.IsBroken = true
err := file.State.Event(context.Background(), "break")
if err != nil {
log.Errorf("File %s is stale: %v", fileName, err)
http.Error(resp, "File is stale, please try again", http.StatusLocked)
return
}
if cfg.EnableRepair() {
log.Warnf("File %s cannot be unrestricted (link=%s) (repairing...)", fileName, file.Link)
torMgr.TriggerRepair(torrent)
@@ -148,7 +154,12 @@ func (dl *Downloader) streamFileToResponse(
downloadResp, err := dl.client.Do(dlReq)
if err != nil {
if file != nil && unrestrict.Streamable == 1 {
file.IsBroken = true
err := file.State.Event(context.Background(), "break")
if err != nil {
log.Errorf("File %s is stale: %v", file.Path, err)
http.Error(resp, "File is stale, please try again", http.StatusLocked)
return
}
if cfg.EnableRepair() && torrent != nil {
log.Warnf("Cannot download file %s: %v (repairing...)", unrestrict.Download, err)
torMgr.TriggerRepair(torrent)
@@ -166,7 +177,12 @@ func (dl *Downloader) streamFileToResponse(
// Check if the download was not successful
if downloadResp.StatusCode/100 != 2 {
if file != nil && unrestrict.Streamable == 1 {
file.IsBroken = true
err := file.State.Event(context.Background(), "break")
if err != nil {
log.Errorf("File %s is stale: %v", file.Path, err)
http.Error(resp, "File is stale, please try again", http.StatusLocked)
return
}
if cfg.EnableRepair() && torrent != nil {
log.Warnf("Received a %s status code for file %s (repairing...)", downloadResp.Status, file.Path)
torMgr.TriggerRepair(torrent)