7 Commits

Author SHA1 Message Date
Mukhtar Akere
df2aa4e361 0.2.7:
- Add support for multiple debrid providers
- Add Torbox support
- Add support for configurable debrid cache checks
- Add support for configurable debrid download uncached torrents
2024-11-25 16:48:23 +01:00
Mukhtar Akere
b51cb954f8 Merge branch 'beta' 2024-11-25 16:39:47 +01:00
Mukhtar Akere
8bdb2e3547 Hotfix & Updated Readme 2024-11-23 23:41:49 +01:00
Mukhtar Akere
2c9a076cd2 Hotfix 2024-11-23 21:10:42 +01:00
Mukhtar Akere
d2a77620bc Features:
- Add Torbox(Tested)
- Fix RD cache check
- Minor fixes
2024-11-23 19:52:15 +01:00
Mukhtar Akere
4b8f1ccfb6 Changelog 0.2.6 2024-10-08 15:43:38 +01:00
Mukhtar Akere
f118c5b794 Changelog 0.2.5 2024-10-01 11:17:31 +01:00
22 changed files with 994 additions and 176 deletions

View File

@@ -67,4 +67,20 @@
- Add file download support(Sequential Download) - Add file download support(Sequential Download)
- Fix http handler error - Fix http handler error
- Fix *arrs map failing concurrently - Fix *arrs map failing concurrently
- Fix cache not being updated - Fix cache not being updated
#### 0.2.5
- Fix ContentPath not being set prior
- Rewrote Readme
- Cleaned up the code
#### 0.2.6
- Delete torrent for empty matched files
- Update Readme
#### 0.2.7
- Add support for multiple debrid providers
- Add Torbox support
- Add support for configurable debrid cache checks
- Add support for configurable debrid download uncached torrents

View File

@@ -1,13 +1,22 @@
### GoBlackHole(with Debrid Proxy Support) ### GoBlackHole(with Debrid Proxy Support)
This is a Golang implementation go Torrent QbitTorrent with a **Real Debrid Proxy Support**. This is a Golang implementation go Torrent QbitTorrent with a **Real Debrid & Torbox Support**.
### Features
#### Uses
- Mock Qbittorent API that supports the Arrs(Sonarr, Radarr, etc) - Mock Qbittorent API that supports the Arrs(Sonarr, Radarr, etc)
- Proxy support for the Arrs - Proxy support for the Arrs
- Real Debrid Support
- Torbox Support
- Multi-Debrid Providers support
The proxy is useful in filtering out un-cached Real Debrid torrents The proxy is useful in filtering out un-cached Real Debrid torrents
### Supported Debrid Providers
- Real Debrid
- Torbox
### Changelog ### Changelog
- View the [CHANGELOG.md](CHANGELOG.md) for the latest changes - View the [CHANGELOG.md](CHANGELOG.md) for the latest changes
@@ -37,6 +46,8 @@ services:
- QBIT_PORT=8282 # qBittorrent Port. This is optional. You can set this in the config file - QBIT_PORT=8282 # qBittorrent Port. This is optional. You can set this in the config file
- PORT=8181 # Proxy Port. This is optional. You can set this in the config file - PORT=8181 # Proxy Port. This is optional. You can set this in the config file
restart: unless-stopped restart: unless-stopped
depends_on:
- rclone # If you are using rclone with docker
``` ```
@@ -50,16 +61,29 @@ Download the binary from the releases page and run it with the config file.
#### Config #### Config
```json ```json
{ {
"debrid": { "debrids": [
"name": "realdebrid", {
"host": "https://api.real-debrid.com/rest/1.0", "name": "torbox",
"api_key": "realdebrid_api_key", "host": "https://api.torbox.app/v1",
"folder": "data/realdebrid/torrents/", "api_key": "torbox_api_key",
"rate_limit": "250/minute" "folder": "data/realdebrid/torrents/",
}, "rate_limit": "250/minute",
"download_uncached": false,
"check_cached": 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_cached": false
}
],
"proxy": { "proxy": {
"enabled": true, "enabled": true,
"port": "8181", "port": "8100",
"debug": false, "debug": false,
"username": "username", "username": "username",
"password": "password", "password": "password",
@@ -68,18 +92,28 @@ Download the binary from the releases page and run it with the config file.
"max_cache_size": 1000, "max_cache_size": 1000,
"qbittorrent": { "qbittorrent": {
"port": "8282", "port": "8282",
"username": "admin", // deprecated
"password": "admin", // deprecated
"download_folder": "/media/symlinks/", "download_folder": "/media/symlinks/",
"categories": ["sonarr", "radarr"], "categories": ["sonarr", "radarr"],
"refresh_interval": 5 // in seconds "refresh_interval": 5
} }
} }
``` ```
#### Config Notes #### Config Notes
##### Max Cache Size
- The `max_cache_size` key is used to set the maximum number of infohashes that can be stored in the availability cache. This is used to prevent round trip to the debrid provider when using the proxy/Qbittorrent
- The default value is `1000`
- The cache is stored in memory and is not persisted on restart
##### Debrid Config ##### Debrid Config
- This config key is important as it's used for both Blackhole and Proxy - The `debrids` key is an array of debrid providers
- The `name` key is the name of the debrid provider
- The `host` key is the API endpoint of the debrid provider
- The `api_key` key is the API key of the debrid provider
- The `folder` key is the folder where the torrents will be downloaded. e.g `data/realdebrid/torrents/`
- The `rate_limit` key is the rate limit of the debrid provider(null by default)
- The `download_uncached` bool key is used to download uncached torrents(disabled by default)
- The `check_cached` bool key is used to check if the torrent is cached(disabled by default)
##### Proxy Config ##### Proxy Config
- The `enabled` key is used to enable the proxy - The `enabled` key is used to enable the proxy
@@ -93,6 +127,7 @@ Download the binary from the releases page and run it with the config file.
- The `port` key is the port the qBittorrent will listen on - The `port` key is the port the qBittorrent will listen on
- The `download_folder` is the folder where the torrents will be downloaded. e.g `/media/symlinks/` - The `download_folder` is the folder where the torrents will be downloaded. e.g `/media/symlinks/`
- The `categories` key is used to filter out torrents based on the category. e.g `sonarr`, `radarr` - The `categories` key is used to filter out torrents based on the category. e.g `sonarr`, `radarr`
- The `refresh_interval` key is used to set the interval in minutes to refresh the Arrs Monitored Downloads(it's in seconds). The default value is `5` seconds
### Proxy ### Proxy
@@ -133,9 +168,6 @@ Setting Up Qbittorrent in Arr
- [ ] Debrid - [ ] Debrid
- [ ] Add more Debrid Providers - [ ] Add more Debrid Providers
- [ ] Proxy
- [ ] Add more Proxy features
- [ ] Qbittorrent - [ ] Qbittorrent
- [ ] Add more Qbittorrent features - [ ] Add more Qbittorrent features
- [ ] Persist torrents on restart/server crash - [ ] Persist torrents on restart/server crash

View File

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

View File

@@ -12,6 +12,7 @@ type DebridConfig struct {
APIKey string `json:"api_key"` APIKey string `json:"api_key"`
Folder string `json:"folder"` Folder string `json:"folder"`
DownloadUncached bool `json:"download_uncached"` DownloadUncached bool `json:"download_uncached"`
CheckCached bool `json:"check_cached"`
RateLimit string `json:"rate_limit"` // 200/minute or 10/second RateLimit string `json:"rate_limit"` // 200/minute or 10/second
} }
@@ -36,6 +37,7 @@ type QBitTorrentConfig struct {
type Config struct { type Config struct {
Debrid DebridConfig `json:"debrid"` Debrid DebridConfig `json:"debrid"`
Debrids []DebridConfig `json:"debrids"`
Proxy ProxyConfig `json:"proxy"` Proxy ProxyConfig `json:"proxy"`
MaxCacheSize int `json:"max_cache_size"` MaxCacheSize int `json:"max_cache_size"`
QBitTorrent QBitTorrentConfig `json:"qbittorrent"` QBitTorrent QBitTorrentConfig `json:"qbittorrent"`
@@ -60,9 +62,9 @@ func LoadConfig(path string) (*Config, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
if config.Proxy.CachedOnly == nil {
config.Proxy.CachedOnly = new(bool) if config.Debrid.Name != "" {
*config.Proxy.CachedOnly = true config.Debrids = append(config.Debrids, config.Debrid)
} }
return config, nil 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") return resp, fmt.Errorf("max retries exceeded")
} }
func (c *RLHTTPClient) MakeRequest(method string, url string, body io.Reader) ([]byte, error) { func (c *RLHTTPClient) MakeRequest(req *http.Request) ([]byte, error) {
req, err := http.NewRequest(method, url, body)
if err != nil {
return nil, err
}
if c.Headers != nil { if c.Headers != nil {
for key, value := range c.Headers { for key, value := range c.Headers {
req.Header.Set(key, value) req.Header.Set(key, value)
@@ -75,6 +71,7 @@ func (c *RLHTTPClient) MakeRequest(method string, url string, body io.Reader) ([
if err != nil { if err != nil {
return nil, err return nil, err
} }
b, _ := io.ReadAll(res.Body)
statusOk := strconv.Itoa(res.StatusCode)[0] == '2' statusOk := strconv.Itoa(res.StatusCode)[0] == '2'
if !statusOk { if !statusOk {
return nil, fmt.Errorf("unexpected status code: %d", res.StatusCode) 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) log.Println(err)
} }
}(res.Body) }(res.Body)
return io.ReadAll(res.Body) return b, nil
} }
func NewRLHTTPClient(rl *rate.Limiter, headers map[string]string) *RLHTTPClient { func NewRLHTTPClient(rl *rate.Limiter, headers map[string]string) *RLHTTPClient {

View File

@@ -8,20 +8,8 @@ import (
"path/filepath" "path/filepath"
) )
type Service interface { type BaseDebrid struct {
SubmitMagnet(torrent *Torrent) (*Torrent, error) Name string
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 {
Host string `json:"host"` Host string `json:"host"`
APIKey string APIKey string
DownloadUncached bool DownloadUncached bool
@@ -29,12 +17,41 @@ type Debrid struct {
cache *common.Cache cache *common.Cache
MountPath string MountPath string
logger *log.Logger 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 { switch dc.Name {
case "realdebrid": case "realdebrid":
return NewRealDebrid(dc, cache) return NewRealDebrid(dc, cache)
case "torbox":
return NewTorbox(dc, cache)
case "debridlink":
return NewDebridLink(dc, cache)
default: default:
return NewRealDebrid(dc, cache) 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) { func GetLocalCache(infohashes []string, cache *common.Cache) ([]string, map[string]bool) {
result := make(map[string]bool) result := make(map[string]bool)
hashes := make([]string, len(infohashes))
if len(infohashes) == 0 { //if len(infohashes) == 0 {
return hashes, result // return hashes, result
} //}
if len(infohashes) == 1 { //if len(infohashes) == 1 {
if cache.Exists(infohashes[0]) { // if cache.Exists(infohashes[0]) {
return hashes, map[string]bool{infohashes[0]: true} // return hashes, map[string]bool{infohashes[0]: true}
} // }
return infohashes, result // 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) return infohashes, result
for _, h := range infohashes {
_, exists := cachedHashes[h]
if !exists {
hashes = append(hashes, h)
} else {
result[h] = true
}
}
return hashes, 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{ debridTorrent := &Torrent{
InfoHash: magnet.InfoHash, InfoHash: magnet.InfoHash,
Magnet: magnet, Magnet: magnet,
@@ -128,21 +144,30 @@ func ProcessQBitTorrent(d Service, magnet *common.Magnet, arr *Arr, isSymlink bo
Arr: arr, Arr: arr,
Size: magnet.Size, 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) for index, db := range d.debrids {
if err != nil || debridTorrent.Id == "" { log.Println("Processing debrid: ", db.GetName())
logger.Printf("Error submitting magnet: %s", err) logger := db.GetLogger()
return nil, err 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 { type RealDebrid struct {
Host string `json:"host"` BaseDebrid
APIKey string
DownloadUncached bool
client *common.RLHTTPClient
cache *common.Cache
MountPath string
logger *log.Logger
} }
func (r *RealDebrid) GetMountPath() string { func (r *RealDebrid) GetMountPath() string {
@@ -29,7 +23,7 @@ func (r *RealDebrid) GetMountPath() string {
} }
func (r *RealDebrid) GetName() string { func (r *RealDebrid) GetName() string {
return "realdebrid" return r.Name
} }
func (r *RealDebrid) GetLogger() *log.Logger { func (r *RealDebrid) GetLogger() *log.Logger {
@@ -89,7 +83,8 @@ func (r *RealDebrid) IsAvailable(infohashes []string) map[string]bool {
hashStr := strings.Join(validHashes, "/") hashStr := strings.Join(validHashes, "/")
url := fmt.Sprintf("%s/torrents/instantAvailability/%s", r.Host, hashStr) 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 { if err != nil {
log.Println("Error checking availability:", err) log.Println("Error checking availability:", err)
return result return result
@@ -117,7 +112,8 @@ func (r *RealDebrid) SubmitMagnet(torrent *Torrent) (*Torrent, error) {
"magnet": {torrent.Magnet.Link}, "magnet": {torrent.Magnet.Link},
} }
var data structs.RealDebridAddMagnetSchema 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 { if err != nil {
return nil, err return nil, err
} }
@@ -131,7 +127,8 @@ func (r *RealDebrid) SubmitMagnet(torrent *Torrent) (*Torrent, error) {
func (r *RealDebrid) GetTorrent(id string) (*Torrent, error) { func (r *RealDebrid) GetTorrent(id string) (*Torrent, error) {
torrent := &Torrent{} torrent := &Torrent{}
url := fmt.Sprintf("%s/torrents/info/%s", r.Host, id) 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 { if err != nil {
return torrent, err return torrent, err
} }
@@ -152,6 +149,7 @@ func (r *RealDebrid) GetTorrent(id string) (*Torrent, error) {
torrent.Filename = data.Filename torrent.Filename = data.Filename
torrent.OriginalFilename = data.OriginalFilename torrent.OriginalFilename = data.OriginalFilename
torrent.Links = data.Links torrent.Links = data.Links
torrent.Debrid = r
files := GetTorrentFiles(data) files := GetTorrentFiles(data)
torrent.Files = files torrent.Files = files
return torrent, nil 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) { func (r *RealDebrid) CheckStatus(torrent *Torrent, isSymlink bool) (*Torrent, error) {
url := fmt.Sprintf("%s/torrents/info/%s", r.Host, torrent.Id) url := fmt.Sprintf("%s/torrents/info/%s", r.Host, torrent.Id)
req, _ := http.NewRequest(http.MethodGet, url, nil)
for { for {
resp, err := r.client.MakeRequest(http.MethodGet, url, nil) resp, err := r.client.MakeRequest(req)
if err != nil { if err != nil {
log.Println("ERROR Checking file: ", err) log.Println("ERROR Checking file: ", err)
return torrent, err return torrent, err
@@ -179,12 +178,14 @@ func (r *RealDebrid) CheckStatus(torrent *Torrent, isSymlink bool) (*Torrent, er
torrent.Seeders = data.Seeders torrent.Seeders = data.Seeders
torrent.Links = data.Links torrent.Links = data.Links
torrent.Status = status torrent.Status = status
torrent.Debrid = r
if status == "error" || status == "dead" || status == "magnet_error" { if status == "error" || status == "dead" || status == "magnet_error" {
return torrent, fmt.Errorf("torrent: %s has error", torrent.Name) return torrent, fmt.Errorf("torrent: %s has error", torrent.Name)
} else if status == "waiting_files_selection" { } else if status == "waiting_files_selection" {
files := GetTorrentFiles(data) files := GetTorrentFiles(data)
torrent.Files = files torrent.Files = files
if len(files) == 0 { if len(files) == 0 {
r.DeleteTorrent(torrent)
return torrent, fmt.Errorf("no video files found") return torrent, fmt.Errorf("no video files found")
} }
filesId := make([]string, 0) filesId := make([]string, 0)
@@ -195,7 +196,8 @@ func (r *RealDebrid) CheckStatus(torrent *Torrent, isSymlink bool) (*Torrent, er
"files": {strings.Join(filesId, ",")}, "files": {strings.Join(filesId, ",")},
} }
payload := strings.NewReader(p.Encode()) 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 { if err != nil {
return torrent, err return torrent, err
} }
@@ -209,7 +211,6 @@ func (r *RealDebrid) CheckStatus(torrent *Torrent, isSymlink bool) (*Torrent, er
return torrent, err return torrent, err
} }
} }
break break
} else if status == "downloading" { } else if status == "downloading" {
if !r.DownloadUncached { if !r.DownloadUncached {
@@ -227,7 +228,8 @@ func (r *RealDebrid) CheckStatus(torrent *Torrent, isSymlink bool) (*Torrent, er
func (r *RealDebrid) DeleteTorrent(torrent *Torrent) { func (r *RealDebrid) DeleteTorrent(torrent *Torrent) {
url := fmt.Sprintf("%s/torrents/delete/%s", r.Host, torrent.Id) 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 { if err == nil {
r.logger.Printf("Torrent: %s deleted\n", torrent.Name) r.logger.Printf("Torrent: %s deleted\n", torrent.Name)
} else { } else {
@@ -245,7 +247,8 @@ func (r *RealDebrid) GetDownloadLinks(torrent *Torrent) error {
payload := gourl.Values{ payload := gourl.Values{
"link": {link}, "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 { if err != nil {
return err return err
} }
@@ -264,8 +267,8 @@ func (r *RealDebrid) GetDownloadLinks(torrent *Torrent) error {
return nil return nil
} }
func (r *RealDebrid) GetDownloadUncached() bool { func (r *RealDebrid) GetCheckCached() bool {
return r.DownloadUncached return r.CheckCached
} }
func NewRealDebrid(dc common.DebridConfig, cache *common.Cache) *RealDebrid { func NewRealDebrid(dc common.DebridConfig, cache *common.Cache) *RealDebrid {
@@ -276,12 +279,16 @@ func NewRealDebrid(dc common.DebridConfig, cache *common.Cache) *RealDebrid {
client := common.NewRLHTTPClient(rl, headers) client := common.NewRLHTTPClient(rl, headers)
logger := common.NewLogger(dc.Name, os.Stdout) logger := common.NewLogger(dc.Name, os.Stdout)
return &RealDebrid{ return &RealDebrid{
Host: dc.Host, BaseDebrid: BaseDebrid{
APIKey: dc.APIKey, Name: "realdebrid",
DownloadUncached: dc.DownloadUncached, Host: dc.Host,
client: client, APIKey: dc.APIKey,
cache: cache, DownloadUncached: dc.DownloadUncached,
MountPath: dc.Folder, client: client,
logger: logger, 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]

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

@@ -0,0 +1,290 @@
package debrid
import (
"bytes"
"encoding/json"
"fmt"
"goBlack/common"
"goBlack/pkg/debrid/structs"
"log"
"mime/multipart"
"net/http"
gourl "net/url"
"os"
"path"
"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,
}
}
if len(files) > 0 && name == data.Hash {
cleanPath := path.Clean(files[0].Name)
torrent.OriginalFilename = strings.Split(strings.TrimPrefix(cleanPath, "/"), "/")[0]
}
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

@@ -36,13 +36,14 @@ type Torrent struct {
Magnet *common.Magnet `json:"magnet"` Magnet *common.Magnet `json:"magnet"`
Files []TorrentFile `json:"files"` Files []TorrentFile `json:"files"`
Status string `json:"status"` Status string `json:"status"`
Added string `json:"added"`
Progress float64 `json:"progress"` Progress float64 `json:"progress"`
Speed int64 `json:"speed"` Speed int64 `json:"speed"`
Seeders int `json:"seeders"` Seeders int `json:"seeders"`
Links []string `json:"links"` Links []string `json:"links"`
DownloadLinks []TorrentDownloadLinks `json:"download_links"` DownloadLinks []TorrentDownloadLinks `json:"download_links"`
Debrid *Debrid Debrid Service
Arr *Arr Arr *Arr
} }
@@ -57,12 +58,11 @@ func (t *Torrent) GetSymlinkFolder(parent string) string {
} }
func (t *Torrent) GetMountFolder(rClonePath string) string { func (t *Torrent) GetMountFolder(rClonePath string) string {
pathWithNoExt := common.RemoveExtension(t.OriginalFilename)
if common.FileReady(filepath.Join(rClonePath, t.OriginalFilename)) { if common.FileReady(filepath.Join(rClonePath, t.OriginalFilename)) {
return t.OriginalFilename return t.OriginalFilename
} else if common.FileReady(filepath.Join(rClonePath, t.Filename)) { } else if common.FileReady(filepath.Join(rClonePath, t.Filename)) {
return t.Filename return t.Filename
} else if common.FileReady(filepath.Join(rClonePath, pathWithNoExt)) { } else if pathWithNoExt := common.RemoveExtension(t.OriginalFilename); common.FileReady(filepath.Join(rClonePath, pathWithNoExt)) {
return pathWithNoExt return pathWithNoExt
} else { } else {
return "" return ""
@@ -74,6 +74,7 @@ type TorrentFile struct {
Name string `json:"name"` Name string `json:"name"`
Size int64 `json:"size"` Size int64 `json:"size"`
Path string `json:"path"` Path string `json:"path"`
Link string `json:"link"`
} }
func getEventId(eventType string) int { func getEventId(eventType string) int {

View File

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

View File

@@ -58,7 +58,7 @@ func (q *QBit) processSymlink(torrent *Torrent, debridTorrent *debrid.Torrent, a
ready := make(chan debrid.TorrentFile, len(files)) ready := make(chan debrid.TorrentFile, len(files))
q.logger.Printf("Checking %d files...", len(files)) q.logger.Printf("Checking %d files...", len(files))
rCloneBase := q.debrid.GetMountPath() rCloneBase := debridTorrent.Debrid.GetMountPath()
torrentPath, err := q.getTorrentPath(rCloneBase, debridTorrent) // /MyTVShow/ torrentPath, err := q.getTorrentPath(rCloneBase, debridTorrent) // /MyTVShow/
if err != nil { if err != nil {
q.MarkAsFailed(torrent) q.MarkAsFailed(torrent)
@@ -125,7 +125,7 @@ func (q *QBit) createSymLink(path string, torrentMountPath string, file debrid.T
torrentFilePath := filepath.Join(torrentMountPath, file.Name) // debridFolder/MyTVShow/MyTVShow.S01E01.720p.mkv torrentFilePath := filepath.Join(torrentMountPath, file.Name) // debridFolder/MyTVShow/MyTVShow.S01E01.720p.mkv
err := os.Symlink(torrentFilePath, fullPath) err := os.Symlink(torrentFilePath, fullPath)
if err != nil { if err != nil {
q.logger.Printf("Failed to create symlink: %s\n", fullPath) q.logger.Printf("Failed to create symlink: %s: %v\n", fullPath, err)
} }
// Check if the file exists // Check if the file exists
if !common.FileReady(fullPath) { if !common.FileReady(fullPath) {

View File

@@ -24,6 +24,7 @@ func (q *QBit) AddRoutes(r chi.Router) http.Handler {
r.Get("/resume", q.handleTorrentsResume) r.Get("/resume", q.handleTorrentsResume)
r.Get("/recheck", q.handleTorrentRecheck) r.Get("/recheck", q.handleTorrentRecheck)
r.Get("/properties", q.handleTorrentProperties) r.Get("/properties", q.handleTorrentProperties)
r.Get("/files", q.handleTorrentFiles)
}) })
r.Route("/app", func(r chi.Router) { r.Route("/app", func(r chi.Router) {

View File

@@ -5,6 +5,5 @@ import (
) )
func (q *QBit) handleLogin(w http.ResponseWriter, r *http.Request) { func (q *QBit) handleLogin(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("Ok."))
w.Write([]byte("Ok."))
} }

View File

@@ -163,3 +163,13 @@ func (q *QBit) handleTorrentProperties(w http.ResponseWriter, r *http.Request) {
properties := q.GetTorrentProperties(torrent) properties := q.GetTorrentProperties(torrent)
JSONResponse(w, properties, http.StatusOK) JSONResponse(w, properties, http.StatusOK)
} }
func (q *QBit) handleTorrentFiles(w http.ResponseWriter, r *http.Request) {
hash := r.URL.Query().Get("hash")
torrent := q.storage.Get(hash)
if torrent == nil {
return
}
files := q.GetTorrentFiles(torrent)
JSONResponse(w, files, http.StatusOK)
}

View File

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

View File

@@ -58,9 +58,7 @@ func (q *QBit) Process(ctx context.Context, magnet *common.Magnet, category stri
} }
return err return err
} }
torrent.ID = debridTorrent.Id torrent = q.UpdateTorrentMin(torrent, debridTorrent)
torrent.DebridTorrent = debridTorrent
torrent.Name = debridTorrent.Name
q.storage.AddOrUpdate(torrent) q.storage.AddOrUpdate(torrent)
go q.processFiles(torrent, debridTorrent, arr, isSymlink) // We can send async for file processing not to delay the response go q.processFiles(torrent, debridTorrent, arr, isSymlink) // We can send async for file processing not to delay the response
return nil return nil
@@ -74,23 +72,14 @@ func (q *QBit) CreateTorrentFromMagnet(magnet *common.Magnet, category string) *
Size: magnet.Size, Size: magnet.Size,
Category: category, Category: category,
State: "downloading", State: "downloading",
AddedOn: time.Now().Unix(),
MagnetUri: magnet.Link, MagnetUri: magnet.Link,
Tracker: "udp://tracker.opentrackr.org:1337", Tracker: "udp://tracker.opentrackr.org:1337",
UpLimit: -1, UpLimit: -1,
DlLimit: -1, DlLimit: -1,
FlPiecePrio: false, AutoTmm: false,
ForceStart: false, Ratio: 1,
AutoTmm: false, RatioLimit: 1,
Availability: 2,
MaxRatio: -1,
MaxSeedingTime: -1,
NumComplete: 10,
NumIncomplete: 0,
NumLeechs: 1,
Ratio: 1,
RatioLimit: 1,
} }
return torrent return torrent
} }
@@ -98,15 +87,17 @@ func (q *QBit) CreateTorrentFromMagnet(magnet *common.Magnet, category string) *
func (q *QBit) processFiles(torrent *Torrent, debridTorrent *debrid.Torrent, arr *debrid.Arr, isSymlink bool) { func (q *QBit) processFiles(torrent *Torrent, debridTorrent *debrid.Torrent, arr *debrid.Arr, isSymlink bool) {
for debridTorrent.Status != "downloaded" { for debridTorrent.Status != "downloaded" {
progress := debridTorrent.Progress progress := debridTorrent.Progress
q.logger.Printf("Progress: %.2f%%", progress) q.logger.Printf("%s Download Progress: %.2f%%", debridTorrent.Debrid.GetName(), progress)
time.Sleep(5 * time.Second) time.Sleep(5 * time.Second)
dbT, err := q.debrid.CheckStatus(debridTorrent, isSymlink) dbT, err := debridTorrent.Debrid.CheckStatus(debridTorrent, isSymlink)
if err != nil { if err != nil {
q.logger.Printf("Error checking status: %v", err) q.logger.Printf("Error checking status: %v", err)
q.MarkAsFailed(torrent) q.MarkAsFailed(torrent)
q.RefreshArr(arr)
return return
} }
debridTorrent = dbT debridTorrent = dbT
torrent = q.UpdateTorrentMin(torrent, debridTorrent)
} }
if isSymlink { if isSymlink {
q.processSymlink(torrent, debridTorrent, arr) q.processSymlink(torrent, debridTorrent, arr)

View File

@@ -174,20 +174,20 @@ type Torrent struct {
TorrentPath string `json:"-"` TorrentPath string `json:"-"`
AddedOn int64 `json:"added_on,omitempty"` AddedOn int64 `json:"added_on,omitempty"`
AmountLeft int64 `json:"amount_left,omitempty"` AmountLeft int64 `json:"amount_left"`
AutoTmm bool `json:"auto_tmm"` AutoTmm bool `json:"auto_tmm"`
Availability float64 `json:"availability"` Availability float64 `json:"availability,omitempty"`
Category string `json:"category,omitempty"` Category string `json:"category,omitempty"`
Completed int64 `json:"completed,omitempty"` Completed int64 `json:"completed"`
CompletionOn int64 `json:"completion_on,omitempty"` CompletionOn int64 `json:"completion_on,omitempty"`
ContentPath string `json:"content_path,omitempty"` ContentPath string `json:"content_path"`
DlLimit int64 `json:"dl_limit,omitempty"` DlLimit int64 `json:"dl_limit"`
Dlspeed int64 `json:"dlspeed,omitempty"` Dlspeed int64 `json:"dlspeed"`
Downloaded int64 `json:"downloaded,omitempty"` Downloaded int64 `json:"downloaded"`
DownloadedSession int64 `json:"downloaded_session,omitempty"` DownloadedSession int64 `json:"downloaded_session"`
Eta int64 `json:"eta,omitempty"` Eta int64 `json:"eta"`
FlPiecePrio bool `json:"f_l_piece_prio"` FlPiecePrio bool `json:"f_l_piece_prio,omitempty"`
ForceStart bool `json:"force_start"` ForceStart bool `json:"force_start,omitempty"`
Hash string `json:"hash"` Hash string `json:"hash"`
LastActivity int64 `json:"last_activity,omitempty"` LastActivity int64 `json:"last_activity,omitempty"`
MagnetUri string `json:"magnet_uri,omitempty"` MagnetUri string `json:"magnet_uri,omitempty"`
@@ -202,7 +202,7 @@ type Torrent struct {
Progress float32 `json:"progress"` Progress float32 `json:"progress"`
Ratio int64 `json:"ratio,omitempty"` Ratio int64 `json:"ratio,omitempty"`
RatioLimit int64 `json:"ratio_limit,omitempty"` RatioLimit int64 `json:"ratio_limit,omitempty"`
SavePath string `json:"save_path,omitempty"` SavePath string `json:"save_path"`
SeedingTimeLimit int64 `json:"seeding_time_limit,omitempty"` SeedingTimeLimit int64 `json:"seeding_time_limit,omitempty"`
SeenComplete int64 `json:"seen_complete,omitempty"` SeenComplete int64 `json:"seen_complete,omitempty"`
SeqDl bool `json:"seq_dl"` SeqDl bool `json:"seq_dl"`
@@ -259,6 +259,17 @@ type TorrentProperties struct {
UpSpeedAvg int64 `json:"up_speed_avg,omitempty"` UpSpeedAvg int64 `json:"up_speed_avg,omitempty"`
} }
type TorrentFile struct {
Index int `json:"index,omitempty"`
Name string `json:"name,omitempty"`
Size int64 `json:"size,omitempty"`
Progress int64 `json:"progress,omitempty"`
Priority int64 `json:"priority,omitempty"`
IsSeed bool `json:"is_seed,omitempty"`
PieceRange []int64 `json:"piece_range,omitempty"`
Availability float64 `json:"availability,omitempty"`
}
func NewAppPreferences() *AppPreferences { func NewAppPreferences() *AppPreferences {
preferences := &AppPreferences{ preferences := &AppPreferences{
AddTrackers: "", AddTrackers: "",

View File

@@ -16,31 +16,19 @@ func (q *QBit) MarkAsFailed(t *Torrent) *Torrent {
return t return t
} }
func (q *QBit) UpdateTorrent(t *Torrent, debridTorrent *debrid.Torrent) *Torrent { func (q *QBit) UpdateTorrentMin(t *Torrent, debridTorrent *debrid.Torrent) *Torrent {
rcLoneMount := q.debrid.GetMountPath()
if debridTorrent == nil && t.ID != "" {
debridTorrent, _ = q.debrid.GetTorrent(t.ID)
}
if debridTorrent == nil { if debridTorrent == nil {
q.logger.Printf("Torrent with ID %s not found in %s", t.ID, q.debrid.GetName())
return t return t
} }
if debridTorrent.Status != "downloaded" {
debridTorrent, _ = q.debrid.GetTorrent(t.ID)
}
if t.TorrentPath == "" { addedOn, err := time.Parse(time.RFC3339, debridTorrent.Added)
t.TorrentPath = filepath.Base(debridTorrent.GetMountFolder(rcLoneMount)) if err != nil {
addedOn = time.Now()
} }
totalSize := float64(debridTorrent.Bytes)
totalSize := float64(cmp.Or(debridTorrent.Bytes, 1.0))
progress := cmp.Or(debridTorrent.Progress, 100.0) progress := cmp.Or(debridTorrent.Progress, 100.0)
progress = progress / 100.0 progress = progress / 100.0
var sizeCompleted int64 sizeCompleted := int64(totalSize * progress)
sizeCompleted = int64(totalSize * progress)
savePath := filepath.Join(q.DownloadFolder, t.Category) + string(os.PathSeparator)
torrentPath := filepath.Join(savePath, t.TorrentPath) + string(os.PathSeparator)
var speed int64 var speed int64
if debridTorrent.Speed != 0 { if debridTorrent.Speed != 0 {
@@ -50,9 +38,11 @@ func (q *QBit) UpdateTorrent(t *Torrent, debridTorrent *debrid.Torrent) *Torrent
if speed != 0 { if speed != 0 {
eta = int64((totalSize - float64(sizeCompleted)) / float64(speed)) eta = int64((totalSize - float64(sizeCompleted)) / float64(speed))
} }
t.ID = debridTorrent.Id
t.Size = debridTorrent.Bytes t.Name = debridTorrent.Name
t.AddedOn = addedOn.Unix()
t.DebridTorrent = debridTorrent t.DebridTorrent = debridTorrent
t.Size = int64(totalSize)
t.Completed = sizeCompleted t.Completed = sizeCompleted
t.Downloaded = sizeCompleted t.Downloaded = sizeCompleted
t.DownloadedSession = sizeCompleted t.DownloadedSession = sizeCompleted
@@ -60,29 +50,58 @@ func (q *QBit) UpdateTorrent(t *Torrent, debridTorrent *debrid.Torrent) *Torrent
t.UploadedSession = sizeCompleted t.UploadedSession = sizeCompleted
t.AmountLeft = int64(totalSize) - sizeCompleted t.AmountLeft = int64(totalSize) - sizeCompleted
t.Progress = float32(progress) t.Progress = float32(progress)
t.SavePath = savePath
t.ContentPath = torrentPath
t.Eta = eta t.Eta = eta
t.Dlspeed = speed t.Dlspeed = speed
t.Upspeed = speed t.Upspeed = speed
t.SavePath = filepath.Join(q.DownloadFolder, t.Category) + string(os.PathSeparator)
t.ContentPath = filepath.Join(t.SavePath, t.Name) + string(os.PathSeparator)
return t
}
func (q *QBit) UpdateTorrent(t *Torrent, debridTorrent *debrid.Torrent) *Torrent {
db := debridTorrent.Debrid
rcLoneMount := db.GetMountPath()
if debridTorrent == nil && t.ID != "" {
debridTorrent, _ = db.GetTorrent(t.ID)
}
if debridTorrent == nil {
q.logger.Printf("Torrent with ID %s not found in %s", t.ID, db.GetName())
return t
}
if debridTorrent.Status != "downloaded" {
debridTorrent, _ = db.GetTorrent(t.ID)
}
if t.TorrentPath == "" {
t.TorrentPath = filepath.Base(debridTorrent.GetMountFolder(rcLoneMount))
}
savePath := filepath.Join(q.DownloadFolder, t.Category) + string(os.PathSeparator)
torrentPath := filepath.Join(savePath, t.TorrentPath) + string(os.PathSeparator)
t = q.UpdateTorrentMin(t, debridTorrent)
t.ContentPath = torrentPath
if t.IsReady() { if t.IsReady() {
t.State = "pausedUP" t.State = "pausedUP"
q.storage.AddOrUpdate(t) q.storage.Update(t)
return t return t
} }
ticker := time.NewTicker(3 * time.Second)
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
for { for {
select { select {
case <-ticker.C: case <-ticker.C:
if t.IsReady() { if t.IsReady() {
t.State = "pausedUP" t.State = "pausedUP"
q.storage.AddOrUpdate(t) q.storage.Update(t)
ticker.Stop()
return t return t
} else {
return q.UpdateTorrent(t, debridTorrent)
} }
updatedT := q.UpdateTorrent(t, debridTorrent)
t = updatedT
case <-time.After(10 * time.Minute): // Add a timeout
return t
} }
} }
} }
@@ -123,3 +142,18 @@ func (q *QBit) GetTorrentProperties(t *Torrent) *TorrentProperties {
ShareRatio: 100, ShareRatio: 100,
} }
} }
func (q *QBit) GetTorrentFiles(t *Torrent) []*TorrentFile {
files := make([]*TorrentFile, 0)
if t.DebridTorrent == nil {
return files
}
for index, file := range t.DebridTorrent.Files {
files = append(files, &TorrentFile{
Index: index,
Name: file.Path,
Size: file.Size,
})
}
return files
}