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