start porting: playlists endpoints

This commit is contained in:
cwilvx
2024-07-01 09:22:52 +03:00
parent a3c4558d52
commit ff7343a7be
5 changed files with 196 additions and 62 deletions
+57 -49
View File
@@ -12,13 +12,14 @@ from flask_openapi3 import Tag
from flask_openapi3 import APIBlueprint, FileStorage from flask_openapi3 import APIBlueprint, FileStorage
from app import models from app import models
from app.db.sqlite.playlists import SQLitePlaylistMethods from app.db.libdata import TrackTable
from app.db.userdata import PlaylistTable
from app.lib import playlistlib from app.lib import playlistlib
from app.lib.albumslib import sort_by_track_no from app.lib.albumslib import sort_by_track_no
from app.lib.home.recentlyadded import get_recently_added_playlist from app.lib.home.recentlyadded import get_recently_added_playlist
from app.lib.home.recentlyplayed import get_recently_played_playlist from app.lib.home.recentlyplayed import get_recently_played_playlist
from app.models.playlist import Playlist
from app.serializers.playlist import serialize_for_card from app.serializers.playlist import serialize_for_card
from app.store.tracks import TrackStore
from app.utils.dates import create_new_date, date_string_to_time_passed from app.utils.dates import create_new_date, date_string_to_time_passed
from app.utils.remove_duplicates import remove_duplicates from app.utils.remove_duplicates import remove_duplicates
from app.settings import Paths from app.settings import Paths
@@ -26,8 +27,6 @@ from app.settings import Paths
tag = Tag(name="Playlists", description="Get and manage playlists") tag = Tag(name="Playlists", description="Get and manage playlists")
api = APIBlueprint("playlists", __name__, url_prefix="/playlists", abp_tags=[tag]) api = APIBlueprint("playlists", __name__, url_prefix="/playlists", abp_tags=[tag])
PL = SQLitePlaylistMethods
class SendAllPlaylistsQuery(BaseModel): class SendAllPlaylistsQuery(BaseModel):
no_images: bool = Field(False, description="Whether to include images") no_images: bool = Field(False, description="Whether to include images")
@@ -38,7 +37,7 @@ def send_all_playlists(query: SendAllPlaylistsQuery):
""" """
Gets all the playlists. Gets all the playlists.
""" """
playlists = PL.get_all_playlists() playlists = PlaylistTable.get_all()
playlists = list(playlists) playlists = list(playlists)
for playlist in playlists: for playlist in playlists:
@@ -63,18 +62,21 @@ def insert_playlist(name: str, image: str = None):
"image": image, "image": image,
"last_updated": create_new_date(), "last_updated": create_new_date(),
"name": name, "name": name,
"trackhashes": json.dumps([]), "trackhashes": [],
"settings": json.dumps( "settings": {
{ "has_gif": False,
"has_gif": False, "banner_pos": 50,
"banner_pos": 50, "square_img": True if image else False,
"square_img": True if image else False, "pinned": False,
"pinned": False, },
}
),
} }
return PL.insert_one_playlist(playlist) rowid = PlaylistTable.add_one(playlist)
if rowid:
playlist["id"] = rowid
return Playlist(**playlist)
return None
class CreatePlaylistBody(BaseModel): class CreatePlaylistBody(BaseModel):
@@ -88,9 +90,10 @@ def create_playlist(body: CreatePlaylistBody):
Creates a new playlist. Accepts POST method with a JSON body. Creates a new playlist. Accepts POST method with a JSON body.
""" """
existing_playlist_count = PL.count_playlist_by_name(body.name) # existing_playlist_count = PL.count_playlist_by_name(body.name)
exists = PlaylistTable.check_exists_by_name(body.name)
if existing_playlist_count > 0: if exists:
return {"error": "Playlist already exists"}, 409 return {"error": "Playlist already exists"}, 409
playlist = insert_playlist(body.name) playlist = insert_playlist(body.name)
@@ -105,8 +108,7 @@ def get_path_trackhashes(path: str):
""" """
Returns a list of trackhashes in a folder. Returns a list of trackhashes in a folder.
""" """
tracks = TrackStore.get_tracks_in_path(path) tracks = TrackTable.get_tracks_in_path(path)
tracks = sorted(tracks, key=lambda t: t.last_mod)
return [t.trackhash for t in tracks] return [t.trackhash for t in tracks]
@@ -114,17 +116,17 @@ def get_album_trackhashes(albumhash: str):
""" """
Returns a list of trackhashes in an album. Returns a list of trackhashes in an album.
""" """
tracks = TrackStore.get_tracks_by_albumhash(albumhash) tracks = TrackTable.get_tracks_by_albumhash(albumhash)
tracks = sort_by_track_no(tracks) tracks = sort_by_track_no(tracks)
return [t["trackhash"] for t in tracks] return [t.trackhash for t in tracks]
def get_artist_trackhashes(artisthash: str): def get_artist_trackhashes(artisthash: str):
""" """
Returns a list of trackhashes for an artist. Returns a list of trackhashes for an artist.
""" """
tracks = TrackStore.get_tracks_by_artisthash(artisthash) tracks = TrackTable.get_tracks_by_artisthash(artisthash)
return [t.trackhash for t in tracks] return [t.trackhash for t in tracks]
@@ -164,10 +166,11 @@ def add_item_to_playlist(path: PlaylistIDPath, body: AddItemToPlaylistBody):
else: else:
trackhashes = [] trackhashes = []
insert_count = PL.add_tracks_to_playlist(int(playlist_id), trackhashes) # insert_count = PL.add_tracks_to_playlist(int(playlist_id), trackhashes)
PlaylistTable.append_to_playlist(int(playlist_id), trackhashes)
if insert_count == 0: # if insert_count == 0:
return {"error": "Item already exists in playlist"}, 409 # return {"error": "Item already exists in playlist"}, 409
return {"msg": "Done"}, 200 return {"msg": "Done"}, 200
@@ -179,11 +182,10 @@ class GetPlaylistQuery(BaseModel):
def format_custom_playlist(playlist: models.Playlist, tracks: list[models.Track]): def format_custom_playlist(playlist: models.Playlist, tracks: list[models.Track]):
duration = sum(t.duration for t in tracks) duration = sum(t.duration for t in tracks)
playlist.set_duration(duration) playlist.duration = duration
playlist = serialize_for_card(playlist)
return { return {
"info": playlist, "info": serialize_for_card(playlist),
"tracks": tracks, "tracks": tracks,
} }
@@ -209,19 +211,20 @@ def get_playlist(path: PlaylistIDPath, query: GetPlaylistQuery):
playlist, tracks = handler() playlist, tracks = handler()
return format_custom_playlist(playlist, tracks) return format_custom_playlist(playlist, tracks)
playlist = PL.get_playlist_by_id(int(playlistid)) # playlist = PL.get_playlist_by_id(int(playlistid))
playlist = PlaylistTable.get_by_id(playlistid)
if playlist is None: if playlist is None:
return {"msg": "Playlist not found"}, 404 return {"msg": "Playlist not found"}, 404
tracks = TrackStore.get_tracks_by_trackhashes(list(playlist.trackhashes)) # tracks = TrackStore.get_tracks_by_trackhashes(list(playlist.trackhashes))
tracks = TrackTable.get_tracks_by_trackhashes(playlist.trackhashes)
tracks = remove_duplicates(tracks) tracks = remove_duplicates(tracks)
duration = sum(t.duration for t in tracks) duration = sum(t.duration for t in tracks)
playlist.last_updated = date_string_to_time_passed(playlist.last_updated) playlist.last_updated = date_string_to_time_passed(playlist.last_updated)
playlist.set_duration(duration) playlist.duration = duration
playlist.set_count(len(tracks))
if not playlist.has_image: if not playlist.has_image:
playlist.images = playlistlib.get_first_4_images(tracks) playlist.images = playlistlib.get_first_4_images(tracks)
@@ -247,7 +250,8 @@ def update_playlist_info(path: PlaylistIDPath, form: UpdatePlaylistForm):
Update playlist Update playlist
""" """
playlistid = path.playlistid playlistid = path.playlistid
db_playlist = PL.get_playlist_by_id(playlistid) # db_playlist = PL.get_playlist_by_id(playlistid)
db_playlist = PlaylistTable.get_by_id(playlistid)
if db_playlist is None: if db_playlist is None:
return {"error": "Playlist not found"}, 404 return {"error": "Playlist not found"}, 404
@@ -286,7 +290,8 @@ def update_playlist_info(path: PlaylistIDPath, form: UpdatePlaylistForm):
p_tuple = (*playlist.values(),) p_tuple = (*playlist.values(),)
PL.update_playlist(playlistid, playlist) # PL.update_playlist(playlistid, playlist)
PlaylistTable.update_one(playlistid, playlist)
playlist = models.Playlist(*p_tuple) playlist = models.Playlist(*p_tuple)
playlist.last_updated = date_string_to_time_passed(playlist.last_updated) playlist.last_updated = date_string_to_time_passed(playlist.last_updated)
@@ -301,7 +306,9 @@ def pin_unpin_playlist(path: PlaylistIDPath):
""" """
Pin playlist. Pin playlist.
""" """
playlist = PL.get_playlist_by_id(path.playlistid) # playlist = PL.get_playlist_by_id(path.playlistid)
playlist = PlaylistTable.get_by_id(path.playlistid)
if playlist is None: if playlist is None:
return {"error": "Playlist not found"}, 404 return {"error": "Playlist not found"}, 404
@@ -313,8 +320,8 @@ def pin_unpin_playlist(path: PlaylistIDPath):
except KeyError: except KeyError:
settings["pinned"] = True settings["pinned"] = True
PL.update_settings(path.playlistid, settings) # PL.update_settings(path.playlistid, settings)
PlaylistTable.update_settings(path.playlistid, settings)
return {"msg": "Done"}, 200 return {"msg": "Done"}, 200
@@ -323,12 +330,14 @@ def remove_playlist_image(path: PlaylistIDPath):
""" """
Clear playlist image. Clear playlist image.
""" """
playlist = PL.get_playlist_by_id(path.playlistid) # playlist = PL.get_playlist_by_id(path.playlistid)
playlist = PlaylistTable.get_by_id(path.playlistid)
if playlist is None: if playlist is None:
return {"error": "Playlist not found"}, 404 return {"error": "Playlist not found"}, 404
PL.remove_banner(path.playlistid) # PL.remove_banner(path.playlistid)
PlaylistTable.remove_image(path.playlistid)
playlist.image = None playlist.image = None
playlist.thumb = None playlist.thumb = None
@@ -346,7 +355,8 @@ def remove_playlist(path: PlaylistIDPath):
""" """
Delete playlist Delete playlist
""" """
PL.delete_playlist(path.playlistid) # PL.delete_playlist(path.playlistid)
PlaylistTable.remove_one(path.playlistid)
return {"msg": "Done"}, 200 return {"msg": "Done"}, 200
@@ -368,15 +378,12 @@ def remove_tracks_from_playlist(
# index: int; # index: int;
# } # }
PL.remove_tracks_from_playlist(path.playlistid, body.tracks) # PL.remove_tracks_from_playlist(path.playlistid, body.tracks)
PlaylistTable.remove_from_playlist(path.playlistid, body.tracks)
return {"msg": "Done"}, 200 return {"msg": "Done"}, 200
def playlist_name_exists(name: str) -> bool:
return PL.count_playlist_by_name(name) > 0
class SavePlaylistAsItemBody(BaseModel): class SavePlaylistAsItemBody(BaseModel):
itemtype: str = Field(..., description="The type of item", example="tracks") itemtype: str = Field(..., description="The type of item", example="tracks")
playlist_name: str = Field(..., description="The name of the playlist") playlist_name: str = Field(..., description="The name of the playlist")
@@ -394,7 +401,7 @@ def save_item_as_playlist(body: SavePlaylistAsItemBody):
playlist_name = body.playlist_name playlist_name = body.playlist_name
itemhash = body.itemhash itemhash = body.itemhash
if playlist_name_exists(playlist_name): if PlaylistTable.check_exists_by_name(playlist_name):
return {"error": "Playlist already exists"}, 409 return {"error": "Playlist already exists"}, 409
if itemtype == "tracks": if itemtype == "tracks":
@@ -437,8 +444,9 @@ def save_item_as_playlist(body: SavePlaylistAsItemBody):
img, str(playlist.id), "image/webp", filename=filename img, str(playlist.id), "image/webp", filename=filename
) )
PL.add_tracks_to_playlist(playlist.id, trackhashes) # PL.add_tracks_to_playlist(playlist.id, trackhashes)
playlist.set_count(len(trackhashes)) PlaylistTable.append_to_playlist(playlist.id, trackhashes)
playlist.count = len(trackhashes)
images = playlistlib.get_first_4_images(trackhashes=trackhashes) images = playlistlib.get_first_4_images(trackhashes=trackhashes)
playlist.images = [img["image"] for img in images] playlist.images = [img["image"] for img in images]
+4
View File
@@ -78,6 +78,10 @@ class Base(MappedAsDataclass, DeclarativeBase):
with DbManager(commit=True) as conn: with DbManager(commit=True) as conn:
conn.execute(delete(cls)) conn.execute(delete(cls))
@classmethod
def remove_one(cls, id: int):
cls.execute(delete(cls).where(cls.id == id), commit=True)
@classmethod @classmethod
def all(cls): def all(cls):
return cls.execute(select(cls)) return cls.execute(select(cls))
+108 -1
View File
@@ -22,6 +22,8 @@ from app.db.utils import (
albums_to_dataclasses, albums_to_dataclasses,
artists_to_dataclasses, artists_to_dataclasses,
favorites_to_dataclass, favorites_to_dataclass,
playlist_to_dataclass,
playlists_to_dataclasses,
plugin_to_dataclasses, plugin_to_dataclasses,
similar_artist_to_dataclass, similar_artist_to_dataclass,
similar_artists_to_dataclass, similar_artists_to_dataclass,
@@ -168,7 +170,6 @@ class FavoritesTable(Base):
JSON(), nullable=True, default_factory=dict JSON(), nullable=True, default_factory=dict
) )
@classmethod @classmethod
def get_all(cls): def get_all(cls):
with DbManager() as conn: with DbManager() as conn:
@@ -274,3 +275,109 @@ class ScrobbleTable(Base):
) )
return tracklog_to_dataclasses(result.fetchall()) return tracklog_to_dataclasses(result.fetchall())
class PlaylistTable(Base):
__tablename__ = "playlist"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(), index=True)
last_updated: Mapped[int] = mapped_column(Integer())
image: Mapped[str] = mapped_column(String(), nullable=True)
userid: Mapped[int] = mapped_column(
Integer(), ForeignKey("user.id", ondelete="cascade")
)
settings: Mapped[dict[str, Any]] = mapped_column(JSON())
trackhashes: Mapped[list[str]] = mapped_column(JSON(), default_factory=list)
extra: Mapped[dict[str, Any]] = mapped_column(
JSON(), nullable=True, default_factory=dict
)
@classmethod
def get_all(cls):
result = cls.all()
return playlists_to_dataclasses(result)
@classmethod
def add_one(cls, playlist: dict[str, Any]):
playlist["userid"] = get_current_userid()
result = cls.insert_one(playlist)
return result.lastrowid
@classmethod
def check_exists_by_name(cls, name: str):
result = cls.execute(
select(cls).where((cls.name == name) & (cls.userid == get_current_userid()))
)
return result.fetchone() is not None
@classmethod
def append_to_playlist(cls, id: int, trackhashes: list[str]):
print("type(trackhashes):", type(trackhashes))
return cls.execute(
update(cls)
.where((cls.id == id) & (cls.userid == get_current_userid()))
.values(trackhashes=cls.trackhashes + trackhashes),
commit=True,
)
@classmethod
def remove_from_playlist(cls, id: int, trackhashes: list[dict[str, Any]]):
# CHECKPOINT: Properly remove tracks from a playlist
# Without messing up the order in case of duplicates
tracks = cls.execute(
select(cls.trackhashes).where(
(cls.id == id) & (cls.userid == get_current_userid())
)
)
results = tracks.fetchone()
if results:
dbhashes: list[str] = results[0]
for item in trackhashes:
if dbhashes.index(item["trackhash"]) == item["index"]:
dbhashes.remove(item["trackhash"])
return cls.execute(
update(cls)
.where((cls.id == id) & (cls.userid == get_current_userid()))
.values(trackhashes=dbhashes),
commit=True,
)
@classmethod
def get_by_id(cls, id: int):
result = cls.execute(
select(cls).where((cls.id == id) & (cls.userid == get_current_userid()))
)
result = result.fetchone()
if result:
return playlist_to_dataclass(result)
@classmethod
def update_one(cls, id: int, playlist: dict[str, Any]):
return cls.execute(
update(cls)
.where((cls.id == id) & (cls.userid == get_current_userid()))
.values(playlist),
commit=True,
)
@classmethod
def update_settings(cls, id: int, settings: dict[str, Any]):
return cls.execute(
update(cls)
.where((cls.id == id) & (cls.userid == get_current_userid()))
.values(settings=settings),
commit=True,
)
@classmethod
def remove_image(cls, id: int):
return cls.execute(
update(cls)
.where((cls.id == id) & (cls.userid == get_current_userid()))
.values(image=None),
commit=True,
)
+12
View File
@@ -4,6 +4,7 @@ from app.models import Album as AlbumModel, Artist as ArtistModel, Track as Trac
from app.models.favorite import Favorite from app.models.favorite import Favorite
from app.models.lastfm import SimilarArtist from app.models.lastfm import SimilarArtist
from app.models.logger import TrackLog from app.models.logger import TrackLog
from app.models.playlist import Playlist
from app.models.plugins import Plugin from app.models.plugins import Plugin
from app.models.user import User from app.models.user import User
@@ -75,9 +76,20 @@ def plugin_to_dataclass(entry: Any):
def plugin_to_dataclasses(entries: Any): def plugin_to_dataclasses(entries: Any):
return [plugin_to_dataclass(entry) for entry in entries] return [plugin_to_dataclass(entry) for entry in entries]
def tracklog_to_dataclass(entry: Any): def tracklog_to_dataclass(entry: Any):
entry_dict = entry._asdict() entry_dict = entry._asdict()
return TrackLog(**entry_dict) return TrackLog(**entry_dict)
def tracklog_to_dataclasses(entries: Any): def tracklog_to_dataclasses(entries: Any):
return [tracklog_to_dataclass(entry) for entry in entries] return [tracklog_to_dataclass(entry) for entry in entries]
def playlist_to_dataclass(entry: Any):
entry_dict = entry._asdict()
return Playlist(**entry_dict)
def playlists_to_dataclasses(entries: Any):
return [playlist_to_dataclass(entry) for entry in entries]
+14 -11
View File
@@ -2,6 +2,7 @@ import dataclasses
import json import json
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Any
from app import settings from app import settings
@@ -14,10 +15,12 @@ class Playlist:
image: str | None image: str | None
last_updated: str last_updated: str
name: str name: str
settings: str | dict settings: dict
trackhashes: str | list[str] userid: int
trackhashes: list[str]
extra: dict[str, Any] = dataclasses.field(default_factory=dict)
thumb: str | None = "" thumb: str = ""
count: int = 0 count: int = 0
duration: int = 0 duration: int = 0
has_image: bool = False has_image: bool = False
@@ -25,11 +28,11 @@ class Playlist:
pinned: bool = False pinned: bool = False
def __post_init__(self): def __post_init__(self):
self.trackhashes = json.loads(str(self.trackhashes)) # self.trackhashes = json.loads(str(self.trackhashes))
self.count = len(self.trackhashes) # self.count = len(self.trackhashes)
if isinstance(self.settings, str): # if isinstance(self.settings, str):
self.settings = dict(json.loads(self.settings)) # self.settings = dict(json.loads(self.settings))
self.pinned = self.settings.get("pinned", False) self.pinned = self.settings.get("pinned", False)
self.has_image = ( self.has_image = (
@@ -42,11 +45,11 @@ class Playlist:
self.image = "None" self.image = "None"
self.thumb = "None" self.thumb = "None"
def set_duration(self, duration: int): # def set_duration(self, duration: int):
self.duration = duration # self.duration = duration
def set_count(self, count: int): # def set_count(self, count: int):
self.count = count # self.count = count
def clear_lists(self): def clear_lists(self):
""" """