From 894a8eaba13121e2a676c89a9d4102741caffedf Mon Sep 17 00:00:00 2001 From: Contre Date: Tue, 9 Dec 2025 22:30:35 +0100 Subject: [PATCH 1/5] feat(importing->analyze): Analyze jobs after directory_jobs --- src/features/analyze/acoustid_job.go | 154 ++++++++++++++++++------ src/features/analyze/lyrics_job.go | 138 ++++++++++++++++----- src/features/config/config.go | 11 +- src/features/config/handlers.go | 10 ++ src/features/hosting/server.go | 9 ++ src/features/importing/directory_job.go | 65 +++++++++- views/config/config_form.html | 42 +++++-- 7 files changed, 345 insertions(+), 84 deletions(-) diff --git a/src/features/analyze/acoustid_job.go b/src/features/analyze/acoustid_job.go index 37418e9..a5b1fee 100644 --- a/src/features/analyze/acoustid_job.go +++ b/src/features/analyze/acoustid_job.go @@ -6,6 +6,7 @@ import ( "log/slog" "github.com/contre95/soulsolid/src/features/jobs" + "github.com/contre95/soulsolid/src/music" ) // AcoustIDJobTask handles AcoustID analysis job execution @@ -27,48 +28,64 @@ func (t *AcoustIDJobTask) MetadataKeys() []string { // Execute performs the AcoustID analysis operation func (t *AcoustIDJobTask) Execute(ctx context.Context, job *jobs.Job, progressUpdater func(int, string)) (map[string]any, error) { - // Get total track count for progress reporting - totalTracks, err := t.service.libraryService.GetTracksCount(ctx) - if err != nil { - return nil, fmt.Errorf("failed to get tracks count: %w", err) - } + var tracksToProcess []*music.Track + var totalTracks int + + // Check if specific track IDs are provided + if trackIDs, ok := job.Metadata["track_ids"].([]interface{}); ok && len(trackIDs) > 0 { + // Process specific tracks + job.Logger.Info("Starting AcoustID analysis for specific tracks", "trackCount", len(trackIDs), "color", "blue") + progressUpdater(0, fmt.Sprintf("Starting analysis of %d tracks", len(trackIDs))) + + // Convert interface{} to string slice + trackIDStrings := make([]string, len(trackIDs)) + for i, id := range trackIDs { + if idStr, ok := id.(string); ok { + trackIDStrings[i] = idStr + } + } - if totalTracks == 0 { - job.Logger.Info("No tracks found in library") - return map[string]any{ - "totalTracks": 0, - "processed": 0, - "acoustidsAdded": 0, - "fingerprintsAdded": 0, - "skipped": 0, - }, nil - } + // Get tracks by IDs + for _, trackID := range trackIDStrings { + track, err := t.service.libraryService.GetTrack(ctx, trackID) + if err != nil { + job.Logger.Warn("Failed to get track for analysis", "trackID", trackID, "error", err) + continue + } + tracksToProcess = append(tracksToProcess, track) + } + totalTracks = len(tracksToProcess) + } else { + // Process all tracks in library + var err error + totalTracks, err = t.service.libraryService.GetTracksCount(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get tracks count: %w", err) + } - job.Logger.Info("Starting AcoustID analysis", "totalTracks", totalTracks, "color", "blue") - progressUpdater(0, fmt.Sprintf("Starting analysis of %d tracks", totalTracks)) + if totalTracks == 0 { + job.Logger.Info("No tracks found in library") + return map[string]any{ + "totalTracks": 0, + "processed": 0, + "acoustidsAdded": 0, + "fingerprintsAdded": 0, + "skipped": 0, + }, nil + } + + job.Logger.Info("Starting AcoustID analysis", "totalTracks", totalTracks, "color", "blue") + progressUpdater(0, fmt.Sprintf("Starting analysis of %d tracks", totalTracks)) + } processed := 0 updated := 0 fingerprintsAdded := 0 skipped := 0 - // Process tracks in batches to avoid loading all into memory - batchSize := 100 - for offset := 0; offset < totalTracks; offset += batchSize { - select { - case <-ctx.Done(): - job.Logger.Info("AcoustID analysis cancelled", "processed", processed, "acoustidsAdded", updated, "fingerprintsAdded", fingerprintsAdded) - return nil, ctx.Err() - default: - } - - // Get next batch of tracks - tracks, err := t.service.libraryService.GetTracksPaginated(ctx, batchSize, offset) - if err != nil { - return nil, fmt.Errorf("failed to get tracks batch (offset %d): %w", offset, err) - } - - for _, track := range tracks { + if len(tracksToProcess) > 0 { + // Process specific tracks + for _, track := range tracksToProcess { select { case <-ctx.Done(): job.Logger.Info("AcoustID analysis cancelled", "processed", processed, "acoustidsAdded", updated, "fingerprintsAdded", fingerprintsAdded) @@ -119,6 +136,75 @@ func (t *AcoustIDJobTask) Execute(ctx context.Context, job *jobs.Job, progressUp processed++ } + } else { + // Process tracks in batches to avoid loading all into memory + batchSize := 100 + for offset := 0; offset < totalTracks; offset += batchSize { + select { + case <-ctx.Done(): + job.Logger.Info("AcoustID analysis cancelled", "processed", processed, "acoustidsAdded", updated, "fingerprintsAdded", fingerprintsAdded) + return nil, ctx.Err() + default: + } + + // Get next batch of tracks + tracks, err := t.service.libraryService.GetTracksPaginated(ctx, batchSize, offset) + if err != nil { + return nil, fmt.Errorf("failed to get tracks batch (offset %d): %w", offset, err) + } + + for _, track := range tracks { + select { + case <-ctx.Done(): + job.Logger.Info("AcoustID analysis cancelled", "processed", processed, "acoustidsAdded", updated, "fingerprintsAdded", fingerprintsAdded) + return nil, ctx.Err() + default: + } + + progress := (processed * 100) / totalTracks + progressUpdater(progress, fmt.Sprintf("Processing track %d/%d: %s", processed+1, totalTracks, track.Title)) + + // Skip tracks that already have AcoustID + acoustID := "" + if track.Attributes != nil { + acoustID = track.Attributes["acoustid"] + } + if acoustID != "" { + job.Logger.Info("Skipping track with existing AcoustID", "trackID", track.ID, "title", track.Title, "acoustID", acoustID, "color", "orange") + skipped++ + continue + } + + // Call the existing AddChromaprintAndAcoustID method + job.Logger.Info("Analyzing track fingerprint", "trackID", track.ID, "title", track.Title, "artist", track.Artists, "color", "cyan") + err := t.service.taggingService.AddChromaprintAndAcoustID(ctx, track.ID) + if err != nil { + job.Logger.Warn("Failed to add fingerprint and AcoustID for track", "trackID", track.ID, "title", track.Title, "error", err, "color", "orange") + // Continue with other tracks - don't fail the entire job + } else { + // Check if AcoustID was actually added + updatedTrack, err := t.service.libraryService.GetTrack(ctx, track.ID) + if err != nil { + job.Logger.Warn("Failed to verify AcoustID addition for track", "trackID", track.ID, "title", track.Title, "error", err, "color", "orange") + fingerprintsAdded++ // Assume fingerprint was added + } else { + acoustID := "" + if updatedTrack.Attributes != nil { + acoustID = updatedTrack.Attributes["acoustid"] + } + if acoustID != "" { + updated++ + job.Logger.Info("Successfully added AcoustID for track", "trackID", track.ID, "title", track.Title, "color", "green") + } else { + fingerprintsAdded++ + job.Logger.Info("Added fingerprint for track, AcoustID lookup failed or not configured", "trackID", track.ID, "title", track.Title, "color", "yellow") + } + } + } + + processed++ + } + } } job.Logger.Info("AcoustID analysis completed", "totalTracks", totalTracks, "processed", processed, "acoustidsAdded", updated, "fingerprintsAdded", fingerprintsAdded, "skipped", skipped, "color", "green") diff --git a/src/features/analyze/lyrics_job.go b/src/features/analyze/lyrics_job.go index 7134286..527bfb4 100644 --- a/src/features/analyze/lyrics_job.go +++ b/src/features/analyze/lyrics_job.go @@ -6,6 +6,7 @@ import ( "log/slog" "github.com/contre95/soulsolid/src/features/jobs" + "github.com/contre95/soulsolid/src/music" ) // LyricsJobTask handles lyrics analysis job execution @@ -46,46 +47,62 @@ func (t *LyricsJobTask) Execute(ctx context.Context, job *jobs.Job, progressUpda job.Logger.Info("Enabled lyrics providers", "providers", enabledProviders, "color", "blue") - // Get total track count for progress reporting - totalTracks, err := t.service.libraryService.GetTracksCount(ctx) - if err != nil { - return nil, fmt.Errorf("failed to get tracks count: %w", err) - } + var tracksToProcess []*music.Track + var totalTracks int - if totalTracks == 0 { - job.Logger.Info("No tracks found in library") - return map[string]any{ - "totalTracks": 0, - "processed": 0, - "updated": 0, - }, nil - } + // Check if specific track IDs are provided + if trackIDs, ok := job.Metadata["track_ids"].([]interface{}); ok && len(trackIDs) > 0 { + // Process specific tracks + job.Logger.Info("Starting lyrics analysis for specific tracks", "trackCount", len(trackIDs), "color", "blue") + progressUpdater(0, fmt.Sprintf("Starting analysis of %d tracks", len(trackIDs))) + + // Convert interface{} to string slice + trackIDStrings := make([]string, len(trackIDs)) + for i, id := range trackIDs { + if idStr, ok := id.(string); ok { + trackIDStrings[i] = idStr + } + } + + // Get tracks by IDs + for _, trackID := range trackIDStrings { + track, err := t.service.libraryService.GetTrack(ctx, trackID) + if err != nil { + job.Logger.Warn("Failed to get track for analysis", "trackID", trackID, "error", err) + continue + } + tracksToProcess = append(tracksToProcess, track) + } + totalTracks = len(tracksToProcess) + } else { + // Process all tracks in library + var err error + totalTracks, err = t.service.libraryService.GetTracksCount(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get tracks count: %w", err) + } - job.Logger.Info("Starting lyrics analysis", "totalTracks", totalTracks, "color", "blue") - progressUpdater(0, fmt.Sprintf("Starting analysis of %d tracks", totalTracks)) + if totalTracks == 0 { + job.Logger.Info("No tracks found in library") + return map[string]any{ + "totalTracks": 0, + "processed": 0, + "updated": 0, + }, nil + } + + job.Logger.Info("Starting lyrics analysis", "totalTracks", totalTracks, "color", "blue") + progressUpdater(0, fmt.Sprintf("Starting analysis of %d tracks", totalTracks)) + } processed := 0 updated := 0 skipped := 0 errors := 0 - // Process tracks in batches to avoid loading all into memory - batchSize := 100 - for offset := 0; offset < totalTracks; offset += batchSize { - select { - case <-ctx.Done(): - job.Logger.Info("Lyrics analysis cancelled", "processed", processed, "updated", updated) - return nil, ctx.Err() - default: - } - - // Get next batch of tracks - tracks, err := t.service.libraryService.GetTracksPaginated(ctx, batchSize, offset) - if err != nil { - return nil, fmt.Errorf("failed to get tracks batch (offset %d): %w", offset, err) - } - - for _, track := range tracks { + if len(tracksToProcess) > 0 { + // Process specific tracks + for _, track := range tracksToProcess { select { case <-ctx.Done(): job.Logger.Info("Lyrics analysis cancelled", "processed", processed, "updated", updated) @@ -124,6 +141,63 @@ func (t *LyricsJobTask) Execute(ctx context.Context, job *jobs.Job, progressUpda processed++ } + } else { + // Process tracks in batches to avoid loading all into memory + batchSize := 100 + for offset := 0; offset < totalTracks; offset += batchSize { + select { + case <-ctx.Done(): + job.Logger.Info("Lyrics analysis cancelled", "processed", processed, "updated", updated) + return nil, ctx.Err() + default: + } + + // Get next batch of tracks + tracks, err := t.service.libraryService.GetTracksPaginated(ctx, batchSize, offset) + if err != nil { + return nil, fmt.Errorf("failed to get tracks batch (offset %d): %w", offset, err) + } + + for _, track := range tracks { + select { + case <-ctx.Done(): + job.Logger.Info("Lyrics analysis cancelled", "processed", processed, "updated", updated) + return nil, ctx.Err() + default: + } + + progress := (processed * 100) / totalTracks + progressUpdater(progress, fmt.Sprintf("Processing track %d/%d: %s", processed+1, totalTracks, track.Title)) + + // Skip tracks that already have lyrics + if track.Metadata.Lyrics != "" { + job.Logger.Info("Skipping track with existing lyrics", "trackID", track.ID, "title", track.Title, "lyricsLength", len(track.Metadata.Lyrics), "color", "orange") + skipped++ + continue + } + + // Get the specified provider from job metadata + provider, ok := job.Metadata["provider"].(string) + if !ok || provider == "" { + job.Logger.Error("No provider specified in job metadata") + return nil, fmt.Errorf("no lyrics provider specified in job metadata") + } + + // Try to fetch lyrics for this track using the specified provider + job.Logger.Info("Fetching lyrics for track", "trackID", track.ID, "title", track.Title, "artist", track.Artists, "album", track.Album, "provider", provider, "color", "cyan") + err := t.service.lyricsService.AddLyrics(ctx, track.ID, provider) + if err != nil { + job.Logger.Error("Failed to add lyrics for track", "trackID", track.ID, "title", track.Title, "provider", provider, "error", err.Error(), "manual_fix", "track") + errors++ + // Continue with other tracks - don't fail the entire job + } else { + updated++ + job.Logger.Info("Successfully added lyrics for track", "trackID", track.ID, "title", track.Title, "provider", provider, "color", "green") + } + + processed++ + } + } } job.Logger.Info("Lyrics analysis completed", "totalTracks", totalTracks, "processed", processed, "updated", updated, "skipped", skipped, "color", "green") diff --git a/src/features/config/config.go b/src/features/config/config.go index fa078bd..538fbeb 100644 --- a/src/features/config/config.go +++ b/src/features/config/config.go @@ -28,11 +28,12 @@ type WebhookConfig struct { } type Import struct { - Move bool `yaml:"move"` // If not copies - AlwaysQueue bool `yaml:"always_queue"` - Duplicates string `yaml:"duplicates"` // "replace", "skip", "queue" - PathOptions Paths `yaml:"paths"` - AutoStartWatcher bool `yaml:"auto_start_watcher"` + Move bool `yaml:"move"` // If not copies + AlwaysQueue bool `yaml:"always_queue"` + Duplicates string `yaml:"duplicates"` // "replace", "skip", "queue" + PathOptions Paths `yaml:"paths"` + AutoStartWatcher bool `yaml:"auto_start_watcher"` + AutoAnalyze []string `yaml:"auto_analyze"` // e.g., ["acoustid", "lyrics"] } type Paths struct { diff --git a/src/features/config/handlers.go b/src/features/config/handlers.go index 2af2380..b7d6527 100644 --- a/src/features/config/handlers.go +++ b/src/features/config/handlers.go @@ -52,6 +52,16 @@ func (h *Handler) UpdateSettings(c *fiber.Ctx) error { Move: c.FormValue("import.move") == "true", AlwaysQueue: c.FormValue("import.always_queue") == "true", Duplicates: c.FormValue("import.duplicates"), + AutoAnalyze: func() []string { + var autoAnalyze []string + if c.FormValue("import.auto_analyze.acoustid") == "true" { + autoAnalyze = append(autoAnalyze, "acoustid") + } + if c.FormValue("import.auto_analyze.lyrics") == "true" { + autoAnalyze = append(autoAnalyze, "lyrics") + } + return autoAnalyze + }(), PathOptions: Paths{ DefaultPath: c.FormValue("import.paths.default_path"), Compilations: c.FormValue("import.paths.compilations"), diff --git a/src/features/hosting/server.go b/src/features/hosting/server.go index a6c440e..1657e2d 100644 --- a/src/features/hosting/server.go +++ b/src/features/hosting/server.go @@ -48,6 +48,15 @@ func NewServer(cfg *config.Manager, importingService *importing.Service, library remainingSeconds := seconds % 60 return fmt.Sprintf("%d:%02d", minutes, remainingSeconds) }) + + engine.AddFunc("contains", func(slice []string, item string) bool { + for _, s := range slice { + if s == item { + return true + } + } + return false + }) engine.AddFunc("formatDuration", func(seconds int) string { if seconds == 0 { return "0 min" diff --git a/src/features/importing/directory_job.go b/src/features/importing/directory_job.go index ddc14bb..643c6a5 100644 --- a/src/features/importing/directory_job.go +++ b/src/features/importing/directory_job.go @@ -44,7 +44,7 @@ func (e *DirectoryImportTask) MetadataKeys() []string { func (e *DirectoryImportTask) Execute(ctx context.Context, job *jobs.Job, progressUpdater func(int, string)) (map[string]any, error) { path := job.Metadata["path"].(string) - stats, err := e.runDirectoryImport(ctx, path, progressUpdater, job.Logger, job) + stats, importedTrackIDs, err := e.runDirectoryImport(ctx, path, progressUpdater, job.Logger, job) if err != nil { return nil, fmt.Errorf("failed to import directory: %w", err) } @@ -70,10 +70,67 @@ func (e *DirectoryImportTask) Execute(ctx context.Context, job *jobs.Job, progre return map[string]any{"stats": stats, "msg": finalMessage}, errors.New("Some tracks failed to process") } + // Start analyze jobs for newly imported tracks if configured + if len(importedTrackIDs) > 0 { + e.startAnalyzeJobsForImportedTracks(ctx, importedTrackIDs, job.Logger) + } + // Full success - all tracks processed without errors (including skips) return map[string]any{"stats": stats, "msg": finalMessage}, nil } +// startAnalyzeJobsForImportedTracks starts analyze jobs for newly imported tracks based on config +func (e *DirectoryImportTask) startAnalyzeJobsForImportedTracks(ctx context.Context, trackIDs []string, logger *slog.Logger) { + config := e.service.config.Get().Import + autoAnalyze := config.AutoAnalyze + + if len(autoAnalyze) == 0 || len(trackIDs) == 0 { + return + } + + logger.Info("Starting auto-analyze jobs for imported tracks", "trackCount", len(trackIDs), "analyzeTypes", autoAnalyze) + + for _, analyzeType := range autoAnalyze { + var jobType string + var jobName string + var metadata map[string]any + + switch analyzeType { + case "acoustid": + jobType = "analyze_acoustid" + jobName = "Analyze AcoustID for Imported Tracks" + metadata = map[string]any{"track_ids": trackIDs} + case "lyrics": + jobType = "analyze_lyrics" + jobName = "Analyze Lyrics for Imported Tracks" + // For lyrics, we need to get the default provider from config + lyricsConfig := e.service.config.Get().Lyrics + var defaultProvider string + for provider, cfg := range lyricsConfig.Providers { + if cfg.Enabled { + defaultProvider = provider + break + } + } + if defaultProvider == "" { + logger.Warn("No enabled lyrics provider found, skipping lyrics analysis for imported tracks") + continue + } + metadata = map[string]any{"track_ids": trackIDs, "provider": defaultProvider} + default: + logger.Warn("Unknown auto-analyze type", "type", analyzeType) + continue + } + + jobID, err := e.service.jobService.StartJob(jobType, jobName, metadata) + if err != nil { + logger.Error("Failed to start analyze job for imported tracks", "type", analyzeType, "error", err) + } else { + logger.Info("Started analyze job for imported tracks", "type", analyzeType, "jobID", jobID) + } + } +} + // Cleanup does nothing for directory imports. func (e *DirectoryImportTask) Cleanup(job *jobs.Job) error { return nil @@ -206,9 +263,10 @@ func (e *DirectoryImportTask) findExistingTrack(ctx context.Context, trackToImpo return existingTrack, nil } -func (e *DirectoryImportTask) runDirectoryImport(ctx context.Context, pathToImport string, progressUpdater func(int, string), logger *slog.Logger, job *jobs.Job) (ImportStats, error) { +func (e *DirectoryImportTask) runDirectoryImport(ctx context.Context, pathToImport string, progressUpdater func(int, string), logger *slog.Logger, job *jobs.Job) (ImportStats, []string, error) { logger.Info("Service.runDirectoryImport: starting import", "path", pathToImport) var stats ImportStats + var importedTrackIDs []string moveFiles := e.service.config.Get().Import.Move config := e.service.config.Get().Import @@ -298,6 +356,7 @@ func (e *DirectoryImportTask) runDirectoryImport(ctx context.Context, pathToImpo stats.Errors++ } else { stats.TracksImported++ + importedTrackIDs = append(importedTrackIDs, trackToImport.ID) logger.Info("Service.runDirectoryImport: Track Imported", "title", trackToImport.Title, "color", "green") } } @@ -319,5 +378,5 @@ func (e *DirectoryImportTask) runDirectoryImport(ctx context.Context, pathToImpo progressUpdater(100, "Import completed") } - return stats, err + return stats, importedTrackIDs, err } diff --git a/views/config/config_form.html b/views/config/config_form.html index bc6eb15..b4e1a03 100644 --- a/views/config/config_form.html +++ b/views/config/config_form.html @@ -215,16 +215,38 @@

-
- - -
- +
+ + +
+
+ +
+
+ + +
+
+ + +
+
+

Automatically run analysis jobs on newly imported tracks

+
+ From f24bb09de41ecf20fa91dde32cb33053999cb84b Mon Sep 17 00:00:00 2001 From: Contre Date: Tue, 9 Dec 2025 22:58:49 +0100 Subject: [PATCH 2/5] feat(acoust_id): Added summary badge to the acoust_id job --- src/features/analyze/acoustid_job.go | 9 ++++++++- views/jobs/job_card_footer.html | 8 +++++--- views/jobs/job_card_header.html | 8 +++++--- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/features/analyze/acoustid_job.go b/src/features/analyze/acoustid_job.go index a5b1fee..f5a09c9 100644 --- a/src/features/analyze/acoustid_job.go +++ b/src/features/analyze/acoustid_job.go @@ -71,6 +71,7 @@ func (t *AcoustIDJobTask) Execute(ctx context.Context, job *jobs.Job, progressUp "acoustidsAdded": 0, "fingerprintsAdded": 0, "skipped": 0, + "msg": "AcoustID analysis completed - no tracks found in library", }, nil } @@ -208,7 +209,12 @@ func (t *AcoustIDJobTask) Execute(ctx context.Context, job *jobs.Job, progressUp } job.Logger.Info("AcoustID analysis completed", "totalTracks", totalTracks, "processed", processed, "acoustidsAdded", updated, "fingerprintsAdded", fingerprintsAdded, "skipped", skipped, "color", "green") - progressUpdater(100, fmt.Sprintf("Analysis completed - %d AcoustIDs added, %d fingerprints added, %d skipped", updated, fingerprintsAdded, skipped)) + + // Create completion message for job summary + finalMessage := fmt.Sprintf("AcoustID analysis completed - %d AcoustIDs added, %d fingerprints added, %d skipped", updated, fingerprintsAdded, skipped) + job.Logger.Info(finalMessage) + + progressUpdater(100, finalMessage) return map[string]any{ "totalTracks": totalTracks, @@ -216,6 +222,7 @@ func (t *AcoustIDJobTask) Execute(ctx context.Context, job *jobs.Job, progressUp "acoustidsAdded": updated, "fingerprintsAdded": fingerprintsAdded, "skipped": skipped, + "msg": finalMessage, }, nil } diff --git a/views/jobs/job_card_footer.html b/views/jobs/job_card_footer.html index f469fe0..69243c8 100644 --- a/views/jobs/job_card_footer.html +++ b/views/jobs/job_card_footer.html @@ -8,9 +8,11 @@ {{ if and (ne $job.Metadata nil) (index $job.Metadata "msg") }} {{ $stats := index $job.Metadata "stats" }} {{ $colorClass := "bg-red-50/80 dark:bg-red-900/30 border border-red-200/60 dark:border-red-800/60 text-red-700 dark:text-red-300" }} - {{ if eq $job.Type "analyze_lyrics" }} - {{ $colorClass = "bg-pink-50/80 dark:bg-pink-900/30 border border-pink-200/60 dark:border-pink-800/60 text-pink-700 dark:text-pink-300" }} - {{ else if eq $job.Status "cancelled" }} + {{ if eq $job.Type "analyze_lyrics" }} + {{ $colorClass = "bg-pink-50/80 dark:bg-pink-900/30 border border-pink-200/60 dark:border-pink-800/60 text-pink-700 dark:text-pink-300" }} + {{ else if eq $job.Type "analyze_acoustid" }} + {{ $colorClass = "bg-teal-50/80 dark:bg-teal-900/30 border border-teal-200/60 dark:border-teal-800/60 text-teal-700 dark:text-teal-300" }} + {{ else if eq $job.Status "cancelled" }} {{ $colorClass = "bg-gray-50/80 dark:bg-gray-900/30 border border-gray-200/60 dark:border-gray-800/60 text-gray-700 dark:text-gray-300" }} {{ else if and (eq $job.Status "completed") $stats (gt $stats.TracksImported 0) }} {{ $colorClass = "bg-green-50/80 dark:bg-green-900/30 border border-green-200/60 dark:border-green-800/60 text-green-700 dark:text-green-300" }} diff --git a/views/jobs/job_card_header.html b/views/jobs/job_card_header.html index 9f3aa7d..ed627b2 100644 --- a/views/jobs/job_card_header.html +++ b/views/jobs/job_card_header.html @@ -9,9 +9,11 @@

{{ $job.Nam {{ $job.Type }} {{ else if eq $job.Type "dap_sync" }} {{ $job.Type }} - {{ else if eq $job.Type "analyze_lyrics" }} - {{ $job.Type }} - {{ else }} + {{ else if eq $job.Type "analyze_lyrics" }} + {{ $job.Type }} + {{ else if eq $job.Type "analyze_acoustid" }} + {{ $job.Type }} + {{ else }} {{ $job.Type }} {{ end }} {{ $job.CreatedAt.Format "Jan 2, 15:04" }} From 78286989b5b9848478b6360697306d3aca12110f Mon Sep 17 00:00:00 2001 From: Contre Date: Tue, 9 Dec 2025 23:05:34 +0100 Subject: [PATCH 3/5] feat(acoust_id): Added summary badge to the acoust_id job --- src/features/analyze/lyrics_job.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/features/analyze/lyrics_job.go b/src/features/analyze/lyrics_job.go index 527bfb4..a7091c9 100644 --- a/src/features/analyze/lyrics_job.go +++ b/src/features/analyze/lyrics_job.go @@ -88,6 +88,9 @@ func (t *LyricsJobTask) Execute(ctx context.Context, job *jobs.Job, progressUpda "totalTracks": 0, "processed": 0, "updated": 0, + "skipped": 0, + "errors": 0, + "msg": "Lyrics analysis completed - no tracks found in library", }, nil } From e2b10feb82faf3937a016e8068844a71d5795c54 Mon Sep 17 00:00:00 2001 From: Contre Date: Wed, 10 Dec 2025 15:05:24 +0100 Subject: [PATCH 4/5] feat(fix): use fiber error codes --- src/features/jobs/handlers.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/features/jobs/handlers.go b/src/features/jobs/handlers.go index 706082f..c7e309f 100644 --- a/src/features/jobs/handlers.go +++ b/src/features/jobs/handlers.go @@ -44,9 +44,9 @@ func (h *Handler) HandleStartJob(c *fiber.Ctx) error { if err != nil { // Check if this is an HTMX request if c.Get("HX-Request") == "true" { - return c.Status(500).SendString(fmt.Sprintf("Failed to start job: %s", err.Error())) + return c.Status(fiber.StatusInternalServerError).SendString(fmt.Sprintf("Failed to start job: %s", err.Error())) } - return c.Status(500).JSON(fiber.Map{"error": err.Error()}) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } // Trigger HTMX to refresh the badge immediately @@ -65,7 +65,7 @@ func (h *Handler) HandleJobStatus(c *fiber.Ctx) error { jobID := c.Params("id") job, exists := h.service.GetJob(jobID) if !exists { - return c.Status(404).SendString("Job not found") + return c.Status(fiber.StatusNotFound).SendString("Job not found") } baseURL := c.BaseURL() @@ -85,7 +85,7 @@ func (h *Handler) HandleJobLogs(c *fiber.Ctx) error { jobID := c.Params("id") job, exists := h.service.GetJob(jobID) if !exists { - return c.Status(404).SendString("Job not found") + return c.Status(fiber.StatusNotFound).SendString("Job not found") } if job.LogPath == "" { @@ -94,7 +94,7 @@ func (h *Handler) HandleJobLogs(c *fiber.Ctx) error { logContent, err := os.ReadFile(job.LogPath) if err != nil { - return c.Status(500).SendString("Failed to read log file.") + return c.Status(fiber.StatusInternalServerError).SendString("Failed to read log file.") } // Check if color parameter is set @@ -124,7 +124,7 @@ func (h *Handler) HandleJobProgress(c *fiber.Ctx) error { jobID := c.Params("id") job, exists := h.service.GetJob(jobID) if !exists { - return c.Status(404).SendString("Job not found.") + return c.Status(fiber.StatusNotFound).SendString("Job not found") } if job.Status == JobStatusCompleted || job.Status == JobStatusFailed || job.Status == JobStatusCancelled { @@ -166,13 +166,13 @@ func (h *Handler) HandleCancelJob(c *fiber.Ctx) error { err := h.service.CancelJob(jobID) if err != nil { - return c.Status(500).JSON(fiber.Map{"error": err.Error()}) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } // Get the updated job to render the card job, exists := h.service.GetJob(jobID) if !exists { - return c.Status(404).SendString("Job not found") + return c.Status(fiber.StatusNotFound).SendString("Job not found") } return c.Render("jobs/job_card", fiber.Map{ @@ -196,7 +196,7 @@ func (h *Handler) HandleCleanupJobs(c *fiber.Ctx) error { func (h *Handler) HandleClearFinishedJobs(c *fiber.Ctx) error { err := h.service.ClearFinishedJobs() if err != nil { - return c.Status(500).JSON(fiber.Map{"error": err.Error()}) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } if c.Get("HX-Request") == "true" { From 8d40a3ee898e85072816ff3cfbfe904409665678 Mon Sep 17 00:00:00 2001 From: Contre Date: Wed, 10 Dec 2025 17:29:17 +0100 Subject: [PATCH 5/5] feat(fix): directory_jobs color --- src/features/importing/directory_job.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/features/importing/directory_job.go b/src/features/importing/directory_job.go index 643c6a5..1b2c287 100644 --- a/src/features/importing/directory_job.go +++ b/src/features/importing/directory_job.go @@ -57,25 +57,28 @@ func (e *DirectoryImportTask) Execute(ctx context.Context, job *jobs.Job, progre totalProcessed := stats.TracksImported + stats.Skipped + stats.Queued + stats.Errors finalMessage := fmt.Sprintf("Directory import finished. Processed %d tracks (%d imported, %d queued, %d skipped, %d errors).", totalProcessed, stats.TracksImported, stats.Queued, stats.Skipped, stats.Errors) - job.Logger.Info(finalMessage) // Determine job status - consider skips and queued as successful if stats.TracksImported == 0 && stats.Skipped == 0 && stats.Queued == 0 && stats.Errors > 0 { // Complete failure - no tracks processed successfully slog.Warn("No tracks were successfully processed", "stats", stats) + job.Logger.Info(finalMessage) return map[string]any{"stats": stats, "msg": finalMessage}, errors.New("No tracks were successfully processed") } else if stats.Errors > 0 { // Partial success - some failures occurred - slog.Warn("Some tracks failed to process", "stats", stats) - return map[string]any{"stats": stats, "msg": finalMessage}, errors.New("Some tracks failed to process") + partialMessage := fmt.Sprintf("Partial import: %d tracks imported, %d errors occurred.", stats.TracksImported, stats.Errors) + job.Logger.Info(partialMessage, "color", "orange") + return map[string]any{"stats": stats, "msg": partialMessage}, fmt.Errorf("partial import: %d tracks imported, %d errors", stats.TracksImported, stats.Errors) } + // Full success - all tracks processed without errors (including skips) + job.Logger.Info(finalMessage, "color", "green") + // Start analyze jobs for newly imported tracks if configured if len(importedTrackIDs) > 0 { e.startAnalyzeJobsForImportedTracks(ctx, importedTrackIDs, job.Logger) } - // Full success - all tracks processed without errors (including skips) return map[string]any{"stats": stats, "msg": finalMessage}, nil } @@ -364,10 +367,7 @@ func (e *DirectoryImportTask) runDirectoryImport(ctx context.Context, pathToImpo // Update progress after processing each file processedFiles++ if progressUpdater != nil && totalFiles > 0 { - progress := (processedFiles * 100) / totalFiles - if progress > 100 { - progress = 100 - } + progress := min((processedFiles*100)/totalFiles, 100) progressUpdater(progress, fmt.Sprintf("Processed: %s", filepath.Base(path))) } }