node build fixed
This commit is contained in:
811
seanime-2.9.10/internal/debrid/realdebrid/realdebrid.go
Normal file
811
seanime-2.9.10/internal/debrid/realdebrid/realdebrid.go
Normal file
@@ -0,0 +1,811 @@
|
||||
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
|
||||
}
|
||||
150
seanime-2.9.10/internal/debrid/realdebrid/realdebrid_test.go
Normal file
150
seanime-2.9.10/internal/debrid/realdebrid/realdebrid_test.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package realdebrid
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/require"
|
||||
"seanime/internal/debrid/debrid"
|
||||
"seanime/internal/test_utils"
|
||||
"seanime/internal/util"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTorBox_GetTorrents(t *testing.T) {
|
||||
test_utils.InitTestProvider(t)
|
||||
logger := util.NewLogger()
|
||||
|
||||
rd := NewRealDebrid(logger)
|
||||
|
||||
err := rd.Authenticate(test_utils.ConfigData.Provider.RealDebridApiKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
fmt.Println("=== All torrents ===")
|
||||
|
||||
torrents, err := rd.GetTorrents()
|
||||
require.NoError(t, err)
|
||||
|
||||
util.Spew(torrents)
|
||||
}
|
||||
|
||||
func TestTorBox_AddTorrent(t *testing.T) {
|
||||
t.Skip("Skipping test that adds a torrent to RealDebrid")
|
||||
|
||||
test_utils.InitTestProvider(t)
|
||||
|
||||
// Already added
|
||||
magnet := "magnet:?xt=urn:btih:80431b4f9a12f4e06616062d3d3973b9ef99b5e6&dn=%5BSubsPlease%5D%20Bocchi%20the%20Rock%21%20-%2001%20%281080p%29%20%5BE04F4EFB%5D.mkv&tr=http%3A%2F%2Fnyaa.tracker.wf%3A7777%2Fannounce&tr=udp%3A%2F%2Fopen.stealth.si%3A80%2Fannounce&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337%2Fannounce&tr=udp%3A%2F%2Fexodus.desync.com%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.torrent.eu.org%3A451%2Fannounce"
|
||||
|
||||
logger := util.NewLogger()
|
||||
|
||||
rd := NewRealDebrid(logger)
|
||||
|
||||
err := rd.Authenticate(test_utils.ConfigData.Provider.RealDebridApiKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
torrentId, err := rd.AddTorrent(debrid.AddTorrentOptions{
|
||||
MagnetLink: magnet,
|
||||
InfoHash: "80431b4f9a12f4e06616062d3d3973b9ef99b5e6",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
torrentId2, err := rd.AddTorrent(debrid.AddTorrentOptions{
|
||||
MagnetLink: magnet,
|
||||
InfoHash: "80431b4f9a12f4e06616062d3d3973b9ef99b5e6",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, torrentId, torrentId2)
|
||||
|
||||
fmt.Println(torrentId)
|
||||
}
|
||||
|
||||
func TestTorBox_getTorrentInfo(t *testing.T) {
|
||||
|
||||
test_utils.InitTestProvider(t)
|
||||
|
||||
logger := util.NewLogger()
|
||||
|
||||
rd := NewRealDebridT(logger)
|
||||
|
||||
err := rd.Authenticate(test_utils.ConfigData.Provider.RealDebridApiKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
ti, err := rd.getTorrentInfo("W3IWF5TX3AE6G")
|
||||
require.NoError(t, err)
|
||||
|
||||
util.Spew(ti)
|
||||
}
|
||||
|
||||
func TestTorBox_GetDownloadUrl(t *testing.T) {
|
||||
|
||||
test_utils.InitTestProvider(t)
|
||||
|
||||
logger := util.NewLogger()
|
||||
|
||||
rd := NewRealDebridT(logger)
|
||||
|
||||
err := rd.Authenticate(test_utils.ConfigData.Provider.RealDebridApiKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
urls, err := rd.GetTorrentDownloadUrl(debrid.DownloadTorrentOptions{
|
||||
ID: "W3IWF5TX3AE6G",
|
||||
FileId: "11",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
util.Spew(strings.Split(urls, ","))
|
||||
}
|
||||
|
||||
func TestTorBox_InstantAvailability(t *testing.T) {
|
||||
|
||||
test_utils.InitTestProvider(t)
|
||||
|
||||
logger := util.NewLogger()
|
||||
|
||||
rd := NewRealDebridT(logger)
|
||||
|
||||
err := rd.Authenticate(test_utils.ConfigData.Provider.RealDebridApiKey)
|
||||
require.NoError(t, err)
|
||||
avail := rd.GetInstantAvailability([]string{"9f4961a9c71eeb53abce2ef2afc587b452dee5eb"})
|
||||
require.NoError(t, err)
|
||||
|
||||
util.Spew(avail)
|
||||
}
|
||||
|
||||
func TestTorBox_ChooseFileAndDownload(t *testing.T) {
|
||||
//t.Skip("Skipping test that adds a torrent to RealDebrid")
|
||||
|
||||
test_utils.InitTestProvider(t)
|
||||
|
||||
magnet := "magnet:?xt=urn:btih:80431b4f9a12f4e06616062d3d3973b9ef99b5e6&dn=%5BSubsPlease%5D%20Bocchi%20the%20Rock%21%20-%2001%20%281080p%29%20%5BE04F4EFB%5D.mkv&tr=http%3A%2F%2Fnyaa.tracker.wf%3A7777%2Fannounce&tr=udp%3A%2F%2Fopen.stealth.si%3A80%2Fannounce&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337%2Fannounce&tr=udp%3A%2F%2Fexodus.desync.com%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.torrent.eu.org%3A451%2Fannounce"
|
||||
|
||||
logger := util.NewLogger()
|
||||
|
||||
rd := NewRealDebrid(logger)
|
||||
|
||||
err := rd.Authenticate(test_utils.ConfigData.Provider.RealDebridApiKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should add the torrent and get the torrent info
|
||||
torrentInfo, err := rd.GetTorrentInfo(debrid.GetTorrentInfoOptions{
|
||||
MagnetLink: magnet,
|
||||
InfoHash: "80431b4f9a12f4e06616062d3d3973b9ef99b5e6",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// The torrent should have one file
|
||||
require.Len(t, torrentInfo.Files, 1)
|
||||
|
||||
file := torrentInfo.Files[0]
|
||||
|
||||
// Download the file
|
||||
resp, err := rd.AddTorrent(debrid.AddTorrentOptions{
|
||||
MagnetLink: magnet,
|
||||
InfoHash: "80431b4f9a12f4e06616062d3d3973b9ef99b5e6",
|
||||
SelectFileId: file.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
util.Spew(resp)
|
||||
}
|
||||
Reference in New Issue
Block a user