add docs for playlist endpoints

+ limit recently added tracks to 100
This commit is contained in:
mungai-njoroge
2024-03-10 19:44:12 +03:00
committed by Mungai Njoroge
parent 4edb3a5e7a
commit ae031014a9
8 changed files with 207 additions and 166 deletions
+1
View File
@@ -1,6 +1,7 @@
# What's New?
- Hovering on recent favorite item will show how long ago it was ♥ed
- Recently added playlist returns a max of 100 tracks, but without a cutoff period
# Development
- API documentation on /openapi
+4 -2
View File
@@ -8,6 +8,8 @@ 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
@@ -68,8 +70,8 @@ def create_api():
app.register_api(artist.api)
app.register_api(send_file.api)
app.register_api(search.api)
app.register_blueprint(folder.api)
app.register_blueprint(playlist.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)
+6 -3
View File
@@ -26,7 +26,7 @@ api = APIBlueprint("artist", __name__, url_prefix="/artist", abp_tags=[bp_tag])
@api.get("/<string:artisthash>")
def get_artist(path: ArtistHashSchema, query: TrackLimitSchema):
"""
Get artist data.
Get artist
Returns artist data, tracks and genres for the given artisthash.
"""
@@ -89,6 +89,9 @@ class GetArtistAlbumsQuery(AlbumLimitSchema):
@api.get("/<artisthash>/albums")
def get_artist_albums(path: ArtistHashSchema, query: GetArtistAlbumsQuery):
"""
Get artist albums.
"""
return_all = query.all
artisthash = path.artisthash
@@ -177,7 +180,7 @@ def get_artist_albums(path: ArtistHashSchema, query: GetArtistAlbumsQuery):
@api.get("/<artisthash>/tracks")
def get_all_artist_tracks(path: ArtistHashSchema):
"""
Get all artist tracks
Get artist tracks
Returns all artists by a given artist.
"""
@@ -189,7 +192,7 @@ def get_all_artist_tracks(path: ArtistHashSchema):
@api.get("/<artisthash>/similar")
def get_similar_artists(path: ArtistHashSchema, query: ArtistLimitSchema):
"""
Returns similar artists.
Get similar artists.
"""
limit = query.limit
+59 -37
View File
@@ -1,11 +1,14 @@
"""
Contains all the folder routes.
"""
import os
from pathlib import Path
import psutil
from flask import Blueprint, request
from pydantic import BaseModel, Field
from flask_openapi3 import Tag
from flask_openapi3 import APIBlueprint
from showinfm import show_in_file_manager
from app import settings
@@ -15,24 +18,24 @@ from app.serializers.track import serialize_track
from app.store.tracks import TrackStore as store
from app.utils.wintools import is_windows, win_replace_slash
api = Blueprint("folder", __name__, url_prefix="")
tag = Tag(name="Folders", description="Get folders and tracks in a directory")
api = APIBlueprint("folder", __name__, url_prefix="/folder", abp_tags=[tag])
@api.route("/folder", methods=["POST"])
def get_folder_tree():
class FolderTree(BaseModel):
folder: str = Field("$home", description="The folder to things from")
tracks_only: bool = Field(False, description="Whether to only get tracks")
@api.post("")
def get_folder_tree(body: FolderTree):
"""
Get folder
Returns a list of all the folders and tracks in the given folder.
"""
data = request.get_json()
req_dir = "$home"
tracks_only = False
if data is not None:
try:
req_dir: str = data["folder"]
tracks_only: bool = data["tracks_only"]
except KeyError:
req_dir = "$home"
req_dir = body.folder
tracks_only = body.tracks_only
root_dirs = db.get_root_dirs()
root_dirs.sort()
@@ -92,18 +95,23 @@ def get_all_drives(is_win: bool = False):
return drives
@api.route("/folder/dir-browser", methods=["POST"])
def list_folders():
"""
Returns a list of all the folders in the given folder.
"""
data = request.get_json()
is_win = is_windows()
class DirBrowserBody(BaseModel):
folder: str = Field(
"$root",
description="The folder to list directories from",
)
try:
req_dir: str = data["folder"]
except KeyError:
req_dir = "$root"
@api.post("/folder/dir-browser")
def list_folders(body: DirBrowserBody):
"""
List folders
Returns a list of all the folders in the given folder.
Used when selecting root dirs.
"""
req_dir = body.folder
is_win = is_windows()
if req_dir == "$root":
return {
@@ -131,26 +139,40 @@ def list_folders():
}
@api.route("/folder/show-in-files")
def open_in_file_manager():
path = request.args.get("path")
class FolderOpenInFileManagerQuery(BaseModel):
path: str = Field(
description="The path to open in the file manager",
)
if path is None:
return {"error": "No path provided."}, 400
show_in_file_manager(path)
@api.get("/folder/show-in-files")
def open_in_file_manager(query: FolderOpenInFileManagerQuery):
"""
Open in file manager
Opens the given path in the file manager on the host machine.
"""
show_in_file_manager(query.path)
return {"success": True}
@api.route("/folder/tracks/all")
def get_tracks_in_path():
path = request.args.get("path")
class GetTracksInPathQuery(BaseModel):
path: str = Field(
description="The path to get tracks from",
)
if path is None:
return {"error": "No path provided."}, 400
tracks = store.get_tracks_in_path(path)
@api.get("/folder/tracks/all")
def get_tracks_in_path(query: GetTracksInPathQuery):
"""
Get tracks in path
Gets all (or a max of 300) tracks from the given path and its subdirectories.
Used when adding tracks to the queue.
"""
tracks = store.get_tracks_in_path(query.path)
tracks = sorted(tracks, key=lambda i: i.last_mod)
tracks = (serialize_track(t) for t in tracks if Path(t.filepath).exists())
+118 -110
View File
@@ -1,12 +1,15 @@
"""
All playlist-related routes.
"""
import json
from datetime import datetime
import pathlib
from flask import Blueprint, request
from PIL import UnidentifiedImageError, Image
from pydantic import BaseModel, Field
from flask_openapi3 import Tag
from flask_openapi3 import APIBlueprint, FileStorage
from app import models
from app.db.sqlite.playlists import SQLitePlaylistMethods
@@ -18,23 +21,26 @@ from app.utils.dates import create_new_date, date_string_to_time_passed
from app.utils.remove_duplicates import remove_duplicates
from app.settings import Paths
api = Blueprint("playlist", __name__, url_prefix="/")
tag = Tag(name="Playlists", description="Get and manage playlists")
api = APIBlueprint("playlists", __name__, url_prefix="/playlists", abp_tags=[tag])
PL = SQLitePlaylistMethods
@api.route("/playlists", methods=["GET"])
def send_all_playlists():
class SendAllPlaylistsQuery(BaseModel):
no_images: bool = Field(False, description="Whether to include images")
@api.get("", methods=["GET"])
def send_all_playlists(query: SendAllPlaylistsQuery):
"""
Gets all the playlists.
"""
no_images = request.args.get("no_images", False)
playlists = PL.get_all_playlists()
playlists = list(playlists)
for playlist in playlists:
if not no_images:
if not query.no_images:
playlist.images = playlistlib.get_first_4_images(
trackhashes=playlist.trackhashes
)
@@ -69,22 +75,23 @@ def insert_playlist(name: str, image: str = None):
return PL.insert_one_playlist(playlist)
@api.route("/playlist/new", methods=["POST"])
def create_playlist():
class CreatePlaylistBody(BaseModel):
name: str = Field(..., description="The name of the playlist")
@api.post("/new")
def create_playlist(body: CreatePlaylistBody):
"""
New playlist
Creates a new playlist. Accepts POST method with a JSON body.
"""
data = request.get_json()
if data is None:
return {"error": "Playlist name not provided"}, 400
existing_playlist_count = PL.count_playlist_by_name(data["name"])
existing_playlist_count = PL.count_playlist_by_name(body.name)
if existing_playlist_count > 0:
return {"error": "Playlist already exists"}, 409
playlist = insert_playlist(data["name"])
playlist = insert_playlist(body.name)
if playlist is None:
return {"error": "Playlist could not be created"}, 500
@@ -119,25 +126,30 @@ def get_artist_trackhashes(artisthash: str):
return [t.trackhash for t in tracks]
@api.route("/playlist/<playlist_id>/add", methods=["POST"])
def add_item_to_playlist(playlist_id: str):
class PlaylistIDPath(BaseModel):
# INFO: playlistid string examples: "recentlyadded"
playlistid: int | str = Field(..., description="The ID of the playlist")
class AddItemToPlaylistBody(BaseModel):
itemtype: str = Field(
default="tracks",
description="The type of item to add",
examples=["tracks", "folder", "album", "artist"],
)
itemhash: str = Field(..., description="The hash of the item to add")
@api.post("/<playlistid>/add")
def add_item_to_playlist(path: PlaylistIDPath, body: AddItemToPlaylistBody):
"""
Takes a playlist ID and a track hash, and adds the track to the playlist
Add to playlist.
If itemtype is not "tracks", itemhash is expected to be a folder, album or artist hash.
"""
data = request.get_json()
if data is None:
return {"error": "Track hash not provided"}, 400
try:
itemtype = data["itemtype"]
except KeyError:
itemtype = None
try:
itemhash: str = data["itemhash"]
except KeyError:
itemhash = None
itemtype = body.itemtype
itemhash = body.itemhash
playlist_id = path.playlistid
if itemtype == "tracks":
trackhashes = itemhash.split(",")
@@ -158,13 +170,17 @@ def add_item_to_playlist(playlist_id: str):
return {"msg": "Done"}, 200
@api.route("/playlist/<playlistid>")
def get_playlist(playlistid: str):
class GetPlaylistQuery(BaseModel):
no_tracks: bool = Field(False, description="Whether to include tracks")
@api.get("/<playlistid>")
def get_playlist(path: PlaylistIDPath, query: GetPlaylistQuery):
"""
Gets a playlist by id, and if it exists, it gets all the tracks in the playlist and returns them.
Get playlist by id
"""
no_tracks = request.args.get("no_tracks", "false")
no_tracks = no_tracks == "true"
no_tracks = query.no_tracks
playlistid = path.playlistid
is_recently_added = playlistid == "recentlyadded"
@@ -201,31 +217,40 @@ def get_playlist(playlistid: str):
return {"info": playlist, "tracks": tracks if not no_tracks else []}
@api.route("/playlist/<playlistid>/update", methods=["PUT"])
def update_playlist_info(playlistid: str):
if playlistid is None:
return {"error": "Playlist ID not provided"}, 400
class UpdatePlaylistForm(BaseModel):
image: FileStorage = Field(None, description="The image file")
name: str = Field(..., description="The name of the playlist")
settings: str = Field(
...,
description="The settings of the playlist",
example='{"has_gif": false, "banner_pos": 50, "square_img": false, "pinned": false}',
)
db_playlist = PL.get_playlist_by_id(int(playlistid))
@api.put("/<playlistid>/update", methods=["PUT"])
def update_playlist_info(path: PlaylistIDPath, form: UpdatePlaylistForm):
"""
Update playlist
"""
playlistid = path.playlistid
db_playlist = PL.get_playlist_by_id(playlistid)
if db_playlist is None:
return {"error": "Playlist not found"}, 404
image = None
image = form.image
if "image" in request.files:
image = request.files["image"]
if form.image:
image = form.image
data = request.form
settings = json.loads(data.get("settings"))
settings = json.loads(form.settings)
settings["has_gif"] = False
playlist = {
"id": int(playlistid),
"id": playlistid,
"image": db_playlist.image,
"last_updated": create_new_date(),
"name": str(data.get("name")).strip(),
"name": str(form.name).strip(),
"settings": settings,
"trackhashes": json.dumps([]),
}
@@ -247,7 +272,7 @@ def update_playlist_info(playlistid: str):
p_tuple = (*playlist.values(),)
PL.update_playlist(int(playlistid), playlist)
PL.update_playlist(playlistid, playlist)
playlist = models.Playlist(*p_tuple)
playlist.last_updated = date_string_to_time_passed(playlist.last_updated)
@@ -257,12 +282,12 @@ def update_playlist_info(playlistid: str):
}
@api.route("/playlist/<playlistid>/pin_unpin", methods=["GET"])
def pin_unpin_playlist(playlistid: str):
@api.post("/<playlistid>/pin_unpin")
def pin_unpin_playlist(path: PlaylistIDPath):
"""
Pins or unpins a playlist.
Pin playlist.
"""
playlist = PL.get_playlist_by_id(int(playlistid))
playlist = PL.get_playlist_by_id(path.playlistid)
if playlist is None:
return {"error": "Playlist not found"}, 404
@@ -274,23 +299,22 @@ def pin_unpin_playlist(playlistid: str):
except KeyError:
settings["pinned"] = True
PL.update_settings(int(playlistid), settings)
PL.update_settings(path.playlistid, settings)
return {"msg": "Done"}, 200
@api.route("/playlist/<playlistid>/remove-img", methods=["GET"])
def remove_playlist_image(playlistid: str):
@api.delete("/<playlistid>/remove-img")
def remove_playlist_image(path: PlaylistIDPath):
"""
Removes the playlist image.
Clear playlist image.
"""
pid = int(playlistid)
playlist = PL.get_playlist_by_id(pid)
playlist = PL.get_playlist_by_id(path.playlistid)
if playlist is None:
return {"error": "Playlist not found"}, 404
PL.remove_banner(pid)
PL.remove_banner(path.playlistid)
playlist.image = None
playlist.thumb = None
@@ -303,78 +327,62 @@ def remove_playlist_image(playlistid: str):
return {"playlist": playlist}, 200
@api.route("/playlist/delete", methods=["POST"])
def remove_playlist():
@api.delete("/<playlistid>/delete", methods=["DELETE"])
def remove_playlist(path: PlaylistIDPath):
"""
Deletes a playlist by ID.
Delete playlist
"""
message = {"error": "Playlist ID not provided"}
data = request.get_json()
if data is None:
return message, 400
try:
pid = data["pid"]
except KeyError:
return message, 400
PL.delete_playlist(pid)
PL.delete_playlist(path.playlistid)
return {"msg": "Done"}, 200
@api.route("/playlist/<pid>/remove-tracks", methods=["POST"])
def remove_tracks_from_playlist(pid: int):
data = request.get_json()
class RemoveTracksFromPlaylistBody(BaseModel):
tracks: list[dict] = Field(..., description="A list of trackhashes to remove")
if data is None:
return {"error": "Track index not provided"}, 400
@api.post("/<playlistid>/remove-tracks")
def remove_tracks_from_playlist(
path: PlaylistIDPath, body: RemoveTracksFromPlaylistBody
):
"""
Remove track from playlist
"""
# A track looks like this:
# {
# trackhash: str;
# index: int;
# }
tracks = data["tracks"]
PL.remove_tracks_from_playlist(pid, tracks)
PL.remove_tracks_from_playlist(path.playlistid, body.tracks)
return {"msg": "Done"}, 200
def playlist_exists(name: str) -> bool:
def playlist_name_exists(name: str) -> bool:
return PL.count_playlist_by_name(name) > 0
@api.route("/playlist/save-item", methods=["POST"])
def save_item_as_playlist():
data = request.get_json()
msg = {"error": "'itemtype', 'playlist_name' and 'itemhash' not provided"}, 400
class SavePlaylistAsItemBody(BaseModel):
itemtype: str = Field(..., description="The type of item", example="tracks")
playlist_name: str = Field(..., description="The name of the playlist")
itemhash: str = Field(..., description="The hash of the item to save")
if data is None:
return msg
try:
playlist_name = data["playlist_name"]
except KeyError:
playlist_name = None
@api.post("/save-item")
def save_item_as_playlist(body: SavePlaylistAsItemBody):
"""
Save as playlist
if playlist_exists(playlist_name):
Saves a track, album, artist or folder as a playlist
"""
itemtype = body.itemtype
playlist_name = body.playlist_name
itemhash = body.itemhash
if playlist_name_exists(playlist_name):
return {"error": "Playlist already exists"}, 409
try:
itemtype = data["itemtype"]
except KeyError:
itemtype = None
try:
itemhash: str = data["itemhash"]
except KeyError:
itemhash = None
if itemtype is None or playlist_name is None or itemhash is None:
return msg
if itemtype == "tracks":
trackhashes = itemhash.split(",")
elif itemtype == "folder":
+9 -5
View File
@@ -3,8 +3,8 @@ Contains all the search routes.
"""
from flask import request
from pydantic import BaseModel, Field
from unidecode import unidecode
from pydantic import BaseModel, Field
from flask_openapi3 import Tag
from flask_openapi3 import APIBlueprint
@@ -75,7 +75,7 @@ class SearchQuery(BaseModel):
@api.get("/tracks")
def search_tracks(query: SearchQuery):
"""
Searches for tracks
Search tracks
"""
query = query.q
@@ -92,7 +92,7 @@ def search_tracks(query: SearchQuery):
@api.get("/albums")
def search_albums(query: SearchQuery):
"""
Searches for albums.
Search albums.
"""
query = query.q
@@ -109,7 +109,7 @@ def search_albums(query: SearchQuery):
@api.get("/artists")
def search_artists(query: SearchQuery):
"""
Searches for artists.
Search artists.
"""
query = query.q
@@ -134,7 +134,9 @@ class TopResultsQuery(SearchQuery):
@api.get("/top")
def get_top_results(query: TopResultsQuery):
"""
Returns the top results for the search query.
Get top results
Returns the top results for the given query.
"""
query = query.q
@@ -156,6 +158,8 @@ class SearchLoadMoreQuery(SearchQuery):
@api.get("/loadmore")
def search_load_more(query: SearchLoadMoreQuery):
"""
Load more
Returns more songs, albums or artists from a search query.
NOTE: You must first initiate a search using the `/search` endpoint.
+3 -5
View File
@@ -12,7 +12,7 @@ from app.serializers.artist import serialize_for_card
from itertools import groupby
from app.utils.dates import timestamp_from_days_ago, timestamp_to_time_passed
from app.utils.dates import timestamp_to_time_passed
older_albums = set()
older_artists = set()
@@ -216,8 +216,6 @@ def get_recent_items(limit: int = 7):
return recent_items
def get_recent_tracks(cutoff_days: int):
def get_recent_tracks(limit: int):
tracks = sorted(TrackStore.tracks, key=lambda t: t.created_date, reverse=True)
timestamp = timestamp_from_days_ago(cutoff_days)
return [t for t in tracks if t.created_date > timestamp]
return tracks[:limit]
+7 -4
View File
@@ -1,6 +1,7 @@
"""
This library contains all the functions related to playlists.
"""
import os
import random
import string
@@ -62,7 +63,7 @@ def create_gif_thumbnail(image: Any, img_path: str):
def save_p_image(
img: Image, pid: str, content_type: str = None, filename: str = None
img: Image, pid: int, content_type: str = None, filename: str = None
) -> str:
"""
Saves a playlist banner image and returns the filepath.
@@ -72,7 +73,7 @@ def save_p_image(
random_str = "".join(random.choices(string.ascii_letters + string.digits, k=5))
if not filename:
filename = pid + str(random_str) + ".webp"
filename = str(pid) + str(random_str) + ".webp"
full_img_path = os.path.join(settings.Paths.get_playlist_img_path(), filename)
@@ -134,7 +135,7 @@ def get_first_4_images(
return duplicate_images(images)
def get_recently_added_playlist(cutoff: int = 14):
def get_recently_added_playlist(limit: int = 100):
playlist = Playlist(
id="recentlyadded",
name="Recently Added",
@@ -144,8 +145,10 @@ def get_recently_added_playlist(cutoff: int = 14):
trackhashes=[],
)
tracks = get_recent_tracks(cutoff)
tracks = get_recent_tracks(limit=limit)
try:
# Create date to show as last updated
date = datetime.fromtimestamp(tracks[0].created_date)
except IndexError:
return playlist, []