From ad657f02a88c49725268781d0f5a00ed8c20a4c4 Mon Sep 17 00:00:00 2001 From: ramonskie Date: Thu, 25 Jun 2026 13:43:24 +0200 Subject: [PATCH] Fix playlist sync status labels --- tests/test_playlist_sync_status.py | 65 ++++++++++++++++++++++++++++++ web_server.py | 59 ++++++++++++++++++++++++--- webui/static/sync-spotify.js | 2 +- 3 files changed, 119 insertions(+), 7 deletions(-) create mode 100644 tests/test_playlist_sync_status.py diff --git a/tests/test_playlist_sync_status.py b/tests/test_playlist_sync_status.py new file mode 100644 index 00000000..317b8a6f --- /dev/null +++ b/tests/test_playlist_sync_status.py @@ -0,0 +1,65 @@ +from datetime import datetime, timedelta + +from web_server import ( + _format_playlist_sync_status, + _resolve_spotify_playlist_sync_status, +) + + +class _StubDatabase: + def __init__(self, mirrored): + self._mirrored = mirrored + + def get_mirrored_playlist_by_source(self, source, source_playlist_id, profile_id): + assert source == 'spotify' + assert source_playlist_id == 'spotify-playlist-1' + assert profile_id == 7 + return self._mirrored + + +def _status(minutes_ago=0, **overrides): + timestamp = (datetime(2026, 6, 25, 12, 0, 0) - timedelta(minutes=minutes_ago)).isoformat() + return {'last_synced': timestamp, **overrides} + + +def test_resolve_spotify_playlist_sync_status_uses_mirrored_auto_sync_status(): + sync_statuses = { + 'auto_mirror_42': _status(matched_tracks=12), + } + + status = _resolve_spotify_playlist_sync_status( + 'spotify-playlist-1', + sync_statuses, + database=_StubDatabase({'id': 42}), + profile_id=7, + ) + + assert status['matched_tracks'] == 12 + + +def test_resolve_spotify_playlist_sync_status_prefers_newest_status(): + sync_statuses = { + 'spotify-playlist-1': _status(minutes_ago=20, matched_tracks=1), + 'auto_mirror_42': _status(minutes_ago=5, matched_tracks=2), + } + + status = _resolve_spotify_playlist_sync_status( + 'spotify-playlist-1', + sync_statuses, + database=_StubDatabase({'id': 42}), + profile_id=7, + ) + + assert status['matched_tracks'] == 2 + + +def test_format_playlist_sync_status_treats_missing_snapshot_as_synced(): + status = _status() + + assert _format_playlist_sync_status(status, 'current-snapshot') == 'Synced: Jun 25, 12:00' + + +def test_format_playlist_sync_status_marks_snapshot_mismatch_as_last_sync(): + status = _status(snapshot_id='old-snapshot') + + assert _format_playlist_sync_status(status, 'current-snapshot') == 'Last Sync: Jun 25, 12:00' diff --git a/web_server.py b/web_server.py index 4e330c92..63cab778 100644 --- a/web_server.py +++ b/web_server.py @@ -20765,6 +20765,56 @@ def _save_sync_status_file(sync_statuses): except Exception as e: logger.error(f"Error saving sync status: {e}") +def _sync_status_timestamp(status_info): + """Return comparable timestamp for a persisted sync-status record.""" + if not status_info or 'last_synced' not in status_info: + return None + try: + return datetime.fromisoformat(status_info['last_synced']) + except (TypeError, ValueError): + return None + +def _latest_sync_status(*status_infos): + """Pick the newest non-empty sync-status record.""" + candidates = [s for s in status_infos if s] + if not candidates: + return {} + return max(candidates, key=lambda s: _sync_status_timestamp(s) or datetime.min) + +def _mirrored_spotify_sync_status(playlist_id, sync_statuses, *, database=None, profile_id=None): + """Return auto-sync status for a Spotify playlist mirrored into SoulSync.""" + try: + db = database or get_database() + profile = profile_id if profile_id is not None else get_current_profile_id() + mirrored = db.get_mirrored_playlist_by_source('spotify', str(playlist_id), profile) + if not mirrored: + return {} + return sync_statuses.get(f"auto_mirror_{mirrored.get('id')}", {}) + except Exception as e: + logger.debug("Spotify mirrored sync-status lookup failed for %s: %s", playlist_id, e) + return {} + +def _resolve_spotify_playlist_sync_status(playlist_id, sync_statuses, *, database=None, profile_id=None): + """Resolve direct or mirrored sync status for a Spotify playlist card.""" + direct_status = sync_statuses.get(playlist_id, {}) + mirrored_status = _mirrored_spotify_sync_status( + playlist_id, + sync_statuses, + database=database, + profile_id=profile_id, + ) + return _latest_sync_status(direct_status, mirrored_status) + +def _format_playlist_sync_status(status_info, playlist_snapshot): + """Build user-facing sync-status text from persisted status + snapshot.""" + if 'last_synced' not in status_info: + return "Never Synced" + last_sync_time = datetime.fromisoformat(status_info['last_synced']).strftime('%b %d, %H:%M') + stored_snapshot = status_info.get('snapshot_id') + if stored_snapshot and playlist_snapshot and playlist_snapshot != stored_snapshot: + return f"Last Sync: {last_sync_time}" + return f"Synced: {last_sync_time}" + def _update_and_save_sync_status(playlist_id, playlist_name, playlist_owner, snapshot_id, **kwargs): """Updates the sync status for a given playlist and saves to file (same logic as GUI).""" try: @@ -20807,16 +20857,14 @@ def get_spotify_playlists(): # Add regular playlists first for p in playlists: - status_info = sync_statuses.get(p.id, {}) - sync_status = "Never Synced" + status_info = _resolve_spotify_playlist_sync_status(p.id, sync_statuses) # Handle snapshot_id safely - may not exist in core Playlist class playlist_snapshot = getattr(p, 'snapshot_id', '') + sync_status = _format_playlist_sync_status(status_info, playlist_snapshot) if 'last_synced' in status_info: stored_snapshot = status_info.get('snapshot_id') - last_sync_time = datetime.fromisoformat(status_info['last_synced']).strftime('%b %d, %H:%M') - if playlist_snapshot != stored_snapshot: - sync_status = f"Last Sync: {last_sync_time}" + if stored_snapshot and playlist_snapshot and playlist_snapshot != stored_snapshot: logger.info( "Playlist sync status: name=%s id=%s snapshot=%r stored_snapshot=%r result=Needs Sync display=%s", p.name, @@ -20826,7 +20874,6 @@ def get_spotify_playlists(): sync_status, ) else: - sync_status = f"Synced: {last_sync_time}" logger.info( "Playlist sync status: name=%s id=%s snapshot=%r stored_snapshot=%r result=Synced display=%s", p.name, diff --git a/webui/static/sync-spotify.js b/webui/static/sync-spotify.js index 6af72167..cc888f1f 100644 --- a/webui/static/sync-spotify.js +++ b/webui/static/sync-spotify.js @@ -1639,7 +1639,7 @@ function renderSpotifyPlaylists() { container.innerHTML = spotifyPlaylists.map(p => { let statusClass = 'status-never-synced'; if (p.sync_status.startsWith('Synced')) statusClass = 'status-synced'; - if (p.sync_status === 'Needs Sync') statusClass = 'status-needs-sync'; + if (p.sync_status === 'Needs Sync' || p.sync_status.startsWith('Last Sync')) statusClass = 'status-needs-sync'; // This HTML structure creates the interactive playlist cards return `