Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Collections #964

Merged
merged 40 commits into from
Jul 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
715a7d3
main navigation components extracted
zurdi15 Jun 28, 2024
02f4bf4
collections view added
zurdi15 Jun 28, 2024
70200d8
Merge branch 'feature/fav_for_siblings' into feature/fav_collection
zurdi15 Jun 28, 2024
35ddac5
Merge branch 'feature/fav_for_siblings' into feature/collections
zurdi15 Jun 28, 2024
1156a10
Merge branch 'feature/fav_for_siblings' into feature/collections
zurdi15 Jun 28, 2024
2515b1f
Merge branch 'feature/fav_for_siblings' into feature/collections
zurdi15 Jun 28, 2024
ed01149
Merge branch 'feature/fav_for_siblings' into feature/collections
zurdi15 Jun 28, 2024
bfb8ab3
tweak collections/platforms drawers
zurdi15 Jun 28, 2024
08ff388
little ui tweaks
zurdi15 Jun 28, 2024
9f2f98d
Merge branch 'feature/fav_for_siblings' into feature/collections
zurdi15 Jul 1, 2024
cc9e05f
collections migration added
zurdi15 Jul 1, 2024
2b6a868
cleanup scan socket
zurdi15 Jul 1, 2024
ac6975a
fixed pinia router typed
zurdi15 Jul 2, 2024
1d6ba70
collections get_roms endpoint added
zurdi15 Jul 2, 2024
fd54e4d
split create/edit collection dialog
zurdi15 Jul 2, 2024
57d3ad3
added collection cover placeholder
zurdi15 Jul 2, 2024
857573e
Merge remote-tracking branch 'origin/master' into feature/collections
zurdi15 Jul 2, 2024
97f9792
fixed create collection dialog
zurdi15 Jul 2, 2024
23e0a06
added update collection endpoint
zurdi15 Jul 2, 2024
50bcd00
added remove rom from collection dialog
zurdi15 Jul 2, 2024
39708ff
fixes from trunk
zurdi15 Jul 3, 2024
59c2a51
added collection cover management
zurdi15 Jul 3, 2024
69ed831
added running scan loader to drawer btn
zurdi15 Jul 3, 2024
b1b55f1
fixed migrations
zurdi15 Jul 3, 2024
2a7def8
added reactivity to collections
zurdi15 Jul 3, 2024
040fd83
added reactivity to gallery
zurdi15 Jul 3, 2024
1a0a06d
fixed detail view
zurdi15 Jul 3, 2024
7570c3f
added reactivity tu current user
zurdi15 Jul 3, 2024
9a42838
check steamgriddb api key on endpoint
zurdi15 Jul 3, 2024
5126c56
get collections for detailed rom info
zurdi15 Jul 3, 2024
f2f202d
add user collections to details
zurdi15 Jul 3, 2024
0d5620e
Update backend/endpoints/collections.py
zurdi15 Jul 4, 2024
124e1e7
Update backend/endpoints/collections.py
zurdi15 Jul 4, 2024
3ac40e1
Update frontend/src/components/common/SearchCover.vue
zurdi15 Jul 4, 2024
febc579
changes from PR comments
zurdi15 Jul 4, 2024
0bd4c6f
fixed from PR comment
zurdi15 Jul 4, 2024
7fafef0
fixed add to collection for non-admin users
zurdi15 Jul 4, 2024
91dcec3
fixed edit collection permission for non-admin user
zurdi15 Jul 5, 2024
3c658d0
Update frontend/src/stores/users.ts
zurdi15 Jul 5, 2024
ebaa5d6
fixed filter roms both by platform and collection
zurdi15 Jul 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions backend/alembic/versions/0022_collections_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
"""empty message

Revision ID: 0022_collections
Revises: 0021_rom_user
Create Date: 2024-07-01 23:23:39.090219

"""

import json
import os
import shutil

import sqlalchemy as sa
from alembic import op
from config import RESOURCES_BASE_PATH
from sqlalchemy.dialects import mysql

# revision identifiers, used by Alembic.
revision = "0022_collections"
down_revision = "0021_rom_user"
branch_labels = None
depends_on = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"collections",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("name", sa.String(length=400), nullable=False),
sa.Column("description", sa.Text(), nullable=True),
sa.Column("path_cover_l", sa.String(length=1000), nullable=True),
sa.Column("path_cover_s", sa.String(length=1000), nullable=True),
sa.Column("url_cover", sa.Text(), nullable=True),
sa.Column("roms", sa.JSON(), nullable=False),
sa.Column("user_id", sa.Integer(), nullable=False),
sa.Column("is_public", sa.Boolean(), nullable=False),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
)
with op.batch_alter_table("rom_user", schema=None) as batch_op:
batch_op.alter_column(
"is_main_sibling",
existing_type=mysql.TINYINT(display_width=1),
nullable=True,
)

connection = op.get_bind()
roms = connection.execute(
sa.text(
"SELECT id, name, platform_id, path_cover_s, path_cover_l, path_screenshots FROM roms"
)
).fetchall()

# Define the path for the new folder
roms_folder_path = os.path.join(RESOURCES_BASE_PATH, "roms")

# Create the new folder if it doesn't exist
os.makedirs(roms_folder_path, exist_ok=True)

# List all items in the base directory
for folder in os.listdir(RESOURCES_BASE_PATH):
folder_path = os.path.join(RESOURCES_BASE_PATH, folder)

# Check if the item is a directory and not the new folder itself
if os.path.isdir(folder_path) and folder != "roms":
# Move the folder to the new folder
shutil.move(folder_path, roms_folder_path)

# Update paths for each rom
for rom in roms:
path_cover_s = rom.path_cover_s
path_cover_l = rom.path_cover_l
path_screenshots = rom.path_screenshots

# Add "roms/" prefix to path_cover_s and path_cover_l
if path_cover_s:
path_cover_s = f"roms/{path_cover_s}"
if path_cover_l:
path_cover_l = f"roms/{path_cover_l}"

# Add "roms/" prefix to each path in path_screenshots
if path_screenshots:
path_screenshots_list = json.loads(path_screenshots)
path_screenshots_list = [f"roms/{path}" for path in path_screenshots_list]
path_screenshots = json.dumps(path_screenshots_list)

# Update the database with the new paths
connection.execute(
sa.text(
"UPDATE roms SET path_cover_s = :path_cover_s, path_cover_l = :path_cover_l, path_screenshots = :path_screenshots WHERE id = :id"
),
{
"path_cover_s": path_cover_s,
"path_cover_l": path_cover_l,
"path_screenshots": path_screenshots,
"id": rom.id,
},
)
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("rom_user", schema=None) as batch_op:
batch_op.alter_column(
"is_main_sibling",
existing_type=mysql.TINYINT(display_width=1),
nullable=False,
)

op.drop_table("collections")
# ### end Alembic commands ###
234 changes: 234 additions & 0 deletions backend/endpoints/collections.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
import json
from shutil import rmtree

from config import RESOURCES_BASE_PATH
from decorators.auth import protected_route
from endpoints.responses import MessageResponse
from endpoints.responses.collection import CollectionSchema
from exceptions.endpoint_exceptions import (
CollectionAlreadyExistsException,
CollectionNotFoundInDatabaseException,
CollectionPermissionError,
)
from fastapi import APIRouter, Request, UploadFile
from handler.database import db_collection_handler
from handler.filesystem import fs_resource_handler
from logger.logger import log
from models.collection import Collection
from sqlalchemy.inspection import inspect

router = APIRouter()


@protected_route(router.post, "/collections", ["collections.write"])
async def add_collection(
request: Request,
artwork: UploadFile | None = None,
) -> CollectionSchema:
"""Create collection endpoint

Args:
request (Request): Fastapi Request object

Returns:
CollectionSchema: Just created collection
"""

data = await request.form()
cleaned_data = {
"name": data.get("name", ""),
"description": data.get("description", ""),
"url_cover": data.get("url_cover", ""),
"is_public": data.get("is_public", False),
"user_id": request.user.id,
}
db_collection = db_collection_handler.get_collection_by_name(
cleaned_data["name"], request.user.id
)

if db_collection:
raise CollectionAlreadyExistsException(cleaned_data["name"])

_added_collection = db_collection_handler.add_collection(Collection(**cleaned_data))

if artwork is not None:
file_ext = artwork.filename.split(".")[-1]
(
path_cover_l,
path_cover_s,
artwork_path,
) = fs_resource_handler.build_artwork_path(_added_collection, file_ext)

artwork_file = artwork.file.read()
file_location_s = f"{artwork_path}/small.{file_ext}"
with open(file_location_s, "wb+") as artwork_s:
artwork_s.write(artwork_file)
fs_resource_handler.resize_cover_to_small(file_location_s)

file_location_l = f"{artwork_path}/big.{file_ext}"
with open(file_location_l, "wb+") as artwork_l:
artwork_l.write(artwork_file)
else:
path_cover_s, path_cover_l = fs_resource_handler.get_cover(
overwrite=True,
entity=_added_collection,
url_cover=_added_collection.url_cover,
)
zurdi15 marked this conversation as resolved.
Show resolved Hide resolved

_added_collection.path_cover_s = path_cover_s
_added_collection.path_cover_l = path_cover_l
# Update the collection with the cover path and update database
return db_collection_handler.update_collection(
_added_collection.id,
{
c: getattr(_added_collection, c)
for c in inspect(_added_collection).mapper.column_attrs.keys()
},
)


@protected_route(router.get, "/collections", ["collections.read"])
def get_collections(request: Request) -> list[CollectionSchema]:
"""Get collections endpoint

Args:
request (Request): Fastapi Request object
id (int, optional): Collection id. Defaults to None.

Returns:
list[CollectionSchema]: List of collections
"""

return db_collection_handler.get_collections(user_id=request.user.id)


@protected_route(router.get, "/collections/{id}", ["collections.read"])
def get_collection(request: Request, id: int) -> CollectionSchema:
"""Get collections endpoint

Args:
request (Request): Fastapi Request object
id (int, optional): Collection id. Defaults to None.

Returns:
CollectionSchema: Collection
"""

collection = db_collection_handler.get_collection(id)

if not collection:
raise CollectionNotFoundInDatabaseException(id)

return collection


@protected_route(router.put, "/collections/{id}", ["collections.write"])
async def update_collection(
request: Request,
id: int,
remove_cover: bool = False,
artwork: UploadFile | None = None,
) -> CollectionSchema:
"""Update collection endpoint

Args:
request (Request): Fastapi Request object

Returns:
MessageResponse: Standard message response
"""

data = await request.form()
collection = db_collection_handler.get_collection(id)

if collection.user_id != request.user.id:
raise CollectionPermissionError(id)

if not collection:
raise CollectionNotFoundInDatabaseException(id)

try:
roms = json.loads(data["roms"])
except json.JSONDecodeError as e:
raise ValueError("Invalid list for roms field in update collection") from e
except KeyError:
roms = collection.roms

cleaned_data = {
"name": data.get("name", collection.name),
"description": data.get("description", collection.description),
"roms": list(set(roms)),
"url_cover": data.get("url_cover", collection.url_cover),
"is_public": data.get("is_public", collection.is_public),
"user_id": request.user.id,
}

if remove_cover:
cleaned_data.update(fs_resource_handler.remove_cover(collection))
cleaned_data.update({"url_cover": ""})
else:
if artwork is not None:
file_ext = artwork.filename.split(".")[-1]
(
path_cover_l,
path_cover_s,
artwork_path,
) = fs_resource_handler.build_artwork_path(collection, file_ext)

cleaned_data["path_cover_l"] = path_cover_l
cleaned_data["path_cover_s"] = path_cover_s

artwork_file = artwork.file.read()
file_location_s = f"{artwork_path}/small.{file_ext}"
with open(file_location_s, "wb+") as artwork_s:
artwork_s.write(artwork_file)
fs_resource_handler.resize_cover_to_small(file_location_s)

file_location_l = f"{artwork_path}/big.{file_ext}"
with open(file_location_l, "wb+") as artwork_l:
artwork_l.write(artwork_file)
else:
cleaned_data["url_cover"] = data.get("url_cover", collection.url_cover)
path_cover_s, path_cover_l = fs_resource_handler.get_cover(
overwrite=cleaned_data["url_cover"] != collection.url_cover,
entity=collection,
url_cover=cleaned_data.get("url_cover", ""),
)
cleaned_data.update(
{"path_cover_s": path_cover_s, "path_cover_l": path_cover_l}
)

return db_collection_handler.update_collection(id, cleaned_data)


@protected_route(router.delete, "/collections/{id}", ["collections.write"])
async def delete_collections(request: Request, id: int) -> MessageResponse:
"""Delete collections endpoint

Args:
request (Request): Fastapi Request object
{
"collections": List of rom's ids to delete
}

Raises:
HTTPException: Collection not found

Returns:
MessageResponse: Standard message response
"""

collection = db_collection_handler.get_collection(id)

if not collection:
raise CollectionNotFoundInDatabaseException(id)

log.info(f"Deleting {collection.name} from database")
db_collection_handler.delete_collection(id)

try:
rmtree(f"{RESOURCES_BASE_PATH}/{collection.fs_resources_path}")
except FileNotFoundError:
log.error(f"Couldn't find resources to delete for {collection.name}")

return {"msg": f"{collection.name} deleted successfully!"}
3 changes: 1 addition & 2 deletions backend/endpoints/platform.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,7 @@ async def add_platforms(request: Request) -> PlatformSchema:
except PlatformAlreadyExistsException:
log.info(f"Detected platform: {fs_slug}")
scanned_platform = scan_platform(fs_slug, [fs_slug])
platform = db_platform_handler.add_platform(scanned_platform)
return platform
return db_platform_handler.add_platform(scanned_platform)


@protected_route(router.get, "/platforms", ["platforms.read"])
Expand Down
24 changes: 24 additions & 0 deletions backend/endpoints/responses/collection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from datetime import datetime

from pydantic import BaseModel


class CollectionSchema(BaseModel):
id: int
name: str
description: str
path_cover_l: str | None
path_cover_s: str | None
has_cover: bool
url_cover: str
roms: set[int]
rom_count: int
user_id: int
user__username: str
is_public: bool

created_at: datetime
updated_at: datetime

class Config:
from_attributes = True
Loading
Loading