diff --git a/backend/alembic/versions/0042_add_missing_rom_flag.py b/backend/alembic/versions/0042_add_missing_rom_flag.py new file mode 100644 index 000000000..5708cd3b6 --- /dev/null +++ b/backend/alembic/versions/0042_add_missing_rom_flag.py @@ -0,0 +1,36 @@ +"""empty message + +Revision ID: 0042_add_missing_fields +Revises: 0041_assets_t_thumb_cleanup +Create Date: 2025-06-11 + +""" + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "0042_add_missing_fields" +down_revision = "0041_assets_t_thumb_cleanup" +branch_labels = None +depends_on = None + + +def upgrade(): + with op.batch_alter_table("platforms", schema=None) as batch_op: + batch_op.add_column( + sa.Column("missing", sa.Boolean(), nullable=False, server_default="0") + ) + + with op.batch_alter_table("roms", schema=None) as batch_op: + batch_op.add_column( + sa.Column("missing", sa.Boolean(), nullable=False, server_default="0") + ) + + +def downgrade(): + with op.batch_alter_table("platforms", schema=None) as batch_op: + batch_op.drop_column("missing") + + with op.batch_alter_table("roms", schema=None) as batch_op: + batch_op.drop_column("missing") diff --git a/backend/endpoints/responses/platform.py b/backend/endpoints/responses/platform.py index 4c07d3301..4404a0ba5 100644 --- a/backend/endpoints/responses/platform.py +++ b/backend/endpoints/responses/platform.py @@ -32,6 +32,8 @@ class PlatformSchema(BaseModel): updated_at: datetime fs_size_bytes: int + missing: bool + class Config: from_attributes = True diff --git a/backend/endpoints/responses/rom.py b/backend/endpoints/responses/rom.py index 754706546..e8f7f6b58 100644 --- a/backend/endpoints/responses/rom.py +++ b/backend/endpoints/responses/rom.py @@ -225,6 +225,8 @@ class RomSchema(BaseModel): created_at: datetime updated_at: datetime + missing: bool + class Config: from_attributes = True diff --git a/backend/endpoints/sockets/scan.py b/backend/endpoints/sockets/scan.py index c3a3efb60..e90c709d8 100644 --- a/backend/endpoints/sockets/scan.py +++ b/backend/endpoints/sockets/scan.py @@ -210,9 +210,9 @@ async def scan_platforms( # This protects against accidental deletion of entries when # the folder structure is not correct or the drive is not mounted if len(fs_platforms) > 0: - purged_platforms = db_platform_handler.purge_platforms(fs_platforms) + purged_platforms = db_platform_handler.mark_missing_platforms(fs_platforms) if len(purged_platforms) > 0: - log.warning("Purging platforms not found in the filesystem:") + log.warning("Missing platforms not found in the filesystem:") for p in purged_platforms: log.warning(f" - {p.slug}") @@ -330,12 +330,12 @@ async def _identify_platform( # This protects against accidental deletion of entries when # the folder structure is not correct or the drive is not mounted if len(fs_roms) > 0: - purged_roms = db_rom_handler.purge_roms( + missing_roms = db_rom_handler.mark_missing_roms( platform.id, [rom["fs_name"] for rom in fs_roms] ) - if len(purged_roms) > 0: - log.warning("Purging roms not found in the filesystem:") - for r in purged_roms: + if len(missing_roms) > 0: + log.warning("Missing roms not found in the filesystem:") + for r in missing_roms: log.warning(f" - {r.fs_name}") # Same protection for firmware diff --git a/backend/handler/database/platforms_handler.py b/backend/handler/database/platforms_handler.py index 29babe6a0..05d1c3988 100644 --- a/backend/handler/database/platforms_handler.py +++ b/backend/handler/database/platforms_handler.py @@ -4,7 +4,7 @@ from typing import Sequence from decorators.database import begin_session from models.platform import Platform from models.rom import Rom -from sqlalchemy import delete, or_, select +from sqlalchemy import delete, or_, select, update from sqlalchemy.orm import Query, Session, selectinload from .base_handler import DBBaseHandler @@ -72,13 +72,13 @@ class DBPlatformsHandler(DBBaseHandler): ) @begin_session - def purge_platforms( + def mark_missing_platforms( self, fs_platforms_to_keep: list[str], query: Query = None, session: Session = None, ) -> Sequence[Platform]: - purged_platforms = ( + missing_platforms = ( session.scalars( select(Platform) .order_by(Platform.name.asc()) @@ -93,8 +93,9 @@ class DBPlatformsHandler(DBBaseHandler): .all() ) session.execute( - delete(Platform) + update(Platform) .where(or_(Platform.fs_slug.not_in(fs_platforms_to_keep), Platform.slug.is_(None))) # type: ignore[attr-defined] + .values(**{"missing": True}) .execution_options(synchronize_session="fetch") ) - return purged_platforms + return missing_platforms diff --git a/backend/handler/database/roms_handler.py b/backend/handler/database/roms_handler.py index 96a0cfca8..59ad097af 100644 --- a/backend/handler/database/roms_handler.py +++ b/backend/handler/database/roms_handler.py @@ -674,10 +674,10 @@ class DBRomsHandler(DBBaseHandler): ) @begin_session - def purge_roms( + def mark_missing_roms( self, platform_id: int, fs_roms_to_keep: list[str], session: Session = None ) -> Sequence[Rom]: - purged_roms = ( + missing_roms = ( session.scalars( select(Rom) .order_by(Rom.fs_name.asc()) @@ -692,15 +692,16 @@ class DBRomsHandler(DBBaseHandler): .all() ) session.execute( - delete(Rom) + update(Rom) .where( and_( Rom.platform_id == platform_id, Rom.fs_name.not_in(fs_roms_to_keep) ) ) + .values(**{"missing": True}) .execution_options(synchronize_session="evaluate") ) - return purged_roms + return missing_roms @begin_session def add_rom_user( diff --git a/backend/handler/scan_handler.py b/backend/handler/scan_handler.py index 14758a9ac..8ab78f23c 100644 --- a/backend/handler/scan_handler.py +++ b/backend/handler/scan_handler.py @@ -175,6 +175,7 @@ async def scan_platform( extra=LOGGER_MODULE_NAME, ) + platform_attrs["missing"] = False return Platform(**platform_attrs) @@ -397,6 +398,7 @@ async def scan_rom( extra=LOGGER_MODULE_NAME, ) + romm_attrs["missing"] = False return Rom(**rom_attrs) diff --git a/backend/handler/tests/test_db_handler.py b/backend/handler/tests/test_db_handler.py index bae77ffec..eb06220b0 100644 --- a/backend/handler/tests/test_db_handler.py +++ b/backend/handler/tests/test_db_handler.py @@ -27,7 +27,7 @@ def test_platforms(): assert platform is not None assert platform.name == "test_platform" - db_platform_handler.purge_platforms([]) + db_platform_handler.mark_missing_platforms([]) platforms = db_platform_handler.get_platforms() assert len(platforms) == 0 @@ -63,7 +63,7 @@ def test_roms(rom: Rom, platform: Platform): roms = db_rom_handler.get_roms_scalar(platform_id=platform.id) assert len(roms) == 1 - db_rom_handler.purge_roms(rom_2.platform_id, []) + db_rom_handler.mark_missing_roms(rom_2.platform_id, []) roms = db_rom_handler.get_roms_scalar(platform_id=platform.id) assert len(roms) == 0 diff --git a/backend/models/platform.py b/backend/models/platform.py index 535bf9bf2..cdfd463a1 100644 --- a/backend/models/platform.py +++ b/backend/models/platform.py @@ -50,11 +50,13 @@ class Platform(BaseModel): select(func.count(Rom.id)).where(Rom.platform_id == id).scalar_subquery() ) - def __repr__(self) -> str: - return self.name + missing: Mapped[bool] = mapped_column(default=False, nullable=False) @cached_property def fs_size_bytes(self) -> int: from handler.database import db_stats_handler return db_stats_handler.get_platform_filesize(self.id) + + def __repr__(self) -> str: + return self.name diff --git a/backend/models/rom.py b/backend/models/rom.py index 4c1670724..8fb4be215 100644 --- a/backend/models/rom.py +++ b/backend/models/rom.py @@ -203,6 +203,8 @@ class Rom(BaseModel): back_populates="roms", ) + missing: Mapped[bool] = mapped_column(default=False, nullable=False) + @property def platform_slug(self) -> str: return self.platform.slug diff --git a/frontend/src/__generated__/models/DetailedRomSchema.ts b/frontend/src/__generated__/models/DetailedRomSchema.ts index 0a31860b9..0b99cf7fb 100644 --- a/frontend/src/__generated__/models/DetailedRomSchema.ts +++ b/frontend/src/__generated__/models/DetailedRomSchema.ts @@ -62,6 +62,7 @@ export type DetailedRomSchema = { full_path: string; created_at: string; updated_at: string; + missing: boolean; merged_ra_metadata: (RomRAMetadata | null); merged_screenshots: Array; siblings: Array; diff --git a/frontend/src/__generated__/models/PlatformSchema.ts b/frontend/src/__generated__/models/PlatformSchema.ts index 86120701e..736986374 100644 --- a/frontend/src/__generated__/models/PlatformSchema.ts +++ b/frontend/src/__generated__/models/PlatformSchema.ts @@ -27,6 +27,7 @@ export type PlatformSchema = { created_at: string; updated_at: string; fs_size_bytes: number; + missing: boolean; readonly display_name: string; }; diff --git a/frontend/src/__generated__/models/SimpleRomSchema.ts b/frontend/src/__generated__/models/SimpleRomSchema.ts index da57242e9..064ad88f3 100644 --- a/frontend/src/__generated__/models/SimpleRomSchema.ts +++ b/frontend/src/__generated__/models/SimpleRomSchema.ts @@ -56,6 +56,7 @@ export type SimpleRomSchema = { full_path: string; created_at: string; updated_at: string; + missing: boolean; siblings: Array; rom_user: RomUserSchema; }; diff --git a/frontend/src/components/common/Game/Card/Base.vue b/frontend/src/components/common/Game/Card/Base.vue index 69fc572d7..580b2c987 100644 --- a/frontend/src/components/common/Game/Card/Base.vue +++ b/frontend/src/components/common/Game/Card/Base.vue @@ -238,6 +238,26 @@ onBeforeUnmount(() => { + + - -