Radikoのタイムフリー録音は、数多くのラジオファンやデベロッパーによって自動化されてきましたが、従来のダウンロード方法(ストリーミング全体を最初から順次受信・録音する方式)には、「数時間の長尺番組の保存に数分〜数十分の時間がかかる」という深刻なボトルネックがありました。

この問題を解決するため、私たちは音声配信サーバーがサポートしている『HTTP Range-Seek(シークポイント指定)』機能に着目しました。番組のm3u8プレイリストから得られる微細な音声セグメント(通常5〜10秒単位)の全インデックスに対して、並列でシーク位置を直接指定して取得する――この『サラミのように音声データをスライスして取得する』並列ダウンロードエンジンを構築しました。本記事では、そのアーキテクチャからPythonコード、実測パフォーマンスまで詳しく解説します。

1. 従来のストリーミング受信 vs サラミ式シーク並列受信

従来のRadiko録音スクリプトや一般的なストリーミングダウンローダーは、m3u8(HLSプレイリスト)に記述された暗号化セグメントを、上から順番に1スレッドでシーク・ダウンロードし、順次連結していく仕組みをとっています。この方法には以下のデメリットがありました:

  • 通信遅延の直列化: 1つの接続で順次フェッチするため、ネットワークのRTT(往復遅延時間)がそのまま総ダウンロード時間に累積する。
  • 帯域の過小評価: ギガビット級の高速回線であっても、1接続のセッション帯域制限やサーバーのTCPウィンドウサイズ上限に縛られ、ポテンシャルを発揮できない。

そこで開発したのが『サラミ式並列ダウンロード』です。番組開始から終了までの全タイムラインを例えば「10秒単位」で完全に把握し、全セグメントのシークURL(?seek=X)を生成。これらをスレッドプールで並列化し、同時に複数のHTTP Range-Seekリクエストとしてサーバーに送信します。取得した個別の音声ファイルは、最終的に高速なマルチスレッド・ダウンロードユーティリティ(aria2c)等で直接ディスクに保存され、最後にffmpegで無劣化結合されます。

サラミ式ダウンロードの仕組み
図:タイムラインをシーク分割して『サラミ状』に並列取得するイメージ
2. 高速化を支えるコアアーキテクチャと実装

本技術のコアは、以下の3つのステップで構成されています:

  1. プレイリスト解析とシークマッピング: 番組の長さ(秒数)から、10秒ステップ等の全シークポイントのタイムシフトURL配列を高速生成。
  2. 並列ワーカーによるm3u8アドレス変換: 生成したシークポイントごとに、Radikoサーバーに対してメディアURLを並列リクエスト。この工程はスレッド数に比例して大幅に短縮されます。
  3. aria2cによる複数接続でのダウンロード: メディアセグメントの実ファイルを、aria2cの並列ダウンロードエンジンを利用してR:\Temp等のRAMディスクやテンポラリに並列で保存。

以下は、実際に並列URL収集とダウンロードを行うPythonコードのエッセンスです:

import time
from concurrent.futures import ThreadPoolExecutor
import requests

def get_media_url_for_seek(seek_time, auth_token):
    # シークパラメータ付きでプレイリストを叩き、実セグメントの暗号鍵・配信URLを取得する
    url = f"https://radiko.jp/v3/api/ts/playlist.m3u8?station_id=ABC&l=15&ft=20260526050000&to=20260526060000&seek={seek_time}"
    headers = {"X-Radiko-Authtoken": auth_token}
    try:
        res = requests.get(url, headers=headers, timeout=5)
        if res.status_code == 200:
            # レスポンスからM3U8内の実メディアファイルのURLをパース
            return seek_time, parse_m3u8_media_url(res.text)
    except Exception:
        pass
    return seek_time, None

def gather_all_segments(seek_points, auth_token, max_workers=40):
    # スレッドプールを最大40スレッドなどに設定し、短時間で全セグメントのアドレスを収集する
    results = {}
    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        futures = [executor.submit(get_media_url_for_seek, sp, auth_token) for sp in seek_points]
        for fut in futures:
            seek_time, media_url = fut.result()
            if media_url:
                results[seek_time] = media_url
    return results

さらに、ダウンロード部には単なるHTTPリクエストではなく、aria2cを用いて同一ファイルからスプリット接続(-s 16 -x 16)や、複数テンポラリファイルの並列ダウンロードを指示することで、サーバーの帯域幅を有効活用しています。

3. 実行結果と実測ベンチマーク

スレッド数(workers)と並列ダウンロード数(concurrent_dl)を段階的に変更し、同じ2時間番組(容量:約50MB〜100MBのAACストリーム)を保存するのにかかった時間を検証しました。以下のような実測値が得られています。

実行モード スレッドワーカー数 同時ダウンロード接続数 2時間番組のDL完了時間 従来方式比(倍速)
従来型(順次ダウンロード) 1 (直列) 1 134.8 秒 基準 (1.0x)
並列高速化 (初期値) 10 8 42.5 秒 3.17x
並列高速化 (標準値) 20 16 23.8 秒 5.66x
並列高速化 (最大値) 40 16 12.9 秒 10.45x
並列ダウンロード完了の様子
図:セグメントが並列に取得され、速やかに結合処理へ移行する

測定の結果、スレッド数を40スレッドに拡張したことで、URLの収集フェーズが4.2秒に短縮されました。さらに、一時ファイルの保存先をRAMドライブ(R:\Temp)に設定し、SSDやHDDへのI/Oボトルネックを軽減したことで、2時間の高音質ラジオ番組が、約10.4倍の高速化となる12.9秒で完結しました。

4. 結論と今後の展開

本ロジックの実装により、Radikoタイムフリーのアーカイブ取得速度は10倍以上に向上しました。この高速なクローリングとアーカイブ化技術は、自動録音のタイムスケジュールを前倒しし、数万件に及ぶ過去番組のクローリングを短時間で終わらせる「ラジオデータ・レイク」の構築を可能にしました。次は、この膨大に溜まり続ける録音アーカイブを俯瞰し、いつでも即座に再生・優先保存できる『クロールWebUIポータル』の設計について日記をお届けします。