スクレイピング+Spotify-APIで、アイドル楽曲大賞投票結果のプレイリストを作成しました
はじめに
自分のため、そして楽曲派のオタクの方々のために、2015年度から2023年度までのアイドル楽曲大賞における①メジャーアイドル楽曲部門、②インディーズ/地方アイドル楽曲部門のランキング1~100位までのプレイリストを作成した。
タイトルに書いてあるとおり、スクレイピング+Spotify-APIで自動作成したのでその手順とハマったポイントをまとめていく。
要約(プレイリストを見に来た人向け)
- 2022年度以前のプレイリストについては下記記事にまとめた。
- もし順位が違う、関係ない曲が入っているなどの問題があればぜひコメントやご連絡いただけると本当に助かります……
要約(使用技術を見に来た人向け)
- Rubyを使ったWebスクレイピングとSpotify-APIのプレイリスト作成機能を組み合わせることで、1800曲分のプレイリストを自動作成した。
- 今回はSpotify-APIのRubyラッパー、RSpotifyを利用した。
- 当該記事で記載しているコードについては下記リポジトリを参照。
アイドル楽曲大賞とは
ライターのピロスエさんが毎年12月に開催している、1年間で発表されたアイドル楽曲のランキングをみんなの投票で決めようという企画。
タワーレコードの一部店舗ではランキング上位の作品を特集したコーナーが作られるなど、注目度の高いイベントになっている。
【#新宿アイドル班】
— タワーレコード新宿店 (@TOWER_Shinjuku) 2023年12月30日
昨日発表となりました年末の恒例行事、#アイドル楽曲大賞 👑早速集積コーナーを立ち上げました✨皆さまの推しはランクインしてましたか❓アイドルの皆さん2023年お疲れ様でした🙌来年も良い楽曲と出会えますように…☺️ pic.twitter.com/yOqUA2JpJ3
具体的にどう動かしたか
①ランキングのスクレイピング(IdolMusicAwardCrawler)
投票結果のページのtableタグ内にデータが格納されているので、シンプルなopen-uri+nokogiriの構成で取って来る。
なお、CSVの区切り文字に\t(タブ文字)を使っているのは、通常の,(カンマ)区切りだとMirror,Mirrorでバグるし、;(セミコロン)区切りだとC;ONでバグるからである(エスケープしろという意見は粋じゃないので無視する)。
②Spotify APIによる楽曲取得、プレイリスト作成
②(1)OAuth認証(generate_spotify_auth.rb)
scrape_idol_music_award/generate_spotify_auth.rb at main · Otter-K/scrape_idol_music_award · GitHub
APIからプレイリストを作成するためには、作成主体となるSpotifyアカウントでOAuth認証を通す必要がある。
そのためOmniAuth2.0+Sinatraを使ってローカルでサーバーを立てようと思ったのだが、最新版の情報があまり見つからず地味に手こずった。
OmniAuth2.0+Sinatraのハマりポイントは別記事にまとめた。
②(2)楽曲取得(PlaylistCreator)
scrape_idol_music_award/create_playlist.rb at main · Otter-K/scrape_idol_music_award · GitHub
RSpotifyを使っているため、 RSpotify::Track.searchでパッと書けると思った……が、うまくいかない。
具体的に言うと、公式APIドキュメントに乗っているfilterを使ったクエリがRSpotify::Track.searchでは通らなかった。
色々調査してみると、Client Credentials Grantフローで発行したトークンだとfilterが使えない模様。何となく怪しい挙動な気がする……
RSpotifyの検索時に使ってるトークンはRSpotify.authenticateで発行したやつをずっと使っているので、そこで詰まっていた。
def prepare_user
RSpotify::User.new(JSON.parse(ENV['OAUTH_TOKENS'])).tap do |user|
# ユーザー認証後のTokenでないと検索結果が安定しないためToken更新
RSpotify.instance_variable_set(:@client_token, user.credentials['token'])
end
end
User.newをラップして、無理やりトークン更新処理を追加して対応した。
②(3)プレイリスト作成、楽曲追加(PlaylistCreator)
ここまで来たら特に詰まるポイントはなかった。
プレイリスト作成して、①で作ったCSVをもとに取得した楽曲を追加していくのを繰り返すのみ。
強いて言えばrate limitに気を付けないとエラー吐くぐらい。
おわりに
僕の投票結果(インディーズ/地方アイドル楽曲のみ)を載せておくのでぜひ聴いてください。
1位 コロニアルスタイル / NUANCE
2位 革命 / vividbird
3位 STARB×CKS SHOWER / AFTERS
4位 Sparkle / SANDAL TELEPHONE
5位 生意気ねチェリー / 美味しい曖昧 open.spotify.com
あと赤レンガ空中さんぽのMVもかなり見てほしいです。よろしくお願いします。
Sinatra + OmniAuth2.0でハマったこと
Sinatra + OmniAuth2.0の情報が少ねえ!!
手軽にOAuthの認可サーバー立てようと思ってSinatraを使ったらハマった。 OAuthの知識ほとんどゼロでも動くところまで持っていけたので、今後のためにも対処法をメモしておく。
環境
Ruby 3.2.2
rbenv 1.2.0
Gemバージョン
sinatra (3.2.0) puma (6.4.2) omniauth (2.1.2) omniauth-oauth2 (1.8.0) omniauth-spotify (0.0.13)
参考にした記事
『OmniAuthを使ってみる - Kludge Factory』がめちゃくちゃ参考になった。
2015年の記事ではあるものの、OmniAuthの嬉しさから具体的にどうセットアップするかまで体系的にまとまっていて、かなり助けられた。
ハマりポイント① GETリクエストが通らない
CVE-2015-9284という脆弱性が発見されてから、GETリクエストでアクセスすると404エラーが出る。
get '/' do
erb "<a href='/auth/twitter'>ログイン</a><br>"
end
みたいにシンプルに書けないので、
get '/' do
html = <<-HTML
<form action="/auth/twitter" method="POST" enctype="multipart/form-data">
<input type="submit" value="ログイン">
</form>
HTML
erb html
end
って感じでformタグ作ってPOSTリクエストを飛ばすようにする。
参考リンク: https://qiita.com/megane42/items/fc46c26d6fd1187e77c0
ハマりポイント② 404 Forbiddenになる
formでPOST飛ばすように変更してもエラーを吐く。
Puma caught this error: EOFError (EOFError) [MY_PATH]/.bundle/ruby/3.2.0/gems/rack-2.2.8/lib/rack/multipart/parser.rb:373:in `handle_empty_content!' [MY_PATH]/.bundle/ruby/3.2.0/gems/rack-2.2.8/lib/rack/multipart/parser.rb:199:in `on_read' [MY_PATH]/.bundle/ruby/3.2.0/gems/rack-2.2.8/lib/rack/multipart/parser.rb:80:in `block in parse' [MY_PATH]/.bundle/ruby/3.2.0/gems/rack-2.2.8/lib/rack/multipart/parser.rb:78:in `loop' [MY_PATH]/.bundle/ruby/3.2.0/gems/rack-2.2.8/lib/rack/multipart/parser.rb:78:in `parse' [MY_PATH]/.bundle/ruby/3.2.0/gems/rack-2.2.8/lib/rack/multipart.rb:53:in `extract_multipart' [MY_PATH]/.bundle/ruby/3.2.0/gems/rack-2.2.8/lib/rack/request.rb:594:in `parse_multipart' [MY_PATH]/.bundle/ruby/3.2.0/gems/rack-2.2.8/lib/rack/request.rb:446:in `POST'
エラーメッセージ見てもピンとこないのでSinatra側のログを見ると、
W, [2024-01-17T21:52:30.068908 #93108] WARN -- omniauth: Attack prevented by OmniAuth::AuthenticityTokenProtection E, [2024-01-17T21:52:30.068953 #93108] ERROR -- omniauth: (spotify) Authentication failure! authenticity_error: OmniAuth::AuthenticityError, Forbidden E, [2024-01-17T21:52:30.069048 #93108] ERROR -- omniauth: (spotify) Authentication failure! Forbidden: OmniAuth::AuthenticityError, Forbidden
認証でミスってるっぽい。
ググると出てくるのは基本Rails用の対処法(gem 'omniauth-rails_csrf_protection'を追加する)だけで、Sinatra用のやつが中々見つからない。
あとはprovider_ignores_state: trueをOmniAuth::Builderのところに追加するというのもあったけど、そもそも通らなかったし普通に対処法として良くなさそう。
色々ググると、authenticity_tokenを送らないと通らないことがわかった。ので、
get '/' do
html = <<-HTML
<form action="/auth/twitter" method="POST" enctype="multipart/form-data">
+ <input type="hidden" name="authenticity_token" value='#{request.env['rack.session']['csrf']}'>
<input type="submit" value="ログイン">
</form>
HTML
erb html
end
inputを追加したら通った。
参考リンク: https://blog.orz.at/2021/04/19/omniauth-twitter-forbidden/
コード全体
OmniAuth::Builderのところはproviderごとに変える。
require 'dotenv'
require 'sinatra'
require 'sinatra/reloader'
require 'omniauth'
require 'omniauth-spotify'
require 'json'
Dotenv.load
class SinatraApp < Sinatra::Base
register Sinatra::Reloader
configure do
set :sessions, true
set :inline_templates, true
end
use OmniAuth::Builder do
provider :spotify,
ENV['APP_TOKEN'],
ENV['APP_SECRET_TOKEN'],
scope: 'user-read-private playlist-read-private playlist-read-collaborative playlist-modify-public'
end
get '/' do
html = <<-HTML
<form action="/auth/spotify" method="POST" enctype="multipart/form-data">
<input type="hidden" name="authenticity_token" value='#{request.env['rack.session']['csrf']}'>
<input type="submit">
</form>
HTML
erb html
end
get '/auth/:provider/callback' do
result = request.env['omniauth.auth']
erb "<pre>#{JSON.pretty_generate(result)}</pre>"
end
end
SinatraApp.run! if __FILE__ == $PROGRAM_NAME
制作物置き場
アイドル楽曲大賞クローラー
アイドル楽曲大賞2023の楽曲部門投票結果をまとめたプレイリストがなかったために作成。
nokogiriを使って投票結果をCSV化するスクリプトと、rspotify(Spotify-APIのRubyラッパー)を使ってCSVからプレイリストを作成するスクリプトを組み合わせて、過去の投票結果も含めたプレイリストを作成した。
作成したプレイリスト
メジャーアイドル楽曲部門
open.spotify.com
インディーズ/地方アイドル楽曲部門
open.spotify.com
過去実施分のプレイリスト
2022年度
メジャーアイドル楽曲部門
アイドル楽曲大賞2022 メジャーアイドル楽曲部門 1~100位 - playlist by K_K | Spotify
インディーズ/地方アイドル楽曲部門
アイドル楽曲大賞2022 インディーズ/地方アイドル楽曲部門 1~100位 - playlist by K_K | Spotify
2021年度
メジャーアイドル楽曲部門
アイドル楽曲大賞2021 メジャーアイドル楽曲部門 1~100位 - playlist by K_K | Spotify
インディーズ/地方アイドル楽曲部門
アイドル楽曲大賞2021 インディーズ/地方アイドル楽曲部門 1~100位 - playlist by K_K | Spotify
2020年度
メジャーアイドル楽曲部門
アイドル楽曲大賞2020 メジャーアイドル楽曲部門 1~100位 - playlist by K_K | Spotify
インディーズ/地方アイドル楽曲部門
アイドル楽曲大賞2020 インディーズ/地方アイドル楽曲部門 1~100位 - playlist by K_K | Spotify
2019年度
メジャーアイドル楽曲部門
アイドル楽曲大賞2019 メジャーアイドル楽曲部門 1~100位 - playlist by K_K | Spotify
インディーズ/地方アイドル楽曲部門
アイドル楽曲大賞2019 インディーズ/地方アイドル楽曲部門 1~100位 - playlist by K_K | Spotify
2018年度
メジャーアイドル楽曲部門
アイドル楽曲大賞2018 メジャーアイドル楽曲部門 1~100位 - playlist by K_K | Spotify
インディーズ/地方アイドル楽曲部門
アイドル楽曲大賞2018 インディーズ/地方アイドル楽曲部門 1~100位 - playlist by K_K | Spotify
2017年度
メジャーアイドル楽曲部門
アイドル楽曲大賞2017 メジャーアイドル楽曲部門 1~100位 - playlist by K_K | Spotify
インディーズ/地方アイドル楽曲部門
アイドル楽曲大賞2017 インディーズ/地方アイドル楽曲部門 1~100位 - playlist by K_K | Spotify
2016年度
メジャーアイドル楽曲部門
アイドル楽曲大賞2016 メジャーアイドル楽曲部門 1~100位 - playlist by K_K | Spotify
インディーズ/地方アイドル楽曲部門
アイドル楽曲大賞2016 インディーズ/地方アイドル楽曲部門 1~100位 - playlist by K_K | Spotify
2015年度
メジャーアイドル楽曲部門
アイドル楽曲大賞2015 メジャーアイドル楽曲部門 1~100位 - playlist by K_K | Spotify
インディーズ/地方アイドル楽曲部門
アイドル楽曲大賞2015 インディーズ/地方アイドル楽曲部門 1~100位 - playlist by K_K | Spotify