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

第65回最近の『らじる★らじる』(その3)

年末年始の行事や雑用が重なって前回から少し間が空いてしまいましたが、今回も「らじる★らじる」「聴き逃し配信」を楽しむ方法の続きです。

前回紹介したように、⁠らじる★らじる」の聴き逃し配信ページではondemand_detail.jsというJavaScriptを使って番組情報データベースにアクセスし、HLS(HTTP Live Streaming)形式のデータ配信用URLを含む番組情報を入手して音声を再生しています。この手順をPythonでなぞることで、データ配信用のURLを入手できることも確認しました。

このAPIからは番組のタイトルや放送日時、出演者、簡単な番組情報等は得られるものの、あくまで聴き逃し配信のプレイヤー用なので、放送された曲目リストや演奏者、作曲者といった細かい情報を得ることはできません。

図1 聴き逃し配信のプレイヤー
図1 聴き逃し配信のプレイヤー

一方、各番組のホームページを見ると、より詳しい楽曲リストも提供されています。

図2 番組ホームページの楽曲情報
図2 番組ホームページの楽曲情報

そうなると、両者を結びつけて楽曲情報も記録したくなるのが「エア・チェック」マニアのさがでしょう(苦笑⁠⁠。というわけで、今回は聴き逃し配信から詳細な楽曲情報を入手する方法を考えてみます。

楽曲情報の確認

実のところ、⁠らじる★らじる」の各番組のホームページは、少し変わった作りになっています。たとえば、先に見た「歌謡スクランブル」のホームページのソースコードを見ると、前半はスカスカのHTMLなのに対し、後半にはJSON形式のデータがぎっしりと詰め込まれています。ぱっと見、どういうデータかよくわからないものの、しばらく追いかけていると楽曲情報らしい部分が見つかりました。

図3 JSONデータに埋め込まれた楽曲情報
図3 JSONデータに埋め込まれた楽曲情報

細かな仕組みまでは追求していませんが、ホームページではこの部分に埋め込まれたJSONデータを用いて楽曲情報等を表示しているようです。そこでこのJSONデータを調べてみることにしました。

まずはHTMLファイルをダウンロードして眺めてみると、どうやら id="NUXT_DATA" というラベルが付いたscriptタグの中にJSONデータが埋め込まれているようです。

<script type="application/json" data-nuxt-data="nuxt-app" data-ssr="true" id="__NUXT_DATA__">
....
</script></body></html>

この部分を切り出して独立したファイルとしてセーブし、少し見やすくするために、,(コンマ)[{が続く部分に改行を入れてみました。

[["ShallowReactive",1],
{"data":2,"state":4,"once":6,"_errors":7,"serverRendered":9,"path":10,"pinia":11},

先の方まで見ていくと、楽曲リストらしきデータがあります。しかしながら、よく見る⁠key:value⁠スタイルのJSON形式とは異なり、数字を手掛かりにデータを参照しているようです。

[345,354,362,371,379,388,397,405,414,422,431,440,449,457,464,472,480,488,496,504],
{"name":346,"nameruby":347,"lyricist":38,"composer":38,"arranger":38,"location":38,"provider":38,"label":348,"duration":349,"code":350,"byArtist":351},"襟裳岬","エリモミサキ","ビクター","PT4M19S","VICL41001",
[352],
{"name":353,"role":38,"part":38},"森進一",
{"name":355,"nameruby":355,"lyricist":38,"composer":38,"arranger":38,"location":38,"provider":38,"label":356,"duration":357,"code":358,"byArtist":359},"ヘッドライト","コロムビア","PT3M37S","COCA13790",
[360],
{"name":361,"role":38,"part":38},"新沼謙治",

JSONのリストや辞書型の記法はPythonと同じなので、[...]をリスト、{...}を辞書型のデータと考えると、192行目の[345,354,362,…]の部分が曲のリスト、たぶん[345]に該当するのが次の193行目の辞書型データで"name:346"が"襟裳岬"、"nameruby:347"が"エリモミサキ"、"label:348"が"ビクター"、"duration:349"が"PT4M19S"に対応する、という関係になっているようです。

そう考えて見ていくと、どうやらこのデータ構造は、配列にデータを保持し、添字を使ってそのデータにアクセスする仕組みになっていて、データの最小単位は⁠襟裳岬⁠⁠森進一⁠といった文字列で、それらを構造化するためにリストや辞書型を使っているらしいことに気づきました。上記例では、配列の添字と行番号が一致していなくてわかりにくいので、JSONデータをパースして実際の配列に落しこんでみましょう。このあたりはPythonの得意技です。

$ python
Python 3.11.14 (main, Jan 10 2026, 23:29:44) [GCC 15.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import json
>>> with open('test_01.json','r') as f:
...   json_dt = json.load(f)
... 
>>> for i in range(0,len(json_dt)) :
...   print(f"{i}:{json_dt[i]}")
... 
0:['ShallowReactive', 1]
1:{'data': 2, 'state': 4, 'once': 6, '_errors': 7, 'serverRendered
...
344:[345, 354, 362, 371, 379, 388, 397, 405, 414, 422, 431, 440, 449, 457, 464, 472, 480, 488, 496, 504]
345:{'name': 346, 'nameruby': 347, 'lyricist': 38, 'composer': 38, 'arranger': 38, 'location': 38, 'provider': 38, 'label': 348, 'duration': 349, 'code': 350, 'byArtist': 351}
346:襟裳岬
347:エリモミサキ
348:ビクター
349:PT4M19S
350:VICL41001
351:[352]
352:{'name': 353, 'role': 38, 'part': 38}
353:森進一
354:{'name': 355, 'nameruby': 355, 'lyricist': 38, 'composer': 38, 'arranger': 38, 'location': 38, 'provider': 38, 'label': 356, 'duration': 357, 'code': 358, 'byArtist': 359}
355:ヘッドライト
356:コロムビア
357:PT3M37S
358:COCA13790
359:[360]
360:{'name': 361, 'role': 38, 'part': 38}
361:新沼謙治

こうしてみれば、344番目の箱(配列要素)に入っているのが放送した曲の一覧、そこに含まれている345や354が1曲の単位で、そこには辞書型で曲のタイトル('name')やその読み('nameruby'⁠⁠、発売元('label')や演奏時間('duration')が記録され、'byArtist:351'のように参照先がリストや辞書型だった場合は、再度そこに記録された添字を手掛りに手繰たぐっていく、という形であることがよくわかるでしょう。

演者名('byArtist')は複数の場合を想定しているのか、更なる参照先へのポインタ(のリスト)だったりするのでややこしいですが、とりあえずこんな感じで手繰っていけば曲名と歌手のデータは取れそうです。

>>> for x in json_dt[344]:
...   title = json_dt[json_dt[x]['name']]
...   for artist in json_dt[json_dt[x]['byArtist']] :
...     print(f"{title}/{json_dt[json_dt[artist]['name']]}")
... 
襟裳岬/森進一
ヘッドライト/新沼謙治
桜前線/小柳ルミ子
沈丁花/石川さゆり
春待ちれんげ草/小林幸子
...

なお、後付けで調べたところ、ここで紹介しているような配列と添字を使って繰り返しが多いJSONデータを圧縮する方法は、Nuxt/Vueフレームワークが用いているインラインデータのシリアライズ形式(devalue等によるもの)だそうです。

番組情報との結び付き

楽曲リストは取れそうなので、他にも面白そうな情報は無いかとJSONファイルを眺めていると、埋め込まれている楽曲リストは1つではないことに気づきました。

{'name': 265, 'nameRuby': 343}
[726, 734, 743, 751, 759, 767, 775, 783, 791, 798, 806, 814, ...
{'name': 727, 'nameruby': 728, 'lyricist': 38, 'composer': 38, 'arranger': 38,
 'location': 38, 'provider': 38, 'label': 356, 'duration': 729, 'code': 730, 'byArtist': 731}
早春賦
ソウシュンフ
PT3M2S
COCP31489-90
[732]
{'name': 733, 'role': 38, 'part': 38}
ダ・カーポ
...
[1268, 1276, 1282, 1288, 1294, 1300, 1305, 1310, 1316, ...
{'name': 1269, 'nameruby': 1270, 'lyricist': 38, 'composer': 38, 'arranger': 38,
 'location': 38, 'provider': 38, 'label': 877, 'duration': 1271, 'code': 1272, 'byArtist': 1273}
としごろ
トシゴロ
PT2M36S
SRCL4431-2
[1274]
{'name': 1275, 'role': 38, 'part': 38}
山口百恵
...

当初は1つのエピソード(たとえば「春色のアルバム 2⁠⁠)に1つのページがあるのかな、と思っていたものの、どうやら各番組は1つのページで複数のエピソードの情報を保持しているようです。さて、それではどうやってエピソードと楽曲情報を結びつけているのだろう、と楽曲リストである344の参照元を調べてみたところ、その手前、333にある'musicList'のようです。

333:{'displayVideoMode': 334, 'displayVideoRange': 335, 'displayAudioMode': 336,
     'audioMode': 337, 'supportCaption': 177, 'supportSign': 177, 'supportHybridcast': 177,
     'supportDataBroadcast': 177, 'isInteractive': 177, 'isChangeable': 177,
     'releaseLevel': 338, 'programType': 339, 'coverage': 340, 'actList': 341,
     'musicList': 344, 'eventShareStatus': 512}

もう一段遡って、この333を参照しているところを調べると、321で'misc'として参照しています。

321:{'id': 295, 'startDate': 296, 'endDate': 297, 'identifierGroup': 322,
     'misc': 333, 'publishedOn': 513}

ここの'id': 295あたりがそれらしいな、と調べてみると、ビンゴ!、チャンネル('r3'=FM)と放送エリア('130'=東京)放送日(20260310⁠⁠、eventID(69883)を組み合わせた文字列r3-130-2026031069883が記録されていました。

295:r3-130-2026031069883
296:2026-03-10T12:30:00+09:00
297:2026-03-10T14:00:03+09:00

実は、この'r3-130-2026031069883'という番組IDは、前回紹介したストリーミング用データのURLを取るAPI(raapi)からも'aa_contents_id'として返っています。たとえば、前回見た「名曲スケッチ」の場合、こうなっていました。

"aa_contents_id": "[radio]vod;名曲スケッチ 「波を越えて」「ドナウ川のさざ波」;
    r2,130;2025121666476;2025-12-16T09:45:00+09:00_2025-12-16T09:55:00+09:00",

この'aa_contents_id'の情報を使えば、聴き逃し配信の各エピソードとその番組のホームページにある楽曲情報を結びつけることができそうです。

聴き逃し配信のURLから楽曲情報を取ってみる

さて、それでは前回のスクリプトを元に、楽曲情報を取る機能を追加してみましょう。

#!/usr/bin/python
#-*- coding: utf-8 -*-

import sys, requests, json, re

def get_music_list(bangumi_url, contents_id):
    '''
    各番組のホームページから、contents id を使って楽曲情報を拾う。
    各ホームページには、後半に"__NUXT_DATA__"としてJSONデータが入っているので、
    その部分を切り出してJSON化する
    '''
    res2 = requests.get(bangumi_url)
    m = re.search(r'<script[^>]*__NUXT_DATA__[^>]*>(.*?)</script>', res2.text, re.S)
    json_dt = json.loads(m.group(1))

    find = False
    for x in json_dt:
        # contents_id を手掛りに
        if isinstance(x, str) and contents_id in x:  
            find = True
        # その直下にある musicList を拾う
        if find == True and isinstance(x,dict) and 'musicList' in x.keys(): 
            music_list = x['musicList']
            break

    if find == True:  # contents_id が見つかっていれば、
        for x in json_dt[music_list]:  # musicList から、曲名、演者を拾う
            title = json_dt[json_dt[x]['name']]
            print(f"{title}/", end="")
            for artist in json_dt[json_dt[x]['byArtist']] : # 'byArtist'はリストへのポインタ
                print(f"{json_dt[json_dt[artist]['name']]}", end=" ")
            print("")

def main():
    '''
    引数として、聴き逃し配信のページにある p=LG96ZW5KZ4_01 を指定。
    対象が複数ある場合は、2番目の引数で番号を指定。0が最新(デフォルト)
    '''
    kikinogashi_id = sys.argv[1]
    if len(sys.argv) == 3:
        serial = int(sys.argv[2])
    else:
        serial = 0
    if 'p=' in kikinogashi_id:
        kikinogashi_id = kikinogashi_id.replace('p=','')

    (pr, ser) = kikinogashi_id.split('_')
    api_url = f"https://www.nhk.or.jp/radio-api/app/v1/web/ondemand/series?site_id={pr}&corner_site_id={ser}"
    res = requests.get(api_url)  # api_url を叩いて HLS用URL等を入手
        
    bangumi_url = res.json()['series_url']
    episode_info = res.json()['episodes']
    title = episode_info[serial]['program_title']
    ch_area = episode_info[serial]['aa_contents_id'].split(';')[2].replace(',','-')
    event_id = episode_info[serial]['aa_contents_id'].split(';')[3]
    # contents id は、"ch-エリア-日付+event_id" で構成
    contents_id = ch_area + '-' + event_id
    hls_url = episode_info[serial]['stream_url']

    print(f"title:{title}\nhls_url:{hls_url}\nid:{contents_id}")
    get_music_list(bangumi_url, contents_id)
    
if __name__ == "__main__":
    main()

このスクリプトは、引数として聴き逃し配信のページを指定するp=LG96ZW5KZ4_01等を渡すと、番組情報APIからHLSのURLを含む情報を入手し、その中にあるcontents_idを使って番組のホームページにある該当する楽曲情報を拾います。聴き逃し配信が複数ある場合、2つめの引数として番組番号を指定します。

たとえば「歌謡スクランブル」ならこんな感じ。

$ python ./dl_test2.py p=N8M9ZPVK4L_01 3
title:歌謡スクランブル ライト 木漏れ日のメロディー
hls_url:https://vod-stream.nhk.jp/radioondemand/r/N8M9ZPVK4L/s/stream_N8M9ZPVK4L_7c42e94d6918f6fe008579fec3e47440/index.m3u8
id:r3-130-2026030772739
風をあつめて/はっぴいえんど 
想い出がいっぱい/H2O 
やさしさに包まれたなら/荒井由実(松任谷由実) 
赤いスイートピー/松田聖子 
夢をあきらめないで/岡村孝子 
RIDE ON TIME/山下達郎 
夢で逢えたら/吉田美奈子 

「古楽の楽しみ」ならばこんな感じになりました。

$ python ./dl_test2.py p=NWYPY4N3WW_01
title:古楽の楽しみ 疾風怒濤期の音楽
hls_url:https://vod-stream.nhk.jp/radioondemand/r/NWYPY4N3WW/s/stream_NWYPY4N3WW_5121749dc8bf0afc79524b63011b8d07/index.m3u8
id:r3-130-2026030668813
バレエ組曲「ドン・フアン」から第30曲、終曲/ル・コンセール・デ・ナシオン ジョルディ・サヴァール 
交響曲 ニ短調/コンチェルト・ケルン 
「交響曲 第49番 ヘ短調」から第2楽章/フライブルク・バロックオーケストラ ゴットフリート・フォン・デア・ゴルツ 
チェンバロ協奏曲 ヘ短調/シャレフ・アド・エル イル・ガルデリーノ 

英数字が全角で記録されているあたりは少し気になるものの、とりあえずこの程度でも楽曲ファイルのメタタグに書き込む情報には間に合うでしょう。


前にも触れたように、NHKラジオは2026年度に現在の3波体制からAM/FMの2波体制に変わり、ラジオ第2で放送されている語学番組や高校講座といった教育番組の多くはリアルタイムで聴くのが難しいFMの深夜枠に移動するようです。そのためもあって、最近では「らじる★らじる」「聴き逃し配信」も重要な情報発信の場と認識されており、今後、廃止や縮小されることは無さそうです。

一方、関連技術の進歩に伴ない、⁠らじる★らじる」「聴き逃し配信」は、数年ごとに大きな模様替えが行われ、古いスクリプトが使えなくなることがありました。今回紹介したようなNuxt/Vueのフレームワークも2025年から採用され、以前のスクリプトでは対応できなくなったように記憶しています。

2026年度の改編は放送波の削減が目的で放送機器回りはかなり変更されるだろうものの、インターネット周り、特に最近更新されたNuxt/Vueを使うフレームワークが大きく変わることはないでしょう。しかしながら、'r1'、'r2'、'r3'といったチャンネル名は変更されそうだから、改編の前後は少し注意して見守る必要がありそうです。

おすすめ記事

記事・ニュース一覧