Torrent Queuing for Botched torrent (#83)

* Implement a queue for handling failed torrent

* Add checks for getting slots

* Few other cleanups, change some function names
This commit is contained in:
Mukhtar Akere
2025-06-07 17:23:41 +01:00
committed by GitHub
parent 84603b084b
commit 5bf1dab5e6
30 changed files with 556 additions and 239 deletions

View File

@@ -18,17 +18,18 @@ import (
)
type AllDebrid struct {
Name string
name string
Host string `json:"host"`
APIKey string
accounts map[string]types.Account
DownloadUncached bool
client *request.Client
MountPath string
logger zerolog.Logger
checkCached bool
addSamples bool
MountPath string
logger zerolog.Logger
checkCached bool
addSamples bool
minimumFreeSlot int
}
func (ad *AllDebrid) GetProfile() (*types.Profile, error) {
@@ -59,7 +60,7 @@ func New(dc config.Debrid) (*AllDebrid, error) {
}
}
return &AllDebrid{
Name: "alldebrid",
name: "alldebrid",
Host: "http://api.alldebrid.com/v4.1",
APIKey: dc.APIKey,
accounts: accounts,
@@ -69,14 +70,15 @@ func New(dc config.Debrid) (*AllDebrid, error) {
logger: logger.New(dc.Name),
checkCached: dc.CheckCached,
addSamples: dc.AddSamples,
minimumFreeSlot: dc.MinimumFreeSlot,
}, nil
}
func (ad *AllDebrid) GetName() string {
return ad.Name
func (ad *AllDebrid) Name() string {
return ad.name
}
func (ad *AllDebrid) GetLogger() zerolog.Logger {
func (ad *AllDebrid) Logger() zerolog.Logger {
return ad.logger
}
@@ -204,7 +206,7 @@ func (ad *AllDebrid) GetTorrent(torrentId string) (*types.Torrent, error) {
OriginalFilename: name,
Files: make(map[string]types.File),
InfoHash: data.Hash,
Debrid: ad.Name,
Debrid: ad.name,
MountPath: ad.MountPath,
Added: time.Unix(data.CompletionDate, 0).Format(time.RFC3339),
}
@@ -244,7 +246,7 @@ func (ad *AllDebrid) UpdateTorrent(t *types.Torrent) error {
t.OriginalFilename = name
t.Folder = name
t.MountPath = ad.MountPath
t.Debrid = ad.Name
t.Debrid = ad.name
t.Bytes = data.Size
t.Seeders = data.Seeders
t.Added = time.Unix(data.CompletionDate, 0).Format(time.RFC3339)
@@ -406,7 +408,7 @@ func (ad *AllDebrid) GetTorrents() ([]*types.Torrent, error) {
OriginalFilename: magnet.Filename,
Files: make(map[string]types.File),
InfoHash: magnet.Hash,
Debrid: ad.Name,
Debrid: ad.name,
MountPath: ad.MountPath,
Added: time.Unix(magnet.CompletionDate, 0).Format(time.RFC3339),
})
@@ -444,3 +446,9 @@ func (ad *AllDebrid) ResetActiveDownloadKeys() {
func (ad *AllDebrid) DeleteDownloadLink(linkId string) error {
return nil
}
func (ad *AllDebrid) GetAvailableSlots() (int, error) {
// This function is a placeholder for AllDebrid
//TODO: Implement the logic to check available slots for AllDebrid
return 0, fmt.Errorf("GetAvailableSlots not implemented for AllDebrid")
}

View File

@@ -18,7 +18,7 @@ import (
)
type DebridLink struct {
Name string
name string
Host string `json:"host"`
APIKey string
accounts map[string]types.Account
@@ -56,7 +56,7 @@ func New(dc config.Debrid) (*DebridLink, error) {
}
}
return &DebridLink{
Name: "debridlink",
name: "debridlink",
Host: "https://debrid-link.com/api/v2",
APIKey: dc.APIKey,
accounts: accounts,
@@ -73,11 +73,11 @@ func (dl *DebridLink) GetProfile() (*types.Profile, error) {
return nil, nil
}
func (dl *DebridLink) GetName() string {
return dl.Name
func (dl *DebridLink) Name() string {
return dl.name
}
func (dl *DebridLink) GetLogger() zerolog.Logger {
func (dl *DebridLink) Logger() zerolog.Logger {
return dl.logger
}
@@ -163,7 +163,7 @@ func (dl *DebridLink) GetTorrent(torrentId string) (*types.Torrent, error) {
Filename: name,
OriginalFilename: name,
MountPath: dl.MountPath,
Debrid: dl.Name,
Debrid: dl.name,
Added: time.Unix(t.Created, 0).Format(time.RFC3339),
}
cfg := config.Get()
@@ -288,7 +288,7 @@ func (dl *DebridLink) SubmitMagnet(t *types.Torrent) (*types.Torrent, error) {
t.Filename = name
t.OriginalFilename = name
t.MountPath = dl.MountPath
t.Debrid = dl.Name
t.Debrid = dl.name
t.Added = time.Unix(data.Created, 0).Format(time.RFC3339)
for _, f := range data.Files {
file := types.File{
@@ -428,7 +428,7 @@ func (dl *DebridLink) getTorrents(page, perPage int) ([]*types.Torrent, error) {
OriginalFilename: t.Name,
InfoHash: t.HashString,
Files: make(map[string]types.File),
Debrid: dl.Name,
Debrid: dl.name,
MountPath: dl.MountPath,
Added: time.Unix(t.Created, 0).Format(time.RFC3339),
}
@@ -476,3 +476,8 @@ func (dl *DebridLink) ResetActiveDownloadKeys() {
func (dl *DebridLink) DeleteDownloadLink(linkId string) error {
return nil
}
func (dl *DebridLink) GetAvailableSlots() (int, error) {
//TODO: Implement the logic to check available slots for DebridLink
return 0, fmt.Errorf("GetAvailableSlots not implemented for DebridLink")
}

View File

@@ -25,7 +25,7 @@ import (
)
type RealDebrid struct {
Name string
name string
Host string `json:"host"`
APIKey string
@@ -41,10 +41,12 @@ type RealDebrid struct {
logger zerolog.Logger
UnpackRar bool
rarSemaphore chan struct{}
checkCached bool
addSamples bool
Profile *types.Profile
rarSemaphore chan struct{}
checkCached bool
addSamples bool
Profile *types.Profile
minimumFreeSlot int // Minimum number of active pots to maintain (used for cached stuffs, etc.)
}
func New(dc config.Debrid) (*RealDebrid, error) {
@@ -71,7 +73,7 @@ func New(dc config.Debrid) (*RealDebrid, error) {
}
r := &RealDebrid{
Name: "realdebrid",
name: "realdebrid",
Host: "https://api.real-debrid.com/rest/1.0",
APIKey: dc.APIKey,
accounts: accounts,
@@ -98,6 +100,7 @@ func New(dc config.Debrid) (*RealDebrid, error) {
rarSemaphore: make(chan struct{}, 2),
checkCached: dc.CheckCached,
addSamples: dc.AddSamples,
minimumFreeSlot: dc.MinimumFreeSlot,
}
if _, err := r.GetProfile(); err != nil {
@@ -107,11 +110,11 @@ func New(dc config.Debrid) (*RealDebrid, error) {
}
}
func (r *RealDebrid) GetName() string {
return r.Name
func (r *RealDebrid) Name() string {
return r.name
}
func (r *RealDebrid) GetLogger() zerolog.Logger {
func (r *RealDebrid) Logger() zerolog.Logger {
return r.logger
}
@@ -337,15 +340,30 @@ func (r *RealDebrid) addTorrent(t *types.Torrent) (*types.Torrent, error) {
return nil, err
}
req.Header.Add("Content-Type", "application/x-bittorrent")
resp, err := r.client.MakeRequest(req)
resp, err := r.client.Do(req)
if err != nil {
return nil, err
}
if err = json.Unmarshal(resp, &data); err != nil {
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
// Handle multiple_downloads
if resp.StatusCode == 509 {
return nil, utils.TooManyActiveDownloadsError
}
bodyBytes, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("realdebrid API error: Status: %d || Body: %s", resp.StatusCode, string(bodyBytes))
}
defer resp.Body.Close()
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("reading response body: %w", err)
}
if err = json.Unmarshal(bodyBytes, &data); err != nil {
return nil, err
}
t.Id = data.Id
t.Debrid = r.Name
t.Debrid = r.name
t.MountPath = r.MountPath
return t, nil
}
@@ -357,15 +375,30 @@ func (r *RealDebrid) addMagnet(t *types.Torrent) (*types.Torrent, error) {
}
var data AddMagnetSchema
req, _ := http.NewRequest(http.MethodPost, url, strings.NewReader(payload.Encode()))
resp, err := r.client.MakeRequest(req)
resp, err := r.client.Do(req)
if err != nil {
return nil, err
}
if err = json.Unmarshal(resp, &data); err != nil {
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
// Handle multiple_downloads
if resp.StatusCode == 509 {
return nil, utils.TooManyActiveDownloadsError
}
bodyBytes, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("realdebrid API error: Status: %d || Body: %s", resp.StatusCode, string(bodyBytes))
}
defer resp.Body.Close()
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("reading response body: %w", err)
}
if err = json.Unmarshal(bodyBytes, &data); err != nil {
return nil, err
}
t.Id = data.Id
t.Debrid = r.Name
t.Debrid = r.name
t.MountPath = r.MountPath
return t, nil
}
@@ -384,7 +417,7 @@ func (r *RealDebrid) GetTorrent(torrentId string) (*types.Torrent, error) {
}
if resp.StatusCode != http.StatusOK {
if resp.StatusCode == http.StatusNotFound {
return nil, request.TorrentNotFoundError
return nil, utils.TorrentNotFoundError
}
return nil, fmt.Errorf("realdebrid API error: Status: %d || Body: %s", resp.StatusCode, string(bodyBytes))
}
@@ -406,7 +439,7 @@ func (r *RealDebrid) GetTorrent(torrentId string) (*types.Torrent, error) {
Filename: data.Filename,
OriginalFilename: data.OriginalFilename,
Links: data.Links,
Debrid: r.Name,
Debrid: r.name,
MountPath: r.MountPath,
}
t.Files = r.getTorrentFiles(t, data) // Get selected files
@@ -427,7 +460,7 @@ func (r *RealDebrid) UpdateTorrent(t *types.Torrent) error {
}
if resp.StatusCode != http.StatusOK {
if resp.StatusCode == http.StatusNotFound {
return request.TorrentNotFoundError
return utils.TorrentNotFoundError
}
return fmt.Errorf("realdebrid API error: Status: %d || Body: %s", resp.StatusCode, string(bodyBytes))
}
@@ -447,7 +480,7 @@ func (r *RealDebrid) UpdateTorrent(t *types.Torrent) error {
t.OriginalFilename = data.OriginalFilename
t.Links = data.Links
t.MountPath = r.MountPath
t.Debrid = r.Name
t.Debrid = r.name
t.Added = data.Added
t.Files, _ = r.getSelectedFiles(t, data) // Get selected files
@@ -478,7 +511,7 @@ func (r *RealDebrid) CheckStatus(t *types.Torrent, isSymlink bool) (*types.Torre
t.Seeders = data.Seeders
t.Links = data.Links
t.Status = status
t.Debrid = r.Name
t.Debrid = r.name
t.MountPath = r.MountPath
if status == "waiting_files_selection" {
t.Files = r.getTorrentFiles(t, data)
@@ -499,6 +532,9 @@ func (r *RealDebrid) CheckStatus(t *types.Torrent, isSymlink bool) (*types.Torre
return t, err
}
if res.StatusCode != http.StatusNoContent {
if res.StatusCode == 509 {
return nil, utils.TooManyActiveDownloadsError
}
return t, fmt.Errorf("realdebrid API error: Status: %d", res.StatusCode)
}
} else if status == "downloaded" {
@@ -593,7 +629,7 @@ func (r *RealDebrid) CheckLink(link string) error {
return err
}
if resp.StatusCode == http.StatusNotFound {
return request.HosterUnavailableError // File has been removed
return utils.HosterUnavailableError // File has been removed
}
return nil
}
@@ -622,17 +658,17 @@ func (r *RealDebrid) _getDownloadLink(file *types.File) (*types.DownloadLink, er
}
switch data.ErrorCode {
case 19:
return nil, request.HosterUnavailableError // File has been removed
return nil, utils.HosterUnavailableError // File has been removed
case 23:
return nil, request.TrafficExceededError
return nil, utils.TrafficExceededError
case 24:
return nil, request.HosterUnavailableError // Link has been nerfed
return nil, utils.HosterUnavailableError // Link has been nerfed
case 34:
return nil, request.TrafficExceededError // traffic exceeded
return nil, utils.TrafficExceededError // traffic exceeded
case 35:
return nil, request.HosterUnavailableError
return nil, utils.HosterUnavailableError
case 36:
return nil, request.TrafficExceededError // traffic exceeded
return nil, utils.TrafficExceededError // traffic exceeded
default:
return nil, fmt.Errorf("realdebrid API error: Status: %d || Code: %d", resp.StatusCode, data.ErrorCode)
}
@@ -674,7 +710,7 @@ func (r *RealDebrid) GetDownloadLink(t *types.Torrent, file *types.File) (*types
downloadLink, err := r._getDownloadLink(file)
retries := 0
if err != nil {
if errors.Is(err, request.TrafficExceededError) {
if errors.Is(err, utils.TrafficExceededError) {
// Retries generating
retries = 5
} else {
@@ -688,7 +724,7 @@ func (r *RealDebrid) GetDownloadLink(t *types.Torrent, file *types.File) (*types
if err == nil {
return downloadLink, nil
}
if !errors.Is(err, request.TrafficExceededError) {
if !errors.Is(err, utils.TrafficExceededError) {
return nil, err
}
// Add a delay before retrying
@@ -750,7 +786,7 @@ func (r *RealDebrid) getTorrents(offset int, limit int) (int, []*types.Torrent,
Links: t.Links,
Files: make(map[string]types.File),
InfoHash: t.Hash,
Debrid: r.Name,
Debrid: r.name,
MountPath: r.MountPath,
Added: t.Added.Format(time.RFC3339),
})
@@ -941,3 +977,17 @@ func (r *RealDebrid) GetProfile() (*types.Profile, error) {
}
return profile, nil
}
func (r *RealDebrid) GetAvailableSlots() (int, error) {
url := fmt.Sprintf("%s/torrents/activeCount", r.Host)
req, _ := http.NewRequest(http.MethodGet, url, nil)
resp, err := r.client.MakeRequest(req)
if err != nil {
return 0, nil
}
var data AvailableSlotsResponse
if json.Unmarshal(resp, &data) != nil {
return 0, fmt.Errorf("error unmarshalling available slots response: %w", err)
}
return data.TotalSlots - data.ActiveSlots - r.minimumFreeSlot, nil // Ensure we maintain minimum active pots
}

View File

@@ -151,3 +151,8 @@ type profileResponse struct {
Premium int `json:"premium"`
Expiration time.Time `json:"expiration"`
}
type AvailableSlotsResponse struct {
ActiveSlots int `json:"nb"`
TotalSlots int `json:"limit"`
}

View File

@@ -24,7 +24,7 @@ import (
)
type Torbox struct {
Name string
name string
Host string `json:"host"`
APIKey string
accounts map[string]types.Account
@@ -67,7 +67,7 @@ func New(dc config.Debrid) (*Torbox, error) {
}
return &Torbox{
Name: "torbox",
name: "torbox",
Host: "https://api.torbox.app/v1",
APIKey: dc.APIKey,
accounts: accounts,
@@ -80,11 +80,11 @@ func New(dc config.Debrid) (*Torbox, error) {
}, nil
}
func (tb *Torbox) GetName() string {
return tb.Name
func (tb *Torbox) Name() string {
return tb.name
}
func (tb *Torbox) GetLogger() zerolog.Logger {
func (tb *Torbox) Logger() zerolog.Logger {
return tb.logger
}
@@ -166,7 +166,7 @@ func (tb *Torbox) SubmitMagnet(torrent *types.Torrent) (*types.Torrent, error) {
torrentId := strconv.Itoa(dt.Id)
torrent.Id = torrentId
torrent.MountPath = tb.MountPath
torrent.Debrid = tb.Name
torrent.Debrid = tb.name
return torrent, nil
}
@@ -215,7 +215,7 @@ func (tb *Torbox) GetTorrent(torrentId string) (*types.Torrent, error) {
Filename: data.Name,
OriginalFilename: data.Name,
MountPath: tb.MountPath,
Debrid: tb.Name,
Debrid: tb.name,
Files: make(map[string]types.File),
Added: data.CreatedAt.Format(time.RFC3339),
}
@@ -250,7 +250,7 @@ func (tb *Torbox) GetTorrent(torrentId string) (*types.Torrent, error) {
}
t.OriginalFilename = strings.Split(cleanPath, "/")[0]
t.Debrid = tb.Name
t.Debrid = tb.name
return t, nil
}
@@ -279,7 +279,7 @@ func (tb *Torbox) UpdateTorrent(t *types.Torrent) error {
t.Filename = name
t.OriginalFilename = name
t.MountPath = tb.MountPath
t.Debrid = tb.Name
t.Debrid = tb.name
cfg := config.Get()
for _, f := range data.Files {
fileName := filepath.Base(f.Name)
@@ -311,7 +311,7 @@ func (tb *Torbox) UpdateTorrent(t *types.Torrent) error {
}
t.OriginalFilename = strings.Split(cleanPath, "/")[0]
t.Debrid = tb.Name
t.Debrid = tb.name
return nil
}
@@ -470,3 +470,8 @@ func (tb *Torbox) ResetActiveDownloadKeys() {
func (tb *Torbox) DeleteDownloadLink(linkId string) error {
return nil
}
func (tb *Torbox) GetAvailableSlots() (int, error) {
//TODO: Implement the logic to check available slots for Torbox
return 0, fmt.Errorf("not implemented")
}