2021-02-10

Spotify APIを使ってCDのジャケット画像を取得する

cd_jacket_img

PythonでSpotify APIを使って、CDのジャケット画像の取得を試みます。

概要

Windows 10で管理しているミュージックディレクトリを使って、Spotify APIのQueryを作成して、CDのジャケット画像のURLを取得します。ジャケット画像を取得することで、プレイヤーソフトでジャケットが表示されるようになります。手作業だと手間がかかる作業ですが、大部分を自動取得できるようにします。

Repository: https://github.com/rmc8/cd_jacket_scraper_for_spotify

APIを使う準備

ここでは、APIを使うためのキーの取得と、Pythonのライブラリの導入をします。

APIキーの取得

Developerページにログインします。Spotifyのアカウントでログインできます。アカウントがない場合には、Spotifyのアカウントを作成してください。

ログインできたら、[CREATE ANN APP]をクリックします。
「App name」には「CD Jacket scraper」などわかる名前を入力して、「App description」には「Get the CD jacket」などわかるような説明を入力します。その後、2つあるチェックボックスにチェックをします。チェックするとPermissionやGuidelineなどに同意することになります。入力内容に誤りがないことを確認して、[CREATE]ボタンをクリックします。

クリック後、APPが作成されダッシュボードが表示されます。ダッシュボード内にある、[SHO CLIENT SECRET]をクリックして、Client IDとClient SecretをWindowsの環境変数に登録します。

[FYI]環境変数の設定

Client IDは、変数名を「SPOTIFY_CLIENT_ID」にして、変数値にダッシュボードに表示されている値を貼り付けます。同様に、Client Secretは、変数名を「SPOTIFY_SECRET_ID」にして、変数値にダッシュボードの値を貼り付けます。変数の登録が完了したら、変数を使用できる状態にするため、PCを一度再起動してください

Pythonのライブラリを導入する

pipなどで下記ライブラリを導入してください。

  • spotipy
  • requests
  • PySimpleGUI
pip install spotipy requests PySimpleGUI

大まかな動作内容

プログラム(main.py)を実行すると、インプットとなるディレクトリのファイルパスを選択するダイアログが表示されます。選択したディレクトリのパスをrootとして使い、os.walk()を使ってrootディレクトリの配下にあるディレクトリを探索します。ここでは以下のディレクトリ構造を例にします。

D:\
├─(K)NoW_NAME
│  ├─Freesia(TVアニメ「サクラクエスト」EDテーマ)
│  │  ├─1-Freesia.flac
│  │  ├─2-Blue Rose.flac
│  │  └─3-ASTER.flac
│  └─混沌の中で踊れ
│      ├─01 - Who am I.flac
│      ├─02 - Night SURFING.flac
│      └─08 - BAD NICK.flac
├─sub_dir
│  ├─Art Blakey And THE JAZZ MESSENGERS
│  │  └─Impulse!
│  │      ├─01 - Alamode.flac
│  │      ├─02 - Invitation.flac
│  │      └─06 - Gee Baby, Ain't I Good To You.flac
│  └─上北 健
│      └─SCOOP
│          ├─1-false color.flac
│          ├─2-DIARY.flac
│          └─9-アイニイキル.flac
└─蟲師
    └─蟲師 オリジナルサウンドトラック 蟲音 全 Soundtrack 弐
        ├─01 - 夢路.flac
        ├─02 - 「天辺の糸」.flac
        └─24 - 「緑の座」(OnAir Ver).flac

D:\をrootとしたときに、Freesia(TVアニメ「サクラクエスト」EDテーマ), 混沌の中で踊れ, Impulse!, … , 蟲師 オリジナルサウンドトラック 蟲音 全 Soundtrack 弐と最下部のディレクトリにたどり着くまで検索します。この最下部のディレクトリが、アルバム名にあたり、最下部のディレクトリから1つ上位のディレクトリがアーティス名の構造となります。

この構造を用いて、アルバム名とアーティスト名でSpotify APIで検索して、画像URLを取得します。画像URLから画像を取得して、fr"d:\\\\(.*\\\\)*{artist_name}\\\\{album_name}\\\\{album_name}\.jpg" の形式で保存します。

コードの解説

main.pyのmain関数を使って順番に解説します。Pythonのバージョンは3.8以降を前提とします。

rootディレクトリの取得

search_dirに検索するディレクトリのファイルパスを格納します。関数は以下の通りです。

def sel_input_dir():
    layout = [
        [sg.Text("ディレクトリ"), sg.InputText(key="ret"),
         sg.FolderBrowse(key="dir")],
        [sg.Submit(), sg.Exit()],
    ]
    window = sg.Window("Input", layout)
    while True:
        event, values = window.read()
        if event == "Submit":
            ret = values["ret"]
            break
        elif event in (sg.WINDOW_CLOSED, "Exit"):
            ret = None
            break
    window.close()
    return ret

PySimpleGUIでディレクトリのパスを取得するダイアログの表示とパスを返す処理を記載しています。layout変数で、GUIの構成を記して、window変数でWindowのタイトルと layoutを格納したWindowオブジェクトを取得します。

無限ループ内にwindowを読み出す処理を記載して、eventを感知する変数とInputボックス値を格納した辞書を取得します。if文で[Submit]・[Exit]・Windowを閉じるボタンを押されたか判定をして、Eventの種類に応じて処理を振り分けます。Windowを閉じたりExitしたりしたら、返り値にNoneを設定し、Submitが押されたときには、ディレクトリのパスを返り値に設定します。それぞれのケースで返り値の設定が完了したあと、無限ループを終了して、Windowを閉じてGUIの入力結果を返します。

なお、Noneを返された場合にはmain関数内の処理でプログラムを終了するようになっています。

search_dir = sel_input_dir()
# 中略
if search_dir is None:
    exit()

Spotify APIに接続する

spotify_auth関数を使って、Spotify APIに接続します。

def spotify_auth():
    CLIENT_ID = os.environ["SPOTIFY_CLIENT_ID"]
    CLIENT_SECRET = os.environ["SPOTIFY_SECRET_ID"]
    client_credentials_manager = spotipy.oauth2.SpotifyClientCredentials(
        CLIENT_ID, CLIENT_SECRET
    )
    return spotipy.Spotify(client_credentials_manager=client_credentials_manager)

環境変数から、Client IDとClient Secretの値を取得します。os.environを使うと、辞書形式で{"変数名1": "変数値1", ... , "変数名N": "変数値N", } のように値を読み出せます。Githubなど外部にアップロードしては困る値があるときに、環境変数などをつかって値を外だしするとセキュリティや値の変更など対応しやすくなります。API接続のコードの書き方は、ドキュメントに従って書けば特に考える必要もないと思われます。

Document: Welcome to Spotipy!

ファイルを探索する

get_cd_infoジェネレータで、ファイルパス・アーティスト名・アルバム名を取得します。

def get_cd_info(path: str) -> Tuple[str, str, str]:
    """
    Search to the lowest directory and return the name of the CD, the name of the artist and its file path.
    Args:
        path ([str]): File path to search
    Yields:
        [Tuple[str]]: dir_path, artist, cd_name
    """
    for root, dirs, _ in os.walk(path):
        if not dirs:
            p = pathlib.Path(root)
            yield root, p.parent.name, p.name
        for dir in dirs:
            get_cd_info(f"{path}\\{dir}")

引数pathで探索するディレクトリのPathを受け取り、os.walk()で探索をします。for文では、rootでディレクトリのPath、dirsでroot内にあるディレクトリのリストを受け取っています。アンダースコアは未使用ですが、root内にあるファイルのリストを取得できます。

dirsリストに値があるかif文で判定します。dirsリストに値がない場合、最下層のディレクトリであると判断できます。空リストをif文にかけるとFalseになるので、notを使ってTrueにします。その後、if文内でPathlibを使ってディレクトリ名(アルバム名)と親ディレクトリの名前(アーティスト名)を抽出して、yieldでディレクトリのパスとまとめて返します。

ディレクトリがあるときは、for文で順番にディレクトリを読み出して、get_cd_infoジェネレータに下位のディレクトリを引数に設定して呼び出します。

for文ではファイルのリストをアンダースコアにして未使用の状態ですが、yieldの前に[".flac", ".mp3", ".wav", ".dsf"]など、音声ファイルが入っている場合にのみyieldで値を返すようも書き換えられます。この処理を加えると、大量のディレクトリを探索しても誤って画像を取得することを防げます。

画像のファイルパスを生成する

ジェネレータから取得した値を使って、画像の保存先のパスを生成します。最下部のディレクトリまで探索するので、path変数にD:\(K)NoW_NAME\Freesia(TVアニメ「サクラクエスト」EDテーマ)のようにアーティスト名とアルバム名を含んだ状態で値が格納されます。f文字列を使って、{path}\{アルバム名}.jpgの形式で画像のファイルパスを生成します。os.path.existsで画像のファイルパスが存在している場合には、処理済みとして次のパスを取得し、存在しない場合にはAPIで画像のURLの取得を試みます。

アルバム名からノイズを除去する

アルバム名には(通常盤)・(限定盤)・[Disk1]・[Disk-2]・[DISK 3]などAPIで検索の妨げになるワードが含まれることがあります。CDのデータベースでアルバム名や曲名など取得すると、ディスクや盤の違いを判別するためにこのような情報が付与されます。正規表現を用いて、ノイズを除去します。

album = re.sub(r"\s*(\[|\(|(|【){1}.*(\]|\)|)|】)", "", album)
album = re.sub(r"\s*(Disk|DISK){1}(\s|\-)*[0-9]{1,2}", "", album)

Spotify Search APIで検索する

Spotify Search APIを使って2回検索します。1回目はアーティスト名+アルバム名で検索します。フィルターが強くなりすぎる場合もあるため、2回目はアルバム名のみで検索をします。

for try_num in range(2):
    sleep(1)
	# 中略(正規表現)
    query = album if try_num else f'"{artist}"&"{album}"'
    q_type = "album" if try_num else "artist,album"
    res = sp.search(query, limit=1, offset=0, type=q_type)
    items = res["albums"]["items"]
    if (img_url := _get_img_url(items)) is None:
	    if try_num:
		    print(f"[SKIP] {img_path}")
        continue

for文で2回検索する処理を書き、try_num変数で検索Queryの切り替えをできるようにします。{Trueのときの値} if {条件式・値} else {Falseの時の値}の形式でクエリの切り替え処理を書きます。if文の条件式の値(int型)が0であればFalseとなり、それ以外であればTrueとなります。その挙動をつかい、1回目の処理ではquery変数にアーティスト名とアルバム名をセットします。日本語の検索をできるようにダブルクォーテーションで囲い、複数検索に対応するため&をつかって検索の語句を結びます。リトライの際は単にalbum変数をつかいアルバム名だけで検索できるようにします。同様にq_type変数で、artist+albumで検索するように設定して、リトライの際にはalbumのみで検索する設定をします。

APIの使用方法は日本語で検索しても正確でないものも、情報が不十分なものも含まれています。不明点がある場合には、公式のドキュメントを調べたり、英語でも検索したりすることもお勧めします。

Document: Web API Reference

Queryなど設定が完了したら、searchを使い検索します。limit引数で取得する検索結果の件数、offset引数で1番目の値の取得位置の調整ができます。Searchすると以下のようなResponseを得られます。

{
   "albums":{
      "href":"https://api.spotify.com/v1/search?query=Kiss+The+Sun&type=album&offset=0&limit=1",
      "items":[
         {
            "album_type":"album",
            "artists":[
               {
                  "external_urls":{
                     "spotify":"https://open.spotify.com/artist/2RJ0cQlHOmiZ7JuHBwgUbV"
                  },
                  "href":"https://api.spotify.com/v1/artists/2RJ0cQlHOmiZ7JuHBwgUbV",
                  "id":"2RJ0cQlHOmiZ7JuHBwgUbV",
                  "name":"Vampires Everywhere!",
                  "type":"artist",
                  "uri":"spotify:artist:2RJ0cQlHOmiZ7JuHBwgUbV"
               }
            ],
            "available_markets":[
               "JP",
               "US"
            ],
            "external_urls":{
               "spotify":"https://open.spotify.com/album/6gyVPhElDPStV68cCFQlY5"
            },
            "href":"https://api.spotify.com/v1/albums/6gyVPhElDPStV68cCFQlY5",
            "id":"6gyVPhElDPStV68cCFQlY5",
            "images":[
               {
                  "height":640,
                  "url":"https://i.scdn.co/image/ab67616d0000b2738fa44d0ead2ee4635e5b4d16",
                  "width":640
               },
               {
                  "height":300,
                  "url":"https://i.scdn.co/image/ab67616d00001e028fa44d0ead2ee4635e5b4d16",
                  "width":300
               },
               {
                  "height":64,
                  "url":"https://i.scdn.co/image/ab67616d000048518fa44d0ead2ee4635e5b4d16",
                  "width":64
               }
            ],
            "name":"Kiss the Sun Goodbye",
            "release_date":"2010-07-16",
            "release_date_precision":"day",
            "total_tracks":12,
            "type":"album",
            "uri":"spotify:album:6gyVPhElDPStV68cCFQlY5"
         }
      ],
      "limit":1,
      "next":"https://api
.spotify.com/v1/search?query=Kiss+The+Sun&type=album&offset=1&limit=1",
      "offset":0,
      "previous":"None",
      "total":134
   }
}

画像がある場合、jsonの0番目のアイテムのimagesキーに画像のURLがあります。_get_img_urlを使いURLの取得を試み、URLの取得ができれば画像を保存する処理をします。URLの取得に失敗したときは、検索条件を緩和して画像の取得をリトライします。

短時間にたくさんのRequestをしてサーバーに負担をかけてしまわないように、sleep(1)でRequestの間隔を1秒あけるようにしています。

画像を保存する

URLの取得が成功したら、requestsでgetを実行して画像の取得をします。その後、requestsのResponseをディレクトリ内に書き込むことで画像を保存できます。画像を保存した後は、for文をbreakさせて再処理を実行しないようにします。

実行後のイメージ

実行前は以下のようにほとんどジャケットが画像がない状態でした。

実行後は8割強ほど画像が取得できました。

まとめ

100行足らずのコードで、500を超えるアーティストで多量のCDジャケットを取得できました。手作業ではとても時間がかかり飽きる作業ですが、コードを書いて実行するだけで大部分の画像取得作業を自動で完了させられます。
Pythonを書く手間も習得のコストも大きくはかからないので、CD画像の取得に役立てたら幸いです。