mtg-sdk-pythonで四苦八苦する①

# はじめに

[MTG Arena](https://magic.wizards.com/ja/mtgarena)でエクスポートしたデッキリスト（以下、デッキリスト）を画像出力したい。
リミテッド向けとして、以下の特徴を持つとする。

* 日本語版のカード画像を表示する。
* クリーチャー呪文/非クリーチャー呪文/土地で分ける。
* マナ総量で列を分ける。
* カード名だけでなく、カードのイラストが上1/3くらい見えるようにする。
* デッキに複数枚あるカードは複数枚表示する（基本土地を除く）。

上記を全て満たすデッキ画像生成器を寡聞にして知らないので自作する。
カード画像は[MTG Developers](https://magicthegathering.io/)で[Gatherer](https://gatherer.wizards.com/Pages/Default.aspx)の画像のURLを取得してダウンロードする。
カード・タイプやマナ総量の確認には[python-mtga](https://github.com/poslogithub/python-mtga)を使う。必然的に言語はPythonを使う。
MTG DevelopersのAPIエンドポイントへのアクセスは[Python SDK](https://github.com/MagicTheGathering/mtg-sdk-python)（mtg-sdk-python）を使う。

# ここで躓くよmtg-sdk-python

デッキリストには以下の情報が含まれる。

* カード名
* セットの略号
* コレクター番号

いずれもmtg-sdk-pythonで検索キーとして使用できる。
カード名を英語以外の言語（以下、他言語）で指定して検索する場合には`language`の指定が必要なので注意（[参考](https://github.com/MagicTheGathering/mtg-sdk-python#how-to-get-cards-from-specific-language)）。
また、戻り値はリストなので、カード情報は`for`文で回して参照する。

```py
from mtgsdk import Card

cards = Card.where(language='Japanese').where(name='漆月魁渡').where(set='NEO').where(number=226).all()

for card in cards:
	print(card.name, card.set, card.number, card.image_url)
```
```
Kaito Shizuki NEO 226 http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=548538&type=card
```
![Kaito Shizuki](http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=548538&type=card "Kaito Shizuki")
アイエエエ！？　英語版！？　英語版ナンデ！？

## 他言語版の`image_url`は`card.foreign_names`から取得する

`card.foreign_names`は辞書`foreign_name`（仮名）を要素とするリストである。
[MTG DevelopersのDocs](https://docs.magicthegathering.io/#api_v1cards_list)には「各オブジェクトはキー`language`、`name`、`multiverseid`を持つ」的なことが書かれているが、実際はもっと多い。例えば《[漆月魁渡](https://mtg-jp.com/products/card-gallery/0000223/548538/)》の`foreign_name`は以下となっている。

```py
{
    'name': '漆月魁渡', 
    'text': 'あなたの終了ステップの開始時に、このターンに漆月魁渡が戦場に出た場合、これはフェイズ・アウトする。\n+1：カード１枚を引く。その後、このターンにあなたが攻撃していないかぎりカード１枚を捨てる。\n−2：「このクリーチャーはブロックされない。」を持つ青の１/１の忍者・クリーチャー・トークン １体を生成する。\n−7：あなたは「あなたがコントロールしているクリーチャー１体がプレイヤー１人に戦闘ダメージを与えるたび、あなたのライブラリーから青や黒であるクリーチャー・カード１枚を探し、戦場に出す。その後、ライブラリーを切り直す。」を持つ紋章を得る。', 
    'type': '伝説のプレインズウォーカー — 魁渡', 
    'flavor': None, 
    'imageUrl': 'http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=550063&type=card', 
    'language': 'Japanese', 
    'multiverseid': 550063
}
```
つまり、`foreign_name['language'] == 'Japanese'`である`foreign_name`の`foreign_name['imageUrl']`を取得すればよい。
ついでに、カード名で検索する意味は無いので、セットの略号とコレクター番号だけで検索することにする。

```py
from mtgsdk import Card

cards = Card.where(set='NEO').where(number=226).all()

for card in cards:
	for foreign_name in card.foreign_names:
		if foreign_name['language'] == 'Japanese':
			print(foreign_name['name'], card.set, card.number, foreign_name['imageUrl'])
			break
```
```
漆月魁渡 NEO 226 http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=550063&type=card
```
![漆月魁渡](http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=550063&type=card "漆月魁渡")
ヨシ！ この調子でどんどん行きましょう！

```py
from mtgsdk import Card

cards = Card.where(set='ELD').where(number=329).all()    # フェイに呪われた王、コルヴォルド

for card in cards:
	for foreign_name in card.foreign_names:
		if foreign_name['language'] == 'Japanese':
			print(foreign_name['name'], card.set, card.number, foreign_name['imageUrl'])
			break
```
```
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
TypeError: 'NoneType' object is not iterable
```
は？

## `card.foreign_names is None`であるカードが存在する

`card.foreign_names is None`である可能性は、MTG Developersのドキュメントでも言及されている。
《[フェイに呪われた王、コルヴォルド](https://mtg-jp.com/products/card-gallery/0000184/476047/)》は[エルドレインの王権のBrawl Deck](https://magic.wizards.com/en/articles/archive/card-preview/inside-throne-eldraine-brawl-decks-2019-09-04)限定収録カードで、英語版しか印刷されていない。
そのため、[Gathererにも英語版のカード画像だけしか無い](https://gatherer.wizards.com/Pages/Card/Languages.aspx?multiverseid=476047)。
MTG Arenaでは日本語版カード画像があるように見えるが、あれは多分レイアウトにアートとテキストを重ねて生成したもので、印刷用の日本語版カード画像というのはどこにも無い。
仕方が無いので、その場合は英語版のカード画像を取得するようにする。

```py
from mtgsdk import Card

cards = Card.where(set='ELD').where(number=329).all()

for card in cards:
	if card.foreign_names:
		for foreign_name in card.foreign_names:
			if foreign_name['language'] == 'Japanese':
				print(foreign_name['name'], card.set, card.number, foreign_name['imageUrl'])
				break
	else:
		print(card.name, card.set, card.number, card.image_url)
```
```
Korvold, Fae-Cursed King ELD 329 http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=476047&type=card
```
![Korvold, Fae-Cursed King](http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=476047&type=card "Korvold, Fae-Cursed King")
まあ、Brawl Deck限定収録カードなんていうニッチなカードで試したのが良くなかったのだろう。
[本流のセット](http://mtgwiki.com/wiki/%E6%9C%AC%E6%B5%81%E3%81%AE%E3%82%BB%E3%83%83%E3%83%88)のカードなら、そんな問題は起きまい。

```py
from mtgsdk import Card

cards = Card.where(set='NEO').where(number=4).all()	# 蛾との親睦


for card in cards:
	if card.foreign_names:
		for foreign_name in card.foreign_names:
			if foreign_name['language'] == 'Japanese':
				print(foreign_name['name'], card.set, card.number, foreign_name['imageUrl'])
				break
	else:
		print(card.name, card.set, card.number, card.image_url)
```
```
Befriending the Moths // Imperial Moth NEO 4 http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=548294&type=card
Befriending the Moths // Imperial Moth NEO 4 http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=548295&type=card
```
![Befriending the Moths // Imperial Moth](http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=548294&type=card "Befriending the Moths // Imperial Moth") ![Befriending the Moths // Imperial Moth](http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=548295&type=card "Befriending the Moths // Imperial Moth") 
は？？

## 日本語版カードが印刷されているにもかかわらず、`card.foreign_names is None`であるカードが存在する

[日本語版カード画像は存在する](https://gatherer.wizards.com/Pages/Card/Details.aspx?multiverseid=549819)のだが、`card.foreign_names is None`であるため`multiverseid`が特定できず、日本語版カード画像のURLを特定できない。
MTG Developers側の問題なので手の打ちようが無い。
2022/4/1時点で、カルドハイム以降の両面カードが`card.foreign_names is None`であるようだ。

## セットの略号とコレクター番号だけではカード画像が一意に定まらない

ご丁寧に`card.name`も同じで、`card.names`に至っては`None`である。なんでや。
`card.multiverseid`で区別することは可能だが、どちらが第一面かは分からない。
先にヒットした方が第一面であることをお祈りしましょう。

```py
from mtgsdk import Card

cards = Card.where(set='NEO').where(number=4).all()	# 蛾との親睦

name = None
image_url = None
for card in cards:
	if card.foreign_names:
		for foreign_name in card.foreign_names:
			if foreign_name['language'] == 'Japanese':
				name = foreign_name['name']
				image_url = foreign_name['imageUrl']
				break
	else:
		name = card.name
		image_url = card.image_url
	if image_url:
		break

print(name, card.set, card.number, image_url)
```
```
Befriending the Moths // Imperial Moth NEO 4 http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=548294&type=card
```
![Befriending the Moths // Imperial Moth](http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=548294&type=card "Befriending the Moths // Imperial Moth")

流石にもう問題は起こらないでしょう！

```py
from mtgsdk import Card

cards = Card.where(set='MID').where(number=268).all()	# 平地

name = None
image_url = None
for card in cards:
	if card.foreign_names:
		for foreign_name in card.foreign_names:
			if foreign_name['language'] == 'Japanese':
				name = foreign_name['name']
				image_url = foreign_name['imageUrl']
				break
	else:
		name = card.name
		image_url = card.image_url
	break

print(name, card.set, card.number, image_url)
```
```
平地 MID 268 http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=538277&type=card
```
![平地](http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=538277&type=card "平地")
なんでや！！！

## 日本語版カード画像がカード裏面の画像であるカードが存在する

『イニストラード：真夜中の狩り』のフルアート基本土地でこの問題が起きる。『イニストラード：真紅の契り』もフルアート基本土地だけど、この問題は起きない。
『イニストラード：真夜中の狩り』のフルアート基本土地は日本語版が印刷されて...います。Gathererくん仕事して？
実際の画像データ以外からカード画像がカード裏面の画像かどうかを判別する方法は無い。
なので、どうしても回避したい場合は、ダウンロードしてハッシュ値確認でもするしかない。

```py
from hashlib import md5
from urllib.request import urlopen
from mtgsdk import Card

CARD_BACK_IMAGE_MD5 = 'db0c48db407a907c16ade38de048a441'  # あらかじめ確認したカード裏面の画像のMD5

cards = Card.where(set='MID').where(number=268).all()

name = None
image_url = None
for card in cards:
	if card.foreign_names:
		for foreign_name in card.foreign_names:
			if foreign_name['language'] == 'Japanese':
				image_url = foreign_name['imageUrl']
				if image_url:
					with urlopen(url=image_url) as response:
						image_data = response.read()
						if md5(image_data).hexdigest() != CARD_BACK_IMAGE_MD5:
							name = foreign_name['name']
						else:
							image_url = None
				break
	if image_url is None:
		name = card.name
		image_url = card.image_url
	if image_url:
		break

print(name, card.set, card.number, image_url)
```
```
Plains MID 268 http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=538227&type=card
```
![平地](http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=538227&type=card "平地")
ここまでやる必要はあるのだろうか。poslogithubは訝しんだ。