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)")