mirror of
https://github.com/rommapp/romm.git
synced 2026-02-18 00:27:41 +01:00
Mega refactor of asset
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -47,6 +47,7 @@ romm_mock
|
||||
|
||||
# testing
|
||||
backend/romm_test/resources
|
||||
backend/romm_test/assets
|
||||
backend/romm_test/logs
|
||||
backend/romm_test/config
|
||||
.pytest_cache
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
mkdir -p romm_mock/library/roms/switch
|
||||
touch romm_mock/library/roms/switch/metroid.xci
|
||||
mkdir -p romm_mock/resources
|
||||
mkdir -p romm_mock/assets
|
||||
mkdir -p romm_mock/config
|
||||
touch romm_mock/config.yml
|
||||
```
|
||||
@@ -87,6 +88,7 @@ npm install
|
||||
```sh
|
||||
mkdir assets/romm
|
||||
ln -s ../../../romm_mock/resources assets/romm/resources
|
||||
ln -s ../../../romm_mock/assets assets/romm/assets
|
||||
```
|
||||
|
||||
### - Run the frontend
|
||||
|
||||
58
backend/alembic/versions/0019_migrate_to_mysql.py
Normal file
58
backend/alembic/versions/0019_migrate_to_mysql.py
Normal file
@@ -0,0 +1,58 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 0019_migrate_to_mysql
|
||||
Revises: 0018_increase_file_extension
|
||||
Create Date: 2024-01-24 13:54:32.458301
|
||||
|
||||
"""
|
||||
import os
|
||||
from sqlalchemy import create_engine, text
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from config import ROMM_DB_DRIVER
|
||||
from config.config_manager import ConfigManager, SQLITE_DB_BASE_PATH
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '0019_migrate_to_mysql'
|
||||
down_revision = '0018_increase_file_extension'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
if ROMM_DB_DRIVER != "mariadb":
|
||||
raise Exception("Version 3.0 requires MariaDB as database driver!")
|
||||
|
||||
# Skip if sqlite database is not mounted
|
||||
if not os.path.exists(f"{SQLITE_DB_BASE_PATH}/romm.db"):
|
||||
return
|
||||
|
||||
maria_engine = create_engine(ConfigManager.get_db_engine(), pool_pre_ping=True)
|
||||
maria_session = sessionmaker(bind=maria_engine, expire_on_commit=False)
|
||||
|
||||
sqlite_engine = create_engine(f"sqlite:////{SQLITE_DB_BASE_PATH}/romm.db", pool_pre_ping=True)
|
||||
sqlite_session = sessionmaker(bind=sqlite_engine, expire_on_commit=False)
|
||||
|
||||
# Copy all data from sqlite to maria
|
||||
with maria_session.begin() as maria_conn:
|
||||
with sqlite_session.begin() as sqlite_conn:
|
||||
maria_conn.execute(text("SET FOREIGN_KEY_CHECKS=0"))
|
||||
|
||||
tables = sqlite_conn.execute(text("SELECT name FROM sqlite_master WHERE type='table';")).fetchall()
|
||||
for table_name in tables:
|
||||
table_name = table_name[0]
|
||||
if table_name == "alembic_version":
|
||||
continue
|
||||
|
||||
table_data = sqlite_conn.execute(text(f"SELECT * FROM {table_name}")).fetchall()
|
||||
|
||||
# Insert data into MariaDB table
|
||||
for row in table_data:
|
||||
maria_conn.execute(text(f"INSERT INTO {table_name} VALUES {tuple(row)}"))
|
||||
|
||||
maria_conn.execute(text("SET FOREIGN_KEY_CHECKS=1"))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
pass
|
||||
56
backend/alembic/versions/0020_assets_user_id.py
Normal file
56
backend/alembic/versions/0020_assets_user_id.py
Normal file
@@ -0,0 +1,56 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 0020_assets_user_id
|
||||
Revises: 0019_migrate_to_mysql
|
||||
Create Date: 2024-02-01 17:08:02.103046
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "0020_assets_user_id"
|
||||
down_revision = "0019_migrate_to_mysql"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("saves", schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column("user_id", sa.Integer(), nullable=False))
|
||||
batch_op.create_foreign_key(
|
||||
"saves_user_FK", "users", ["user_id"], ["id"], ondelete="CASCADE"
|
||||
)
|
||||
|
||||
with op.batch_alter_table("screenshots", schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column("user_id", sa.Integer(), nullable=False))
|
||||
batch_op.create_foreign_key(
|
||||
"screenshots_user_FK", "users", ["user_id"], ["id"], ondelete="CASCADE"
|
||||
)
|
||||
|
||||
with op.batch_alter_table("states", schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column("user_id", sa.Integer(), nullable=False))
|
||||
batch_op.create_foreign_key(
|
||||
"states_user_FK", "users", ["user_id"], ["id"], ondelete="CASCADE"
|
||||
)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("states", schema=None) as batch_op:
|
||||
batch_op.drop_constraint("states_user_FK", type_="foreignkey")
|
||||
batch_op.drop_column("user_id")
|
||||
|
||||
with op.batch_alter_table("screenshots", schema=None) as batch_op:
|
||||
batch_op.drop_constraint("screenshots_user_FK", type_="foreignkey")
|
||||
batch_op.drop_column("user_id")
|
||||
|
||||
with op.batch_alter_table("saves", schema=None) as batch_op:
|
||||
batch_op.drop_constraint("saves_user_FK", type_="foreignkey")
|
||||
batch_op.drop_column("user_id")
|
||||
|
||||
# ### end Alembic commands ###
|
||||
@@ -14,9 +14,10 @@ ROMM_HOST: Final = os.environ.get("ROMM_HOST", DEV_HOST)
|
||||
# PATHS
|
||||
ROMM_BASE_PATH: Final = os.environ.get("ROMM_BASE_PATH", "/romm")
|
||||
LIBRARY_BASE_PATH: Final = f"{ROMM_BASE_PATH}/library"
|
||||
FRONTEND_LIBRARY_PATH: Final = "/assets/romm/library"
|
||||
RESOURCES_BASE_PATH: Final = f"{ROMM_BASE_PATH}/resources"
|
||||
ASSETS_BASE_PATH: Final = f"{ROMM_BASE_PATH}/assets"
|
||||
FRONTEND_RESOURCES_PATH: Final = "/assets/romm/resources"
|
||||
|
||||
FRONTEND_ASSETS_PATH: Final = "/assets/romm/assets"
|
||||
# DEFAULT RESOURCES
|
||||
DEFAULT_URL_COVER_L: Final = (
|
||||
"https://images.igdb.com/igdb/image/upload/t_cover_big/nocover.png"
|
||||
|
||||
@@ -38,9 +38,6 @@ class Config:
|
||||
PLATFORMS_BINDING: dict[str, str]
|
||||
PLATFORMS_VERSIONS: dict[str, str]
|
||||
ROMS_FOLDER_NAME: str
|
||||
SAVES_FOLDER_NAME: str
|
||||
STATES_FOLDER_NAME: str
|
||||
SCREENSHOTS_FOLDER_NAME: str
|
||||
HIGH_PRIO_STRUCTURE_PATH: str
|
||||
|
||||
def __init__(self, **entries):
|
||||
@@ -133,15 +130,6 @@ class ConfigManager:
|
||||
ROMS_FOLDER_NAME=pydash.get(
|
||||
self._raw_config, "filesystem.roms_folder", "roms"
|
||||
),
|
||||
SAVES_FOLDER_NAME=pydash.get(
|
||||
self._raw_config, "filesystem.saves_folder", "saves"
|
||||
),
|
||||
STATES_FOLDER_NAME=pydash.get(
|
||||
self._raw_config, "filesystem.states_folder", "states"
|
||||
),
|
||||
SCREENSHOTS_FOLDER_NAME=pydash.get(
|
||||
self._raw_config, "filesystem.screenshots_folder", "screenshots"
|
||||
),
|
||||
)
|
||||
|
||||
def _validate_config(self):
|
||||
@@ -212,40 +200,6 @@ class ConfigManager:
|
||||
)
|
||||
sys.exit(3)
|
||||
|
||||
if not isinstance(self.config.SAVES_FOLDER_NAME, str):
|
||||
log.critical("Invalid config.yml: filesystem.saves_folder must be a string")
|
||||
sys.exit(3)
|
||||
|
||||
if self.config.SAVES_FOLDER_NAME == "":
|
||||
log.critical(
|
||||
"Invalid config.yml: filesystem.saves_folder cannot be an empty string"
|
||||
)
|
||||
sys.exit(3)
|
||||
|
||||
if not isinstance(self.config.STATES_FOLDER_NAME, str):
|
||||
log.critical(
|
||||
"Invalid config.yml: filesystem.states_folder must be a string"
|
||||
)
|
||||
sys.exit(3)
|
||||
|
||||
if self.config.STATES_FOLDER_NAME == "":
|
||||
log.critical(
|
||||
"Invalid config.yml: filesystem.states_folder cannot be an empty string"
|
||||
)
|
||||
sys.exit(3)
|
||||
|
||||
if not isinstance(self.config.SCREENSHOTS_FOLDER_NAME, str):
|
||||
log.critical(
|
||||
"Invalid config.yml: filesystem.screenshots_folder must be a string"
|
||||
)
|
||||
sys.exit(3)
|
||||
|
||||
if self.config.SCREENSHOTS_FOLDER_NAME == "":
|
||||
log.critical(
|
||||
"Invalid config.yml: filesystem.screenshots_folder cannot be an empty string"
|
||||
)
|
||||
sys.exit(3)
|
||||
|
||||
def read_config(self) -> None:
|
||||
try:
|
||||
with open(self.config_file) as config_file:
|
||||
|
||||
@@ -18,9 +18,6 @@ def test_config_loader():
|
||||
assert loader.config.PLATFORMS_BINDING == {"gc": "ngc"}
|
||||
assert loader.config.PLATFORMS_VERSIONS == {"naomi": "arcade"}
|
||||
assert loader.config.ROMS_FOLDER_NAME == "ROMS"
|
||||
assert loader.config.SAVES_FOLDER_NAME == "SAVES"
|
||||
assert loader.config.STATES_FOLDER_NAME == "STATES"
|
||||
assert loader.config.SCREENSHOTS_FOLDER_NAME == "SCREENSHOTS"
|
||||
|
||||
|
||||
def test_empty_config_loader():
|
||||
@@ -35,6 +32,3 @@ def test_empty_config_loader():
|
||||
assert loader.config.PLATFORMS_BINDING == {}
|
||||
assert loader.config.PLATFORMS_VERSIONS == {}
|
||||
assert loader.config.ROMS_FOLDER_NAME == "roms"
|
||||
assert loader.config.SAVES_FOLDER_NAME == "saves"
|
||||
assert loader.config.STATES_FOLDER_NAME == "states"
|
||||
assert loader.config.SCREENSHOTS_FOLDER_NAME == "screenshots"
|
||||
|
||||
@@ -1,17 +1,39 @@
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import FileResponse
|
||||
from decorators.auth import protected_route
|
||||
from config import LIBRARY_BASE_PATH
|
||||
from config import LIBRARY_BASE_PATH, ASSETS_BASE_PATH
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@protected_route(router.head, "/raw/{path:path}", ["assets.read"])
|
||||
@protected_route(router.head, "/raw/roms/{path:path}", ["roms.read"])
|
||||
def head_raw_rom(request: Request, path: str):
|
||||
rom_path = f"{LIBRARY_BASE_PATH}/{path}"
|
||||
return FileResponse(path=rom_path, filename=path.split("/")[-1])
|
||||
|
||||
|
||||
@protected_route(router.get, "/raw/roms/{path:path}", ["roms.read"])
|
||||
def get_raw_rom(request: Request, path: str):
|
||||
"""Download a single rom file
|
||||
|
||||
Args:
|
||||
request (Request): Fastapi Request object
|
||||
|
||||
Returns:
|
||||
FileResponse: Returns a single rom file
|
||||
"""
|
||||
|
||||
rom_path = f"{LIBRARY_BASE_PATH}/{path}"
|
||||
return FileResponse(path=rom_path, filename=path.split("/")[-1])
|
||||
|
||||
|
||||
@protected_route(router.head, "/raw/assets/{path:path}", ["assets.read"])
|
||||
def head_raw_asset(request: Request, path: str):
|
||||
asset_path = f"{LIBRARY_BASE_PATH}/{path}"
|
||||
asset_path = f"{ASSETS_BASE_PATH}/{path}"
|
||||
return FileResponse(path=asset_path, filename=path.split("/")[-1])
|
||||
|
||||
@protected_route(router.get, "/raw/{path:path}", ["assets.read"])
|
||||
|
||||
@protected_route(router.get, "/raw/assets/{path:path}", ["assets.read"])
|
||||
def get_raw_asset(request: Request, path: str):
|
||||
"""Download a single asset file
|
||||
|
||||
@@ -22,6 +44,5 @@ def get_raw_asset(request: Request, path: str):
|
||||
FileResponse: Returns a single asset file
|
||||
"""
|
||||
|
||||
asset_path = f"{LIBRARY_BASE_PATH}/{path}"
|
||||
|
||||
asset_path = f"{ASSETS_BASE_PATH}/{path}"
|
||||
return FileResponse(path=asset_path, filename=path.split("/")[-1])
|
||||
|
||||
@@ -11,7 +11,4 @@ class ConfigResponse(TypedDict):
|
||||
PLATFORMS_BINDING: dict[str, str]
|
||||
PLATFORMS_VERSIONS: dict[str, str]
|
||||
ROMS_FOLDER_NAME: str
|
||||
SAVES_FOLDER_NAME: str
|
||||
STATES_FOLDER_NAME: str
|
||||
SCREENSHOTS_FOLDER_NAME: str
|
||||
HIGH_PRIO_STRUCTURE_PATH: str
|
||||
|
||||
@@ -46,7 +46,6 @@ class RomSchema(BaseModel):
|
||||
url_screenshots: list[str]
|
||||
merged_screenshots: list[str]
|
||||
full_path: str
|
||||
download_path: str
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
@@ -20,7 +20,6 @@ from fastapi_pagination.ext.sqlalchemy import paginate
|
||||
from handler import (
|
||||
db_platform_handler,
|
||||
db_rom_handler,
|
||||
fs_asset_handler,
|
||||
fs_resource_handler,
|
||||
fs_rom_handler,
|
||||
)
|
||||
@@ -257,7 +256,7 @@ async def update_rom(
|
||||
)
|
||||
|
||||
cleaned_data.update(
|
||||
fs_asset_handler.get_rom_screenshots(
|
||||
fs_resource_handler.get_rom_screenshots(
|
||||
platform_fs_slug=platform_fs_slug,
|
||||
rom_name=cleaned_data["name"],
|
||||
url_screenshots=cleaned_data.get("url_screenshots", []),
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
from config.config_manager import config_manager as cm
|
||||
from decorators.auth import protected_route
|
||||
from endpoints.responses import MessageResponse
|
||||
from endpoints.responses.assets import UploadedSavesResponse, SaveSchema
|
||||
from fastapi import APIRouter, File, HTTPException, Request, UploadFile, status
|
||||
from handler import db_save_handler, fs_asset_handler, db_rom_handler, db_screenshot_handler
|
||||
from handler.scan_handler import scan_save, build_asset_file_path
|
||||
from handler import (
|
||||
db_save_handler,
|
||||
fs_asset_handler,
|
||||
db_rom_handler,
|
||||
db_screenshot_handler,
|
||||
)
|
||||
from handler.scan_handler import scan_save
|
||||
from logger.logger import log
|
||||
from config import LIBRARY_BASE_PATH
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -27,19 +30,18 @@ def add_saves(
|
||||
detail="No saves were uploaded",
|
||||
)
|
||||
|
||||
saves_path = build_asset_file_path(
|
||||
rom.platform.fs_slug, folder=cm.config.SAVES_FOLDER_NAME, emulator=emulator
|
||||
saves_path = fs_asset_handler.build_saves_file_path(
|
||||
user=request.user, platform_fs_slug=rom.platform.fs_slug, emulator=emulator
|
||||
)
|
||||
|
||||
for save in saves:
|
||||
fs_asset_handler._write_file(
|
||||
file=save, path=f"{LIBRARY_BASE_PATH}/{saves_path}"
|
||||
)
|
||||
fs_asset_handler.write_file(file=save, path=saves_path)
|
||||
|
||||
# Scan or update save
|
||||
scanned_save = scan_save(
|
||||
file_name=save.filename,
|
||||
platform_slug=rom.platform.fs_slug,
|
||||
user=request.user,
|
||||
platform_fs_slug=rom.platform.fs_slug,
|
||||
emulator=emulator,
|
||||
)
|
||||
db_save = db_save_handler.get_save_by_filename(rom.id, save.filename)
|
||||
@@ -50,6 +52,7 @@ def add_saves(
|
||||
continue
|
||||
|
||||
scanned_save.rom_id = rom.id
|
||||
scanned_save.user_id = request.user.id
|
||||
scanned_save.emulator = emulator
|
||||
db_save_handler.add_save(scanned_save)
|
||||
|
||||
@@ -79,9 +82,7 @@ async def update_save(request: Request, id: int) -> SaveSchema:
|
||||
|
||||
if "file" in data:
|
||||
file: UploadFile = data["file"]
|
||||
fs_asset_handler._write_file(
|
||||
file=file, path=f"{LIBRARY_BASE_PATH}/{db_save.file_path}"
|
||||
)
|
||||
fs_asset_handler.write_file(file=file, path=db_save.file_path)
|
||||
db_save_handler.update_save(db_save.id, {"file_size_bytes": file.size})
|
||||
|
||||
db_save = db_save_handler.get_save(id)
|
||||
@@ -119,7 +120,7 @@ async def delete_saves(request: Request) -> MessageResponse:
|
||||
error = f"Save file {save.file_name} not found for platform {save.rom.platform_slug}"
|
||||
log.error(error)
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=error)
|
||||
|
||||
|
||||
if save.screenshot:
|
||||
db_screenshot_handler.delete_screenshot(save.screenshot.id)
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from config.config_manager import config_manager as cm
|
||||
from decorators.auth import protected_route
|
||||
from endpoints.responses.assets import UploadedScreenshotsResponse
|
||||
from fastapi import APIRouter, File, HTTPException, Request, UploadFile, status
|
||||
@@ -22,16 +21,18 @@ def add_screenshots(
|
||||
detail="No screenshots were uploaded",
|
||||
)
|
||||
|
||||
screenshots_path = fs_asset_handler.build_upload_file_path(
|
||||
rom.platform.fs_slug, folder=cm.config.SCREENSHOTS_FOLDER_NAME
|
||||
screenshots_path = fs_asset_handler.build_screenshots_file_path(
|
||||
user=request.user, platform_fs_slug=rom.platform_slug
|
||||
)
|
||||
|
||||
for screenshot in screenshots:
|
||||
fs_asset_handler._write_file(file=screenshot, path=screenshots_path)
|
||||
fs_asset_handler.write_file(file=screenshot, path=screenshots_path)
|
||||
|
||||
# Scan or update screenshot
|
||||
scanned_screenshot = scan_screenshot(
|
||||
file_name=screenshot.filename, platform_slug=rom.platform_slug
|
||||
file_name=screenshot.filename,
|
||||
user=request.user,
|
||||
platform_fs_slug=rom.platform_slug,
|
||||
)
|
||||
db_screenshot = db_screenshot_handler.get_screenshot_by_filename(
|
||||
file_name=screenshot.filename, rom_id=rom.id
|
||||
@@ -44,6 +45,7 @@ def add_screenshots(
|
||||
continue
|
||||
|
||||
scanned_screenshot.rom_id = rom.id
|
||||
scanned_screenshot.user_id = request.user.id
|
||||
db_screenshot_handler.add_screenshot(scanned_screenshot)
|
||||
|
||||
rom = db_rom_handler.get_roms(rom_id)
|
||||
|
||||
@@ -10,23 +10,15 @@ from exceptions.fs_exceptions import (
|
||||
from handler import (
|
||||
db_platform_handler,
|
||||
db_rom_handler,
|
||||
db_save_handler,
|
||||
db_screenshot_handler,
|
||||
db_state_handler,
|
||||
fs_asset_handler,
|
||||
fs_platform_handler,
|
||||
fs_resource_handler,
|
||||
fs_rom_handler,
|
||||
socket_handler,
|
||||
)
|
||||
from handler.fs_handler import Asset
|
||||
from handler.redis_handler import high_prio_queue, redis_url
|
||||
from handler.scan_handler import (
|
||||
scan_platform,
|
||||
scan_rom,
|
||||
scan_save,
|
||||
scan_screenshot,
|
||||
scan_state,
|
||||
)
|
||||
from logger.logger import log
|
||||
|
||||
@@ -129,133 +121,9 @@ async def scan_platforms(
|
||||
},
|
||||
)
|
||||
|
||||
# Scanning saves
|
||||
fs_saves = fs_asset_handler.get_assets(
|
||||
platform.fs_slug, rom.file_name_no_ext, Asset.SAVES
|
||||
)
|
||||
if len(fs_saves) > 0:
|
||||
log.info(f"\t · {len(fs_saves)} saves found")
|
||||
|
||||
for fs_emulator, fs_save_filename in fs_saves:
|
||||
scanned_save = scan_save(
|
||||
file_name=fs_save_filename,
|
||||
platform_slug=platform.fs_slug,
|
||||
emulator=fs_emulator,
|
||||
)
|
||||
|
||||
save = db_save_handler.get_save_by_filename(rom.id, fs_save_filename)
|
||||
if save:
|
||||
# Update file size if changed
|
||||
if save.file_size_bytes != scanned_save.file_size_bytes:
|
||||
db_save_handler.update_save(
|
||||
save.id, {"file_size_bytes": scanned_save.file_size_bytes}
|
||||
)
|
||||
continue
|
||||
|
||||
scanned_save.emulator = fs_emulator
|
||||
|
||||
if rom:
|
||||
scanned_save.rom_id = rom.id
|
||||
db_save_handler.add_save(scanned_save)
|
||||
|
||||
# Scanning states
|
||||
fs_states = fs_asset_handler.get_assets(
|
||||
platform.fs_slug, rom.file_name_no_ext, Asset.STATES
|
||||
)
|
||||
if len(fs_states) > 0:
|
||||
log.info(f"\t · {len(fs_states)} states found")
|
||||
|
||||
for fs_emulator, fs_state_filename in fs_states:
|
||||
scanned_state = scan_state(
|
||||
file_name=fs_state_filename,
|
||||
platform_slug=platform.fs_slug,
|
||||
emulator=fs_emulator,
|
||||
)
|
||||
|
||||
state = db_state_handler.get_state_by_filename(
|
||||
rom.id, fs_state_filename
|
||||
)
|
||||
if state:
|
||||
# Update file size if changed
|
||||
if state.file_size_bytes != scanned_state.file_size_bytes:
|
||||
db_state_handler.update_state(
|
||||
state.id, {"file_size_bytes": scanned_state.file_size_bytes}
|
||||
)
|
||||
|
||||
continue
|
||||
|
||||
scanned_state.emulator = fs_emulator
|
||||
|
||||
if rom:
|
||||
scanned_state.rom_id = rom.id
|
||||
db_state_handler.add_state(scanned_state)
|
||||
|
||||
# Scanning screenshots
|
||||
fs_screenshots = fs_asset_handler.get_assets(
|
||||
platform.fs_slug, rom.file_name_no_ext, Asset.SCREENSHOTS
|
||||
)
|
||||
if len(fs_screenshots) > 0:
|
||||
log.info(f"\t · {len(fs_screenshots)} screenshots found")
|
||||
|
||||
for _, fs_screenshot_filename in fs_screenshots:
|
||||
scanned_screenshot = scan_screenshot(
|
||||
file_name=fs_screenshot_filename, platform_slug=platform.fs_slug
|
||||
)
|
||||
|
||||
screenshot = db_screenshot_handler.get_screenshot_by_filename(
|
||||
fs_screenshot_filename
|
||||
)
|
||||
if screenshot:
|
||||
# Update file size if changed
|
||||
if screenshot.file_size_bytes != scanned_screenshot.file_size_bytes:
|
||||
db_screenshot_handler.update_screenshot(
|
||||
screenshot.id,
|
||||
{"file_size_bytes": scanned_screenshot.file_size_bytes},
|
||||
)
|
||||
continue
|
||||
|
||||
if rom:
|
||||
scanned_screenshot.rom_id = rom.id
|
||||
db_screenshot_handler.add_screenshot(scanned_screenshot)
|
||||
|
||||
db_save_handler.purge_saves(rom.id, [s for _e, s in fs_saves])
|
||||
db_state_handler.purge_states(rom.id, [s for _e, s in fs_states])
|
||||
db_screenshot_handler.purge_screenshots(
|
||||
rom.id, [s for _e, s in fs_screenshots]
|
||||
)
|
||||
db_rom_handler.purge_roms(
|
||||
platform.id, [rom["file_name"] for rom in fs_roms]
|
||||
)
|
||||
|
||||
# Scanning screenshots outside platform folders
|
||||
fs_screenshots = fs_asset_handler.get_screenshots()
|
||||
log.info("Screenshots")
|
||||
log.info(f" · {len(fs_screenshots)} screenshots found")
|
||||
for fs_platform, fs_screenshot_filename in fs_screenshots:
|
||||
scanned_screenshot = scan_screenshot(
|
||||
file_name=fs_screenshot_filename, platform_slug=fs_platform
|
||||
)
|
||||
|
||||
screenshot = db_screenshot_handler.get_screenshot_by_filename(
|
||||
fs_screenshot_filename
|
||||
)
|
||||
if screenshot:
|
||||
# Update file size if changed
|
||||
if screenshot.file_size_bytes != scanned_screenshot.file_size_bytes:
|
||||
db_screenshot_handler.update_screenshot(
|
||||
screenshot.id,
|
||||
{"file_size_bytes": scanned_screenshot.file_size_bytes},
|
||||
)
|
||||
continue
|
||||
|
||||
rom = db_rom_handler.get_rom_by_filename_no_tags(
|
||||
scanned_screenshot.file_name_no_tags
|
||||
)
|
||||
if rom:
|
||||
scanned_screenshot.rom_id = rom.id
|
||||
db_screenshot_handler.add_screenshot(scanned_screenshot)
|
||||
|
||||
# db_screenshot_handler.purge_screenshots([s for _e, s in fs_screenshots])
|
||||
db_platform_handler.purge_platforms(fs_platforms)
|
||||
|
||||
log.info(emoji.emojize(":check_mark: Scan completed "))
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
from config.config_manager import config_manager as cm
|
||||
from decorators.auth import protected_route
|
||||
from endpoints.responses import MessageResponse
|
||||
from endpoints.responses.assets import UploadedStatesResponse, StateSchema
|
||||
from fastapi import APIRouter, File, HTTPException, Request, UploadFile, status
|
||||
from handler import db_state_handler, db_rom_handler, fs_asset_handler, db_screenshot_handler
|
||||
from handler.scan_handler import scan_state, build_asset_file_path
|
||||
from handler import (
|
||||
db_state_handler,
|
||||
db_rom_handler,
|
||||
fs_asset_handler,
|
||||
db_screenshot_handler,
|
||||
)
|
||||
from handler.scan_handler import scan_state
|
||||
from logger.logger import log
|
||||
from config import LIBRARY_BASE_PATH
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -26,20 +29,19 @@ def add_states(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="No states were uploaded",
|
||||
)
|
||||
|
||||
states_path = build_asset_file_path(
|
||||
rom.platform.fs_slug, folder=cm.config.STATES_FOLDER_NAME, emulator=emulator
|
||||
|
||||
states_path = fs_asset_handler.build_states_file_path(
|
||||
user=request.user, platform_fs_slug=rom.platform.fs_slug, emulator=emulator
|
||||
)
|
||||
|
||||
for state in states:
|
||||
fs_asset_handler._write_file(
|
||||
file=state, path=f"{LIBRARY_BASE_PATH}/{states_path}"
|
||||
)
|
||||
fs_asset_handler.write_file(file=state, path=states_path)
|
||||
|
||||
# Scan or update state
|
||||
scanned_state = scan_state(
|
||||
file_name=state.filename,
|
||||
platform_slug=rom.platform.fs_slug,
|
||||
user=request.user,
|
||||
platform_fs_slug=rom.platform.fs_slug,
|
||||
emulator=emulator,
|
||||
)
|
||||
db_state = db_state_handler.get_state_by_filename(rom.id, state.filename)
|
||||
@@ -50,6 +52,7 @@ def add_states(
|
||||
continue
|
||||
|
||||
scanned_state.rom_id = rom.id
|
||||
scanned_state.user_id = request.user.id
|
||||
scanned_state.emulator = emulator
|
||||
db_state_handler.add_state(scanned_state)
|
||||
|
||||
@@ -79,9 +82,7 @@ async def update_state(request: Request, id: int) -> StateSchema:
|
||||
|
||||
if "file" in data:
|
||||
file: UploadFile = data["file"]
|
||||
fs_asset_handler._write_file(
|
||||
file=file, path=f"{LIBRARY_BASE_PATH}/{db_state.file_path}"
|
||||
)
|
||||
fs_asset_handler.write_file(file=file, path=db_state.file_path)
|
||||
db_state_handler.update_state(db_state.id, {"file_size_bytes": file.size})
|
||||
|
||||
db_state = db_state_handler.get_state(id)
|
||||
|
||||
@@ -18,6 +18,3 @@ def test_config():
|
||||
assert config.get('EXCLUDED_MULTI_PARTS_FILES') == []
|
||||
assert config.get('PLATFORMS_BINDING') == {}
|
||||
assert config.get('ROMS_FOLDER_NAME') == 'roms'
|
||||
assert config.get('SAVES_FOLDER_NAME') == 'saves'
|
||||
assert config.get('STATES_FOLDER_NAME') == 'states'
|
||||
assert config.get('SCREENSHOTS_FOLDER_NAME') == 'screenshots'
|
||||
|
||||
@@ -6,13 +6,15 @@ from endpoints.forms.identity import UserForm
|
||||
from endpoints.responses import MessageResponse
|
||||
from endpoints.responses.identity import UserSchema
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
from handler import auth_handler, db_user_handler, fs_resource_handler
|
||||
from handler import auth_handler, db_user_handler, fs_asset_handler
|
||||
from models.user import Role, User
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@protected_route(router.post, "/users", ["users.write"], status_code=status.HTTP_201_CREATED)
|
||||
@protected_route(
|
||||
router.post, "/users", ["users.write"], status_code=status.HTTP_201_CREATED
|
||||
)
|
||||
def add_user(request: Request, username: str, password: str, role: str) -> UserSchema:
|
||||
"""Create user endpoint
|
||||
|
||||
@@ -131,7 +133,9 @@ def update_user(
|
||||
cleaned_data["username"] = form_data.username.lower()
|
||||
|
||||
if form_data.password:
|
||||
cleaned_data["hashed_password"] = auth_handler.get_password_hash(form_data.password)
|
||||
cleaned_data["hashed_password"] = auth_handler.get_password_hash(
|
||||
form_data.password
|
||||
)
|
||||
|
||||
# You can't change your own role
|
||||
if form_data.role and request.user.id != id:
|
||||
@@ -142,10 +146,9 @@ def update_user(
|
||||
cleaned_data["enabled"] = form_data.enabled # type: ignore[assignment]
|
||||
|
||||
if form_data.avatar is not None:
|
||||
cleaned_data["avatar_path"], avatar_user_path = fs_resource_handler.build_avatar_path(
|
||||
form_data.avatar.filename, form_data.username
|
||||
)
|
||||
file_location = f"{avatar_user_path}/{form_data.avatar.filename}"
|
||||
user_avatar_path = fs_asset_handler.build_avatar_path(user=user)
|
||||
file_location = f"{user_avatar_path}/{form_data.avatar.filename}"
|
||||
cleaned_data["avatar_path"] = file_location
|
||||
with open(file_location, "wb+") as file_object:
|
||||
file_object.write(form_data.avatar.file.read())
|
||||
|
||||
|
||||
@@ -53,8 +53,7 @@ class AuthHandler:
|
||||
def get_password_hash(self, password):
|
||||
return self.pwd_context.hash(password)
|
||||
|
||||
@staticmethod
|
||||
def clear_session(req: HTTPConnection | Request):
|
||||
def clear_session(self, req: HTTPConnection | Request):
|
||||
session_id = req.session.get("session_id")
|
||||
if session_id:
|
||||
cache.delete(f"romm:{session_id}") # type: ignore[attr-defined]
|
||||
|
||||
@@ -6,8 +6,7 @@ from sqlalchemy.orm import Session
|
||||
|
||||
|
||||
class DBRomsHandler(DBHandler):
|
||||
@staticmethod
|
||||
def _filter(data, platform_id, search_term):
|
||||
def _filter(self, data, platform_id, search_term):
|
||||
if platform_id:
|
||||
data = data.filter_by(platform_id=platform_id)
|
||||
|
||||
@@ -21,8 +20,7 @@ class DBRomsHandler(DBHandler):
|
||||
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def _order(data, order_by, order_dir):
|
||||
def _order(self, data, order_by, order_dir):
|
||||
if order_by == "id":
|
||||
_column = Rom.id
|
||||
else:
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
from abc import ABC
|
||||
from enum import Enum
|
||||
from typing import Final
|
||||
|
||||
from config import LIBRARY_BASE_PATH, ROMM_BASE_PATH
|
||||
from config.config_manager import config_manager as cm
|
||||
|
||||
RESOURCES_BASE_PATH: Final = f"{ROMM_BASE_PATH}/resources"
|
||||
DEFAULT_WIDTH_COVER_L: Final = 264 # Width of big cover of IGDB
|
||||
DEFAULT_HEIGHT_COVER_L: Final = 352 # Height of big cover of IGDB
|
||||
DEFAULT_WIDTH_COVER_S: Final = 90 # Width of small cover of IGDB
|
||||
@@ -90,37 +84,13 @@ class FSHandler(ABC):
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def get_fs_structure(fs_slug: str, folder: str = cm.config.ROMS_FOLDER_NAME):
|
||||
return (
|
||||
f"{folder}/{fs_slug}"
|
||||
if os.path.exists(cm.config.HIGH_PRIO_STRUCTURE_PATH)
|
||||
else f"{fs_slug}/{folder}"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_file_name_with_no_extension(file_name: str) -> str:
|
||||
def get_file_name_with_no_extension(self, file_name: str) -> str:
|
||||
return re.sub(EXTENSION_REGEX, "", file_name).strip()
|
||||
|
||||
@staticmethod
|
||||
def get_file_name_with_no_tags(file_name: str) -> str:
|
||||
file_name_no_extension = FSHandler.get_file_name_with_no_extension(file_name)
|
||||
def get_file_name_with_no_tags(self, file_name: str) -> str:
|
||||
file_name_no_extension = self.get_file_name_with_no_extension(file_name)
|
||||
return re.split(TAG_REGEX, file_name_no_extension)[0].strip()
|
||||
|
||||
@staticmethod
|
||||
def parse_file_extension(file_name) -> str:
|
||||
def parse_file_extension(self, file_name) -> str:
|
||||
match = re.search(EXTENSION_REGEX, file_name)
|
||||
return match.group(1) if match else ""
|
||||
|
||||
@staticmethod
|
||||
def remove_file(file_name: str, file_path: str):
|
||||
try:
|
||||
os.remove(f"{LIBRARY_BASE_PATH}/{file_path}/{file_name}")
|
||||
except IsADirectoryError:
|
||||
shutil.rmtree(f"{LIBRARY_BASE_PATH}/{file_path}/{file_name}")
|
||||
|
||||
def build_upload_file_path(
|
||||
self, fs_slug: str, folder: str = cm.config.ROMS_FOLDER_NAME
|
||||
):
|
||||
file_path = self.get_fs_structure(fs_slug, folder=folder)
|
||||
return f"{LIBRARY_BASE_PATH}/{file_path}"
|
||||
|
||||
@@ -1,130 +1,66 @@
|
||||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote
|
||||
|
||||
import requests
|
||||
from config import LIBRARY_BASE_PATH
|
||||
from config.config_manager import config_manager as cm
|
||||
from fastapi import UploadFile
|
||||
from handler.fs_handler import RESOURCES_BASE_PATH, Asset, FSHandler
|
||||
from handler.fs_handler import FSHandler
|
||||
from logger.logger import log
|
||||
from models.user import User
|
||||
from config import ASSETS_BASE_PATH
|
||||
|
||||
|
||||
class FSAssetsHandler(FSHandler):
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def _write_file(file: UploadFile, path: str) -> None:
|
||||
def remove_file(self, file_name: str, file_path: str):
|
||||
try:
|
||||
os.remove(os.path.join(ASSETS_BASE_PATH, file_path, file_name))
|
||||
except IsADirectoryError:
|
||||
shutil.rmtree(os.path.join(ASSETS_BASE_PATH, file_path, file_name))
|
||||
|
||||
def write_file(self, file: UploadFile, path: str) -> None:
|
||||
log.info(f" - Uploading {file.filename}")
|
||||
file_location = f"{path}/{file.filename}"
|
||||
file_location = os.path.join(ASSETS_BASE_PATH, path, file.filename)
|
||||
Path(path).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with open(file_location, "wb") as f:
|
||||
shutil.copyfileobj(file.file, f)
|
||||
|
||||
@staticmethod
|
||||
def _store_screenshot(fs_slug: str, rom_name: str, url: str, idx: int):
|
||||
"""Store roms resources in filesystem
|
||||
def get_asset_size(self, file_name: str, asset_path: str) -> int:
|
||||
return os.path.getsize(os.path.join(ASSETS_BASE_PATH, asset_path, file_name))
|
||||
|
||||
Args:
|
||||
fs_slug: short name of the platform
|
||||
file_name: name of rom
|
||||
url: url to get the screenshot
|
||||
"""
|
||||
screenshot_file: str = f"{idx}.jpg"
|
||||
screenshot_path: str = f"{RESOURCES_BASE_PATH}/{fs_slug}/{rom_name}/screenshots"
|
||||
res = requests.get(url, stream=True, timeout=120)
|
||||
if res.status_code == 200:
|
||||
Path(screenshot_path).mkdir(parents=True, exist_ok=True)
|
||||
with open(f"{screenshot_path}/{screenshot_file}", "wb") as f:
|
||||
shutil.copyfileobj(res.raw, f)
|
||||
def user_folder_path(self, user: User):
|
||||
return os.path.join("users", user.fs_safe_folder_name)
|
||||
|
||||
@staticmethod
|
||||
def _get_screenshot_path(fs_slug: str, rom_name: str, idx: str):
|
||||
"""Returns rom cover filesystem path adapted to frontend folder structure
|
||||
|
||||
Args:
|
||||
fs_slug: short name of the platform
|
||||
file_name: name of rom
|
||||
idx: index number of screenshot
|
||||
"""
|
||||
return f"{fs_slug}/{rom_name}/screenshots/{idx}.jpg"
|
||||
|
||||
def get_rom_screenshots(
|
||||
self, platform_fs_slug: str, rom_name: str, url_screenshots: list
|
||||
) -> dict:
|
||||
q_rom_name = quote(rom_name)
|
||||
|
||||
path_screenshots: list[str] = []
|
||||
for idx, url in enumerate(url_screenshots):
|
||||
self._store_screenshot(platform_fs_slug, rom_name, url, idx)
|
||||
path_screenshots.append(
|
||||
self._get_screenshot_path(platform_fs_slug, q_rom_name, str(idx))
|
||||
)
|
||||
|
||||
return {"path_screenshots": path_screenshots}
|
||||
|
||||
def get_assets(
|
||||
self, platform_slug: str, rom_file_name_no_ext: str, asset_type: Asset
|
||||
):
|
||||
asset_folder_name = {
|
||||
Asset.SAVES: cm.config.SAVES_FOLDER_NAME,
|
||||
Asset.STATES: cm.config.STATES_FOLDER_NAME,
|
||||
Asset.SCREENSHOTS: cm.config.SCREENSHOTS_FOLDER_NAME,
|
||||
}
|
||||
|
||||
assets_path = self.get_fs_structure(
|
||||
platform_slug, folder=asset_folder_name[asset_type]
|
||||
# /users/557365723a31/profile
|
||||
def build_avatar_path(self, user: User):
|
||||
user_avatar_path = os.path.join(
|
||||
ASSETS_BASE_PATH, self.user_folder_path(user), "profile"
|
||||
)
|
||||
Path(user_avatar_path).mkdir(parents=True, exist_ok=True)
|
||||
return user_avatar_path
|
||||
|
||||
assets_file_path = f"{LIBRARY_BASE_PATH}/{assets_path}"
|
||||
def _build_asset_file_path(
|
||||
self, user: User, folder: str, platform_fs_slug, emulator: str = None
|
||||
):
|
||||
user_folder_path = self.user_folder_path(user)
|
||||
if emulator:
|
||||
return os.path.join(user_folder_path, folder, platform_fs_slug, emulator)
|
||||
return os.path.join(user_folder_path, folder, platform_fs_slug)
|
||||
|
||||
assets: list[str] = []
|
||||
# /users/557365723a31/saves/n64/mupen64plus
|
||||
def build_saves_file_path(
|
||||
self, user: User, platform_fs_slug: str, emulator: str = None
|
||||
):
|
||||
return self._build_asset_file_path(user, "saves", platform_fs_slug, emulator)
|
||||
|
||||
try:
|
||||
emulators = list(os.walk(assets_file_path))[0][1]
|
||||
for emulator in emulators:
|
||||
assets += [
|
||||
(emulator, file)
|
||||
for file in list(os.walk(f"{assets_file_path}/{emulator}"))[0][2]
|
||||
if rom_file_name_no_ext
|
||||
== self.get_file_name_with_no_extension(file)
|
||||
]
|
||||
# /users/557365723a31/states/n64/mupen64plus
|
||||
def build_states_file_path(
|
||||
self, user: User, platform_fs_slug: str, emulator: str = None
|
||||
):
|
||||
return self._build_asset_file_path(user, "states", platform_fs_slug, emulator)
|
||||
|
||||
assets += [
|
||||
(None, file)
|
||||
for file in list(os.walk(assets_file_path))[0][2]
|
||||
if rom_file_name_no_ext == self.get_file_name_with_no_extension(file)
|
||||
]
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
return assets
|
||||
|
||||
@staticmethod
|
||||
def get_screenshots():
|
||||
screenshots_path = f"{LIBRARY_BASE_PATH}/{cm.config.SCREENSHOTS_FOLDER_NAME}"
|
||||
|
||||
fs_screenshots = []
|
||||
|
||||
try:
|
||||
platforms = list(os.walk(screenshots_path))[0][1]
|
||||
for platform in platforms:
|
||||
fs_screenshots += [
|
||||
(platform, file)
|
||||
for file in list(os.walk(f"{screenshots_path}/{platform}"))[0][2]
|
||||
]
|
||||
|
||||
fs_screenshots += [
|
||||
(None, file) for file in list(os.walk(screenshots_path))[0][2]
|
||||
]
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
return fs_screenshots
|
||||
|
||||
@staticmethod
|
||||
def get_asset_size(asset_path: str, file_name: str):
|
||||
return os.stat(f"{LIBRARY_BASE_PATH}/{asset_path}/{file_name}").st_size
|
||||
# /users/557365723a31/screenshots/n64
|
||||
def build_screenshots_file_path(self, user: User, platform_fs_slug: str):
|
||||
return self._build_asset_file_path(user, "screenshots", platform_fs_slug)
|
||||
|
||||
@@ -10,8 +10,7 @@ class FSPlatformsHandler(FSHandler):
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def _exclude_platforms(platforms: list):
|
||||
def _exclude_platforms(self, platforms: list):
|
||||
return [
|
||||
platform
|
||||
for platform in platforms
|
||||
|
||||
@@ -10,13 +10,13 @@ from config import (
|
||||
DEFAULT_PATH_COVER_S,
|
||||
DEFAULT_URL_COVER_L,
|
||||
DEFAULT_URL_COVER_S,
|
||||
RESOURCES_BASE_PATH,
|
||||
)
|
||||
from handler.fs_handler import (
|
||||
DEFAULT_HEIGHT_COVER_L,
|
||||
DEFAULT_HEIGHT_COVER_S,
|
||||
DEFAULT_WIDTH_COVER_L,
|
||||
DEFAULT_WIDTH_COVER_S,
|
||||
RESOURCES_BASE_PATH,
|
||||
CoverSize,
|
||||
FSHandler,
|
||||
)
|
||||
@@ -27,8 +27,7 @@ class FSResourceHandler(FSHandler):
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def _cover_exists(fs_slug: str, rom_name: str, size: CoverSize):
|
||||
def _cover_exists(self, fs_slug: str, rom_name: str, size: CoverSize):
|
||||
"""Check if rom cover exists in filesystem
|
||||
|
||||
Args:
|
||||
@@ -44,8 +43,7 @@ class FSResourceHandler(FSHandler):
|
||||
)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _resize_cover(cover_path: str, size: CoverSize) -> None:
|
||||
def _resize_cover(self, cover_path: str, size: CoverSize) -> None:
|
||||
"""Resizes the cover image to the standard size
|
||||
|
||||
Args:
|
||||
@@ -99,8 +97,7 @@ class FSResourceHandler(FSHandler):
|
||||
shutil.copyfileobj(res.raw, f)
|
||||
self._resize_cover(f"{cover_path}/{cover_file}", size)
|
||||
|
||||
@staticmethod
|
||||
def _get_cover_path(fs_slug: str, rom_name: str, size: CoverSize):
|
||||
def _get_cover_path(self, fs_slug: str, rom_name: str, size: CoverSize):
|
||||
"""Returns rom cover filesystem path adapted to frontend folder structure
|
||||
|
||||
Args:
|
||||
@@ -152,8 +149,7 @@ class FSResourceHandler(FSHandler):
|
||||
if not self._cover_exists("default", "default", cover["size"]):
|
||||
self._store_cover("default", "default", cover["url"], cover["size"])
|
||||
|
||||
@staticmethod
|
||||
def build_artwork_path(rom_name: str, fs_slug: str, file_ext: str):
|
||||
def build_artwork_path(self, rom_name: str, fs_slug: str, file_ext: str):
|
||||
q_rom_name = quote(rom_name)
|
||||
strtime = str(datetime.datetime.now().timestamp())
|
||||
|
||||
@@ -162,9 +158,43 @@ class FSResourceHandler(FSHandler):
|
||||
artwork_path = f"{RESOURCES_BASE_PATH}/{fs_slug}/{rom_name}/cover"
|
||||
Path(artwork_path).mkdir(parents=True, exist_ok=True)
|
||||
return path_cover_l, path_cover_s, artwork_path
|
||||
|
||||
def _store_screenshot(self, fs_slug: str, rom_name: str, url: str, idx: int):
|
||||
"""Store roms resources in filesystem
|
||||
|
||||
@staticmethod
|
||||
def build_avatar_path(avatar_path: str, username: str):
|
||||
avatar_user_path = f"{RESOURCES_BASE_PATH}/users/{username}"
|
||||
Path(avatar_user_path).mkdir(parents=True, exist_ok=True)
|
||||
return f"users/{username}/{avatar_path}", avatar_user_path
|
||||
Args:
|
||||
fs_slug: short name of the platform
|
||||
file_name: name of rom
|
||||
url: url to get the screenshot
|
||||
"""
|
||||
screenshot_file = f"{idx}.jpg"
|
||||
screenshot_path = f"{RESOURCES_BASE_PATH}/{fs_slug}/{rom_name}/screenshots"
|
||||
res = requests.get(url, stream=True, timeout=120)
|
||||
if res.status_code == 200:
|
||||
Path(screenshot_path).mkdir(parents=True, exist_ok=True)
|
||||
with open(f"{screenshot_path}/{screenshot_file}", "wb") as f:
|
||||
shutil.copyfileobj(res.raw, f)
|
||||
|
||||
def _get_screenshot_path(self, fs_slug: str, rom_name: str, idx: str):
|
||||
"""Returns rom cover filesystem path adapted to frontend folder structure
|
||||
|
||||
Args:
|
||||
fs_slug: short name of the platform
|
||||
file_name: name of rom
|
||||
idx: index number of screenshot
|
||||
"""
|
||||
return f"{fs_slug}/{rom_name}/screenshots/{idx}.jpg"
|
||||
|
||||
def get_rom_screenshots(
|
||||
self, platform_fs_slug: str, rom_name: str, url_screenshots: list
|
||||
) -> dict:
|
||||
q_rom_name = quote(rom_name)
|
||||
|
||||
path_screenshots: list[str] = []
|
||||
for idx, url in enumerate(url_screenshots):
|
||||
self._store_screenshot(platform_fs_slug, rom_name, url, idx)
|
||||
path_screenshots.append(
|
||||
self._get_screenshot_path(platform_fs_slug, q_rom_name, str(idx))
|
||||
)
|
||||
|
||||
return {"path_screenshots": path_screenshots}
|
||||
|
||||
@@ -2,6 +2,7 @@ import fnmatch
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
|
||||
from config import LIBRARY_BASE_PATH
|
||||
from config.config_manager import config_manager as cm
|
||||
@@ -21,8 +22,20 @@ class FSRomsHandler(FSHandler):
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def parse_tags(file_name: str) -> tuple:
|
||||
def get_fs_structure(self, fs_slug: str):
|
||||
return (
|
||||
f"{cm.config.ROMS_FOLDER_NAME}/{fs_slug}"
|
||||
if os.path.exists(cm.config.HIGH_PRIO_STRUCTURE_PATH)
|
||||
else f"{fs_slug}/{cm.config.ROMS_FOLDER_NAME}"
|
||||
)
|
||||
|
||||
def remove_file(self, file_name: str, file_path: str):
|
||||
try:
|
||||
os.remove(f"{LIBRARY_BASE_PATH}/{file_path}/{file_name}")
|
||||
except IsADirectoryError:
|
||||
shutil.rmtree(f"{LIBRARY_BASE_PATH}/{file_path}/{file_name}")
|
||||
|
||||
def parse_tags(self, file_name: str) -> tuple:
|
||||
rev = ""
|
||||
regs = []
|
||||
langs = []
|
||||
@@ -91,8 +104,7 @@ class FSRomsHandler(FSHandler):
|
||||
# Return files that are not in the filtered list.
|
||||
return [f for f in files if f not in excluded_files]
|
||||
|
||||
@staticmethod
|
||||
def _exclude_multi_roms(roms) -> list[str]:
|
||||
def _exclude_multi_roms(self, roms) -> list[str]:
|
||||
excluded_names = cm.config.EXCLUDED_MULTI_FILES
|
||||
filtered_files: list = []
|
||||
|
||||
@@ -148,9 +160,8 @@ class FSRomsHandler(FSHandler):
|
||||
for rom in fs_roms
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def get_rom_file_size(
|
||||
roms_path: str, file_name: str, multi: bool, multi_files: list = []
|
||||
self, roms_path: str, file_name: str, multi: bool, multi_files: list = []
|
||||
):
|
||||
files = (
|
||||
[f"{LIBRARY_BASE_PATH}/{roms_path}/{file_name}"]
|
||||
@@ -162,8 +173,7 @@ class FSRomsHandler(FSHandler):
|
||||
)
|
||||
return sum([os.stat(file).st_size for file in files])
|
||||
|
||||
@staticmethod
|
||||
def file_exists(path: str, file_name: str):
|
||||
def file_exists(self, path: str, file_name: str):
|
||||
"""Check if file exists in filesystem
|
||||
|
||||
Args:
|
||||
@@ -183,3 +193,7 @@ class FSRomsHandler(FSHandler):
|
||||
f"{LIBRARY_BASE_PATH}/{file_path}/{old_name}",
|
||||
f"{LIBRARY_BASE_PATH}/{file_path}/{new_name}",
|
||||
)
|
||||
|
||||
def build_upload_file_path(self, fs_slug: str):
|
||||
file_path = self.get_fs_structure(fs_slug)
|
||||
return f"{LIBRARY_BASE_PATH}/{file_path}"
|
||||
|
||||
@@ -11,8 +11,7 @@ class GHHandler:
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def get_version() -> str:
|
||||
def get_version(self) -> str:
|
||||
"""Returns current version or branch name."""
|
||||
if not __version__ == "<version>":
|
||||
return __version__
|
||||
|
||||
@@ -82,8 +82,7 @@ class IGDBHandler:
|
||||
|
||||
return wrapper
|
||||
|
||||
@staticmethod
|
||||
def normalize_search_term(search_term: str) -> str:
|
||||
def _normalize_search_term(self, search_term: str) -> str:
|
||||
return (
|
||||
search_term.replace("\u2122", "") # Remove trademark symbol
|
||||
.replace("\u00ae", "") # Remove registered symbol
|
||||
@@ -143,8 +142,7 @@ class IGDBHandler:
|
||||
|
||||
return pydash.get(exact_matches or roms, "[0]", {})
|
||||
|
||||
@staticmethod
|
||||
def _normalize_cover_url(url: str) -> str:
|
||||
def _normalize_cover_url(self, url: str) -> str:
|
||||
return f"https:{url.replace('https:', '')}"
|
||||
|
||||
def _search_cover(self, rom_id: int) -> str:
|
||||
@@ -172,8 +170,7 @@ class IGDBHandler:
|
||||
if "url" in r.keys()
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
async def _ps2_opl_format(match: re.Match[str], search_term: str) -> str:
|
||||
async def _ps2_opl_format(self, match: re.Match[str], search_term: str) -> str:
|
||||
serial_code = match.group(1)
|
||||
|
||||
with open(PS2_OPL_INDEX_FILE, "r") as index_json:
|
||||
@@ -184,8 +181,7 @@ class IGDBHandler:
|
||||
|
||||
return search_term
|
||||
|
||||
@staticmethod
|
||||
async def _switch_titledb_format(match: re.Match[str], search_term: str) -> str:
|
||||
async def _switch_titledb_format(self, match: re.Match[str], search_term: str) -> str:
|
||||
titledb_index = {}
|
||||
title_id = match.group(1)
|
||||
|
||||
@@ -207,8 +203,7 @@ class IGDBHandler:
|
||||
|
||||
return search_term
|
||||
|
||||
@staticmethod
|
||||
async def _switch_productid_format(match: re.Match[str], search_term: str) -> str:
|
||||
async def _switch_productid_format(self, match: re.Match[str], search_term: str) -> str:
|
||||
product_id_index = {}
|
||||
product_id = match.group(1)
|
||||
|
||||
@@ -317,7 +312,7 @@ class IGDBHandler:
|
||||
if platform_idgb_id in ARCADE_IGDB_IDS:
|
||||
search_term = await self._mame_format(search_term)
|
||||
|
||||
search_term = self.normalize_search_term(search_term)
|
||||
search_term = self._normalize_search_term(search_term)
|
||||
|
||||
res = (
|
||||
self._search_rom(uc(search_term), platform_idgb_id, MAIN_GAME_CATEGORY)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
import emoji
|
||||
@@ -14,6 +13,7 @@ from logger.logger import log
|
||||
from models.assets import Save, Screenshot, State
|
||||
from models.platform import Platform
|
||||
from models.rom import Rom
|
||||
from models.user import User
|
||||
|
||||
SWAPPED_PLATFORM_BINDINGS = dict((v, k) for k, v in cm.config.PLATFORMS_BINDING.items())
|
||||
|
||||
@@ -160,7 +160,7 @@ async def scan_rom(
|
||||
)
|
||||
)
|
||||
rom_attrs.update(
|
||||
fs_asset_handler.get_rom_screenshots(
|
||||
fs_resource_handler.get_rom_screenshots(
|
||||
platform_fs_slug=platform.slug,
|
||||
rom_name=rom_attrs["name"],
|
||||
url_screenshots=rom_attrs["url_screenshots"],
|
||||
@@ -185,34 +185,28 @@ def _scan_asset(file_name: str, path: str):
|
||||
}
|
||||
|
||||
|
||||
def build_asset_file_path(fs_slug: str, folder: str, emulator: str = None):
|
||||
saves_path = fs_asset_handler.get_fs_structure(fs_slug, folder=folder)
|
||||
|
||||
if emulator:
|
||||
return os.path.join(saves_path, emulator)
|
||||
|
||||
return saves_path
|
||||
|
||||
|
||||
def scan_save(file_name: str, platform_slug: str, emulator: str = None) -> Save:
|
||||
saves_path = build_asset_file_path(
|
||||
platform_slug, folder=cm.config.SAVES_FOLDER_NAME, emulator=emulator
|
||||
def scan_save(
|
||||
file_name: str, user: User, platform_fs_slug: str, emulator: str = None
|
||||
) -> Save:
|
||||
saves_path = fs_asset_handler.build_saves_file_path(
|
||||
user=user, platform_fs_slug=platform_fs_slug, emulator=emulator
|
||||
)
|
||||
return Save(**_scan_asset(file_name, saves_path))
|
||||
|
||||
|
||||
def scan_state(file_name: str, platform_slug: str, emulator: str = None) -> State:
|
||||
states_path = build_asset_file_path(
|
||||
platform_slug, folder=cm.config.STATES_FOLDER_NAME, emulator=emulator
|
||||
def scan_state(
|
||||
file_name: str, user: User, platform_fs_slug: str, emulator: str = None
|
||||
) -> State:
|
||||
states_path = fs_asset_handler.build_states_file_path(
|
||||
user=user, platform_fs_slug=platform_fs_slug, emulator=emulator
|
||||
)
|
||||
return State(**_scan_asset(file_name, states_path))
|
||||
|
||||
|
||||
def scan_screenshot(file_name: str, platform_slug: str = None) -> Screenshot:
|
||||
if not platform_slug:
|
||||
return Screenshot(**_scan_asset(file_name, cm.config.SCREENSHOTS_FOLDER_NAME))
|
||||
|
||||
screenshots_path = build_asset_file_path(
|
||||
platform_slug, folder=cm.config.SCREENSHOTS_FOLDER_NAME
|
||||
def scan_screenshot(
|
||||
file_name: str, user: User, platform_fs_slug: str = None
|
||||
) -> Screenshot:
|
||||
screenshots_path = fs_asset_handler.build_screenshots_file_path(
|
||||
user=user, platform_fs_slug=platform_fs_slug
|
||||
)
|
||||
return Screenshot(**_scan_asset(file_name, screenshots_path))
|
||||
|
||||
@@ -27,6 +27,9 @@ class BaseAsset(BaseModel):
|
||||
rom_id = Column(
|
||||
Integer(), ForeignKey("roms.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
user_id = Column(
|
||||
Integer(), ForeignKey("users.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def full_path(self) -> str:
|
||||
@@ -34,7 +37,7 @@ class BaseAsset(BaseModel):
|
||||
|
||||
@cached_property
|
||||
def download_path(self) -> str:
|
||||
return f"/api/raw/{self.full_path}?timestamp={self.updated_at}"
|
||||
return f"/api/raw/assets/{self.full_path}?timestamp={self.updated_at}"
|
||||
|
||||
|
||||
class Save(BaseAsset):
|
||||
@@ -44,6 +47,7 @@ class Save(BaseAsset):
|
||||
emulator = Column(String(length=50), nullable=True)
|
||||
|
||||
rom = relationship("Rom", lazy="selectin", back_populates="saves")
|
||||
user = relationship("User", lazy="selectin", back_populates="saves")
|
||||
|
||||
@cached_property
|
||||
def screenshot(self) -> Optional["Screenshot"]:
|
||||
@@ -64,6 +68,7 @@ class State(BaseAsset):
|
||||
emulator = Column(String(length=50), nullable=True)
|
||||
|
||||
rom = relationship("Rom", lazy="selectin", back_populates="states")
|
||||
user = relationship("User", lazy="selectin", back_populates="states")
|
||||
|
||||
@cached_property
|
||||
def screenshot(self) -> Optional["Screenshot"]:
|
||||
@@ -82,3 +87,4 @@ class Screenshot(BaseAsset):
|
||||
__table_args__ = {"extend_existing": True}
|
||||
|
||||
rom = relationship("Rom", lazy="selectin", back_populates="screenshots")
|
||||
user = relationship("User", lazy="selectin", back_populates="screenshots")
|
||||
|
||||
@@ -8,7 +8,16 @@ from config import (
|
||||
)
|
||||
from models.assets import Save, Screenshot, State
|
||||
from models.base import BaseModel
|
||||
from sqlalchemy import JSON, Boolean, Column, ForeignKey, Integer, String, Text, BigInteger
|
||||
from sqlalchemy import (
|
||||
JSON,
|
||||
Boolean,
|
||||
Column,
|
||||
ForeignKey,
|
||||
Integer,
|
||||
String,
|
||||
Text,
|
||||
BigInteger,
|
||||
)
|
||||
from sqlalchemy.orm import Mapped, relationship
|
||||
|
||||
SORT_COMPARE_REGEX = r"^([Tt]he|[Aa]|[Aa]nd)\s"
|
||||
@@ -86,7 +95,7 @@ class Rom(BaseModel):
|
||||
|
||||
@cached_property
|
||||
def download_path(self) -> str:
|
||||
return f"/api/raw/{self.full_path}"
|
||||
return f"/api/raw/roms/{self.full_path}"
|
||||
|
||||
@cached_property
|
||||
def has_cover(self) -> bool:
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import enum
|
||||
|
||||
from models.base import BaseModel
|
||||
from models.assets import Save, Screenshot, State
|
||||
from sqlalchemy import Boolean, Column, Enum, Integer, String
|
||||
from starlette.authentication import SimpleUser
|
||||
from sqlalchemy.orm import Mapped, relationship
|
||||
|
||||
|
||||
class Role(enum.Enum):
|
||||
@@ -23,6 +25,18 @@ class User(BaseModel, SimpleUser):
|
||||
role: Role = Column(Enum(Role), default=Role.VIEWER)
|
||||
avatar_path: str = Column(String(length=255), default="")
|
||||
|
||||
saves: Mapped[list[Save]] = relationship(
|
||||
"Save",
|
||||
lazy="selectin",
|
||||
back_populates="user",
|
||||
)
|
||||
states: Mapped[list[State]] = relationship(
|
||||
"State", lazy="selectin", back_populates="user"
|
||||
)
|
||||
screenshots: Mapped[list[Screenshot]] = relationship(
|
||||
"Screenshot", lazy="selectin", back_populates="user"
|
||||
)
|
||||
|
||||
@property
|
||||
def oauth_scopes(self):
|
||||
from handler.auth_handler import DEFAULT_SCOPES, FULL_SCOPES, WRITE_SCOPES
|
||||
@@ -34,3 +48,8 @@ class User(BaseModel, SimpleUser):
|
||||
return WRITE_SCOPES
|
||||
|
||||
return DEFAULT_SCOPES
|
||||
|
||||
@property
|
||||
def fs_safe_folder_name(self):
|
||||
# Uses the ID to avoid issues with username changes
|
||||
return f'User:{self.id}'.encode("utf-8").hex()
|
||||
|
||||
@@ -14,6 +14,7 @@ COPY ./frontend/assets/platforms ${WEBSERVER_FOLDER}/assets/platforms
|
||||
COPY ./frontend/assets/webrcade/feed ${WEBSERVER_FOLDER}/assets/webrcade/feed
|
||||
RUN mkdir -p ${WEBSERVER_FOLDER}/assets/romm && \
|
||||
ln -s /romm/resources ${WEBSERVER_FOLDER}/assets/romm/resources
|
||||
ln -s /romm/assets ${WEBSERVER_FOLDER}/assets/romm/assets
|
||||
|
||||
# install generall required packages
|
||||
RUN apk add --upgrade \
|
||||
|
||||
@@ -43,6 +43,3 @@ system:
|
||||
|
||||
filesystem:
|
||||
roms_folder: 'roms' # The folder where your roms are located
|
||||
saves_folder: 'saves' # The folder where your save files are located
|
||||
states_folder: 'states' # The folder where your states are located
|
||||
screenshots_folder: 'screenshots' # The folder where your screenshots are located
|
||||
|
||||
@@ -40,7 +40,8 @@ services:
|
||||
- SCHEDULED_UPDATE_MAME_XML_CRON=0 5 * * * # Cron expression for the scheduled update (default: 0 5 * * * - At 5:00 AM every day)
|
||||
volumes:
|
||||
- "/path/to/library:/romm/library"
|
||||
- "/path/to/resources:/romm/resources" # [Optional] Path where roms metadata (covers) are stored
|
||||
- "/path/to/assets:/romm/assets" # Path where saves, states and other assets are stored
|
||||
- "/path/to/resources:/romm/resources" # Path where roms metadata (covers) are stored
|
||||
- "/path/to/config:/romm/config" # [Optional] Path where config is stored
|
||||
- "/path/to/database:/romm/database" # [Optional] Only needed if ROMM_DB_DRIVER=sqlite or not set
|
||||
- "/path/to/logs:/romm/logs" # [Optional] Path where logs are stored
|
||||
|
||||
@@ -13,9 +13,6 @@ export type ConfigResponse = {
|
||||
PLATFORMS_BINDING: Record<string, string>;
|
||||
PLATFORMS_VERSIONS: Record<string, string>;
|
||||
ROMS_FOLDER_NAME: string;
|
||||
SAVES_FOLDER_NAME: string;
|
||||
STATES_FOLDER_NAME: string;
|
||||
SCREENSHOTS_FOLDER_NAME: string;
|
||||
HIGH_PRIO_STRUCTURE_PATH: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -41,7 +41,6 @@ export type EnhancedRomSchema = {
|
||||
url_screenshots: Array<string>;
|
||||
merged_screenshots: Array<string>;
|
||||
full_path: string;
|
||||
download_path: string;
|
||||
sibling_roms: Array<RomSchema>;
|
||||
};
|
||||
|
||||
|
||||
1
frontend/src/__generated__/models/RomSchema.ts
generated
1
frontend/src/__generated__/models/RomSchema.ts
generated
@@ -40,6 +40,5 @@ export type RomSchema = {
|
||||
url_screenshots: Array<string>;
|
||||
merged_screenshots: Array<string>;
|
||||
full_path: string;
|
||||
download_path: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ function closeDialog() {
|
||||
<v-img
|
||||
:src="
|
||||
user.avatar_path
|
||||
? `/assets/romm/resources/${user.avatar_path}`
|
||||
? `/assets/romm/assets/${user.avatar_path}`
|
||||
: defaultAvatarPath
|
||||
"
|
||||
/>
|
||||
|
||||
@@ -61,7 +61,7 @@ async function logout() {
|
||||
<v-img
|
||||
:src="
|
||||
auth.user?.avatar_path
|
||||
? `/assets/romm/resources/${auth.user?.avatar_path}`
|
||||
? `/assets/romm/assets/${auth.user?.avatar_path}`
|
||||
: defaultAvatarPath
|
||||
"
|
||||
/>
|
||||
|
||||
@@ -123,7 +123,7 @@ onMounted(() => {
|
||||
<v-img
|
||||
:src="
|
||||
item.raw.avatar_path
|
||||
? `/assets/romm/resources/${item.raw.avatar_path}`
|
||||
? `/assets/romm/assets/${item.raw.avatar_path}`
|
||||
: defaultAvatarPath
|
||||
"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user