5 Commits

Author SHA1 Message Date
Mukhtar Akere
dc6ee2f020 fix umask for windows
Some checks failed
GoReleaser / goreleaser (push) Has been cancelled
Release Docker Build / docker (push) Has been cancelled
2025-03-23 07:14:46 +01:00
Mukhtar Akere
fce0fc0215 update changelog 2025-03-22 06:11:15 +01:00
Mukhtar Akere
26f6f384a3 Fix arr download_uncached settings 2025-03-16 05:43:25 +01:00
Mukhtar Akere
b91aa1db38 Add a precacher to significantly improve importing to arrs/plex 2025-03-15 23:12:37 +01:00
Mukhtar Akere
e2ff3b26de Add Umask support 2025-03-15 21:30:19 +01:00
18 changed files with 138 additions and 21 deletions

View File

@@ -5,7 +5,7 @@ tmp_dir = "tmp"
[build]
args_bin = ["--config", "data/"]
bin = "./tmp/main"
cmd = "bash -c 'go build -ldflags \"-X github.com/sirrobot01/debrid-blackhole/pkg/version.Version=0.0.0 -X github.com/sirrobot01/debrid-blackhole/pkg/version.Channel=nightly\" -o ./tmp/main .'"
cmd = "bash -c 'go build -ldflags \"-X github.com/sirrobot01/debrid-blackhole/pkg/version.Version=0.0.0 -X github.com/sirrobot01/debrid-blackhole/pkg/version.Channel=dev\" -o ./tmp/main .'"
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata", "data"]
exclude_file = []

View File

@@ -163,4 +163,10 @@
- Add an option to skip the repair worker for a specific arr
- Arr specific uncached downloading option
- Option to download uncached torrents from UI
- Remove QbitTorrent Log level(Use the global log level)
- Remove QbitTorrent Log level(Use the global log level)
#### 0.5.1
- Faster import by prefetching newly downloaded torrents
- Fix UMASK issue due to the docker container
- Arr-Selective uncached downloading

View File

@@ -12,11 +12,22 @@ import (
"github.com/sirrobot01/debrid-blackhole/pkg/version"
"github.com/sirrobot01/debrid-blackhole/pkg/web"
"github.com/sirrobot01/debrid-blackhole/pkg/worker"
"os"
"runtime/debug"
"strconv"
"sync"
)
func Start(ctx context.Context) error {
if umaskStr := os.Getenv("UMASK"); umaskStr != "" {
umask, err := strconv.ParseInt(umaskStr, 8, 32)
if err != nil {
return fmt.Errorf("invalid UMASK value: %s", umaskStr)
}
SetUmask(int(umask))
}
cfg := config.GetConfig()
var wg sync.WaitGroup
errChan := make(chan error)

View File

@@ -0,0 +1,9 @@
//go:build !windows
package decypharr
import "syscall"
func SetUmask(umask int) {
syscall.Umask(umask)
}

View File

@@ -0,0 +1,8 @@
//go:build windows
// +build windows
package decypharr
func SetUmask(umask int) {
// No-op on Windows
}

View File

@@ -51,6 +51,7 @@
"download_folder": "/mnt/symlinks/",
"categories": ["sonarr", "radarr"],
"refresh_interval": 5,
"skip_pre_cache": false
},
"arrs": [
{

View File

@@ -41,6 +41,7 @@ type QBitTorrent struct {
DownloadFolder string `json:"download_folder"`
Categories []string `json:"categories"`
RefreshInterval int `json:"refresh_interval"`
SkipPreCache bool `json:"skip_pre_cache"`
}
type Arr struct {
@@ -49,7 +50,7 @@ type Arr struct {
Token string `json:"token"`
Cleanup bool `json:"cleanup"`
SkipRepair bool `json:"skip_repair"`
DownloadUncached bool `json:"download_uncached"`
DownloadUncached *bool `json:"download_uncached"`
}
type Repair struct {

View File

@@ -31,11 +31,11 @@ type Arr struct {
Type Type `json:"type"`
Cleanup bool `json:"cleanup"`
SkipRepair bool `json:"skip_repair"`
DownloadUncached bool `json:"download_uncached"`
DownloadUncached *bool `json:"download_uncached"`
client *http.Client
}
func New(name, host, token string, cleanup, skipRepair, downloadUncached bool) *Arr {
func New(name, host, token string, cleanup, skipRepair bool, downloadUncached *bool) *Arr {
return &Arr{
Name: name,
Host: host,

View File

@@ -192,7 +192,7 @@ func (ad *AllDebrid) CheckStatus(torrent *torrent.Torrent, isSymlink bool) (*tor
}
break
} else if slices.Contains(ad.GetDownloadingStatus(), status) {
if !ad.DownloadUncached && !torrent.DownloadUncached {
if !torrent.DownloadUncached {
return torrent, fmt.Errorf("torrent: %s not cached", torrent.Name)
}
// Break out of the loop if the torrent is downloading.
@@ -280,6 +280,10 @@ func (ad *AllDebrid) GetDownloadingStatus() []string {
return []string{"downloading"}
}
func (ad *AllDebrid) GetDownloadUncached() bool {
return ad.DownloadUncached
}
func New(dc config.Debrid, cache *cache.Cache) *AllDebrid {
rl := request.ParseRateLimit(dc.RateLimit)
headers := map[string]string{

View File

@@ -47,14 +47,14 @@ func createDebrid(dc config.Debrid, cache *cache.Cache) engine.Service {
}
}
func ProcessTorrent(d *engine.Engine, magnet *utils.Magnet, a *arr.Arr, isSymlink, downloadUncached bool) (*torrent.Torrent, error) {
func ProcessTorrent(d *engine.Engine, magnet *utils.Magnet, a *arr.Arr, isSymlink, overrideDownloadUncached bool) (*torrent.Torrent, error) {
debridTorrent := &torrent.Torrent{
InfoHash: magnet.InfoHash,
Magnet: magnet,
Name: magnet.Name,
Arr: a,
Size: magnet.Size,
DownloadUncached: cmp.Or(downloadUncached, a.DownloadUncached),
InfoHash: magnet.InfoHash,
Magnet: magnet,
Name: magnet.Name,
Arr: a,
Size: magnet.Size,
}
errs := make([]error, 0)
@@ -63,6 +63,17 @@ func ProcessTorrent(d *engine.Engine, magnet *utils.Magnet, a *arr.Arr, isSymlin
logger := db.GetLogger()
logger.Info().Msgf("Processing debrid: %s", db.GetName())
// Override first, arr second, debrid third
if overrideDownloadUncached {
debridTorrent.DownloadUncached = true
} else if a.DownloadUncached != nil {
// Arr cached is set
debridTorrent.DownloadUncached = *a.DownloadUncached
} else {
debridTorrent.DownloadUncached = db.GetDownloadUncached()
}
logger.Info().Msgf("Torrent Hash: %s", debridTorrent.InfoHash)
if db.GetCheckCached() {
hash, exists := db.IsAvailable([]string{debridTorrent.InfoHash})[debridTorrent.InfoHash]

View File

@@ -217,7 +217,7 @@ func (dl *DebridLink) CheckStatus(torrent *torrent.Torrent, isSymlink bool) (*to
}
break
} else if slices.Contains(dl.GetDownloadingStatus(), status) {
if !dl.DownloadUncached && !torrent.DownloadUncached {
if !torrent.DownloadUncached {
return torrent, fmt.Errorf("torrent: %s not cached", torrent.Name)
}
// Break out of the loop if the torrent is downloading.
@@ -271,6 +271,10 @@ func (dl *DebridLink) GetCheckCached() bool {
return dl.CheckCached
}
func (dl *DebridLink) GetDownloadUncached() bool {
return dl.DownloadUncached
}
func New(dc config.Debrid, cache *cache.Cache) *DebridLink {
rl := request.ParseRateLimit(dc.RateLimit)
headers := map[string]string{

View File

@@ -13,6 +13,7 @@ type Service interface {
DeleteTorrent(tr *torrent.Torrent)
IsAvailable(infohashes []string) map[string]bool
GetCheckCached() bool
GetDownloadUncached() bool
GetTorrent(torrent *torrent.Torrent) (*torrent.Torrent, error)
GetTorrents() ([]*torrent.Torrent, error)
GetName() string

View File

@@ -249,12 +249,9 @@ func (r *RealDebrid) CheckStatus(t *torrent.Torrent, isSymlink bool) (*torrent.T
}
break
} else if slices.Contains(r.GetDownloadingStatus(), status) {
if !r.DownloadUncached && !t.DownloadUncached {
if !t.DownloadUncached {
return t, fmt.Errorf("torrent: %s not cached", t.Name)
}
// Break out of the loop if the torrent is downloading.
// This is necessary to prevent infinite loop since we moved to sync downloading and async processing
break
} else {
return t, fmt.Errorf("torrent: %s has error: %s", t.Name, status)
}
@@ -384,6 +381,10 @@ func (r *RealDebrid) GetDownloadingStatus() []string {
return []string{"downloading", "magnet_conversion", "queued", "compressing", "uploading"}
}
func (r *RealDebrid) GetDownloadUncached() bool {
return r.DownloadUncached
}
func New(dc config.Debrid, cache *cache.Cache) *RealDebrid {
rl := request.ParseRateLimit(dc.RateLimit)
headers := map[string]string{

View File

@@ -231,7 +231,7 @@ func (tb *Torbox) CheckStatus(torrent *torrent.Torrent, isSymlink bool) (*torren
}
break
} else if slices.Contains(tb.GetDownloadingStatus(), status) {
if !tb.DownloadUncached && !torrent.DownloadUncached {
if !torrent.DownloadUncached {
return torrent, fmt.Errorf("torrent: %s not cached", torrent.Name)
}
// Break out of the loop if the torrent is downloading.
@@ -332,6 +332,10 @@ func (tb *Torbox) GetTorrents() ([]*torrent.Torrent, error) {
return nil, fmt.Errorf("not implemented")
}
func (tb *Torbox) GetDownloadUncached() bool {
return tb.DownloadUncached
}
func New(dc config.Debrid, cache *cache.Cache) *Torbox {
rl := request.ParseRateLimit(dc.RateLimit)
headers := map[string]string{

View File

@@ -6,6 +6,7 @@ import (
"github.com/cavaliergopher/grab/v3"
"github.com/sirrobot01/debrid-blackhole/internal/utils"
debrid "github.com/sirrobot01/debrid-blackhole/pkg/debrid/torrent"
"io"
"net/http"
"os"
"path/filepath"
@@ -202,4 +203,56 @@ func (q *QBit) createSymLink(path string, torrentMountPath string, file debrid.F
// It's okay if the symlink already exists
q.logger.Debug().Msgf("Failed to create symlink: %s: %v", fullPath, err)
}
if q.SkipPreCache {
return
}
go func() {
err := q.preCacheFile(torrentFilePath)
if err != nil {
q.logger.Debug().Msgf("Failed to pre-cache file: %s: %v", torrentFilePath, err)
}
}()
}
func (q *QBit) preCacheFile(filePath string) error {
q.logger.Trace().Msgf("Pre-caching file: %s", filePath)
file, err := os.Open(filePath)
if err != nil {
return fmt.Errorf("error opening file: %v", err)
}
defer file.Close()
// Pre-cache the file header (first 256KB) using 16KB chunks.
q.readSmallChunks(file, 0, 256*1024, 16*1024)
q.readSmallChunks(file, 1024*1024, 64*1024, 16*1024)
return nil
}
func (q *QBit) readSmallChunks(file *os.File, startPos int64, totalToRead int, chunkSize int) {
_, err := file.Seek(startPos, 0)
if err != nil {
return
}
buf := make([]byte, chunkSize)
bytesRemaining := totalToRead
for bytesRemaining > 0 {
toRead := chunkSize
if bytesRemaining < chunkSize {
toRead = bytesRemaining
}
n, err := file.Read(buf[:toRead])
if err != nil {
if err == io.EOF {
break
}
return
}
bytesRemaining -= n
}
return
}

View File

@@ -59,7 +59,8 @@ func (q *QBit) authContext(next http.Handler) http.Handler {
// Check if arr exists
a := svc.Arr.Get(category)
if a == nil {
a = arr.New(category, "", "", false, false, false)
downloadUncached := false
a = arr.New(category, "", "", false, false, &downloadUncached)
}
if err == nil {
host = strings.TrimSpace(host)

View File

@@ -19,6 +19,7 @@ type QBit struct {
logger zerolog.Logger
Tags []string
RefreshInterval int
SkipPreCache bool
}
func New() *QBit {
@@ -35,5 +36,6 @@ func New() *QBit {
Storage: NewTorrentStorage(filepath.Join(_cfg.Path, "torrents.json")),
logger: logger.NewLogger("qbit", _cfg.LogLevel, os.Stdout),
RefreshInterval: refreshInterval,
SkipPreCache: cfg.SkipPreCache,
}
}

View File

@@ -311,7 +311,7 @@ func (ui *Handler) handleAddContent(w http.ResponseWriter, r *http.Request) {
_arr := svc.Arr.Get(arrName)
if _arr == nil {
_arr = arr.New(arrName, "", "", false, false, false)
_arr = arr.New(arrName, "", "", false, false, &downloadUncached)
}
// Handle URLs