YouTubeをHackしているコードを解析してみた
注意事項
本資料は技術的解析を目的としたものであり、悪用は禁止されています
本資料の内容は、技術的な解析および学術的な目的に限定して提供されています。ここで提供される情報を悪用し、不正な行為や違法な目的で使用することは固く禁じられています。情報の不適切な使用は、法律に違反する可能性があり、法的責任を問われることがあります。常に倫理的かつ法的に適正な範囲内で、技術を活用してください。
導入
今回は、以下のGitHubリポジトリがYouTubeの内部APIを特定し、正規のYouTube APIを介さずに動画をダウンロードできる仕組みになっていることが判明したため、その技術的な仕組みについて解析してみました。
対象の 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
この記事が参加している募集
この記事が気に入ったらサポートをしてみませんか?