init adding rclone

This commit is contained in:
Mukhtar Akere
2025-02-12 22:11:58 +01:00
parent c386495d3d
commit 878f78468f
41 changed files with 6741 additions and 0 deletions

153
pkg/qbit/downloader.go Normal file
View File

@@ -0,0 +1,153 @@
package qbit
import (
"fmt"
"github.com/sirrobot01/debrid-blackhole/common"
debrid "github.com/sirrobot01/debrid-blackhole/pkg/debrid/torrent"
"github.com/sirrobot01/debrid-blackhole/pkg/downloaders"
"os"
"path/filepath"
"sync"
"time"
)
func (q *QBit) ProcessManualFile(torrent *Torrent) (string, error) {
debridTorrent := torrent.DebridTorrent
q.logger.Info().Msgf("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 {
// add previous error to the error and return
return "", fmt.Errorf("failed to create directory: %s: %v", parent, err)
}
q.downloadFiles(torrent, parent)
return torrentPath, nil
}
func (q *QBit) downloadFiles(torrent *Torrent, parent string) {
debridTorrent := torrent.DebridTorrent
var wg sync.WaitGroup
semaphore := make(chan struct{}, 5)
totalSize := int64(0)
for _, file := range debridTorrent.Files {
totalSize += file.Size
}
debridTorrent.Mu.Lock()
debridTorrent.SizeDownloaded = 0 // Reset downloaded bytes
debridTorrent.Progress = 0 // Reset progress
debridTorrent.Mu.Unlock()
client := downloaders.GetGrabClient()
progressCallback := func(downloaded int64, speed int64) {
debridTorrent.Mu.Lock()
defer debridTorrent.Mu.Unlock()
torrent.Mu.Lock()
defer torrent.Mu.Unlock()
// Update total downloaded bytes
debridTorrent.SizeDownloaded += downloaded
debridTorrent.Speed = speed
// Calculate overall progress
if totalSize > 0 {
debridTorrent.Progress = float64(debridTorrent.SizeDownloaded) / float64(totalSize) * 100
}
q.UpdateTorrentMin(torrent, debridTorrent)
}
for _, link := range debridTorrent.DownloadLinks {
if link.DownloadLink == "" {
q.logger.Info().Msgf("No download link found for %s", link.Filename)
continue
}
wg.Add(1)
semaphore <- struct{}{}
go func(link debrid.DownloadLinks) {
defer wg.Done()
defer func() { <-semaphore }()
filename := link.Filename
err := downloaders.NormalGrab(
client,
link.DownloadLink,
filepath.Join(parent, filename),
progressCallback,
)
if err != nil {
q.logger.Error().Msgf("Failed to download %s: %v", filename, err)
} else {
q.logger.Info().Msgf("Downloaded %s", filename)
}
}(link)
}
wg.Wait()
q.logger.Info().Msgf("Downloaded all files for %s", debridTorrent.Name)
}
func (q *QBit) ProcessSymlink(torrent *Torrent) (string, error) {
debridTorrent := torrent.DebridTorrent
var wg sync.WaitGroup
files := debridTorrent.Files
ready := make(chan debrid.File, len(files))
if len(files) == 0 {
return "", fmt.Errorf("no video files found")
}
q.logger.Info().Msgf("Checking %d files...", len(files))
rCloneBase := debridTorrent.MountPath
torrentPath, err := q.getTorrentPath(rCloneBase, debridTorrent) // /MyTVShow/
if err != nil {
return "", fmt.Errorf("failed to get torrent path: %v", err)
}
// Fix for alldebrid
newTorrentPath := torrentPath
if newTorrentPath == "" {
// Alldebrid at times doesn't return the parent folder for single file torrents
newTorrentPath = common.RemoveExtension(debridTorrent.Name) // MyTVShow
}
torrentSymlinkPath := filepath.Join(q.DownloadFolder, debridTorrent.Arr.Name, newTorrentPath) // /mnt/symlinks/{category}/MyTVShow/
err = os.MkdirAll(torrentSymlinkPath, os.ModePerm)
if err != nil {
return "", fmt.Errorf("failed to create directory: %s: %v", torrentSymlinkPath, err)
}
torrentRclonePath := filepath.Join(rCloneBase, torrentPath) // leave it as is
q.logger.Debug().Msgf("Debrid torrent path: %s\nSymlink Path: %s", torrentRclonePath, torrentSymlinkPath)
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.Info().Msgf("File is ready: %s", f.Path)
q.createSymLink(torrentSymlinkPath, torrentRclonePath, f)
}
return torrentPath, nil
}
func (q *QBit) getTorrentPath(rclonePath string, debridTorrent *debrid.Torrent) (string, error) {
for {
q.logger.Debug().Msgf("Checking for torrent path: %s", rclonePath)
torrentPath, err := debridTorrent.GetMountFolder(rclonePath)
if err == nil {
q.logger.Debug().Msgf("Found torrent path: %s", torrentPath)
return torrentPath, err
}
time.Sleep(time.Second)
}
}
func (q *QBit) createSymLink(path string, torrentMountPath string, file debrid.File) {
// 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.Path) // debridFolder/MyTVShow/MyTVShow.S01E01.720p.mkv
err := os.Symlink(torrentFilePath, fullPath)
if err != nil {
q.logger.Info().Msgf("Failed to create symlink: %s: %v", fullPath, err)
}
}

376
pkg/qbit/http.go Normal file
View File

@@ -0,0 +1,376 @@
package qbit
import (
"context"
"encoding/base64"
"github.com/go-chi/chi/v5"
"github.com/sirrobot01/debrid-blackhole/internal/request"
"github.com/sirrobot01/debrid-blackhole/pkg/arr"
"github.com/sirrobot01/debrid-blackhole/pkg/service"
"net/http"
"path/filepath"
"strings"
)
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) CategoryContext(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
category := strings.Trim(r.URL.Query().Get("category"), "")
if category == "" {
// Get from form
_ = r.ParseForm()
category = r.Form.Get("category")
if category == "" {
// Get from multipart form
_ = r.ParseMultipartForm(32 << 20)
category = r.FormValue("category")
}
}
ctx := r.Context()
ctx = context.WithValue(r.Context(), "category", strings.TrimSpace(category))
next.ServeHTTP(w, r.WithContext(ctx))
})
}
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"))
category := r.Context().Value("category").(string)
a := &arr.Arr{
Name: category,
}
if err == nil {
a.Host = strings.TrimSpace(host)
a.Token = strings.TrimSpace(token)
}
svc := service.GetService()
svc.Arr.AddOrUpdate(a)
ctx := context.WithValue(r.Context(), "arr", a)
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")
var hashes []string
if _hashes != "" {
hashes = strings.Split(_hashes, "|")
}
if hashes == nil {
// Get hashes from form
_ = r.ParseForm()
hashes = r.Form["hashes"]
}
for i, hash := range hashes {
hashes[i] = strings.TrimSpace(hash)
}
ctx := context.WithValue(r.Context(), "hashes", hashes)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func (q *QBit) handleLogin(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("Ok."))
}
func (q *QBit) handleVersion(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("v4.3.2"))
}
func (q *QBit) handleWebAPIVersion(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("2.7"))
}
func (q *QBit) handlePreferences(w http.ResponseWriter, r *http.Request) {
preferences := NewAppPreferences()
preferences.WebUiUsername = q.Username
preferences.SavePath = q.DownloadFolder
preferences.TempPath = filepath.Join(q.DownloadFolder, "temp")
request.JSONResponse(w, preferences, http.StatusOK)
}
func (q *QBit) handleBuildInfo(w http.ResponseWriter, r *http.Request) {
res := BuildInfo{
Bitness: 64,
Boost: "1.75.0",
Libtorrent: "1.2.11.0",
Openssl: "1.1.1i",
Qt: "5.15.2",
Zlib: "1.2.11",
}
request.JSONResponse(w, res, http.StatusOK)
}
func (q *QBit) handleShutdown(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
func (q *QBit) handleTorrentsInfo(w http.ResponseWriter, r *http.Request) {
//log all url params
ctx := r.Context()
category := ctx.Value("category").(string)
filter := strings.Trim(r.URL.Query().Get("filter"), "")
hashes, _ := ctx.Value("hashes").([]string)
torrents := q.Storage.GetAll(category, filter, hashes)
request.JSONResponse(w, torrents, http.StatusOK)
}
func (q *QBit) handleTorrentsAdd(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// Parse form based on content type
contentType := r.Header.Get("Content-Type")
if strings.Contains(contentType, "multipart/form-data") {
if err := r.ParseMultipartForm(32 << 20); err != nil {
q.logger.Info().Msgf("Error parsing multipart form: %v", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
} else if strings.Contains(contentType, "application/x-www-form-urlencoded") {
if err := r.ParseForm(); err != nil {
q.logger.Info().Msgf("Error parsing form: %v", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
} else {
http.Error(w, "Invalid content type", http.StatusBadRequest)
return
}
isSymlink := strings.ToLower(r.FormValue("sequentialDownload")) != "true"
category := r.FormValue("category")
atleastOne := false
ctx = context.WithValue(ctx, "isSymlink", isSymlink)
// Handle magnet URLs
if urls := r.FormValue("urls"); urls != "" {
var urlList []string
for _, u := range strings.Split(urls, "\n") {
urlList = append(urlList, strings.TrimSpace(u))
}
for _, url := range urlList {
if err := q.AddMagnet(ctx, url, category); err != nil {
q.logger.Info().Msgf("Error adding magnet: %v", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
atleastOne = true
}
}
// Handle torrent files
if r.MultipartForm != nil && r.MultipartForm.File != nil {
if files := r.MultipartForm.File["torrents"]; len(files) > 0 {
for _, fileHeader := range files {
if err := q.AddTorrent(ctx, fileHeader, category); err != nil {
q.logger.Info().Msgf("Error adding torrent: %v", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
atleastOne = true
}
}
}
if !atleastOne {
http.Error(w, "No valid URLs or torrents provided", http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK)
}
func (q *QBit) handleTorrentsDelete(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
hashes, _ := ctx.Value("hashes").([]string)
if len(hashes) == 0 {
http.Error(w, "No hashes provided", http.StatusBadRequest)
return
}
for _, hash := range hashes {
q.Storage.Delete(hash)
}
w.WriteHeader(http.StatusOK)
}
func (q *QBit) handleTorrentsPause(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
hashes, _ := ctx.Value("hashes").([]string)
for _, hash := range hashes {
torrent := q.Storage.Get(hash)
if torrent == nil {
continue
}
go q.PauseTorrent(torrent)
}
w.WriteHeader(http.StatusOK)
}
func (q *QBit) handleTorrentsResume(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
hashes, _ := ctx.Value("hashes").([]string)
for _, hash := range hashes {
torrent := q.Storage.Get(hash)
if torrent == nil {
continue
}
go q.ResumeTorrent(torrent)
}
w.WriteHeader(http.StatusOK)
}
func (q *QBit) handleTorrentRecheck(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
hashes, _ := ctx.Value("hashes").([]string)
for _, hash := range hashes {
torrent := q.Storage.Get(hash)
if torrent == nil {
continue
}
go q.RefreshTorrent(torrent)
}
w.WriteHeader(http.StatusOK)
}
func (q *QBit) handleCategories(w http.ResponseWriter, r *http.Request) {
var categories = map[string]TorrentCategory{}
for _, cat := range q.Categories {
path := filepath.Join(q.DownloadFolder, cat)
categories[cat] = TorrentCategory{
Name: cat,
SavePath: path,
}
}
request.JSONResponse(w, categories, http.StatusOK)
}
func (q *QBit) handleCreateCategory(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
http.Error(w, "Failed to parse form data", http.StatusBadRequest)
return
}
name := r.Form.Get("category")
if name == "" {
http.Error(w, "No name provided", http.StatusBadRequest)
return
}
q.Categories = append(q.Categories, name)
request.JSONResponse(w, nil, http.StatusOK)
}
func (q *QBit) handleTorrentProperties(w http.ResponseWriter, r *http.Request) {
hash := r.URL.Query().Get("hash")
torrent := q.Storage.Get(hash)
properties := q.GetTorrentProperties(torrent)
request.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)
request.JSONResponse(w, files, http.StatusOK)
}
func (q *QBit) handleSetCategory(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
category := ctx.Value("category").(string)
hashes, _ := ctx.Value("hashes").([]string)
torrents := q.Storage.GetAll("", "", hashes)
for _, torrent := range torrents {
torrent.Category = category
q.Storage.AddOrUpdate(torrent)
}
request.JSONResponse(w, nil, http.StatusOK)
}
func (q *QBit) handleAddTorrentTags(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
http.Error(w, "Failed to parse form data", http.StatusBadRequest)
return
}
ctx := r.Context()
hashes, _ := ctx.Value("hashes").([]string)
tags := strings.Split(r.FormValue("tags"), ",")
for i, tag := range tags {
tags[i] = strings.TrimSpace(tag)
}
torrents := q.Storage.GetAll("", "", hashes)
for _, t := range torrents {
q.SetTorrentTags(t, tags)
}
request.JSONResponse(w, nil, http.StatusOK)
}
func (q *QBit) handleRemoveTorrentTags(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
http.Error(w, "Failed to parse form data", http.StatusBadRequest)
return
}
ctx := r.Context()
hashes, _ := ctx.Value("hashes").([]string)
tags := strings.Split(r.FormValue("tags"), ",")
for i, tag := range tags {
tags[i] = strings.TrimSpace(tag)
}
torrents := q.Storage.GetAll("", "", hashes)
for _, torrent := range torrents {
q.RemoveTorrentTags(torrent, tags)
}
request.JSONResponse(w, nil, http.StatusOK)
}
func (q *QBit) handleGetTags(w http.ResponseWriter, r *http.Request) {
request.JSONResponse(w, q.Tags, http.StatusOK)
}
func (q *QBit) handleCreateTags(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
http.Error(w, "Failed to parse form data", http.StatusBadRequest)
return
}
tags := strings.Split(r.FormValue("tags"), ",")
for i, tag := range tags {
tags[i] = strings.TrimSpace(tag)
}
q.AddTags(tags)
request.JSONResponse(w, nil, http.StatusOK)
}

90
pkg/qbit/import.go Normal file
View File

@@ -0,0 +1,90 @@
package qbit
import (
"fmt"
"github.com/sirrobot01/debrid-blackhole/internal/utils"
"github.com/sirrobot01/debrid-blackhole/pkg/service"
"time"
"github.com/google/uuid"
"github.com/sirrobot01/debrid-blackhole/pkg/arr"
"github.com/sirrobot01/debrid-blackhole/pkg/debrid"
)
type ImportRequest struct {
ID string `json:"id"`
Path string `json:"path"`
URI string `json:"uri"`
Arr *arr.Arr `json:"arr"`
IsSymlink bool `json:"isSymlink"`
SeriesId int `json:"series"`
Seasons []int `json:"seasons"`
Episodes []string `json:"episodes"`
Failed bool `json:"failed"`
FailedAt time.Time `json:"failedAt"`
Reason string `json:"reason"`
Completed bool `json:"completed"`
CompletedAt time.Time `json:"completedAt"`
Async bool `json:"async"`
}
type ManualImportResponseSchema struct {
Priority string `json:"priority"`
Status string `json:"status"`
Result string `json:"result"`
Queued time.Time `json:"queued"`
Trigger string `json:"trigger"`
SendUpdatesToClient bool `json:"sendUpdatesToClient"`
UpdateScheduledTask bool `json:"updateScheduledTask"`
Id int `json:"id"`
}
func NewImportRequest(uri string, arr *arr.Arr, isSymlink bool) *ImportRequest {
return &ImportRequest{
ID: uuid.NewString(),
URI: uri,
Arr: arr,
Failed: false,
Completed: false,
Async: false,
IsSymlink: isSymlink,
}
}
func (i *ImportRequest) Fail(reason string) {
i.Failed = true
i.FailedAt = time.Now()
i.Reason = reason
}
func (i *ImportRequest) Complete() {
i.Completed = true
i.CompletedAt = time.Now()
}
func (i *ImportRequest) Process(q *QBit) (err error) {
// Use this for now.
// This sends the torrent to the arr
svc := service.GetService()
magnet, err := utils.GetMagnetFromUrl(i.URI)
if err != nil {
return fmt.Errorf("error parsing magnet link: %w", err)
}
torrent := CreateTorrentFromMagnet(magnet, i.Arr.Name, "manual")
debridTorrent, err := debrid.ProcessTorrent(svc.Debrid, magnet, i.Arr, i.IsSymlink)
if err != nil || debridTorrent == nil {
if debridTorrent != nil {
dbClient := service.GetDebrid().GetByName(debridTorrent.Debrid)
go dbClient.DeleteTorrent(debridTorrent)
}
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, i.Arr, i.IsSymlink)
return nil
}

50
pkg/qbit/misc.go Normal file
View File

@@ -0,0 +1,50 @@
package qbit
import (
"github.com/google/uuid"
"github.com/sirrobot01/debrid-blackhole/internal/utils"
debrid "github.com/sirrobot01/debrid-blackhole/pkg/debrid/torrent"
"os"
"path/filepath"
"strings"
"sync"
"time"
)
func checkFileLoop(wg *sync.WaitGroup, dir string, file debrid.File, ready chan<- debrid.File) {
defer wg.Done()
ticker := time.NewTicker(1 * time.Second) // Check every second
defer ticker.Stop()
path := filepath.Join(dir, file.Path)
for {
select {
case <-ticker.C:
_, err := os.Stat(path)
if !os.IsNotExist(err) {
ready <- file
return
}
}
}
}
func CreateTorrentFromMagnet(magnet *utils.Magnet, category, source string) *Torrent {
torrent := &Torrent{
ID: uuid.NewString(),
Hash: strings.ToLower(magnet.InfoHash),
Name: magnet.Name,
Size: magnet.Size,
Category: category,
Source: source,
State: "downloading",
MagnetUri: magnet.Link,
Tracker: "udp://tracker.opentrackr.org:1337",
UpLimit: -1,
DlLimit: -1,
AutoTmm: false,
Ratio: 1,
RatioLimit: 1,
}
return torrent
}

38
pkg/qbit/qbit.go Normal file
View File

@@ -0,0 +1,38 @@
package qbit
import (
"cmp"
"github.com/rs/zerolog"
"github.com/sirrobot01/debrid-blackhole/internal/config"
"github.com/sirrobot01/debrid-blackhole/internal/logger"
"os"
)
type QBit struct {
Username string `json:"username"`
Password string `json:"password"`
Port string `json:"port"`
DownloadFolder string `json:"download_folder"`
Categories []string `json:"categories"`
Storage *TorrentStorage
debug bool
logger zerolog.Logger
Tags []string
RefreshInterval int
}
func New() *QBit {
cfg := config.GetConfig().QBitTorrent
port := cmp.Or(cfg.Port, os.Getenv("QBIT_PORT"), "8282")
refreshInterval := cmp.Or(cfg.RefreshInterval, 10)
return &QBit{
Username: cfg.Username,
Password: cfg.Password,
Port: port,
DownloadFolder: cfg.DownloadFolder,
Categories: cfg.Categories,
Storage: NewTorrentStorage(cmp.Or(os.Getenv("TORRENT_FILE"), "/data/qbit_torrents.json")),
logger: logger.NewLogger("qbit", cfg.LogLevel, os.Stdout),
RefreshInterval: refreshInterval,
}
}

47
pkg/qbit/routes.go Normal file
View File

@@ -0,0 +1,47 @@
package qbit
import (
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"net/http"
)
func (q *QBit) Routes() http.Handler {
r := chi.NewRouter()
if q.logger.GetLevel().String() == "debug" {
r.Use(middleware.Logger)
}
r.Use(q.CategoryContext)
r.Post("/auth/login", q.handleLogin)
r.Group(func(r chi.Router) {
r.Use(q.authContext)
r.Route("/torrents", func(r chi.Router) {
r.Use(HashesCtx)
r.Get("/info", q.handleTorrentsInfo)
r.Post("/add", q.handleTorrentsAdd)
r.Post("/delete", q.handleTorrentsDelete)
r.Get("/categories", q.handleCategories)
r.Post("/createCategory", q.handleCreateCategory)
r.Post("/setCategory", q.handleSetCategory)
r.Post("/addTags", q.handleAddTorrentTags)
r.Post("/removeTags", q.handleRemoveTorrentTags)
r.Post("/createTags", q.handleCreateTags)
r.Get("/tags", q.handleGetTags)
r.Get("/pause", q.handleTorrentsPause)
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) {
r.Get("/version", q.handleVersion)
r.Get("/webapiVersion", q.handleWebAPIVersion)
r.Get("/preferences", q.handlePreferences)
r.Get("/buildInfo", q.handleBuildInfo)
r.Get("/shutdown", q.handleShutdown)
})
})
return r
}

151
pkg/qbit/storage.go Normal file
View File

@@ -0,0 +1,151 @@
package qbit
import (
"encoding/json"
"os"
"sync"
)
type TorrentStorage struct {
torrents map[string]*Torrent
mu sync.RWMutex
order []string
filename string // Added to store the filename for persistence
}
func loadTorrentsFromJSON(filename string) (map[string]*Torrent, error) {
data, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
torrents := make(map[string]*Torrent)
if err := json.Unmarshal(data, &torrents); err != nil {
return nil, err
}
return torrents, nil
}
func NewTorrentStorage(filename string) *TorrentStorage {
// Open the JSON file and read the data
torrents, err := loadTorrentsFromJSON(filename)
if err != nil {
torrents = make(map[string]*Torrent)
}
order := make([]string, 0, len(torrents))
for id := range torrents {
order = append(order, id)
}
// Create a new TorrentStorage
return &TorrentStorage{
torrents: torrents,
order: order,
filename: filename,
}
}
func (ts *TorrentStorage) Add(torrent *Torrent) {
ts.mu.Lock()
defer ts.mu.Unlock()
ts.torrents[torrent.Hash] = torrent
ts.order = append(ts.order, torrent.Hash)
_ = ts.saveToFile()
}
func (ts *TorrentStorage) AddOrUpdate(torrent *Torrent) {
ts.mu.Lock()
defer ts.mu.Unlock()
if _, exists := ts.torrents[torrent.Hash]; !exists {
ts.order = append(ts.order, torrent.Hash)
}
ts.torrents[torrent.Hash] = torrent
_ = ts.saveToFile()
}
func (ts *TorrentStorage) GetByID(id string) *Torrent {
ts.mu.RLock()
defer ts.mu.RUnlock()
for _, torrent := range ts.torrents {
if torrent.ID == id {
return torrent
}
}
return nil
}
func (ts *TorrentStorage) Get(hash string) *Torrent {
ts.mu.RLock()
defer ts.mu.RUnlock()
return ts.torrents[hash]
}
func (ts *TorrentStorage) GetAll(category string, filter string, hashes []string) []*Torrent {
ts.mu.RLock()
defer ts.mu.RUnlock()
torrents := make([]*Torrent, 0)
for _, id := range ts.order {
torrent := ts.torrents[id]
if category != "" && torrent.Category != category {
continue
}
if filter != "" && torrent.State != filter {
continue
}
torrents = append(torrents, torrent)
}
if len(hashes) > 0 {
filtered := make([]*Torrent, 0, len(torrents))
for _, hash := range hashes {
if torrent := ts.torrents[hash]; torrent != nil {
filtered = append(filtered, torrent)
}
}
torrents = filtered
}
return torrents
}
func (ts *TorrentStorage) Update(torrent *Torrent) {
ts.mu.Lock()
defer ts.mu.Unlock()
ts.torrents[torrent.Hash] = torrent
_ = ts.saveToFile()
}
func (ts *TorrentStorage) Delete(hash string) {
ts.mu.Lock()
defer ts.mu.Unlock()
torrent, exists := ts.torrents[hash]
if !exists {
return
}
delete(ts.torrents, hash)
for i, id := range ts.order {
if id == hash {
ts.order = append(ts.order[:i], ts.order[i+1:]...)
break
}
}
// Delete the torrent folder
if torrent.ContentPath != "" {
err := os.RemoveAll(torrent.ContentPath)
if err != nil {
return
}
}
_ = ts.saveToFile()
}
func (ts *TorrentStorage) Save() error {
ts.mu.RLock()
defer ts.mu.RUnlock()
return ts.saveToFile()
}
// saveToFile is a helper function to write the current state to the JSON file
func (ts *TorrentStorage) saveToFile() error {
data, err := json.MarshalIndent(ts.torrents, "", " ")
if err != nil {
return err
}
return os.WriteFile(ts.filename, data, 0644)
}

304
pkg/qbit/torrent.go Normal file
View File

@@ -0,0 +1,304 @@
package qbit
import (
"cmp"
"context"
"fmt"
"github.com/sirrobot01/debrid-blackhole/internal/utils"
"github.com/sirrobot01/debrid-blackhole/pkg/arr"
db "github.com/sirrobot01/debrid-blackhole/pkg/debrid"
debrid "github.com/sirrobot01/debrid-blackhole/pkg/debrid/torrent"
"github.com/sirrobot01/debrid-blackhole/pkg/service"
"io"
"mime/multipart"
"os"
"path/filepath"
"slices"
"strings"
"time"
)
// All torrent related helpers goes here
func (q *QBit) AddMagnet(ctx context.Context, url, category string) error {
magnet, err := utils.GetMagnetFromUrl(url)
if err != nil {
return fmt.Errorf("error parsing magnet link: %w", err)
}
err = q.Process(ctx, magnet, category)
if err != nil {
return fmt.Errorf("failed to process torrent: %w", 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 := utils.GetMagnetFromFile(reader, fileHeader.Filename)
if err != nil {
return fmt.Errorf("error reading file: %s \n %w", fileHeader.Filename, err)
}
err = q.Process(ctx, magnet, category)
if err != nil {
return fmt.Errorf("failed to process torrent: %w", err)
}
return nil
}
func (q *QBit) Process(ctx context.Context, magnet *utils.Magnet, category string) error {
svc := service.GetService()
torrent := CreateTorrentFromMagnet(magnet, category, "auto")
a, ok := ctx.Value("arr").(*arr.Arr)
if !ok {
return fmt.Errorf("arr not found in context")
}
isSymlink := ctx.Value("isSymlink").(bool)
debridTorrent, err := db.ProcessTorrent(svc.Debrid, magnet, a, isSymlink)
if err != nil || debridTorrent == nil {
if debridTorrent != nil {
dbClient := service.GetDebrid().GetByName(debridTorrent.Debrid)
go dbClient.DeleteTorrent(debridTorrent)
}
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, a, isSymlink) // We can send async for file processing not to delay the response
return nil
}
func (q *QBit) ProcessFiles(torrent *Torrent, debridTorrent *debrid.Torrent, arr *arr.Arr, isSymlink bool) {
debridClient := service.GetDebrid().GetByName(debridTorrent.Debrid)
for debridTorrent.Status != "downloaded" {
progress := debridTorrent.Progress
q.logger.Debug().Msgf("%s -> (%s) Download Progress: %.2f%%", debridTorrent.Debrid, debridTorrent.Name, progress)
time.Sleep(10 * time.Second)
dbT, err := debridClient.CheckStatus(debridTorrent, isSymlink)
if err != nil {
q.logger.Error().Msgf("Error checking status: %v", err)
go debridClient.DeleteTorrent(debridTorrent)
q.MarkAsFailed(torrent)
_ = arr.Refresh()
return
}
debridTorrent = dbT
torrent = q.UpdateTorrentMin(torrent, debridTorrent)
}
var (
torrentPath string
err error
)
debridTorrent.Arr = arr
if isSymlink {
torrentPath, err = q.ProcessSymlink(torrent)
} else {
torrentPath, err = q.ProcessManualFile(torrent)
}
if err != nil {
q.MarkAsFailed(torrent)
go debridClient.DeleteTorrent(debridTorrent)
q.logger.Info().Msgf("Error: %v", err)
return
}
torrent.TorrentPath = filepath.Base(torrentPath)
q.UpdateTorrent(torrent, debridTorrent)
_ = arr.Refresh()
}
func (q *QBit) MarkAsFailed(t *Torrent) *Torrent {
t.State = "error"
q.Storage.AddOrUpdate(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 := debridTorrent.Bytes
progress := cmp.Or(debridTorrent.Progress, 100)
progress = progress / 100.0
sizeCompleted := int64(float64(totalSize) * progress)
var speed int64
if debridTorrent.Speed != 0 {
speed = debridTorrent.Speed
}
var eta int
if speed != 0 {
eta = int((totalSize - sizeCompleted) / speed)
}
t.ID = debridTorrent.Id
t.Name = debridTorrent.Name
t.AddedOn = addedOn.Unix()
t.DebridTorrent = debridTorrent
t.Debrid = debridTorrent.Debrid
t.Size = totalSize
t.Completed = sizeCompleted
t.Downloaded = sizeCompleted
t.DownloadedSession = sizeCompleted
t.Uploaded = sizeCompleted
t.UploadedSession = sizeCompleted
t.AmountLeft = totalSize - sizeCompleted
t.Progress = 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 {
_db := service.GetDebrid().GetByName(debridTorrent.Debrid)
rcLoneMount := _db.GetMountPath()
if debridTorrent == nil && t.ID != "" {
debridTorrent, _ = _db.GetTorrent(t.ID)
}
if debridTorrent == nil {
q.logger.Info().Msgf("Torrent with ID %s not found in %s", t.ID, _db.GetName())
return t
}
if debridTorrent.Status != "downloaded" {
debridTorrent, _ = _db.GetTorrent(t.ID)
}
if t.TorrentPath == "" {
tPath, _ := debridTorrent.GetMountFolder(rcLoneMount)
t.TorrentPath = filepath.Base(tPath)
}
savePath := filepath.Join(q.DownloadFolder, t.Category) + string(os.PathSeparator)
torrentPath := filepath.Join(savePath, t.TorrentPath) + string(os.PathSeparator)
t = q.UpdateTorrentMin(t, debridTorrent)
t.ContentPath = torrentPath
if t.IsReady() {
t.State = "pausedUP"
q.Storage.Update(t)
return t
}
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if t.IsReady() {
t.State = "pausedUP"
q.Storage.Update(t)
return t
}
updatedT := q.UpdateTorrent(t, debridTorrent)
t = updatedT
case <-time.After(10 * time.Minute): // Add a timeout
return t
}
}
}
func (q *QBit) ResumeTorrent(t *Torrent) bool {
return true
}
func (q *QBit) PauseTorrent(t *Torrent) bool {
return true
}
func (q *QBit) RefreshTorrent(t *Torrent) bool {
return true
}
func (q *QBit) GetTorrentProperties(t *Torrent) *TorrentProperties {
return &TorrentProperties{
AdditionDate: t.AddedOn,
Comment: "Debrid Blackhole <https://github.com/sirrobot01/debrid-blackhole>",
CreatedBy: "Debrid Blackhole <https://github.com/sirrobot01/debrid-blackhole>",
CreationDate: t.AddedOn,
DlLimit: -1,
UpLimit: -1,
DlSpeed: t.Dlspeed,
UpSpeed: t.Upspeed,
TotalSize: t.Size,
TotalUploaded: t.Uploaded,
TotalDownloaded: t.Downloaded,
TotalUploadedSession: t.UploadedSession,
TotalDownloadedSession: t.DownloadedSession,
LastSeen: time.Now().Unix(),
NbConnectionsLimit: 100,
Peers: 0,
PeersTotal: 2,
SeedingTime: 1,
Seeds: 100,
ShareRatio: 100,
}
}
func (q *QBit) GetTorrentFiles(t *Torrent) []*TorrentFile {
files := make([]*TorrentFile, 0)
if t.DebridTorrent == nil {
return files
}
for _, file := range t.DebridTorrent.Files {
files = append(files, &TorrentFile{
Name: file.Path,
Size: file.Size,
})
}
return files
}
func (q *QBit) SetTorrentTags(t *Torrent, tags []string) bool {
torrentTags := strings.Split(t.Tags, ",")
for _, tag := range tags {
if tag == "" {
continue
}
if !slices.Contains(torrentTags, tag) {
torrentTags = append(torrentTags, tag)
}
if !slices.Contains(q.Tags, tag) {
q.Tags = append(q.Tags, tag)
}
}
t.Tags = strings.Join(torrentTags, ",")
q.Storage.Update(t)
return true
}
func (q *QBit) RemoveTorrentTags(t *Torrent, tags []string) bool {
torrentTags := strings.Split(t.Tags, ",")
newTorrentTags := utils.RemoveItem(torrentTags, tags...)
q.Tags = utils.RemoveItem(q.Tags, tags...)
t.Tags = strings.Join(newTorrentTags, ",")
q.Storage.Update(t)
return true
}
func (q *QBit) AddTags(tags []string) bool {
for _, tag := range tags {
if tag == "" {
continue
}
if !slices.Contains(q.Tags, tag) {
q.Tags = append(q.Tags, tag)
}
}
return true
}
func (q *QBit) RemoveTags(tags []string) bool {
q.Tags = utils.RemoveItem(q.Tags, tags...)
return true
}

430
pkg/qbit/types.go Normal file
View File

@@ -0,0 +1,430 @@
package qbit
import (
"github.com/sirrobot01/debrid-blackhole/pkg/debrid/torrent"
"sync"
)
type BuildInfo struct {
Libtorrent string `json:"libtorrent"`
Bitness int `json:"bitness"`
Boost string `json:"boost"`
Openssl string `json:"openssl"`
Qt string `json:"qt"`
Zlib string `json:"zlib"`
}
type AppPreferences struct {
AddTrackers string `json:"add_trackers"`
AddTrackersEnabled bool `json:"add_trackers_enabled"`
AltDlLimit int `json:"alt_dl_limit"`
AltUpLimit int `json:"alt_up_limit"`
AlternativeWebuiEnabled bool `json:"alternative_webui_enabled"`
AlternativeWebuiPath string `json:"alternative_webui_path"`
AnnounceIp string `json:"announce_ip"`
AnnounceToAllTiers bool `json:"announce_to_all_tiers"`
AnnounceToAllTrackers bool `json:"announce_to_all_trackers"`
AnonymousMode bool `json:"anonymous_mode"`
AsyncIoThreads int `json:"async_io_threads"`
AutoDeleteMode int `json:"auto_delete_mode"`
AutoTmmEnabled bool `json:"auto_tmm_enabled"`
AutorunEnabled bool `json:"autorun_enabled"`
AutorunProgram string `json:"autorun_program"`
BannedIPs string `json:"banned_IPs"`
BittorrentProtocol int `json:"bittorrent_protocol"`
BypassAuthSubnetWhitelist string `json:"bypass_auth_subnet_whitelist"`
BypassAuthSubnetWhitelistEnabled bool `json:"bypass_auth_subnet_whitelist_enabled"`
BypassLocalAuth bool `json:"bypass_local_auth"`
CategoryChangedTmmEnabled bool `json:"category_changed_tmm_enabled"`
CheckingMemoryUse int `json:"checking_memory_use"`
CreateSubfolderEnabled bool `json:"create_subfolder_enabled"`
CurrentInterfaceAddress string `json:"current_interface_address"`
CurrentNetworkInterface string `json:"current_network_interface"`
Dht bool `json:"dht"`
DiskCache int `json:"disk_cache"`
DiskCacheTtl int `json:"disk_cache_ttl"`
DlLimit int `json:"dl_limit"`
DontCountSlowTorrents bool `json:"dont_count_slow_torrents"`
DyndnsDomain string `json:"dyndns_domain"`
DyndnsEnabled bool `json:"dyndns_enabled"`
DyndnsPassword string `json:"dyndns_password"`
DyndnsService int `json:"dyndns_service"`
DyndnsUsername string `json:"dyndns_username"`
EmbeddedTrackerPort int `json:"embedded_tracker_port"`
EnableCoalesceReadWrite bool `json:"enable_coalesce_read_write"`
EnableEmbeddedTracker bool `json:"enable_embedded_tracker"`
EnableMultiConnectionsFromSameIp bool `json:"enable_multi_connections_from_same_ip"`
EnableOsCache bool `json:"enable_os_cache"`
EnablePieceExtentAffinity bool `json:"enable_piece_extent_affinity"`
EnableSuperSeeding bool `json:"enable_super_seeding"`
EnableUploadSuggestions bool `json:"enable_upload_suggestions"`
Encryption int `json:"encryption"`
ExportDir string `json:"export_dir"`
ExportDirFin string `json:"export_dir_fin"`
FilePoolSize int `json:"file_pool_size"`
IncompleteFilesExt bool `json:"incomplete_files_ext"`
IpFilterEnabled bool `json:"ip_filter_enabled"`
IpFilterPath string `json:"ip_filter_path"`
IpFilterTrackers bool `json:"ip_filter_trackers"`
LimitLanPeers bool `json:"limit_lan_peers"`
LimitTcpOverhead bool `json:"limit_tcp_overhead"`
LimitUtpRate bool `json:"limit_utp_rate"`
ListenPort int `json:"listen_port"`
Locale string `json:"locale"`
Lsd bool `json:"lsd"`
MailNotificationAuthEnabled bool `json:"mail_notification_auth_enabled"`
MailNotificationEmail string `json:"mail_notification_email"`
MailNotificationEnabled bool `json:"mail_notification_enabled"`
MailNotificationPassword string `json:"mail_notification_password"`
MailNotificationSender string `json:"mail_notification_sender"`
MailNotificationSmtp string `json:"mail_notification_smtp"`
MailNotificationSslEnabled bool `json:"mail_notification_ssl_enabled"`
MailNotificationUsername string `json:"mail_notification_username"`
MaxActiveDownloads int `json:"max_active_downloads"`
MaxActiveTorrents int `json:"max_active_torrents"`
MaxActiveUploads int `json:"max_active_uploads"`
MaxConnec int `json:"max_connec"`
MaxConnecPerTorrent int `json:"max_connec_per_torrent"`
MaxRatio int `json:"max_ratio"`
MaxRatioAct int `json:"max_ratio_act"`
MaxRatioEnabled bool `json:"max_ratio_enabled"`
MaxSeedingTime int `json:"max_seeding_time"`
MaxSeedingTimeEnabled bool `json:"max_seeding_time_enabled"`
MaxUploads int `json:"max_uploads"`
MaxUploadsPerTorrent int `json:"max_uploads_per_torrent"`
OutgoingPortsMax int `json:"outgoing_ports_max"`
OutgoingPortsMin int `json:"outgoing_ports_min"`
Pex bool `json:"pex"`
PreallocateAll bool `json:"preallocate_all"`
ProxyAuthEnabled bool `json:"proxy_auth_enabled"`
ProxyIp string `json:"proxy_ip"`
ProxyPassword string `json:"proxy_password"`
ProxyPeerConnections bool `json:"proxy_peer_connections"`
ProxyPort int `json:"proxy_port"`
ProxyTorrentsOnly bool `json:"proxy_torrents_only"`
ProxyType int `json:"proxy_type"`
ProxyUsername string `json:"proxy_username"`
QueueingEnabled bool `json:"queueing_enabled"`
RandomPort bool `json:"random_port"`
RecheckCompletedTorrents bool `json:"recheck_completed_torrents"`
ResolvePeerCountries bool `json:"resolve_peer_countries"`
RssAutoDownloadingEnabled bool `json:"rss_auto_downloading_enabled"`
RssMaxArticlesPerFeed int `json:"rss_max_articles_per_feed"`
RssProcessingEnabled bool `json:"rss_processing_enabled"`
RssRefreshInterval int `json:"rss_refresh_interval"`
SavePath string `json:"save_path"`
SavePathChangedTmmEnabled bool `json:"save_path_changed_tmm_enabled"`
SaveResumeDataInterval int `json:"save_resume_data_interval"`
ScanDirs ScanDirs `json:"scan_dirs"`
ScheduleFromHour int `json:"schedule_from_hour"`
ScheduleFromMin int `json:"schedule_from_min"`
ScheduleToHour int `json:"schedule_to_hour"`
ScheduleToMin int `json:"schedule_to_min"`
SchedulerDays int `json:"scheduler_days"`
SchedulerEnabled bool `json:"scheduler_enabled"`
SendBufferLowWatermark int `json:"send_buffer_low_watermark"`
SendBufferWatermark int `json:"send_buffer_watermark"`
SendBufferWatermarkFactor int `json:"send_buffer_watermark_factor"`
SlowTorrentDlRateThreshold int `json:"slow_torrent_dl_rate_threshold"`
SlowTorrentInactiveTimer int `json:"slow_torrent_inactive_timer"`
SlowTorrentUlRateThreshold int `json:"slow_torrent_ul_rate_threshold"`
SocketBacklogSize int `json:"socket_backlog_size"`
StartPausedEnabled bool `json:"start_paused_enabled"`
StopTrackerTimeout int `json:"stop_tracker_timeout"`
TempPath string `json:"temp_path"`
TempPathEnabled bool `json:"temp_path_enabled"`
TorrentChangedTmmEnabled bool `json:"torrent_changed_tmm_enabled"`
UpLimit int `json:"up_limit"`
UploadChokingAlgorithm int `json:"upload_choking_algorithm"`
UploadSlotsBehavior int `json:"upload_slots_behavior"`
Upnp bool `json:"upnp"`
UpnpLeaseDuration int `json:"upnp_lease_duration"`
UseHttps bool `json:"use_https"`
UtpTcpMixedMode int `json:"utp_tcp_mixed_mode"`
WebUiAddress string `json:"web_ui_address"`
WebUiBanDuration int `json:"web_ui_ban_duration"`
WebUiClickjackingProtectionEnabled bool `json:"web_ui_clickjacking_protection_enabled"`
WebUiCsrfProtectionEnabled bool `json:"web_ui_csrf_protection_enabled"`
WebUiDomainList string `json:"web_ui_domain_list"`
WebUiHostHeaderValidationEnabled bool `json:"web_ui_host_header_validation_enabled"`
WebUiHttpsCertPath string `json:"web_ui_https_cert_path"`
WebUiHttpsKeyPath string `json:"web_ui_https_key_path"`
WebUiMaxAuthFailCount int `json:"web_ui_max_auth_fail_count"`
WebUiPort int `json:"web_ui_port"`
WebUiSecureCookieEnabled bool `json:"web_ui_secure_cookie_enabled"`
WebUiSessionTimeout int `json:"web_ui_session_timeout"`
WebUiUpnp bool `json:"web_ui_upnp"`
WebUiUsername string `json:"web_ui_username"`
WebUiPassword string `json:"web_ui_password"`
SSLKey string `json:"ssl_key"`
SSLCert string `json:"ssl_cert"`
RSSDownloadRepack string `json:"rss_download_repack_proper_episodes"`
RSSSmartEpisodeFilters string `json:"rss_smart_episode_filters"`
WebUiUseCustomHttpHeaders bool `json:"web_ui_use_custom_http_headers"`
WebUiUseCustomHttpHeadersEnabled bool `json:"web_ui_use_custom_http_headers_enabled"`
}
type ScanDirs struct{}
type TorrentCategory struct {
Name string `json:"name"`
SavePath string `json:"savePath"`
}
type Torrent struct {
ID string `json:"-"`
DebridTorrent *torrent.Torrent `json:"-"`
Debrid string `json:"debrid"`
TorrentPath string `json:"-"`
AddedOn int64 `json:"added_on,omitempty"`
AmountLeft int64 `json:"amount_left"`
AutoTmm bool `json:"auto_tmm"`
Availability float64 `json:"availability,omitempty"`
Category string `json:"category,omitempty"`
Completed int64 `json:"completed"`
CompletionOn int `json:"completion_on,omitempty"`
ContentPath string `json:"content_path"`
DlLimit int `json:"dl_limit"`
Dlspeed int64 `json:"dlspeed"`
Downloaded int64 `json:"downloaded"`
DownloadedSession int64 `json:"downloaded_session"`
Eta int `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"`
MaxRatio int `json:"max_ratio,omitempty"`
MaxSeedingTime int `json:"max_seeding_time,omitempty"`
Name string `json:"name,omitempty"`
NumComplete int `json:"num_complete,omitempty"`
NumIncomplete int `json:"num_incomplete,omitempty"`
NumLeechs int `json:"num_leechs,omitempty"`
NumSeeds int `json:"num_seeds,omitempty"`
Priority int `json:"priority,omitempty"`
Progress float64 `json:"progress"`
Ratio int `json:"ratio,omitempty"`
RatioLimit int `json:"ratio_limit,omitempty"`
SavePath string `json:"save_path"`
SeedingTimeLimit int `json:"seeding_time_limit,omitempty"`
SeenComplete int64 `json:"seen_complete,omitempty"`
SeqDl bool `json:"seq_dl"`
Size int64 `json:"size,omitempty"`
State string `json:"state,omitempty"`
SuperSeeding bool `json:"super_seeding"`
Tags string `json:"tags,omitempty"`
TimeActive int `json:"time_active,omitempty"`
TotalSize int64 `json:"total_size,omitempty"`
Tracker string `json:"tracker,omitempty"`
UpLimit int64 `json:"up_limit,omitempty"`
Uploaded int64 `json:"uploaded,omitempty"`
UploadedSession int64 `json:"uploaded_session,omitempty"`
Upspeed int64 `json:"upspeed,omitempty"`
Source string `json:"source,omitempty"`
Mu sync.Mutex `json:"-"`
}
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"`
CompletionDate int64 `json:"completion_date,omitempty"`
CreatedBy string `json:"created_by,omitempty"`
CreationDate int64 `json:"creation_date,omitempty"`
DlLimit int `json:"dl_limit,omitempty"`
DlSpeed int64 `json:"dl_speed,omitempty"`
DlSpeedAvg int `json:"dl_speed_avg,omitempty"`
Eta int `json:"eta,omitempty"`
LastSeen int64 `json:"last_seen,omitempty"`
NbConnections int `json:"nb_connections,omitempty"`
NbConnectionsLimit int `json:"nb_connections_limit,omitempty"`
Peers int `json:"peers,omitempty"`
PeersTotal int `json:"peers_total,omitempty"`
PieceSize int64 `json:"piece_size,omitempty"`
PiecesHave int64 `json:"pieces_have,omitempty"`
PiecesNum int64 `json:"pieces_num,omitempty"`
Reannounce int `json:"reannounce,omitempty"`
SavePath string `json:"save_path,omitempty"`
SeedingTime int `json:"seeding_time,omitempty"`
Seeds int `json:"seeds,omitempty"`
SeedsTotal int `json:"seeds_total,omitempty"`
ShareRatio int `json:"share_ratio,omitempty"`
TimeElapsed int64 `json:"time_elapsed,omitempty"`
TotalDownloaded int64 `json:"total_downloaded,omitempty"`
TotalDownloadedSession int64 `json:"total_downloaded_session,omitempty"`
TotalSize int64 `json:"total_size,omitempty"`
TotalUploaded int64 `json:"total_uploaded,omitempty"`
TotalUploadedSession int64 `json:"total_uploaded_session,omitempty"`
TotalWasted int64 `json:"total_wasted,omitempty"`
UpLimit int `json:"up_limit,omitempty"`
UpSpeed int64 `json:"up_speed,omitempty"`
UpSpeedAvg int `json:"up_speed_avg,omitempty"`
}
type TorrentFile struct {
Index int `json:"index,omitempty"`
Name string `json:"name,omitempty"`
Size int64 `json:"size,omitempty"`
Progress int `json:"progress,omitempty"`
Priority int `json:"priority,omitempty"`
IsSeed bool `json:"is_seed,omitempty"`
PieceRange []int `json:"piece_range,omitempty"`
Availability float64 `json:"availability,omitempty"`
}
func NewAppPreferences() *AppPreferences {
preferences := &AppPreferences{
AddTrackers: "",
AddTrackersEnabled: false,
AltDlLimit: 10240,
AltUpLimit: 10240,
AlternativeWebuiEnabled: false,
AlternativeWebuiPath: "",
AnnounceIp: "",
AnnounceToAllTiers: true,
AnnounceToAllTrackers: false,
AnonymousMode: false,
AsyncIoThreads: 4,
AutoDeleteMode: 0,
AutoTmmEnabled: false,
AutorunEnabled: false,
AutorunProgram: "",
BannedIPs: "",
BittorrentProtocol: 0,
BypassAuthSubnetWhitelist: "",
BypassAuthSubnetWhitelistEnabled: false,
BypassLocalAuth: false,
CategoryChangedTmmEnabled: false,
CheckingMemoryUse: 32,
CreateSubfolderEnabled: true,
CurrentInterfaceAddress: "",
CurrentNetworkInterface: "",
Dht: true,
DiskCache: -1,
DiskCacheTtl: 60,
DlLimit: 0,
DontCountSlowTorrents: false,
DyndnsDomain: "changeme.dyndns.org",
DyndnsEnabled: false,
DyndnsPassword: "",
DyndnsService: 0,
DyndnsUsername: "",
EmbeddedTrackerPort: 9000,
EnableCoalesceReadWrite: true,
EnableEmbeddedTracker: false,
EnableMultiConnectionsFromSameIp: false,
EnableOsCache: true,
EnablePieceExtentAffinity: false,
EnableSuperSeeding: false,
EnableUploadSuggestions: false,
Encryption: 0,
ExportDir: "",
ExportDirFin: "",
FilePoolSize: 40,
IncompleteFilesExt: false,
IpFilterEnabled: false,
IpFilterPath: "",
IpFilterTrackers: false,
LimitLanPeers: true,
LimitTcpOverhead: false,
LimitUtpRate: true,
ListenPort: 31193,
Locale: "en",
Lsd: true,
MailNotificationAuthEnabled: false,
MailNotificationEmail: "",
MailNotificationEnabled: false,
MailNotificationPassword: "",
MailNotificationSender: "qBittorrentNotification@example.com",
MailNotificationSmtp: "smtp.changeme.com",
MailNotificationSslEnabled: false,
MailNotificationUsername: "",
MaxActiveDownloads: 3,
MaxActiveTorrents: 5,
MaxActiveUploads: 3,
MaxConnec: 500,
MaxConnecPerTorrent: 100,
MaxRatio: -1,
MaxRatioAct: 0,
MaxRatioEnabled: false,
MaxSeedingTime: -1,
MaxSeedingTimeEnabled: false,
MaxUploads: -1,
MaxUploadsPerTorrent: -1,
OutgoingPortsMax: 0,
OutgoingPortsMin: 0,
Pex: true,
PreallocateAll: false,
ProxyAuthEnabled: false,
ProxyIp: "0.0.0.0",
ProxyPassword: "",
ProxyPeerConnections: false,
ProxyPort: 8080,
ProxyTorrentsOnly: false,
ProxyType: 0,
ProxyUsername: "",
QueueingEnabled: false,
RandomPort: false,
RecheckCompletedTorrents: false,
ResolvePeerCountries: true,
RssAutoDownloadingEnabled: false,
RssMaxArticlesPerFeed: 50,
RssProcessingEnabled: false,
RssRefreshInterval: 30,
SavePathChangedTmmEnabled: false,
SaveResumeDataInterval: 60,
ScanDirs: ScanDirs{},
ScheduleFromHour: 8,
ScheduleFromMin: 0,
ScheduleToHour: 20,
ScheduleToMin: 0,
SchedulerDays: 0,
SchedulerEnabled: false,
SendBufferLowWatermark: 10,
SendBufferWatermark: 500,
SendBufferWatermarkFactor: 50,
SlowTorrentDlRateThreshold: 2,
SlowTorrentInactiveTimer: 60,
SlowTorrentUlRateThreshold: 2,
SocketBacklogSize: 30,
StartPausedEnabled: false,
StopTrackerTimeout: 1,
TempPathEnabled: false,
TorrentChangedTmmEnabled: true,
UpLimit: 0,
UploadChokingAlgorithm: 1,
UploadSlotsBehavior: 0,
Upnp: true,
UpnpLeaseDuration: 0,
UseHttps: false,
UtpTcpMixedMode: 0,
WebUiAddress: "*",
WebUiBanDuration: 3600,
WebUiClickjackingProtectionEnabled: true,
WebUiCsrfProtectionEnabled: true,
WebUiDomainList: "*",
WebUiHostHeaderValidationEnabled: true,
WebUiHttpsCertPath: "",
WebUiHttpsKeyPath: "",
WebUiMaxAuthFailCount: 5,
WebUiPort: 8080,
WebUiSecureCookieEnabled: true,
WebUiSessionTimeout: 3600,
WebUiUpnp: false,
// Fields in the struct but not in the JSON (set to zero values):
WebUiPassword: "",
SSLKey: "",
SSLCert: "",
RSSDownloadRepack: "",
RSSSmartEpisodeFilters: "",
WebUiUseCustomHttpHeaders: false,
WebUiUseCustomHttpHeadersEnabled: false,
}
return preferences
}

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

@@ -0,0 +1,39 @@
package qbit
import (
"context"
"github.com/sirrobot01/debrid-blackhole/pkg/service"
"time"
)
func (q *QBit) StartWorker(ctx context.Context) {
q.logger.Info().Msg("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.Info().Msg("Qbit Refresh Worker stopped")
return
case <-refreshTicker.C:
torrents := q.Storage.GetAll("", "", nil)
if len(torrents) > 0 {
q.RefreshArrs()
}
}
}
}
func (q *QBit) RefreshArrs() {
arrs := service.GetService().Arr
for _, arr := range arrs.GetAll() {
err := arr.Refresh()
if err != nil {
return
}
}
}