期待を持っていたGoogleChromeの拡張機能である「webRequest」がいつの間にか正式にリリースされていました。
これによってリクエスト/レスポンスヘッダをスクリプトから変更する事が可能に。
Web Requests - Google Chrome Extensions - Google Code
下記サイトがこの上なく参考になりました。
webRequest APIをざっくり理解する。(あるいはChrome拡張の作り方) | mzsm.me
この機能を使ってニコニコ動画の視聴ページ(/watch)から動画をダウンロード&時報ブロックの拡張を作ってみました。
自分も今までいくつかのニコニコダウンロード拡張を使っていたのですが、外部サービス経由でユーザーが増えるとダウンタイムが増えたり、ファイル名が"smile.mp4"固定だったり・・と。どうにも使い勝手が悪かった。
その理由がニコニコの動画サーバーから送られてくるこのレスポンスヘッダ。
Content-Disposition: inline; filename="smile.mp4"
ファイル名が"smile.mp4"なのはまさにこれが原因です。また、URLを新しいタブで開いてもダウンロード開始せずにブラウザ内で再生されるのは"inline"が原因です。動画保存に手間をかけさせたいという運営の努力の痕が見えます。
これを上書きできるwebRequestApiの前では残念ながら無力ですけど。
作った拡張を例に、そんな素晴らしいwebRequestApiを解説してみます。
動画ファイルにアクセスする際動画IDがヒストリーに含まれたクッキーが必要なのですが、クッキーの処理はブラウザがやってくれるので今回は関係ありません。
先ずmanifest.jsonについて。
permissionsに"webRequest"を追加するのはもちろんですが、アクセス先のホストのマッチパターンも書きます。このホストが拡張のインストール時に表示されます。res.nimg.jpは後述の時報ブロックの為。
{ "permissions": [ "webRequest", "http://*.nicovideo.jp/", "http://res.nimg.jp/" ], }
バックグラウンドページです。
webRequestの準備を行います。addListenerの第一引数が実際に処理を行う関数です。
関数の引数のresponseHeadersを調べ、Content-Dispositionヘッダを書き換え、それをreturnする事でヘッダを書き換え、任意のファイル名でダウンロードを行う事が出来ます。
動画にはmp4やflvと言った様々な種類があるのに注意。
25行目の["responseHeaders"]を指定しないと、レスポンスヘッダを書き換える事が出来ません。
chrome.webRequest.onHeadersReceived.addListener( function(details){ var hasHeader=false; var ext,m; if(movieTitle==""){return}; for(var i=0;i<details.responseHeaders.length;i++){ if(details.responseHeaders[i].name=="Content-Disposition"){ ext="mp4"; if(m=details.responseHeaders[i].value.match(/.+.([a-z0-9]+)"$/)){//" ext=m[1]; } details.responseHeaders[i].value="attachment; filename=""+movieTitle+"."+ext+"""; hasHeader=true; break; } } if(!hasHeader){ details.responseHeaders.push({name:"Content-Disposition",value:"attachment; filename=""+movieTitle+".mp4""}); } movieTitle=""; return {responseHeaders:details.responseHeaders}; },{ urls: ["*://*.nicovideo.jp/*"], types:["main_frame"] },["responseHeaders"] );
"blocking"を指定するとアクセスブロックが出来ます。おまけで時報ブロック機能も入れておきました。
flash内から呼び出されるURLのタイプはobjectらしいです。
chrome.webRequest.onBeforeRequest.addListener( function(details){ return {cancel:true}; },{ urls: ["http://res.nimg.jp/swf/system/marquee/default/*","http://flapi.nicovideo.jp/api/getmarquee_new*"], types:["object"] },["blocking"] );
webRequestの使い方はこんな感じ。
webRequestは様々な関数があるけど、使い分け方は不勉強でわかりません。
- onAuthRequired
- onAuthRequired
- onBeforeRedirect
- onBeforeRequest
- onBeforeSendHeaders
- onCompleted
- onErrorOccurred
- onHeadersReceived
- onResponseStarted
- onSendHeaders
特にonAuthRequiredにワクワクするけど、どういう意味なのかな。
以下全コードです。webRequestApiと関係ないから端折った視聴ページにボタンを追加する処理とか、タイトルを取得するコードも書いてあります。
{ "name":"ニコニコ直ダウンローダー", "description":"外部サーバーに頼らず、ファイル名を自動で付けてニコニコ動画を1クリックでダウンロードします。", "version": "1.1", "background_page":"background.html", "permissions": [ "webRequest", "webRequestBlocking", "http://*.nicovideo.jp/", "http://res.nimg.jp/" ], "content_scripts":[{ "matches": [ "http://*.nicovideo.jp/*"], "js":["script.js"] }] }
BUTTON_CSS_BASE="margin:3px;padding:2px;font-family:monospace;"; BUTTON_CSS_ENA="border:2px solid #90c0f0;background-color:#FFFFFF;color:#000000;"; BUTTON_CSS_DSA="border:2px solid #C0C0C0;background-color:#A0A0A0;color:#FFFF00;"; BUTTON_DOM_ID="chrome_Ex_nico_movie_get_down_buttonR"; vid=null;loadinit=false; if(document.URL.match(//watch/([0-9a-z]+)/)){vid=RegExp.$1} if(document.URL.match(//watch/lv/)){vid=null;} console.log(vid) if(vid!=null){ OnLoad(); } function OnLoad(){ var _td; _td = document.createElement("button"); _td.setAttribute("style",BUTTON_CSS_BASE+BUTTON_CSS_ENA); _td.setAttribute("id",BUTTON_DOM_ID); _td.innerHTML ='動画DL'; _td.addEventListener("click",goMovie,false); document.querySelector("#itab td").appendChild(_td); } function goMovie(){ chrome.extension.sendRequest({ action:"goMovie", args :[vid] },function(response){ } ); }
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <script> movieTitle=""; vidG=""; chrome.extension.onRequest.addListener(function(message,sender,sendResponse){sendResponse(eval(message.action).apply(sender,message.args));}); chrome.webRequest.onHeadersReceived.addListener( function(details){ var hasHeader=false; var ext,m; if(movieUrl!=details.url){return}; for(var i=0;i<details.responseHeaders.length;i++){ if(details.responseHeaders[i].name=="Content-Disposition"){ ext="mp4"; if(m=details.responseHeaders[i].value.match(/.+.([a-z0-9]+)"$/)){//" ext=m[1]; } details.responseHeaders[i].value="attachment; filename=""+movieTitle+"."+ext+"""; hasHeader=true; break; } } if(!hasHeader){ details.responseHeaders.push({name:"Content-Disposition",value:"attachment; filename=""+movieTitle+".mp4""}); } movieUrl=""; return {responseHeaders:details.responseHeaders}; },{ urls: ["*://*.nicovideo.jp/*"], types:["main_frame"] },["responseHeaders","blocking"] ); chrome.webRequest.onBeforeRequest.addListener( function(details){ console.log(details); return {cancel:true}; },{ urls: ["http://res.nimg.jp/swf/system/marquee/default/*","http://flapi.nicovideo.jp/api/getmarquee_new*"], types:["object"] },["blocking"] ); function goMovie(vid){ vidG=vid; //先ずはタイトルを取る title=html2Title(loadTextFile("http://www.nicovideo.jp/watch/"+vid)); //動画urlを取る movieUrl=html2url(loadTextFile("http://flapi.nicovideo.jp/api/getflv?v="+vid)); catchMovie=true; movieTitle=title+" - ["+vidG+"]"; window.open(movieUrl); } function loadTextFile(url){ httpObj= new XMLHttpRequest(); httpObj.open("GET",url,false); httpObj.send(null); return httpObj.responseText; } function html2url(html){ try{ html=decodeURIComponent(/url=([^&]+)/.exec(html)[1]); return html; }catch(e){ return ""; } } function html2Title(html){ try{ html=/<p id="video_title"><!-- google_ad_section_start -->(.+?)<!-- google_ad_section_end -->/.exec(html)[1]; return html; }catch(e){ return ""; } } </script> </head> </html>
以上が全コードです。
非表示ではなく、アクセス遮断を行う広告ブロッカーも近い将来に登場する事でしょう。
もちろん、これより優れたダウンローダーも。