257 lines
5.7 KiB
Go
257 lines
5.7 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"github.com/sirrobot01/decypharr/pkg/debrid/types"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type fileInfo struct {
|
|
id string
|
|
name string
|
|
size int64
|
|
mode os.FileMode
|
|
modTime time.Time
|
|
isDir bool
|
|
}
|
|
|
|
func (fi *fileInfo) Name() string { return fi.name }
|
|
func (fi *fileInfo) Size() int64 { return fi.size }
|
|
func (fi *fileInfo) Mode() os.FileMode { return fi.mode }
|
|
func (fi *fileInfo) ModTime() time.Time { return fi.modTime }
|
|
func (fi *fileInfo) IsDir() bool { return fi.isDir }
|
|
func (fi *fileInfo) ID() string { return fi.id }
|
|
func (fi *fileInfo) Sys() interface{} { return nil }
|
|
|
|
func (c *Cache) RefreshListings(refreshRclone bool) {
|
|
// Copy the torrents to a string|time map
|
|
c.torrents.refreshListing() // refresh torrent listings
|
|
|
|
if refreshRclone {
|
|
if err := c.refreshRclone(); err != nil {
|
|
c.logger.Error().Err(err).Msg("Failed to refresh rclone") // silent error
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Cache) refreshTorrents(ctx context.Context) {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
default:
|
|
}
|
|
|
|
if !c.torrentsRefreshMu.TryLock() {
|
|
return
|
|
}
|
|
defer c.torrentsRefreshMu.Unlock()
|
|
|
|
// Get all torrents from the debrid service
|
|
debTorrents, err := c.client.GetTorrents()
|
|
if err != nil {
|
|
c.logger.Error().Err(err).Msg("Failed to get torrents")
|
|
return
|
|
}
|
|
|
|
if len(debTorrents) == 0 {
|
|
// Maybe an error occurred
|
|
return
|
|
}
|
|
|
|
currentTorrentIds := make(map[string]struct{}, len(debTorrents))
|
|
for _, t := range debTorrents {
|
|
currentTorrentIds[t.Id] = struct{}{}
|
|
}
|
|
|
|
// Let's implement deleting torrents removed from debrid
|
|
deletedTorrents := make([]string, 0)
|
|
cachedTorrents := c.torrents.getIdMaps()
|
|
for id := range cachedTorrents {
|
|
if _, exists := currentTorrentIds[id]; !exists {
|
|
deletedTorrents = append(deletedTorrents, id)
|
|
}
|
|
}
|
|
|
|
if len(deletedTorrents) > 0 {
|
|
go c.validateAndDeleteTorrents(deletedTorrents)
|
|
}
|
|
|
|
newTorrents := make([]*types.Torrent, 0)
|
|
for _, t := range debTorrents {
|
|
if _, exists := cachedTorrents[t.Id]; !exists {
|
|
newTorrents = append(newTorrents, t)
|
|
}
|
|
}
|
|
|
|
if len(newTorrents) == 0 {
|
|
return
|
|
}
|
|
|
|
c.logger.Trace().Msgf("Found %d new torrents", len(newTorrents))
|
|
|
|
workChan := make(chan *types.Torrent, min(100, len(newTorrents)))
|
|
errChan := make(chan error, len(newTorrents))
|
|
var wg sync.WaitGroup
|
|
counter := 0
|
|
|
|
for i := 0; i < c.workers; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for t := range workChan {
|
|
if err := c.ProcessTorrent(t); err != nil {
|
|
c.logger.Error().Err(err).Msgf("Failed to process new torrent %s", t.Id)
|
|
errChan <- err
|
|
}
|
|
counter++
|
|
}
|
|
}()
|
|
}
|
|
|
|
for _, t := range newTorrents {
|
|
workChan <- t
|
|
}
|
|
close(workChan)
|
|
wg.Wait()
|
|
|
|
c.listingDebouncer.Call(false)
|
|
|
|
c.logger.Debug().Msgf("Processed %d new torrents", counter)
|
|
}
|
|
|
|
func (c *Cache) refreshRclone() error {
|
|
cfg := c.config
|
|
dirs := strings.FieldsFunc(cfg.RcRefreshDirs, func(r rune) bool {
|
|
return r == ',' || r == '&'
|
|
})
|
|
if len(dirs) == 0 {
|
|
dirs = []string{"__all__"}
|
|
}
|
|
if c.mounter != nil {
|
|
return c.mounter.RefreshDir(dirs)
|
|
} else {
|
|
return c.refreshRcloneWithRC(dirs)
|
|
}
|
|
}
|
|
|
|
func (c *Cache) refreshRcloneWithRC(dirs []string) error {
|
|
cfg := c.config
|
|
|
|
if cfg.RcUrl == "" {
|
|
return nil
|
|
}
|
|
|
|
client := http.DefaultClient
|
|
// Create form data
|
|
data := c.buildRcloneRequestData(dirs)
|
|
|
|
if err := c.sendRcloneRequest(client, "vfs/forget", data); err != nil {
|
|
c.logger.Error().Err(err).Msg("Failed to send rclone vfs/forget request")
|
|
}
|
|
|
|
if err := c.sendRcloneRequest(client, "vfs/refresh", data); err != nil {
|
|
c.logger.Error().Err(err).Msg("Failed to send rclone vfs/refresh request")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Cache) buildRcloneRequestData(dirs []string) string {
|
|
var data strings.Builder
|
|
for index, dir := range dirs {
|
|
if dir != "" {
|
|
if index == 0 {
|
|
data.WriteString("dir=" + dir)
|
|
} else {
|
|
data.WriteString("&dir" + fmt.Sprint(index+1) + "=" + dir)
|
|
}
|
|
}
|
|
}
|
|
return data.String()
|
|
}
|
|
|
|
func (c *Cache) sendRcloneRequest(client *http.Client, endpoint, data string) error {
|
|
req, err := http.NewRequest("POST", fmt.Sprintf("%s/%s", c.config.RcUrl, endpoint), strings.NewReader(data))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
|
|
if c.config.RcUser != "" && c.config.RcPass != "" {
|
|
req.SetBasicAuth(c.config.RcUser, c.config.RcPass)
|
|
}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != 200 {
|
|
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
|
|
return fmt.Errorf("failed to perform %s: %s - %s", endpoint, resp.Status, string(body))
|
|
}
|
|
|
|
_, _ = io.Copy(io.Discard, resp.Body)
|
|
return nil
|
|
}
|
|
|
|
func (c *Cache) refreshTorrent(torrentId string) *CachedTorrent {
|
|
|
|
if torrentId == "" {
|
|
c.logger.Error().Msg("Torrent ID is empty")
|
|
return nil
|
|
}
|
|
|
|
torrent, err := c.client.GetTorrent(torrentId)
|
|
if err != nil {
|
|
c.logger.Error().Err(err).Msgf("Failed to get torrent %s", torrentId)
|
|
return nil
|
|
}
|
|
addedOn, err := time.Parse(time.RFC3339, torrent.Added)
|
|
if err != nil {
|
|
addedOn = time.Now()
|
|
}
|
|
ct := CachedTorrent{
|
|
Torrent: torrent,
|
|
AddedOn: addedOn,
|
|
IsComplete: len(torrent.Files) > 0,
|
|
}
|
|
c.setTorrent(ct, func(torrent CachedTorrent) {
|
|
go c.listingDebouncer.Call(true)
|
|
})
|
|
|
|
return &ct
|
|
}
|
|
|
|
func (c *Cache) refreshDownloadLinks(ctx context.Context) {
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
default:
|
|
}
|
|
|
|
if !c.downloadLinksRefreshMu.TryLock() {
|
|
return
|
|
}
|
|
defer c.downloadLinksRefreshMu.Unlock()
|
|
|
|
links, err := c.client.GetDownloadLinks()
|
|
|
|
if err != nil {
|
|
c.logger.Error().Err(err).Msg("Failed to get download links")
|
|
return
|
|
}
|
|
|
|
c.client.Accounts().SetDownloadLinks(links)
|
|
|
|
c.logger.Debug().Msgf("Refreshed download %d links", c.client.Accounts().GetLinksCount())
|
|
}
|