* Implement a queue for handling failed torrent * Add checks for getting slots * Few other cleanups, change some function names
193 lines
4.8 KiB
Go
193 lines
4.8 KiB
Go
package store
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"github.com/sirrobot01/decypharr/internal/request"
|
|
"github.com/sirrobot01/decypharr/internal/utils"
|
|
"github.com/sirrobot01/decypharr/pkg/arr"
|
|
debridTypes "github.com/sirrobot01/decypharr/pkg/debrid/types"
|
|
"net/http"
|
|
"net/url"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type ImportType string
|
|
|
|
const (
|
|
ImportTypeQBitTorrent ImportType = "qbit"
|
|
ImportTypeAPI ImportType = "api"
|
|
)
|
|
|
|
type ImportRequest struct {
|
|
DownloadFolder string `json:"downloadFolder"`
|
|
SelectedDebrid string `json:"debrid"`
|
|
Magnet *utils.Magnet `json:"magnet"`
|
|
Arr *arr.Arr `json:"arr"`
|
|
IsSymlink bool `json:"isSymlink"`
|
|
DownloadUncached bool `json:"downloadUncached"`
|
|
CallBackUrl string `json:"callBackUrl"`
|
|
|
|
Status string `json:"status"`
|
|
CompletedAt time.Time `json:"completedAt,omitempty"`
|
|
Error error `json:"error,omitempty"`
|
|
|
|
Type ImportType `json:"type"`
|
|
Async bool `json:"async"`
|
|
}
|
|
|
|
func NewImportRequest(debrid string, downloadFolder string, magnet *utils.Magnet, arr *arr.Arr, isSymlink, downloadUncached bool, callBackUrl string, importType ImportType) *ImportRequest {
|
|
return &ImportRequest{
|
|
Status: "started",
|
|
DownloadFolder: downloadFolder,
|
|
SelectedDebrid: debrid,
|
|
Magnet: magnet,
|
|
Arr: arr,
|
|
IsSymlink: isSymlink,
|
|
DownloadUncached: downloadUncached,
|
|
CallBackUrl: callBackUrl,
|
|
Type: importType,
|
|
}
|
|
}
|
|
|
|
type importResponse struct {
|
|
Status string `json:"status"`
|
|
CompletedAt time.Time `json:"completedAt"`
|
|
Error error `json:"error"`
|
|
Torrent *Torrent `json:"torrent"`
|
|
Debrid *debridTypes.Torrent `json:"debrid"`
|
|
}
|
|
|
|
func (i *ImportRequest) sendCallback(torrent *Torrent, debridTorrent *debridTypes.Torrent) {
|
|
if i.CallBackUrl == "" {
|
|
return
|
|
}
|
|
|
|
// Check if the callback URL is valid
|
|
if _, err := url.ParseRequestURI(i.CallBackUrl); err != nil {
|
|
return
|
|
}
|
|
|
|
client := request.New()
|
|
payload, err := json.Marshal(&importResponse{
|
|
Status: i.Status,
|
|
Error: i.Error,
|
|
CompletedAt: i.CompletedAt,
|
|
Torrent: torrent,
|
|
Debrid: debridTorrent,
|
|
})
|
|
if err != nil {
|
|
return
|
|
}
|
|
req, err := http.NewRequest("POST", i.CallBackUrl, bytes.NewReader(payload))
|
|
if err != nil {
|
|
return
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
_, _ = client.Do(req)
|
|
|
|
}
|
|
|
|
func (i *ImportRequest) markAsFailed(err error, torrent *Torrent, debridTorrent *debridTypes.Torrent) {
|
|
i.Status = "failed"
|
|
i.Error = err
|
|
i.CompletedAt = time.Now()
|
|
i.sendCallback(torrent, debridTorrent)
|
|
}
|
|
|
|
func (i *ImportRequest) markAsCompleted(torrent *Torrent, debridTorrent *debridTypes.Torrent) {
|
|
i.Status = "completed"
|
|
i.Error = nil
|
|
i.CompletedAt = time.Now()
|
|
i.sendCallback(torrent, debridTorrent)
|
|
}
|
|
|
|
type ImportQueue struct {
|
|
queue map[string]chan *ImportRequest // Map to hold queues for different debrid services
|
|
mu sync.RWMutex // Mutex to protect access to the queue map
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
capacity int // Capacity of each channel in the queue
|
|
}
|
|
|
|
func NewImportQueue(ctx context.Context, capacity int) *ImportQueue {
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
return &ImportQueue{
|
|
queue: make(map[string]chan *ImportRequest),
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
capacity: capacity,
|
|
}
|
|
}
|
|
|
|
func (iq *ImportQueue) Push(req *ImportRequest) error {
|
|
if req == nil {
|
|
return fmt.Errorf("import request cannot be nil")
|
|
}
|
|
|
|
iq.mu.Lock()
|
|
defer iq.mu.Unlock()
|
|
|
|
if _, exists := iq.queue[req.SelectedDebrid]; !exists {
|
|
iq.queue[req.SelectedDebrid] = make(chan *ImportRequest, iq.capacity) // Create a new channel for the debrid service
|
|
}
|
|
|
|
select {
|
|
case iq.queue[req.SelectedDebrid] <- req:
|
|
return nil
|
|
case <-iq.ctx.Done():
|
|
return fmt.Errorf("retry queue is shutting down")
|
|
}
|
|
}
|
|
|
|
func (iq *ImportQueue) TryPop(selectedDebrid string) (*ImportRequest, error) {
|
|
iq.mu.RLock()
|
|
defer iq.mu.RUnlock()
|
|
|
|
if ch, exists := iq.queue[selectedDebrid]; exists {
|
|
select {
|
|
case req := <-ch:
|
|
return req, nil
|
|
case <-iq.ctx.Done():
|
|
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)
|
|
}
|
|
|
|
func (iq *ImportQueue) Size(selectedDebrid string) int {
|
|
iq.mu.RLock()
|
|
defer iq.mu.RUnlock()
|
|
|
|
if ch, exists := iq.queue[selectedDebrid]; exists {
|
|
return len(ch)
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (iq *ImportQueue) Close() {
|
|
iq.cancel()
|
|
iq.mu.Lock()
|
|
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)
|
|
}
|