YouTubeの動画サムネイルをダウンロードするコンテキストメニューを作る

YouTubeのサムネイルを別タブで表示したい場面がありまして、URLを手打ちするのは面倒くさいなぁ…と思い、
動画上で右クリックしたコンテキストメニューの一番下に「サムネイル表示」リンクを追加することにしました。

## YouTubeサムネイルURLのパターン

いくつかありますが、以下のどちらかで大きめの画像が取れます。
上がダメなら下で取れます。
http://img.youtube.com/vi/ビデオID/maxresdefault.jpg
http://img.youtube.com/vi/ビデオID/sddefault.jpg

ビデオIDはYouTube動画の v パラメータです。※URLは私の投稿した動画です。
https://www.youtube.com/watch?v=Z_LmFZdGhMo
の`Z_LmFZdGhMo`の部分です。

## ダウンロードリンクを作る

URLは上記の通りなので、URLをパースしてvの値を取ってみます。
ブラウザからは location.search でクエリストリングが取れるので、ここから取得を試みます。
上記のURLの場合、location.searchは以下が返ってきます。
`?v=Z_LmFZdGhMo`

最初の?がいらないので、replaceするかsubstringして取り払います。
`location.search.substring(1);`

=でパースして、配列の二つ目をとるとビデオIDがとれます。
`location.search.substring(1).split("=")[1];`

サムネイルのURLが作れました。
`'http://img.youtube.com/vi/' + location.search.substring(1).split("=")[1] + '/maxresdefault.jpg';`

## コンテキストメニューのDOMを取得する

コンテキストメニューに画像のリンクを追加したいので、対象のDOMを取得します。
Webブラウザの開発コンソール（大体F12を押すと出てきます）を開いて、インスペクタで対象をクリックするとIDやクラスが取れます。
しかし残念ながら、今回取りたいのはコンテキストメニューです。コンテキストメニュー外をクリックすると消えてしまいます。
こういう場合は、インスペクタの画面で各エレメントにマウスオーバーしていくと、画面上の対象がオーバーレイされるので、これで判断していきます。

今回は以下のエレメントがコンテキストメニューだと分かりました。
`<div class="ytp-popup ytp-contextmenu">`

## コンテキストメニューの表示を監視する

追加したい対象のエレメントは分かりましたので、これが表示された場合にリンクボタンを追記してあげればOKです。
表示の監視をする場合、最初に思いつくのは定期実行です。
JavaScript の場合は setTimeout がありますが、これを使うとただでさえ重たいYouTubeが更に重くなってしまいます、快適さを犠牲にしてまではやりたくありません。
この場合は MutationObserver を使って、DOM変更された場合に処理を動かします。

MDN: https://developer.mozilla.org/ja/docs/Web/API/MutationObserver

## コードを書く

コンテキストメニューに合うように、リンクエレメントを作ります。
```js
var e = document.createElement('div');
e.setAttribute('class', 'ytp-menuitem');
e.innerHTML = '<div class="ytp-menuitem-icon"><img width="20" style="margin-left:2px; filter: invert(100%);" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAYAAADhAJiYAAAAz0lEQVR4Ae3TIQjCYBCGYcMf17Eng9HeYR3jOusY1zHaWc+LBoPRYFgwGo2GPxgM51sGMhzCbTfGuA+efG+5xVznCyhQQwZSIYVqOcRIrgmqDYOumiAx5kHqIGl4kAd5kI4HPRCnEvTCBumYQRE53pCWDM2KsYKyjoMHtFdZB5VoFnCE4ISA9hLcrIJqJPjeEhcs0bUV4tBBEWv8WsC/pUMHZei7om9QhBg5a4JKw6CtJijBDvcBQ57YI2iCjOdB+kWIGcXKCQQpvknPN799AEmydQIh3ZMSAAAAAElFTkSuQmCC"></div><div class="ytp-menuitem-label">サムネイル表示</div><div class="ytp-menuitem-content"></div></div>';
e.onclick = function() {
	var pm = location.search.substring(1).split('&'), vid;
	for(var i = 0, len = pm.length; i < len; i++) {
		if(pm[i].search('v=') !== -1) { vid = pm[i].split('=')[1]; }
	}
	var a = document.createElement('a');
	a.href = 'http://img.youtube.com/vi/'+vid+'/maxresdefault.jpg';
	a.download = vid+'.jpg';
	a.target = '_blank';
	document.body.appendChild(a);
	a.click();
	a.remove();
}
```

コールバックを書きます。

```js
var ob = new MutationObserver(function(mu) {
	if(mu[0].addedNodes[0].className=='ytp-popup ytp-contextmenu'){
		document.getElementsByClassName('ytp-popup ytp-contextmenu')[0].getElementsByClassName('ytp-panel-menu')[0].appendChild(e);
		var baseHeight = Number(String(document.getElementsByClassName('ytp-popup ytp-contextmenu')[0].style.height).match(/([0-9]+)/)[0]);
		document.getElementsByClassName('ytp-popup ytp-contextmenu')[0].style.height=(baseHeight+40)+'px';
		document.getElementsByClassName('ytp-popup ytp-contextmenu')[0].getElementsByClassName('ytp-panel')[0].style.height=(baseHeight+40)+'px';
		document.getElementsByClassName('ytp-popup ytp-contextmenu')[0].getElementsByClassName('ytp-panel-menu')[0].style.height=(baseHeight+40)+'px';
		ob.disconnect();
	}
});
```

コンテキストメニューのエレメントはbody直下にあるようなので、変更監視はbodyを指定します。
```js
ob.observe(document.body, { childList: true });
```

上記をまとめたJSをViolentMonkeyなどのユーザスクリプトを実行する拡張機能に設定しました。
動画上で右クリックすると、コンテキストメニューの一番下に「サムネイル表示」リンクが追加されました。
クリックするとサムネイルが表示されます。

動画によっては sddefault.jpg までしかない場合もありますので、気になる場合は変更するかサムネイル表示リンクを増やしてもいいと思います。