YouTubeダウンロードでお世話になっていた拡張機能が動かなくなった。代替サービスを探すもパッとするものがなかったので、自分でツールを作ることにした。
要件
今、Chrome系ブラウザで見ている動画を、{動画タイトル名}.mp4
で名前を付けて保存したい。1
Chrome拡張機能ではYouTubeページの変数にアクセスできないが、ブックマークレットならできるのでクリティカルな情報を直接取得できる
YouTubeのページをよく調べてみると ytInitialPlayerResponse
という変数に動画の情報(タイトルなど)や、URLなど、欲しい情報が全て詰まってるから、この変数を読み取れば簡単に機能を実現できるのではと思った。
それではじめChrome拡張機能2で作ろうとしたが、Chrome拡張はウェブページのDOM要素にはアクセスできても変数や関数といったスクリプト要素にはどうしてもアクセスできない(空間が別)3。先人たちはどうしてたのか見ると、どうも自分のサーバーに送って解析して‥みたいなことをしていたらしい。
それでふと思ったのが、ブックマークレットはそのページで直に実行されるから、ページに定義されている ytInitialPlayerResponse
にもアクセスできるよな、ってこと。4
ブックマークレットを編集するのに便利なサイト
-
Online JavaScript beautifier
- bookmarkletをjavascriptに整形する
-
bookmarklet maker
- javascript をブックマークレット化する
- 参考:Bookmarkletを編集して再びBookmarkletにする | 普通のサラリーマンのiPhone日記
特にbookmarklet maker はとても重宝している。少し修正してはbookmarklet maker にかけて様子を見るということは結構やる。
以下にあるスクリプトをブックマークレットとして使用する場合もbookmarklet maker にかけよう。
注意: 最近のchromeではブックマークレットのリンクをブックマークに追加しようとするとURLが about:blank#blocked
という形に変更されてしまう。
今のところ、ブックマークURLを直接編集することで対応できるが、そのうちブックマークレット自体が完全に動かなくなる日も来るのかもしれない。セキュリティの問題なのかもね。
動画本体URLと、動画のタイトルの取得
配列 ytInitialPlayerResponse.streamingData.formats
の要素のキー url
が実際の動画のURLで、たとえばこれは https://‥.googlevideo.com/videoplayback‥
みたいなのになっている。これをダウンロードしたらOK。配列になってるのは品質の違うファイルが複数用意されているため。ただしこのファイルはタイトル情報を持っていないため、ダウンロードしようとすると video.mp4
というふうに、タイトルなしでDLされるため、タイトル情報を別に取得して、動画ファイル名として設定しておきたい。
動画のタイトルは ytInitialPlayerResponse.videoDetails.title
から参照できる。
a要素href属性に直にURLを指定してダウンロードしようとすると失敗する(クロスオリジンのため)
a要素を生成してそのdownload属性に動画タイトルを設定して、click()
すればダウンロードが始まるはず‥(JavaScript でファイル保存・開くダイアログを出して読み書きするまとめ - Qiita)
ところが‥
javascript: (function(d) {
var url = ytInitialPlayerResponse.streamingData.formats[0].url;
var title = ytInitialPlayerResponse.videoDetails.title;
var a = d.createElement('a');
a.href = url;
a.download = title;
a.click();
})(document);
これを実行しても、ダウンロードされず、代わりにダウンロード先として指定した内容がそのまま直に開かれてしまう(ここではmp4のファイルが別タブで直接開かれる)。これはブラウザがクロスオリジンを認めていないため。つまり www.youtube.com
から *.googlevideo.com
にあるファイルをダウンロードさせることはできない(YouTube動画ファイルはyoutube.comではなくgooglevideo.comに置かれている。)
- 参考
Blob化してDLすることでクロスオリジン制限を回避する
<a>のdownload属性は同一オリジンじゃないとダウンロード出来ない - nwtgck / Ryo Ota の代替案1 にある方法5、XMLHttpRequest を介してDLすることでクロスオリジン制限を回避する(ただし、ファイルサイズが大きい場合、クリックしてから保存ダイアログが出るまでにラグが生じる)。
- blob & createObjectURL について - Qiita
- window.URL.createObjectURL()は、URLのメソッドです。Blob、Fileを参照するための一時的なURLを作成します。
javascript: (function(d) {
var url = ytInitialPlayerResponse.streamingData.formats[0].url;
var title = ytInitialPlayerResponse.videoDetails.title;
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.responseType = 'blob';
xhr.onload = function(){
var a = d.createElement('a'), blob = xhr.response;
a.download = title;
a.rel = 'noopener';
a.href = URL.createObjectURL(blob);
setTimeout(function(){ URL.revokeObjectURL(a.href) }, 4E4);
setTimeout(function(){ a.click() }, 0);
};
xhr.onerror = function(){
console.error('could not download file');
};
xhr.send();
})(document);
DLするファイルの品質を選択できるようにする。ついでにファイル名にアップロード日やチャンネル名を含める
今までダウンロードURLを formats[0].url
の決め打ちにしていたが、DLしたいファイルの品質を選びたいときもあるだろう。formats
を走査して、品質一覧から選択できるようにする(formatsの最初の要素に 360p、次の要素に 720p の動画情報が含まれていることが多いようだ。6)。
さらに、ファイル名にその動画のアップロード日やチャンネル名を含んでいると後から整理するときに便利だ。
ファイル名のフォーマットは ${author}(${uldt})${title}
(チャンネル名、アップロード日、動画タイトル)にしたが、都合に応じて適宜カスタマイズしよう。
javascript: (function(d) {
var title = ytInitialPlayerResponse.videoDetails.title;
var fms = ytInitialPlayerResponse.streamingData.formats,
msg = `${title}\nDL対象をindexで指定してください\n`;
for (let i=0,len=fms.length; i<len; i++){
msg += `${i}:%20${fms[i].qualityLabel}%20(${fms[i].quality})\n`;
}
var targ = window.prompt(msg, 0);
if (!targ){
return
} else if (!targ.match(/^\d+$/) || targ >= fms.length){
alert(`無効な値です:%20"${targ}"`);
return
}
var author = ytInitialPlayerResponse.videoDetails.author;
var uldt = ytInitialPlayerResponse.microformat.playerMicroformatRenderer.uploadDate;
var xhr = new XMLHttpRequest();
xhr.open('GET', fms[targ].url, true);
xhr.responseType = 'blob';
xhr.onload = function(){
var a = d.createElement('a'), blob = xhr.response;
a.download = `${author}(${uldt})${title}`.replace(/[./\\:*?"<>|]/g,
s=>{return String.fromCharCode(s.charCodeAt(0)+0xFEE0)});
a.rel = 'noopener';
a.href = URL.createObjectURL(blob);
setTimeout(function(){ URL.revokeObjectURL(a.href) }, 4E4);
setTimeout(function(){ a.click() }, 0);
};
xhr.onerror = function(){
console.error('could not download file');
};
xhr.send();
})(document);
これで、希望するファイル品質を index 番号で入力して選択DLできるようになった。さらにファイル名に含んでいると不都合が出そうな文字 [./\\:*?"<>|]
は全角文字に置換することで回避できるようにした。7
注意点
別の動画に飛んだ時にはページをリロードする必要がある
変数 ytInitialPlayerResponse
を使えばDLに必要な情報は取得できるということが分かったが、この変数はYouTubeのページを遷移しても更新されない。つまりAAAAという動画のページを開いたとき、ytInitialPlayerResponse
には動画AAAAの情報がセットされるが、その後にその動画のリンクからBBBBという動画ページへ遷移しても、ytInitialPlayerResponse
の中身はAAAAの情報のままである。
ytInitialPlayerResponse
の情報をBBBBのものに更新するにはページを再読み込みする必要がある。
開いてからしばらく放置していたページの動画はDLできない
動画のリンクに有効期限があるらしく、それを過ぎた動画はDLできないためだと思う。
YouTubeをDLするのに他に有用と思われるツール
-
記事の後の方を見てもらえばわかるが、最終的には、アップロード日、チャンネル名、動画タイトルを含めたファイル名を付けるようにした。 ↩
-
最近はYouTubeをDLするような拡張はGoogleストアに「ダメだよ」される ↩
-
かつては拡張からでも
location.href
を介してjavascriptを投げることができたらしい。今現在はそんなことはできなくなっている。Chrome拡張でページのグローバル変数にアクセスしたい|UTAGE.WORKS カワシマ [フリーランスウェブクリエイター]|note ↩ -
すでに誰かが作ってるだろうと思って「YouTube ダウンロード ブックマークレット」で検索してみたが、出てきたのは動かなくなった古いやつか、よくわからないサイトに飛ばされる粗悪なものだけだった。 ↩
-
「eligrey/FileSaver.js のここらへん」という言葉を頼りに参考にした。
setTimeout(function(){ URL.revokeObjectURL(a.href) }, 4E4);
というのは、40秒後にこのリンクを無効にするという意味ね。URL.createObjectURL()
で作成したリンクは不要になったらrevokeObjectURL()
で都度削除してメモリを解放した方がいいらしい。 ↩ -
それ以上の品質の動画は
ytInitialPlayerResponse.streamingData.adaptiveFormats
の方にあるが、この動画urlには音声が含まれていない。 ↩ -
入り組んだJavaScriptの変数を調べるときには JavaScriptのコード整形ツール にかけて整形してから調べると欲しい要素がどこにあるか見つけやすくなるよ。 ↩