163 lines
5.8 KiB
Python
163 lines
5.8 KiB
Python
#!/usr/bin/env python3
|
||
"""Скачать арты триальной деки с Cardfight!! Vanguard Wiki.
|
||
|
||
Пример:
|
||
python tools/import_card_art.py \
|
||
--url "https://cardfight.fandom.com/wiki/Trial_Deck_1:_Blaster_Blade" \
|
||
--output client/Assets/Vanguard/Resources/CardArt
|
||
|
||
Fandom иногда отвечает Cloudflare 403 на запросы не из браузера. В этом
|
||
случае сохраните HTML/API-ответ страницы из браузера и передайте:
|
||
python tools/import_card_art.py --html saved.html --output ...
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import html
|
||
import json
|
||
import re
|
||
import sys
|
||
import urllib.parse
|
||
import urllib.request
|
||
from pathlib import Path
|
||
|
||
|
||
def fetch(url: str) -> str:
|
||
request = urllib.request.Request(
|
||
url,
|
||
headers={
|
||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
||
"AppleWebKit/537.36 Chrome/126 Safari/537.36",
|
||
"Accept": "text/html,application/json;q=0.9,*/*;q=0.8",
|
||
},
|
||
)
|
||
with urllib.request.urlopen(request, timeout=60) as response:
|
||
return response.read().decode("utf-8", errors="replace")
|
||
|
||
|
||
def page_html(url: str, local_html: str | None) -> str:
|
||
if local_html:
|
||
raw = Path(local_html).read_text(encoding="utf-8", errors="replace")
|
||
if raw.lstrip().startswith("{"):
|
||
payload = json.loads(raw)
|
||
return payload["parse"]["text"]
|
||
return raw
|
||
|
||
# Пытаемся через API: он отдаёт HTML статьи без рекламной обвязки.
|
||
page = url.split("/wiki/", 1)[-1]
|
||
api = (
|
||
"https://cardfight.fandom.com/api.php?action=parse&format=json"
|
||
"&formatversion=2&prop=text&page=" + urllib.parse.quote(page)
|
||
)
|
||
raw = fetch(api)
|
||
payload = json.loads(raw)
|
||
if "error" in payload:
|
||
raise RuntimeError(payload["error"].get("info", "Wiki API error"))
|
||
return payload["parse"]["text"]
|
||
|
||
|
||
def set_gallery_links(page: str) -> list[str]:
|
||
links = re.findall(r'href="(/wiki/Set_Gallery:[^"]+)"', page)
|
||
return list(dict.fromkeys("https://cardfight.fandom.com" + link for link in links))
|
||
|
||
|
||
def image_candidates(html_text: str, code: str) -> list[str]:
|
||
"""Ищем картинку, у которой alt начинается с кода принта."""
|
||
pattern = re.compile(r"<img\b[^>]*>", re.I)
|
||
result: list[tuple[int, str]] = []
|
||
for tag in pattern.findall(html_text):
|
||
alt_match = re.search(r'\balt="([^"]*)"', tag, re.I)
|
||
src_match = re.search(r'\bsrc="([^"]*)"', tag, re.I)
|
||
if not alt_match or not src_match:
|
||
continue
|
||
|
||
alt = html.unescape(alt_match.group(1))
|
||
src = html.unescape(src_match.group(1))
|
||
if not alt.startswith(code) or not src.startswith("https://static.wikia.nocookie.net/"):
|
||
continue
|
||
|
||
score = 0
|
||
if "Sample" in alt:
|
||
score += 10
|
||
if "Holo" in alt or "Parallel" in alt:
|
||
score += 5
|
||
result.append((score, src))
|
||
|
||
result.sort(key=lambda item: item[0])
|
||
urls = []
|
||
for _, src in result:
|
||
# Берём оригинал, а не превью 126px.
|
||
src = re.sub(r"/scale-to-width-down/\d+", "", src)
|
||
src = src.split("?")[0] + "?format=original"
|
||
if src not in urls:
|
||
urls.append(src)
|
||
return urls
|
||
|
||
|
||
def save_image(url: str, output: Path, image_key: str) -> None:
|
||
extension = ".png" if ".png" in url.lower() else ".jpg"
|
||
target = output / (image_key + extension)
|
||
if target.exists():
|
||
return
|
||
|
||
request = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
|
||
with urllib.request.urlopen(request, timeout=60) as response:
|
||
data = response.read()
|
||
target.write_bytes(data)
|
||
print(f" {target.name} ({len(data) // 1024} KiB)")
|
||
|
||
|
||
def main() -> int:
|
||
parser = argparse.ArgumentParser()
|
||
parser.add_argument("--url", required=False, help="Страница триал-деки")
|
||
parser.add_argument("--html", help="Сохранённый HTML/API-ответ страницы")
|
||
parser.add_argument("--output", required=True, help="Папка CardArt")
|
||
args = parser.parse_args()
|
||
|
||
if not args.url and not args.html:
|
||
parser.error("нужен --url или --html")
|
||
|
||
output = Path(args.output)
|
||
output.mkdir(parents=True, exist_ok=True)
|
||
|
||
source_url = args.url or "https://cardfight.fandom.com/wiki/"
|
||
page = page_html(source_url, args.html)
|
||
|
||
galleries = set_gallery_links(page)
|
||
if not galleries:
|
||
print("Не нашёл ссылку Set_Gallery на странице.", file=sys.stderr)
|
||
return 1
|
||
|
||
# Английские VGE-галереи приоритетнее, чтобы не перезаписать арт
|
||
# японской/тайский версией.
|
||
galleries.sort(key=lambda value: 0 if "/VGE-" in value else 1 if "/VG-" in value else 2)
|
||
|
||
downloaded = 0
|
||
for gallery in galleries:
|
||
try:
|
||
gallery_html = page_html(gallery, None)
|
||
except Exception as exc: # noqa: BLE001
|
||
print(f"Пропуск {gallery}: {exc}", file=sys.stderr)
|
||
continue
|
||
|
||
# Коды вида TD01/001EN, alt картинки: "TD01/001EN Name".
|
||
codes = sorted(set(re.findall(r"\b(TD\d{2}/\d{3})(?:EN)?\b", gallery_html)))
|
||
for code in codes:
|
||
urls = image_candidates(gallery_html, code)
|
||
if not urls:
|
||
continue
|
||
image_key = code.lower().replace("/", "_")
|
||
try:
|
||
save_image(urls[0], output, image_key)
|
||
downloaded += 1
|
||
except Exception as exc: # noqa: BLE001
|
||
print(f"Ошибка {code}: {exc}", file=sys.stderr)
|
||
|
||
print(f"Готово: обработано карт {downloaded}.")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|