mirror of https://github.com/Nezreka/SoulSync.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
315 lines
12 KiB
315 lines
12 KiB
"""
|
|
Library endpoints — browse artists, albums, tracks, genres, and stats.
|
|
"""
|
|
|
|
from flask import request, current_app
|
|
from database.music_database import get_database
|
|
from .auth import require_api_key
|
|
from .helpers import api_success, api_error, build_pagination, parse_pagination, parse_fields, parse_profile_id
|
|
from .serializers import serialize_artist, serialize_album, serialize_track
|
|
|
|
|
|
def register_routes(bp):
|
|
|
|
@bp.route("/library/artists", methods=["GET"])
|
|
@require_api_key
|
|
def list_artists():
|
|
"""List library artists with optional search, letter filter, and pagination."""
|
|
page, limit = parse_pagination(request)
|
|
search = request.args.get("search", "")
|
|
letter = request.args.get("letter", "all")
|
|
watchlist = request.args.get("watchlist", "all")
|
|
fields = parse_fields(request)
|
|
profile_id = parse_profile_id(request)
|
|
|
|
try:
|
|
db = get_database()
|
|
result = db.get_library_artists(
|
|
search_query=search,
|
|
letter=letter,
|
|
page=page,
|
|
limit=limit,
|
|
watchlist_filter=watchlist,
|
|
profile_id=profile_id,
|
|
)
|
|
artists = result.get("artists", [])
|
|
pag = result.get("pagination", {})
|
|
pagination = build_pagination(
|
|
page, limit, pag.get("total_count", len(artists))
|
|
)
|
|
# Artists from get_library_artists are already dicts with external IDs
|
|
serialized = [serialize_artist(a, fields) for a in artists]
|
|
return api_success({"artists": serialized}, pagination=pagination)
|
|
except Exception as e:
|
|
return api_error("LIBRARY_ERROR", str(e), 500)
|
|
|
|
@bp.route("/library/artists/<artist_id>", methods=["GET"])
|
|
@require_api_key
|
|
def get_artist(artist_id):
|
|
"""Get a single artist by ID with all metadata and album list."""
|
|
fields = parse_fields(request)
|
|
try:
|
|
db = get_database()
|
|
artist = db.api_get_artist(int(artist_id))
|
|
if not artist:
|
|
return api_error("NOT_FOUND", f"Artist {artist_id} not found.", 404)
|
|
|
|
albums = db.api_get_albums_by_artist(int(artist_id))
|
|
return api_success({
|
|
"artist": serialize_artist(artist, fields),
|
|
"albums": [serialize_album(a, fields) for a in albums],
|
|
})
|
|
except ValueError:
|
|
return api_error("BAD_REQUEST", "artist_id must be an integer.", 400)
|
|
except Exception as e:
|
|
return api_error("LIBRARY_ERROR", str(e), 500)
|
|
|
|
@bp.route("/library/artists/<artist_id>/albums", methods=["GET"])
|
|
@require_api_key
|
|
def get_artist_albums(artist_id):
|
|
"""List albums for an artist with full metadata."""
|
|
fields = parse_fields(request)
|
|
try:
|
|
db = get_database()
|
|
albums = db.api_get_albums_by_artist(int(artist_id))
|
|
return api_success({"albums": [serialize_album(a, fields) for a in albums]})
|
|
except ValueError:
|
|
return api_error("BAD_REQUEST", "artist_id must be an integer.", 400)
|
|
except Exception as e:
|
|
return api_error("LIBRARY_ERROR", str(e), 500)
|
|
|
|
@bp.route("/library/albums", methods=["GET"])
|
|
@require_api_key
|
|
def list_albums():
|
|
"""List/search albums with pagination and optional filters."""
|
|
page, limit = parse_pagination(request)
|
|
search = request.args.get("search", "")
|
|
fields = parse_fields(request)
|
|
|
|
artist_id = request.args.get("artist_id")
|
|
year = request.args.get("year")
|
|
|
|
try:
|
|
artist_id_int = int(artist_id) if artist_id else None
|
|
except ValueError:
|
|
return api_error("BAD_REQUEST", "artist_id must be an integer.", 400)
|
|
try:
|
|
year_int = int(year) if year else None
|
|
except ValueError:
|
|
return api_error("BAD_REQUEST", "year must be an integer.", 400)
|
|
|
|
try:
|
|
db = get_database()
|
|
result = db.api_list_albums(
|
|
search=search,
|
|
artist_id=artist_id_int,
|
|
year=year_int,
|
|
page=page,
|
|
limit=limit,
|
|
)
|
|
albums = result.get("albums", [])
|
|
total = result.get("total", 0)
|
|
pagination = build_pagination(page, limit, total)
|
|
return api_success(
|
|
{"albums": [serialize_album(a, fields) for a in albums]},
|
|
pagination=pagination,
|
|
)
|
|
except Exception as e:
|
|
return api_error("LIBRARY_ERROR", str(e), 500)
|
|
|
|
@bp.route("/library/albums/<album_id>", methods=["GET"])
|
|
@require_api_key
|
|
def get_album(album_id):
|
|
"""Get a single album by ID with all metadata and embedded tracks."""
|
|
fields = parse_fields(request)
|
|
try:
|
|
db = get_database()
|
|
album = db.api_get_album(int(album_id))
|
|
if not album:
|
|
return api_error("NOT_FOUND", f"Album {album_id} not found.", 404)
|
|
|
|
tracks = db.api_get_tracks_by_album(int(album_id))
|
|
return api_success({
|
|
"album": serialize_album(album, fields),
|
|
"tracks": [serialize_track(t, fields) for t in tracks],
|
|
})
|
|
except ValueError:
|
|
return api_error("BAD_REQUEST", "album_id must be an integer.", 400)
|
|
except Exception as e:
|
|
return api_error("LIBRARY_ERROR", str(e), 500)
|
|
|
|
@bp.route("/library/albums/<album_id>/tracks", methods=["GET"])
|
|
@require_api_key
|
|
def get_album_tracks(album_id):
|
|
"""List tracks in an album with full metadata."""
|
|
fields = parse_fields(request)
|
|
try:
|
|
db = get_database()
|
|
tracks = db.api_get_tracks_by_album(int(album_id))
|
|
return api_success({"tracks": [serialize_track(t, fields) for t in tracks]})
|
|
except ValueError:
|
|
return api_error("BAD_REQUEST", "album_id must be an integer.", 400)
|
|
except Exception as e:
|
|
return api_error("LIBRARY_ERROR", str(e), 500)
|
|
|
|
@bp.route("/library/tracks/<track_id>", methods=["GET"])
|
|
@require_api_key
|
|
def get_track(track_id):
|
|
"""Get a single track by ID with all metadata."""
|
|
fields = parse_fields(request)
|
|
try:
|
|
db = get_database()
|
|
track = db.api_get_track(int(track_id))
|
|
if not track:
|
|
return api_error("NOT_FOUND", f"Track {track_id} not found.", 404)
|
|
|
|
return api_success({"track": serialize_track(track, fields)})
|
|
except ValueError:
|
|
return api_error("BAD_REQUEST", "track_id must be an integer.", 400)
|
|
except Exception as e:
|
|
return api_error("LIBRARY_ERROR", str(e), 500)
|
|
|
|
@bp.route("/library/tracks", methods=["GET"])
|
|
@require_api_key
|
|
def library_search_tracks():
|
|
"""Search tracks by title and/or artist."""
|
|
title = request.args.get("title", "")
|
|
artist = request.args.get("artist", "")
|
|
try:
|
|
limit = min(200, max(1, int(request.args.get("limit", 50))))
|
|
except (ValueError, TypeError):
|
|
limit = 50
|
|
fields = parse_fields(request)
|
|
|
|
if not title and not artist:
|
|
return api_error("BAD_REQUEST", "Provide at least 'title' or 'artist' query param.", 400)
|
|
|
|
try:
|
|
db = get_database()
|
|
tracks = db.search_tracks(title=title, artist=artist, limit=limit)
|
|
if not tracks:
|
|
return api_success({"tracks": []})
|
|
|
|
# Re-query by IDs to get full row data
|
|
track_ids = [t.id for t in tracks]
|
|
full_tracks = db.api_get_tracks_by_ids(track_ids)
|
|
|
|
return api_success({"tracks": [serialize_track(t, fields) for t in full_tracks]})
|
|
except Exception as e:
|
|
return api_error("LIBRARY_ERROR", str(e), 500)
|
|
|
|
@bp.route("/library/genres", methods=["GET"])
|
|
@require_api_key
|
|
def list_genres():
|
|
"""List all genres with occurrence counts.
|
|
|
|
Query params:
|
|
source: 'artists' or 'albums' (default: 'artists')
|
|
"""
|
|
source = request.args.get("source", "artists")
|
|
if source not in ("artists", "albums"):
|
|
return api_error("BAD_REQUEST", "source must be 'artists' or 'albums'.", 400)
|
|
|
|
try:
|
|
db = get_database()
|
|
genres = db.api_get_genres(table=source)
|
|
return api_success({"genres": genres, "source": source})
|
|
except Exception as e:
|
|
return api_error("LIBRARY_ERROR", str(e), 500)
|
|
|
|
@bp.route("/library/recently-added", methods=["GET"])
|
|
@require_api_key
|
|
def recently_added():
|
|
"""Get recently added content ordered by created_at.
|
|
|
|
Query params:
|
|
type: 'albums', 'artists', or 'tracks' (default: 'albums')
|
|
limit: max items to return (default: 50, max: 200)
|
|
"""
|
|
entity_type = request.args.get("type", "albums")
|
|
if entity_type not in ("albums", "artists", "tracks"):
|
|
return api_error("BAD_REQUEST", "type must be 'albums', 'artists', or 'tracks'.", 400)
|
|
|
|
try:
|
|
limit = min(200, max(1, int(request.args.get("limit", 50))))
|
|
except (ValueError, TypeError):
|
|
limit = 50
|
|
fields = parse_fields(request)
|
|
|
|
try:
|
|
db = get_database()
|
|
items = db.api_get_recently_added(entity_type=entity_type, limit=limit)
|
|
|
|
serializer = {
|
|
"artists": serialize_artist,
|
|
"albums": serialize_album,
|
|
"tracks": serialize_track,
|
|
}[entity_type]
|
|
|
|
return api_success({
|
|
"items": [serializer(item, fields) for item in items],
|
|
"type": entity_type,
|
|
})
|
|
except Exception as e:
|
|
return api_error("LIBRARY_ERROR", str(e), 500)
|
|
|
|
@bp.route("/library/lookup", methods=["GET"])
|
|
@require_api_key
|
|
def lookup_by_external_id():
|
|
"""Look up a library entity by external provider ID.
|
|
|
|
Query params:
|
|
type: 'artist', 'album', or 'track' (required)
|
|
provider: 'spotify', 'musicbrainz', 'itunes', 'deezer', 'audiodb' (required)
|
|
id: the external ID value (required)
|
|
"""
|
|
entity_type = request.args.get("type")
|
|
provider = request.args.get("provider")
|
|
external_id = request.args.get("id")
|
|
fields = parse_fields(request)
|
|
|
|
if not entity_type or not provider or not external_id:
|
|
return api_error("BAD_REQUEST", "Required params: type, provider, id.", 400)
|
|
|
|
table_map = {"artist": "artists", "album": "albums", "track": "tracks"}
|
|
table = table_map.get(entity_type)
|
|
if not table:
|
|
return api_error("BAD_REQUEST", "type must be 'artist', 'album', or 'track'.", 400)
|
|
|
|
if provider not in ("spotify", "musicbrainz", "itunes", "deezer", "audiodb"):
|
|
return api_error("BAD_REQUEST", "provider must be spotify, musicbrainz, itunes, deezer, or audiodb.", 400)
|
|
|
|
try:
|
|
db = get_database()
|
|
result = db.api_lookup_by_external_id(table, provider, external_id)
|
|
if not result:
|
|
return api_error("NOT_FOUND", f"No {entity_type} found for {provider} ID: {external_id}", 404)
|
|
|
|
serializer = {
|
|
"artists": serialize_artist,
|
|
"albums": serialize_album,
|
|
"tracks": serialize_track,
|
|
}[table]
|
|
|
|
return api_success({entity_type: serializer(result, fields)})
|
|
except Exception as e:
|
|
return api_error("LIBRARY_ERROR", str(e), 500)
|
|
|
|
@bp.route("/library/stats", methods=["GET"])
|
|
@require_api_key
|
|
def library_stats():
|
|
"""Get library statistics (artist/album/track counts, DB info)."""
|
|
try:
|
|
db = get_database()
|
|
info = db.get_database_info_for_server()
|
|
stats = db.get_statistics_for_server()
|
|
return api_success({
|
|
"artists": stats.get("artists", 0),
|
|
"albums": stats.get("albums", 0),
|
|
"tracks": stats.get("tracks", 0),
|
|
"database_size_mb": info.get("database_size_mb"),
|
|
"last_update": info.get("last_update"),
|
|
})
|
|
except Exception as e:
|
|
return api_error("LIBRARY_ERROR", str(e), 500)
|