diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index ba998fdde..266c878a1 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -62,7 +62,7 @@ jobs: uv run pytest -vv --maxfail=10 --junitxml=pytest-report.xml --cov --cov-report xml:coverage.xml --cov-config=.coveragerc . - name: Publish test results - uses: EnricoMi/publish-unit-test-result-action/linux@sha-3a74b29 + uses: EnricoMi/publish-unit-test-result-action/linux@v2.20.0 if: (!cancelled()) with: files: | diff --git a/backend/endpoints/responses/rom.py b/backend/endpoints/responses/rom.py index 0898550c6..e0b6eb48c 100644 --- a/backend/endpoints/responses/rom.py +++ b/backend/endpoints/responses/rom.py @@ -242,6 +242,7 @@ class RomSchema(BaseModel): path_manual: str | None url_manual: str | None + is_identifying: bool = False is_unidentified: bool is_identified: bool diff --git a/backend/endpoints/sockets/scan.py b/backend/endpoints/sockets/scan.py index 4b12c7027..da39b3866 100644 --- a/backend/endpoints/sockets/scan.py +++ b/backend/endpoints/sockets/scan.py @@ -234,6 +234,7 @@ async def _identify_rom( fs_rom=fs_rom, metadata_sources=metadata_sources, newly_added=newly_added, + socket_manager=socket_manager, ) scan_stats.scanned_roms += 1 @@ -242,6 +243,14 @@ async def _identify_rom( _added_rom = db_rom_handler.add_rom(scanned_rom) + if _added_rom.is_identified: + await socket_manager.emit( + "scan:scanning_rom", + SimpleRomSchema.from_orm_with_factory(_added_rom).model_dump( + exclude={"created_at", "updated_at", "rom_user"} + ), + ) + # Delete the existing rom files in the DB db_rom_handler.purge_rom_files(_added_rom.id) @@ -316,14 +325,9 @@ async def _identify_rom( await socket_manager.emit( "scan:scanning_rom", - { - "platform_name": platform.name, - "platform_slug": platform.slug, - "platform_fs_slug": platform.fs_slug, - **SimpleRomSchema.from_orm_with_factory(_added_rom).model_dump( - exclude={"created_at", "updated_at", "rom_user"} - ), - }, + SimpleRomSchema.from_orm_with_factory(_added_rom).model_dump( + exclude={"created_at", "updated_at", "rom_user"} + ), ) await socket_manager.emit("", None) @@ -460,25 +464,25 @@ async def scan_platforms( if not roms_ids: roms_ids = [] - sm = _get_socket_manager() + socket_manager = _get_socket_manager() if not metadata_sources: log.error("No metadata sources provided") - await sm.emit("scan:done_ko", "No metadata sources provided") + await socket_manager.emit("scan:done_ko", "No metadata sources provided") return None try: fs_platforms: list[str] = await fs_platform_handler.get_platforms() except FolderStructureNotMatchException as e: log.error(e) - await sm.emit("scan:done_ko", e.message) + await socket_manager.emit("scan:done_ko", e.message) return None scan_stats = ScanStats() async def stop_scan(): log.info(f"{emoji.EMOJI_STOP_SIGN} Scan stopped manually") - await sm.emit("scan:done", scan_stats.__dict__) + await socket_manager.emit("scan:done", scan_stats.__dict__) redis_client.delete(STOP_SCAN_FLAG) try: @@ -506,7 +510,7 @@ async def scan_platforms( fs_platforms=fs_platforms, roms_ids=roms_ids, metadata_sources=metadata_sources, - socket_manager=sm, + socket_manager=socket_manager, ) missed_platforms = db_platform_handler.mark_missing_platforms(fs_platforms) @@ -516,13 +520,13 @@ async def scan_platforms( log.warning(f" - {p.slug} ({p.fs_slug})") log.info(f"{emoji.EMOJI_CHECK_MARK} Scan completed") - await sm.emit("scan:done", scan_stats.__dict__) + await socket_manager.emit("scan:done", scan_stats.__dict__) except ScanStoppedException: await stop_scan() except Exception as e: log.error(f"Error in scan_platform: {e}") # Catch all exceptions and emit error to the client - await sm.emit("scan:done_ko", str(e)) + await socket_manager.emit("scan:done_ko", str(e)) # Re-raise the exception to be caught by the error handler raise e diff --git a/backend/handler/scan_handler.py b/backend/handler/scan_handler.py index c871809d8..20aec585f 100644 --- a/backend/handler/scan_handler.py +++ b/backend/handler/scan_handler.py @@ -2,8 +2,11 @@ import asyncio import enum from typing import Any +import socketio # type: ignore + from config.config_manager import config_manager as cm -from handler.database import db_platform_handler +from endpoints.responses.rom import SimpleRomSchema +from handler.database import db_platform_handler, db_rom_handler from handler.filesystem import fs_asset_handler, fs_firmware_handler from handler.filesystem.roms_handler import FSRom from handler.metadata import ( @@ -281,6 +284,7 @@ async def scan_rom( fs_rom: FSRom, metadata_sources: list[str], newly_added: bool, + socket_manager: socketio.AsyncRedisManager | None = None, ) -> Rom: if not metadata_sources: log.error("No metadata sources provided") @@ -384,6 +388,19 @@ async def scan_rom( return HasheousRom(hasheous_id=None, igdb_id=None, tgdb_id=None, ra_id=None) + _added_rom = db_rom_handler.add_rom(Rom(**rom_attrs)) + _added_rom.is_identifying = True + + if socket_manager: + await socket_manager.emit( + "scan:scanning_rom", + { + **SimpleRomSchema.from_orm_with_factory(_added_rom).model_dump( + exclude={"created_at", "updated_at", "rom_user"} + ), + }, + ) + # Run hash fetches concurrently ( playmatch_hash_match, diff --git a/backend/models/rom.py b/backend/models/rom.py index 7f1954ac1..009332c30 100644 --- a/backend/models/rom.py +++ b/backend/models/rom.py @@ -247,6 +247,10 @@ class Rom(BaseModel): back_populates="roms", ) + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self._is_identifying = False + @property def platform_slug(self) -> str: return self.platform.slug @@ -369,6 +373,15 @@ class Rom(BaseModel): ) return self.ra_metadata + # Used only during scan process + @property + def is_identifying(self) -> bool: + return self._is_identifying or False + + @is_identifying.setter + def is_identifying(self, value: bool) -> None: + self._is_identifying = value + def __repr__(self) -> str: return self.fs_name diff --git a/backend/tests/handler/test_fastapi.py b/backend/tests/handler/test_fastapi.py index e6e91594a..3926329de 100644 --- a/backend/tests/handler/test_fastapi.py +++ b/backend/tests/handler/test_fastapi.py @@ -1,5 +1,6 @@ import pytest +from handler.database import db_platform_handler from handler.scan_handler import MetadataSource, ScanType, scan_platform, scan_rom from models.platform import Platform from models.rom import Rom, RomFile @@ -28,9 +29,15 @@ async def test_scan_platform(): @pytest.mark.vcr async def test_scan_rom(): - platform = Platform(fs_slug="n64", igdb_id=4) + platform = Platform(id=1, slug="n64", fs_slug="n64", name="Nintendo 64", igdb_id=4) + platform = db_platform_handler.add_platform(platform) + rom = Rom( fs_name="Paper Mario (USA).z64", + fs_name_no_tags="Paper Mario", + fs_name_no_ext="Paper Mario", + fs_extension="z64", + fs_path="n64/Paper Mario (USA)", name="Paper Mario", igdb_id=3340, fs_size_bytes=1024, diff --git a/frontend/src/__generated__/models/DetailedRomSchema.ts b/frontend/src/__generated__/models/DetailedRomSchema.ts index 302b792ce..1365734dc 100644 --- a/frontend/src/__generated__/models/DetailedRomSchema.ts +++ b/frontend/src/__generated__/models/DetailedRomSchema.ts @@ -62,6 +62,7 @@ export type DetailedRomSchema = { has_manual: boolean; path_manual: (string | null); url_manual: (string | null); + is_identifying?: boolean; is_unidentified: boolean; is_identified: boolean; revision: (string | null); diff --git a/frontend/src/__generated__/models/SimpleRomSchema.ts b/frontend/src/__generated__/models/SimpleRomSchema.ts index 2da97adcb..d41e89c58 100644 --- a/frontend/src/__generated__/models/SimpleRomSchema.ts +++ b/frontend/src/__generated__/models/SimpleRomSchema.ts @@ -56,6 +56,7 @@ export type SimpleRomSchema = { has_manual: boolean; path_manual: (string | null); url_manual: (string | null); + is_identifying?: boolean; is_unidentified: boolean; is_identified: boolean; revision: (string | null); diff --git a/frontend/src/components/common/Navigation/ScanBtn.vue b/frontend/src/components/common/Navigation/ScanBtn.vue index 47de62173..209f6b664 100644 --- a/frontend/src/components/common/Navigation/ScanBtn.vue +++ b/frontend/src/components/common/Navigation/ScanBtn.vue @@ -57,9 +57,18 @@ socket.on( socket.on("scan:scanning_rom", (rom: SimpleRom) => { scanningStore.set(true); + + // Remove the ROM from the recent list and add it back to the top + romsStore.removeFromRecent(rom); romsStore.addToRecent(rom); + if (romsStore.currentPlatform?.id === rom.platform_id) { - romsStore.add([rom]); + const existingRom = romsStore.allRoms.find((r) => r.id === rom.id); + if (existingRom) { + romsStore.update(rom); + } else { + romsStore.add([rom]); + } } let scannedPlatform = scanningPlatforms.value.find( @@ -78,7 +87,15 @@ socket.on("scan:scanning_rom", (rom: SimpleRom) => { scannedPlatform = scanningPlatforms.value[0]; } - scannedPlatform?.roms.push(rom); + // Check if ROM already exists in the store + const existingRom = scannedPlatform?.roms.find((r) => r.id === rom.id); + if (existingRom) { + scannedPlatform.roms = scannedPlatform.roms.map((r) => + r.id === rom.id ? rom : r, + ); + } else { + scannedPlatform?.roms.push(rom); + } }); socket.on("scan:done", () => { diff --git a/frontend/src/views/Scan.vue b/frontend/src/views/Scan.vue index 26bc96d4a..66ff63e48 100644 --- a/frontend/src/views/Scan.vue +++ b/frontend/src/views/Scan.vue @@ -365,105 +365,113 @@ async function stopScan() { with-filename >