Fix discovery modal persistence, artist dict handling, and rate limiter scope
This commit is contained in:
parent
05b5c376e9
commit
e11ee8622e
4 changed files with 83 additions and 22 deletions
|
|
@ -18,7 +18,7 @@ logger = get_logger("api_v1")
|
|||
# ---------------------------------------------------------------------------
|
||||
limiter = Limiter(
|
||||
key_func=get_remote_address,
|
||||
default_limits=["60 per minute"],
|
||||
default_limits=[], # No global default — limits are applied per-blueprint
|
||||
storage_uri="memory://",
|
||||
)
|
||||
|
||||
|
|
@ -43,6 +43,9 @@ def create_api_blueprint():
|
|||
from .listenbrainz import register_routes as reg_listenbrainz
|
||||
from .cache import register_routes as reg_cache
|
||||
|
||||
# ---- rate-limit only /api/v1 routes (not the whole app) ----
|
||||
limiter.limit("60 per minute")(bp)
|
||||
|
||||
reg_library(bp)
|
||||
reg_system(bp)
|
||||
reg_search(bp)
|
||||
|
|
|
|||
|
|
@ -20371,7 +20371,7 @@ def update_tidal_discovery_match():
|
|||
result['status'] = '✅ Found'
|
||||
result['status_class'] = 'found'
|
||||
result['spotify_track'] = spotify_track['name']
|
||||
result['spotify_artist'] = ', '.join(spotify_track['artists']) if isinstance(spotify_track['artists'], list) else spotify_track['artists']
|
||||
result['spotify_artist'] = _join_artist_names(spotify_track['artists']) if isinstance(spotify_track['artists'], list) else _extract_artist_name(spotify_track['artists'])
|
||||
result['spotify_album'] = spotify_track['album']
|
||||
result['spotify_id'] = spotify_track['id']
|
||||
|
||||
|
|
@ -20812,10 +20812,24 @@ def _run_playlist_discovery_worker(playlists, automation_id=None):
|
|||
log_line=f'Error: {str(e)}', log_type='error')
|
||||
|
||||
|
||||
def _extract_artist_name(artist):
|
||||
"""Extract artist name string from either a string or dict ({"name": "..."}) format."""
|
||||
if isinstance(artist, dict):
|
||||
return artist.get('name', '')
|
||||
return artist or ''
|
||||
|
||||
def _extract_artist_names(artists):
|
||||
"""Extract a list of artist name strings from a list that may contain dicts or strings."""
|
||||
return [_extract_artist_name(a) for a in (artists or [])]
|
||||
|
||||
def _join_artist_names(artists):
|
||||
"""Join artist names from a list that may contain dicts or strings."""
|
||||
return ', '.join(_extract_artist_names(artists))
|
||||
|
||||
def _get_discovery_cache_key(title, artist):
|
||||
"""Normalize title/artist for discovery cache lookup using matching_engine."""
|
||||
norm_title = matching_engine.clean_title(title)
|
||||
norm_artist = matching_engine.clean_artist(artist)
|
||||
norm_artist = matching_engine.clean_artist(_extract_artist_name(artist))
|
||||
return (norm_title, norm_artist)
|
||||
|
||||
|
||||
|
|
@ -20835,6 +20849,11 @@ def _validate_discovery_cache_artist(source_artist, cached_match):
|
|||
for cand_artist in cached_artists:
|
||||
if not cand_artist:
|
||||
continue
|
||||
# Handle both string artists and dict artists ({"name": "..."})
|
||||
if isinstance(cand_artist, dict):
|
||||
cand_artist = cand_artist.get('name', '')
|
||||
if not cand_artist:
|
||||
continue
|
||||
cand_normalized = matching_engine.normalize_string(cand_artist)
|
||||
if source_artist_cleaned in cand_normalized:
|
||||
return True
|
||||
|
|
@ -21585,7 +21604,7 @@ def update_youtube_discovery_match():
|
|||
result['status'] = '✅ Found'
|
||||
result['status_class'] = 'found'
|
||||
result['spotify_track'] = spotify_track['name']
|
||||
result['spotify_artist'] = ', '.join(spotify_track['artists']) if isinstance(spotify_track['artists'], list) else spotify_track['artists']
|
||||
result['spotify_artist'] = _join_artist_names(spotify_track['artists']) if isinstance(spotify_track['artists'], list) else _extract_artist_name(spotify_track['artists'])
|
||||
result['spotify_album'] = spotify_track['album']
|
||||
result['spotify_id'] = spotify_track['id']
|
||||
|
||||
|
|
@ -21669,7 +21688,7 @@ def _run_youtube_discovery_worker(url_hash):
|
|||
'status': '✅ Found',
|
||||
'status_class': 'found',
|
||||
'spotify_track': cached_match.get('name', ''),
|
||||
'spotify_artist': cached_match.get('artists', [''])[0] if cached_match.get('artists') else '',
|
||||
'spotify_artist': _extract_artist_name(cached_match.get('artists', [''])[0]) if cached_match.get('artists') else '',
|
||||
'spotify_album': cached_match.get('album', {}).get('name', '') if isinstance(cached_match.get('album'), dict) else cached_match.get('album', ''),
|
||||
'duration': f"{track['duration_ms'] // 60000}:{(track['duration_ms'] % 60000) // 1000:02d}" if track['duration_ms'] else '0:00',
|
||||
'discovery_source': discovery_source,
|
||||
|
|
@ -21807,7 +21826,7 @@ def _run_youtube_discovery_worker(url_hash):
|
|||
'status': '✅ Found' if matched_track else '❌ Not Found',
|
||||
'status_class': 'found' if matched_track else 'not-found',
|
||||
'spotify_track': matched_track.name if matched_track else '',
|
||||
'spotify_artist': matched_track.artists[0] if matched_track else '',
|
||||
'spotify_artist': _extract_artist_name(matched_track.artists[0]) if matched_track else '',
|
||||
'spotify_album': matched_track.album if matched_track else '',
|
||||
'duration': f"{track['duration_ms'] // 60000}:{(track['duration_ms'] % 60000) // 1000:02d}" if track['duration_ms'] else '0:00',
|
||||
'discovery_source': discovery_source,
|
||||
|
|
@ -21932,7 +21951,7 @@ def _run_listenbrainz_discovery_worker(playlist_mbid):
|
|||
'status': '✅ Found',
|
||||
'status_class': 'found',
|
||||
'spotify_track': cached_match.get('name', ''),
|
||||
'spotify_artist': cached_match.get('artists', [''])[0] if cached_match.get('artists') else '',
|
||||
'spotify_artist': _extract_artist_name(cached_match.get('artists', [''])[0]) if cached_match.get('artists') else '',
|
||||
'spotify_album': cached_match.get('album', {}).get('name', '') if isinstance(cached_match.get('album'), dict) else cached_match.get('album', ''),
|
||||
'duration': f"{duration_ms // 60000}:{(duration_ms % 60000) // 1000:02d}" if duration_ms else '0:00',
|
||||
'discovery_source': discovery_source,
|
||||
|
|
@ -22069,7 +22088,7 @@ def _run_listenbrainz_discovery_worker(playlist_mbid):
|
|||
'status': '✅ Found' if matched_track else '❌ Not Found',
|
||||
'status_class': 'found' if matched_track else 'not-found',
|
||||
'spotify_track': matched_track.name if matched_track else '',
|
||||
'spotify_artist': matched_track.artists[0] if matched_track else '',
|
||||
'spotify_artist': _extract_artist_name(matched_track.artists[0]) if matched_track else '',
|
||||
'spotify_album': matched_track.album if matched_track else '',
|
||||
'duration': f"{duration_ms // 60000}:{(duration_ms % 60000) // 1000:02d}" if duration_ms else '0:00',
|
||||
'discovery_source': discovery_source,
|
||||
|
|
@ -27033,7 +27052,7 @@ def update_listenbrainz_discovery_match():
|
|||
result['spotify_track'] = spotify_track.get('name', '') if spotify_track else ''
|
||||
# Join all artists (matching YouTube/Tidal/Beatport format)
|
||||
artists = spotify_track.get('artists', []) if spotify_track else []
|
||||
result['spotify_artist'] = ', '.join(artists) if isinstance(artists, list) else artists
|
||||
result['spotify_artist'] = _join_artist_names(artists) if isinstance(artists, list) else _extract_artist_name(artists)
|
||||
# Album comes as a string from the frontend fix modal
|
||||
album = spotify_track.get('album', '') if spotify_track else ''
|
||||
result['spotify_album'] = album if isinstance(album, str) else album.get('name', '') if isinstance(album, dict) else ''
|
||||
|
|
@ -28944,7 +28963,7 @@ def update_beatport_discovery_match():
|
|||
result['status'] = '✅ Found'
|
||||
result['status_class'] = 'found'
|
||||
result['spotify_track'] = spotify_track['name']
|
||||
result['spotify_artist'] = ', '.join(spotify_track['artists']) if isinstance(spotify_track['artists'], list) else spotify_track['artists']
|
||||
result['spotify_artist'] = _join_artist_names(spotify_track['artists']) if isinstance(spotify_track['artists'], list) else _extract_artist_name(spotify_track['artists'])
|
||||
result['spotify_album'] = spotify_track['album']
|
||||
result['spotify_id'] = spotify_track['id']
|
||||
|
||||
|
|
|
|||
|
|
@ -22292,6 +22292,12 @@ async function startYouTubeDiscovery(urlHash) {
|
|||
return;
|
||||
}
|
||||
|
||||
// Update frontend phase to match backend
|
||||
const state = listenbrainzPlaylistStates[urlHash] || youtubePlaylistStates[urlHash];
|
||||
if (state) {
|
||||
state.phase = 'discovering';
|
||||
}
|
||||
|
||||
// Start polling for progress
|
||||
startYouTubeDiscoveryPolling(urlHash);
|
||||
|
||||
|
|
@ -22570,9 +22576,15 @@ function openYouTubeDiscoveryModal(urlHash) {
|
|||
|
||||
// Set initial progress if we have discovery results
|
||||
if (state.discoveryResults && state.discoveryResults.length > 0) {
|
||||
// Compute progress from results if discoveryProgress is missing/zero
|
||||
let progress = state.discoveryProgress || 0;
|
||||
const matches = state.spotifyMatches || 0;
|
||||
if (progress === 0 && state.discoveryResults.length > 0 && state.playlist.tracks.length > 0) {
|
||||
progress = Math.min(100, Math.round((state.discoveryResults.length / state.playlist.tracks.length) * 100));
|
||||
}
|
||||
const progressData = {
|
||||
progress: state.discoveryProgress || 0,
|
||||
spotify_matches: state.spotifyMatches || 0,
|
||||
progress: progress,
|
||||
spotify_matches: matches || state.discoveryResults.filter(r => r.status_class === 'found').length,
|
||||
spotify_total: state.playlist.tracks.length,
|
||||
results: state.discoveryResults
|
||||
};
|
||||
|
|
@ -42422,7 +42434,9 @@ function renderMirroredCard(p, container) {
|
|||
`;
|
||||
card.addEventListener('click', () => {
|
||||
const st = youtubePlaylistStates[hash];
|
||||
if (st && st.phase && st.phase !== 'fresh') {
|
||||
// Treat as non-fresh if phase is set, or if a poller/discovery modal exists
|
||||
const hasActiveDiscovery = activeYouTubePollers[hash] || document.getElementById(`youtube-discovery-modal-${hash}`);
|
||||
if (st && ((st.phase && st.phase !== 'fresh') || hasActiveDiscovery)) {
|
||||
if (st.phase === 'downloading' || st.phase === 'download_complete') {
|
||||
// Open download modal directly (follows Tidal/YouTube card click pattern)
|
||||
const spotifyPlaylistId = st.convertedSpotifyPlaylistId;
|
||||
|
|
@ -42775,9 +42789,11 @@ async function clearMirroredDiscovery(playlistId, name) {
|
|||
const data = await res.json();
|
||||
if (data.success) {
|
||||
showToast(`Cleared discovery for ${name} (${data.cleared} tracks)`, 'success');
|
||||
// Also clear the discovery state so the card goes back to fresh
|
||||
// Also clear the discovery state and remove stale modal DOM
|
||||
const hash = `mirrored_${playlistId}`;
|
||||
delete youtubePlaylistStates[hash];
|
||||
const staleModal = document.getElementById(`youtube-discovery-modal-${hash}`);
|
||||
if (staleModal) staleModal.remove();
|
||||
loadMirroredPlaylists();
|
||||
} else {
|
||||
showToast(data.error || 'Failed to clear discovery', 'error');
|
||||
|
|
@ -43240,9 +43256,10 @@ async function discoverMirroredPlaylist(playlistId) {
|
|||
|
||||
// If state already exists (discovery in progress or completed), just reopen the modal
|
||||
const existingState = youtubePlaylistStates[tempHash];
|
||||
if (existingState && existingState.phase !== 'fresh') {
|
||||
const hasActiveDiscovery = activeYouTubePollers[tempHash] || document.getElementById(`youtube-discovery-modal-${tempHash}`);
|
||||
if (existingState && (existingState.phase !== 'fresh' || hasActiveDiscovery)) {
|
||||
openYouTubeDiscoveryModal(tempHash);
|
||||
// Resume polling if discovery is in progress
|
||||
// Resume polling if discovery is in progress but poller stopped
|
||||
if (existingState.phase === 'discovering' && !activeYouTubePollers[tempHash]) {
|
||||
startYouTubeDiscoveryPolling(tempHash);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28886,23 +28886,45 @@ body {
|
|||
.automations-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
/* --- New Automation Button --- */
|
||||
/* --- New Automation Button (rainbow glow) --- */
|
||||
@keyframes btn-rainbow-glow {
|
||||
0% { box-shadow: 0 4px 18px rgba(255, 0, 100, 0.3), 0 2px 6px rgba(0,0,0,0.2); border-color: rgba(255, 0, 100, 0.35); }
|
||||
20% { box-shadow: 0 4px 18px rgba(255, 140, 0, 0.3), 0 2px 6px rgba(0,0,0,0.2); border-color: rgba(255, 140, 0, 0.35); }
|
||||
40% { box-shadow: 0 4px 18px rgba(0, 210, 120, 0.3), 0 2px 6px rgba(0,0,0,0.2); border-color: rgba(0, 210, 120, 0.35); }
|
||||
60% { box-shadow: 0 4px 18px rgba(0, 140, 255, 0.3), 0 2px 6px rgba(0,0,0,0.2); border-color: rgba(0, 140, 255, 0.35); }
|
||||
80% { box-shadow: 0 4px 18px rgba(160, 0, 255, 0.3), 0 2px 6px rgba(0,0,0,0.2); border-color: rgba(160, 0, 255, 0.35); }
|
||||
100% { box-shadow: 0 4px 18px rgba(255, 0, 100, 0.3), 0 2px 6px rgba(0,0,0,0.2); border-color: rgba(255, 0, 100, 0.35); }
|
||||
}
|
||||
@keyframes btn-rainbow-glow-hover {
|
||||
0% { box-shadow: 0 6px 28px rgba(255, 0, 100, 0.45), 0 3px 10px rgba(0,0,0,0.25); border-color: rgba(255, 0, 100, 0.5); }
|
||||
20% { box-shadow: 0 6px 28px rgba(255, 140, 0, 0.45), 0 3px 10px rgba(0,0,0,0.25); border-color: rgba(255, 140, 0, 0.5); }
|
||||
40% { box-shadow: 0 6px 28px rgba(0, 210, 120, 0.45), 0 3px 10px rgba(0,0,0,0.25); border-color: rgba(0, 210, 120, 0.5); }
|
||||
60% { box-shadow: 0 6px 28px rgba(0, 140, 255, 0.45), 0 3px 10px rgba(0,0,0,0.25); border-color: rgba(0, 140, 255, 0.5); }
|
||||
80% { box-shadow: 0 6px 28px rgba(160, 0, 255, 0.45), 0 3px 10px rgba(0,0,0,0.25); border-color: rgba(160, 0, 255, 0.5); }
|
||||
100% { box-shadow: 0 6px 28px rgba(255, 0, 100, 0.45), 0 3px 10px rgba(0,0,0,0.25); border-color: rgba(255, 0, 100, 0.5); }
|
||||
}
|
||||
.auto-new-btn {
|
||||
background: linear-gradient(135deg, rgb(var(--accent-rgb)) 0%, rgb(var(--accent-light-rgb)) 100%);
|
||||
position: relative;
|
||||
background: linear-gradient(135deg, rgba(255,255,255,0.10) 0%, rgba(255,255,255,0.04) 100%);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border: 1.5px solid rgba(255,255,255,0.15);
|
||||
padding: 10px 22px;
|
||||
border-radius: 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
letter-spacing: 0.3px;
|
||||
animation: btn-rainbow-glow 6s linear infinite;
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
.auto-new-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(var(--accent-rgb), 0.4);
|
||||
filter: brightness(1.1);
|
||||
background: linear-gradient(135deg, rgba(255,255,255,0.16) 0%, rgba(255,255,255,0.08) 100%);
|
||||
animation: btn-rainbow-glow-hover 4s linear infinite;
|
||||
}
|
||||
.auto-new-btn:active {
|
||||
transform: translateY(0) scale(0.97);
|
||||
}
|
||||
|
||||
/* --- Stats Summary Bar --- */
|
||||
|
|
|
|||
Loading…
Reference in a new issue