NHKラジオ語学講座のストリーミングデータをMP3形式で自動ダウンロードする(JSONフォーマット対応版)

2024年4月9日現在、下記の方法でNHKラジオ講座をダウンロードし、時間のあるときに聞いて語学力の維持に努めています。

上記記事のコメントをいただく中で何度か”ニュースで学ぶ「現代英語」”のダウンロードについての要望があったものの、ダウンロードリンクの取得方法がわからず対応を断念していました。

しかし、コメントにてJSON形式でのダウンロードリンクを教えていただき中身を確認したところ、ダウンロードに必要な情報を取得できることがわかりました。

JSON形式のデータは”ニュースで学ぶ「現代英語」”だけでなく、すべての講座についてデータが提供されており、XM形式とは異なりJSON形式ではその日の放送のダウンロードリンクが即日で拾えるという点で大きなアドバンテージがあります。

そこでXML形式のスクリプトをベースにJSON形式に対応したスクリプトを作成しましたので、ここにメモとして残しておきます。

ストリーミングデータを手動でダウンロードする

自動化のためには、まず手動で最小手順を確認するのが常道です。ダウンロードのための最小手順を以下に示します。

JSONからストリーミングデータのURLとMP3タグ情報を取得する

まず、下記ファイルへアクセスします。

すると、下記のようなJSONデータを取得できます。下記はダウンロードに必要な情報のみ表示しています。ダウンロードURLは’file_name’、番組の内容は’file_tilte’、放送日は’aa_vinfo3’にあるので、これらを利用してダウンロードし、ファイル名とMP3タグを付与すれば良いことになります。

{
  "main": {
  ...
    "detail_list": [
      {
        "headline_id": "3959675",
        "headline": null,
        "headline_sub": null,
        "headline_image": null,
        "file_list": [
          {
            "seq": 1,
            "file_id": "3959675",
            "file_title": "ニュースで学ぶ「現代英語」 ミャンマー 徴兵避け若者の出国相次ぐ",
            "file_title_sub": "「現代英語」とは、世界で実際に使われている英語です。最新の英語ニュースを通して、生きた英語を身につけます。講師:伊藤サム パートナー:クリスティ・ウェスト",
            "file_name": "https://vod-stream.nhk.jp/radioondemand/r/7512/s/stream_7512_dc1584c4e6daed4da991f07e04db5005/index.m3u8",
            "open_time": "2024-04-01T09:45:00+09:00",
            "close_time": "2024-04-08T09:45:00+09:00",
            "onair_date": "4月1日(月)午前9:30放送",
            "share_url": "https://www.nhk.or.jp/radioondemand/share/410_2686.html?p=7512_01_3959675",
            "aa_contents_id": "[radio]vod;ニュースで学ぶ「現代英語」 ミャンマー 徴兵避け若者の出国相次ぐ;r2,130;2024040168079;2024-04-01T09:30:00+09:00_2024-04-01T09:45:00+09:00",
            "aa_measurement_id": "vod",
            "aa_vinfo1": "ニュースで学ぶ「現代英語」 ミャンマー 徴兵避け若者の出国相次ぐ",
            "aa_vinfo2": "r2,130",
            "aa_vinfo3": "2024040168079",
            "aa_vinfo4": "2024-04-01T09:30:00+09:00_2024-04-01T09:45:00+09:00"
          }
        ]
      },
      ...
    }
  }
}Code language: JavaScript (javascript)

各番組のストリーミングをmp3ファイルに変換する

ストリーミングファイルのダウンロードにはffmpegを使用します。ffmpegで下記のコマンドラインオプションで起動するとmp3ファイル形式でダウンロードできます。

ffmpeg -http_seekable 0 -i <file_name属性のURL> -c:a mp3 "<mp3のダウンロードパス>"Code language: HTML, XML (xml)

下記はコマンドラインの設定例になります。

ffmpeg -http_seekable 0 -i https://vod-stream.nhk.jp/radioondemand/r/7512/s/stream_7512_dc1584c4e6daed4da991f07e04db5005/index.m3u8 -c:a mp3 ".¥ニュースで学ぶ「現代英語」 2024年04月01日放送分.mp3"Code language: JavaScript (javascript)

上記コマンドラインはffmpeg 4.4.4と5.1.2で動作を確認しています。

ダウンロード処理を自動化する

上記JSONの情報を利用してダウンロード処理を自動化した方法が下記になります。

自動ダウンロード用Pythonスクリプト

自動化には引き続きPythonを利用しています。Windows/Mac/Linux上で同じファイルで同じ処理をさせることができることと、読みやすく修正しやすいスクリプトを書くことができるためです。下記スクリプトをUTF-8で保存し実行することで、ファイルの変更なしでWindows/Mac/Linux上のPython 3.9で動作することを確認済みです。

import os
import urllib.request
import subprocess
import datetime
from os.path import expanduser
import sys
import json
import unicodedata

def main():
    #OS(実行環境)依存のパラメータをセットする
    if sys.platform=='win32': #Windows
        path_delimiter="\\"
        today=datetime.date.today()
        download_dir=".\\download"
        ffmpeg_bin=".\\win\\ffmpeg.exe"
    elif sys.platform=='darwin': #Mac
        path_delimiter="/"
        today=datetime.date.today()
        download_dir=expanduser("~")+"/Downloads/NHK語学講座"
        ffmpeg_bin="./mac/ffmpeg"
    else: #Linux(Synology-NAS)
        path_delimiter="/"
        download_dir="/volume1/music/NHK語学講座"
        ffmpeg_bin="/volume1/@appstore/ffmpeg/bin/ffmpeg"

    #各語学講座のjsonのURL、講座名、ダウンロード完了済みかどうかをチェックするファイルサイズ、MP3タイトルタグから消去するプレフィックス文字列を定義する
    url_kouza_size_prefix_filters  = []
    # url_kouza_size_prefix_filters += [['https://www.nhk.or.jp/radioondemand/json/6805/bangumi_6805_01.json', '小学生の基礎英語',           4801000, '', '' ]]
    # url_kouza_size_prefix_filters += [['https://www.nhk.or.jp/radioondemand/json/6806/bangumi_6806_01.json', '中学生の基礎英語レベル1',     7201000, '', '' ]]
    # url_kouza_size_prefix_filters += [['https://www.nhk.or.jp/radioondemand/json/6807/bangumi_6807_01.json', '中学生の基礎英語レベル2',     7201000, '', '' ]]
    # url_kouza_size_prefix_filters += [['https://www.nhk.or.jp/radioondemand/json/6808/bangumi_6808_01.json', '中高生の基礎英語inEnglish',  7201000, '中高生の基礎英語 in English ', '' ]]
    # url_kouza_size_prefix_filters += [['https://www.nhk.or.jp/radioondemand/json/6809/bangumi_6809_01.json', 'ラジオビジネス英語',         7201000, 'ラジオビジネス英語 ', '' ]]
    url_kouza_size_prefix_filters += [['https://www.nhk.or.jp/radioondemand/json/0916/bangumi_0916_01.json', 'ラジオ英会話',              7201000, 'ラジオ英会話 ', '' ]]
    # url_kouza_size_prefix_filters += [['https://www.nhk.or.jp/radioondemand/json/2331/bangumi_2331_01.json', '英会話タイムトライアル',      4801000, '英会話タイムトライアル', '' ]]
    url_kouza_size_prefix_filters += [['https://www.nhk.or.jp/radioondemand/json/7512/bangumi_7512_01.json', 'ニュースで学ぶ「現代英語」',   7201000, 'ニュースで学ぶ「現代英語」 ', '' ]]
    # url_kouza_size_prefix_filters += [['https://www.nhk.or.jp/radioondemand/json/4121/bangumi_4121_01.json', 'ボキャブライダー',           2401000, '', '' ]]
    # url_kouza_size_prefix_filters += [['https://www.nhk.or.jp/radioondemand/json/0915/bangumi_0915_01.json', 'まいにち中国語',             7201000, 'まいにち中国語 ', '' ]]
    # url_kouza_size_prefix_filters += [['https://www.nhk.or.jp/radioondemand/json/6581/bangumi_6581_01.json', 'ステップアップ中国語',        7201000, 'ステップアップ中国語', '' ]]
    # url_kouza_size_prefix_filters += [['https://www.nhk.or.jp/radioondemand/json/0951/bangumi_0951_01.json', 'まいにちハングル講座',        7201000, 'まいにちハングル講座 ', '' ]]
    # url_kouza_size_prefix_filters += [['https://www.nhk.or.jp/radioondemand/json/6810/bangumi_6810_01.json', 'ステップアップハングル講座',   7201000, 'ステップアップ ハングル講座 ', '' ]]
    url_kouza_size_prefix_filters += [['https://www.nhk.or.jp/radioondemand/json/0946/bangumi_0946_01.json', 'まいにちイタリア語【初級編】', 7201000, 'まいにちイタリア語 初級編 ', 'まいにちイタリア語 初級編 ' ]]
    url_kouza_size_prefix_filters += [['https://www.nhk.or.jp/radioondemand/json/0946/bangumi_0946_01.json', 'まいにちイタリア語【応用編】',  7201000, 'まいにちイタリア語 応用編 ', 'まいにちイタリア語 応用編 ' ]]
    url_kouza_size_prefix_filters += [['https://www.nhk.or.jp/radioondemand/json/0943/bangumi_0943_01.json', 'まいにちドイツ語【初級編】',    7201000, 'まいにちドイツ語 初級編 ', 'まいにちドイツ語 初級編 ' ]]
    url_kouza_size_prefix_filters += [['https://www.nhk.or.jp/radioondemand/json/0943/bangumi_0943_01.json', 'まいにちドイツ語【応用編】',   7201000, 'まいにちドイツ語 応用編 ', 'まいにちドイツ語 応用編 ' ]]
    url_kouza_size_prefix_filters += [['https://www.nhk.or.jp/radioondemand/json/0953/bangumi_0953_01.json', 'まいにちフランス語【初級編】',  7201000, 'まいにちフランス語 初級編 ', 'まいにちフランス語 初級編 ' ]]
    url_kouza_size_prefix_filters += [['https://www.nhk.or.jp/radioondemand/json/0953/bangumi_0953_01.json', 'まいにちフランス語【応用編】', 7201000, 'まいにちフランス語 応用編 ', 'まいにちフランス語 応用編 ' ]]
    url_kouza_size_prefix_filters += [['https://www.nhk.or.jp/radioondemand/json/0948/bangumi_0948_01.json', 'まいにちスペイン語【初級編】',  7201000, 'まいにちスペイン語 初級編 ', 'まいにちスペイン語 初級編 ' ]]
    url_kouza_size_prefix_filters += [['https://www.nhk.or.jp/radioondemand/json/0948/bangumi_0948_01.json', 'まいにちスペイン語【応用編】', 7201000, 'まいにちスペイン語 応用編 ', 'まいにちスペイン語 応用編 ' ]]
    # url_kouza_size_prefix_filters += [['https://www.nhk.or.jp/radioondemand/json/0956/bangumi_0956_01.json', 'まいにちロシア語【初級編】',    7201000, 'まいにちロシア語 初級編 ', 'まいにちロシア語 初級編 ' ]]
    # url_kouza_size_prefix_filters += [['https://www.nhk.or.jp/radioondemand/json/0956/bangumi_0956_01.json', 'まいにちロシア語【応用編】',   7201000, 'まいにちロシア語 応用編 ', 'まいにちロシア語 応用編 ' ]]
    # url_kouza_size_prefix_filters += [['https://www.nhk.or.jp/radioondemand/json/0937/bangumi_0937_01.json', 'アラビア語講座',              7201000, 'アラビア語講座 ', '' ]]
    # url_kouza_size_prefix_filters += [['https://www.nhk.or.jp/radioondemand/json/2769/bangumi_2769_01.json', 'ポルトガル語講座【入門編】',     7201000, 'ポルトガル語講座 入門 ', '' ]]

    # ダウンロード先のフォルダがない場合はフォルダを作成する
    os.makedirs(download_dir, exist_ok=True)

    #各語学講座のストリーミングデータをダウンロードする
    for url_kouza_size_prefix_filter in url_kouza_size_prefix_filters:
        #URL/講座名(=MP3タグ名)/ファイルサイズを格納する
        url=url_kouza_size_prefix_filter[0]
        kouza=url_kouza_size_prefix_filter[1]
        size=url_kouza_size_prefix_filter[2]
        title_replace=url_kouza_size_prefix_filter[3]
        filter=url_kouza_size_prefix_filter[4]
        # print(f"url:{url} / kouza:{kouza} / size:{size} / title_replace:{title_replace} / filter:{filter}")

        # JSONコンテンツを読み出す
        bangumi_json = download_dir+"/bangumi.json"
        urllib.request.urlretrieve(url, bangumi_json)
        with open(bangumi_json,'r',encoding="utf-8") as f:
            json_dict = json.load(f)
        os.remove(bangumi_json)

        # 各LessonのストリーミングデータをMP3に変換してダウンロードする
        for json_element in json_dict.values():
            for json_program in json_element['detail_list']:
                #放送年月日を取得する
                month=int(json_program['file_list'][0]['aa_vinfo3'][4:6])
                day=int(json_program['file_list'][0]['aa_vinfo3'][6:8])
                year=int(json_program['file_list'][0]['aa_vinfo3'][0:4])
                if month<4:
                    nendo=year-1
                else:
                    nendo=year
                contents=json_program['file_list'][0]['file_title']
                # print(f"year:{year} / month:{month} / day:{day} / content:{contents}")

                #フィルタが定義されており、かつfile_titleがフィルタと一致しない場合はスキップする
                if filter!='' and contents.find(filter) != 0:
                    continue

                # MP3に埋め込むタグ情報をセットする
                tag_title="{0}年{1}月{2}日放送分「{3}」".format(year,str(month).zfill(2),str(day).zfill(2),unicodedata.normalize('NFKC', contents).replace(title_replace, '').replace('\u3000',' ')).replace('「「','「').replace('」」','」')
                tag_year=nendo
                tag_album=kouza+"["+str(nendo)+"年度]"
                #print(f"tag_title:{tag_title} / tag_year:{tag_year} / tag_album:{tag_album}")

                # MP3のダウンロードパスをセットする
                download_subdir=download_dir+path_delimiter+kouza+"["+str(nendo)+"年度]"
                os.makedirs(download_subdir, exist_ok=True)
                download_filename=kouza+" "+"{0}年{1}月{2}日放送分".format(year,str(month).zfill(2),str(day).zfill(2))+".mp3"
                download_path=download_subdir+path_delimiter+download_filename
                #print(f"download_path:{download_path}")

                # ストリーミングファイルのURLをセットする
                download_url=json_program['file_list'][0]['file_name']
                #print(f"download_url:{download_url}")

                # ffmpegのダウンロード処理用コマンドラインを生成する
                command_line=f"{ffmpeg_bin}" \
                            f" -http_seekable 0" \
                            f" -i {download_url}" \
                            f" -id3v2_version 3" \
                            f" -metadata artist=\"NHK\" -metadata title=\"{tag_title}\"" \
                            f" -metadata album=\"{tag_album}\" -metadata date=\"{tag_year}\"" \
                            f" -ar 44100 -ab 64k -c:a mp3" \
                            f" \"{download_path}\""
                print(command_line)

                # ダウンロード処理を実行する
                if( os.path.isfile(download_path)):
                    # すでにダウンロード済みファイルがある場合
                    if( os.path.getsize(download_path)<=size ):
                        #ファイルサイズが想定サイズに満たないときはダウンロード処理を行う
                        os.remove(download_path)
                        subprocess.run(command_line,shell=True)
                else:
                    # ダウンロード済みファイルがない場合
                    #  -> ダウンロード処理を行う
                    subprocess.run(command_line,shell=True)

if __name__ == "__main__":
    main()

上記のスクリプトは、ダウンロード処理の自動化の他に以下の処理を追加しています。

  • 実行環境でパラメータを自動で変更する処理を追加
  • 年度ごとにダウンロードフォルダを分ける処理を追加
  • mp3のalbum,title,yearタグに講座情報とコンテンツを追加
  • mp3のtitleタグから冗長な情報を削除
  • タグ内の全角英数字および記号を半角に変換
  • ソートが正しく動作するようにファイル名・タグ名の月日の表示を0でパディングするように修正
  • WindowsのExplolerでmp3の再生時間が正しく表示されない問題に対処するため、ffmpegのエンコードオプションに’-ar 44100 -ab 64k’を明示的に追加
  • ダウンロード済みのファイルが存在する場合、ダウンロードが不完全かどうかをファイルサイズでチェックして、サイズが足りない場合は再ダウンロードする処理を追加
  • ヨーロッパ系言語講座の初級編と応用編を別フォルダに保存する処理を追加

スクリプトはリスト’url_kouza_size_prefix_filter’にセットした講座情報を一つずつ上から下へ処理していくだけなので、プログラミング経験があれば処理内容は理解できると思います。

mp3のタグ名やダウンロード時のフォルダ構成やファイル名など、それぞれの要望に合わせて細かな調整もスクリプトの当該部分を変更するだけで、簡単に対応できます。

ファイルがダウンロード済みの場合はダウンロードをスキップするようにしているので、何度実行してもダウンロードは一回のみになります。この仕様により、毎日定時に自動実行するようにすれば、ネットワークトラブルなどでダウンロードに失敗した場合でも週内はリトライがかかるため、ダウンロード漏れの可能性を減らすことができます。

Synology NASで自動ダウンロードする

Synology NASは単体でpythonとffmpegを実行することができるので、NAS単体で上記のスクリプトを動作させることができます。

また、SynologyのNASはスケジュール実行が簡単に設定できるので、定期的なダウンロード処理をする手段として使わない手はありません。

しかし、Synologyにデフォルトでインストールされているffmpegのバージョンは4.1.3と大変古く、上記の方法によるダウンロードができません。このため、有志が配布しているffmpegを下記手順でインストールする必要があります。(2024年4月5日現在では4.4.4がインストールされます)

Fig. パッケージセンターのコミュニティに http://packages.synocommunity.com/ を追加
Fig. ffmpegを検索してインストール

なお、このときにインストールされるffmpegのパスは /volume1/@appstore/ffmpeg/bin/ffmpeg になり旧版が上書きされるわけではないので、パスを指定せずにffmpegを呼び出してもここでインストールしたffmpegは起動しません。このため、pythonスクリプトから新たにインストールしたffmpegを起動する場合は絶対パスで指定する必要があります。

ffmpegのデフォルトパスを変更する方法もありますが、スクリプト内で絶対パスで指定するほうがNASシステムへの影響がなく、システムアップデート時に修正が上書きされて修正を元に戻されて動作しなくなるトラブルもないので安全です。

Pythonはパッケージセンターのものを指定してインストールすればいいです。バージョンは3.9以上であれば問題はないと思います。

あとはコントロールパネルにあるタスクスケジューラーで定期実行するように設定すれば毎週自動的にダウンロードされるようになります。

正しくダウンロードされたかをチェックする

自動ダウンロード処理が実行されても、自宅のネットワークトラブルやXML仕様の変更など、様々な要因でダウンロードに失敗する可能性があります。

番組によらずファイルサイズは再生時間に比例するので、Everythingを使うなどして最新のダウンロードファイルのファイルサイズを一覧できれば、ファイルサイズがすべて同じかどうかを確認するだけで大丈夫です。ただし、確認のたびにPCを起動しNAS上のフォルダを開いてファイルサイズを確認するというのは手順が多く、毎週定期的に行う作業手順としては少々煩雑です。

私の場合はNASに付属している音楽再生ソフト(Synologyの場合はAudioStation)のスマートプレイリスト(条件にあった曲を自動で検索してリストアップ)を利用して、スマホとPCのブラウザ上でダウンロードチェックできるようにしています。

リスト表示の条件は以下のように設定しています。

SynologyのAudioStationにはファイルサイズを表示する機能はないので、ファイル再生時間が全て15分になっているかどうかでダウンロードが正しく行われているかを確認しています。

SynologyのAudioStationはスマホアプリもあり、このプレイリストを表示してダウンロードチェックをしたあと、このプレイリスト上でファイルをスマホにダウンロードする手順でダウンロードチェックをしてます。

参考になれば幸いです。

「NHKラジオ語学講座のストリーミングデータをMP3形式で自動ダウンロードする(JSONフォーマット対応版)」への6件のフィードバック

  1. ファイル名がニュースで英会話となるのですが
    ニュースで学ぶ「現代英語」ではないのですか?

  2. ヨーロッパ系語学講座が
    json・ URLが初級編と応用編で一つになったけど
    以前の様に分けることが出来るでしょうか?

    • 本記事をご覧いただき、コメントもありがとうございます。
      ヨーロッパ系言語の講座は初級編と応用編でダウンロード先のフォルダを分けてダウンロードするように実装してあります。
      よろしくお願いします。

Leave comments