システム開発記 / Radikoダウンロード最適化

本記事は、Radikoタイムフリー一括ダウンロードシステムにおいて、曖昧重複判定の改善、物理ファイルからのデータベース再構築による約9,600件(5.2GB)の物理ファイル重複排除、および等倍速制限をバイパスするシーク分割並列技術の優位性を技術検証した記録です。

複数の放送局で同一時間帯に放送されるネット番組が、開始秒数の数秒のズレによって別番組として認識され、重複ダウンロードが発生してストレージを圧迫する問題が発生していました。本プロジェクトでは、ロジックの改善とデータベースの再構築により5.2GBの物理重複を解消したほか、HLSダウンロードの最適化のため「N_m3u8DL-RE」の導入検証を実施し、Radiko特有の帯域制限とそのバイパス手法の有効性を立証しました。

Radiko一括ダウンロード最適化イメージ
ダウンロード最適化の概念図。重複する番組を検出しデータベースを同期することで、不要なダウンロードを回避しHDD容量を保護します。
ロジックの緩和秒単位一致から「日付(YYYYMMDD)+正規化タイトル」一致へと判定キーを緩和し、数秒のズレによる重複を見逃さない仕組みを構築。
データベース再構築物理フォルダから音声ファイルのメタ情報をスキャンして復元し、過去のダウンロード履歴CSVを自動で同期・復旧。
等倍速制限の壁N_m3u8DL-REによる検証により、配信サーバー側の等倍速制限が発覚。現行の「シーク分割並列方式」の優位性が証明されました。

課題:秒数のズレとデータベースの不整合

Radikoのタイムシフト番組は、同じネット番組であっても放送局ごとに開始時刻(秒単位)が数秒〜数分ずれることがあります。従来のロジックでは「放送開始時刻の完全一致」をキーとしていたため、同じ番組が局数分(数十本)ダウンロードされてしまう致命的な無駄が発生していました。

さらに、過去にダウンロードした数万件のファイル情報がメタデータCSV(downloads_YYYYMM.csv)と同期されておらず、過去の資産を活かした重複判定が全く機能していませんでした。これにより、過去にダウンロード済みの番組が再び別局経由でダウンロードされるなど、ストレージ容量を大きく浪費していました。

アプローチ1:バッチ内でのリアルタイム重複排除

同じバッチ内で同時に並列実行されるタスクの中で、同一タイトル・同日放送の番組がある場合、キューに投入する前に最優先される局を1局だけ選び、残りの低優先度局のダウンロードを即座に「重複(dedup)」としてスキップ登録する処理を bulk_downloader.py に追加しました。

bulk_downloader.py: バッチ内でのワンパス重複排除ロジック
# 優先度順にソートされた pending_dl から同日同タイトルの重複を排除
batch_seen = {}
final_pending_dl = []
batch_dedup_list = []

for item in pending_dl:
    s_id, s_at, e_at, t, desc, pfm = item
    norm_t = normalize_title(t)
    if _is_risky_dedup(t, norm_t):
        final_pending_dl.append(item)
        continue
    d8 = s_at[:8]
    key = (d8, norm_t)
    if key not in batch_seen:
        batch_seen[key] = (s_id, s_at)
        final_pending_dl.append(item)
    else:
        winner_sid, winner_sat = batch_seen[key]
        batch_dedup_list.append((s_id, s_at, e_at, t, desc, pfm, winner_sid, winner_sat))

ソートによって最優先局が最初に処理されるため、2局目以降は自動的にスキップされ、CSVには「〇〇局の〇〇時のファイルを参照(deduplicated_by)」というメタ情報のみが高速に書き込まれます。

アプローチ2:物理フォルダからのCSV再構築

データベースCSVが不完全な問題を解消するため、物理フォルダ(H:\\radiko)を直接巡回スキャンし、命名規則 タイトル_局名_開始日時.m4a からメタデータをパースして、対応する月別CSVに status = completed として登録し直すスクリプトを開発・実行しました。

reconstruct_csv_from_files.py: 物理ファイルからのメタデータ復元
def parse_filename(filepath):
    basename = os.path.basename(filepath)
    name_without_ext, ext = os.path.splitext(basename)
    if ext.lower() != '.m4a':
        return None

    match_start_at = re.search(r'_([0-9]{14})$', name_without_ext)
    if not match_start_at:
        return None
    start_at = match_start_at.group(1)
    leftover = name_without_ext[:-15]

    parts = filepath.split(os.sep)
    station_id = parts[-3] # 親フォルダの親
    title = leftover[:-len(f"_{station_id}")] if leftover.endswith(f"_{station_id}") else leftover

    return {
        "title": title,
        "station_id": station_id,
        "start_at": start_at,
        "file_path": filepath
    }

この結果、過去の 26,967 件の物理ファイルの記録がデータベースに完全に蘇り、重複判定のインデックスが極めて強力なものとなりました。その後、クリーンアップツール(cleanup_archive.py)を実行した結果、9,640 個の物理的な重複ファイルが削除され、5.2 GB(5262.4 MB)の空き容量を回収することに成功しました。

技術検証:N_m3u8DL-RE vs 現行「シーク並列方式」

さらに、プロセスの起動コスト削減やCPU負荷の軽減を狙い、高機能HLSダウンローダー N_m3u8DL-RE の採用について同一の30分番組を用いたベンチマーク比較を行いました。

その結果、約400倍という大きなパフォーマンス差が確認されました。

ダウンロード方式 30分番組のDL時間 特徴と課題
現行:シーク分割並列(aria2c) 約 2.5 秒 番組を複数セクションに「サラミ・スライス」して別接続で並列シーク要求するため、サーバー側の等倍速帯域制限を回避して高速に完了。
N_m3u8DL-RE(通常HLS) 約 17.0 分 等倍速の帯域制限(レートリミット)に引っかかり、実再生時間とほぼ同等の時間をかけてダウンロードするため、非常に時間がかかる。

Radikoの配信サーバー(Smartstream)は、単一のHLSセッションからセグメント(.ts / .aac)を連続して要求されると、等倍速(リアルタイム再生速度)程度にしか帯域を渡さない流量制限を設けています。通常設計のダウンローダーである N_m3u8DL-RE はこの制限を直に受けるため、どれだけ並列数を上げても大幅な高速化はできません。

一方、現行の「シーク分割並列ダウンロード方式」は、あらかじめ番組を細かく(たとえば5分ごとや60秒ごとに)分割し、それぞれ異なる seek=YYYYMMDDHHMMSS を付与した別々のHLSセッションとして、aria2c の複数スレッドで同時にリクエストを送ります。各接続で等倍速制限がかかっていても、並列化することで制限を回避し、高速なダウンロードを実現しています。この検証結果から、現行のシーク並列方式がRadikoダウンロードにおいて最適な手法であることを確認し、現行の処理モデルを継続採用としました。

今回の最適化による成果

403エラーの解消FM807とJOEU-FMのエロアIDマッピングを修正し、403エラーによるDL失敗を解消。
5.2GBの空き容量確保重複解消処理により9,640本の重複ファイルを削除し、ストレージ容量を最適化。
重複ダウンロードの防止リアルタイムなバッチ内重複判定の追加により、今後のネット番組の無駄な同時DLをシャットアウト。

今回の結論。Radiko一括ダウンロードシステムの最適化にあたり、メタデータの再構築がどれほど重要であるかを痛感しました。また、等倍速制限という配信側の壁に対して、現行の「シーク並列分割技術」が技術的に有効であることが検証できました。引き続きこの最適化されたパイプラインで安定した稼働を目指します。