812 lines
22 KiB
Go
812 lines
22 KiB
Go
package realdebrid
|
|
|
|
import (
|
|
"bytes"
|
|
"cmp"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"net/url"
|
|
"path/filepath"
|
|
"seanime/internal/debrid/debrid"
|
|
"seanime/internal/util"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/rs/zerolog"
|
|
"github.com/samber/mo"
|
|
)
|
|
|
|
type (
|
|
RealDebrid struct {
|
|
baseUrl string
|
|
apiKey mo.Option[string]
|
|
client *http.Client
|
|
logger *zerolog.Logger
|
|
}
|
|
|
|
ErrorResponse struct {
|
|
Error string `json:"error"`
|
|
ErrorDetails string `json:"error_details"`
|
|
ErrorCode int `json:"error_code"`
|
|
}
|
|
|
|
Torrent struct {
|
|
ID string `json:"id"`
|
|
Filename string `json:"filename"`
|
|
Hash string `json:"hash"`
|
|
Bytes int64 `json:"bytes"`
|
|
Host string `json:"host"`
|
|
Split int `json:"split"`
|
|
Progress float64 `json:"progress"`
|
|
Status string `json:"status"`
|
|
Added string `json:"added"`
|
|
Links []string `json:"links"`
|
|
Ended string `json:"ended"`
|
|
Speed int64 `json:"speed"`
|
|
Seeders int `json:"seeders"`
|
|
}
|
|
|
|
TorrentInfo struct {
|
|
ID string `json:"id"`
|
|
Filename string `json:"filename"`
|
|
OriginalFilename string `json:"original_filename"`
|
|
Hash string `json:"hash"`
|
|
Bytes int64 `json:"bytes"` // Size of selected files
|
|
OriginalBytes int64 `json:"original_bytes"` // Size of the torrent
|
|
Host string `json:"host"`
|
|
Split int `json:"split"`
|
|
Progress float64 `json:"progress"`
|
|
Status string `json:"status"`
|
|
Added string `json:"added"`
|
|
Files []*TorrentInfoFile `json:"files"`
|
|
Links []string `json:"links"`
|
|
Ended string `json:"ended"`
|
|
Speed int64 `json:"speed"`
|
|
Seeders int `json:"seeders"`
|
|
}
|
|
|
|
TorrentInfoFile struct {
|
|
ID int `json:"id"`
|
|
Path string `json:"path"` // e.g. "/Big Buck Bunny/Big Buck Bunny.mp4"
|
|
Bytes int64 `json:"bytes"`
|
|
Selected int `json:"selected"` // 1 if selected, 0 if not
|
|
}
|
|
|
|
InstantAvailabilityItem struct {
|
|
Hash string `json:"hash"`
|
|
Files []struct {
|
|
Filename string `json:"filename"`
|
|
Filesize int `json:"filesize"`
|
|
} `json:"files"`
|
|
}
|
|
)
|
|
|
|
func NewRealDebrid(logger *zerolog.Logger) debrid.Provider {
|
|
return &RealDebrid{
|
|
baseUrl: "https://api.real-debrid.com/rest/1.0",
|
|
apiKey: mo.None[string](),
|
|
client: &http.Client{
|
|
Timeout: time.Second * 10,
|
|
},
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
func NewRealDebridT(logger *zerolog.Logger) *RealDebrid {
|
|
return &RealDebrid{
|
|
baseUrl: "https://api.real-debrid.com/rest/1.0",
|
|
apiKey: mo.None[string](),
|
|
client: &http.Client{
|
|
Timeout: time.Second * 30,
|
|
},
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
func (t *RealDebrid) GetSettings() debrid.Settings {
|
|
return debrid.Settings{
|
|
ID: "realdebrid",
|
|
Name: "RealDebrid",
|
|
}
|
|
}
|
|
|
|
func (t *RealDebrid) doQuery(method, uri string, body io.Reader, contentType string) (ret []byte, err error) {
|
|
apiKey, found := t.apiKey.Get()
|
|
if !found {
|
|
return nil, debrid.ErrNotAuthenticated
|
|
}
|
|
|
|
req, err := http.NewRequest(method, uri, body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Add("Content-Type", contentType)
|
|
req.Header.Add("Authorization", "Bearer "+apiKey)
|
|
|
|
resp, err := t.client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
var errResp ErrorResponse
|
|
|
|
if err := json.NewDecoder(resp.Body).Decode(&errResp); err != nil {
|
|
t.logger.Error().Err(err).Msg("realdebrid: Failed to decode response")
|
|
return nil, fmt.Errorf("failed to decode response: %w", err)
|
|
}
|
|
|
|
// If the error details are empty, we'll just return the response body
|
|
if errResp.ErrorDetails == "" && errResp.ErrorCode == 0 {
|
|
content, _ := io.ReadAll(resp.Body)
|
|
return content, nil
|
|
}
|
|
|
|
return nil, fmt.Errorf("failed to query API: %s, %s", resp.Status, errResp.ErrorDetails)
|
|
}
|
|
|
|
content, _ := io.ReadAll(resp.Body)
|
|
//fmt.Println(string(content))
|
|
|
|
return content, nil
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
func (t *RealDebrid) Authenticate(apiKey string) error {
|
|
t.apiKey = mo.Some(apiKey)
|
|
return nil
|
|
}
|
|
|
|
// {
|
|
// "string": { // First hash
|
|
// "string": [ // hoster, ex: "rd"
|
|
// // All file IDs variants
|
|
// {
|
|
// "int": { // file ID, you must ask all file IDs from this array on /selectFiles to get instant downloading
|
|
// "filename": "string",
|
|
// "filesize": int
|
|
// },
|
|
// },
|
|
type instantAvailabilityResponse map[string]map[string][]map[int]instantAvailabilityFile
|
|
type instantAvailabilityFile struct {
|
|
Filename string `json:"filename"`
|
|
Filesize int64 `json:"filesize"`
|
|
}
|
|
|
|
func (t *RealDebrid) GetInstantAvailability(hashes []string) map[string]debrid.TorrentItemInstantAvailability {
|
|
|
|
t.logger.Trace().Strs("hashes", hashes).Msg("realdebrid: Checking instant availability")
|
|
|
|
availability := make(map[string]debrid.TorrentItemInstantAvailability)
|
|
|
|
if len(hashes) == 0 {
|
|
return availability
|
|
}
|
|
|
|
return t.getInstantAvailabilityT(hashes, 3, 100)
|
|
}
|
|
|
|
func (t *RealDebrid) getInstantAvailabilityT(hashes []string, retries int, limit int) (ret map[string]debrid.TorrentItemInstantAvailability) {
|
|
|
|
ret = make(map[string]debrid.TorrentItemInstantAvailability)
|
|
|
|
var hashBatches [][]string
|
|
|
|
for i := 0; i < len(hashes); i += limit {
|
|
end := i + limit
|
|
if end > len(hashes) {
|
|
end = len(hashes)
|
|
}
|
|
hashBatches = append(hashBatches, hashes[i:end])
|
|
}
|
|
|
|
for _, batch := range hashBatches {
|
|
|
|
hashParams := ""
|
|
for _, hash := range batch {
|
|
hashParams += "/" + hash
|
|
}
|
|
|
|
resp, err := t.doQuery("GET", t.baseUrl+"/torrents/instantAvailability"+hashParams, nil, "application/json")
|
|
if err != nil {
|
|
t.logger.Error().Err(err).Msg("realdebrid: Failed to get instant availability")
|
|
return
|
|
}
|
|
|
|
//fmt.Println(string(resp))
|
|
|
|
var instantAvailability instantAvailabilityResponse
|
|
err = json.Unmarshal(resp, &instantAvailability)
|
|
if err != nil {
|
|
if limit != 1 && retries > 0 {
|
|
t.logger.Warn().Msg("realdebrid: Retrying instant availability request")
|
|
return t.getInstantAvailabilityT(hashes, retries-1, int(math.Ceil(float64(limit)/10)))
|
|
} else {
|
|
t.logger.Error().Err(err).Msg("realdebrid: Failed to parse instant availability")
|
|
return
|
|
}
|
|
}
|
|
|
|
for hash, hosters := range instantAvailability {
|
|
currentHash := ""
|
|
for _, _hash := range hashes {
|
|
if strings.EqualFold(hash, _hash) {
|
|
currentHash = _hash
|
|
break
|
|
}
|
|
}
|
|
|
|
if currentHash == "" {
|
|
continue
|
|
}
|
|
|
|
avail := debrid.TorrentItemInstantAvailability{
|
|
CachedFiles: make(map[string]*debrid.CachedFile),
|
|
}
|
|
|
|
for hoster, hosterI := range hosters {
|
|
if hoster != "rd" {
|
|
continue
|
|
}
|
|
|
|
for _, hosterFiles := range hosterI {
|
|
for fileId, file := range hosterFiles {
|
|
avail.CachedFiles[strconv.Itoa(fileId)] = &debrid.CachedFile{
|
|
Name: file.Filename,
|
|
Size: file.Filesize,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(avail.CachedFiles) > 0 {
|
|
ret[currentHash] = avail
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (t *RealDebrid) AddTorrent(opts debrid.AddTorrentOptions) (string, error) {
|
|
|
|
// Check if the torrent is already added
|
|
// If it is, return the torrent ID
|
|
torrentId := ""
|
|
if opts.InfoHash != "" {
|
|
torrents, err := t.getTorrents(false)
|
|
if err == nil {
|
|
for _, torrent := range torrents {
|
|
if torrent.Hash == opts.InfoHash {
|
|
t.logger.Debug().Str("torrentId", torrent.ID).Msg("realdebrid: Torrent already added")
|
|
torrentId = torrent.ID
|
|
break
|
|
}
|
|
}
|
|
}
|
|
time.Sleep(1 * time.Second)
|
|
}
|
|
|
|
// If the torrent wasn't already added, add it
|
|
if torrentId == "" {
|
|
resp, err := t.addMagnet(opts.MagnetLink)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
torrentId = resp.ID
|
|
}
|
|
|
|
// If a file ID is provided, select the file to start downloading it
|
|
if opts.SelectFileId != "" {
|
|
// Select the file to download
|
|
err := t.selectCachedFiles(torrentId, opts.SelectFileId)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
return torrentId, nil
|
|
}
|
|
|
|
// GetTorrentStreamUrl blocks until the torrent is downloaded and returns the stream URL for the torrent file by calling GetTorrentDownloadUrl.
|
|
func (t *RealDebrid) GetTorrentStreamUrl(ctx context.Context, opts debrid.StreamTorrentOptions, itemCh chan debrid.TorrentItem) (streamUrl string, err error) {
|
|
|
|
t.logger.Trace().Str("torrentId", opts.ID).Str("fileId", opts.FileId).Msg("realdebrid: Retrieving stream link")
|
|
|
|
doneCh := make(chan struct{})
|
|
|
|
go func(ctx context.Context) {
|
|
defer func() {
|
|
close(doneCh)
|
|
}()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
err = ctx.Err()
|
|
return
|
|
case <-time.After(4 * time.Second):
|
|
ti, _err := t.getTorrentInfo(opts.ID)
|
|
if _err != nil {
|
|
t.logger.Error().Err(_err).Msg("realdebrid: Failed to get torrent")
|
|
err = fmt.Errorf("realdebrid: Failed to get torrent: %w", _err)
|
|
return
|
|
}
|
|
|
|
dt := toDebridTorrent(&Torrent{
|
|
ID: ti.ID,
|
|
Filename: ti.Filename,
|
|
Hash: ti.Hash,
|
|
Bytes: ti.Bytes,
|
|
Host: ti.Host,
|
|
Split: ti.Split,
|
|
Progress: ti.Progress,
|
|
Status: ti.Status,
|
|
Added: ti.Added,
|
|
Links: ti.Links,
|
|
Ended: ti.Ended,
|
|
Speed: ti.Speed,
|
|
Seeders: ti.Seeders,
|
|
})
|
|
itemCh <- *dt
|
|
|
|
// Check if the torrent is ready
|
|
if dt.IsReady {
|
|
time.Sleep(1 * time.Second)
|
|
|
|
files := make([]*TorrentInfoFile, 0)
|
|
for _, f := range ti.Files {
|
|
if f.Selected == 1 {
|
|
files = append(files, f)
|
|
}
|
|
}
|
|
|
|
if len(files) == 0 {
|
|
err = fmt.Errorf("realdebrid: No files downloaded")
|
|
return
|
|
}
|
|
|
|
for idx, f := range files {
|
|
if strconv.Itoa(f.ID) == opts.FileId {
|
|
resp, err := t.unrestrictLink(ti.Links[idx])
|
|
if err != nil {
|
|
t.logger.Error().Err(err).Msg("realdebrid: Failed to get download URL")
|
|
return
|
|
}
|
|
|
|
streamUrl = resp.Download
|
|
return
|
|
}
|
|
}
|
|
err = fmt.Errorf("realdebrid: File not found")
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}(ctx)
|
|
|
|
<-doneCh
|
|
|
|
return
|
|
}
|
|
|
|
type unrestrictLinkResponse struct {
|
|
ID string `json:"id"`
|
|
Filename string `json:"filename"`
|
|
MimeType string `json:"mimeType"`
|
|
Filesize int64 `json:"filesize"`
|
|
Link string `json:"link"`
|
|
Host string `json:"host"`
|
|
Chunks int `json:"chunks"`
|
|
Crc int `json:"crc"`
|
|
Download string `json:"download"` // Generated download link
|
|
Streamable int `json:"streamable"`
|
|
}
|
|
|
|
// GetTorrentDownloadUrl returns the download URL for the torrent file.
|
|
// If no opts.FileId is provided, it will return a comma-separated list of download URLs for all selected files in the torrent.
|
|
func (t *RealDebrid) GetTorrentDownloadUrl(opts debrid.DownloadTorrentOptions) (downloadUrl string, err error) {
|
|
|
|
t.logger.Trace().Str("torrentId", opts.ID).Msg("realdebrid: Retrieving download link")
|
|
|
|
torrentInfo, err := t.getTorrentInfo(opts.ID)
|
|
if err != nil {
|
|
return "", fmt.Errorf("realdebrid: Failed to get download URL: %w", err)
|
|
}
|
|
|
|
files := make([]*TorrentInfoFile, 0)
|
|
for _, f := range torrentInfo.Files {
|
|
if f.Selected == 1 {
|
|
files = append(files, f)
|
|
}
|
|
}
|
|
|
|
downloadUrl = ""
|
|
|
|
if opts.FileId != "" {
|
|
var file *TorrentInfoFile
|
|
var link string
|
|
for idx, f := range files {
|
|
if strconv.Itoa(f.ID) == opts.FileId {
|
|
file = f
|
|
link = torrentInfo.Links[idx]
|
|
break
|
|
}
|
|
}
|
|
|
|
if file == nil || link == "" {
|
|
return "", fmt.Errorf("realdebrid: File not found")
|
|
}
|
|
|
|
unrestrictLink, err := t.unrestrictLink(link)
|
|
if err != nil {
|
|
return "", fmt.Errorf("realdebrid: Failed to get download URL: %w", err)
|
|
}
|
|
|
|
return unrestrictLink.Download, nil
|
|
}
|
|
|
|
for idx := range files {
|
|
link := torrentInfo.Links[idx]
|
|
unrestrictLink, err := t.unrestrictLink(link)
|
|
if err != nil {
|
|
return "", fmt.Errorf("realdebrid: Failed to get download URL: %w", err)
|
|
}
|
|
if downloadUrl != "" {
|
|
downloadUrl += ","
|
|
}
|
|
downloadUrl += unrestrictLink.Download
|
|
}
|
|
|
|
return downloadUrl, nil
|
|
}
|
|
|
|
func (t *RealDebrid) GetTorrent(id string) (ret *debrid.TorrentItem, err error) {
|
|
torrent, err := t.getTorrent(id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ret = toDebridTorrent(torrent)
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
// GetTorrentInfo uses the info hash to return the torrent's data.
|
|
// This adds the torrent to the user's account without downloading it and removes it after getting the info.
|
|
func (t *RealDebrid) GetTorrentInfo(opts debrid.GetTorrentInfoOptions) (ret *debrid.TorrentInfo, err error) {
|
|
|
|
if opts.MagnetLink == "" {
|
|
return nil, fmt.Errorf("realdebrid: Magnet link is required")
|
|
}
|
|
|
|
// Add the torrent to the user's account without downloading it
|
|
resp, err := t.addMagnet(opts.MagnetLink)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("realdebrid: Failed to get info: %w", err)
|
|
}
|
|
|
|
torrent, err := t.getTorrentInfo(resp.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
go func() {
|
|
// Remove the torrent
|
|
err = t.DeleteTorrent(torrent.ID)
|
|
if err != nil {
|
|
t.logger.Error().Err(err).Msg("realdebrid: Failed to delete torrent")
|
|
}
|
|
}()
|
|
|
|
ret = toDebridTorrentInfo(torrent)
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func (t *RealDebrid) GetTorrents() (ret []*debrid.TorrentItem, err error) {
|
|
|
|
torrents, err := t.getTorrents(true)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("realdebrid: Failed to get torrents: %w", err)
|
|
}
|
|
|
|
for _, t := range torrents {
|
|
ret = append(ret, toDebridTorrent(t))
|
|
}
|
|
|
|
slices.SortFunc(ret, func(i, j *debrid.TorrentItem) int {
|
|
return cmp.Compare(j.AddedAt, i.AddedAt)
|
|
})
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// selectCachedFiles
|
|
// Real Debrid will re-download cached torrent if we select only a few files from the torrent.
|
|
// To avoid this, we'll select all *cached* files in the torrent if the file we want to download is cached.
|
|
func (t *RealDebrid) selectCachedFiles(id string, idStr string) (err error) {
|
|
|
|
t.logger.Trace().Str("torrentId", id).Str("fileId", "all").Msg("realdebrid: Selecting all files")
|
|
|
|
return t._selectFiles(id, "all")
|
|
|
|
//t.logger.Trace().Str("torrentId", id).Str("fileId", idStr).Msg("realdebrid: Selecting cached files")
|
|
//// If the file ID is "all" or a list of IDs, just call selectFiles
|
|
//if idStr == "all" || strings.Contains(idStr, ",") {
|
|
// return t._selectFiles(id, idStr)
|
|
//}
|
|
//
|
|
//// Get the torrent info
|
|
//torrent, err := t.getTorrent(id)
|
|
//if err != nil {
|
|
// return err
|
|
//}
|
|
//
|
|
//// Get the instant availability
|
|
//avail := t.GetInstantAvailability([]string{torrent.Hash})
|
|
//if _, ok := avail[torrent.Hash]; !ok {
|
|
// return t._selectFiles(id, idStr)
|
|
//}
|
|
//
|
|
//// Get all cached file IDs
|
|
//ids := make([]string, 0)
|
|
//for fileIdStr := range avail[torrent.Hash].CachedFiles {
|
|
// if fileIdStr != "" {
|
|
// ids = append(ids, fileIdStr)
|
|
// }
|
|
//}
|
|
//
|
|
//// If the selected file isn't cached, we'll just download it alone
|
|
//if !slices.Contains(ids, idStr) {
|
|
// return t._selectFiles(id, idStr)
|
|
//}
|
|
//
|
|
//// Download all cached files
|
|
//return t._selectFiles(id, strings.Join(ids, ","))
|
|
}
|
|
|
|
func (t *RealDebrid) _selectFiles(id string, idStr string) (err error) {
|
|
|
|
var body bytes.Buffer
|
|
writer := multipart.NewWriter(&body)
|
|
|
|
t.logger.Trace().Str("torrentId", id).Str("fileId", idStr).Msg("realdebrid: Selecting files")
|
|
|
|
err = writer.WriteField("files", idStr)
|
|
if err != nil {
|
|
t.logger.Error().Err(err).Msg("realdebrid: Failed to write field 'files'")
|
|
return fmt.Errorf("realdebrid: Failed to select files: %w", err)
|
|
}
|
|
|
|
_, err = t.doQuery("POST", t.baseUrl+fmt.Sprintf("/torrents/selectFiles/%s", id), &body, writer.FormDataContentType())
|
|
if err != nil {
|
|
t.logger.Error().Err(err).Msg("realdebrid: Failed to select files")
|
|
return fmt.Errorf("realdebrid: Failed to select files: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type addMagnetResponse struct {
|
|
ID string `json:"id"`
|
|
URI string `json:"uri"`
|
|
}
|
|
|
|
func (t *RealDebrid) addMagnet(magnet string) (ret *addMagnetResponse, err error) {
|
|
|
|
var body bytes.Buffer
|
|
writer := multipart.NewWriter(&body)
|
|
|
|
t.logger.Trace().Str("magnetLink", magnet).Msg("realdebrid: Adding torrent")
|
|
|
|
err = writer.WriteField("magnet", magnet)
|
|
if err != nil {
|
|
t.logger.Error().Err(err).Msg("realdebrid: Failed to write field 'magnet'")
|
|
return nil, fmt.Errorf("torbox: Failed to add torrent: %w", err)
|
|
}
|
|
|
|
resp, err := t.doQuery("POST", t.baseUrl+"/torrents/addMagnet", &body, writer.FormDataContentType())
|
|
if err != nil {
|
|
t.logger.Error().Err(err).Msg("realdebrid: Failed to add torrent")
|
|
return nil, fmt.Errorf("realdebrid: Failed to add torrent: %w", err)
|
|
}
|
|
|
|
err = json.Unmarshal(resp, &ret)
|
|
if err != nil {
|
|
t.logger.Error().Err(err).Msg("realdebrid: Failed to parse torrent")
|
|
return nil, fmt.Errorf("realdebrid: Failed to parse torrent: %w", err)
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func (t *RealDebrid) unrestrictLink(link string) (ret *unrestrictLinkResponse, err error) {
|
|
|
|
var body bytes.Buffer
|
|
writer := multipart.NewWriter(&body)
|
|
|
|
err = writer.WriteField("link", link)
|
|
if err != nil {
|
|
t.logger.Error().Err(err).Msg("realdebrid: Failed to write field 'link'")
|
|
return nil, fmt.Errorf("realdebrid: Failed to unrestrict link: %w", err)
|
|
}
|
|
|
|
resp, err := t.doQuery("POST", t.baseUrl+"/unrestrict/link", &body, writer.FormDataContentType())
|
|
if err != nil {
|
|
t.logger.Error().Err(err).Msg("realdebrid: Failed to unrestrict link")
|
|
return nil, fmt.Errorf("realdebrid: Failed to unrestrict link: %w", err)
|
|
}
|
|
|
|
err = json.Unmarshal(resp, &ret)
|
|
if err != nil {
|
|
t.logger.Error().Err(err).Msg("realdebrid: Failed to parse unrestrict link")
|
|
return nil, fmt.Errorf("realdebrid: Failed to parse unrestrict link: %w", err)
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func (t *RealDebrid) getTorrents(activeOnly bool) (ret []*Torrent, err error) {
|
|
_url, _ := url.Parse(t.baseUrl + "/torrents")
|
|
q := _url.Query()
|
|
if activeOnly {
|
|
q.Set("filter", "active")
|
|
} else {
|
|
q.Set("limit", "500")
|
|
}
|
|
|
|
resp, err := t.doQuery("GET", _url.String(), nil, "application/json")
|
|
if err != nil {
|
|
t.logger.Error().Err(err).Msg("realdebrid: Failed to get torrents")
|
|
return nil, fmt.Errorf("realdebrid: Failed to get torrents: %w", err)
|
|
}
|
|
|
|
err = json.Unmarshal(resp, &ret)
|
|
if err != nil {
|
|
t.logger.Error().Err(err).Msg("realdebrid: Failed to parse torrents")
|
|
return nil, fmt.Errorf("realdebrid: Failed to parse torrents: %w", err)
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func (t *RealDebrid) getTorrent(id string) (ret *Torrent, err error) {
|
|
|
|
resp, err := t.doQuery("GET", t.baseUrl+fmt.Sprintf("/torrents/info/%s", id), nil, "application/json")
|
|
if err != nil {
|
|
t.logger.Error().Err(err).Msg("realdebrid: Failed to get torrent")
|
|
return nil, fmt.Errorf("realdebrid: Failed to get torrent: %w", err)
|
|
}
|
|
|
|
var ti TorrentInfo
|
|
|
|
err = json.Unmarshal(resp, &ti)
|
|
if err != nil {
|
|
t.logger.Error().Err(err).Msg("realdebrid: Failed to parse torrent")
|
|
return nil, fmt.Errorf("realdebrid: Failed to parse torrent: %w", err)
|
|
}
|
|
|
|
ret = &Torrent{
|
|
ID: ti.ID,
|
|
Filename: ti.Filename,
|
|
Hash: ti.Hash,
|
|
Bytes: ti.Bytes,
|
|
Host: ti.Host,
|
|
Split: ti.Split,
|
|
Progress: ti.Progress,
|
|
Status: ti.Status,
|
|
Added: ti.Added,
|
|
Links: ti.Links,
|
|
Ended: ti.Ended,
|
|
Speed: ti.Speed,
|
|
Seeders: ti.Seeders,
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func (t *RealDebrid) getTorrentInfo(id string) (ret *TorrentInfo, err error) {
|
|
|
|
resp, err := t.doQuery("GET", t.baseUrl+fmt.Sprintf("/torrents/info/%s", id), nil, "application/json")
|
|
if err != nil {
|
|
t.logger.Error().Err(err).Msg("realdebrid: Failed to get torrent")
|
|
return nil, fmt.Errorf("realdebrid: Failed to get torrent: %w", err)
|
|
}
|
|
|
|
err = json.Unmarshal(resp, &ret)
|
|
if err != nil {
|
|
t.logger.Error().Err(err).Msg("realdebrid: Failed to parse torrent")
|
|
return nil, fmt.Errorf("realdebrid: Failed to parse torrent: %w", err)
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func toDebridTorrent(t *Torrent) (ret *debrid.TorrentItem) {
|
|
|
|
status := toDebridTorrentStatus(t)
|
|
|
|
ret = &debrid.TorrentItem{
|
|
ID: t.ID,
|
|
Name: t.Filename,
|
|
Hash: t.Hash,
|
|
Size: t.Bytes,
|
|
FormattedSize: util.Bytes(uint64(t.Bytes)),
|
|
CompletionPercentage: int(t.Progress),
|
|
ETA: "",
|
|
Status: status,
|
|
AddedAt: t.Added,
|
|
Speed: util.ToHumanReadableSpeed(int(t.Speed)),
|
|
Seeders: t.Seeders,
|
|
IsReady: status == debrid.TorrentItemStatusCompleted,
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func toDebridTorrentInfo(t *TorrentInfo) (ret *debrid.TorrentInfo) {
|
|
|
|
var files []*debrid.TorrentItemFile
|
|
for _, f := range t.Files {
|
|
name := filepath.Base(f.Path)
|
|
|
|
files = append(files, &debrid.TorrentItemFile{
|
|
ID: strconv.Itoa(f.ID),
|
|
Index: f.ID,
|
|
Name: name, // e.g. "Big Buck Bunny.mp4"
|
|
Path: f.Path, // e.g. "/Big Buck Bunny/Big Buck Bunny.mp4"
|
|
Size: f.Bytes,
|
|
})
|
|
}
|
|
|
|
ret = &debrid.TorrentInfo{
|
|
ID: &t.ID,
|
|
Name: t.Filename,
|
|
Hash: t.Hash,
|
|
Size: t.OriginalBytes,
|
|
Files: files,
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func toDebridTorrentStatus(t *Torrent) debrid.TorrentItemStatus {
|
|
switch t.Status {
|
|
case "downloading", "queued":
|
|
return debrid.TorrentItemStatusDownloading
|
|
case "waiting_files_selection", "magnet_conversion":
|
|
return debrid.TorrentItemStatusStalled
|
|
case "downloaded", "dead":
|
|
return debrid.TorrentItemStatusCompleted
|
|
case "uploading":
|
|
return debrid.TorrentItemStatusSeeding
|
|
case "paused":
|
|
return debrid.TorrentItemStatusPaused
|
|
default:
|
|
return debrid.TorrentItemStatusOther
|
|
}
|
|
}
|
|
|
|
func (t *RealDebrid) DeleteTorrent(id string) error {
|
|
|
|
_, err := t.doQuery("DELETE", t.baseUrl+fmt.Sprintf("/torrents/delete/%s", id), nil, "application/json")
|
|
if err != nil {
|
|
t.logger.Error().Err(err).Msg("realdebrid: Failed to delete torrent")
|
|
return fmt.Errorf("realdebrid: Failed to delete torrent: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|