見出し画像

YouTubeをHackしているコードを解析してみた

注意事項

本資料は技術的解析を目的としたものであり、悪用は禁止されています

本資料の内容は、技術的な解析および学術的な目的に限定して提供されています。ここで提供される情報を悪用し、不正な行為や違法な目的で使用することは固く禁じられています。情報の不適切な使用は、法律に違反する可能性があり、法的責任を問われることがあります。常に倫理的かつ法的に適正な範囲内で、技術を活用してください。

YouTube API 利用規約(概要)利用許諾
YouTube APIは、正当かつ合法な目的のために使用することが許可されています。使用者はAPIを利用することで、YouTubeとGoogleの利用規約、およびAPIのポリシーに従うことを同意するものとします。
データの使用と保護
APIを通じて取得したデータは、プライバシー法およびユーザーのプライバシーを遵守する形で使用しなければなりません。データを第三者と共有したり、商業目的で無断使用することは禁止されています。ユーザーのデータにアクセスする場合、事前に明確な同意を得る必要があります。
ブランドガイドラインの遵守
YouTubeのロゴ、商標、またはその他のブランド資産を使用する際は、YouTubeのブランドガイドラインに従う必要があります。APIから取得した動画のサムネイルやタイトルなどは、改変せずに表示することが求められます。
APIリクエストとレート制限
APIのリクエストには制限があり、このリミットを超える使用は禁止されています。また、APIの過度の使用やサービスの妨害行為も禁止されています。YouTubeは、利用状況に応じてリクエストの制限を調整する権利を持っています。
禁止行為
APIを使用して以下の行為を行うことは禁じられています:
YouTubeのコンテンツやサービスに悪影響を与える行為
不正なアクセスや、他者のアカウント情報の取得
違法行為、プライバシー侵害、著作権の侵害
APIのリバースエンジニアリング
セキュリティと責任
使用者は、APIアクセスのセキュリティを確保し、YouTubeまたはそのユーザーに対して損害を与えるような行為を防ぐ責任を負います。APIの脆弱性を悪用しないこと、またはそのような行為に関与しないことが義務付けられます。
規約の変更と遵守
YouTubeは、APIの規約を予告なく変更する権利を持っています。変更が行われた場合、利用者はそれに従い、規定の範囲内でAPIを利用し続ける義務があります。
APIの停止または終了
YouTubeは、規約違反やその他の理由により、APIへのアクセスを制限または終了する権利を有します。また、APIそのものの提供を停止することも可能です。
責任制限
YouTubeおよびGoogleは、APIの利用に関連する損害や損失に対して責任を負いません。APIを利用するリスクは利用者自身が負うことになります。

https://developers.google.com/youtube/terms/api-services-terms-of-service

導入


今回は、以下のGitHubリポジトリがYouTubeの内部APIを特定し、正規のYouTube APIを介さずに動画をダウンロードできる仕組みになっていることが判明したため、その技術的な仕組みについて解析してみました。

API(Application Programming Interface)は、ソフトウェアやアプリケーション同士がデータや機能をやり取りするためのインターフェースです。APIは定義されたルールに基づいて、外部のプログラムが特定の機能やデータにアクセスできるようにします。これにより、開発者は既存のサービスやデータを利用しながら、効率的に新しいアプリケーションを開発できます。

例として、YouTubeのAPIを使うと、開発者はYouTubeの動画情報を取得したり、再生リストを管理したりすることができます。

対象の Github

https://github.com/Hexer10/youtube_explode_dart?tab=readme-ov-file

このリポジトリ全体を解説するには膨大な時間がかかるため、今回は内部APIを利用して動画をダウンロードする部分に焦点を当てて解説していきます。

まず、動画をダウンロードするまでのコード実行の流れを追っていきます。
サンプルとして、ターゲットにしたのは以下の動画です。

URL:
https://www.youtube.com/watch?v=d6i4AtCxrDo

(hump back 最高すぎる。。。。)

コード解析

基本的には YoutubeClient.cs が動作するだけなので、このファイルの動作を詳細に追っていきます。

1.
YoutubeClient.cs


YoutubeClient.cs は YoutubeExplode/ 配下にあります。
public class YoutubeClient のインスタンスが生成されると、以下の関数が実行されます

48 - 51行目

/// <summary>
/// Operations related to YouTube videos.
/// </summary>
public VideoClient Videos { get; }

2.
VideoClient.cs


YoutubeExplode/Videos/ 配下にあります。
public class VideoClient(HttpClient http) のインスタンスが生成されると、以下の関数が実行されます。

19 - 22行目

/// <summary>
/// Operations related to media streams of YouTube videos.
/// </summary>
public StreamClient Streams { get; } = new(http);

3.
StreamClient.cs.       


YoutubeExplode/Videos/Streams 配下にあります。
こちらが今回の解析対象の本丸となります。

i ) 
250 - 278行目
videoIDを引数にして、
GetStreamInfosAsync( VideoId videoId, CancellationToken cancellationToken = default )
関数が走ります。

    private async ValueTask<IReadOnlyList<IStreamInfo>> GetStreamInfosAsync(
        VideoId videoId,
        CancellationToken cancellationToken = default
    )
    {
        try
        {
            // Try to get player response from a cipher-less client
            var playerResponse = await _controller.GetPlayerResponseAsync(
                videoId,
                cancellationToken
            );

            return await GetStreamInfosAsync(videoId, playerResponse, cancellationToken);
        }
        catch (VideoUnplayableException)
        {
            // Try to get player response from a client with cipher
            var cipherManifest = await ResolveCipherManifestAsync(cancellationToken);

            var playerResponse = await _controller.GetPlayerResponseAsync(
                videoId,
                cipherManifest.SignatureTimestamp,
                cancellationToken
            );

            return await GetStreamInfosAsync(videoId, playerResponse, cancellationToken);
        }
    }

ここで、
private readonly StreamController _controller = new(http);
は 
VideoController(HttpClient http)
を継承しています。
GetStreamInfosAsync 関数で走っている GetPlayerResponseAsync は VideoController の中に定義されています。

var playerResponse = await _controller.GetPlayerResponseAsync(
                videoId,
                cancellationToken
            );

ii ) 
VideoController.cs. 
YoutubeExplode/Videos/ 配下にあります。
はい。ゴールしました。
内部APIが定義された場所とHeaderも書かれてますね。

    public async ValueTask<PlayerResponse> GetPlayerResponseAsync(
        VideoId videoId,
        CancellationToken cancellationToken = default
    )
    {
        // The most optimal client to impersonate is any mobile client, because they
        // don't require signature deciphering (for both normal and n-parameter signatures).
        // However, we can't use the ANDROID client because it has a limitation, preventing it
        // from downloading multiple streams from the same manifest (or the same stream multiple times).
        // https://github.com/Tyrrrz/YoutubeExplode/issues/705
        // Previously, we were using ANDROID_TESTSUITE as a workaround, which appeared to offer the same
        // functionality, but without the aforementioned limitation. However, YouTube discontinued this
        // client, so now we have to use IOS instead.
        // https://github.com/Tyrrrz/YoutubeExplode/issues/817
        using var request = new HttpRequestMessage(
            HttpMethod.Post,
            "https://www.youtube.com/youtubei/v1/player"
        );

        request.Content = new StringContent(
            // lang=json
            $$"""
            {
              "videoId": {{Json.Serialize(videoId)}},
              "contentCheckOk": true,
              "context": {
                "client": {
                  "clientName": "IOS",
                  "clientVersion": "19.29.1",
                  "deviceMake": "Apple",
                  "deviceModel": "iPhone16,2",
                  "hl": "en",
                  "osName": "iPhone",
                  "osVersion": "17.5.1.21F90",
                  "timeZone": "UTC",
                  "userAgent": "com.google.ios.youtube/19.29.1 (iPhone16,2; U; CPU iOS 17_5_1 like Mac OS X;)",
                  "gl": "US",
                  "utcOffsetMinutes": 0
                }
              }
            }
            """
        );

        // User agent appears to be sometimes required when impersonating Android
        // https://github.com/iv-org/invidious/issues/3230#issuecomment-1226887639
        request.Headers.Add(
            "User-Agent",
            "com.google.ios.youtube/19.29.1 (iPhone16,2; U; CPU iOS 17_5_1 like Mac OS X)"
        );

        using var response = await Http.SendAsync(request, cancellationToken);

詳しくフォーカスするとここ

using var request = new HttpRequestMessage(
            HttpMethod.Post,
            "https://www.youtube.com/youtubei/v1/player"
        );

        request.Content = new StringContent(
            // lang=json
            $$"""
            {
              "videoId": {{Json.Serialize(videoId)}},
              "contentCheckOk": true,
              "context": {
                "client": {
                  "clientName": "IOS",
                  "clientVersion": "19.29.1",
                  "deviceMake": "Apple",
                  "deviceModel": "iPhone16,2",
                  "hl": "en",
                  "osName": "iPhone",
                  "osVersion": "17.5.1.21F90",
                  "timeZone": "UTC",
                  "userAgent": "com.google.ios.youtube/19.29.1 (iPhone16,2; U; CPU iOS 17_5_1 like Mac OS X;)",
                  "gl": "US",
                  "utcOffsetMinutes": 0
                }
              }
            }
            """
        );
request.Headers.Add(
            "User-Agent",
            "com.google.ios.youtube/19.29.1 (iPhone16,2; U; CPU iOS 17_5_1 like Mac OS X)"
        );

次は返り値となっている下記を詳しく見てみましょう。
playerResponse

var playerResponse = await _controller.GetPlayerResponseAsync(videoId, cancellationToken);

iii ) 
playerResponse の解析
実際のデータを見てみましょう。
(いっぱいデータが入っているので、必要な部分だけ抜粋)

{
  status: 200,
  statusText: 'OK',
.....
data: {
.....
    videoDetails: {
      videoId: 'd6i4AtCxrDo',
      title: 'Hump Back - 「拝啓、少年よ」Music Video',
      lengthSeconds: '189',
      keywords: [Array],
      channelId: 'UC9G6cVN89XFM5DCZGU8yn-Q',
      isOwnerViewing: false,

ちゃんと取れてそうですね。
あとはこの中にある streamingData から動画データを引っ張ってくれば普通に機能しそう。

最終確認日:
2024/7/1

この記事が参加している募集

この記事が気に入ったらサポートをしてみませんか?