scan db on artist page

pull/15/head
Broque Thomas 8 months ago
parent d13bd255d0
commit 245c6dfbf2

@ -1692,6 +1692,351 @@ def get_artist_discography(artist_id):
traceback.print_exc()
return jsonify({"error": str(e)}), 500
@app.route('/api/artist/<artist_id>/completion', methods=['POST'])
def check_artist_discography_completion(artist_id):
"""Check completion status for artist's albums and singles"""
try:
data = request.get_json()
if not data or 'discography' not in data:
return jsonify({"error": "Missing discography data"}), 400
discography = data['discography']
test_mode = data.get('test_mode', False) # Add test mode for demonstration
albums_completion = []
singles_completion = []
# Get database instance
from database.music_database import MusicDatabase
db = MusicDatabase()
# Get artist name - should be provided by the frontend
artist_name = data.get('artist_name', 'Unknown Artist')
# If no artist name provided, try to infer it from the request
if artist_name == 'Unknown Artist':
print(f"⚠️ No artist name provided in request, attempting to infer from discography data")
# Try to extract from first album's title by using a simple search
all_items = discography.get('albums', []) + discography.get('singles', [])
if all_items and spotify_client and spotify_client.is_authenticated():
try:
first_item = all_items[0]
# Search for the first track to get artist name
search_results = spotify_client.search_tracks(first_item.get('name', ''), limit=1)
if search_results and len(search_results) > 0:
artist_name = search_results[0].artists[0] if search_results[0].artists else "Unknown Artist"
print(f"🎤 Inferred artist name from search: {artist_name}")
except Exception as e:
print(f"⚠️ Could not infer artist name: {e}")
artist_name = "Unknown Artist"
print(f"🎤 Checking completion for artist: {artist_name}")
# Process albums
for album in discography.get('albums', []):
completion_data = _check_album_completion(db, album, artist_name, test_mode)
albums_completion.append(completion_data)
# Process singles/EPs
for single in discography.get('singles', []):
completion_data = _check_single_completion(db, single, artist_name, test_mode)
singles_completion.append(completion_data)
return jsonify({
"albums": albums_completion,
"singles": singles_completion
})
except Exception as e:
print(f"❌ Error checking discography completion: {e}")
import traceback
traceback.print_exc()
return jsonify({"error": str(e)}), 500
def _check_album_completion(db: 'MusicDatabase', album_data: dict, artist_name: str, test_mode: bool = False) -> dict:
"""Check completion status for a single album"""
try:
album_name = album_data.get('name', '')
total_tracks = album_data.get('total_tracks', 0)
album_id = album_data.get('id', '')
print(f"🔍 Checking album: '{album_name}' ({total_tracks} tracks)")
if test_mode:
# Generate test data to demonstrate the feature
import random
owned_tracks = random.randint(0, max(1, total_tracks))
expected_tracks = total_tracks
confidence = random.uniform(0.7, 1.0)
db_album = True # Simulate found album
print(f"🧪 TEST MODE: Simulating {owned_tracks}/{expected_tracks} tracks for '{album_name}'")
else:
# Check if album exists in database with completeness info
try:
db_album, confidence, owned_tracks, expected_tracks, is_complete = db.check_album_exists_with_completeness(
title=album_name,
artist=artist_name,
expected_track_count=total_tracks if total_tracks > 0 else None,
confidence_threshold=0.7 # Slightly lower threshold for better matching
)
except Exception as db_error:
print(f"⚠️ Database error for album '{album_name}': {db_error}")
# Return error state for this album
return {
"id": album_id,
"name": album_name,
"status": "error",
"owned_tracks": 0,
"expected_tracks": total_tracks,
"completion_percentage": 0,
"confidence": 0.0,
"found_in_db": False,
"error_message": str(db_error)
}
# Calculate completion percentage
if expected_tracks > 0:
completion_percentage = (owned_tracks / expected_tracks) * 100
elif total_tracks > 0:
completion_percentage = (owned_tracks / total_tracks) * 100
else:
completion_percentage = 100 if owned_tracks > 0 else 0
# Determine completion status based on percentage
if completion_percentage >= 90 and owned_tracks > 0:
status = "completed"
elif completion_percentage >= 60:
status = "nearly_complete"
elif completion_percentage > 0:
status = "partial"
else:
status = "missing"
print(f" 📊 Result: {owned_tracks}/{expected_tracks or total_tracks} tracks ({completion_percentage:.1f}%) - {status}")
return {
"id": album_id,
"name": album_name,
"status": status,
"owned_tracks": owned_tracks,
"expected_tracks": expected_tracks or total_tracks,
"completion_percentage": round(completion_percentage, 1),
"confidence": round(confidence, 2) if confidence else 0.0,
"found_in_db": db_album is not None
}
except Exception as e:
print(f"❌ Error checking album completion for '{album_data.get('name', 'Unknown')}': {e}")
return {
"id": album_data.get('id', ''),
"name": album_data.get('name', 'Unknown'),
"status": "error",
"owned_tracks": 0,
"expected_tracks": album_data.get('total_tracks', 0),
"completion_percentage": 0,
"confidence": 0.0,
"found_in_db": False
}
def _check_single_completion(db: 'MusicDatabase', single_data: dict, artist_name: str, test_mode: bool = False) -> dict:
"""Check completion status for a single/EP (treat EPs like albums, singles as single tracks)"""
try:
single_name = single_data.get('name', '')
total_tracks = single_data.get('total_tracks', 1)
single_id = single_data.get('id', '')
album_type = single_data.get('album_type', 'single')
print(f"🎵 Checking {album_type}: '{single_name}' ({total_tracks} tracks)")
if test_mode:
# Generate test data for singles/EPs
import random
if album_type == 'ep' or total_tracks > 1:
owned_tracks = random.randint(0, total_tracks)
expected_tracks = total_tracks
confidence = random.uniform(0.7, 1.0)
print(f"🧪 TEST MODE: EP with {owned_tracks}/{expected_tracks} tracks")
else:
owned_tracks = random.choice([0, 1]) # 50/50 chance
expected_tracks = 1
confidence = random.uniform(0.7, 1.0) if owned_tracks else 0.0
print(f"🧪 TEST MODE: Single with {owned_tracks}/{expected_tracks} tracks")
elif album_type == 'ep' or total_tracks > 1:
# Treat EPs like albums
try:
db_album, confidence, owned_tracks, expected_tracks, is_complete = db.check_album_exists_with_completeness(
title=single_name,
artist=artist_name,
expected_track_count=total_tracks,
confidence_threshold=0.7
)
except Exception as db_error:
print(f"⚠️ Database error for EP '{single_name}': {db_error}")
owned_tracks, expected_tracks, confidence = 0, total_tracks, 0.0
# Calculate completion percentage
if expected_tracks > 0:
completion_percentage = (owned_tracks / expected_tracks) * 100
else:
completion_percentage = (owned_tracks / total_tracks) * 100
# Determine status
if completion_percentage >= 90 and owned_tracks > 0:
status = "completed"
elif completion_percentage >= 60:
status = "nearly_complete"
elif completion_percentage > 0:
status = "partial"
else:
status = "missing"
print(f" 📊 EP Result: {owned_tracks}/{expected_tracks or total_tracks} tracks ({completion_percentage:.1f}%) - {status}")
else:
# Single track - just check if the track exists
try:
db_track, confidence = db.check_track_exists(
title=single_name,
artist=artist_name,
confidence_threshold=0.7
)
except Exception as db_error:
print(f"⚠️ Database error for single '{single_name}': {db_error}")
db_track, confidence = None, 0.0
owned_tracks = 1 if db_track else 0
expected_tracks = 1
completion_percentage = 100 if db_track else 0
status = "completed" if db_track else "missing"
print(f" 🎵 Single Result: {owned_tracks}/1 tracks ({completion_percentage:.1f}%) - {status}")
return {
"id": single_id,
"name": single_name,
"status": status,
"owned_tracks": owned_tracks,
"expected_tracks": expected_tracks or total_tracks,
"completion_percentage": round(completion_percentage, 1),
"confidence": round(confidence, 2) if confidence else 0.0,
"found_in_db": (db_album if album_type == 'ep' or total_tracks > 1 else db_track) is not None,
"type": album_type
}
except Exception as e:
print(f"❌ Error checking single/EP completion for '{single_data.get('name', 'Unknown')}': {e}")
return {
"id": single_data.get('id', ''),
"name": single_data.get('name', 'Unknown'),
"status": "error",
"owned_tracks": 0,
"expected_tracks": single_data.get('total_tracks', 1),
"completion_percentage": 0,
"confidence": 0.0,
"found_in_db": False,
"type": single_data.get('album_type', 'single')
}
@app.route('/api/artist/<artist_id>/completion-stream', methods=['POST'])
def check_artist_discography_completion_stream(artist_id):
"""Stream completion status for artist's albums and singles one by one"""
# Capture request data BEFORE the generator function
try:
data = request.get_json()
if not data or 'discography' not in data:
return jsonify({"error": "Missing discography data"}), 400
except Exception as e:
return jsonify({"error": "Invalid request data"}), 400
# Extract data for the generator
discography = data['discography']
test_mode = data.get('test_mode', False)
artist_name = data.get('artist_name', 'Unknown Artist')
def generate_completion_stream():
try:
print(f"🎤 Starting streaming completion check for artist: {artist_name}")
# Get database instance
from database.music_database import MusicDatabase
db = MusicDatabase()
# Process albums one by one
total_items = len(discography.get('albums', [])) + len(discography.get('singles', []))
processed_count = 0
# Send initial status
yield f"data: {json.dumps({'type': 'start', 'total_items': total_items, 'artist_name': artist_name})}\n\n"
# Process albums
for album in discography.get('albums', []):
try:
completion_data = _check_album_completion(db, album, artist_name, test_mode)
completion_data['type'] = 'album_completion'
completion_data['container_type'] = 'albums'
processed_count += 1
completion_data['progress'] = round((processed_count / total_items) * 100, 1)
yield f"data: {json.dumps(completion_data)}\n\n"
# Small delay to make the streaming effect visible
import time
time.sleep(0.1) # 100ms delay between items
except Exception as e:
error_data = {
'type': 'error',
'container_type': 'albums',
'id': album.get('id', ''),
'name': album.get('name', 'Unknown'),
'error': str(e)
}
yield f"data: {json.dumps(error_data)}\n\n"
# Process singles/EPs
for single in discography.get('singles', []):
try:
completion_data = _check_single_completion(db, single, artist_name, test_mode)
completion_data['type'] = 'single_completion'
completion_data['container_type'] = 'singles'
processed_count += 1
completion_data['progress'] = round((processed_count / total_items) * 100, 1)
yield f"data: {json.dumps(completion_data)}\n\n"
# Small delay to make the streaming effect visible
time.sleep(0.1) # 100ms delay between items
except Exception as e:
error_data = {
'type': 'error',
'container_type': 'singles',
'id': single.get('id', ''),
'name': single.get('name', 'Unknown'),
'error': str(e)
}
yield f"data: {json.dumps(error_data)}\n\n"
# Send completion signal
yield f"data: {json.dumps({'type': 'complete', 'processed_count': processed_count})}\n\n"
except Exception as e:
print(f"❌ Error in streaming completion check: {e}")
import traceback
traceback.print_exc()
yield f"data: {json.dumps({'type': 'error', 'error': str(e)})}\n\n"
return Response(
generate_completion_stream(),
content_type='text/event-stream',
headers={
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Cache-Control'
}
)
@app.route('/api/stream/start', methods=['POST'])
def stream_start():
"""Start streaming a track in the background"""

@ -9375,7 +9375,10 @@ async function loadArtistDiscography(artistId) {
// Check cache first
if (artistsPageState.cache.discography[artistId]) {
console.log('📦 Using cached discography');
displayArtistDiscography(artistsPageState.cache.discography[artistId]);
const cachedDiscography = artistsPageState.cache.discography[artistId];
displayArtistDiscography(cachedDiscography);
// Still check completion status for cached data
await checkDiscographyCompletion(artistId, cachedDiscography);
return;
}
@ -9413,6 +9416,9 @@ async function loadArtistDiscography(artistId) {
// Display results
displayArtistDiscography(discography);
// Check completion status for all albums and singles
await checkDiscographyCompletion(artistId, discography);
} catch (error) {
console.error('❌ Failed to load discography:', error);
showDiscographyError(error.message);
@ -9476,6 +9482,183 @@ function displayArtistDiscography(discography) {
}
}
/**
* Check completion status for entire discography with streaming updates
*/
async function checkDiscographyCompletion(artistId, discography) {
console.log(`🔍 Starting streaming completion check for artist: ${artistId}`);
try {
// Use EventSource for Server-Sent Events streaming
const eventSource = new EventSource('data:text/plain,'); // Dummy EventSource
// Use fetch with streaming response
const response = await fetch(`/api/artist/${artistId}/completion-stream`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
discography: discography,
artist_name: artistsPageState.selectedArtist?.name || 'Unknown Artist',
test_mode: window.location.search.includes('test=true')
})
});
if (!response.ok) {
throw new Error(`Failed to start completion check: ${response.status}`);
}
// Handle streaming response
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.slice(6));
handleStreamingCompletionUpdate(data);
} catch (e) {
console.warn('Failed to parse streaming data:', line);
}
}
}
}
} catch (error) {
console.error('❌ Failed to check completion status:', error);
showCompletionError();
}
}
/**
* Handle individual streaming completion updates
*/
function handleStreamingCompletionUpdate(data) {
console.log('🔄 Streaming update received:', data.type, data.name || data.artist_name);
switch (data.type) {
case 'start':
console.log(`🎤 Starting completion check for ${data.artist_name} (${data.total_items} items)`);
// Could show a progress indicator here
break;
case 'album_completion':
updateAlbumCompletionOverlay(data, 'albums');
console.log(`📀 Updated album: ${data.name} (${data.status})`);
break;
case 'single_completion':
updateAlbumCompletionOverlay(data, 'singles');
console.log(`🎵 Updated single: ${data.name} (${data.status})`);
break;
case 'error':
console.error('❌ Error processing item:', data.name, data.error);
// Could show error for specific item
break;
case 'complete':
console.log(`✅ Completion check finished (${data.processed_count} items processed)`);
break;
default:
console.log('Unknown streaming update type:', data.type);
}
}
/**
* Update completion overlay for a specific album/single
*/
function updateAlbumCompletionOverlay(completionData, containerType) {
const containerId = containerType === 'albums' ? 'album-cards-container' : 'singles-cards-container';
const container = document.getElementById(containerId);
if (!container) {
console.warn(`Container ${containerId} not found`);
return;
}
// Find the album card by data-album-id
const albumCard = container.querySelector(`[data-album-id="${completionData.id}"]`);
if (!albumCard) {
console.warn(`Album card not found for ID: ${completionData.id}`);
return;
}
const overlay = albumCard.querySelector('.completion-overlay');
if (!overlay) {
console.warn(`Completion overlay not found for album: ${completionData.name}`);
return;
}
// Remove existing status classes
overlay.classList.remove('checking', 'completed', 'nearly_complete', 'partial', 'missing', 'error');
// Add new status class
overlay.classList.add(completionData.status);
// Update overlay text and content
const statusText = getCompletionStatusText(completionData);
const progressText = `${completionData.owned_tracks}/${completionData.expected_tracks}`;
overlay.innerHTML = `
<span class="completion-status">${statusText}</span>
<span class="completion-progress">${progressText}</span>
`;
// Add tooltip with more details
overlay.title = `${completionData.name}\n${statusText} (${completionData.completion_percentage}%)\nTracks: ${completionData.owned_tracks}/${completionData.expected_tracks}\nConfidence: ${completionData.confidence}`;
// Add brief flash animation to indicate update
overlay.style.animation = 'none';
overlay.offsetHeight; // Trigger reflow
overlay.style.animation = 'completionOverlayFadeIn 0.6s cubic-bezier(0.4, 0, 0.2, 1)';
console.log(`📊 Updated overlay for "${completionData.name}": ${statusText} (${completionData.completion_percentage}%)`);
}
/**
* Get human-readable status text for completion overlay
*/
function getCompletionStatusText(completionData) {
switch (completionData.status) {
case 'completed':
return 'Complete';
case 'nearly_complete':
return 'Nearly Complete';
case 'partial':
return 'Partial';
case 'missing':
return 'Missing';
case 'error':
return 'Error';
default:
return 'Unknown';
}
}
/**
* Show error state on all completion overlays
*/
function showCompletionError() {
const allOverlays = document.querySelectorAll('.completion-overlay.checking');
allOverlays.forEach(overlay => {
overlay.classList.remove('checking');
overlay.classList.add('error');
overlay.innerHTML = '<span class="completion-status">Error</span>';
overlay.title = 'Failed to check completion status';
});
}
/**
* Create HTML for an album/single card
*/
@ -9491,8 +9674,11 @@ function createAlbumCard(album) {
`background: linear-gradient(135deg, rgba(29, 185, 84, 0.2) 0%, rgba(24, 156, 71, 0.1) 100%);`;
return `
<div class="album-card" data-album-id="${album.id}">
<div class="album-card" data-album-id="${album.id}" data-album-name="${escapeHtml(album.name)}" data-album-type="${album.album_type}" data-total-tracks="${album.total_tracks || 0}">
<div class="album-card-image" style="${backgroundStyle}"></div>
<div class="completion-overlay checking">
<span class="completion-status">Checking...</span>
</div>
<div class="album-card-content">
<div class="album-card-name" title="${escapeHtml(album.name)}">${escapeHtml(album.name)}</div>
<div class="album-card-year">${year || 'Unknown'}</div>

@ -5908,7 +5908,7 @@ body {
/* Elegant shadow system */
box-shadow:
0 10px 30px rgba(0, 0, 0, 0.5),
0 10px 20px rgba(0, 0, 0, 0.5),
0 0 0 1px rgba(29, 185, 84, 0.05),
inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
@ -6361,6 +6361,140 @@ body {
font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif;
}
/* Completion Status Overlay */
.completion-overlay {
position: absolute;
top: 12px;
right: 12px;
padding: 6px 12px;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.8px;
border-radius: 12px;
backdrop-filter: blur(8px);
border: 1px solid;
z-index: 10;
font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif;
/* Smooth entrance animation */
animation: completionOverlayFadeIn 0.6s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
/* Completion status variants */
.completion-overlay.completed {
background: linear-gradient(135deg,
rgba(46, 204, 64, 0.9) 0%,
rgba(34, 139, 47, 0.95) 100%);
color: #ffffff;
border-color: rgba(46, 204, 64, 0.6);
box-shadow:
0 4px 12px rgba(0, 0, 0, 0.3),
0 0 0 1px rgba(46, 204, 64, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
}
.completion-overlay.nearly_complete {
background: linear-gradient(135deg,
rgba(255, 193, 7, 0.9) 0%,
rgba(255, 152, 0, 0.95) 100%);
color: #ffffff;
border-color: rgba(255, 193, 7, 0.6);
box-shadow:
0 4px 12px rgba(0, 0, 0, 0.3),
0 0 0 1px rgba(255, 193, 7, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
}
.completion-overlay.partial {
background: linear-gradient(135deg,
rgba(255, 111, 97, 0.9) 0%,
rgba(255, 87, 51, 0.95) 100%);
color: #ffffff;
border-color: rgba(255, 111, 97, 0.6);
box-shadow:
0 4px 12px rgba(0, 0, 0, 0.3),
0 0 0 1px rgba(255, 111, 97, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
}
.completion-overlay.missing {
background: linear-gradient(135deg,
rgba(108, 117, 125, 0.9) 0%,
rgba(73, 80, 87, 0.95) 100%);
color: rgba(255, 255, 255, 0.9);
border-color: rgba(108, 117, 125, 0.6);
box-shadow:
0 4px 12px rgba(0, 0, 0, 0.3),
0 0 0 1px rgba(108, 117, 125, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
.completion-overlay.error {
background: linear-gradient(135deg,
rgba(220, 53, 69, 0.9) 0%,
rgba(176, 42, 55, 0.95) 100%);
color: #ffffff;
border-color: rgba(220, 53, 69, 0.6);
box-shadow:
0 4px 12px rgba(0, 0, 0, 0.3),
0 0 0 1px rgba(220, 53, 69, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
}
/* Hover effects for completion overlays */
.album-card:hover .completion-overlay,
.artist-card:hover .completion-overlay {
transform: scale(1.05);
box-shadow:
0 6px 16px rgba(0, 0, 0, 0.4),
0 0 0 1px currentColor,
inset 0 1px 0 rgba(255, 255, 255, 0.25);
}
/* Loading state for completion overlay */
.completion-overlay.checking {
background: linear-gradient(135deg,
rgba(29, 185, 84, 0.9) 0%,
rgba(24, 156, 71, 0.95) 100%);
color: #ffffff;
border-color: rgba(29, 185, 84, 0.6);
animation: completionOverlayPulse 2s ease-in-out infinite;
}
/* Progress indicator inside overlay for detailed completion info */
.completion-overlay .completion-progress {
display: block;
font-size: 9px;
margin-top: 2px;
opacity: 0.8;
font-weight: 500;
}
/* Completion overlay animations */
@keyframes completionOverlayFadeIn {
from {
opacity: 0;
transform: translateY(-4px) scale(0.8);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes completionOverlayPulse {
0%, 100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.7;
transform: scale(1.05);
}
}
/* Loading states for album cards */
.album-card.loading .album-card-image {
background: linear-gradient(

Loading…
Cancel
Save