NHKラジオニュースサイトの音声ファイルのありか

最近 Amazon Echo Dot を入手して遊んでいる @riocampos です。

Amazon Echoでニュースを聞く

Echo Dot へ向かって「Alexa、ニュース」と言うと NHK ラジオニュースの最新ニュースを流し、そのあとに天気予報を喋ってくれます。便利です。
さて、そのニュース元である NHK ラジオニュースの音声ファイルはどこにあるのかを確認してみました。

音声ファイルの場所を示すファイル

https://api.nhk.or.jp/r-news/v1/newslist.js
です。中身を見てみましょう。

radionews(
  {"lastBuildDate":"Fri, 02 Feb 2018 15:00:07 +09:00",
    "news":[
      {
        "startdate":"Thu, 01 Feb 2018 19:00:03 +09:00",
        "enddate":"Thu, 01 Feb 2018 19:30:00 +09:00",
        "title":"夜7時NHKきょうのニュース",
        "soundlist":{
          "sound_normal":{"size":"14376717","type":"mp3","duration":"1797","filename":"20180201190003_16134_1_1_1"},
          "sound_fast":{"size":"8631693","type":"mp3","duration":"1078","filename":"20180201190003_16134_2_1_1"},
          "sound_slow":{"size":"17263053","type":"mp3","duration":"2157","filename":"20180201190003_16134_3_1_2"}
        }
      },
      {
        "startdate":"Thu, 01 Feb 2018 22:00:03 +09:00",
        "enddate":"Thu, 01 Feb 2018 23:10:00 +09:00",
        "title":"夜10時NHKジャーナル",
        "soundlist":{
          "sound_normal":{"size":"16788717","type":"mp3","duration":"4197","filename":"20180201220003_16379_1_1_2"},
          "sound_fast":{"size":"20152269","type":"mp3","duration":"2518","filename":"20180201220003_16379_2_1_1"},
          "sound_slow":{"size":"20152125","type":"mp3","duration":"5037","filename":"20180201220003_16379_3_1_3"}
        }
      },
      {
        "startdate":"Fri, 02 Feb 2018 07:00:03 +09:00",
        "enddate":"Fri, 02 Feb 2018 07:20:00 +09:00",
        "title":"朝7時NHKけさのニュース",
        "soundlist":{
          "sound_normal":{"size":"9577485","type":"mp3","duration":"1197","filename":"20180202070003_15388_1_1_1"},
          "sound_fast":{"size":"5752269","type":"mp3","duration":"718","filename":"20180202070003_15388_2_1_2"},
          "sound_slow":{"size":"11504205","type":"mp3","duration":"1437","filename":"20180202070003_15388_3_1_3"}
        }
      },
      {
        "startdate":"Fri, 02 Feb 2018 08:00:03 +09:00",
        "enddate":"Fri, 02 Feb 2018 08:05:00 +09:00",
        "title":"午前8時のNHKニュース",
        "soundlist":{
          "sound_normal":{"size":"2376621","type":"mp3","duration":"297","filename":"20180202080003_13346_1_1_1"},
          "sound_fast":{"size":"1431693","type":"mp3","duration":"178","filename":"20180202080003_13346_2_1_2"},
          "sound_slow":{"size":"2863341","type":"mp3","duration":"357","filename":"20180202080003_13346_3_1_3"}
        }
      },
      {
        "startdate":"Fri, 02 Feb 2018 12:00:03 +09:00",
        "enddate":"Fri, 02 Feb 2018 12:15:00 +09:00",
        "title":"正午のNHKニュース",
        "soundlist":{
          "sound_normal":{"size":"7177293","type":"mp3","duration":"897","filename":"20180202120003_15633_1_1_1"},
          "sound_fast":{"size":"4312269","type":"mp3","duration":"538","filename":"20180202120003_15633_2_1_1"},
          "sound_slow":{"size":"8623917","type":"mp3","duration":"1077","filename":"20180202120003_15633_3_1_2"}
        }
      }
    ]
  }
);

ニュースのスピードは「ふつう・ゆっくり・はやい」の三種類から選べるので、音声ファイルもそれぞれ3種類あります。
例えば「正午のNHKニュース」のふつうスピードの音声ファイルへのリンクは
http://www.nhk.or.jp/r-news/ondemand/mp3/20180202120003_15633_1_1_1.mp3?201802025155441
のような形式になっています。"正午のNHKニュース"のところの"filename"要素は"20180202120003_15633_1_1_1"になっているので、つまり

http://www.nhk.or.jp/r-news/ondemand/mp3/

に目的の"filename"要素+".mp3"としてしまえば、音声ファイルの URL が得られますね。
なお、この URL の"?"以降ですが、どうやらアクセスした日時 YYYY/MM/DD HH:MM:SS に対して

?YYYYMMDD5HHMMSS

となっているようです。他の日にセパレータ"5"が変わっていないかどうかをチェックしたいと思います*1

Rubyで音声ファイルURLを取り出す

中身はほぼ JSON なのですが、JSON じゃなく JavaScript ファイルになっています。っていうか JSONP って言うんでしたっけ、よく分かりません。
このため、そのままでは JavaScript 以外だと取り扱いづらくなっています。JSON にしてしまいましょう。
いつものように Ruby で取り扱います。
JavaScript 的には radionews 関数として読めるようになっているので、その内側の部分を取得するようにしてしまえば JSONになりますね。

> require 'json'
=> true
> require 'open-uri'
=> true
> js = open('https://api.nhk.or.jp/r-news/v1/newslist.js').read;

> json = js[/radionews\(([^)]+)\)/, 1];

> hash = JSON.parse(json)
=> {"lastBuildDate"=>"Fri, 02 Feb 2018 15:00:07 +09:00",
 "news"=>
  [{"startdate"=>"Thu, 01 Feb 2018 19:00:03 +09:00",
    "enddate"=>"Thu, 01 Feb 2018 19:30:00 +09:00",
    "title"=>"夜7時NHKきょうのニュース",
    "soundlist"=>
     {"sound_normal"=>{"size"=>"14376717", "type"=>"mp3", "duration"=>"1797", "filename"=>"20180201190003_16134_1_1_1"},
      "sound_fast"=>{"size"=>"8631693", "type"=>"mp3", "duration"=>"1078", "filename"=>"20180201190003_16134_2_1_1"},
      "sound_slow"=>{"size"=>"17263053", "type"=>"mp3", "duration"=>"2157", "filename"=>"20180201190003_16134_3_1_2"}}},
   {"startdate"=>"Thu, 01 Feb 2018 22:00:03 +09:00",
    "enddate"=>"Thu, 01 Feb 2018 23:10:00 +09:00",
    "title"=>"夜10時NHKジャーナル",
    "soundlist"=>
     {"sound_normal"=>{"size"=>"16788717", "type"=>"mp3", "duration"=>"4197", "filename"=>"20180201220003_16379_1_1_2"},
      "sound_fast"=>{"size"=>"20152269", "type"=>"mp3", "duration"=>"2518", "filename"=>"20180201220003_16379_2_1_1"},
      "sound_slow"=>{"size"=>"20152125", "type"=>"mp3", "duration"=>"5037", "filename"=>"20180201220003_16379_3_1_3"}}},
   {"startdate"=>"Fri, 02 Feb 2018 07:00:03 +09:00",
    "enddate"=>"Fri, 02 Feb 2018 07:20:00 +09:00",
    "title"=>"朝7時NHKけさのニュース",
    "soundlist"=>
     {"sound_normal"=>{"size"=>"9577485", "type"=>"mp3", "duration"=>"1197", "filename"=>"20180202070003_15388_1_1_1"},
      "sound_fast"=>{"size"=>"5752269", "type"=>"mp3", "duration"=>"718", "filename"=>"20180202070003_15388_2_1_2"},
      "sound_slow"=>{"size"=>"11504205", "type"=>"mp3", "duration"=>"1437", "filename"=>"20180202070003_15388_3_1_3"}}},
   {"startdate"=>"Fri, 02 Feb 2018 08:00:03 +09:00",
    "enddate"=>"Fri, 02 Feb 2018 08:05:00 +09:00",
    "title"=>"午前8時のNHKニュース",
    "soundlist"=>
     {"sound_normal"=>{"size"=>"2376621", "type"=>"mp3", "duration"=>"297", "filename"=>"20180202080003_13346_1_1_1"},
      "sound_fast"=>{"size"=>"1431693", "type"=>"mp3", "duration"=>"178", "filename"=>"20180202080003_13346_2_1_2"},
      "sound_slow"=>{"size"=>"2863341", "type"=>"mp3", "duration"=>"357", "filename"=>"20180202080003_13346_3_1_3"}}},
   {"startdate"=>"Fri, 02 Feb 2018 12:00:03 +09:00",
    "enddate"=>"Fri, 02 Feb 2018 12:15:00 +09:00",
    "title"=>"正午のNHKニュース",
    "soundlist"=>
     {"sound_normal"=>{"size"=>"7177293", "type"=>"mp3", "duration"=>"897", "filename"=>"20180202120003_15633_1_1_1"},
      "sound_fast"=>{"size"=>"4312269", "type"=>"mp3", "duration"=>"538", "filename"=>"20180202120003_15633_2_1_1"},
      "sound_slow"=>{"size"=>"8623917", "type"=>"mp3", "duration"=>"1077", "filename"=>"20180202120003_15633_3_1_2"}}}]}

目的の hash が得られました。

正午のニュースの音声ファイルURLを取得

まずは正午のニュースのハッシュ要素 noon を得ましょう。

> noon = hash["news"].find { |item| item["title"]["正午"] }                                                          
=> {"startdate"=>"Fri, 02 Feb 2018 12:00:03 +09:00",
 "enddate"=>"Fri, 02 Feb 2018 12:15:00 +09:00",
 "title"=>"正午のNHKニュース",
 "soundlist"=>
  {"sound_normal"=>{"size"=>"7177293", "type"=>"mp3", "duration"=>"897", "filename"=>"20180202120003_15633_1_1_1"},
   "sound_fast"=>{"size"=>"4312269", "type"=>"mp3", "duration"=>"538", "filename"=>"20180202120003_15633_2_1_1"},
   "sound_slow"=>{"size"=>"8623917", "type"=>"mp3", "duration"=>"1077", "filename"=>"20180202120003_15633_3_1_2"}}}

定時ニュースは5分間ですが、けさのニュースは20分、正午のニュースは15分、(夜7時の)きょうのニュースは30分、NHK ジャーナルは10分間なので、これらのニュースを聞くことが多いかと思います。それぞれ"けさ"、"正午"、"きょう"、"ジャーナル"を Array#find のピックアップ要素にしてやれば抜き出せますね。

続いて通常スピードの音声ファイルの"filename"要素を得ましょう。

> normal_speed_file = noon["soundlist"]["sound_normal"]["filename"]                                          
=> "20180202120003_15633_1_1_1"

では音声ファイルのURLを出してみましょう。

> base_url = "http://www.nhk.or.jp/r-news/ondemand/mp3/";
> mp3 = ".mp3";
> url = base_url + normal_speed_file + mp3                                                                   
=> "http://www.nhk.or.jp/r-news/ondemand/mp3/20180202120003_15633_1_1_1.mp3"

これで目的の URL が得られました。めでたしめでたし。

*1:2/2は5、2/4は0

Web上の画像を付けてツイート(1つ・複数)Twitter gemバージョン6.2.0以降の場合

Twitter gem バージョン6.2.0は去る2017年の11/8リリースなのですが、昨日までこの変更に気付いてませんでした(ずっと6.1.0以前を使ってたのさ…)。
さて、以前の記事(Web上の画像を付けてツイート(1つ・複数) - 別館 子子子子子子(ねこのここねこ))で

Twitter::REST::Tweets#update_with_media が楽だと思ったのだが、 API 側が deprecated になった

と書いたのですが、バージョン6.2.0で Twitter::REST::Tweets#uploadが private に変更された(正確には Twitter::REST::Media#upload から Twitter::REST::Tweets#uploadupload メソッドを引っ越して更に private に変更した)ため以前の書き方は使えなくなり*1Twitter gem を使って画像などのメディアをアップするには、API 側に関係なく Twitter::REST::Tweets#update_with_media を使わねばならぬようになりました…。ですので、バージョン6.2.0向けに書き換える必要があります。ぷんすか。

書き方

画像が一つの場合

この場合は img_url 先が画像ではなく動画であってもいけますね、多分。

gem 'twitter', '>= 6.2.0'
require 'twitter'
require 'open-uri'

img = open(img_url)
client.update_with_media(text, img)
画像が複数の場合(例は4画像)
gem 'twitter', '>= 6.2.0'
require 'twitter'
require 'open-uri'

img_urls = [img_url1, img_url2, img_url3, img_url4]
imgs = img_urls.map { |img_url| open(img_url) }
client.update_with_media(text, imgs)
実用例

以前の記事と同様に url_exist? メソッドを使って、画像 URL 先にファイルが本当に存在するか確認しています。また img_urls 配列の中身が5つ以上であった場合には冒頭4つに限定しておきます。

gem 'twitter', '>= 6.2.0'
require 'twitter'
require 'open-uri'

img_urls = [img_url1, img_url2, img_url3,...]
imgs = img_urls[0, 4].map { |img_url| url_exist?(img_url) ? open(img_url) : nil }.compact
client.update_with_media(text, imgs)

twitter_rescue do ブロックを作ってあげれば、より安心です。そして以前の記事の如くメソッドにしてやればラクになりますね。

Twitter::REST::Tweets#update_with_mediaの引数mediaについて

バージョン6.1.0まで
  • media (File, Hash) — A File object with your picture (PNG, JPEG or GIF)

Method: Twitter::REST::Tweets#update_with_media — Documentation for twitter (6.1.0)

画像が1つしか添付出来ない時代の API 向け。なので配列を渡すと

The IO object for media must respond to to_io (Twitter::Error::UnacceptableIO)

と怒られます。

バージョン6.2.0から
  • media (File, Array) — An image file or array of image files (PNG, JPEG or GIF).

Method: Twitter::REST::Tweets#update_with_media — Documentation for twitter (6.2.0)

こちらは配列を渡しても、ちゃんとその中身が IO オブジェクトであるかどうか確認してくれます。

[DEPRECATED] :mime_type option deprecated, use :content_typeと出るのは?

Twitter gem はバージョン6.2.0から http-form_data gem を使っているのですが、この gem のバージョン2.0.0から :mime_type キーが :content_type キーへと変更されました。Twitter gem の private メソッドである Twitter::REST::Request#merge_multipart_file! メソッドで、 HTTP::FormData::File.new のオプションに :mime_type キーが利用されているために
http-form_data gem が [DEPRECATED] を出力しています。
Twitter::REST::Request#merge_multipart_file! メソッドに関する修正ブランチはまだメインブランチに取り込まれていませんが、いずれ取り込まれるのではないでしょうか。

Looks like this option was deprecated here in v1.0.2 of https://github.com/httprb/form_data. The changelog entry is https://github.com/httprb/form_data/commit/5b902fab8d5b6493a400b2f82ac748d8ec0f25d3:title=here].
The changes to this gem would need to be made here.
DEPRECATED :mime_type option deprecated, use :content_type · Issue #881 · sferik/twitter

それでもエラーが出る場合は…もしかして画像ファイルサイズが小さいのかも

画像ファイルサイズが小さいと、Ruby は Tempfile にせずにオンメモリで StringIO のまま処理しようとし、そのために Twitter gem との衝突が生じるようです(バージョン6.1.0以前のときに「Tempfile が保存出来ない」とのエラーが出ていたのはこれが原因だったのかもしれないけど、よくわからない)。

media_url の画像ファイルのサイズが10kb以下と小さい場合に、表題の例外エラーが発生します。
この理由は、ruby の open-uri の open メソッドが、対象ファイルが10kb以下の場合は Tempfile ではなくて StringIO のオブジェクトを返し、gem twitterの update_with_media が to_io メソッドを持たない StringIO オブジェクトを受け付けないため。
twitter gemのupdate_with_mediaで”The IO object for media must respond to to_io”エラー | EasyRamble

対策としては、上に引用した blog や以下の blog のように、StringIO オブジェクトから Tempfile オブジェクトを作ってやることが必要になります。ちょっと面倒ですね。(以下では一旦 open-uri を使ってるけど、上記 blog に類するように Tempfile.open([File.basename(img_url), File.extname(img_url)]) としてに全ての画像 URL 先ファイルを Tempfile にしてしまうやり方でもいいんじゃないかと思ったり。)

  • Solution

... This will handle both the normal and StringIO cases for you by converting StringIO’s to a File.

# lib/twitter/image.rb
module Twitter::Image
  # The Twitter gem is particular about the type of IO object it
  #   recieves when tweeting an image. If an image is < 10kb, Ruby opens it as a
  #   StringIO object. Which is not supported by the Twitter gem/api.
  #
  #   This method ensures we always have a valid IO object for Twitter.
  def self.open_from_url(image_url)
    image_file = open(image_url)
    return image_file unless image_file.is_a?(StringIO)

    file_name = File.basename(image_url)

    temp_file = Tempfile.new(file_name)
    temp_file.binmode
    temp_file.write(image_file.read)
    temp_file.close

    open(temp_file.path)
  end
end

Now, when you’re tweeting an image. You can do this.

image = Twitter::Image.open_from_url(image_url)
twitter_client.update_with_media("Tweet tweet", image)

The IO object for media must respond to to_io

上に引用したblogでは、もう一つの対策として、定数 OpenURI::Buffer::StringMax を強引に書き換える方法も示してあります。open-uri を多用しない*2のであれば、これもアリじゃないかと思います。

OpenURI::Buffer.send :remove_const, 'StringMax' if OpenURI::Buffer.const_defined?('StringMax')
OpenURI::Buffer.const_set 'StringMax', 0

twitter gemのupdate_with_mediaで”The IO object for media must respond to to_io”エラー | EasyRamble

*1:なぜ後方互換性を潰した…

*2:全て Tempfile を作ることになるので、画像ファイルが小さく且つ open-uri を多用している場合には、速度が落ちるおそれがある。

gogakuondemand.rbをv1704_3に更新

NHK大好きな[twitter:@riocampos]です。
毎度おなじみ gogakuondemand.rb を更新しました。
今回の更新は「短期集中!3か月英会話」のストリーミングURL変更への対応です。
このコメントを頂いたので更新しました。

□ tamatama 2017/10/30 11:37
短期集中3か月英会話の10月25日分だけ→ r/17-e3m-4255-092.mp4

r→mp4 になっております。何で?
□ tamatama 2017/10/30 11:39
訂正 mp4 →r に
□ tamatama 2017/10/30 21:06
先ほど25日分だけと書き込みしましたが
短期集中3ヶ月英会話が10月23日、24日、25日ともURLが、mp4→ rに変更になりました。

確認したところ、10/23と10/24で URL が mp4 でも r でもOKになっており、10/25だけ r のみになっていました。多分NHK担当者のケアレスミスでしょうw でも仕方ないので対策しました。

いつものところ
NHK語学講座のラジオ番組ストリーミングを取得するRubyスクリプトgogakuondemand.rb(v1704_3 '17/11/2更新版) - 別館 子子子子子子(ねこのここねこ)
をご覧ください。
ダウンロードもそこから行えます。
よろしくお願いします。

honto.jpからamazon.co.jpのカスタマーレビューのサイトを開く

基本的に honto.jp を使って電子書籍を買っている @riocampos です。紙の本を買った上で電子書籍が半額を買うのが目的です*1
ただし honto.jp の弱点はレビューが弱いところ。booklog と連携しているけど、やはり弱い。ということで、気になった本を読んだ人の意見がたくさん載っている amazon.co.jp の同書籍のサイトを見てみたい、と思うわけです。なので簡単に honto.jp から amazon サイトを開きたい。

先例

honto から Amazon.co.jp のカスタマーレビューを開くブックマークレット - edp02の日記
これだと一つ目の bookmarklet では書名検索のみであってカスタマーレビューまで開かないのです。
ということで ISBN(正確にはASIN)に基づいたカスタマーレビューの URL を生成して開くようにすれば良いのでは、と思いました。

つくってみた

javascript:void(function(d,w){r=d.evaluate('//li[contains(.,"ISBN")]/text()',d,null,XPathResult.STRING_TYPE,null);if(r&&r.stringValue){o=r.stringValue.substr(5).trim();if(~o.indexOf('-')){i=o.split('-').slice(1,4).join('')}else{i=o.substr(3,9)}a=i.split('');s=0;for(j=0;j<9;j++){s+=parseInt(a[j])*(10-j)}r=11-s%parseInt('11');if(r==11){r='0'}else{if(r==10){r='X'}else{r+=''}}t=i+r;w.open('https://www.amazon.co.jp/product-reviews/'+t)}else{w.alert('ISBN Not Found.')}})(document,window);

honto.jp には ISBN-13 が載っているのですが ASIN に対応する ISBN-10 が載っていません、ということで bookmarklet の中で ISBN-13 から ISBN-10 に変換しています。そして ISBN-10(ASIN)に対応したカスタマーレビューのサイトの URL は[]https://www.amazon.co.jp/product-reviews/[]{ASIN} ですので、この URL を開くようにしています。
なお、ISBN が載っていない本のサイトもあるので、その場合には「ISBN Not Found.」とアラートが出ます。

なぞ

MacChrome でこの bookmarklet が動作するのに、iOSChrome で動作しないのが謎。
さらに、MacSafari で動作しないのに、iOSSafari で動作するのも謎。なんでだ。

*1:講談社のマンガで半額にならないのが時々あるのが辛い>_

らじる★らじる m3u8 を ffmpeg で録音する(8放送局)2017/9 以降対応(2021/4更新)(2024/1金沢追加)

NHKのネット配信サービスであるらじる★らじる

従来は https://gist.github.com/riocampos/5656450 のように rtmpdump を使う必要がありましたが、2017年9月から m3u8 というか HLS による配信へ変更されました*1。m3u8 なので10秒単位での録音になります*2。ので改めてエントリ(2022/2/23に M3U8URL の更新をしました)。

録音コマンド

まず m4a(mp4 AAC)の場合。

ffmpeg -i M3U8URL -c copy outputfilename.m4a

ファイルサイズ的に m4a が最も小さくなります。また m4a ファイルのときだけ "-c copy" オプションが使えます。
次に mp3 の場合。

ffmpeg -i M3U8URL -write_xing 0 outputfilename.mp3 

Mac かつ保存形式が mp3 ファイルの場合には、ファイルの時間表示を正しくさせるために "-write_xing 0" オプション必須。(参考: https://trac.ffmpeg.org/ticket/2697

ffmpeg コマンドに含まれる M3U8URL は以下の通りです。(2021/4以降*3

ラジオ第2(各放送局共通だがJOAK扱い*4
https://radio-stream.nhk.jp/hls/live/2023501/nhkradiruakr2/master.m3u8
札幌放送局(JOIK)ラジオ第1
https://radio-stream.nhk.jp/hls/live/2023545/nhkradiruikr1/master.m3u8
札幌放送局(JOIK)NHK-FM
https://radio-stream.nhk.jp/hls/live/2023546/nhkradiruikfm/master.m3u8
仙台放送局(JOHK)ラジオ第1
https://radio-stream.nhk.jp/hls/live/2023543/nhkradiruhkr1/master.m3u8
仙台放送局(JOHK)NHK-FM
https://radio-stream.nhk.jp/hls/live/2023544/nhkradiruhkfm/master.m3u8
東京放送局JOAK)ラジオ第1
https://radio-stream.nhk.jp/hls/live/2023229/nhkradiruakr1/master.m3u8
東京放送局JOAKNHK-FM
https://radio-stream.nhk.jp/hls/live/2023507/nhkradiruakfm/master.m3u8
金沢放送局(JOJK)ラジオ第1 *5
https://radio-stream.nhk.jp/hls/live/2024459/nhkradirulife1/master.m3u8
名古屋放送局(JOCK)ラジオ第1
https://radio-stream.nhk.jp/hls/live/2023510/nhkradiruckr1/master.m3u8
名古屋放送局(JOCK)NHK-FM
https://radio-stream.nhk.jp/hls/live/2023511/nhkradiruckfm/master.m3u8
大阪放送局(JOBK)ラジオ第1
https://radio-stream.nhk.jp/hls/live/2023508/nhkradirubkr1/master.m3u8
大阪放送局(JOBK)NHK-FM
https://radio-stream.nhk.jp/hls/live/2023509/nhkradirubkfm/master.m3u8
広島放送局(JOFK)ラジオ第1
https://radio-stream.nhk.jp/hls/live/2023512/nhkradirufkr1/master.m3u8
広島放送局(JOFK)NHK-FM
https://radio-stream.nhk.jp/hls/live/2023513/nhkradirufkfm/master.m3u8
松山放送局(JOZK)ラジオ第1
https://radio-stream.nhk.jp/hls/live/2023547/nhkradiruzkr1/master.m3u8
松山放送局(JOZK)NHK-FM
https://radio-stream.nhk.jp/hls/live/2023548/nhkradiruzkfm/master.m3u8
福岡放送局(JOLK)ラジオ第1
https://radio-stream.nhk.jp/hls/live/2023541/nhkradirulkr1/master.m3u8
福岡放送局(JOLK)NHK-FM
https://radio-stream.nhk.jp/hls/live/2023542/nhkradirulkfm/master.m3u8

なお M3U8URL は http://www.nhk.or.jp/radio/config/config_web.xml に記載されています。
また、これらの M3U8URLへのリンクをHLS対応のブラウザで開くと、各放送を直接聴けます*6。この辺りは radiko よりも気軽で良いですね。

例:大阪放送局のラジオ第1をm4aで5分(300秒)間録音する
ffmpeg -i https://radio-stream.nhk.jp/hls/live/2023508/nhkradirubkr1/master.m3u8 -to 300 -c copy r1.m4a

2021/3までのM3U8URL

参考のため残しておきます。が、2022/2/28で廃止されました。


いよいよ昨年更新した古い送信設備を停止する時が来ました。
今月(2月)28日未明に5.7.9以下のバージョンでは同時配信が聴けなくなります。

ラジオ第2OLD(各放送局共通だがJOAK扱い)
https://nhkradioakr2-i.akamaihd.net/hls/live/511929/1-r2/1-r2-01.m3u8
札幌放送局(JOIK)ラジオ第1OLD
https://nhkradioikr1-i.akamaihd.net/hls/live/512098/1-r1/1-r1-01.m3u8
札幌放送局(JOIK)NHK-FM OLD
https://nhkradioikfm-i.akamaihd.net/hls/live/512100/1-fm/1-fm-01.m3u8
仙台放送局(JOHK)ラジオ第1OLD
https://nhkradiohkr1-i.akamaihd.net/hls/live/512075/1-r1/1-r1-01.m3u8
仙台放送局(JOHK)NHK-FM OLD
https://nhkradiohkfm-i.akamaihd.net/hls/live/512076/1-fm/1-fm-01.m3u8
東京放送局JOAK)ラジオ第1OLD
https://nhkradioakr1-i.akamaihd.net/hls/live/511633/1-r1/1-r1-01.m3u8
東京放送局JOAKNHK-FM OLD
https://nhkradioakfm-i.akamaihd.net/hls/live/512290/1-fm/1-fm-01.m3u8
名古屋放送局(JOCK)ラジオ第1OLD
https://nhkradiockr1-i.akamaihd.net/hls/live/512072/1-r1/1-r1-01.m3u8
名古屋放送局(JOCK)NHK-FM OLD
https://nhkradiockfm-i.akamaihd.net/hls/live/512074/1-fm/1-fm-01.m3u8
大阪放送局(JOBK)ラジオ第1OLD
https://nhkradiobkr1-i.akamaihd.net/hls/live/512291/1-r1/1-r1-01.m3u8
大阪放送局(JOBK)NHK-FM OLD
https://nhkradiobkfm-i.akamaihd.net/hls/live/512070/1-fm/1-fm-01.m3u8
広島放送局(JOFK)ラジオ第1OLD
https://nhkradiofkr1-i.akamaihd.net/hls/live/512086/1-r1/1-r1-01.m3u8
広島放送局(JOFK)NHK-FM OLD
https://nhkradiofkfm-i.akamaihd.net/hls/live/512087/1-fm/1-fm-01.m3u8
松山放送局(JOZK)ラジオ第1OLD
https://nhkradiozkr1-i.akamaihd.net/hls/live/512103/1-r1/1-r1-01.m3u8
松山放送局(JOZK)NHK-FM OLD
https://nhkradiozkfm-i.akamaihd.net/hls/live/512106/1-fm/1-fm-01.m3u8
福岡放送局(JOLK)ラジオ第1OLD
https://nhkradiolkr1-i.akamaihd.net/hls/live/512088/1-r1/1-r1-01.m3u8
福岡放送局(JOLK)NHK-FM OLD
https://nhkradiolkfm-i.akamaihd.net/hls/live/512097/1-fm/1-fm-01.m3u8

おまけ

ffmpeg の各種オプションに関しては https://ffmpeg.org/ffmpeg.html を見てください。日本語だと FFMPEGのオプション (FFMPEG -H)|cagylogic (WebArchive) 辺りで充分かと。
-t オプションと -to オプションだけで済むと思いますが。余談ですが -t オプションと -to オプションの違いは -ss オプションなどで起点指定した場合に出てきます(詳しくは Seeking – FFmpeg 参照)。

*1:HLS のより詳しい解説は第95回 「らじる☆らじる」をHLS経由で:玩式草子─ソフトウェアとたわむれる日々|gihyo.jp … 技術評論社 をご参照ください。

*2:-toオプションなどを使うと10秒よりも短く区切れます。

*3:正確には2021/3/31更新のようです。参考のWebArchive:更新直前/更新直後

*4:なおNHK東京のラジオ第2コールサインはJOAB。コールサインについては全国放送局リンク<ラジオ局> 略称(愛称)/コールサイン/開局日/周波数一覧などをご参考に。

*5:2024/1能登半島地震の災害報道

*6:iPhoneSafariiOS 10.3.3)ではバッファ時間による遅延が45秒でした。

gogakuondemand.rbをv1704_2に更新

NHK大好きな[twitter:@riocampos]です。
毎度おなじみ gogakuondemand.rb を更新しました。
今回の更新は「短期集中!3か月英会話」のストリーミングURL変更への対応です。
このコメントを頂いたので更新しました。

tama 2017/07/17 13:33
短期集中3か月英会話のURLが r → mp4 に変わりました。http://d.hatena.ne.jp/riocampos+tech/20130731/p1#c1500266015

いつものところ
NHK語学講座のラジオ番組ストリーミングを取得するRubyスクリプトgogakuondemand.rb(v1704_2 '17/7/19更新版) - 別館 子子子子子子(ねこのここねこ)
をご覧ください。
ダウンロードもそこから行えます。
よろしくお願いします。

らじる★らじる聴き逃しサービスをダウンロードするための手始め:JSONファイルのありか

おことわり

ここに記載した情報は、私的利用に限定した使用に限ります*1著作権侵害は禁じます。

さて本編

今日の10時から、らじる★らじる聴き逃しサービス*2が充実しました。

いままでもラジオ第2の番組を中心に一部のストリーミングサービス*3が提供されていましたが、対応番組がかなり拡大されました。

ダウンロードしたいよねー

さて。
語学番組のダウンロードをいろいろとやっている私としては、やはり聴き逃しサービス(ストリーミングサービス)もダウンロードしたいと思ったわけですよハイ。
ってことで上記の「聴き逃し番組を探す」サイトを確認してみると、以下の JSON ファイルが取得されていますね。

内容はこんな感じ。

{
 "data_list":[
  {"site_id":"0045","program_name":"NHK\u30b8\u30e3\u30fc\u30ca\u30eb","media_code":"05","corner_id":"01","corner_name":null,"thumbnail_p":"https:\/\/www.nhk.or.jp\/radioondemand\/json\/0045\/img\/program_g_100.png","thumbnail_c":null,"detail_json":"https:\/\/www.nhk.or.jp\/radioondemand\/json\/0045\/bangumi_0045_01.json","open_time":"2017-05-25T12:00:00+0900","close_time":"2017-06-01T12:00:00+0900","onair_date":"2017\u5e745\u670824\u65e5(\u6c34)\u653e\u9001","link_url":null},
  {"site_id":"2914","program_name":"\u30b4\u30b8\u3060\u3063\u3061\u3083\uff01","media_code":"05","corner_id":"01","corner_name":null,"thumbnail_p":"https:\/\/www.nhk.or.jp\/radioondemand\/json\/2914\/img\/program_g_292.jpg","thumbnail_c":null,"detail_json":"https:\/\/www.nhk.or.jp\/radioondemand\/json\/2914\/bangumi_2914_01.json","open_time":"2017-05-25T12:00:00+0900","close_time":"2017-06-01T17:00:00+0900","onair_date":"2017\u5e745\u670812\u65e5(\u91d1)\u653e\u9001","link_url":null},
  …
 ]
}

これを Ruby で読ませて JSON.parse すると次のようになります。

{"data_list"=>
  [{"site_id"=>"0045",
    "program_name"=>"NHKジャーナル",
    "media_code"=>"05",
    "corner_id"=>"01",
    "corner_name"=>nil,
    "thumbnail_p"=>"https://www.nhk.or.jp/radioondemand/json/0045/img/program_g_100.png",
    "thumbnail_c"=>nil,
    "detail_json"=>"https://www.nhk.or.jp/radioondemand/json/0045/bangumi_0045_01.json",
    "open_time"=>"2017-05-25T12:00:00+0900",
    "close_time"=>"2017-06-01T12:00:00+0900",
    "onair_date"=>"2017年5月24日(水)放送",
    "link_url"=>nil},
   {"site_id"=>"2914",
    "program_name"=>"ゴジだっちゃ!",
    "media_code"=>"05",
    "corner_id"=>"01",
    "corner_name"=>nil,
    "thumbnail_p"=>"https://www.nhk.or.jp/radioondemand/json/2914/img/program_g_292.jpg",
    "thumbnail_c"=>nil,
    "detail_json"=>"https://www.nhk.or.jp/radioondemand/json/2914/bangumi_2914_01.json",
    "open_time"=>"2017-05-25T12:00:00+0900",
    "close_time"=>"2017-06-01T17:00:00+0900",
    "onair_date"=>"2017年5月12日(金)放送",
    "link_url"=>nil},
    :
  ]}

まあいろいろとあるわけですが、大事なのは detail_json 要素。ここに各番組のデータが含まれている JSON ファイルの URL が書いてあります。

では番組別 JSON ファイルを見てみましょう

上記の冒頭にある「NHKジャーナル」の JSON ファイル

を取得しましょう。

{
 "main":{
  "site_id":"0045",
  "program_name":"NHKジャーナル",
  "mode":0,
  "media_type":"radio",
  "media_code":"05",
  "media_name":"NHKラジオ第1",
  "site_detail":"「NHKジャーナル」は、“今日1日・時代の動きが見える”ニュース番組です。世界や日本の今日の出来事を解説を加えながらわかりやすく伝えます。また、地域のホットな話題やスポーツ、気象など、あらゆる情報が満載です。",
  "navi":"news",
  "navi_name":"ニュース",
  "cast":null,
  "thumbnail_p":"https:\/\/www.nhk.or.jp\/radioondemand\/json\/0045\/img\/program_g_100.png",
  "thumbnail_c":null,"site_logo":"http:\/\/www2.nhk.or.jp\/prog\/img\/45\/45.jpg",
  "week":"月火水木金",
  "schedule":"毎週月曜〜金曜 午後10時〜11時10分",
  "official_url":"http:\/\/www4.nhk.or.jp\/nhkjournal\/","share_url":"http:\/\/nhk.jp\/radio\/?p=0045_01",
  "corner_id":"01",
  "corner_name":null,
  "corner_detail":null,
  "noindex_flag":false,
  "detail_list":[
   {"headline_id":"08","headline":null,"headline_sub":null,"headline_image":null,"file_list":[{"seq":1,"file_id":"3826","file_title":"2017年5月24日(水)","file_title_sub":null,"basefile":"\/var\/www\/netradio_stg\/web\/data\/sound\/0045\/stream_0045_9ec5f956f8759c6b2d4a163161beb3c3.mp4","file_name":"https:\/\/nhks-vh.akamaihd.net\/i\/radioondemand\/r\/0045\/s\/stream_0045_9ec5f956f8759c6b2d4a163161beb3c3.mp4\/master.m3u8","open_time":"2017-05-25T12:00:00+09:00","close_time":"2017-06-01T12:00:00+09:00","aa_contents_id":"[radio]vod;NHKジャーナル;r1,130;2017052471692;2017-05-24T22:00:00+09:00_2017-05-24T23:10:00+09:00","aa_measurement_id":"vod","aa_vinfo1":"NHKジャーナル","aa_vinfo2":"r1,130","aa_vinfo3":"2017052471692","aa_vinfo4":"2017-05-24T22:00:00+09:00_2017-05-24T23:10:00+09:00","onair_date":"2017年5月24日(水)放送"}]},
   {"headline_id":"02","headline":null,"headline_sub":null,"headline_image":null,"file_list":[{"seq":1,"file_id":"3514","file_title":"2017年5月23日(火)","file_title_sub":null,"basefile":"\/var\/www\/netradio_stg\/web\/data\/sound\/0045\/stream_0045_835e14e46f29566a23d71ad789a89eb7.mp4","file_name":"https:\/\/nhks-vh.akamaihd.net\/i\/radioondemand\/r\/0045\/s\/stream_0045_835e14e46f29566a23d71ad789a89eb7.mp4\/master.m3u8","open_time":"2017-05-24T12:00:00+09:00","close_time":"2017-05-31T12:00:00+09:00","aa_contents_id":"[radio]vod;NHKジャーナル;r1,130;2017052371460;2017-05-23T22:00:00+09:00_2017-05-23T23:10:00+09:00","aa_measurement_id":"vod","aa_vinfo1":"NHKジャーナル","aa_vinfo2":"r1,130","aa_vinfo3":"2017052371460","aa_vinfo4":"2017-05-23T22:00:00+09:00_2017-05-23T23:10:00+09:00","onair_date":"2017年5月23日(火)放送"}]},
   {"headline_id":"07","headline":null,"headline_sub":null,"headline_image":null,"file_list":[{"seq":1,"file_id":"3794","file_title":"2017年5月22日(月)","file_title_sub":null,"basefile":"\/var\/www\/netradio_stg\/web\/data\/sound\/0045\/stream_0045_e32cdda15c72ad5e92b5b38db7b2e749.mp4","file_name":"https:\/\/nhks-vh.akamaihd.net\/i\/radioondemand\/r\/0045\/s\/stream_0045_e32cdda15c72ad5e92b5b38db7b2e749.mp4\/master.m3u8","open_time":"2017-05-23T12:00:00+09:00","close_time":"2017-05-30T12:00:00+09:00","aa_contents_id":"[radio]vod;NHKジャーナル;r1,130;2017052271231;2017-05-22T22:00:00+09:00_2017-05-22T23:10:00+09:00","aa_measurement_id":"vod","aa_vinfo1":"NHKジャーナル","aa_vinfo2":"r1,130","aa_vinfo3":"2017052271231","aa_vinfo4":"2017-05-22T22:00:00+09:00_2017-05-22T23:10:00+09:00","onair_date":"2017年5月22日(月)放送"}]}
  ]
 }
}

これをまた Ruby で読ませて JSON.parse しましょう。

{"main"=>
  {"site_id"=>"0045",
   "program_name"=>"NHKジャーナル",
   "mode"=>0,
   "media_type"=>"radio",
   "media_code"=>"05",
   "media_name"=>"NHKラジオ第1",
   "site_detail"=>
    "「NHKジャーナル」は、“今日1日・時代の動きが見える”ニュース番組です。世界や日本の今日の出来事を解説を加えながらわか
りやすく伝えます。また、地域のホットな話題やスポーツ、気象など、あらゆる情報が満載です。",
   "navi"=>"news",
   "navi_name"=>"ニュース",
   "cast"=>nil,
   "thumbnail_p"=>"https://www.nhk.or.jp/radioondemand/json/0045/img/program_g_100.png",
   "thumbnail_c"=>nil,
   "site_logo"=>"http://www2.nhk.or.jp/prog/img/45/45.jpg",
   "week"=>"月火水木金",
   "schedule"=>"毎週月曜〜金曜 午後10時〜11時10分",
   "official_url"=>"http://www4.nhk.or.jp/nhkjournal/",
   "share_url"=>"http://nhk.jp/radio/?p=0045_01",
   "corner_id"=>"01",
   "corner_name"=>nil,
   "corner_detail"=>nil,
   "noindex_flag"=>false,
   "detail_list"=>
    [{"headline_id"=>"08",
      "headline"=>nil,
      "headline_sub"=>nil,
      "headline_image"=>nil,
      "file_list"=>
       [{"seq"=>1,
         "file_id"=>"3826",
         "file_title"=>"2017年5月24日(水)",
         "file_title_sub"=>nil,
         "basefile"=>"/var/www/netradio_stg/web/data/sound/0045/stream_0045_9ec5f956f8759c6b2d4a163161beb3c3.mp4",
         "file_name"=>
          "https://nhks-vh.akamaihd.net/i/radioondemand/r/0045/s/stream_0045_9ec5f956f8759c6b2d4a163161beb3c3.mp4/master.m3u8",
         "open_time"=>"2017-05-25T12:00:00+09:00",
         "close_time"=>"2017-06-01T12:00:00+09:00",
         "aa_contents_id"=>
          "[radio]vod;NHKジャーナル;r1,130;2017052471692;2017-05-24T22:00:00+09:00_2017-05-24T23:10:00+09:00",
         "aa_measurement_id"=>"vod",
         "aa_vinfo1"=>"NHKジャーナル",
         "aa_vinfo2"=>"r1,130",
         "aa_vinfo3"=>"2017052471692",
         "aa_vinfo4"=>"2017-05-24T22:00:00+09:00_2017-05-24T23:10:00+09:00",
         "onair_date"=>"2017年5月24日(水)放送"}]},
     {"headline_id"=>"02",
      "headline"=>nil,
      "headline_sub"=>nil,
      "headline_image"=>nil,
      "file_list"=>
       [{"seq"=>1,
         "file_id"=>"3514",
         "file_title"=>"2017年5月23日(火)",
         "file_title_sub"=>nil,
         "basefile"=>"/var/www/netradio_stg/web/data/sound/0045/stream_0045_835e14e46f29566a23d71ad789a89eb7.mp4",
         "file_name"=>
          "https://nhks-vh.akamaihd.net/i/radioondemand/r/0045/s/stream_0045_835e14e46f29566a23d71ad789a89eb7.mp4/master.m3u8",
         "open_time"=>"2017-05-24T12:00:00+09:00",
         "close_time"=>"2017-05-31T12:00:00+09:00",
         "aa_contents_id"=>
          "[radio]vod;NHKジャーナル;r1,130;2017052371460;2017-05-23T22:00:00+09:00_2017-05-23T23:10:00+09:00",
         "aa_measurement_id"=>"vod",
         "aa_vinfo1"=>"NHKジャーナル",
         "aa_vinfo2"=>"r1,130",
         "aa_vinfo3"=>"2017052371460",
         "aa_vinfo4"=>"2017-05-23T22:00:00+09:00_2017-05-23T23:10:00+09:00",
         "onair_date"=>"2017年5月23日(火)放送"}]},
     {"headline_id"=>"07",
      "headline"=>nil,
      "headline_sub"=>nil,
      "headline_image"=>nil,
      "file_list"=>
       [{"seq"=>1,
         "file_id"=>"3794",
         "file_title"=>"2017年5月22日(月)",
         "file_title_sub"=>nil,
         "basefile"=>"/var/www/netradio_stg/web/data/sound/0045/stream_0045_e32cdda15c72ad5e92b5b38db7b2e749.mp4",
         "file_name"=>
          "https://nhks-vh.akamaihd.net/i/radioondemand/r/0045/s/stream_0045_e32cdda15c72ad5e92b5b38db7b2e749.mp4/master.m3u8",
         "open_time"=>"2017-05-23T12:00:00+09:00",
         "close_time"=>"2017-05-30T12:00:00+09:00",
         "aa_contents_id"=>
          "[radio]vod;NHKジャーナル;r1,130;2017052271231;2017-05-22T22:00:00+09:00_2017-05-22T23:10:00+09:00",
         "aa_measurement_id"=>"vod",
         "aa_vinfo1"=>"NHKジャーナル",
         "aa_vinfo2"=>"r1,130",
         "aa_vinfo3"=>"2017052271231",
         "aa_vinfo4"=>"2017-05-22T22:00:00+09:00_2017-05-22T23:10:00+09:00",
         "onair_date"=>"2017年5月22日(月)放送"}]}]}}

ということで出てきました、m3u8 ファイルの URL*4。file_name要素に出てきます。
昨日(5/24)のNHKジャーナルの音声ファイルのURLは、onair_date 要素の「2017年5月24日(水)放送」の含まれているハッシュの file_name 要素

であると分かりました。配信開始時刻は open_time 要素にある 2017-05-25T12:00:00+09:00 、配信終了時刻は
 close_time 要素の 2017-06-01T12:00:00+09:00 ですね。
あとはよしなにしてくださいw((ffmpeg -i https://nhks-vh.akamaihd.net/i/radioondemand/r/0045/s/stream_0045_9ec5f956f8759c6b2d4a163161beb3c3.mp4/master.m3u8 nhk_journal20170524.mp3 なお、macOS では ffmpeg のオプションとして -write_xing 0 を付けると時間表示が正しくなります。参考:#2697 (MP3 output duration incorrect in OS X) – FFmpeg))。

おまけ

ちなみにこの「NHKジャーナル」の site_id つまり 0045 (聴き逃しサービスのリンクURL http://www.nhk.or.jp/radio/ondemand/detail.html?p=0045_01 にも一部使われていますね)は、番組サイトへのリンクに関係する数になっています。(パディングの0を無視して) http://nhk.jp/P45 へジャンプすると、そこから http://www4.nhk.or.jp/nhkjournal/ へ転送されます。*5。聴き逃しサービスの他の site_id も基本的に同じような数、つまり番組に関連付いたユニークな数が使われています。ただし地方局の番組(らじる★らじるで未配信の局)の site_id では番組に関連付いた数になっていないのが残念です。

*1:著作権法 第三十条(私的使用のための複製)

*2:「聞き逃し」じゃないのよね

*3:正しくはオンデマンド配信だと思うのだが…NHKさんはいろいろと間違った日本語を定着させてますよね

*4:JSONのままでも読めますけどねw

*5:http://nhk.jp/P045http://nhk.jp/P0045http://nhk.jp/P00045 でも http://www4.nhk.or.jp/nhkjournal/ へ転送されます。ちなみに末尾にスラッシュを付けた http://nhk.jp/P45/ だとエラーサイトへ転送されます