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

第45回『らじる★らじる』聴き逃しサービス(1)

遅ればせながら新年明けましておめでとうございます。筆者の怠慢で休載が多くなっているこの連載ですが、コードたわむれる苦しさや楽しさ、面白さを皆さんに少しでもお伝えできるよう、まったりと続けていく予定なので、本年もよろしくお願いします。

さて、筆者は「エア・チェック」世代の人間なので、NHKラジオのインターネットラジオ配信サービスらじる★らじるには以前から興味を持っており、この連載でも過去に何度かとりあげたことがあります。

改めて確認したところ、⁠らじる★らじる」を最初に取りあげたのは、配信が始まった直後の2011/12でした。配信方法も当初のWMPからRTMPを経て、現在はHLSになっていて、時の流れを感じてしまいます。

最近はNHKラジオもインターネット配信に力を入れているようで、放送した番組の多くが「聴き逃し」サービスとして、放送後一週間ほどサイトから自由に聴けるようになっています。

図1 「らじる★らじる」の「聴き逃し番組」ページ
「らじる★らじる」の「聴き逃し番組」ページ

見落していた番組をこの「聴き逃し」サービスで見つけることも楽しみのひとつなものの、⁠エア・チェック」世代の人間のさがとして、気になる番組は録音して手元に残しておきたくなります。そこで、この「聴き逃し」サービスについて調べてみることにしました。

「聴き逃し」サービスのプレイヤー

「聴き逃し」サービスのページでは、公開している各番組へのリンクが張ってあり、それをクリックすると専用のプレイヤーが起動するようになっています。ブラウザの「ページのソースを表示」機能を使って「聴き逃し」サービスのページや起動されるプレイヤーのソースを調べたところ、HTMLで書いてあるのは全体の枠組み程度で、番組へのリンクを作ったり、番組のデータをダウンロードするなど、主要な機能は全てJavaScriptで書かれているようです。

図2 たぶん、このあたりが各番組へのリンクのはず…
たぶん、このあたりが各番組へのリンクのはず…

何か手がかりは無いかとプレイヤーのHTMLのコードをあれこれ眺めていると、再生用のJavaScriptっぽい名前の"player_ondemand.js?v202207"へのリンクがあり、このリンクを辿ってみたところ改行等の無い長い長いコードが表示されました。なお、本来このコードは1行になっているものの、紹介の都合上、下図ではブラウザの表示画面オプションで「折り返し」を指定し、画面内に収まるよう表示しています。

図3 player_ondemand.jsのコード
player_ondemand.jsのコード

見るだけで「ぞっ」とするコードなものの、これはJavaScriptではよく見られる風習です。というものの、JavaScriptはページを開くたびにダウンロードされるため、1バイトでも削減することが求められ、サイトにアップロードする際には完成したコードからインデントや改行を全て除去してしまう仕来しきたりがあります。もちろん、内部処理をあまり見せたくない、という意図もあるでしょう。

そのために用いられる"JavaScript Obfuscator(不明瞭化)"というツールもあるそうです。

Pythonのようなインデントに意味がある言語とは異なり、JavaScriptでは空白(タブ)や改行には意味が無いので、ブラウザ(のJavaScriptエンジン)は長い長い1行でも問題なく解釈(パース)するものの、人の目でそのようなコードを読むには何らかの方法で整形する必要があります。その際に便利なのがjsbeautifierというツールです。

jsbeatutifierはPythonで書かれたスクリプトで、PythonのパッケージデータベースPypiにも登録されているので、"pip"コマンドでインストールすることができます。

$ sudo pip install jsbeautifier
[sudo] kojima のパスワード: xxxxxx
Collecting jsbeautifier
  Downloading jsbeautifier-1.14.7.tar.gz (74 kB)
     |████████████████████████████████| 74 kB 137 kB/s 
  Installing build dependencies ... done
...
Successfully installed editorconfig-0.12.3 jsbeautifier-1.14.7

今回はpipを"sudo"経由で実行したので、ダウンロードしたパッケージはシステム領域にインストールされ、"/usr/bin/js-beautify"コマンドが利用可能になります。一方、pipを一般ユーザ権限で実行すると、システム領域ではなくユーザのホームディレクトリ内(通常は~/.local/bin/)にインストールされますので、このディレクトリにパスを通すか、起動する際にパス名から指定する必要があります。

"js-beautifier"は整形したいファイルを引数に指定するだけではなく、標準入力から取りこんだファイルも処理できるので、"wget"で上記JavaScriptファイルを取り込み、"js-beatutifier"で整形してから保存してみます。

$ wget -O- 'https://www.nhk.or.jp/radio/js/player_ondemand.js?v202207' | js-beautify > player_ondemand.js
$ cat -n player_ondemand.js
   1  /////////らじる★らじる 聴き逃しプレーヤー/////////
   2  var hostserver, ieVer, jsonpath = "nhk.or.jp/radioondemand/json/",
   3      newspath = "nhk.or.jp/s-media/news/news-site/list/v1/all.json",
   4      tophref = window == window.parent ? location.href : window.top.location.href,
   5      nowDate = new Date,
   6      $q = [];
   7  $(function() {
   8      if ($("#header h1 a").on("click", function() {
   9              openerLink("/radio/")
  10          }), setBodyClass(), getQuery("p"));
  11      else {
  12          $("#ODindex a").on("click", function() {
  13              openerLink("/radio/ondemand/")
  14          }), $("#bangumi").remove(), $("#ODindex a").html("<span>\u8074\u304D\u9003\u3057\u756A\u7D44\u4E00\u89A7\u3078</span>");
  15          return
  16      }
  17      $("#ODindex").slideUp("fast"), ("radionews" == ($q = getQuery("p").split("_"))[0] ||
    "F261" == $q[0] || "F139" == $q[0]) && $("body").addClass("radionews"), "F139" == $q[0] ?
    (hostserver = "dev", jsonpath = "//dev-www." + jsonpath, newspath = "//dev-www." + newspath) :
    tophref.indexOf("//dev-") > -1 ? (hostserver = "dev", jsonpath = "//dev-www." + jsonpath,
    newspath = "//www." + newspath) : tophref.indexOf("//stg-") > -1 ? (hostserver = "stg",
    jsonpath = "//www." + jsonpath, newspath = "//www." + newspath) :
    tophref.indexOf("//www") > -1 ? (hostserver = "www", jsonpath = "//www." + jsonpath,
    newspath = "//www." + newspath) : tophref.indexOf("//nhk") > -1 && (hostserver = "ssf",
    jsonpath = "https://www." + jsonpath, newspath = "https://www." + newspath),
    $.getJSON($("body.radionews").length ? newspath :
    jsonpath + $q[0] + "/bangumi_" + $q[0] + "_" + $q[1] + ".json", function(a) {
  18          callback(a.main)
  19      })
...    

改行やインデントが無かった元のコードに比べてずいぶん見やすくはなったものの、JavaScriptの癖というか文法の特徴で、条件演算子("?..:..")やコンマ演算子(",")で行が長々と続くため、まだまだ解読は容易ではありません。

JavaScriptプレイヤーの解読

JavaScriptはきちんと勉強したことが無いので、Cなどの基本知識で読み解けるあたりから調べていったところ、どうやらこの"player_ondemand.js"自体には音声データ等を再生するような機能はなく、ここから呼び出す"nol_audioplayer.js"、さらにそこから呼び出される"ifr.html"を経由して、最終的にはHTML5用のJavaScriptな動画プレイヤーVideo.jsを使って音声データを再生しているようです。

さてそれではこの"player_ondemand.js"の役割は…… とコードを読み直したところ、どうやら前節リストの17行目あたりの長い行で、参照先のURLを設定しているようです。そこでこの行をもう少し読みやすく整形することにしました。

少し調べたところ、JavaScriptの場合、⁠条件演算子("? ... : ... ")を"if ... else if ... "の代わりに用い、C等では

    if (condition1) { return value1; }
    else if (condition2) { return value2; }
    else if (condition3) { return value3; }
    else { return value4; }

このように書くコードを、

   return condition1 ? value1
         : condition2 ? value2
         : condition3 ? value3
         : value4;

のように書けるそうです。また、⁠コンマ演算子(","⁠⁠」は連結された式を左から順に評価(=実行)して一番最後の結果を返すとのことなので、これらを手掛りに、先のリストの17~19行目を手動で再整形してみました。

  $("#ODindex").slideUp("fast"),
  ("radionews" == ($q = getQuery("p").split("_"))[0] || "F261" == $q[0] || "F139" == $q[0]) && $("body").addClass("radionews"),
   "F139" == $q[0] ?
        (hostserver = "dev", jsonpath = "//dev-www." + jsonpath, newspath = "//dev-www." + newspath)
   : tophref.indexOf("//dev-") > -1 ?
        (hostserver = "dev", jsonpath = "//dev-www." + jsonpath, newspath = "//www." + newspath)
   : tophref.indexOf("//stg-") > -1 ?
        (hostserver = "stg", jsonpath = "//www." + jsonpath, newspath = "//www." + newspath)
   : tophref.indexOf("//www") > -1 ?
        (hostserver = "www", jsonpath = "//www." + jsonpath, newspath = "//www." + newspath)
   : tophref.indexOf("//nhk") > -1 &&
       (hostserver = "ssf", jsonpath = "https://www." + jsonpath, newspath = "https://www." + newspath), $.getJSON($("body.radionews").length ?
         newspath
   : jsonpath + $q[0] + "/bangumi_" + $q[0] + "_" + $q[1] + ".json", function(a) {callback(a.main)}
  )

うーん…… これでもロジックがよくわからないので、使っている変数の設定も追加してPython風に書き直してみましょう。

  jsonpath = "nhk.or.jp/radioondemand/json/"
  newspath = "nhk.or.jp/s-media/news/news-site/list/v1/all.json"
  if window == window.parent :
     tophref = location.href
  else :
     topref = window.top.location.href
  $q = []
  
  $q = getQuery("p").split("_")
  if ( "radionews" == $q[0] || "F261" == $q[0] || "F139" == $q[0] ) :
      $("body").addClass("radionews")
  if ("F139" == $q[0]) :
     hostserver = "dev"
     jsonpath = "//dev-www." + jsonpath
     newspath = "//dev-www." + newspath
  elif (tophref.indexOf("//dev-") > -1) :
     hostserver = "dev"
     jsonpath = "//dev-www." + jsonpath
     newspath = "//www." + newspath)
  elif (tophref.indexOf("//stg-") > -1) :
     hostserver = "stg"
     jsonpath = "//www." + jsonpath
     newspath = "//www." + newspath)
  elif (tophref.indexOf("//www") > -1) :
     hostserver = "www"
     jsonpath = "//www." + jsonpath
     newspath = "//www." + newspath)
  elif (tophref.indexOf("//nhk") > -1 :
     hostserver = "ssf"
     jsonpath = "https://www." + jsonpath
     newspath = "https://www." + newspath
     if  $.getJSON($("body.radionews").length ) :
         newspath
  else:
     jsonpath + $q[0] + "/bangumi_" + $q[0] + "_" + $q[1] + ".json"
     function(a) {callback(a.main)}

ロジックをきちんと理解できたわけではないものの、どうやらgetQuery()で"p=..."みたいなクエリを受けとり、それを"_"で分割し、最初の部分($q[0])が"F139"や"F261"、あるいは呼び出し元が"dev-"とか"stg-"だったらニュース用のサイト、そうでなければ "jsonpath(nhk.or.jp/radioondemand/json/)+$q[0]+/bangumi_$q[0]_$q[1].json" というURLを返す、という処理のようです。

さて、それではどこかに"p=..."を投げるところはあるのかな、と「聴き逃し」サービスのページに戻って確認すると、番組を再生するためのリンクがjavascript:openPlayer('p=0442_01_3834250')のようになっています。

どうやらこれっぽい、と予想して、$q[0]=0442、$q[1]=01を当てはめた"www.nhk.or.jp/radioondemand/json/0442/bangumi_0442_01.json"というURLにアクセスすると、"ビンゴ!"、番組情報のJSONデータが入手できました。

$ curl https://www.nhk.or.jp/radioondemand/json/0442/bangumi_0442_01.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",
 "site_detail":"5分のサウンドトリップへようこそ!\r\nリスナーのみなさんを音だけの世界にお連れします。\r\n
 想像をかきたて、記憶を呼び覚まし、心を潤す音の数々。\r\n音響デザイナーがお届けする、5分間の音の旅をお楽しみ下さい。",
 "thumbnail_p":"https://www.nhk.or.jp/radioondemand/json/0442/img/g442.jpg",
 "thumbnail_c":null,
 "schedule":"【ラジオ第1】土11:50~日19:55~【ラジオ第2】月~金14:20~土12:10~
 ....

この番組情報JSONデータには"filename:"としてストリーミングデータのURLも記載されており、VLC等でそのURLを開くとその番組の音声データが再生されました。もう少し関連するコードを解読したい気もするものの、とりあえず番組へのリンクから必要なJSONデータを入手できるようになったので、次はこれを使って録音するためのスクリプトを考えてみましょう。


今回紹介したNHKの聴き逃しサービスは、従来「カルチャーラジオ」「古典購読」といった教養番組で先行実施されていて、その際の聴取期間は3ヵ月に設定されていました。一方、このサービスを広く一般娯楽番組まで広げた現在では、聴取期間は基本的に1週間になっています。どうせオンラインなのだからもう少し長くてもいいのでは、と思っていたところ、どうやらこの「1週間」の制限は著作権がらみの縛りのようです。

というのも、JASRACの著作権使用料表によると、音声番組のダウンロード使用は「7日以内」⁠30日以内」⁠制限なし」の3段階に分かれていて、それぞれに値段が異なるそうです。NHK等の放送局はJASRACと個別の契約を結んでいるから、この表の価格がそのまま適用されるわけではないものの、その契約でも再生期限には恐らく「1週間」の縛りがあるのでしょう。

JASRAC管理楽曲も流す娯楽番組ではそのような縛りも妥当かな、と思う一方、⁠名曲スケッチ」「音の風景」といった独自番組までその縛りに合わせる必要もないのでは、とも思ってしまいます。

おすすめ記事

記事・ニュース一覧