🎵

Spotify APIで好きなアーティストのインスト楽曲プレイリストを作成する

に公開

これは何か

pythonでSpotify APIを利用し、指定したアーティストのインスト楽曲を集めたプレイリストを自動で作成する方法について説明します。

※Spotify APIを利用するにはspotifyのサブスクに加入している必要があります。

動機

Spotifyにはユーザが作成したプレイリストを他のユーザが利用できる機能があります。私は、よく他のユーザの方々が作ってくださっているインスト楽曲(ボーカルの入っていない楽曲)のプレイリストを聞きながら作業をします。

しかし、アーティストの楽曲をもれなく集め、また最新の楽曲をフォローしているプレイリストはあまり見つかりません。

これは、そもそもアーティストの全楽曲から特定の条件を満たす楽曲をもれなく集めることが大変な作業であり、さらに新しい楽曲がリリースされるたびにその楽曲をプレイリストに追加していくのが大変な作業であるためと考えられます(リリース日などを考慮してソートしようとすると、更に労力がかかります)。

そこで、Spotify APIを利用して、特定のアーティストのインスト楽曲を自動で収集してプレイリストを作成することができないかと考え、実際にpythonとspotipyライブラリで実装を行いました。spotifyのサブスクに加入しており、pythonについての一定の知識があれば、自分の好きなアーティストの楽曲のプレイリストを作成できます。

現在は、楽曲の更新に応じて自動でスクリプトが実行されるようにはなっていません。たまに手動で実行して、その時点の最新のインスト楽曲を集めたプレイリストを作成する、という用途を想定しています。個人的に利用するプレイリストを作成するのが目的なので、これで十分という判断です。

インスト楽曲のプレイリストだけでなく、ボーカルの入った楽曲のみのプレイリストの作成などもできます。

前提

作成したいのは学マスのインスト楽曲を集めたプレイリストです。以下のような特徴があります。

  • アーティスト(初星学園)自身がインスト楽曲をリリースしている。
  • インスト楽曲には楽曲名に"Instrumental"というキーワードが含まれている。

要件

上の前提を踏まえ、以下の要件でスクリプトを作成しました。

  • 指定したアーティストの楽曲のみを収集する。
  • 楽曲名に含まれるキーワード(例:"Instrumental")でフィルタリングを行う。

重複処理

  • 名前が完全に同じ楽曲の別バージョン(ボーカルが異なる、など)が存在する場合は、リリース日が古いものを残す。

ソート

  • プレイリスト内の楽曲は、リリース日の降順(最新のものが先頭)となるようにソートする。

プレイリストの作成/更新

  • プレイリストの名前を指定し、もし同名のプレイリストが存在する場合は、既存のプレイリストを更新する。

プレイリストの更新を行う際に、既存のプレイリストとの差分のみ更新を行うことはせず、毎回全ての楽曲を削除し、新たに追加する形としました。これは、差分更新を行う場合に楽曲が意図したソート順になることを保証するのがやや面倒だからです。
ただし、これにより、プレイリストを更新するたびにプレイリスト内の楽曲の更新日時がリセットされることになります。(既存の楽曲は更新日時がリセットされないほうが望ましくはあるのですが、自分が聴くプレイリストを作るのが目的なので、気にしないことにします)


プレイリストを更新すると、全楽曲の更新日時がリセットされる

手順

手順を簡単に説明します。

Spotify Developerアカウントの作成

Spotify APIを利用するために、まずはSpotify Developerアカウントを作成します。

  • https://developer.spotify.com/ でアカウントを作成してください。
  • ダッシュボード https://developer.spotify.com/dashboard にアクセスし、Create appをクリック
  • App nameとApp descriptionに任意の名前と説明を入力
  • Redirect URIにはhttp://127.0.0.1:8080/を追加する(スクリプトをローカルで実行する場合、これで十分です)
  • Saveをクリック
  • Client IDとClient Secretが取得できます。

スクリプトの準備

$ mkdir spotify-playlist-creator
$ cd spotify-playlist-creator
$ touch create_playlist_with_keyword.py

作成したcreate_playlist_with_keyword.pyに以下のコードをコピーしてください。もし、既存のコードで対応できないような条件やソート方法を適用したい場合、このコードをカスタマイズしてみてください。

create_playlist_with_keyword.py
from dataclasses import dataclass
import sys
from typing import Dict, List, Any
import json

from dotenv import load_dotenv
import spotipy
from spotipy.oauth2 import SpotifyOAuth


load_dotenv() # load environment variables from .env file

SCOPE = "playlist-modify-public playlist-modify-private"

@dataclass
class Config:
    artist_name: str
    playlist_name: str
    playlist_description: str
    keyword: str
    include_keyword: bool

def load_config(path: str) -> Config:
    try:
        with open(path, 'r', encoding='utf-8') as f:
            data = json.load(f)
            return Config(**data)
    except Exception as e:
        raise e

def main():
    assert len(sys.argv) == 2, "Usage: python create_playlist_with_keyword.py <config_file_path>"
    config_file_path = sys.argv[1]
    config = load_config(config_file_path)


    sp = spotipy.Spotify(auth_manager=SpotifyOAuth(
        scope=SCOPE
    ))

    user_info = sp.current_user()
    user_id = user_info['id']

    # search artist
    artist_results = sp.search(q=f'artist:{config.artist_name}', type='artist')
    items = artist_results['artists']['items']

    if not items:
        print(f'No artist found for {config.artist_name}')
        return

    artist_id = items[0]['id']
    print(f"artist ID: {artist_id}")

    # fetch tracks of the target artist
    target_tracks = fetch_tracks_with_keyword(
        sp=sp,
        artist_id=artist_id,
        keyword=config.keyword,
        should_include_keyword=config.include_keyword,
        country='JP',
        keep_older=True
    )

    # sort tracks by release date
    target_tracks = sort_tracks_by_release_date(target_tracks, newest_first=True)
    track_uris = [track.uri for track in target_tracks]

    if not track_uris:
        print("No tracks found. Exiting.")
        return

    playlist_id = find_or_create_playlist(sp, user_id, config)
    existing_tracks_uris = get_playlist_track_uris(sp, playlist_id)

    # compare existing tracks with new tracks
    # and exit if there are no diffs
    cur_track_uris_set = set(existing_tracks_uris)
    new_track_uris_set = set(track_uris)
    if cur_track_uris_set == new_track_uris_set:
        print("Playlist is already up to date. No changes made.")
        return

    # update playlist
    replace_playlist_tracks(
        sp=sp,
        user_id=user_id,
        playlist_id=playlist_id,
        current_track_uris=existing_tracks_uris,
        new_track_uris=track_uris
    )


@dataclass
class TrackInfo:
    name: str
    uri: str
    release_date: str



def _get_all_pagenated_items(sp: spotipy.Spotify, results: Dict[str, Any]) -> List[Dict[str, Any]]:
    """Helper function to get all items from paginated Spotify results."""
    items = results['items']

    while results['next']:
        try:
            results = sp.next(results)
            items.extend(results['items'])
        except Exception as e:
            print(f"Error fetching paginated items: {e}")
            break

    return items


def fetch_tracks_with_keyword(sp: spotipy.Spotify, artist_id: str, keyword: str, should_include_keyword: bool, country: str = 'JP', keep_older: bool = True) -> List[TrackInfo]:
    """fetch list of tracks including/excluding keyword in their title

    Args:
        sp: spotipy.Spotify instance
        artist_id: Spotify artist ID
        keyword: Keyword to search for in track titles
        should_include_keyword: If True, include tracks with the keyword; if False, exclude them
        country: Country code (default: 'JP')
        keep_older: If True, keep older version of duplicate track names

    Returns:
        tracks: List[TrackInfo]
            A list of TrackInfo objects.
    """
    track_dict = {}
    album_types = ['album', 'single']
    all_albums = []

    for album_type in album_types:
        try:
            results = sp.artist_albums(artist_id, album_type=album_type, country=country, limit=50)
            all_albums.extend(_get_all_pagenated_items(sp, results))
        except Exception as e:
            print(f"Error fetching {album_type}s: {e}")
            exit(1)


    print(f"Total albums/singles fetched: {len(all_albums)}")

    album_ids = [album['id'] for album in all_albums]

    for i in range(0, len(album_ids), 20):
        batch_ids = album_ids[i:i + 20]
        try:
            albums_with_tracks = sp.albums(batch_ids)['albums']
        except Exception as e:
            print(f"Error fetching albums with tracks: {e}")
            continue

        for album in albums_with_tracks:
            if not album:
                continue

            for track in album['tracks']['items']:
                track_name = track['name']

                if should_include_keyword and keyword not in track_name:
                    continue
                if not should_include_keyword and keyword in track_name:
                    continue

                should_add_or_update = False
                if track_name in track_dict:
                    existing_date = track_dict[track_name].release_date
                    new_date = album['release_date']
                    if (keep_older and new_date < existing_date) \
                        or (not keep_older and new_date > existing_date):
                        should_add_or_update = True
                else: # New track
                    should_add_or_update = True

                if should_add_or_update:
                    track_info = TrackInfo(
                        name=track_name,
                        uri=track['uri'],
                        release_date=album['release_date']
                    )
                    track_dict[track_name] = track_info
                    print(f"Added/Updated track: {track_name} - {album['name']} ({album['release_date']})")

    track_list = list(track_dict.values())
    return track_list


def sort_tracks_by_release_date(tracks: List[TrackInfo], newest_first: bool = True) -> List[TrackInfo]:
    """Sort tracks by their release date.

    Args:
        tracks: List of TrackInfo objects.
        newest_first: If True, sort by newest first; otherwise, oldest first.

    Returns:
        Sorted list of TrackInfo objects.
    """
    return sorted(tracks, key=lambda x: x.release_date, reverse=newest_first)


def find_or_create_playlist(sp: spotipy.Spotify, user_id: str, config: Config) -> str:
    """Find an existing playlist by name or create a new one if it doesn't exist.

    Args:
        sp: spotipy.Spotify instance
        user_id: Spotify user ID
        playlist_name: Name of the playlist to find or create
    Returns:
        Playlist ID of the found or created playlist.
    """

    results = sp.current_user_playlists()
    all_playlists = _get_all_pagenated_items(sp, results)
    print(f"users' total playlists: {len(all_playlists)}")

    for playlist in all_playlists:
        if playlist['name'] == config.playlist_name:
            playlist_id = playlist['id']
            print(f"✅ Found existing playlist '{config.playlist_name}' (ID: {playlist_id}).")
            return playlist_id

    playlist = sp.user_playlist_create(
        user=user_id,
        name=config.playlist_name,
        public=True,
        description=config.playlist_description
    )
    playlist_id = playlist['id']
    print(f"✅ Created new playlist '{config.playlist_name}' (ID: {playlist_id}).")

    return playlist_id


def get_playlist_track_uris(sp: spotipy.Spotify, playlist_id: str) -> List[str]:
    """Get all tracks from a playlist.

    Args:
        sp: spotipy.Spotify instance
        playlist_id: Spotify playlist ID
    Returns:
        List of track URIs in the playlist.
    """
    all_tracks: List[Dict[str, Any]] = []

    results = sp.playlist_items(playlist_id)
    all_tracks.extend(_get_all_pagenated_items(sp, results))

    track_uris = []
    for item in all_tracks:
        # is_local: True if the track is user's local file
        if item['track'] and not item['is_local']:
            track_uris.append(item['track']['uri'])

    return track_uris


def replace_playlist_tracks(sp: spotipy.Spotify, user_id: str, playlist_id: str, current_track_uris: List[str], new_track_uris: List[str]) -> None:
    """Replace all tracks in a playlist with new tracks.

    Args:
        sp: spotipy.Spotify instance
        user_id: Spotify user ID
        playlist_id: Spotify playlist ID
        current_track_uris: List of current track URIs in the playlist
        new_track_uris: List of new track URIs to add to the playlist
    """
    # 1. Remove all existing tracks
    if current_track_uris:
        chunk_size = 100
        for i in range(0, len(current_track_uris), chunk_size):
            tracks_chunk = current_track_uris[i:i + chunk_size]
            sp.user_playlist_remove_all_occurrences_of_tracks(
                user=user_id,
                playlist_id=playlist_id,
                tracks=tracks_chunk
            )
        print("   deleted all existing tracks.")

    # 2. Add new tracks
    if new_track_uris:
        chunk_size = 100
        for i in range(0, len(new_track_uris), chunk_size):
            tracks_chunk = new_track_uris[i:i + chunk_size]
            sp.playlist_add_items(
                playlist_id=playlist_id,
                items=tracks_chunk
            )
        print(f"✅ Updated playlist. Current number of tracks: {len(new_track_uris)}")


if __name__ == "__main__":
    main()

必要なライブラリのインストール

$ pip install spotipy dotenv

spotipyはpythonでSpotify APIを利用するためのライブラリ、dotenvは、環境変数を.envファイルから読み込むためのライブラリです。

環境変数の設定

プロジェクトルートに.envファイルを作成してください。

$ touch .env

前の手順で取得したClient IDとClient Secretを.envファイルに設定してください。

SPOTIPY_CLIENT_ID="your_client_id"
SPOTIPY_CLIENT_SECRET="your_client_secret"
SPOTIPY_REDIRECT_URI="http://127.0.0.1:8080/"

REDIRECT_URIは、Spotify Developerダッシュボードで設定したものと同じにしてください。初回実行の認証に使用されます。

作成するプレイリストの設定

作成するプレイリストの設定はjsonファイルに記載します。

$ touch my_playlist.json

以下のように記載します。自分の作りたいプレイリストに合わせて適宜書き換えてください。

{
    "artist_name": "初星学園",
    "playlist_name": "学マス - Instrumental",
    "playlist_description": "初星学園のインスト楽曲集",
    "keyword": "Instrumental",
    "include_keyword": true
}
  • artist_nameは、楽曲を収集したいアーティスト名です。
  • playlist_nameは、作成するプレイリストの名前です。同名のプレイリストが存在する場合は、既存のプレイリストを更新します。
  • playlist_descriptionは、作成するプレイリストの説明文です。
  • keywordは、プレイリスト内の楽曲が含むべき/含むべきでない単語です。
  • include_keywordがtrueの場合、楽曲名にkeywordを含む楽曲を収集します。falseの場合、keywordを含まない楽曲を収集します。

スクリプトの実行

以下のコマンドでスクリプトを実行します。

$ python create_playlist_with_keyword.py my_playlist.json

スクリプトの実行が成功すれば、指定したアーティストのインスト楽曲を集めたプレイリストが作成されます。spotifyアプリやWebプレイヤーで確認してください。
初回は認証のためにブラウザが起動するかもしれません。ログインして認証を許可してください。

まとめ

spotify APIを利用し、特定のアーティストの楽曲を収集してプレイリストを作成する方法について説明しました。上の手順を踏むことで、自分だけのプレイリストを簡単に作成できるようになります。

改善点

  • 全てのアーティストがインスト楽曲をリリースしているわけではないため、他のアーティストが公開するインストカバー楽曲を収集できるようにする。
  • spotify artist IDを使ってアーティストを直接指定できるようにする(今は名前で指定している)

Discussion