n64-tracker/fix_coverart_mapping.py

187 lines
9.2 KiB
Python

import json
import os
import re
import unicodedata
COVERART_DIR = "/Users/matt/Desktop/VideoGameList/Coverart"
GAMES_JSON = "/Users/matt/Desktop/VideoGameList/games.json"
# ---------------------------------------------------------------------------
# Manual overrides: games.json title -> coverart stem (without -01.png)
# ---------------------------------------------------------------------------
MANUAL_OVERRIDES = {
"1080° Snowboarding": "1080 Snowboarding",
"Aero Fighters Assault": "AeroFighters Assault",
"All Star Tennis '99": "All Star Tennis 99",
"All-Star Baseball '99": "All-Star Baseball 99",
"Bass Hunter 64": "In-Fisherman Bass Hunter 64",
"Beetle Adventure Racing": "Beetle Adventure Racing!",
"Bomberman 64 1997": "Bomberman 64",
"Bust-A-Move 3 DX": "Bust-A-Move _99",
"ClayFighter 63\u2153": "Clay Fighter 63 1_3",
"ClayFighter: Sculptor's Cut": "Clay Fighter_ Sculptor_s Cut",
"Daffy Duck Starring as Duck Dodgers": "Looney Tunes_ Duck Dodgers_ Starring Daffy Duck",
"Daikatana": "John Romero_s Daikatana",
"Disney's Tarzan": "Tarzan",
"Donald Duck: Goin Quackers": "Donald Duck_ Goin_ Quackers",
"Duke Nukem Zero Hour": "Duke Nukem_ Zero Hour",
"Extreme-G 2": "Extreme-G_ XG2",
"FIFA '99": "FIFA 99",
"FIFA 64": "FIFA Soccer 64",
"FIFA: Road to World Cup '98": "FIFA_ Road to World Cup 98",
"G.A.S.P!! Fighters' NEXTream": "Deadly Arts",
"Holy Magic Century": "Quest 64",
"Lego Racers": "LEGO Racers",
"Madden NFL '99": "Madden NFL 99",
"Michael Owens WLS 2000": "Mia Hamm Soccer 64",
"Ms. Pac-Man: Maze Madness": "Ms. Pac-Man Maze Madness",
"Mystical Ninja Starring Goemon 2": "Goemon_s Great Adventure",
"NASCAR '99": "Nascar 99",
"NBA In The Zone '98": "NBA in the Zone _98",
"NBA In The Zone 2000": "NBA in the Zone 2000",
"NBA Jam '99": "NBA Jam 99",
"NBA Live '99": "NBA Live 99",
"NBA Pro '99": "NBA Hangtime",
"NFL Quarterback Club '98": "NFL Quarterback Club 98",
"NFL Quarterback Club '99": "NFL Quarterback Club 99",
"NFL Quarterback Club 2001": "NFL QB Club 2001",
"NHL '99": "NHL 99",
"NHL Breakaway '98": "NHL Breakaway 98",
"NHL Breakaway '99": "NHL Breakaway 99",
"NHL Pro '99": "NHL Blades of Steel _99",
"Olympic Hockey '98": "Olympic Hockey 98",
"Operation WinBack": "WinBack_ Covert Operations",
"Pokémon Puzzle League": "Pokemon Puzzle League",
"Pokémon Snap": "Pokemon Snap",
"Pokémon Stadium": "Pokemon Stadium",
"Pokémon Stadium 2": "Pokemon Stadium 2",
"Power Rangers Lightspeed Rescue": "Power Rangers_ Lightspeed Rescue",
"Quake 64": "Quake",
"Rainbow Six": "Tom Clancy_s Rainbow Six",
"Rampage World Tour": "Rampage_ World Tour",
"Ridge Racer 64": "RR64_ Ridge Racer 64",
"Rugrats: Treasure Hunt": "Rugrats_ Scavenger Hunt",
"Rush 2: Extreme Racing": "Rush 2_ Extreme Racing USA",
"San Francisco Rush": "San Francisco Rush_ Extreme Racing",
"Shadowgate 64: Trials of the Four Towers": "ShadowGate 64_ Trials of the Four Towers",
"Star Wars Episode I: Racer": "Star Wars Episode I_ Racer",
"Star Wars: Episode I Battle for Naboo": "Star Wars_ Episode I_ Battle for Naboo",
"Starcraft 64": "StarCraft 64",
"Supercross 2000": "SuperCross 2000",
"Superman 64": "Superman",
"The Powerpuff Girls: Chemical X-Traction": "The Powerpuff Girls_ Chemical X-traction",
"Tony Hawk's Skateboarding": "Tony Hawk_s Pro Skater",
"Toy Story 2: Buzz Lightyear to the Rescue":"Toy Story 2_ Buzz Lightyear to the Rescue!",
"Twisted Edge: Extreme Snowboarding": "Twisted Edge Extreme Snowboarding",
"V-Rally Edition '99": "V-Rally Edition 99",
"WCW vs. nWo: World Tour": "WCW Vs. nWo_ World Tour",
"WCW/nWo Revenge": "WCW_nWo Revenge",
"WipeOut 64": "Wipeout 64",
"World Cup '98": "World Cup 98",
"Xena: Warrior Princess": "Xena_ Warrior Princess_ The Talisman of Fate",
}
# ---------------------------------------------------------------------------
# Normalisation helper
# ---------------------------------------------------------------------------
def normalize(s):
"""Strip accents, remove degree/punctuation, lowercase, collapse spaces."""
# Decompose unicode (é -> e + combining accent) then strip combining chars
s = unicodedata.normalize("NFD", s)
s = "".join(c for c in s if unicodedata.category(c) != "Mn")
# Remove degree symbol and vulgar fraction one-third
s = s.replace("°", "").replace("\u2153", "")
# Replace underscores (used in filenames instead of ' : etc.)
s = s.replace("_", " ")
# Remove punctuation / special chars; keep alphanumerics and spaces
s = re.sub(r"[^a-z0-9 ]", "", s.lower())
# Collapse whitespace
s = re.sub(r"\s+", "", s)
return s
# ---------------------------------------------------------------------------
# Build coverart lookup { stem -> filename_with_extension }
# ---------------------------------------------------------------------------
coverart_files = {} # stem (without -01.png) -> full filename
for fname in os.listdir(COVERART_DIR):
if fname.endswith("-01.png"):
stem = fname[:-len("-01.png")]
coverart_files[stem] = fname
# Normalised lookup: normalized_stem -> stem
norm_to_stem = {normalize(stem): stem for stem in coverart_files}
# ---------------------------------------------------------------------------
# Load games
# ---------------------------------------------------------------------------
with open(GAMES_JSON, "r", encoding="utf-8") as f:
games = json.load(f)
unmatched_before = [g for g in games if not g.get("cover_art_local")]
print(f"Games without cover_art_local before fix: {len(unmatched_before)}")
print()
matched = []
unmatched = []
for game in games:
if game.get("cover_art_local"):
continue # already set, skip
title = game.get("title", "")
# --- 1. Manual override ---
if title in MANUAL_OVERRIDES:
stem = MANUAL_OVERRIDES[title]
if stem in coverart_files:
game["cover_art_local"] = f"Coverart/{coverart_files[stem]}"
matched.append((title, coverart_files[stem], "manual override"))
continue
else:
print(f" WARNING: override stem not found in Coverart: {stem!r}")
# --- 2. Fuzzy / normalised match ---
norm_title = normalize(title)
if norm_title in norm_to_stem:
stem = norm_to_stem[norm_title]
game["cover_art_local"] = f"Coverart/{coverart_files[stem]}"
matched.append((title, coverart_files[stem], "fuzzy match"))
continue
# --- 3. Try stripping trailing year (4 digits) ---
title_no_year = re.sub(r"\s+\d{4}$", "", title).strip()
norm_no_year = normalize(title_no_year)
if norm_no_year != norm_title and norm_no_year in norm_to_stem:
stem = norm_to_stem[norm_no_year]
game["cover_art_local"] = f"Coverart/{coverart_files[stem]}"
matched.append((title, coverart_files[stem], "fuzzy match (no year)"))
continue
unmatched.append(title)
# ---------------------------------------------------------------------------
# Report
# ---------------------------------------------------------------------------
print("=" * 70)
print(f"MATCHED ({len(matched)}):")
print("=" * 70)
for title, fname, method in matched:
print(f" [{method}]")
print(f" Game: {title!r}")
print(f" File: {fname!r}")
print()
print("=" * 70)
print(f"STILL UNMATCHED ({len(unmatched)}):")
print("=" * 70)
for t in unmatched:
print(f" {t!r}")
# ---------------------------------------------------------------------------
# Save updated games.json
# ---------------------------------------------------------------------------
with open(GAMES_JSON, "w", encoding="utf-8") as f:
json.dump(games, f, ensure_ascii=False, indent=2)
print()
print(f"Saved updated games.json (matched {len(matched)}/{len(unmatched_before)} previously unset entries)")