- Speed up repairs when checking links \n
- Remove run on start for repairs since it causes issues \n - Add support for arr-specific debrid - Support for queuing system - Support for no-op when sending torrents to debrid
This commit is contained in:
@@ -23,8 +23,7 @@ Here's a minimal configuration to get started:
|
|||||||
},
|
},
|
||||||
"repair": {
|
"repair": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"interval": "12h",
|
"interval": "12h"
|
||||||
"run_on_start": false
|
|
||||||
},
|
},
|
||||||
"use_auth": false,
|
"use_auth": false,
|
||||||
"log_level": "info"
|
"log_level": "info"
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ To enable and configure the Repair Worker, add the following to your `config.jso
|
|||||||
"repair": {
|
"repair": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"interval": "12h",
|
"interval": "12h",
|
||||||
"run_on_start": false,
|
|
||||||
"use_webdav": false,
|
"use_webdav": false,
|
||||||
"zurg_url": "http://localhost:9999",
|
"zurg_url": "http://localhost:9999",
|
||||||
"auto_process": true
|
"auto_process": true
|
||||||
@@ -30,7 +29,6 @@ To enable and configure the Repair Worker, add the following to your `config.jso
|
|||||||
|
|
||||||
- `enabled`: Set to `true` to enable the Repair Worker.
|
- `enabled`: Set to `true` to enable the Repair Worker.
|
||||||
- `interval`: The time interval for the Repair Worker to run (e.g., `12h`, `1d`).
|
- `interval`: The time interval for the Repair Worker to run (e.g., `12h`, `1d`).
|
||||||
- `run_on_start`: If set to `true`, the Repair Worker will run immediately after Decypharr starts.
|
|
||||||
- `use_webdav`: If set to `true`, the Repair Worker will use WebDAV for file operations.
|
- `use_webdav`: If set to `true`, the Repair Worker will use WebDAV for file operations.
|
||||||
- `zurg_url`: The URL for the Zurg service (if using).
|
- `zurg_url`: The URL for the Zurg service (if using).
|
||||||
- `auto_process`: If set to `true`, the Repair Worker will automatically process files that it finds issues with.
|
- `auto_process`: If set to `true`, the Repair Worker will automatically process files that it finds issues with.
|
||||||
|
|||||||
@@ -53,12 +53,12 @@ type Arr struct {
|
|||||||
Cleanup bool `json:"cleanup,omitempty"`
|
Cleanup bool `json:"cleanup,omitempty"`
|
||||||
SkipRepair bool `json:"skip_repair,omitempty"`
|
SkipRepair bool `json:"skip_repair,omitempty"`
|
||||||
DownloadUncached *bool `json:"download_uncached,omitempty"`
|
DownloadUncached *bool `json:"download_uncached,omitempty"`
|
||||||
|
SelectedDebrid string `json:"selected_debrid,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Repair struct {
|
type Repair struct {
|
||||||
Enabled bool `json:"enabled,omitempty"`
|
Enabled bool `json:"enabled,omitempty"`
|
||||||
Interval string `json:"interval,omitempty"`
|
Interval string `json:"interval,omitempty"`
|
||||||
RunOnStart bool `json:"run_on_start,omitempty"`
|
|
||||||
ZurgURL string `json:"zurg_url,omitempty"`
|
ZurgURL string `json:"zurg_url,omitempty"`
|
||||||
AutoProcess bool `json:"auto_process,omitempty"`
|
AutoProcess bool `json:"auto_process,omitempty"`
|
||||||
UseWebDav bool `json:"use_webdav,omitempty"`
|
UseWebDav bool `json:"use_webdav,omitempty"`
|
||||||
|
|||||||
+4
-2
@@ -34,10 +34,11 @@ type Arr struct {
|
|||||||
Cleanup bool `json:"cleanup"`
|
Cleanup bool `json:"cleanup"`
|
||||||
SkipRepair bool `json:"skip_repair"`
|
SkipRepair bool `json:"skip_repair"`
|
||||||
DownloadUncached *bool `json:"download_uncached"`
|
DownloadUncached *bool `json:"download_uncached"`
|
||||||
|
SelectedDebrid string `json:"selected_debrid,omitempty"` // The debrid service selected for this arr
|
||||||
client *request.Client
|
client *request.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(name, host, token string, cleanup, skipRepair bool, downloadUncached *bool) *Arr {
|
func New(name, host, token string, cleanup, skipRepair bool, downloadUncached *bool, selectedDebrid string) *Arr {
|
||||||
return &Arr{
|
return &Arr{
|
||||||
Name: name,
|
Name: name,
|
||||||
Host: host,
|
Host: host,
|
||||||
@@ -47,6 +48,7 @@ func New(name, host, token string, cleanup, skipRepair bool, downloadUncached *b
|
|||||||
SkipRepair: skipRepair,
|
SkipRepair: skipRepair,
|
||||||
DownloadUncached: downloadUncached,
|
DownloadUncached: downloadUncached,
|
||||||
client: request.New(),
|
client: request.New(),
|
||||||
|
SelectedDebrid: selectedDebrid,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,7 +147,7 @@ func NewStorage() *Storage {
|
|||||||
arrs := make(map[string]*Arr)
|
arrs := make(map[string]*Arr)
|
||||||
for _, a := range config.Get().Arrs {
|
for _, a := range config.Get().Arrs {
|
||||||
name := a.Name
|
name := a.Name
|
||||||
arrs[name] = New(name, a.Host, a.Token, a.Cleanup, a.SkipRepair, a.DownloadUncached)
|
arrs[name] = New(name, a.Host, a.Token, a.Cleanup, a.SkipRepair, a.DownloadUncached, a.SelectedDebrid)
|
||||||
}
|
}
|
||||||
return &Storage{
|
return &Storage{
|
||||||
Arrs: arrs,
|
Arrs: arrs,
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ func createDebridClient(dc config.Debrid) (types.Client, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Process(ctx context.Context, store *Storage, selectedDebrid string, magnet *utils.Magnet, a *arr.Arr, isSymlink, overrideDownloadUncached bool) (*types.Torrent, error) {
|
func Process(ctx context.Context, store *Storage, selectedDebrid string, magnet *utils.Magnet, a *arr.Arr, action string, overrideDownloadUncached bool) (*types.Torrent, error) {
|
||||||
|
|
||||||
debridTorrent := &types.Torrent{
|
debridTorrent := &types.Torrent{
|
||||||
InfoHash: magnet.InfoHash,
|
InfoHash: magnet.InfoHash,
|
||||||
@@ -200,6 +200,7 @@ func Process(ctx context.Context, store *Storage, selectedDebrid string, magnet
|
|||||||
Str("Arr", a.Name).
|
Str("Arr", a.Name).
|
||||||
Str("Hash", debridTorrent.InfoHash).
|
Str("Hash", debridTorrent.InfoHash).
|
||||||
Str("Name", debridTorrent.Name).
|
Str("Name", debridTorrent.Name).
|
||||||
|
Str("Action", action).
|
||||||
Msg("Processing torrent")
|
Msg("Processing torrent")
|
||||||
|
|
||||||
if !overrideDownloadUncached && a.DownloadUncached == nil {
|
if !overrideDownloadUncached && a.DownloadUncached == nil {
|
||||||
@@ -215,7 +216,7 @@ func Process(ctx context.Context, store *Storage, selectedDebrid string, magnet
|
|||||||
_logger.Info().Str("id", dbt.Id).Msgf("Torrent: %s submitted to %s", dbt.Name, db.Name())
|
_logger.Info().Str("id", dbt.Id).Msgf("Torrent: %s submitted to %s", dbt.Name, db.Name())
|
||||||
store.lastUsed = index
|
store.lastUsed = index
|
||||||
|
|
||||||
torrent, err := db.CheckStatus(dbt, isSymlink)
|
torrent, err := db.CheckStatus(dbt)
|
||||||
if err != nil && torrent != nil && torrent.Id != "" {
|
if err != nil && torrent != nil && torrent.Id != "" {
|
||||||
// Delete the torrent if it was not downloaded
|
// Delete the torrent if it was not downloaded
|
||||||
go func(id string) {
|
go func(id string) {
|
||||||
|
|||||||
@@ -259,7 +259,7 @@ func (ad *AllDebrid) UpdateTorrent(t *types.Torrent) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ad *AllDebrid) CheckStatus(torrent *types.Torrent, isSymlink bool) (*types.Torrent, error) {
|
func (ad *AllDebrid) CheckStatus(torrent *types.Torrent) (*types.Torrent, error) {
|
||||||
for {
|
for {
|
||||||
err := ad.UpdateTorrent(torrent)
|
err := ad.UpdateTorrent(torrent)
|
||||||
|
|
||||||
@@ -269,13 +269,7 @@ func (ad *AllDebrid) CheckStatus(torrent *types.Torrent, isSymlink bool) (*types
|
|||||||
status := torrent.Status
|
status := torrent.Status
|
||||||
if status == "downloaded" {
|
if status == "downloaded" {
|
||||||
ad.logger.Info().Msgf("Torrent: %s downloaded", torrent.Name)
|
ad.logger.Info().Msgf("Torrent: %s downloaded", torrent.Name)
|
||||||
if !isSymlink {
|
return torrent, nil
|
||||||
|
|
||||||
if err = ad.GetFileDownloadLinks(torrent); err != nil {
|
|
||||||
return torrent, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
} else if utils.Contains(ad.GetDownloadingStatus(), status) {
|
} else if utils.Contains(ad.GetDownloadingStatus(), status) {
|
||||||
if !torrent.DownloadUncached {
|
if !torrent.DownloadUncached {
|
||||||
return torrent, fmt.Errorf("torrent: %s not cached", torrent.Name)
|
return torrent, fmt.Errorf("torrent: %s not cached", torrent.Name)
|
||||||
@@ -288,7 +282,6 @@ func (ad *AllDebrid) CheckStatus(torrent *types.Torrent, isSymlink bool) (*types
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
return torrent, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ad *AllDebrid) DeleteTorrent(torrentId string) error {
|
func (ad *AllDebrid) DeleteTorrent(torrentId string) error {
|
||||||
|
|||||||
@@ -316,7 +316,7 @@ func (dl *DebridLink) SubmitMagnet(t *types.Torrent) (*types.Torrent, error) {
|
|||||||
return t, nil
|
return t, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dl *DebridLink) CheckStatus(torrent *types.Torrent, isSymlink bool) (*types.Torrent, error) {
|
func (dl *DebridLink) CheckStatus(torrent *types.Torrent) (*types.Torrent, error) {
|
||||||
for {
|
for {
|
||||||
err := dl.UpdateTorrent(torrent)
|
err := dl.UpdateTorrent(torrent)
|
||||||
if err != nil || torrent == nil {
|
if err != nil || torrent == nil {
|
||||||
@@ -325,11 +325,7 @@ func (dl *DebridLink) CheckStatus(torrent *types.Torrent, isSymlink bool) (*type
|
|||||||
status := torrent.Status
|
status := torrent.Status
|
||||||
if status == "downloaded" {
|
if status == "downloaded" {
|
||||||
dl.logger.Info().Msgf("Torrent: %s downloaded", torrent.Name)
|
dl.logger.Info().Msgf("Torrent: %s downloaded", torrent.Name)
|
||||||
|
return torrent, nil
|
||||||
if err = dl.GetFileDownloadLinks(torrent); err != nil {
|
|
||||||
return torrent, err
|
|
||||||
}
|
|
||||||
break
|
|
||||||
} else if utils.Contains(dl.GetDownloadingStatus(), status) {
|
} else if utils.Contains(dl.GetDownloadingStatus(), status) {
|
||||||
if !torrent.DownloadUncached {
|
if !torrent.DownloadUncached {
|
||||||
return torrent, fmt.Errorf("torrent: %s not cached", torrent.Name)
|
return torrent, fmt.Errorf("torrent: %s not cached", torrent.Name)
|
||||||
@@ -342,7 +338,6 @@ func (dl *DebridLink) CheckStatus(torrent *types.Torrent, isSymlink bool) (*type
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
return torrent, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dl *DebridLink) DeleteTorrent(torrentId string) error {
|
func (dl *DebridLink) DeleteTorrent(torrentId string) error {
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
package realdebrid
|
||||||
@@ -468,7 +468,7 @@ func (r *RealDebrid) UpdateTorrent(t *types.Torrent) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RealDebrid) CheckStatus(t *types.Torrent, isSymlink bool) (*types.Torrent, error) {
|
func (r *RealDebrid) CheckStatus(t *types.Torrent) (*types.Torrent, error) {
|
||||||
url := fmt.Sprintf("%s/torrents/info/%s", r.Host, t.Id)
|
url := fmt.Sprintf("%s/torrents/info/%s", r.Host, t.Id)
|
||||||
req, _ := http.NewRequest(http.MethodGet, url, nil)
|
req, _ := http.NewRequest(http.MethodGet, url, nil)
|
||||||
for {
|
for {
|
||||||
@@ -525,12 +525,7 @@ func (r *RealDebrid) CheckStatus(t *types.Torrent, isSymlink bool) (*types.Torre
|
|||||||
}
|
}
|
||||||
|
|
||||||
r.logger.Info().Msgf("Torrent: %s downloaded to RD", t.Name)
|
r.logger.Info().Msgf("Torrent: %s downloaded to RD", t.Name)
|
||||||
if !isSymlink {
|
return t, nil
|
||||||
if err = r.GetFileDownloadLinks(t); err != nil {
|
|
||||||
return t, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
} else if utils.Contains(r.GetDownloadingStatus(), status) {
|
} else if utils.Contains(r.GetDownloadingStatus(), status) {
|
||||||
if !t.DownloadUncached {
|
if !t.DownloadUncached {
|
||||||
return t, fmt.Errorf("torrent: %s not cached", t.Name)
|
return t, fmt.Errorf("torrent: %s not cached", t.Name)
|
||||||
@@ -541,7 +536,6 @@ func (r *RealDebrid) CheckStatus(t *types.Torrent, isSymlink bool) (*types.Torre
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
return t, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RealDebrid) DeleteTorrent(torrentId string) error {
|
func (r *RealDebrid) DeleteTorrent(torrentId string) error {
|
||||||
@@ -555,63 +549,55 @@ func (r *RealDebrid) DeleteTorrent(torrentId string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *RealDebrid) GetFileDownloadLinks(t *types.Torrent) error {
|
func (r *RealDebrid) GetFileDownloadLinks(t *types.Torrent) error {
|
||||||
filesCh := make(chan types.File, len(t.Files))
|
|
||||||
errCh := make(chan error, len(t.Files))
|
|
||||||
linksCh := make(chan *types.DownloadLink)
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
wg.Add(len(t.Files))
|
var mu sync.Mutex
|
||||||
for _, f := range t.Files {
|
var firstErr error
|
||||||
|
|
||||||
|
files := make(map[string]types.File)
|
||||||
|
links := make(map[string]*types.DownloadLink)
|
||||||
|
|
||||||
|
_files := t.GetFiles()
|
||||||
|
wg.Add(len(_files))
|
||||||
|
|
||||||
|
for _, f := range _files {
|
||||||
go func(file types.File) {
|
go func(file types.File) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
|
||||||
link, err := r.GetDownloadLink(t, &file)
|
link, err := r.GetDownloadLink(t, &file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errCh <- err
|
mu.Lock()
|
||||||
|
if firstErr == nil {
|
||||||
|
firstErr = err
|
||||||
|
}
|
||||||
|
mu.Unlock()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if link == nil {
|
if link == nil {
|
||||||
errCh <- fmt.Errorf("realdebrid API error: download link not found for file %s", file.Name)
|
mu.Lock()
|
||||||
|
if firstErr == nil {
|
||||||
|
firstErr = fmt.Errorf("realdebrid API error: download link not found for file %s", file.Name)
|
||||||
|
}
|
||||||
|
mu.Unlock()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
linksCh <- link
|
|
||||||
file.DownloadLink = link
|
file.DownloadLink = link
|
||||||
filesCh <- file
|
|
||||||
|
mu.Lock()
|
||||||
|
files[file.Name] = file
|
||||||
|
links[link.Link] = link
|
||||||
|
mu.Unlock()
|
||||||
}(f)
|
}(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
wg.Wait()
|
||||||
wg.Wait()
|
|
||||||
close(filesCh)
|
|
||||||
close(linksCh)
|
|
||||||
close(errCh)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Collect results
|
if firstErr != nil {
|
||||||
files := make(map[string]types.File, len(t.Files))
|
return firstErr
|
||||||
for file := range filesCh {
|
|
||||||
files[file.Name] = file
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect download links
|
|
||||||
links := make(map[string]*types.DownloadLink)
|
|
||||||
for link := range linksCh {
|
|
||||||
if link == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
links[link.Link] = link
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add links to cache
|
// Add links to cache
|
||||||
r.accounts.SetDownloadLinks(links)
|
r.accounts.SetDownloadLinks(links)
|
||||||
|
|
||||||
// Check for errors
|
|
||||||
for err := range errCh {
|
|
||||||
if err != nil {
|
|
||||||
return err // Return the first error encountered
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Files = files
|
t.Files = files
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -312,7 +312,7 @@ func (tb *Torbox) UpdateTorrent(t *types.Torrent) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tb *Torbox) CheckStatus(torrent *types.Torrent, isSymlink bool) (*types.Torrent, error) {
|
func (tb *Torbox) CheckStatus(torrent *types.Torrent) (*types.Torrent, error) {
|
||||||
for {
|
for {
|
||||||
err := tb.UpdateTorrent(torrent)
|
err := tb.UpdateTorrent(torrent)
|
||||||
|
|
||||||
@@ -322,12 +322,7 @@ func (tb *Torbox) CheckStatus(torrent *types.Torrent, isSymlink bool) (*types.To
|
|||||||
status := torrent.Status
|
status := torrent.Status
|
||||||
if status == "downloaded" {
|
if status == "downloaded" {
|
||||||
tb.logger.Info().Msgf("Torrent: %s downloaded", torrent.Name)
|
tb.logger.Info().Msgf("Torrent: %s downloaded", torrent.Name)
|
||||||
if !isSymlink {
|
return torrent, nil
|
||||||
if err = tb.GetFileDownloadLinks(torrent); err != nil {
|
|
||||||
return torrent, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
} else if utils.Contains(tb.GetDownloadingStatus(), status) {
|
} else if utils.Contains(tb.GetDownloadingStatus(), status) {
|
||||||
if !torrent.DownloadUncached {
|
if !torrent.DownloadUncached {
|
||||||
return torrent, fmt.Errorf("torrent: %s not cached", torrent.Name)
|
return torrent, fmt.Errorf("torrent: %s not cached", torrent.Name)
|
||||||
@@ -340,7 +335,6 @@ func (tb *Torbox) CheckStatus(torrent *types.Torrent, isSymlink bool) (*types.To
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
return torrent, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tb *Torbox) DeleteTorrent(torrentId string) error {
|
func (tb *Torbox) DeleteTorrent(torrentId string) error {
|
||||||
|
|||||||
@@ -34,8 +34,6 @@ func (c *Cache) GetDownloadLink(torrentName, filename, fileLink string) (string,
|
|||||||
// Check link cache
|
// Check link cache
|
||||||
if dl, err := c.checkDownloadLink(fileLink); dl != "" && err == nil {
|
if dl, err := c.checkDownloadLink(fileLink); dl != "" && err == nil {
|
||||||
return dl, nil
|
return dl, nil
|
||||||
} else {
|
|
||||||
c.logger.Trace().Msgf("Download link check failed: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if req, inFlight := c.downloadLinkRequests.Load(fileLink); inFlight {
|
if req, inFlight := c.downloadLinkRequests.Load(fileLink); inFlight {
|
||||||
@@ -54,6 +52,13 @@ func (c *Cache) GetDownloadLink(torrentName, filename, fileLink string) (string,
|
|||||||
c.downloadLinkRequests.Delete(fileLink)
|
c.downloadLinkRequests.Delete(fileLink)
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if dl == nil || dl.DownloadLink == "" {
|
||||||
|
err = fmt.Errorf("download link is empty for %s in torrent %s", filename, torrentName)
|
||||||
|
req.Complete("", err)
|
||||||
|
c.downloadLinkRequests.Delete(fileLink)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
req.Complete(dl.DownloadLink, err)
|
req.Complete(dl.DownloadLink, err)
|
||||||
c.downloadLinkRequests.Delete(fileLink)
|
c.downloadLinkRequests.Delete(fileLink)
|
||||||
return dl.DownloadLink, err
|
return dl.DownloadLink, err
|
||||||
|
|||||||
@@ -90,18 +90,25 @@ func (c *Cache) GetBrokenFiles(t *CachedTorrent, filenames []string) []string {
|
|||||||
|
|
||||||
files = t.Files
|
files = t.Files
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
wg.Add(len(files))
|
||||||
|
|
||||||
for _, f := range files {
|
for _, f := range files {
|
||||||
// Check if file link is still missing
|
// Check if file link is still missing
|
||||||
if f.Link == "" {
|
go func(f types.File) {
|
||||||
brokenFiles = append(brokenFiles, f.Name)
|
defer wg.Done()
|
||||||
} else {
|
if f.Link == "" {
|
||||||
// Check if file.Link not in the downloadLink Cache
|
brokenFiles = append(brokenFiles, f.Name)
|
||||||
if err := c.client.CheckLink(f.Link); err != nil {
|
} else {
|
||||||
if errors.Is(err, utils.HosterUnavailableError) {
|
// Check if file.Link not in the downloadLink Cache
|
||||||
brokenFiles = append(brokenFiles, f.Name)
|
if err := c.client.CheckLink(f.Link); err != nil {
|
||||||
|
if errors.Is(err, utils.HosterUnavailableError) {
|
||||||
|
brokenFiles = append(brokenFiles, f.Name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to reinsert the torrent if it's broken
|
// Try to reinsert the torrent if it's broken
|
||||||
@@ -202,7 +209,7 @@ func (c *Cache) reInsertTorrent(ct *CachedTorrent) (*CachedTorrent, error) {
|
|||||||
return ct, fmt.Errorf("failed to submit magnet: empty torrent")
|
return ct, fmt.Errorf("failed to submit magnet: empty torrent")
|
||||||
}
|
}
|
||||||
newTorrent.DownloadUncached = false // Set to false, avoid re-downloading
|
newTorrent.DownloadUncached = false // Set to false, avoid re-downloading
|
||||||
newTorrent, err = c.client.CheckStatus(newTorrent, true)
|
newTorrent, err = c.client.CheckStatus(newTorrent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if newTorrent != nil && newTorrent.Id != "" {
|
if newTorrent != nil && newTorrent.Id != "" {
|
||||||
// Delete the torrent if it was not downloaded
|
// Delete the torrent if it was not downloaded
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
|
|
||||||
type Client interface {
|
type Client interface {
|
||||||
SubmitMagnet(tr *Torrent) (*Torrent, error)
|
SubmitMagnet(tr *Torrent) (*Torrent, error)
|
||||||
CheckStatus(tr *Torrent, isSymlink bool) (*Torrent, error)
|
CheckStatus(tr *Torrent) (*Torrent, error)
|
||||||
GetFileDownloadLinks(tr *Torrent) error
|
GetFileDownloadLinks(tr *Torrent) error
|
||||||
GetDownloadLink(tr *Torrent, file *File) (*DownloadLink, error)
|
GetDownloadLink(tr *Torrent, file *File) (*DownloadLink, error)
|
||||||
DeleteTorrent(torrentId string) error
|
DeleteTorrent(torrentId string) error
|
||||||
|
|||||||
+1
-1
@@ -87,7 +87,7 @@ func (q *QBit) authContext(next http.Handler) http.Handler {
|
|||||||
a := arrs.Get(category)
|
a := arrs.Get(category)
|
||||||
if a == nil {
|
if a == nil {
|
||||||
downloadUncached := false
|
downloadUncached := false
|
||||||
a = arr.New(category, "", "", false, false, &downloadUncached)
|
a = arr.New(category, "", "", false, false, &downloadUncached, "")
|
||||||
}
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
host = strings.TrimSpace(host)
|
host = strings.TrimSpace(host)
|
||||||
|
|||||||
+8
-6
@@ -88,12 +88,15 @@ func (q *QBit) handleTorrentsAdd(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
isSymlink := strings.ToLower(r.FormValue("sequentialDownload")) != "true"
|
action := "symlink"
|
||||||
|
if strings.ToLower(r.FormValue("sequentialDownload")) != "true" {
|
||||||
|
action = "download"
|
||||||
|
}
|
||||||
debridName := r.FormValue("debrid")
|
debridName := r.FormValue("debrid")
|
||||||
category := r.FormValue("category")
|
category := r.FormValue("category")
|
||||||
_arr := getArr(ctx)
|
_arr := getArr(ctx)
|
||||||
if _arr == nil {
|
if _arr == nil {
|
||||||
_arr = arr.New(category, "", "", false, false, nil)
|
_arr = arr.New(category, "", "", false, false, nil, "")
|
||||||
}
|
}
|
||||||
atleastOne := false
|
atleastOne := false
|
||||||
|
|
||||||
@@ -104,7 +107,7 @@ func (q *QBit) handleTorrentsAdd(w http.ResponseWriter, r *http.Request) {
|
|||||||
urlList = append(urlList, strings.TrimSpace(u))
|
urlList = append(urlList, strings.TrimSpace(u))
|
||||||
}
|
}
|
||||||
for _, url := range urlList {
|
for _, url := range urlList {
|
||||||
if err := q.addMagnet(ctx, url, _arr, debridName, isSymlink); err != nil {
|
if err := q.addMagnet(ctx, url, _arr, debridName, action); err != nil {
|
||||||
q.logger.Error().Err(err).Msgf("Error adding magnet")
|
q.logger.Error().Err(err).Msgf("Error adding magnet")
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
@@ -117,7 +120,7 @@ func (q *QBit) handleTorrentsAdd(w http.ResponseWriter, r *http.Request) {
|
|||||||
if r.MultipartForm != nil && r.MultipartForm.File != nil {
|
if r.MultipartForm != nil && r.MultipartForm.File != nil {
|
||||||
if files := r.MultipartForm.File["torrents"]; len(files) > 0 {
|
if files := r.MultipartForm.File["torrents"]; len(files) > 0 {
|
||||||
for _, fileHeader := range files {
|
for _, fileHeader := range files {
|
||||||
if err := q.addTorrent(ctx, fileHeader, _arr, debridName, isSymlink); err != nil {
|
if err := q.addTorrent(ctx, fileHeader, _arr, debridName, action); err != nil {
|
||||||
q.logger.Error().Err(err).Msgf("Error adding torrent")
|
q.logger.Error().Err(err).Msgf("Error adding torrent")
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
@@ -241,8 +244,7 @@ func (q *QBit) handleTorrentFiles(w http.ResponseWriter, r *http.Request) {
|
|||||||
if torrent == nil {
|
if torrent == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
files := q.getTorrentFiles(torrent)
|
request.JSONResponse(w, torrent.Files, http.StatusOK)
|
||||||
request.JSONResponse(w, files, http.StatusOK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *QBit) handleSetCategory(w http.ResponseWriter, r *http.Request) {
|
func (q *QBit) handleSetCategory(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
+4
-18
@@ -13,14 +13,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// All torrent-related helpers goes here
|
// All torrent-related helpers goes here
|
||||||
func (q *QBit) addMagnet(ctx context.Context, url string, arr *arr.Arr, debrid string, isSymlink bool) error {
|
func (q *QBit) addMagnet(ctx context.Context, url string, arr *arr.Arr, debrid string, action string) error {
|
||||||
magnet, err := utils.GetMagnetFromUrl(url)
|
magnet, err := utils.GetMagnetFromUrl(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error parsing magnet link: %w", err)
|
return fmt.Errorf("error parsing magnet link: %w", err)
|
||||||
}
|
}
|
||||||
_store := store.Get()
|
_store := store.Get()
|
||||||
|
|
||||||
importReq := store.NewImportRequest(debrid, q.DownloadFolder, magnet, arr, isSymlink, false, "", store.ImportTypeQBitTorrent)
|
importReq := store.NewImportRequest(debrid, q.DownloadFolder, magnet, arr, action, false, "", store.ImportTypeQBitTorrent)
|
||||||
|
|
||||||
err = _store.AddTorrent(ctx, importReq)
|
err = _store.AddTorrent(ctx, importReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -29,7 +29,7 @@ func (q *QBit) addMagnet(ctx context.Context, url string, arr *arr.Arr, debrid s
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *QBit) addTorrent(ctx context.Context, fileHeader *multipart.FileHeader, arr *arr.Arr, debrid string, isSymlink bool) error {
|
func (q *QBit) addTorrent(ctx context.Context, fileHeader *multipart.FileHeader, arr *arr.Arr, debrid string, action string) error {
|
||||||
file, _ := fileHeader.Open()
|
file, _ := fileHeader.Open()
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
var reader io.Reader = file
|
var reader io.Reader = file
|
||||||
@@ -38,7 +38,7 @@ func (q *QBit) addTorrent(ctx context.Context, fileHeader *multipart.FileHeader,
|
|||||||
return fmt.Errorf("error reading file: %s \n %w", fileHeader.Filename, err)
|
return fmt.Errorf("error reading file: %s \n %w", fileHeader.Filename, err)
|
||||||
}
|
}
|
||||||
_store := store.Get()
|
_store := store.Get()
|
||||||
importReq := store.NewImportRequest(debrid, q.DownloadFolder, magnet, arr, isSymlink, false, "", store.ImportTypeQBitTorrent)
|
importReq := store.NewImportRequest(debrid, q.DownloadFolder, magnet, arr, action, false, "", store.ImportTypeQBitTorrent)
|
||||||
err = _store.AddTorrent(ctx, importReq)
|
err = _store.AddTorrent(ctx, importReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to process torrent: %w", err)
|
return fmt.Errorf("failed to process torrent: %w", err)
|
||||||
@@ -83,20 +83,6 @@ func (q *QBit) GetTorrentProperties(t *store.Torrent) *TorrentProperties {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *QBit) getTorrentFiles(t *store.Torrent) []*TorrentFile {
|
|
||||||
files := make([]*TorrentFile, 0)
|
|
||||||
if t.DebridTorrent == nil {
|
|
||||||
return files
|
|
||||||
}
|
|
||||||
for _, file := range t.DebridTorrent.GetFiles() {
|
|
||||||
files = append(files, &TorrentFile{
|
|
||||||
Name: file.Path,
|
|
||||||
Size: file.Size,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return files
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *QBit) setTorrentTags(t *store.Torrent, tags []string) bool {
|
func (q *QBit) setTorrentTags(t *store.Torrent, tags []string) bool {
|
||||||
torrentTags := strings.Split(t.Tags, ",")
|
torrentTags := strings.Split(t.Tags, ",")
|
||||||
for _, tag := range tags {
|
for _, tag := range tags {
|
||||||
|
|||||||
@@ -202,17 +202,6 @@ type TorrentProperties struct {
|
|||||||
UpSpeedAvg int `json:"up_speed_avg,omitempty"`
|
UpSpeedAvg int `json:"up_speed_avg,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TorrentFile struct {
|
|
||||||
Index int `json:"index,omitempty"`
|
|
||||||
Name string `json:"name,omitempty"`
|
|
||||||
Size int64 `json:"size,omitempty"`
|
|
||||||
Progress int `json:"progress,omitempty"`
|
|
||||||
Priority int `json:"priority,omitempty"`
|
|
||||||
IsSeed bool `json:"is_seed,omitempty"`
|
|
||||||
PieceRange []int `json:"piece_range,omitempty"`
|
|
||||||
Availability float64 `json:"availability,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func getAppPreferences() *AppPreferences {
|
func getAppPreferences() *AppPreferences {
|
||||||
preferences := &AppPreferences{
|
preferences := &AppPreferences{
|
||||||
AddTrackers: "",
|
AddTrackers: "",
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ type Repair struct {
|
|||||||
arrs *arr.Storage
|
arrs *arr.Storage
|
||||||
deb *debrid.Storage
|
deb *debrid.Storage
|
||||||
interval string
|
interval string
|
||||||
runOnStart bool
|
|
||||||
ZurgURL string
|
ZurgURL string
|
||||||
IsZurg bool
|
IsZurg bool
|
||||||
useWebdav bool
|
useWebdav bool
|
||||||
@@ -86,7 +85,6 @@ func New(arrs *arr.Storage, engine *debrid.Storage) *Repair {
|
|||||||
arrs: arrs,
|
arrs: arrs,
|
||||||
logger: logger.New("repair"),
|
logger: logger.New("repair"),
|
||||||
interval: cfg.Repair.Interval,
|
interval: cfg.Repair.Interval,
|
||||||
runOnStart: cfg.Repair.RunOnStart,
|
|
||||||
ZurgURL: cfg.Repair.ZurgURL,
|
ZurgURL: cfg.Repair.ZurgURL,
|
||||||
useWebdav: cfg.Repair.UseWebDav,
|
useWebdav: cfg.Repair.UseWebDav,
|
||||||
autoProcess: cfg.Repair.AutoProcess,
|
autoProcess: cfg.Repair.AutoProcess,
|
||||||
@@ -121,15 +119,6 @@ func (r *Repair) Reset() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repair) Start(ctx context.Context) error {
|
func (r *Repair) Start(ctx context.Context) error {
|
||||||
//r.ctx = ctx
|
|
||||||
if r.runOnStart {
|
|
||||||
r.logger.Info().Msgf("Running initial repair")
|
|
||||||
go func() {
|
|
||||||
if err := r.AddJob([]string{}, []string{}, r.autoProcess, true); err != nil {
|
|
||||||
r.logger.Error().Err(err).Msg("Error running initial repair")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
r.scheduler, _ = gocron.NewScheduler(gocron.WithLocation(time.Local))
|
r.scheduler, _ = gocron.NewScheduler(gocron.WithLocation(time.Local))
|
||||||
|
|
||||||
|
|||||||
@@ -56,8 +56,7 @@ Loop:
|
|||||||
return resp.Err()
|
return resp.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) processDownload(torrent *Torrent) (string, error) {
|
func (s *Store) processDownload(torrent *Torrent, debridTorrent *types.Torrent) (string, error) {
|
||||||
debridTorrent := torrent.DebridTorrent
|
|
||||||
s.logger.Info().Msgf("Downloading %d files...", len(debridTorrent.Files))
|
s.logger.Info().Msgf("Downloading %d files...", len(debridTorrent.Files))
|
||||||
torrentPath := filepath.Join(torrent.SavePath, utils.RemoveExtension(debridTorrent.OriginalFilename))
|
torrentPath := filepath.Join(torrent.SavePath, utils.RemoveExtension(debridTorrent.OriginalFilename))
|
||||||
torrentPath = utils.RemoveInvalidChars(torrentPath)
|
torrentPath = utils.RemoveInvalidChars(torrentPath)
|
||||||
@@ -66,12 +65,11 @@ func (s *Store) processDownload(torrent *Torrent) (string, error) {
|
|||||||
// add the previous error to the error and return
|
// add the previous error to the error and return
|
||||||
return "", fmt.Errorf("failed to create directory: %s: %v", torrentPath, err)
|
return "", fmt.Errorf("failed to create directory: %s: %v", torrentPath, err)
|
||||||
}
|
}
|
||||||
s.downloadFiles(torrent, torrentPath)
|
s.downloadFiles(torrent, debridTorrent, torrentPath)
|
||||||
return torrentPath, nil
|
return torrentPath, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) downloadFiles(torrent *Torrent, parent string) {
|
func (s *Store) downloadFiles(torrent *Torrent, debridTorrent *types.Torrent, parent string) {
|
||||||
debridTorrent := torrent.DebridTorrent
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
totalSize := int64(0)
|
totalSize := int64(0)
|
||||||
@@ -151,8 +149,7 @@ func (s *Store) downloadFiles(torrent *Torrent, parent string) {
|
|||||||
s.logger.Info().Msgf("Downloaded all files for %s", debridTorrent.Name)
|
s.logger.Info().Msgf("Downloaded all files for %s", debridTorrent.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) processSymlink(torrent *Torrent) (string, error) {
|
func (s *Store) processSymlink(torrent *Torrent, debridTorrent *types.Torrent) (string, error) {
|
||||||
debridTorrent := torrent.DebridTorrent
|
|
||||||
files := debridTorrent.Files
|
files := debridTorrent.Files
|
||||||
if len(files) == 0 {
|
if len(files) == 0 {
|
||||||
return "", fmt.Errorf("no video files found")
|
return "", fmt.Errorf("no video files found")
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@ func createTorrentFromMagnet(req *ImportRequest) *Torrent {
|
|||||||
magnet := req.Magnet
|
magnet := req.Magnet
|
||||||
arrName := req.Arr.Name
|
arrName := req.Arr.Name
|
||||||
torrent := &Torrent{
|
torrent := &Torrent{
|
||||||
ID: "",
|
ID: req.Id,
|
||||||
Hash: strings.ToLower(magnet.InfoHash),
|
Hash: strings.ToLower(magnet.InfoHash),
|
||||||
Name: magnet.Name,
|
Name: magnet.Name,
|
||||||
Size: magnet.Size,
|
Size: magnet.Size,
|
||||||
|
|||||||
+99
-52
@@ -2,9 +2,11 @@ package store
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"cmp"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/sirrobot01/decypharr/internal/request"
|
"github.com/sirrobot01/decypharr/internal/request"
|
||||||
"github.com/sirrobot01/decypharr/internal/utils"
|
"github.com/sirrobot01/decypharr/internal/utils"
|
||||||
"github.com/sirrobot01/decypharr/pkg/arr"
|
"github.com/sirrobot01/decypharr/pkg/arr"
|
||||||
@@ -23,11 +25,12 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ImportRequest struct {
|
type ImportRequest struct {
|
||||||
|
Id string `json:"id"`
|
||||||
DownloadFolder string `json:"downloadFolder"`
|
DownloadFolder string `json:"downloadFolder"`
|
||||||
SelectedDebrid string `json:"debrid"`
|
SelectedDebrid string `json:"debrid"`
|
||||||
Magnet *utils.Magnet `json:"magnet"`
|
Magnet *utils.Magnet `json:"magnet"`
|
||||||
Arr *arr.Arr `json:"arr"`
|
Arr *arr.Arr `json:"arr"`
|
||||||
IsSymlink bool `json:"isSymlink"`
|
Action string `json:"action"`
|
||||||
DownloadUncached bool `json:"downloadUncached"`
|
DownloadUncached bool `json:"downloadUncached"`
|
||||||
CallBackUrl string `json:"callBackUrl"`
|
CallBackUrl string `json:"callBackUrl"`
|
||||||
|
|
||||||
@@ -39,14 +42,15 @@ type ImportRequest struct {
|
|||||||
Async bool `json:"async"`
|
Async bool `json:"async"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewImportRequest(debrid string, downloadFolder string, magnet *utils.Magnet, arr *arr.Arr, isSymlink, downloadUncached bool, callBackUrl string, importType ImportType) *ImportRequest {
|
func NewImportRequest(debrid string, downloadFolder string, magnet *utils.Magnet, arr *arr.Arr, action string, downloadUncached bool, callBackUrl string, importType ImportType) *ImportRequest {
|
||||||
return &ImportRequest{
|
return &ImportRequest{
|
||||||
|
Id: uuid.New().String(),
|
||||||
Status: "started",
|
Status: "started",
|
||||||
DownloadFolder: downloadFolder,
|
DownloadFolder: downloadFolder,
|
||||||
SelectedDebrid: debrid,
|
SelectedDebrid: cmp.Or(arr.SelectedDebrid, debrid), // Use debrid from arr if available
|
||||||
Magnet: magnet,
|
Magnet: magnet,
|
||||||
Arr: arr,
|
Arr: arr,
|
||||||
IsSymlink: isSymlink,
|
Action: action,
|
||||||
DownloadUncached: downloadUncached,
|
DownloadUncached: downloadUncached,
|
||||||
CallBackUrl: callBackUrl,
|
CallBackUrl: callBackUrl,
|
||||||
Type: importType,
|
Type: importType,
|
||||||
@@ -106,21 +110,22 @@ func (i *ImportRequest) markAsCompleted(torrent *Torrent, debridTorrent *debridT
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ImportQueue struct {
|
type ImportQueue struct {
|
||||||
queue map[string]chan *ImportRequest // Map to hold queues for different debrid services
|
queue []*ImportRequest
|
||||||
mu sync.RWMutex // Mutex to protect access to the queue map
|
mu sync.RWMutex
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
capacity int // Capacity of each channel in the queue
|
cond *sync.Cond // For blocking operations
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewImportQueue(ctx context.Context, capacity int) *ImportQueue {
|
func NewImportQueue(ctx context.Context, capacity int) *ImportQueue {
|
||||||
ctx, cancel := context.WithCancel(ctx)
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
return &ImportQueue{
|
iq := &ImportQueue{
|
||||||
queue: make(map[string]chan *ImportRequest),
|
queue: make([]*ImportRequest, 0, capacity),
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
capacity: capacity,
|
|
||||||
}
|
}
|
||||||
|
iq.cond = sync.NewCond(&iq.mu)
|
||||||
|
return iq
|
||||||
}
|
}
|
||||||
|
|
||||||
func (iq *ImportQueue) Push(req *ImportRequest) error {
|
func (iq *ImportQueue) Push(req *ImportRequest) error {
|
||||||
@@ -131,62 +136,104 @@ func (iq *ImportQueue) Push(req *ImportRequest) error {
|
|||||||
iq.mu.Lock()
|
iq.mu.Lock()
|
||||||
defer iq.mu.Unlock()
|
defer iq.mu.Unlock()
|
||||||
|
|
||||||
if _, exists := iq.queue[req.SelectedDebrid]; !exists {
|
select {
|
||||||
iq.queue[req.SelectedDebrid] = make(chan *ImportRequest, iq.capacity) // Create a new channel for the debrid service
|
case <-iq.ctx.Done():
|
||||||
|
return fmt.Errorf("queue is shutting down")
|
||||||
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(iq.queue) >= cap(iq.queue) {
|
||||||
|
return fmt.Errorf("queue is full")
|
||||||
|
}
|
||||||
|
|
||||||
|
iq.queue = append(iq.queue, req)
|
||||||
|
iq.cond.Signal() // Wake up any waiting Pop()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (iq *ImportQueue) Pop() (*ImportRequest, error) {
|
||||||
|
iq.mu.Lock()
|
||||||
|
defer iq.mu.Unlock()
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case iq.queue[req.SelectedDebrid] <- req:
|
|
||||||
return nil
|
|
||||||
case <-iq.ctx.Done():
|
case <-iq.ctx.Done():
|
||||||
return fmt.Errorf("retry queue is shutting down")
|
return nil, fmt.Errorf("queue is shutting down")
|
||||||
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(iq.queue) == 0 {
|
||||||
|
return nil, fmt.Errorf("no import requests available")
|
||||||
|
}
|
||||||
|
|
||||||
|
req := iq.queue[0]
|
||||||
|
iq.queue = iq.queue[1:]
|
||||||
|
return req, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (iq *ImportQueue) TryPop(selectedDebrid string) (*ImportRequest, error) {
|
// Delete specific request by ID
|
||||||
iq.mu.RLock()
|
func (iq *ImportQueue) Delete(requestID string) bool {
|
||||||
defer iq.mu.RUnlock()
|
iq.mu.Lock()
|
||||||
|
defer iq.mu.Unlock()
|
||||||
|
|
||||||
if ch, exists := iq.queue[selectedDebrid]; exists {
|
for i, req := range iq.queue {
|
||||||
select {
|
if req.Id == requestID {
|
||||||
case req := <-ch:
|
// Remove from slice
|
||||||
return req, nil
|
iq.queue = append(iq.queue[:i], iq.queue[i+1:]...)
|
||||||
case <-iq.ctx.Done():
|
return true
|
||||||
return nil, fmt.Errorf("queue is shutting down")
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("no import request available for %s", selectedDebrid)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("no queue exists for %s", selectedDebrid)
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (iq *ImportQueue) Size(selectedDebrid string) int {
|
// DeleteWhere requests matching a condition
|
||||||
|
func (iq *ImportQueue) DeleteWhere(predicate func(*ImportRequest) bool) int {
|
||||||
|
iq.mu.Lock()
|
||||||
|
defer iq.mu.Unlock()
|
||||||
|
|
||||||
|
deleted := 0
|
||||||
|
for i := len(iq.queue) - 1; i >= 0; i-- {
|
||||||
|
if predicate(iq.queue[i]) {
|
||||||
|
iq.queue = append(iq.queue[:i], iq.queue[i+1:]...)
|
||||||
|
deleted++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return deleted
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find request without removing it
|
||||||
|
func (iq *ImportQueue) Find(requestID string) *ImportRequest {
|
||||||
iq.mu.RLock()
|
iq.mu.RLock()
|
||||||
defer iq.mu.RUnlock()
|
defer iq.mu.RUnlock()
|
||||||
|
|
||||||
if ch, exists := iq.queue[selectedDebrid]; exists {
|
for _, req := range iq.queue {
|
||||||
return len(ch)
|
if req.Id == requestID {
|
||||||
|
return req
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return 0
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (iq *ImportQueue) Size() int {
|
||||||
|
iq.mu.RLock()
|
||||||
|
defer iq.mu.RUnlock()
|
||||||
|
return len(iq.queue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (iq *ImportQueue) IsEmpty() bool {
|
||||||
|
return iq.Size() == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all requests (copy to avoid race conditions)
|
||||||
|
func (iq *ImportQueue) List() []*ImportRequest {
|
||||||
|
iq.mu.RLock()
|
||||||
|
defer iq.mu.RUnlock()
|
||||||
|
|
||||||
|
result := make([]*ImportRequest, len(iq.queue))
|
||||||
|
copy(result, iq.queue)
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func (iq *ImportQueue) Close() {
|
func (iq *ImportQueue) Close() {
|
||||||
iq.cancel()
|
iq.cancel()
|
||||||
iq.mu.Lock()
|
iq.cond.Broadcast()
|
||||||
defer iq.mu.Unlock()
|
|
||||||
|
|
||||||
for _, ch := range iq.queue {
|
|
||||||
// Drain remaining items before closing
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ch:
|
|
||||||
// Discard remaining items
|
|
||||||
default:
|
|
||||||
close(ch)
|
|
||||||
goto nextChannel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
nextChannel:
|
|
||||||
}
|
|
||||||
iq.queue = make(map[string]chan *ImportRequest)
|
|
||||||
}
|
}
|
||||||
|
|||||||
+91
-40
@@ -16,7 +16,7 @@ import (
|
|||||||
|
|
||||||
func (s *Store) AddTorrent(ctx context.Context, importReq *ImportRequest) error {
|
func (s *Store) AddTorrent(ctx context.Context, importReq *ImportRequest) error {
|
||||||
torrent := createTorrentFromMagnet(importReq)
|
torrent := createTorrentFromMagnet(importReq)
|
||||||
debridTorrent, err := debridTypes.Process(ctx, s.debrid, importReq.SelectedDebrid, importReq.Magnet, importReq.Arr, importReq.IsSymlink, importReq.DownloadUncached)
|
debridTorrent, err := debridTypes.Process(ctx, s.debrid, importReq.SelectedDebrid, importReq.Magnet, importReq.Arr, importReq.Action, importReq.DownloadUncached)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var httpErr *utils.HTTPError
|
var httpErr *utils.HTTPError
|
||||||
@@ -25,8 +25,8 @@ func (s *Store) AddTorrent(ctx context.Context, importReq *ImportRequest) error
|
|||||||
case "too_many_active_downloads":
|
case "too_many_active_downloads":
|
||||||
// Handle too much active downloads error
|
// Handle too much active downloads error
|
||||||
s.logger.Warn().Msgf("Too many active downloads for %s, adding to queue", importReq.Magnet.Name)
|
s.logger.Warn().Msgf("Too many active downloads for %s, adding to queue", importReq.Magnet.Name)
|
||||||
err := s.addToQueue(importReq)
|
|
||||||
if err != nil {
|
if err := s.addToQueue(importReq); err != nil {
|
||||||
s.logger.Error().Err(err).Msgf("Failed to add %s to queue", importReq.Magnet.Name)
|
s.logger.Error().Err(err).Msgf("Failed to add %s to queue", importReq.Magnet.Name)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -65,9 +65,9 @@ func (s *Store) addToQueue(importReq *ImportRequest) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) processFromQueue(ctx context.Context, selectedDebrid string) error {
|
func (s *Store) processFromQueue(ctx context.Context) error {
|
||||||
// Pop the next import request from the queue
|
// Pop the next import request from the queue
|
||||||
importReq, err := s.importsQueue.TryPop(selectedDebrid)
|
importReq, err := s.importsQueue.Pop()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -105,10 +105,13 @@ func (s *Store) trackAvailableSlots(ctx context.Context) {
|
|||||||
availableSlots[name] = slots
|
availableSlots[name] = slots
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.importsQueue.Size() <= 0 {
|
||||||
|
// Queue is empty, no need to process
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
for name, slots := range availableSlots {
|
for name, slots := range availableSlots {
|
||||||
if s.importsQueue.Size(name) <= 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
s.logger.Debug().Msgf("Available slots for %s: %d", name, slots)
|
s.logger.Debug().Msgf("Available slots for %s: %d", name, slots)
|
||||||
// If slots are available, process the next import request from the queue
|
// If slots are available, process the next import request from the queue
|
||||||
for slots > 0 {
|
for slots > 0 {
|
||||||
@@ -116,7 +119,7 @@ func (s *Store) trackAvailableSlots(ctx context.Context) {
|
|||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return // Exit if context is done
|
return // Exit if context is done
|
||||||
default:
|
default:
|
||||||
if err := s.processFromQueue(ctx, name); err != nil {
|
if err := s.processFromQueue(ctx); err != nil {
|
||||||
s.logger.Error().Err(err).Msg("Error processing from queue")
|
s.logger.Error().Err(err).Msg("Error processing from queue")
|
||||||
return // Exit on error
|
return // Exit on error
|
||||||
}
|
}
|
||||||
@@ -139,7 +142,7 @@ func (s *Store) processFiles(torrent *Torrent, debridTorrent *types.Torrent, imp
|
|||||||
_arr := importReq.Arr
|
_arr := importReq.Arr
|
||||||
for debridTorrent.Status != "downloaded" {
|
for debridTorrent.Status != "downloaded" {
|
||||||
s.logger.Debug().Msgf("%s <- (%s) Download Progress: %.2f%%", debridTorrent.Debrid, debridTorrent.Name, debridTorrent.Progress)
|
s.logger.Debug().Msgf("%s <- (%s) Download Progress: %.2f%%", debridTorrent.Debrid, debridTorrent.Name, debridTorrent.Progress)
|
||||||
dbT, err := client.CheckStatus(debridTorrent, importReq.IsSymlink)
|
dbT, err := client.CheckStatus(debridTorrent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if dbT != nil && dbT.Id != "" {
|
if dbT != nil && dbT.Id != "" {
|
||||||
// Delete the torrent if it was not downloaded
|
// Delete the torrent if it was not downloaded
|
||||||
@@ -174,17 +177,43 @@ func (s *Store) processFiles(torrent *Torrent, debridTorrent *types.Torrent, imp
|
|||||||
|
|
||||||
// Check if debrid supports webdav by checking cache
|
// Check if debrid supports webdav by checking cache
|
||||||
timer := time.Now()
|
timer := time.Now()
|
||||||
if importReq.IsSymlink {
|
|
||||||
|
onFailed := func(err error) {
|
||||||
|
if err != nil {
|
||||||
|
s.markTorrentAsFailed(torrent)
|
||||||
|
go func() {
|
||||||
|
_ = client.DeleteTorrent(debridTorrent.Id)
|
||||||
|
}()
|
||||||
|
s.logger.Error().Err(err).Msgf("Error occured while processing torrent %s", debridTorrent.Name)
|
||||||
|
importReq.markAsFailed(err, torrent, debridTorrent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSuccess := func(torrentSymlinkPath string) {
|
||||||
|
torrent.TorrentPath = torrentSymlinkPath
|
||||||
|
s.updateTorrent(torrent, debridTorrent)
|
||||||
|
s.logger.Info().Msgf("Adding %s took %s", debridTorrent.Name, time.Since(timer))
|
||||||
|
|
||||||
|
go importReq.markAsCompleted(torrent, debridTorrent) // Mark the import request as completed, send callback if needed
|
||||||
|
go func() {
|
||||||
|
if err := request.SendDiscordMessage("download_complete", "success", torrent.discordContext()); err != nil {
|
||||||
|
s.logger.Error().Msgf("Error sending discord message: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
_arr.Refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
switch importReq.Action {
|
||||||
|
case "symlink":
|
||||||
|
// Symlink action, we will create a symlink to the torrent
|
||||||
|
s.logger.Debug().Msgf("Post-Download Action: Symlink")
|
||||||
cache := deb.Cache()
|
cache := deb.Cache()
|
||||||
if cache != nil {
|
if cache != nil {
|
||||||
s.logger.Info().Msgf("Using internal webdav for %s", debridTorrent.Debrid)
|
s.logger.Info().Msgf("Using internal webdav for %s", debridTorrent.Debrid)
|
||||||
|
|
||||||
// Use webdav to download the file
|
// Use webdav to download the file
|
||||||
|
|
||||||
if err := cache.Add(debridTorrent); err != nil {
|
if err := cache.Add(debridTorrent); err != nil {
|
||||||
s.logger.Error().Msgf("Error adding torrent to cache: %v", err)
|
onFailed(err)
|
||||||
s.markTorrentAsFailed(torrent)
|
|
||||||
importReq.markAsFailed(err, torrent, debridTorrent)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,31 +223,45 @@ func (s *Store) processFiles(torrent *Torrent, debridTorrent *types.Torrent, imp
|
|||||||
|
|
||||||
} else {
|
} else {
|
||||||
// User is using either zurg or debrid webdav
|
// User is using either zurg or debrid webdav
|
||||||
torrentSymlinkPath, err = s.processSymlink(torrent) // /mnt/symlinks/{category}/MyTVShow/
|
torrentSymlinkPath, err = s.processSymlink(torrent, debridTorrent) // /mnt/symlinks/{category}/MyTVShow/
|
||||||
}
|
}
|
||||||
} else {
|
if err != nil {
|
||||||
torrentSymlinkPath, err = s.processDownload(torrent)
|
onFailed(err)
|
||||||
}
|
return
|
||||||
if err != nil {
|
}
|
||||||
s.markTorrentAsFailed(torrent)
|
if torrentSymlinkPath == "" {
|
||||||
go func() {
|
err = fmt.Errorf("symlink path is empty for %s", debridTorrent.Name)
|
||||||
_ = client.DeleteTorrent(debridTorrent.Id)
|
onFailed(err)
|
||||||
}()
|
}
|
||||||
s.logger.Error().Err(err).Msgf("Error occured while processing torrent %s", debridTorrent.Name)
|
onSuccess(torrentSymlinkPath)
|
||||||
importReq.markAsFailed(err, torrent, debridTorrent)
|
|
||||||
return
|
return
|
||||||
}
|
case "download":
|
||||||
torrent.TorrentPath = torrentSymlinkPath
|
// Download action, we will download the torrent to the specified folder
|
||||||
s.updateTorrent(torrent, debridTorrent)
|
// Generate download links
|
||||||
s.logger.Info().Msgf("Adding %s took %s", debridTorrent.Name, time.Since(timer))
|
s.logger.Debug().Msgf("Post-Download Action: Download")
|
||||||
|
if err := client.GetFileDownloadLinks(debridTorrent); err != nil {
|
||||||
go importReq.markAsCompleted(torrent, debridTorrent) // Mark the import request as completed, send callback if needed
|
onFailed(err)
|
||||||
go func() {
|
return
|
||||||
if err := request.SendDiscordMessage("download_complete", "success", torrent.discordContext()); err != nil {
|
|
||||||
s.logger.Error().Msgf("Error sending discord message: %v", err)
|
|
||||||
}
|
}
|
||||||
}()
|
s.logger.Debug().Msgf("Download Post-Download Action")
|
||||||
_arr.Refresh()
|
torrentSymlinkPath, err = s.processDownload(torrent, debridTorrent)
|
||||||
|
if err != nil {
|
||||||
|
onFailed(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if torrentSymlinkPath == "" {
|
||||||
|
err = fmt.Errorf("download path is empty for %s", debridTorrent.Name)
|
||||||
|
onFailed(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
onSuccess(torrentSymlinkPath)
|
||||||
|
case "none":
|
||||||
|
s.logger.Debug().Msgf("Post-Download Action: None")
|
||||||
|
// No action, just update the torrent and mark it as completed
|
||||||
|
onSuccess(torrent.TorrentPath)
|
||||||
|
default:
|
||||||
|
// Action is none, do nothing, fallthrough
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) markTorrentAsFailed(t *Torrent) *Torrent {
|
func (s *Store) markTorrentAsFailed(t *Torrent) *Torrent {
|
||||||
@@ -253,10 +296,18 @@ func (s *Store) partialTorrentUpdate(t *Torrent, debridTorrent *types.Torrent) *
|
|||||||
if speed != 0 {
|
if speed != 0 {
|
||||||
eta = int((totalSize - sizeCompleted) / speed)
|
eta = int((totalSize - sizeCompleted) / speed)
|
||||||
}
|
}
|
||||||
t.ID = debridTorrent.Id
|
files := make([]*File, 0, len(debridTorrent.Files))
|
||||||
|
for index, file := range debridTorrent.GetFiles() {
|
||||||
|
files = append(files, &File{
|
||||||
|
Index: index,
|
||||||
|
Name: file.Path,
|
||||||
|
Size: file.Size,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
t.DebridID = debridTorrent.Id
|
||||||
t.Name = debridTorrent.Name
|
t.Name = debridTorrent.Name
|
||||||
t.AddedOn = addedOn.Unix()
|
t.AddedOn = addedOn.Unix()
|
||||||
t.DebridTorrent = debridTorrent
|
t.Files = files
|
||||||
t.Debrid = debridTorrent.Debrid
|
t.Debrid = debridTorrent.Debrid
|
||||||
t.Size = totalSize
|
t.Size = totalSize
|
||||||
t.Completed = sizeCompleted
|
t.Completed = sizeCompleted
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package store
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/sirrobot01/decypharr/pkg/debrid/types"
|
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -183,10 +182,18 @@ func (ts *TorrentStorage) Delete(hash, category string, removeFromDebrid bool) {
|
|||||||
if torrent == nil {
|
if torrent == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if removeFromDebrid && torrent.ID != "" && torrent.Debrid != "" {
|
st := Get()
|
||||||
dbClient := Get().debrid.Client(torrent.Debrid)
|
// Check if torrent is queued for download
|
||||||
|
|
||||||
|
if torrent.State == "queued" && torrent.ID != "" {
|
||||||
|
// Remove the torrent from the import queue if it exists
|
||||||
|
st.importsQueue.Delete(torrent.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if removeFromDebrid && torrent.DebridID != "" && torrent.Debrid != "" {
|
||||||
|
dbClient := st.debrid.Client(torrent.Debrid)
|
||||||
if dbClient != nil {
|
if dbClient != nil {
|
||||||
_ = dbClient.DeleteTorrent(torrent.ID)
|
_ = dbClient.DeleteTorrent(torrent.DebridID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,14 +219,21 @@ func (ts *TorrentStorage) DeleteMultiple(hashes []string, removeFromDebrid bool)
|
|||||||
defer ts.mu.Unlock()
|
defer ts.mu.Unlock()
|
||||||
toDelete := make(map[string]string)
|
toDelete := make(map[string]string)
|
||||||
|
|
||||||
|
st := Get()
|
||||||
|
|
||||||
for _, hash := range hashes {
|
for _, hash := range hashes {
|
||||||
for key, torrent := range ts.torrents {
|
for key, torrent := range ts.torrents {
|
||||||
if torrent == nil {
|
if torrent == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if torrent.State == "queued" && torrent.ID != "" {
|
||||||
|
// Remove the torrent from the import queue if it exists
|
||||||
|
st.importsQueue.Delete(torrent.ID)
|
||||||
|
}
|
||||||
if torrent.Hash == hash {
|
if torrent.Hash == hash {
|
||||||
if removeFromDebrid && torrent.ID != "" && torrent.Debrid != "" {
|
if removeFromDebrid && torrent.DebridID != "" && torrent.Debrid != "" {
|
||||||
toDelete[torrent.ID] = torrent.Debrid
|
toDelete[torrent.DebridID] = torrent.Debrid
|
||||||
}
|
}
|
||||||
delete(ts.torrents, key)
|
delete(ts.torrents, key)
|
||||||
if torrent.ContentPath != "" {
|
if torrent.ContentPath != "" {
|
||||||
@@ -238,7 +252,7 @@ func (ts *TorrentStorage) DeleteMultiple(hashes []string, removeFromDebrid bool)
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
clients := Get().debrid.Clients()
|
clients := st.debrid.Clients()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for id, debrid := range toDelete {
|
for id, debrid := range toDelete {
|
||||||
@@ -274,73 +288,3 @@ func (ts *TorrentStorage) Reset() {
|
|||||||
defer ts.mu.Unlock()
|
defer ts.mu.Unlock()
|
||||||
ts.torrents = make(Torrents)
|
ts.torrents = make(Torrents)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Torrent struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Debrid string `json:"debrid"`
|
|
||||||
TorrentPath string `json:"-"`
|
|
||||||
DebridTorrent *types.Torrent `json:"-"`
|
|
||||||
|
|
||||||
AddedOn int64 `json:"added_on,omitempty"`
|
|
||||||
AmountLeft int64 `json:"amount_left"`
|
|
||||||
AutoTmm bool `json:"auto_tmm"`
|
|
||||||
Availability float64 `json:"availability,omitempty"`
|
|
||||||
Category string `json:"category,omitempty"`
|
|
||||||
Completed int64 `json:"completed"`
|
|
||||||
CompletionOn int `json:"completion_on,omitempty"`
|
|
||||||
ContentPath string `json:"content_path"`
|
|
||||||
DlLimit int `json:"dl_limit"`
|
|
||||||
Dlspeed int64 `json:"dlspeed"`
|
|
||||||
Downloaded int64 `json:"downloaded"`
|
|
||||||
DownloadedSession int64 `json:"downloaded_session"`
|
|
||||||
Eta int `json:"eta"`
|
|
||||||
FlPiecePrio bool `json:"f_l_piece_prio,omitempty"`
|
|
||||||
ForceStart bool `json:"force_start,omitempty"`
|
|
||||||
Hash string `json:"hash"`
|
|
||||||
LastActivity int64 `json:"last_activity,omitempty"`
|
|
||||||
MagnetUri string `json:"magnet_uri,omitempty"`
|
|
||||||
MaxRatio int `json:"max_ratio,omitempty"`
|
|
||||||
MaxSeedingTime int `json:"max_seeding_time,omitempty"`
|
|
||||||
Name string `json:"name,omitempty"`
|
|
||||||
NumComplete int `json:"num_complete,omitempty"`
|
|
||||||
NumIncomplete int `json:"num_incomplete,omitempty"`
|
|
||||||
NumLeechs int `json:"num_leechs,omitempty"`
|
|
||||||
NumSeeds int `json:"num_seeds,omitempty"`
|
|
||||||
Priority int `json:"priority,omitempty"`
|
|
||||||
Progress float64 `json:"progress"`
|
|
||||||
Ratio int `json:"ratio,omitempty"`
|
|
||||||
RatioLimit int `json:"ratio_limit,omitempty"`
|
|
||||||
SavePath string `json:"save_path"`
|
|
||||||
SeedingTimeLimit int `json:"seeding_time_limit,omitempty"`
|
|
||||||
SeenComplete int64 `json:"seen_complete,omitempty"`
|
|
||||||
SeqDl bool `json:"seq_dl"`
|
|
||||||
Size int64 `json:"size,omitempty"`
|
|
||||||
State string `json:"state,omitempty"`
|
|
||||||
SuperSeeding bool `json:"super_seeding"`
|
|
||||||
Tags string `json:"tags,omitempty"`
|
|
||||||
TimeActive int `json:"time_active,omitempty"`
|
|
||||||
TotalSize int64 `json:"total_size,omitempty"`
|
|
||||||
Tracker string `json:"tracker,omitempty"`
|
|
||||||
UpLimit int64 `json:"up_limit,omitempty"`
|
|
||||||
Uploaded int64 `json:"uploaded,omitempty"`
|
|
||||||
UploadedSession int64 `json:"uploaded_session,omitempty"`
|
|
||||||
Upspeed int64 `json:"upspeed,omitempty"`
|
|
||||||
Source string `json:"source,omitempty"`
|
|
||||||
|
|
||||||
sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Torrent) IsReady() bool {
|
|
||||||
return (t.AmountLeft <= 0 || t.Progress == 1) && t.TorrentPath != ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Torrent) discordContext() string {
|
|
||||||
format := `
|
|
||||||
**Name:** %s
|
|
||||||
**Arr:** %s
|
|
||||||
**Hash:** %s
|
|
||||||
**MagnetURI:** %s
|
|
||||||
**Debrid:** %s
|
|
||||||
`
|
|
||||||
return fmt.Sprintf(format, t.Name, t.Category, t.Hash, t.MagnetUri, t.Debrid)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type File struct {
|
||||||
|
Index int `json:"index,omitempty"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Size int64 `json:"size,omitempty"`
|
||||||
|
Progress int `json:"progress,omitempty"`
|
||||||
|
Priority int `json:"priority,omitempty"`
|
||||||
|
IsSeed bool `json:"is_seed,omitempty"`
|
||||||
|
PieceRange []int `json:"piece_range,omitempty"`
|
||||||
|
Availability float64 `json:"availability,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Torrent struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
DebridID string `json:"debrid_id"`
|
||||||
|
Debrid string `json:"debrid"`
|
||||||
|
TorrentPath string `json:"-"`
|
||||||
|
Files []*File `json:"files,omitempty"`
|
||||||
|
|
||||||
|
AddedOn int64 `json:"added_on,omitempty"`
|
||||||
|
AmountLeft int64 `json:"amount_left"`
|
||||||
|
AutoTmm bool `json:"auto_tmm"`
|
||||||
|
Availability float64 `json:"availability,omitempty"`
|
||||||
|
Category string `json:"category,omitempty"`
|
||||||
|
Completed int64 `json:"completed"`
|
||||||
|
CompletionOn int `json:"completion_on,omitempty"`
|
||||||
|
ContentPath string `json:"content_path"`
|
||||||
|
DlLimit int `json:"dl_limit"`
|
||||||
|
Dlspeed int64 `json:"dlspeed"`
|
||||||
|
Downloaded int64 `json:"downloaded"`
|
||||||
|
DownloadedSession int64 `json:"downloaded_session"`
|
||||||
|
Eta int `json:"eta"`
|
||||||
|
FlPiecePrio bool `json:"f_l_piece_prio,omitempty"`
|
||||||
|
ForceStart bool `json:"force_start,omitempty"`
|
||||||
|
Hash string `json:"hash"`
|
||||||
|
LastActivity int64 `json:"last_activity,omitempty"`
|
||||||
|
MagnetUri string `json:"magnet_uri,omitempty"`
|
||||||
|
MaxRatio int `json:"max_ratio,omitempty"`
|
||||||
|
MaxSeedingTime int `json:"max_seeding_time,omitempty"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
NumComplete int `json:"num_complete,omitempty"`
|
||||||
|
NumIncomplete int `json:"num_incomplete,omitempty"`
|
||||||
|
NumLeechs int `json:"num_leechs,omitempty"`
|
||||||
|
NumSeeds int `json:"num_seeds,omitempty"`
|
||||||
|
Priority int `json:"priority,omitempty"`
|
||||||
|
Progress float64 `json:"progress"`
|
||||||
|
Ratio int `json:"ratio,omitempty"`
|
||||||
|
RatioLimit int `json:"ratio_limit,omitempty"`
|
||||||
|
SavePath string `json:"save_path"`
|
||||||
|
SeedingTimeLimit int `json:"seeding_time_limit,omitempty"`
|
||||||
|
SeenComplete int64 `json:"seen_complete,omitempty"`
|
||||||
|
SeqDl bool `json:"seq_dl"`
|
||||||
|
Size int64 `json:"size,omitempty"`
|
||||||
|
State string `json:"state,omitempty"`
|
||||||
|
SuperSeeding bool `json:"super_seeding"`
|
||||||
|
Tags string `json:"tags,omitempty"`
|
||||||
|
TimeActive int `json:"time_active,omitempty"`
|
||||||
|
TotalSize int64 `json:"total_size,omitempty"`
|
||||||
|
Tracker string `json:"tracker,omitempty"`
|
||||||
|
UpLimit int64 `json:"up_limit,omitempty"`
|
||||||
|
Uploaded int64 `json:"uploaded,omitempty"`
|
||||||
|
UploadedSession int64 `json:"uploaded_session,omitempty"`
|
||||||
|
Upspeed int64 `json:"upspeed,omitempty"`
|
||||||
|
Source string `json:"source,omitempty"`
|
||||||
|
|
||||||
|
sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Torrent) IsReady() bool {
|
||||||
|
return (t.AmountLeft <= 0 || t.Progress == 1) && t.TorrentPath != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Torrent) discordContext() string {
|
||||||
|
format := `
|
||||||
|
**Name:** %s
|
||||||
|
**Arr:** %s
|
||||||
|
**Hash:** %s
|
||||||
|
**MagnetURI:** %s
|
||||||
|
**Debrid:** %s
|
||||||
|
`
|
||||||
|
return fmt.Sprintf(format, t.Name, t.Category, t.Hash, t.MagnetUri, t.Debrid)
|
||||||
|
}
|
||||||
+5
-4
@@ -33,7 +33,7 @@ func (wb *Web) handleAddContent(w http.ResponseWriter, r *http.Request) {
|
|||||||
errs := make([]string, 0)
|
errs := make([]string, 0)
|
||||||
|
|
||||||
arrName := r.FormValue("arr")
|
arrName := r.FormValue("arr")
|
||||||
notSymlink := r.FormValue("notSymlink") == "true"
|
action := r.FormValue("action")
|
||||||
debridName := r.FormValue("debrid")
|
debridName := r.FormValue("debrid")
|
||||||
callbackUrl := r.FormValue("callbackUrl")
|
callbackUrl := r.FormValue("callbackUrl")
|
||||||
downloadFolder := r.FormValue("downloadFolder")
|
downloadFolder := r.FormValue("downloadFolder")
|
||||||
@@ -45,7 +45,7 @@ func (wb *Web) handleAddContent(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
_arr := _store.Arr().Get(arrName)
|
_arr := _store.Arr().Get(arrName)
|
||||||
if _arr == nil {
|
if _arr == nil {
|
||||||
_arr = arr.New(arrName, "", "", false, false, &downloadUncached)
|
_arr = arr.New(arrName, "", "", false, false, &downloadUncached, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle URLs
|
// Handle URLs
|
||||||
@@ -64,7 +64,7 @@ func (wb *Web) handleAddContent(w http.ResponseWriter, r *http.Request) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
importReq := store.NewImportRequest(debridName, downloadFolder, magnet, _arr, !notSymlink, downloadUncached, callbackUrl, store.ImportTypeAPI)
|
importReq := store.NewImportRequest(debridName, downloadFolder, magnet, _arr, action, downloadUncached, callbackUrl, store.ImportTypeAPI)
|
||||||
if err := _store.AddTorrent(ctx, importReq); err != nil {
|
if err := _store.AddTorrent(ctx, importReq); err != nil {
|
||||||
wb.logger.Error().Err(err).Str("url", url).Msg("Failed to add torrent")
|
wb.logger.Error().Err(err).Str("url", url).Msg("Failed to add torrent")
|
||||||
errs = append(errs, fmt.Sprintf("URL %s: %v", url, err))
|
errs = append(errs, fmt.Sprintf("URL %s: %v", url, err))
|
||||||
@@ -89,7 +89,7 @@ func (wb *Web) handleAddContent(w http.ResponseWriter, r *http.Request) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
importReq := store.NewImportRequest(debridName, downloadFolder, magnet, _arr, !notSymlink, downloadUncached, callbackUrl, store.ImportTypeAPI)
|
importReq := store.NewImportRequest(debridName, downloadFolder, magnet, _arr, action, downloadUncached, callbackUrl, store.ImportTypeAPI)
|
||||||
err = _store.AddTorrent(ctx, importReq)
|
err = _store.AddTorrent(ctx, importReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
wb.logger.Error().Err(err).Str("file", fileHeader.Filename).Msg("Failed to add torrent")
|
wb.logger.Error().Err(err).Str("file", fileHeader.Filename).Msg("Failed to add torrent")
|
||||||
@@ -251,6 +251,7 @@ func (wb *Web) handleUpdateConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
Cleanup: a.Cleanup,
|
Cleanup: a.Cleanup,
|
||||||
SkipRepair: a.SkipRepair,
|
SkipRepair: a.SkipRepair,
|
||||||
DownloadUncached: a.DownloadUncached,
|
DownloadUncached: a.DownloadUncached,
|
||||||
|
SelectedDebrid: a.SelectedDebrid,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
currentConfig.Arrs = updatedConfig.Arrs
|
currentConfig.Arrs = updatedConfig.Arrs
|
||||||
|
|||||||
@@ -279,13 +279,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<small class="form-text text-muted">Use Internal Webdav for repair(make sure webdav is enabled in the debrid section</small>
|
<small class="form-text text-muted">Use Internal Webdav for repair(make sure webdav is enabled in the debrid section</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 mb-3">
|
|
||||||
<div class="form-check">
|
|
||||||
<input type="checkbox" class="form-check-input" name="repair.run_on_start" id="repair.run_on_start">
|
|
||||||
<label class="form-check-label" for="repair.run_on_start">Run on Start</label>
|
|
||||||
</div>
|
|
||||||
<small class="form-text text-muted">Run repair on startup</small>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4 mb-3">
|
<div class="col-md-4 mb-3">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input type="checkbox" class="form-check-input" name="repair.auto_process" id="repair.auto_process">
|
<input type="checkbox" class="form-check-input" name="repair.auto_process" id="repair.auto_process">
|
||||||
@@ -650,6 +643,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<select class="form-select" name="arr[${index}].selected_debrid" id="arr[${index}].selected_debrid">
|
||||||
|
<option value="" selected disabled>Select Arr Debrid</option>
|
||||||
|
<option value="realdebrid">Real Debrid</option>
|
||||||
|
<option value="alldebrid">AllDebrid</option>
|
||||||
|
<option value="debrid_link">Debrid Link</option>
|
||||||
|
<option value="torbox">Torbox</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div class="col-md-2 mb-3">
|
<div class="col-md-2 mb-3">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<label for="arr[${index}].cleanup" class="form-check-label">Cleanup Queue</label>
|
<label for="arr[${index}].cleanup" class="form-check-label">Cleanup Queue</label>
|
||||||
@@ -1068,7 +1070,6 @@
|
|||||||
repair: {
|
repair: {
|
||||||
enabled: document.querySelector('[name="repair.enabled"]').checked,
|
enabled: document.querySelector('[name="repair.enabled"]').checked,
|
||||||
interval: document.querySelector('[name="repair.interval"]').value,
|
interval: document.querySelector('[name="repair.interval"]').value,
|
||||||
run_on_start: document.querySelector('[name="repair.run_on_start"]').checked,
|
|
||||||
zurg_url: document.querySelector('[name="repair.zurg_url"]').value,
|
zurg_url: document.querySelector('[name="repair.zurg_url"]').value,
|
||||||
workers: parseInt(document.querySelector('[name="repair.workers"]').value),
|
workers: parseInt(document.querySelector('[name="repair.workers"]').value),
|
||||||
use_webdav: document.querySelector('[name="repair.use_webdav"]').checked,
|
use_webdav: document.querySelector('[name="repair.use_webdav"]').checked,
|
||||||
@@ -1149,7 +1150,8 @@
|
|||||||
token: document.querySelector(`[name="arr[${i}].token"]`).value,
|
token: document.querySelector(`[name="arr[${i}].token"]`).value,
|
||||||
cleanup: document.querySelector(`[name="arr[${i}].cleanup"]`).checked,
|
cleanup: document.querySelector(`[name="arr[${i}].cleanup"]`).checked,
|
||||||
skip_repair: document.querySelector(`[name="arr[${i}].skip_repair"]`).checked,
|
skip_repair: document.querySelector(`[name="arr[${i}].skip_repair"]`).checked,
|
||||||
download_uncached: document.querySelector(`[name="arr[${i}].download_uncached"]`).checked
|
download_uncached: document.querySelector(`[name="arr[${i}].download_uncached"]`).checked,
|
||||||
|
selectedDebrid: document.querySelector(`[name="arr[${i}].selected_debrid"]`).value
|
||||||
};
|
};
|
||||||
|
|
||||||
if (arr.name && arr.host) {
|
if (arr.name && arr.host) {
|
||||||
|
|||||||
@@ -18,12 +18,21 @@
|
|||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-md-6">
|
<div class="col">
|
||||||
|
<label for="downloadAction" class="form-label">Post Download Action</label>
|
||||||
|
<select class="form-select" id="downloadAction" name="downloadAction">
|
||||||
|
<option value="symlink" selected>Symlink</option>
|
||||||
|
<option value="download">Download</option>
|
||||||
|
<option value="none">None</option>
|
||||||
|
</select>
|
||||||
|
<small class="text-muted">Choose how to handle the added torrent (Default to symlinks)</small>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
<label for="downloadFolder" class="form-label">Download Folder</label>
|
<label for="downloadFolder" class="form-label">Download Folder</label>
|
||||||
<input type="text" class="form-control" id="downloadFolder" name="downloadFolder" placeholder="Enter Download Folder (e.g /downloads/torrents)">
|
<input type="text" class="form-control" id="downloadFolder" name="downloadFolder" placeholder="Enter Download Folder (e.g /downloads/torrents)">
|
||||||
<small class="text-muted">Default is your qbittorent download_folder</small>
|
<small class="text-muted">Default is your qbittorent download_folder</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col">
|
||||||
<label for="arr" class="form-label">Arr (if any)</label>
|
<label for="arr" class="form-label">Arr (if any)</label>
|
||||||
<input type="text" class="form-control" id="arr" name="arr" placeholder="Enter Category (e.g sonarr, radarr, radarr4k)">
|
<input type="text" class="form-control" id="arr" name="arr" placeholder="Enter Category (e.g sonarr, radarr, radarr4k)">
|
||||||
<small class="text-muted">Optional, leave empty if not using Arr</small>
|
<small class="text-muted">Optional, leave empty if not using Arr</small>
|
||||||
@@ -45,12 +54,6 @@
|
|||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-md-2 mb-3">
|
|
||||||
<div class="form-check d-inline-block me-3">
|
|
||||||
<input type="checkbox" class="form-check-input" id="isSymlink" name="notSymlink">
|
|
||||||
<label class="form-check-label" for="isSymlink">No Symlinks</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-2 mb-3">
|
<div class="col-md-2 mb-3">
|
||||||
<div class="form-check d-inline-block">
|
<div class="form-check d-inline-block">
|
||||||
<input type="checkbox" class="form-check-input" name="downloadUncached" id="downloadUncached">
|
<input type="checkbox" class="form-check-input" name="downloadUncached" id="downloadUncached">
|
||||||
@@ -74,21 +77,21 @@
|
|||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const loadSavedDownloadOptions = () => {
|
const loadSavedDownloadOptions = () => {
|
||||||
const savedCategory = localStorage.getItem('downloadCategory');
|
const savedCategory = localStorage.getItem('downloadCategory');
|
||||||
const savedSymlink = localStorage.getItem('downloadSymlink');
|
const savedAction = localStorage.getItem('downloadAction');
|
||||||
const savedDownloadUncached = localStorage.getItem('downloadUncached');
|
const savedDownloadUncached = localStorage.getItem('downloadUncached');
|
||||||
document.getElementById('arr').value = savedCategory || '';
|
document.getElementById('arr').value = savedCategory || '';
|
||||||
document.getElementById('isSymlink').checked = savedSymlink === 'true';
|
document.getElementById('downloadAction').value = savedAction || 'symlink';
|
||||||
document.getElementById('downloadUncached').checked = savedDownloadUncached === 'true';
|
document.getElementById('downloadUncached').checked = savedDownloadUncached === 'true';
|
||||||
document.getElementById('downloadFolder').value = localStorage.getItem('downloadFolder') || downloadFolder || '';
|
document.getElementById('downloadFolder').value = localStorage.getItem('downloadFolder') || downloadFolder || '';
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveCurrentDownloadOptions = () => {
|
const saveCurrentDownloadOptions = () => {
|
||||||
const arr = document.getElementById('arr').value;
|
const arr = document.getElementById('arr').value;
|
||||||
const isSymlink = document.getElementById('isSymlink').checked;
|
const downloadAction = document.getElementById('downloadAction').value;
|
||||||
const downloadUncached = document.getElementById('downloadUncached').checked;
|
const downloadUncached = document.getElementById('downloadUncached').checked;
|
||||||
const downloadFolder = document.getElementById('downloadFolder').value;
|
const downloadFolder = document.getElementById('downloadFolder').value;
|
||||||
localStorage.setItem('downloadCategory', arr);
|
localStorage.setItem('downloadCategory', arr);
|
||||||
localStorage.setItem('downloadSymlink', isSymlink.toString());
|
localStorage.setItem('downloadAction', downloadAction);
|
||||||
localStorage.setItem('downloadUncached', downloadUncached.toString());
|
localStorage.setItem('downloadUncached', downloadUncached.toString());
|
||||||
localStorage.setItem('downloadFolder', downloadFolder);
|
localStorage.setItem('downloadFolder', downloadFolder);
|
||||||
};
|
};
|
||||||
@@ -136,7 +139,7 @@
|
|||||||
|
|
||||||
formData.append('arr', document.getElementById('arr').value);
|
formData.append('arr', document.getElementById('arr').value);
|
||||||
formData.append('downloadFolder', document.getElementById('downloadFolder').value);
|
formData.append('downloadFolder', document.getElementById('downloadFolder').value);
|
||||||
formData.append('notSymlink', document.getElementById('isSymlink').checked);
|
formData.append('action', document.getElementById('downloadAction').value);
|
||||||
formData.append('downloadUncached', document.getElementById('downloadUncached').checked);
|
formData.append('downloadUncached', document.getElementById('downloadUncached').checked);
|
||||||
formData.append('debrid', document.getElementById('debrid') ? document.getElementById('debrid').value : '');
|
formData.append('debrid', document.getElementById('debrid') ? document.getElementById('debrid').value : '');
|
||||||
|
|
||||||
@@ -168,7 +171,7 @@
|
|||||||
|
|
||||||
// Save the download options to local storage when they change
|
// Save the download options to local storage when they change
|
||||||
document.getElementById('arr').addEventListener('change', saveCurrentDownloadOptions);
|
document.getElementById('arr').addEventListener('change', saveCurrentDownloadOptions);
|
||||||
document.getElementById('isSymlink').addEventListener('change', saveCurrentDownloadOptions);
|
document.getElementById('downloadAction').addEventListener('change', saveCurrentDownloadOptions);
|
||||||
|
|
||||||
// Read the URL parameters for a magnet link and add it to the download queue if found
|
// Read the URL parameters for a magnet link and add it to the download queue if found
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
|||||||
+7
-14
@@ -127,7 +127,7 @@ func (f *File) stream() (*http.Response, error) {
|
|||||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusPartialContent {
|
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusPartialContent {
|
||||||
f.downloadLink = ""
|
f.downloadLink = ""
|
||||||
|
|
||||||
cleanupResp := func() {
|
cleanupResp := func(resp *http.Response) {
|
||||||
if resp.Body != nil {
|
if resp.Body != nil {
|
||||||
_, _ = io.Copy(io.Discard, resp.Body)
|
_, _ = io.Copy(io.Discard, resp.Body)
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
@@ -138,7 +138,7 @@ func (f *File) stream() (*http.Response, error) {
|
|||||||
case http.StatusServiceUnavailable:
|
case http.StatusServiceUnavailable:
|
||||||
// Read the body to check for specific error messages
|
// Read the body to check for specific error messages
|
||||||
body, readErr := io.ReadAll(resp.Body)
|
body, readErr := io.ReadAll(resp.Body)
|
||||||
resp.Body.Close()
|
cleanupResp(resp)
|
||||||
|
|
||||||
if readErr != nil {
|
if readErr != nil {
|
||||||
_log.Trace().Msgf("Failed to read response body: %v", readErr)
|
_log.Trace().Msgf("Failed to read response body: %v", readErr)
|
||||||
@@ -156,10 +156,10 @@ func (f *File) stream() (*http.Response, error) {
|
|||||||
return nil, fmt.Errorf("service unavailable: %s", bodyStr)
|
return nil, fmt.Errorf("service unavailable: %s", bodyStr)
|
||||||
|
|
||||||
case http.StatusNotFound:
|
case http.StatusNotFound:
|
||||||
cleanupResp()
|
cleanupResp(resp)
|
||||||
// Mark download link as not found
|
// Mark download link as not found
|
||||||
// Regenerate a new download link
|
// Regenerate a new download link
|
||||||
_log.Trace().Msgf("File not found (404) for %s. Marking link as invalid and regenerating", f.name)
|
_log.Trace().Msgf("Link not found (404) for %s. Marking link as invalid and regenerating", f.name)
|
||||||
f.cache.MarkDownloadLinkAsInvalid(f.link, downloadLink, "link_not_found")
|
f.cache.MarkDownloadLinkAsInvalid(f.link, downloadLink, "link_not_found")
|
||||||
// Generate a new download link
|
// Generate a new download link
|
||||||
downloadLink, err := f.getDownloadLink()
|
downloadLink, err := f.getDownloadLink()
|
||||||
@@ -191,16 +191,9 @@ func (f *File) stream() (*http.Response, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if newResp.StatusCode != http.StatusOK && newResp.StatusCode != http.StatusPartialContent {
|
if newResp.StatusCode != http.StatusOK && newResp.StatusCode != http.StatusPartialContent {
|
||||||
cleanupBody := func() {
|
cleanupResp(newResp)
|
||||||
if newResp.Body != nil {
|
|
||||||
_, _ = io.Copy(io.Discard, newResp.Body)
|
|
||||||
newResp.Body.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanupBody()
|
|
||||||
_log.Trace().Msgf("Regenerated link also failed with status %d", newResp.StatusCode)
|
_log.Trace().Msgf("Regenerated link also failed with status %d", newResp.StatusCode)
|
||||||
f.cache.MarkDownloadLinkAsInvalid(f.link, downloadLink, "link_not_found")
|
f.cache.MarkDownloadLinkAsInvalid(f.link, downloadLink, newResp.Status)
|
||||||
return nil, fmt.Errorf("failed with status code %d even after link regeneration", newResp.StatusCode)
|
return nil, fmt.Errorf("failed with status code %d even after link regeneration", newResp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,7 +201,7 @@ func (f *File) stream() (*http.Response, error) {
|
|||||||
|
|
||||||
default:
|
default:
|
||||||
body, _ := io.ReadAll(resp.Body)
|
body, _ := io.ReadAll(resp.Body)
|
||||||
resp.Body.Close()
|
cleanupResp(resp)
|
||||||
|
|
||||||
_log.Trace().Msgf("Unexpected status code %d for %s: %s", resp.StatusCode, f.name, string(body))
|
_log.Trace().Msgf("Unexpected status code %d for %s: %s", resp.StatusCode, f.name, string(body))
|
||||||
return nil, fmt.Errorf("unexpected status code %d: %s", resp.StatusCode, string(body))
|
return nil, fmt.Errorf("unexpected status code %d: %s", resp.StatusCode, string(body))
|
||||||
|
|||||||
Reference in New Issue
Block a user