続・玩式草子 ―戯れせんとや生まれけん―

第46回『らじる★らじる』聴き逃がしサービス(2)

前回解読したように「らじる★らじる」の聴き逃しサービスは、番組IDを受け取ったJavaScriptなプレイヤー(player_ondemand.js)が番組情報JSONデータへのリンクを生成し、そのJSONデータに登録されている番組名や解説、音声データの配信URL等を用いて以後の処理を進めていることがわかりました。そこで今回は、このJSONデータを自前で料理し、必要な情報を取り出す方法を考えましょう。

JSONデータの処理

JSON(JavaScript Object Notation)は、その名の通りJavaScript用に開発されたデータオブジェクトの記述法で、プログラム中で使う変数や配列、リストといったデータを、その構成情報を保ちつつ、人間が読める文字列に変換する仕組みです。

JSONデータは"{ }"(中カッコ)でデータの構造化、":"(コロン)で変数名とその内容を対応づけます。例えば、前回の最後で紹介した

{"main":{"site_id":"0442","program_name":"音の風景","mode":0,"media_type":"radio",
 "media_code":"05,06,07","media_name":"NHKラジオ第1、NHKラジオ第2、NHK-FM",

のようなデータの場合、main[]という大きなデータブロック(配列)の中に、

site_id = "0442"
program_name = "音の風景"
mode = 0
media_type = "radio"

といった変数が存在していることを示します。

JSONデータは「人間が読める(plain text⁠⁠」文字列ではあるものの、前回見たJavaScript同様、改行やインデントの無い、長い長い1行の文字列なので、そのままでは解読するのが大変です。そこで今回はPythonのPrettyPrint(pprint)機能を使って整形表示してみます。また、せっかくなので、聴き逃し番組を示す"p=0442_01_3834250"のような引数を受けとって、その番組のJSONデータを表示するようにしてみましょう。まずは必要最低限の機能を実装したスクリプトを書いてみました。

 1  #!/usr/bin/python
 2  
 3  import sys,pprint,json,requests
 4  
 5  query = sys.argv[1].split("=")[1]  # "p=0442_01_3834250"の"0442_01_3834250"を取り出す
 6  q = []
 7  q = query.split("_")
 8  # print(q)
 9  
10  base_url = 'https://www.nhk.or.jp/radioondemand/json/'
11  bangumi_id = "{}/bangumi_{}_{}.json".format(q[0], q[0], q[1])
12  url = base_url + bangumi_id
13  # print(url)
14  
15  response = requests.get(url)
16  # print(response.text)
17  
18  prog_info = json.loads(response.text)
19  pprint.pprint(prog_info)

3行目でインポートしている各モジュールのうち、sys、pprint、jsonはPython本体に付属しているものの、requestsは別途配布されているので、見つからない場合は "pip install requests" する必要があります。

HTTP回りを処理するPython付属のモジュールはurllibですが、requestsモジュールを使う方が簡単に書けます。Plamo Linuxではrequestsはデフォルトでインストールするようにしています。

5行目は引数として指定された"p=0442_01_3854250"みたいな文字列を"="で2つに分け、後半部("0442_01_3854250")を取り出す指示です。この部分をさらに"_"で3つに分け、q[]に収めています(7行目⁠⁠。

このq[]を使って、前回見た"player_ondemand.js"と同じ方法でJSONデータへのURLを作り(10-12行目⁠⁠、そのURLへrequests.get()でアクセス、返ってきたresponseオブジェクトからその内容(response.text)json.loads()でPythonのデータ構造に読み込み、その結果をpprintで出力する、という流れです。なお、あちこちに残しているコメントアウトしたprint文は各段階のチェック用です。

このスクリプトを"get_json.py"という名前で保存し、⁠聴き逃し」番組のページにあるプレイヤーへのリンクをコピーして食わせてみます。

$ python ./get_json.py 'p=0308_01_3837107'
  {'main': {'cast': None,
          'corner_detail': None,
          'corner_id': '01',
          'corner_name': None,
          'detail_list': [{'file_list': [{'aa_contents_id': '[radio]vod;名曲スケッチ「フルート協奏曲\u3000'
                                                            'ニ長調\u3000'
                                                            'K。314」\u3000'
                                                            '「協奏交響曲\u3000'
                                                            'K。364」;r3,130;2023012670918;2023-01-26T00:50:00+09:00_2023-01-26T01:00:00+09:00',
                                          'aa_measurement_id': 'vod',
                                          'aa_vinfo1': '名曲スケッチ「フルート協奏曲\u3000'
                                                       'ニ長調\u3000K。314」\u3000'
                                                       '「協奏交響曲\u3000K。364」',
                                          'aa_vinfo2': 'r3,130',
                                          'aa_vinfo3': '2023012670918',
                                          'aa_vinfo4': '2023-01-26T00:50:00+09:00_2023-01-26T01:00:00+09:00',
                                          'close_time': '2023-02-02T01:00:00+09:00',
                                          'file_id': '3837551',
                                          'file_name': 'https://nhks-vh.akamaihd.net/i/radioondemand/r/308/s/stream_308_9bac22215bc547c9d67b69c1774452b2.mp4/master.m3u8?set-akamai-hls-revision=5',
                                          'file_title': '名曲スケッチ「フルート協奏曲\u3000'
                                                        'ニ長調\u3000K。314」\u3000'
                                                        '「協奏交響曲\u3000K。364」',
....

「名曲スケッチ」のページにあるリンクを処理するとこんな結果になりました。この出力を見ると、番組情報として返されるJSONデータには"main['detail_list']"の中に、実際の番組情報を持つ辞書型のfile_list{}がリストの要素として複数並んでいるようです。

ざっと見たところ、"0308_01_3837107"というIDのうち、"0308"が「名曲スケッチ」という番組ID、"3837107"がその番組の中の1つのファイル、という扱いになっていて、JSONデータは番組IDに対して返るため、実際に聴きたい番組のデータはさらにそこから取り出さないといけないようです。そのためのコードを追加して、再実行してみました。

$ cat -n get_json.py
    ....
    21	for i in prog_info['main']['detail_list'] :
    22	    for j in i['file_list']:
    23	        if j['file_id'] == q[2] :
    24	            pprint.pprint(j)

$ python ./get_json.py 'p=0308_01_3837107'
{'aa_contents_id': '[radio]vod;名曲スケッチ「序曲“1812年”」\u3000'
                   '「イタリア奇想曲」;r3,130;2023012470437;2023-01-24T00:50:00+09:00_2023-01-24T01:00:00+09:00',
 'aa_measurement_id': 'vod',
 'aa_vinfo1': '名曲スケッチ「序曲“1812年”」\u3000「イタリア奇想曲」',
 'aa_vinfo2': 'r3,130',
 'aa_vinfo3': '2023012470437',
 'aa_vinfo4': '2023-01-24T00:50:00+09:00_2023-01-24T01:00:00+09:00',
 'close_time': '2023-01-31T01:00:00+09:00',
 'file_id': '3837107',
 'file_name': 'https://nhks-vh.akamaihd.net/i/radioondemand/r/308/s/stream_308_3c1e4c834be190baf52dc08cc16d1c5f.mp4/master.m3u8?set-akamai-hls-revision=5',
 'file_title': '名曲スケッチ「序曲“1812年”」\u3000「イタリア奇想曲」',
 'file_title_sub': '',
 'onair_date': '1月24日(火)午前0:50放送',
 'open_time': '2023-01-24T01:00:00+09:00',
 'seq': 1,
 'share_url': 'https://www2.nhk.or.jp/radio/pg/sharer.cgi?p=0308_01_3837107'}

file_list['file_name']に登録されているのが音声データの配信元URLで、smplayer等でこのURLを開けば目的の番組が再生できました。

配信データの録音

専用プレイヤーへのリンクから聴きたい番組のURLやタイトルを取り出せるようになったので、次はこの番組を録音してみましょう。このような処理には動画処理用万能ツールffmpegが便利です。

ffmpegには膨大な機能とそれを指定するためのオプションがあるものの、あまり凝ったことをしないのなら、入力元のURLと出力先のファイル名を指定するだけで利用できます。まずはコマンドラインからテストしてみます。

$ ffmpeg -i 'https://nhks-vh.akamaihd.net/i/radioondemand/r/308/s/stream_308_3c1e4c834be190baf52dc
  08cc16d1c5f.mp4/master.m3u8?set-akamai-hls-revision=5' output.mp4
ffmpeg version 4.3.3 Copyright (c) 2000-2021 the FFmpeg developers
built with gcc 11.1.0 (GCC)
  ... 
[hls @ 0x9ab000] Skip ('#EXT-X-VERSION:3')
[hls @ 0x9ab000] Skip ('#EXT-X-INDEPENDENT-SEGMENTS')
[hls @ 0x9ab000] Opening 'https://vod-stream.nhk.jp/radioondemand/r/308/s/stream_308_3c1e4c834be19
0baf52dc08cc16d1c5f/index_48k.m3u8?aka_me_session_id=AAAAAAAAAAAZxdRjAAAAAMItWy+n4fZKo860UpflRwlVi
p+F8LhDEFEYA%2fuAA+86w%2fGi6kXpOAE2PtytMb0AJMzJGTF41dgU&aka_media_format_type=hls' for reading
[hls @ 0x9ab000] Skip ('#EXT-X-VERSION:3')
[hls @ 0x9ab000] Opening 'https://vod-stream.nhk.jp/radioondemand/r/308/s/stream_308_3c1e4c834be19
  0baf52dc08cc16d1c5f/serve.key?aka_me_session_id=AAAAAAAAAAAZxdRjAAAAAMItWy+n4fZKo860UpflRwlVip+F
  8LhDEFEYA%2fuAA+86w%2fGi6kXpOAE2PtytMb0AJMzJGTF41dgU' for reading
....
[aac @ 0xc10b00] Packet corrupt (stream = 0, dts = NOPTS).
[aac @ 0xbefbc0] TYPE_FIL: Input buffer exhausted before END element found
Error while decoding stream #0:0: Invalid data found when processing input
size=    9242kB time=00:10:00.02 bitrate= 126.2kbits/s speed=40.4x    
video:0kB audio:9132kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: 1.197712%
[aac @ 0xc1cb80] Qavg: 731.169
$ file output.mp4
  output.mp4: ISO Media, MP4 Base Media v1 [IS0 14496-12:2003]

あれこれエラーメッセージは出ているもののoutput.mp4は作成され、smplayer等で再生できました。

さて、それではこの機能をスクリプトに組み込んでみましょう。Pythonにはffmpegを使うためのバインディング(python-ffmpeg)も開発されているものの、とりあえずsubprocessモジュールを使ってコマンドラインと同じ形でffmpegを使うことにします。そのため、先のコードに少し追加しました。

21  for i in prog_info['main']['detail_list'] :
22      for j in i['file_list']:
23          if j['file_id'] == q[2] :
24              # pprint.pprint(j)
25              URL = j['file_name']
26              filename = j['file_title'].replace('\u3000','_') + '.mp4'
27              # print(URL, filename)
28              break
29  
30  import subprocess
31  cmd = ['ffmpeg', '-i', URL, filename]
32  # print(cmd)
33  subprocess.run(cmd)

25行目と26行目でJSONデータから配信元のURLと保存先のファイル名を作成し、31行目で先に試したffmpegのコマンドラインを作成、33行目でそれを実行する、という流れです。

26行目の"replace()"はファイル名中の全角空白("\u3000")をアンダースコア("_")に変換する処理です。これは必須ではないものの、環境によってはファイル名中の全角空白が半角空白にマップされ、意図しない動作をすることがあったので、念のため安全な文字に変換しています。

さて、それでは実際に録音できるか試してみましょう。

$ python ./json_01.py 'p=0308_01_3837107'
  ffmpeg version 4.3.3 Copyright (c) 2000-2021 the FFmpeg developers
  ...
    Input #0, hls, from 'https://nhks-vh.akamaihd.net/i/radioondemand/r/308/s/stream_308_3c1e4c834b
 e190baf52dc08cc16d1c5f.mp4/master.m3u8?set-akamai-hls-revision=5':
    Duration: 00:10:00.06, start: 1.999989, bitrate: 0 kb/s
    Program 0 
    Metadata:
      variant_bitrate : 48625
    Stream #0:0: Audio: aac (HE-AAC), 48000 Hz, stereo, fltp, 46 kb/s
  ...
Output #0, mp4, to '名曲スケッチ「序曲“1812年”」_「イタリア奇想曲」.mp4':
Metadata:
  encoder         : Lavf58.45.100
  Stream #0:0: Audio: aac (LC) (mp4a / 0x6134706D), 48000 Hz, stereo, fltp, 128 kb/s
 ...
   [aac @ 0xe54e40] TYPE_FIL: Input buffer exhausted before END element found
Error while decoding stream #0:0: Invalid data found when processing input
size=    9242kB time=00:10:00.02 bitrate= 126.2kbits/s speed=32.9x    
video:0kB audio:9132kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: 1.197712%
[aac @ 0xe54500] Qavg: 731.169

$ ls  *mp4
名曲スケッチ「序曲“1812年”」_「イタリア奇想曲」.mp4

後述するような問題はあるものの、とりあえず今回作った骨組みだけのスクリプトでも聴き逃しサービスの番組をダウンロードできました。


今回作成したスクリプトは実際に動くものの、試してみられた方は気づくように、長い番組を録音しようとしても頭から十数分程度しか録音できず、録音したデータにも音飛びやノイズが目立ちます。

あれこれ試してみた結果、どうやらこれはエラーメッセージ等が示すように、録音に利用しているffmpegが期待するHLSの書式と聴き逃しサービスが提供しているHLSの書式が少し異なっているためで、ffmpegの代わりにgstreamerやvlcを使うと多少は改善するようです。次回はそのあたりの改善方法と番組情報JSONデータをさらに利用する方法を考えてみます。

おすすめ記事

記事・ニュース一覧