mirror of
https://github.com/Dvorinka/SpotifyRecAlg.git
synced 2026-06-03 20:13:03 +00:00
514 lines
17 KiB
Python
514 lines
17 KiB
Python
"""
|
|
Enhanced Search API for SwingMusic
|
|
Integrates global music catalog search with existing local search
|
|
"""
|
|
|
|
import asyncio
|
|
import logging
|
|
from typing import Any
|
|
|
|
from flask import Blueprint, jsonify, request
|
|
|
|
from swingmusic.api.search import search_items as local_search
|
|
from swingmusic.db.spotify import UserCatalogPreferencesTable
|
|
from swingmusic.services.music_catalog import music_catalog_service
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Create blueprint
|
|
enhanced_search_bp = Blueprint("enhanced_search", __name__, url_prefix="/api/search")
|
|
|
|
|
|
@enhanced_search_bp.route("/global", methods=["POST"])
|
|
def global_search():
|
|
"""
|
|
Search across global music catalog (Spotify)
|
|
|
|
Request body:
|
|
{
|
|
"query": "search query",
|
|
"type": "all|tracks|albums|artists|playlists",
|
|
"limit": 20,
|
|
"user_id": 1
|
|
}
|
|
"""
|
|
try:
|
|
data = request.get_json()
|
|
if not data or not data.get("query"):
|
|
return jsonify({"error": "Search query is required"}), 400
|
|
|
|
query = data["query"].strip()
|
|
search_type = data.get("type", "all")
|
|
limit = min(data.get("limit", 20), 50) # Cap at 50
|
|
user_id = data.get("user_id")
|
|
|
|
# Get user preferences if available
|
|
user_prefs = None
|
|
if user_id:
|
|
user_prefs = UserCatalogPreferencesTable.get_or_create(user_id)
|
|
limit = min(limit, user_prefs.max_search_results)
|
|
|
|
# Run async search
|
|
loop = asyncio.new_event_loop()
|
|
asyncio.set_event_loop(loop)
|
|
|
|
try:
|
|
result = loop.run_until_complete(
|
|
music_catalog_service.search_global_catalog(query, search_type, limit)
|
|
)
|
|
finally:
|
|
loop.close()
|
|
|
|
# Filter based on user preferences
|
|
if user_prefs and not user_prefs.show_explicit:
|
|
result.tracks = [track for track in result.tracks if not track.explicit]
|
|
result.albums = [album for album in result.albums if not album.explicit]
|
|
|
|
# Convert to dict for JSON response
|
|
response_data = {
|
|
"query": result.query,
|
|
"total": result.total,
|
|
"tracks": [_catalog_item_to_dict(track) for track in result.tracks],
|
|
"albums": [_catalog_item_to_dict(album) for album in result.albums],
|
|
"artists": [_catalog_item_to_dict(artist) for artist in result.artists],
|
|
"playlists": [
|
|
_catalog_item_to_dict(playlist) for playlist in result.playlists
|
|
],
|
|
"source": "global_catalog",
|
|
"cache_info": {
|
|
"from_cache": False, # Cache detection would require tracking query timestamps
|
|
"expires_at": None,
|
|
},
|
|
}
|
|
|
|
return jsonify(response_data)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in global search: {e}")
|
|
return jsonify({"error": "Search failed"}), 500
|
|
|
|
|
|
@enhanced_search_bp.route("/combined", methods=["POST"])
|
|
def combined_search():
|
|
"""
|
|
Search both local library and global catalog
|
|
|
|
Request body:
|
|
{
|
|
"query": "search query",
|
|
"include_local": true,
|
|
"include_global": true,
|
|
"type": "all|tracks|albums|artists",
|
|
"limit": 20,
|
|
"user_id": 1
|
|
}
|
|
"""
|
|
try:
|
|
data = request.get_json()
|
|
if not data or not data.get("query"):
|
|
return jsonify({"error": "Search query is required"}), 400
|
|
|
|
query = data["query"].strip()
|
|
include_local = data.get("include_local", True)
|
|
include_global = data.get("include_global", True)
|
|
search_type = data.get("type", "all")
|
|
limit = min(data.get("limit", 20), 50)
|
|
user_id = data.get("user_id")
|
|
|
|
results = {
|
|
"query": query,
|
|
"local": {"tracks": [], "albums": [], "artists": []},
|
|
"global": {"tracks": [], "albums": [], "artists": [], "playlists": []},
|
|
"total": 0,
|
|
}
|
|
|
|
# Search local library
|
|
if include_local:
|
|
try:
|
|
# Use existing local search
|
|
local_results = local_search(query, search_type)
|
|
results["local"] = (
|
|
local_results
|
|
if local_results
|
|
else {"tracks": [], "albums": [], "artists": []}
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Error in local search: {e}")
|
|
|
|
# Search global catalog
|
|
if include_global:
|
|
loop = asyncio.new_event_loop()
|
|
asyncio.set_event_loop(loop)
|
|
|
|
try:
|
|
global_results = loop.run_until_complete(
|
|
music_catalog_service.search_global_catalog(
|
|
query, search_type, limit
|
|
)
|
|
)
|
|
|
|
# Filter based on user preferences
|
|
user_prefs = None
|
|
if user_id:
|
|
user_prefs = UserCatalogPreferencesTable.get_or_create(user_id)
|
|
if not user_prefs.show_explicit:
|
|
global_results.tracks = [
|
|
track
|
|
for track in global_results.tracks
|
|
if not track.explicit
|
|
]
|
|
global_results.albums = [
|
|
album
|
|
for album in global_results.albums
|
|
if not album.explicit
|
|
]
|
|
|
|
results["global"] = {
|
|
"tracks": [
|
|
_catalog_item_to_dict(track) for track in global_results.tracks
|
|
],
|
|
"albums": [
|
|
_catalog_item_to_dict(album) for album in global_results.albums
|
|
],
|
|
"artists": [
|
|
_catalog_item_to_dict(artist)
|
|
for artist in global_results.artists
|
|
],
|
|
"playlists": [
|
|
_catalog_item_to_dict(playlist)
|
|
for playlist in global_results.playlists
|
|
],
|
|
}
|
|
|
|
finally:
|
|
loop.close()
|
|
|
|
# Calculate total
|
|
results["total"] = (
|
|
len(results["local"].get("tracks", []))
|
|
+ len(results["local"].get("albums", []))
|
|
+ len(results["local"].get("artists", []))
|
|
+ len(results["global"].get("tracks", []))
|
|
+ len(results["global"].get("albums", []))
|
|
+ len(results["global"].get("artists", []))
|
|
+ len(results["global"].get("playlists", []))
|
|
)
|
|
|
|
return jsonify(results)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in combined search: {e}")
|
|
return jsonify({"error": "Search failed"}), 500
|
|
|
|
|
|
@enhanced_search_bp.route("/suggestions", methods=["GET"])
|
|
def search_suggestions():
|
|
"""
|
|
Get search suggestions based on query and user preferences
|
|
|
|
Query parameters:
|
|
- q: search query
|
|
- type: tracks|albums|artists|all
|
|
- limit: number of suggestions (default 10)
|
|
- user_id: user ID for preferences
|
|
"""
|
|
try:
|
|
query = request.args.get("q", "").strip()
|
|
if not query or len(query) < 2:
|
|
return jsonify({"suggestions": []})
|
|
|
|
search_type = request.args.get("type", "all")
|
|
limit = min(int(request.args.get("limit", 10)), 20)
|
|
user_id = request.args.get("user_id")
|
|
|
|
# Get user preferences
|
|
user_prefs = None
|
|
if user_id:
|
|
user_prefs = UserCatalogPreferencesTable.get_or_create(user_id)
|
|
limit = min(limit, user_prefs.max_search_results)
|
|
|
|
# Search cached items for fast suggestions
|
|
item_types = None
|
|
if search_type != "all":
|
|
item_types = [search_type]
|
|
|
|
loop = asyncio.new_event_loop()
|
|
asyncio.set_event_loop(loop)
|
|
|
|
try:
|
|
# For suggestions, search both cache and live
|
|
suggestions = []
|
|
|
|
# Search cached items first (fast)
|
|
from swingmusic.db.spotify import GlobalCatalogCacheTable
|
|
|
|
cached_items = GlobalCatalogCacheTable.search_cached(
|
|
query, item_types, limit
|
|
)
|
|
|
|
for item in cached_items:
|
|
if user_prefs and not user_prefs.show_explicit and item.explicit:
|
|
continue
|
|
|
|
suggestion = {
|
|
"id": item.spotify_id,
|
|
"type": item.item_type,
|
|
"title": item.title,
|
|
"artist": item.artist,
|
|
"album": item.album,
|
|
"image_url": item.image_url,
|
|
"popularity": item.popularity,
|
|
"source": "cache",
|
|
}
|
|
suggestions.append(suggestion)
|
|
|
|
# If we need more suggestions, search global catalog
|
|
if len(suggestions) < limit:
|
|
remaining = limit - len(suggestions)
|
|
global_results = loop.run_until_complete(
|
|
music_catalog_service.search_global_catalog(
|
|
query, search_type, remaining
|
|
)
|
|
)
|
|
|
|
for track in global_results.tracks[:remaining]:
|
|
if user_prefs and not user_prefs.show_explicit and track.explicit:
|
|
continue
|
|
|
|
suggestion = {
|
|
"id": track.spotify_id,
|
|
"type": "track",
|
|
"title": track.title,
|
|
"artist": track.artist,
|
|
"album": track.album,
|
|
"image_url": track.image_url,
|
|
"popularity": track.popularity,
|
|
"source": "global",
|
|
}
|
|
suggestions.append(suggestion)
|
|
|
|
return jsonify({"suggestions": suggestions[:limit]})
|
|
|
|
finally:
|
|
loop.close()
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in search suggestions: {e}")
|
|
return jsonify({"suggestions": []})
|
|
|
|
|
|
@enhanced_search_bp.route("/artist/<artist_id>", methods=["GET"])
|
|
def get_artist_info(artist_id: str):
|
|
"""
|
|
Get comprehensive artist information including top tracks and albums
|
|
|
|
Path parameters:
|
|
- artist_id: Spotify artist ID
|
|
|
|
Query parameters:
|
|
- user_id: user ID for preferences
|
|
"""
|
|
try:
|
|
user_id = request.args.get("user_id")
|
|
|
|
# Get user preferences
|
|
user_prefs = None
|
|
if user_id:
|
|
user_prefs = UserCatalogPreferencesTable.get_or_create(user_id)
|
|
|
|
loop = asyncio.new_event_loop()
|
|
asyncio.set_event_loop(loop)
|
|
|
|
try:
|
|
artist_info = loop.run_until_complete(
|
|
music_catalog_service.get_artist_info(artist_id)
|
|
)
|
|
|
|
if not artist_info:
|
|
return jsonify({"error": "Artist not found"}), 404
|
|
|
|
# Filter based on user preferences
|
|
if user_prefs and not user_prefs.show_explicit:
|
|
artist_info.top_tracks = [
|
|
track
|
|
for track in artist_info.top_tracks or []
|
|
if not track.explicit
|
|
]
|
|
artist_info.albums = [
|
|
album for album in artist_info.albums or [] if not album.explicit
|
|
]
|
|
|
|
response_data = {
|
|
"spotify_id": artist_info.spotify_id,
|
|
"name": artist_info.name,
|
|
"image_url": artist_info.image_url,
|
|
"followers": artist_info.followers,
|
|
"popularity": artist_info.popularity,
|
|
"genres": artist_info.genres or [],
|
|
"top_tracks": [
|
|
_catalog_item_to_dict(track)
|
|
for track in (artist_info.top_tracks or [])
|
|
],
|
|
"albums": [
|
|
_catalog_item_to_dict(album) for album in (artist_info.albums or [])
|
|
],
|
|
"related_artists": artist_info.related_artists or [],
|
|
}
|
|
|
|
return jsonify(response_data)
|
|
|
|
finally:
|
|
loop.close()
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting artist info: {e}")
|
|
return jsonify({"error": "Failed to get artist info"}), 500
|
|
|
|
|
|
@enhanced_search_bp.route("/album/<album_id>", methods=["GET"])
|
|
def get_album_details(album_id: str):
|
|
"""
|
|
Get detailed album information with tracklist
|
|
|
|
Path parameters:
|
|
- album_id: Spotify album ID
|
|
|
|
Query parameters:
|
|
- user_id: user ID for preferences
|
|
"""
|
|
try:
|
|
user_id = request.args.get("user_id")
|
|
|
|
# Get user preferences
|
|
user_prefs = None
|
|
if user_id:
|
|
user_prefs = UserCatalogPreferencesTable.get_or_create(user_id)
|
|
|
|
loop = asyncio.new_event_loop()
|
|
asyncio.set_event_loop(loop)
|
|
|
|
try:
|
|
album = loop.run_until_complete(
|
|
music_catalog_service.get_album_details(album_id)
|
|
)
|
|
|
|
if not album:
|
|
return jsonify({"error": "Album not found"}), 404
|
|
|
|
# Filter based on user preferences
|
|
if user_prefs and not user_prefs.show_explicit and album.explicit:
|
|
return jsonify({"error": "Explicit content filtered"}), 403
|
|
|
|
response_data = _catalog_item_to_dict(album)
|
|
|
|
# Add tracklist if available in data
|
|
if album.data and "tracks" in album.data:
|
|
response_data["tracks"] = [
|
|
_catalog_item_to_dict(track) for track in album.data["tracks"]
|
|
]
|
|
|
|
return jsonify(response_data)
|
|
|
|
finally:
|
|
loop.close()
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting album details: {e}")
|
|
return jsonify({"error": "Failed to get album details"}), 500
|
|
|
|
|
|
@enhanced_search_bp.route("/preferences/<int:user_id>", methods=["GET", "POST"])
|
|
def user_preferences(user_id: int):
|
|
"""Get or update user catalog search preferences"""
|
|
try:
|
|
if request.method == "GET":
|
|
prefs = UserCatalogPreferencesTable.get_or_create(user_id)
|
|
return jsonify(
|
|
{
|
|
"user_id": prefs.user_id,
|
|
"show_explicit": prefs.show_explicit,
|
|
"default_quality": prefs.default_quality,
|
|
"auto_download": prefs.auto_download,
|
|
"show_suggestions": prefs.show_suggestions,
|
|
"preferred_genres": prefs.preferred_genres or [],
|
|
"excluded_genres": prefs.excluded_genres or [],
|
|
"max_search_results": prefs.max_search_results,
|
|
"cache_ttl_preference": prefs.cache_ttl_preference,
|
|
}
|
|
)
|
|
|
|
elif request.method == "POST":
|
|
data = request.get_json()
|
|
if not data:
|
|
return jsonify({"error": "No data provided"}), 400
|
|
|
|
# Update only provided fields
|
|
update_data = {}
|
|
allowed_fields = [
|
|
"show_explicit",
|
|
"default_quality",
|
|
"auto_download",
|
|
"show_suggestions",
|
|
"preferred_genres",
|
|
"excluded_genres",
|
|
"max_search_results",
|
|
"cache_ttl_preference",
|
|
]
|
|
|
|
for field in allowed_fields:
|
|
if field in data:
|
|
update_data[field] = data[field]
|
|
|
|
if update_data:
|
|
UserCatalogPreferencesTable.update_preferences(user_id, update_data)
|
|
|
|
return jsonify({"message": "Preferences updated successfully"})
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error handling user preferences: {e}")
|
|
return jsonify({"error": "Failed to handle preferences"}), 500
|
|
|
|
|
|
def _catalog_item_to_dict(item) -> dict[str, Any]:
|
|
"""Convert CatalogItem to dictionary for JSON response"""
|
|
if hasattr(item, "__dict__"):
|
|
# It's a dataclass instance
|
|
return {
|
|
"spotify_id": item.spotify_id,
|
|
"type": item.item_type.value
|
|
if hasattr(item.item_type, "value")
|
|
else str(item.item_type),
|
|
"title": item.title,
|
|
"artist": item.artist,
|
|
"album": item.album,
|
|
"duration_ms": item.duration_ms,
|
|
"popularity": item.popularity,
|
|
"preview_url": item.preview_url,
|
|
"image_url": item.image_url,
|
|
"release_date": item.release_date,
|
|
"explicit": item.explicit,
|
|
"data": item.data,
|
|
}
|
|
else:
|
|
# It's likely a database model
|
|
return {
|
|
"spotify_id": getattr(item, "spotify_id", None),
|
|
"type": getattr(item, "item_type", None),
|
|
"title": getattr(item, "title", None),
|
|
"artist": getattr(item, "artist", None),
|
|
"album": getattr(item, "album", None),
|
|
"duration_ms": getattr(item, "duration_ms", None),
|
|
"popularity": getattr(item, "popularity", None),
|
|
"preview_url": getattr(item, "preview_url", None),
|
|
"image_url": getattr(item, "image_url", None),
|
|
"release_date": getattr(item, "release_date", None),
|
|
"explicit": getattr(item, "explicit", False),
|
|
"data": getattr(item, "data", None),
|
|
}
|
|
|
|
|
|
def register_enhanced_search_api(app):
|
|
"""Register enhanced search API with Flask app"""
|
|
app.register_blueprint(enhanced_search_bp)
|
|
logger.info("Enhanced search API registered")
|