From 7d064a8562964c50fa4fa7eebe0076abf4b2ce40 Mon Sep 17 00:00:00 2001 From: mungai-njoroge Date: Mon, 4 Mar 2024 01:31:46 +0300 Subject: [PATCH] add api docs for artist routes + extract hash and limit schemas --- app/api/__init__.py | 20 ++++++++--- app/api/album.py | 81 +++++++++++++++++------------------------ app/api/apischemas.py | 83 +++++++++++++++++++++++++++++++++++++++++++ app/api/artist.py | 60 ++++++++++++++++--------------- 4 files changed, 163 insertions(+), 81 deletions(-) create mode 100644 app/api/apischemas.py diff --git a/app/api/__init__.py b/app/api/__init__.py index 257e54bd..ebf4806c 100644 --- a/app/api/__init__.py +++ b/app/api/__init__.py @@ -2,6 +2,7 @@ This module combines all API blueprints into a single Flask app instance. """ +import datetime from flask_cors import CORS from flask_compress import Compress @@ -28,6 +29,19 @@ from app.api import ( getall, ) +open_api_description = f""" +The REST API exposed by your Swing Music server + +### Definition of terms: + +#### 1. `limit`: The number of items to return. + +In endpoints that request multiple lists of items, this represents the number of items to return for each list. + +--- + +[MIT License](https://github.com/swing-opensource/swingmusic?tab=MIT-1-ov-file#MIT-1-ov-file) | Copyright (c) {datetime.datetime.now().year} [Mungai Njoroge](https://mungai.vercel.app) +""" def create_api(): """ @@ -36,9 +50,7 @@ def create_api(): api_info = Info( title=f"Swing Music", version=f"v{Keys.SWINGMUSIC_APP_VERSION}", - license={"name": "MIT", "url": "https://github.com/swing-opensource/swingmusic?tab=MIT-1-ov-file#MIT-1-ov-file"}, - contact={"name": "Mungai Njoroge", "url": "https://mungai.vercel.app", "email": "geoffreymungai45@gmail.com"}, - description="The REST API exposed by your Swing Music server", + description=open_api_description, ) app = OpenAPI(__name__, info=api_info) @@ -52,7 +64,7 @@ def create_api(): with app.app_context(): app.register_api(album.api) - app.register_blueprint(artist.api) + app.register_api(artist.api) app.register_blueprint(send_file.api) app.register_blueprint(search.api) app.register_blueprint(folder.api) diff --git a/app/api/album.py b/app/api/album.py index eef04953..c2f77dfb 100644 --- a/app/api/album.py +++ b/app/api/album.py @@ -7,6 +7,7 @@ import random from flask_openapi3 import Tag from flask_openapi3 import APIBlueprint from pydantic import BaseModel, Field +from app.api.apischemas import AlbumHashSchema, AlbumLimitSchema, ArtistHashSchema from app.db.sqlite.albumcolors import SQLiteAlbumMethods as adb from app.db.sqlite.favorite import SQLiteFavoriteMethods as favdb @@ -23,22 +24,15 @@ from app.utils.hashing import create_hash get_albums_by_albumartist = adb.get_albums_by_albumartist check_is_fav = favdb.check_is_favorite -book_tag = Tag(name="Album", description="Single album") -api = APIBlueprint("album", __name__, url_prefix="", abp_tags=[book_tag]) +bp_tag = Tag(name="Album", description="Single album") +api = APIBlueprint("album", __name__, url_prefix="", abp_tags=[bp_tag]) -class GetAlbumBody(BaseModel): - albumhash: str = Field( - description="The hash of the album to get", - example="49e4819273", - min_length=Defaults.HASH_LENGTH, - max_length=Defaults.HASH_LENGTH, - ) - - -@api.post("/album", summary="Get album") -def get_album_tracks_and_info(body: GetAlbumBody): +@api.post("/album") +def get_album_tracks_and_info(body: AlbumHashSchema): """ + Get album and tracks + Returns album info and tracks for the given albumhash. """ albumhash = body.albumhash @@ -84,43 +78,39 @@ def get_album_tracks_and_info(body: GetAlbumBody): "info": album, } -class GetAlbumTracksQuery(BaseModel): - albumhash: str = Field( - description="The hash of the album", - example="49e4819273", - min_length=Defaults.HASH_LENGTH, - max_length=Defaults.HASH_LENGTH, - ) -@api.get("/album//tracks", summary="Get album tracks") -def get_album_tracks(query: GetAlbumTracksQuery): +@api.get("/album//tracks") +def get_album_tracks(path: AlbumHashSchema): """ + Get album tracks + Returns all the tracks in the given album, sorted by disc and track number. + NOTE: No album info is returned. """ - tracks = TrackStore.get_tracks_by_albumhash(query.albumhash) + tracks = TrackStore.get_tracks_by_albumhash(path.albumhash) tracks = sort_by_track_no(tracks) return tracks -class GetMoreFromArtistsBody(BaseModel): + +class GetMoreFromArtistsBody(AlbumLimitSchema): albumartists: str = Field( description="The artist hashes to get more albums from", - example=Defaults.API_ARTISTHASH - ) - limit: int = Field( - description="The maximum number of albums to return per artist", - example=7, - default=7, + example=Defaults.API_ARTISTHASH, ) + base_title: str = Field( description="The base title of the album to exclude from the results.", example=Defaults.API_ALBUMNAME, default=None, ) -@api.post("/album/from-artist", summary="More from artist") + +@api.post("/album/from-artist") def get_more_from_artist(body: GetMoreFromArtistsBody): """ + Get more from artist + Returns more albums from the given artist hashes. """ albumartists = body.albumartists @@ -151,7 +141,7 @@ def get_more_from_artist(body: GetMoreFromArtistsBody): return albums -class GetAlbumVersionsBody(BaseModel): +class GetAlbumVersionsBody(ArtistHashSchema): og_album_title: str = Field( description="The original album title (album.og_title)", example=Defaults.API_ALBUMNAME, @@ -160,14 +150,13 @@ class GetAlbumVersionsBody(BaseModel): description="The base title of the album to exclude from the results.", example=Defaults.API_ALBUMNAME, ) - artisthash: str = Field( - description="The artist hash", - example=Defaults.API_ARTISTHASH, - ) -@api.post("/album/other-versions", summary="Get other versions") + +@api.post("/album/other-versions") def get_album_versions(body: GetAlbumVersionsBody): """ + Get other versions + Returns other versions of the given album. """ og_album_title = body.og_album_title @@ -189,20 +178,16 @@ def get_album_versions(body: GetAlbumVersionsBody): return albums -class GetSimilarAlbumsQuery(BaseModel): - artisthash: str = Field( - description="The artist hash", - example=Defaults.API_ARTISTHASH, - ) - limit: int = Field( - description="The maximum number of albums to return", - example=Defaults.API_CARD_LIMIT, - default=Defaults.API_CARD_LIMIT, - ) -@api.get("/album/similar", summary="Get similar albums") +class GetSimilarAlbumsQuery(ArtistHashSchema, AlbumLimitSchema): + pass + + +@api.get("/album/similar") def get_similar_albums(query: GetSimilarAlbumsQuery): """ + Get similar albums + Returns similar albums to the given album. """ artisthash = query.artisthash diff --git a/app/api/apischemas.py b/app/api/apischemas.py new file mode 100644 index 00000000..a4031491 --- /dev/null +++ b/app/api/apischemas.py @@ -0,0 +1,83 @@ +""" +Reusable Pydantic basic schemas for the API +""" + +from pydantic import BaseModel, Field + +from app.settings import Defaults + + + +class AlbumHashSchema(BaseModel): + """ + Extending this class will give you a model with the `albumhash` field + """ + + albumhash: str = Field( + description="The album hash", + example=Defaults.API_ALBUMHASH, + min_length=Defaults.HASH_LENGTH, + max_length=Defaults.HASH_LENGTH, + ) + + +class ArtistHashSchema(BaseModel): + """ + Extending this class will give you a model with the `artisthash` field + """ + + artisthash: str = Field( + description="The artist hash", + example=Defaults.API_ARTISTHASH, + min_length=Defaults.HASH_LENGTH, + max_length=Defaults.HASH_LENGTH, + ) + + +class GenericLimitSchema(BaseModel): + """ + Extending this class will give you a model with the `limit` field + """ + + limit: int = Field( + description="The number of items to return", + example=Defaults.API_CARD_LIMIT, + default=Defaults.API_CARD_LIMIT + ) + + +# INFO: The following 3 classes are duplicated to specify the type of items +class TrackLimitSchema(BaseModel): + """ + Extending this class will give you a model with the `limit` field + """ + + limit: int = Field( + description="The number of tracks to return", + example=Defaults.API_CARD_LIMIT, + default=Defaults.API_CARD_LIMIT + ) + + +class AlbumLimitSchema(BaseModel): + """ + Extending this class will give you a model with the `limit` field + """ + + limit: int = Field( + description="The number of albums to return", + example=Defaults.API_CARD_LIMIT, + default=Defaults.API_CARD_LIMIT + ) + + +class ArtistLimitSchema(BaseModel): + """ + Extending this class will give you a model with the `limit` field + """ + + limit: int = Field( + description="The number of artists to return", + example=Defaults.API_CARD_LIMIT, + default=Defaults.API_CARD_LIMIT + ) diff --git a/app/api/artist.py b/app/api/artist.py index 235653c9..e26a014c 100644 --- a/app/api/artist.py +++ b/app/api/artist.py @@ -1,11 +1,15 @@ """ Contains all the artist(s) routes. """ + import math import random from datetime import datetime -from flask import Blueprint, request +from flask import request +from flask_openapi3 import APIBlueprint, Tag +from pydantic import BaseModel, Field +from app.api.apischemas import AlbumLimitSchema, ArtistHashSchema, ArtistLimitSchema, TrackLimitSchema from app.db.sqlite.favorite import SQLiteFavoriteMethods as favdb from app.db.sqlite.lastfm.similar_artists import SQLiteLastFMSimilarArtists as fmdb @@ -16,20 +20,19 @@ from app.store.albums import AlbumStore from app.store.artists import ArtistStore from app.store.tracks import TrackStore -api = Blueprint("artist", __name__, url_prefix="/") +bp_tag = Tag(name="Artist", description="Single artist") +api = APIBlueprint("artist", __name__, url_prefix="/", abp_tags=[bp_tag]) -@api.route("/artist/", methods=["GET"]) -def get_artist(artisthash: str): +@api.get("/artist/") +def get_artist(path: ArtistHashSchema, query: TrackLimitSchema): """ Get artist data. + + Returns artist data, tracks and genres for the given artisthash. """ - limit = request.args.get("limit") - - if limit is None: - limit = 6 - - limit = int(limit) + artisthash = path.artisthash + limit = query.limit artist = ArtistStore.get_artist_by_hash(artisthash) @@ -79,16 +82,18 @@ def get_artist(artisthash: str): } -@api.route("/artist//albums", methods=["GET"]) -def get_artist_albums(artisthash: str): - limit = request.args.get("limit") +class GetArtistAlbumsQuery(AlbumLimitSchema): + all: bool = Field( + description="Whether to ignore limit and return all albums", default=False + ) - if limit is None: - limit = 6 - return_all = request.args.get("all") +@api.get("/artist//albums") +def get_artist_albums(path: ArtistHashSchema, query: GetArtistAlbumsQuery): + return_all = query.all + artisthash = path.artisthash - limit = int(limit) + limit = query.limit all_albums = AlbumStore.get_albums_by_artisthash(artisthash) @@ -170,29 +175,26 @@ def get_artist_albums(artisthash: str): } -@api.route("/artist//tracks", methods=["GET"]) -def get_all_artist_tracks(artisthash: str): +@api.get("/artist//tracks") +def get_all_artist_tracks(path: ArtistHashSchema): """ + Get all artist tracks + Returns all artists by a given artist. """ - tracks = TrackStore.get_tracks_by_artisthash(artisthash) + tracks = TrackStore.get_tracks_by_artisthash(path.artisthash) return {"tracks": serialize_tracks(tracks)} -@api.route("/artist//similar", methods=["GET"]) -def get_similar_artists(artisthash: str): +@api.get("/artist//similar") +def get_similar_artists(path: ArtistHashSchema, query: ArtistLimitSchema): """ Returns similar artists. """ - limit = request.args.get("limit") + limit = query.limit - if limit is None: - limit = 6 - - limit = int(limit) - - artist = ArtistStore.get_artist_by_hash(artisthash) + artist = ArtistStore.get_artist_by_hash(path.artisthash) if artist is None: return {"error": "Artist not found"}, 404