Update repair; fix minor bugs with namings
This commit is contained in:
1
go.mod
1
go.mod
@@ -24,6 +24,7 @@ require (
|
|||||||
require (
|
require (
|
||||||
github.com/anacrolix/missinggo v1.3.0 // indirect
|
github.com/anacrolix/missinggo v1.3.0 // indirect
|
||||||
github.com/anacrolix/missinggo/v2 v2.7.3 // indirect
|
github.com/anacrolix/missinggo/v2 v2.7.3 // indirect
|
||||||
|
github.com/beevik/etree v1.5.0 // indirect
|
||||||
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
|
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -36,6 +36,8 @@ github.com/anacrolix/tagflag v1.1.0/go.mod h1:Scxs9CV10NQatSmbyjqmqmeQNwGzlNe0CM
|
|||||||
github.com/anacrolix/torrent v1.55.0 h1:s9yh/YGdPmbN9dTa+0Inh2dLdrLQRvEAj1jdFW/Hdd8=
|
github.com/anacrolix/torrent v1.55.0 h1:s9yh/YGdPmbN9dTa+0Inh2dLdrLQRvEAj1jdFW/Hdd8=
|
||||||
github.com/anacrolix/torrent v1.55.0/go.mod h1:sBdZHBSZNj4de0m+EbYg7vvs/G/STubxu/GzzNbojsE=
|
github.com/anacrolix/torrent v1.55.0/go.mod h1:sBdZHBSZNj4de0m+EbYg7vvs/G/STubxu/GzzNbojsE=
|
||||||
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||||
|
github.com/beevik/etree v1.5.0 h1:iaQZFSDS+3kYZiGoc9uKeOkUY3nYMXOKLl6KIJxiJWs=
|
||||||
|
github.com/beevik/etree v1.5.0/go.mod h1:gPNJNaBGVZ9AwsidazFZyygnd+0pAU38N4D+WemwKNs=
|
||||||
github.com/benbjohnson/immutable v0.2.0/go.mod h1:uc6OHo6PN2++n98KHLxW8ef4W42ylHiQSENghE1ezxI=
|
github.com/benbjohnson/immutable v0.2.0/go.mod h1:uc6OHo6PN2++n98KHLxW8ef4W42ylHiQSENghE1ezxI=
|
||||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ func NewLogger(prefix string) zerolog.Logger {
|
|||||||
Level(zerolog.InfoLevel)
|
Level(zerolog.InfoLevel)
|
||||||
|
|
||||||
// Set the log level
|
// Set the log level
|
||||||
|
level = strings.ToLower(level)
|
||||||
switch level {
|
switch level {
|
||||||
case "debug":
|
case "debug":
|
||||||
logger = logger.Level(zerolog.DebugLevel)
|
logger = logger.Level(zerolog.DebugLevel)
|
||||||
@@ -82,6 +83,8 @@ func NewLogger(prefix string) zerolog.Logger {
|
|||||||
logger = logger.Level(zerolog.WarnLevel)
|
logger = logger.Level(zerolog.WarnLevel)
|
||||||
case "error":
|
case "error":
|
||||||
logger = logger.Level(zerolog.ErrorLevel)
|
logger = logger.Level(zerolog.ErrorLevel)
|
||||||
|
case "trace":
|
||||||
|
logger = logger.Level(zerolog.TraceLevel)
|
||||||
}
|
}
|
||||||
return logger
|
return logger
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package request
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -288,3 +289,21 @@ func JSONResponse(w http.ResponseWriter, data interface{}, code int) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Gzip(body []byte) []byte {
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if len(body) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
gz := gzip.NewWriter(&b)
|
||||||
|
_, err := gz.Write(body)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
err = gz.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return b.Bytes()
|
||||||
|
}
|
||||||
|
|||||||
@@ -204,12 +204,12 @@ func (ad *AllDebrid) CheckStatus(torrent *types.Torrent, isSymlink bool) (*types
|
|||||||
return torrent, nil
|
return torrent, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ad *AllDebrid) DeleteTorrent(torrent *types.Torrent) {
|
func (ad *AllDebrid) DeleteTorrent(torrentId string) {
|
||||||
url := fmt.Sprintf("%s/magnet/delete?id=%s", ad.Host, torrent.Id)
|
url := fmt.Sprintf("%s/magnet/delete?id=%s", ad.Host, torrentId)
|
||||||
req, _ := http.NewRequest(http.MethodGet, url, nil)
|
req, _ := http.NewRequest(http.MethodGet, url, nil)
|
||||||
_, err := ad.client.MakeRequest(req)
|
_, err := ad.client.MakeRequest(req)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
ad.logger.Info().Msgf("Torrent: %s deleted", torrent.Name)
|
ad.logger.Info().Msgf("Torrent: %s deleted", torrentId)
|
||||||
} else {
|
} else {
|
||||||
ad.logger.Info().Msgf("Error deleting torrent: %s", err)
|
ad.logger.Info().Msgf("Error deleting torrent: %s", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ func (c *Cache) load() (map[string]*CachedTorrent, error) {
|
|||||||
if len(ct.Files) != 0 {
|
if len(ct.Files) != 0 {
|
||||||
// We can assume the torrent is complete
|
// We can assume the torrent is complete
|
||||||
ct.IsComplete = true
|
ct.IsComplete = true
|
||||||
ct.Torrent.Name = utils.RemoveExtension(ct.Torrent.Filename) // Update the name
|
ct.Torrent.Name = utils.RemoveExtension(ct.Torrent.OriginalFilename) // Update the name
|
||||||
torrents[ct.Id] = &ct
|
torrents[ct.Id] = &ct
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -403,9 +403,6 @@ func (c *Cache) ProcessTorrent(t *types.Torrent, refreshRclone bool) error {
|
|||||||
IsComplete: len(t.Files) > 0,
|
IsComplete: len(t.Files) > 0,
|
||||||
}
|
}
|
||||||
c.setTorrent(ct)
|
c.setTorrent(ct)
|
||||||
if err := c.RefreshRclone(); err != nil {
|
|
||||||
c.logger.Debug().Err(err).Msg("Failed to refresh rclone")
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import (
|
|||||||
"github.com/sirrobot01/debrid-blackhole/pkg/debrid/types"
|
"github.com/sirrobot01/debrid-blackhole/pkg/debrid/types"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
"sort"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -42,7 +40,7 @@ func (c *Cache) refreshListings() {
|
|||||||
}
|
}
|
||||||
// Atomic store of the complete ready-to-use slice
|
// Atomic store of the complete ready-to-use slice
|
||||||
c.listings.Store(files)
|
c.listings.Store(files)
|
||||||
c.resetPropfindResponse()
|
_ = c.RefreshXml()
|
||||||
if err := c.RefreshRclone(); err != nil {
|
if err := c.RefreshRclone(); err != nil {
|
||||||
c.logger.Debug().Err(err).Msg("Failed to refresh rclone")
|
c.logger.Debug().Err(err).Msg("Failed to refresh rclone")
|
||||||
}
|
}
|
||||||
@@ -179,27 +177,3 @@ func (c *Cache) refreshDownloadLinks() {
|
|||||||
c.downloadLinks[k] = v.DownloadLink
|
c.downloadLinks[k] = v.DownloadLink
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cache) resetPropfindResponse() {
|
|
||||||
// Right now, parents are hardcoded
|
|
||||||
parents := []string{"__all__", "torrents"}
|
|
||||||
// Reset only the parent directories
|
|
||||||
// Convert the parents to a keys
|
|
||||||
// This is a bit hacky, but it works
|
|
||||||
// Instead of deleting all the keys, we only delete the parent keys, e.g __all__/ or torrents/
|
|
||||||
keys := make([]string, 0, len(parents))
|
|
||||||
for _, p := range parents {
|
|
||||||
// Construct the key
|
|
||||||
// construct url
|
|
||||||
url := filepath.Join("/webdav", c.client.GetName(), p)
|
|
||||||
url = path.Clean(url)
|
|
||||||
key0 := fmt.Sprintf("propfind:%s:0", url)
|
|
||||||
key1 := fmt.Sprintf("propfind:%s:1", url)
|
|
||||||
keys = append(keys, key0, key1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete the keys
|
|
||||||
for _, k := range keys {
|
|
||||||
c.PropfindResp.Delete(k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
118
pkg/debrid/debrid/xml.go
Normal file
118
pkg/debrid/debrid/xml.go
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
package debrid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/beevik/etree"
|
||||||
|
"github.com/sirrobot01/debrid-blackhole/internal/request"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
path "path/filepath"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Cache) RefreshXml() error {
|
||||||
|
parents := []string{"__all__", "torrents"}
|
||||||
|
for _, parent := range parents {
|
||||||
|
if err := c.refreshParentXml(parent); err != nil {
|
||||||
|
return fmt.Errorf("failed to refresh XML for %s: %v", parent, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.logger.Debug().Msgf("Refreshed XML cache for %s", c.client.GetName())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) refreshParentXml(parent string) error {
|
||||||
|
// Define the WebDAV namespace
|
||||||
|
davNS := "DAV:"
|
||||||
|
|
||||||
|
// Create the root multistatus element
|
||||||
|
doc := etree.NewDocument()
|
||||||
|
doc.CreateProcInst("xml", `version="1.0" encoding="UTF-8"`)
|
||||||
|
|
||||||
|
multistatus := doc.CreateElement("D:multistatus")
|
||||||
|
multistatus.CreateAttr("xmlns:D", davNS)
|
||||||
|
|
||||||
|
// Get the current timestamp in RFC1123 format (WebDAV format)
|
||||||
|
currentTime := time.Now().UTC().Format(http.TimeFormat)
|
||||||
|
|
||||||
|
// Add the parent directory
|
||||||
|
parentPath := fmt.Sprintf("/webdav/%s/%s/", c.client.GetName(), parent)
|
||||||
|
addDirectoryResponse(multistatus, parentPath, parent, currentTime)
|
||||||
|
|
||||||
|
// Add torrents to the XML
|
||||||
|
torrents := c.GetListing()
|
||||||
|
for _, torrent := range torrents {
|
||||||
|
torrentName := torrent.Name()
|
||||||
|
torrentPath := fmt.Sprintf("/webdav/%s/%s/%s/",
|
||||||
|
c.client.GetName(),
|
||||||
|
url.PathEscape(torrentName),
|
||||||
|
parent,
|
||||||
|
)
|
||||||
|
|
||||||
|
addDirectoryResponse(multistatus, torrentPath, torrentName, currentTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to XML string
|
||||||
|
xmlData, err := doc.WriteToBytes()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to generate XML: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store in cache
|
||||||
|
// Construct the keys
|
||||||
|
baseUrl := path.Clean(fmt.Sprintf("/webdav/%s/%s", c.client.GetName()))
|
||||||
|
key0 := fmt.Sprintf("propfind:%s:0", baseUrl)
|
||||||
|
key1 := fmt.Sprintf("propfind:%s:1", baseUrl)
|
||||||
|
|
||||||
|
res := PropfindResponse{
|
||||||
|
Data: xmlData,
|
||||||
|
GzippedData: request.Gzip(xmlData),
|
||||||
|
Ts: time.Now(),
|
||||||
|
}
|
||||||
|
c.PropfindResp.Store(key0, res)
|
||||||
|
c.PropfindResp.Store(key1, res)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func addDirectoryResponse(multistatus *etree.Element, href, displayName, modTime string) *etree.Element {
|
||||||
|
responseElem := multistatus.CreateElement("D:response")
|
||||||
|
|
||||||
|
// Add href
|
||||||
|
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 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
|
||||||
|
}
|
||||||
@@ -223,12 +223,12 @@ func (dl *DebridLink) CheckStatus(torrent *types.Torrent, isSymlink bool) (*type
|
|||||||
return torrent, nil
|
return torrent, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dl *DebridLink) DeleteTorrent(torrent *types.Torrent) {
|
func (dl *DebridLink) DeleteTorrent(torrentId string) {
|
||||||
url := fmt.Sprintf("%s/seedbox/%s/remove", dl.Host, torrent.Id)
|
url := fmt.Sprintf("%s/seedbox/%s/remove", dl.Host, torrentId)
|
||||||
req, _ := http.NewRequest(http.MethodDelete, url, nil)
|
req, _ := http.NewRequest(http.MethodDelete, url, nil)
|
||||||
_, err := dl.client.MakeRequest(req)
|
_, err := dl.client.MakeRequest(req)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
dl.logger.Info().Msgf("Torrent: %s deleted", torrent.Name)
|
dl.logger.Info().Msgf("Torrent: %s deleted", torrentId)
|
||||||
} else {
|
} else {
|
||||||
dl.logger.Info().Msgf("Error deleting torrent: %s", err)
|
dl.logger.Info().Msgf("Error deleting torrent: %s", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ func (r *RealDebrid) UpdateTorrent(t *types.Torrent) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
name := utils.RemoveInvalidChars(data.OriginalFilename)
|
name := utils.RemoveExtension(data.OriginalFilename)
|
||||||
t.Name = name
|
t.Name = name
|
||||||
t.Bytes = data.Bytes
|
t.Bytes = data.Bytes
|
||||||
t.Folder = name
|
t.Folder = name
|
||||||
@@ -182,7 +182,7 @@ func (r *RealDebrid) UpdateTorrent(t *types.Torrent) error {
|
|||||||
t.Speed = data.Speed
|
t.Speed = data.Speed
|
||||||
t.Seeders = data.Seeders
|
t.Seeders = data.Seeders
|
||||||
t.Filename = data.Filename
|
t.Filename = data.Filename
|
||||||
t.OriginalFilename = data.OriginalFilename
|
t.OriginalFilename = name
|
||||||
t.Links = data.Links
|
t.Links = data.Links
|
||||||
t.MountPath = r.MountPath
|
t.MountPath = r.MountPath
|
||||||
t.Debrid = r.Name
|
t.Debrid = r.Name
|
||||||
@@ -208,7 +208,7 @@ func (r *RealDebrid) CheckStatus(t *types.Torrent, isSymlink bool) (*types.Torre
|
|||||||
t.Name = name // Important because some magnet changes the name
|
t.Name = name // Important because some magnet changes the name
|
||||||
t.Folder = name
|
t.Folder = name
|
||||||
t.Filename = data.Filename
|
t.Filename = data.Filename
|
||||||
t.OriginalFilename = data.OriginalFilename
|
t.OriginalFilename = name
|
||||||
t.Bytes = data.Bytes
|
t.Bytes = data.Bytes
|
||||||
t.Progress = data.Progress
|
t.Progress = data.Progress
|
||||||
t.Speed = data.Speed
|
t.Speed = data.Speed
|
||||||
@@ -257,12 +257,12 @@ func (r *RealDebrid) CheckStatus(t *types.Torrent, isSymlink bool) (*types.Torre
|
|||||||
return t, nil
|
return t, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RealDebrid) DeleteTorrent(torrent *types.Torrent) {
|
func (r *RealDebrid) DeleteTorrent(torrentId string) {
|
||||||
url := fmt.Sprintf("%s/torrents/delete/%s", r.Host, torrent.Id)
|
url := fmt.Sprintf("%s/torrents/delete/%s", r.Host, torrentId)
|
||||||
req, _ := http.NewRequest(http.MethodDelete, url, nil)
|
req, _ := http.NewRequest(http.MethodDelete, url, nil)
|
||||||
_, err := r.client.MakeRequest(req)
|
_, err := r.client.MakeRequest(req)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
r.logger.Info().Msgf("Torrent: %s deleted", torrent.Name)
|
r.logger.Info().Msgf("Torrent: %s deleted", torrentId)
|
||||||
} else {
|
} else {
|
||||||
r.logger.Info().Msgf("Error deleting torrent: %s", err)
|
r.logger.Info().Msgf("Error deleting torrent: %s", err)
|
||||||
}
|
}
|
||||||
@@ -382,7 +382,7 @@ func (r *RealDebrid) getTorrents(offset int, limit int) (int, []*types.Torrent,
|
|||||||
}
|
}
|
||||||
torrents = append(torrents, &types.Torrent{
|
torrents = append(torrents, &types.Torrent{
|
||||||
Id: t.Id,
|
Id: t.Id,
|
||||||
Name: utils.RemoveInvalidChars(t.Filename),
|
Name: utils.RemoveInvalidChars(t.Filename), // This changes when we get the files
|
||||||
Bytes: t.Bytes,
|
Bytes: t.Bytes,
|
||||||
Progress: t.Progress,
|
Progress: t.Progress,
|
||||||
Status: t.Status,
|
Status: t.Status,
|
||||||
|
|||||||
@@ -232,14 +232,14 @@ func (tb *Torbox) CheckStatus(torrent *types.Torrent, isSymlink bool) (*types.To
|
|||||||
return torrent, nil
|
return torrent, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tb *Torbox) DeleteTorrent(torrent *types.Torrent) {
|
func (tb *Torbox) DeleteTorrent(torrentId string) {
|
||||||
url := fmt.Sprintf("%s/api/torrents/controltorrent/%s", tb.Host, torrent.Id)
|
url := fmt.Sprintf("%s/api/torrents/controltorrent/%s", tb.Host, torrentId)
|
||||||
payload := map[string]string{"torrent_id": torrent.Id, "action": "Delete"}
|
payload := map[string]string{"torrent_id": torrentId, "action": "Delete"}
|
||||||
jsonPayload, _ := json.Marshal(payload)
|
jsonPayload, _ := json.Marshal(payload)
|
||||||
req, _ := http.NewRequest(http.MethodDelete, url, bytes.NewBuffer(jsonPayload))
|
req, _ := http.NewRequest(http.MethodDelete, url, bytes.NewBuffer(jsonPayload))
|
||||||
_, err := tb.client.MakeRequest(req)
|
_, err := tb.client.MakeRequest(req)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
tb.logger.Info().Msgf("Torrent: %s deleted", torrent.Name)
|
tb.logger.Info().Msgf("Torrent: %s deleted", torrentId)
|
||||||
} else {
|
} else {
|
||||||
tb.logger.Info().Msgf("Error deleting torrent: %s", err)
|
tb.logger.Info().Msgf("Error deleting torrent: %s", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ type Client interface {
|
|||||||
GenerateDownloadLinks(tr *Torrent) error
|
GenerateDownloadLinks(tr *Torrent) error
|
||||||
GetDownloadLink(tr *Torrent, file *File) *File
|
GetDownloadLink(tr *Torrent, file *File) *File
|
||||||
ConvertLinksToFiles(links []string) []File
|
ConvertLinksToFiles(links []string) []File
|
||||||
DeleteTorrent(tr *Torrent)
|
DeleteTorrent(torrentId string)
|
||||||
IsAvailable(infohashes []string) map[string]bool
|
IsAvailable(infohashes []string) map[string]bool
|
||||||
GetCheckCached() bool
|
GetCheckCached() bool
|
||||||
GetDownloadUncached() bool
|
GetDownloadUncached() bool
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ func (i *ImportRequest) Process(q *QBit) (err error) {
|
|||||||
if err != nil || debridTorrent == nil {
|
if err != nil || debridTorrent == nil {
|
||||||
if debridTorrent != nil {
|
if debridTorrent != nil {
|
||||||
dbClient := service.GetDebrid().GetByName(debridTorrent.Debrid)
|
dbClient := service.GetDebrid().GetByName(debridTorrent.Debrid)
|
||||||
go dbClient.DeleteTorrent(debridTorrent)
|
go dbClient.DeleteTorrent(debridTorrent.Id)
|
||||||
}
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = fmt.Errorf("failed to process torrent")
|
err = fmt.Errorf("failed to process torrent")
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ func (q *QBit) Process(ctx context.Context, magnet *utils.Magnet, category strin
|
|||||||
if err != nil || debridTorrent == nil {
|
if err != nil || debridTorrent == nil {
|
||||||
if debridTorrent != nil {
|
if debridTorrent != nil {
|
||||||
dbClient := service.GetDebrid().GetByName(debridTorrent.Debrid)
|
dbClient := service.GetDebrid().GetByName(debridTorrent.Debrid)
|
||||||
go dbClient.DeleteTorrent(debridTorrent)
|
go dbClient.DeleteTorrent(debridTorrent.Id)
|
||||||
}
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = fmt.Errorf("failed to process torrent")
|
err = fmt.Errorf("failed to process torrent")
|
||||||
@@ -81,7 +81,7 @@ func (q *QBit) ProcessFiles(torrent *Torrent, debridTorrent *debrid.Torrent, arr
|
|||||||
dbT, err := client.CheckStatus(debridTorrent, isSymlink)
|
dbT, err := client.CheckStatus(debridTorrent, isSymlink)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
q.logger.Error().Msgf("Error checking status: %v", err)
|
q.logger.Error().Msgf("Error checking status: %v", err)
|
||||||
go client.DeleteTorrent(debridTorrent)
|
go client.DeleteTorrent(debridTorrent.Id)
|
||||||
q.MarkAsFailed(torrent)
|
q.MarkAsFailed(torrent)
|
||||||
if err := arr.Refresh(); err != nil {
|
if err := arr.Refresh(); err != nil {
|
||||||
q.logger.Error().Msgf("Error refreshing arr: %v", err)
|
q.logger.Error().Msgf("Error refreshing arr: %v", err)
|
||||||
@@ -116,26 +116,10 @@ func (q *QBit) ProcessFiles(torrent *Torrent, debridTorrent *debrid.Torrent, arr
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if err := cache.RefreshRclone(); err != nil {
|
||||||
|
q.logger.Trace().Msgf("Error refreshing rclone: %v", err)
|
||||||
|
}
|
||||||
rclonePath := filepath.Join(debridTorrent.MountPath, debridTorrent.Name)
|
rclonePath := filepath.Join(debridTorrent.MountPath, debridTorrent.Name)
|
||||||
|
|
||||||
// Check if folder exists here
|
|
||||||
if _, err := os.Stat(rclonePath); os.IsNotExist(err) {
|
|
||||||
q.logger.Debug().Msgf("Folder does not exist: %s", rclonePath)
|
|
||||||
|
|
||||||
// Check if torrent is in the listing
|
|
||||||
listing := cache.GetListing()
|
|
||||||
for _, t := range listing {
|
|
||||||
if t.Name() == debridTorrent.Name {
|
|
||||||
q.logger.Debug().Msgf("Torrent found in listing: %s", debridTorrent.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if torrent is in the webdav
|
|
||||||
if t := cache.GetTorrentByName(debridTorrent.Name); t == nil {
|
|
||||||
q.logger.Debug().Msgf("Torrent not found in webdav: %s", debridTorrent.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
torrentSymlinkPath, err = q.createSymlinks(debridTorrent, rclonePath, debridTorrent.Name)
|
torrentSymlinkPath, err = q.createSymlinks(debridTorrent, rclonePath, debridTorrent.Name)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
@@ -147,7 +131,7 @@ func (q *QBit) ProcessFiles(torrent *Torrent, debridTorrent *debrid.Torrent, arr
|
|||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
q.MarkAsFailed(torrent)
|
q.MarkAsFailed(torrent)
|
||||||
go client.DeleteTorrent(debridTorrent)
|
go client.DeleteTorrent(debridTorrent.Id)
|
||||||
q.logger.Info().Msgf("Error: %v", err)
|
q.logger.Info().Msgf("Error: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"github.com/sirrobot01/debrid-blackhole/internal/logger"
|
"github.com/sirrobot01/debrid-blackhole/internal/logger"
|
||||||
"github.com/sirrobot01/debrid-blackhole/internal/request"
|
"github.com/sirrobot01/debrid-blackhole/internal/request"
|
||||||
"github.com/sirrobot01/debrid-blackhole/pkg/arr"
|
"github.com/sirrobot01/debrid-blackhole/pkg/arr"
|
||||||
"github.com/sirrobot01/debrid-blackhole/pkg/debrid/types"
|
"github.com/sirrobot01/debrid-blackhole/pkg/debrid/debrid"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -29,7 +29,7 @@ import (
|
|||||||
type Repair struct {
|
type Repair struct {
|
||||||
Jobs map[string]*Job
|
Jobs map[string]*Job
|
||||||
arrs *arr.Storage
|
arrs *arr.Storage
|
||||||
deb types.Client
|
deb *debrid.Engine
|
||||||
duration time.Duration
|
duration time.Duration
|
||||||
runOnStart bool
|
runOnStart bool
|
||||||
ZurgURL string
|
ZurgURL string
|
||||||
@@ -39,7 +39,7 @@ type Repair struct {
|
|||||||
filename string
|
filename string
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(arrs *arr.Storage) *Repair {
|
func New(arrs *arr.Storage, engine *debrid.Engine) *Repair {
|
||||||
cfg := config.GetConfig()
|
cfg := config.GetConfig()
|
||||||
duration, err := parseSchedule(cfg.Repair.Interval)
|
duration, err := parseSchedule(cfg.Repair.Interval)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -53,6 +53,7 @@ func New(arrs *arr.Storage) *Repair {
|
|||||||
ZurgURL: cfg.Repair.ZurgURL,
|
ZurgURL: cfg.Repair.ZurgURL,
|
||||||
autoProcess: cfg.Repair.AutoProcess,
|
autoProcess: cfg.Repair.AutoProcess,
|
||||||
filename: filepath.Join(cfg.Path, "repair.json"),
|
filename: filepath.Join(cfg.Path, "repair.json"),
|
||||||
|
deb: engine,
|
||||||
}
|
}
|
||||||
if r.ZurgURL != "" {
|
if r.ZurgURL != "" {
|
||||||
r.IsZurg = true
|
r.IsZurg = true
|
||||||
@@ -70,6 +71,7 @@ const (
|
|||||||
JobPending JobStatus = "pending"
|
JobPending JobStatus = "pending"
|
||||||
JobFailed JobStatus = "failed"
|
JobFailed JobStatus = "failed"
|
||||||
JobCompleted JobStatus = "completed"
|
JobCompleted JobStatus = "completed"
|
||||||
|
JobProcessing JobStatus = "processing"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Job struct {
|
type Job struct {
|
||||||
@@ -185,12 +187,21 @@ func (r *Repair) AddJob(arrsNames []string, mediaIDs []string, autoProcess, recu
|
|||||||
r.reset(job)
|
r.reset(job)
|
||||||
r.Jobs[key] = job
|
r.Jobs[key] = job
|
||||||
go r.saveToFile()
|
go r.saveToFile()
|
||||||
err := r.repair(job)
|
go func() {
|
||||||
go r.saveToFile()
|
if err := r.repair(job); err != nil {
|
||||||
return err
|
r.logger.Error().Err(err).Msg("Error running repair")
|
||||||
|
r.logger.Error().Err(err).Msg("Error running repair")
|
||||||
|
job.FailedAt = time.Now()
|
||||||
|
job.Error = err.Error()
|
||||||
|
job.Status = JobFailed
|
||||||
|
job.CompletedAt = time.Now()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repair) repair(job *Job) error {
|
func (r *Repair) repair(job *Job) error {
|
||||||
|
defer r.saveToFile()
|
||||||
if err := r.preRunChecks(); err != nil {
|
if err := r.preRunChecks(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -331,6 +342,161 @@ func (r *Repair) Start(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Repair) getUniquePaths(media arr.Content) map[string]string {
|
||||||
|
// Use zurg setup to check file availability with zurg
|
||||||
|
// This reduces bandwidth usage significantly
|
||||||
|
|
||||||
|
uniqueParents := make(map[string]string)
|
||||||
|
files := media.Files
|
||||||
|
for _, file := range files {
|
||||||
|
target := getSymlinkTarget(file.Path)
|
||||||
|
if target != "" {
|
||||||
|
file.IsSymlink = true
|
||||||
|
dir, f := filepath.Split(target)
|
||||||
|
parent := filepath.Base(filepath.Clean(dir))
|
||||||
|
// Set target path folder/file.mkv
|
||||||
|
file.TargetPath = f
|
||||||
|
uniqueParents[parent] = target
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return uniqueParents
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repair) clean(job *Job) error {
|
||||||
|
// Create a new error group
|
||||||
|
g, ctx := errgroup.WithContext(context.Background())
|
||||||
|
|
||||||
|
uniqueItems := make(map[string]string)
|
||||||
|
mu := sync.Mutex{}
|
||||||
|
|
||||||
|
// Limit concurrent goroutines
|
||||||
|
g.SetLimit(runtime.NumCPU() * 4)
|
||||||
|
|
||||||
|
for _, a := range job.Arrs {
|
||||||
|
a := a // Capture range variable
|
||||||
|
g.Go(func() error {
|
||||||
|
// Check if context was canceled
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
items, err := r.cleanArr(job, a, "")
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error().Err(err).Msgf("Error cleaning %s", a)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safely append the found items to the shared slice
|
||||||
|
if len(items) > 0 {
|
||||||
|
mu.Lock()
|
||||||
|
for k, v := range items {
|
||||||
|
uniqueItems[k] = v
|
||||||
|
}
|
||||||
|
mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.Wait(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(uniqueItems) == 0 {
|
||||||
|
job.CompletedAt = time.Now()
|
||||||
|
job.Status = JobCompleted
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := request.SendDiscordMessage("repair_clean_complete", "success", job.discordContext()); err != nil {
|
||||||
|
r.logger.Error().Msgf("Error sending discord message: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cache := r.deb.Caches["realdebrid"]
|
||||||
|
if cache == nil {
|
||||||
|
return fmt.Errorf("cache not found")
|
||||||
|
}
|
||||||
|
torrents := cache.GetTorrents()
|
||||||
|
|
||||||
|
dangling := make([]string, 0)
|
||||||
|
for _, t := range torrents {
|
||||||
|
if _, ok := uniqueItems[t.Name]; !ok {
|
||||||
|
dangling = append(dangling, t.Id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.logger.Info().Msgf("Found %d delapitated items", len(dangling))
|
||||||
|
|
||||||
|
if len(dangling) == 0 {
|
||||||
|
job.CompletedAt = time.Now()
|
||||||
|
job.Status = JobCompleted
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
client := r.deb.Clients["realdebrid"]
|
||||||
|
if client == nil {
|
||||||
|
return fmt.Errorf("client not found")
|
||||||
|
}
|
||||||
|
for _, id := range dangling {
|
||||||
|
client.DeleteTorrent(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repair) cleanArr(j *Job, _arr string, tmdbId string) (map[string]string, error) {
|
||||||
|
uniqueItems := make(map[string]string)
|
||||||
|
a := r.arrs.Get(_arr)
|
||||||
|
|
||||||
|
r.logger.Info().Msgf("Starting repair for %s", a.Name)
|
||||||
|
media, err := a.GetMedia(tmdbId)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Info().Msgf("Failed to get %s media: %v", a.Name, err)
|
||||||
|
return uniqueItems, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new error group
|
||||||
|
g, ctx := errgroup.WithContext(context.Background())
|
||||||
|
|
||||||
|
mu := sync.Mutex{}
|
||||||
|
|
||||||
|
// Limit concurrent goroutines
|
||||||
|
g.SetLimit(runtime.NumCPU() * 4)
|
||||||
|
|
||||||
|
for _, m := range media {
|
||||||
|
m := m // Create a new variable scoped to the loop iteration
|
||||||
|
g.Go(func() error {
|
||||||
|
// Check if context was canceled
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
u := r.getUniquePaths(m)
|
||||||
|
for k, v := range u {
|
||||||
|
mu.Lock()
|
||||||
|
uniqueItems[k] = v
|
||||||
|
mu.Unlock()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.Wait(); err != nil {
|
||||||
|
return uniqueItems, err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.logger.Info().Msgf("Repair completed for %s. %d unique items", a.Name, len(uniqueItems))
|
||||||
|
return uniqueItems, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Repair) repairArr(j *Job, _arr string, tmdbId string) ([]arr.ContentFile, error) {
|
func (r *Repair) repairArr(j *Job, _arr string, tmdbId string) ([]arr.ContentFile, error) {
|
||||||
brokenItems := make([]arr.ContentFile, 0)
|
brokenItems := make([]arr.ContentFile, 0)
|
||||||
a := r.arrs.Get(_arr)
|
a := r.arrs.Get(_arr)
|
||||||
@@ -575,6 +741,7 @@ func (r *Repair) ProcessJob(id string) error {
|
|||||||
if job == nil {
|
if job == nil {
|
||||||
return fmt.Errorf("job %s not found", id)
|
return fmt.Errorf("job %s not found", id)
|
||||||
}
|
}
|
||||||
|
// All validation checks remain the same
|
||||||
if job.Status != JobPending {
|
if job.Status != JobPending {
|
||||||
return fmt.Errorf("job %s not pending", id)
|
return fmt.Errorf("job %s not pending", id)
|
||||||
}
|
}
|
||||||
@@ -598,6 +765,7 @@ func (r *Repair) ProcessJob(id string) error {
|
|||||||
|
|
||||||
// Create a new error group
|
// Create a new error group
|
||||||
g := new(errgroup.Group)
|
g := new(errgroup.Group)
|
||||||
|
g.SetLimit(runtime.NumCPU() * 4)
|
||||||
|
|
||||||
for arrName, items := range brokenItems {
|
for arrName, items := range brokenItems {
|
||||||
items := items
|
items := items
|
||||||
@@ -612,7 +780,6 @@ func (r *Repair) ProcessJob(id string) error {
|
|||||||
if err := a.DeleteFiles(items); err != nil {
|
if err := a.DeleteFiles(items); err != nil {
|
||||||
r.logger.Error().Err(err).Msgf("Failed to delete broken items for %s", arrName)
|
r.logger.Error().Err(err).Msgf("Failed to delete broken items for %s", arrName)
|
||||||
return nil
|
return nil
|
||||||
|
|
||||||
}
|
}
|
||||||
// Search for missing items
|
// Search for missing items
|
||||||
if err := a.SearchMissing(items); err != nil {
|
if err := a.SearchMissing(items); err != nil {
|
||||||
@@ -620,20 +787,29 @@ func (r *Repair) ProcessJob(id string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update job status to in-progress
|
||||||
|
job.Status = JobProcessing
|
||||||
|
r.saveToFile()
|
||||||
|
|
||||||
|
// Launch a goroutine to wait for completion and update the job
|
||||||
|
go func() {
|
||||||
if err := g.Wait(); err != nil {
|
if err := g.Wait(); err != nil {
|
||||||
job.FailedAt = time.Now()
|
job.FailedAt = time.Now()
|
||||||
job.Error = err.Error()
|
job.Error = err.Error()
|
||||||
job.CompletedAt = time.Now()
|
job.CompletedAt = time.Now()
|
||||||
job.Status = JobFailed
|
job.Status = JobFailed
|
||||||
return err
|
r.logger.Error().Err(err).Msgf("Job %s failed", id)
|
||||||
}
|
} else {
|
||||||
|
|
||||||
job.CompletedAt = time.Now()
|
job.CompletedAt = time.Now()
|
||||||
job.Status = JobCompleted
|
job.Status = JobCompleted
|
||||||
|
r.logger.Info().Msgf("Job %s completed successfully", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.saveToFile()
|
||||||
|
}()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ func New() *Service {
|
|||||||
arrs := arr.NewStorage()
|
arrs := arr.NewStorage()
|
||||||
deb := debrid.NewEngine()
|
deb := debrid.NewEngine()
|
||||||
instance = &Service{
|
instance = &Service{
|
||||||
Repair: repair.New(arrs),
|
Repair: repair.New(arrs, deb),
|
||||||
Arr: arrs,
|
Arr: arrs,
|
||||||
Debrid: deb,
|
Debrid: deb,
|
||||||
}
|
}
|
||||||
@@ -43,7 +43,7 @@ func Update() *Service {
|
|||||||
arrs := arr.NewStorage()
|
arrs := arr.NewStorage()
|
||||||
deb := debrid.NewEngine()
|
deb := debrid.NewEngine()
|
||||||
instance = &Service{
|
instance = &Service{
|
||||||
Repair: repair.New(arrs),
|
Repair: repair.New(arrs, deb),
|
||||||
Arr: arrs,
|
Arr: arrs,
|
||||||
Debrid: deb,
|
Debrid: deb,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -375,15 +375,20 @@ func (ui *Handler) handleRepairMedia(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
svc := service.GetService()
|
svc := service.GetService()
|
||||||
|
|
||||||
|
var arrs []string
|
||||||
|
|
||||||
|
if req.ArrName != "" {
|
||||||
_arr := svc.Arr.Get(req.ArrName)
|
_arr := svc.Arr.Get(req.ArrName)
|
||||||
if _arr == nil {
|
if _arr == nil {
|
||||||
http.Error(w, "No Arrs found to repair", http.StatusNotFound)
|
http.Error(w, "No Arrs found to repair", http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
arrs = append(arrs, req.ArrName)
|
||||||
|
}
|
||||||
|
|
||||||
if req.Async {
|
if req.Async {
|
||||||
go func() {
|
go func() {
|
||||||
if err := svc.Repair.AddJob([]string{req.ArrName}, req.MediaIds, req.AutoProcess, false); err != nil {
|
if err := svc.Repair.AddJob(arrs, req.MediaIds, req.AutoProcess, false); err != nil {
|
||||||
ui.logger.Error().Err(err).Msg("Failed to repair media")
|
ui.logger.Error().Err(err).Msg("Failed to repair media")
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -459,12 +464,10 @@ func (ui *Handler) handleProcessRepairJob(w http.ResponseWriter, r *http.Request
|
|||||||
http.Error(w, "No job ID provided", http.StatusBadRequest)
|
http.Error(w, "No job ID provided", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
go func() {
|
|
||||||
svc := service.GetService()
|
svc := service.GetService()
|
||||||
if err := svc.Repair.ProcessJob(id); err != nil {
|
if err := svc.Repair.ProcessJob(id); err != nil {
|
||||||
ui.logger.Error().Err(err).Msg("Failed to process repair job")
|
ui.logger.Error().Err(err).Msg("Failed to process repair job")
|
||||||
}
|
}
|
||||||
}()
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<form id="repairForm">
|
<form id="repairForm">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="arrSelect" class="form-label">Select Arr Instance</label>
|
<label for="arrSelect" class="form-label">Select Arr Instance</label>
|
||||||
<select class="form-select" id="arrSelect" required>
|
<select class="form-select" id="arrSelect">
|
||||||
<option value="">Select an Arr instance</option>
|
<option value="">Select an Arr instance</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -174,12 +174,6 @@
|
|||||||
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Repairing...';
|
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Repairing...';
|
||||||
let mediaIds = document.getElementById('mediaIds').value.split(',').map(id => id.trim());
|
let mediaIds = document.getElementById('mediaIds').value.split(',').map(id => id.trim());
|
||||||
let arr = document.getElementById('arrSelect').value;
|
let arr = document.getElementById('arrSelect').value;
|
||||||
if (!arr) {
|
|
||||||
createToast('Please select an Arr instance', 'warning');
|
|
||||||
submitBtn.disabled = false;
|
|
||||||
submitBtn.innerHTML = originalText;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/internal/repair', {
|
const response = await fetch('/internal/repair', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -187,7 +181,7 @@
|
|||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
arr: document.getElementById('arrSelect').value,
|
arr: arr,
|
||||||
mediaIds: mediaIds,
|
mediaIds: mediaIds,
|
||||||
async: document.getElementById('isAsync').checked,
|
async: document.getElementById('isAsync').checked,
|
||||||
autoProcess: document.getElementById('autoProcess').checked,
|
autoProcess: document.getElementById('autoProcess').checked,
|
||||||
@@ -262,17 +256,15 @@
|
|||||||
// Determine status
|
// Determine status
|
||||||
let status = 'In Progress';
|
let status = 'In Progress';
|
||||||
let statusClass = 'text-primary';
|
let statusClass = 'text-primary';
|
||||||
let canDelete = false;
|
let canDelete = job.status !== "started";
|
||||||
let totalItems = job.broken_items ? Object.values(job.broken_items).reduce((sum, arr) => sum + arr.length, 0) : 0;
|
let totalItems = job.broken_items ? Object.values(job.broken_items).reduce((sum, arr) => sum + arr.length, 0) : 0;
|
||||||
|
|
||||||
if (job.status === 'failed') {
|
if (job.status === 'failed') {
|
||||||
status = 'Failed';
|
status = 'Failed';
|
||||||
statusClass = 'text-danger';
|
statusClass = 'text-danger';
|
||||||
canDelete = true;
|
|
||||||
} else if (job.status === 'completed') {
|
} else if (job.status === 'completed') {
|
||||||
status = 'Completed';
|
status = 'Completed';
|
||||||
statusClass = 'text-success';
|
statusClass = 'text-success';
|
||||||
canDelete = true;
|
|
||||||
} else if (job.status === 'pending') {
|
} else if (job.status === 'pending') {
|
||||||
status = 'Pending';
|
status = 'Pending';
|
||||||
statusClass = 'text-warning';
|
statusClass = 'text-warning';
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package webdav
|
package webdav
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/sirrobot01/debrid-blackhole/pkg/debrid/debrid"
|
"github.com/sirrobot01/debrid-blackhole/pkg/debrid/debrid"
|
||||||
"io"
|
"io"
|
||||||
@@ -11,11 +12,11 @@ import (
|
|||||||
|
|
||||||
var sharedClient = &http.Client{
|
var sharedClient = &http.Client{
|
||||||
Transport: &http.Transport{
|
Transport: &http.Transport{
|
||||||
// These settings help maintain persistent connections.
|
|
||||||
MaxIdleConns: 100,
|
MaxIdleConns: 100,
|
||||||
IdleConnTimeout: 90 * time.Second,
|
IdleConnTimeout: 90 * time.Second,
|
||||||
DisableCompression: false,
|
DisableCompression: false,
|
||||||
DisableKeepAlives: false,
|
DisableKeepAlives: false,
|
||||||
|
Proxy: http.ProxyFromEnvironment,
|
||||||
},
|
},
|
||||||
Timeout: 0,
|
Timeout: 0,
|
||||||
}
|
}
|
||||||
@@ -39,6 +40,24 @@ type File struct {
|
|||||||
link string
|
link string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type bufferedReadCloser struct {
|
||||||
|
*bufio.Reader
|
||||||
|
closer io.Closer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new bufferedReadCloser with a larger buffer
|
||||||
|
func newBufferedReadCloser(rc io.ReadCloser) *bufferedReadCloser {
|
||||||
|
return &bufferedReadCloser{
|
||||||
|
Reader: bufio.NewReaderSize(rc, 64*1024), // Increase to 1MB buffer
|
||||||
|
closer: rc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close implements ReadCloser interface
|
||||||
|
func (brc *bufferedReadCloser) Close() error {
|
||||||
|
return brc.closer.Close()
|
||||||
|
}
|
||||||
|
|
||||||
// File interface implementations for File
|
// File interface implementations for File
|
||||||
|
|
||||||
func (f *File) Close() error {
|
func (f *File) Close() error {
|
||||||
@@ -82,40 +101,48 @@ func (f *File) Read(p []byte) (n int, err error) {
|
|||||||
return n, nil
|
return n, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we haven't started streaming or a seek was requested,
|
// If we haven't started streaming the file yet or need to reposition
|
||||||
// close the existing stream and start a new HTTP GET request.
|
|
||||||
if f.reader == nil || f.seekPending {
|
if f.reader == nil || f.seekPending {
|
||||||
|
// Close existing reader if we're repositioning
|
||||||
if f.reader != nil && f.seekPending {
|
if f.reader != nil && f.seekPending {
|
||||||
f.reader.Close()
|
f.reader.Close()
|
||||||
f.reader = nil
|
f.reader = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new HTTP GET request for the file's URL.
|
downloadLink := f.GetDownloadLink()
|
||||||
req, err := http.NewRequest("GET", f.GetDownloadLink(), nil)
|
if downloadLink == "" {
|
||||||
|
return 0, fmt.Errorf("failed to get download link for file")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create an HTTP GET request to the file's URL.
|
||||||
|
req, err := http.NewRequest("GET", downloadLink, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("failed to create HTTP request: %w", err)
|
return 0, fmt.Errorf("failed to create HTTP request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we've already read some data, request only the remaining bytes.
|
// Request only the bytes starting from our current offset
|
||||||
if f.offset > 0 {
|
if f.offset > 0 {
|
||||||
req.Header.Set("Range", fmt.Sprintf("bytes=%d-", f.offset))
|
req.Header.Set("Range", fmt.Sprintf("bytes=%d-", f.offset))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute the HTTP request.
|
// Add important headers for streaming
|
||||||
|
req.Header.Set("Connection", "keep-alive")
|
||||||
|
req.Header.Set("Accept", "*/*")
|
||||||
|
req.Header.Set("User-Agent", "Infuse/7.0.2 (iOS)")
|
||||||
|
req.Header.Set("Accept-Encoding", "gzip, deflate, br")
|
||||||
|
|
||||||
resp, err := sharedClient.Do(req)
|
resp, err := sharedClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("HTTP request error: %w", err)
|
return 0, fmt.Errorf("HTTP request error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Accept a 200 (OK) or 206 (Partial Content) status.
|
// Check response codes more carefully
|
||||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusPartialContent {
|
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusPartialContent {
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
return 0, fmt.Errorf("unexpected HTTP status: %d", resp.StatusCode)
|
return 0, fmt.Errorf("unexpected HTTP status: %d", resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the response body as our reader.
|
f.reader = newBufferedReadCloser(resp.Body)
|
||||||
f.reader = resp.Body
|
|
||||||
// Reset the seek pending flag now that we've reinitialized the reader.
|
|
||||||
f.seekPending = false
|
f.seekPending = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,10 +150,12 @@ func (f *File) Read(p []byte) (n int, err error) {
|
|||||||
n, err = f.reader.Read(p)
|
n, err = f.reader.Read(p)
|
||||||
f.offset += int64(n)
|
f.offset += int64(n)
|
||||||
|
|
||||||
// When we reach the end of the stream, close the reader.
|
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
f.reader.Close()
|
f.reader.Close()
|
||||||
f.reader = nil
|
f.reader = nil
|
||||||
|
} else if err != nil {
|
||||||
|
f.reader.Close()
|
||||||
|
f.reader = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return n, err
|
return n, err
|
||||||
@@ -137,12 +166,12 @@ func (f *File) Seek(offset int64, whence int) (int64, error) {
|
|||||||
return 0, os.ErrInvalid
|
return 0, os.ErrInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
var newOffset int64
|
newOffset := f.offset
|
||||||
switch whence {
|
switch whence {
|
||||||
case io.SeekStart:
|
case io.SeekStart:
|
||||||
newOffset = offset
|
newOffset = offset
|
||||||
case io.SeekCurrent:
|
case io.SeekCurrent:
|
||||||
newOffset = f.offset + offset
|
newOffset += offset
|
||||||
case io.SeekEnd:
|
case io.SeekEnd:
|
||||||
newOffset = f.size + offset
|
newOffset = f.size + offset
|
||||||
default:
|
default:
|
||||||
@@ -156,7 +185,7 @@ func (f *File) Seek(offset int64, whence int) (int64, error) {
|
|||||||
newOffset = f.size
|
newOffset = f.size
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we're seeking to a new position, mark the reader for reset.
|
// Only mark seek as pending if position actually changed
|
||||||
if newOffset != f.offset {
|
if newOffset != f.offset {
|
||||||
f.offset = newOffset
|
f.offset = newOffset
|
||||||
f.seekPending = true
|
f.seekPending = true
|
||||||
@@ -184,6 +213,24 @@ func (f *File) Stat() (os.FileInfo, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *File) ReadAt(p []byte, off int64) (n int, err error) {
|
||||||
|
// Save current position
|
||||||
|
|
||||||
|
// Seek to requested position
|
||||||
|
_, err = f.Seek(off, io.SeekStart)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the data
|
||||||
|
n, err = f.Read(p)
|
||||||
|
|
||||||
|
// Don't restore position for Infuse compatibility
|
||||||
|
// Infuse expects sequential reads after the initial seek
|
||||||
|
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
func (f *File) Write(p []byte) (n int, err error) {
|
func (f *File) Write(p []byte) (n int, err error) {
|
||||||
return 0, os.ErrPermission
|
return 0, os.ErrPermission
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package webdav
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"compress/gzip"
|
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -68,7 +67,7 @@ func (h *Handler) RemoveAll(ctx context.Context, name string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if filename == "" {
|
if filename == "" {
|
||||||
h.cache.GetClient().DeleteTorrent(cachedTorrent.Torrent)
|
h.cache.GetClient().DeleteTorrent(cachedTorrent.Torrent.Id)
|
||||||
h.cache.OnRemove(cachedTorrent.Id)
|
h.cache.OnRemove(cachedTorrent.Id)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -259,7 +258,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
// - Otherwise, for deeper (torrent folder) paths, use a longer TTL.
|
// - Otherwise, for deeper (torrent folder) paths, use a longer TTL.
|
||||||
ttl := 30 * time.Minute
|
ttl := 30 * time.Minute
|
||||||
if h.isParentPath(r.URL.Path) {
|
if h.isParentPath(r.URL.Path) {
|
||||||
ttl = 20 * time.Second
|
ttl = 30 * time.Second
|
||||||
}
|
}
|
||||||
|
|
||||||
if served := h.serveFromCacheIfValid(w, r, cacheKey, ttl); served {
|
if served := h.serveFromCacheIfValid(w, r, cacheKey, ttl); served {
|
||||||
@@ -281,22 +280,12 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
responseData := responseRecorder.Body.Bytes()
|
responseData := responseRecorder.Body.Bytes()
|
||||||
|
|
||||||
// Create compressed version
|
// Create compressed version
|
||||||
var gzippedData []byte
|
|
||||||
if len(responseData) > 0 {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
gzw := gzip.NewWriter(&buf)
|
|
||||||
if _, err := gzw.Write(responseData); err == nil {
|
|
||||||
if err := gzw.Close(); err == nil {
|
|
||||||
gzippedData = buf.Bytes()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
h.cache.PropfindResp.Store(cacheKey, debrid.PropfindResponse{
|
//h.cache.PropfindResp.Store(cacheKey, debrid.PropfindResponse{
|
||||||
Data: responseData,
|
// Data: responseData,
|
||||||
GzippedData: gzippedData,
|
// GzippedData: request.Gzip(responseData),
|
||||||
Ts: time.Now(),
|
// Ts: time.Now(),
|
||||||
})
|
//})
|
||||||
|
|
||||||
// Forward the captured response to the client.
|
// Forward the captured response to the client.
|
||||||
for k, v := range responseRecorder.Header() {
|
for k, v := range responseRecorder.Header() {
|
||||||
@@ -417,7 +406,6 @@ func (h *Handler) serveFromCacheIfValid(w http.ResponseWriter, r *http.Request,
|
|||||||
|
|
||||||
if time.Since(respCache.Ts) >= ttl {
|
if time.Since(respCache.Ts) >= ttl {
|
||||||
// Remove expired cache entry
|
// Remove expired cache entry
|
||||||
h.cache.PropfindResp.Delete(cacheKey)
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "application/xml; charset=utf-8")
|
w.Header().Set("Content-Type", "application/xml; charset=utf-8")
|
||||||
|
|||||||
Reference in New Issue
Block a user