From 0c3a55b9cec23ca171b40f408446f9592e168e21 Mon Sep 17 00:00:00 2001 From: cwilvx Date: Tue, 28 Jan 2025 10:45:03 +0300 Subject: [PATCH] first draft --- TODO.md | 2 +- app/api/__init__.py | 5 +- app/api/pages.py | 144 ++++++++++++++++++++++++++++++++++++++++++ app/db/userdata.py | 44 +++++++++++++ app/lib/pagelib.py | 54 ++++++++++++++++ app/models/mix.py | 1 + app/store/homepage.py | 23 ++++++- 7 files changed, 269 insertions(+), 4 deletions(-) create mode 100644 app/api/pages.py create mode 100644 app/lib/pagelib.py diff --git a/TODO.md b/TODO.md index b668a9db..cd34050f 100644 --- a/TODO.md +++ b/TODO.md @@ -21,7 +21,7 @@ - # DONE +# DONE - Support auth headers - Add recently played playlist diff --git a/app/api/__init__.py b/app/api/__init__.py index 671e9b15..184be334 100644 --- a/app/api/__init__.py +++ b/app/api/__init__.py @@ -22,6 +22,7 @@ from app.api import ( favorites, folder, imgserver, + pages, playlist, search, settings, @@ -32,7 +33,7 @@ from app.api import ( getall, auth, stream, - backup_and_restore + backup_and_restore, ) # TODO: Move this description to a separate file @@ -110,7 +111,7 @@ def create_api(): app.register_api(colors.api) app.register_api(lyrics.api) app.register_api(backup_and_restore.api) - + app.register_api(pages.api) # Plugins app.register_api(plugins.api) app.register_api(lyrics_plugin.api) diff --git a/app/api/pages.py b/app/api/pages.py new file mode 100644 index 00000000..2bc3e3cc --- /dev/null +++ b/app/api/pages.py @@ -0,0 +1,144 @@ +""" +Contains all the page routes. +""" + +from typing import Any + +from flask_openapi3 import Tag +from flask_openapi3 import APIBlueprint +from pydantic import BaseModel, Field + +from app.db.userdata import PageTable +from app.lib.pagelib import recover_page_items, validate_page_items +from app.utils.auth import get_current_userid + +bp_tag = Tag(name="Pages", description="Pages") +api = APIBlueprint("pages", __name__, url_prefix="/pages", abp_tags=[bp_tag]) + + +class CreatePageBody(BaseModel): + name: str = Field(description="The name of the page", example="My Page") + description: str = Field( + description="The description of the page", example="My Page" + ) + items: list[dict[str, Any]] = Field( + description="The items to add to the page", + example=[{"type": "album", "hash": "1234567890"}], + ) + + +@api.post("") +def create_page(body: CreatePageBody): + """ + Create a new page. + """ + items = validate_page_items(body.items) + + if len(items) == 0: + return {"error": "No items to add"}, 400 + + payload = { + "name": body.name, + "items": items, + "userid": get_current_userid(), + "extra": { + "description": body.description, + }, + } + + print(payload) + PageTable.insert_one(payload) + + return {"message": "Page created"}, 201 + + +class AddPageItemsBody(BaseModel): + items: list[dict[str, Any]] = Field( + description="The items to add to the page", + example=[{"type": "album", "hash": "1234567890"}], + ) + + +class AddPageItemsPath(BaseModel): + page_id: int = Field(description="The ID of the page to add items to", example=1) + + +@api.post("//items") +def add_page_items(path: AddPageItemsPath, body: AddPageItemsBody): + """ + Add items to a page. + """ + new_items = validate_page_items(body.items) + + page = PageTable.get_by_id(path.page_id) + + if page is None: + return {"error": "Page not found"}, 404 + + page["items"].extend(new_items) + PageTable.update_items(page["id"], page["items"]) + + return {"message": "Items added to page"} + + +@api.get("") +def get_pages(): + """ + Get all pages. + """ + return PageTable.get_all() + + +class GetPageBody(BaseModel): + page_id: int = Field(description="The ID of the page to get", example=1) + + +@api.get("/") +def get_page(path: GetPageBody): + """ + Get a page. + """ + page = PageTable.get_by_id(path.page_id) + if not page: + return {"error": "Page not found"}, 404 + + items = recover_page_items(page["items"]) + return { + "id": page["id"], + "name": page["name"], + "items": items, + "extra": page["extra"], + } + + +class UpdatePageBody(BaseModel): + name: str = Field(description="The name of the page") + description: str = Field(description="The description of the page", default="") + + +@api.put("/") +def update_page(path: GetPageBody, body: UpdatePageBody): + """ + Update a page. + """ + payload = { + "id": path.page_id, + "name": body.name, + "extra": {"description": body.description}, + } + + PageTable.update_one(payload) + return {"page": payload} + + +class DeletePagePath(BaseModel): + page_id: int = Field(description="The ID of the page to delete") + + +@api.delete("/") +def delete_page(path: DeletePagePath): + """ + Delete a page. + """ + PageTable.delete_by_id(path.page_id) + return {"message": "Page deleted"} diff --git a/app/db/userdata.py b/app/db/userdata.py index a5c66887..8db27e8f 100644 --- a/app/db/userdata.py +++ b/app/db/userdata.py @@ -589,3 +589,47 @@ class MixTable(Base): cls.update_one(mix.id, mix) return mix.extra["trackmix_saved"] + + +class PageTable(Base): + __tablename__ = "page" + + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(String(), index=True) + userid: Mapped[int] = mapped_column( + Integer(), ForeignKey("user.id", ondelete="cascade"), index=True + ) + items: Mapped[list[dict[str, Any]]] = mapped_column(JSON(), default_factory=list) + extra: Mapped[dict[str, Any]] = mapped_column( + JSON(), nullable=True, default_factory=dict + ) + + @classmethod + def to_dict(cls, entry: Any) -> dict[str, Any]: + return entry._asdict() + + @classmethod + def get_all(cls): + result = cls.execute(select(cls)) + return [cls.to_dict(entry) for entry in result.fetchall()] + + @classmethod + def get_by_id(cls, id: int): + result = cls.execute(select(cls).where(cls.id == id)) + return cls.to_dict(result.fetchone()) + + @classmethod + def delete_by_id(cls, id: int): + return cls.execute(delete(cls).where(cls.id == id), commit=True) + + @classmethod + def update_items(cls, id: int, items: list[dict[str, Any]]): + return cls.execute( + update(cls).where(cls.id == id).values(items=items), commit=True + ) + + @classmethod + def update_one(cls, payload: dict[str, Any]): + return cls.execute( + update(cls).where(cls.id == payload["id"]).values(payload), commit=True + ) diff --git a/app/lib/pagelib.py b/app/lib/pagelib.py new file mode 100644 index 00000000..8d57eee0 --- /dev/null +++ b/app/lib/pagelib.py @@ -0,0 +1,54 @@ +from typing import Any +from app.serializers.album import serialize_for_card +from app.serializers.artist import serialize_for_card as serialize_artist +from app.store.albums import AlbumStore +from app.store.artists import ArtistStore + + +def validate_page_items(items: list[dict[str, str]]): + """ + Validate the items in a page before adding them to the database. + """ + validated: list[dict[str, str]] = [] + + for item in items: + if item["type"] == "album": + album = AlbumStore.albummap.get(item["hash"]) + + if album is not None: + validated.append(item) + elif item["type"] == "artist": + artist = ArtistStore.artistmap.get(item["hash"]) + + if artist is not None: + validated.append(item) + else: + raise ValueError(f"Invalid item type: {item['type']}") + + return validated + + +def recover_page_items(items: list[dict[str, str]]): + """ + Recover the items in a page. + """ + recovered: list[dict[str, Any]] = [] + + for item in items: + if item["type"] == "album": + album = AlbumStore.albummap.get(item["hash"]) + + if album is not None: + recovered.append( + {"item": serialize_for_card(album.album), "type": "album"} + ) + elif item["type"] == "artist": + artist = ArtistStore.artistmap.get(item["hash"]) + + if artist is not None: + recovered.append( + {"item": serialize_artist(artist.artist), "type": "artist"} + ) + + recovered.reverse() + return recovered diff --git a/app/models/mix.py b/app/models/mix.py index 097788df..ab0a700b 100644 --- a/app/models/mix.py +++ b/app/models/mix.py @@ -68,3 +68,4 @@ class Mix: @classmethod def mixes_to_dataclasses(cls, entries: Any): return [cls.mix_to_dataclass(entry) for entry in entries] + diff --git a/app/store/homepage.py b/app/store/homepage.py index 07a8cf80..c6605114 100644 --- a/app/store/homepage.py +++ b/app/store/homepage.py @@ -1,5 +1,7 @@ from typing import Any +from app.db.userdata import PageTable +from app.lib.pagelib import recover_page_items from app.store.homepageentries import ( BecauseYouListenedToArtistHomepageEntry, GenericRecoverableEntry, @@ -63,12 +65,31 @@ class HomepageStore: @classmethod def get_homepage_items(cls, limit: int): # return a dict of entry name to entry items - return [ + pages = PageTable.get_all() + pagedata = [] + + for page in pages: + pagedata.append( + { + page["id"]: { + "id": page["id"], + "title": page["name"], + "description": page["extra"]["description"], + "items": recover_page_items(page["items"]), + "url": f"pages/{page['id']}", + } + } + ) + + homedata = [ {entry: cls.entries[entry].get_items(get_current_userid(), limit)} for entry in cls.entries.keys() if len(cls.entries[entry].items) ] + recently_added = homedata.pop() + return homedata + pagedata + [recently_added] + @classmethod def find_mix(cls, mixid: str): mixentries = ["artist_mixes", "custom_mixes"]