mirror of
https://github.com/Dvorinka/swingmusic-extended.git
synced 2026-06-05 04:53:01 +00:00
fix: removing favorites not being commited to db
+ improve: collision of hashes on the favorite table + rename pages to collections + bump mix sleep time from 6hrs to 12hrs
This commit is contained in:
+2
-2
@@ -18,11 +18,11 @@ from .plugins import mixes as mixes_plugin
|
|||||||
from app.api import (
|
from app.api import (
|
||||||
album,
|
album,
|
||||||
artist,
|
artist,
|
||||||
|
collections,
|
||||||
colors,
|
colors,
|
||||||
favorites,
|
favorites,
|
||||||
folder,
|
folder,
|
||||||
imgserver,
|
imgserver,
|
||||||
pages,
|
|
||||||
playlist,
|
playlist,
|
||||||
search,
|
search,
|
||||||
settings,
|
settings,
|
||||||
@@ -112,7 +112,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)
|
app.register_api(collections.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,182 @@
|
|||||||
|
"""
|
||||||
|
Contains all the collection 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 CollectionTable
|
||||||
|
from app.lib.pagelib import recover_page_items, remove_page_items, validate_page_items
|
||||||
|
from app.utils.auth import get_current_userid
|
||||||
|
|
||||||
|
bp_tag = Tag(name="Collections", description="Collections")
|
||||||
|
api = APIBlueprint(
|
||||||
|
"collections", __name__, url_prefix="/collections", abp_tags=[bp_tag]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CreateCollectionBody(BaseModel):
|
||||||
|
name: str = Field(description="The name of the collection")
|
||||||
|
description: str = Field(description="The description of the collection")
|
||||||
|
items: list[dict[str, Any]] = Field(
|
||||||
|
description="The items to add to the collection",
|
||||||
|
json_schema_extra={"example": [{"type": "album", "hash": "1234567890"}]},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@api.post("")
|
||||||
|
def create_collection(body: CreateCollectionBody):
|
||||||
|
"""
|
||||||
|
Create a new collection.
|
||||||
|
"""
|
||||||
|
items = validate_page_items(body.items, existing=[])
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
CollectionTable.insert_one(payload)
|
||||||
|
|
||||||
|
return {"message": "collection created"}, 201
|
||||||
|
|
||||||
|
|
||||||
|
@api.get("")
|
||||||
|
def get_collections():
|
||||||
|
"""
|
||||||
|
Get all collections.
|
||||||
|
"""
|
||||||
|
return [collection for collection in CollectionTable.get_all()]
|
||||||
|
|
||||||
|
|
||||||
|
class AddCollectionItemBody(BaseModel):
|
||||||
|
item: dict[str, Any] = Field(
|
||||||
|
description="The item to add to the collection",
|
||||||
|
json_schema_extra={"example": {"type": "album", "hash": "1234567890"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AddCollectionItemPath(BaseModel):
|
||||||
|
collection_id: int = Field(
|
||||||
|
description="The ID of the collection to add items to",
|
||||||
|
json_schema_extra={"example": 1},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@api.post("/<int:collection_id>/items")
|
||||||
|
def add_collection_item(path: AddCollectionItemPath, body: AddCollectionItemBody):
|
||||||
|
"""
|
||||||
|
Add an item to a collection.
|
||||||
|
"""
|
||||||
|
collection = CollectionTable.get_by_id(path.collection_id)
|
||||||
|
|
||||||
|
if collection is None:
|
||||||
|
return {"error": "Collection not found"}, 404
|
||||||
|
|
||||||
|
new_items = validate_page_items([body.item], existing=collection["items"])
|
||||||
|
|
||||||
|
if len(new_items) == 0:
|
||||||
|
return {"error": "items already in collection"}, 400
|
||||||
|
|
||||||
|
collection["items"].extend(new_items)
|
||||||
|
CollectionTable.update_items(collection["id"], collection["items"])
|
||||||
|
|
||||||
|
return {"message": "Items added to collection"}
|
||||||
|
|
||||||
|
|
||||||
|
class RemoveCollectionItemBody(BaseModel):
|
||||||
|
item: dict[str, Any] = Field(
|
||||||
|
description="The item to remove from the collection",
|
||||||
|
json_schema_extra={"example": {"type": "album", "hash": "1234567890"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RemoveCollectionItemPath(BaseModel):
|
||||||
|
collection_id: int = Field(
|
||||||
|
description="The ID of the collection to remove items from"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@api.delete("/<int:collection_id>/items")
|
||||||
|
def remove_collection_item(
|
||||||
|
path: RemoveCollectionItemPath, body: RemoveCollectionItemBody
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Remove an item from a collection.
|
||||||
|
"""
|
||||||
|
collection = CollectionTable.get_by_id(path.collection_id)
|
||||||
|
|
||||||
|
if collection is None:
|
||||||
|
return {"error": "Collection not found"}, 404
|
||||||
|
|
||||||
|
remaining = remove_page_items(collection["items"], body.item)
|
||||||
|
CollectionTable.update_items(collection["id"], remaining)
|
||||||
|
|
||||||
|
return {"message": "Item removed from collection"}
|
||||||
|
|
||||||
|
|
||||||
|
class GetCollectionBody(BaseModel):
|
||||||
|
collection_id: int = Field(description="The ID of the collection to get")
|
||||||
|
|
||||||
|
|
||||||
|
@api.get("/<int:collection_id>")
|
||||||
|
def get_collection(path: GetCollectionBody):
|
||||||
|
"""
|
||||||
|
Get a collection.
|
||||||
|
"""
|
||||||
|
collection = CollectionTable.get_by_id(path.collection_id)
|
||||||
|
if not collection:
|
||||||
|
return {"error": "Collection not found"}, 404
|
||||||
|
|
||||||
|
items = recover_page_items(collection["items"])
|
||||||
|
return {
|
||||||
|
"id": collection["id"],
|
||||||
|
"name": collection["name"],
|
||||||
|
"items": items,
|
||||||
|
"extra": collection["extra"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateCollectionBody(BaseModel):
|
||||||
|
name: str = Field(description="The name of the collection")
|
||||||
|
description: str = Field(
|
||||||
|
description="The description of the collection", default=""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@api.put("/<int:collection_id>")
|
||||||
|
def update_collection(path: GetCollectionBody, body: UpdateCollectionBody):
|
||||||
|
"""
|
||||||
|
Update a collection.
|
||||||
|
"""
|
||||||
|
payload = {
|
||||||
|
"id": path.collection_id,
|
||||||
|
"name": body.name,
|
||||||
|
"extra": {"description": body.description},
|
||||||
|
}
|
||||||
|
|
||||||
|
CollectionTable.update_one(payload)
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteCollectionPath(BaseModel):
|
||||||
|
collection_id: int = Field(description="The ID of the collection to delete")
|
||||||
|
|
||||||
|
|
||||||
|
@api.delete("/<int:collection_id>")
|
||||||
|
def delete_collection(path: DeleteCollectionPath):
|
||||||
|
"""
|
||||||
|
Delete a collection.
|
||||||
|
"""
|
||||||
|
CollectionTable.delete_by_id(path.collection_id)
|
||||||
|
return {"message": "Collection deleted"}
|
||||||
@@ -39,9 +39,8 @@ class FavoritesAddBody(BaseModel):
|
|||||||
description="The hash of the item",
|
description="The hash of the item",
|
||||||
min_length=Defaults.HASH_LENGTH,
|
min_length=Defaults.HASH_LENGTH,
|
||||||
max_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)
|
type: str = Field(description="The type of the item")
|
||||||
|
|
||||||
|
|
||||||
def toggle_fav(type: str, hash: str):
|
def toggle_fav(type: str, hash: str):
|
||||||
@@ -110,7 +109,6 @@ class GetAllOfTypeQuery(GenericLimitSchema):
|
|||||||
|
|
||||||
start: int = Field(
|
start: int = Field(
|
||||||
description="Where to start from",
|
description="Where to start from",
|
||||||
example=Defaults.API_CARD_LIMIT,
|
|
||||||
default=Defaults.API_CARD_LIMIT,
|
default=Defaults.API_CARD_LIMIT,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -167,19 +165,16 @@ class GetAllFavoritesQuery(BaseModel):
|
|||||||
|
|
||||||
track_limit: int = Field(
|
track_limit: int = Field(
|
||||||
description="The number of tracks to return",
|
description="The number of tracks to return",
|
||||||
example=Defaults.API_CARD_LIMIT,
|
|
||||||
default=Defaults.API_CARD_LIMIT,
|
default=Defaults.API_CARD_LIMIT,
|
||||||
)
|
)
|
||||||
|
|
||||||
album_limit: int = Field(
|
album_limit: int = Field(
|
||||||
description="The number of albums to return",
|
description="The number of albums to return",
|
||||||
example=Defaults.API_CARD_LIMIT,
|
|
||||||
default=Defaults.API_CARD_LIMIT,
|
default=Defaults.API_CARD_LIMIT,
|
||||||
)
|
)
|
||||||
|
|
||||||
artist_limit: int = Field(
|
artist_limit: int = Field(
|
||||||
description="The number of artists to return",
|
description="The number of artists to return",
|
||||||
example=Defaults.API_CARD_LIMIT,
|
|
||||||
default=Defaults.API_CARD_LIMIT,
|
default=Defaults.API_CARD_LIMIT,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,173 +0,0 @@
|
|||||||
"""
|
|
||||||
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, remove_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, existing=[])
|
|
||||||
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
PageTable.insert_one(payload)
|
|
||||||
|
|
||||||
return {"message": "Page created"}, 201
|
|
||||||
|
|
||||||
|
|
||||||
@api.get("")
|
|
||||||
def get_pages():
|
|
||||||
"""
|
|
||||||
Get all pages.
|
|
||||||
"""
|
|
||||||
return [page for page in PageTable.get_all()]
|
|
||||||
|
|
||||||
|
|
||||||
class AddPageItemBody(BaseModel):
|
|
||||||
item: dict[str, Any] = Field(
|
|
||||||
description="The item to add to the page",
|
|
||||||
example={"type": "album", "hash": "1234567890"},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AddPageItemPath(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_item(path: AddPageItemPath, body: AddPageItemBody):
|
|
||||||
"""
|
|
||||||
Add an item to a page.
|
|
||||||
"""
|
|
||||||
page = PageTable.get_by_id(path.page_id)
|
|
||||||
|
|
||||||
if page is None:
|
|
||||||
return {"error": "Page not found"}, 404
|
|
||||||
|
|
||||||
new_items = validate_page_items([body.item], existing=page["items"])
|
|
||||||
|
|
||||||
if len(new_items) == 0:
|
|
||||||
return {"error": "items already in page"}, 400
|
|
||||||
|
|
||||||
page["items"].extend(new_items)
|
|
||||||
PageTable.update_items(page["id"], page["items"])
|
|
||||||
|
|
||||||
return {"message": "Items added to page"}
|
|
||||||
|
|
||||||
|
|
||||||
class RemovePageItemBody(BaseModel):
|
|
||||||
item: dict[str, Any] = Field(
|
|
||||||
description="The item to remove from the page",
|
|
||||||
example={"type": "album", "hash": "1234567890"},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class RemovePageItemPath(BaseModel):
|
|
||||||
page_id: int = Field(description="The ID of the page to remove items from")
|
|
||||||
|
|
||||||
|
|
||||||
@api.delete("/<int:page_id>/items")
|
|
||||||
def remove_page_item(path: RemovePageItemPath, body: RemovePageItemBody):
|
|
||||||
"""
|
|
||||||
Remove an item from a page.
|
|
||||||
"""
|
|
||||||
page = PageTable.get_by_id(path.page_id)
|
|
||||||
|
|
||||||
if page is None:
|
|
||||||
return {"error": "Page not found"}, 404
|
|
||||||
|
|
||||||
remaining = remove_page_items(page["items"], body.item)
|
|
||||||
PageTable.update_items(page["id"], remaining)
|
|
||||||
|
|
||||||
return {"message": "Item removed from page"}
|
|
||||||
|
|
||||||
|
|
||||||
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"}
|
|
||||||
+1
-1
@@ -9,7 +9,7 @@ class Mixes(CronJob):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
name: str = "mixes"
|
name: str = "mixes"
|
||||||
hours: int = 6
|
hours: int = 12
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|||||||
+21
-4
@@ -222,6 +222,9 @@ class FavoritesTable(Base):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def insert_item(cls, item: dict[str, Any]):
|
def insert_item(cls, item: dict[str, Any]):
|
||||||
|
# guard against hash collisions for different item types
|
||||||
|
item["hash"] = f"{item['type']}_{item['hash']}"
|
||||||
|
|
||||||
item["timestamp"] = int(datetime.datetime.now().timestamp())
|
item["timestamp"] = int(datetime.datetime.now().timestamp())
|
||||||
item["userid"] = get_current_userid()
|
item["userid"] = get_current_userid()
|
||||||
|
|
||||||
@@ -232,16 +235,29 @@ class FavoritesTable(Base):
|
|||||||
return next(
|
return next(
|
||||||
cls.execute(
|
cls.execute(
|
||||||
delete(cls).where(
|
delete(cls).where(
|
||||||
(cls.hash == item["hash"]) & (cls.type == item["type"])
|
(cls.hash == item["hash"])
|
||||||
)
|
| (cls.hash == f"{item['type']}_{item['hash']}")
|
||||||
|
),
|
||||||
|
commit=True,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def check_exists(cls, hash: str, type: str):
|
def check_exists(cls, hash: str, type: str):
|
||||||
result = cls.execute(select(cls).where((cls.hash == hash) & (cls.type == type)))
|
result = cls.execute(
|
||||||
|
select(cls).where((cls.hash == hash) | (cls.hash == f"{type}_{hash}"))
|
||||||
|
)
|
||||||
|
|
||||||
return next(result).scalar() is not None
|
return next(result).scalar() is not None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_by_hash(cls, hash: str, type: str):
|
||||||
|
result = cls.execute(
|
||||||
|
select(cls).where((cls.hash == hash) | (cls.hash == f"{type}_{hash}"))
|
||||||
|
)
|
||||||
|
|
||||||
|
return next(result).scalars().all()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_all_of_type(cls, type: str, start: int, limit: int):
|
def get_all_of_type(cls, type: str, start: int, limit: int):
|
||||||
result = cls.execute(
|
result = cls.execute(
|
||||||
@@ -647,7 +663,8 @@ class MixTable(Base):
|
|||||||
return mix.extra["trackmix_saved"]
|
return mix.extra["trackmix_saved"]
|
||||||
|
|
||||||
|
|
||||||
class PageTable(Base):
|
class CollectionTable(Base):
|
||||||
|
# INFO: table name was kept as page to avoid breaking existing data
|
||||||
__tablename__ = "page"
|
__tablename__ = "page"
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
|||||||
@@ -9,3 +9,7 @@ class Favorite:
|
|||||||
timestamp: int
|
timestamp: int
|
||||||
userid: int
|
userid: int
|
||||||
extra: dict[str, Any]
|
extra: dict[str, Any]
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
# remove the type prefix from the hash
|
||||||
|
self.hash = self.hash.replace(f"{self.type}_", "")
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from app.db.userdata import PageTable
|
from app.db.userdata import CollectionTable
|
||||||
from app.lib.pagelib import recover_page_items
|
from app.lib.pagelib import recover_page_items
|
||||||
from app.store.homepageentries import (
|
from app.store.homepageentries import (
|
||||||
BecauseYouListenedToArtistHomepageEntry,
|
BecauseYouListenedToArtistHomepageEntry,
|
||||||
@@ -65,7 +65,7 @@ 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
|
||||||
pages = PageTable.get_all()
|
pages = CollectionTable.get_all()
|
||||||
pagedata = []
|
pagedata = []
|
||||||
|
|
||||||
for page in pages:
|
for page in pages:
|
||||||
@@ -76,7 +76,7 @@ class HomepageStore:
|
|||||||
"title": page["name"],
|
"title": page["name"],
|
||||||
"description": page["extra"]["description"],
|
"description": page["extra"]["description"],
|
||||||
"items": recover_page_items(page["items"], for_homepage=True),
|
"items": recover_page_items(page["items"], for_homepage=True),
|
||||||
"url": f"pages/{page['id']}",
|
"url": f"collections/{page['id']}",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user