RomM元数据系统:多源数据聚合与智能匹配
RomM元数据系统通过集成IGDB、Screenscraper、MobyGames、SteamGridDB和RetroAchievements五大主流游戏数据库服务,为游戏ROM提供了强大的元数据聚合能力。系统采用模块化架构设计,为每个数据提供商实现独立的处理器类,通过智能匹配算法实现高效的元数据获取,支持哈希匹配、文件名智能解析和多源数据聚合,确保游戏信息的全面性和准确性。
IGDB、Screenscraper、MobyGames集成
RomM的元数据系统通过集成三大主流游戏数据库服务——IGDB、Screenscraper和MobyGames,为游戏ROM提供了强大的元数据聚合能力。这种多源数据聚合策略确保了游戏信息的全面性和准确性,同时通过智能匹配算法实现了高效的元数据获取。
多源数据提供商的架构设计
RomM采用模块化的架构设计,为每个数据提供商实现了独立的处理器类,这些处理器都继承自统一的基类MetadataHandler,确保了接口的一致性和扩展性。
IGDB集成实现
IGDB(Internet Game Database)作为Twitch旗下的游戏数据库,提供了丰富的游戏元数据。RomM通过OAuth 2.0认证与IGDB API进行交互:
class IGDBHandler(MetadataHandler):
def __init__(self) -> None:
self.BASE_URL = "https://api.igdb.com/v4"
self.headers = {
"Client-ID": IGDB_CLIENT_ID,
"Accept": "application/json",
}
self.twitch_auth = TwitchAuth()
@staticmethod
def check_twitch_token(func):
@functools.wraps(func)
async def wrapper(*args):
token = await args[0].twitch_auth.get_oauth_token()
args[0].headers["Authorization"] = f"Bearer {token}"
return await func(*args)
return wrapper
IGDB处理器支持丰富的元数据提取,包括:
| 元数据类型 | 数据字段 | 说明 |
|---|---|---|
| 基本信息 | 游戏名称、ID、slug | 核心标识信息 |
| 媒体资源 | 封面、截图、视频 | 视觉资源链接 |
| 评分信息 | 总评分、聚合评分 | 0-100分制评分 |
| 分类信息 | 类型、系列、公司 | 游戏分类信息 |
| 时间信息 | 首发日期 | 时间戳格式 |
| 关联游戏 | DLC、重制版、移植版 | 游戏关系网络 |
Screenscraper集成特性
Screenscraper专注于游戏封面、截图和手册的提供,特别支持多语言和多区域的内容:
def build_ss_rom(game: SSGame) -> SSRom:
name_preferred_regions = ["us", "wor", "ss", "eu", "jp"]
res_name = ""
for region in name_preferred_regions:
res_name = next(
(name["text"] for name in game.get("noms", [])
if name.get("region") == region), ""
)
if res_name:
break
Screenscraper的区域优先级机制确保了最佳内容的获取:
MobyGames的专业数据集成
MobyGames以其详细的游戏信息和专业的编辑内容著称,RomM通过API密钥进行认证:
class MobyGamesHandler(MetadataHandler):
def __init__(self) -> None:
self.moby_service = MobyGamesService()
async def get_rom(self, fs_name: str, platform_moby_id: int) -> MobyGamesRom:
if not MOBY_API_ENABLED:
return MobyGamesRom(moby_id=None)
search_term = self.normalize_search_term(fs_name)
roms = await self.moby_service.list_games(
platform_ids=[platform_moby_id],
title=search_term
)
MobyGames提供的数据结构包括:
class MobyMetadata(TypedDict):
moby_score: str # MobyGames专属评分
genres: list[str] # 游戏类型列表
alternate_titles: list[str] # 替代标题
platforms: list[MobyMetadataPlatform] # 支持平台信息
智能匹配算法
RomM实现了复杂的智能匹配算法来处理不同文件名格式:
# PS2 OPL格式支持
match = PS2_OPL_REGEX.match(fs_name)
if platform_moby_id == PS2_MOBY_ID and match:
search_term = await self._ps2_opl_format(match, search_term)
# Sony序列号格式支持
match = SONY_SERIAL_REGEX.search(fs_name, re.IGNORECASE)
if platform_moby_id == PS1_MOBY_ID and match:
search_term = await self._ps1_serial_format(match, search_term)
# Switch TitleID格式支持
match = SWITCH_TITLEDB_REGEX.search(fs_name)
if platform_moby_id == SWITCH_MOBY_ID and match:
search_term, index_entry = await self._switch_titledb_format(match, search_term)
数据聚合与冲突解决
当从多个源获取数据时,RomM采用优先级策略来解决数据冲突:
| 数据字段 | 优先级 | 说明 |
|---|---|---|
| 游戏名称 | IGDB > Screenscraper > MobyGames | 名称一致性 |
| 游戏封面 | Screenscraper > IGDB > MobyGames | 视觉质量优先 |
| 游戏描述 | MobyGames > IGDB > Screenscraper | 内容专业性 |
| 评分信息 | IGDB > MobyGames > Screenscraper | 评分权威性 |
配置与认证管理
每个数据提供商都需要相应的认证配置:
# 环境变量配置
IGDB_CLIENT_ID = os.environ.get("IGDB_CLIENT_ID", "")
IGDB_CLIENT_SECRET = os.environ.get("IGDB_CLIENT_SECRET", "")
SCREENSCRAPER_USER = os.environ.get("SCREENSCRAPER_USER", "")
SCREENSCRAPER_PASSWORD = os.environ.get("SCREENSCRAPER_PASSWORD", "")
MOBYGAMES_API_KEY = os.environ.get("MOBYGAMES_API_KEY", "")
错误处理与重试机制
每个处理器都实现了完善的错误处理:
async def _request(self, url: str, data: str) -> list:
try:
res = await httpx_client.post(url, content=data, headers=self.headers, timeout=120)
res.raise_for_status()
return res.json()
except httpx.HTTPStatusError as exc:
if exc.response.status_code == 401:
log.info("Token invalid: fetching new one...")
token = await self.twitch_auth._update_twitch_token()
self.headers["Authorization"] = f"Bearer {token}"
# 重试逻辑
这种多源数据聚合架构不仅提供了丰富的游戏元数据,还通过智能匹配和冲突解决机制确保了数据质量,为RomM用户提供了卓越的游戏库管理体验。
游戏元数据自动获取与匹配算法
RomM作为一款专业的自托管ROM管理器,其核心功能之一就是能够自动从多个元数据源获取游戏信息并实现精准匹配。该系统采用了先进的哈希匹配、文件名智能解析和多源数据聚合算法,确保即使面对命名不规范或文件损坏的ROM文件也能实现高精度的元数据匹配。
多层级匹配策略架构
RomM采用分层匹配策略,从最高精度的哈希匹配到基于文件名的模糊搜索,构建了一个完整的匹配体系:
哈希匹配算法实现
RomM集成了两个专业的哈希匹配服务:Playmatch和Hasheous,通过文件哈希值实现精确匹配:
Playmatch哈希服务:
async def lookup_rom(self, files: list[RomFile]) -> PlaymatchRomMatch:
first_file = next((file for file in files if file.file_size_bytes > 0), None)
if first_file is None:
return PlaymatchRomMatch(igdb_id=None)
response = await self._request(
self.identify_url,
{
"fileName": first_file.file_name,
"fileSize": first_file.file_size_bytes,
"md5": first_file.md5_hash,
"sha1": first_file.sha1_hash,
},
)
Hasheous哈希服务: 支持多种哈希算法和数字签名验证,提供更全面的匹配能力:
| 哈希类型 | 支持情况 | 匹配精度 |
|---|---|---|
| MD5 | ✅ 支持 | 高 |
| SHA1 | ✅ 支持 | 高 |
| CRC32 | ✅ 支持 | 中 |
| 文件大小 | ✅ 支持 | 中 |
文件名智能解析算法
当哈希匹配失败时,RomM采用先进的文件名解析算法:
基础规范化处理:
def _normalize_search_term(name: str, remove_articles: bool = True,
remove_punctuation: bool = True) -> str:
# 移除冠词(a, an, the)
name = LEADING_ARTICLE_PATTERN.sub("", name)
name = COMMA_ARTICLE_PATTERN.sub("", name)
# 移除标点符号和规范化空格
name = NON_WORD_SPACE_PATTERN.sub(" ", name)
name = MULTIPLE_SPACE_PATTERN.sub(" ", name)
# Unicode标准化和重音移除
normalized = unicodedata.normalize("NFD", name)
name = "".join(c for c in normalized if not unicodedata.combining(c))
return name.strip()
特殊格式识别: RomM能够识别多种特殊文件命名格式:
- Sony序列号格式:
SLUS-12345→ 自动查询PS1/PS2/PSP游戏数据库 - Switch标题ID:
0100000000010000→ 通过TitleDB查询游戏信息 - PS2 OPL格式:
SLUS_123.45→ OpenPS2Loader格式解析 - MAME ROM名称:直接匹配MAME XML数据库
多源API并行查询与结果聚合
RomM支持同时向多个元数据源发起查询,并通过智能算法选择最佳结果:
并行查询机制:
async def scan_rom(scan_type: ScanType, platform: Platform,
rom: Rom, fs_rom: FSRom, metadata_sources: list[str]):
# 并行执行多个元数据源查询
tasks = []
if MetadataSource.IGDB in metadata_sources:
tasks.append(fetch_igdb_rom(playmatch_hash_match, hasheous_hash_match))
if MetadataSource.SS in metadata_sources:
tasks.append(fetch_ss_rom())
if MetadataSource.MOBY in metadata_sources:
tasks.append(fetch_moby_rom())
results = await asyncio.gather(*tasks)
结果评分与选择算法: RomM采用加权评分系统选择最佳匹配结果:
| 匹配因素 | 权重 | 说明 |
|---|---|---|
| 精确哈希匹配 | 1.0 | 最高优先级 |
| 文件名完全匹配 | 0.9 | 名称完全相同 |
| 平台一致性 | 0.8 | 目标平台匹配 |
| 发布日期 | 0.7 | 发布日期接近 |
| 用户评分 | 0.6 | 评分高的优先 |
平台识别与映射系统
RomM内置了强大的平台识别系统,支持400+游戏平台:
平台识别流程:
- 通过文件系统slug识别基础平台
- 查询平台绑定配置(用户自定义映射)
- 检查平台版本关系(如"ps2-psx" → "ps2")
- 从多个元数据源获取平台元数据
- 聚合最佳平台信息
智能缓存与性能优化
RomM实现了多级缓存系统提升匹配性能:
Redis缓存策略:
- 查询结果缓存:24小时
- 平台信息缓存:永久(除非配置变更)
- 哈希匹配结果:72小时
- 文件索引数据:内存缓存+磁盘持久化
批量处理优化:
# 使用批量处理减少API调用
async def batch_process_roms(roms: list[Rom]):
with sync_cache.pipeline() as pipe:
for data_batch in batched(roms.items(), 2000, strict=False):
data_map = {k: json.dumps(v) for k, v in dict(data_batch).items()}
pipe.hset("romm:rom_cache", mapping=data_map)
pipe.execute()
错误处理与降级策略
RomM设计了完善的错误处理机制确保系统稳定性:
- API故障转移:当主要元数据源不可用时自动切换到备用源
- 超时控制:所有外部API调用都有超时限制(默认120秒)
- 重试机制:对临时性失败自动重试(最多2次)
- 降级处理:当所有元数据源都不可用时使用本地缓存或文件名解析
匹配质量评估与反馈
系统会记录每次匹配的质量指标:
| 指标类型 | 收集方式 | 用途 |
|---|---|---|
| 匹配置信度 | 算法计算 | 评估匹配质量 |
| 用户确认率 | 用户交互 | 优化算法参数 |
| 失败原因 | 错误日志 | 改进匹配逻辑 |
| API响应时间 | 性能监控 | 优化服务选择 |
通过这套复杂的多源数据聚合与智能匹配算法,RomM能够实现高达95%以上的自动匹配准确率,极大减少了用户手动匹配的工作量,为游戏收藏管理提供了强大的技术支持。
SteamGridDB自定义封面支持
在RomM元数据系统中,SteamGridDB作为重要的自定义封面资源提供者,为用户提供了丰富的游戏封面、图标和背景图像选择。通过深度集成SteamGridDB API,RomM能够智能匹配并获取高质量的自定义游戏封面,极大地提升了游戏库的视觉体验。
核心架构设计
RomM通过模块化的服务架构实现对SteamGridDB的集成,主要包含以下几个核心组件:
API认证与请求处理
SteamGridDB服务的认证机制通过中间件模式实现,确保每个请求都携带正确的API密钥:
async def auth_middleware(
req: aiohttp.ClientRequest, handler: aiohttp.ClientHandlerType
) -> aiohttp.ClientResponse:
"""SteamGridDB API认证中间件"""
req.headers["Authorization"] = f"Bearer {STEAMGRIDDB_API_KEY}"
return await handler(req)
智能搜索与匹配算法
RomM实现了先进的游戏名称匹配算法,通过Levenshtein距离计算来找到最匹配的SteamGridDB游戏条目:
async def get_details_by_names(self, game_names: list[str]) -> SGDBRom:
for game_name in game_names:
search_term = self.normalize_search_term(game_name, remove_articles=False)
games = await self.sgdb_service.search_games(term=search_term)
# 计算编辑距离并排序匹配结果
game_distances = []
for game in games:
game_name_normalized = self.normalize_search_term(
game["name"], remove_articles=False
)
distance = levenshtein_distance(
game_name_normalized, search_term_normalized
)
game_distances.append((game, distance))
game_distances.sort(key=lambda x: x[1])
# 在阈值范围内选择最佳匹配
for game, distance in game_distances:
if distance <= self.max_levenshtein_distance:
# 获取游戏封面资源
game_details = await self._get_game_covers(
game_id=game["id"],
game_name=game["name"],
types=(SGDBType.STATIC,),
is_nsfw=False,
is_humor=False,
is_epilepsy=False,
)
# 返回第一个有效资源
first_resource = next(
(res for res in game_details["resources"] if res["url"]), None
)
if first_resource:
return SGDBRom(
sgdb_id=game["id"], url_cover=first_resource["url"]
)
封面资源获取与过滤
系统支持多种封面类型和尺寸的获取,并提供丰富的过滤选项:
| 参数类型 | 可选值 | 说明 |
|---|---|---|
| dimensions | STEAM_VERTICAL, GOG_GALAXY_TILE, GOG_GALAXY_COVER, SQUARE_512, SQUARE_1024 | 封面尺寸规格 |
| types | STATIC, ANIMATED | 封面类型(静态/动态) |
| is_nsfw | true, false, "any" | 是否包含NSFW内容 |
| is_humor | true, false, "any" | 是否包含幽默内容 |
| is_epilepsy | true, false, "any" | 是否包含闪光内容 |
async def _get_game_covers(
self,
game_id: int,
game_name: str,
dimensions: tuple[SGDBDimension, ...] = (
SGDBDimension.STEAM_VERTICAL,
SGDBDimension.GOG_GALAXY_TILE,
SGDBDimension.GOG_GALAXY_COVER,
SGDBDimension.SQUARE_512,
SGDBDimension.SQUARE_1024,
),
types: tuple[SGDBType, ...] = (SGDBType.STATIC, SGDBType.ANIMATED),
is_nsfw: bool | None = None,
is_humor: bool | None = None,
is_epilepsy: bool | None = None,
) -> SGDBResult:
"""获取指定游戏的封面资源"""
game_covers = [
cover
async for cover in self.sgdb_service.iter_grids_for_game(
game_id=game_id,
dimensions=dimensions,
types=types,
is_nsfw=is_nsfw,
is_humor=is_humor,
is_epilepsy=is_epilepsy,
)
]
return SGDBResult(
name=game_name,
resources=[
SGDBResource(
thumb=cover["thumb"],
url=cover["url"],
type="animated" if cover["thumb"].endswith(".webm") else "static",
)
for cover in game_covers
],
)
批量处理与性能优化
RomM采用异步编程模型和批量处理机制来优化SteamGridDB API的调用性能:
错误处理与容错机制
系统实现了完善的错误处理机制,确保在API调用失败时仍能正常运作:
async def _request(self, url: str, request_timeout: int = 120) -> dict:
try:
res = await aiohttp_session.get(
url,
middlewares=(auth_middleware,),
timeout=ClientTimeout(total=request_timeout),
)
res.raise_for_status()
return await res.json()
except aiohttp.ClientResponseError as exc:
if exc.status == http.HTTPStatus.UNAUTHORIZED:
raise SGDBInvalidAPIKeyException from exc
# 记录错误但返回空字典继续执行
log.error(exc)
return {}
except json.decoder.JSONDecodeError as exc:
log.error("Failed to decode JSON response from SteamGridDB: %s", str(exc))
return {}
配置与启用
SteamGridDB服务的启用依赖于环境配置中的API密钥:
# 用于在前端显示SteamGridDB API状态
STEAMGRIDDB_API_ENABLED: Final = bool(STEAMGRIDDB_API_KEY)
当API密钥未配置时,系统会优雅地跳过SteamGridDB相关的功能,确保核心功能的正常运行。
通过这种深度集成,RomM为用户提供了强大的自定义封面管理能力,使得游戏库的视觉呈现更加个性化和专业化。系统不仅能够自动匹配和获取封面,还提供了丰富的过滤和选择选项,满足不同用户的个性化需求。
RetroAchievements成就系统集成
RomM通过深度集成RetroAchievements平台,为复古游戏爱好者提供了完整的成就系统体验。这一集成不仅让玩家能够追踪自己的游戏成就进度,还提供了丰富的元数据增强功能,让游戏库管理变得更加智能和有趣。
核心架构设计
RomM的RetroAchievements集成采用了模块化的架构设计,主要包含以下几个核心组件:
API认证与请求处理
RomM实现了完整的RetroAchievements API认证机制,通过中间件模式处理所有API请求:
async def auth_middleware(
req: aiohttp.ClientRequest, handler: aiohttp.ClientHandlerType
) -> aiohttp.ClientResponse:
"""RetroAchievements API认证中间件"""
req.url = req.url.update_query({"y": RETROACHIEVEMENTS_API_KEY})
return await handler(req)
这种设计确保了所有请求都自动包含必要的认证参数,同时提供了完善的错误处理和重试机制:
| 错误类型 | 处理策略 | 重试机制 |
|---|---|---|
| 连接错误 | 返回503状态码 | 不重试 |
| 超时错误 | 自动重试一次 | 2秒后重试 |
| 速率限制 | 等待2秒后重试 | 自动重试 |
| JSON解析错误 | 返回空字典 | 不重试 |
游戏匹配与元数据提取
RomM使用智能哈希匹配算法来识别游戏并获取对应的成就数据:
async def _search_rom(self, rom: Rom, ra_hash: str) -> RAGameListItem | None:
"""通过哈希值搜索RetroAchievements游戏"""
if not rom.platform.ra_id:
return None
# 获取平台所有游戏哈希列表
roms = await self.ra_service.get_game_list(
system_id=rom.platform.ra_id,
only_games_with_achievements=True,
include_hashes=True,
)
# 哈希匹配算法
for r in roms:
if ra_hash in r.get("Hashes", ()):
return r
return None
成就数据结构
RomM定义了完整的成就数据类型结构,确保数据的完整性和一致性:
interface RAGameRomAchievement {
ra_id: number | null;
title: string | null;
description: string | null;
points: number | null;
num_awarded: number | null;
num_awarded_hardcore: number | null;
badge_id: string | null;
badge_url_lock: string | null;
badge_path_lock: string | null;
badge_url: string | null;
badge_path: string | null;
display_order: number | null;
type: string | null;
}
interface RAMetadata {
first_release_date: number | null;
genres: string[];
companies: string[];
achievements: RAGameRomAchievement[];
}
用户进度追踪
RomM提供了完整的用户成就进度追踪功能,支持批量获取和分页处理:
async def iter_user_completion_progress(
self,
username: str,
) -> AsyncIterator[RAUserCompletionProgressResult]:
"""迭代获取用户完成进度"""
page_size = 500 # API最大分页大小
offset = 0
while True:
response = await self.get_user_completion_progress(
username,
limit=page_size,
offset=offset or None,
)
# 处理当前页结果
for result in response["Results"]:
yield result
# 分页控制
offset += len(response["Results"])
if len(response["Results"]) < page_size or offset >= response["Total"]:
break
本地缓存机制
为了提高性能和减少API调用,RomM实现了智能的本地缓存系统:
缓存策略配置:
# 缓存刷新天数配置(默认7天)
REFRESH_RETROACHIEVEMENTS_CACHE_DAYS = 7
# 缓存文件路径生成
def _get_hashes_file_path(self, platform_id: int) -> str:
platform_resources_path = fs_resource_handler.get_platform_resources_path(platform_id)
return os.path.join(platform_resources_path, "ra_hashes.json")
徽章资源管理
RomM自动下载和管理成就徽章资源,提供本地和远程访问支持:
def extract_metadata_from_rom_details(rom: Rom, rom_details: RAGameExtendedDetails) -> RAMetadata:
"""从游戏详情中提取元数据和成就信息"""
achievements = []
for achievement in rom_details.get("Achievements", {}).values():
badge_name = achievement.get("BadgeName", "")
achievements.append(RAGameRomAchievement(
badge_id=badge_name,
badge_url_lock=f"https://media.retroachievements.org/Badge/{badge_name}_lock.png",
badge_path_lock=f"{fs_resource_handler.get_ra_badges_path(rom.platform.id, rom.id)}/{badge_name}_lock.png",
badge_url=f"https://media.retroachievements.org/Badge/{badge_name}.png",
badge_path=f"{fs_resource_handler.get_ra_badges_path(rom.platform.id, rom.id)}/{badge_name}.png",
# ... 其他成就字段
))
return RAMetadata(achievements=achievements)
平台支持与映射
RomM维护了完整的平台映射表,确保不同平台游戏能够正确匹配到RetroAchievements系统:
| RomM平台Slug | RetroAchievements ID | 平台名称 |
|---|---|---|
| nes | 1 | Nintendo Entertainment System |
| snes | 2 | Super Nintendo Entertainment System |
| n64 | 3 | Nintendo 64 |
| gb | 4 | Game Boy |
| gbc | 5 | Game Boy Color |
| gba | 6 | Game Boy Advance |
| genesis | 7 | Sega Genesis |
| segacd | 8 | Sega CD |
| saturn | 9 | Sega Saturn |
| psx | 10 | PlayStation |
性能优化策略
RomM采用了多种性能优化策略来确保成就系统的流畅运行:
- 批量处理:使用分页和迭代器模式处理大量数据
- 缓存机制:本地缓存减少API调用次数
- 异步处理:全异步架构提高并发性能
- 错误恢复:智能重试机制处理临时故障
- 资源懒加载:按需加载成就徽章等资源
配置与集成
要启用RetroAchievements集成,用户需要在配置文件中设置API密钥:
retroachievements:
api_key: "your_retroachievements_api_key_here"
cache_days: 7
enabled: true
前端界面通过专门的store来管理RetroAchievements状态:
// 前端心跳检测RetroAchievements状态
const heartbeatStore = useHeartbeatStore()
heartbeatStore.services.push({
name: "RetroAchievements",
status: "unknown",
enabled: !!config.retroachievements?.api_key
})
通过这样深度而全面的集成,RomM为复古游戏收藏家提供了一个强大而完善的成就系统解决方案,让游戏收藏和管理变得更加有趣和有意义。
总结
RomM元数据系统通过深度集成多个专业游戏数据库,构建了一个完整的多源数据聚合与智能匹配解决方案。系统不仅提供了丰富的游戏元数据,还通过智能匹配算法、冲突解决机制和性能优化策略确保了数据质量和系统稳定性。从基础的游戏信息到自定义封面和成就系统,RomM为游戏收藏管理提供了全方位的技术支持,实现了高达95%以上的自动匹配准确率,极大提升了游戏库管理的体验和效率。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



