download.py 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843
  1. """
  2. Manage the logic of downloading the pokedex and source images.
  3. """
  4. import re
  5. import json
  6. import asyncio
  7. from pathlib import Path
  8. from dataclasses import dataclass, asdict
  9. from collections import defaultdict
  10. from aiohttp import ClientSession
  11. JS_TO_JSON = re.compile(r"\b([a-zA-Z][a-zA-Z0-9]*?):")
  12. # the dex from showdown assumes only strawberry alcremie, since
  13. # that's what's in showdown, but we might as well add the rest
  14. ALCREMIE_SWEETS = [
  15. "Strawberry", "Berry", "Love", "Star",
  16. "Clover", "Flower", "Ribbon",
  17. ]
  18. # https://bulbapedia.bulbagarden.net/wiki/List_of_Pok%C3%A9mon_with_gender_differences
  19. # there are some pokemon with notable gender diffs that the dex doesn't cover
  20. # judgement calls made arbitrarily
  21. GENDER_DIFFS = (
  22. "hippopotas", "hippowdon",
  23. "unfezant",
  24. "frillish", "jellicent",
  25. "pyroar",
  26. # meowstic, indeedee, basculegion, oinkologne are already handled in the dex
  27. )
  28. @dataclass
  29. class Form:
  30. name: str
  31. traits: list[str]
  32. types: list[str]
  33. color: str
  34. @dataclass
  35. class Pokemon:
  36. num: int
  37. species: str
  38. forms: list[Form]
  39. async def download_pokedex() -> dict:
  40. async with ClientSession() as session:
  41. async with session.get("https://play.pokemonshowdown.com/data/pokedex.js") as res:
  42. res.raise_for_status()
  43. text = await res.text("utf-8")
  44. # this is not json of course, but it's close
  45. # start by taking out the ; and definition
  46. cleaned = text.replace("exports.BattlePokedex = ", "").strip(";")
  47. # then convert the keys to strings
  48. converted = re.sub(JS_TO_JSON, lambda m: f'"{m.group(1)}":', cleaned)
  49. # and fix Type: Null, Farfetch'd, Sirfetch'd
  50. fixed = converted.replace(
  51. '""Type": Null"', '"Type: Null"').replace("\u2019", "'")
  52. # then, parse it
  53. return json.loads(fixed)
  54. START_IN_BASE_FORM = (
  55. "Castform", # castform can't start battle in weather forms
  56. "Cherrim", # castform can't start battle in sunshine form
  57. "Aegislash", # aegislash can't start battle in blade form
  58. "Wishiwashi", # wishiwashi can't start battle in schooling form
  59. "Mimikyu", # mimikyu can't start battle in busted form
  60. "Cramorant", # cramorant can't start battle in gulping/gorging form
  61. "Eiscue", # eiscue can't start battle in noice form
  62. "Morpeko", # morpeko can't start battle in hangry form
  63. "Palafin", # palafin can only start in zero form
  64. "Gimmighoul", # gimmighoul roaming is only in PGO
  65. )
  66. def get_traits(species: str, form_info: dict) -> list[str]:
  67. traits = []
  68. if len(form_info.get("evos", [])) > 0:
  69. traits.append("nfe")
  70. kind = form_info["formeKind"].lower()
  71. if kind in ("mega", "mega-x", "mega-y", "primal"):
  72. traits.extend(("mega", "nostart"))
  73. if kind in ("gmax", "eternamax", "rapid-strike-gmax"):
  74. traits.extend(("gmax", "nostart"))
  75. if kind in ("alola", "galar", "hisui", "paldea"):
  76. traits.extend(("regional", kind))
  77. if species in START_IN_BASE_FORM and kind != "base":
  78. traits.append("nostart")
  79. # special cases
  80. if species == "Necrozma" and kind == "ultra":
  81. # necrozma can't start battle in ultra form
  82. traits.append("nostart")
  83. if species == "Tauros" and "paldea" in kind:
  84. # paldean tauros has dumb names
  85. traits.extend(("regional", "paldea"))
  86. if species == "Minior" and kind != "meteor":
  87. # minior can only start the battle in meteor form
  88. traits.append("nostart")
  89. if species == "Darmanitan" and "zen" in kind:
  90. # darmanitan cannot start in zen form
  91. traits.append("nostart")
  92. if "galar" in kind:
  93. # also there's a galar-zen form to handle
  94. traits.extend(("regional", "galar"))
  95. return sorted(set(traits))
  96. def clean_dex(raw: dict) -> dict[int, Pokemon]:
  97. regrouped = defaultdict(list)
  98. for key, entry in raw.items():
  99. isNonstandard = entry.get("isNonstandard", None)
  100. baseSpecies = entry.get("baseSpecies", None)
  101. forme = entry.get("forme", None)
  102. if isNonstandard not in (None, "Past", "Unobtainable"):
  103. continue # remove CAP etc.
  104. if baseSpecies in ("Pikachu", "Pichu") and forme is not None:
  105. continue # remove pikachu spam + spiky ear pichu
  106. if forme is not None and "Totem" in forme:
  107. continue # remove totem pokemon
  108. if baseSpecies == "Toxtricity" and forme == "Low-Key-Gmax":
  109. continue # remove low-key-gmax since it is sort of a duplicate
  110. num = entry["num"]
  111. # non-cosmetic forms get separate entries automatically
  112. # but keeping the separate unown forms would be ridiculous
  113. if key != "unown" and len(cosmetic := entry.get("cosmeticFormes", [])) > 0:
  114. cosmetic.append(f'{entry["name"]}-{entry["baseForme"]}')
  115. if key == "alcremie":
  116. # oh god this thing
  117. cosmetic = [
  118. f"{cf}-{sweet}"
  119. for cf in cosmetic
  120. for sweet in ALCREMIE_SWEETS
  121. ]
  122. regrouped[num].extend({
  123. **entry,
  124. "forme": cf.replace(" ", "-"),
  125. "formeKind": "cosmetic",
  126. } for cf in cosmetic)
  127. elif key in GENDER_DIFFS:
  128. regrouped[num].append({
  129. **entry,
  130. "forme": f'{entry["name"]}-M',
  131. "formeKind": "cosmetic",
  132. })
  133. regrouped[num].append({
  134. **entry,
  135. "forme": f'{entry["name"]}-F',
  136. "formeKind": "cosmetic",
  137. })
  138. else:
  139. regrouped[num].append({
  140. **entry,
  141. "forme": entry["name"],
  142. "formeKind": entry.get("forme", "base").lower(),
  143. })
  144. return {
  145. i: Pokemon(
  146. num=i,
  147. species=(
  148. # doubles as an assertion that forms is not empty
  149. species := (forms := regrouped[i])[0].get("baseSpecies", forms[0]["name"])
  150. ),
  151. forms=[
  152. Form(
  153. name=f.get("forme", f["name"]),
  154. traits=get_traits(species, f),
  155. types=f["types"],
  156. color=f["color"],
  157. ) for f in forms
  158. ]
  159. ) for i in range(1, max(regrouped.keys()) + 1)
  160. }
  161. async def load_pokedex(dex_file: Path, force_dex: bool) -> dict:
  162. if dex_file.is_file() and not force_dex:
  163. with open(dex_file) as infile:
  164. loaded = json.load(infile)
  165. dex = {
  166. int(num): Pokemon(
  167. num=entry["num"],
  168. species=entry["species"],
  169. forms=[Form(**f) for f in entry["forms"]],
  170. ) for num, entry in loaded.items()
  171. }
  172. else:
  173. # first download the pokedex
  174. raw_dex = await download_pokedex()
  175. # clean and reorganize it
  176. dex = clean_dex(raw_dex)
  177. # output dex for auditing and reloading
  178. with open(dex_file, "w") as out:
  179. json.dump({
  180. str(i): asdict(pkmn)
  181. for i, pkmn in dex.items()
  182. }, out, indent=2)
  183. return dex
  184. SHOWDOWN_REPLACEMENTS = [
  185. ("mega-", "mega"), # charizard, mewtwo
  186. ("paldea-", "paldea"), # tauros
  187. ("mr. ", "mr"), # mr mime + mr rime
  188. (" jr.", "jr"), # mime jr
  189. ("'d", "d"), # farfetch'd and sirfetch'd
  190. ("nidoran-m", "nidoranm"),
  191. ("-f", "f"), # gender diff forms TODO maybe only do this for some, clean up later fixes
  192. (re.compile(r"-m$"), ""), # gender diff forms
  193. (re.compile(r"^ho-oh$"), "hooh"),
  194. ("burmy-plant", "burmy"),
  195. ("shellos-west", "shellos"),
  196. ("gastrodon-west", "gastrodon"),
  197. ("hippopotasf", "hippopotas-f"),
  198. ("hippowdonf", "hippowdon-f"),
  199. ("rotomf", "rotom-f"), # rotom fan + frost special case
  200. ("porygon-z", "porygonz"),
  201. ("arceusf", "arceus-f"), # fire, flying, fairy, fighting special case
  202. ("unfezantf", "unfezant-f"),
  203. ("-striped", "striped"), # basculin special cases
  204. ("-galar-zen", "-galarzen"), # darmanitan special cases
  205. ("-spring", ""), # deerling, sawsbuck special cases
  206. ("frillishf", "frillish-f"),
  207. ("jellicentf", "jellicent-f"),
  208. ("pyroarf", "pyroar-f"),
  209. ("flabébé", "flabebe"),
  210. ("flabebe-red", "flabebe"),
  211. ("floette-red", "floette"),
  212. ("florges-red", "florges"),
  213. ("vivillonfancy", "vivillon-fancy"),
  214. ("vivillon-meadow", "vivillon"),
  215. ("vivillon-icy-snow", "vivillon-icysnow"),
  216. ("vivillon-high-plains", "vivillon-highplains"),
  217. ("meowsticf", "meowstic-f"),
  218. ("furfrou-la-reine", "furfrou-lareine"),
  219. ("furfrou-natural", "furfrou"),
  220. ("%", ""), # zygarde 10%,
  221. ("oricorio-pom-pom", "oricorio-pompom"),
  222. ("oricorio-pa'u", "oricorio-pau"),
  223. ("type: null", "typenull"),
  224. ("silvallyf", "silvally-f"), # fire, flying, fairy, fighting special case
  225. ("minior-red", "minior"),
  226. ("mo-o", "moo"), # jangmo-o, hakamo-o, kommo-o
  227. ("tapu ", "tapu"), # tapus
  228. ("dusk-mane", "duskmane"), # necrozma
  229. ("dawn-wings", "dawnwings"), # necrozma
  230. ("low-key", "lowkey"), # toxtricity
  231. ("swirlflower", "swirl-flower"), # alcremie
  232. ("creamflower", "cream-flower"), # alcremie
  233. ("-swirl", "swirl"), # alcremie
  234. ("-cream", "cream"), # alcremie
  235. ("-vanillacream-strawberry", ""), # alcremie
  236. ("-strawberry", ""), # alcremie
  237. ("indeedeef", "indeedee-f"),
  238. ("rapid-strike", "rapidstrike"), # Urshifu
  239. ("rapidstrike-gmax", "rapidstrikegmax"), # Urshifu
  240. ("basculegionf", "basculegion-f"),
  241. ("oinkolognef", "oinkologne-f"),
  242. ("mausholdfour", "maushold-four"),
  243. ("tatsugiri-curly", "tatsugiri"),
  244. # dudunsparce (note - no visual diff on showdown as of 3/17)
  245. ("three-segment", "threesegment"),
  246. ("wo-chien", "wochien"),
  247. ("chien-pao", "chienpao"),
  248. ("ting-lu", "tinglu"),
  249. ("chi-yu", "chiyu"),
  250. ("roaring moon", "roaringmoon"),
  251. ("iron valiant", "ironvaliant"),
  252. ("walking wake", "walkingwake"),
  253. ("iron leaves", "ironleaves"),
  254. ]
  255. def get_showdown_urls(pkmn: Pokemon, form: Form) -> list[tuple[str, str]]:
  256. name = form.name.lower()
  257. for pat, ins in SHOWDOWN_REPLACEMENTS:
  258. if isinstance(pat, re.Pattern):
  259. name = re.sub(pat, ins, name)
  260. else:
  261. name = name.replace(pat, ins)
  262. if 984 <= pkmn.num <= 995:
  263. # paradox mons
  264. name = name.replace(" ", "")
  265. return [
  266. (f"https://play.pokemonshowdown.com/sprites/ani/{name}.gif", "gif"),
  267. (f"https://play.pokemonshowdown.com/sprites/ani-back/{name}.gif", "gif"),
  268. (f"https://play.pokemonshowdown.com/sprites/gen5/{name}.png", "png"),
  269. (f"https://play.pokemonshowdown.com/sprites/gen5-back/{name}.png", "png"),
  270. ]
  271. SEREBII_SPECIAL = {
  272. "Castform-Rainy": "r",
  273. "Castform-Snowy": "i",
  274. "Castform-Sunny": "s",
  275. "Deoxys-Attack": "a",
  276. "Deoxys-Defense": "d",
  277. "Deoxys-Speed": "s",
  278. "Burmy-Plant": "p",
  279. "Burmy-Sandy": "s",
  280. "Burmy-Trash": "t",
  281. # wormadam plant is default form
  282. "Wormadam-Sandy": "c",
  283. "Wormadam-Trash": "t",
  284. "Cherrim-Sunshine": "s",
  285. "Shellos-East": "e",
  286. "Gastrodon-East": "e",
  287. "Tauros-Paldea-Blaze": "b",
  288. "Tauros-Paldea-Aqua": "a",
  289. "Rotom-Heat": "h",
  290. "Rotom-Wash": "w",
  291. "Rotom-Frost": "f",
  292. "Rotom-Fan": "s",
  293. "Rotom-Mow": "m",
  294. "Dialga-Origin": "o",
  295. "Palkia-Origin": "o",
  296. "Giratina-Origin": "o",
  297. "Shaymin-Sky": "s",
  298. "Unfezant-F": "f",
  299. "Basculin-Blue-Striped": "b",
  300. "Basculin-White-Striped": "w",
  301. "Darmanitan-Zen": "z",
  302. "Darmanitan-Galar-Zen": "gz",
  303. "Deerling-Summer": "s",
  304. "Deerling-Autumn": "a",
  305. "Deerling-Winter": "w",
  306. "Sawsbuck-Summer": "s",
  307. "Sawsbuck-Autumn": "a",
  308. "Sawsbuck-Winter": "w",
  309. "Frillish-F": "f",
  310. "Jellicent-F": "f",
  311. "Kyurem-Black": "b",
  312. "Kyurem-White": "w",
  313. "Keldeo-Resolute": "r",
  314. "Meloetta-Pirouette": "s",
  315. "Genesect-Douse": "w",
  316. "Genesect-Shock": "e",
  317. "Genesect-Burn": "f",
  318. "Genesect-Chill": "i",
  319. "Greninja-Ash": "a",
  320. "Vivillon-Archipelago": "a",
  321. "Vivillon-Continental": "c",
  322. "Vivillon-Elegant": "e",
  323. "Vivillon-Fancy": "f",
  324. "Vivillon-Garden": "g",
  325. "Vivillon-High-Plains": "h",
  326. "Vivillon-Icy-Snow": "i",
  327. "Vivillon-Jungle": "j",
  328. "Vivillon-Marine": "ma",
  329. "Vivillon-Meadow": "m",
  330. "Vivillon-Modern": "mo",
  331. "Vivillon-Monsoon": "mon",
  332. "Vivillon-Ocean": "o",
  333. "Vivillon-Pokeball": "pb",
  334. "Vivillon-Polar": "p",
  335. "Vivillon-River": "r",
  336. "Vivillon-Sandstorm": "s",
  337. "Vivillon-Savanna": "sa",
  338. "Vivillon-Sun": "su",
  339. "Vivillon-Tundra": "t",
  340. "Pyroar-F": "f",
  341. "Flabébé-Blue": "b",
  342. "Flabébé-Orange": "o",
  343. "Flabébé-White": "w",
  344. "Flabébé-Yellow": "y",
  345. "Flabébé-Red": "r",
  346. "Floette-Blue": "b",
  347. "Floette-Orange": "o",
  348. "Floette-White": "w",
  349. "Floette-Yellow": "y",
  350. "Floette-Red": "r",
  351. "Florges-Blue": "b",
  352. "Florges-Orange": "o",
  353. "Florges-White": "w",
  354. "Florges-Yellow": "y",
  355. "Florges-Red": "r",
  356. "Meowstic-F": "f",
  357. "Furfrou-Dandy": "da",
  358. "Furfrou-Debutante": "de",
  359. "Furfrou-Diamond": "d",
  360. "Furfrou-Heart": "h",
  361. "Furfrou-Kabuki": "k",
  362. "Furfrou-La-Reine": "l",
  363. "Furfrou-Matron": "m",
  364. "Furfrou-Pharaoh": "p",
  365. "Furfrou-Star": "s",
  366. "Aegislash-Blade": "b",
  367. "Zygarde-10%": "10",
  368. "Zygarde-Complete": "c",
  369. "Hoopa-Unbound": "u",
  370. "Pumpkaboo-Small": "s", # kinda dumb but w/e
  371. "Pumpkaboo-Large": "l",
  372. "Pumpkaboo-Super": "h",
  373. "Gourgeist-Small": "s",
  374. "Gourgeist-Large": "l",
  375. "Gourgeist-Super": "h",
  376. "Oricorio-Pom-Pom": "p",
  377. "Oricorio-Pa'u": "pau",
  378. "Oricorio-Sensu": "s",
  379. "Lycanroc-Midnight": "m",
  380. "Lycanroc-Dusk": "d",
  381. "Wishiwashi-School": "s",
  382. "Minior-Blue": "b",
  383. "Minior-Green": "g",
  384. "Minior-Indigo": "i",
  385. "Minior-Orange": "o",
  386. "Minior-Red": "r",
  387. "Minior-Violet": "v",
  388. "Minior-Yellow": "y",
  389. "Mimikyu-Busted": "b",
  390. "Necrozma-Dusk-Mane": "dm",
  391. "Necrozma-Dawn-Wings": "dw",
  392. "Necrozma-Ultra": "m",
  393. "Magearna-Original": "o",
  394. "Cramorant-Gulping": "gu",
  395. "Cramorant-Gorging": "go",
  396. "Toxtricity-Low-Key": "l",
  397. "Sinistea-Antique": "f",
  398. "Polteageist-Antique": "f",
  399. "Eiscue-Noice": "n",
  400. "Indeedee-F": "f",
  401. "Morpeko-Hangry": "h",
  402. "Zacian-Crowned": "c",
  403. "Zamazenta-Crowned": "c",
  404. "Eternatus-Eternamax": "e",
  405. "Zarude-Dada": "d",
  406. "Calyrex-Ice": "i",
  407. "Calyrex-Shadow": "s",
  408. "Urshifu-Rapid-Strike": "r",
  409. "Urshifu-Rapid-Strike-Gmax": "rgi",
  410. "Basculegion-F": "f",
  411. "Oinkologne-F": "f",
  412. "Maushold-Four": "f",
  413. "Palafin-Hero": "h",
  414. "Tatsugiri-Droopy": "d",
  415. "Tatsugiri-Stretchy": "s",
  416. "Dudunsparce-Three-Segment": "t",
  417. "Gimmighoul-Roaming": "r",
  418. }
  419. SEREBII_USE_SPECIES = (
  420. "Shellos-West", "Gastrodon-West",
  421. "Hippopotas-M", "Hippowdon-M",
  422. "Unfezant-M",
  423. "Deerling-Spring", "Sawsbuck-Spring",
  424. "Frillish-M", "Jellicent-M",
  425. "Pyroar-M",
  426. "Furfrou-Natural",
  427. "Minior-Meteor",
  428. "Tatsugiri-Curly",
  429. )
  430. SEREBII_IGNORE_MISSING = (
  431. "Hippopotas-F", "Hippowdon-F", "Floette-Eternal",
  432. "Squawkabilly-Blue", "Squawkabilly-Yellow", "Squawkabilly-White",
  433. )
  434. def get_serebii_url(pkmn: Pokemon, form: Form) -> str | None:
  435. if form.name == pkmn.species or form.name in SEREBII_USE_SPECIES:
  436. return f"https://www.serebii.net/pokemon/art/{pkmn.num:03d}.png"
  437. if form.name in SEREBII_SPECIAL:
  438. return f"https://www.serebii.net/pokemon/art/{pkmn.num:03d}-{SEREBII_SPECIAL[form.name]}.png"
  439. if "gmax" in form.traits:
  440. return f"https://www.serebii.net/pokemon/art/{pkmn.num:03d}-gi.png"
  441. if "mega" in form.traits:
  442. if "Mega-X" in form.name:
  443. return f"https://www.serebii.net/pokemon/art/{pkmn.num:03d}-mx.png"
  444. elif "Mega-Y" in form.name:
  445. return f"https://www.serebii.net/pokemon/art/{pkmn.num:03d}-my.png"
  446. else:
  447. return f"https://www.serebii.net/pokemon/art/{pkmn.num:03d}-m.png"
  448. if "alola" in form.traits:
  449. return f"https://www.serebii.net/pokemon/art/{pkmn.num:03d}-a.png"
  450. if "galar" in form.traits:
  451. return f"https://www.serebii.net/pokemon/art/{pkmn.num:03d}-g.png"
  452. if "hisui" in form.traits:
  453. return f"https://www.serebii.net/pokemon/art/{pkmn.num:03d}-h.png"
  454. if "paldea" in form.traits:
  455. return f"https://www.serebii.net/pokemon/art/{pkmn.num:03d}-p.png"
  456. if pkmn.num in (493, 773): # arceus and silvally
  457. type_name = form.name.split('-')[1].lower()
  458. return f"https://www.serebii.net/pokemon/art/{pkmn.num}-{type_name}.png"
  459. if pkmn.num == 869: # alcremie
  460. _, cream_name, cream_kind, candy_name = form.name.lower().split("-")
  461. image_name = "869-"
  462. if cream_name == "matcha":
  463. image_name += "mac"
  464. elif cream_name == "mint":
  465. image_name += "mic"
  466. elif cream_name != "vanilla":
  467. image_name += cream_name[0] + cream_kind[0]
  468. if candy_name != "strawberry":
  469. if candy_name == "love" and cream_name == "mint":
  470. # for some reason this name breaks the pattern
  471. image_name += "heart"
  472. else:
  473. image_name += candy_name
  474. image_name = image_name.strip("-")
  475. return f"https://www.serebii.net/swordshield/pokemon/{image_name}.png"
  476. if "-Therian" in form.name:
  477. return f"https://www.serebii.net/pokemon/art/{pkmn.num:03d}-s.png"
  478. if form.name not in SEREBII_IGNORE_MISSING:
  479. print(f"No Serebii URL known for {form.name}")
  480. async def download(session: ClientSession, url: str, filename: Path) -> tuple[str, Exception | bool]:
  481. if filename.is_file():
  482. return url, False
  483. try:
  484. async with session.get(url) as res:
  485. res.raise_for_status()
  486. with open(filename, "wb") as out:
  487. out.write(await res.read())
  488. except Exception as ex:
  489. return url, ex
  490. return url, True
  491. async def download_all_for_pokemon(pkmn: Pokemon, image_dir: Path) -> dict[str, dict[str, Exception | bool]]:
  492. results = defaultdict(dict)
  493. async with ClientSession() as session:
  494. for form in pkmn.forms:
  495. urls = []
  496. urls += get_showdown_urls(pkmn, form)
  497. urls.append((get_serebii_url(pkmn, form), "png"))
  498. # TODO more sources
  499. results[form.name].update(await asyncio.gather(*[
  500. download(session, url, image_dir.joinpath(f"{form.name}-{i}.{ext}"))
  501. for i, (url, ext) in enumerate(urls) if url is not None
  502. ]))
  503. return results
  504. async def download_all(image_dir: Path, pkmn: list[Pokemon]) -> dict[str, dict[str, Exception | bool]]:
  505. image_dir.mkdir(parents=True, exist_ok=True)
  506. log = {}
  507. for p in pkmn:
  508. log.update(await download_all_for_pokemon(p, image_dir))
  509. return log
  510. KNOWN_MISSING = [
  511. "https://play.pokemonshowdown.com/sprites/ani/venusaur-gmax.gif",
  512. "https://play.pokemonshowdown.com/sprites/ani-back/venusaur-gmax.gif",
  513. "https://play.pokemonshowdown.com/sprites/ani/blastoise-gmax.gif",
  514. "https://play.pokemonshowdown.com/sprites/ani-back/blastoise-gmax.gif",
  515. "https://play.pokemonshowdown.com/sprites/ani/growlithe-hisui.gif",
  516. "https://play.pokemonshowdown.com/sprites/ani-back/growlithe-hisui.gif",
  517. "https://play.pokemonshowdown.com/sprites/ani/arcanine-hisui.gif",
  518. "https://play.pokemonshowdown.com/sprites/ani-back/arcanine-hisui.gif",
  519. "https://play.pokemonshowdown.com/sprites/ani/voltorb-hisui.gif",
  520. "https://play.pokemonshowdown.com/sprites/ani-back/voltorb-hisui.gif",
  521. "https://play.pokemonshowdown.com/sprites/ani/electrode-hisui.gif",
  522. "https://play.pokemonshowdown.com/sprites/ani-back/electrode-hisui.gif",
  523. "https://play.pokemonshowdown.com/sprites/ani/tauros-paldeacombat.gif",
  524. "https://play.pokemonshowdown.com/sprites/ani-back/tauros-paldeacombat.gif",
  525. "https://play.pokemonshowdown.com/sprites/ani/tauros-paldeablaze.gif",
  526. "https://play.pokemonshowdown.com/sprites/ani-back/tauros-paldeablaze.gif",
  527. "https://play.pokemonshowdown.com/sprites/ani/tauros-paldeaaqua.gif",
  528. "https://play.pokemonshowdown.com/sprites/ani-back/tauros-paldeaaqua.gif",
  529. "https://play.pokemonshowdown.com/sprites/ani/wooper-paldea.gif",
  530. "https://play.pokemonshowdown.com/sprites/ani-back/wooper-paldea.gif",
  531. "https://play.pokemonshowdown.com/sprites/ani/qwilfish-hisui.gif",
  532. "https://play.pokemonshowdown.com/sprites/ani-back/qwilfish-hisui.gif",
  533. "https://play.pokemonshowdown.com/sprites/ani/sneasel-hisui.gif",
  534. "https://play.pokemonshowdown.com/sprites/ani-back/sneasel-hisui.gif",
  535. "https://play.pokemonshowdown.com/sprites/ani/dialga-origin.gif",
  536. "https://play.pokemonshowdown.com/sprites/ani-back/dialga-origin.gif",
  537. "https://play.pokemonshowdown.com/sprites/ani/palkia-origin.gif",
  538. "https://play.pokemonshowdown.com/sprites/ani-back/palkia-origin.gif",
  539. "https://play.pokemonshowdown.com/sprites/ani/lilligant-hisui.gif",
  540. "https://play.pokemonshowdown.com/sprites/ani-back/lilligant-hisui.gif",
  541. "https://play.pokemonshowdown.com/sprites/ani/basculin-whitestriped.gif",
  542. "https://play.pokemonshowdown.com/sprites/ani-back/basculin-whitestriped.gif",
  543. "https://play.pokemonshowdown.com/sprites/ani/braviary-hisui.gif",
  544. "https://play.pokemonshowdown.com/sprites/ani-back/braviary-hisui.gif",
  545. "https://play.pokemonshowdown.com/sprites/ani/sliggoo-hisui.gif",
  546. "https://play.pokemonshowdown.com/sprites/ani-back/sliggoo-hisui.gif",
  547. "https://play.pokemonshowdown.com/sprites/ani/goodra-hisui.gif",
  548. "https://play.pokemonshowdown.com/sprites/ani-back/goodra-hisui.gif",
  549. "https://play.pokemonshowdown.com/sprites/ani/avalugg-hisui.gif",
  550. "https://play.pokemonshowdown.com/sprites/ani-back/avalugg-hisui.gif",
  551. "https://play.pokemonshowdown.com/sprites/ani/rillaboom-gmax.gif",
  552. "https://play.pokemonshowdown.com/sprites/ani-back/rillaboom-gmax.gif",
  553. "https://play.pokemonshowdown.com/sprites/ani/cinderace-gmax.gif",
  554. "https://play.pokemonshowdown.com/sprites/ani-back/cinderace-gmax.gif",
  555. "https://play.pokemonshowdown.com/sprites/ani-back/inteleon-gmax.gif",
  556. "https://play.pokemonshowdown.com/sprites/ani/urshifu-gmax.gif",
  557. "https://play.pokemonshowdown.com/sprites/ani-back/urshifu-gmax.gif",
  558. "https://play.pokemonshowdown.com/sprites/ani/urshifu-rapidstrikegmax.gif",
  559. "https://play.pokemonshowdown.com/sprites/ani-back/urshifu-rapidstrikegmax.gif",
  560. "https://play.pokemonshowdown.com/sprites/ani/wyrdeer.gif",
  561. "https://play.pokemonshowdown.com/sprites/ani-back/wyrdeer.gif",
  562. "https://play.pokemonshowdown.com/sprites/ani/kleavor.gif",
  563. "https://play.pokemonshowdown.com/sprites/ani-back/kleavor.gif",
  564. "https://play.pokemonshowdown.com/sprites/ani/ursaluna.gif",
  565. "https://play.pokemonshowdown.com/sprites/ani-back/ursaluna.gif",
  566. "https://play.pokemonshowdown.com/sprites/ani/sneasler.gif",
  567. "https://play.pokemonshowdown.com/sprites/ani-back/sneasler.gif",
  568. "https://play.pokemonshowdown.com/sprites/ani/overqwil.gif",
  569. "https://play.pokemonshowdown.com/sprites/ani-back/overqwil.gif",
  570. "https://play.pokemonshowdown.com/sprites/ani/enamorus.gif",
  571. "https://play.pokemonshowdown.com/sprites/ani-back/enamorus.gif",
  572. "https://play.pokemonshowdown.com/sprites/ani/enamorus-therian.gif",
  573. "https://play.pokemonshowdown.com/sprites/ani-back/enamorus-therian.gif",
  574. "https://play.pokemonshowdown.com/sprites/ani/pawmi.gif",
  575. "https://play.pokemonshowdown.com/sprites/ani-back/pawmi.gif",
  576. "https://play.pokemonshowdown.com/sprites/ani/pawmo.gif",
  577. "https://play.pokemonshowdown.com/sprites/ani-back/pawmo.gif",
  578. "https://play.pokemonshowdown.com/sprites/ani/pawmot.gif",
  579. "https://play.pokemonshowdown.com/sprites/ani-back/pawmot.gif",
  580. "https://play.pokemonshowdown.com/sprites/ani/tandemaus.gif",
  581. "https://play.pokemonshowdown.com/sprites/ani-back/tandemaus.gif",
  582. "https://play.pokemonshowdown.com/sprites/ani/maushold.gif",
  583. "https://play.pokemonshowdown.com/sprites/ani-back/maushold.gif",
  584. "https://play.pokemonshowdown.com/sprites/ani/maushold-four.gif",
  585. "https://play.pokemonshowdown.com/sprites/ani-back/maushold-four.gif",
  586. "https://play.pokemonshowdown.com/sprites/ani/fidough.gif",
  587. "https://play.pokemonshowdown.com/sprites/ani-back/fidough.gif",
  588. "https://play.pokemonshowdown.com/sprites/ani/dachsbun.gif",
  589. "https://play.pokemonshowdown.com/sprites/ani-back/dachsbun.gif",
  590. "https://play.pokemonshowdown.com/sprites/ani/smoliv.gif",
  591. "https://play.pokemonshowdown.com/sprites/ani-back/smoliv.gif",
  592. "https://play.pokemonshowdown.com/sprites/ani/dolliv.gif",
  593. "https://play.pokemonshowdown.com/sprites/ani-back/dolliv.gif",
  594. "https://play.pokemonshowdown.com/sprites/ani/arboliva.gif",
  595. "https://play.pokemonshowdown.com/sprites/ani-back/arboliva.gif",
  596. "https://play.pokemonshowdown.com/sprites/ani/squawkabilly.gif",
  597. "https://play.pokemonshowdown.com/sprites/ani-back/squawkabilly.gif",
  598. "https://play.pokemonshowdown.com/sprites/ani/squawkabilly-blue.gif",
  599. "https://play.pokemonshowdown.com/sprites/ani-back/squawkabilly-blue.gif",
  600. "https://play.pokemonshowdown.com/sprites/ani/squawkabilly-yellow.gif",
  601. "https://play.pokemonshowdown.com/sprites/ani-back/squawkabilly-yellow.gif",
  602. "https://play.pokemonshowdown.com/sprites/ani/squawkabilly-white.gif",
  603. "https://play.pokemonshowdown.com/sprites/ani-back/squawkabilly-white.gif",
  604. "https://play.pokemonshowdown.com/sprites/ani/nacli.gif",
  605. "https://play.pokemonshowdown.com/sprites/ani-back/nacli.gif",
  606. "https://play.pokemonshowdown.com/sprites/ani/naclstack.gif",
  607. "https://play.pokemonshowdown.com/sprites/ani-back/naclstack.gif",
  608. "https://play.pokemonshowdown.com/sprites/ani/garganacl.gif",
  609. "https://play.pokemonshowdown.com/sprites/ani-back/garganacl.gif",
  610. "https://play.pokemonshowdown.com/sprites/ani/charcadet.gif",
  611. "https://play.pokemonshowdown.com/sprites/ani-back/charcadet.gif",
  612. "https://play.pokemonshowdown.com/sprites/ani/armarouge.gif",
  613. "https://play.pokemonshowdown.com/sprites/ani-back/armarouge.gif",
  614. "https://play.pokemonshowdown.com/sprites/ani/ceruledge.gif",
  615. "https://play.pokemonshowdown.com/sprites/ani-back/ceruledge.gif",
  616. "https://play.pokemonshowdown.com/sprites/ani/tadbulb.gif",
  617. "https://play.pokemonshowdown.com/sprites/ani-back/tadbulb.gif",
  618. "https://play.pokemonshowdown.com/sprites/ani/bellibolt.gif",
  619. "https://play.pokemonshowdown.com/sprites/ani-back/bellibolt.gif",
  620. "https://play.pokemonshowdown.com/sprites/ani/wattrel.gif",
  621. "https://play.pokemonshowdown.com/sprites/ani-back/wattrel.gif",
  622. "https://play.pokemonshowdown.com/sprites/ani/kilowattrel.gif",
  623. "https://play.pokemonshowdown.com/sprites/ani-back/kilowattrel.gif",
  624. "https://play.pokemonshowdown.com/sprites/ani/maschiff.gif",
  625. "https://play.pokemonshowdown.com/sprites/ani-back/maschiff.gif",
  626. "https://play.pokemonshowdown.com/sprites/ani/mabosstiff.gif",
  627. "https://play.pokemonshowdown.com/sprites/ani-back/mabosstiff.gif",
  628. "https://play.pokemonshowdown.com/sprites/ani/shroodle.gif",
  629. "https://play.pokemonshowdown.com/sprites/ani-back/shroodle.gif",
  630. "https://play.pokemonshowdown.com/sprites/ani/grafaiai.gif",
  631. "https://play.pokemonshowdown.com/sprites/ani-back/grafaiai.gif",
  632. "https://play.pokemonshowdown.com/sprites/ani/bramblin.gif",
  633. "https://play.pokemonshowdown.com/sprites/ani-back/bramblin.gif",
  634. "https://play.pokemonshowdown.com/sprites/ani/brambleghast.gif",
  635. "https://play.pokemonshowdown.com/sprites/ani-back/brambleghast.gif",
  636. "https://play.pokemonshowdown.com/sprites/ani/toedscool.gif",
  637. "https://play.pokemonshowdown.com/sprites/ani-back/toedscool.gif",
  638. "https://play.pokemonshowdown.com/sprites/ani/toedscruel.gif",
  639. "https://play.pokemonshowdown.com/sprites/ani-back/toedscruel.gif",
  640. "https://play.pokemonshowdown.com/sprites/ani/klawf.gif",
  641. "https://play.pokemonshowdown.com/sprites/ani-back/klawf.gif",
  642. "https://play.pokemonshowdown.com/sprites/ani/capsakid.gif",
  643. "https://play.pokemonshowdown.com/sprites/ani-back/capsakid.gif",
  644. "https://play.pokemonshowdown.com/sprites/ani/scovillain.gif",
  645. "https://play.pokemonshowdown.com/sprites/ani-back/scovillain.gif",
  646. "https://play.pokemonshowdown.com/sprites/ani/tinkatink.gif",
  647. "https://play.pokemonshowdown.com/sprites/ani-back/tinkatink.gif",
  648. "https://play.pokemonshowdown.com/sprites/ani/tinkatuff.gif",
  649. "https://play.pokemonshowdown.com/sprites/ani-back/tinkatuff.gif",
  650. "https://play.pokemonshowdown.com/sprites/ani/tinkaton.gif",
  651. "https://play.pokemonshowdown.com/sprites/ani-back/tinkaton.gif",
  652. "https://play.pokemonshowdown.com/sprites/ani/bombirdier.gif",
  653. "https://play.pokemonshowdown.com/sprites/ani-back/bombirdier.gif",
  654. "https://play.pokemonshowdown.com/sprites/ani/varoom.gif",
  655. "https://play.pokemonshowdown.com/sprites/ani-back/varoom.gif",
  656. "https://play.pokemonshowdown.com/sprites/ani/revavroom.gif",
  657. "https://play.pokemonshowdown.com/sprites/ani-back/revavroom.gif",
  658. "https://play.pokemonshowdown.com/sprites/ani/cyclizar.gif",
  659. "https://play.pokemonshowdown.com/sprites/ani-back/cyclizar.gif",
  660. "https://play.pokemonshowdown.com/sprites/ani/orthworm.gif",
  661. "https://play.pokemonshowdown.com/sprites/ani-back/orthworm.gif",
  662. "https://play.pokemonshowdown.com/sprites/ani/glimmet.gif",
  663. "https://play.pokemonshowdown.com/sprites/ani-back/glimmet.gif",
  664. "https://play.pokemonshowdown.com/sprites/ani/glimmora.gif",
  665. "https://play.pokemonshowdown.com/sprites/ani-back/glimmora.gif",
  666. "https://play.pokemonshowdown.com/sprites/ani/flamigo.gif",
  667. "https://play.pokemonshowdown.com/sprites/ani-back/flamigo.gif",
  668. "https://play.pokemonshowdown.com/sprites/ani/cetoddle.gif",
  669. "https://play.pokemonshowdown.com/sprites/ani-back/cetoddle.gif",
  670. "https://play.pokemonshowdown.com/sprites/ani/cetitan.gif",
  671. "https://play.pokemonshowdown.com/sprites/ani-back/cetitan.gif",
  672. "https://play.pokemonshowdown.com/sprites/ani/tatsugiri-droopy.gif",
  673. "https://play.pokemonshowdown.com/sprites/ani-back/tatsugiri-droopy.gif",
  674. "https://play.pokemonshowdown.com/sprites/ani/tatsugiri-stretchy.gif",
  675. "https://play.pokemonshowdown.com/sprites/ani-back/tatsugiri-stretchy.gif",
  676. "https://play.pokemonshowdown.com/sprites/ani/tatsugiri.gif",
  677. "https://play.pokemonshowdown.com/sprites/ani-back/tatsugiri.gif",
  678. "https://play.pokemonshowdown.com/sprites/ani/annihilape.gif",
  679. "https://play.pokemonshowdown.com/sprites/ani-back/annihilape.gif",
  680. "https://play.pokemonshowdown.com/sprites/ani/clodsire.gif",
  681. "https://play.pokemonshowdown.com/sprites/ani-back/clodsire.gif",
  682. "https://play.pokemonshowdown.com/sprites/ani/kingambit.gif",
  683. "https://play.pokemonshowdown.com/sprites/ani-back/kingambit.gif",
  684. "https://play.pokemonshowdown.com/sprites/ani/greattusk.gif",
  685. "https://play.pokemonshowdown.com/sprites/ani-back/greattusk.gif",
  686. "https://play.pokemonshowdown.com/sprites/ani/screamtail.gif",
  687. "https://play.pokemonshowdown.com/sprites/ani-back/screamtail.gif",
  688. "https://play.pokemonshowdown.com/sprites/ani/brutebonnet.gif",
  689. "https://play.pokemonshowdown.com/sprites/ani-back/brutebonnet.gif",
  690. "https://play.pokemonshowdown.com/sprites/ani/fluttermane.gif",
  691. "https://play.pokemonshowdown.com/sprites/ani-back/fluttermane.gif",
  692. "https://play.pokemonshowdown.com/sprites/ani/slitherwing.gif",
  693. "https://play.pokemonshowdown.com/sprites/ani-back/slitherwing.gif",
  694. "https://play.pokemonshowdown.com/sprites/ani/sandyshocks.gif",
  695. "https://play.pokemonshowdown.com/sprites/ani-back/sandyshocks.gif",
  696. "https://play.pokemonshowdown.com/sprites/ani/irontreads.gif",
  697. "https://play.pokemonshowdown.com/sprites/ani-back/irontreads.gif",
  698. "https://play.pokemonshowdown.com/sprites/ani/ironbundle.gif",
  699. "https://play.pokemonshowdown.com/sprites/ani-back/ironbundle.gif",
  700. "https://play.pokemonshowdown.com/sprites/ani/ironhands.gif",
  701. "https://play.pokemonshowdown.com/sprites/ani-back/ironhands.gif",
  702. "https://play.pokemonshowdown.com/sprites/ani/ironjugulis.gif",
  703. "https://play.pokemonshowdown.com/sprites/ani-back/ironjugulis.gif",
  704. "https://play.pokemonshowdown.com/sprites/ani/ironmoth.gif",
  705. "https://play.pokemonshowdown.com/sprites/ani-back/ironmoth.gif",
  706. "https://play.pokemonshowdown.com/sprites/ani/ironthorns.gif",
  707. "https://play.pokemonshowdown.com/sprites/ani-back/ironthorns.gif",
  708. "https://play.pokemonshowdown.com/sprites/ani/frigibax.gif",
  709. "https://play.pokemonshowdown.com/sprites/ani-back/frigibax.gif",
  710. "https://play.pokemonshowdown.com/sprites/ani/arctibax.gif",
  711. "https://play.pokemonshowdown.com/sprites/ani-back/arctibax.gif",
  712. "https://play.pokemonshowdown.com/sprites/ani/baxcalibur.gif",
  713. "https://play.pokemonshowdown.com/sprites/ani-back/baxcalibur.gif",
  714. "https://play.pokemonshowdown.com/sprites/ani/gimmighoul.gif",
  715. "https://play.pokemonshowdown.com/sprites/ani-back/gimmighoul.gif",
  716. "https://play.pokemonshowdown.com/sprites/ani/gimmighoul-roaming.gif",
  717. "https://play.pokemonshowdown.com/sprites/ani-back/gimmighoul-roaming.gif",
  718. "https://play.pokemonshowdown.com/sprites/ani/gholdengo.gif",
  719. "https://play.pokemonshowdown.com/sprites/ani-back/gholdengo.gif",
  720. "https://play.pokemonshowdown.com/sprites/ani/wochien.gif",
  721. "https://play.pokemonshowdown.com/sprites/ani-back/wochien.gif",
  722. "https://play.pokemonshowdown.com/sprites/ani/chienpao.gif",
  723. "https://play.pokemonshowdown.com/sprites/ani-back/chienpao.gif",
  724. "https://play.pokemonshowdown.com/sprites/ani/tinglu.gif",
  725. "https://play.pokemonshowdown.com/sprites/ani-back/tinglu.gif",
  726. "https://play.pokemonshowdown.com/sprites/ani/chiyu.gif",
  727. "https://play.pokemonshowdown.com/sprites/ani-back/chiyu.gif",
  728. "https://play.pokemonshowdown.com/sprites/ani/roaringmoon.gif",
  729. "https://play.pokemonshowdown.com/sprites/ani-back/roaringmoon.gif",
  730. "https://play.pokemonshowdown.com/sprites/ani/ironvaliant.gif",
  731. "https://play.pokemonshowdown.com/sprites/ani-back/ironvaliant.gif",
  732. "https://play.pokemonshowdown.com/sprites/ani/koraidon.gif",
  733. "https://play.pokemonshowdown.com/sprites/ani-back/koraidon.gif",
  734. "https://play.pokemonshowdown.com/sprites/ani/miraidon.gif",
  735. "https://play.pokemonshowdown.com/sprites/ani-back/miraidon.gif",
  736. "https://play.pokemonshowdown.com/sprites/ani/walkingwake.gif",
  737. "https://play.pokemonshowdown.com/sprites/ani-back/walkingwake.gif",
  738. "https://play.pokemonshowdown.com/sprites/ani/ironleaves.gif",
  739. "https://play.pokemonshowdown.com/sprites/ani-back/ironleaves.gif",
  740. ]
  741. KNOWN_MISSING_PNGS = ("vivillon", "furfrou", "alcremie")
  742. async def main(
  743. dex_file: Path, image_dir: Path, startIndex: int, endIndex: int,
  744. log_skipped: bool, force_dex: bool, dex_only: bool
  745. ):
  746. dex = await load_pokedex(dex_file, force_dex)
  747. if dex_only:
  748. return
  749. log = await download_all(image_dir, (dex[i] for i in range(startIndex, endIndex + 1)))
  750. new_downloads = 0
  751. for form, result in log.items():
  752. for url, info in result.items():
  753. if isinstance(info, Exception):
  754. if url not in KNOWN_MISSING and not (".png" in url and any(s in url for s in KNOWN_MISSING_PNGS)):
  755. print(f"{form}: FAILED {url} - {info}")
  756. elif not info:
  757. if log_skipped:
  758. print(f"{form}: SKIPPED {url} - {info}")
  759. else:
  760. print(f"{form}: SUCCESS {url}")
  761. new_downloads += 1
  762. print(f"New Downloads: {new_downloads}")
  763. if __name__ == "__main__":
  764. from argparse import ArgumentParser
  765. parser = ArgumentParser(
  766. prog="Image Retriever",
  767. description="Retrieve pokedex and images",
  768. )
  769. parser.add_argument(
  770. "-d", "--pokedex", default="data/pokedex.json", type=Path, help="Pokedex file"
  771. )
  772. parser.add_argument(
  773. "--refresh-dex", action="store_true", help="Update the pokedex"
  774. )
  775. parser.add_argument(
  776. "--pokedex-only", action="store_true", help="Quit before image download"
  777. )
  778. parser.add_argument(
  779. "-o", "--output", default="images", type=Path, help="Image output directory"
  780. )
  781. parser.add_argument(
  782. "--log-skipped", action="store_true", help="Log skipped images"
  783. )
  784. parser.add_argument(
  785. "bounds", type=lambda a: map(int, a.split("-")), default="1-151", nargs="?",
  786. help="Range of dex numbers to download, inclusive"
  787. )
  788. args = parser.parse_args()
  789. start, end = args.bounds
  790. asyncio.run(main(
  791. args.pokedex, args.output, start, end,
  792. args.log_skipped, args.refresh_dex, args.pokedex_only
  793. ))