0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

NYT動画のアーカイブ術:有料記事の動画コンテンツを技術的に解析する実践ノート

0
Posted at

NYT動画のアーカイブ術:有料記事の動画コンテンツを技術的に解析する実践ノート

お疲れ様です、ニュースアーカイブとメディア解析を趣味でやっているエンジニアです。

先日、海外の政治ドキュメンタリーを調査するプロジェクトで「The New York Times」の動画コンテンツをローカルに保存する必要が出てきました。「え、NYTimesの動画ってダウンロードできるの?」と思われるかもしれませんが、実は適切なアプローチを取れば、技術的には十分解析可能です。

ただし、最初に断っておきます。本記事で紹介する手法は、あくまで個人の学習・研究・アーカイブ目的での利用を前提としています。NYTimesの利用規約や著作権法、DMCAなどの法的枠組みは必ずご自身で確認し、適法な範囲でご活用ください。有料コンテンツの無断複製・再配布は絶対にダメ、ゼッタイ。


NYTimes動画の「見えない壁」を技術的に分解する

YouTubeやVimeoと違い、NYTimesの動画配信にはいくつかの「意図的な障壁」があります。

  1. サードパーティプレーヤーの採用
    多くの動画がBrightcoveやJW Playerなどの外部プレーヤーで埋め込まれており、DOM構造が多重ネストされています。

  2. トークンベースの認証フロー
    動画メタデータの取得には、ページ読み込み時に発行される一時的なトークンが必要で、単純なcurlでは403を返されます。

  3. HLS + 暗号化セグメント
    動画本体はHLS形式で配信され、#EXT-X-KEYタグによりAES-128で暗号化されているケースがあります。

  4. User-Agent / Refererの厳密チェック
    ブラウザと同等のヘッダーを再現しないと、CDNレベルでリクエストが弾かれます。

これらの課題を「手動で」突破するのは現実的ではありません。そこで、私が実際に運用している**「保守性重視・最小依存」の解析アプローチ**を紹介します。


核心部分:動画メタデータの取得ロジック(Python例)

まず、NYTimes記事ページからBrightcoveのvideo_idを抽出し、API経由でm3u8 URLを取得する基本的な流れです。

import requests
import re
from bs4 import BeautifulSoup

def extract_nyt_video_info(article_url: str) -> dict:
    headers = {
        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
        'Referer': 'https://www.nytimes.com/'
    }
    
    # 1. 記事ページを取得
    resp = requests.get(article_url, headers=headers)
    resp.raise_for_status()
    
    # 2. Brightcove video_idを抽出(data-video-id属性またはembedコードから)
    soup = BeautifulSoup(resp.text, 'html.parser')
    
    # pattern A: data-video-id属性
    video_elem = soup.find(attrs={'data-video-id': True})
    if video_elem:
        video_id = video_elem['data-video-id']
        account_id = video_elem.get('data-account', '3012582793001')  # NYT default
        return fetch_brightcove_manifest(video_id, account_id, headers)
    
    # pattern B: iframe embed
    iframe = soup.find('iframe', src=re.compile('brightcove'))
    if iframe:
        # srcからvideo_idをパース(実装は省略)
        pass
    
    raise ValueError("動画情報を抽出できませんでした")

def fetch_brightcove_manifest(video_id: str, account_id: str, headers: dict) -> dict:
    # Brightcove Playback API
    api_url = f'https://edge.api.brightcove.com/playback/v1/accounts/{account_id}/videos/{video_id}'
    api_headers = {
        **headers,
        'Accept': 'application/json;pk=BCpkADawqM1...'  # 実際のPKはページソースから取得
    }
    
    resp = requests.get(api_url, headers=api_headers)
    resp.raise_for_status()
    data = resp.json()
    
    # HLSマニフェストのURLを抽出
    for source in data.get('sources', []):
        if source.get('format') == 'application/x-mpegURL':
            return {
                'manifest_url': source['src'],
                'title': data.get('name'),
                'duration': data.get('duration')
            }
    
    raise ValueError("HLSマニフェストが見つかりません")

💡 ポイント: Brightcoveのpk(パブリックキー)はページソース内の<script>タグに含まれています。本番では正規表現で動的に抽出するか、Playwrightでブラウザ挙動を再現する方が堅牢です。


pic (1)low.png

暗号化セグメントの処理(AES-128対応)

NYTimesの一部動画では、HLSセグメントがAES-128で暗号化されています。この場合、#EXT-X-KEYタグからキーを取得し、復号処理を追加する必要があります。

from Crypto.Cipher import AES
from urllib.parse import urljoin

def decrypt_segment(encrypted_data: bytes, key_uri: str, headers: dict) -> bytes:
    # キーの取得
    key_resp = requests.get(key_uri, headers=headers)
    key_resp.raise_for_status()
    key = key_resp.content[:16]  # AES-128は16バイト
    
    # IVの処理(指定がない場合はセグメント番号を使用)
    # 簡易実装のため、ここではIV=0を仮定
    iv = b'\x00' * 16
    
    cipher = AES.new(key, AES.MODE_CBC, iv)
    return cipher.decrypt(encrypted_data)

※ 実際のプロダクションでは、IVの正しい抽出・パディング処理・エラーハンドリングを厳密に実装してください。また、DRM(Widevine等)が適用されているコンテンツは、技術的に回避することが法律で禁止されている場合があります。


「毎回スクリプトを動かすのは面倒」→ Webツール化の選択

上記のような処理を毎回Pythonスクリプトで実行するのは、非エンジニアのチームメンバーにはハードルが高いですよね。そこで、私はこのロジックをWebインターフェースにラップし、URLを貼り付けるだけでダウンロードできるようにしました。

今回ご紹介している NYTimes動画ダウンローダー は、まさにそのような「技術的バックエンド + 直感的UI」を組み合わせたツールです。

技術スタックの裏側(少しだけ公開)

  • フロントエンド: React + TypeScript。URL入力・解析状態・エラーハンドリングをコンポーネント単位で管理。
  • バックエンド: Python(FastAPI) + Redis。動画解析タスクをキューイングし、並列処理で応答速度を確保。
  • セキュリティ: 入力値のサニタイズ、CORS制限、レートリミット(1IP/分間5リクエスト)により悪用を防止。
  • プライバシー: 動画データの一時保存はメモリ内のみ。ダウンロード完了後即破棄。アクセスログにも動画URLを記録しません。

特に「クライアント側で完結」と謳っている点は重要で、サーバーに動画キャッシュが残らないため、法的リスクとストレージコストの両方を抑えられます。


実際の使い流れ(技術者目線で深掘り)

  1. URLの正規化とバリデーション
    ユーザーがコピーしたURLには?smid=tw-nytimesのようなパラメータが付いていることがあります。ツール内部ではurllib.parseでクエリを整理し、nytimes.com/*/video/パターンにマッチするか検証しています。

  2. プレーヤー種類の自動判別
    Brightcove、JW Player、native HTML5 video... NYTimesは記事ごとにプレーヤーを使い分けています。ツールではDOM解析とHTTPレスポンスのヘッダーを組み合わせて、最適なパーサーを動的に選択。

  3. フォールバック機構の多層化
    1つ目のAPI呼び出しで失敗した場合、代替のエンドポイント・ヘッドレスブラウザによるレンダリング・キャッシュされたメタデータの参照を順次試行。これにより「たまに動かない」というストレスを大幅に低減しています。

  4. 解像度とフォーマットの選択ロジック
    マニフェストに含まれる複数の解像度から、ユーザーに選択肢を提示。モバイル利用を想定して「720p推奨」のデフォルト設定も用意。MP4変換が必要な場合は、バックエンドでffmpegを非同期実行しています。


開発中にハマったポイント(実体験・失敗談)

① Brightcoveのpkトークンがページごとに異なる

最初は「NYTimes全体で共通のpkを使える」と思い込んでいたのですが、実際は記事ごとに異なるケースがありました。対策として、ツールでは毎回ページソースからpkを動的に抽出するロジックを実装。これで「昨日まで動いたのに今日は403」という問題が解消されました。

② CloudFrontの署名付きURL有効期限

NYTimesが利用するCloudFront CDNでは、マニフェスト内のセグメントURLに?Policy=...&Signature=...のような署名パラメータが付与されており、有効期限が短い(5〜15分)ことがあります。対策として、マニフェスト取得→セグメントダウンロードの一連処理を1セッションで完結させる設計にしました。

③ 日本語エラーメッセージのバランス

「トークンが失効しました」と「認証エラー」など、技術的なエラー文言をどう日本語化するかは悩みどころです。本ツールでは、エンジニアには詳細コードを、一般ユーザーには平易な説明を併記する「二段階エラー表示」を採用。Qiita読者の皆様なら、このバランス感、共感していただけるかと。


法的・倫理的なラインについて(ここはしっかり書きます)

技術的に「できる」ことと、「してよい」ことは別問題です。本ツールの利用にあたっては、以下の点を強く意識しています。

推奨される使い方

  • 自分が購読している有料記事の動画コンテンツを、オフラインで個人視聴するためのバックアップ
  • 学術研究・ジャーナリズム調査での一時的なアーカイブ(再配布なし)
  • 動画配信技術の学習・解析目的での利用

避けるべき使い方

  • ダウンロードした動画をYouTubeやSNSに再アップロード
  • 有料コンテンツの回避を目的とした大量ダウンロード
  • 商用サービスでの無断統合・再配信

ツールページにも明記していますが、ユーザー自身の行動に対する法的責任はユーザー本人にあります。技術者は「できること」を提供するだけでなく、「どう使うべきか」のガイドラインも併せて提示する責任があると考えています。


Qiita読者への特別ヒント:自作ツールに「機能追加」するなら

もし本記事を読んで「自分でも作ってみよう」と思われた方へ、追加で実装すると喜ばれる機能を3つご紹介します。

  1. メタデータのJSON/CSV出力
    動画タイトル・投稿日・記事見出し・再生時間などを構造化データで出力できるようにすると、研究用途での活用が格段に便利になります。
// 簡易例:メタデータ出力ボタン
document.querySelector('#export-meta').addEventListener('click', () => {
  const blob = new Blob([JSON.stringify(videoMeta, null, 2)], {type: 'application/json'});
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url; a.download = 'nyt-video-meta.json';
  a.click();
  URL.revokeObjectURL(url);
});
  1. 進捗のリアルタイム可視化
    WebSocketまたはServer-Sent Events(SSE)で、バックエンドのタスク進捗をフロントにプッシュ。ユーザーの「待ってる感」を軽減しつつ、技術的には「非同期処理の可観測性」も向上します。

  2. 複数URLのバッチ処理
    研究用途では「関連記事の動画をまとめて取得したい」というニーズがあります。テキストエリアに複数URLを貼り付け→キューに投入→完了順にダウンロード、というフローを実装すると、プロユースに一気に近づきます。


おわりに:技術は「知る自由」のために、でも「守る責任」と共に

NYTimes動画のダウンロードというテーマは、一見「便利ツール」の域を出ないように見えるかもしれません。しかし、裏側を覗くと、HTTPプロトコル・DOM解析・暗号処理・非同期アーキテクチャ・UI/UX設計・法遵守…と、フルスタックエンジニアの腕が試される要素が詰まっています。

今回ご紹介した NYTimes動画ダウンローダー は、そうした技術的課題を「誰でも使える形」に落とし込んだ一つの答えです。もちろん完璧ではなく、NYTimesの仕様変更には随時対応が必要ですが、オープンな技術スタックで構築しているため、コミュニティからのフィードバックも歓迎しています。

📌 最後に一言
技術の可能性を追求する一方で、ジャーナリストやクリエイターへのリスペクトを忘れずに。
「便利」と「合法」、「アクセス」と「対価」のバランスを取ることこそ、プロフェッショナルの腕の見せ所だと信じています。

何か質問や技術的な議論があれば、コメント欄で気軽にどうぞ。
それでは、よいメディアアーカイブライトを!🗞️✨


参考リンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?