@ -11701,276 +11701,22 @@ def check_artist_discography_completion(artist_id):
data = request . get_json ( )
if not data or ' discography ' not in data :
return jsonify ( { " error " : " Missing discography data " } ) , 400
from core . metadata_service import check_artist_discography_completion as _check_artist_discography_completion
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
} )
source_override = ( data . get ( ' source ' ) or ' ' ) . strip ( ) . lower ( ) or None
result = _check_artist_discography_completion (
discography ,
artist_name = data . get ( ' artist_name ' , ' Unknown Artist ' ) ,
source_override = source_override ,
)
return jsonify ( result )
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 , 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 ' , ' ' )
# If total_tracks is 0 (Discogs masters don't include track counts),
# try to fetch the real count from the source
if total_tracks == 0 and album_id :
try :
fallback = _get_metadata_fallback_client ( )
album_detail = fallback . get_album_tracks ( str ( album_id ) )
if album_detail and album_detail . get ( ' items ' ) :
total_tracks = len ( album_detail [ ' items ' ] )
logger . debug ( f " Fetched track count for ' { album_name } ' : { total_tracks } " )
except Exception :
pass
print ( f " Checking album: ' { album_name } ' ( { total_tracks } tracks) " )
formats = [ ]
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 :
# Get active server for database checking
active_server = config_manager . get_active_media_server ( )
db_album , confidence , owned_tracks , expected_tracks , is_complete , formats = 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
server_source = active_server # Check only the active server
)
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 ) ,
" formats " : [ ]
}
# 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 — exact match, no percentage rounding
if owned_tracks > 0 and owned_tracks > = ( expected_tracks or total_tracks ) :
status = " completed "
elif owned_tracks > 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 ,
" formats " : formats
}
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 ,
" formats " : [ ]
}
def _check_single_completion ( db , 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 ' )
formats = [ ]
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 :
# Get active server for database checking
active_server = config_manager . get_active_media_server ( )
db_album , confidence , owned_tracks , expected_tracks , is_complete , formats = db . check_album_exists_with_completeness (
title = single_name ,
artist = artist_name ,
expected_track_count = total_tracks ,
confidence_threshold = 0.7 ,
server_source = active_server # Check only the active server
)
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 — exact match, no percentage rounding
if owned_tracks > 0 and owned_tracks > = ( expected_tracks or total_tracks ) :
status = " completed "
elif owned_tracks > 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 :
active_server = config_manager . get_active_media_server ( )
db_track , confidence = db . check_track_exists (
title = single_name ,
artist = artist_name ,
confidence_threshold = 0.7 ,
server_source = active_server
)
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 "
# Extract format from single track
if db_track and db_track . file_path :
import os
ext = os . path . splitext ( db_track . file_path ) [ 1 ] . lstrip ( ' . ' ) . upper ( )
if ext == ' MP3 ' and db_track . bitrate :
formats = [ f " MP3- { db_track . bitrate } " ]
elif ext :
formats = [ ext ]
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 ,
" formats " : formats
}
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 ' ) ,
" formats " : [ ]
}
@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 """
@ -11984,75 +11730,22 @@ def check_artist_discography_completion_stream(artist_id):
# Extract data for the generator
discography = data [ ' discography ' ]
test_mode = data . get ( ' test_mode ' , False )
artist_name = data . get ( ' artist_name ' , ' Unknown Artist ' )
source_override = ( data . get ( ' source ' ) or ' ' ) . strip ( ) . lower ( ) or None
from core . metadata_service import iter_artist_discography_completion_events
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 "
for event in iter_artist_discography_completion_events (
discography ,
artist_name = artist_name ,
source_override = source_override ,
) :
yield f " data: { json . dumps ( event ) } \n \n "
if event . get ( ' type ' ) in ( ' album_completion ' , ' single_completion ' ) :
# 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 ' : ' 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
@ -12084,8 +11777,8 @@ def library_completion_stream():
def generate ( ) :
try :
from database. music_database import MusicDatabase
db = MusicD atabase( )
from core. metadata_service import check_album_completion , check_single_completion
db = get_d atabase( )
categories = [ ' albums ' , ' eps ' , ' singles ' ]
all_items = [ ]
@ -12106,9 +11799,9 @@ def library_completion_stream():
}
if category == ' singles ' :
result = _ check_single_completion( db , mapped , artist_name )
result = check_single_completion( db , mapped , artist_name )
else :
result = _ check_album_completion( db , mapped , artist_name )
result = check_album_completion( db , mapped , artist_name )
result [ ' spotify_id ' ] = item . get ( ' spotify_id ' , ' ' )
result [ ' category ' ] = category