Finish documentation for all endpoints

+ fix #193 (settings https redirect)
+ fix open api docs on binary
+ fix git error on binary
+ remove flask-restful

hopefully, I didn't break something 😩
This commit is contained in:
mungai-njoroge
2024-03-24 15:57:58 +03:00
committed by Mungai Njoroge
parent 99ec11565c
commit 0af1ae1d8e
22 changed files with 547 additions and 418 deletions
+11 -12
View File
@@ -8,8 +8,6 @@ from flask_compress import Compress
from flask_openapi3 import Info
from flask_openapi3 import OpenAPI
from pydantic import BaseModel, Field
from flask_openapi3 import FileStorage
from app.settings import Keys
from .plugins import lyrics as lyrics_plugin
@@ -46,6 +44,7 @@ In endpoints that request multiple lists of items, this represents the number of
[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():
"""
Creates the Flask instance, registers modules and registers all the API blueprints.
@@ -72,23 +71,23 @@ def create_api():
app.register_api(search.api)
app.register_api(folder.api)
app.register_api(playlist.api)
app.register_blueprint(favorites.api)
app.register_blueprint(imgserver.api)
app.register_blueprint(settings.api)
app.register_blueprint(colors.api)
app.register_blueprint(lyrics.api)
app.register_api(favorites.api)
app.register_api(imgserver.api)
app.register_api(settings.api)
app.register_api(colors.api)
app.register_api(lyrics.api)
# Plugins
app.register_blueprint(plugins.api)
app.register_blueprint(lyrics_plugin.api)
app.register_api(plugins.api)
app.register_api(lyrics_plugin.api)
# Logger
app.register_blueprint(logger.api_bp)
app.register_api(logger.api)
# Home
app.register_blueprint(home.api_bp)
app.register_api(home.api)
# Flask Restful
app.register_blueprint(getall.api_bp)
app.register_api(getall.api)
return app
+1 -2
View File
@@ -98,7 +98,6 @@ def get_artist_albums(path: ArtistHashSchema, query: GetArtistAlbumsQuery):
limit = query.limit
all_albums = AlbumStore.get_albums_by_artisthash(artisthash)
# start: check for missing albums. ie. compilations and features
all_tracks = TrackStore.get_tracks_by_artisthash(artisthash)
@@ -163,7 +162,7 @@ def get_artist_albums(path: ArtistHashSchema, query: GetArtistAlbumsQuery):
if artist is None:
return {"error": "Artist not found"}, 404
if return_all is not None and return_all == "true":
if return_all:
limit = len(all_albums)
singles_and_eps = singles + eps
+11 -5
View File
@@ -1,12 +1,18 @@
from flask import Blueprint
from flask_openapi3 import Tag
from flask_openapi3 import APIBlueprint
from app.api.apischemas import AlbumHashSchema
from app.store.albums import AlbumStore as Store
api = Blueprint("colors", __name__, url_prefix="/colors")
bp_tag = Tag(name="Colors", description="Get item colors")
api = APIBlueprint("colors", __name__, url_prefix="/colors", abp_tags=[bp_tag])
@api.route("/album/<albumhash>")
def get_album_color(albumhash: str):
album = Store.get_album_by_hash(albumhash)
@api.get("/album/<albumhash>")
def get_album_color(path: AlbumHashSchema):
"""
Get album color
"""
album = Store.get_album_by_hash(path.albumhash)
msg = {"color": ""}
+77 -77
View File
@@ -1,7 +1,12 @@
from typing import List, TypeVar
from flask import Blueprint, request
from flask_openapi3 import Tag
from flask_openapi3 import APIBlueprint
from pydantic import BaseModel, Field
from app.api.apischemas import GenericLimitSchema
from app.models import FavType
from app.settings import Defaults
from app.utils.bisection import use_bisection
from app.db.sqlite.favorite import SQLiteFavoriteMethods as favdb
from app.serializers.track import serialize_track, serialize_tracks
@@ -13,8 +18,8 @@ from app.store.tracks import TrackStore
from app.store.artists import ArtistStore
from app.utils.dates import timestamp_to_time_passed
api = Blueprint("favorite", __name__, url_prefix="/")
bp_tag = Tag(name="Favorites", description="Your favorite items")
api = APIBlueprint("favorites", __name__, url_prefix="/favorites", abp_tags=[bp_tag])
T = TypeVar("T")
@@ -24,18 +29,23 @@ def remove_none(items: List[T]) -> List[T]:
return [i for i in items if i is not None]
@api.route("/favorite/add", methods=["POST"])
def add_favorite():
class FavoritesAddBody(BaseModel):
hash: str = Field(
description="The hash of the item",
min_length=Defaults.HASH_LENGTH,
max_length=Defaults.HASH_LENGTH,
example=Defaults.API_ALBUMHASH,
)
type: str = Field(description="The type of the item", example=FavType.album)
@api.post("/add")
def add_favorite(body: FavoritesAddBody):
"""
Adds a favorite to the database.
"""
data = request.get_json()
if data is None:
return {"error": "No data provided"}, 400
itemhash = data.get("hash")
itemtype = data.get("type")
itemhash = body.hash
itemtype = body.type
favdb.insert_one_favorite(itemtype, itemhash)
@@ -45,18 +55,13 @@ def add_favorite():
return {"msg": "Added to favorites"}
@api.route("/favorite/remove", methods=["POST"])
def remove_favorite():
@api.post("/remove")
def remove_favorite(body: FavoritesAddBody):
"""
Removes a favorite from the database.
"""
data = request.get_json()
if data is None:
return {"error": "No data provided"}, 400
itemhash = data.get("hash")
itemtype = data.get("type")
itemhash = body.hash
itemtype = body.type
favdb.delete_favorite(itemtype, itemhash)
@@ -66,15 +71,12 @@ def remove_favorite():
return {"msg": "Removed from favorites"}
@api.route("/albums/favorite")
def get_favorite_albums():
limit = request.args.get("limit")
if limit is None:
limit = 6
limit = int(limit)
@api.get("/albums")
def get_favorite_albums(query: GenericLimitSchema):
"""
Get favorite albums
"""
limit = query.limit
albums = favdb.get_fav_albums()
albumhashes = [a[1] for a in albums]
albumhashes.reverse()
@@ -90,15 +92,12 @@ def get_favorite_albums():
return {"albums": serialize_for_card_many(fav_albums[:limit])}
@api.route("/tracks/favorite")
def get_favorite_tracks():
limit = request.args.get("limit")
if limit is None:
limit = 6
limit = int(limit)
@api.get("/tracks")
def get_favorite_tracks(query: GenericLimitSchema):
"""
Get favorite tracks
"""
limit = query.limit
tracks = favdb.get_fav_tracks()
trackhashes = [t[1] for t in tracks]
trackhashes.reverse()
@@ -113,15 +112,12 @@ def get_favorite_tracks():
return {"tracks": serialize_tracks(tracks[:limit])}
@api.route("/artists/favorite")
def get_favorite_artists():
limit = request.args.get("limit")
if limit is None:
limit = 6
limit = int(limit)
@api.get("/artists")
def get_favorite_artists(query: GenericLimitSchema):
"""
Get favorite artists
"""
limit = query.limit
artists = favdb.get_fav_artists()
artisthashes = [a[1] for a in artists]
artisthashes.reverse()
@@ -137,27 +133,38 @@ def get_favorite_artists():
return {"artists": artists[:limit]}
@api.route("/favorites")
def get_all_favorites():
class GetAllFavoritesQuery(BaseModel):
"""
Extending this class will give you a model with the `limit` field
"""
track_limit: int = Field(
description="The number of tracks to return",
example=Defaults.API_CARD_LIMIT,
default=Defaults.API_CARD_LIMIT,
)
album_limit: int = Field(
description="The number of albums to return",
example=Defaults.API_CARD_LIMIT,
default=Defaults.API_CARD_LIMIT,
)
artist_limit: int = Field(
description="The number of artists to return",
example=Defaults.API_CARD_LIMIT,
default=Defaults.API_CARD_LIMIT,
)
@api.get("")
def get_all_favorites(query: GetAllFavoritesQuery):
"""
Returns all the favorites in the database.
"""
track_limit = request.args.get("track_limit")
album_limit = request.args.get("album_limit")
artist_limit = request.args.get("artist_limit")
if track_limit is None:
track_limit = 6
if album_limit is None:
album_limit = 6
if artist_limit is None:
artist_limit = 6
track_limit = int(track_limit)
album_limit = int(album_limit)
artist_limit = int(artist_limit)
track_limit = query.track_limit
album_limit = query.album_limit
artist_limit = query.artist_limit
# largest is x2 to accound for broken hashes if any
largest = max(track_limit, album_limit, artist_limit)
@@ -266,20 +273,13 @@ def get_all_favorites():
}
@api.route("/favorites/check")
def check_favorite():
@api.get("/check")
def check_favorite(query: FavoritesAddBody):
"""
Checks if a favorite exists in the database.
"""
itemhash = request.args.get("hash")
itemtype = request.args.get("type")
if itemhash is None:
return {"error": "No hash provided"}, 400
if itemtype is None:
return {"error": "No type provided"}, 400
itemhash = query.hash
itemtype = query.type
exists = favdb.check_is_favorite(itemhash, itemtype)
return {"is_favorite": exists}
+120 -5
View File
@@ -1,10 +1,125 @@
from flask import Blueprint
from flask_restful import Api
from .resources import Albums
from flask_openapi3 import Tag
from flask_openapi3 import APIBlueprint
from pydantic import BaseModel, Field
api_bp = Blueprint("getall", __name__, url_prefix="/getall")
api = Api(api_bp)
from datetime import datetime
from app.api.apischemas import GenericLimitSchema
from app.store.albums import AlbumStore
from app.store.artists import ArtistStore
from app.serializers.album import serialize_for_card as serialize_album
from app.serializers.artist import serialize_for_card as serialize_artist
from app.utils import format_number
from app.utils.dates import (
create_new_date,
date_string_to_time_passed,
seconds_to_time_string,
)
bp_tag = Tag(name="Get all", description="List all items")
api = APIBlueprint("getall", __name__, url_prefix="/getall", abp_tags=[bp_tag])
api.add_resource(Albums, "/<itemtype>")
class GetAllItemsBody(GenericLimitSchema):
start: int = Field(
description="The start index of the items to return",
example=0,
default=0,
)
sortby: str = Field(
description="The key to sort items by",
example="created_date",
default="created_date",
)
reverse: int = Field(
description="Reverse the sort",
example=1,
default=1,
)
class GetAllItemsPath(BaseModel):
itemtype: str = Field(
description="The type of items to return (albums | artists)",
example="albums",
default="albums",
)
@api.get("/<itemtype>")
def get_all_items(path: GetAllItemsPath, query: GetAllItemsBody):
"""
Get all items
Used to show all albums or artists in the library
"""
is_albums = path.itemtype == "albums"
is_artists = path.itemtype == "artists"
items = AlbumStore.albums
if is_artists:
items = ArtistStore.artists
start = query.start
limit = query.limit
sort = query.sortby
reverse = query.reverse == 1
# if sort == "":
# sort = "created_date"
sort_is_count = sort == "count"
sort_is_duration = sort == "duration"
sort_is_create_date = sort == "created_date"
sort_is_date = is_albums and sort == "date"
sort_is_artist = is_albums and sort == "albumartists"
sort_is_artist_trackcount = is_artists and sort == "trackcount"
sort_is_artist_albumcount = is_artists and sort == "albumcount"
lambda_sort = lambda x: getattr(x, sort)
if sort_is_artist:
lambda_sort = lambda x: getattr(x, sort)[0].name
sorted_items = sorted(items, key=lambda_sort, reverse=reverse)
items = sorted_items[start : start + limit]
album_list = []
for item in items:
item_dict = serialize_album(item) if is_albums else serialize_artist(item)
if sort_is_date:
item_dict["help_text"] = item.date
if sort_is_create_date:
date = create_new_date(datetime.fromtimestamp(item.created_date))
timeago = date_string_to_time_passed(date)
item_dict["help_text"] = timeago
if sort_is_count:
item_dict["help_text"] = (
f"{format_number(item.count)} track{'' if item.count == 1 else 's'}"
)
if sort_is_duration:
item_dict["help_text"] = seconds_to_time_string(item.duration)
if sort_is_artist_trackcount:
item_dict["help_text"] = (
f"{format_number(item.trackcount)} track{'' if item.trackcount == 1 else 's'}"
)
if sort_is_artist_albumcount:
item_dict["help_text"] = (
f"{format_number(item.albumcount)} album{'' if item.albumcount == 1 else 's'}"
)
album_list.append(item_dict)
return {"items": album_list, "total": len(sorted_items)}
-93
View File
@@ -1,93 +0,0 @@
from flask_restful import Resource, reqparse
from datetime import datetime
from app.store.albums import AlbumStore
from app.store.artists import ArtistStore
from app.serializers.album import serialize_for_card as serialize_album
from app.serializers.artist import serialize_for_card as serialize_artist
from app.utils import format_number
from app.utils.dates import (
create_new_date,
date_string_to_time_passed,
seconds_to_time_string,
)
parser = reqparse.RequestParser()
parser.add_argument("start", type=int, default=0, location="args")
parser.add_argument("limit", type=int, default=20, location="args")
parser.add_argument("sortby", type=str, default="created_date", location="args")
parser.add_argument("reverse", type=str, default="1", location="args")
class Albums(Resource):
def get(self, itemtype: str):
is_albums = itemtype == "albums"
is_artists = itemtype == "artists"
items = AlbumStore.albums
if is_artists:
items = ArtistStore.artists
args = parser.parse_args()
start = args["start"]
limit = args["limit"]
sort = args["sortby"]
reverse = args["reverse"] == "1"
if sort == "":
sort = "created_date"
sort_is_count = sort == "count"
sort_is_duration = sort == "duration"
sort_is_create_date = sort == "created_date"
sort_is_date = is_albums and sort == "date"
sort_is_artist = is_albums and sort == "albumartists"
sort_is_artist_trackcount = is_artists and sort == "trackcount"
sort_is_artist_albumcount = is_artists and sort == "albumcount"
lambda_sort = lambda x: getattr(x, sort)
if sort_is_artist:
lambda_sort = lambda x: getattr(x, sort)[0].name
sorted_items = sorted(items, key=lambda_sort, reverse=reverse)
items = sorted_items[start : start + limit]
album_list = []
for item in items:
item_dict = serialize_album(item) if is_albums else serialize_artist(item)
if sort_is_date:
item_dict["help_text"] = item.date
if sort_is_create_date:
date = create_new_date(datetime.fromtimestamp(item.created_date))
timeago = date_string_to_time_passed(date)
item_dict["help_text"] = timeago
if sort_is_count:
item_dict[
"help_text"
] = f"{format_number(item.count)} track{'' if item.count == 1 else 's'}"
if sort_is_duration:
item_dict["help_text"] = seconds_to_time_string(item.duration)
if sort_is_artist_trackcount:
item_dict[
"help_text"
] = f"{format_number(item.trackcount)} track{'' if item.trackcount == 1 else 's'}"
if sort_is_artist_albumcount:
item_dict[
"help_text"
] = f"{format_number(item.albumcount)} album{'' if item.albumcount == 1 else 's'}"
album_list.append(item_dict)
return {"items": album_list, "total": len(sorted_items)}
+21 -7
View File
@@ -1,11 +1,25 @@
from flask import Blueprint
from flask_restful import Api
from flask_openapi3 import Tag
from flask_openapi3 import APIBlueprint
from .recents import RecentlyAdded, RecentlyPlayed
from app.api.apischemas import GenericLimitSchema
from app.lib.home.recentlyadded import get_recent_items
from app.lib.home.recentlyplayed import get_recently_played
api_bp = Blueprint("home", __name__, url_prefix="/home")
api = Api(api_bp)
bp_tag = Tag(name="Home", description="Homepage items")
api = APIBlueprint("home", __name__, url_prefix="/home", abp_tags=[bp_tag])
api.add_resource(RecentlyAdded, "/recents/added")
api.add_resource(RecentlyPlayed, "/recents/played")
@api.get("/recents/added")
def get_recently_added(query: GenericLimitSchema):
"""
Get recently added
"""
return {"items": get_recent_items(query.limit)}
@api.get("/recents/played")
def get_recent_plays(query: GenericLimitSchema):
"""
Get recently played
"""
return {"items": get_recently_played(query.limit)}
-24
View File
@@ -1,24 +0,0 @@
from flask_restful import Resource, reqparse
from app.lib.home.recentlyadded import get_recent_items
from app.lib.home.recentlyplayed import get_recently_played
parser = reqparse.RequestParser()
parser.add_argument("limit", type=int, required=False, default=7, location="args")
class RecentlyAdded(Resource):
def get(self):
args = parser.parse_args()
limit = args["limit"]
return {"items": get_recent_items(limit)}
class RecentlyPlayed(Resource):
def get(self):
args = parser.parse_args()
limit = args["limit"]
return {"items": get_recently_played(limit)}
+76 -42
View File
@@ -1,88 +1,122 @@
from pathlib import Path
from flask_openapi3 import Tag
from flask_openapi3 import APIBlueprint
from pydantic import BaseModel, Field
from flask import send_from_directory
from flask import Blueprint, send_from_directory
from app.settings import Defaults, Paths
from app.settings import Paths
api = Blueprint("imgserver", __name__, url_prefix="/img")
@api.route("/")
def hello():
return "<h1>Image Server</h1>"
bp_tag = Tag(
name="Images", description="Image filenames are constructured as '{itemhash}.webp'"
)
api = APIBlueprint("imgserver", __name__, url_prefix="/img", abp_tags=[bp_tag])
def send_fallback_img(filename: str = "default.webp"):
path = Paths.get_assets_path()
img = Path(path) / filename
folder = Paths.get_assets_path()
img = Path(folder) / filename
if not img.exists():
return "", 404
return send_from_directory(path, filename)
return send_from_directory(folder, filename)
@api.route("/t/o/<imgpath>")
def send_original_thumbnail(imgpath: str):
path = Paths.get_original_thumb_path()
fpath = Path(path) / imgpath
class ImagePath(BaseModel):
imgpath: str = Field(
description="The image filename",
example=Defaults.API_ALBUMHASH + ".webp",
)
@api.get("/t/o/<imgpath>")
def send_original_thumbnail(path: ImagePath):
"""
Get original thumbnail
"""
folder = Paths.get_original_thumb_path()
fpath = Path(folder) / path.imgpath
if fpath.exists():
return send_from_directory(path, imgpath)
return send_from_directory(folder, path.imgpath)
return send_fallback_img()
@api.route("/t/<imgpath>")
def send_lg_thumbnail(imgpath: str):
path = Paths.get_lg_thumb_path()
fpath = Path(path) / imgpath
@api.get("/t/<imgpath>")
def send_lg_thumbnail(path: ImagePath):
"""
Get large thumbnail (500 x 500)
"""
folder = Paths.get_lg_thumb_path()
fpath = Path(folder) / path.imgpath
if fpath.exists():
return send_from_directory(path, imgpath)
return send_from_directory(folder, path.imgpath)
return send_fallback_img()
@api.route("/t/s/<imgpath>")
def send_sm_thumbnail(imgpath: str):
path = Paths.get_sm_thumb_path()
fpath = Path(path) / imgpath
@api.get("/t/s/<imgpath>")
def send_sm_thumbnail(path: ImagePath):
"""
Get small thumbnail (64 x 64)
"""
folder = Paths.get_sm_thumb_path()
fpath = Path(folder) / path.imgpath
if fpath.exists():
return send_from_directory(path, imgpath)
return send_from_directory(folder, path.imgpath)
return send_fallback_img()
@api.route("/a/<imgpath>")
def send_lg_artist_image(imgpath: str):
path = Paths.get_artist_img_lg_path()
fpath = Path(path) / imgpath
@api.get("/a/<imgpath>")
def send_lg_artist_image(path: ImagePath):
"""
Get large artist image (500 x 500)
"""
folder = Paths.get_artist_img_lg_path()
fpath = Path(folder) / path.imgpath
if fpath.exists():
return send_from_directory(path, imgpath)
return send_from_directory(folder, path.imgpath)
return send_fallback_img("artist.webp")
@api.route("/a/s/<imgpath>")
def send_sm_artist_image(imgpath: str):
path = Paths.get_artist_img_sm_path()
fpath = Path(path) / imgpath
@api.get("/a/s/<imgpath>")
def send_sm_artist_image(path: ImagePath):
"""
Get small artist image (64 x 64)
"""
folder = Paths.get_artist_img_sm_path()
fpath = Path(folder) / path.imgpath
if fpath.exists():
return send_from_directory(path, imgpath)
return send_from_directory(folder, path.imgpath)
return send_fallback_img("artist.webp")
@api.route("/p/<imgpath>")
def send_playlist_image(imgpath: str):
path = Paths.get_playlist_img_path()
fpath = Path(path) / imgpath
class PlaylistImagePath(BaseModel):
imgpath: str = Field(
description="The image path",
example="1.webp",
)
@api.get("/p/<imgpath>")
def send_playlist_image(path: PlaylistImagePath):
"""
Get playlist image
Images are constructed as '{playlist_id}.webp'
"""
folder = Paths.get_playlist_img_path()
fpath = Path(folder) / path.imgpath
if fpath.exists():
return send_from_directory(path, imgpath)
return send_from_directory(folder, path.imgpath)
return send_fallback_img("playlist.svg")
+33 -6
View File
@@ -1,11 +1,38 @@
from flask import Blueprint
from flask_restful import Api
from flask_openapi3 import Tag
from flask_openapi3 import APIBlueprint
from pydantic import Field
from app.api.apischemas import TrackHashSchema
from app.api.logger.tracks import LogTrack
from app.db.sqlite.logger.tracks import SQLiteTrackLogger as db
from app.settings import Defaults
bp_tag = Tag(name="Logger", description="Log item plays")
api = APIBlueprint("logger", __name__, url_prefix="/logger", abp_tags=[bp_tag])
api_bp = Blueprint("logger", __name__, url_prefix="/logger")
api = Api(api_bp)
class LogTrackBody(TrackHashSchema):
timestamp: int = Field(description="The timestamp of the track", example=1622217600)
duration: int = Field(
description="The duration of the track in seconds", example=300
)
source: str = Field(
description="The play source of the track",
example=f"al:{Defaults.API_ALBUMHASH}",
)
api.add_resource(LogTrack, "/track/log")
@api.post("/track/log")
def log_track(body: LogTrackBody):
"""
Log a track play to the database.
"""
trackhash = body.trackhash
timestamp = body.timestamp
duration = body.duration
source = body.source
last_row = db.insert_track(
trackhash=trackhash, timestamp=timestamp, duration=duration, source=source
)
return {"last_row": last_row}
-19
View File
@@ -1,19 +0,0 @@
from flask_restful import Resource, reqparse
from app.db.sqlite.logger.tracks import SQLiteTrackLogger as db
parser = reqparse.RequestParser()
parser.add_argument("trackhash", type=str, required=True)
parser.add_argument("timestamp", type=int, required=True)
parser.add_argument("duration", type=int, required=True)
parser.add_argument("source", type=str, required=True)
class LogTrack(Resource):
def post(self):
args = parser.parse_args(strict=True)
last_row = db.insert_track(
args["trackhash"], args["duration"], args["source"], args["timestamp"]
)
return {"last_row": last_row}
+24 -21
View File
@@ -1,5 +1,8 @@
from flask import Blueprint, request
from flask_openapi3 import Tag
from flask_openapi3 import APIBlueprint
from pydantic import Field
from app.api.apischemas import TrackHashSchema
from app.lib.lyrics import (
get_lyrics,
check_lyrics_file,
@@ -7,21 +10,24 @@ from app.lib.lyrics import (
get_lyrics_from_tags,
)
api = Blueprint("lyrics", __name__, url_prefix="")
bp_tag = Tag(name="Lyrics", description="Get lyrics")
api = APIBlueprint("lyrics", __name__, url_prefix="/lyrics", abp_tags=[bp_tag])
@api.route("/lyrics", methods=["POST"])
def send_lyrics():
class SendLyricsBody(TrackHashSchema):
filepath: str = Field(
description="The path to the file",
example="/path/to/file.mp3",
)
@api.post("")
def send_lyrics(body: SendLyricsBody):
"""
Returns the lyrics for a track
"""
data = request.get_json()
filepath = data.get("filepath", None)
trackhash = data.get("trackhash", None)
if filepath is None or trackhash is None:
return {"error": "No filepath or trackhash provided"}, 400
filepath = body.filepath
trackhash = body.trackhash
is_synced = True
lyrics, copyright = get_lyrics(filepath)
@@ -38,15 +44,13 @@ def send_lyrics():
return {"lyrics": lyrics, "synced": is_synced, "copyright": copyright}, 200
@api.route("/lyrics/check", methods=["POST"])
def check_lyrics():
data = request.get_json()
filepath = data.get("filepath", None)
trackhash = data.get("trackhash", None)
if filepath is None or trackhash is None:
return {"error": "No filepath or trackhash provided"}, 400
@api.post("/check")
def check_lyrics(body: SendLyricsBody):
"""
Checks if lyrics exist for a track
"""
filepath = body.filepath
trackhash = body.trackhash
exists = check_lyrics_file(filepath, trackhash)
@@ -56,4 +60,3 @@ def check_lyrics():
exists = get_lyrics_from_tags(filepath, just_check=True)
return {"exists": exists}, 200
+38 -15
View File
@@ -1,37 +1,60 @@
from flask import Blueprint, request
from flask_openapi3 import Tag
from flask_openapi3 import APIBlueprint
from pydantic import BaseModel, Field
from app.db.sqlite.plugins import PluginsMethods
api = Blueprint("plugins", __name__, url_prefix="/plugins")
bp_tag = Tag(name="Plugins", description="Manage plugins")
api = APIBlueprint("plugins", __name__, url_prefix="/plugins", abp_tags=[bp_tag])
@api.route("/", methods=["GET"])
@api.get("/")
def get_all_plugins():
"""
List all plugins
"""
plugins = PluginsMethods.get_all_plugins()
return {"plugins": plugins}
@api.route("/setactive", methods=["GET"])
def activate_deactivate_plugin():
name = request.args.get("plugin", None)
state = request.args.get("state", None)
class PluginBody(BaseModel):
plugin: str = Field(description="The plugin name", example="lyrics")
if not name or not state:
return {"error": "Missing plugin or state"}, 400
PluginsMethods.plugin_set_active(name, int(state))
class PluginActivateBody(PluginBody):
active: bool = Field(
description="New plugin active state", example=False, default=False
)
@api.post("/setactive")
def activate_deactivate_plugin(body: PluginActivateBody):
"""
Activate/Deactivate plugin
"""
name = body.plugin
active = 1 if body.active else 0
PluginsMethods.plugin_set_active(name, active)
return {"message": "OK"}, 200
@api.route("/settings", methods=["POST"])
def update_plugin_settings():
data = request.get_json()
class PluginSettingsBody(PluginBody):
settings: dict = Field(
description="The new plugin settings", example={"key": "value"}
)
plugin = data.get("plugin", None)
settings = data.get("settings", None)
@api.post("/settings")
def update_plugin_settings(body: PluginSettingsBody):
"""
Update plugin settings
"""
plugin = body.plugin
settings = body.settings
if not plugin or not settings:
return {"error": "Missing plugin or settings"}, 400
+28 -11
View File
@@ -1,24 +1,41 @@
from flask import Blueprint, request
from flask_openapi3 import Tag
from flask_openapi3 import APIBlueprint
from pydantic import Field
from app.api.apischemas import TrackHashSchema
from app.lib.lyrics import format_synced_lyrics
from app.plugins.lyrics import Lyrics
from app.settings import Defaults
from app.utils.hashing import create_hash
api = Blueprint("lyricsplugin", __name__, url_prefix="/plugins/lyrics")
bp_tag = Tag(name="Lyrics Plugin", description="Musixmatch lyrics plugin")
api = APIBlueprint(
"lyricsplugin", __name__, url_prefix="/plugins/lyrics", abp_tags=[bp_tag]
)
@api.route("/search", methods=["POST"])
def search_lyrics():
data = request.get_json()
class LyricsSearchBody(TrackHashSchema):
title: str = Field(description="The track title ", example=Defaults.API_TRACKNAME)
artist: str = Field(description="The track artist ", example=Defaults.API_ARTISTNAME)
album: str = Field(description="The track track album ", example=Defaults.API_ALBUMNAME)
filepath: str = Field(
description="Track filepath to save the lyrics file relative to",
example="/home/cwilvx/temp/crazy song.mp3",
)
trackhash = data.get("trackhash", "")
title = data.get("title", "")
artist = data.get("artist", "")
album = data.get("album", "")
filepath = data.get("filepath", None)
@api.post("/search")
def search_lyrics(body: LyricsSearchBody):
"""
Search for lyrics by title and artist
"""
title = body.title
artist = body.artist
album = body.album
filepath = body.filepath
trackhash = body.trackhash
finder = Lyrics()
data = finder.search_lyrics_by_title_and_artist(title, artist)
if not data:
+46 -26
View File
@@ -1,4 +1,8 @@
from flask import Blueprint, request
from typing import Any
from flask import request
from flask_openapi3 import Tag
from flask_openapi3 import APIBlueprint
from pydantic import BaseModel, Field
from app.db.sqlite.plugins import PluginsMethods as pdb
from app.db.sqlite.settings import SettingsSQLMethods as sdb
@@ -12,7 +16,8 @@ from app.store.tracks import TrackStore
from app.utils.generators import get_random_str
from app.utils.threading import background
api = Blueprint("settings", __name__, url_prefix="")
bp_tag = Tag(name="Settings", description="Customize stuff")
api = APIBlueprint("settings", __name__, url_prefix="/notsettings", abp_tags=[bp_tag])
def get_child_dirs(parent: str, children: list[str]):
@@ -77,23 +82,24 @@ def finalize(new_: list[str], removed_: list[str], db_dirs_: list[str]):
rebuild_store(db_dirs_)
@api.route("/settings/add-root-dirs", methods=["POST"])
def add_root_dirs():
class AddRootDirsBody(BaseModel):
new_dirs: list[str] = Field(
description="The new directories to add",
example=["/home/user/Music", "/home/user/Downloads"],
)
removed: list[str] = Field(
description="The directories to remove",
example=["/home/user/Downloads"],
)
@api.post("/add-root-dirs")
def add_root_dirs(body: AddRootDirsBody):
"""
Add custom root directories to the database.
"""
msg = {"msg": "Failed! No directories were given."}
data = request.get_json()
if data is None:
return msg, 400
try:
new_dirs: list[str] = data["new_dirs"]
removed_dirs: list[str] = data["removed"]
except KeyError:
return msg, 400
new_dirs = body.new_dirs
removed_dirs = body.removed
db_dirs = sdb.get_root_dirs()
_h = "$home"
@@ -132,10 +138,10 @@ def add_root_dirs():
return {"root_dirs": db_dirs}
@api.route("/settings/get-root-dirs", methods=["GET"])
@api.get("/get-root-dirs")
def get_root_dirs():
"""
Get custom root directories from the database.
Get root directories
"""
dirs = sdb.get_root_dirs()
@@ -154,10 +160,10 @@ mapp = {
}
@api.route("/settings/", methods=["GET"])
@api.get("")
def get_all_settings():
"""
Get all settings from the database.
Get all settings
"""
settings = sdb.get_all_settings()
@@ -195,10 +201,24 @@ def reload_all_for_set_setting():
reload_everything(get_random_str())
@api.route("/settings/set", methods=["POST"])
def set_setting():
key = request.get_json().get("key")
value = request.get_json().get("value")
class SetSettingBody(BaseModel):
key: str = Field(
description="The setting key",
example="artist_separators",
)
value: Any = Field(
description="The setting value",
example=",",
)
@api.post("/set")
def set_setting(body: SetSettingBody):
"""
Set a setting.
"""
key = body.key
value = body.value
if key is None or value is None or key == "root_dirs":
return {"msg": "Invalid arguments!"}, 400
@@ -235,10 +255,10 @@ def run_populate():
populate.Populate(instance_key=get_random_str())
@api.route("/settings/trigger-scan", methods=["GET"])
@api.get("/trigger-scan")
def trigger_scan():
"""
Triggers a scan.
Triggers scan for new music
"""
run_populate()
+11 -1
View File
@@ -1,6 +1,7 @@
"""
Handles arguments passed to the program.
"""
import os.path
import sys
@@ -9,6 +10,7 @@ import PyInstaller.__main__ as bundler
from app import settings
from app.logger import log
from app.print_help import HELP_MESSAGE
from app.utils.paths import getFlaskOpenApiPath
from app.utils.xdg_utils import get_xdg_config_dir
from app.utils.wintools import is_windows
@@ -45,6 +47,8 @@ class HandleArgs:
config_keys = [
"SWINGMUSIC_APP_VERSION",
"GIT_LATEST_COMMIT_HASH",
"GIT_CURRENT_BRANCH",
]
lines = []
@@ -65,6 +69,8 @@ class HandleArgs:
_s = ";" if is_windows() else ":"
flask_openapi_path = getFlaskOpenApiPath()
bundler.run(
[
"manage.py",
@@ -74,6 +80,7 @@ class HandleArgs:
"--clean",
f"--add-data=assets{_s}assets",
f"--add-data=client{_s}client",
f"--add-data={flask_openapi_path}/templates/static{_s}flask_openapi3/templates/static",
f"--icon=assets/logo-fill.light.ico",
"-y",
]
@@ -176,5 +183,8 @@ class HandleArgs:
@staticmethod
def handle_version():
if any((a in ARGS for a in ALLARGS.version)):
print(settings.Keys.SWINGMUSIC_APP_VERSION)
print(f"VERSION: v{settings.Keys.SWINGMUSIC_APP_VERSION}")
print(
f"COMMIT#: {settings.Keys.GIT_CURRENT_BRANCH}/{settings.Keys.GIT_LATEST_COMMIT_HASH}"
)
sys.exit(0)
+2
View File
@@ -1 +1,3 @@
SWINGMUSIC_APP_VERSION = ""
GIT_LATEST_COMMIT_HASH = ""
GIT_CURRENT_BRANCH = ""
+2 -2
View File
@@ -66,9 +66,9 @@ class PluginsMethods:
return []
@classmethod
def plugin_set_active(cls, name: str, state: int):
def plugin_set_active(cls, name: str, active: int):
with SQLiteManager(userdata_db=True) as cur:
cur.execute("UPDATE plugins SET active=? WHERE name=?", (state, name))
cur.execute("UPDATE plugins SET active=? WHERE name=?", (active, name))
cur.close()
@classmethod
+33 -3
View File
@@ -3,6 +3,7 @@ Contains default configs
"""
import os
import subprocess
import sys
from typing import Any
@@ -103,6 +104,7 @@ class Defaults:
API_TRACKHASH = "0853280a12"
API_ALBUMNAME = "Rumours"
API_ARTISTNAME = "girl in red"
API_TRACKNAME = "Apartment 402"
API_CARD_LIMIT = 6
@@ -242,21 +244,49 @@ class TCOLOR:
# credits: https://stackoverflow.com/a/287944
def getLatestCommitHash():
"""
Returns the latest git commit hash for the current branch
"""
try:
hash = subprocess.check_output(["git", "rev-parse", "--short", "HEAD"])
return hash.decode("utf-8").strip()
except:
return ""
def getCurrentBranch():
"""
Returns the current git branch
"""
try:
branch = subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"])
return branch.decode("utf-8").strip()
except:
return ""
class Keys:
SWINGMUSIC_APP_VERSION = os.environ.get("SWINGMUSIC_APP_VERSION")
GIT_LATEST_COMMIT_HASH = "<unset>"
GIT_CURRENT_BRANCH = "<unset>"
@classmethod
def load(cls):
if IS_BUILD:
cls.SWINGMUSIC_APP_VERSION = configs.SWINGMUSIC_APP_VERSION
cls.GIT_LATEST_COMMIT_HASH = configs.GIT_LATEST_COMMIT_HASH
cls.GIT_CURRENT_BRANCH = configs.GIT_CURRENT_BRANCH
else:
cls.GIT_LATEST_COMMIT_HASH = getLatestCommitHash()
cls.GIT_CURRENT_BRANCH = getCurrentBranch()
cls.verify_keys()
@classmethod
def verify_keys(cls):
# if not cls.LASTFM_API_KEY:
# print("ERROR: LASTFM_API_KEY not set in environment")
# sys.exit(0)
pass
@classmethod
+12
View File
@@ -0,0 +1,12 @@
import sys
def getFlaskOpenApiPath():
"""
Used to retrieve the path to the flask_openapi3 package
See: https://github.com/luolingchun/flask-openapi3/issues/147
"""
site_packages_path = [p for p in sys.path if "site-packages" in p][0]
return f"{site_packages_path}/flask_openapi3"
Generated
+1 -46
View File
@@ -11,20 +11,6 @@ files = [
{file = "altgraph-0.17.4.tar.gz", hash = "sha256:1b5afbb98f6c4dcadb2e2ae6ab9fa994bbb8c1d75f4fa96d340f9437ae454406"},
]
[[package]]
name = "aniso8601"
version = "9.0.1"
description = "A library for parsing ISO 8601 strings."
optional = false
python-versions = "*"
files = [
{file = "aniso8601-9.0.1-py2.py3-none-any.whl", hash = "sha256:1d2b7ef82963909e93c4f24ce48d4de9e66009a21bf1c1e1c85bdd0812fe412f"},
{file = "aniso8601-9.0.1.tar.gz", hash = "sha256:72e3117667eedf66951bb2d93f4296a56b94b078a8a95905a052611fb3f1b973"},
]
[package.extras]
dev = ["black", "coverage", "isort", "pre-commit", "pyenchant", "pylint"]
[[package]]
name = "annotated-types"
version = "0.6.0"
@@ -592,26 +578,6 @@ dotenv = ["python-dotenv"]
email = ["email-validator"]
yaml = ["pyyaml"]
[[package]]
name = "flask-restful"
version = "0.3.10"
description = "Simple framework for creating REST APIs"
optional = false
python-versions = "*"
files = [
{file = "Flask-RESTful-0.3.10.tar.gz", hash = "sha256:fe4af2ef0027df8f9b4f797aba20c5566801b6ade995ac63b588abf1a59cec37"},
{file = "Flask_RESTful-0.3.10-py2.py3-none-any.whl", hash = "sha256:1cf93c535172f112e080b0d4503a8d15f93a48c88bdd36dd87269bdaf405051b"},
]
[package.dependencies]
aniso8601 = ">=0.82"
Flask = ">=0.8"
pytz = "*"
six = ">=1.3.0"
[package.extras]
docs = ["sphinx"]
[[package]]
name = "gevent"
version = "23.9.1"
@@ -1697,17 +1663,6 @@ files = [
[package.dependencies]
six = ">=1.5"
[[package]]
name = "pytz"
version = "2023.3.post1"
description = "World timezone definitions, modern and historical"
optional = false
python-versions = "*"
files = [
{file = "pytz-2023.3.post1-py2.py3-none-any.whl", hash = "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7"},
{file = "pytz-2023.3.post1.tar.gz", hash = "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b"},
]
[[package]]
name = "pywin32"
version = "306"
@@ -2513,4 +2468,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
[metadata]
lock-version = "2.0"
python-versions = ">=3.10,<3.12"
content-hash = "7969ca61599f24a005909514cf11d35dd11b6f82f5958bb84c271d8156399755"
content-hash = "feb13f92b7b3a909fcb851860a405b96579feac0e2dde7681ed0e9c381c4f6cd"
-1
View File
@@ -20,7 +20,6 @@ show-in-file-manager = "^1.1.4"
flask-compress = "^1.13"
tabulate = "^0.9.0"
setproctitle = "^1.3.2"
flask-restful = "^0.3.10"
locust = "^2.20.1"
waitress = "^2.1.2"
watchdog = "^4.0.0"