radikoエリアフリー対応タイムフリー録音スクリプト

 Windows 10のWindows Subsystem for Linux(WSL)、Raspberry PiのRaspbian、Debian GNU/Linux等で動作するradikoのエリアフリーにも対応したタイムフリーの録音スクリプトを作成した。
 スクリプトの2~4行目にあるsearchに番組名(の一部)を、mailにエリアフリーでログインするためのメールアドレス(エリアフリーを使わない場合は空欄)、passにログインするためのパスワードを設定して実行する。
 指定した番組名が見つかると放送局名、放送局ID、番組開始日時、番組終了日時、番組名を表示して、番組開始日時-放送局ID-番組名.m4aのファイル名で録音する。同時に番組表を同名のxmlファイルで保存する。
 以下はradikoを録音する際に必要な認証手順や、エリアフリーに必要なログイン等の手順のメモ。

ログイン

  • radikoのエリアフリーはメールアドレスとパスワードでのログインが必要
  • URLは https://radiko.jp/ap/member/login/login
  • POST内容は mail=メールアドレス&pass=パスワード
  • クッキーは認証などで必要になるので保存する
  • レスポンスヘッダに 302 Found が返ってくれば成功

エリアフリー確認

  • ログイン後、念のためにエリアフリーになっているか確認する
  • URLは https://radiko.jp/ap/member/webapi/member/login/check
  • ログイン時のクッキーを読み込んでおく必要がある
  • 取得内容に "areafree":"1" が含まれていればエリアフリーだと判る

認証(第1段階)

  • 認証は第1段階で認証トークンなどを取得する
  • URLは https://radiko.jp/v2/api/auth1 からリダイレクトされる
  • リクエストヘッダは X-Radiko-App: pc_html5 と X-Radiko-App: pc_html5 と X-Radiko-User: dummy_user と X-Radiko-Device: pc
  • レスポンスヘッダから X-RADIKO-AUTHTOKEN: に続けて認証トークンが X-Radiko-KeyLength: に続けてキー長が X-Radiko-KeyOffset: に続けてキー位置が取得できる
  • ログインしている場合は、ログイン時のクッキーを読み込んでおく必要がある

パーシャルキー

  • 認証の第1段階で取得したキー長とキー位置からパーシャルキーを生成する
  • 元となる固定の文字列 bcd151073c03b352e1ef2fd66c32209da9ca0afa のキー位置で示された場所からキー長の分だけ取得した文字列をBase64エンコードしたものがパーシャルキーになる

認証(第2段階)

  • 認証は第2段階で完了してエリアIDなどが取得できる
  • URLは https://radiko.jp/v2/api/auth2 からリダイレクトされる
  • リクエストヘッダは X-Radiko-AuthToken: 認証トークン と X-Radiko-PartialKey: パーシャルキー と X-Radiko-User: dummy_user と X-Radiko-Device: pc
  • レスポンスヘッダに 200 OK が返ってくれば成功
  • ログインしている場合は、ログイン時のクッキーを読み込んでおく必要がある
  • 取得内容のカンマで区切られた末尾からエリアIDが取得できる

放送局一覧

  • エリアフリーの場合の全ての放送局一覧はXML形式で取得できる
  • URLは http://radiko.jp/v3/station/region/full.xml
  • エリアIDで示された地域で聴取可能な放送局一覧はXML形式で取得できる
  • URLは http://radiko.jp/v3/station/list/エリアID.xml
  • 放送局IDはXPathでidタグを取得することで一覧が得られる
  • 放送局名はXPathでidタグの放送局IDを指定することでnameタグから得られる
  • 得られた文字列はエンティティ参照になっているのでUTF-8の文字列などに変換する
  • また、エンティティ参照でしか表せない記号も必要に応じて元の記号に変換する

番組表

  • 番組表はタイムフリーで聴取可能な過去1週間と今後1週間が放送局IDごとにXML形式で取得できる
  • URLは http://radiko.jp/v3/program/station/weekly/放送局ID.xml
  • 番組名の一部を指定してXPathでtitleタグの中身を検索し、親ノードのprogタグのid要素から番組ごとに一意と思われる番組IDが得られる
  • 番組名はXPathでprogタグのid要素で番組IDを指定し、titleタグから得られる
  • 得られた文字列はエンティティ参照になっているのでUTF-8の文字列などに変換する
  • また、エンティティ参照でしか表せない記号も必要に応じて元の記号に変換する
  • 番組開始日時はXPathでprogタグのid要素で番組IDを指定し、ft要素から得られる(形式は年4桁+月2桁+日2桁+時2桁+分2桁+秒2桁)
  • 番組終了日時はXPathでprogタグのid要素で番組IDを指定し、to要素から得られる(形式は年4桁+月2桁+日2桁+時2桁+分2桁+秒2桁)

タイムフリー録音

  • タイムフリー録音は、現在日時が番組終了日時を過ぎているか確認してから行う
  • URLは https://radiko.jp/v2/api/ts/playlist.m3u8?station_id=放送局ID&l=15&ft=番組開始日時&to=番組終了日時
  • リクエストヘッダは X-Radiko-Authtoken: 認証トーク
  • m3u8形式をストリーミングで録音できない場合は、ダウンロードしてm3u8ファイルに書かれたファイルを順次ダウンロードするのを繰り返せば良い

ログアウト

録音スクリプト

#!/bin/sh
search="番組名"
mail="foo@example.com"
pass="hogehoge"
# ログイン
option=""
if [ -n "$mail" ]; then
	wget -q --save-cookies cookie.txt -O login --server-response --post-data="mail=$mail&pass=$pass" https://radiko.jp/ap/member/login/login 2> login.err
	if [ -n "`sed -n -e '/302 Found$/p' login.err`" ]; then
		# ログイン確認
		wget -q --load-cookies cookie.txt -O check https://radiko.jp/ap/member/webapi/member/login/check 2> check.err
		if [ -n "`sed -n -e '/"areafree":"1"/p' check`" ]; then
			option="--load-cookies cookie.txt"
		else
			# ログアウト
			wget -q $option -O logout --server-response https://radiko.jp/ap/member/webapi/member/logout 2> logout.err
		fi
	fi
fi
# 認証
wget -q $option -O auth1 --server-response --trust-server-names --header="X-Radiko-App: pc_html5" --header="X-Radiko-App-Version: 0.0.1" --header="X-Radiko-User: dummy_user" --header="X-Radiko-Device: pc" https://radiko.jp/v2/api/auth1 2> auth1.err
if [ $? -gt 0 ]; then
	exit 1
fi
authtoken=`sed -n -e 's/.*X-RADIKO-AUTHTOKEN: //ip' auth1.err`
keylength=`sed -n -e 's/.*X-Radiko-KeyLength: //ip' auth1.err`
keyoffset=`sed -n -e 's/.*X-Radiko-KeyOffset: //ip' auth1.err`
partialkey=`echo -n "bcd151073c03b352e1ef2fd66c32209da9ca0afa" | sed -n -e "s/.\{$keyoffset\}\(.\{$keylength\}\).*/\1/p" | base64`
wget -q $option -O auth2 --server-response --trust-server-names --header="X-Radiko-AuthToken: $authtoken" --header="X-Radiko-PartialKey: $partialkey" --header="X-Radiko-User: dummy_user" --header="X-Radiko-Device: pc" https://radiko.jp/v2/api/auth2 2> auth2.err
if [ -z "`sed -n -e '/200 OK$/p' auth2.err`" ]; then
	exit 1
fi
# 放送局
if [ -n "$option" ]; then
	wget -q -O station.xml http://radiko.jp/v3/station/region/full.xml
else
	area_id=`sed -n -e 's/,.*//p' auth2`
	wget -q -O station.xml.xml http://radiko.jp/v3/station/list/$area_id.xml
fi
# 録音
for station_id in `xmllint --xpath "//id" station.xml | sed -e "s/<id>\([^<]*\)<\/id>/\1\n/g"`
do
	station_name=`xmllint --xpath "//id[text()='$station_id']/../name/text()" station.xml | nkf --numchar-input | sed -e 's/&lt;/</g;s/&gt;/>/g;s/&amp;/\&/g;s/&apos;/'\''/g;s/&quot;/"/g' | sed -e 'y/"<>|:\*\?\\\\\//”<>|:*?¥//'`
	# 番組表
	wget -q -O program.xml http://radiko.jp/v3/program/station/weekly/$station_id.xml
	for prog_id in `xmllint --xpath "//title[contains(text(),'$search')]/parent::prog/@id" program.xml 2> /dev/null | sed -e 's/[^0-9]*\([0-9][0-9]*\)[^0-9]*/\1\n/g'`
	do
		title=`xmllint --xpath "//prog[@id='$prog_id']/title/text()" program.xml | nkf --numchar-input | sed -e 's/&lt;/</g;s/&gt;/>/g;s/&amp;/\&/g;s/&apos;/'\''/g;s/&quot;/"/g' | sed -e 'y/"<>|:\*\?\\\\\//”<>|:*?¥//'`
		prog_ft=`xmllint --xpath "//prog[@id='$prog_id']/@ft" program.xml | sed -e 's/[^0-9]*\([0-9][0-9]*\)[^0-9]*/\1/'`
		prog_to=`xmllint --xpath "//prog[@id='$prog_id']/@to" program.xml | sed -e 's/[^0-9]*\([0-9][0-9]*\)[^0-9]*/\1/'`
		if [ `TZ="Asia/Tokyo" date +"%Y%m%d%H%M%S"` -gt $prog_to ]; then
			echo "$station_name $station_id $prog_ft $prog_to $title"
			ffmpeg -loglevel error -fflags +discardcorrupt -headers "X-Radiko-Authtoken: $authtoken" -i "https://radiko.jp/v2/api/ts/playlist.m3u8?station_id=$station_id&l=15&ft=$prog_ft&to=$prog_to" -acodec copy -vn -bsf:a aac_adtstoasc -y "$prog_ft-$station_id-$title.m4a"
			cp program.xml "$prog_ft-$station_id-$title.xml"
		fi
	done
done
# ログアウト
if [ -n "$option" ]; then
	wget -q $option -O logout --server-response https://radiko.jp/ap/member/webapi/member/logout 2> logout.err
fi
exit 0

仕様

  • ダウンロードにはwgetコマンドを使っているが、curlコマンド等でも代用できるかと思う
  • 文字列の任意の位置から任意の文字数の取り出し、タグの除去、エンティティ参照の記号の置換、ファイル名に使えない文字の置換にsedコマンドを使っている
  • Base64エンコードbase64コマンドを使っているが、opensslコマンドのbase64オプション等でも代用できるかと思う
  • XPathを記述するのにxmllintコマンドを使っているが、XPathが使える他のコマンドがあれば代用できるかと思う
  • エンティティ参照をUTF-8文字列に変換するのにnkfコマンドを使っているが、xmllintのencodeオプション等でも代用できるかと思う
  • 現在日時を日本標準時で取得するのにdateコマンドを使っている
  • m3u8プレイリストからストリーミング録音するのにffmpegコマンドを使っているが、細切れのaacファイルでも良ければwgetコマンドでのダウンロードでも代用できるかと思う
  • 放送局一覧からXPathでidタグを取得しているが、内容のみを示す「/text()」を付加すると放送局IDがすべて結合されて出力されてしまうので、idタグを含む状態で取得して、sedでidタグを除去して改行を付加している
  • 番組表はv3のapiを使うとweeklyで放送局ごとに前後1週間取得できるので、これを使用してタイムフリーの番組検索を行っている(現在日時より前の番組のみを対象とする)
  • 番組表からXPathで検索文字列が含まれるtitleタグの親ノードのprogタグの一意な値であるidを取得している(絶対的に一意な値ならprogタグのパス位置が良いが取得方法が判らない、またftとtoの開始日時と終了日時のように2つ以上を取得する方法も判らない)
  • XPath2.0だとmatchesで正規表現が使えるが、現時点のxmllintコマンドでは使えないので正規表現による検索は出来ない
  • xmllintコマンドはDebian、Raspbian、Windows Subsystem for Linux(WSL)等ではlibxml2-utilsパッケージに含まれている