Skip to content
Open
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
5 changes: 3 additions & 2 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,8 @@ func newViper() *viper.Viper {
// Playlist sync defaults
v.SetDefault("playlist_sync.sync_interval", "1h")
v.SetDefault("playlist_sync.delete_on_unsync", false)
v.SetDefault("playlist_sync.min_match_confidence", 0.8)
// Governing: ADR-0014, SPEC playlist-sync-navidrome REQ-PLSYNC-003 (default 0.7; issue #330 aligned config with the ADR)
v.SetDefault("playlist_sync.min_match_confidence", 0.7)
v.SetDefault("playlist_sync.include_unmatched_tracks", false)

// Vibes (mixtape generation) defaults
Expand All @@ -416,7 +417,7 @@ func newViper() *viper.Viper {
v.SetDefault("vibes.max_tokens", 4000) // Enough for track list + explanations
v.SetDefault("vibes.timeout_seconds", 120) // 2 minutes
v.SetDefault("vibes.prompts_directory", "") // Falls back to metadata.ai.prompts_directory
v.SetDefault("vibes.min_match_confidence", 0.7) // Lower than playlist sync for more matches
v.SetDefault("vibes.min_match_confidence", 0.7) // Same as playlist sync default (ADR-0014)

// Metadata enrichment defaults
v.SetDefault("metadata.enabled", true)
Expand Down
38 changes: 34 additions & 4 deletions internal/services/playlist_sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@ func (s *PlaylistSyncService) Register(factory providers.Factory) {
// SPEC playlist-sync-navidrome REQ-PLSYNC-032 (remotePlaylistID stored on playlist entity),
// SPEC playlist-sync-navidrome REQ-PLSYNC-060 (SyncEvent audit logging)
func (s *PlaylistSyncService) SyncPlaylistToNavidrome(ctx context.Context, playlistID int) error {
return s.syncPlaylistToNavidrome(ctx, playlistID, nil)
}

// syncPlaylistToNavidrome performs the sync. libraryIndex may be nil (it is
// loaded on demand); callers syncing many playlists in one tick (issue #330)
// pass a shared index so the user's library is loaded once per tick instead
// of once per playlist.
func (s *PlaylistSyncService) syncPlaylistToNavidrome(ctx context.Context, playlistID int, libraryIndex *LibraryIndex) error {
startTime := time.Now()

s.logger.Info("starting playlist sync to Navidrome",
Expand Down Expand Up @@ -187,11 +195,17 @@ func (s *PlaylistSyncService) SyncPlaylistToNavidrome(ctx context.Context, playl
"playlist_id", playlistID,
"source_track_count", len(sourceTracks))

matchResults, err := s.trackMatcher.MatchTracks(ctx, u.ID, sourceTracks)
if err != nil {
return s.handleSyncError(ctx, pl, u, fmt.Errorf("failed to match tracks: %w", err))
// Load the library index on demand if the caller didn't supply a shared
// one (or supplied one built for a different user).
if libraryIndex == nil || libraryIndex.UserID != u.ID {
libraryIndex, err = s.trackMatcher.LoadLibraryIndex(ctx, u.ID)
if err != nil {
return s.handleSyncError(ctx, pl, u, fmt.Errorf("failed to load library index: %w", err))
}
}

matchResults := s.trackMatcher.MatchTracksWithIndex(libraryIndex, sourceTracks)

// Filter to only matched tracks (we can only add tracks that exist in Navidrome)
var matchedTracks []providers.Track
matchedCount := 0
Expand Down Expand Up @@ -415,10 +429,26 @@ func (s *PlaylistSyncService) SyncAllEnabledPlaylists(ctx context.Context, userI
return nil
}

// Governing: ADR-0014; issue #330 — load the user's library once per sync
// tick and share the index across all playlists, instead of re-running the
// full library query per playlist.
//
// If the tick-level load fails, do NOT abort the tick: fall back to
// per-playlist loading (libraryIndex == nil) so each playlist still goes
// through handleSyncError — sync_status=error, SyncEvent audit logging,
// and UI notification per REQ-PLSYNC-060 — instead of failing invisibly.
libraryIndex, err := s.trackMatcher.LoadLibraryIndex(ctx, userID)
if err != nil {
s.logger.Error("failed to load shared library index for sync tick, falling back to per-playlist loading",
"user_id", userID,
"error", err)
libraryIndex = nil
}

var syncErrors []error
successCount := 0
for _, pl := range playlists {
if err := s.SyncPlaylistToNavidrome(ctx, pl.ID); err != nil {
if err := s.syncPlaylistToNavidrome(ctx, pl.ID, libraryIndex); err != nil {
s.logger.Error("failed to sync playlist",
"user_id", userID,
"playlist_id", pl.ID,
Expand Down
Loading
Loading