Skip to content
Merged

. #60

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions internal/arrs/scanner/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -445,8 +445,6 @@ func (m *Manager) triggerSonarrRescanByPath(ctx context.Context, client *sonarr.
libraryDir := ""
if cfg.Health.LibraryDir != nil && *cfg.Health.LibraryDir != "" {
libraryDir = *cfg.Health.LibraryDir
} else {
return fmt.Errorf("Health.LibraryDir is not configured")
}

slog.DebugContext(ctx, "Triggering Sonarr rescan/re-download by path",
Expand Down
12 changes: 0 additions & 12 deletions internal/errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,18 +64,6 @@ func IsNonRetryable(err error) bool {

// Sentinel errors for common non-retryable conditions.
var (
// ErrNoRetryable is kept for backward compatibility with existing code.
ErrNoRetryable = &NonRetryableError{
message: "no retryable errors found",
cause: nil,
}

// ErrNoVideoFiles indicates that an import contains no video files.
ErrNoVideoFiles = &NonRetryableError{
message: "import contains no video files",
cause: nil,
}

// ErrFallbackNotConfigured indicates that SABnzbd fallback is not enabled or configured.
ErrFallbackNotConfigured = &NonRetryableError{
message: "SABnzbd fallback not configured",
Expand Down
9 changes: 1 addition & 8 deletions internal/importer/archive/common.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package archive

import "strconv"

// parseInt safely converts string to int
// ParseInt safely converts string to int
func ParseInt(s string) int {
num := 0
for _, r := range s {
Expand All @@ -14,8 +12,3 @@ func ParseInt(s string) int {
}
return num
}

// FormatInt converts an integer to a string
func FormatInt(n int) string {
return strconv.Itoa(n)
}
6 changes: 0 additions & 6 deletions internal/importer/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,6 @@ var (
// IsNonRetryable checks if an error is non-retryable.
IsNonRetryable = sharedErrors.IsNonRetryable

// ErrNoRetryable is kept for backward compatibility with existing code.
ErrNoRetryable = sharedErrors.ErrNoRetryable

// ErrNoVideoFiles indicates that an import contains no video files.
ErrNoVideoFiles = sharedErrors.ErrNoVideoFiles

// ErrFallbackNotConfigured indicates that SABnzbd fallback is not enabled or configured.
ErrFallbackNotConfigured = sharedErrors.ErrFallbackNotConfigured
)
8 changes: 0 additions & 8 deletions internal/importer/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,6 @@ type QueueManager interface {
IsPaused() bool
// IsRunning returns whether the queue manager is active
IsRunning() bool
// GetWorkerCount returns the current number of workers
GetWorkerCount() int
// UpdateWorkerCount changes the number of active workers
UpdateWorkerCount(count int) error
// CancelProcessing cancels processing of a specific item
CancelProcessing(itemID int64) error
// ProcessItemInBackground starts processing a specific item in the background
Expand Down Expand Up @@ -56,8 +52,6 @@ type QueueOperations interface {
AddToQueue(ctx context.Context, filePath string, relativePath *string, category *string, priority *database.QueuePriority) (*database.ImportQueueItem, error)
// GetQueueStats returns queue statistics
GetQueueStats(ctx context.Context) (*database.QueueStats, error)
// GetStats returns comprehensive service statistics
GetStats(ctx context.Context) (*ServiceStats, error)
}

// SymlinkCreator handles symlink creation for imported files
Expand Down Expand Up @@ -127,8 +121,6 @@ type ImportService interface {
NzbDavImporter
QueueOperations

// Database returns the underlying database connection
Database() *database.DB
// Close releases all resources
Close() error
// SetRcloneClient sets the rclone client for VFS notifications
Expand Down
7 changes: 0 additions & 7 deletions internal/importer/queue/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,13 +162,6 @@ func (m *Manager) IsRunning() bool {
return m.running
}

// GetWorkerCount returns the current worker count
func (m *Manager) GetWorkerCount() int {
m.mu.RLock()
defer m.mu.RUnlock()
return m.config.Workers
}

// CancelProcessing cancels processing for a specific item
func (m *Manager) CancelProcessing(itemID int64) error {
m.cancelMu.RLock()
Expand Down
61 changes: 0 additions & 61 deletions internal/importer/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,6 @@ type Service struct {
paused bool
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup

// Cancellation tracking for processing items
cancelFuncs map[int64]context.CancelFunc
Expand Down Expand Up @@ -401,11 +400,6 @@ func (s *Service) SetArrsService(service *arrs.Service) {
}
}

// Database returns the database instance for processing
func (s *Service) Database() *database.DB {
return s.database
}

// GetQueueStats returns current queue statistics from database
func (s *Service) GetQueueStats(ctx context.Context) (*database.QueueStats, error) {
return s.database.Repository.GetQueueStats(ctx)
Expand Down Expand Up @@ -744,61 +738,6 @@ func (s *Service) handleProcessingFailure(ctx context.Context, item *database.Im
}
}


// ServiceStats holds statistics about the service
type ServiceStats struct {
IsRunning bool `json:"is_running"`
Workers int `json:"workers"`
QueueStats *database.QueueStats `json:"queue_stats,omitempty"`
ScanInfo ScanInfo `json:"scan_info"`
}

// GetStats returns service statistics
func (s *Service) GetStats(ctx context.Context) (*ServiceStats, error) {
stats := &ServiceStats{
IsRunning: s.IsRunning(),
Workers: s.config.Workers,
ScanInfo: s.GetScanStatus(),
}

// Add queue statistics
queueStats, err := s.GetQueueStats(ctx)
if err != nil {
s.log.WarnContext(ctx, "Failed to get queue stats", "error", err)
} else {
stats.QueueStats = queueStats
}

return stats, nil
}

// UpdateWorkerCount updates the worker count configuration (requires service restart to take effect)
// Dynamic worker scaling is not supported - changes only apply on next service restart
func (s *Service) UpdateWorkerCount(count int) error {
if count <= 0 {
return fmt.Errorf("worker count must be greater than 0")
}

s.mu.Lock()
defer s.mu.Unlock()

s.log.InfoContext(s.ctx, "Queue worker count update requested - restart required to take effect",
"current_count", s.config.Workers,
"requested_count", count,
"running", s.running)

// Configuration update is handled at the config manager level
// Changes only take effect on service restart
return nil
}

// GetWorkerCount returns the current configured worker count
func (s *Service) GetWorkerCount() int {
s.mu.RLock()
defer s.mu.RUnlock()
return s.config.Workers
}

// CancelProcessing cancels a processing queue item by cancelling its context
func (s *Service) CancelProcessing(itemID int64) error {
return s.queueManager.CancelProcessing(itemID)
Expand Down
114 changes: 0 additions & 114 deletions internal/importer/utils/file_extensions.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
package utils

import (
"bufio"
"os"
"path/filepath"
"regexp"
"slices"
"strings"

"github.com/gabriel-vasile/mimetype"
)

// This file provides helpers translated from https://github.com/sabnzbd/sabnzbd/blob/develop/sabnzbd/utils/file_extension.py for detecting
Expand Down Expand Up @@ -75,37 +71,6 @@ var allExt = func() []string {
return out
}()

// AllExtensions returns the combined list of known extensions (dot-prefixed).
// If you need to add user-defined extensions, pass them to AllExtensionsWith.
func AllExtensions() []string { return allExt }

// AllExtensionsWith returns combined list plus user-defined dot- or non-dot-prefixed extensions.
func AllExtensionsWith(extra []string) []string {
if len(extra) == 0 {
return allExt
}
set := map[string]struct{}{}
for _, e := range allExt {
set[e] = struct{}{}
}
for _, e := range extra {
e = strings.ToLower(e)
if e == "" {
continue
}
if !strings.HasPrefix(e, ".") {
e = "." + e
}
set[e] = struct{}{}
}
out := make([]string, 0, len(set))
for k := range set {
out = append(out, k)
}
slices.Sort(out)
return out
}

// HasPopularExtension reports whether file_path has a popular extension (case-insensitive)
// or matches known RAR or 7zip patterns (e.g., .rar, .r00, .partXX.rar, .7z, .7z.001).
func HasPopularExtension(filePath string) bool {
Expand All @@ -123,82 +88,3 @@ func HasPopularExtension(filePath string) bool {
base := filepath.Base(filePath)
return rarPattern.MatchString(strings.ToLower(base)) || sevenZipPattern.MatchString(strings.ToLower(base))
}

// AllPossibleExtensions attempts to detect the file's extension(s).
// Unlike Python's puremagic (which may return multiple candidates), we
// typically have one strong match via signature-based detection.
// The returned extensions are dot-prefixed and lowercase.
func AllPossibleExtensions(filePath string) []string {
// Try MIME-based detection
if mt, err := mimetype.DetectFile(filePath); err == nil && mt != nil {
if ext := strings.ToLower(mt.Extension()); ext != "" {
return []string{ext}
}
}
return nil
}

// WhatIsMostLikelyExtension returns the most likely extension (dot-prefixed) for file_path.
// Logic mirrors the Python version:
// 1) If the start of the file is valid UTF-8 text, check for NZB clues, else return .txt
// 2) Otherwise, use signature detection and prefer a popular extension if it matches
// 3) Fallback to the first detected extension or empty string if none.
func WhatIsMostLikelyExtension(filePath string) string {
// 1) Quick text/NZB check on the first ~200 bytes
if ext, ok := sniffTextOrNZB(filePath, 200); ok {
return ext
}

// 2) signature detection
candidates := AllPossibleExtensions(filePath)
if len(candidates) == 0 {
return ""
}
// Prefer popular extension
all := allExt
for _, cand := range candidates {
if slices.Contains(all, strings.ToLower(cand)) {
return strings.ToLower(cand)
}
}
// 3) fallback to first
return strings.ToLower(candidates[0])
}

// sniffTextOrNZB reads up to n bytes and checks if it's valid UTF-8 text and
// whether it contains NZB markers. Returns (ext, true) when determined.
func sniffTextOrNZB(filePath string, n int) (string, bool) {
f, err := os.Open(filePath)
if err != nil {
return "", false
}
defer f.Close()

r := bufio.NewReader(f)
buf, _ := r.Peek(n)
// If valid UTF-8, treat as text
if !isLikelyUTF8(buf) {
return "", false
}
lower := strings.ToLower(string(buf))
if strings.Contains(lower, "!doctype nzb public") || strings.Contains(lower, "<nzb xmlns=") {
return ".nzb", true
}
return ".txt", true
}

// isLikelyUTF8 returns true if b looks like UTF-8 (simple heuristic)
func isLikelyUTF8(b []byte) bool {
// Use Go's decoder by converting to string and back
// If it contains NUL bytes or replacement characters after round-trip,
// consider it unlikely text.
s := string(b)
// If the conversion replaced invalid sequences, the resulting bytes differ
if !slices.Equal([]byte(s), b) {
return false
}
if strings.IndexByte(s, '\x00') >= 0 {
return false
}
return true
}
6 changes: 3 additions & 3 deletions internal/metadata/proto/metadata.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions internal/metadata/proto/metadata.proto
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ message FileMetadata {
repeated SegmentData segment_data = 9; // Segment information (lazy-loaded)
bytes aes_key = 10; // AES encryption key (for AES-encrypted archives)
bytes aes_iv = 11; // AES initialization vector (for AES-encrypted archives)
int64 release_date = 12; // Unix timestamp of the original Usenet post release date
repeated Par2FileReference par2_files = 13; // Associated PAR2 repair files
int64 release_date = 12; // Unix timestamp of the original Usenet post release date
repeated Par2FileReference par2_files = 13; // Associated PAR2 repair files
string nzbdav_id = 14; // ID to maintain compatibility with nzbdav
}

Loading