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?

LinkedIn動画のダウンロード技術を深掘り:認証フローとストリーム解析の実装勘所

0
Posted at

LinkedIn動画のダウンロード技術を深掘り:認証フローとストリーム解析の実装勘所

※本記事は技術的な学習・研究を目的としており、著作権で保護されたコンテンツの無断利用を推奨するものではありません。利用の際は必ず各プラットフォームの利用規約および著作権法を遵守してください。

はじめに:「あのセミナー動画、チームで共有したい」から始まった技術的挑戦

こんにちは、普段はエンタープライズ向け動画配信システムの開発に関わっているエンジニアです。

先日、LinkedInで公開されていた業界カンファレンスの登壇動画を見つけたんですが、「後でチームメンバーと共有したい」「資料と照らし合わせながらオフラインで確認したい」と思った瞬間、あの「ダウンロードボタンが見当たらない」という現実に直面しました。

「LinkedIn APIを使えば取れるでしょ?」と思った方もいるかもしれません。実際、LinkedIn APIドキュメントを眺めると ugcPosts エンドポイントなどでメディア情報を取得できることが書かれています。しかし、2024年現在、個人開発者が動画ダウンロード用途で公式APIを活用するのは、認証スコープの制限や審査プロセスの厳格化により、現実的ではありません。

かといって、怪しいブラウザ拡張機能にログインセッションを渡すのも…ちょっと怖いですよね。

そんな「技術的には可能なのに、手軽に安全に使える手段がない」というギャップを埋めたくて、今回ご紹介する linkedin_downloader_ja の技術アーキテクチャを、開発者の視点から分解してみようと思います。

本記事では、単なる「使い方の紹介」ではなく、「LinkedInの動画メタデータをどう抽出し、どう認証を扱っているか」 という技術的な中身に焦点を当てます。コード例も交えつつ、実装の勘所を共有できれば幸いです。


LinkedIn動画配信の技術的構造:JSON-LDと認証の壁

1. ページソースに隠されたメタデータ

LinkedInの動画投稿ページ(/feed/video/xxx 形式)を開き、ページソースを確認すると、以下のようなJSON-LD(構造化データ)が含まれていることがあります:

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "VideoObject",
  "name": "セミナータイトル",
  "description": "動画の説明テキスト",
  "contentUrl": "https://media.licdn.com/dms/video/D4E05AQHxxx/mp4-720p-30fp-crf28/0/1234567890123?e=xxx&v=beta&t=yyy",
  "thumbnailUrl": "https://media.licdn.com/dms/image/D4E05AQGxxx/thumbnail.jpg",
  "uploadDate": "2024-01-15",
  "duration": "PT15M30S"
}
</script>

contentUrl には、署名付きの直接ダウンロードリンクが含まれている場合があります。ただし、このリンクには有効期限(e パラメータ)と署名(v=beta&t)が付与されており、時間経過やRefererチェックでアクセスが制限されることがあります。

2. 認証が必要なケースの扱い

LinkedInの動画には、大きく分けて2つのパターンがあります:

動画タイプ 特徴 取得難易度
公開投稿 ログイン不要で閲覧可能 ★☆☆(比較的低い)
接続限定/グループ限定 ログイン+権限が必要 ★★★(高い)

当サービスは、公開投稿のメタデータ抽出に特化 しており、認証が必要なコンテンツには対応していません。これは技術的な制限というより、プライバシーと利用規約遵守の観点からの設計判断です。


linkedin_pic (1)low.png

簡易実装:PythonでJSON-LD抽出からダウンロードまで

以下は、学習目的のサンプルコードです。実際の運用では、User-Agentの適切な設定、レート制限対応、エラーリトライなど、より多くの配慮が必要になります。

import requests
import re
import json
from bs4 import BeautifulSoup

def extract_video_metadata(post_url: str) -> dict | None:
    """LinkedIn投稿ページから動画メタデータを抽出"""
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
        "Accept-Language": "ja-JP,ja;q=0.9,en;q=0.8",
    }
    
    try:
        resp = requests.get(post_url, headers=headers, timeout=15)
        resp.raise_for_status()
        
        # JSON-LD スクリプトタグを検索
        soup = BeautifulSoup(resp.text, 'html.parser')
        ld_json = soup.find('script', type='application/ld+json')
        
        if not ld_json:
            # 代替: og:video メタタグを試す
            og_video = soup.find('meta', property='og:video')
            if og_video and og_video.get('content'):
                return {"contentUrl": og_video['content'], "source": "og:video"}
            return None
        
        data = json.loads(ld_json.string)
        
        # VideoObject であることを確認
        if data.get("@type") != "VideoObject" or not data.get("contentUrl"):
            return None
        
        return {
            "title": data.get("name"),
            "description": data.get("description"),
            "contentUrl": data.get("contentUrl"),
            "thumbnail": data.get("thumbnailUrl"),
            "duration": data.get("duration"),
            "source": "json-ld"
        }
        
    except Exception as e:
        print(f"Error: {e}")
        return None

# 使用例
url = "https://www.linkedin.com/feed/video/1234567890"
meta = extract_video_metadata(url)

if meta:
    print(f"タイトル: {meta.get('title')}")
    print(f"動画URL: {meta.get('contentUrl')}")
    print(f"取得元: {meta.get('source')}")
else:
    print("動画メタデータを取得できませんでした")

💡 補足:LinkedInは頻繁にフロントエンドの構造を変更します。このコードは「その時点で動作する」保証はありません。本番環境では、複数の抽出方法をフォールバックチェーンで実装し、頑健性を高めることをおすすめします。

⚠️ 注意点:contentUrl には有効期限付きの署名が含まれる場合があります。取得後すぐにダウンロード処理を開始しないと、403エラーになる可能性があります。


当サービスのアーキテクチャ:なぜ「サーバーに保存しない」設計なのか

linkedin_downloader_ja の最大の特徴は、「ユーザーのダウンロードファイルを一切サーバーにキャッシュしない」 という設計思想にあります。これには明確な技術的・法的な理由があります。

1. プライバシーとセキュリティの観点

  • ユーザーがダウンロードした動画のURL、IPアドレス、利用履歴などを記録しない
  • サーバー側にファイルを一時保存しないため、情報漏洩リスクが原理的に排除される
  • すべてクライアントサイドで完結するフロー(またはプロキシ経由のストリーミング転送)を採用

2. 著作権対応の設計

当サービスは「ダウンロード支援ツール」であり、「コンテンツの再配布プラットフォーム」ではありません。技術的には、以下のようなフローで実現しています:

[ユーザーブラウザ] 
    ↓ (1) LinkedIn投稿URLを送信
[当サーバー] 
    ↓ (2) ページ解析で動画メタデータを取得(キャッシュなし)
    ↓ (3) 直接ダウンロード用リンクを生成・署名付きURLを有効期限内に返却
[ユーザーブラウザ] 
    ↓ (4) LinkedIn CDN (media.licdn.com) から直接ファイルをダウンロード

この「中継のみで保存しない」アーキテクチャにより、当サーバーには著作権対象ファイルが一切存在しない 状態を維持できています。これは法的リスクの低減だけでなく、ストレージコストの削減、スケーラビリティの向上にも寄与しています。

3. 署名付きURLの扱いとフォールバック

LinkedInの動画URLには、以下のようなクエリパラメータが付与されていることがあります:

?e=1712345678&v=beta&t=abcdef1234567890
  • e: 有効期限(Unixタイムスタンプ)
  • v=beta&t: リクエスト署名(改ざん防止)

これらのパラメータをそのままクライアントに返すことで、ブラウザからの直接ダウンロードを可能にしています。ただし、有効期限切れの場合は「再度解析を実行してください」というメッセージを表示し、ユーザーに再取得を促す設計としています。

// 有効期限チェックの簡易例
function isUrlExpired(url) {
  const match = url.match(/[?&]e=(\d+)/);
  if (!match) return false; // 期限パラメータがない場合は不明
  
  const expiry = parseInt(match[1], 10) * 1000; // 秒→ミリ秒
  return Date.now() > expiry;
}

// 使用例
if (isUrlExpired(videoUrl)) {
  showWarning("このリンクは有効期限が切れています。再度解析を実行してください。");
  triggerReFetch();
}

実際の使い方:3ステップで完了するシンプル設計

技術的な中身は複雑でも、ユーザー体験はシンプルであるべき。当サービスのUIは以下の3ステップのみで構成されています。

  1. LinkedIn動画投稿のURLをコピー
    例:https://www.linkedin.com/feed/video/1234567890

  2. 入力欄に貼り付けて「解析を開始」をクリック
    約3〜7秒でメタデータ解析完了。利用可能な解像度オプション(存在する場合)が表示されます。

  3. ダウンロードボタンをクリックして保存
    ブラウザの標準ダウンロード機能を使用するため、追加ソフト不要。

スマホでも同じ操作性

レスポンシブデザインを採用しているため、iPhone / Android のブラウザからも全く同じ手順で利用可能です。LinkedInアプリ内ブラウザから利用する場合は、システムブラウザでの開きを促すメッセージを表示するなど、プラットフォーム固有の挙動にも配慮しています。


技術的なメリット:なぜこのツールが「軽量で高速」なのか

✅ 1. ステートレスなAPI設計

各リクエストが独立して処理されるため、セッション管理やデータベース接続が不要。これにより、サーバーリソースの効率的な利用と、突発的なトラフィック増加への耐性を実現しています。

✅ 2. CDNフレンドリーな配信

動画ファイルはLinkedInのCDN(media.licdn.com)から直接取得するため、当サーバーの帯域幅を圧迫しません。メタデータ取得のみをサーバー側で担当し、実際のファイル転送はクライアント↔LinkedIn間で行う設計です。

✅ 3. クライアントサイドでの進捗表示

解析・ダウンロード中のステータスは、Server-Sent Events(SSE)でリアルタイムにクライアントへ通知。ユーザーが「待たされている感」を感じないよう、技術的に配慮しています。

// SSEによる進捗通知の簡易例
const eventSource = new EventSource('/api/progress?job_id=xxx');
eventSource.onmessage = (e) => {
  const data = JSON.parse(e.data);
  // { step: "parsing_html", percent: 45, message: "JSON-LD解析中..." }
  updateProgressBar(data.percent);
  appendLog(data.message);
  
  if (data.step === "completed") {
    enableDownloadButton(data.downloadUrl);
  }
};

開発者としての想い:オープンな技術を、より多くの人に

私自身、長年エンタープライズ向けコンテンツ配信やAPI連携の開発に携わってきましたが、「技術的にできること」と「ユーザーが実際に使えるもの」の間には、常に大きなギャップがあると感じています。

このツールを作った理由はシンプルです:

「プロフェッショナルなコンテンツを、正当な目的で、手軽に活用したい」

という当たり前の欲求を、技術でサポートしたかったからです。

もちろん、LinkedInのプラットフォームポリシーや著作権法とのバランスは常に意識しています。本サービスは:

  • 公開投稿のメタデータのみを処理
  • 認証が必要なコンテンツ・非公開投稿には対応しない
  • 商用利用・再配布を目的とした利用を規約で禁止

といった制限を設けています。技術は中立ですが、その使い方は責任を持って設計すべきだと考えています。


Qiita読者への技術的プレゼント:自分で作ってみるためのヒント

もし「自分でも似たツールを作ってみたい」と思われた方に向けて、学習リソースをいくつかご紹介します。

🔍 参考になる技術スタック

用途 推奨技術
HTTPクライアント requests (Python), axios (Node.js)
HTML/JSON解析 BeautifulSoup, lxml, cheerio
構造化データ抽出 jsonld.js, schema.org ドキュメント参照
署名付きURL処理 URLパーサー、タイムスタンプ比較ロジック
フロントエンド Next.js + Tailwind, Streamlit(プロトタイプ用)
非同期処理 asyncio + aiohttp, BullMQ + Redis

📚 学習に役立つリソース

💡 実装時のアドバイス

  1. User-Agentの適切な設定:LinkedInはボット対策が厳しめ。ブラウザと見なされるヘッダーが必須。モバイルユーザーエージェントを使うと別構造のHTMLが返ってくる場合があるので注意。
  2. 複数の抽出方法を準備:JSON-LD → og:video → 正規表現 → ... とフォールバックチェーンを実装し、構造変更に強靭な設計に。
  3. 有効期限の扱い:署名付きURLには必ず有効期限チェックロジックを入れ、期限切れ時のユーザーガイドを明確に。
  4. エラーハンドリングの充実:404、403、レート制限、構造変更など、想定される失敗ケースを網羅し、ユーザーに分かりやすいメッセージを表示。

おわりに:技術は、誰かの「ちょっと嬉しい」を創れる

最後になりましたが、linkedin_downloader_ja は、個人開発者が「自分の不便を自分で解決した」ことから始まったプロジェクトです。

Qiitaのコミュニティでは、技術的な深掘りと実用性のバランスが取れた記事が好まれる傾向があります。本記事が、単なる「ツール紹介」ではなく、「技術的な仕組みへの理解」を深めるきっかけになれば幸いです。

もし実際に使ってみて、「ここが分かりにくい」「こういう機能が欲しい」といったフィードバックがありましたら、ぜひサイトの問い合わせフォームからご連絡ください。オープンな技術開発は、ユーザーとの対話から進化していくものと信じています。


📌 免責事項
本サービスは、LinkedIn Corporation とは一切関係ありません。
ダウンロードしたコンテンツの利用は、必ず著作権法および各プラットフォームの利用規約に従ってください。
個人での視聴・学習・正当な引用目的以外の利用(再配布、商用利用、改変など)はご遠慮ください。


タグ: #LinkedIn #動画ダウンロード #Webスクレイピング #Python #JavaScript #JSON-LD #API解析 #Qiita #技術記事 #メディア処理

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?