Adobe AIRでニコニコ動画にアクセスして、FLVをダウンロードしてみる

ここ2~3日、Adobe AIRを使ってニコニコ動画にアクセス・ログイン後、FLVをダウンロードするプログラムを作っていました。
名前は安直にNicoNicoDownloader。
とりあえずダウンロードできるようになったのでご報告。

外観はこんな感じ。
ピクチャ 1

ムービーのURLとメールアドレス、パスワードを入力します。
そして"開始"ボタンを押すとダウンロード開始!

Macだと各ユーザーの 書類/NND/temp.flv という形で保存されます。
tempとかやる気無い名前だね!自覚してますよ!


作成した環境は、
MacOS X 10.5.2
Flex Builder 3
です。


まずはニコニコ動画APIについてお勉強。
Perlでニコニコ動画のflvとコメントxmlをダウンロードする」を参照してもらうとわかると思いますが、


ポイント1
http://www.nicovideo.jp/api/getflv?v=ビデオID にアクセス(GET)して、FLVの場所を取得する(実際に帰ってくるのはFLVのURLだけじゃ無いんだけどね。今回はそこしか使わないから。)

ポイント2
ポイント1のAPIをたたくときとポイント1で取得したURLからFLVをダウンロードするときはその動画のページにアクセスしていなければならない


という2つのポイントが有ります。
これを理解して・・・もPerlが読めないのであんまり意味がない。私が読める言語で同じようなものが書かれていないかな?と探したところ、
ニコニコ動画マイリストを一括ダウンロード&iPod用に変換(JAVA)
を発見。
今回はこれのマネをしてActionScriptで書いてみました。

処理は基本的に1本道です。上から順に、
・ログイン処理
・見てるフリ処理開始
APIにアクセス
・FLVのURLを解析・アクセス
・FLVを保存
・ストリームとかを閉じる
というふうに処理が進んでいきます。
詳しい内容は、ソースの方にしつこいほどコメントを入れたのでそちらをどうぞ。

なお、Flexプロジェクトとして作成したものの一部なので、下のソースコードだけでは動きません。念のため。


以下、NicoNicoDownloader.asです。

import flash.events.Event;
import flash.net.URLLoader;
import flash.net.URLLoaderDataFormat;
import flash.net.URLRequest;
import flash.net.URLVariables;

import mx.controls.Alert;

private var mURL:String;
private var mailAddress:String;
private var password:String;
private var videoID:String;
private var watchLoader:URLLoader;
private var getLoader:URLLoader;
private var downLoader:URLLoader;

public var LOGIN_URL:String = "https://secure.nicovideo.jp/secure/login?site=niconico";
public var TOP_PAGE_URL:String = "http://www.nicovideo.jp/";

public function initNicoNicoDownloader():void
{
	
}

/**
 * ボタンが押されたときの処理。処理の開始。
 */		
public function startButtonClick():void
{
	
	//各種情報を収集する。
	//ムービーのURL。
	mURL = textInput_mUrl.text;
	//ログイン用メールアドレス。
	mailAddress = textInput_mailAddress.text;
	//パスワード
	password = textInput_password.text;
	//ムービーのID。ムービーのURLの一番最後についている英単語と数字のアレ。
	videoID = mURL.substring(mURL.lastIndexOf("/")+1);
	
	//ログイン処理開始
	this.login();
}

/**
 * ログイン処理。
 */
private function login():void
{
	//以降のURLRequestが全て認証情報付きで行われるように、デフォルト値としてセット
	URLRequestDefaults.setLoginCredentialsForHost(TOP_PAGE_URL, mailAddress, password);
	
	//ログインURLにアクセス
	var req:URLRequest = new URLRequest(LOGIN_URL);
	//POSTメソッドです
	req.method = "POST";
	
	//メールアドレスとパスワードをURLエンコードしてリクエストに付加
	var variables : URLVariables = new URLVariables ();
	variables.mail = mailAddress;
	variables.password = password;
	req.data = variables;
	
	//ログイン成功時のリスナーを追加してリクエストを実行
	var loader:URLLoader = new URLLoader();
	loader.addEventListener(HTTPStatusEvent.HTTP_RESPONSE_STATUS, onLoginSuccess);
    loader.load(req);
	
}

/**
 * ログイン作業が成功した場合に呼ばれるリスナー
 */
private function onLoginSuccess(event:HTTPStatusEvent):void 
{
	this.label_status.text = "ログインに成功(" + event.status + ")";
	trace("ログインに成功"+event);
	
	//見てるフリ処理を実施する
	this.watch(TOP_PAGE_URL, mailAddress, password);
	
}

/**
 * 見てるフリ処理を実施する。
 */
private function watch(url:String, userName:String, password:String):void
{
	//以降のURLRequestが全て認証情報付きで行われるように、デフォルト値としてセット
	URLRequestDefaults.setLoginCredentialsForHost(TOP_PAGE_URL, userName, password);
	
	//そのページを見ているフリをする為のリクエストを準備する。
	var watchURL:URLRequest = new URLRequest(mURL);
	watchURL.method = "GET";
	
	watchLoader = new URLLoader();
	watchLoader.addEventListener(Event.COMPLETE, accessSuccess);
	watchLoader.addEventListener(IOErrorEvent.IO_ERROR, ioErrorHandler);
	watchLoader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, securityErrorHandler);            
	
	//HTTPリクエストを実行。
	watchLoader.load(watchURL);
	
}

/**
 * 見ているフリ処理で行っているリクエストが完了したら呼ばれる。
 */
private function accessSuccess(evt:Event):void
{
	this.label_status.text = "アクセス成功";
	trace("アクセス成功" + evt);
	
	//FLVのURLを取得する処理を実行。
	this.getAPIResult(videoID);
}

/**
 * FLVのURLを取得する為のAPIへのアクセスを行う
 */
private function getAPIResult(videoID:String):void
{
	//FLVのURLを取得する為にニコニコ動画のAPIにアクセスする
	var getAPIResult:URLRequest;
	getAPIResult = new URLRequest("http://www.nicovideo.jp/api/getflv?v=" + videoID);
	getAPIResult.method = "GET";
	
	getLoader = new URLLoader();
	getLoader.addEventListener(Event.COMPLETE, getAPIResultSuccess);
	getLoader.addEventListener(IOErrorEvent.IO_ERROR, ioErrorHandler);
	getLoader.addEventListener(ProgressEvent.PROGRESS, progressHandler);
	getLoader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, securityErrorHandler);   
	
	getLoader.load(getAPIResult);
}

/**
 * APIからの応答が得られたら呼ばれる
 */
private function getAPIResultSuccess(evt:Event):void
{
	this.label_status.text = "アドレスの取得に成功";
	trace("アドレスの取得に成功" + evt);
	
	//得られた応答を元にvideoを取得
	this.getVideo(getLoader);
}

/**
 * APIから得られたデータを元に動画をダウンロードする
 */
private function getVideo(loader:URLLoader):void
{
	//APIから得られたデータの"&url="にあるURLを探す
	var videoURL:String = new String();
	videoURL = getLoader.data.substring(getLoader.data.indexOf("&url=")+5,getLoader.data.indexOf("&",getLoader.data.indexOf("&url")+1));
	videoURL = unescape(videoURL);
	trace(videoURL);
	
	//探したURLを使ってFLVのダウンロードを行う。
	var getVideo:URLRequest = new URLRequest(videoURL)
	loader = new URLLoader();
	loader.dataFormat=URLLoaderDataFormat.BINARY;
	loader.addEventListener(Event.COMPLETE, loadSuccess);
	loader.addEventListener(ProgressEvent.PROGRESS, progressHandler);
	loader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, securityErrorHandler);
	loader.load(getVideo);
	
}

/**
 * FLVのダウンロードが完了したら呼ばれる
 */
private function loadSuccess(evt:Event):void{
	this.label_status.text = "ダウンロード成功"
	trace("ダウンロード成功" + evt);
	
	//ダウンロードしたFLVを保存する。
	this.saveFLV(downLoader);
}

/**
 * 取得したURLLoader(FLVデータが含まれる)を使ってFLVディスクに書き出す。
 */
private function saveFLV(loader:URLLoader):void
{
	
	//ファイルを保持するFileオブジェクト
	var file:File = File.documentsDirectory;
	
	//ファイルストリーム。ファイルへの入出力に使う
	var fileStream:FileStream;
	
	file.addEventListener(IOErrorEvent.IO_ERROR, ioErrorHandler);
	file.addEventListener(SecurityErrorEvent.SECURITY_ERROR,securityErrorHandler);
	file.addEventListener(ProgressEvent.PROGRESS, progressHandler);
	
	this.label_status.text = "ファイルを保存中...";
	file = file.resolvePath("NND/temp.flv");
	
	fileStream = new FileStream();
	fileStream.addEventListener(ProgressEvent.PROGRESS, progressHandler);
	fileStream.addEventListener(IOErrorEvent.IO_ERROR, ioErrorHandler);
	fileStream.addEventListener(OutputProgressEvent.OUTPUT_PROGRESS, progressHandler);
	
	fileStream.open(file, FileMode.WRITE);
	fileStream.writeBytes(loader.data);
	
	this.label_status.text =  "保存完了";
	
	//以下、閉じる処理。
	fileStream.close();
   	loader.close();
   	
   	this.allClose();
   	
}

/**
 * 一括してクローズ処理を行う
 */
private function allClose():void
{
	this.downLoader.close();
    this.watchLoader.close();
    this.getLoader.close();
}

//以下、エラー処理とか。

private function securityErrorHandler(evt:SecurityErrorEvent):void{
	Alert.show(evt.text, "SecurityError");
	label_status.text = "停止";
}

private function ioErrorHandler(evt:IOErrorEvent):void{
	Alert.show(evt.text, "IOError");
	label_status.text = "停止";
}

//進捗状況を表示するハンドラ
private function progressHandler(evt:ProgressEvent):void{
	label_status.text = "ダウンロード中" + evt.bytesLoaded + "/" + evt.bytesTotal;
	trace(evt.bytesLoaded + "/" + evt.bytesTotal);
}