Merge pull request #2583 from rommapp/threaded-scan-fixes

Multi-threaded scan fixes
This commit is contained in:
Georges-Antoine Assi
2025-10-21 14:53:08 -04:00
committed by GitHub
29 changed files with 167 additions and 218 deletions

View File

@@ -12,10 +12,10 @@ class ScanStats(TypedDict):
new_platforms: int
identified_platforms: int
scanned_roms: int
added_roms: int
new_roms: int
identified_roms: int
scanned_firmware: int
added_firmware: int
new_firmware: int
class ScanTaskMeta(TypedDict):

View File

@@ -56,17 +56,33 @@ class ScanStats:
new_platforms: int = 0
identified_platforms: int = 0
scanned_roms: int = 0
added_roms: int = 0
new_roms: int = 0
identified_roms: int = 0
scanned_firmware: int = 0
added_firmware: int = 0
new_firmware: int = 0
def update(self, **kwargs):
for key, value in kwargs.items():
if hasattr(self, key):
setattr(self, key, value)
def __post_init__(self):
# Lock for thread-safe updates
self._lock = asyncio.Lock()
update_job_meta({"scan_stats": self.to_dict()})
async def update(self, socket_manager: socketio.AsyncRedisManager, **kwargs):
async with self._lock:
for key, value in kwargs.items():
if hasattr(self, key):
setattr(self, key, value)
update_job_meta({"scan_stats": self.to_dict()})
await socket_manager.emit("scan:update_stats", self.to_dict())
async def increment(self, socket_manager: socketio.AsyncRedisManager, **kwargs):
async with self._lock:
for key, value in kwargs.items():
if hasattr(self, key):
current_value = getattr(self, key)
setattr(self, key, current_value + value)
update_job_meta({"scan_stats": self.to_dict()})
await socket_manager.emit("scan:update_stats", self.to_dict())
def to_dict(self) -> dict[str, Any]:
return {
@@ -76,10 +92,10 @@ class ScanStats:
"new_platforms": self.new_platforms,
"identified_platforms": self.identified_platforms,
"scanned_roms": self.scanned_roms,
"added_roms": self.added_roms,
"new_roms": self.new_roms,
"identified_roms": self.identified_roms,
"scanned_firmware": self.scanned_firmware,
"added_firmware": self.added_firmware,
"new_firmware": self.new_firmware,
}
@@ -92,10 +108,11 @@ async def _identify_firmware(
platform: Platform,
fs_fw: str,
scan_stats: ScanStats,
) -> ScanStats:
socket_manager: socketio.AsyncRedisManager,
) -> None:
# Break early if the flag is set
if redis_client.get(STOP_SCAN_FLAG):
return scan_stats
return
firmware = db_firmware_handler.get_firmware_by_filename(platform.id, fs_fw)
@@ -114,17 +131,16 @@ async def _identify_firmware(
crc_hash=scanned_firmware.crc_hash,
)
scan_stats.update(
scanned_firmware=scan_stats.scanned_firmware + 1,
added_firmware=scan_stats.added_firmware + (1 if not firmware else 0),
await scan_stats.increment(
socket_manager=socket_manager,
scanned_firmware=1,
new_firmware=1 if not firmware else 0,
)
scanned_firmware.missing_from_fs = False
scanned_firmware.is_verified = is_verified
db_firmware_handler.add_firmware(scanned_firmware)
return scan_stats
def _should_scan_rom(scan_type: ScanType, rom: Rom | None, roms_ids: list[int]) -> bool:
"""Decide if a rom should be scanned or not
@@ -166,10 +182,10 @@ async def _identify_rom(
metadata_sources: list[str],
socket_manager: socketio.AsyncRedisManager,
scan_stats: ScanStats,
) -> ScanStats:
) -> None:
# Break early if the flag is set
if redis_client.get(STOP_SCAN_FLAG):
return scan_stats
return
if not _should_scan_rom(scan_type=scan_type, rom=rom, roms_ids=roms_ids):
if rom:
@@ -181,8 +197,7 @@ async def _identify_rom(
if rom.missing_from_fs:
db_rom_handler.update_rom(rom.id, {"missing_from_fs": False})
scan_stats.update(scanned_roms=scan_stats.scanned_roms + 1)
return scan_stats
return
# Update properties that don't require metadata
fs_regions, fs_revisions, fs_languages, fs_other_tags = fs_rom_handler.parse_tags(
@@ -218,7 +233,7 @@ async def _identify_rom(
# Silly checks to make the type checker happy
if not rom:
return scan_stats
return
# Build rom files object before scanning
log.debug(f"Calculating file hashes for {rom.fs_name}...")
@@ -246,11 +261,11 @@ async def _identify_rom(
socket_manager=socket_manager,
)
scan_stats.update(
scanned_roms=scan_stats.scanned_roms + 1,
added_roms=scan_stats.added_roms + (1 if not rom else 0),
identified_roms=scan_stats.identified_roms
+ (1 if scanned_rom.is_identified else 0),
await scan_stats.increment(
socket_manager=socket_manager,
scanned_roms=1,
new_roms=1 if newly_added else 0,
identified_roms=1 if scanned_rom.is_identified else 0,
)
_added_rom = db_rom_handler.add_rom(scanned_rom)
@@ -341,9 +356,6 @@ async def _identify_rom(
exclude={"created_at", "updated_at", "rom_user"}
),
)
await socket_manager.emit("", None)
return scan_stats
async def _identify_platform(
@@ -367,11 +379,11 @@ async def _identify_platform(
if platform:
scanned_platform.id = platform.id
scan_stats.update(
scanned_platforms=scan_stats.scanned_platforms + 1,
new_platforms=scan_stats.new_platforms + (1 if not platform else 0),
identified_platforms=scan_stats.identified_platforms
+ (1 if scanned_platform.is_identified else 0),
await scan_stats.increment(
socket_manager=socket_manager,
scanned_platforms=1,
new_platforms=1 if not platform else 0,
identified_platforms=1 if scanned_platform.is_identified else 0,
)
platform = db_platform_handler.add_platform(scanned_platform)
@@ -382,7 +394,6 @@ async def _identify_platform(
include={"id", "name", "display_name", "slug", "fs_slug", "is_identified"}
),
)
await socket_manager.emit("", None)
# Scanning firmware
try:
@@ -398,7 +409,8 @@ async def _identify_platform(
log.info(f"{hl(str(len(fs_firmware)))} firmware files found")
for fs_fw in fs_firmware:
scan_stats = await _identify_firmware(
await _identify_firmware(
socket_manager=socket_manager,
platform=platform,
fs_fw=fs_fw,
scan_stats=scan_stats,
@@ -421,12 +433,10 @@ async def _identify_platform(
# Create semaphore to limit concurrent ROM scanning
scan_semaphore = asyncio.Semaphore(SCAN_WORKERS)
async def scan_rom_with_semaphore(fs_rom: FSRom, rom: Rom | None) -> dict[str, int]:
"""Scan a single ROM with semaphore limiting and return stats delta"""
async def scan_rom_with_semaphore(fs_rom: FSRom, rom: Rom | None) -> None:
"""Scan a single ROM with semaphore limiting"""
async with scan_semaphore:
# Create a fresh stats object for this ROM to avoid race conditions
rom_scan_stats = ScanStats()
result_stats = await _identify_rom(
await _identify_rom(
platform=platform,
fs_rom=fs_rom,
rom=rom,
@@ -434,15 +444,9 @@ async def _identify_platform(
roms_ids=roms_ids,
metadata_sources=metadata_sources,
socket_manager=socket_manager,
scan_stats=rom_scan_stats,
scan_stats=scan_stats,
)
return {
"scanned_roms": result_stats.scanned_roms,
"added_roms": result_stats.added_roms,
"identified_roms": result_stats.identified_roms,
}
for fs_roms_batch in batched(fs_roms, 200, strict=False):
roms_by_fs_name = db_rom_handler.get_roms_by_fs_name(
platform_id=platform.id,
@@ -458,19 +462,10 @@ async def _identify_platform(
]
# Wait for all ROMs in the batch to complete
batch_results = await asyncio.gather(*scan_tasks, return_exceptions=True)
# Aggregate stats from all ROMs in the batch
for result, fs_rom in zip(batch_results, fs_roms_batch, strict=False):
if isinstance(result, BaseException):
batched_results = await asyncio.gather(*scan_tasks, return_exceptions=True)
for result, fs_rom in zip(batched_results, fs_roms_batch, strict=False):
if isinstance(result, Exception):
log.error(f"Error scanning ROM {fs_rom['fs_name']}: {result}")
else:
scan_stats.update(
scanned_roms=scan_stats.scanned_roms + result["scanned_roms"],
added_roms=scan_stats.added_roms + result["added_roms"],
identified_roms=scan_stats.identified_roms
+ result["identified_roms"],
)
missing_roms = db_rom_handler.mark_missing_roms(
platform.id, [rom["fs_name"] for rom in fs_roms]
@@ -521,10 +516,15 @@ async def scan_platforms(
return scan_stats
# Precalculate total platforms and ROMs
scan_stats.update(total_platforms=len(fs_platforms))
total_roms = 0
for platform_slug in fs_platforms:
fs_roms = await fs_rom_handler.get_roms(Platform(fs_slug=platform_slug))
scan_stats.update(total_roms=scan_stats.total_roms + len(fs_roms))
total_roms += len(fs_roms)
await scan_stats.update(
socket_manager=socket_manager,
total_platforms=len(fs_platforms),
total_roms=total_roms,
)
async def stop_scan():
log.info(f"{emoji.EMOJI_STOP_SIGN} Scan stopped manually")

View File

@@ -14,7 +14,6 @@ from logger.logger import log
from tasks.scheduled.update_switch_titledb import (
SWITCH_PRODUCT_ID_KEY,
SWITCH_TITLEDB_INDEX_KEY,
update_switch_titledb_task,
)
jarowinkler = JaroWinkler()
@@ -190,12 +189,8 @@ class MetadataHandler(abc.ABC):
title_id = match.group(1)
if not (await async_cache.exists(SWITCH_TITLEDB_INDEX_KEY)):
log.warning("Fetching the Switch titleID index file...")
await update_switch_titledb_task.run(force=True)
if not (await async_cache.exists(SWITCH_TITLEDB_INDEX_KEY)):
log.error("Could not fetch the Switch titleID index file")
return search_term, None
log.error("Could not find the Switch titleID index file in cache")
return search_term, None
index_entry = await async_cache.hget(SWITCH_TITLEDB_INDEX_KEY, title_id)
if index_entry:
@@ -215,12 +210,8 @@ class MetadataHandler(abc.ABC):
product_id = "".join(product_id)
if not (await async_cache.exists(SWITCH_PRODUCT_ID_KEY)):
log.warning("Fetching the Switch productID index file...")
await update_switch_titledb_task.run(force=True)
if not (await async_cache.exists(SWITCH_PRODUCT_ID_KEY)):
log.error("Could not fetch the Switch productID index file")
return search_term, None
log.error("Could not find the Switch productID index file in cache")
return search_term, None
index_entry = await async_cache.hget(SWITCH_PRODUCT_ID_KEY, product_id)
if index_entry:

View File

@@ -142,17 +142,8 @@ class LaunchboxHandler(MetadataHandler):
self, file_name: str, platform_slug: str
) -> dict | None:
if not (await async_cache.exists(LAUNCHBOX_METADATA_NAME_KEY)):
log.info("Fetching the Launchbox Metadata.xml file...")
from tasks.scheduled.update_launchbox_metadata import (
update_launchbox_metadata_task,
)
await update_launchbox_metadata_task.run(force=True)
if not (await async_cache.exists(LAUNCHBOX_METADATA_NAME_KEY)):
log.error("Could not fetch the Launchbox Metadata.xml file")
return None
log.error("Could not find the Launchbox Metadata.xml file in cache")
return None
lb_platform = self.get_platform(platform_slug)
platform_name = lb_platform.get("name", None)

View File

@@ -1,6 +1,7 @@
from unittest.mock import Mock
import pytest
import socketio
from endpoints.sockets.scan import ScanStats, _should_scan_rom
from handler.scan_handler import ScanType
@@ -13,61 +14,62 @@ def test_scan_stats():
assert stats.new_platforms == 0
assert stats.identified_platforms == 0
assert stats.scanned_roms == 0
assert stats.added_roms == 0
assert stats.new_roms == 0
assert stats.identified_roms == 0
assert stats.scanned_firmware == 0
assert stats.added_firmware == 0
assert stats.new_firmware == 0
stats.scanned_platforms += 1
stats.new_platforms += 1
stats.identified_platforms += 1
stats.scanned_roms += 1
stats.added_roms += 1
stats.new_roms += 1
stats.identified_roms += 1
stats.scanned_firmware += 1
stats.added_firmware += 1
stats.new_firmware += 1
assert stats.scanned_platforms == 1
assert stats.new_platforms == 1
assert stats.identified_platforms == 1
assert stats.scanned_roms == 1
assert stats.added_roms == 1
assert stats.new_roms == 1
assert stats.identified_roms == 1
assert stats.scanned_firmware == 1
assert stats.added_firmware == 1
assert stats.new_firmware == 1
def test_merging_scan_stats():
async def test_merging_scan_stats():
stats = ScanStats(
scanned_platforms=1,
new_platforms=2,
identified_platforms=3,
scanned_roms=4,
added_roms=5,
new_roms=5,
identified_roms=6,
scanned_firmware=7,
added_firmware=8,
new_firmware=8,
)
stats.update(
await stats.update(
socket_manager=Mock(spec=socketio.AsyncRedisManager),
scanned_platforms=stats.scanned_platforms + 10,
new_platforms=stats.new_platforms + 11,
identified_platforms=stats.identified_platforms + 12,
scanned_roms=stats.scanned_roms + 13,
added_roms=stats.added_roms + 14,
new_roms=stats.new_roms + 14,
identified_roms=stats.identified_roms + 15,
scanned_firmware=stats.scanned_firmware + 16,
added_firmware=stats.added_firmware + 17,
new_firmware=stats.new_firmware + 17,
)
assert stats.scanned_platforms == 11
assert stats.new_platforms == 13
assert stats.identified_platforms == 15
assert stats.scanned_roms == 17
assert stats.added_roms == 19
assert stats.new_roms == 19
assert stats.identified_roms == 21
assert stats.scanned_firmware == 23
assert stats.added_firmware == 25
assert stats.new_firmware == 25
class TestShouldScanRom:

View File

@@ -268,54 +268,6 @@ class TestMetadataHandlerMethods:
assert index_entry is not None
assert index_entry["publisher"] == "Nintendo"
@pytest.mark.asyncio
async def test_switch_titledb_format_cache_missing_fetch_success(
self, handler: MetadataHandler
):
"""Test Switch TitleDB format when cache is missing but fetch succeeds."""
with patch.object(
async_cache, "exists", new_callable=AsyncMock
) as mock_exists, patch.object(
async_cache, "hget", new_callable=AsyncMock
) as mock_hget, patch(
"handler.metadata.base_handler.update_switch_titledb_task"
) as mock_task:
# First call returns False (cache missing), second returns True (after fetch)
mock_exists.side_effect = [False, True]
mock_hget.return_value = json.dumps({"name": "Fetched Game"})
mock_task.run = AsyncMock()
match = re.match(SWITCH_TITLEDB_REGEX, "70123456789012")
assert match is not None
result = await handler._switch_titledb_format(match, "original")
mock_task.run.assert_called_once_with(force=True)
assert result[0] == "Fetched Game"
@pytest.mark.asyncio
async def test_switch_titledb_format_cache_missing_fetch_fails(
self, handler: MetadataHandler
):
"""Test Switch TitleDB format when cache is missing and fetch fails."""
with patch.object(
async_cache, "exists", new_callable=AsyncMock
) as mock_exists, patch(
"handler.metadata.base_handler.update_switch_titledb_task"
) as mock_task, patch(
"handler.metadata.base_handler.log"
) as mock_log:
mock_exists.return_value = False # Cache always missing
mock_task.run = AsyncMock()
match = re.match(SWITCH_TITLEDB_REGEX, "70123456789012")
assert match is not None
result = await handler._switch_titledb_format(match, "original")
mock_log.error.assert_called()
assert result == ("original", None)
@pytest.mark.asyncio
async def test_switch_titledb_format_not_found(self, handler: MetadataHandler):
"""Test Switch TitleDB format when title ID not found."""

View File

@@ -9,9 +9,9 @@ export type ScanStats = {
new_platforms: number;
identified_platforms: number;
scanned_roms: number;
added_roms: number;
new_roms: number;
identified_roms: number;
scanned_firmware: number;
added_firmware: number;
new_firmware: number;
};

View File

@@ -121,7 +121,7 @@ async function updatePlatform() {
}
async function scan() {
scanningStore.set(true);
scanningStore.setScanning(true);
if (!socket.connected) socket.connect();

View File

@@ -42,7 +42,7 @@ function scrollToTop() {
});
}
async function onScan() {
scanningStore.set(true);
scanningStore.setScanning(true);
const romCount = romsStore.selectedRoms.length;
emitter?.emit("snackbarShow", {
msg: `Scanning ${romCount} game${romCount > 1 ? "s" : ""}...`,

View File

@@ -13,10 +13,10 @@ const scanProgress = computed(() => {
total_roms,
scanned_platforms,
scanned_roms,
added_roms,
new_roms,
identified_roms,
scanned_firmware,
added_firmware,
new_firmware,
} = props.scanStats;
return {
@@ -26,10 +26,10 @@ const scanProgress = computed(() => {
),
roms: `${scanned_roms}/${total_roms}`,
romsPercentage: Math.round((scanned_roms / total_roms) * 100),
addedRoms: added_roms,
newRoms: new_roms,
metadataRoms: identified_roms,
scannedFirmware: scanned_firmware,
addedFirmware: added_firmware,
newFirmware: new_firmware,
};
});
</script>
@@ -92,7 +92,7 @@ const scanProgress = computed(() => {
<v-icon icon="mdi-plus-circle" size="20" />
</v-avatar>
<div class="font-weight-bold">
{{ scanProgress.addedRoms }}
{{ scanProgress.newRoms }}
</div>
<div class="text-uppercase">Added</div>
</div>

View File

@@ -56,7 +56,7 @@ async function resetLastPlayed() {
}
async function onScan() {
scanningStore.set(true);
scanningStore.setScanning(true);
emitter?.emit("snackbarShow", {
msg: `Refreshing ${props.rom.name} metadata...`,
icon: "mdi-loading mdi-spin",

View File

@@ -123,7 +123,7 @@ async function uploadRoms() {
timeout: 3000,
});
scanningStore.set(true);
scanningStore.setScanning(true);
if (!socket.connected) socket.connect();
setTimeout(() => {

View File

@@ -3,6 +3,7 @@ import type { Emitter } from "mitt";
import { storeToRefs } from "pinia";
import { inject, onBeforeUnmount } from "vue";
import { useI18n } from "vue-i18n";
import type { ScanStats } from "@/__generated__";
import socket from "@/services/socket";
import storeAuth from "@/stores/auth";
import storeNavigation from "@/stores/navigation";
@@ -49,7 +50,7 @@ socket.on(
fs_slug: string;
is_identified: boolean;
}) => {
scanningStore.set(true);
scanningStore.setScanning(true);
scanningPlatforms.value = scanningPlatforms.value.filter(
(platform) => platform.display_name !== display_name,
);
@@ -65,7 +66,7 @@ socket.on(
);
socket.on("scan:scanning_rom", (rom: SimpleRom) => {
scanningStore.set(true);
scanningStore.setScanning(true);
// Remove the ROM from the recent list and add it back to the top
romsStore.removeFromRecent(rom);
@@ -109,7 +110,7 @@ socket.on("scan:scanning_rom", (rom: SimpleRom) => {
});
socket.on("scan:done", () => {
scanningStore.set(false);
scanningStore.setScanning(false);
socket.disconnect();
emitter?.emit("refreshDrawer", null);
@@ -122,7 +123,7 @@ socket.on("scan:done", () => {
});
socket.on("scan:done_ko", (msg) => {
scanningStore.set(false);
scanningStore.setScanning(false);
emitter?.emit("snackbarShow", {
msg: `Scan failed: ${msg}`,
@@ -132,6 +133,10 @@ socket.on("scan:done_ko", (msg) => {
socket.disconnect();
});
socket.on("scan:update_stats", (stats: ScanStats) => {
scanningStore.setScanStats(stats);
});
onBeforeUnmount(() => {
socket.off("scan:scanning_platform");
socket.off("scan:scanning_rom");

View File

@@ -15,11 +15,11 @@
"partial-metadata": "Unvollständige Metadaten",
"partial-metadata-desc": "Scanne Spiele mit unvollständigen Metadaten",
"platforms-scanned-n": "Plattformen: {n} gescannte | Plattformen: {n} gescannt",
"platforms-scanned-with-details": "Plattformen: {n_platforms} gescannt, darunter {n_new_platforms} neue und {n_identified_platforms} identifizierte",
"platforms-scanned-with-details": "Plattformen: {n_scanned_platforms} gescannt aus {n_total_platforms}, darunter {n_new_platforms} neue und {n_identified_platforms} identifizierte",
"quick-scan": "Schneller Scan",
"quick-scan-desc": "Nur neue Dateien scannen",
"roms-scanned-n": "Roms: {n} gescannte | Roms: {n} gescannt",
"roms-scanned-with-details": "Roms: {n_roms} gescannt, dabei {n_added_roms} neue und {n_identified_roms} identifizierte",
"roms-scanned-with-details": "Roms: {n_scanned_roms} gescannt aus {n_total_roms}, darunter {n_new_roms} neue und {n_identified_roms} identifizierte",
"scan": "Scannen",
"scan-options": "Scan-Optionen",
"select-one-source": "Bitte wähle mindestens eine Metadatenquelle, wenn du die Bibliothek mit Cover-Artworks und Metadaten anreichern möchtest",

View File

@@ -15,11 +15,11 @@
"partial-metadata": "Partial metadata",
"partial-metadata-desc": "Scan games with partial metadata matches",
"platforms-scanned-n": "Platforms: {n} scanned",
"platforms-scanned-with-details": "Platforms: {n_platforms} scanned, with {n_new_platforms} new and {n_identified_platforms} identified",
"platforms-scanned-with-details": "Platforms: {n_scanned_platforms} scanned out of {n_total_platforms}, with {n_new_platforms} new and {n_identified_platforms} identified",
"quick-scan": "Quick scan",
"quick-scan-desc": "Scan new files only",
"roms-scanned-n": "Roms: {n} scanned",
"roms-scanned-with-details": "Roms: {n_roms} scanned, with {n_added_roms} new and {n_identified_roms} identified",
"roms-scanned-with-details": "Roms: {n_scanned_roms} scanned out of {n_total_roms}, with {n_new_roms} new and {n_identified_roms} identified",
"scan": "Scan",
"scan-options": "Scan options",
"select-one-source": "Please select at least one metadata source to enrich your library with artwork and metadata",

View File

@@ -15,11 +15,11 @@
"partial-metadata": "Partial metadata",
"partial-metadata-desc": "Scan games with partial metadata matches",
"platforms-scanned-n": "Platforms: {n} scanned",
"platforms-scanned-with-details": "Platforms: {n_platforms} scanned, with {n_new_platforms} new and {n_identified_platforms} identified",
"platforms-scanned-with-details": "Platforms: {n_scanned_platforms} scanned out of {n_total_platforms}, with {n_new_platforms} new and {n_identified_platforms} identified",
"quick-scan": "Quick scan",
"quick-scan-desc": "Scan new files only",
"roms-scanned-n": "Roms: {n} scanned",
"roms-scanned-with-details": "Roms: {n_roms} scanned, with {n_added_roms} new and {n_identified_roms} identified",
"roms-scanned-with-details": "Roms: {n_scanned_roms} scanned out of {n_total_roms}, with {n_new_roms} new and {n_identified_roms} identified",
"scan": "Scan",
"scan-options": "Scan options",
"select-one-source": "Please select at least one metadata source to enrich your library with artwork and metadata",

View File

@@ -15,11 +15,11 @@
"partial-metadata": "Metadatos parciales",
"partial-metadata-desc": "Escanea únicamente juegos identificados con metadatos a medias",
"platforms-scanned-n": "Plataformas: {n} escaneada | Plataformas: {n} escaneadas",
"platforms-scanned-with-details": "Plataformas: {n_platforms} escaneadas, {n_new_platforms} nuevas y {n_identified_platforms} identificadas",
"platforms-scanned-with-details": "Plataformas: {n_scanned_platforms} escaneadas de {n_total_platforms}, con {n_new_platforms} nuevas y {n_identified_platforms} identificadas",
"quick-scan": "Escaneo rápido",
"quick-scan-desc": "Escanea tu biblioteca en busca nuevos ficheros",
"roms-scanned-n": "Roms: {n} escaneado | Roms: {n} escaneados",
"roms-scanned-with-details": "Roms: {n_roms} escaneados, {n_added_roms} nuevos y {n_identified_roms} identificados",
"roms-scanned-with-details": "Roms: {n_scanned_roms} escaneados de {n_total_roms}, con {n_new_roms} nuevos y {n_identified_roms} identificados",
"scan": "Escanear",
"scan-options": "Tipo de escaneo",
"select-one-source": "Por favor, elige al menos una fuente de metadatos para enriquecer tu biblioteca con carátulas y metadatos",

View File

@@ -15,11 +15,11 @@
"partial-metadata": "Métadonnées partielles",
"partial-metadata-desc": "Scanner uniquement les jeux identifiés avec des métadonnées partielles",
"platforms-scanned-n": "Plateformes : {n} scannée | Plateformes : {n} scannées",
"platforms-scanned-with-details": "Plateformes : {n_platforms} scannées, {n_new_platforms} nouvelles et {n_identified_platforms} identifiées",
"platforms-scanned-with-details": "Plateformes : {n_scanned_platforms} scannées sur {n_total_platforms}, avec {n_new_platforms} nouvelles et {n_identified_platforms} identifiées",
"quick-scan": "Scan rapide",
"quick-scan-desc": "Scanner votre bibliothèque à la recherche de nouveaux fichiers",
"roms-scanned-n": "Roms : {n} scannée | Roms : {n} scannées",
"roms-scanned-with-details": "Roms : {n_roms} scannées, {n_added_roms} nouvelles et {n_identified_roms} identifiées",
"roms-scanned-with-details": "Roms : {n_scanned_roms} scannées sur {n_total_roms}, avec {n_new_roms} nouvelles et {n_identified_roms} identifiées",
"scan": "Scanner",
"scan-options": "Type de scan",
"select-one-source": "Veuillez choisir au moins une source de métadonnées pour enrichir votre bibliothèque avec des jaquettes et des métadonnées",

View File

@@ -15,11 +15,11 @@
"partial-metadata": "Metadati parziali",
"partial-metadata-desc": "Scansiona i giochi con metadati parziali",
"platforms-scanned-n": "Piattaforme: {n} scansionate",
"platforms-scanned-with-details": "Piattaforme: {n_platforms} scansionate, con {n_new_platforms} nuove e {n_identified_platforms} identificate",
"platforms-scanned-with-details": "Piattaforme: {n_scanned_platforms} scansionate su {n_total_platforms}, con {n_new_platforms} nuove e {n_identified_platforms} identificate",
"quick-scan": "Scansione rapida",
"quick-scan-desc": "Scansiona solo i nuovi file",
"roms-scanned-n": "Rom: {n} scansionate",
"roms-scanned-with-details": "Rom: {n_roms} scansionate, con {n_added_roms} nuove e {n_identified_roms} identificate",
"roms-scanned-with-details": "Rom: {n_scanned_roms} scansionate su {n_total_roms}, con {n_new_roms} nuove e {n_identified_roms} identificate",
"scan": "Scansiona",
"scan-options": "Opzioni di scansione",
"select-one-source": "Seleziona almeno una fonte di metadati per arricchire la tua libreria con artwork e metadati",

View File

@@ -15,11 +15,11 @@
"partial-metadata": "部分的なメタデータ",
"partial-metadata-desc": "メタデータが部分的に一致するゲームをスキャン",
"platforms-scanned-n": "プラットフォーム: {n} スキャン済み",
"platforms-scanned-with-details": "プラットフォーム: {n_platforms} スキャン済み 新規: {n_new_platforms} 識別済み: {n_identified_platforms}",
"platforms-scanned-with-details": "プラットフォーム: {n_scanned_platforms}/{n_total_platforms} スキャン済み 新規: {n_new_platforms} 識別済み: {n_identified_platforms}",
"quick-scan": "クイックスキャン",
"quick-scan-desc": "新規ファイルのみを検索",
"roms-scanned-n": "Rom: {n} スキャン済み",
"roms-scanned-with-details": "Rom: {n_roms} スキャン済み 新規: {n_added_roms} 識別済み: {n_identified_roms}",
"roms-scanned-with-details": "Rom: {n_scanned_roms}/{n_total_roms} スキャン済み 新規: {n_new_roms} 識別済み: {n_identified_roms}",
"scan": "スキャン",
"scan-options": "スキャンオプション",
"select-one-source": "アートワークやメタデータを使用したい場合は少なくとも1つのソースを選択してください",

View File

@@ -15,11 +15,11 @@
"partial-metadata": "일부 메타데이터",
"partial-metadata-desc": "일부 메타데이터와 대응된 게임들 스캔",
"platforms-scanned-n": "플랫폼: {n}개 스캔됨",
"platforms-scanned-with-details": "플랫폼: {n_platforms}개 스캔됨, 새로운 플랫폼: {n_new_platforms}개, 확인된 플랫폼:{n_identified_platforms}개",
"platforms-scanned-with-details": "플랫폼: {n_scanned_platforms}/{n_total_platforms}개 스캔됨, 새로운 플랫폼: {n_new_platforms}개, 확인된 플랫폼: {n_identified_platforms}개",
"quick-scan": "빠른 스캔",
"quick-scan-desc": "새 파일만 검색",
"roms-scanned-n": "롬: {n}개 스캔됨",
"roms-scanned-with-details": "롬: {n_roms}개 스캔됨, 새로운 롬: {n_added_roms}개, 확인된 롬: {n_identified_roms}개",
"roms-scanned-with-details": "롬: {n_scanned_roms}/{n_total_roms}개 스캔됨, 새로운 롬: {n_new_roms}개, 확인된 롬: {n_identified_roms}개",
"scan": "스캔",
"scan-options": "스캔 옵션",
"select-one-source": "표지와 메타데이터로 라이브러리를 꾸미고 싶으시면 메타데이터 DB를 하나 이상 선택해주세요",

View File

@@ -15,11 +15,11 @@
"partial-metadata": "Częściowe metadane",
"partial-metadata-desc": "Skanuj gry z częściowym dopasowaniem metadanych",
"platforms-scanned-n": "Platformy: zeskanowano {n}",
"platforms-scanned-with-details": "Platformy: zeskanowano {n_platforms}, dodano {n_new_platforms} nowych i zidentyfikowano {n_identified_platforms}",
"platforms-scanned-with-details": "Platformy: {n_scanned_platforms} zeskanowano z {n_total_platforms}, z {n_new_platforms} nowych i {n_identified_platforms} zidentyfikowanych",
"quick-scan": "Szybkie skanowanie",
"quick-scan-desc": "Skanuj tylko nowe pliki",
"roms-scanned-n": "ROM-y: zeskanowano {n}",
"roms-scanned-with-details": "ROM-y: zeskanowano {n_roms}, dodano {n_added_roms} nowych i zidentyfikowano {n_identified_roms}",
"roms-scanned-with-details": "ROM-y: {n_scanned_roms} zeskanowano z {n_total_roms}, z {n_new_roms} nowych i {n_identified_roms} zidentyfikowanych",
"scan": "Skanuj",
"scan-options": "Opcje skanowania",
"select-one-source": "Wybierz co najmniej jedno źródło metadanych, aby wzbogacić bibliotekę o grafiki i informacje",

View File

@@ -15,11 +15,11 @@
"partial-metadata": "Metadados parciais",
"partial-metadata-desc": "Escanear jogos com correspondências parciais de metadados",
"platforms-scanned-n": "Plataformas: {n} escaneada | Plataformas: {n} escaneadas",
"platforms-scanned-with-details": "Plataformas: {n_platforms} escaneadas, com {n_new_platforms} novas e {n_identified_platforms} identificadas",
"platforms-scanned-with-details": "Plataformas: {n_scanned_platforms} escaneadas de {n_total_platforms}, com {n_new_platforms} novas e {n_identified_platforms} identificadas",
"quick-scan": "Escaneamento rápido",
"quick-scan-desc": "Escanear apenas novos arquivos",
"roms-scanned-n": "Roms: {n} escaneado | Roms: {n} escaneados",
"roms-scanned-with-details": "Roms: {n_roms} escaneados, com {n_added_roms} novos e {n_identified_roms} identificados",
"roms-scanned-with-details": "Roms: {n_scanned_roms} escaneados de {n_total_roms}, com {n_new_roms} novos e {n_identified_roms} identificados",
"scan": "Escanear",
"scan-options": "Opções de escaneamento",
"select-one-source": "Por favor, selecione pelo menos uma fonte de metadados para enriquecer sua biblioteca com arte e metadados",

View File

@@ -15,11 +15,11 @@
"partial-metadata": "Metadate parțiale",
"partial-metadata-desc": "Scanează doar jocurile identificate cu metadate incomplete",
"platforms-scanned-n": "Platforme: {n} scanată | Platforme: {n} scanate",
"platforms-scanned-with-details": "Platforme: {n_platforms} scanate, {n_new_platforms} noi și {n_identified_platforms} identificate",
"platforms-scanned-with-details": "Platforme: {n_scanned_platforms} scanate din {n_total_platforms}, cu {n_new_platforms} noi și {n_identified_platforms} identificate",
"quick-scan": "Scanare rapidă",
"quick-scan-desc": "Scanează biblioteca pentru a găsi fișiere noi",
"roms-scanned-n": "Roms: {n} scanată | Roms: {n} scanate",
"roms-scanned-with-details": "Rom-uri: {n_roms} scanate, {n_added_roms} noi și {n_identified_roms} identificate",
"roms-scanned-with-details": "Rom-uri: {n_scanned_roms} scanate din {n_total_roms}, cu {n_new_roms} noi și {n_identified_roms} identificate",
"scan": "Scanează",
"scan-options": "Tip de scanare",
"select-one-source": "Vă rugăm să alegeți cel puțin o sursă de metadate pentru a îmbogăți biblioteca cu coperți și metadate",

View File

@@ -15,11 +15,11 @@
"partial-metadata": "Частичные метаданные",
"partial-metadata-desc": "Сканировать игры с частичными совпадениями метаданных",
"platforms-scanned-n": "Платформы: {n} отсканировано",
"platforms-scanned-with-details": "Платформы: {n_platforms} отсканировано, {n_new_platforms} новых и {n_identified_platforms} опознано",
"platforms-scanned-with-details": "Платформы: {n_scanned_platforms} из {n_total_platforms} отсканировано, {n_new_platforms} новых и {n_identified_platforms} опознано",
"quick-scan": "Быстрое сканирование",
"quick-scan-desc": "Сканировать только новые файлы",
"roms-scanned-n": "Ромы: {n} отсканировано",
"roms-scanned-with-details": "Ромы: {n_roms} отсканировано, {n_added_roms} новых и {n_identified_roms} опознано",
"roms-scanned-with-details": "Ромы: {n_scanned_roms} из {n_total_roms} отсканировано, {n_new_roms} новых и {n_identified_roms} опознано",
"scan": "Сканировать",
"scan-options": "Параметры сканирования",
"select-one-source": "Пожалуйста, выберите хотя бы один источник метаданных, чтобы обогатить свою библиотеку иллюстрациями и метаданными",

View File

@@ -15,11 +15,11 @@
"partial-metadata": "部分元数据",
"partial-metadata-desc": "扫描部分元数据匹配的游戏",
"platforms-scanned-n": "平台:{n} 已扫描",
"platforms-scanned-with-details": "平台:{n_platforms} 已扫描,新增 {n_new_platforms},识别 {n_identified_platforms}",
"platforms-scanned-with-details": "平台:{n_scanned_platforms}/{n_total_platforms} 已扫描,新增 {n_new_platforms},识别 {n_identified_platforms}",
"quick-scan": "快速扫描",
"quick-scan-desc": "仅扫描新文件",
"roms-scanned-n": "Roms{n} 已扫描",
"roms-scanned-with-details": "Roms{n_roms} 已扫描,新增 {n_added_roms},识别 {n_identified_roms}",
"roms-scanned-with-details": "Roms{n_scanned_roms}/{n_total_roms} 已扫描,新增 {n_new_roms},识别 {n_identified_roms}",
"scan": "扫描",
"scan-options": "扫描选项",
"select-one-source": "请至少选择一个元数据源,以丰富您的游戏库",

View File

@@ -15,11 +15,11 @@
"partial-metadata": "部分元數據",
"partial-metadata-desc": "只掃描部分元數據匹配的遊戲",
"platforms-scanned-n": "已掃描 {n} 個平台",
"platforms-scanned-with-details": "平台:掃描 {n_platforms},新增 {n_new_platforms},識別 {n_identified_platforms}",
"platforms-scanned-with-details": "平台:{n_scanned_platforms}/{n_total_platforms} 已掃描,新增 {n_new_platforms},識別 {n_identified_platforms}",
"quick-scan": "快速掃描",
"quick-scan-desc": "只掃描新檔案",
"roms-scanned-n": "已掃描 {n} 個 Rom",
"roms-scanned-with-details": "Rom掃描 {n_roms},新增 {n_added_roms},識別 {n_identified_roms}",
"roms-scanned-with-details": "Rom{n_scanned_roms}/{n_total_roms} 已掃描,新增 {n_new_roms},識別 {n_identified_roms}",
"scan": "掃描",
"scan-options": "掃描選項",
"select-one-source": "請至少選擇一個元數據來源,以豐富您的遊戲庫",

View File

@@ -1,4 +1,5 @@
import { defineStore } from "pinia";
import type { ScanStats } from "@/__generated__";
import type { SimpleRom } from "@/stores/roms";
import type { Platform } from "./platforms";
@@ -10,30 +11,30 @@ export default defineStore("scanning", {
state: () => ({
scanning: false,
scanningPlatforms: [] as ScanningPlatform[],
scanStats: {
scanned_platforms: 0,
new_platforms: 0,
identified_platforms: 0,
scanned_roms: 0,
added_roms: 0,
identified_roms: 0,
},
scanStats: {} as ScanStats,
}),
actions: {
set(scanning: boolean) {
setScanning(scanning: boolean) {
this.scanning = scanning;
},
setScanStats(stats: ScanStats) {
this.scanStats = stats;
},
reset() {
this.scanning = false;
this.scanningPlatforms = [] as ScanningPlatform[];
this.scanStats = {
total_platforms: 0,
scanned_platforms: 0,
new_platforms: 0,
identified_platforms: 0,
total_roms: 0,
scanned_roms: 0,
added_roms: 0,
new_roms: 0,
identified_roms: 0,
scanned_firmware: 0,
new_firmware: 0,
};
},
},

View File

@@ -95,7 +95,7 @@ const scanOptions = [
const scanType = ref("quick");
async function scan() {
scanningStore.set(true);
scanningStore.setScanning(true);
scanningPlatforms.value = [];
if (!socket.connected) socket.connect();
@@ -317,13 +317,13 @@ async function stopScan() {
</div>
<!-- Scan log -->
<v-row ref="scan-log-ref" no-gutters class="scan-log overflow-y-scroll">
<v-row
ref="scan-log-ref"
no-gutters
class="scan-log overflow-y-scroll mb-4"
>
<v-col>
<v-card
elevation="0"
class="bg-surface mx-auto mt-2 mb-14"
max-width="800"
>
<v-card elevation="0" class="bg-surface mx-auto mt-2" max-width="800">
<v-card-text class="pa-0">
<v-expansion-panels
ref="expansion-panels-ref"
@@ -502,7 +502,7 @@ async function stopScan() {
<!-- Scan stats -->
<div
v-if="scanningPlatforms.length > 0"
class="text-caption position-fixed d-flex w-100 m-1 justify-center"
class="text-caption position-sticky d-flex w-100 m-1 justify-center"
style="bottom: 0.5rem"
>
<v-chip
@@ -518,18 +518,21 @@ async function stopScan() {
>
<v-icon left> mdi-controller </v-icon>
<span v-if="xs" class="ml-2">{{
t("scan.platforms-scanned-n", scanningPlatforms.length)
t("scan.platforms-scanned-n", scanStats.scanned_platforms)
}}</span>
<span v-else class="ml-2">{{
t("scan.platforms-scanned-with-details", {
n_platforms: scanningPlatforms.length,
n_scanned_platforms: scanStats.scanned_platforms,
n_total_platforms: scanStats.total_platforms,
n_new_platforms: scanStats.new_platforms,
n_identified_platforms: scanStats.identified_platforms,
n_identified_platforms: Math.min(
scanStats.identified_platforms,
scanStats.scanned_platforms,
),
})
}}</span>
</v-chip>
<v-chip
v-if="scanningPlatforms.length > 0"
color="primary"
size="small"
text-color="white"
@@ -541,9 +544,13 @@ async function stopScan() {
}}</span>
<span v-else class="ml-2">{{
t("scan.roms-scanned-with-details", {
n_roms: scanStats.scanned_roms,
n_added_roms: scanStats.added_roms,
n_identified_roms: scanStats.identified_roms,
n_scanned_roms: scanStats.scanned_roms,
n_total_roms: scanStats.total_roms,
n_new_roms: scanStats.new_roms,
n_identified_roms: Math.min(
scanStats.identified_roms,
scanStats.scanned_roms,
),
})
}}</span>
</v-chip>