Changelog 0.2.2

This commit is contained in:
Mukhtar Akere
2024-09-15 03:33:28 +01:00
parent d5e07dc961
commit 329e4c60f5
22 changed files with 445 additions and 245 deletions

View File

@@ -50,3 +50,8 @@
- 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

View File

@@ -68,10 +68,11 @@ Download the binary from the releases page and run it with the config file.
"max_cache_size": 1000,
"qbittorrent": {
"port": "8282",
"username": "admin",
"password": "admin",
"username": "admin", // deprecated
"password": "admin", // deprecated
"download_folder": "/media/symlinks/",
"categories": ["sonarr", "radarr"]
"categories": ["sonarr", "radarr"],
"refresh_interval": 5 // in seconds
}
}
```
@@ -90,7 +91,6 @@ Download the binary from the releases page and run it with the config file.
##### Qbittorrent Config
- 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 `categories` key is used to filter out torrents based on the category. e.g `sonarr`, `radarr`
@@ -120,8 +120,8 @@ Setting Up Qbittorrent in Arr
- Settings -> Download Client -> Add Client -> qBittorrent
- Host: `localhost` # or the IP of the server
- Port: `8282` # or the port set in the config file/ docker-compose env
- Username: `admin` # or the username set in the config file
- Password: `admin` # or the password set in the config file
- Username: `http://sonarr:8989` # Your arr host with http/https
- Password: `sonarr_token` # Your arr token
- Category: e.g `sonarr`, `radarr`
- Use SSL -> `No`
- Test

View File

@@ -25,12 +25,13 @@ type ProxyConfig struct {
}
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"`
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 {

View File

@@ -1,7 +1,9 @@
package common
import (
"path/filepath"
"regexp"
"strings"
)
var (
@@ -15,8 +17,26 @@ func RegexMatch(regex string, value string) bool {
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 {
re := regexp.MustCompile(VIDEOMATCH)
re := regexp.MustCompile(VIDEOMATCH + "|" + SUBMATCH + "|" + SAMPLEMATCH)
// Find the last index of the matched extension
loc := re.FindStringIndex(value)

View File

@@ -13,6 +13,7 @@ import (
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"regexp"
"strings"
@@ -124,7 +125,6 @@ func OpenMagnetHttpURL(magnetLink string) (*Magnet, error) {
Link: mi.Magnet(&hash, &info).String(),
}
return magnet, nil
}
func GetMagnetInfo(magnetLink string) (*Magnet, error) {
@@ -254,3 +254,22 @@ func GetInfohashFromURL(url string) (string, error) {
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
}

View File

@@ -119,10 +119,7 @@ func GetLocalCache(infohashes []string, cache *common.Cache) ([]string, map[stri
return hashes, result
}
func ProcessQBitTorrent(d Service, magnet *common.Magnet, category string) (*Torrent, error) {
arr := &Arr{
CompletedFolder: category,
}
func ProcessQBitTorrent(d Service, magnet *common.Magnet, arr *Arr) (*Torrent, error) {
debridTorrent := &Torrent{
InfoHash: magnet.InfoHash,
Magnet: magnet,

View File

@@ -46,7 +46,7 @@ func GetTorrentFiles(data structs.RealDebridTorrentInfo) []TorrentFile {
fileId := f.ID
file := &TorrentFile{
Name: name,
Path: filepath.Join(common.RemoveExtension(data.OriginalFilename), name),
Path: name,
Size: int64(f.Bytes),
Id: strconv.Itoa(fileId),
}
@@ -138,13 +138,17 @@ func (r *RealDebrid) GetTorrent(id string) (*Torrent, error) {
if err != nil {
return torrent, err
}
name := common.RemoveExtension(data.OriginalFilename)
name := common.RemoveInvalidChars(data.OriginalFilename)
torrent.Id = id
torrent.Name = name
torrent.Bytes = data.Bytes
torrent.Folder = name
torrent.Progress = data.Progress
torrent.Status = data.Status
torrent.Speed = data.Speed
torrent.Seeders = data.Seeders
torrent.Filename = data.Filename
torrent.OriginalFilename = data.OriginalFilename
files := GetTorrentFiles(data)
torrent.Files = files
return torrent, nil
@@ -161,9 +165,11 @@ func (r *RealDebrid) CheckStatus(torrent *Torrent) (*Torrent, error) {
var data structs.RealDebridTorrentInfo
err = json.Unmarshal(resp, &data)
status := data.Status
name := 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.Progress = data.Progress
torrent.Speed = data.Speed

View File

@@ -70,17 +70,17 @@ type RealDebridAddMagnetSchema struct {
}
type RealDebridTorrentInfo struct {
ID string `json:"id"`
Filename string `json:"filename"`
OriginalFilename string `json:"original_filename"`
Hash string `json:"hash"`
Bytes int64 `json:"bytes"`
OriginalBytes int `json:"original_bytes"`
Host string `json:"host"`
Split int `json:"split"`
Progress int `json:"progress"`
Status string `json:"status"`
Added string `json:"added"`
ID string `json:"id"`
Filename string `json:"filename"`
OriginalFilename string `json:"original_filename"`
Hash string `json:"hash"`
Bytes int64 `json:"bytes"`
OriginalBytes int `json:"original_bytes"`
Host string `json:"host"`
Split int `json:"split"`
Progress float64 `json:"progress"`
Status string `json:"status"`
Added string `json:"added"`
Files []struct {
ID int `json:"id"`
Path string `json:"path"`
@@ -89,7 +89,7 @@ type RealDebridTorrentInfo struct {
} `json:"files"`
Links []string `json:"links"`
Ended string `json:"ended,omitempty"`
Speed int `json:"speed,omitempty"`
Speed int64 `json:"speed,omitempty"`
Seeders int `json:"seeders,omitempty"`
}

View File

@@ -1,24 +1,15 @@
package debrid
import (
"encoding/json"
"goBlack/common"
"log"
"net/http"
gourl "net/url"
"os"
"path/filepath"
"strconv"
"strings"
)
type Arr struct {
WatchFolder string `json:"watch_folder"`
CompletedFolder string `json:"completed_folder"`
Debrid common.DebridConfig `json:"debrid"`
Token string `json:"token"`
URL string `json:"url"`
Client *common.RLHTTPClient
Name string `json:"name"`
Token string `json:"token"`
Host string `json:"host"`
}
type ArrHistorySchema struct {
@@ -34,26 +25,40 @@ 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"`
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 int `json:"progress"`
Speed int `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"`
Progress float64 `json:"progress"`
Speed int64 `json:"speed"`
Seeders int `json:"seeders"`
Debrid *Debrid
Arr *Arr
}
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 {
@@ -63,17 +68,6 @@ type TorrentFile struct {
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 {
switch eventType {
case "grabbed":
@@ -91,31 +85,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) {
if remove {
err := os.Remove(t.Filename)
@@ -124,29 +93,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
}

View File

@@ -289,8 +289,6 @@ func (p *Proxy) ProcessXMLResponse(resp *http.Response) *http.Response {
p.logger.Printf("[%s Report]: %d/%d items are cached || Found %d infohash", indexer, len(newItems), len(rss.Channel.Items), len(hashes))
} else {
// This will prevent the indexer from being disabled by the arr
//filename := common.RandomString(10) + ".xml"
//_ = os.WriteFile(filename, body, 0644)
p.logger.Printf("[%s Report]: No Items are cached; Return only first item with [UnCached]", indexer)
item := rss.Channel.Items[0]
item.Title = fmt.Sprintf("%s [UnCached]", item.Title)

101
pkg/qbit/arr.go Normal file
View File

@@ -0,0 +1,101 @@
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 {
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
}

View File

@@ -10,7 +10,8 @@ func (q *QBit) AddRoutes(r chi.Router) http.Handler {
r.Post("/auth/login", q.handleLogin)
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.Use(HashesCtx)
r.Get("/info", q.handleTorrentsInfo)

View File

@@ -2,49 +2,9 @@ package qbit
import (
"net/http"
"time"
)
func (q *QBit) handleLogin(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
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."))
}

View File

@@ -19,6 +19,7 @@ func (q *QBit) handleTorrentsInfo(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]
switch contentType {
case "multipart/form-data":
@@ -52,7 +53,7 @@ func (q *QBit) handleTorrentsAdd(w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
go q.Process(magnet, category)
go q.Process(ctx, magnet, category)
}
@@ -68,7 +69,7 @@ func (q *QBit) handleTorrentsAdd(w http.ResponseWriter, r *http.Request) {
q.logger.Printf("Error reading file: %s", fileHeader.Filename)
return
}
go q.Process(magnet, category)
go q.Process(ctx, magnet, category)
}
}

View File

@@ -2,6 +2,7 @@ package qbit
import (
"cmp"
"context"
"fmt"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
@@ -10,46 +11,51 @@ import (
"log"
"net/http"
"os"
"sync"
"time"
)
type QBit struct {
Username string `json:"username"`
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
type WorkerType struct {
ticker *time.Ticker
ctx context.Context
}
var (
sessions sync.Map
)
type Worker struct {
types map[string]WorkerType
}
const (
sidLength = 32
cookieName = "SID"
)
type QBit struct {
Username string `json:"username"`
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 map[string]string // host:token (Used for refreshing in worker)
RefreshInterval int
}
func NewQBit(config *common.Config, deb debrid.Service, cache *common.Cache) *QBit {
cfg := config.QBitTorrent
storage := NewTorrentStorage("torrents.json")
port := cmp.Or(cfg.Port, os.Getenv("QBIT_PORT"), "8182")
refreshInterval := cmp.Or(cfg.RefreshInterval, 10)
return &QBit{
Username: cfg.Username,
Password: cfg.Password,
Port: port,
DownloadFolder: cfg.DownloadFolder,
Categories: cfg.Categories,
debrid: deb,
cache: cache,
debug: cfg.Debug,
storage: storage,
logger: common.NewLogger("QBit", os.Stdout),
Username: cfg.Username,
Password: cfg.Password,
Port: port,
DownloadFolder: cfg.DownloadFolder,
Categories: cfg.Categories,
debrid: deb,
cache: cache,
debug: cfg.Debug,
storage: storage,
logger: common.NewLogger("QBit", os.Stdout),
arrs: make(map[string]string),
RefreshInterval: refreshInterval,
}
}
@@ -61,6 +67,10 @@ func (q *QBit) Start() {
q.AddRoutes(r)
ctx := context.Background()
go q.StartWorker(ctx)
q.logger.Printf("Starting QBit server on :%s", q.Port)
port := fmt.Sprintf(":%s", q.Port)
q.logger.Fatal(http.ListenAndServe(port, r))

View File

@@ -3,6 +3,7 @@ package qbit
import (
"context"
"crypto/subtle"
"encoding/base64"
"github.com/go-chi/chi/v5"
"net/http"
"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[host] = token
next.ServeHTTP(w, r.WithContext(ctx))
return
}
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func HashesCtx(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_hashes := chi.URLParam(r, "hashes")

View File

@@ -1,6 +1,7 @@
package qbit
import (
"context"
"github.com/google/uuid"
"goBlack/common"
"goBlack/pkg/debrid"
@@ -11,10 +12,15 @@ import (
"time"
)
func (q *QBit) Process(magnet *common.Magnet, category string) (*Torrent, error) {
func (q *QBit) Process(ctx context.Context, magnet *common.Magnet, category string) (*Torrent, error) {
torrent := q.CreateTorrentFromMagnet(magnet, category)
go q.storage.AddOrUpdate(torrent)
debridTorrent, err := debrid.ProcessQBitTorrent(q.debrid, magnet, category)
arr := &debrid.Arr{
Name: category,
Token: ctx.Value("token").(string),
Host: ctx.Value("host").(string),
}
debridTorrent, err := debrid.ProcessQBitTorrent(q.debrid, magnet, arr)
if err != nil || debridTorrent == nil {
// Mark as failed
q.logger.Printf("Failed to process torrent: %s: %v", magnet.Name, err)
@@ -22,9 +28,9 @@ func (q *QBit) Process(magnet *common.Magnet, category string) (*Torrent, error)
return torrent, err
}
torrent.ID = debridTorrent.Id
torrent.Name = debridTorrent.Name // Update the name before adding it to *arrs storage
torrent.DebridTorrent = debridTorrent
go q.processFiles(torrent, debridTorrent)
torrent.Name = debridTorrent.Name
q.processFiles(torrent, debridTorrent, arr)
return torrent, nil
}
@@ -57,22 +63,29 @@ func (q *QBit) CreateTorrentFromMagnet(magnet *common.Magnet, category string) *
return torrent
}
func (q *QBit) processFiles(torrent *Torrent, debridTorrent *debrid.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))
rCloneMountPath := q.debrid.GetMountPath()
path := filepath.Join(q.DownloadFolder, debridTorrent.Arr.CompletedFolder, debridTorrent.Folder) // /mnt/symlinks/{category}/MyTVShow/
err := os.MkdirAll(path, os.ModePerm)
rCloneBase := q.debrid.GetMountPath()
torrentPath, err := q.getTorrentPath(rCloneBase, debridTorrent) // /MyTVShow/
if err != nil {
q.logger.Printf("Failed to create directory: %s\n", path)
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)
return
}
torrentRclonePath := filepath.Join(rCloneBase, torrentPath)
for _, file := range files {
wg.Add(1)
go checkFileLoop(&wg, rCloneMountPath, file, ready)
go checkFileLoop(&wg, torrentRclonePath, file, ready)
}
go func() {
@@ -82,23 +95,49 @@ func (q *QBit) processFiles(torrent *Torrent, debridTorrent *debrid.Torrent) {
for f := range ready {
q.logger.Println("File is ready:", f.Path)
q.createSymLink(path, debridTorrent, f)
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.logger.Printf("%s COMPLETED \n", debridTorrent.Name)
q.RefreshArr(arr)
}
func (q *QBit) createSymLink(path string, torrent *debrid.Torrent, file debrid.TorrentFile) {
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
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) {
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)
}
}

View File

@@ -95,7 +95,7 @@ func (ts *TorrentStorage) GetAll(category string, filter string, hashes []string
filtered = append(filtered, torrent)
}
}
return filtered
torrents = filtered
}
return torrents
}

View File

@@ -171,6 +171,7 @@ type TorrentCategory struct {
type Torrent struct {
ID string `json:"-"`
DebridTorrent *debrid.Torrent `json:"-"`
TorrentPath string `json:"-"`
AddedOn int64 `json:"added_on,omitempty"`
AmountLeft int64 `json:"amount_left,omitempty"`
@@ -218,6 +219,10 @@ type Torrent struct {
Upspeed int64 `json:"upspeed,omitempty"`
}
func (t *Torrent) IsReady() bool {
return t.AmountLeft <= 0 && t.TorrentPath != ""
}
type TorrentProperties struct {
AdditionDate int64 `json:"addition_date,omitempty"`
Comment string `json:"comment,omitempty"`

View File

@@ -17,6 +17,7 @@ func (q *QBit) MarkAsFailed(t *Torrent) *Torrent {
}
func (q *QBit) UpdateTorrent(t *Torrent, debridTorrent *debrid.Torrent) *Torrent {
rcLoneMount := q.debrid.GetMountPath()
if debridTorrent == nil && t.ID != "" {
debridTorrent, _ = q.debrid.GetTorrent(t.ID)
}
@@ -24,31 +25,40 @@ 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())
return t
}
totalSize := cmp.Or(debridTorrent.Bytes, 1)
progress := int64(cmp.Or(debridTorrent.Progress, 100))
progress = progress / 100.0
if debridTorrent.Status != "downloaded" {
debridTorrent, _ = q.debrid.GetTorrent(t.ID)
}
sizeCompleted := totalSize * progress
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, debridTorrent.Folder) + string(os.PathSeparator)
torrentPath := filepath.Join(savePath, t.TorrentPath) + string(os.PathSeparator)
var speed int64
if debridTorrent.Speed != 0 {
speed = int64(debridTorrent.Speed)
speed = debridTorrent.Speed
}
var eta int64
if speed != 0 {
eta = (totalSize - sizeCompleted) / speed
eta = int64((totalSize - float64(sizeCompleted)) / float64(speed))
}
t.Name = debridTorrent.Name
t.Size = debridTorrent.Bytes
t.DebridTorrent = debridTorrent
t.Completed = sizeCompleted
t.Downloaded = sizeCompleted
t.DownloadedSession = sizeCompleted
t.Uploaded = sizeCompleted
t.UploadedSession = sizeCompleted
t.AmountLeft = totalSize - sizeCompleted
t.AmountLeft = int64(totalSize) - sizeCompleted
t.Progress = float32(progress)
t.SavePath = savePath
t.ContentPath = torrentPath
@@ -56,12 +66,25 @@ func (q *QBit) UpdateTorrent(t *Torrent, debridTorrent *debrid.Torrent) *Torrent
t.Dlspeed = speed
t.Upspeed = speed
if t.AmountLeft == 0 {
if t.IsReady() {
t.State = "pausedUP"
q.storage.AddOrUpdate(t)
return t
}
ticker := time.NewTicker(3 * time.Second)
for {
select {
case <-ticker.C:
if t.IsReady() {
t.State = "pausedUP"
q.storage.AddOrUpdate(t)
ticker.Stop()
return t
} else {
return q.UpdateTorrent(t, debridTorrent)
}
}
}
go q.storage.AddOrUpdate(t)
return t
}
func (q *QBit) ResumeTorrent(t *Torrent) bool {

View File

@@ -1,24 +1,22 @@
package qbit
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"goBlack/common"
"goBlack/pkg/debrid"
"net/http"
"os"
"path/filepath"
"sync"
"time"
)
func generateSID() (string, error) {
bytes := make([]byte, sidLength)
if _, err := rand.Read(bytes); err != nil {
return "", err
}
return hex.EncodeToString(bytes), nil
}
//func generateSID() (string, error) {
// bytes := make([]byte, sidLength)
// if _, err := rand.Read(bytes); err != nil {
// return "", err
// }
// return hex.EncodeToString(bytes), nil
//}
func JSONResponse(w http.ResponseWriter, data interface{}, code int) {
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)
}
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) {
defer wg.Done()
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 {
select {
case <-ticker.C:
if fileReady(path) {
if common.FileReady(path) {
ready <- file
return
}

41
pkg/qbit/worker.go Normal file
View File

@@ -0,0 +1,41 @@
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:
q.RefreshArrs()
}
}
}
func (q *QBit) RefreshArrs() {
torrents := q.storage.GetAll("", "", nil)
if len(torrents) == 0 {
return
}
for host, token := range q.arrs {
arr := &debrid.Arr{
Name: "",
Token: token,
Host: host,
}
q.RefreshArr(arr)
}
}