soulsync/api/library.py
Broque Thomas d9aa8303a7 Add SoulSync REST API (v1) with API key authentication
Adds a full public REST API at /api/v1/ with 32 endpoints covering library, search, downloads, wishlist, watchlist, playlists, system status, and settings. Includes API key authentication (Bearer token), per-endpoint rate limiting, and consistent JSON response format. API keys can be generated and managed from the Settings page. No changes to existing functionality — the API delegates to the same backend services the web UI uses.
2026-03-03 09:49:00 -08:00

158 lines
5.4 KiB
Python

"""
Library endpoints — browse artists, albums, tracks, 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
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")
try:
db = get_database()
result = db.get_library_artists(
search_query=search,
letter=letter,
page=page,
limit=limit,
watchlist_filter=watchlist,
)
artists = result.get("artists", [])
pag = result.get("pagination", {})
pagination = build_pagination(
page, limit, pag.get("total_count", len(artists))
)
return api_success({"artists": artists}, 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 album list."""
try:
db = get_database()
artist = db.get_artist(int(artist_id))
if not artist:
return api_error("NOT_FOUND", f"Artist {artist_id} not found.", 404)
albums = db.get_albums_by_artist(int(artist_id))
return api_success({
"artist": _serialize_artist(artist),
"albums": [_serialize_album(a) 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."""
try:
db = get_database()
albums = db.get_albums_by_artist(int(artist_id))
return api_success({"albums": [_serialize_album(a) 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/<album_id>/tracks", methods=["GET"])
@require_api_key
def get_album_tracks(album_id):
"""List tracks in an album."""
try:
db = get_database()
tracks = db.get_tracks_by_album(int(album_id))
return api_success({"tracks": [_serialize_track(t) 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", 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", "")
limit = min(200, max(1, int(request.args.get("limit", 50))))
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)
return api_success({"tracks": [_serialize_track(t) for t in tracks]})
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)
# ---- serialization helpers ----
def _serialize_artist(a):
return {
"id": a.id,
"name": a.name,
"thumb_url": a.thumb_url,
"genres": a.genres or [],
"summary": a.summary,
}
def _serialize_album(a):
return {
"id": a.id,
"artist_id": a.artist_id,
"title": a.title,
"year": a.year,
"thumb_url": a.thumb_url,
"genres": a.genres or [],
"track_count": a.track_count,
"duration": a.duration,
}
def _serialize_track(t):
return {
"id": t.id,
"album_id": t.album_id,
"artist_id": t.artist_id,
"title": t.title,
"track_number": t.track_number,
"duration": t.duration,
"file_path": t.file_path,
"bitrate": t.bitrate,
}