つね日ごろから音楽の流行にはついていかなければいけないな、と心がけていて
個人的に流行の確度が高いと思っている Spotify の 「バイラルトップ 50 - 日本」 を
上位だけでも毎日かならず聴くようにしています。
Apple Music や Amazon Music でも日本で今よく聴かれている曲、
といったプレイリストがあるにはあるのですが、
同じアーティストの曲が上位にきたりしがちで、いまいちだと感じていました。
スクリプトを作った理由
- 最近 USB-DAC を導入し、 PC でのハイレゾ再生ができるようになった
- ただし Spotify は音質が Apple Music や Amazon Music Unlimited と比べると
そこまでよろしくない (ロスレス, ハイレゾでのストリーミングがされていない) - なので、 Soundiiz や Tune My Music といったサービスでプレイリストを同期して、
Apple Music / Amazon Music Unlimited で聴きたい - ただし、 Spotify の公式チャートはプレイリスト形式ではなく、エクスポートできない
というものがありました。
そこで、 スクリプトを作成して、Soundiiz や Tune My Music といったサービスでも
読み込めるプレイリストに変換してしまうことにしました。
ひとまず名前は Spotify Japan Viral 50 Playlist Updater とでも名付けます。
今回は、 Python で Spotify Web API を利用するこのとのできる、
Spotipy というライブラリを使用しました。
Spotify はログインしてしまえば、こちらのチャートから CSV がダウンロードできるので、
その自動化には Playwright を使うことにします。
大まかな流れとしては以下のとおりです。
- Playwright で Charts サイトにログインして CSV を取得
- Spotipy + Refresh Token でプレイリストを操作
- GitHub Actions + Docker で定期実行
セットアップ手順
とりあえずローカルでの実行手順を説明します。
ひとまずは Spotify プレイリストを操作するための Web アプリケーションが必要になるので、
Spotify for Developers にログインして、 Dashboard から Create app をします。

App name, App Description は適当で OK で、今回はサーバーサイドのみで実行するため、
Redirect URIs を http://127.0.0.1:8888/callback とします。127.0.0.1 を localhost と書きたいところですが、セキュリティ的によろしくないとされています。
Which API/SDKs are you planning to use? の項目は、
今回プレイリストを操作するだけなので、 Web API のみで大丈夫です。
そして、操作する公開プレイリストを Spotify で適当に作っておきます。
次に、 .env を作成します。
SPOTIPY_CLIENT_ID=xxxx
SPOTIPY_CLIENT_SECRET=xxxx
SPOTIPY_REDIRECT_URI=http://127.0.0.1:8888/callback
SPOTIFY_REFRESH_TOKEN=xxxx
SPOTIFY_PLAYLIST_ID=プレイリストID
STATE_JSON_B64=base64化したstate.jsonSPOTIPY_CLIENT_ID と SPOTIPY_CLIENT_SECRETS は
先ほどつくった Web アプリケーションのページから拾ってきてください。SPOTIFY_PLAYLIST_ID はプレイリストの https://open.spotify.com/playlist/
につづいている文字列が ID になります。
SPOTIFY_REFRESH_TOKEN と STATE_JSON_B64 は後ほどでてきます。
% python3 save_session.py
Log in manually and wait for the 'Download' button to appear.
Press Enter after login is complete...
Saved session to state.json
Copy the following and add it to your secrets as STATE_JSON_B64:
eyJ.....実行すると headless モードではない Chromium が立ち上がるので、ログインしてください。
もしかすると日本語が文字化けしているかもしれないですが、雰囲気でがんばってください。
ログインに成功すると、長い文字列 (Base64 エンコードされた JSON) が生成されるので、
それを全部、先ほど作った .env の STATE_JSON_B64 にコピペしてください。
save_session.py の中身はこちら
from playwright.sync_api import sync_playwright
import base64
def save_login_state_and_encode():
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
context = browser.new_context()
page = context.new_page()
page.goto("https://charts.spotify.com")
print("Log in manually and wait for the 'Download' button to appear.")
input("Press Enter after login is complete...")
context.storage_state(path="state.json")
browser.close()
print("Saved session to state.json")
with open("state.json", "rb") as f:
encoded = base64.b64encode(f.read()).decode("utf-8")
print("\nCopy the following and add it to your secrets as STATE_JSON_B64:\n")
print(encoded)
if __name__ == "__main__":
save_login_state_and_encode()セッションは定期的に死んでしまうので、Refresh Token を使って、自動的に更新します。
% python get_refresh_token.py
Open this URL to authorize: https://accounts.spotify.com/authorize?client_id=*****&response_type=code&redirect_uri=http%3A%2F%2F127.0.0.1%3A8888%2Fcallback&scope=playlist-modify-public+playlist-modify-private
Paste the redirected URL here: http://127.0.0.1:8888/callback?code=*****
/get_refresh_token.py:20: DeprecationWarning: You're using 'as_dict = True'.get_access_token will return the token string directly in future versions. Please adjust your code accordingly, or use get_cached_token instead.
token_info = sp_oauth.get_access_token(code, as_dict=True)
Your refresh_token is: *****DeprecationWarning が出ていて get_cached_token() を使うよう書いてありますが、
今回のように最初にリフレッシュトークンを取得するワンショットの用途においては適していません。
なので一旦は無視して大丈夫です。
URL に飛ぶと、 このサイトにアクセスできません とエラーになると思いますが、127.0.0.1 にアクセスしているため、正常です。そのまま URL を全部コピペしてください。
トークンを取得できたら、 .env の SPOTIFY_REFRESH_TOKEN に入力できます。
get_refresh_token.py の中身はこちら
import os
from spotipy.oauth2 import SpotifyOAuth
from dotenv import load_dotenv
load_dotenv()
sp_oauth = SpotifyOAuth(
client_id=os.getenv("SPOTIPY_CLIENT_ID"),
client_secret=os.getenv("SPOTIPY_CLIENT_SECRET"),
redirect_uri=os.getenv("SPOTIPY_REDIRECT_URI"),
scope="playlist-modify-public playlist-modify-private"
)
auth_url = sp_oauth.get_authorize_url()
print("Open this URL to authorize:", auth_url)
response = input("Paste the redirected URL here: ")
code = sp_oauth.parse_response_code(response)
token_info = sp_oauth.get_access_token(code)
print("Your refresh_token is:", token_info["refresh_token"])ここまでできたら、あとは update_playlist.py を実行すればプレイリストが更新されます。
% python3 update_playlist.py
Starting script...
state.json extracted.
Downloading CSV from Spotify Charts...
CSV downloaded: viral.csv
Authenticating with Spotify API...
playlist_id: *****
Fetching current playlist tracks...
Removed 100 track(s) from the playlist.
Number of URIs to add: 100
Added 100 track(s) to the playlist.これで、プレイリストの中身が完全に空っぽにした後にチャートの曲が追加される処理が走ります。
update_playlist.py の中身はこちら
import csv
import os
import base64
from dotenv import load_dotenv
from spotipy import Spotify
from spotipy.oauth2 import SpotifyOAuth
from playwright.sync_api import sync_playwright
print("Starting script...")
load_dotenv()
def decode_state_json():
encoded = os.getenv("STATE_JSON_B64")
if not encoded:
print("STATE_JSON_B64 is not defined.")
return False
decoded = base64.b64decode(encoded).decode("utf-8")
with open("state.json", "w", encoding="utf-8") as f:
f.write(decoded)
print("state.json extracted.")
return True
def download_spotify_csv():
print("Downloading CSV from Spotify Charts...")
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
context = browser.new_context(
storage_state="state.json",
accept_downloads=True,
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
locale="ja-JP",
timezone_id="Asia/Tokyo",
geolocation={"longitude": 139.6917, "latitude": 35.6895},
permissions=["geolocation"]
)
page = context.new_page()
page.add_init_script("Object.defineProperty(navigator, 'webdriver', {get: () => undefined})")
try:
page.goto("https://charts.spotify.com/charts/view/viral-jp-daily/latest", timeout=30000)
page.wait_for_load_state("domcontentloaded")
page.evaluate("document.getElementById('onetrust-banner-sdk')?.remove()")
page.locator('button[data-encore-id="buttonTertiary"]').first.wait_for(timeout=15000)
with page.expect_download(timeout=15000) as download_info:
page.locator('button[data-encore-id="buttonTertiary"]').first.click()
download = download_info.value
download.save_as("viral.csv")
print("CSV downloaded: viral.csv")
except Exception as e:
print("CSV download failed:", e)
try:
page.screenshot(path="debug.png", full_page=True)
except:
pass
finally:
browser.close()
def get_spotify_client():
refresh_token = os.getenv("SPOTIFY_REFRESH_TOKEN")
if not refresh_token:
print("SPOTIFY_REFRESH_TOKEN is not set.")
exit(1)
auth_manager = SpotifyOAuth(
client_id=os.getenv("SPOTIPY_CLIENT_ID"),
client_secret=os.getenv("SPOTIPY_CLIENT_SECRET"),
redirect_uri=os.getenv("SPOTIPY_REDIRECT_URI"),
)
try:
token_info = auth_manager.refresh_access_token(refresh_token)
return Spotify(auth=token_info["access_token"])
except Exception as e:
print("Failed to get access token:", e)
exit(1)
def update_playlist():
print("Authenticating with Spotify API...")
sp = get_spotify_client()
playlist_id = os.getenv("SPOTIFY_PLAYLIST_ID")
print("playlist_id:", playlist_id)
if not os.path.exists("viral.csv"):
print("viral.csv not found.")
return
print("Fetching current playlist tracks...")
results = sp.playlist_items(playlist_id)
track_uris = [item["track"]["uri"] for item in results["items"] if item["track"]]
if track_uris:
sp.playlist_remove_all_occurrences_of_items(playlist_id, track_uris)
print(f"Removed {len(track_uris)} track(s) from the playlist.")
with open("viral.csv", newline="", encoding="utf-8") as csvfile:
reader = csv.DictReader(csvfile)
uris = [row["uri"] for row in reader if row["uri"].startswith("spotify:track:")]
print("Number of URIs to add:", len(uris))
if uris:
sp.playlist_add_items(playlist_id, uris)
print(f"Added {len(uris)} track(s) to the playlist.")
if __name__ == "__main__":
if decode_state_json():
download_spotify_csv()
update_playlist()browser = p.chromium.launch(headless=True) の行を False にすると、
実際に Chromium が立ち上がって処理を眺めることができます。
User Agent や Geolocation の偽装をしているのは、開発中にうまく取得できなかったときの
トライアルアンドエラーの名残です。(なくてもおそらく動くとは思います。)
GitHub Actions と Docker を用いた定期実行
ここまで問題なくできているのであれば、あとは簡単です。
まず、 .env の内容を GitHub Secrets に登録します。
リポジトリの Settings > Secrets and variables > Actions に保存してください。
Dockerfile と Workflow ファイルのサンプルは以下になります。
多分そのままで動くと思います。
FROM mcr.microsoft.com/playwright/python:v1.42.0-jammy
WORKDIR /app
COPY . /app
RUN pip install --no-cache-dir -r requirements.txt
RUN playwright install --with-deps chromium
CMD ["python", "update_playlist.py"].github/workflows には以下のような Workflow ファイルを置いてください。
name: Run Spotify Updater (via Docker)
on:
schedule:
- cron: '0 0 * * *' # 毎日9:00 JST
workflow_dispatch:
jobs:
run-updater:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build Docker image
run: docker build -t spotify-updater .
- name: Run updater container
run: |
echo "$STATE_JSON_B64" | base64 -d > state.json
docker run --rm \
-e SPOTIPY_CLIENT_ID=${{ secrets.SPOTIPY_CLIENT_ID }} \
-e SPOTIPY_CLIENT_SECRET=${{ secrets.SPOTIPY_CLIENT_SECRET }} \
-e SPOTIPY_REDIRECT_URI=${{ secrets.SPOTIPY_REDIRECT_URI }} \
-e SPOTIFY_REFRESH_TOKEN=${{ secrets.SPOTIFY_REFRESH_TOKEN }} \
-e SPOTIFY_PLAYLIST_ID=${{ secrets.SPOTIFY_PLAYLIST_ID }} \
-e STATE_JSON_B64="${{ secrets.STATE_JSON_B64 }}" \
spotify-updaterあとは GitHub に git push すれば定期的にスクリプトが実行されます。
今回は Viral 50 Japan のチャートを変換していますが、
Spotify Charts にあるものであれば同様に動くと思います。
その場合は update_playlist.py を適宜書き換えてみてください。
以下が今回作成できた、サンプルプレイリストです。
https://open.spotify.com/playlist/4Bf4jxM1WylLwqLmld8mbUわたしが実際に Soundiiz でつかっているものなので、
今回の手順がめんどくさい方はそのまま使ってしまって大丈夫です。
参考にしていただければ幸いです。
PC でのハイレゾ再生環境については気が向いたらそのうち書こうと思います。