Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df2aa4e361 | ||
|
|
b51cb954f8 | ||
|
|
8bdb2e3547 | ||
|
|
2c9a076cd2 | ||
|
|
d2a77620bc | ||
|
|
4b8f1ccfb6 | ||
|
|
f118c5b794 | ||
|
|
f6c6144601 | ||
|
|
ff74e279d9 | ||
|
|
ba147ac56c | ||
|
|
01981114cb | ||
|
|
2ec0354881 | ||
|
|
329e4c60f5 | ||
|
|
d5e07dc961 | ||
|
|
f622cbfe63 |
42
CHANGELOG.md
42
CHANGELOG.md
@@ -43,4 +43,44 @@
|
|||||||
- Implement 0.2.0-beta changes
|
- Implement 0.2.0-beta changes
|
||||||
- Removed Blackhole
|
- Removed Blackhole
|
||||||
- Added QbitTorrent API
|
- Added QbitTorrent API
|
||||||
- Cleaned up the code
|
- Cleaned up the code
|
||||||
|
|
||||||
|
#### 0.2.1
|
||||||
|
|
||||||
|
- Fix Uncached torrents not being downloaded/downloaded
|
||||||
|
- Minor bug fixed
|
||||||
|
- Fix Race condition in the cache and file system
|
||||||
|
|
||||||
|
#### 0.2.2
|
||||||
|
- Fix name mismatch in the cache
|
||||||
|
- Fix directory mapping with mounts
|
||||||
|
- Add Support for refreshing the *arrs
|
||||||
|
|
||||||
|
#### 0.2.3
|
||||||
|
|
||||||
|
- Delete uncached items from RD
|
||||||
|
- Fail if the torrent is not cached(optional)
|
||||||
|
- Fix cache not being updated
|
||||||
|
|
||||||
|
#### 0.2.4
|
||||||
|
|
||||||
|
- Add file download support(Sequential Download)
|
||||||
|
- Fix http handler error
|
||||||
|
- Fix *arrs map failing concurrently
|
||||||
|
- 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
|
||||||
@@ -18,6 +18,7 @@ ADD . .
|
|||||||
RUN CGO_ENABLED=0 GOOS=$(echo $TARGETPLATFORM | cut -d '/' -f1) GOARCH=$(echo $TARGETPLATFORM | cut -d '/' -f2) go build -o /blackhole
|
RUN CGO_ENABLED=0 GOOS=$(echo $TARGETPLATFORM | cut -d '/' -f1) GOARCH=$(echo $TARGETPLATFORM | cut -d '/' -f2) go build -o /blackhole
|
||||||
|
|
||||||
FROM scratch
|
FROM scratch
|
||||||
|
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||||
COPY --from=builder /blackhole /blackhole
|
COPY --from=builder /blackhole /blackhole
|
||||||
|
|
||||||
EXPOSE 8181
|
EXPOSE 8181
|
||||||
|
|||||||
73
README.md
73
README.md
@@ -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,17 +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",
|
|
||||||
"password": "admin",
|
|
||||||
"download_folder": "/media/symlinks/",
|
"download_folder": "/media/symlinks/",
|
||||||
"categories": ["sonarr", "radarr"]
|
"categories": ["sonarr", "radarr"],
|
||||||
|
"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
|
||||||
@@ -90,9 +125,9 @@ Download the binary from the releases page and run it with the config file.
|
|||||||
|
|
||||||
##### Qbittorrent Config
|
##### Qbittorrent Config
|
||||||
- The `port` key is the port the qBittorrent will listen on
|
- The `port` key is the port the qBittorrent will listen on
|
||||||
- The `username` and `password` keys are used for basic authentication
|
|
||||||
- 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
|
||||||
|
|
||||||
@@ -120,10 +155,11 @@ Setting Up Qbittorrent in Arr
|
|||||||
- Settings -> Download Client -> Add Client -> qBittorrent
|
- Settings -> Download Client -> Add Client -> qBittorrent
|
||||||
- Host: `localhost` # or the IP of the server
|
- Host: `localhost` # or the IP of the server
|
||||||
- Port: `8282` # or the port set in the config file/ docker-compose env
|
- Port: `8282` # or the port set in the config file/ docker-compose env
|
||||||
- Username: `admin` # or the username set in the config file
|
- Username: `http://sonarr:8989` # Your arr host with http/https
|
||||||
- Password: `admin` # or the password set in the config file
|
- Password: `sonarr_token` # Your arr token
|
||||||
- Category: e.g `sonarr`, `radarr`
|
- Category: e.g `sonarr`, `radarr`
|
||||||
- Use SSL -> `No`
|
- Use SSL -> `No`
|
||||||
|
- Sequential Download -> `No`|`Yes` (If you want to download the torrents locally instead of symlink)
|
||||||
- Test
|
- Test
|
||||||
- Save
|
- Save
|
||||||
|
|
||||||
@@ -132,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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -12,28 +12,35 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ProxyConfig struct {
|
||||||
|
Port string `json:"port"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Debug bool `json:"debug"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
CachedOnly *bool `json:"cached_only"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type QBitTorrentConfig struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
Port string `json:"port"`
|
||||||
|
Debug bool `json:"debug"`
|
||||||
|
DownloadFolder string `json:"download_folder"`
|
||||||
|
Categories []string `json:"categories"`
|
||||||
|
RefreshInterval int `json:"refresh_interval"`
|
||||||
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Debrid DebridConfig `json:"debrid"`
|
Debrid DebridConfig `json:"debrid"`
|
||||||
Proxy struct {
|
Debrids []DebridConfig `json:"debrids"`
|
||||||
Port string `json:"port"`
|
Proxy ProxyConfig `json:"proxy"`
|
||||||
Enabled bool `json:"enabled"`
|
MaxCacheSize int `json:"max_cache_size"`
|
||||||
Debug bool `json:"debug"`
|
QBitTorrent QBitTorrentConfig `json:"qbittorrent"`
|
||||||
Username string `json:"username"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
CachedOnly *bool `json:"cached_only"`
|
|
||||||
}
|
|
||||||
MaxCacheSize int `json:"max_cache_size"`
|
|
||||||
QBitTorrent struct {
|
|
||||||
Username string `json:"username"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
Port string `json:"port"`
|
|
||||||
Debug bool `json:"debug"`
|
|
||||||
DownloadFolder string `json:"download_folder"`
|
|
||||||
Categories []string `json:"categories"`
|
|
||||||
} `json:"qbittorrent"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadConfig(path string) (*Config, error) {
|
func LoadConfig(path string) (*Config, error) {
|
||||||
@@ -55,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
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
VIDEOMATCH = "(?i)(\\.)(YUV|WMV|WEBM|VOB|VIV|SVI|ROQ|RMVB|RM|OGV|OGG|NSV|MXF|MTS|M2TS|TS|MPG|MPEG|M2V|MP2|MPE|MPV|MP4|M4P|M4V|MOV|QT|MNG|MKV|FLV|DRC|AVI|ASF|AMV|MKA|F4V|3GP|3G2|DIVX|X264|X265)$"
|
VIDEOMATCH = "(?i)(\\.)(YUV|WMV|WEBM|VOB|VIV|SVI|ROQ|RMVB|RM|OGV|OGG|NSV|MXF|MPG|MPEG|M2V|MP2|MPE|MPV|MP4|M4P|M4V|MOV|QT|MNG|MKV|FLV|DRC|AVI|ASF|AMV|MKA|F4V|3GP|3G2|DIVX|X264|X265)$"
|
||||||
|
MUSICMATCH = "(?i)(\\.)(?:MP3|WAV|FLAC|AAC|OGG|WMA|AIFF|ALAC|M4A|APE|AC3|DTS|M4P|MID|MIDI|MKA|MP2|MPA|RA|VOC|WV|AMR)$"
|
||||||
SUBMATCH = "(?i)(\\.)(SRT|SUB|SBV|ASS|VTT|TTML|DFXP|STL|SCC|CAP|SMI|TTXT|TDS|USF|JSS|SSA|PSB|RT|LRC|SSB)$"
|
SUBMATCH = "(?i)(\\.)(SRT|SUB|SBV|ASS|VTT|TTML|DFXP|STL|SCC|CAP|SMI|TTXT|TDS|USF|JSS|SSA|PSB|RT|LRC|SSB)$"
|
||||||
SAMPLEMATCH = `(?i)(^|[\\/]|[._-])(sample|trailer|thumb)s?([._-]|$)`
|
SAMPLEMATCH = `(?i)(^|[\\/]|[._-])(sample|trailer|thumb)s?([._-]|$)`
|
||||||
)
|
)
|
||||||
@@ -15,9 +18,26 @@ func RegexMatch(regex string, value string) bool {
|
|||||||
return re.MatchString(value)
|
return re.MatchString(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func RemoveInvalidChars(value string) string {
|
||||||
|
return strings.Map(func(r rune) rune {
|
||||||
|
if r == filepath.Separator || r == ':' {
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
if filepath.IsAbs(string(r)) {
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
if strings.ContainsRune(filepath.VolumeName("C:"+string(r)), r) {
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
if r < 32 || strings.ContainsRune(`<>:"/\|?*`, r) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}, value)
|
||||||
|
}
|
||||||
|
|
||||||
func RemoveExtension(value string) string {
|
func RemoveExtension(value string) string {
|
||||||
pattern := "(?i)(\\.)(YUV|WMV|WEBM|VOB|VIV|SVI|ROQ|RMVB|RM|OGV|OGG|NSV|MXF|MTS|M2TS|TS|MPG|MPEG|M2V|MP2|MPE|MPV|MP4|M4P|M4V|MOV|QT|MNG|MKV|FLV|DRC|AVI|ASF|AMV|MKA|F4V|3GP|3G2|DIVX|X264|X265)$"
|
re := regexp.MustCompile(VIDEOMATCH + "|" + SUBMATCH + "|" + SAMPLEMATCH + "|" + MUSICMATCH)
|
||||||
re := regexp.MustCompile(pattern)
|
|
||||||
|
|
||||||
// Find the last index of the matched extension
|
// Find the last index of the matched extension
|
||||||
loc := re.FindStringIndex(value)
|
loc := re.FindStringIndex(value)
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package common
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"context"
|
||||||
"encoding/base32"
|
"encoding/base32"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -12,9 +13,11 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Magnet struct {
|
type Magnet struct {
|
||||||
@@ -122,7 +125,6 @@ func OpenMagnetHttpURL(magnetLink string) (*Magnet, error) {
|
|||||||
Link: mi.Magnet(&hash, &info).String(),
|
Link: mi.Magnet(&hash, &info).String(),
|
||||||
}
|
}
|
||||||
return magnet, nil
|
return magnet, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetMagnetInfo(magnetLink string) (*Magnet, error) {
|
func GetMagnetInfo(magnetLink string) (*Magnet, error) {
|
||||||
@@ -211,3 +213,63 @@ func NewLogger(prefix string, output *os.File) *log.Logger {
|
|||||||
f := fmt.Sprintf("[%s] ", prefix)
|
f := fmt.Sprintf("[%s] ", prefix)
|
||||||
return log.New(output, f, log.LstdFlags)
|
return log.New(output, f, log.LstdFlags)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetInfohashFromURL(url string) (string, error) {
|
||||||
|
// Download the torrent file
|
||||||
|
var magnetLink string
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||||
|
if len(via) >= 3 {
|
||||||
|
return fmt.Errorf("stopped after 3 redirects")
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(req.URL.String(), "magnet:") {
|
||||||
|
// Stop the redirect chain
|
||||||
|
magnetLink = req.URL.String()
|
||||||
|
return http.ErrUseLastResponse
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if magnetLink != "" {
|
||||||
|
return ExtractInfoHash(magnetLink), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
mi, err := metainfo.Load(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
hash := mi.HashInfoBytes()
|
||||||
|
infoHash := hash.HexString()
|
||||||
|
return infoHash, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func JoinURL(base string, paths ...string) (string, error) {
|
||||||
|
// Parse the base URL
|
||||||
|
u, err := url.Parse(base)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Join the path components
|
||||||
|
u.Path = path.Join(u.Path, path.Join(paths...))
|
||||||
|
|
||||||
|
// Return the resulting URL as a string
|
||||||
|
return u.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func FileReady(path string) bool {
|
||||||
|
_, err := os.Stat(path)
|
||||||
|
return !os.IsNotExist(err) // Returns true if the file exists
|
||||||
|
}
|
||||||
|
|||||||
9
go.mod
9
go.mod
@@ -4,10 +4,12 @@ go 1.22
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/anacrolix/torrent v1.55.0
|
github.com/anacrolix/torrent v1.55.0
|
||||||
|
github.com/cavaliergopher/grab/v3 v3.0.1
|
||||||
github.com/elazarl/goproxy v0.0.0-20240726154733-8b0c20506380
|
github.com/elazarl/goproxy v0.0.0-20240726154733-8b0c20506380
|
||||||
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2
|
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2
|
||||||
github.com/fsnotify/fsnotify v1.7.0
|
|
||||||
github.com/go-chi/chi/v5 v5.1.0
|
github.com/go-chi/chi/v5 v5.1.0
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/valyala/fasthttp v1.55.0
|
||||||
github.com/valyala/fastjson v1.6.4
|
github.com/valyala/fastjson v1.6.4
|
||||||
golang.org/x/time v0.6.0
|
golang.org/x/time v0.6.0
|
||||||
)
|
)
|
||||||
@@ -15,11 +17,12 @@ require (
|
|||||||
require (
|
require (
|
||||||
github.com/anacrolix/missinggo v1.3.0 // indirect
|
github.com/anacrolix/missinggo v1.3.0 // indirect
|
||||||
github.com/anacrolix/missinggo/v2 v2.7.3 // indirect
|
github.com/anacrolix/missinggo/v2 v2.7.3 // indirect
|
||||||
|
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||||
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
|
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
|
||||||
github.com/google/go-cmp v0.6.0 // indirect
|
github.com/google/go-cmp v0.6.0 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
|
||||||
github.com/huandu/xstrings v1.3.2 // indirect
|
github.com/huandu/xstrings v1.3.2 // indirect
|
||||||
|
github.com/klauspost/compress v1.17.9 // indirect
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
golang.org/x/net v0.27.0 // indirect
|
golang.org/x/net v0.27.0 // indirect
|
||||||
golang.org/x/sys v0.22.0 // indirect
|
|
||||||
golang.org/x/text v0.16.0 // indirect
|
golang.org/x/text v0.16.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
14
go.sum
14
go.sum
@@ -35,6 +35,8 @@ github.com/anacrolix/tagflag v1.0.0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pm
|
|||||||
github.com/anacrolix/tagflag v1.1.0/go.mod h1:Scxs9CV10NQatSmbyjqmqmeQNwGzlNe0CMUMIxqHIG8=
|
github.com/anacrolix/tagflag v1.1.0/go.mod h1:Scxs9CV10NQatSmbyjqmqmeQNwGzlNe0CMUMIxqHIG8=
|
||||||
github.com/anacrolix/torrent v1.55.0 h1:s9yh/YGdPmbN9dTa+0Inh2dLdrLQRvEAj1jdFW/Hdd8=
|
github.com/anacrolix/torrent v1.55.0 h1:s9yh/YGdPmbN9dTa+0Inh2dLdrLQRvEAj1jdFW/Hdd8=
|
||||||
github.com/anacrolix/torrent v1.55.0/go.mod h1:sBdZHBSZNj4de0m+EbYg7vvs/G/STubxu/GzzNbojsE=
|
github.com/anacrolix/torrent v1.55.0/go.mod h1:sBdZHBSZNj4de0m+EbYg7vvs/G/STubxu/GzzNbojsE=
|
||||||
|
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||||
|
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||||
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||||
github.com/benbjohnson/immutable v0.2.0/go.mod h1:uc6OHo6PN2++n98KHLxW8ef4W42ylHiQSENghE1ezxI=
|
github.com/benbjohnson/immutable v0.2.0/go.mod h1:uc6OHo6PN2++n98KHLxW8ef4W42ylHiQSENghE1ezxI=
|
||||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||||
@@ -44,6 +46,8 @@ github.com/bradfitz/iter v0.0.0-20140124041915-454541ec3da2/go.mod h1:PyRFw1Lt2w
|
|||||||
github.com/bradfitz/iter v0.0.0-20190303215204-33e6a9893b0c/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo=
|
github.com/bradfitz/iter v0.0.0-20190303215204-33e6a9893b0c/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo=
|
||||||
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 h1:GKTyiRCL6zVf5wWaqKnf+7Qs6GbEPfd4iMOitWzXJx8=
|
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 h1:GKTyiRCL6zVf5wWaqKnf+7Qs6GbEPfd4iMOitWzXJx8=
|
||||||
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8/go.mod h1:spo1JLcs67NmW1aVLEgtA8Yy1elc+X8y5SRW1sFW4Og=
|
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8/go.mod h1:spo1JLcs67NmW1aVLEgtA8Yy1elc+X8y5SRW1sFW4Og=
|
||||||
|
github.com/cavaliergopher/grab/v3 v3.0.1 h1:4z7TkBfmPjmLAAmkkAZNX/6QJ1nNFdv3SdIHXju0Fr4=
|
||||||
|
github.com/cavaliergopher/grab/v3 v3.0.1/go.mod h1:1U/KNnD+Ft6JJiYoYBAimKH2XrYptb8Kl3DFGmsjpq4=
|
||||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@@ -62,8 +66,6 @@ github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8
|
|||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
|
||||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
|
||||||
github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
|
github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
|
||||||
github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
|
github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
|
||||||
github.com/glycerine/go-unsnap-stream v0.0.0-20190901134440-81cf024a9e0a/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
|
github.com/glycerine/go-unsnap-stream v0.0.0-20190901134440-81cf024a9e0a/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
|
||||||
@@ -123,6 +125,8 @@ github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVY
|
|||||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
|
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||||
|
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
@@ -189,6 +193,10 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
|
|||||||
github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||||
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||||
github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
|
github.com/valyala/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8k8=
|
||||||
|
github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRVfcxxoHiOM=
|
||||||
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
|
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
|
||||||
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
|
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
|
||||||
github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
||||||
@@ -233,8 +241,6 @@ golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
|
||||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||||
|
|||||||
@@ -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) (*Torrent, error)
|
|
||||||
DownloadLink(torrent *Torrent) error
|
|
||||||
Process(arr *Arr, magnet string) (*Torrent, error)
|
|
||||||
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,35 +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, category string) (*Torrent, error) {
|
func ProcessQBitTorrent(d *DebridService, magnet *common.Magnet, arr *Arr, isSymlink bool) (*Torrent, error) {
|
||||||
arr := &Arr{
|
|
||||||
CompletedFolder: category,
|
|
||||||
}
|
|
||||||
debridTorrent := &Torrent{
|
debridTorrent := &Torrent{
|
||||||
InfoHash: magnet.InfoHash,
|
InfoHash: magnet.InfoHash,
|
||||||
Magnet: magnet,
|
Magnet: magnet,
|
||||||
@@ -131,19 +144,30 @@ func ProcessQBitTorrent(d Service, magnet *common.Magnet, category string) (*Tor
|
|||||||
Arr: arr,
|
Arr: arr,
|
||||||
Size: magnet.Size,
|
Size: magnet.Size,
|
||||||
}
|
}
|
||||||
logger := d.GetLogger()
|
|
||||||
logger.Printf("Torrent Name: %s", debridTorrent.Name)
|
|
||||||
if !d.GetDownloadUncached() {
|
|
||||||
hash, exists := d.IsAvailable([]string{debridTorrent.InfoHash})[debridTorrent.InfoHash]
|
|
||||||
if !exists || !hash {
|
|
||||||
return debridTorrent, fmt.Errorf("torrent is not cached")
|
|
||||||
}
|
|
||||||
logger.Printf("Torrent: %s is cached", debridTorrent.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
debridTorrent, err := d.SubmitMagnet(debridTorrent)
|
for index, db := range d.debrids {
|
||||||
if err != nil || debridTorrent.Id == "" {
|
log.Println("Processing debrid: ", db.GetName())
|
||||||
return nil, err
|
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)
|
return nil, fmt.Errorf("failed to process torrent")
|
||||||
}
|
}
|
||||||
|
|||||||
269
pkg/debrid/debrid_link.go
Normal file
269
pkg/debrid/debrid_link.go
Normal 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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
@@ -40,13 +34,15 @@ func GetTorrentFiles(data structs.RealDebridTorrentInfo) []TorrentFile {
|
|||||||
files := make([]TorrentFile, 0)
|
files := make([]TorrentFile, 0)
|
||||||
for _, f := range data.Files {
|
for _, f := range data.Files {
|
||||||
name := filepath.Base(f.Path)
|
name := filepath.Base(f.Path)
|
||||||
if (!common.RegexMatch(common.VIDEOMATCH, name) && !common.RegexMatch(common.SUBMATCH, name)) || common.RegexMatch(common.SAMPLEMATCH, name) {
|
if (!common.RegexMatch(common.VIDEOMATCH, name) &&
|
||||||
|
!common.RegexMatch(common.SUBMATCH, name) &&
|
||||||
|
!common.RegexMatch(common.MUSICMATCH, name)) || common.RegexMatch(common.SAMPLEMATCH, name) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fileId := f.ID
|
fileId := f.ID
|
||||||
file := &TorrentFile{
|
file := &TorrentFile{
|
||||||
Name: name,
|
Name: name,
|
||||||
Path: filepath.Join(common.RemoveExtension(data.OriginalFilename), name),
|
Path: name,
|
||||||
Size: int64(f.Bytes),
|
Size: int64(f.Bytes),
|
||||||
Id: strconv.Itoa(fileId),
|
Id: strconv.Itoa(fileId),
|
||||||
}
|
}
|
||||||
@@ -55,28 +51,6 @@ func GetTorrentFiles(data structs.RealDebridTorrentInfo) []TorrentFile {
|
|||||||
return files
|
return files
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RealDebrid) Process(arr *Arr, magnet string) (*Torrent, error) {
|
|
||||||
torrent, err := GetTorrentInfo(magnet)
|
|
||||||
torrent.Arr = arr
|
|
||||||
if err != nil {
|
|
||||||
return torrent, err
|
|
||||||
}
|
|
||||||
log.Printf("Torrent Name: %s", torrent.Name)
|
|
||||||
if !r.DownloadUncached {
|
|
||||||
hash, exists := r.IsAvailable([]string{torrent.InfoHash})[torrent.InfoHash]
|
|
||||||
if !exists || !hash {
|
|
||||||
return torrent, fmt.Errorf("torrent is not cached")
|
|
||||||
}
|
|
||||||
log.Printf("Torrent: %s is cached", torrent.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
torrent, err = r.SubmitMagnet(torrent)
|
|
||||||
if err != nil || torrent.Id == "" {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return r.CheckStatus(torrent)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RealDebrid) IsAvailable(infohashes []string) map[string]bool {
|
func (r *RealDebrid) IsAvailable(infohashes []string) map[string]bool {
|
||||||
// Check if the infohashes are available in the local cache
|
// Check if the infohashes are available in the local cache
|
||||||
hashes, result := GetLocalCache(infohashes, r.cache)
|
hashes, result := GetLocalCache(infohashes, r.cache)
|
||||||
@@ -109,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
|
||||||
@@ -137,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
|
||||||
}
|
}
|
||||||
@@ -151,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
|
||||||
}
|
}
|
||||||
@@ -160,22 +137,29 @@ func (r *RealDebrid) GetTorrent(id string) (*Torrent, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return torrent, err
|
return torrent, err
|
||||||
}
|
}
|
||||||
name := common.RemoveExtension(data.OriginalFilename)
|
name := common.RemoveInvalidChars(data.OriginalFilename)
|
||||||
torrent.Id = id
|
torrent.Id = id
|
||||||
torrent.Name = name
|
torrent.Name = name
|
||||||
torrent.Bytes = data.Bytes
|
torrent.Bytes = data.Bytes
|
||||||
torrent.Folder = name
|
torrent.Folder = name
|
||||||
torrent.Progress = data.Progress
|
torrent.Progress = data.Progress
|
||||||
torrent.Status = data.Status
|
torrent.Status = data.Status
|
||||||
|
torrent.Speed = data.Speed
|
||||||
|
torrent.Seeders = data.Seeders
|
||||||
|
torrent.Filename = data.Filename
|
||||||
|
torrent.OriginalFilename = data.OriginalFilename
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RealDebrid) CheckStatus(torrent *Torrent) (*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
|
||||||
@@ -183,17 +167,25 @@ func (r *RealDebrid) CheckStatus(torrent *Torrent) (*Torrent, error) {
|
|||||||
var data structs.RealDebridTorrentInfo
|
var data structs.RealDebridTorrentInfo
|
||||||
err = json.Unmarshal(resp, &data)
|
err = json.Unmarshal(resp, &data)
|
||||||
status := data.Status
|
status := data.Status
|
||||||
torrent.Folder = common.RemoveExtension(data.OriginalFilename)
|
name := common.RemoveInvalidChars(data.OriginalFilename)
|
||||||
|
torrent.Name = name // Important because some magnet changes the name
|
||||||
|
torrent.Folder = name
|
||||||
|
torrent.Filename = data.Filename
|
||||||
|
torrent.OriginalFilename = data.OriginalFilename
|
||||||
torrent.Bytes = data.Bytes
|
torrent.Bytes = data.Bytes
|
||||||
torrent.Progress = data.Progress
|
torrent.Progress = data.Progress
|
||||||
torrent.Speed = data.Speed
|
torrent.Speed = data.Speed
|
||||||
torrent.Seeders = data.Seeders
|
torrent.Seeders = data.Seeders
|
||||||
|
torrent.Links = data.Links
|
||||||
|
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)
|
||||||
@@ -204,29 +196,79 @@ func (r *RealDebrid) CheckStatus(torrent *Torrent) (*Torrent, error) {
|
|||||||
"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
|
||||||
}
|
}
|
||||||
} else if status == "downloaded" {
|
} else if status == "downloaded" {
|
||||||
log.Printf("Torrent: %s downloaded\n", torrent.Name)
|
files := GetTorrentFiles(data)
|
||||||
err = r.DownloadLink(torrent)
|
torrent.Files = files
|
||||||
if err != nil {
|
log.Printf("Torrent: %s downloaded to RD\n", torrent.Name)
|
||||||
return torrent, err
|
if !isSymlink {
|
||||||
|
err = r.GetDownloadLinks(torrent)
|
||||||
|
if err != nil {
|
||||||
|
return torrent, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break
|
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
|
return torrent, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RealDebrid) DownloadLink(torrent *Torrent) error {
|
func (r *RealDebrid) DeleteTorrent(torrent *Torrent) {
|
||||||
|
url := fmt.Sprintf("%s/torrents/delete/%s", 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 *RealDebrid) GetDownloadLinks(torrent *Torrent) error {
|
||||||
|
url := fmt.Sprintf("%s/unrestrict/link/", r.Host)
|
||||||
|
downloadLinks := make([]TorrentDownloadLinks, 0)
|
||||||
|
for _, link := range torrent.Links {
|
||||||
|
if link == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
payload := gourl.Values{
|
||||||
|
"link": {link},
|
||||||
|
}
|
||||||
|
req, _ := http.NewRequest(http.MethodPost, url, strings.NewReader(payload.Encode()))
|
||||||
|
resp, err := r.client.MakeRequest(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var data structs.RealDebridUnrestrictResponse
|
||||||
|
if err = json.Unmarshal(resp, &data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
download := TorrentDownloadLinks{
|
||||||
|
Link: data.Link,
|
||||||
|
Filename: data.Filename,
|
||||||
|
DownloadLink: data.Download,
|
||||||
|
}
|
||||||
|
downloadLinks = append(downloadLinks, download)
|
||||||
|
}
|
||||||
|
torrent.DownloadLinks = downloadLinks
|
||||||
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 {
|
||||||
@@ -237,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
13
pkg/debrid/service.go
Normal 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]
|
||||||
|
}
|
||||||
45
pkg/debrid/structs/debrid_link.go
Normal file
45
pkg/debrid/structs/debrid_link.go
Normal 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]
|
||||||
@@ -70,17 +70,17 @@ type RealDebridAddMagnetSchema struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RealDebridTorrentInfo struct {
|
type RealDebridTorrentInfo struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Filename string `json:"filename"`
|
Filename string `json:"filename"`
|
||||||
OriginalFilename string `json:"original_filename"`
|
OriginalFilename string `json:"original_filename"`
|
||||||
Hash string `json:"hash"`
|
Hash string `json:"hash"`
|
||||||
Bytes int64 `json:"bytes"`
|
Bytes int64 `json:"bytes"`
|
||||||
OriginalBytes int `json:"original_bytes"`
|
OriginalBytes int `json:"original_bytes"`
|
||||||
Host string `json:"host"`
|
Host string `json:"host"`
|
||||||
Split int `json:"split"`
|
Split int `json:"split"`
|
||||||
Progress int `json:"progress"`
|
Progress float64 `json:"progress"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Added string `json:"added"`
|
Added string `json:"added"`
|
||||||
Files []struct {
|
Files []struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
@@ -89,8 +89,19 @@ type RealDebridTorrentInfo struct {
|
|||||||
} `json:"files"`
|
} `json:"files"`
|
||||||
Links []string `json:"links"`
|
Links []string `json:"links"`
|
||||||
Ended string `json:"ended,omitempty"`
|
Ended string `json:"ended,omitempty"`
|
||||||
Speed int `json:"speed,omitempty"`
|
Speed int64 `json:"speed,omitempty"`
|
||||||
Seeders int `json:"seeders,omitempty"`
|
Seeders int `json:"seeders,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5e6e2e77fd3921a7903a41336c844cc409bf8788/14527C07BDFDDFC642963238BB6E7507B9742947/66A1CD1A5C7F4014877A51AC2620E857E3BB4D16
|
type RealDebridUnrestrictResponse struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Filename string `json:"filename"`
|
||||||
|
MimeType string `json:"mimeType"`
|
||||||
|
Filesize int64 `json:"filesize"`
|
||||||
|
Link string `json:"link"`
|
||||||
|
Host string `json:"host"`
|
||||||
|
Chunks int64 `json:"chunks"`
|
||||||
|
Crc int64 `json:"crc"`
|
||||||
|
Download string `json:"download"`
|
||||||
|
Streamable int `json:"streamable"`
|
||||||
|
}
|
||||||
|
|||||||
75
pkg/debrid/structs/torbox.go
Normal file
75
pkg/debrid/structs/torbox.go
Normal 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
290
pkg/debrid/torbox.go
Normal 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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,24 +1,15 @@
|
|||||||
package debrid
|
package debrid
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"goBlack/common"
|
"goBlack/common"
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
gourl "net/url"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Arr struct {
|
type Arr struct {
|
||||||
WatchFolder string `json:"watch_folder"`
|
Name string `json:"name"`
|
||||||
CompletedFolder string `json:"completed_folder"`
|
Token string `json:"token"`
|
||||||
Debrid common.DebridConfig `json:"debrid"`
|
Host string `json:"host"`
|
||||||
Token string `json:"token"`
|
|
||||||
URL string `json:"url"`
|
|
||||||
Client *common.RLHTTPClient
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ArrHistorySchema struct {
|
type ArrHistorySchema struct {
|
||||||
@@ -34,26 +25,48 @@ type ArrHistorySchema struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Torrent struct {
|
type Torrent struct {
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
InfoHash string `json:"info_hash"`
|
InfoHash string `json:"info_hash"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Folder string `json:"folder"`
|
Folder string `json:"folder"`
|
||||||
Filename string `json:"filename"`
|
Filename string `json:"filename"`
|
||||||
Size int64 `json:"size"`
|
OriginalFilename string `json:"original_filename"`
|
||||||
Bytes int64 `json:"bytes"` // Size of only the files that are downloaded
|
Size int64 `json:"size"`
|
||||||
Magnet *common.Magnet `json:"magnet"`
|
Bytes int64 `json:"bytes"` // Size of only the files that are downloaded
|
||||||
Files []TorrentFile `json:"files"`
|
Magnet *common.Magnet `json:"magnet"`
|
||||||
Status string `json:"status"`
|
Files []TorrentFile `json:"files"`
|
||||||
Progress int `json:"progress"`
|
Status string `json:"status"`
|
||||||
Speed int `json:"speed"`
|
Added string `json:"added"`
|
||||||
Seeders int `json:"seeders"`
|
Progress float64 `json:"progress"`
|
||||||
|
Speed int64 `json:"speed"`
|
||||||
|
Seeders int `json:"seeders"`
|
||||||
|
Links []string `json:"links"`
|
||||||
|
DownloadLinks []TorrentDownloadLinks `json:"download_links"`
|
||||||
|
|
||||||
Debrid *Debrid
|
Debrid Service
|
||||||
Arr *Arr
|
Arr *Arr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TorrentDownloadLinks struct {
|
||||||
|
Filename string `json:"filename"`
|
||||||
|
Link string `json:"link"`
|
||||||
|
DownloadLink string `json:"download_link"`
|
||||||
|
}
|
||||||
|
|
||||||
func (t *Torrent) GetSymlinkFolder(parent string) string {
|
func (t *Torrent) GetSymlinkFolder(parent string) string {
|
||||||
return filepath.Join(parent, t.Arr.CompletedFolder, t.Folder)
|
return filepath.Join(parent, t.Arr.Name, t.Folder)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Torrent) GetMountFolder(rClonePath string) string {
|
||||||
|
if common.FileReady(filepath.Join(rClonePath, t.OriginalFilename)) {
|
||||||
|
return t.OriginalFilename
|
||||||
|
} else if common.FileReady(filepath.Join(rClonePath, t.Filename)) {
|
||||||
|
return t.Filename
|
||||||
|
} else if pathWithNoExt := common.RemoveExtension(t.OriginalFilename); common.FileReady(filepath.Join(rClonePath, pathWithNoExt)) {
|
||||||
|
return pathWithNoExt
|
||||||
|
} else {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type TorrentFile struct {
|
type TorrentFile struct {
|
||||||
@@ -61,17 +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 (arr *Arr) GetHeaders() map[string]string {
|
|
||||||
return map[string]string{
|
|
||||||
"X-Api-Key": arr.Token,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (arr *Arr) GetURL() string {
|
|
||||||
url, _ := gourl.JoinPath(arr.URL, "api/v3/")
|
|
||||||
return url
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getEventId(eventType string) int {
|
func getEventId(eventType string) int {
|
||||||
@@ -91,31 +94,6 @@ func getEventId(eventType string) int {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (arr *Arr) GetHistory(downloadId, eventType string) *ArrHistorySchema {
|
|
||||||
eventId := getEventId(eventType)
|
|
||||||
query := gourl.Values{}
|
|
||||||
if downloadId != "" {
|
|
||||||
query.Add("downloadId", downloadId)
|
|
||||||
}
|
|
||||||
if eventId != 0 {
|
|
||||||
query.Add("eventId", strconv.Itoa(eventId))
|
|
||||||
|
|
||||||
}
|
|
||||||
query.Add("pageSize", "100")
|
|
||||||
url := arr.GetURL() + "history/" + "?" + query.Encode()
|
|
||||||
resp, err := arr.Client.MakeRequest(http.MethodGet, url, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var data *ArrHistorySchema
|
|
||||||
err = json.Unmarshal(resp, &data)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return data
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Torrent) Cleanup(remove bool) {
|
func (t *Torrent) Cleanup(remove bool) {
|
||||||
if remove {
|
if remove {
|
||||||
err := os.Remove(t.Filename)
|
err := os.Remove(t.Filename)
|
||||||
@@ -124,29 +102,3 @@ func (t *Torrent) Cleanup(remove bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Torrent) MarkAsFailed() error {
|
|
||||||
downloadId := strings.ToUpper(t.Magnet.InfoHash)
|
|
||||||
history := t.Arr.GetHistory(downloadId, "grabbed")
|
|
||||||
if history == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
torrentId := 0
|
|
||||||
for _, record := range history.Records {
|
|
||||||
if strings.EqualFold(record.DownloadID, downloadId) {
|
|
||||||
torrentId = record.ID
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if torrentId != 0 {
|
|
||||||
url, err := gourl.JoinPath(t.Arr.GetURL(), "history/failed/", strconv.Itoa(torrentId))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = t.Arr.Client.MakeRequest(http.MethodPost, url, nil)
|
|
||||||
if err == nil {
|
|
||||||
log.Printf("Marked torrent: %s as failed", t.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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),
|
||||||
}
|
}
|
||||||
@@ -211,12 +211,12 @@ func (item Item) getHash() string {
|
|||||||
}
|
}
|
||||||
infohash = hash
|
infohash = hash
|
||||||
if infohash == "" {
|
if infohash == "" {
|
||||||
//Get torrent file from http link
|
if strings.Contains(magnetLink, "http") {
|
||||||
//Takes too long, not worth it
|
h, _ := common.GetInfohashFromURL(magnetLink)
|
||||||
//magnet, err := common.OpenMagnetHttpURL(magnetLink)
|
if h != "" {
|
||||||
//if err == nil && magnet != nil && magnet.InfoHash != "" {
|
infohash = h
|
||||||
// log.Printf("Magnet: %s", magnet.InfoHash)
|
}
|
||||||
//}
|
}
|
||||||
}
|
}
|
||||||
return infohash
|
return infohash
|
||||||
|
|
||||||
@@ -249,7 +249,6 @@ func (p *Proxy) ProcessXMLResponse(resp *http.Response) *http.Response {
|
|||||||
if len(rss.Channel.Items) > 0 {
|
if len(rss.Channel.Items) > 0 {
|
||||||
indexer = rss.Channel.Items[0].ProwlarrIndexer.Text
|
indexer = rss.Channel.Items[0].ProwlarrIndexer.Text
|
||||||
} else {
|
} else {
|
||||||
p.logger.Println("No items found in RSS feed")
|
|
||||||
resp.Body = io.NopCloser(bytes.NewReader(body))
|
resp.Body = io.NopCloser(bytes.NewReader(body))
|
||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
@@ -319,9 +318,7 @@ func (p *Proxy) Start() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
proxy.OnRequest(
|
proxy.OnRequest(goproxy.ReqHostMatches(regexp.MustCompile("^.443$"))).HandleConnect(goproxy.AlwaysMitm)
|
||||||
goproxy.ReqHostMatches(regexp.MustCompile("^.443$")),
|
|
||||||
UrlMatches(regexp.MustCompile("^.*/api\\?t=(search|tvsearch|movie)(&.*)?$"))).HandleConnect(goproxy.AlwaysMitm)
|
|
||||||
proxy.OnResponse(
|
proxy.OnResponse(
|
||||||
UrlMatches(regexp.MustCompile("^.*/api\\?t=(search|tvsearch|movie)(&.*)?$")),
|
UrlMatches(regexp.MustCompile("^.*/api\\?t=(search|tvsearch|movie)(&.*)?$")),
|
||||||
goproxy.StatusCodeIs(http.StatusOK, http.StatusAccepted)).DoFunc(
|
goproxy.StatusCodeIs(http.StatusOK, http.StatusAccepted)).DoFunc(
|
||||||
|
|||||||
103
pkg/qbit/arr.go
Normal file
103
pkg/qbit/arr.go
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
package qbit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"cmp"
|
||||||
|
"encoding/json"
|
||||||
|
"goBlack/common"
|
||||||
|
"goBlack/pkg/debrid"
|
||||||
|
"net/http"
|
||||||
|
gourl "net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (q *QBit) RefreshArr(arr *debrid.Arr) {
|
||||||
|
if arr.Token == "" || arr.Host == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
url, err := common.JoinURL(arr.Host, "api/v3/command")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
payload := map[string]string{"name": "RefreshMonitoredDownloads"}
|
||||||
|
jsonPayload, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonPayload))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("X-Api-Key", arr.Token)
|
||||||
|
|
||||||
|
resp, reqErr := client.Do(req)
|
||||||
|
if reqErr == nil {
|
||||||
|
statusOk := strconv.Itoa(resp.StatusCode)[0] == '2'
|
||||||
|
if statusOk {
|
||||||
|
if q.debug {
|
||||||
|
q.logger.Printf("Refreshed monitored downloads for %s", cmp.Or(arr.Name, arr.Host))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if reqErr != nil {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *QBit) GetArrHistory(arr *debrid.Arr, downloadId, eventType string) *debrid.ArrHistorySchema {
|
||||||
|
query := gourl.Values{}
|
||||||
|
if downloadId != "" {
|
||||||
|
query.Add("downloadId", downloadId)
|
||||||
|
}
|
||||||
|
query.Add("eventType", eventType)
|
||||||
|
query.Add("pageSize", "100")
|
||||||
|
url, _ := common.JoinURL(arr.Host, "history")
|
||||||
|
url += "?" + query.Encode()
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var data *debrid.ArrHistorySchema
|
||||||
|
|
||||||
|
if err = json.NewDecoder(resp.Body).Decode(&data); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *QBit) MarkArrAsFailed(torrent *Torrent, arr *debrid.Arr) error {
|
||||||
|
downloadId := strings.ToUpper(torrent.Hash)
|
||||||
|
history := q.GetArrHistory(arr, downloadId, "grabbed")
|
||||||
|
if history == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
torrentId := 0
|
||||||
|
for _, record := range history.Records {
|
||||||
|
if strings.EqualFold(record.DownloadID, downloadId) {
|
||||||
|
torrentId = record.ID
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if torrentId != 0 {
|
||||||
|
url, err := common.JoinURL(arr.Host, "history/failed/", strconv.Itoa(torrentId))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest(http.MethodPost, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
client := &http.Client{}
|
||||||
|
_, err = client.Do(req)
|
||||||
|
if err == nil {
|
||||||
|
q.logger.Printf("Marked torrent: %s as failed", torrent.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
134
pkg/qbit/downloader.go
Normal file
134
pkg/qbit/downloader.go
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
package qbit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"goBlack/common"
|
||||||
|
"goBlack/pkg/debrid"
|
||||||
|
"goBlack/pkg/qbit/downloaders"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (q *QBit) processManualFiles(torrent *Torrent, debridTorrent *debrid.Torrent, arr *debrid.Arr) {
|
||||||
|
q.logger.Printf("Downloading %d files...", len(debridTorrent.DownloadLinks))
|
||||||
|
torrentPath := common.RemoveExtension(debridTorrent.OriginalFilename)
|
||||||
|
parent := common.RemoveInvalidChars(filepath.Join(q.DownloadFolder, debridTorrent.Arr.Name, torrentPath))
|
||||||
|
err := os.MkdirAll(parent, os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
q.logger.Printf("Failed to create directory: %s\n", parent)
|
||||||
|
q.MarkAsFailed(torrent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
torrent.TorrentPath = torrentPath
|
||||||
|
q.downloadFiles(debridTorrent, parent)
|
||||||
|
q.UpdateTorrent(torrent, debridTorrent)
|
||||||
|
q.RefreshArr(arr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *QBit) downloadFiles(debridTorrent *debrid.Torrent, parent string) {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
semaphore := make(chan struct{}, 5)
|
||||||
|
client := downloaders.GetHTTPClient()
|
||||||
|
for _, link := range debridTorrent.DownloadLinks {
|
||||||
|
if link.DownloadLink == "" {
|
||||||
|
q.logger.Printf("No download link found for %s\n", link.Filename)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
wg.Add(1)
|
||||||
|
semaphore <- struct{}{}
|
||||||
|
go func(link debrid.TorrentDownloadLinks) {
|
||||||
|
defer wg.Done()
|
||||||
|
defer func() { <-semaphore }()
|
||||||
|
err := downloaders.NormalHTTP(client, link.DownloadLink, filepath.Join(parent, link.Filename))
|
||||||
|
if err != nil {
|
||||||
|
q.logger.Printf("Error downloading %s: %v\n", link.DownloadLink, err)
|
||||||
|
} else {
|
||||||
|
q.logger.Printf("Downloaded %s successfully\n", link.DownloadLink)
|
||||||
|
}
|
||||||
|
}(link)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
q.logger.Printf("Downloaded all files for %s\n", debridTorrent.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *QBit) processSymlink(torrent *Torrent, debridTorrent *debrid.Torrent, arr *debrid.Arr) {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
files := debridTorrent.Files
|
||||||
|
ready := make(chan debrid.TorrentFile, len(files))
|
||||||
|
|
||||||
|
q.logger.Printf("Checking %d files...", len(files))
|
||||||
|
rCloneBase := debridTorrent.Debrid.GetMountPath()
|
||||||
|
torrentPath, err := q.getTorrentPath(rCloneBase, debridTorrent) // /MyTVShow/
|
||||||
|
if err != nil {
|
||||||
|
q.MarkAsFailed(torrent)
|
||||||
|
q.logger.Printf("Error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
torrentSymlinkPath := filepath.Join(q.DownloadFolder, debridTorrent.Arr.Name, torrentPath) // /mnt/symlinks/{category}/MyTVShow/
|
||||||
|
err = os.MkdirAll(torrentSymlinkPath, os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
q.logger.Printf("Failed to create directory: %s\n", torrentSymlinkPath)
|
||||||
|
q.MarkAsFailed(torrent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
torrentRclonePath := filepath.Join(rCloneBase, torrentPath)
|
||||||
|
for _, file := range files {
|
||||||
|
wg.Add(1)
|
||||||
|
go checkFileLoop(&wg, torrentRclonePath, file, ready)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
close(ready)
|
||||||
|
}()
|
||||||
|
|
||||||
|
for f := range ready {
|
||||||
|
q.logger.Println("File is ready:", f.Path)
|
||||||
|
q.createSymLink(torrentSymlinkPath, torrentRclonePath, f)
|
||||||
|
}
|
||||||
|
// Update the torrent when all files are ready
|
||||||
|
torrent.TorrentPath = filepath.Base(torrentPath) // Quite important
|
||||||
|
q.UpdateTorrent(torrent, debridTorrent)
|
||||||
|
q.RefreshArr(arr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *QBit) getTorrentPath(rclonePath string, debridTorrent *debrid.Torrent) (string, error) {
|
||||||
|
pathChan := make(chan string)
|
||||||
|
errChan := make(chan error)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
torrentPath := debridTorrent.GetMountFolder(rclonePath)
|
||||||
|
if torrentPath != "" {
|
||||||
|
pathChan <- torrentPath
|
||||||
|
return
|
||||||
|
}
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case path := <-pathChan:
|
||||||
|
return path, nil
|
||||||
|
case err := <-errChan:
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *QBit) createSymLink(path string, torrentMountPath string, file debrid.TorrentFile) {
|
||||||
|
|
||||||
|
// Combine the directory and filename to form a full path
|
||||||
|
fullPath := filepath.Join(path, file.Name) // /mnt/symlinks/{category}/MyTVShow/MyTVShow.S01E01.720p.mkv
|
||||||
|
// Create a symbolic link if file doesn't exist
|
||||||
|
torrentFilePath := filepath.Join(torrentMountPath, file.Name) // debridFolder/MyTVShow/MyTVShow.S01E01.720p.mkv
|
||||||
|
err := os.Symlink(torrentFilePath, fullPath)
|
||||||
|
if err != nil {
|
||||||
|
q.logger.Printf("Failed to create symlink: %s: %v\n", fullPath, err)
|
||||||
|
}
|
||||||
|
// Check if the file exists
|
||||||
|
if !common.FileReady(fullPath) {
|
||||||
|
q.logger.Printf("Symlink not ready: %s\n", fullPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
59
pkg/qbit/downloaders/fasthttp.go
Normal file
59
pkg/qbit/downloaders/fasthttp.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package downloaders
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"github.com/valyala/fasthttp"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetFastHTTPClient() *fasthttp.Client {
|
||||||
|
return &fasthttp.Client{
|
||||||
|
TLSConfig: &tls.Config{InsecureSkipVerify: true},
|
||||||
|
StreamResponseBody: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NormalFastHTTP(client *fasthttp.Client, url, filename string) error {
|
||||||
|
req := fasthttp.AcquireRequest()
|
||||||
|
resp := fasthttp.AcquireResponse()
|
||||||
|
defer fasthttp.ReleaseRequest(req)
|
||||||
|
defer fasthttp.ReleaseResponse(resp)
|
||||||
|
|
||||||
|
req.SetRequestURI(url)
|
||||||
|
req.Header.SetMethod(fasthttp.MethodGet)
|
||||||
|
|
||||||
|
if err := client.Do(req, resp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the response status code
|
||||||
|
if resp.StatusCode() != fasthttp.StatusOK {
|
||||||
|
return fmt.Errorf("unexpected status code: %d", resp.StatusCode())
|
||||||
|
}
|
||||||
|
file, err := os.Create(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func(file *os.File) {
|
||||||
|
err := file.Close()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error closing file:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}(file)
|
||||||
|
bodyStream := resp.BodyStream()
|
||||||
|
if bodyStream == nil {
|
||||||
|
// Write to memory and then to file
|
||||||
|
_, err := file.Write(resp.Body())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if _, err := io.Copy(file, bodyStream); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
55
pkg/qbit/downloaders/grab.go
Normal file
55
pkg/qbit/downloaders/grab.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package downloaders
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"github.com/cavaliergopher/grab/v3"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetGrabClient() *grab.Client {
|
||||||
|
tr := &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||||
|
Proxy: http.ProxyFromEnvironment,
|
||||||
|
}
|
||||||
|
return &grab.Client{
|
||||||
|
UserAgent: "qBitTorrent",
|
||||||
|
HTTPClient: &http.Client{
|
||||||
|
Transport: tr,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NormalGrab(client *grab.Client, url, filename string) error {
|
||||||
|
req, err := grab.NewRequest(filename, url)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
resp := client.Do(req)
|
||||||
|
if err := resp.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
t := time.NewTicker(2 * time.Second)
|
||||||
|
defer t.Stop()
|
||||||
|
Loop:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-t.C:
|
||||||
|
fmt.Printf(" %s: transferred %d / %d bytes (%.2f%%)\n",
|
||||||
|
resp.Filename,
|
||||||
|
resp.BytesComplete(),
|
||||||
|
resp.Size,
|
||||||
|
100*resp.Progress())
|
||||||
|
|
||||||
|
case <-resp.Done:
|
||||||
|
// download is complete
|
||||||
|
break Loop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := resp.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
44
pkg/qbit/downloaders/http.go
Normal file
44
pkg/qbit/downloaders/http.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package downloaders
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetHTTPClient() *http.Client {
|
||||||
|
tr := &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||||
|
}
|
||||||
|
return &http.Client{Transport: tr}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NormalHTTP(client *http.Client, url, filename string) error {
|
||||||
|
file, err := os.Create(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// Send the HTTP GET request
|
||||||
|
resp, err := client.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error downloading file:", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// Check server response
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("server returned non-200 status: %d %s", resp.StatusCode, resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the response body to file
|
||||||
|
_, err = io.Copy(file, resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -10,7 +10,8 @@ func (q *QBit) AddRoutes(r chi.Router) http.Handler {
|
|||||||
r.Post("/auth/login", q.handleLogin)
|
r.Post("/auth/login", q.handleLogin)
|
||||||
|
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
r.Use(q.authMiddleware)
|
//r.Use(q.authMiddleware)
|
||||||
|
r.Use(q.authContext)
|
||||||
r.Route("/torrents", func(r chi.Router) {
|
r.Route("/torrents", func(r chi.Router) {
|
||||||
r.Use(HashesCtx)
|
r.Use(HashesCtx)
|
||||||
r.Get("/info", q.handleTorrentsInfo)
|
r.Get("/info", q.handleTorrentsInfo)
|
||||||
@@ -23,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) {
|
||||||
|
|||||||
@@ -7,12 +7,10 @@ import (
|
|||||||
|
|
||||||
func (q *QBit) handleVersion(w http.ResponseWriter, r *http.Request) {
|
func (q *QBit) handleVersion(w http.ResponseWriter, r *http.Request) {
|
||||||
_, _ = w.Write([]byte("v4.3.2"))
|
_, _ = w.Write([]byte("v4.3.2"))
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *QBit) handleWebAPIVersion(w http.ResponseWriter, r *http.Request) {
|
func (q *QBit) handleWebAPIVersion(w http.ResponseWriter, r *http.Request) {
|
||||||
_, _ = w.Write([]byte("2.7"))
|
_, _ = w.Write([]byte("2.7"))
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *QBit) handlePreferences(w http.ResponseWriter, r *http.Request) {
|
func (q *QBit) handlePreferences(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
@@ -2,49 +2,8 @@ package qbit
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (q *QBit) handleLogin(w http.ResponseWriter, r *http.Request) {
|
func (q *QBit) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||||
err := r.ParseForm()
|
_, _ = w.Write([]byte("Ok."))
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Failed to parse form data", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
username := r.Form.Get("username")
|
|
||||||
password := r.Form.Get("password")
|
|
||||||
|
|
||||||
// In a real implementation, you'd verify credentials here
|
|
||||||
// For this mock, we'll accept any non-empty username and password
|
|
||||||
if username == "" || password == "" {
|
|
||||||
http.Error(w, "Invalid credentials", http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if username != q.Username || password != q.Password {
|
|
||||||
http.Error(w, "Invalid credentials", http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate a new SID
|
|
||||||
sid, err := generateSID()
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Failed to generate session ID", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the SID cookie
|
|
||||||
http.SetCookie(w, &http.Cookie{
|
|
||||||
Name: cookieName,
|
|
||||||
Value: sid,
|
|
||||||
Path: "/",
|
|
||||||
HttpOnly: true,
|
|
||||||
MaxAge: 315360000,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Store the session
|
|
||||||
sessions.Store(sid, time.Now().Add(24*time.Hour))
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write([]byte("Ok."))
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
package qbit
|
package qbit
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"goBlack/common"
|
"context"
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -19,6 +18,7 @@ func (q *QBit) handleTorrentsInfo(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (q *QBit) handleTorrentsAdd(w http.ResponseWriter, r *http.Request) {
|
func (q *QBit) handleTorrentsAdd(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
contentType := strings.Split(r.Header.Get("Content-Type"), ";")[0]
|
contentType := strings.Split(r.Header.Get("Content-Type"), ";")[0]
|
||||||
switch contentType {
|
switch contentType {
|
||||||
case "multipart/form-data":
|
case "multipart/form-data":
|
||||||
@@ -37,43 +37,37 @@ func (q *QBit) handleTorrentsAdd(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
files := r.MultipartForm.File["torrents"]
|
isSymlink := strings.ToLower(r.FormValue("sequentialDownload")) != "true"
|
||||||
|
q.logger.Printf("isSymlink: %v\n", isSymlink)
|
||||||
urls := r.FormValue("urls")
|
urls := r.FormValue("urls")
|
||||||
category := r.FormValue("category")
|
category := r.FormValue("category")
|
||||||
|
|
||||||
if len(files) == 0 && urls == "" {
|
|
||||||
http.Error(w, "No torrent provided", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var urlList []string
|
var urlList []string
|
||||||
if urls != "" {
|
if urls != "" {
|
||||||
urlList = strings.Split(urls, "\n")
|
urlList = strings.Split(urls, "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx = context.WithValue(ctx, "isSymlink", isSymlink)
|
||||||
|
|
||||||
for _, url := range urlList {
|
for _, url := range urlList {
|
||||||
magnet, err := common.GetMagnetFromUrl(url)
|
if err := q.AddMagnet(ctx, url, category); err != nil {
|
||||||
if err != nil {
|
q.logger.Printf("Error adding magnet: %v\n", err)
|
||||||
q.logger.Printf("Error parsing magnet link: %v\n", err)
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
go q.Process(magnet, category)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, fileHeader := range files {
|
if contentType == "multipart/form-data" {
|
||||||
file, _ := fileHeader.Open()
|
files := r.MultipartForm.File["torrents"]
|
||||||
defer file.Close()
|
for _, fileHeader := range files {
|
||||||
var reader io.Reader = file
|
if err := q.AddTorrent(ctx, fileHeader, category); err != nil {
|
||||||
magnet, err := common.GetMagnetFromFile(reader, fileHeader.Filename)
|
q.logger.Printf("Error adding torrent: %v\n", err)
|
||||||
if err != nil {
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
return
|
||||||
q.logger.Printf("Error reading file: %s", fileHeader.Filename)
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
go q.Process(magnet, category)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package qbit
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"cmp"
|
"cmp"
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/go-chi/chi/v5/middleware"
|
"github.com/go-chi/chi/v5/middleware"
|
||||||
@@ -11,56 +12,68 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type QBit struct {
|
type WorkerType struct {
|
||||||
Username string `json:"username"`
|
ticker *time.Ticker
|
||||||
Password string `json:"password"`
|
ctx context.Context
|
||||||
Port string `json:"port"`
|
|
||||||
DownloadFolder string `json:"download_folder"`
|
|
||||||
Categories []string `json:"categories"`
|
|
||||||
debrid debrid.Service
|
|
||||||
cache *common.Cache
|
|
||||||
storage *TorrentStorage
|
|
||||||
debug bool
|
|
||||||
logger *log.Logger
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
type Worker struct {
|
||||||
sessions sync.Map
|
types map[string]WorkerType
|
||||||
)
|
}
|
||||||
|
|
||||||
const (
|
type QBit struct {
|
||||||
sidLength = 32
|
Username string `json:"username"`
|
||||||
cookieName = "SID"
|
Password string `json:"password"`
|
||||||
)
|
Port string `json:"port"`
|
||||||
|
DownloadFolder string `json:"download_folder"`
|
||||||
|
Categories []string `json:"categories"`
|
||||||
|
debrid *debrid.DebridService
|
||||||
|
cache *common.Cache
|
||||||
|
storage *TorrentStorage
|
||||||
|
debug bool
|
||||||
|
logger *log.Logger
|
||||||
|
arrs sync.Map // host:token (Used for refreshing in worker)
|
||||||
|
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")
|
||||||
|
refreshInterval := cmp.Or(cfg.RefreshInterval, 10)
|
||||||
return &QBit{
|
return &QBit{
|
||||||
Username: cfg.Username,
|
Username: cfg.Username,
|
||||||
Password: cfg.Password,
|
Password: cfg.Password,
|
||||||
Port: port,
|
Port: port,
|
||||||
DownloadFolder: cfg.DownloadFolder,
|
DownloadFolder: cfg.DownloadFolder,
|
||||||
Categories: cfg.Categories,
|
Categories: cfg.Categories,
|
||||||
debrid: deb,
|
debrid: deb,
|
||||||
cache: cache,
|
cache: cache,
|
||||||
debug: cfg.Debug,
|
debug: cfg.Debug,
|
||||||
storage: storage,
|
storage: storage,
|
||||||
logger: common.NewLogger("QBit", os.Stdout),
|
logger: common.NewLogger("QBit", os.Stdout),
|
||||||
|
arrs: sync.Map{},
|
||||||
|
RefreshInterval: refreshInterval,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *QBit) Start() {
|
func (q *QBit) Start() {
|
||||||
|
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
r.Use(middleware.Logger)
|
if q.debug {
|
||||||
|
r.Use(middleware.Logger)
|
||||||
|
}
|
||||||
r.Use(middleware.Recoverer)
|
r.Use(middleware.Recoverer)
|
||||||
|
|
||||||
q.AddRoutes(r)
|
q.AddRoutes(r)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
go q.StartWorker(ctx)
|
||||||
|
|
||||||
q.logger.Printf("Starting QBit server on :%s", q.Port)
|
q.logger.Printf("Starting QBit server on :%s", q.Port)
|
||||||
port := fmt.Sprintf(":%s", q.Port)
|
port := fmt.Sprintf(":%s", q.Port)
|
||||||
q.logger.Fatal(http.ListenAndServe(port, r))
|
q.logger.Fatal(http.ListenAndServe(port, r))
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package qbit
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
|
"encoding/base64"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -23,6 +24,42 @@ func (q *QBit) authMiddleware(next http.Handler) http.Handler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DecodeAuthHeader(header string) (string, string, error) {
|
||||||
|
encodedTokens := strings.Split(header, " ")
|
||||||
|
if len(encodedTokens) != 2 {
|
||||||
|
return "", "", nil
|
||||||
|
}
|
||||||
|
encodedToken := encodedTokens[1]
|
||||||
|
|
||||||
|
bytes, err := base64.StdEncoding.DecodeString(encodedToken)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
bearer := string(bytes)
|
||||||
|
|
||||||
|
colonIndex := strings.LastIndex(bearer, ":")
|
||||||
|
host := bearer[:colonIndex]
|
||||||
|
token := bearer[colonIndex+1:]
|
||||||
|
|
||||||
|
return host, token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *QBit) authContext(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
host, token, err := DecodeAuthHeader(r.Header.Get("Authorization"))
|
||||||
|
ctx := r.Context()
|
||||||
|
if err == nil {
|
||||||
|
ctx = context.WithValue(r.Context(), "host", host)
|
||||||
|
ctx = context.WithValue(ctx, "token", token)
|
||||||
|
q.arrs.Store(host, token)
|
||||||
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func HashesCtx(next http.Handler) http.Handler {
|
func HashesCtx(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
_hashes := chi.URLParam(r, "hashes")
|
_hashes := chi.URLParam(r, "hashes")
|
||||||
|
|||||||
148
pkg/qbit/qbit.go
148
pkg/qbit/qbit.go
@@ -1,28 +1,67 @@
|
|||||||
package qbit
|
package qbit
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"goBlack/common"
|
"goBlack/common"
|
||||||
"goBlack/pkg/debrid"
|
"goBlack/pkg/debrid"
|
||||||
"os"
|
"io"
|
||||||
"path/filepath"
|
"mime/multipart"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (q *QBit) Process(magnet *common.Magnet, category string) (*Torrent, error) {
|
func (q *QBit) AddMagnet(ctx context.Context, url, category string) error {
|
||||||
torrent := q.CreateTorrentFromMagnet(magnet, category)
|
magnet, err := common.GetMagnetFromUrl(url)
|
||||||
go q.storage.AddOrUpdate(torrent)
|
if err != nil {
|
||||||
debridTorrent, err := debrid.ProcessQBitTorrent(q.debrid, magnet, category)
|
q.logger.Printf("Error parsing magnet link: %v\n", err)
|
||||||
if err != nil || debridTorrent == nil {
|
return err
|
||||||
// Mark as failed
|
|
||||||
q.MarkAsFailed(torrent)
|
|
||||||
return torrent, err
|
|
||||||
}
|
}
|
||||||
torrent.ID = debridTorrent.Id
|
err = q.Process(ctx, magnet, category)
|
||||||
q.processFiles(torrent, debridTorrent)
|
if err != nil {
|
||||||
return torrent, nil
|
q.logger.Println("Failed to process magnet:", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *QBit) AddTorrent(ctx context.Context, fileHeader *multipart.FileHeader, category string) error {
|
||||||
|
file, _ := fileHeader.Open()
|
||||||
|
defer file.Close()
|
||||||
|
var reader io.Reader = file
|
||||||
|
magnet, err := common.GetMagnetFromFile(reader, fileHeader.Filename)
|
||||||
|
if err != nil {
|
||||||
|
q.logger.Printf("Error reading file: %s", fileHeader.Filename)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = q.Process(ctx, magnet, category)
|
||||||
|
if err != nil {
|
||||||
|
q.logger.Println("Failed to process torrent:", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *QBit) Process(ctx context.Context, magnet *common.Magnet, category string) error {
|
||||||
|
torrent := q.CreateTorrentFromMagnet(magnet, category)
|
||||||
|
arr := &debrid.Arr{
|
||||||
|
Name: category,
|
||||||
|
Token: ctx.Value("token").(string),
|
||||||
|
Host: ctx.Value("host").(string),
|
||||||
|
}
|
||||||
|
isSymlink := ctx.Value("isSymlink").(bool)
|
||||||
|
debridTorrent, err := debrid.ProcessQBitTorrent(q.debrid, magnet, arr, isSymlink)
|
||||||
|
if err != nil || debridTorrent == nil {
|
||||||
|
if err == nil {
|
||||||
|
err = fmt.Errorf("failed to process torrent")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
torrent = q.UpdateTorrentMin(torrent, debridTorrent)
|
||||||
|
q.storage.AddOrUpdate(torrent)
|
||||||
|
go q.processFiles(torrent, debridTorrent, arr, isSymlink) // We can send async for file processing not to delay the response
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *QBit) CreateTorrentFromMagnet(magnet *common.Magnet, category string) *Torrent {
|
func (q *QBit) CreateTorrentFromMagnet(magnet *common.Magnet, category string) *Torrent {
|
||||||
@@ -33,69 +72,36 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *QBit) processFiles(torrent *Torrent, debridTorrent *debrid.Torrent) {
|
func (q *QBit) processFiles(torrent *Torrent, debridTorrent *debrid.Torrent, arr *debrid.Arr, isSymlink bool) {
|
||||||
var wg sync.WaitGroup
|
for debridTorrent.Status != "downloaded" {
|
||||||
files := debridTorrent.Files
|
progress := debridTorrent.Progress
|
||||||
ready := make(chan debrid.TorrentFile, len(files))
|
q.logger.Printf("%s Download Progress: %.2f%%", debridTorrent.Debrid.GetName(), progress)
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
q.logger.Printf("Checking %d files...", len(files))
|
dbT, err := debridTorrent.Debrid.CheckStatus(debridTorrent, isSymlink)
|
||||||
rCloneMountPath := q.debrid.GetMountPath()
|
if err != nil {
|
||||||
path := filepath.Join(q.DownloadFolder, debridTorrent.Arr.CompletedFolder, debridTorrent.Folder) // /mnt/symlinks/{category}/MyTVShow/
|
q.logger.Printf("Error checking status: %v", err)
|
||||||
err := os.MkdirAll(path, os.ModePerm)
|
q.MarkAsFailed(torrent)
|
||||||
if err != nil {
|
q.RefreshArr(arr)
|
||||||
q.logger.Printf("Failed to create directory: %s\n", path)
|
return
|
||||||
|
}
|
||||||
|
debridTorrent = dbT
|
||||||
|
torrent = q.UpdateTorrentMin(torrent, debridTorrent)
|
||||||
}
|
}
|
||||||
|
if isSymlink {
|
||||||
for _, file := range files {
|
q.processSymlink(torrent, debridTorrent, arr)
|
||||||
wg.Add(1)
|
} else {
|
||||||
go checkFileLoop(&wg, rCloneMountPath, file, ready)
|
q.processManualFiles(torrent, debridTorrent, arr)
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
wg.Wait()
|
|
||||||
close(ready)
|
|
||||||
}()
|
|
||||||
|
|
||||||
for f := range ready {
|
|
||||||
q.logger.Println("File is ready:", f.Path)
|
|
||||||
q.createSymLink(path, debridTorrent, f)
|
|
||||||
|
|
||||||
}
|
|
||||||
// Update the torrent when all files are ready
|
|
||||||
q.UpdateTorrent(torrent, debridTorrent)
|
|
||||||
q.logger.Printf("%s COMPLETED \n", debridTorrent.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *QBit) createSymLink(path string, torrent *debrid.Torrent, file debrid.TorrentFile) {
|
|
||||||
|
|
||||||
// Combine the directory and filename to form a full path
|
|
||||||
fullPath := filepath.Join(path, file.Name) // /mnt/symlinks/{category}/MyTVShow/MyTVShow.S01E01.720p.mkv
|
|
||||||
// Create a symbolic link if file doesn't exist
|
|
||||||
torrentMountPath := filepath.Join(q.debrid.GetMountPath(), torrent.Folder, file.Name) // debridFolder/MyTVShow/MyTVShow.S01E01.720p.mkv
|
|
||||||
_ = os.Symlink(torrentMountPath, fullPath)
|
|
||||||
// Check if the file exists
|
|
||||||
if !fileReady(fullPath) {
|
|
||||||
q.logger.Printf("Failed to create symlink: %s\n", fullPath)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ func (ts *TorrentStorage) Get(hash string) *Torrent {
|
|||||||
func (ts *TorrentStorage) GetAll(category string, filter string, hashes []string) []*Torrent {
|
func (ts *TorrentStorage) GetAll(category string, filter string, hashes []string) []*Torrent {
|
||||||
ts.mu.RLock()
|
ts.mu.RLock()
|
||||||
defer ts.mu.RUnlock()
|
defer ts.mu.RUnlock()
|
||||||
torrents := make([]*Torrent, 0, len(ts.torrents))
|
torrents := make([]*Torrent, 0)
|
||||||
for _, id := range ts.order {
|
for _, id := range ts.order {
|
||||||
torrent := ts.torrents[id]
|
torrent := ts.torrents[id]
|
||||||
if category != "" && torrent.Category != category {
|
if category != "" && torrent.Category != category {
|
||||||
@@ -95,7 +95,7 @@ func (ts *TorrentStorage) GetAll(category string, filter string, hashes []string
|
|||||||
filtered = append(filtered, torrent)
|
filtered = append(filtered, torrent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return filtered
|
torrents = filtered
|
||||||
}
|
}
|
||||||
return torrents
|
return torrents
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package qbit
|
package qbit
|
||||||
|
|
||||||
|
import "goBlack/pkg/debrid"
|
||||||
|
|
||||||
type BuildInfo struct {
|
type BuildInfo struct {
|
||||||
Libtorrent string `json:"libtorrent"`
|
Libtorrent string `json:"libtorrent"`
|
||||||
Bitness int `json:"bitness"`
|
Bitness int `json:"bitness"`
|
||||||
@@ -167,23 +169,25 @@ type TorrentCategory struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Torrent struct {
|
type Torrent struct {
|
||||||
ID string `json:"-"`
|
ID string `json:"-"`
|
||||||
|
DebridTorrent *debrid.Torrent `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"`
|
||||||
@@ -198,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"`
|
||||||
@@ -215,6 +219,10 @@ type Torrent struct {
|
|||||||
Upspeed int64 `json:"upspeed,omitempty"`
|
Upspeed int64 `json:"upspeed,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *Torrent) IsReady() bool {
|
||||||
|
return t.AmountLeft <= 0 && t.TorrentPath != ""
|
||||||
|
}
|
||||||
|
|
||||||
type TorrentProperties struct {
|
type TorrentProperties struct {
|
||||||
AdditionDate int64 `json:"addition_date,omitempty"`
|
AdditionDate int64 `json:"addition_date,omitempty"`
|
||||||
Comment string `json:"comment,omitempty"`
|
Comment string `json:"comment,omitempty"`
|
||||||
@@ -251,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: "",
|
||||||
|
|||||||
@@ -16,52 +16,94 @@ 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 {
|
||||||
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
|
||||||
}
|
}
|
||||||
totalSize := cmp.Or(debridTorrent.Bytes, 1)
|
|
||||||
progress := int64(cmp.Or(debridTorrent.Progress, 100))
|
|
||||||
progress = progress / 100.0
|
|
||||||
|
|
||||||
sizeCompleted := totalSize * progress
|
addedOn, err := time.Parse(time.RFC3339, debridTorrent.Added)
|
||||||
savePath := filepath.Join(q.DownloadFolder, t.Category) + string(os.PathSeparator)
|
if err != nil {
|
||||||
torrentPath := filepath.Join(savePath, debridTorrent.Folder) + string(os.PathSeparator)
|
addedOn = time.Now()
|
||||||
|
}
|
||||||
|
totalSize := float64(debridTorrent.Bytes)
|
||||||
|
progress := cmp.Or(debridTorrent.Progress, 100.0)
|
||||||
|
progress = progress / 100.0
|
||||||
|
sizeCompleted := int64(totalSize * progress)
|
||||||
|
|
||||||
var speed int64
|
var speed int64
|
||||||
if debridTorrent.Speed != 0 {
|
if debridTorrent.Speed != 0 {
|
||||||
speed = int64(debridTorrent.Speed)
|
speed = debridTorrent.Speed
|
||||||
}
|
}
|
||||||
var eta int64
|
var eta int64
|
||||||
if speed != 0 {
|
if speed != 0 {
|
||||||
eta = (totalSize - sizeCompleted) / speed
|
eta = int64((totalSize - float64(sizeCompleted)) / float64(speed))
|
||||||
}
|
}
|
||||||
|
t.ID = debridTorrent.Id
|
||||||
t.Name = debridTorrent.Name
|
t.Name = debridTorrent.Name
|
||||||
t.Size = debridTorrent.Bytes
|
t.AddedOn = addedOn.Unix()
|
||||||
|
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
|
||||||
t.Uploaded = sizeCompleted
|
t.Uploaded = sizeCompleted
|
||||||
t.UploadedSession = sizeCompleted
|
t.UploadedSession = sizeCompleted
|
||||||
t.AmountLeft = totalSize - sizeCompleted
|
t.AmountLeft = int64(totalSize) - sizeCompleted
|
||||||
t.Progress = 100
|
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
|
||||||
|
}
|
||||||
|
|
||||||
if t.AmountLeft == 0 {
|
func (q *QBit) UpdateTorrent(t *Torrent, debridTorrent *debrid.Torrent) *Torrent {
|
||||||
t.State = "pausedUP"
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
go q.storage.AddOrUpdate(t)
|
if t.TorrentPath == "" {
|
||||||
return t
|
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() {
|
||||||
|
t.State = "pausedUP"
|
||||||
|
q.storage.Update(t)
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
ticker := time.NewTicker(2 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
if t.IsReady() {
|
||||||
|
t.State = "pausedUP"
|
||||||
|
q.storage.Update(t)
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
updatedT := q.UpdateTorrent(t, debridTorrent)
|
||||||
|
t = updatedT
|
||||||
|
|
||||||
|
case <-time.After(10 * time.Minute): // Add a timeout
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *QBit) ResumeTorrent(t *Torrent) bool {
|
func (q *QBit) ResumeTorrent(t *Torrent) bool {
|
||||||
@@ -100,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
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,24 +1,22 @@
|
|||||||
package qbit
|
package qbit
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"goBlack/common"
|
||||||
"goBlack/pkg/debrid"
|
"goBlack/pkg/debrid"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func generateSID() (string, error) {
|
//func generateSID() (string, error) {
|
||||||
bytes := make([]byte, sidLength)
|
// bytes := make([]byte, sidLength)
|
||||||
if _, err := rand.Read(bytes); err != nil {
|
// if _, err := rand.Read(bytes); err != nil {
|
||||||
return "", err
|
// return "", err
|
||||||
}
|
// }
|
||||||
return hex.EncodeToString(bytes), nil
|
// return hex.EncodeToString(bytes), nil
|
||||||
}
|
//}
|
||||||
|
|
||||||
func JSONResponse(w http.ResponseWriter, data interface{}, code int) {
|
func JSONResponse(w http.ResponseWriter, data interface{}, code int) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
@@ -26,11 +24,6 @@ func JSONResponse(w http.ResponseWriter, data interface{}, code int) {
|
|||||||
json.NewEncoder(w).Encode(data)
|
json.NewEncoder(w).Encode(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func fileReady(path string) bool {
|
|
||||||
_, err := os.Stat(path)
|
|
||||||
return !os.IsNotExist(err) // Returns true if the file exists
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkFileLoop(wg *sync.WaitGroup, dir string, file debrid.TorrentFile, ready chan<- debrid.TorrentFile) {
|
func checkFileLoop(wg *sync.WaitGroup, dir string, file debrid.TorrentFile, ready chan<- debrid.TorrentFile) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
ticker := time.NewTicker(1 * time.Second) // Check every second
|
ticker := time.NewTicker(1 * time.Second) // Check every second
|
||||||
@@ -39,7 +32,7 @@ func checkFileLoop(wg *sync.WaitGroup, dir string, file debrid.TorrentFile, read
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
if fileReady(path) {
|
if common.FileReady(path) {
|
||||||
ready <- file
|
ready <- file
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
46
pkg/qbit/worker.go
Normal file
46
pkg/qbit/worker.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package qbit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"goBlack/pkg/debrid"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (q *QBit) StartWorker(ctx context.Context) {
|
||||||
|
q.logger.Println("Qbit Worker started")
|
||||||
|
q.StartRefreshWorker(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *QBit) StartRefreshWorker(ctx context.Context) {
|
||||||
|
refreshCtx := context.WithValue(ctx, "worker", "refresh")
|
||||||
|
refreshTicker := time.NewTicker(time.Duration(q.RefreshInterval) * time.Second)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-refreshCtx.Done():
|
||||||
|
q.logger.Println("Qbit Refresh Worker stopped")
|
||||||
|
return
|
||||||
|
case <-refreshTicker.C:
|
||||||
|
torrents := q.storage.GetAll("", "", nil)
|
||||||
|
if len(torrents) > 0 {
|
||||||
|
q.RefreshArrs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *QBit) RefreshArrs() {
|
||||||
|
q.arrs.Range(func(key, value interface{}) bool {
|
||||||
|
host, ok := key.(string)
|
||||||
|
token, ok2 := value.(string)
|
||||||
|
if !ok || !ok2 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
arr := &debrid.Arr{
|
||||||
|
Name: "",
|
||||||
|
Token: token,
|
||||||
|
Host: host,
|
||||||
|
}
|
||||||
|
q.RefreshArr(arr)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user