diff --git a/README.md b/README.md index 11a301d..c6e4647 100644 --- a/README.md +++ b/README.md @@ -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 } } ``` diff --git a/cmd/main.go b/cmd/main.go index 2990d9d..21b4bdc 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -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 diff --git a/common/config.go b/common/config.go index 71bd0d8..911a441 100644 --- a/common/config.go +++ b/common/config.go @@ -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 diff --git a/common/request.go b/common/request.go index e4b29f6..6a2410a 100644 --- a/common/request.go +++ b/common/request.go @@ -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 { diff --git a/pkg/debrid/debrid.go b/pkg/debrid/debrid.go index 9993c4e..4c3619a 100644 --- a/pkg/debrid/debrid.go +++ b/pkg/debrid/debrid.go @@ -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") } diff --git a/pkg/debrid/debrid_link.go b/pkg/debrid/debrid_link.go new file mode 100644 index 0000000..3252e26 --- /dev/null +++ b/pkg/debrid/debrid_link.go @@ -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, + }, + } +} diff --git a/pkg/debrid/realdebrid.go b/pkg/debrid/realdebrid.go index cb8b03b..fe76ee6 100644 --- a/pkg/debrid/realdebrid.go +++ b/pkg/debrid/realdebrid.go @@ -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, + }, } } diff --git a/pkg/debrid/service.go b/pkg/debrid/service.go new file mode 100644 index 0000000..0fcfce6 --- /dev/null +++ b/pkg/debrid/service.go @@ -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] +} diff --git a/pkg/debrid/structs/debrid_link.go b/pkg/debrid/structs/debrid_link.go new file mode 100644 index 0000000..9ecc28e --- /dev/null +++ b/pkg/debrid/structs/debrid_link.go @@ -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] diff --git a/pkg/debrid/structs/torbox.go b/pkg/debrid/structs/torbox.go new file mode 100644 index 0000000..18ce122 --- /dev/null +++ b/pkg/debrid/structs/torbox.go @@ -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] diff --git a/pkg/debrid/torbox.go b/pkg/debrid/torbox.go new file mode 100644 index 0000000..7ac4ba1 --- /dev/null +++ b/pkg/debrid/torbox.go @@ -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, + }, + } +} diff --git a/pkg/debrid/torrent.go b/pkg/debrid/torrent.go index 46a66e9..2d03ebe 100644 --- a/pkg/debrid/torrent.go +++ b/pkg/debrid/torrent.go @@ -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 { diff --git a/pkg/proxy/proxy.go b/pkg/proxy/proxy.go index 80ee233..30e312f 100644 --- a/pkg/proxy/proxy.go +++ b/pkg/proxy/proxy.go @@ -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), } diff --git a/pkg/qbit/downloader.go b/pkg/qbit/downloader.go index 7b2704a..2d9933e 100644 --- a/pkg/qbit/downloader.go +++ b/pkg/qbit/downloader.go @@ -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) diff --git a/pkg/qbit/main.go b/pkg/qbit/main.go index 48c4767..55e8e02 100644 --- a/pkg/qbit/main.go +++ b/pkg/qbit/main.go @@ -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") diff --git a/pkg/qbit/qbit.go b/pkg/qbit/qbit.go index cb980eb..e6a3112 100644 --- a/pkg/qbit/qbit.go +++ b/pkg/qbit/qbit.go @@ -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) diff --git a/pkg/qbit/torrent.go b/pkg/qbit/torrent.go index b94c662..d126cc7 100644 --- a/pkg/qbit/torrent.go +++ b/pkg/qbit/torrent.go @@ -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