- Add support for multi-season imports

- Improve in-memoery storage, whic reduces memory usage
- Fix issues with rclone integration
This commit is contained in:
Mukhtar Akere
2025-08-24 16:25:37 +01:00
parent f8667938b6
commit 618eb73067
10 changed files with 979 additions and 418 deletions
+437 -130
View File
@@ -1,10 +1,15 @@
package wire
import (
"crypto/md5"
"fmt"
"github.com/google/uuid"
"net/http"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
"time"
@@ -14,6 +19,244 @@ import (
"github.com/sirrobot01/decypharr/internal/utils"
)
// Multi-season detection patterns
var (
// Pre-compiled patterns for multi-season replacement
multiSeasonReplacements = []multiSeasonPattern{
// S01-08 -> S01 (or whatever target season)
{regexp.MustCompile(`(?i)S(\d{1,2})-\d{1,2}`), "S%02d"},
// S01-S08 -> S01
{regexp.MustCompile(`(?i)S(\d{1,2})-S\d{1,2}`), "S%02d"},
// Season 1-8 -> Season 1
{regexp.MustCompile(`(?i)Season\.?\s*\d{1,2}-\d{1,2}`), "Season %02d"},
// Seasons 1-8 -> Season 1
{regexp.MustCompile(`(?i)Seasons\.?\s*\d{1,2}-\d{1,2}`), "Season %02d"},
// Complete Series -> Season X
{regexp.MustCompile(`(?i)Complete\.?Series`), "Season %02d"},
// All Seasons -> Season X
{regexp.MustCompile(`(?i)All\.?Seasons?`), "Season %02d"},
}
// Also pre-compile other patterns
seasonPattern = regexp.MustCompile(`(?i)(?:season\.?\s*|s)(\d{1,2})`)
qualityIndicators = regexp.MustCompile(`(?i)\b(2160p|1080p|720p|BluRay|WEB-DL|HDTV|x264|x265|HEVC)`)
multiSeasonIndicators = []*regexp.Regexp{
regexp.MustCompile(`(?i)complete\.?series`),
regexp.MustCompile(`(?i)all\.?seasons?`),
regexp.MustCompile(`(?i)season\.?\s*\d+\s*-\s*\d+`),
regexp.MustCompile(`(?i)s\d+\s*-\s*s?\d+`),
regexp.MustCompile(`(?i)seasons?\s*\d+\s*-\s*\d+`),
}
)
type multiSeasonPattern struct {
pattern *regexp.Regexp
replacement string
}
type SeasonInfo struct {
SeasonNumber int
Files []types.File
InfoHash string
Name string
}
func (s *Store) replaceMultiSeasonPattern(name string, targetSeason int) string {
result := name
// Apply each pre-compiled pattern replacement
for _, msp := range multiSeasonReplacements {
if msp.pattern.MatchString(result) {
replacement := fmt.Sprintf(msp.replacement, targetSeason)
result = msp.pattern.ReplaceAllString(result, replacement)
s.logger.Debug().Msgf("Applied pattern replacement: %s -> %s", name, result)
return result
}
}
// If no multi-season pattern found, try to insert season info intelligently
return s.insertSeasonIntoName(result, targetSeason)
}
func (s *Store) insertSeasonIntoName(name string, seasonNum int) string {
// Check if season info already exists
if seasonPattern.MatchString(name) {
return name // Already has season info, keep as is
}
// Try to find a good insertion point (before quality indicators)
if loc := qualityIndicators.FindStringIndex(name); loc != nil {
// Insert season before quality info
before := strings.TrimSpace(name[:loc[0]])
after := name[loc[0]:]
return fmt.Sprintf("%s S%02d %s", before, seasonNum, after)
}
// If no quality indicators found, append at the end
return fmt.Sprintf("%s S%02d", name, seasonNum)
}
func (s *Store) detectMultiSeason(debridTorrent *types.Torrent) (bool, []SeasonInfo, error) {
torrentName := debridTorrent.Name
files := debridTorrent.GetFiles()
s.logger.Debug().Msgf("Analyzing torrent for multi-season: %s", torrentName)
// Find all seasons present in the files
seasonsFound := s.findAllSeasons(files)
// Check if this is actually a multi-season torrent
isMultiSeason := len(seasonsFound) > 1 || s.hasMultiSeasonIndicators(torrentName)
if !isMultiSeason {
return false, nil, nil
}
s.logger.Info().Msgf("Multi-season torrent detected with seasons: %v", getSortedSeasons(seasonsFound))
// Group files by season
seasonGroups := s.groupFilesBySeason(files, seasonsFound)
// Create SeasonInfo objects with proper naming
var seasons []SeasonInfo
for seasonNum, seasonFiles := range seasonGroups {
if len(seasonFiles) == 0 {
continue
}
// Generate season-specific name preserving all metadata
seasonName := s.generateSeasonSpecificName(torrentName, seasonNum)
seasons = append(seasons, SeasonInfo{
SeasonNumber: seasonNum,
Files: seasonFiles,
InfoHash: s.generateSeasonHash(debridTorrent.InfoHash, seasonNum),
Name: seasonName,
})
}
return true, seasons, nil
}
// generateSeasonSpecificName creates season name preserving all original metadata
func (s *Store) generateSeasonSpecificName(originalName string, seasonNum int) string {
// Find and replace the multi-season pattern with single season
seasonName := s.replaceMultiSeasonPattern(originalName, seasonNum)
s.logger.Debug().Msgf("Generated season name for S%02d: %s", seasonNum, seasonName)
return seasonName
}
func (s *Store) findAllSeasons(files []types.File) map[int]bool {
seasons := make(map[int]bool)
for _, file := range files {
// Check filename first
if season := s.extractSeason(file.Name); season > 0 {
seasons[season] = true
continue
}
// Check full path
if season := s.extractSeason(file.Path); season > 0 {
seasons[season] = true
}
}
return seasons
}
// extractSeason pulls season number from a string
func (s *Store) extractSeason(text string) int {
matches := seasonPattern.FindStringSubmatch(text)
if len(matches) > 1 {
if num, err := strconv.Atoi(matches[1]); err == nil && num > 0 && num < 100 {
return num
}
}
return 0
}
func (s *Store) hasMultiSeasonIndicators(torrentName string) bool {
for _, pattern := range multiSeasonIndicators {
if pattern.MatchString(torrentName) {
return true
}
}
return false
}
// groupFilesBySeason puts files into season buckets
func (s *Store) groupFilesBySeason(files []types.File, knownSeasons map[int]bool) map[int][]types.File {
groups := make(map[int][]types.File)
// Initialize groups
for season := range knownSeasons {
groups[season] = []types.File{}
}
for _, file := range files {
// Try to find season from filename or path
season := s.extractSeason(file.Name)
if season == 0 {
season = s.extractSeason(file.Path)
}
// If we found a season and it's known, add the file
if season > 0 && knownSeasons[season] {
groups[season] = append(groups[season], file)
} else {
// If no season found, try path-based inference
inferredSeason := s.inferSeasonFromPath(file.Path, knownSeasons)
if inferredSeason > 0 {
groups[inferredSeason] = append(groups[inferredSeason], file)
} else if len(knownSeasons) == 1 {
// If only one season exists, default to it
for season := range knownSeasons {
groups[season] = append(groups[season], file)
}
}
}
}
return groups
}
func (s *Store) inferSeasonFromPath(path string, knownSeasons map[int]bool) int {
pathParts := strings.Split(path, "/")
for _, part := range pathParts {
if season := s.extractSeason(part); season > 0 && knownSeasons[season] {
return season
}
}
return 0
}
// Helper to get sorted season list for logging
func getSortedSeasons(seasons map[int]bool) []int {
var result []int
for season := range seasons {
result = append(result, season)
}
return result
}
// generateSeasonHash creates a unique hash for a season based on original hash
func (s *Store) generateSeasonHash(originalHash string, seasonNumber int) string {
source := fmt.Sprintf("%s-season-%d", originalHash, seasonNumber)
hash := md5.Sum([]byte(source))
return fmt.Sprintf("%x", hash)
}
func grabber(client *grab.Client, url, filename string, byterange *[2]int64, progressCallback func(int64, int64)) error {
req, err := grab.NewRequest(filename, url)
if err != nil {
@@ -150,105 +393,21 @@ func (s *Store) downloadFiles(torrent *Torrent, debridTorrent *types.Torrent, pa
s.logger.Info().Msgf("Downloaded all files for %s", debridTorrent.Name)
}
func (s *Store) processSymlink(torrent *Torrent, debridTorrent *types.Torrent) (string, error) {
files := debridTorrent.Files
func (s *Store) processSymlink(debridTorrent *types.Torrent, torrentRclonePath, torrentSymlinkPath string) (string, error) {
files := debridTorrent.GetFiles()
if len(files) == 0 {
return "", fmt.Errorf("no valid files found")
}
s.logger.Info().Msgf("Checking symlinks for %d files...", len(files))
rCloneBase := debridTorrent.MountPath
torrentPath, err := s.getTorrentPath(rCloneBase, debridTorrent) // /MyTVShow/
// This returns filename.ext for alldebrid instead of the parent folder filename/
torrentFolder := torrentPath
if err != nil {
return "", fmt.Errorf("failed to get torrent path: %v", err)
}
// Check if the torrent path is a file
torrentRclonePath := filepath.Join(rCloneBase, torrentPath) // leave it as is
if debridTorrent.Debrid == "alldebrid" && utils.IsMediaFile(torrentPath) {
// Alldebrid hotfix for single file torrents
torrentFolder = utils.RemoveExtension(torrentFolder)
torrentRclonePath = rCloneBase // /mnt/rclone/magnets/ // Remove the filename since it's in the root folder
}
torrentSymlinkPath := filepath.Join(torrent.SavePath, torrentFolder) // /mnt/symlinks/{category}/MyTVShow/
err = os.MkdirAll(torrentSymlinkPath, os.ModePerm)
s.logger.Info().Msgf("Creating symlinks for %d files ...", len(files))
// Create symlink directory
err := os.MkdirAll(torrentSymlinkPath, os.ModePerm)
if err != nil {
return "", fmt.Errorf("failed to create directory: %s: %v", torrentSymlinkPath, err)
}
realPaths := make(map[string]string)
err = filepath.WalkDir(torrentRclonePath, func(path string, d os.DirEntry, err error) error {
if err != nil {
return nil
}
if !d.IsDir() {
filename := d.Name()
rel, _ := filepath.Rel(torrentRclonePath, path)
realPaths[filename] = rel
}
return nil
})
if err != nil {
s.logger.Warn().Msgf("Error while scanning rclone path: %v", err)
}
pending := make(map[string]types.File)
for _, file := range files {
if realRelPath, ok := realPaths[file.Name]; ok {
file.Path = realRelPath
}
pending[file.Path] = file
}
ticker := time.NewTicker(200 * time.Millisecond)
defer ticker.Stop()
timeout := time.After(30 * time.Minute)
filePaths := make([]string, 0, len(pending))
for len(pending) > 0 {
select {
case <-ticker.C:
for path, file := range pending {
fullFilePath := filepath.Join(torrentRclonePath, file.Path)
if _, err := os.Stat(fullFilePath); !os.IsNotExist(err) {
fileSymlinkPath := filepath.Join(torrentSymlinkPath, file.Name)
if err := os.Symlink(fullFilePath, fileSymlinkPath); err != nil && !os.IsExist(err) {
s.logger.Warn().Msgf("Failed to create symlink: %s: %v", fileSymlinkPath, err)
} else {
filePaths = append(filePaths, fileSymlinkPath)
delete(pending, path)
s.logger.Info().Msgf("File is ready: %s", file.Name)
}
}
}
case <-timeout:
s.logger.Warn().Msgf("Timeout waiting for files, %d files still pending", len(pending))
return torrentSymlinkPath, fmt.Errorf("timeout waiting for files: %d files still pending", len(pending))
}
}
if s.skipPreCache {
return torrentSymlinkPath, nil
}
go func() {
s.logger.Debug().Msgf("Pre-caching %s", debridTorrent.Name)
if err := utils.PreCacheFile(filePaths); err != nil {
s.logger.Error().Msgf("Failed to pre-cache file: %s", err)
} else {
s.logger.Trace().Msgf("Pre-cached %d files", len(filePaths))
}
}()
return torrentSymlinkPath, nil
}
func (s *Store) createSymlinksWebdav(torrent *Torrent, debridTorrent *types.Torrent, rclonePath, torrentFolder string) (string, error) {
files := debridTorrent.Files
symlinkPath := filepath.Join(torrent.SavePath, torrentFolder) // /mnt/symlinks/{category}/MyTVShow/
err := os.MkdirAll(symlinkPath, os.ModePerm)
if err != nil {
return "", fmt.Errorf("failed to create directory: %s: %v", symlinkPath, err)
}
// Track pending files
remainingFiles := make(map[string]types.File)
for _, file := range files {
remainingFiles[file.Name] = file
@@ -257,61 +416,209 @@ func (s *Store) createSymlinksWebdav(torrent *Torrent, debridTorrent *types.Torr
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
timeout := time.After(30 * time.Minute)
filePaths := make([]string, 0, len(files))
filePaths := make([]string, 0, len(remainingFiles))
var checkDirectory func(string) // Recursive function
checkDirectory = func(dirPath string) {
entries, err := os.ReadDir(dirPath)
if err != nil {
return
}
for _, entry := range entries {
entryName := entry.Name()
fullPath := filepath.Join(dirPath, entryName)
// Check if this matches a remaining file
if file, exists := remainingFiles[entryName]; exists {
fileSymlinkPath := filepath.Join(torrentSymlinkPath, file.Name)
if err := os.Symlink(fullPath, fileSymlinkPath); err == nil || os.IsExist(err) {
filePaths = append(filePaths, fileSymlinkPath)
delete(remainingFiles, entryName)
s.logger.Info().Msgf("File is ready: %s", file.Name)
}
} else if entry.IsDir() {
// If not found and it's a directory, check inside
checkDirectory(fullPath)
}
}
}
for len(remainingFiles) > 0 {
select {
case <-ticker.C:
entries, err := os.ReadDir(rclonePath)
if err != nil {
continue
}
// Check which files exist in this batch
for _, entry := range entries {
filename := entry.Name()
if file, exists := remainingFiles[filename]; exists {
fullFilePath := filepath.Join(rclonePath, filename)
fileSymlinkPath := filepath.Join(symlinkPath, file.Name)
if err := os.Symlink(fullFilePath, fileSymlinkPath); err != nil && !os.IsExist(err) {
s.logger.Debug().Msgf("Failed to create symlink: %s: %v", fileSymlinkPath, err)
} else {
filePaths = append(filePaths, fileSymlinkPath)
delete(remainingFiles, filename)
s.logger.Info().Msgf("File is ready: %s", file.Name)
}
}
}
checkDirectory(torrentRclonePath)
case <-timeout:
s.logger.Warn().Msgf("Timeout waiting for files, %d files still pending", len(remainingFiles))
return symlinkPath, fmt.Errorf("timeout waiting for files")
return torrentSymlinkPath, fmt.Errorf("timeout waiting for files: %d files still pending", len(remainingFiles))
}
}
if s.skipPreCache {
return symlinkPath, nil
// Pre-cache files if enabled
if !s.skipPreCache && len(filePaths) > 0 {
go func() {
s.logger.Debug().Msgf("Pre-caching %s", debridTorrent.Name)
if err := utils.PreCacheFile(filePaths); err != nil {
s.logger.Error().Msgf("Failed to pre-cache file: %s", err)
} else {
s.logger.Debug().Msgf("Pre-cached %d files", len(filePaths))
}
}()
}
go func() {
s.logger.Debug().Msgf("Pre-caching %s", debridTorrent.Name)
if err := utils.PreCacheFile(filePaths); err != nil {
s.logger.Error().Msgf("Failed to pre-cache file: %s", err)
} else {
s.logger.Debug().Msgf("Pre-cached %d files", len(filePaths))
}
}() // Pre-cache the files in the background
// Pre-cache the first 256KB and 1MB of the file
return symlinkPath, nil
return torrentSymlinkPath, nil
}
func (s *Store) getTorrentPath(rclonePath string, debridTorrent *types.Torrent) (string, error) {
// getTorrentPaths returns mountPath and symlinkPath for a torrent
func (s *Store) getTorrentPaths(arrFolder string, debridTorrent *types.Torrent) (string, string, error) {
for {
torrentPath, err := debridTorrent.GetMountFolder(rclonePath)
torrentFolder, err := debridTorrent.GetMountFolder(debridTorrent.MountPath)
if err == nil {
return torrentPath, err
// Found mountPath
mountPath := filepath.Join(debridTorrent.MountPath, torrentFolder)
if debridTorrent.Debrid == "alldebrid" && utils.IsMediaFile(torrentFolder) {
torrentFolder = utils.RemoveExtension(torrentFolder)
mountPath = debridTorrent.MountPath
}
// Return mountPath and symlink path
return mountPath, filepath.Join(arrFolder, torrentFolder), nil
}
time.Sleep(100 * time.Millisecond)
}
}
func (s *Store) processMultiSeasonSymlinks(torrent *Torrent, debridTorrent *types.Torrent, seasons []SeasonInfo, importReq *ImportRequest) error {
for _, seasonInfo := range seasons {
// Create a season-specific debrid torrent
seasonDebridTorrent := debridTorrent.Copy()
// Update the season torrent with season-specific data
seasonDebridTorrent.InfoHash = seasonInfo.InfoHash
seasonDebridTorrent.Name = seasonInfo.Name
seasonTorrent := torrent.Copy()
seasonTorrent.ID = seasonInfo.InfoHash
seasonTorrent.Name = seasonInfo.Name
seasonTorrent.Hash = seasonInfo.InfoHash
torrentFiles := make([]*File, 0)
size := int64(0)
// Filter files to only include this season's files
seasonFiles := make(map[string]types.File)
for index, file := range seasonInfo.Files {
seasonFiles[file.Name] = file
torrentFiles = append(torrentFiles, &File{
Index: index,
Name: file.Path,
Size: file.Size,
})
size += file.Size
}
seasonDebridTorrent.Files = seasonFiles
seasonTorrent.Files = torrentFiles
seasonTorrent.Size = size
// Create a season-specific torrent record
// Create season folder path using the extracted season name
seasonFolderName := seasonInfo.Name
s.logger.Info().Msgf("Processing season %s with %d files", seasonTorrent.Name, len(seasonInfo.Files))
var err error
cache := s.debrid.Debrid(debridTorrent.Debrid).Cache()
var torrentRclonePath, torrentSymlinkPath string
if cache != nil {
torrentRclonePath = filepath.Join(debridTorrent.MountPath, cache.GetTorrentFolder(debridTorrent))
} else {
// Regular mount mode
torrentRclonePath, _, err = s.getTorrentPaths(seasonTorrent.SavePath, seasonDebridTorrent)
if err != nil {
return err
}
}
torrentSymlinkPath = filepath.Join(seasonTorrent.SavePath, seasonFolderName)
torrentSymlinkPath, err = s.processSymlink(seasonDebridTorrent, torrentRclonePath, torrentSymlinkPath)
if err != nil {
return err
}
if torrentSymlinkPath == "" {
return fmt.Errorf("no symlink found for season %d", seasonInfo.SeasonNumber)
}
// Update season torrent with final path
seasonTorrent.TorrentPath = torrentSymlinkPath
seasonTorrent.ContentPath = torrentSymlinkPath
seasonTorrent.State = "pausedUP"
// Add the season torrent to storage
s.torrents.AddOrUpdate(seasonTorrent)
s.logger.Info().Str("path", torrentSymlinkPath).Msgf("Successfully created season %d torrent: %s", seasonInfo.SeasonNumber, seasonTorrent.ID)
}
s.torrents.Delete(torrent.Hash, "", false)
s.logger.Info().Msgf("Multi-season processing completed for %s", debridTorrent.Name)
return nil
}
// processMultiSeasonDownloads handles multi-season torrent downloading
func (s *Store) processMultiSeasonDownloads(torrent *Torrent, debridTorrent *types.Torrent, seasons []SeasonInfo, importReq *ImportRequest) error {
s.logger.Info().Msgf("Creating separate download records for %d seasons", len(seasons))
for _, seasonInfo := range seasons {
// Create a season-specific debrid torrent
seasonDebridTorrent := debridTorrent.Copy()
// Update the season torrent with season-specific data
seasonDebridTorrent.InfoHash = seasonInfo.InfoHash
seasonDebridTorrent.Name = seasonInfo.Name
// Filter files to only include this season's files
seasonFiles := make(map[string]types.File)
for _, file := range seasonInfo.Files {
seasonFiles[file.Name] = file
}
seasonDebridTorrent.Files = seasonFiles
// Create a season-specific torrent record
seasonTorrent := torrent.Copy()
seasonTorrent.ID = uuid.New().String()
seasonTorrent.Name = seasonInfo.Name
seasonTorrent.Hash = seasonInfo.InfoHash
seasonTorrent.SavePath = torrent.SavePath
s.logger.Info().Msgf("Downloading season %d with %d files", seasonInfo.SeasonNumber, len(seasonInfo.Files))
// Generate download links for season files
client := s.debrid.Debrid(debridTorrent.Debrid).Client()
if err := client.GetFileDownloadLinks(seasonDebridTorrent); err != nil {
s.logger.Error().Msgf("Failed to get download links for season %d: %v", seasonInfo.SeasonNumber, err)
return fmt.Errorf("failed to get download links for season %d: %v", seasonInfo.SeasonNumber, err)
}
// Download files for this season
seasonDownloadPath, err := s.processDownload(seasonTorrent, seasonDebridTorrent)
if err != nil {
s.logger.Error().Msgf("Failed to download season %d: %v", seasonInfo.SeasonNumber, err)
return fmt.Errorf("failed to download season %d: %v", seasonInfo.SeasonNumber, err)
}
// Update season torrent with final path
seasonTorrent.TorrentPath = seasonDownloadPath
torrent.ContentPath = seasonDownloadPath
seasonTorrent.State = "pausedUP"
// Add the season torrent to storage
s.torrents.AddOrUpdate(seasonTorrent)
s.logger.Info().Msgf("Successfully downloaded season %d torrent: %s", seasonInfo.SeasonNumber, seasonTorrent.ID)
}
s.logger.Debug().Msgf("Deleting original torrent with hash: %s, category: %s", torrent.Hash, torrent.Category)
s.torrents.Delete(torrent.Hash, torrent.Category, false)
s.logger.Info().Msgf("Multi-season download processing completed for %s", debridTorrent.Name)
return nil
}
+59 -9
View File
@@ -6,7 +6,6 @@ import (
"errors"
"fmt"
"math"
"os"
"path/filepath"
"time"
@@ -99,8 +98,7 @@ func (s *Store) processFiles(torrent *Torrent, debridTorrent *types.Torrent, imp
backoff.Reset(nextInterval)
}
}
var torrentSymlinkPath string
var err error
var torrentSymlinkPath, torrentRclonePath string
debridTorrent.Arr = _arr
// Check if debrid supports webdav by checking cache
@@ -134,11 +132,20 @@ func (s *Store) processFiles(torrent *Torrent, debridTorrent *types.Torrent, imp
}()
}
// Check for multi-season torrent support
isMultiSeason, seasons, err := s.detectMultiSeason(debridTorrent)
if err != nil {
s.logger.Warn().Msgf("Error detecting multi-season for %s: %v", debridTorrent.Name, err)
// Continue with normal processing if detection fails
isMultiSeason = false
}
switch importReq.Action {
case "symlink":
// Symlink action, we will create a symlink to the torrent
s.logger.Debug().Msgf("Post-Download Action: Symlink")
cache := deb.Cache()
if cache != nil {
s.logger.Info().Msgf("Using internal webdav for %s", debridTorrent.Debrid)
// Use webdav to download the file
@@ -146,14 +153,45 @@ func (s *Store) processFiles(torrent *Torrent, debridTorrent *types.Torrent, imp
onFailed(err)
return
}
}
if isMultiSeason {
s.logger.Info().Msgf("Processing multi-season torrent with %d seasons", len(seasons))
// Remove any torrent already added
err := s.processMultiSeasonSymlinks(torrent, debridTorrent, seasons, importReq)
if err == nil {
// If an error occurred during multi-season processing, send it to normal processing
s.logger.Info().Msgf("Adding %s took %s", debridTorrent.Name, time.Since(timer))
go importReq.markAsCompleted(torrent, debridTorrent) // Mark the import request as completed, send callback if needed
go func() {
if err := request.SendDiscordMessage("download_complete", "success", torrent.discordContext()); err != nil {
s.logger.Error().Msgf("Error sending discord message: %v", err)
}
}()
go func() {
_arr.Refresh()
}()
return
}
}
if cache != nil {
torrentRclonePath = filepath.Join(debridTorrent.MountPath, cache.GetTorrentFolder(debridTorrent)) // /mnt/remote/realdebrid/MyTVShow
torrentSymlinkPath = filepath.Join(torrent.SavePath, utils.RemoveExtension(debridTorrent.Name)) // /mnt/symlinks/{category}/MyTVShow/
rclonePath := filepath.Join(debridTorrent.MountPath, cache.GetTorrentFolder(debridTorrent)) // /mnt/remote/realdebrid/MyTVShow
torrentFolderNoExt := utils.RemoveExtension(debridTorrent.Name)
torrentSymlinkPath, err = s.createSymlinksWebdav(torrent, debridTorrent, rclonePath, torrentFolderNoExt) // /mnt/symlinks/{category}/MyTVShow/
} else {
// User is using either zurg or debrid webdav
torrentSymlinkPath, err = s.processSymlink(torrent, debridTorrent) // /mnt/symlinks/{category}/MyTVShow/
torrentRclonePath, torrentSymlinkPath, err = s.getTorrentPaths(torrent.SavePath, debridTorrent)
if err != nil {
onFailed(err)
return
}
}
torrentSymlinkPath, err = s.processSymlink(debridTorrent, torrentRclonePath, torrentSymlinkPath)
if err != nil {
onFailed(err)
return
@@ -168,6 +206,19 @@ func (s *Store) processFiles(torrent *Torrent, debridTorrent *types.Torrent, imp
// Download action, we will download the torrent to the specified folder
// Generate download links
s.logger.Debug().Msgf("Post-Download Action: Download")
if isMultiSeason {
s.logger.Info().Msgf("Processing multi-season download with %d seasons", len(seasons))
err := s.processMultiSeasonDownloads(torrent, debridTorrent, seasons, importReq)
if err != nil {
onFailed(err)
return
}
// Multi-season processing completed successfully
onSuccess(torrent.SavePath)
return
}
if err := client.GetFileDownloadLinks(debridTorrent); err != nil {
onFailed(err)
return
@@ -252,7 +303,6 @@ func (s *Store) partialTorrentUpdate(t *Torrent, debridTorrent *types.Torrent) *
t.Eta = eta
t.Dlspeed = speed
t.Upspeed = speed
t.ContentPath = filepath.Join(t.SavePath, t.Name) + string(os.PathSeparator)
return t
}
@@ -267,7 +317,7 @@ func (s *Store) updateTorrent(t *Torrent, debridTorrent *types.Torrent) *Torrent
}
}
t = s.partialTorrentUpdate(t, debridTorrent)
t.ContentPath = t.TorrentPath + string(os.PathSeparator)
t.ContentPath = t.TorrentPath
if t.IsReady() {
t.State = "pausedUP"
+30 -41
View File
@@ -167,44 +167,33 @@ func (ts *TorrentStorage) Update(torrent *Torrent) {
func (ts *TorrentStorage) Delete(hash, category string, removeFromDebrid bool) {
ts.mu.Lock()
defer ts.mu.Unlock()
key := keyPair(hash, category)
torrent, exists := ts.torrents[key]
if !exists && category == "" {
// Remove the torrent without knowing the category
for k, t := range ts.torrents {
if t.Hash == hash {
key = k
torrent = t
break
wireStore := Get()
for key, torrent := range ts.torrents {
if torrent == nil {
continue
}
if torrent.Hash == hash && (category == "" || torrent.Category == category) {
if torrent.State == "queued" && torrent.ID != "" {
// Remove the torrent from the import queue if it exists
wireStore.importsQueue.Delete(torrent.ID)
}
}
}
if removeFromDebrid && torrent.DebridID != "" && torrent.Debrid != "" {
dbClient := wireStore.debrid.Client(torrent.Debrid)
if dbClient != nil {
_ = dbClient.DeleteTorrent(torrent.DebridID)
}
}
delete(ts.torrents, key)
if torrent == nil {
return
}
st := Get()
// Check if torrent is queued for download
if torrent.State == "queued" && torrent.ID != "" {
// Remove the torrent from the import queue if it exists
st.importsQueue.Delete(torrent.ID)
}
if removeFromDebrid && torrent.DebridID != "" && torrent.Debrid != "" {
dbClient := st.debrid.Client(torrent.Debrid)
if dbClient != nil {
_ = dbClient.DeleteTorrent(torrent.DebridID)
}
}
delete(ts.torrents, key)
// Delete the torrent folder
if torrent.ContentPath != "" {
err := os.RemoveAll(torrent.ContentPath)
if err != nil {
return
// Delete the torrent folder
if torrent.ContentPath != "" {
err := os.RemoveAll(torrent.ContentPath)
if err != nil {
return
}
}
break
}
}
go func() {
@@ -227,12 +216,11 @@ func (ts *TorrentStorage) DeleteMultiple(hashes []string, removeFromDebrid bool)
if torrent == nil {
continue
}
if torrent.State == "queued" && torrent.ID != "" {
// Remove the torrent from the import queue if it exists
st.importsQueue.Delete(torrent.ID)
}
if torrent.Hash == hash {
if torrent.State == "queued" && torrent.ID != "" {
// Remove the torrent from the import queue if it exists
st.importsQueue.Delete(torrent.ID)
}
if removeFromDebrid && torrent.DebridID != "" && torrent.Debrid != "" {
toDelete[torrent.DebridID] = torrent.Debrid
}
@@ -243,6 +231,7 @@ func (ts *TorrentStorage) DeleteMultiple(hashes []string, removeFromDebrid bool)
return
}
}
break
}
}
}
+54
View File
@@ -72,6 +72,60 @@ type Torrent struct {
sync.Mutex
}
func (t *Torrent) Copy() *Torrent {
return &Torrent{
ID: t.ID,
DebridID: t.DebridID,
Debrid: t.Debrid,
TorrentPath: t.TorrentPath,
AddedOn: t.AddedOn,
AmountLeft: t.AmountLeft,
AutoTmm: t.AutoTmm,
Availability: t.Availability,
Category: t.Category,
Completed: t.Completed,
CompletionOn: t.CompletionOn,
ContentPath: t.ContentPath,
DlLimit: t.DlLimit,
Dlspeed: t.Dlspeed,
Downloaded: t.Downloaded,
DownloadedSession: t.DownloadedSession,
Eta: t.Eta,
FlPiecePrio: t.FlPiecePrio,
ForceStart: t.ForceStart,
Hash: t.Hash,
LastActivity: t.LastActivity,
MagnetUri: t.MagnetUri,
MaxRatio: t.MaxRatio,
MaxSeedingTime: t.MaxSeedingTime,
Name: t.Name,
NumComplete: t.NumComplete,
NumIncomplete: t.NumIncomplete,
NumLeechs: t.NumLeechs,
NumSeeds: t.NumSeeds,
Priority: t.Priority,
Progress: t.Progress,
Ratio: t.Ratio,
RatioLimit: t.RatioLimit,
SavePath: t.SavePath,
SeedingTimeLimit: t.SeedingTimeLimit,
SeenComplete: t.SeenComplete,
SeqDl: t.SeqDl,
Size: t.Size,
State: t.State,
SuperSeeding: t.SuperSeeding,
Tags: t.Tags,
TimeActive: t.TimeActive,
TotalSize: t.TotalSize,
Tracker: t.Tracker,
UpLimit: t.UpLimit,
Uploaded: t.Uploaded,
UploadedSession: t.UploadedSession,
Upspeed: t.Upspeed,
Source: t.Source,
}
}
func (t *Torrent) IsReady() bool {
return (t.AmountLeft <= 0 || t.Progress == 1) && t.TorrentPath != ""
}