ホーム >> 左脳Script >> Adobe AIR >> ニコニコ動画をダウンロードするには

ニコニコ動画をダウンロードするには


ニコニコ動画へのインターネットショートカットが残っていた。
開いてみた。
消えていた。

何故保存しておかなかったのだろう・・・


と言うわけで、どうやったら(アンな動画やコンな動画が)保存できるのか調べて(Adobe AIR で)試してみました。




ニコニコ動画に関する調査

使えそうなAPI(でもあんまり使ったらニコニコ重くなるので注意)挙げてみます。

  • 「http://tn-skr.smilevideo.jp/smile?i=動画id(smなし)」
    サムネイル画像取得。

    サムネサーバが追加されたみたいです。
    • http://tn-skr1.smilevideo.jp/
    • http://tn-skr2.smilevideo.jp/
    →引用:http://efcl.info/2008/0203/res54/

    後述の「/api/getthumbinfo/」で正確な?サムネイル画像URLを得る事が出来ます。結局は上記いずれかのドメインから始まる画像のURLになるようです。全ての動画のサムネイル画像が、上記のどれからでも得られるかどうか保障がありません。「/api/getthumbinfo/」で正確な?サムネイル画像URLを取得するほうが間違いは無いでしょう。

  • http://www.nicovideo.jp/api/getflv/動画ID(sm*******)
    動画の実体であるflvファイルのurlを取得できます。



ログイン

ニコニコ動画は無料(エコノミー会員)ですが会員制です。当然、情報を得るには認証をする必要があります。

ログインページに、メールアドレスとパスワードをpostするルーチン。

private function nico2login(mail:String, pass:String, resultCallback:Function):void 
{
    var parameter:URLVariables = new URLVariables();
    parameter.mail = mail;
    parameter.password = pass;
    //
    var loginRequest:URLRequest = new URLRequest();
    loginRequest.url = "https://secure.nicovideo.jp/secure/login?site=niconico";
    loginRequest.method = URLRequestMethod.POST;
    loginRequest.data = parameter;
    //
    var result:URLStream = new URLStream();
    result.addEventListener(IOErrorEvent.IO_ERROR,function (e:IOErrorEvent):void 
    {
        resultCallback(false);  //  明らかな失敗
    });
    result.addEventListener(HTTPStatusEvent.HTTP_RESPONSE_STATUS,function (e:HTTPStatusEvent):void 
    {
        var reg:int = 0;
        if (    e   )
        {
            for each(var line:Object in e.responseHeaders)
            {
                if (    "x-niconico-authflag" == line.name  )
                {
                    reg = int(line.value);
                }
            }
        }
        result.close(); //  ログインが出来ればよいので、余計なページを読まないで閉じる。
        resultCallback(Boolean(reg));   //  結果
    });
    result.load(loginRequest);
}
認証アクセスが終了すると、resultCallback に Boolean で認証の合否をコールバック通知します。URLStream を使ったのは「見もしない余計なページを読まないで閉じる。」事で回線を軽く?する狙いがあります。

レスポンスヘッダの内容( x-niconico-authflag の値)から、認証の合否を判定しています。
これは今回のアクセスでレスポンスヘッダの内容を観察した結果、使えると判断したものです。

ニコニコ動画のページや API のアクセスについて回るステータスのようです。実は、検索してもあまり情報が出てきませんでした。


flv情報取得

情報取得のAPIは「http://www.nicovideo.jp/api/getflv/動画ID(sm*******)」で、帰ってくるモノは単純なテキストです。中身は「名前=値&名前=値&名前=値&・・・」の形になっています。

これは、Flash の Loadvarsクラス、古くは loadvariables で取得するのに便利なフォーマットのようです。
Action Script 3 では、URLloader で、URLLoaderDataFormat.VARIABLES を指定すると簡単に読み込む事が出来ます。
→参考:http://void.heteml.jp/blog/archives/2007/08/loadvars_onload.html

以下、flvファイルの情報を表示するコード。

var flvRequest:URLRequest = new URLRequest();
flvRequest.url = "http://www.nicovideo.jp/api/getflv/" + num;
//
var ldr:URLLoader = new URLLoader();
ldr.dataFormat = URLLoaderDataFormat.VARIABLES;
ldr.addEventListener(IOErrorEvent.IO_ERROR,function ():void 
{
    trace("GetFlv IO error");
});
ldr.addEventListener(Event.COMPLETE,function ():void 
{
    for(var key:String in ldr.data) 
    {
        trace(key + "=" + ldr.data[key] );
    }
});
ldr.load(flvRequest);


実際に取得できるパラメータの内訳は以下の通り。
この辺りは「"http://www.nicovideo.jp/api/getflv/"」で検索する(ダブルクォーテーションで括らないと、直接アクセスするので注意)と沢山ヒットします。
→参考:http://blog.hitobashira.org/?eid=21
→引用:http://www.cathodemusic.net/api/

  • thread_id=スレッドID
  • l=不明
  • url=動画ファイル(flv等)のurl
  • link=動画の詳細情報のurl
  • ms=コメントのurl
  • deleted=削除され視聴できない場合"1"
  • user_id=ユーザーID
  • is_premium=プレミアムユーザなら"1"エコノミーなら"0"
  • nickname=ユーザー名
  • time=日時?
  • done=不明
  • ng_up=不明
  • error=nvalid_v1の場合、削除済み、invalid_v2の場合、非表示、invalid_v3の場合、権利者削除。cant_get_detailの場合、その情報さえ残っていない。

ダウンロードに必要なパラメータは「url」と「deleted」あたりでしょうか。


また、以下のコードでレスポンスヘッダも確認してみました。

ldr.addEventListener(HTTPStatusEvent.HTTP_RESPONSE_STATUS,function (e:HTTPStatusEvent):void 
{   //  HTTPレスポンスヘッダ
    var r:String = "";
    if (    e   )
    {
        for each(var line:Object in e.responseHeaders)
        {
            trace(line.name + " : " + line.value);
        }
    }
});
結果「Content-Type: text/plain;」でした。また「x-niconico-authflag: 1;」も含まれていました。


flvダウンロード

オリジナルflvファイル名などの情報は無い(HTTPレスポンスには、filename指定があるが"smile.flv"固定)ので、「動画ID.flv」等、こちらで適当に決めてしまいましょう。 後は、アクセス結果をファイルにするだけです。

全てを一度メモリに読み込んで、ファイルに書き出す方法が一番簡単です。しかし、flvファイルの大きさは一様ではなく、かなり大きな物になる可能性もあります。

そのようなファイルの場合「少しずつ読み込み、つど書き出す事で、メモリに溜め込まない」方法が定石となります。

var save:File = new File("t:/temp/" + id + ".flv");
var src:FileStream = new URLStream();
var dst:FileStream = new FileStream();
src.addEventListener(ProgressEvent.PROGRESS,function (e:ProgressEvent):void
{
    var tmp:ByteArray = new ByteArray();
    src.readBytes(tmp, 0, src.bytesAvailable);
    dst.writeBytes(tmp);
});
src.addEventListener(Event.COMPLETE,function (e:Event):void 
{
    src.close();
    dst.close();
});
dst.openAsync(save, FileMode.WRITE);
src.load(new URLRequest(url));
大雑把にファイル保存記述。このコードはエラーを考慮していないので参考程度に。


やってみた

が、どうも上手く行きません。ダウンロードできないのです。セッションが切れてしまったのか?と、思いきや全然違う模様。

ヒントは無いかと、情報の海を彷徨って・・・
これは試す価値がありそうです。

ひとまず「見ているフリ」から試して見ます。
AIR用の認証ルーチン。の、認証パラメータ部分に「対象視聴ページへの誘導スイッチ」を追加。

private function nico2login(num:String, mail:String, pass:String, resultCallback:Function):void 
{
    var parameter:URLVariables = new URLVariables();
    parameter.mail = mail;
    parameter.password = pass;
    parameter.next_url = "/watch/" + num;
    //
    ~
試してみると、バッチリ成功!ダウンロード出来るようになりました。
「parameter.next_url」をコメントアウトすると「403 Forbidden」になったので、 「対象ページに移動」の指定だけで問題は回避できたと言えるでしょう。

リファラの方法も試してみます。
「見ているフリ」の方法は無効にして、試してみた。

flvダウンロードの URLRequest に、リファラ指定を追加。

var req:URLRequest = new URLRequest(url);
req.requestHeaders.push(new URLRequestHeader("Referer", "http://www.nicovideo.jp/watch/" + num));

しかし、結果として此方は失敗した。リファラ偽装では許してもらえなかったようだ。

以上2点の実験から、「ニコニコ動画flvファイルのhttp経由ダウンロードは、リファラは見ていない」 「少なくとも、視聴ページを要求したアクセス履歴が直前に必要」という事になると思われます。


動画の詳細情報を取得する

flv の URLアドレス を知る為だけであれば、「/api/getflv/」で十分なのですが、正しいサムネイル画像URL、flvファイルの物理的なサイズ、その他の情報など、動画に関する様々な情報を取得するには「http://www.nicovideo.jp/api/getthumbinfo/動画ID(sm*******)」 を使う必要があります。

このAPIでは、結果がXMLで返ってきます
また、ログイン認証しなくても情報を得ることが出来るようです。

ActionScript では教科書的な XMLテキストのダウンロードコード。

var infoRequest:URLRequest = new URLRequest();
infoRequest.url = "http://www.nicovideo.jp/api/getthumbinfo/" + num;
//
var ldr:URLLoader = new URLLoader();
ldr.dataFormat = URLLoaderDataFormat.TEXT;
//  情報取得で致命的なアクセスエラー
ldr.addEventListener(IOErrorEvent.IO_ERROR,function ():void 
{
    trace("GetThumbinfo IO error");
});
//  情報取得アクセス完了
ldr.addEventListener(Event.COMPLETE,function ():void 
{
    var res:XML;
    try
    {
        res = new XML(ldr.data);
    }
    catch (e:Error)
    {
        res = null;
    }
    trace(res);
});
//  情報取得アクセス開始
ldr.load(infoRequest);

返ってくるXMLを見ると、「<size_high>」「<size_low>」が flvの大きさに相当する事が容易に推測できます。
エコノミーモードのLowレゾは、「/api/getflv/」からの flvファイルURLの末尾に"low"が付くので判別できるようです。


レジューム

ダウンロードも時間帯によっては非常に重いです。当然、途中で切れる事もあります。
ファイルをリトライの度に最初から落としていては、とても無駄が多いですね。そこで、ダウンロードが終わっている箇所から無駄なく続きをダウンロードする機能をつけてみましょう。

ここで、ダウンロード開始前にファイルの正確なサイズが必要になります。実は、このレジューム機能の為に「/api/getthumbinfo/」の情報取得機能を説明をしました。


ダウンロードレジュームを実現させる為には、httpヘッダに「Range」を設定する必要があります。
以下、リクエストヘッダにダウンロード範囲を指定するコード。

var req:URLRequest = new URLRequest(url);
if (    total > 0   &&  range > 0   )
{   //  reage による ダウンロード範囲指定。(レジューム指定)
    var value:String = "bytes=" + range.toString() + "-" + total.toString();
    req.requestHeaders.push(new URLRequestHeader("Range", value));
}

更に、リクエストがタイムアウトで切れたり、エラーで終わってしまったりした際に、 再びリクエストを投げる(回数制限有りのリトライ)ようにしてみました。

以下、ダウンロードルーチンです。

//  ファイルダウンロード
private function downloadFile(url:String, total:Number, save:File,
                            reTry:int = 10, timeoutSec:int = 60):void 
{
    var src:URLStream = new URLStream();
    var dst:FileStream = new FileStream();
    //
    var range:Number = save.exists ? save.size : 0; //  途中からダウンロード
    //  タイムアウト
    var time:Timer = new Timer(1000);   //  1秒
    var timeCount:int = 0;
    time.addEventListener(TimerEvent.TIMER,function ():void 
    {
        timeCount++;
        if (    timeCount > timeoutSec  )
        {
            fsClose();
            trace("タイムアウト");
        }
    });
    //
    function fsClose():void 
    {
        src.close();
        dst.close();
        time.stop();
    }
    //  書き込むファイルストリーム
    dst.addEventListener(IOErrorEvent.IO_ERROR,function (e:IOErrorEvent):void 
    {
        trace("書き出しエラー");
        fsClose();
    });
    dst.addEventListener(Event.CLOSE,function (e:Event):void 
    {
        if (    total > range   )
        {   //  途中から再開。
            if (    reTry-- )
            {
                fsStart();
                trace("リトライ:" + reTry.toString());
            }
            else
            {
                trace("保存未完了");   //  保存未完
            }
        }
        else
        {
            trace("保存完了");  //  保存完了
        }
    });
    //  読み込むURLストリーム
    src.addEventListener(ProgressEvent.PROGRESS,function (e:ProgressEvent):void 
    {
        trace(range + " : " + e.bytesLoaded + "/" + e.bytesTotal + "(" + (e.bytesLoaded / e.bytesTotal));
        //
        timeCount = 0;  //  タイムアウトリセット
        range += src.bytesAvailable;
        //
        var tmp:ByteArray = new ByteArray();
        src.readBytes(tmp, 0, src.bytesAvailable);
        dst.writeBytes(tmp);
    });
    src.addEventListener(IOErrorEvent.IO_ERROR,function (e:IOErrorEvent):void 
    {
        trace("ダウンロードエラー");
        fsClose();
    });
    src.addEventListener(Event.COMPLETE,function (e:Event):void 
    {
        trace("ダウンロード完了");
        fsClose();
    });
    src.addEventListener(HTTPStatusEvent.HTTP_RESPONSE_STATUS,function (e:HTTPStatusEvent):void 
    {
        if (    e   )
        {
            for each(var line:Object in e.responseHeaders)
            {
                if (    String(line.name).match(/Content\-Type/i)
                    &&  String(line.value).match(/text\/plain/i)    )
                {
                    trace("flvファイルではない");
                    fsClose();
                    break;
                }
            }
        }
    });
    //  ダウンロード開始
    function fsStart():void 
    {
        if (    range < total   )
        {
            var req:URLRequest = new URLRequest(url);
            if (    total > 0   &&  range > 0   )
            {   //  reage による ダウンロード範囲指定。(レジューム指定)
                var value:String = "bytes=" + range.toString() + "-" + total.toString();
                req.requestHeaders.push(new URLRequestHeader("Range", value));
            }
            //  書き出しファイルオープン
            dst.openAsync(save, FileMode.UPDATE);
            dst.position = range;   //  レジューム開始位置
            src.load(req);
            //  タイムアウトタイマーカウント開始
            timeCount = 0;
            time.start();
        }
        else
        {
            trace("保存完了");  //  保存完了
        }
    }
    //  保存開始
    fsStart();
    trace("保存開始");
}
ちょっと trace が多いけどキニシナイ!

実際にわざわざ重い時間帯(夜の8時ごろ~12時ごろ)に、幾つか動画をダウンロードしてみました(ニコニコ動画さん負荷かけてゴメンナサイ)。
応答が無くても(それなりにしつこく)リトライし、ダウンロードが途中で切れても「続きをダウンロード」する事によって、ちゃんと全体をダウンロードできる事を確認しました。

ただし、動画が古めだと「何故かレスポンスが悪い傾向がある」ように思えたのですが・・・気のせいなのでしょうか?!


結論:「ニコ動ローダー」作成のポイント

ダウンロードまでの手順は以下のようになるでしょう。
  1. 「/api/getthumbinfo/」で動画の存在やファイルの大きさ等の詳細情報を得る。
  2. ログインする。
  3. 「/api/getflv/」で動画ファイルの実体があるURLを得る。
  4. 閲覧ページを開く。「見ているフリ」をする。
  5. ダウンロードする。

今回の模索実験では、ログインと同時に閲覧ページを開いて(正確にはリクエストし、レスポンスを得るだけ)いますが、「/api/getflv/」自体は、「見ているフリ」をする必要が無かったので、上記の順に記述しました。

また、現時点(2009年12月11日)では、対象動画の「存在の有無」「サムネイル画像」「ファイルサイズ」はログイン情報がなくても取得できるようです。


雑感・メモ

  • ダウンロードした動画を眺めていたが、あの横に流れるログも非常に動画の雰囲気を盛り上げていると痛感。やはり「動画と同時にログがのたうつアノ画面こそが楽しい」のだと理解。
    コメントログとかのアニメーションも再現したくなる。そこまでやるとただのニコ動プレーヤだね。
  • 「/api/getflv/」もその内「/api/getfvlinfo/」とかになってXMLを返してくるようになりそうなキガスル。そのうちネ。
    おっと、携帯動画がある限りはこのままか?
  • 今回の調査中に「setTimeout→Timer」推奨の事実を初めて知ったorz=3
  • レジューム機能の部分で「ファイルのサイズを知る必要」と書いているが、実はなくてもちゃんとダウンロードできそうだと、後から判ってしまった。HttpのRange機能の調査不足だったorz



他参考リンク

ニコ動保存の基礎として、Perl だけれど参考にさせて頂きました。でも「見てるフリ」してないんよね。今でもアノコードでおとせるのだろうか?申し訳ありません。してました。ものスゴイ斜め読みで、重ね重ね申し訳ありません。あぁ、はずかしいorz
コードを最初に見た際に「何故ページにアクセスしているのか」判らなかったので、記憶から抜けていたようです。自分で嵌って見ないと理解できないコードってのもある物なんですね・・・
http://blog.livedoor.jp/dankogai/archives/50900305.html





トラックバック(1)

トラックバックURL: http://n-yagi.0r2.net/sanoupulurun/mt-tb.cgi/240

どうも。毎日が眠気との戦いです。 今日、(とは言ってもかなり前に見つけて今書いてるわけですが)smIDのビデオの実体flvを参照する方法を発見いたしました... 続きを読む

コメントする

ホーム >> 左脳Script >> Adobe AIR >> ニコニコ動画をダウンロードするには

アーカイブ

このブログ記事について

このページは、n-yagiが2009年12月13日 21:34に書いたブログ記事です。

ひとつ前のブログ記事は「理不尽な VerifyError とシーケンス ~ifとbreakとcontinue~」です。

次のブログ記事は「Embed で テキスト を埋め込むには」です。

最近のコンテンツはインデックスページで見られます。過去に書かれたものはアーカイブのページで見られます。

Creative Commons License
このブログはクリエイティブ・コモンズでライセンスされています。