Skip to content
Draft
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
163 changes: 128 additions & 35 deletions src/features/analyze/acoustid_job.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -27,48 +28,65 @@ 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)
}

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,
"msg": "AcoustID analysis completed - no tracks found in library",
}, nil
}

job.Logger.Info("Starting AcoustID analysis", "totalTracks", totalTracks, "color", "blue")
progressUpdater(0, fmt.Sprintf("Starting analysis of %d tracks", totalTracks))
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)
Expand Down Expand Up @@ -119,17 +137,92 @@ 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")
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,
"processed": processed,
"acoustidsAdded": updated,
"fingerprintsAdded": fingerprintsAdded,
"skipped": skipped,
"msg": finalMessage,
}, nil
}

Expand Down
141 changes: 109 additions & 32 deletions src/features/analyze/lyrics_job.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -46,46 +47,65 @@ 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,
"skipped": 0,
"errors": 0,
"msg": "Lyrics analysis completed - no tracks found in library",
}, 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)
Expand Down Expand Up @@ -124,6 +144,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", "<a href='/ui/library/tag/edit/"+track.ID+"' target='_blank'>track</a>")
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")
Expand Down
11 changes: 6 additions & 5 deletions src/features/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading