mirror of
https://github.com/Dvorinka/swingmusic-extended.git
synced 2026-06-05 04:53:01 +00:00
first draft
This commit is contained in:
@@ -21,7 +21,7 @@
|
|||||||
<!-- CHECKPOINT -->
|
<!-- CHECKPOINT -->
|
||||||
<!-- ALBUM PAGE! -->
|
<!-- ALBUM PAGE! -->
|
||||||
|
|
||||||
# DONE
|
# DONE
|
||||||
|
|
||||||
- Support auth headers
|
- Support auth headers
|
||||||
- Add recently played playlist
|
- Add recently played playlist
|
||||||
|
|||||||
+3
-2
@@ -22,6 +22,7 @@ from app.api import (
|
|||||||
favorites,
|
favorites,
|
||||||
folder,
|
folder,
|
||||||
imgserver,
|
imgserver,
|
||||||
|
pages,
|
||||||
playlist,
|
playlist,
|
||||||
search,
|
search,
|
||||||
settings,
|
settings,
|
||||||
@@ -32,7 +33,7 @@ from app.api import (
|
|||||||
getall,
|
getall,
|
||||||
auth,
|
auth,
|
||||||
stream,
|
stream,
|
||||||
backup_and_restore
|
backup_and_restore,
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO: Move this description to a separate file
|
# TODO: Move this description to a separate file
|
||||||
@@ -110,7 +111,7 @@ def create_api():
|
|||||||
app.register_api(colors.api)
|
app.register_api(colors.api)
|
||||||
app.register_api(lyrics.api)
|
app.register_api(lyrics.api)
|
||||||
app.register_api(backup_and_restore.api)
|
app.register_api(backup_and_restore.api)
|
||||||
|
app.register_api(pages.api)
|
||||||
# Plugins
|
# Plugins
|
||||||
app.register_api(plugins.api)
|
app.register_api(plugins.api)
|
||||||
app.register_api(lyrics_plugin.api)
|
app.register_api(lyrics_plugin.api)
|
||||||
|
|||||||
@@ -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("/<int:page_id>/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("/<int:page_id>")
|
||||||
|
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("/<int:page_id>")
|
||||||
|
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("/<int:page_id>")
|
||||||
|
def delete_page(path: DeletePagePath):
|
||||||
|
"""
|
||||||
|
Delete a page.
|
||||||
|
"""
|
||||||
|
PageTable.delete_by_id(path.page_id)
|
||||||
|
return {"message": "Page deleted"}
|
||||||
@@ -589,3 +589,47 @@ class MixTable(Base):
|
|||||||
cls.update_one(mix.id, mix)
|
cls.update_one(mix.id, mix)
|
||||||
|
|
||||||
return mix.extra["trackmix_saved"]
|
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
|
||||||
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -68,3 +68,4 @@ class Mix:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def mixes_to_dataclasses(cls, entries: Any):
|
def mixes_to_dataclasses(cls, entries: Any):
|
||||||
return [cls.mix_to_dataclass(entry) for entry in entries]
|
return [cls.mix_to_dataclass(entry) for entry in entries]
|
||||||
|
|
||||||
|
|||||||
+22
-1
@@ -1,5 +1,7 @@
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from app.db.userdata import PageTable
|
||||||
|
from app.lib.pagelib import recover_page_items
|
||||||
from app.store.homepageentries import (
|
from app.store.homepageentries import (
|
||||||
BecauseYouListenedToArtistHomepageEntry,
|
BecauseYouListenedToArtistHomepageEntry,
|
||||||
GenericRecoverableEntry,
|
GenericRecoverableEntry,
|
||||||
@@ -63,12 +65,31 @@ class HomepageStore:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def get_homepage_items(cls, limit: int):
|
def get_homepage_items(cls, limit: int):
|
||||||
# return a dict of entry name to entry items
|
# 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)}
|
{entry: cls.entries[entry].get_items(get_current_userid(), limit)}
|
||||||
for entry in cls.entries.keys()
|
for entry in cls.entries.keys()
|
||||||
if len(cls.entries[entry].items)
|
if len(cls.entries[entry].items)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
recently_added = homedata.pop()
|
||||||
|
return homedata + pagedata + [recently_added]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def find_mix(cls, mixid: str):
|
def find_mix(cls, mixid: str):
|
||||||
mixentries = ["artist_mixes", "custom_mixes"]
|
mixentries = ["artist_mixes", "custom_mixes"]
|
||||||
|
|||||||
Reference in New Issue
Block a user