Files
zurg/internal/torrent/manager.go
Ben Sarmiento beba993364 Add bins
2024-05-23 22:20:19 +02:00

322 lines
7.8 KiB
Go

package torrent
import (
"io"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/debridmediamanager/zurg/internal/config"
"github.com/debridmediamanager/zurg/internal/fs"
"github.com/debridmediamanager/zurg/pkg/logutil"
"github.com/debridmediamanager/zurg/pkg/realdebrid"
mapset "github.com/deckarep/golang-set/v2"
cmap "github.com/orcaman/concurrent-map/v2"
"github.com/panjf2000/ants/v2"
)
const (
INT_ALL = "int__all__"
)
type TorrentManager struct {
requiredVersion string
Config config.ConfigInterface
api *realdebrid.RealDebrid
workerPool *ants.Pool
log *logutil.Logger
DirectoryMap cmap.ConcurrentMap[string, cmap.ConcurrentMap[string, *Torrent]] // directory -> accessKey -> Torrent
DownloadMap cmap.ConcurrentMap[string, *realdebrid.Download]
RootNode *fs.FileNode
RefreshKillSwitch chan struct{}
RepairKillSwitch chan struct{}
RemountTrigger chan struct{}
latestState *LibraryState
repairTrigger chan *Torrent
repairQueue mapset.Set[*Torrent]
repairRunning bool
repairRunningMu sync.Mutex
trashBin mapset.Set[string]
repairBin mapset.Set[string] // same as trash bin, but only if the torrent has been downloaded
}
// NewTorrentManager creates a new torrent manager
// it will fetch all torrents and their info in the background
// 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.10.0",
Config: cfg,
api: api,
workerPool: workerPool,
log: log,
DirectoryMap: cmap.New[cmap.ConcurrentMap[string, *Torrent]](),
DownloadMap: cmap.New[*realdebrid.Download](),
RootNode: fs.NewFileNode("root", true),
RefreshKillSwitch: make(chan struct{}, 1),
RepairKillSwitch: make(chan struct{}, 1),
RemountTrigger: make(chan struct{}, 1),
latestState: &LibraryState{log: log},
}
t.trashBin = mapset.NewSet[string]()
t.initializeDirectories()
t.workerPool.Submit(func() {
t.refreshTorrents()
t.setNewLatestState(t.getCurrentState())
t.StartRefreshJob()
t.StartRepairJob()
t.TriggerRepair(nil)
})
t.workerPool.Submit(func() {
t.mountDownloads()
t.StartDownloadsJob()
})
return t
}
// proxy function
func (t *TorrentManager) UnrestrictLinkUntilOk(link string) *realdebrid.Download {
ret, err := t.api.UnrestrictLink(link, t.Config.ShouldServeFromRclone())
if err != nil {
t.log.Warnf("Cannot unrestrict link %s: %v", link, err)
return nil
}
if ret != nil && ret.Link != "" && ret.Filename != "" {
if t.Config.EnableDownloadMount() {
t.DownloadMap.Set(ret.Filename, ret)
}
}
return ret
}
func (t *TorrentManager) UnrestrictFileUntilOk(file *File) *realdebrid.Download {
if !file.State.Is("ok_file") {
return nil
}
return t.UnrestrictLinkUntilOk(file.Link)
}
func (t *TorrentManager) GetKey(torrent *Torrent) string {
if !t.Config.ShouldIgnoreRenames() && torrent.Rename != "" {
return torrent.Rename
}
if t.Config.EnableRetainRDTorrentName() {
return torrent.Name
}
// drop the extension from the name
if t.Config.EnableRetainFolderNameExtension() && strings.Contains(torrent.Name, torrent.OriginalName) {
return torrent.Name
} else {
ret := strings.TrimSuffix(torrent.OriginalName, ".mp4")
ret = strings.TrimSuffix(ret, ".mkv")
return ret
}
}
func (t *TorrentManager) GetPath(file *File) string {
if !t.Config.ShouldIgnoreRenames() && file.Rename != "" {
return file.Rename
}
filename := filepath.Base(file.Path)
return filename
}
/// torrent functions
func (t *TorrentManager) getTorrentFiles() mapset.Set[string] {
files, err := filepath.Glob("data/*.torrent_zurg")
if err != nil {
t.log.Warnf("Cannot get files in data directory: %v", err)
return nil
}
return mapset.NewSet[string](files...)
}
func (t *TorrentManager) writeTorrentToFile(torrent *Torrent) {
filePath := "data/" + torrent.Hash + ".torrent_zurg"
file, err := os.Create(filePath)
if err != nil {
t.log.Warnf("Cannot create file %s: %v", filePath, err)
return
}
defer file.Close()
torrent.Version = t.requiredVersion
jsonData, err := json.Marshal(torrent)
if err != nil {
t.log.Warnf("Cannot marshal torrent: %v", err)
return
}
if _, err := file.Write(jsonData); err != nil {
t.log.Warnf("Cannot write to file %s: %v", filePath, err)
return
}
t.log.Debugf("Saved torrent %s (hash=%s) to file", t.GetKey(torrent), torrent.Hash)
}
func (t *TorrentManager) readTorrentFromFile(hash string) *Torrent {
filePath := "data/" + hash + ".torrent_zurg"
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 torrent *Torrent
if err := json.Unmarshal(jsonData, &torrent); err != nil {
return nil
}
if torrent.Version != t.requiredVersion {
return nil
}
return torrent
}
func (t *TorrentManager) deleteTorrentFile(hash string) {
filePath := "data/" + hash + ".torrent_zurg"
_ = os.Remove(filePath)
}
/// end torrent functions
/// info functions
func (t *TorrentManager) getInfoFiles() mapset.Set[string] {
files, err := filepath.Glob("data/*.info_zurg")
if err != nil {
t.log.Warnf("Cannot get files in data directory: %v", err)
return nil
}
return mapset.NewSet[string](files...)
}
func (t *TorrentManager) writeInfoToFile(info *realdebrid.TorrentInfo) {
filePath := "data/" + info.ID + ".info_zurg"
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_zurg"
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_zurg"
_ = os.Remove(filePath)
}
/// end info functions
func (t *TorrentManager) mountDownloads() {
if !t.Config.EnableDownloadMount() {
return
}
t.DownloadMap.Clear()
_ = t.workerPool.Submit(func() {
page := 1
offset := 0
for {
downloads, totalDownloads, err := t.api.GetDownloads(page, offset)
if err != nil {
// if we get an error, we just stop
t.log.Warnf("Cannot get downloads on page %d: %v", page, err)
continue
}
for i := range downloads {
t.DownloadMap.Set(downloads[i].Filename, &downloads[i])
}
offset += len(downloads)
page++
if offset >= totalDownloads {
break
}
}
t.log.Infof("Compiled into %d downloads", t.DownloadMap.Count())
})
}
func (t *TorrentManager) StartDownloadsJob() {
_ = t.workerPool.Submit(func() {
remountTicker := time.NewTicker(time.Duration(t.Config.GetDownloadsEveryMins()) * time.Minute)
defer remountTicker.Stop()
for {
select {
case <-remountTicker.C:
t.mountDownloads()
case <-t.RemountTrigger:
t.mountDownloads()
}
}
})
}
func (t *TorrentManager) initializeDirectories() {
// create internal directories
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))
}
}