diff --git a/.golangci.yml b/.golangci.yml index 998b41f..2013069 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -4,6 +4,7 @@ run: timeout: 5m linters: + default: none enable: - govet - ineffassign diff --git a/internal/agent/transport_test.go b/internal/agent/transport_test.go index e2270a8..a9c7e5d 100644 --- a/internal/agent/transport_test.go +++ b/internal/agent/transport_test.go @@ -83,8 +83,8 @@ func TestWSTransportConnectAndAuth(t *testing.T) { // Send registered response conn.WriteJSON(wsRegisteredMessage{ - Type: "registered", - User: UserInfo{Name: "WS User", Plan: "pro", IsPro: true}, + Type: "registered", + User: UserInfo{Name: "WS User", Plan: "pro", IsPro: true}, Features: FeatureFlags{Torrent: true}, }) diff --git a/internal/agent/types.go b/internal/agent/types.go index 4375d9d..a5d2a81 100644 --- a/internal/agent/types.go +++ b/internal/agent/types.go @@ -62,8 +62,8 @@ type Task struct { Title string `json:"title"` ContentID *int `json:"contentId,omitempty"` IMDbID string `json:"imdbId,omitempty"` - PreferredMethod string `json:"preferredMethod"` // auto | debrid | usenet | torrent - Mode string `json:"mode,omitempty"` // download | stream + PreferredMethod string `json:"preferredMethod"` // auto | debrid | usenet | torrent + Mode string `json:"mode,omitempty"` // download | stream DirectURL string `json:"directUrl,omitempty"` // HTTPS download URL (debrid, etc.) DirectFileName string `json:"directFileName,omitempty"` // Original filename from direct URL NzbID string `json:"nzbId,omitempty"` // Pre-resolved NZB ID from server @@ -88,8 +88,8 @@ type StreamRequest struct { // StatusUpdate is sent by the CLI to report download progress. type StatusUpdate struct { TaskID string `json:"taskId"` - Status string `json:"status,omitempty"` // downloading | completed | failed - Progress int `json:"progress,omitempty"` // 0-100 + Status string `json:"status,omitempty"` // downloading | completed | failed + Progress int `json:"progress,omitempty"` // 0-100 DownloadedBytes int64 `json:"downloadedBytes,omitempty"` TotalBytes int64 `json:"totalBytes,omitempty"` SpeedBps int64 `json:"speedBps,omitempty"` @@ -249,9 +249,9 @@ type ConfigureDebridRequest struct { // ConfigureDebridResponse is returned after configuring a debrid provider. type ConfigureDebridResponse struct { - Success bool `json:"success"` + Success bool `json:"success"` Account DebridAccount `json:"account"` - Error string `json:"error,omitempty"` + Error string `json:"error,omitempty"` } // DebridAccount holds verified debrid account info. diff --git a/internal/arr/client.go b/internal/arr/client.go index 96857a6..0ab5aa0 100644 --- a/internal/arr/client.go +++ b/internal/arr/client.go @@ -11,16 +11,16 @@ import ( // Client talks to a single *arr instance (Sonarr, Radarr, or Prowlarr). type Client struct { - baseURL string - apiKey string + baseURL string + apiKey string httpClient *http.Client } // NewClient creates a client for the given *arr instance. func NewClient(baseURL, apiKey string) *Client { return &Client{ - baseURL: strings.TrimRight(baseURL, "/"), - apiKey: apiKey, + baseURL: strings.TrimRight(baseURL, "/"), + apiKey: apiKey, httpClient: &http.Client{Timeout: 15 * time.Second}, } } diff --git a/internal/arr/mapper_test.go b/internal/arr/mapper_test.go index 6c1585c..ab302e2 100644 --- a/internal/arr/mapper_test.go +++ b/internal/arr/mapper_test.go @@ -128,7 +128,7 @@ func TestExtractBlocklistedHashes(t *testing.T) { {Data: BlocklistData{InfoHash: "AAAA"}}, {Data: BlocklistData{InfoHash: "AAAA"}}, // duplicate {Data: BlocklistData{InfoHash: "BBBB"}}, - {Data: BlocklistData{InfoHash: ""}}, // empty + {Data: BlocklistData{InfoHash: ""}}, // empty } hashes := ExtractBlocklistedHashes(items) if len(hashes) != 2 { @@ -139,8 +139,8 @@ func TestExtractBlocklistedHashes(t *testing.T) { func TestExtractDownloadedHashes(t *testing.T) { records := []HistoryRecord{ {EventType: "downloadFolderImported", Data: HistoryData{InfoHash: "hash1"}}, - {EventType: "grabbed", Data: HistoryData{InfoHash: "hash2"}}, // not imported - {EventType: "downloadFolderImported", Data: HistoryData{InfoHash: "hash1"}}, // duplicate + {EventType: "grabbed", Data: HistoryData{InfoHash: "hash2"}}, // not imported + {EventType: "downloadFolderImported", Data: HistoryData{InfoHash: "hash1"}}, // duplicate {EventType: "downloadFolderImported", Data: HistoryData{InfoHash: "hash3"}}, } hashes := ExtractDownloadedHashes(records) diff --git a/internal/arr/types.go b/internal/arr/types.go index 8e341ae..e795994 100644 --- a/internal/arr/types.go +++ b/internal/arr/types.go @@ -112,11 +112,11 @@ type Tag struct { // HistoryRecord is a single entry from /api/v3/history. type HistoryRecord struct { - ID int `json:"id"` - EventType string `json:"eventType"` // "grabbed", "downloadFolderImported", etc. - DownloadID string `json:"downloadId"` - SourceTitle string `json:"sourceTitle"` - Data HistoryData `json:"data"` + ID int `json:"id"` + EventType string `json:"eventType"` // "grabbed", "downloadFolderImported", etc. + DownloadID string `json:"downloadId"` + SourceTitle string `json:"sourceTitle"` + Data HistoryData `json:"data"` } // HistoryData holds the nested data of a history record. @@ -127,14 +127,14 @@ type HistoryData struct { // HistoryResponse wraps the paginated history from *arr. type HistoryResponse struct { - Records []HistoryRecord `json:"records"` - TotalRecords int `json:"totalRecords"` + Records []HistoryRecord `json:"records"` + TotalRecords int `json:"totalRecords"` } // BlocklistItem is an item the user explicitly rejected. type BlocklistItem struct { - ID int `json:"id"` - SourceTitle string `json:"sourceTitle"` + ID int `json:"id"` + SourceTitle string `json:"sourceTitle"` Data BlocklistData `json:"data"` } @@ -145,8 +145,8 @@ type BlocklistData struct { // BlocklistResponse wraps paginated blocklist from *arr. type BlocklistResponse struct { - Records []BlocklistItem `json:"records"` - TotalRecords int `json:"totalRecords"` + Records []BlocklistItem `json:"records"` + TotalRecords int `json:"totalRecords"` } // Instance represents a discovered *arr application. diff --git a/internal/cmd/clean.go b/internal/cmd/clean.go index 18e7af0..19c5a5f 100644 --- a/internal/cmd/clean.go +++ b/internal/cmd/clean.go @@ -341,4 +341,3 @@ func CleanableBytes() int64 { return total } - diff --git a/internal/cmd/clean_test.go b/internal/cmd/clean_test.go index 0d918c6..a8a712e 100644 --- a/internal/cmd/clean_test.go +++ b/internal/cmd/clean_test.go @@ -55,7 +55,6 @@ func TestFileSize_NonExistent(t *testing.T) { } } - func TestRunClean_DryRun(t *testing.T) { err := runClean(true, false, false) if err != nil { diff --git a/internal/cmd/config_menu.go b/internal/cmd/config_menu.go index a338066..07297f7 100644 --- a/internal/cmd/config_menu.go +++ b/internal/cmd/config_menu.go @@ -18,8 +18,8 @@ var configCategories = []string{"downloads", "organization", "notifications", "d func newConfigCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "config [category]", - Short: "Edit settings interactively", + Use: "config [category]", + Short: "Edit settings interactively", Long: `Edit unarr settings interactively with a category-based menu. Categories: diff --git a/internal/cmd/daemon_install.go b/internal/cmd/daemon_install.go index 6de10c4..5087a20 100644 --- a/internal/cmd/daemon_install.go +++ b/internal/cmd/daemon_install.go @@ -236,4 +236,3 @@ func runDaemonUninstall() error { fmt.Println() return nil } - diff --git a/internal/cmd/root.go b/internal/cmd/root.go index 712038e..bd8f734 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -6,9 +6,9 @@ import ( "github.com/fatih/color" "github.com/spf13/cobra" + tc "github.com/torrentclaw/go-client" "github.com/torrentclaw/unarr/internal/config" "github.com/torrentclaw/unarr/internal/sentry" - tc "github.com/torrentclaw/go-client" ) var ( diff --git a/internal/cmd/stream_handler.go b/internal/cmd/stream_handler.go index bd2081f..9bb9657 100644 --- a/internal/cmd/stream_handler.go +++ b/internal/cmd/stream_handler.go @@ -155,4 +155,3 @@ func handleStreamTask(parentCtx context.Context, at agent.Task, reporter *engine } } } - diff --git a/internal/engine/stream.go b/internal/engine/stream.go index ddf9b00..aa69e43 100644 --- a/internal/engine/stream.go +++ b/internal/engine/stream.go @@ -14,8 +14,6 @@ import ( "github.com/anacrolix/torrent" ) - - // StreamConfig holds settings for the streaming engine. type StreamConfig struct { DataDir string @@ -30,7 +28,7 @@ type StreamConfig struct { type StreamStatus int const ( - StreamStatusMetadata StreamStatus = iota + StreamStatusMetadata StreamStatus = iota StreamStatusBuffering StreamStatusReady StreamStatusError diff --git a/internal/engine/stream_test.go b/internal/engine/stream_test.go index eb8a541..8357a5a 100644 --- a/internal/engine/stream_test.go +++ b/internal/engine/stream_test.go @@ -354,7 +354,7 @@ type responseRecorder struct { body *strings.Builder } -func (r *responseRecorder) Header() http.Header { return r.headers } +func (r *responseRecorder) Header() http.Header { return r.headers } func (r *responseRecorder) WriteHeader(code int) { r.statusCode = code } func (r *responseRecorder) Write(b []byte) (int, error) { if r.statusCode == 0 { diff --git a/internal/engine/task.go b/internal/engine/task.go index 78513bc..d07a689 100644 --- a/internal/engine/task.go +++ b/internal/engine/task.go @@ -191,6 +191,8 @@ func (t *Task) ToStatusUpdate() agent.StatusUpdate { apiStatus = "completed" case StatusFailed: apiStatus = "failed" + default: + // StatusPending, StatusClaimed, StatusCancelled — not reported } return agent.StatusUpdate{ diff --git a/internal/engine/task_test.go b/internal/engine/task_test.go index 00d2b1a..e9f6ccc 100644 --- a/internal/engine/task_test.go +++ b/internal/engine/task_test.go @@ -173,8 +173,8 @@ func TestToStatusUpdate(t *testing.T) { func TestToStatusUpdateGranularStates(t *testing.T) { tests := []struct { - status TaskStatus - wantAPI string + status TaskStatus + wantAPI string }{ {StatusResolving, "resolving"}, {StatusDownloading, "downloading"}, diff --git a/internal/engine/torrent.go b/internal/engine/torrent.go index ecb2b68..16d4150 100644 --- a/internal/engine/torrent.go +++ b/internal/engine/torrent.go @@ -11,9 +11,9 @@ import ( "sync" "time" - alog "github.com/anacrolix/log" "github.com/anacrolix/dht/v2" "github.com/anacrolix/dht/v2/krpc" + alog "github.com/anacrolix/log" "github.com/anacrolix/torrent" "github.com/anacrolix/torrent/storage" "github.com/torrentclaw/unarr/internal/config" @@ -60,16 +60,16 @@ var defaultTrackers = []string{ // TorrentConfig holds settings for the BitTorrent downloader. type TorrentConfig struct { - DataDir string - MetadataTimeout time.Duration // how long to wait for torrent metadata (default 15m, 0 = unlimited) - StallTimeout time.Duration // no progress during download for this long = stall (default 10m) - MaxTimeout time.Duration // absolute maximum per torrent (default 0 = unlimited) - MaxDownloadRate int64 // bytes/s, 0 = unlimited - MaxUploadRate int64 // bytes/s, 0 = unlimited - ListenPort int // fixed port for incoming peers (default 42069, 0 = random) - SeedEnabled bool - SeedRatio float64 // target seed ratio (default 0, meaning seed until SeedTime) - SeedTime time.Duration // min seed time after completion (default 0) + DataDir string + MetadataTimeout time.Duration // how long to wait for torrent metadata (default 15m, 0 = unlimited) + StallTimeout time.Duration // no progress during download for this long = stall (default 10m) + MaxTimeout time.Duration // absolute maximum per torrent (default 0 = unlimited) + MaxDownloadRate int64 // bytes/s, 0 = unlimited + MaxUploadRate int64 // bytes/s, 0 = unlimited + ListenPort int // fixed port for incoming peers (default 42069, 0 = random) + SeedEnabled bool + SeedRatio float64 // target seed ratio (default 0, meaning seed until SeedTime) + SeedTime time.Duration // min seed time after completion (default 0) } // TorrentDownloader downloads torrents via BitTorrent P2P. diff --git a/internal/engine/usenet.go b/internal/engine/usenet.go index bc85a68..fda121b 100644 --- a/internal/engine/usenet.go +++ b/internal/engine/usenet.go @@ -21,7 +21,7 @@ import ( // activeDownload holds the state for a single in-progress usenet download. type activeDownload struct { cancel context.CancelFunc - taskDir string // populated after MkdirAll; empty before + taskDir string // populated after MkdirAll; empty before tracker *download.ProgressTracker // populated after tracker creation; nil before } @@ -471,4 +471,3 @@ func sanitizeDir(name string) string { } return name } - diff --git a/internal/library/mediainfo/types.go b/internal/library/mediainfo/types.go index 030a31d..bf52f80 100644 --- a/internal/library/mediainfo/types.go +++ b/internal/library/mediainfo/types.go @@ -10,7 +10,7 @@ type MediaInfo struct { // VideoInfo represents the primary video stream metadata. type VideoInfo struct { - Codec string `json:"codec"` // "hevc", "h264", "av1" + Codec string `json:"codec"` // "hevc", "h264", "av1" Width int `json:"width"` Height int `json:"height"` BitDepth int `json:"bitDepth"` // 8, 10, 12 diff --git a/internal/library/resolve.go b/internal/library/resolve.go index ca4a9dd..531fa3b 100644 --- a/internal/library/resolve.go +++ b/internal/library/resolve.go @@ -8,9 +8,9 @@ import ( ) var ( - seasonRegex = regexp.MustCompile(`(?i)S(\d{1,2})E(\d{1,2})`) - seasonOnly = regexp.MustCompile(`(?i)S(\d{1,2})(?:\b|$)`) - altEpRegex = regexp.MustCompile(`(?i)(\d{1,2})x(\d{2})`) + seasonRegex = regexp.MustCompile(`(?i)S(\d{1,2})E(\d{1,2})`) + seasonOnly = regexp.MustCompile(`(?i)S(\d{1,2})(?:\b|$)`) + altEpRegex = regexp.MustCompile(`(?i)(\d{1,2})x(\d{2})`) ) // ResolveResolution maps a pixel height to a standard resolution label. diff --git a/internal/library/types.go b/internal/library/types.go index e11fde4..ca89e8c 100644 --- a/internal/library/types.go +++ b/internal/library/types.go @@ -4,18 +4,18 @@ import "github.com/torrentclaw/unarr/internal/library/mediainfo" // LibraryItem represents a single scanned media file. type LibraryItem struct { - FilePath string `json:"filePath"` - FileName string `json:"fileName"` - FileSize int64 `json:"fileSize"` - ModTime string `json:"modTime"` // ISO 8601 - Title string `json:"title"` - Year string `json:"year,omitempty"` - Season int `json:"season,omitempty"` - Episode int `json:"episode,omitempty"` - Quality string `json:"quality,omitempty"` // "1080p" etc (from filename) - Codec string `json:"codec,omitempty"` // "x265" etc (from filename) + FilePath string `json:"filePath"` + FileName string `json:"fileName"` + FileSize int64 `json:"fileSize"` + ModTime string `json:"modTime"` // ISO 8601 + Title string `json:"title"` + Year string `json:"year,omitempty"` + Season int `json:"season,omitempty"` + Episode int `json:"episode,omitempty"` + Quality string `json:"quality,omitempty"` // "1080p" etc (from filename) + Codec string `json:"codec,omitempty"` // "x265" etc (from filename) MediaInfo *mediainfo.MediaInfo `json:"mediaInfo,omitempty"` - ScanError string `json:"scanError,omitempty"` + ScanError string `json:"scanError,omitempty"` } // LibraryCache is the on-disk cache of scanned library items. diff --git a/internal/ui/format_test.go b/internal/ui/format_test.go index 614b6e4..7f3d2c5 100644 --- a/internal/ui/format_test.go +++ b/internal/ui/format_test.go @@ -161,5 +161,5 @@ func TestFormatContentType(t *testing.T) { } } -func ptr[T any](v T) *T { return &v } -func intPtr(v int) *int { return &v } +func ptr[T any](v T) *T { return &v } +func intPtr(v int) *int { return &v } diff --git a/internal/usenet/nzb/parser.go b/internal/usenet/nzb/parser.go index b82a5f9..e8e5f58 100644 --- a/internal/usenet/nzb/parser.go +++ b/internal/usenet/nzb/parser.go @@ -50,11 +50,11 @@ type xmlMeta struct { } type xmlFile struct { - Poster string `xml:"poster,attr"` - Date string `xml:"date,attr"` - Subject string `xml:"subject,attr"` - Groups xmlGroups `xml:"groups"` - Segments xmlSegments `xml:"segments"` + Poster string `xml:"poster,attr"` + Date string `xml:"date,attr"` + Subject string `xml:"subject,attr"` + Groups xmlGroups `xml:"groups"` + Segments xmlSegments `xml:"segments"` } type xmlGroups struct { @@ -263,8 +263,9 @@ func (f *File) TotalBytes() int64 { // subjectFilenameRe matches the filename in a typical Usenet subject line. // Examples: -// "Movie.2024.1080p.mkv" yEnc (1/50) -// [PRiVATE]-[#a]- "file.rar" yEnc (01/99) +// +// "Movie.2024.1080p.mkv" yEnc (1/50) +// [PRiVATE]-[#a]- "file.rar" yEnc (01/99) var subjectFilenameRe = regexp.MustCompile(`"([^"]+)"`) // Filename extracts the filename from the subject line. diff --git a/internal/usenet/postprocess/extract.go b/internal/usenet/postprocess/extract.go index 6388f19..0a9b582 100644 --- a/internal/usenet/postprocess/extract.go +++ b/internal/usenet/postprocess/extract.go @@ -105,7 +105,7 @@ func IsPasswordProtected(archivePath string) bool { return false } - switch extType { + switch extType { //nolint:exhaustive // ExtractorNone handled above case ExtractorUnrar: cmd := exec.Command(extPath, "t", "-p-", archivePath) output, err := cmd.CombinedOutput()