fix mounts; backward compatibility

This commit is contained in:
Mukhtar Akere
2025-02-13 05:07:14 +01:00
parent 6f4f72d781
commit bfd2596367
21 changed files with 71 additions and 466 deletions

View File

@@ -31,10 +31,10 @@ RUN --mount=type=cache,target=/go/pkg/mod \
# Stage 2: Create directory structure # Stage 2: Create directory structure
FROM alpine:3.19 as dirsetup FROM alpine:3.19 as dirsetup
RUN mkdir -p /data/logs && \ RUN mkdir -p /app/logs && \
chmod 777 /data/logs && \ chmod 777 /app/logs && \
touch /data/logs/decypharr.log && \ touch /app/logs/decypharr.log && \
chmod 666 /data/logs/decypharr.log chmod 666 /app/logs/decypharr.log
# Stage 3: Final image # Stage 3: Final image
FROM gcr.io/distroless/static-debian12:nonroot FROM gcr.io/distroless/static-debian12:nonroot
@@ -51,14 +51,15 @@ COPY --from=builder --chown=nonroot:nonroot /blackhole /usr/bin/blackhole
COPY --from=builder --chown=nonroot:nonroot /healthcheck /usr/bin/healthcheck COPY --from=builder --chown=nonroot:nonroot /healthcheck /usr/bin/healthcheck
# Copy pre-made directory structure # Copy pre-made directory structure
COPY --from=dirsetup --chown=nonroot:nonroot /data /data COPY --from=dirsetup --chown=nonroot:nonroot /app /app
# Metadata # Metadata
ENV LOG_PATH=/data/logs ENV LOG_PATH=/app/logs
EXPOSE 8181 8282 EXPOSE 8181 8282
VOLUME ["/data", "/app"] VOLUME ["/app"]
USER nonroot:nonroot USER nonroot:nonroot
HEALTHCHECK CMD ["/usr/bin/healthcheck"] HEALTHCHECK CMD ["/usr/bin/healthcheck"]
CMD ["/usr/bin/blackhole", "--config", "/data/config.json"] CMD ["/usr/bin/blackhole", "--config", "/app"]

View File

@@ -61,7 +61,7 @@ services:
user: "1000:1000" user: "1000:1000"
volumes: volumes:
- /mnt/:/mnt - /mnt/:/mnt
- ~/plex/configs/blackhole/:/data # Path to the config file. config.json - ~/plex/configs/blackhole/:/app # config.json must be in this directory
environment: environment:
- PUID=1000 - PUID=1000
- PGID=1000 - PGID=1000
@@ -78,7 +78,7 @@ services:
Download the binary from the releases page and run it with the config file. Download the binary from the releases page and run it with the config file.
```bash ```bash
./blackhole --config /path/to/ ./blackhole --config /app
``` ```
### Usage ### Usage
@@ -104,7 +104,7 @@ Download the binary from the releases page and run it with the config file.
#### Basic Sample Config #### Basic Sample Config
This is the default config file. You can create a `config.json` file in the root directory of the project or mount it to /data in the docker-compose file. This is the default config file. You can create a `config.json` file in the root directory of the project or mount it to /app in the docker-compose file.
```json ```json
{ {
"debrids": [ "debrids": [
@@ -130,7 +130,8 @@ This is the default config file. You can create a `config.json` file in the root
"enabled": false, "enabled": false,
"interval": "12h", "interval": "12h",
"run_on_start": false "run_on_start": false
} },
"use_auth": false
} }
``` ```
@@ -145,7 +146,7 @@ Full config are [here](doc/config.full.json)
- The `log_level` key is used to set the log level of the application. The default value is `info`. log level can be set to `debug`, `info`, `warn`, `error` - The `log_level` key is used to set the log level of the application. The default value is `info`. log level can be set to `debug`, `info`, `warn`, `error`
- 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 `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 `allowed_file_types` key is an array of allowed file types that can be downloaded. By default, all movie, tv show and music file types are allowed - The `allowed_file_types` key is an array of allowed file types that can be downloaded. By default, all movie, tv show and music file types are allowed
- `use_auth` is used to enable basic authentication for the UI. - The `use_auth` is used to enable basic authentication for the UI. The default value is `false`
##### Debrid Config ##### Debrid Config
- The `debrids` key is an array of debrid providers - The `debrids` key is an array of debrid providers

View File

@@ -1,4 +1,4 @@
package cmd package decypharr
import ( import (
"context" "context"

View File

@@ -37,7 +37,7 @@ func RemoveInvalidChars(value string) string {
} }
func RemoveExtension(value string) string { func RemoveExtension(value string) string {
re := regexp.MustCompile(VIDEOMATCH + "|" + SAMPLEMATCH + "|" + MUSICMATCH) re := regexp.MustCompile(VIDEOMATCH + "|" + MUSICMATCH)
// Find the last index of the matched extension // Find the last index of the matched extension
loc := re.FindStringIndex(value) loc := re.FindStringIndex(value)

View File

@@ -197,21 +197,16 @@ func validateConfig(config *Config) error {
return nil return nil
} }
func SetConfigPath(path string) { func SetConfigPath(path string) error {
// Backward compatibility
// Check if the path is not a dir
if fi, err := os.Stat(path); err == nil && !fi.IsDir() {
// Get the directory of the file
path = filepath.Dir(path)
}
configPath = path configPath = path
return nil
} }
func GetConfig() *Config { func GetConfig() *Config {
once.Do(func() { once.Do(func() {
instance = &Config{} // Initialize instance first instance = &Config{} // Initialize instance first
if err := instance.loadConfig(); err != nil { if err := instance.loadConfig(); err != nil {
_, err := fmt.Fprintf(os.Stderr, "Configuration Error: %v\n", err) _, err := fmt.Fprintf(os.Stderr, "configuration Error: %v\n", err)
if err != nil { if err != nil {
return return
} }

View File

@@ -20,8 +20,10 @@ func GetLogPath() string {
cfg := config.GetConfig() cfg := config.GetConfig()
logsDir := filepath.Join(cfg.Path, "logs") logsDir := filepath.Join(cfg.Path, "logs")
if err := os.MkdirAll(logsDir, 0755); err != nil { if _, err := os.Stat(logsDir); os.IsNotExist(err) {
panic(fmt.Sprintf("Failed to create logs directory: %v", err)) if err := os.MkdirAll(logsDir, 0755); err != nil {
panic(fmt.Sprintf("Failed to create logs directory: %v", err))
}
} }
return filepath.Join(logsDir, "decypharr.log") return filepath.Join(logsDir, "decypharr.log")

View File

@@ -105,7 +105,7 @@ func (c *RLHTTPClient) MakeRequest(req *http.Request) ([]byte, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
statusOk := res.StatusCode == http.StatusOK || res.StatusCode == http.StatusCreated statusOk := res.StatusCode >= 200 && res.StatusCode < 300
if !statusOk { if !statusOk {
// Add status code error to the body // Add status code error to the body
b = append(b, []byte(fmt.Sprintf("\nstatus code: %d", res.StatusCode))...) b = append(b, []byte(fmt.Sprintf("\nstatus code: %d", res.StatusCode))...)

10
main.go
View File

@@ -3,20 +3,22 @@ package main
import ( import (
"context" "context"
"flag" "flag"
"github.com/sirrobot01/debrid-blackhole/cmd" "github.com/sirrobot01/debrid-blackhole/cmd/decypharr"
"github.com/sirrobot01/debrid-blackhole/internal/config" "github.com/sirrobot01/debrid-blackhole/internal/config"
"log" "log"
) )
func main() { func main() {
var configPath string var configPath string
flag.StringVar(&configPath, "config", "config.json", "path to the config file") flag.StringVar(&configPath, "config", "/data", "path to the data folder")
flag.Parse() flag.Parse()
config.SetConfigPath(configPath) if err := config.SetConfigPath(configPath); err != nil {
log.Fatal(err)
}
config.GetConfig() config.GetConfig()
ctx := context.Background() ctx := context.Background()
if err := cmd.Start(ctx); err != nil { if err := decypharr.Start(ctx); err != nil {
log.Fatal(err) log.Fatal(err)
} }

View File

@@ -29,10 +29,6 @@ type AllDebrid struct {
CheckCached bool CheckCached bool
} }
func (ad *AllDebrid) GetMountPath() string {
return ad.MountPath
}
func (ad *AllDebrid) GetName() string { func (ad *AllDebrid) GetName() string {
return ad.Name return ad.Name
} }
@@ -77,7 +73,6 @@ func (ad *AllDebrid) SubmitMagnet(torrent *torrent.Torrent) (*torrent.Torrent, e
} }
magnet := magnets[0] magnet := magnets[0]
torrentId := strconv.Itoa(magnet.ID) torrentId := strconv.Itoa(magnet.ID)
ad.logger.Info().Msgf("Torrent: %s added with id: %s", torrent.Name, torrentId)
torrent.Id = torrentId torrent.Id = torrentId
return torrent, nil return torrent, nil
@@ -170,12 +165,6 @@ func (ad *AllDebrid) GetTorrent(id string) (*torrent.Torrent, error) {
t.Seeders = data.Seeders t.Seeders = data.Seeders
index := -1 index := -1
files := flattenFiles(data.Files, "", &index) files := flattenFiles(data.Files, "", &index)
parentFolder := data.Filename
if data.NbLinks == 1 {
// All debrid doesn't return the parent folder for single file torrents
parentFolder = ""
}
t.OriginalFilename = parentFolder
t.Files = files t.Files = files
} }
return t, nil return t, nil

View File

@@ -1,360 +0,0 @@
package cache
import (
"bufio"
"encoding/json"
"fmt"
"github.com/rs/zerolog"
"github.com/sirrobot01/debrid-blackhole/internal/logger"
"os"
"path/filepath"
"runtime"
"sync"
"time"
"github.com/sirrobot01/debrid-blackhole/internal/config"
"github.com/sirrobot01/debrid-blackhole/pkg/debrid/engine"
"github.com/sirrobot01/debrid-blackhole/pkg/debrid/torrent"
)
type DownloadLinkCache struct {
Link string `json:"download_link"`
}
type CachedTorrent struct {
*torrent.Torrent
LastRead time.Time `json:"last_read"`
IsComplete bool `json:"is_complete"`
DownloadLinks map[string]DownloadLinkCache `json:"download_links"`
}
var (
_logInstance zerolog.Logger
once sync.Once
)
func getLogger() zerolog.Logger {
once.Do(func() {
_logInstance = logger.NewLogger("cache", "info", os.Stdout)
})
return _logInstance
}
type Cache struct {
dir string
client engine.Service
torrents *sync.Map // key: torrent.Id, value: *CachedTorrent
torrentsNames *sync.Map // key: torrent.Name, value: torrent.Id
LastUpdated time.Time `json:"last_updated"`
}
type Manager struct {
caches map[string]*Cache
}
func NewManager(debridService *engine.Engine) *Manager {
cfg := config.GetConfig()
cm := &Manager{
caches: make(map[string]*Cache),
}
for _, debrid := range debridService.GetDebrids() {
c := New(debrid, cfg.Path)
cm.caches[debrid.GetName()] = c
}
return cm
}
func (m *Manager) GetCaches() map[string]*Cache {
return m.caches
}
func (m *Manager) GetCache(debridName string) *Cache {
return m.caches[debridName]
}
func New(debridService engine.Service, basePath string) *Cache {
return &Cache{
dir: filepath.Join(basePath, "cache", debridService.GetName(), "torrents"),
torrents: &sync.Map{},
torrentsNames: &sync.Map{},
client: debridService,
}
}
func (c *Cache) Start() error {
_logger := getLogger()
_logger.Info().Msg("Starting cache for: " + c.client.GetName())
if err := c.Load(); err != nil {
return fmt.Errorf("failed to load cache: %v", err)
}
if err := c.Sync(); err != nil {
return fmt.Errorf("failed to sync cache: %v", err)
}
return nil
}
func (c *Cache) Load() error {
_logger := getLogger()
if err := os.MkdirAll(c.dir, 0755); err != nil {
return fmt.Errorf("failed to create cache directory: %w", err)
}
files, err := os.ReadDir(c.dir)
if err != nil {
return fmt.Errorf("failed to read cache directory: %w", err)
}
for _, file := range files {
if file.IsDir() || filepath.Ext(file.Name()) != ".json" {
continue
}
filePath := filepath.Join(c.dir, file.Name())
data, err := os.ReadFile(filePath)
if err != nil {
_logger.Debug().Err(err).Msgf("Failed to read file: %s", filePath)
continue
}
var ct CachedTorrent
if err := json.Unmarshal(data, &ct); err != nil {
_logger.Debug().Err(err).Msgf("Failed to unmarshal file: %s", filePath)
continue
}
if len(ct.Files) > 0 {
c.torrents.Store(ct.Torrent.Id, &ct)
c.torrentsNames.Store(ct.Torrent.Name, ct.Torrent.Id)
}
}
return nil
}
func (c *Cache) GetTorrent(id string) *CachedTorrent {
if value, ok := c.torrents.Load(id); ok {
return value.(*CachedTorrent)
}
return nil
}
func (c *Cache) GetTorrentByName(name string) *CachedTorrent {
if id, ok := c.torrentsNames.Load(name); ok {
return c.GetTorrent(id.(string))
}
return nil
}
func (c *Cache) SaveTorrent(ct *CachedTorrent) error {
data, err := json.MarshalIndent(ct, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal torrent: %w", err)
}
fileName := ct.Torrent.Id + ".json"
filePath := filepath.Join(c.dir, fileName)
tmpFile := filePath + ".tmp"
f, err := os.Create(tmpFile)
if err != nil {
return fmt.Errorf("failed to create temp file: %w", err)
}
defer f.Close()
w := bufio.NewWriter(f)
if _, err := w.Write(data); err != nil {
return fmt.Errorf("failed to write data: %w", err)
}
if err := w.Flush(); err != nil {
return fmt.Errorf("failed to flush data: %w", err)
}
return os.Rename(tmpFile, filePath)
}
func (c *Cache) SaveAll() error {
const batchSize = 100
var wg sync.WaitGroup
_logger := getLogger()
tasks := make(chan *CachedTorrent, batchSize)
for i := 0; i < runtime.NumCPU(); i++ {
wg.Add(1)
go func() {
defer wg.Done()
for ct := range tasks {
if err := c.SaveTorrent(ct); err != nil {
_logger.Error().Err(err).Msg("failed to save torrent")
}
}
}()
}
c.torrents.Range(func(_, value interface{}) bool {
tasks <- value.(*CachedTorrent)
return true
})
close(tasks)
wg.Wait()
c.LastUpdated = time.Now()
return nil
}
func (c *Cache) Sync() error {
_logger := getLogger()
torrents, err := c.client.GetTorrents()
if err != nil {
return fmt.Errorf("failed to sync torrents: %v", err)
}
workers := runtime.NumCPU() * 200
workChan := make(chan *torrent.Torrent, len(torrents))
errChan := make(chan error, len(torrents))
var wg sync.WaitGroup
for i := 0; i < workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for t := range workChan {
if err := c.processTorrent(t); err != nil {
errChan <- err
}
}
}()
}
for _, t := range torrents {
workChan <- t
}
close(workChan)
wg.Wait()
close(errChan)
for err := range errChan {
_logger.Error().Err(err).Msg("sync error")
}
_logger.Info().Msgf("Synced %d torrents", len(torrents))
return nil
}
func (c *Cache) processTorrent(t *torrent.Torrent) error {
if existing, ok := c.torrents.Load(t.Id); ok {
ct := existing.(*CachedTorrent)
if ct.IsComplete {
return nil
}
}
c.AddTorrent(t)
return nil
}
func (c *Cache) AddTorrent(t *torrent.Torrent) {
_logger := getLogger()
if len(t.Files) == 0 {
tNew, err := c.client.GetTorrent(t.Id)
_logger.Debug().Msgf("Getting torrent files for %s", t.Id)
if err != nil {
_logger.Debug().Msgf("Failed to get torrent files for %s: %v", t.Id, err)
return
}
t = tNew
}
if len(t.Files) == 0 {
_logger.Debug().Msgf("No files found for %s", t.Id)
return
}
ct := &CachedTorrent{
Torrent: t,
LastRead: time.Now(),
IsComplete: len(t.Files) > 0,
DownloadLinks: make(map[string]DownloadLinkCache),
}
c.torrents.Store(t.Id, ct)
c.torrentsNames.Store(t.Name, t.Id)
go func() {
if err := c.SaveTorrent(ct); err != nil {
_logger.Debug().Err(err).Msgf("Failed to save torrent %s", t.Id)
}
}()
}
func (c *Cache) RefreshTorrent(torrentId string) *CachedTorrent {
_logger := getLogger()
t, err := c.client.GetTorrent(torrentId)
if err != nil {
_logger.Debug().Msgf("Failed to get torrent files for %s: %v", torrentId, err)
return nil
}
if len(t.Files) == 0 {
return nil
}
ct := &CachedTorrent{
Torrent: t,
LastRead: time.Now(),
IsComplete: len(t.Files) > 0,
DownloadLinks: make(map[string]DownloadLinkCache),
}
c.torrents.Store(t.Id, ct)
c.torrentsNames.Store(t.Name, t.Id)
go func() {
if err := c.SaveTorrent(ct); err != nil {
_logger.Debug().Err(err).Msgf("Failed to save torrent %s", t.Id)
}
}()
return ct
}
func (c *Cache) GetFileDownloadLink(t *CachedTorrent, file *torrent.File) (string, error) {
_logger := getLogger()
if linkCache, ok := t.DownloadLinks[file.Id]; ok {
return linkCache.Link, nil
}
if file.Link == "" {
t = c.RefreshTorrent(t.Id)
if t == nil {
return "", fmt.Errorf("torrent not found")
}
file = t.Torrent.GetFile(file.Id)
}
_logger.Debug().Msgf("Getting download link for %s", t.Name)
link := c.client.GetDownloadLink(t.Torrent, file)
if link == nil {
return "", fmt.Errorf("download link not found")
}
t.DownloadLinks[file.Id] = DownloadLinkCache{
Link: link.DownloadLink,
}
go func() {
if err := c.SaveTorrent(t); err != nil {
_logger.Debug().Err(err).Msgf("Failed to save torrent %s", t.Id)
}
}()
return link.DownloadLink, nil
}
func (c *Cache) GetTorrents() *sync.Map {
return c.torrents
}

View File

@@ -81,7 +81,7 @@ func ProcessTorrent(d *engine.Engine, magnet *utils.Magnet, a *arr.Arr, isSymlin
errs = append(errs, err) errs = append(errs, err)
continue continue
} }
logger.Info().Msgf("Torrent: %s submitted to %s", dbt.Name, db.GetName()) logger.Info().Msgf("Torrent: %s(id=%s) submitted to %s", dbt.Name, dbt.Id, db.GetName())
d.LastUsed = index d.LastUsed = index
return db.CheckStatus(dbt, isSymlink) return db.CheckStatus(dbt, isSymlink)
} }

View File

@@ -11,7 +11,6 @@ import (
"github.com/sirrobot01/debrid-blackhole/internal/request" "github.com/sirrobot01/debrid-blackhole/internal/request"
"github.com/sirrobot01/debrid-blackhole/pkg/debrid/torrent" "github.com/sirrobot01/debrid-blackhole/pkg/debrid/torrent"
"log"
"net/http" "net/http"
"os" "os"
"strings" "strings"
@@ -29,10 +28,6 @@ type DebridLink struct {
CheckCached bool CheckCached bool
} }
func (dl *DebridLink) GetMountPath() string {
return dl.MountPath
}
func (dl *DebridLink) GetName() string { func (dl *DebridLink) GetName() string {
return dl.Name return dl.Name
} }
@@ -176,7 +171,6 @@ func (dl *DebridLink) SubmitMagnet(t *torrent.Torrent) (*torrent.Torrent, error)
} }
data := *res.Value data := *res.Value
status := "downloading" status := "downloading"
log.Printf("Torrent: %s added with id: %s", t.Name, data.ID)
name := common.RemoveInvalidChars(data.Name) name := common.RemoveInvalidChars(data.Name)
t.Id = data.ID t.Id = data.ID
t.Name = name t.Name = name

View File

@@ -12,7 +12,6 @@ type Service interface {
GetDownloadLink(tr *torrent.Torrent, file *torrent.File) *torrent.DownloadLinks GetDownloadLink(tr *torrent.Torrent, file *torrent.File) *torrent.DownloadLinks
DeleteTorrent(tr *torrent.Torrent) DeleteTorrent(tr *torrent.Torrent)
IsAvailable(infohashes []string) map[string]bool IsAvailable(infohashes []string) map[string]bool
GetMountPath() string
GetCheckCached() bool GetCheckCached() bool
GetTorrent(id string) (*torrent.Torrent, error) GetTorrent(id string) (*torrent.Torrent, error)
GetTorrents() ([]*torrent.Torrent, error) GetTorrents() ([]*torrent.Torrent, error)

View File

@@ -31,10 +31,6 @@ type RealDebrid struct {
CheckCached bool CheckCached bool
} }
func (r *RealDebrid) GetMountPath() string {
return r.MountPath
}
func (r *RealDebrid) GetName() string { func (r *RealDebrid) GetName() string {
return r.Name return r.Name
} }
@@ -156,9 +152,9 @@ func (r *RealDebrid) SubmitMagnet(t *torrent.Torrent) (*torrent.Torrent, error)
return nil, err return nil, err
} }
err = json.Unmarshal(resp, &data) err = json.Unmarshal(resp, &data)
r.logger.Info().Msgf("Torrent: %s added with id: %s", t.Name, data.Id)
t.Id = data.Id t.Id = data.Id
t.Debrid = r.Name
t.MountPath = r.MountPath
return t, nil return t, nil
} }
@@ -218,6 +214,8 @@ func (r *RealDebrid) CheckStatus(t *torrent.Torrent, isSymlink bool) (*torrent.T
t.Seeders = data.Seeders t.Seeders = data.Seeders
t.Links = data.Links t.Links = data.Links
t.Status = status t.Status = status
t.Debrid = r.Name
t.MountPath = r.MountPath
downloadingStatus := []string{"downloading", "magnet_conversion", "queued", "compressing", "uploading"} downloadingStatus := []string{"downloading", "magnet_conversion", "queued", "compressing", "uploading"}
if status == "waiting_files_selection" { if status == "waiting_files_selection" {
files := GetTorrentFiles(data, true) // Validate files to be selected files := GetTorrentFiles(data, true) // Validate files to be selected

View File

@@ -11,7 +11,6 @@ import (
"github.com/sirrobot01/debrid-blackhole/internal/request" "github.com/sirrobot01/debrid-blackhole/internal/request"
"github.com/sirrobot01/debrid-blackhole/pkg/debrid/torrent" "github.com/sirrobot01/debrid-blackhole/pkg/debrid/torrent"
"log"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
gourl "net/url" gourl "net/url"
@@ -35,10 +34,6 @@ type Torbox struct {
CheckCached bool CheckCached bool
} }
func (tb *Torbox) GetMountPath() string {
return tb.MountPath
}
func (tb *Torbox) GetName() string { func (tb *Torbox) GetName() string {
return tb.Name return tb.Name
} }
@@ -130,8 +125,9 @@ func (tb *Torbox) SubmitMagnet(torrent *torrent.Torrent) (*torrent.Torrent, erro
} }
dt := *data.Data dt := *data.Data
torrentId := strconv.Itoa(dt.Id) torrentId := strconv.Itoa(dt.Id)
log.Printf("Torrent: %s added with id: %s", torrent.Name, torrentId)
torrent.Id = torrentId torrent.Id = torrentId
torrent.MountPath = tb.MountPath
torrent.Debrid = tb.Name
return torrent, nil return torrent, nil
} }

View File

@@ -52,14 +52,13 @@ Loop:
func (q *QBit) ProcessManualFile(torrent *Torrent) (string, error) { func (q *QBit) ProcessManualFile(torrent *Torrent) (string, error) {
debridTorrent := torrent.DebridTorrent debridTorrent := torrent.DebridTorrent
q.logger.Info().Msgf("Downloading %d files...", len(debridTorrent.DownloadLinks)) q.logger.Info().Msgf("Downloading %d files...", len(debridTorrent.DownloadLinks))
torrentPath := common.RemoveExtension(debridTorrent.OriginalFilename) torrentPath := common.RemoveInvalidChars(filepath.Join(q.DownloadFolder, debridTorrent.Arr.Name, common.RemoveExtension(debridTorrent.OriginalFilename)))
parent := common.RemoveInvalidChars(filepath.Join(q.DownloadFolder, debridTorrent.Arr.Name, torrentPath)) err := os.MkdirAll(torrentPath, os.ModePerm)
err := os.MkdirAll(parent, os.ModePerm)
if err != nil { if err != nil {
// add previous error to the error and return // add previous error to the error and return
return "", fmt.Errorf("failed to create directory: %s: %v", parent, err) return "", fmt.Errorf("failed to create directory: %s: %v", torrentPath, err)
} }
q.downloadFiles(torrent, parent) q.downloadFiles(torrent, torrentPath)
return torrentPath, nil return torrentPath, nil
} }
@@ -140,25 +139,26 @@ func (q *QBit) ProcessSymlink(torrent *Torrent) (string, error) {
if len(files) == 0 { if len(files) == 0 {
return "", fmt.Errorf("no video files found") return "", fmt.Errorf("no video files found")
} }
q.logger.Info().Msgf("Checking %d files...", len(files)) q.logger.Info().Msgf("Checking symlinks for %d files...", len(files))
rCloneBase := debridTorrent.MountPath rCloneBase := debridTorrent.MountPath
torrentPath, err := q.getTorrentPath(rCloneBase, debridTorrent) // /MyTVShow/ torrentPath, err := q.getTorrentPath(rCloneBase, debridTorrent) // /MyTVShow/
// This returns filename.ext for alldebrid instead of the parent folder filename/
torrentFolder := torrentPath
if err != nil { if err != nil {
return "", fmt.Errorf("failed to get torrent path: %v", err) return "", fmt.Errorf("failed to get torrent path: %v", err)
} }
// Fix for alldebrid // Check if the torrent path is a file
newTorrentPath := torrentPath torrentRclonePath := filepath.Join(rCloneBase, torrentPath) // leave it as is
if newTorrentPath == "" { if debridTorrent.Debrid == "alldebrid" && len(files) == 1 {
// Alldebrid at times doesn't return the parent folder for single file torrents // Alldebrid hotfix for single file torrents
newTorrentPath = common.RemoveExtension(debridTorrent.Name) // MyTVShow torrentFolder = common.RemoveExtension(torrentFolder)
torrentRclonePath = rCloneBase // /mnt/rclone/magnets/ // Remove the filename since it's in the root folder
} }
torrentSymlinkPath := filepath.Join(q.DownloadFolder, debridTorrent.Arr.Name, newTorrentPath) // /mnt/symlinks/{category}/MyTVShow/ torrentSymlinkPath := filepath.Join(q.DownloadFolder, debridTorrent.Arr.Name, torrentFolder) // /mnt/symlinks/{category}/MyTVShow/
err = os.MkdirAll(torrentSymlinkPath, os.ModePerm) err = os.MkdirAll(torrentSymlinkPath, os.ModePerm)
if err != nil { if err != nil {
return "", fmt.Errorf("failed to create directory: %s: %v", torrentSymlinkPath, err) 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 { for _, file := range files {
wg.Add(1) wg.Add(1)
go checkFileLoop(&wg, torrentRclonePath, file, ready) go checkFileLoop(&wg, torrentRclonePath, file, ready)
@@ -173,12 +173,11 @@ func (q *QBit) ProcessSymlink(torrent *Torrent) (string, error) {
q.logger.Info().Msgf("File is ready: %s", f.Path) q.logger.Info().Msgf("File is ready: %s", f.Path)
q.createSymLink(torrentSymlinkPath, torrentRclonePath, f) q.createSymLink(torrentSymlinkPath, torrentRclonePath, f)
} }
return torrentPath, nil return torrentSymlinkPath, nil
} }
func (q *QBit) getTorrentPath(rclonePath string, debridTorrent *debrid.Torrent) (string, error) { func (q *QBit) getTorrentPath(rclonePath string, debridTorrent *debrid.Torrent) (string, error) {
for { for {
q.logger.Debug().Msgf("Checking for torrent path: %s", rclonePath)
torrentPath, err := debridTorrent.GetMountFolder(rclonePath) torrentPath, err := debridTorrent.GetMountFolder(rclonePath)
if err == nil { if err == nil {
q.logger.Debug().Msgf("Found torrent path: %s", torrentPath) q.logger.Debug().Msgf("Found torrent path: %s", torrentPath)

View File

@@ -74,6 +74,7 @@ func (i *ImportRequest) Process(q *QBit) (err error) {
torrent := CreateTorrentFromMagnet(magnet, i.Arr.Name, "manual") torrent := CreateTorrentFromMagnet(magnet, i.Arr.Name, "manual")
debridTorrent, err := debrid.ProcessTorrent(svc.Debrid, magnet, i.Arr, i.IsSymlink) debridTorrent, err := debrid.ProcessTorrent(svc.Debrid, magnet, i.Arr, i.IsSymlink)
if err != nil || debridTorrent == nil { if err != nil || debridTorrent == nil {
fmt.Println("Error deleting torrent: ", err)
if debridTorrent != nil { if debridTorrent != nil {
dbClient := service.GetDebrid().GetByName(debridTorrent.Debrid) dbClient := service.GetDebrid().GetByName(debridTorrent.Debrid)
go dbClient.DeleteTorrent(debridTorrent) go dbClient.DeleteTorrent(debridTorrent)

View File

@@ -6,6 +6,7 @@ import (
"github.com/sirrobot01/debrid-blackhole/internal/config" "github.com/sirrobot01/debrid-blackhole/internal/config"
"github.com/sirrobot01/debrid-blackhole/internal/logger" "github.com/sirrobot01/debrid-blackhole/internal/logger"
"os" "os"
"path/filepath"
) )
type QBit struct { type QBit struct {
@@ -22,7 +23,8 @@ type QBit struct {
} }
func New() *QBit { func New() *QBit {
cfg := config.GetConfig().QBitTorrent _cfg := config.GetConfig()
cfg := _cfg.QBitTorrent
port := cmp.Or(cfg.Port, os.Getenv("QBIT_PORT"), "8282") port := cmp.Or(cfg.Port, os.Getenv("QBIT_PORT"), "8282")
refreshInterval := cmp.Or(cfg.RefreshInterval, 10) refreshInterval := cmp.Or(cfg.RefreshInterval, 10)
return &QBit{ return &QBit{
@@ -31,7 +33,7 @@ func New() *QBit {
Port: port, Port: port,
DownloadFolder: cfg.DownloadFolder, DownloadFolder: cfg.DownloadFolder,
Categories: cfg.Categories, Categories: cfg.Categories,
Storage: NewTorrentStorage(cmp.Or(os.Getenv("TORRENT_FILE"), "/data/qbit_torrents.json")), Storage: NewTorrentStorage(filepath.Join(_cfg.Path, "torrents.json")),
logger: logger.NewLogger("qbit", cfg.LogLevel, os.Stdout), logger: logger.NewLogger("qbit", cfg.LogLevel, os.Stdout),
RefreshInterval: refreshInterval, RefreshInterval: refreshInterval,
} }

View File

@@ -2,15 +2,11 @@ package qbit
import ( import (
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"net/http" "net/http"
) )
func (q *QBit) Routes() http.Handler { func (q *QBit) Routes() http.Handler {
r := chi.NewRouter() r := chi.NewRouter()
if q.logger.GetLevel().String() == "debug" {
r.Use(middleware.Logger)
}
r.Use(q.CategoryContext) r.Use(q.CategoryContext)
r.Post("/auth/login", q.handleLogin) r.Post("/auth/login", q.handleLogin)

View File

@@ -90,14 +90,14 @@ func (q *QBit) ProcessFiles(torrent *Torrent, debridTorrent *debrid.Torrent, arr
torrent = q.UpdateTorrentMin(torrent, debridTorrent) torrent = q.UpdateTorrentMin(torrent, debridTorrent)
} }
var ( var (
torrentPath string torrentSymlinkPath string
err error err error
) )
debridTorrent.Arr = arr debridTorrent.Arr = arr
if isSymlink { if isSymlink {
torrentPath, err = q.ProcessSymlink(torrent) torrentSymlinkPath, err = q.ProcessSymlink(torrent) // /mnt/symlinks/{category}/MyTVShow/
} else { } else {
torrentPath, err = q.ProcessManualFile(torrent) torrentSymlinkPath, err = q.ProcessManualFile(torrent)
} }
if err != nil { if err != nil {
q.MarkAsFailed(torrent) q.MarkAsFailed(torrent)
@@ -105,7 +105,7 @@ func (q *QBit) ProcessFiles(torrent *Torrent, debridTorrent *debrid.Torrent, arr
q.logger.Info().Msgf("Error: %v", err) q.logger.Info().Msgf("Error: %v", err)
return return
} }
torrent.TorrentPath = filepath.Base(torrentPath) torrent.TorrentPath = torrentSymlinkPath
q.UpdateTorrent(torrent, debridTorrent) q.UpdateTorrent(torrent, debridTorrent)
_ = arr.Refresh() _ = arr.Refresh()
} }
@@ -161,7 +161,6 @@ func (q *QBit) UpdateTorrentMin(t *Torrent, debridTorrent *debrid.Torrent) *Torr
func (q *QBit) UpdateTorrent(t *Torrent, debridTorrent *debrid.Torrent) *Torrent { func (q *QBit) UpdateTorrent(t *Torrent, debridTorrent *debrid.Torrent) *Torrent {
_db := service.GetDebrid().GetByName(debridTorrent.Debrid) _db := service.GetDebrid().GetByName(debridTorrent.Debrid)
rcLoneMount := _db.GetMountPath()
if debridTorrent == nil && t.ID != "" { if debridTorrent == nil && t.ID != "" {
debridTorrent, _ = _db.GetTorrent(t.ID) debridTorrent, _ = _db.GetTorrent(t.ID)
} }
@@ -172,15 +171,9 @@ func (q *QBit) UpdateTorrent(t *Torrent, debridTorrent *debrid.Torrent) *Torrent
if debridTorrent.Status != "downloaded" { if debridTorrent.Status != "downloaded" {
debridTorrent, _ = _db.GetTorrent(t.ID) 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 = q.UpdateTorrentMin(t, debridTorrent)
t.ContentPath = torrentPath t.ContentPath = t.TorrentPath + string(os.PathSeparator)
t.SavePath = t.ContentPath
if t.IsReady() { if t.IsReady() {
t.State = "pausedUP" t.State = "pausedUP"

View File

@@ -3,17 +3,15 @@ package service
import ( import (
"github.com/sirrobot01/debrid-blackhole/pkg/arr" "github.com/sirrobot01/debrid-blackhole/pkg/arr"
"github.com/sirrobot01/debrid-blackhole/pkg/debrid" "github.com/sirrobot01/debrid-blackhole/pkg/debrid"
"github.com/sirrobot01/debrid-blackhole/pkg/debrid/cache"
"github.com/sirrobot01/debrid-blackhole/pkg/debrid/engine" "github.com/sirrobot01/debrid-blackhole/pkg/debrid/engine"
"github.com/sirrobot01/debrid-blackhole/pkg/repair" "github.com/sirrobot01/debrid-blackhole/pkg/repair"
"sync" "sync"
) )
type Service struct { type Service struct {
Repair *repair.Repair Repair *repair.Repair
Arr *arr.Storage Arr *arr.Storage
Debrid *engine.Engine Debrid *engine.Engine
DebridCache *cache.Manager
} }
var ( var (
@@ -26,10 +24,9 @@ func New() *Service {
arrs := arr.NewStorage() arrs := arr.NewStorage()
deb := debrid.New() deb := debrid.New()
instance = &Service{ instance = &Service{
Repair: repair.New(deb, arrs), Repair: repair.New(deb, arrs),
Arr: arrs, Arr: arrs,
Debrid: deb, Debrid: deb,
DebridCache: cache.NewManager(deb),
} }
}) })
return instance return instance