-
-
Notifications
You must be signed in to change notification settings - Fork 653
Expand file tree
/
Copy pathsigil.py
More file actions
99 lines (80 loc) · 3.09 KB
/
Copy pathsigil.py
File metadata and controls
99 lines (80 loc) · 3.09 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
import asyncio
from dataclasses import dataclass
from typing import Any, Final
from handler.metadata.base_handler import UniversalPlatformSlug as UPS
from logger.logger import log
try:
import sigil
except ImportError:
sigil = None # type: ignore[assignment]
SWITCH_SIGIL_SLUG: Final = "switch"
SIGIL_PLATFORM_SLUGS: Final[dict[UPS, str]] = {
UPS.PSP: "psp",
UPS.PSX: "psx",
UPS.PS2: "ps2",
UPS.PSVITA: "psvita",
UPS.SWITCH: SWITCH_SIGIL_SLUG,
UPS.SWITCH_2: SWITCH_SIGIL_SLUG,
UPS.N3DS: "3ds",
UPS.WII: "wii",
UPS.WIIU: "wiiu",
UPS.NGC: "gamecube",
}
# Errors that are expected for arbitrary library files (no title id present,
# format sigil can't parse, missing decryption keys). Logged at debug level.
ROUTINE_SIGIL_ERROR_CODES: Final = frozenset(
{"NOT_FOUND", "UNSUPPORTED", "UNSUPPORTED_FORMAT", "NEEDS_KEY"}
)
_missing_binding_logged = False
class SigilServiceError(Exception): ...
@dataclass(frozen=True)
class SigilExtractionResult:
title_id: str
save_id: str
usage: str
content_type: str | None = None
version: int | None = None
class SigilService:
"""Service to extract platform-native title ids from ROM binaries via the
optional `sigil` cffi binding."""
async def extract_title_id(
self,
platform_slug: UPS | str,
file_path: str,
prod_keys_path: str | None = None,
) -> SigilExtractionResult | None:
global _missing_binding_logged
if sigil is None:
if not _missing_binding_logged:
log.debug("sigil binding not installed, skipping title id extraction")
_missing_binding_logged = True
return None
sigil_slug = SIGIL_PLATFORM_SLUGS.get(platform_slug) # type: ignore[arg-type]
if sigil_slug is None:
return None
kwargs: dict[str, Any] = {"platform": sigil_slug, "filename_fallback": False}
if sigil_slug == SWITCH_SIGIL_SLUG:
kwargs["prod_keys_path"] = prod_keys_path
try:
result = await asyncio.to_thread(sigil.extract, file_path, **kwargs)
except Exception as exc:
code = getattr(exc, "code", None)
is_sigil_error = isinstance(exc, getattr(sigil, "SigilError", ()))
if is_sigil_error and code in ROUTINE_SIGIL_ERROR_CODES:
log.debug(f"Sigil found no title id for {file_path}: {code}")
else:
log.error(f"Sigil extraction failed for {file_path}: {exc}")
return None
raw_content_type = getattr(result, "switch_content_type", None)
content_type = (
raw_content_type if raw_content_type not in (None, "", "unknown") else None
)
return SigilExtractionResult(
title_id=result.title_id,
save_id=result.save_id,
usage=result.usage,
content_type=content_type,
# Version 0 is a valid base-game version, so keep the int as-is;
# a missing field (non-Switch, older binding) maps to None.
version=getattr(result, "title_version", None),
)