mirror of
https://github.com/sebastiandine/Card-Collection-Manager-3.git
synced 2026-08-28 22:01:12 +00:00
240 lines
7.6 KiB
Python
240 lines
7.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Harvest Bulbapedia Unnumbered Promotional cards into classic_missing seed JSON.
|
|
|
|
Fetches:
|
|
- Unnumbered_Promotional_cards_(TCG)/1996-2005
|
|
- Yearly sections on Unnumbered_Promotional_cards_(TCG) (2006+)
|
|
|
|
Writes/updates:
|
|
- tools/pokemon_jp/classic_missing_sets.json (adds UnnumberedPromo)
|
|
- tools/pokemon_jp/classic_missing_prints.json (replaces UnnumberedPromo prints)
|
|
|
|
Synthetic local_ids are sequential 001… (cards are unnumbered in print).
|
|
Each print carries a qualified English title when the setlist row has a
|
|
{{TCG ID|Set|Name|num}} (e.g. "Mewtwo (CoroCoro promo)") plus a
|
|
`bulbapedia_page` hint for enrich_unnumbered_promo_images.py.
|
|
|
|
Run after harvest:
|
|
python tools/pokemon_jp/enrich_unnumbered_promo_images.py
|
|
python tools/pokemon_jp/merge_classic_missing.py
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
import urllib.parse
|
|
import urllib.request
|
|
from pathlib import Path
|
|
|
|
HERE = Path(__file__).resolve().parent
|
|
SETS = HERE / "classic_missing_sets.json"
|
|
PRINTS = HERE / "classic_missing_prints.json"
|
|
|
|
SET_ID = "UnnumberedPromo"
|
|
SET_META = {
|
|
"name_en": "Unnumbered Promotional cards",
|
|
"name_ja": "番号なしプロモーションカード",
|
|
"releaseDate": "1997/03/06",
|
|
}
|
|
|
|
UA = "CCM3-pokemon-jp-etl/1.0 (local; +https://github.com/sebastiandine/Card-Collection-Manager-3)"
|
|
|
|
# {{TCG ID|Set|Name|num}} — name may contain δ / & etc.
|
|
TCG_ID_FULL_RE = re.compile(
|
|
r"\{\{TCG ID\|([^}|]+)\|([^}|]+)(?:\|([^}|]*))?\}\}", re.IGNORECASE
|
|
)
|
|
TCG_RE = re.compile(r"\{\{TCG\|([^}|]+)(?:\|[^}]*)?\}\}", re.IGNORECASE)
|
|
OBP_RE = re.compile(r"\{\{OBP\|([^}|]+)(?:\|[^}]*)?\}\}", re.IGNORECASE)
|
|
SMALL_TAG_RE = re.compile(
|
|
r"<small>\s*'''?\s*\[([^\]]+)\]\s*'''?\s*</small>", re.IGNORECASE
|
|
)
|
|
ITALIC_NOTE_RE = re.compile(r"\(''([^']+)''\)")
|
|
HTML_TAG_RE = re.compile(r"<[^>]+>")
|
|
TEMPLATE_RE = re.compile(r"\{\{[^{}]*\}\}")
|
|
|
|
|
|
def fetch_wikitext(page: str) -> str:
|
|
qs = urllib.parse.urlencode(
|
|
{
|
|
"action": "parse",
|
|
"page": page,
|
|
"prop": "wikitext",
|
|
"format": "json",
|
|
}
|
|
)
|
|
url = f"https://bulbapedia.bulbagarden.net/w/api.php?{qs}"
|
|
req = urllib.request.Request(url, headers={"User-Agent": UA})
|
|
with urllib.request.urlopen(req, timeout=90) as resp:
|
|
data = json.load(resp)
|
|
return data["parse"]["wikitext"]["*"]
|
|
|
|
|
|
def decode_wiki_text(s: str) -> str:
|
|
return (
|
|
s.replace("&", "&")
|
|
.replace("<", "<")
|
|
.replace(">", ">")
|
|
.replace("'", "'")
|
|
.strip()
|
|
)
|
|
|
|
|
|
def bulbapedia_page_from_tcg_id(set_name: str, card_name: str, num: str) -> str:
|
|
"""Best-effort Bulbapedia article title for a TCG ID triple."""
|
|
set_name = decode_wiki_text(set_name)
|
|
card_name = decode_wiki_text(card_name)
|
|
num = decode_wiki_text(num or "").strip()
|
|
if not num or num.lower() == "promo":
|
|
return f"{card_name} ({set_name} promo)"
|
|
return f"{card_name} ({set_name} {num})"
|
|
|
|
|
|
def qualified_name_en(card_name: str, set_name: str, num: str, extras: list[str]) -> str:
|
|
"""Distinct English title: Name (Set promo) plus optional [Jumbo]/ markers."""
|
|
card_name = decode_wiki_text(card_name)
|
|
set_name = decode_wiki_text(set_name)
|
|
num = decode_wiki_text(num or "").strip()
|
|
if set_name:
|
|
if not num or num.lower() == "promo":
|
|
base = f"{card_name} ({set_name} promo)"
|
|
else:
|
|
base = f"{card_name} ({set_name} {num})"
|
|
else:
|
|
base = card_name
|
|
# Avoid duplicating qualifier already present in extras.
|
|
remaining = [
|
|
e
|
|
for e in extras
|
|
if e.lower() not in base.lower() and e.lower() not in {"promo"}
|
|
]
|
|
if remaining:
|
|
return f"{base} ({'; '.join(remaining)})"
|
|
return base
|
|
|
|
|
|
def collect_extras(raw: str) -> list[str]:
|
|
extras: list[str] = []
|
|
for m in SMALL_TAG_RE.finditer(raw):
|
|
extras.append(m.group(1).strip())
|
|
for m in ITALIC_NOTE_RE.finditer(raw):
|
|
extras.append(m.group(1).strip())
|
|
if "Jumbo" in raw and not any("jumbo" in e.lower() for e in extras):
|
|
extras.append("Jumbo")
|
|
if "Mini" in raw and not any("mini" in e.lower() for e in extras):
|
|
extras.append("Mini")
|
|
return extras
|
|
|
|
|
|
def extract_entry(field: str) -> dict | None:
|
|
"""Parse one Setlist/nmentry name field into print metadata."""
|
|
raw = field.strip()
|
|
extras = collect_extras(raw)
|
|
|
|
tcg = TCG_ID_FULL_RE.search(raw)
|
|
if tcg:
|
|
set_name = tcg.group(1).strip()
|
|
card_name = tcg.group(2).strip()
|
|
num = (tcg.group(3) or "").strip()
|
|
name_en = qualified_name_en(card_name, set_name, num, extras)
|
|
page = bulbapedia_page_from_tcg_id(set_name, card_name, num)
|
|
return {
|
|
"name_en": name_en,
|
|
"bulbapedia_page": page,
|
|
"tcg_set": decode_wiki_text(set_name),
|
|
"tcg_name": decode_wiki_text(card_name),
|
|
"tcg_num": decode_wiki_text(num) if num else "promo",
|
|
}
|
|
|
|
name = None
|
|
for rx in (TCG_RE, OBP_RE):
|
|
m = rx.search(raw)
|
|
if m:
|
|
name = decode_wiki_text(m.group(1))
|
|
break
|
|
if not name:
|
|
cleaned = TEMPLATE_RE.sub("", raw)
|
|
cleaned = HTML_TAG_RE.sub("", cleaned)
|
|
cleaned = cleaned.split("|", 1)[0].strip()
|
|
cleaned = re.sub(r"\[\[([^|\]]+)(?:\|[^\]]+)?\]\]", r"\1", cleaned)
|
|
name = decode_wiki_text(cleaned.strip(" '\""))
|
|
if not name:
|
|
return None
|
|
|
|
if extras:
|
|
name = f"{name} ({'; '.join(extras)})"
|
|
return {"name_en": name, "bulbapedia_page": name}
|
|
|
|
|
|
def harvest_entries(wikitext: str) -> list[dict]:
|
|
entries: list[dict] = []
|
|
for m in re.finditer(r"\{\{Setlist/nmentry\|None\|", wikitext):
|
|
start = m.end()
|
|
depth = 0
|
|
i = start
|
|
while i < len(wikitext):
|
|
if wikitext.startswith("{{", i):
|
|
depth += 1
|
|
i += 2
|
|
continue
|
|
if wikitext.startswith("}}", i):
|
|
depth = max(0, depth - 1)
|
|
i += 2
|
|
continue
|
|
if wikitext[i] == "|" and depth == 0:
|
|
break
|
|
i += 1
|
|
parsed = extract_entry(wikitext[start:i])
|
|
if parsed:
|
|
entries.append(parsed)
|
|
return entries
|
|
|
|
|
|
def main() -> None:
|
|
pages = [
|
|
"Unnumbered_Promotional_cards_(TCG)/1996-2005",
|
|
"Unnumbered_Promotional_cards_(TCG)",
|
|
]
|
|
all_entries: list[dict] = []
|
|
for page in pages:
|
|
wt = fetch_wikitext(page)
|
|
got = harvest_entries(wt)
|
|
print(f"{page}: {len(got)} setlist rows")
|
|
all_entries.extend(got)
|
|
|
|
prints: list[dict] = []
|
|
for i, entry in enumerate(all_entries, start=1):
|
|
row = {
|
|
"set_id": SET_ID,
|
|
"local_id": f"{i:03d}",
|
|
"name_en": entry["name_en"],
|
|
"name_ja": "",
|
|
"name_en_source": "manual",
|
|
"bulbapedia_page": entry.get("bulbapedia_page") or entry["name_en"],
|
|
}
|
|
for key in ("tcg_set", "tcg_name", "tcg_num"):
|
|
if entry.get(key):
|
|
row[key] = entry[key]
|
|
prints.append(row)
|
|
|
|
sets_obj: dict = json.loads(SETS.read_text(encoding="utf-8"))
|
|
sets_obj[SET_ID] = SET_META
|
|
SETS.write_text(
|
|
json.dumps(sets_obj, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
|
|
)
|
|
|
|
existing: list[dict] = json.loads(PRINTS.read_text(encoding="utf-8"))
|
|
kept = [p for p in existing if str(p.get("set_id")) != SET_ID]
|
|
kept.extend(prints)
|
|
PRINTS.write_text(
|
|
json.dumps(kept, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
|
|
)
|
|
print(
|
|
f"wrote {SET_ID}: {len(prints)} prints "
|
|
f"(classic_missing_prints total={len(kept)})"
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|