@ -504,611 +504,16 @@ def start_sync():
@app.route ( ' /api/search ' , methods = [ ' POST ' ] )
def search_music ( ) :
"""
Perform real Soulseek search using the actual soulseek_client .
Returns progressive search results matching the GUI ' s SearchThread implementation.
"""
if not soulseek_client :
return jsonify ( { " error " : " Soulseek client not initialized " } ) , 500
# Placeholder: simulates a music search
data = request . get_json ( )
query = data . get ( ' query ' , ' ' ) . strip ( )
if not query :
return jsonify ( { " error " : " No search query provided " } ) , 400
print ( f " 🔍 Starting Soulseek search for: ' { query } ' " )
try :
import asyncio
# Create new event loop for this request
loop = asyncio . new_event_loop ( )
asyncio . set_event_loop ( loop )
try :
# Perform the actual search using soulseek_client
results = loop . run_until_complete ( soulseek_client . search ( query ) )
# Process results to match frontend expectations
if isinstance ( results , tuple ) and len ( results ) == 2 :
tracks , albums = results
else :
# Fallback for backward compatibility
tracks = results if isinstance ( results , list ) else [ ]
albums = [ ]
# Convert track results to JSON-serializable format
tracks_json = [ ]
for track in tracks :
tracks_json . append ( {
" type " : " track " ,
" title " : getattr ( track , ' title ' , ' Unknown Title ' ) ,
" artist " : getattr ( track , ' artist ' , ' Unknown Artist ' ) ,
" album " : getattr ( track , ' album ' , ' Unknown Album ' ) ,
" quality " : getattr ( track , ' quality ' , ' Unknown ' ) ,
" bitrate " : getattr ( track , ' bitrate ' , None ) ,
" duration " : getattr ( track , ' duration ' , None ) ,
" filename " : getattr ( track , ' filename ' , ' ' ) ,
" username " : getattr ( track , ' username ' , ' ' ) ,
" file_size " : getattr ( track , ' file_size ' , 0 ) ,
" search_result_data " : {
# Store the original object data for download purposes
" filename " : getattr ( track , ' filename ' , ' ' ) ,
" username " : getattr ( track , ' username ' , ' ' ) ,
" file_size " : getattr ( track , ' file_size ' , 0 ) ,
}
} )
# Convert album results to JSON-serializable format
albums_json = [ ]
for album in albums :
albums_json . append ( {
" type " : " album " ,
" title " : getattr ( album , ' album_name ' , getattr ( album , ' title ' , ' Unknown Album ' ) ) ,
" artist " : getattr ( album , ' artist ' , ' Unknown Artist ' ) ,
" track_count " : getattr ( album , ' track_count ' , 0 ) ,
" username " : getattr ( album , ' username ' , ' ' ) ,
" size_mb " : getattr ( album , ' total_size ' , 0 ) / ( 1024 * 1024 ) if hasattr ( album , ' total_size ' ) else 0 ,
" tracks " : getattr ( album , ' tracks ' , [ ] ) ,
" search_result_data " : {
# Store the original object data for download purposes
" album_name " : getattr ( album , ' album_name ' , getattr ( album , ' title ' , ' ' ) ) ,
" artist " : getattr ( album , ' artist ' , ' ' ) ,
" username " : getattr ( album , ' username ' , ' ' ) ,
" tracks " : getattr ( album , ' tracks ' , [ ] )
}
} )
total_results = len ( tracks_json ) + len ( albums_json )
print ( f " ✅ Search completed: { len ( tracks_json ) } tracks, { len ( albums_json ) } albums ( { total_results } total) " )
return jsonify ( {
" success " : True ,
" results " : {
" tracks " : tracks_json ,
" albums " : albums_json ,
" total_tracks " : len ( tracks_json ) ,
" total_albums " : len ( albums_json ) ,
" query " : query
}
} )
finally :
# Clean up the event loop
try :
pending = asyncio . all_tasks ( loop )
for task in pending :
task . cancel ( )
if pending :
loop . run_until_complete ( asyncio . gather ( * pending , return_exceptions = True ) )
loop . close ( )
except Exception as e :
print ( f " Error cleaning up search event loop: { e } " )
except Exception as e :
import traceback
traceback . print_exc ( )
print ( f " ❌ Search failed: { e } " )
return jsonify ( { " error " : f " Search failed: { str ( e ) } " } ) , 500
@app.route ( ' /api/search/cancel ' , methods = [ ' POST ' ] )
def cancel_search ( ) :
""" Cancel any active search operations """
# Note: In a full implementation, you would track active search operations
# and cancel them here. For now, this is a placeholder.
print ( " 🛑 Search cancellation requested " )
return jsonify ( { " success " : True , " message " : " Search cancellation requested " } )
# Global download tracking
active_downloads = { } # Dict to track active downloads
completed_downloads = [ ] # List to store completed downloads
@app.route ( ' /api/downloads/start ' , methods = [ ' POST ' ] )
def start_download ( ) :
"""
Start a regular download using the soulseek_client .
This matches the GUI ' s start_download functionality.
"""
if not soulseek_client :
return jsonify ( { " error " : " Soulseek client not initialized " } ) , 500
data = request . get_json ( )
if not data :
return jsonify ( { " error " : " No download data provided " } ) , 400
try :
# Extract search result data
search_data = data . get ( ' search_result_data ' , data )
filename = search_data . get ( ' filename ' )
username = search_data . get ( ' username ' )
if not filename or not username :
return jsonify ( { " error " : " Missing required download parameters (filename, username) " } ) , 400
print ( f " ⬇️ Starting download: ' { filename } ' from ' { username } ' " )
# Create download item for tracking
download_id = f " { username } _ { filename } _ { len ( active_downloads ) } "
download_item = {
" id " : download_id ,
" title " : data . get ( ' title ' , filename ) ,
" artist " : data . get ( ' artist ' , ' Unknown Artist ' ) ,
" filename " : filename ,
" username " : username ,
" status " : " queued " ,
" progress " : 0 ,
" file_size " : data . get ( ' file_size ' , 0 ) ,
" download_speed " : 0 ,
" eta " : None ,
" start_time " : None ,
" spotify_matched " : False
}
active_downloads [ download_id ] = download_item
# Start the actual download using asyncio
import asyncio
import threading
def download_worker ( ) :
loop = asyncio . new_event_loop ( )
asyncio . set_event_loop ( loop )
try :
# This would call the actual soulseek_client.download method
# For now, we'll simulate the download process
result = loop . run_until_complete ( simulate_download ( download_item ) )
print ( f " ✅ Download completed: { download_id } " )
except Exception as e :
print ( f " ❌ Download failed: { download_id } - { e } " )
download_item [ " status " ] = " failed "
download_item [ " error " ] = str ( e )
finally :
loop . close ( )
# Start download in background thread
thread = threading . Thread ( target = download_worker )
thread . daemon = True
thread . start ( )
return jsonify ( {
" success " : True ,
" download_id " : download_id ,
" message " : f " Download started for ' { filename } ' "
} )
except Exception as e :
import traceback
traceback . print_exc ( )
return jsonify ( { " error " : f " Failed to start download: { str ( e ) } " } ) , 500
@app.route ( ' /api/downloads/start-matched ' , methods = [ ' POST ' ] )
def start_matched_download ( ) :
"""
Start a download with confirmed Spotify match data .
This matches the GUI ' s start_matched_download functionality.
"""
if not soulseek_client :
return jsonify ( { " error " : " Soulseek client not initialized " } ) , 500
data = request . get_json ( )
if not data :
return jsonify ( { " error " : " No download data provided " } ) , 400
try :
# Extract search result data and Spotify match data
search_data = data . get ( ' search_result_data ' , data )
spotify_match = data . get ( ' spotify_match ' , { } )
filename = search_data . get ( ' filename ' )
username = search_data . get ( ' username ' )
if not filename or not username :
return jsonify ( { " error " : " Missing required download parameters " } ) , 400
matched_artist = spotify_match . get ( ' artist ' , { } )
matched_album = spotify_match . get ( ' album ' , { } )
print ( f " ⬇️🎵 Starting matched download: ' { filename } ' from ' { username } ' " )
print ( f " 🎤 Matched Artist: { matched_artist . get ( ' name ' , ' Unknown ' ) } " )
print ( f " 💿 Matched Album: { matched_album . get ( ' name ' , ' Unknown ' ) } " )
# Create download item for tracking with Spotify match info
download_id = f " { username } _ { filename } _ { len ( active_downloads ) } _matched "
download_item = {
" id " : download_id ,
" title " : data . get ( ' title ' , filename ) ,
" artist " : data . get ( ' artist ' , ' Unknown Artist ' ) ,
" filename " : filename ,
" username " : username ,
" status " : " queued " ,
" progress " : 0 ,
" file_size " : data . get ( ' file_size ' , 0 ) ,
" download_speed " : 0 ,
" eta " : None ,
" start_time " : None ,
" spotify_matched " : True ,
" matched_artist " : matched_artist ,
" matched_album " : matched_album
}
active_downloads [ download_id ] = download_item
# Start the actual download using asyncio
import asyncio
import threading
def matched_download_worker ( ) :
loop = asyncio . new_event_loop ( )
asyncio . set_event_loop ( loop )
try :
# This would call the actual soulseek_client.download method
# and then apply metadata enhancement with the Spotify match
result = loop . run_until_complete ( simulate_matched_download ( download_item ) )
print ( f " ✅ Matched download completed: { download_id } " )
except Exception as e :
print ( f " ❌ Matched download failed: { download_id } - { e } " )
download_item [ " status " ] = " failed "
download_item [ " error " ] = str ( e )
finally :
loop . close ( )
# Start download in background thread
thread = threading . Thread ( target = matched_download_worker )
thread . daemon = True
thread . start ( )
return jsonify ( {
" success " : True ,
" download_id " : download_id ,
" message " : f " Matched download started for ' { filename } ' "
} )
except Exception as e :
import traceback
traceback . print_exc ( )
return jsonify ( { " error " : f " Failed to start matched download: { str ( e ) } " } ) , 500
@app.route ( ' /api/downloads/status ' , methods = [ ' GET ' ] )
def get_download_status ( ) :
"""
Get the current status of all downloads ( active and completed ) .
This matches the GUI ' s download queue functionality.
"""
try :
# Get real download status from soulseek_client if available
real_downloads = [ ]
if soulseek_client :
try :
import asyncio
loop = asyncio . new_event_loop ( )
asyncio . set_event_loop ( loop )
try :
# This would call soulseek_client.get_all_downloads()
# For now, we'll use our tracked downloads
pass
finally :
loop . close ( )
except Exception as e :
print ( f " Error getting real download status: { e } " )
# Separate active and completed downloads
active = [ ]
completed = [ ]
for download_id , download in active_downloads . items ( ) :
if download [ " status " ] in [ " downloading " , " queued " ] :
active . append ( download )
else :
completed . append ( download )
# Add any completed downloads from our completed list
completed . extend ( completed_downloads )
return jsonify ( {
" success " : True ,
" downloads " : {
" active " : active ,
" completed " : completed ,
" active_count " : len ( active ) ,
" completed_count " : len ( completed )
}
} )
except Exception as e :
print ( f " Error getting download status: { e } " )
return jsonify ( { " error " : f " Failed to get download status: { str ( e ) } " } ) , 500
@app.route ( ' /api/downloads/cancel/<download_id> ' , methods = [ ' POST ' ] )
def cancel_download ( download_id ) :
""" Cancel a specific download """
if download_id in active_downloads :
download = active_downloads [ download_id ]
download [ " status " ] = " cancelled "
print ( f " 🛑 Download cancelled: { download_id } " )
return jsonify ( { " success " : True , " message " : f " Download { download_id } cancelled " } )
else :
return jsonify ( { " error " : " Download not found " } ) , 404
@app.route ( ' /api/downloads/clear-completed ' , methods = [ ' POST ' ] )
def clear_completed_downloads ( ) :
""" Clear all completed downloads from the queue """
global completed_downloads , active_downloads
# Remove completed downloads from active_downloads
to_remove = [ did for did , download in active_downloads . items ( )
if download [ " status " ] in [ " completed " , " failed " , " cancelled " ] ]
for download_id in to_remove :
del active_downloads [ download_id ]
# Clear completed downloads list
cleared_count = len ( completed_downloads )
completed_downloads . clear ( )
print ( f " 🗑️ Cleared { cleared_count + len ( to_remove ) } completed downloads " )
return jsonify ( {
" success " : True ,
" message " : f " Cleared { cleared_count + len ( to_remove ) } completed downloads "
} )
# Helper functions for simulating downloads (replace with real implementations)
async def simulate_download ( download_item ) :
""" Simulate a download process - replace with real soulseek_client.download() """
import asyncio
import time
download_item [ " status " ] = " downloading "
download_item [ " start_time " ] = time . time ( )
# Simulate download progress
for progress in range ( 0 , 101 , 10 ) :
download_item [ " progress " ] = progress
download_item [ " download_speed " ] = 1024 * 1024 # 1 MB/s simulation
await asyncio . sleep ( 0.1 ) # Simulate time
if download_item [ " status " ] == " cancelled " :
return False
download_item [ " status " ] = " completed "
download_item [ " progress " ] = 100
# Move to completed downloads
global completed_downloads
completed_downloads . append ( download_item . copy ( ) )
return True
async def simulate_matched_download ( download_item ) :
""" Simulate a matched download with metadata enhancement """
# First do the regular download
result = await simulate_download ( download_item )
if result and download_item . get ( " spotify_matched " ) :
print ( f " 🎵 Applying metadata enhancement for: { download_item [ ' title ' ] } " )
# Here you would apply the Spotify metadata enhancement
# using the matched_artist and matched_album data
download_item [ " metadata_enhanced " ] = True
return result
# ===== SPOTIFY INTEGRATION ENDPOINTS =====
@app.route ( ' /api/spotify/search-artist ' , methods = [ ' POST ' ] )
def spotify_search_artist ( ) :
"""
Search for artists using Spotify API for the matching modal .
This matches the GUI ' s ArtistSearchThread functionality.
"""
if not spotify_client or not spotify_client . is_authenticated ( ) :
return jsonify ( { " error " : " Spotify client not available or not authenticated " } ) , 500
data = request . get_json ( )
query = data . get ( ' query ' , ' ' ) . strip ( )
if not query :
return jsonify ( { " error " : " No search query provided " } ) , 400
try :
print ( f " 🎵 Searching Spotify for artist: ' { query } ' " )
# Perform artist search using spotify_client
artists = spotify_client . search_artists ( query , limit = 6 ) # Limit to 6 for modal display
# Convert artists to JSON format matching frontend expectations
artists_json = [ ]
for artist in artists :
artist_data = {
" id " : artist . id ,
" name " : artist . name ,
" genres " : getattr ( artist , ' genres ' , [ ] ) ,
" popularity " : getattr ( artist , ' popularity ' , 0 ) ,
" follower_count " : getattr ( artist , ' follower_count ' , 0 ) ,
" image_url " : getattr ( artist , ' image_url ' , None ) ,
" spotify_url " : getattr ( artist , ' spotify_url ' , None ) ,
}
artists_json . append ( artist_data )
print ( f " ✅ Found { len ( artists_json ) } artists for ' { query } ' " )
return jsonify ( {
" success " : True ,
" artists " : artists_json ,
" query " : query
} )
except Exception as e :
import traceback
traceback . print_exc ( )
print ( f " ❌ Spotify artist search failed: { e } " )
return jsonify ( { " error " : f " Artist search failed: { str ( e ) } " } ) , 500
@app.route ( ' /api/spotify/search-album ' , methods = [ ' POST ' ] )
def spotify_search_album ( ) :
"""
Search for albums by a specific artist using Spotify API .
This matches the GUI ' s AlbumSearchThread functionality.
"""
if not spotify_client or not spotify_client . is_authenticated ( ) :
return jsonify ( { " error " : " Spotify client not available or not authenticated " } ) , 500
data = request . get_json ( )
artist_id = data . get ( ' artist_id ' )
query = data . get ( ' query ' , ' ' ) . strip ( )
if not artist_id :
return jsonify ( { " error " : " No artist ID provided " } ) , 400
try :
print ( f " 💿 Searching albums for artist ID: { artist_id } " )
# Get albums by artist using spotify_client
albums = spotify_client . get_artist_albums ( artist_id , limit = 10 )
# If query is provided, filter albums by query
if query :
filtered_albums = [ ]
query_lower = query . lower ( )
for album in albums :
if query_lower in album . name . lower ( ) :
filtered_albums . append ( album )
albums = filtered_albums
# Convert albums to JSON format
albums_json = [ ]
for album in albums :
album_data = {
" id " : album . id ,
" name " : album . name ,
" release_date " : getattr ( album , ' release_date ' , ' ' ) ,
" total_tracks " : getattr ( album , ' total_tracks ' , 0 ) ,
" album_type " : getattr ( album , ' album_type ' , ' album ' ) ,
" image_url " : getattr ( album , ' image_url ' , None ) ,
" spotify_url " : getattr ( album , ' spotify_url ' , None ) ,
" artist " : {
" id " : artist_id ,
" name " : getattr ( album , ' artist_name ' , ' Unknown Artist ' )
}
}
albums_json . append ( album_data )
print ( f " ✅ Found { len ( albums_json ) } albums for artist { artist_id } " )
return jsonify ( {
" success " : True ,
" albums " : albums_json ,
" artist_id " : artist_id ,
" query " : query
} )
except Exception as e :
import traceback
traceback . print_exc ( )
print ( f " ❌ Spotify album search failed: { e } " )
return jsonify ( { " error " : f " Album search failed: { str ( e ) } " } ) , 500
@app.route ( ' /api/spotify/suggestions ' , methods = [ ' POST ' ] )
def spotify_generate_suggestions ( ) :
"""
Generate artist suggestions for a search result using Spotify API .
This matches the GUI ' s generate_auto_artist_suggestions functionality.
"""
if not spotify_client or not spotify_client . is_authenticated ( ) :
return jsonify ( { " error " : " Spotify client not available or not authenticated " } ) , 500
data = request . get_json ( )
original_title = data . get ( ' title ' , ' ' ) . strip ( )
original_artist = data . get ( ' artist ' , ' ' ) . strip ( )
if not original_title and not original_artist :
return jsonify ( { " error " : " No title or artist provided for suggestions " } ) , 400
try :
print ( f " 🎯 Generating Spotify suggestions for: ' { original_title } ' by ' { original_artist } ' " )
suggestions = [ ]
# Strategy 1: Search by artist name if available
if original_artist and original_artist . lower ( ) != ' unknown artist ' :
try :
artist_results = spotify_client . search_artists ( original_artist , limit = 3 )
suggestions . extend ( artist_results )
print ( f " Found { len ( artist_results ) } artist matches " )
except Exception as e :
print ( f " Artist search failed: { e } " )
# Strategy 2: Search by track title to find artist
if original_title and len ( suggestions ) < 3 :
try :
track_results = spotify_client . search_tracks ( original_title , limit = 5 )
for track in track_results :
if hasattr ( track , ' artist ' ) and track . artist not in [ s for s in suggestions ] :
suggestions . append ( track . artist )
if len ( suggestions ) > = 6 : # Limit to 6 total suggestions
break
print ( f " Found { len ( suggestions ) } total suggestions from track search " )
except Exception as e :
print ( f " Track search for suggestions failed: { e } " )
# Strategy 3: Combined search if we still need more
if len ( suggestions ) < 3 and original_artist and original_title :
try :
combined_query = f " { original_artist } { original_title } "
combined_results = spotify_client . search_artists ( combined_query , limit = 3 )
suggestions . extend ( combined_results )
print ( f " Added { len ( combined_results ) } from combined search " )
except Exception as e :
print ( f " Combined search failed: { e } " )
# Remove duplicates and convert to JSON
seen_ids = set ( )
unique_suggestions = [ ]
for artist in suggestions [ : 6 ] : # Limit to 6 suggestions
if artist . id not in seen_ids :
seen_ids . add ( artist . id )
artist_data = {
" id " : artist . id ,
" name " : artist . name ,
" genres " : getattr ( artist , ' genres ' , [ ] ) ,
" popularity " : getattr ( artist , ' popularity ' , 0 ) ,
" follower_count " : getattr ( artist , ' follower_count ' , 0 ) ,
" image_url " : getattr ( artist , ' image_url ' , None ) ,
" confidence_score " : 0.8 if artist . name . lower ( ) == original_artist . lower ( ) else 0.6 ,
" match_reason " : " Direct name match " if artist . name . lower ( ) == original_artist . lower ( ) else " Related artist "
}
unique_suggestions . append ( artist_data )
print ( f " ✅ Generated { len ( unique_suggestions ) } unique suggestions " )
return jsonify ( {
" success " : True ,
" suggestions " : unique_suggestions ,
" original_title " : original_title ,
" original_artist " : original_artist
} )
except Exception as e :
import traceback
traceback . print_exc ( )
print ( f " ❌ Spotify suggestions failed: { e } " )
return jsonify ( { " error " : f " Failed to generate suggestions: { str ( e ) } " } ) , 500
query = data . get ( ' query ' , ' ' )
print ( f " Simulating search for: { query } " )
# In a real implementation, you would call soulseek_client.search()
mock_results = [
{ " title " : " Bohemian Rhapsody " , " artist " : " Queen " , " album " : " A Night at the Opera " , " type " : " track " , " quality " : " FLAC " , " username " : " user1 " , " filename " : " Queen - Bohemian Rhapsody.flac " , " file_size " : 35000000 } ,
{ " title " : " A Night at the Opera " , " artist " : " Queen " , " type " : " album " , " track_count " : 12 , " size_mb " : 350 , " username " : " user2 " }
]
return jsonify ( { " results " : mock_results } )
@app.route ( ' /api/artists ' )
def get_artists ( ) :