記事一覧はこちら

ついにGoogleChromでスタンドアロンのダウンローダーとブロックツールを作る事が出来た!

期待を持っていた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&lt;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>

以上が全コードです。
非表示ではなく、アクセス遮断を行う広告ブロッカーも近い将来に登場する事でしょう。
もちろん、これより優れたダウンローダーも。