Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f118c5b794 | ||
|
|
f6c6144601 | ||
|
|
ff74e279d9 | ||
|
|
ba147ac56c |
14
CHANGELOG.md
14
CHANGELOG.md
@@ -60,4 +60,16 @@
|
||||
|
||||
- Delete uncached items from RD
|
||||
- Fail if the torrent is not cached(optional)
|
||||
- Fix cache not being updated
|
||||
- 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
|
||||
@@ -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
|
||||
|
||||
FROM scratch
|
||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
COPY --from=builder /blackhole /blackhole
|
||||
|
||||
EXPOSE 8181
|
||||
|
||||
11
README.md
11
README.md
@@ -68,16 +68,19 @@ Download the binary from the releases page and run it with the config file.
|
||||
"max_cache_size": 1000,
|
||||
"qbittorrent": {
|
||||
"port": "8282",
|
||||
"username": "admin", // deprecated
|
||||
"password": "admin", // deprecated
|
||||
"download_folder": "/media/symlinks/",
|
||||
"categories": ["sonarr", "radarr"],
|
||||
"refresh_interval": 5 // in seconds
|
||||
"refresh_interval": 5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 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
|
||||
- This config key is important as it's used for both Blackhole and Proxy
|
||||
|
||||
@@ -93,6 +96,7 @@ Download the binary from the releases page and run it with the config file.
|
||||
- The `port` key is the port the qBittorrent will listen on
|
||||
- The `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 `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
|
||||
|
||||
@@ -124,6 +128,7 @@ Setting Up Qbittorrent in Arr
|
||||
- Password: `sonarr_token` # Your arr token
|
||||
- Category: e.g `sonarr`, `radarr`
|
||||
- Use SSL -> `No`
|
||||
- Sequential Download -> `No`|`Yes` (If you want to download the torrents locally instead of symlink)
|
||||
- Test
|
||||
- Save
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
var (
|
||||
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)$"
|
||||
SAMPLEMATCH = `(?i)(^|[\\/]|[._-])(sample|trailer|thumb)s?([._-]|$)`
|
||||
)
|
||||
@@ -36,7 +37,7 @@ func RemoveInvalidChars(value string) string {
|
||||
}
|
||||
|
||||
func RemoveExtension(value string) string {
|
||||
re := regexp.MustCompile(VIDEOMATCH + "|" + SUBMATCH + "|" + SAMPLEMATCH)
|
||||
re := regexp.MustCompile(VIDEOMATCH + "|" + SUBMATCH + "|" + SAMPLEMATCH + "|" + MUSICMATCH)
|
||||
|
||||
// Find the last index of the matched extension
|
||||
loc := re.FindStringIndex(value)
|
||||
|
||||
@@ -217,7 +217,7 @@ func NewLogger(prefix string, output *os.File) *log.Logger {
|
||||
func GetInfohashFromURL(url string) (string, error) {
|
||||
// Download the torrent file
|
||||
var magnetLink string
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
client := &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
|
||||
5
go.mod
5
go.mod
@@ -4,10 +4,12 @@ go 1.22
|
||||
|
||||
require (
|
||||
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/ext v0.0.0-20190711103511-473e67f1d7d2
|
||||
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
|
||||
golang.org/x/time v0.6.0
|
||||
)
|
||||
@@ -15,9 +17,12 @@ require (
|
||||
require (
|
||||
github.com/anacrolix/missinggo v1.3.0 // 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/google/go-cmp v0.6.0 // 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/text v0.16.0 // indirect
|
||||
)
|
||||
|
||||
10
go.sum
10
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/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/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/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=
|
||||
@@ -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-20191230175014-e8f45d346db8 h1:GKTyiRCL6zVf5wWaqKnf+7Qs6GbEPfd4iMOitWzXJx8=
|
||||
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/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -121,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/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/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/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
@@ -187,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.1.0/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/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
|
||||
github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
||||
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
|
||||
type Service interface {
|
||||
SubmitMagnet(torrent *Torrent) (*Torrent, error)
|
||||
CheckStatus(torrent *Torrent) (*Torrent, error)
|
||||
DownloadLink(torrent *Torrent) error
|
||||
CheckStatus(torrent *Torrent, isSymlink bool) (*Torrent, error)
|
||||
GetDownloadLinks(torrent *Torrent) error
|
||||
DeleteTorrent(torrent *Torrent)
|
||||
IsAvailable(infohashes []string) map[string]bool
|
||||
GetMountPath() string
|
||||
@@ -120,7 +120,7 @@ func GetLocalCache(infohashes []string, cache *common.Cache) ([]string, map[stri
|
||||
return hashes, result
|
||||
}
|
||||
|
||||
func ProcessQBitTorrent(d Service, magnet *common.Magnet, arr *Arr) (*Torrent, error) {
|
||||
func ProcessQBitTorrent(d Service, magnet *common.Magnet, arr *Arr, isSymlink bool) (*Torrent, error) {
|
||||
debridTorrent := &Torrent{
|
||||
InfoHash: magnet.InfoHash,
|
||||
Magnet: magnet,
|
||||
@@ -144,5 +144,5 @@ func ProcessQBitTorrent(d Service, magnet *common.Magnet, arr *Arr) (*Torrent, e
|
||||
logger.Printf("Error submitting magnet: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
return d.CheckStatus(debridTorrent)
|
||||
return d.CheckStatus(debridTorrent, isSymlink)
|
||||
}
|
||||
|
||||
@@ -40,7 +40,9 @@ func GetTorrentFiles(data structs.RealDebridTorrentInfo) []TorrentFile {
|
||||
files := make([]TorrentFile, 0)
|
||||
for _, f := range data.Files {
|
||||
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
|
||||
}
|
||||
fileId := f.ID
|
||||
@@ -149,12 +151,13 @@ func (r *RealDebrid) GetTorrent(id string) (*Torrent, error) {
|
||||
torrent.Seeders = data.Seeders
|
||||
torrent.Filename = data.Filename
|
||||
torrent.OriginalFilename = data.OriginalFilename
|
||||
torrent.Links = data.Links
|
||||
files := GetTorrentFiles(data)
|
||||
torrent.Files = files
|
||||
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)
|
||||
for {
|
||||
resp, err := r.client.MakeRequest(http.MethodGet, url, nil)
|
||||
@@ -174,6 +177,8 @@ func (r *RealDebrid) CheckStatus(torrent *Torrent) (*Torrent, error) {
|
||||
torrent.Progress = data.Progress
|
||||
torrent.Speed = data.Speed
|
||||
torrent.Seeders = data.Seeders
|
||||
torrent.Links = data.Links
|
||||
torrent.Status = status
|
||||
if status == "error" || status == "dead" || status == "magnet_error" {
|
||||
return torrent, fmt.Errorf("torrent: %s has error", torrent.Name)
|
||||
} else if status == "waiting_files_selection" {
|
||||
@@ -195,11 +200,16 @@ func (r *RealDebrid) CheckStatus(torrent *Torrent) (*Torrent, error) {
|
||||
return torrent, err
|
||||
}
|
||||
} else if status == "downloaded" {
|
||||
log.Printf("Torrent: %s downloaded\n", torrent.Name)
|
||||
err = r.DownloadLink(torrent)
|
||||
if err != nil {
|
||||
return torrent, err
|
||||
files := GetTorrentFiles(data)
|
||||
torrent.Files = files
|
||||
log.Printf("Torrent: %s downloaded to RD\n", torrent.Name)
|
||||
if !isSymlink {
|
||||
err = r.GetDownloadLinks(torrent)
|
||||
if err != nil {
|
||||
return torrent, err
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
} else if status == "downloading" {
|
||||
if !r.DownloadUncached {
|
||||
@@ -225,7 +235,32 @@ func (r *RealDebrid) DeleteTorrent(torrent *Torrent) {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RealDebrid) DownloadLink(torrent *Torrent) error {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -93,4 +93,15 @@ type RealDebridTorrentInfo struct {
|
||||
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"`
|
||||
}
|
||||
|
||||
@@ -25,25 +25,34 @@ type ArrHistorySchema struct {
|
||||
}
|
||||
|
||||
type Torrent struct {
|
||||
Id string `json:"id"`
|
||||
InfoHash string `json:"info_hash"`
|
||||
Name string `json:"name"`
|
||||
Folder string `json:"folder"`
|
||||
Filename string `json:"filename"`
|
||||
OriginalFilename string `json:"original_filename"`
|
||||
Size int64 `json:"size"`
|
||||
Bytes int64 `json:"bytes"` // Size of only the files that are downloaded
|
||||
Magnet *common.Magnet `json:"magnet"`
|
||||
Files []TorrentFile `json:"files"`
|
||||
Status string `json:"status"`
|
||||
Progress float64 `json:"progress"`
|
||||
Speed int64 `json:"speed"`
|
||||
Seeders int `json:"seeders"`
|
||||
Id string `json:"id"`
|
||||
InfoHash string `json:"info_hash"`
|
||||
Name string `json:"name"`
|
||||
Folder string `json:"folder"`
|
||||
Filename string `json:"filename"`
|
||||
OriginalFilename string `json:"original_filename"`
|
||||
Size int64 `json:"size"`
|
||||
Bytes int64 `json:"bytes"` // Size of only the files that are downloaded
|
||||
Magnet *common.Magnet `json:"magnet"`
|
||||
Files []TorrentFile `json:"files"`
|
||||
Status string `json:"status"`
|
||||
Added string `json:"added"`
|
||||
Progress float64 `json:"progress"`
|
||||
Speed int64 `json:"speed"`
|
||||
Seeders int `json:"seeders"`
|
||||
Links []string `json:"links"`
|
||||
DownloadLinks []TorrentDownloadLinks `json:"download_links"`
|
||||
|
||||
Debrid *Debrid
|
||||
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 {
|
||||
return filepath.Join(parent, t.Arr.Name, t.Folder)
|
||||
}
|
||||
|
||||
@@ -211,13 +211,6 @@ func (item Item) getHash() string {
|
||||
}
|
||||
infohash = hash
|
||||
if infohash == "" {
|
||||
//Get torrent file from http link
|
||||
//Takes too long, not worth it
|
||||
//magnet, err := common.OpenMagnetHttpURL(magnetLink)
|
||||
//if err == nil && magnet != nil && magnet.InfoHash != "" {
|
||||
// log.Printf("Magnet: %s", magnet.InfoHash)
|
||||
//}
|
||||
|
||||
if strings.Contains(magnetLink, "http") {
|
||||
h, _ := common.GetInfohashFromURL(magnetLink)
|
||||
if h != "" {
|
||||
@@ -325,9 +318,7 @@ func (p *Proxy) Start() {
|
||||
})
|
||||
}
|
||||
|
||||
proxy.OnRequest(
|
||||
goproxy.ReqHostMatches(regexp.MustCompile("^.443$")),
|
||||
UrlMatches(regexp.MustCompile("^.*/api\\?t=(search|tvsearch|movie)(&.*)?$"))).HandleConnect(goproxy.AlwaysMitm)
|
||||
proxy.OnRequest(goproxy.ReqHostMatches(regexp.MustCompile("^.443$"))).HandleConnect(goproxy.AlwaysMitm)
|
||||
proxy.OnResponse(
|
||||
UrlMatches(regexp.MustCompile("^.*/api\\?t=(search|tvsearch|movie)(&.*)?$")),
|
||||
goproxy.StatusCodeIs(http.StatusOK, http.StatusAccepted)).DoFunc(
|
||||
|
||||
@@ -40,7 +40,9 @@ func (q *QBit) RefreshArr(arr *debrid.Arr) {
|
||||
if reqErr == nil {
|
||||
statusOk := strconv.Itoa(resp.StatusCode)[0] == '2'
|
||||
if statusOk {
|
||||
q.logger.Printf("Refreshed monitored downloads for %s", cmp.Or(arr.Name, arr.Host))
|
||||
if q.debug {
|
||||
q.logger.Printf("Refreshed monitored downloads for %s", cmp.Or(arr.Name, arr.Host))
|
||||
}
|
||||
}
|
||||
}
|
||||
if reqErr != 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
|
||||
}
|
||||
@@ -24,6 +24,7 @@ func (q *QBit) AddRoutes(r chi.Router) http.Handler {
|
||||
r.Get("/resume", q.handleTorrentsResume)
|
||||
r.Get("/recheck", q.handleTorrentRecheck)
|
||||
r.Get("/properties", q.handleTorrentProperties)
|
||||
r.Get("/files", q.handleTorrentFiles)
|
||||
})
|
||||
|
||||
r.Route("/app", func(r chi.Router) {
|
||||
|
||||
@@ -7,12 +7,10 @@ import (
|
||||
|
||||
func (q *QBit) handleVersion(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte("v4.3.2"))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (q *QBit) handleWebAPIVersion(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte("2.7"))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (q *QBit) handlePreferences(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -5,6 +5,5 @@ import (
|
||||
)
|
||||
|
||||
func (q *QBit) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("Ok."))
|
||||
_, _ = w.Write([]byte("Ok."))
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package qbit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -36,6 +37,8 @@ func (q *QBit) handleTorrentsAdd(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
isSymlink := strings.ToLower(r.FormValue("sequentialDownload")) != "true"
|
||||
q.logger.Printf("isSymlink: %v\n", isSymlink)
|
||||
urls := r.FormValue("urls")
|
||||
category := r.FormValue("category")
|
||||
|
||||
@@ -44,6 +47,8 @@ func (q *QBit) handleTorrentsAdd(w http.ResponseWriter, r *http.Request) {
|
||||
urlList = strings.Split(urls, "\n")
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, "isSymlink", isSymlink)
|
||||
|
||||
for _, url := range urlList {
|
||||
if err := q.AddMagnet(ctx, url, category); err != nil {
|
||||
q.logger.Printf("Error adding magnet: %v\n", err)
|
||||
@@ -158,3 +163,13 @@ func (q *QBit) handleTorrentProperties(w http.ResponseWriter, r *http.Request) {
|
||||
properties := q.GetTorrentProperties(torrent)
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -34,7 +35,7 @@ type QBit struct {
|
||||
storage *TorrentStorage
|
||||
debug bool
|
||||
logger *log.Logger
|
||||
arrs map[string]string // host:token (Used for refreshing in worker)
|
||||
arrs sync.Map // host:token (Used for refreshing in worker)
|
||||
RefreshInterval int
|
||||
}
|
||||
|
||||
@@ -54,7 +55,7 @@ func NewQBit(config *common.Config, deb debrid.Service, cache *common.Cache) *QB
|
||||
debug: cfg.Debug,
|
||||
storage: storage,
|
||||
logger: common.NewLogger("QBit", os.Stdout),
|
||||
arrs: make(map[string]string),
|
||||
arrs: sync.Map{},
|
||||
RefreshInterval: refreshInterval,
|
||||
}
|
||||
}
|
||||
@@ -62,7 +63,9 @@ func NewQBit(config *common.Config, deb debrid.Service, cache *common.Cache) *QB
|
||||
func (q *QBit) Start() {
|
||||
|
||||
r := chi.NewRouter()
|
||||
r.Use(middleware.Logger)
|
||||
if q.debug {
|
||||
r.Use(middleware.Logger)
|
||||
}
|
||||
r.Use(middleware.Recoverer)
|
||||
|
||||
q.AddRoutes(r)
|
||||
|
||||
@@ -52,7 +52,7 @@ func (q *QBit) authContext(next http.Handler) http.Handler {
|
||||
if err == nil {
|
||||
ctx = context.WithValue(r.Context(), "host", host)
|
||||
ctx = context.WithValue(ctx, "token", token)
|
||||
q.arrs[host] = token
|
||||
q.arrs.Store(host, token)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
return
|
||||
}
|
||||
|
||||
128
pkg/qbit/qbit.go
128
pkg/qbit/qbit.go
@@ -8,10 +8,7 @@ import (
|
||||
"goBlack/pkg/debrid"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -53,18 +50,17 @@ func (q *QBit) Process(ctx context.Context, magnet *common.Magnet, category stri
|
||||
Token: ctx.Value("token").(string),
|
||||
Host: ctx.Value("host").(string),
|
||||
}
|
||||
debridTorrent, err := debrid.ProcessQBitTorrent(q.debrid, magnet, arr)
|
||||
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.ID = debridTorrent.Id
|
||||
torrent.DebridTorrent = debridTorrent
|
||||
torrent.Name = debridTorrent.Name
|
||||
torrent = q.UpdateTorrentMin(torrent, debridTorrent)
|
||||
q.storage.AddOrUpdate(torrent)
|
||||
go q.processFiles(torrent, debridTorrent, arr) // We can send async for file processing not to delay the response
|
||||
go q.processFiles(torrent, debridTorrent, arr, isSymlink) // We can send async for file processing not to delay the response
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -76,104 +72,36 @@ func (q *QBit) CreateTorrentFromMagnet(magnet *common.Magnet, category string) *
|
||||
Size: magnet.Size,
|
||||
Category: category,
|
||||
State: "downloading",
|
||||
AddedOn: time.Now().Unix(),
|
||||
MagnetUri: magnet.Link,
|
||||
|
||||
Tracker: "udp://tracker.opentrackr.org:1337",
|
||||
UpLimit: -1,
|
||||
DlLimit: -1,
|
||||
FlPiecePrio: false,
|
||||
ForceStart: false,
|
||||
AutoTmm: false,
|
||||
Availability: 2,
|
||||
MaxRatio: -1,
|
||||
MaxSeedingTime: -1,
|
||||
NumComplete: 10,
|
||||
NumIncomplete: 0,
|
||||
NumLeechs: 1,
|
||||
Ratio: 1,
|
||||
RatioLimit: 1,
|
||||
Tracker: "udp://tracker.opentrackr.org:1337",
|
||||
UpLimit: -1,
|
||||
DlLimit: -1,
|
||||
AutoTmm: false,
|
||||
Ratio: 1,
|
||||
RatioLimit: 1,
|
||||
}
|
||||
return torrent
|
||||
}
|
||||
|
||||
func (q *QBit) processFiles(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)
|
||||
func (q *QBit) processFiles(torrent *Torrent, debridTorrent *debrid.Torrent, arr *debrid.Arr, isSymlink bool) {
|
||||
for debridTorrent.Status != "downloaded" {
|
||||
progress := debridTorrent.Progress
|
||||
q.logger.Printf("RD Download Progress: %.2f%%", progress)
|
||||
time.Sleep(5 * time.Second)
|
||||
dbT, err := q.debrid.CheckStatus(debridTorrent, isSymlink)
|
||||
if err != nil {
|
||||
q.logger.Printf("Error checking status: %v", err)
|
||||
q.MarkAsFailed(torrent)
|
||||
q.RefreshArr(arr)
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
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\n", fullPath)
|
||||
}
|
||||
// Check if the file exists
|
||||
if !common.FileReady(fullPath) {
|
||||
q.logger.Printf("Symlink not ready: %s\n", fullPath)
|
||||
debridTorrent = dbT
|
||||
torrent = q.UpdateTorrentMin(torrent, debridTorrent)
|
||||
}
|
||||
if isSymlink {
|
||||
q.processSymlink(torrent, debridTorrent, arr)
|
||||
} else {
|
||||
q.processManualFiles(torrent, debridTorrent, arr)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ func (ts *TorrentStorage) Get(hash string) *Torrent {
|
||||
func (ts *TorrentStorage) GetAll(category string, filter string, hashes []string) []*Torrent {
|
||||
ts.mu.RLock()
|
||||
defer ts.mu.RUnlock()
|
||||
torrents := make([]*Torrent, 0, len(ts.torrents))
|
||||
torrents := make([]*Torrent, 0)
|
||||
for _, id := range ts.order {
|
||||
torrent := ts.torrents[id]
|
||||
if category != "" && torrent.Category != category {
|
||||
|
||||
@@ -174,20 +174,20 @@ type Torrent struct {
|
||||
TorrentPath string `json:"-"`
|
||||
|
||||
AddedOn int64 `json:"added_on,omitempty"`
|
||||
AmountLeft int64 `json:"amount_left,omitempty"`
|
||||
AmountLeft int64 `json:"amount_left"`
|
||||
AutoTmm bool `json:"auto_tmm"`
|
||||
Availability float64 `json:"availability"`
|
||||
Availability float64 `json:"availability,omitempty"`
|
||||
Category string `json:"category,omitempty"`
|
||||
Completed int64 `json:"completed,omitempty"`
|
||||
Completed int64 `json:"completed"`
|
||||
CompletionOn int64 `json:"completion_on,omitempty"`
|
||||
ContentPath string `json:"content_path,omitempty"`
|
||||
DlLimit int64 `json:"dl_limit,omitempty"`
|
||||
Dlspeed int64 `json:"dlspeed,omitempty"`
|
||||
Downloaded int64 `json:"downloaded,omitempty"`
|
||||
DownloadedSession int64 `json:"downloaded_session,omitempty"`
|
||||
Eta int64 `json:"eta,omitempty"`
|
||||
FlPiecePrio bool `json:"f_l_piece_prio"`
|
||||
ForceStart bool `json:"force_start"`
|
||||
ContentPath string `json:"content_path"`
|
||||
DlLimit int64 `json:"dl_limit"`
|
||||
Dlspeed int64 `json:"dlspeed"`
|
||||
Downloaded int64 `json:"downloaded"`
|
||||
DownloadedSession int64 `json:"downloaded_session"`
|
||||
Eta int64 `json:"eta"`
|
||||
FlPiecePrio bool `json:"f_l_piece_prio,omitempty"`
|
||||
ForceStart bool `json:"force_start,omitempty"`
|
||||
Hash string `json:"hash"`
|
||||
LastActivity int64 `json:"last_activity,omitempty"`
|
||||
MagnetUri string `json:"magnet_uri,omitempty"`
|
||||
@@ -202,7 +202,7 @@ type Torrent struct {
|
||||
Progress float32 `json:"progress"`
|
||||
Ratio int64 `json:"ratio,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"`
|
||||
SeenComplete int64 `json:"seen_complete,omitempty"`
|
||||
SeqDl bool `json:"seq_dl"`
|
||||
@@ -259,6 +259,17 @@ type TorrentProperties struct {
|
||||
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 {
|
||||
preferences := &AppPreferences{
|
||||
AddTrackers: "",
|
||||
|
||||
@@ -16,6 +16,48 @@ func (q *QBit) MarkAsFailed(t *Torrent) *Torrent {
|
||||
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 {
|
||||
rcLoneMount := q.debrid.GetMountPath()
|
||||
if debridTorrent == nil && t.ID != "" {
|
||||
@@ -32,57 +74,33 @@ func (q *QBit) UpdateTorrent(t *Torrent, debridTorrent *debrid.Torrent) *Torrent
|
||||
if t.TorrentPath == "" {
|
||||
t.TorrentPath = filepath.Base(debridTorrent.GetMountFolder(rcLoneMount))
|
||||
}
|
||||
|
||||
totalSize := float64(cmp.Or(debridTorrent.Bytes, 1.0))
|
||||
progress := cmp.Or(debridTorrent.Progress, 100.0)
|
||||
progress = progress / 100.0
|
||||
var sizeCompleted int64
|
||||
|
||||
sizeCompleted = int64(totalSize * progress)
|
||||
savePath := filepath.Join(q.DownloadFolder, t.Category) + string(os.PathSeparator)
|
||||
torrentPath := filepath.Join(savePath, t.TorrentPath) + string(os.PathSeparator)
|
||||
|
||||
var speed int64
|
||||
if debridTorrent.Speed != 0 {
|
||||
speed = debridTorrent.Speed
|
||||
}
|
||||
var eta int64
|
||||
if speed != 0 {
|
||||
eta = int64((totalSize - float64(sizeCompleted)) / float64(speed))
|
||||
}
|
||||
|
||||
t.Size = debridTorrent.Bytes
|
||||
t.DebridTorrent = debridTorrent
|
||||
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.SavePath = savePath
|
||||
t = q.UpdateTorrentMin(t, debridTorrent)
|
||||
t.ContentPath = torrentPath
|
||||
t.Eta = eta
|
||||
t.Dlspeed = speed
|
||||
t.Upspeed = speed
|
||||
|
||||
if t.IsReady() {
|
||||
t.State = "pausedUP"
|
||||
q.storage.AddOrUpdate(t)
|
||||
q.storage.Update(t)
|
||||
return t
|
||||
}
|
||||
ticker := time.NewTicker(3 * time.Second)
|
||||
|
||||
ticker := time.NewTicker(2 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if t.IsReady() {
|
||||
t.State = "pausedUP"
|
||||
q.storage.AddOrUpdate(t)
|
||||
ticker.Stop()
|
||||
q.storage.Update(t)
|
||||
return t
|
||||
} else {
|
||||
return q.UpdateTorrent(t, debridTorrent)
|
||||
}
|
||||
updatedT := q.UpdateTorrent(t, debridTorrent)
|
||||
t = updatedT
|
||||
|
||||
case <-time.After(10 * time.Minute): // Add a timeout
|
||||
return t
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -123,3 +141,18 @@ func (q *QBit) GetTorrentProperties(t *Torrent) *TorrentProperties {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -20,22 +20,27 @@ func (q *QBit) StartRefreshWorker(ctx context.Context) {
|
||||
q.logger.Println("Qbit Refresh Worker stopped")
|
||||
return
|
||||
case <-refreshTicker.C:
|
||||
q.RefreshArrs()
|
||||
torrents := q.storage.GetAll("", "", nil)
|
||||
if len(torrents) > 0 {
|
||||
q.RefreshArrs()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (q *QBit) RefreshArrs() {
|
||||
torrents := q.storage.GetAll("", "", nil)
|
||||
if len(torrents) == 0 {
|
||||
return
|
||||
}
|
||||
for host, token := range q.arrs {
|
||||
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