Features:

- Add Torbox(Tested)
- Fix RD cache check
- Minor fixes
This commit is contained in:
Mukhtar Akere
2024-11-23 19:52:15 +01:00
parent ff74e279d9
commit d2a77620bc
17 changed files with 847 additions and 111 deletions

View File

@@ -50,16 +50,29 @@ Download the binary from the releases page and run it with the config file.
#### Config
```json
{
"debrid": {
"name": "realdebrid",
"host": "https://api.real-debrid.com/rest/1.0",
"api_key": "realdebrid_api_key",
"folder": "data/realdebrid/torrents/",
"rate_limit": "250/minute"
},
"debrids": [
{
"name": "torbox",
"host": "https://api.torbox.app/v1",
"api_key": "torbox_api_key",
"folder": "data/realdebrid/torrents/",
"rate_limit": "250/minute",
"download_uncached": false,
"check_uncached": true
},
{
"name": "realdebrid",
"host": "https://api.real-debrid.com/rest/1.0",
"api_key": "realdebrid_key",
"folder": "data/realdebrid/torrents/",
"rate_limit": "250/minute",
"download_uncached": false,
"check_uncached": false
}
],
"proxy": {
"enabled": true,
"port": "8181",
"port": "8100",
"debug": false,
"username": "username",
"password": "password",
@@ -68,11 +81,13 @@ Download the binary from the releases page and run it with the config file.
"max_cache_size": 1000,
"qbittorrent": {
"port": "8282",
"username": "admin", // deprecated
"password": "admin", // deprecated
"download_folder": "/media/symlinks/",
"categories": ["sonarr", "radarr"],
"refresh_interval": 5 // in seconds
"categories": [
"sonarr",
"radarr"
],
"debug": true,
"refresh_interval": 10
}
}
```

View File

@@ -13,7 +13,7 @@ func Start(config *common.Config) {
maxCacheSize := cmp.Or(config.MaxCacheSize, 1000)
cache := common.NewCache(maxCacheSize)
deb := debrid.NewDebrid(config.Debrid, cache)
deb := debrid.NewDebrid(config.Debrids, cache)
var wg sync.WaitGroup

View File

@@ -12,6 +12,7 @@ type DebridConfig struct {
APIKey string `json:"api_key"`
Folder string `json:"folder"`
DownloadUncached bool `json:"download_uncached"`
CheckCached bool `json:"check_cached"`
RateLimit string `json:"rate_limit"` // 200/minute or 10/second
}
@@ -36,6 +37,7 @@ type QBitTorrentConfig struct {
type Config struct {
Debrid DebridConfig `json:"debrid"`
Debrids []DebridConfig `json:"debrids"`
Proxy ProxyConfig `json:"proxy"`
MaxCacheSize int `json:"max_cache_size"`
QBitTorrent QBitTorrentConfig `json:"qbittorrent"`
@@ -60,9 +62,9 @@ func LoadConfig(path string) (*Config, error) {
if err != nil {
return nil, err
}
if config.Proxy.CachedOnly == nil {
config.Proxy.CachedOnly = new(bool)
*config.Proxy.CachedOnly = true
if config.Debrid.Name != "" {
config.Debrids = append(config.Debrids, config.Debrid)
}
return config, nil

View File

@@ -60,11 +60,7 @@ func (c *RLHTTPClient) Do(req *http.Request) (*http.Response, error) {
return resp, fmt.Errorf("max retries exceeded")
}
func (c *RLHTTPClient) MakeRequest(method string, url string, body io.Reader) ([]byte, error) {
req, err := http.NewRequest(method, url, body)
if err != nil {
return nil, err
}
func (c *RLHTTPClient) MakeRequest(req *http.Request) ([]byte, error) {
if c.Headers != nil {
for key, value := range c.Headers {
req.Header.Set(key, value)
@@ -75,6 +71,7 @@ func (c *RLHTTPClient) MakeRequest(method string, url string, body io.Reader) ([
if err != nil {
return nil, err
}
b, _ := io.ReadAll(res.Body)
statusOk := strconv.Itoa(res.StatusCode)[0] == '2'
if !statusOk {
return nil, fmt.Errorf("unexpected status code: %d", res.StatusCode)
@@ -85,7 +82,7 @@ func (c *RLHTTPClient) MakeRequest(method string, url string, body io.Reader) ([
log.Println(err)
}
}(res.Body)
return io.ReadAll(res.Body)
return b, nil
}
func NewRLHTTPClient(rl *rate.Limiter, headers map[string]string) *RLHTTPClient {

View File

@@ -8,20 +8,8 @@ import (
"path/filepath"
)
type Service interface {
SubmitMagnet(torrent *Torrent) (*Torrent, error)
CheckStatus(torrent *Torrent, isSymlink bool) (*Torrent, error)
GetDownloadLinks(torrent *Torrent) error
DeleteTorrent(torrent *Torrent)
IsAvailable(infohashes []string) map[string]bool
GetMountPath() string
GetDownloadUncached() bool
GetTorrent(id string) (*Torrent, error)
GetName() string
GetLogger() *log.Logger
}
type Debrid struct {
type BaseDebrid struct {
Name string
Host string `json:"host"`
APIKey string
DownloadUncached bool
@@ -29,12 +17,41 @@ type Debrid struct {
cache *common.Cache
MountPath string
logger *log.Logger
CheckCached bool
}
func NewDebrid(dc common.DebridConfig, cache *common.Cache) Service {
type Service interface {
SubmitMagnet(torrent *Torrent) (*Torrent, error)
CheckStatus(torrent *Torrent, isSymlink bool) (*Torrent, error)
GetDownloadLinks(torrent *Torrent) error
DeleteTorrent(torrent *Torrent)
IsAvailable(infohashes []string) map[string]bool
GetMountPath() string
GetCheckCached() bool
GetTorrent(id string) (*Torrent, error)
GetName() string
GetLogger() *log.Logger
}
func NewDebrid(debs []common.DebridConfig, cache *common.Cache) *DebridService {
debrids := make([]Service, 0)
for _, dc := range debs {
d := createDebrid(dc, cache)
d.GetLogger().Println("Debrid Service started")
debrids = append(debrids, d)
}
d := &DebridService{debrids: debrids, lastUsed: 0}
return d
}
func createDebrid(dc common.DebridConfig, cache *common.Cache) Service {
switch dc.Name {
case "realdebrid":
return NewRealDebrid(dc, cache)
case "torbox":
return NewTorbox(dc, cache)
case "debridlink":
return NewDebridLink(dc, cache)
default:
return NewRealDebrid(dc, cache)
}
@@ -95,32 +112,31 @@ func getTorrentInfo(filePath string) (*Torrent, error) {
func GetLocalCache(infohashes []string, cache *common.Cache) ([]string, map[string]bool) {
result := make(map[string]bool)
hashes := make([]string, len(infohashes))
if len(infohashes) == 0 {
return hashes, result
}
if len(infohashes) == 1 {
if cache.Exists(infohashes[0]) {
return hashes, map[string]bool{infohashes[0]: true}
}
return infohashes, result
}
//if len(infohashes) == 0 {
// return hashes, result
//}
//if len(infohashes) == 1 {
// if cache.Exists(infohashes[0]) {
// return hashes, map[string]bool{infohashes[0]: true}
// }
// return infohashes, result
//}
//
//cachedHashes := cache.GetMultiple(infohashes)
//for _, h := range infohashes {
// _, exists := cachedHashes[h]
// if !exists {
// hashes = append(hashes, h)
// } else {
// result[h] = true
// }
//}
cachedHashes := cache.GetMultiple(infohashes)
for _, h := range infohashes {
_, exists := cachedHashes[h]
if !exists {
hashes = append(hashes, h)
} else {
result[h] = true
}
}
return hashes, result
return infohashes, result
}
func ProcessQBitTorrent(d Service, magnet *common.Magnet, arr *Arr, isSymlink bool) (*Torrent, error) {
func ProcessQBitTorrent(d *DebridService, magnet *common.Magnet, arr *Arr, isSymlink bool) (*Torrent, error) {
debridTorrent := &Torrent{
InfoHash: magnet.InfoHash,
Magnet: magnet,
@@ -128,21 +144,30 @@ func ProcessQBitTorrent(d Service, magnet *common.Magnet, arr *Arr, isSymlink bo
Arr: arr,
Size: magnet.Size,
}
logger := d.GetLogger()
logger.Printf("Torrent Hash: %s", debridTorrent.InfoHash)
if !d.GetDownloadUncached() {
hash, exists := d.IsAvailable([]string{debridTorrent.InfoHash})[debridTorrent.InfoHash]
if !exists || !hash {
return debridTorrent, fmt.Errorf("torrent: %s is not cached", debridTorrent.Name)
} else {
logger.Printf("Torrent: %s is cached(or downloading)", debridTorrent.Name)
}
}
debridTorrent, err := d.SubmitMagnet(debridTorrent)
if err != nil || debridTorrent.Id == "" {
logger.Printf("Error submitting magnet: %s", err)
return nil, err
for index, db := range d.debrids {
log.Println("Processing debrid: ", db.GetName())
logger := db.GetLogger()
logger.Printf("Torrent Hash: %s", debridTorrent.InfoHash)
if !db.GetCheckCached() {
hash, exists := db.IsAvailable([]string{debridTorrent.InfoHash})[debridTorrent.InfoHash]
if !exists || !hash {
logger.Printf("Torrent: %s is not cached", debridTorrent.Name)
continue
} else {
logger.Printf("Torrent: %s is cached(or downloading)", debridTorrent.Name)
}
}
debridTorrent, err := db.SubmitMagnet(debridTorrent)
if err != nil || debridTorrent.Id == "" {
logger.Printf("Error submitting magnet: %s", err)
continue
}
logger.Printf("Torrent: %s submitted to %s", debridTorrent.Name, db.GetName())
d.lastUsed = index
debridTorrent.Debrid = db
return db.CheckStatus(debridTorrent, isSymlink)
}
return d.CheckStatus(debridTorrent, isSymlink)
return nil, fmt.Errorf("failed to process torrent")
}

269
pkg/debrid/debrid_link.go Normal file
View File

@@ -0,0 +1,269 @@
package debrid
import (
"bytes"
"encoding/json"
"fmt"
"goBlack/common"
"goBlack/pkg/debrid/structs"
"log"
"net/http"
"os"
"strings"
)
type DebridLink struct {
BaseDebrid
}
func (r *DebridLink) GetMountPath() string {
return r.MountPath
}
func (r *DebridLink) GetName() string {
return r.Name
}
func (r *DebridLink) GetLogger() *log.Logger {
return r.logger
}
func (r *DebridLink) IsAvailable(infohashes []string) map[string]bool {
// Check if the infohashes are available in the local cache
hashes, result := GetLocalCache(infohashes, r.cache)
if len(hashes) == 0 {
// Either all the infohashes are locally cached or none are
r.cache.AddMultiple(result)
return result
}
// Divide hashes into groups of 100
for i := 0; i < len(hashes); i += 200 {
end := i + 200
if end > len(hashes) {
end = len(hashes)
}
// Filter out empty strings
validHashes := make([]string, 0, end-i)
for _, hash := range hashes[i:end] {
if hash != "" {
validHashes = append(validHashes, hash)
}
}
// If no valid hashes in this batch, continue to the next batch
if len(validHashes) == 0 {
continue
}
hashStr := strings.Join(validHashes, ",")
url := fmt.Sprintf("%s/seedbox/cached/%s", r.Host, hashStr)
req, _ := http.NewRequest(http.MethodGet, url, nil)
resp, err := r.client.MakeRequest(req)
if err != nil {
log.Println("Error checking availability:", err)
return result
}
var data structs.DebridLinkAvailableResponse
err = json.Unmarshal(resp, &data)
if err != nil {
log.Println("Error marshalling availability:", err)
return result
}
if data.Value == nil {
return result
}
value := *data.Value
for _, h := range hashes[i:end] {
_, exists := value[h]
if exists {
result[h] = true
}
}
}
r.cache.AddMultiple(result) // Add the results to the cache
return result
}
func (r *DebridLink) GetTorrent(id string) (*Torrent, error) {
torrent := &Torrent{}
url := fmt.Sprintf("%s/seedbox/list/?ids=%s", r.Host, id)
req, _ := http.NewRequest(http.MethodGet, url, nil)
resp, err := r.client.MakeRequest(req)
if err != nil {
return torrent, err
}
var res structs.DebridLinkTorrentInfo
err = json.Unmarshal(resp, &res)
if err != nil {
return torrent, err
}
if res.Success == false {
return torrent, fmt.Errorf("error getting torrent")
}
if res.Value == nil {
return torrent, fmt.Errorf("torrent not found")
}
dt := *res.Value
fmt.Printf("Length of dt: %d\n", len(dt))
fmt.Printf("Raw response: %+v\n", res)
if len(dt) == 0 {
return torrent, fmt.Errorf("torrent not found")
}
data := dt[0]
status := "downloading"
name := common.RemoveInvalidChars(data.Name)
torrent.Id = data.ID
torrent.Name = name
torrent.Bytes = data.TotalSize
torrent.Folder = name
torrent.Progress = data.DownloadPercent
torrent.Status = status
torrent.Speed = data.DownloadSpeed
torrent.Seeders = data.PeersConnected
torrent.Filename = name
torrent.OriginalFilename = name
files := make([]TorrentFile, len(data.Files))
for i, f := range data.Files {
files[i] = TorrentFile{
Id: f.ID,
Name: f.Name,
Size: f.Size,
}
}
torrent.Files = files
torrent.Debrid = r
return torrent, nil
}
func (r *DebridLink) SubmitMagnet(torrent *Torrent) (*Torrent, error) {
url := fmt.Sprintf("%s/seedbox/add", r.Host)
payload := map[string]string{"url": torrent.Magnet.Link}
jsonPayload, _ := json.Marshal(payload)
req, _ := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(jsonPayload))
resp, err := r.client.MakeRequest(req)
if err != nil {
return nil, err
}
var res structs.DebridLinkSubmitTorrentInfo
err = json.Unmarshal(resp, &res)
if err != nil {
return nil, err
}
if res.Success == false || res.Value == nil {
return nil, fmt.Errorf("error adding torrent")
}
data := *res.Value
status := "downloading"
log.Printf("Torrent: %s added with id: %s\n", torrent.Name, data.ID)
name := common.RemoveInvalidChars(data.Name)
torrent.Id = data.ID
torrent.Name = name
torrent.Bytes = data.TotalSize
torrent.Folder = name
torrent.Progress = data.DownloadPercent
torrent.Status = status
torrent.Speed = data.DownloadSpeed
torrent.Seeders = data.PeersConnected
torrent.Filename = name
torrent.OriginalFilename = name
files := make([]TorrentFile, len(data.Files))
for i, f := range data.Files {
files[i] = TorrentFile{
Id: f.ID,
Name: f.Name,
Size: f.Size,
Link: f.DownloadURL,
}
}
torrent.Files = files
torrent.Debrid = r
return torrent, nil
}
func (r *DebridLink) CheckStatus(torrent *Torrent, isSymlink bool) (*Torrent, error) {
for {
torrent, err := r.GetTorrent(torrent.Id)
if err != nil || torrent == nil {
return torrent, err
}
status := torrent.Status
if status == "error" || status == "dead" || status == "magnet_error" {
return torrent, fmt.Errorf("torrent: %s has error", torrent.Name)
} else if status == "downloaded" {
r.logger.Printf("Torrent: %s downloaded\n", torrent.Name)
if !isSymlink {
err = r.GetDownloadLinks(torrent)
if err != nil {
return torrent, err
}
}
break
} else if status == "downloading" {
if !r.DownloadUncached {
go r.DeleteTorrent(torrent)
return torrent, fmt.Errorf("torrent: %s not cached", torrent.Name)
}
// Break out of the loop if the torrent is downloading.
// This is necessary to prevent infinite loop since we moved to sync downloading and async processing
break
}
}
return torrent, nil
}
func (r *DebridLink) DeleteTorrent(torrent *Torrent) {
url := fmt.Sprintf("%s/seedbox/%s/remove", r.Host, torrent.Id)
req, _ := http.NewRequest(http.MethodDelete, url, nil)
_, err := r.client.MakeRequest(req)
if err == nil {
r.logger.Printf("Torrent: %s deleted\n", torrent.Name)
} else {
r.logger.Printf("Error deleting torrent: %s", err)
}
}
func (r *DebridLink) GetDownloadLinks(torrent *Torrent) error {
downloadLinks := make([]TorrentDownloadLinks, 0)
for _, f := range torrent.Files {
dl := TorrentDownloadLinks{
Link: f.Link,
Filename: f.Name,
}
downloadLinks = append(downloadLinks, dl)
}
torrent.DownloadLinks = downloadLinks
return nil
}
func (r *DebridLink) GetCheckCached() bool {
return r.CheckCached
}
func NewDebridLink(dc common.DebridConfig, cache *common.Cache) *DebridLink {
rl := common.ParseRateLimit(dc.RateLimit)
headers := map[string]string{
"Authorization": fmt.Sprintf("Bearer %s", dc.APIKey),
}
client := common.NewRLHTTPClient(rl, headers)
logger := common.NewLogger(dc.Name, os.Stdout)
return &DebridLink{
BaseDebrid: BaseDebrid{
Name: "debridlink",
Host: dc.Host,
APIKey: dc.APIKey,
DownloadUncached: dc.DownloadUncached,
client: client,
cache: cache,
MountPath: dc.Folder,
logger: logger,
CheckCached: dc.CheckCached,
},
}
}

View File

@@ -15,13 +15,7 @@ import (
)
type RealDebrid struct {
Host string `json:"host"`
APIKey string
DownloadUncached bool
client *common.RLHTTPClient
cache *common.Cache
MountPath string
logger *log.Logger
BaseDebrid
}
func (r *RealDebrid) GetMountPath() string {
@@ -29,7 +23,7 @@ func (r *RealDebrid) GetMountPath() string {
}
func (r *RealDebrid) GetName() string {
return "realdebrid"
return r.Name
}
func (r *RealDebrid) GetLogger() *log.Logger {
@@ -89,7 +83,8 @@ func (r *RealDebrid) IsAvailable(infohashes []string) map[string]bool {
hashStr := strings.Join(validHashes, "/")
url := fmt.Sprintf("%s/torrents/instantAvailability/%s", r.Host, hashStr)
resp, err := r.client.MakeRequest(http.MethodGet, url, nil)
req, _ := http.NewRequest(http.MethodGet, url, nil)
resp, err := r.client.MakeRequest(req)
if err != nil {
log.Println("Error checking availability:", err)
return result
@@ -117,7 +112,8 @@ func (r *RealDebrid) SubmitMagnet(torrent *Torrent) (*Torrent, error) {
"magnet": {torrent.Magnet.Link},
}
var data structs.RealDebridAddMagnetSchema
resp, err := r.client.MakeRequest(http.MethodPost, url, strings.NewReader(payload.Encode()))
req, _ := http.NewRequest(http.MethodPost, url, strings.NewReader(payload.Encode()))
resp, err := r.client.MakeRequest(req)
if err != nil {
return nil, err
}
@@ -131,7 +127,8 @@ func (r *RealDebrid) SubmitMagnet(torrent *Torrent) (*Torrent, error) {
func (r *RealDebrid) GetTorrent(id string) (*Torrent, error) {
torrent := &Torrent{}
url := fmt.Sprintf("%s/torrents/info/%s", r.Host, id)
resp, err := r.client.MakeRequest(http.MethodGet, url, nil)
req, _ := http.NewRequest(http.MethodGet, url, nil)
resp, err := r.client.MakeRequest(req)
if err != nil {
return torrent, err
}
@@ -152,6 +149,7 @@ func (r *RealDebrid) GetTorrent(id string) (*Torrent, error) {
torrent.Filename = data.Filename
torrent.OriginalFilename = data.OriginalFilename
torrent.Links = data.Links
torrent.Debrid = r
files := GetTorrentFiles(data)
torrent.Files = files
return torrent, nil
@@ -159,8 +157,9 @@ func (r *RealDebrid) GetTorrent(id string) (*Torrent, error) {
func (r *RealDebrid) CheckStatus(torrent *Torrent, isSymlink bool) (*Torrent, error) {
url := fmt.Sprintf("%s/torrents/info/%s", r.Host, torrent.Id)
req, _ := http.NewRequest(http.MethodGet, url, nil)
for {
resp, err := r.client.MakeRequest(http.MethodGet, url, nil)
resp, err := r.client.MakeRequest(req)
if err != nil {
log.Println("ERROR Checking file: ", err)
return torrent, err
@@ -179,6 +178,7 @@ func (r *RealDebrid) CheckStatus(torrent *Torrent, isSymlink bool) (*Torrent, er
torrent.Seeders = data.Seeders
torrent.Links = data.Links
torrent.Status = status
torrent.Debrid = r
if status == "error" || status == "dead" || status == "magnet_error" {
return torrent, fmt.Errorf("torrent: %s has error", torrent.Name)
} else if status == "waiting_files_selection" {
@@ -195,7 +195,8 @@ func (r *RealDebrid) CheckStatus(torrent *Torrent, isSymlink bool) (*Torrent, er
"files": {strings.Join(filesId, ",")},
}
payload := strings.NewReader(p.Encode())
_, err = r.client.MakeRequest(http.MethodPost, fmt.Sprintf("%s/torrents/selectFiles/%s", r.Host, torrent.Id), payload)
req, _ := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/torrents/selectFiles/%s", r.Host, torrent.Id), payload)
_, err = r.client.MakeRequest(req)
if err != nil {
return torrent, err
}
@@ -209,7 +210,6 @@ func (r *RealDebrid) CheckStatus(torrent *Torrent, isSymlink bool) (*Torrent, er
return torrent, err
}
}
break
} else if status == "downloading" {
if !r.DownloadUncached {
@@ -227,7 +227,8 @@ func (r *RealDebrid) CheckStatus(torrent *Torrent, isSymlink bool) (*Torrent, er
func (r *RealDebrid) DeleteTorrent(torrent *Torrent) {
url := fmt.Sprintf("%s/torrents/delete/%s", r.Host, torrent.Id)
_, err := r.client.MakeRequest(http.MethodDelete, url, nil)
req, _ := http.NewRequest(http.MethodDelete, url, nil)
_, err := r.client.MakeRequest(req)
if err == nil {
r.logger.Printf("Torrent: %s deleted\n", torrent.Name)
} else {
@@ -245,7 +246,8 @@ func (r *RealDebrid) GetDownloadLinks(torrent *Torrent) error {
payload := gourl.Values{
"link": {link},
}
resp, err := r.client.MakeRequest(http.MethodPost, url, strings.NewReader(payload.Encode()))
req, _ := http.NewRequest(http.MethodPost, url, strings.NewReader(payload.Encode()))
resp, err := r.client.MakeRequest(req)
if err != nil {
return err
}
@@ -264,8 +266,8 @@ func (r *RealDebrid) GetDownloadLinks(torrent *Torrent) error {
return nil
}
func (r *RealDebrid) GetDownloadUncached() bool {
return r.DownloadUncached
func (r *RealDebrid) GetCheckCached() bool {
return r.CheckCached
}
func NewRealDebrid(dc common.DebridConfig, cache *common.Cache) *RealDebrid {
@@ -276,12 +278,16 @@ func NewRealDebrid(dc common.DebridConfig, cache *common.Cache) *RealDebrid {
client := common.NewRLHTTPClient(rl, headers)
logger := common.NewLogger(dc.Name, os.Stdout)
return &RealDebrid{
Host: dc.Host,
APIKey: dc.APIKey,
DownloadUncached: dc.DownloadUncached,
client: client,
cache: cache,
MountPath: dc.Folder,
logger: logger,
BaseDebrid: BaseDebrid{
Name: "realdebrid",
Host: dc.Host,
APIKey: dc.APIKey,
DownloadUncached: dc.DownloadUncached,
client: client,
cache: cache,
MountPath: dc.Folder,
logger: logger,
CheckCached: dc.CheckCached,
},
}
}

13
pkg/debrid/service.go Normal file
View File

@@ -0,0 +1,13 @@
package debrid
type DebridService struct {
debrids []Service
lastUsed int
}
func (d *DebridService) Get() Service {
if d.lastUsed == 0 {
return d.debrids[0]
}
return d.debrids[d.lastUsed]
}

View File

@@ -0,0 +1,45 @@
package structs
type DebridLinkAPIResponse[T any] struct {
Success bool `json:"success"`
Value *T `json:"value"` // Use pointer to allow nil
}
type DebridLinkAvailableResponse DebridLinkAPIResponse[map[string]map[string]struct {
Name string `json:"name"`
HashString string `json:"hashString"`
Files []struct {
Name string `json:"name"`
Size int `json:"size"`
} `json:"files"`
}]
type debridLinkTorrentInfo struct {
ID string `json:"id"`
Name string `json:"name"`
HashString string `json:"hashString"`
UploadRatio float64 `json:"uploadRatio"`
ServerID string `json:"serverId"`
Wait bool `json:"wait"`
PeersConnected int `json:"peersConnected"`
Status int `json:"status"`
TotalSize int64 `json:"totalSize"`
Files []struct {
ID string `json:"id"`
Name string `json:"name"`
DownloadURL string `json:"downloadUrl"`
Size int64 `json:"size"`
DownloadPercent int `json:"downloadPercent"`
} `json:"files"`
Trackers []struct {
Announce string `json:"announce"`
} `json:"trackers"`
Created int64 `json:"created"`
DownloadPercent float64 `json:"downloadPercent"`
DownloadSpeed int64 `json:"downloadSpeed"`
UploadSpeed int64 `json:"uploadSpeed"`
}
type DebridLinkTorrentInfo DebridLinkAPIResponse[[]debridLinkTorrentInfo]
type DebridLinkSubmitTorrentInfo DebridLinkAPIResponse[debridLinkTorrentInfo]

View File

@@ -0,0 +1,75 @@
package structs
import "time"
type TorboxAPIResponse[T any] struct {
Success bool `json:"success"`
Error any `json:"error"`
Detail string `json:"detail"`
Data *T `json:"data"` // Use pointer to allow nil
}
type TorBoxAvailableResponse TorboxAPIResponse[map[string]struct {
Name string `json:"name"`
Size int64 `json:"size"`
Hash string `json:"hash"`
}]
type TorBoxAddMagnetResponse TorboxAPIResponse[struct {
Id int `json:"torrent_id"`
Hash string `json:"hash"`
}]
type torboxInfo struct {
Id int `json:"id"`
AuthId string `json:"auth_id"`
Server int `json:"server"`
Hash string `json:"hash"`
Name string `json:"name"`
Magnet interface{} `json:"magnet"`
Size int64 `json:"size"`
Active bool `json:"active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DownloadState string `json:"download_state"`
Seeds int `json:"seeds"`
Peers int `json:"peers"`
Ratio int `json:"ratio"`
Progress float64 `json:"progress"`
DownloadSpeed int64 `json:"download_speed"`
UploadSpeed int `json:"upload_speed"`
Eta int `json:"eta"`
TorrentFile bool `json:"torrent_file"`
ExpiresAt interface{} `json:"expires_at"`
DownloadPresent bool `json:"download_present"`
Files []struct {
Id int `json:"id"`
Md5 interface{} `json:"md5"`
Hash string `json:"hash"`
Name string `json:"name"`
Size int64 `json:"size"`
Zipped bool `json:"zipped"`
S3Path string `json:"s3_path"`
Infected bool `json:"infected"`
Mimetype string `json:"mimetype"`
ShortName string `json:"short_name"`
AbsolutePath string `json:"absolute_path"`
} `json:"files"`
DownloadPath string `json:"download_path"`
InactiveCheck int `json:"inactive_check"`
Availability int `json:"availability"`
DownloadFinished bool `json:"download_finished"`
Tracker interface{} `json:"tracker"`
TotalUploaded int `json:"total_uploaded"`
TotalDownloaded int `json:"total_downloaded"`
Cached bool `json:"cached"`
Owner string `json:"owner"`
SeedTorrent bool `json:"seed_torrent"`
AllowZipped bool `json:"allow_zipped"`
LongTermSeeding bool `json:"long_term_seeding"`
TrackerMessage interface{} `json:"tracker_message"`
}
type TorboxInfoResponse TorboxAPIResponse[torboxInfo]
type TorBoxDownloadLinksResponse TorboxAPIResponse[string]

285
pkg/debrid/torbox.go Normal file
View File

@@ -0,0 +1,285 @@
package debrid
import (
"bytes"
"encoding/json"
"fmt"
"goBlack/common"
"goBlack/pkg/debrid/structs"
"log"
"mime/multipart"
"net/http"
gourl "net/url"
"os"
"slices"
"strconv"
"strings"
)
type Torbox struct {
BaseDebrid
}
func (r *Torbox) GetMountPath() string {
return r.MountPath
}
func (r *Torbox) GetName() string {
return r.Name
}
func (r *Torbox) GetLogger() *log.Logger {
return r.logger
}
func (r *Torbox) IsAvailable(infohashes []string) map[string]bool {
// Check if the infohashes are available in the local cache
hashes, result := GetLocalCache(infohashes, r.cache)
if len(hashes) == 0 {
// Either all the infohashes are locally cached or none are
r.cache.AddMultiple(result)
return result
}
// Divide hashes into groups of 100
for i := 0; i < len(hashes); i += 200 {
end := i + 200
if end > len(hashes) {
end = len(hashes)
}
// Filter out empty strings
validHashes := make([]string, 0, end-i)
for _, hash := range hashes[i:end] {
if hash != "" {
validHashes = append(validHashes, hash)
}
}
// If no valid hashes in this batch, continue to the next batch
if len(validHashes) == 0 {
continue
}
hashStr := strings.Join(validHashes, ",")
url := fmt.Sprintf("%s/api/torrents/checkcached?hash=%s", r.Host, hashStr)
req, _ := http.NewRequest(http.MethodGet, url, nil)
resp, err := r.client.MakeRequest(req)
if err != nil {
log.Println("Error checking availability:", err)
return result
}
var res structs.TorBoxAvailableResponse
err = json.Unmarshal(resp, &res)
if err != nil {
log.Println("Error marshalling availability:", err)
return result
}
if res.Data == nil {
return result
}
for h, cache := range *res.Data {
if cache.Size > 0 {
result[strings.ToUpper(h)] = true
}
}
}
r.cache.AddMultiple(result) // Add the results to the cache
return result
}
func (r *Torbox) SubmitMagnet(torrent *Torrent) (*Torrent, error) {
url := fmt.Sprintf("%s/api/torrents/createtorrent", r.Host)
payload := &bytes.Buffer{}
writer := multipart.NewWriter(payload)
_ = writer.WriteField("magnet", torrent.Magnet.Link)
err := writer.Close()
if err != nil {
return nil, err
}
req, _ := http.NewRequest(http.MethodPost, url, payload)
req.Header.Set("Content-Type", writer.FormDataContentType())
resp, err := r.client.MakeRequest(req)
if err != nil {
return nil, err
}
var data structs.TorBoxAddMagnetResponse
err = json.Unmarshal(resp, &data)
if err != nil {
return nil, err
}
if data.Data == nil {
return nil, fmt.Errorf("error adding torrent")
}
dt := *data.Data
torrentId := strconv.Itoa(dt.Id)
log.Printf("Torrent: %s added with id: %s\n", torrent.Name, torrentId)
torrent.Id = torrentId
return torrent, nil
}
func getStatus(status string, finished bool) string {
if finished {
return "downloaded"
}
downloading := []string{"completed", "cached", "paused", "downloading", "uploading",
"checkingResumeData", "metaDL", "pausedUP", "queuedUP", "checkingUP",
"forcedUP", "allocating", "downloading", "metaDL", "pausedDL",
"queuedDL", "checkingDL", "forcedDL", "checkingResumeData", "moving"}
switch {
case slices.Contains(downloading, status):
return "downloading"
default:
return "error"
}
}
func (r *Torbox) GetTorrent(id string) (*Torrent, error) {
torrent := &Torrent{}
url := fmt.Sprintf("%s/api/torrents/mylist/?id=%s", r.Host, id)
req, _ := http.NewRequest(http.MethodGet, url, nil)
resp, err := r.client.MakeRequest(req)
if err != nil {
return torrent, err
}
var res structs.TorboxInfoResponse
err = json.Unmarshal(resp, &res)
if err != nil {
return torrent, err
}
data := res.Data
name := data.Name
torrent.Id = id
torrent.Name = name
torrent.Bytes = data.Size
torrent.Folder = name
torrent.Progress = data.Progress
torrent.Status = getStatus(data.DownloadState, data.DownloadFinished)
torrent.Speed = data.DownloadSpeed
torrent.Seeders = data.Seeds
torrent.Filename = name
torrent.OriginalFilename = name
files := make([]TorrentFile, len(data.Files))
for i, f := range data.Files {
files[i] = TorrentFile{
Id: strconv.Itoa(f.Id),
Name: f.Name,
Size: f.Size,
}
}
torrent.Files = files
torrent.Debrid = r
return torrent, nil
}
func (r *Torbox) CheckStatus(torrent *Torrent, isSymlink bool) (*Torrent, error) {
for {
tb, err := r.GetTorrent(torrent.Id)
torrent = tb
if err != nil || tb == nil {
return tb, err
}
status := torrent.Status
if status == "error" || status == "dead" || status == "magnet_error" {
return torrent, fmt.Errorf("torrent: %s has error", torrent.Name)
} else if status == "downloaded" {
r.logger.Printf("Torrent: %s downloaded\n", torrent.Name)
if !isSymlink {
err = r.GetDownloadLinks(torrent)
if err != nil {
return torrent, err
}
}
break
} else if status == "downloading" {
if !r.DownloadUncached {
go r.DeleteTorrent(torrent)
return torrent, fmt.Errorf("torrent: %s not cached", torrent.Name)
}
// Break out of the loop if the torrent is downloading.
// This is necessary to prevent infinite loop since we moved to sync downloading and async processing
break
}
}
return torrent, nil
}
func (r *Torbox) DeleteTorrent(torrent *Torrent) {
url := fmt.Sprintf("%s/api//torrents/controltorrent/%s", r.Host, torrent.Id)
payload := map[string]string{"torrent_id": torrent.Id, "action": "Delete"}
jsonPayload, _ := json.Marshal(payload)
req, _ := http.NewRequest(http.MethodDelete, url, bytes.NewBuffer(jsonPayload))
_, err := r.client.MakeRequest(req)
if err == nil {
r.logger.Printf("Torrent: %s deleted\n", torrent.Name)
} else {
r.logger.Printf("Error deleting torrent: %s", err)
}
}
func (r *Torbox) GetDownloadLinks(torrent *Torrent) error {
downloadLinks := make([]TorrentDownloadLinks, 0)
for _, file := range torrent.Files {
url := fmt.Sprintf("%s/api/torrents/requestdl/", r.Host)
query := gourl.Values{}
query.Add("torrent_id", torrent.Id)
query.Add("token", r.APIKey)
query.Add("file_id", file.Id)
url += "?" + query.Encode()
req, _ := http.NewRequest(http.MethodGet, url, nil)
resp, err := r.client.MakeRequest(req)
if err != nil {
return err
}
var data structs.TorBoxDownloadLinksResponse
if err = json.Unmarshal(resp, &data); err != nil {
return err
}
if data.Data == nil {
return fmt.Errorf("error getting download links")
}
idx := 0
link := *data.Data
dl := TorrentDownloadLinks{
Link: link,
Filename: torrent.Files[idx].Name,
DownloadLink: link,
}
downloadLinks = append(downloadLinks, dl)
}
torrent.DownloadLinks = downloadLinks
return nil
}
func (r *Torbox) GetCheckCached() bool {
return r.CheckCached
}
func NewTorbox(dc common.DebridConfig, cache *common.Cache) *Torbox {
rl := common.ParseRateLimit(dc.RateLimit)
headers := map[string]string{
"Authorization": fmt.Sprintf("Bearer %s", dc.APIKey),
}
client := common.NewRLHTTPClient(rl, headers)
logger := common.NewLogger(dc.Name, os.Stdout)
return &Torbox{
BaseDebrid: BaseDebrid{
Name: "torbox",
Host: dc.Host,
APIKey: dc.APIKey,
DownloadUncached: dc.DownloadUncached,
client: client,
cache: cache,
MountPath: dc.Folder,
logger: logger,
CheckCached: dc.CheckCached,
},
}
}

View File

@@ -42,7 +42,7 @@ type Torrent struct {
Links []string `json:"links"`
DownloadLinks []TorrentDownloadLinks `json:"download_links"`
Debrid *Debrid
Debrid Service
Arr *Arr
}
@@ -74,6 +74,7 @@ type TorrentFile struct {
Name string `json:"name"`
Size int64 `json:"size"`
Path string `json:"path"`
Link string `json:"link"`
}
func getEventId(eventType string) int {

View File

@@ -77,7 +77,7 @@ type Proxy struct {
logger *log.Logger
}
func NewProxy(config common.Config, deb debrid.Service, cache *common.Cache) *Proxy {
func NewProxy(config common.Config, deb *debrid.DebridService, cache *common.Cache) *Proxy {
cfg := config.Proxy
port := cmp.Or(os.Getenv("PORT"), cfg.Port, "8181")
return &Proxy{
@@ -87,7 +87,7 @@ func NewProxy(config common.Config, deb debrid.Service, cache *common.Cache) *Pr
username: cfg.Username,
password: cfg.Password,
cachedOnly: *cfg.CachedOnly,
debrid: deb,
debrid: deb.Get(),
cache: cache,
logger: common.NewLogger("Proxy", os.Stdout),
}

View File

@@ -4,6 +4,7 @@ import (
"goBlack/common"
"goBlack/pkg/debrid"
"goBlack/pkg/qbit/downloaders"
"log"
"os"
"path/filepath"
"sync"
@@ -58,7 +59,8 @@ func (q *QBit) processSymlink(torrent *Torrent, debridTorrent *debrid.Torrent, a
ready := make(chan debrid.TorrentFile, len(files))
q.logger.Printf("Checking %d files...", len(files))
rCloneBase := q.debrid.GetMountPath()
rCloneBase := debridTorrent.Debrid.GetMountPath()
log.Println("Rclone base:", rCloneBase)
torrentPath, err := q.getTorrentPath(rCloneBase, debridTorrent) // /MyTVShow/
if err != nil {
q.MarkAsFailed(torrent)

View File

@@ -30,7 +30,7 @@ type QBit struct {
Port string `json:"port"`
DownloadFolder string `json:"download_folder"`
Categories []string `json:"categories"`
debrid debrid.Service
debrid *debrid.DebridService
cache *common.Cache
storage *TorrentStorage
debug bool
@@ -39,7 +39,7 @@ type QBit struct {
RefreshInterval int
}
func NewQBit(config *common.Config, deb debrid.Service, cache *common.Cache) *QBit {
func NewQBit(config *common.Config, deb *debrid.DebridService, cache *common.Cache) *QBit {
cfg := config.QBitTorrent
storage := NewTorrentStorage("torrents.json")
port := cmp.Or(cfg.Port, os.Getenv("QBIT_PORT"), "8182")

View File

@@ -100,7 +100,7 @@ func (q *QBit) processFiles(torrent *Torrent, debridTorrent *debrid.Torrent, arr
progress := debridTorrent.Progress
q.logger.Printf("Progress: %.2f%%", progress)
time.Sleep(5 * time.Second)
dbT, err := q.debrid.CheckStatus(debridTorrent, isSymlink)
dbT, err := debridTorrent.Debrid.CheckStatus(debridTorrent, isSymlink)
if err != nil {
q.logger.Printf("Error checking status: %v", err)
q.MarkAsFailed(torrent)

View File

@@ -17,16 +17,17 @@ func (q *QBit) MarkAsFailed(t *Torrent) *Torrent {
}
func (q *QBit) UpdateTorrent(t *Torrent, debridTorrent *debrid.Torrent) *Torrent {
rcLoneMount := q.debrid.GetMountPath()
db := debridTorrent.Debrid
rcLoneMount := db.GetMountPath()
if debridTorrent == nil && t.ID != "" {
debridTorrent, _ = q.debrid.GetTorrent(t.ID)
debridTorrent, _ = db.GetTorrent(t.ID)
}
if debridTorrent == nil {
q.logger.Printf("Torrent with ID %s not found in %s", t.ID, q.debrid.GetName())
q.logger.Printf("Torrent with ID %s not found in %s", t.ID, db.GetName())
return t
}
if debridTorrent.Status != "downloaded" {
debridTorrent, _ = q.debrid.GetTorrent(t.ID)
debridTorrent, _ = db.GetTorrent(t.ID)
}
if t.TorrentPath == "" {
@@ -50,7 +51,7 @@ func (q *QBit) UpdateTorrent(t *Torrent, debridTorrent *debrid.Torrent) *Torrent
if speed != 0 {
eta = int64((totalSize - float64(sizeCompleted)) / float64(speed))
}
t.Size = debridTorrent.Bytes
t.DebridTorrent = debridTorrent
t.Completed = sizeCompleted