Merge pull request #2479 from rommapp/scan-progress

Progressive scan steps for each game
This commit is contained in:
Georges-Antoine Assi
2025-09-23 19:27:42 -04:00
committed by GitHub
10 changed files with 188 additions and 119 deletions

View File

@@ -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: |

View File

@@ -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

View File

@@ -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

View File

@@ -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,

View File

@@ -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

View File

@@ -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,

View File

@@ -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);

View File

@@ -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);

View File

@@ -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", () => {

View File

@@ -365,105 +365,113 @@ async function stopScan() {
with-filename
>
<template #append>
<v-chip
v-if="rom.is_unidentified"
color="red"
size="x-small"
label
>
Not identified
<v-icon class="ml-1"> mdi-close </v-icon>
</v-chip>
<v-chip
v-if="rom.hasheous_id"
title="Verified with Hasheous"
class="text-white pa-0 mr-1"
size="small"
>
<v-avatar class="bg-romm-green" size="26" rounded="0">
<v-icon>mdi-check-decagram-outline</v-icon>
</v-avatar>
</v-chip>
<v-chip
v-if="rom.igdb_id"
class="pa-0 mr-1"
size="small"
title="IGDB match"
>
<v-avatar size="26" rounded>
<v-img src="/assets/scrappers/igdb.png" />
</v-avatar>
</v-chip>
<v-chip
v-if="rom.ss_id"
class="pa-0 mr-1"
size="small"
title="ScreenScraper match"
>
<v-avatar size="26" rounded>
<v-img src="/assets/scrappers/ss.png" />
</v-avatar>
</v-chip>
<v-chip
v-if="rom.moby_id"
class="pa-0 mr-1"
size="small"
title="MobyGames match"
>
<v-avatar size="26" rounded>
<v-img src="/assets/scrappers/moby.png" />
</v-avatar>
</v-chip>
<v-chip
v-if="rom.launchbox_id"
class="pa-0 mr-1"
size="small"
title="LaunchBox match"
>
<v-avatar size="26" style="background: #185a7c">
<v-img src="/assets/scrappers/launchbox.png" />
</v-avatar>
</v-chip>
<v-chip
v-if="rom.ra_id"
class="pa-0 mr-1"
size="small"
title="RetroAchievements match"
>
<v-avatar size="26" rounded>
<v-img src="/assets/scrappers/ra.png" />
</v-avatar>
</v-chip>
<v-chip
v-if="rom.hasheous_id"
class="pa-1 mr-1 bg-surface"
size="small"
title="Hasheous match"
>
<v-avatar size="18" rounded>
<v-img src="/assets/scrappers/hasheous.png" />
</v-avatar>
</v-chip>
<v-chip
v-if="rom.flashpoint_id"
class="pa-1 mr-1 bg-surface"
size="small"
title="Flashpoint match"
>
<v-avatar size="18" rounded>
<v-img src="/assets/scrappers/flashpoint.png" />
</v-avatar>
</v-chip>
<v-chip
v-if="rom.hltb_id"
class="pa-1 mr-1 bg-surface"
size="small"
title="HowLongToBeat match"
>
<v-avatar size="18" rounded>
<v-img src="/assets/scrappers/hltb.png" />
</v-avatar>
</v-chip>
<template v-if="rom.is_identifying">
<v-chip color="orange" size="x-small" label>
<v-icon class="mr-1"> mdi-search-web </v-icon>
Identifying
</v-chip>
</template>
<template v-else>
<v-chip
v-if="rom.is_unidentified"
color="red"
size="x-small"
label
>
<v-icon class="mr-1"> mdi-close </v-icon>
Not identified
</v-chip>
<v-chip
v-if="rom.hasheous_id"
title="Verified with Hasheous"
class="text-white pa-0 mr-1"
size="small"
>
<v-avatar class="bg-romm-green" size="26" rounded="0">
<v-icon>mdi-check-decagram-outline</v-icon>
</v-avatar>
</v-chip>
<v-chip
v-if="rom.igdb_id"
class="pa-0 mr-1"
size="small"
title="IGDB match"
>
<v-avatar size="26" rounded>
<v-img src="/assets/scrappers/igdb.png" />
</v-avatar>
</v-chip>
<v-chip
v-if="rom.ss_id"
class="pa-0 mr-1"
size="small"
title="ScreenScraper match"
>
<v-avatar size="26" rounded>
<v-img src="/assets/scrappers/ss.png" />
</v-avatar>
</v-chip>
<v-chip
v-if="rom.moby_id"
class="pa-0 mr-1"
size="small"
title="MobyGames match"
>
<v-avatar size="26" rounded>
<v-img src="/assets/scrappers/moby.png" />
</v-avatar>
</v-chip>
<v-chip
v-if="rom.launchbox_id"
class="pa-0 mr-1"
size="small"
title="LaunchBox match"
>
<v-avatar size="26" style="background: #185a7c">
<v-img src="/assets/scrappers/launchbox.png" />
</v-avatar>
</v-chip>
<v-chip
v-if="rom.ra_id"
class="pa-0 mr-1"
size="small"
title="RetroAchievements match"
>
<v-avatar size="26" rounded>
<v-img src="/assets/scrappers/ra.png" />
</v-avatar>
</v-chip>
<v-chip
v-if="rom.hasheous_id"
class="pa-1 mr-1 bg-surface"
size="small"
title="Hasheous match"
>
<v-avatar size="18" rounded>
<v-img src="/assets/scrappers/hasheous.png" />
</v-avatar>
</v-chip>
<v-chip
v-if="rom.flashpoint_id"
class="pa-1 mr-1 bg-surface"
size="small"
title="Flashpoint match"
>
<v-avatar size="18" rounded>
<v-img src="/assets/scrappers/flashpoint.png" />
</v-avatar>
</v-chip>
<v-chip
v-if="rom.hltb_id"
class="pa-1 mr-1 bg-surface"
size="small"
title="HowLongToBeat match"
>
<v-avatar size="18" rounded>
<v-img src="/assets/scrappers/hltb.png" />
</v-avatar>
</v-chip>
</template>
</template>
</RomListItem>
<v-list-item