Fix playlist sync status labels

This commit is contained in:
ramonskie 2026-06-25 13:43:24 +02:00
parent 729a06c6d7
commit ad657f02a8
3 changed files with 119 additions and 7 deletions

View file

@ -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'

View file

@ -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,

View file

@ -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 `