Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b8f1ccfb6 | ||
|
|
f118c5b794 | ||
|
|
f6c6144601 | ||
|
|
ff74e279d9 | ||
|
|
ba147ac56c | ||
|
|
01981114cb | ||
|
|
2ec0354881 | ||
|
|
329e4c60f5 | ||
|
|
d5e07dc961 | ||
|
|
f622cbfe63 |
35
CHANGELOG.md
35
CHANGELOG.md
@@ -43,4 +43,37 @@
|
|||||||
- 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
|
||||||
@@ -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
|
||||||
|
|||||||
21
README.md
21
README.md
@@ -3,7 +3,7 @@
|
|||||||
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 Proxy Support**.
|
||||||
|
|
||||||
#### Uses
|
#### Uses
|
||||||
- Mock Qbittorent API that supports the Arrs(Sonarr, Radarr, etc)
|
- Mock Qbittorent API that supports the Arrs(Sonarr, Radarr, Lidarr etc)
|
||||||
- Proxy support for the Arrs
|
- Proxy support for the Arrs
|
||||||
|
|
||||||
The proxy is useful in filtering out un-cached Real Debrid torrents
|
The proxy is useful in filtering out un-cached Real Debrid torrents
|
||||||
@@ -37,6 +37,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
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -68,15 +70,19 @@ 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
|
- This config key is important as it's used for both Blackhole and Proxy
|
||||||
|
|
||||||
@@ -90,9 +96,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 +126,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
|
||||||
|
|
||||||
|
|||||||
@@ -15,25 +15,30 @@ type DebridConfig struct {
|
|||||||
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 {
|
Proxy ProxyConfig `json:"proxy"`
|
||||||
Port string `json:"port"`
|
MaxCacheSize int `json:"max_cache_size"`
|
||||||
Enabled bool `json:"enabled"`
|
QBitTorrent QBitTorrentConfig `json:"qbittorrent"`
|
||||||
Debug bool `json:"debug"`
|
|
||||||
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) {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ import (
|
|||||||
|
|
||||||
type Service interface {
|
type Service interface {
|
||||||
SubmitMagnet(torrent *Torrent) (*Torrent, error)
|
SubmitMagnet(torrent *Torrent) (*Torrent, error)
|
||||||
CheckStatus(torrent *Torrent) (*Torrent, error)
|
CheckStatus(torrent *Torrent, isSymlink bool) (*Torrent, error)
|
||||||
DownloadLink(torrent *Torrent) error
|
GetDownloadLinks(torrent *Torrent) error
|
||||||
Process(arr *Arr, magnet string) (*Torrent, error)
|
DeleteTorrent(torrent *Torrent)
|
||||||
IsAvailable(infohashes []string) map[string]bool
|
IsAvailable(infohashes []string) map[string]bool
|
||||||
GetMountPath() string
|
GetMountPath() string
|
||||||
GetDownloadUncached() bool
|
GetDownloadUncached() bool
|
||||||
@@ -120,10 +120,7 @@ func GetLocalCache(infohashes []string, cache *common.Cache) ([]string, map[stri
|
|||||||
return hashes, result
|
return hashes, result
|
||||||
}
|
}
|
||||||
|
|
||||||
func ProcessQBitTorrent(d Service, magnet *common.Magnet, category string) (*Torrent, error) {
|
func ProcessQBitTorrent(d Service, 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,
|
||||||
@@ -132,18 +129,20 @@ func ProcessQBitTorrent(d Service, magnet *common.Magnet, category string) (*Tor
|
|||||||
Size: magnet.Size,
|
Size: magnet.Size,
|
||||||
}
|
}
|
||||||
logger := d.GetLogger()
|
logger := d.GetLogger()
|
||||||
logger.Printf("Torrent Name: %s", debridTorrent.Name)
|
logger.Printf("Torrent Hash: %s", debridTorrent.InfoHash)
|
||||||
if !d.GetDownloadUncached() {
|
if !d.GetDownloadUncached() {
|
||||||
hash, exists := d.IsAvailable([]string{debridTorrent.InfoHash})[debridTorrent.InfoHash]
|
hash, exists := d.IsAvailable([]string{debridTorrent.InfoHash})[debridTorrent.InfoHash]
|
||||||
if !exists || !hash {
|
if !exists || !hash {
|
||||||
return debridTorrent, fmt.Errorf("torrent is not cached")
|
return debridTorrent, fmt.Errorf("torrent: %s is not cached", debridTorrent.Name)
|
||||||
|
} else {
|
||||||
|
logger.Printf("Torrent: %s is cached(or downloading)", debridTorrent.Name)
|
||||||
}
|
}
|
||||||
logger.Printf("Torrent: %s is cached", debridTorrent.Name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
debridTorrent, err := d.SubmitMagnet(debridTorrent)
|
debridTorrent, err := d.SubmitMagnet(debridTorrent)
|
||||||
if err != nil || debridTorrent.Id == "" {
|
if err != nil || debridTorrent.Id == "" {
|
||||||
|
logger.Printf("Error submitting magnet: %s", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return d.CheckStatus(debridTorrent)
|
return d.CheckStatus(debridTorrent, isSymlink)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,13 +40,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 +57,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)
|
||||||
@@ -160,19 +140,24 @@ 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
|
||||||
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)
|
||||||
for {
|
for {
|
||||||
resp, err := r.client.MakeRequest(http.MethodGet, url, nil)
|
resp, err := r.client.MakeRequest(http.MethodGet, url, nil)
|
||||||
@@ -183,17 +168,24 @@ 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
|
||||||
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)
|
||||||
@@ -209,11 +201,24 @@ func (r *RealDebrid) CheckStatus(torrent *Torrent) (*Torrent, error) {
|
|||||||
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
|
||||||
|
} 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
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,7 +226,42 @@ func (r *RealDebrid) CheckStatus(torrent *Torrent) (*Torrent, error) {
|
|||||||
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)
|
||||||
|
_, err := r.client.MakeRequest(http.MethodDelete, url, nil)
|
||||||
|
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},
|
||||||
|
}
|
||||||
|
resp, err := r.client.MakeRequest(http.MethodPost, url, strings.NewReader(payload.Encode()))
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,49 @@ 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 *Debrid
|
||||||
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 {
|
||||||
|
pathWithNoExt := common.RemoveExtension(t.OriginalFilename)
|
||||||
|
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 common.FileReady(filepath.Join(rClonePath, pathWithNoExt)) {
|
||||||
|
return pathWithNoExt
|
||||||
|
} else {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type TorrentFile struct {
|
type TorrentFile struct {
|
||||||
@@ -63,17 +77,6 @@ type TorrentFile struct {
|
|||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
switch eventType {
|
switch eventType {
|
||||||
case "grabbed":
|
case "grabbed":
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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 := q.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.Service
|
||||||
|
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.Service, 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("RD Download Progress: %.2f%%", progress)
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
q.logger.Printf("Checking %d files...", len(files))
|
dbT, err := q.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,7 +16,50 @@ func (q *QBit) MarkAsFailed(t *Torrent) *Torrent {
|
|||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (q *QBit) UpdateTorrentMin(t *Torrent, debridTorrent *debrid.Torrent) *Torrent {
|
||||||
|
if debridTorrent == nil {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
addedOn, err := time.Parse(time.RFC3339, debridTorrent.Added)
|
||||||
|
if err != nil {
|
||||||
|
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
|
||||||
|
if debridTorrent.Speed != 0 {
|
||||||
|
speed = debridTorrent.Speed
|
||||||
|
}
|
||||||
|
var eta int64
|
||||||
|
if speed != 0 {
|
||||||
|
eta = int64((totalSize - float64(sizeCompleted)) / float64(speed))
|
||||||
|
}
|
||||||
|
t.ID = debridTorrent.Id
|
||||||
|
t.Name = debridTorrent.Name
|
||||||
|
t.AddedOn = addedOn.Unix()
|
||||||
|
t.DebridTorrent = debridTorrent
|
||||||
|
t.Size = int64(totalSize)
|
||||||
|
t.Completed = sizeCompleted
|
||||||
|
t.Downloaded = sizeCompleted
|
||||||
|
t.DownloadedSession = sizeCompleted
|
||||||
|
t.Uploaded = sizeCompleted
|
||||||
|
t.UploadedSession = sizeCompleted
|
||||||
|
t.AmountLeft = int64(totalSize) - sizeCompleted
|
||||||
|
t.Progress = float32(progress)
|
||||||
|
t.Eta = eta
|
||||||
|
t.Dlspeed = speed
|
||||||
|
t.Upspeed = speed
|
||||||
|
t.SavePath = filepath.Join(q.DownloadFolder, t.Category) + string(os.PathSeparator)
|
||||||
|
t.ContentPath = filepath.Join(t.SavePath, t.Name) + string(os.PathSeparator)
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
func (q *QBit) UpdateTorrent(t *Torrent, debridTorrent *debrid.Torrent) *Torrent {
|
func (q *QBit) UpdateTorrent(t *Torrent, debridTorrent *debrid.Torrent) *Torrent {
|
||||||
|
rcLoneMount := q.debrid.GetMountPath()
|
||||||
if debridTorrent == nil && t.ID != "" {
|
if debridTorrent == nil && t.ID != "" {
|
||||||
debridTorrent, _ = q.debrid.GetTorrent(t.ID)
|
debridTorrent, _ = q.debrid.GetTorrent(t.ID)
|
||||||
}
|
}
|
||||||
@@ -24,44 +67,42 @@ func (q *QBit) UpdateTorrent(t *Torrent, debridTorrent *debrid.Torrent) *Torrent
|
|||||||
q.logger.Printf("Torrent with ID %s not found in %s", t.ID, q.debrid.GetName())
|
q.logger.Printf("Torrent with ID %s not found in %s", t.ID, q.debrid.GetName())
|
||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
totalSize := cmp.Or(debridTorrent.Bytes, 1)
|
if debridTorrent.Status != "downloaded" {
|
||||||
progress := int64(cmp.Or(debridTorrent.Progress, 100))
|
debridTorrent, _ = q.debrid.GetTorrent(t.ID)
|
||||||
progress = progress / 100.0
|
}
|
||||||
|
|
||||||
sizeCompleted := totalSize * progress
|
if t.TorrentPath == "" {
|
||||||
|
t.TorrentPath = filepath.Base(debridTorrent.GetMountFolder(rcLoneMount))
|
||||||
|
}
|
||||||
savePath := filepath.Join(q.DownloadFolder, t.Category) + string(os.PathSeparator)
|
savePath := filepath.Join(q.DownloadFolder, t.Category) + string(os.PathSeparator)
|
||||||
torrentPath := filepath.Join(savePath, debridTorrent.Folder) + string(os.PathSeparator)
|
torrentPath := filepath.Join(savePath, t.TorrentPath) + string(os.PathSeparator)
|
||||||
|
t = q.UpdateTorrentMin(t, debridTorrent)
|
||||||
var speed int64
|
|
||||||
if debridTorrent.Speed != 0 {
|
|
||||||
speed = int64(debridTorrent.Speed)
|
|
||||||
}
|
|
||||||
var eta int64
|
|
||||||
if speed != 0 {
|
|
||||||
eta = (totalSize - sizeCompleted) / speed
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Name = debridTorrent.Name
|
|
||||||
t.Size = debridTorrent.Bytes
|
|
||||||
t.Completed = sizeCompleted
|
|
||||||
t.Downloaded = sizeCompleted
|
|
||||||
t.DownloadedSession = sizeCompleted
|
|
||||||
t.Uploaded = sizeCompleted
|
|
||||||
t.UploadedSession = sizeCompleted
|
|
||||||
t.AmountLeft = totalSize - sizeCompleted
|
|
||||||
t.Progress = 100
|
|
||||||
t.SavePath = savePath
|
|
||||||
t.ContentPath = torrentPath
|
t.ContentPath = torrentPath
|
||||||
t.Eta = eta
|
|
||||||
t.Dlspeed = speed
|
|
||||||
t.Upspeed = speed
|
|
||||||
|
|
||||||
if t.AmountLeft == 0 {
|
if t.IsReady() {
|
||||||
t.State = "pausedUP"
|
t.State = "pausedUP"
|
||||||
|
q.storage.Update(t)
|
||||||
|
return t
|
||||||
}
|
}
|
||||||
|
|
||||||
go q.storage.AddOrUpdate(t)
|
ticker := time.NewTicker(2 * time.Second)
|
||||||
return t
|
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 +141,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