- Hotfixes;
- Speed improvements
This commit is contained in:
@@ -58,12 +58,6 @@ type RepairRequest struct {
|
||||
FileName string
|
||||
}
|
||||
|
||||
type PropfindResponse struct {
|
||||
Data []byte
|
||||
GzippedData []byte
|
||||
Ts time.Time
|
||||
}
|
||||
|
||||
type Cache struct {
|
||||
dir string
|
||||
client types.Client
|
||||
@@ -72,7 +66,6 @@ type Cache struct {
|
||||
torrents *torrentCache
|
||||
downloadLinks *xsync.Map[string, linkCache]
|
||||
invalidDownloadLinks sync.Map
|
||||
PropfindResp *xsync.Map[string, PropfindResponse]
|
||||
folderNaming WebDavFolderNaming
|
||||
|
||||
listingDebouncer *utils.Debouncer[bool]
|
||||
@@ -134,7 +127,6 @@ func New(dc config.Debrid, client types.Client) *Cache {
|
||||
dir: filepath.Join(cfg.Path, "cache", dc.Name), // path to save cache files
|
||||
|
||||
torrents: newTorrentCache(dirFilters),
|
||||
PropfindResp: xsync.NewMap[string, PropfindResponse](),
|
||||
client: client,
|
||||
logger: logger.New(fmt.Sprintf("%s-webdav", client.GetName())),
|
||||
workers: dc.Workers,
|
||||
@@ -338,7 +330,7 @@ func (c *Cache) Sync() error {
|
||||
// Write these torrents to the cache
|
||||
c.setTorrents(cachedTorrents, func() {
|
||||
c.listingDebouncer.Call(false)
|
||||
}) // This is set to false, cos it's likely rclone hs not started yet.
|
||||
}) // Initial calls
|
||||
c.logger.Info().Msgf("Loaded %d torrents from cache", len(cachedTorrents))
|
||||
|
||||
if len(newTorrents) > 0 {
|
||||
@@ -630,7 +622,9 @@ func (c *Cache) ProcessTorrent(t *types.Torrent) error {
|
||||
IsComplete: len(t.Files) > 0,
|
||||
AddedOn: addedOn,
|
||||
}
|
||||
c.setTorrent(ct, nil)
|
||||
c.setTorrent(ct, func(tor *CachedTorrent) {
|
||||
c.listingDebouncer.Call(false)
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -651,7 +645,7 @@ func (c *Cache) AddTorrent(t *types.Torrent) error {
|
||||
AddedOn: addedOn,
|
||||
}
|
||||
c.setTorrent(ct, func(tor *CachedTorrent) {
|
||||
c.listingDebouncer.Call(true)
|
||||
c.RefreshListings(true)
|
||||
})
|
||||
go c.GenerateDownloadLinks(ct)
|
||||
return nil
|
||||
@@ -722,7 +716,9 @@ func (c *Cache) deleteTorrent(id string, removeFromDebrid bool) bool {
|
||||
t.Files = newFiles
|
||||
newId = cmp.Or(newId, t.Id)
|
||||
t.Id = newId
|
||||
c.setTorrent(t, nil)
|
||||
c.setTorrent(t, func(tor *CachedTorrent) {
|
||||
c.RefreshListings(false)
|
||||
})
|
||||
}
|
||||
}
|
||||
return true
|
||||
|
||||
@@ -46,7 +46,6 @@ func (c *Cache) GetDownloadLink(torrentName, filename, fileLink string) (string,
|
||||
|
||||
if req, inFlight := c.downloadLinkRequests.Load(fileLink); inFlight {
|
||||
// Wait for the other request to complete and use its result
|
||||
fmt.Println("Waiting for existing request to complete")
|
||||
result := req.(*downloadLinkRequest)
|
||||
return result.Wait()
|
||||
}
|
||||
|
||||
@@ -31,10 +31,6 @@ func (c *Cache) RefreshListings(refreshRclone bool) {
|
||||
// Copy the torrents to a string|time map
|
||||
c.torrents.refreshListing() // refresh torrent listings
|
||||
|
||||
if err := c.refreshParentXml(); err != nil {
|
||||
c.logger.Debug().Err(err).Msg("Failed to refresh XML")
|
||||
}
|
||||
|
||||
if refreshRclone {
|
||||
if err := c.refreshRclone(); err != nil {
|
||||
c.logger.Trace().Err(err).Msg("Failed to refresh rclone") // silent error
|
||||
@@ -115,7 +111,7 @@ func (c *Cache) refreshTorrents() {
|
||||
close(workChan)
|
||||
wg.Wait()
|
||||
|
||||
c.listingDebouncer.Call(true)
|
||||
c.listingDebouncer.Call(false)
|
||||
|
||||
c.logger.Debug().Msgf("Processed %d new torrents", counter)
|
||||
}
|
||||
@@ -140,13 +136,14 @@ func (c *Cache) refreshRclone() error {
|
||||
MaxIdleConnsPerHost: 5,
|
||||
},
|
||||
}
|
||||
// Create form data
|
||||
data := ""
|
||||
for index, dir := range c.GetDirectories() {
|
||||
if dir != "" {
|
||||
if index == 0 {
|
||||
data += "dir=" + dir
|
||||
} else {
|
||||
data += "&dir" + fmt.Sprint(index) + "=" + dir
|
||||
data += "&dir" + fmt.Sprint(index+1) + "=" + dir
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -203,7 +200,7 @@ func (c *Cache) refreshTorrent(torrentId string) *CachedTorrent {
|
||||
IsComplete: len(torrent.Files) > 0,
|
||||
}
|
||||
c.setTorrent(ct, func(torrent *CachedTorrent) {
|
||||
c.listingDebouncer.Call(true)
|
||||
go c.listingDebouncer.Call(true)
|
||||
})
|
||||
|
||||
return ct
|
||||
|
||||
@@ -17,13 +17,13 @@ func (c *Cache) StartSchedule() error {
|
||||
c.logger.Trace().Msgf("Next download link refresh job: %s", t.Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
|
||||
torrentJob, err := utils.ScheduleJob(ctx, c.torrentRefreshInterval, nil, c.refreshTorrents)
|
||||
if err != nil {
|
||||
c.logger.Error().Err(err).Msg("Failed to add torrent refresh job")
|
||||
}
|
||||
if t, err := torrentJob.NextRun(); err == nil {
|
||||
c.logger.Trace().Msgf("Next torrent refresh job: %s", t.Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
//torrentJob, err := utils.ScheduleJob(ctx, c.torrentRefreshInterval, nil, c.refreshTorrents)
|
||||
//if err != nil {
|
||||
// c.logger.Error().Err(err).Msg("Failed to add torrent refresh job")
|
||||
//}
|
||||
//if t, err := torrentJob.NextRun(); err == nil {
|
||||
// c.logger.Trace().Msgf("Next torrent refresh job: %s", t.Format("2006-01-02 15:04:05"))
|
||||
//}
|
||||
|
||||
// Schedule the reset invalid links job
|
||||
// This job will run every 24 hours
|
||||
|
||||
@@ -1,230 +1 @@
|
||||
package debrid
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"github.com/beevik/etree"
|
||||
"github.com/sirrobot01/decypharr/internal/request"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
DavNS = "DAV:"
|
||||
)
|
||||
|
||||
// Multistatus XML types for WebDAV response
|
||||
type Multistatus struct {
|
||||
XMLName xml.Name `xml:"D:multistatus"`
|
||||
Namespace string `xml:"xmlns:D,attr"`
|
||||
Responses []Response `xml:"D:response"`
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
Href string `xml:"D:href"`
|
||||
Propstat Propstat `xml:"D:propstat"`
|
||||
}
|
||||
|
||||
type Propstat struct {
|
||||
Prop Prop `xml:"D:prop"`
|
||||
Status string `xml:"D:status"`
|
||||
}
|
||||
|
||||
type Prop struct {
|
||||
ResourceType ResourceType `xml:"D:resourcetype"`
|
||||
DisplayName string `xml:"D:displayname"`
|
||||
LastModified string `xml:"D:getlastmodified"`
|
||||
ContentType string `xml:"D:getcontenttype"`
|
||||
ContentLength string `xml:"D:getcontentlength"`
|
||||
SupportedLock SupportedLock `xml:"D:supportedlock"`
|
||||
}
|
||||
|
||||
type ResourceType struct {
|
||||
Collection *struct{} `xml:"D:collection,omitempty"`
|
||||
}
|
||||
|
||||
type SupportedLock struct {
|
||||
LockEntry LockEntry `xml:"D:lockentry"`
|
||||
}
|
||||
|
||||
type LockEntry struct {
|
||||
LockScope LockScope `xml:"D:lockscope"`
|
||||
LockType LockType `xml:"D:locktype"`
|
||||
}
|
||||
|
||||
type LockScope struct {
|
||||
Exclusive *struct{} `xml:"D:exclusive"`
|
||||
}
|
||||
|
||||
type LockType struct {
|
||||
Write *struct{} `xml:"D:write"`
|
||||
}
|
||||
|
||||
func (c *Cache) refreshParentXml() error {
|
||||
// Refresh the defaults first
|
||||
parents := []string{"__all__", "torrents"}
|
||||
torrents := c.GetListing("__all__")
|
||||
clientName := c.client.GetName()
|
||||
customFolders := c.GetCustomFolders()
|
||||
wg := sync.WaitGroup{}
|
||||
totalFolders := len(parents) + len(customFolders)
|
||||
wg.Add(totalFolders)
|
||||
errCh := make(chan error, totalFolders)
|
||||
for _, parent := range parents {
|
||||
parent := parent
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := c.refreshFolderXml(torrents, clientName, parent); err != nil {
|
||||
errCh <- fmt.Errorf("failed to refresh folder %s: %v", parent, err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
// refresh custom folders
|
||||
for _, folder := range customFolders {
|
||||
go func() {
|
||||
folder := folder
|
||||
defer wg.Done()
|
||||
listing := c.GetListing(folder)
|
||||
if err := c.refreshFolderXml(listing, clientName, folder); err != nil {
|
||||
errCh <- fmt.Errorf("failed to refresh folder %s: %v", folder, err)
|
||||
}
|
||||
}()
|
||||
|
||||
}
|
||||
wg.Wait()
|
||||
close(errCh)
|
||||
// if any errors, return the first
|
||||
if err := <-errCh; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cache) refreshFolderXml(torrents []os.FileInfo, clientName, parent string) error {
|
||||
// Get the current timestamp in RFC1123 format
|
||||
currentTime := time.Now().UTC().Format(http.TimeFormat)
|
||||
|
||||
// Create the multistatus response structure
|
||||
ms := Multistatus{
|
||||
Namespace: DavNS,
|
||||
Responses: make([]Response, 0, len(torrents)+1), // Pre-allocate for parent + torrents
|
||||
}
|
||||
|
||||
// Add the parent directory
|
||||
baseUrl := path.Join("webdav", clientName, parent)
|
||||
|
||||
// Add parent response
|
||||
ms.Responses = append(ms.Responses, createDirectoryResponse(baseUrl, parent, currentTime))
|
||||
|
||||
// Add torrents to the response
|
||||
for _, torrent := range torrents {
|
||||
name := torrent.Name()
|
||||
torrentPath := path.Join("/webdav", clientName, parent, name) + "/"
|
||||
ms.Responses = append(ms.Responses, createDirectoryResponse(torrentPath, name, currentTime))
|
||||
}
|
||||
|
||||
// Create a buffer and encode the XML
|
||||
xmlData, err := xml.MarshalIndent(ms, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate XML: %v", err)
|
||||
}
|
||||
|
||||
// Add XML declaration
|
||||
xmlHeader := []byte(xml.Header)
|
||||
xmlOutput := append(xmlHeader, xmlData...)
|
||||
|
||||
// Cache the result
|
||||
cacheKey := fmt.Sprintf("%s:1", baseUrl)
|
||||
|
||||
// Assume Gzip function exists elsewhere
|
||||
gzippedData := request.Gzip(xmlOutput) // Replace with your actual gzip function
|
||||
|
||||
c.PropfindResp.Store(cacheKey, PropfindResponse{
|
||||
Data: xmlOutput,
|
||||
GzippedData: gzippedData,
|
||||
Ts: time.Now(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createDirectoryResponse(href, displayName, modTime string) Response {
|
||||
return Response{
|
||||
Href: href,
|
||||
Propstat: Propstat{
|
||||
Prop: Prop{
|
||||
ResourceType: ResourceType{
|
||||
Collection: &struct{}{},
|
||||
},
|
||||
DisplayName: displayName,
|
||||
LastModified: modTime,
|
||||
ContentType: "httpd/unix-directory",
|
||||
ContentLength: "0",
|
||||
SupportedLock: SupportedLock{
|
||||
LockEntry: LockEntry{
|
||||
LockScope: LockScope{
|
||||
Exclusive: &struct{}{},
|
||||
},
|
||||
LockType: LockType{
|
||||
Write: &struct{}{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: "HTTP/1.1 200 OK",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func addDirectoryResponse(multistatus *etree.Element, href, displayName, modTime string) *etree.Element {
|
||||
responseElem := multistatus.CreateElement("D:response")
|
||||
|
||||
// Add href - ensure it's properly formatted
|
||||
hrefElem := responseElem.CreateElement("D:href")
|
||||
hrefElem.SetText(href)
|
||||
|
||||
// Add propstat
|
||||
propstatElem := responseElem.CreateElement("D:propstat")
|
||||
|
||||
// Add prop
|
||||
propElem := propstatElem.CreateElement("D:prop")
|
||||
|
||||
// Add resource type (collection = directory)
|
||||
resourceTypeElem := propElem.CreateElement("D:resourcetype")
|
||||
resourceTypeElem.CreateElement("D:collection")
|
||||
|
||||
// Add display name
|
||||
displayNameElem := propElem.CreateElement("D:displayname")
|
||||
displayNameElem.SetText(displayName)
|
||||
|
||||
// Add last modified time
|
||||
lastModElem := propElem.CreateElement("D:getlastmodified")
|
||||
lastModElem.SetText(modTime)
|
||||
|
||||
// Add content type for directories
|
||||
contentTypeElem := propElem.CreateElement("D:getcontenttype")
|
||||
contentTypeElem.SetText("httpd/unix-directory")
|
||||
|
||||
// Add length (size) - directories typically have zero size
|
||||
contentLengthElem := propElem.CreateElement("D:getcontentlength")
|
||||
contentLengthElem.SetText("0")
|
||||
|
||||
// Add supported lock
|
||||
lockElem := propElem.CreateElement("D:supportedlock")
|
||||
lockEntryElem := lockElem.CreateElement("D:lockentry")
|
||||
|
||||
lockScopeElem := lockEntryElem.CreateElement("D:lockscope")
|
||||
lockScopeElem.CreateElement("D:exclusive")
|
||||
|
||||
lockTypeElem := lockEntryElem.CreateElement("D:locktype")
|
||||
lockTypeElem.CreateElement("D:write")
|
||||
|
||||
// Add status
|
||||
statusElem := propstatElem.CreateElement("D:status")
|
||||
statusElem.SetText("HTTP/1.1 200 OK")
|
||||
|
||||
return responseElem
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user