AdobeAIRでニコニコ動画のマイリスト登録ランキングトップ100をダウンロードするプログラムをつくってみた

Adobe AIRを使って、ニコニコ動画のマイリスト登録ランキングの上位100件を一括ダウンロードするというニコニコ動画にとっては迷惑きわまりないプログラムを組んでみました。

なお、以下のソースコードを使ってあなたが被った被害(アクセス制限の上にアカウント停止された!とかもね)に関しましては、当方は責任を負いかねます。ご了承ください。


まずは外観。
外観

次にロード中の様子。
ロード中

そしてニコニコからアクセス制限をくらった時の様子(エラーメッセージを表示している時の様子)
エラーメッセージ


基本的にはAdobe AIRでニコニコ動画にアクセスして、FLVをダウンロードしてみるを改良しただけです。

ニコニコ動画にログイン→マイリスト登録ランキングのページを取得して動画へのURLを抽出→抽出したURLへ"見てるフリ"しながらAPIにアクセス→"見てるフリ"しながらflv(mp4)をダウンロード

という処理の流れ。


ニコニコのAPIの使い方がだいたいわかった。Adobe AIRの使い方もだいたいわかった。
後はこの技術をどう使うかなんだが・・・アイデアは他人任せだったりする。誰かいいアイデアください。


以下ソースコード。700行近くあります。ここまでくるとただの迷惑ですね・・・。
もちろん前回のソースコードを改良したものです。似たようなプログラムを組む際の参考にどうぞ。
(前回よりは関数ごとに処理が分けれているはず。使いやすくなってるといいんですけどね。)

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

import mx.controls.Alert;
import mx.formatters.DateFormatter;

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

private var myTimer:Timer;
private var date:Date;

private var urlList:Array;

private var isAccessing:Boolean = false;
private var isNomalGet:Boolean = true;
private var isNomalGetting:Boolean = false;
private var isRankingGetting:Boolean = false;

private var indexRanking:int;
private var retryCount:int;

private var isRetring:Boolean = false;

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 initNicoNicoDougaDownloader():void
{
	watchLoader = new URLLoader();
	getLoader = new URLLoader();
	downLoader = new URLLoader();
}

/**
 * 
 * ボタンが押されたときの処理。1ファイルのみに対してダウンロードを行う。
 * NormalGet。
 * 
 */		
public function startButtonClick():void
{
	//ランキング一括ダウンロードが行われていないかどうか確認。
	if(!isRankingGetting){
		//自分がアクセス中かどうか確認
		if(isAccessing){
			//自分がアクセス中だったら中断、終了。
			this.allClose();
			this.label_status.text = "停止";
			this.startButton.label = "開始";
			this.isNomalGetting = false;
			this.isAccessing = false;
		}else{
			//自分がアクセスしてなかったらアクセスを開始。
			
			isNomalGet = true;
			isAccessing = true;
			isNomalGetting = true;
			
			//各種情報を収集する。
			//ムービーのURL。
			mURL = textInput_mUrl.text;
			//ログイン用メールアドレス。
			mailAddress = textInput_mailAddress.text;
			//パスワード
			password = textInput_password.text;
			
			startButton.label = "停止";
			
			//ログイン処理開始
			this.login();
		}
	}
}

/**
 * 
 * ボタンが押されたときの処理。ランキング100位までの動画を一括してダウンロードする。
 * RankingGet。
 * 
 */
public function startRankingGet():void
{
	//1ファイルのみの取得作業が進行していないかどうか確認
	if(!isNomalGetting){
		//既に自分がアクセス中でないかどうか確認
		if(isAccessing){
			//既に自分がアクセス中だった場合はキャンセルし、終了。
			this.allClose();
			this.label_status.text = "停止";
			this.startRankingGetButton.label = "ランキング取得開始";
			this.isRankingGetting = false;
			this.isAccessing = false;
		}else{
			//自分がアクセスしていなかったらアクセスを開始。
			isNomalGet = false
			isAccessing = true;
			isRankingGetting = true;
			
			//ログイン用メールアドレス。
			mailAddress = textInput_mailAddress.text;
			//パスワード
			password = textInput_password.text;
			
			startRankingGetButton.label = "停止";
			
			//ログイン処理開始
			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);
	
}

/**
 * ログイン作業が成功した場合に呼ばれるリスナー
 * @param event
 * 
 */
private function onLoginSuccess(event:HTTPStatusEvent):void 
{
	this.label_status.text = "ログインに成功(" + event.status + ")";
	trace("ログインに成功"+event);
	
	//リトライ中かどうか。
	if(!isRetring){
		//1ファイルの取得(=NomalGet)か、ランキングの取得か。
		if(isNomalGet){
			//見ているフリを開始
			watchLoader = this.watch(mURL, mailAddress, password);
		}else{
			//マイリスト登録ランキングのページを取得
			listLoader = this.watchRankingMylistDailyAll("http://www.nicovideo.jp/ranking/mylist/daily/all");
		}
	}else{
		//リトライ試行中はwatchを直接実行しに行く
		watchLoader = this.watch(urlList[indexRanking][0], this.mailAddress, this.password);
	}
}

/**
 * マイリスト登録ランキング(本日)のページを参照する
 * @param url マイリスト登録ランキング(本日)のURL
 * @return マイリスト登録ランキングへのアクセスを保持するURLLoader
 * 
 */
private function watchRankingMylistDailyAll(url:String):URLLoader
{
	var request:URLRequest = new URLRequest(url);
	var loader:URLLoader = new URLLoader();
	request.method="GET";
	
	loader.addEventListener(Event.COMPLETE, onRankingWatchSuccess);
	loader.addEventListener(IOErrorEvent.IO_ERROR, ioErrorHandler);
	loader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, securityErrorHandler);
	
	loader.load(request);
	
	return loader;
}

/**
 * マイリスト登録ランキングのページを取得したときに呼ばれるリスナー
 * @param evt
 * 
 */
private function onRankingWatchSuccess(evt:Event):void
{
	this.label_status.text = "マイリスト登録ランキング取得";
	trace("マイリスト登録ランキング取得"+evt);
	
	//ランキングの動画URLと動画名のリストを取得する。
	urlList = this.getRankingMylistDailyAll(listLoader);
	
	indexRanking = 0;
	
	//ランキングを途中から読み込む場合の処理
	if(checkBox_isRestart.selected){
		try{
			indexRanking = parseInt(this.textInput_restartIndex.text) - 1;
			if(indexRanking < 0){
				indexRanking = 0;
			}
		}catch(e:Error){
			Alert.show(e.message, "半角英数字を入力してください!");
			this.allClose();
			return;
		}
	}
	
	trace(urlList[indexRanking]);
	
	date = new Date();
	
	//一つ目読み込み開始
	watchLoader = this.watch(urlList[indexRanking][0], this.mailAddress, this.password);
	
}

/**
 * マイリスト登録ランキング(本日)内の動画URLおよびタイトルのリストを取得する。
 * @param loader マイリスト登録ランキングへアクセスしているURLLoader。
 * @param max 最大読み込み件数。特に指定がない場合は100として扱う。
 * @return (動画のURL,動画の名前)を格納する2次元配列。
 * 
 */
private function getRankingMylistDailyAll(loader:URLLoader, max:int=0):Array
{
	
	if(max == 0){
		max = 100;
	}
	var pattern:RegExp = new RegExp("<a class=\"video\" href=\"http://www.nicovideo.jp/watch/.*\">.*</a>","ig");
	var list:Array = loader.data.match(pattern);
	var urlList:Array = new Array(max);
	trace(list);
	
	for(var i:int = 0; max>0 && i<list.length; max--, i++)
	{
		var key:String = list[i].substring(list[i].indexOf("href=\"")+6,list[i].lastIndexOf("\">"));
		var value:String = list[i].substring(list[i].lastIndexOf("\">")+2,list[i].indexOf("</a>"));
		//trace(i + " : " + key + " : " + value);
		urlList[i] = new Array(key, value);
	}
	
	return urlList;
}


/**
 * 見てるフリ処理を実施する。
 * @param mUrl "見てるフリ"をする対象のムービーのURL
 * @param userName ユーザー名。メールアドレス。
 * @param password パスワード。
 * @return "見てるフリ"をしているURLLoader。
 * 
 */
private function watch(mUrl:String, userName:String, password:String):URLLoader
{
	
	videoID = mUrl.substring(mUrl.lastIndexOf("/")+1);
	
	var loader:URLLoader = new URLLoader();	
	
	//そのページを見ているフリをする為のリクエストを準備する。
	var watchURL:URLRequest = new URLRequest(mUrl);
	watchURL.method = "GET";
	
	loader = new URLLoader();
	loader.addEventListener(Event.COMPLETE, accessSuccess);
	loader.addEventListener(IOErrorEvent.IO_ERROR, ioErrorHandler);
	loader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, securityErrorHandler);            
	
	//HTTPリクエストを実行。
	loader.load(watchURL);
	
	return loader;
}


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

/**
 * ビデオのタイトルを取得する。
 * @param loader "見てるフリ"をしているURLLoader
 * @return "見ているフリ"をしているURLLoaderから取得したHTMLの<title>タグにあるページの名前
 * 
 */
private function getVideoName(loader:URLLoader):String
{
	var videoName:String = "";
	
	//<title>タグからページの名前を取得する。これを使って保存するファイル名を決定する。
	var pattern:RegExp = new RegExp("<title>.*</title>","ig"); 
	videoName = loader.data.match(pattern)[0];
	videoName = videoName.substr(7,videoName.length-15);
	
	return videoName;
}

/**
 * FLVのURLを取得する為のAPIへのアクセスを行う
 * @param videoID 英数字2文字+数字 で表されるビデオのID
 * @return APIへのリクエストを行うURLLoader
 * 
 */
private function getAPIResult(videoID:String):URLLoader
{
	var loader:URLLoader = new URLLoader();
	
	//FLVのURLを取得する為にニコニコ動画のAPIにアクセスする
	var getAPIResult:URLRequest;
	getAPIResult = new URLRequest("http://www.nicovideo.jp/api/getflv?v=" + videoID);
	getAPIResult.method = "GET";
	
	loader.addEventListener(Event.COMPLETE, getAPIResultSuccess);
	loader.addEventListener(IOErrorEvent.IO_ERROR, ioErrorHandler);
	loader.addEventListener(ProgressEvent.PROGRESS, progressHandler);
	loader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, securityErrorHandler);   
	
	loader.load(getAPIResult);
	return loader;
}

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

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

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


/**
 * 取得したURLLoader(FLVデータが含まれる)を使ってFLVディスクに書き出す。
 * @param loader
 * @return 
 * 
 */
private function saveFLV(loader:URLLoader):Boolean
{
	try{
		//ファイルを保持する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 = "ファイルを保存中...";
		
		if(isNomalGet){
			// ファイル名に/が含まれると/の前をディレクトリ名として判断してしまうので、/を全角に置き換えている。
			file = file.resolvePath(this.textInput_saveAdress.text + 
				videoTitle.replace(new RegExp("/"), "/")+"."+videoType);
		}else{
			//ランキングを書き出す場合、日付と日時を使ってファイルをまとめる。
			var df:DateFormatter = new DateFormatter();
			df.formatString = "YYYYMMDDJJNNSS";
			var dateString:String = df.format(date);
			file = file.resolvePath(this.textInput_saveAdress.text + dateString+"/"+videoTitle.replace(new RegExp("/"), "/")+ "." + videoType);
		}
		
		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);
		
		//以下、閉じる処理。
	    fileStream.close();
	   	loader.close();
	   	
	   	this.allClose();
	   	
	   	return true;
	}catch(e:Error){
		trace(e);
		
	}
	return false;
	
}


/**
 * 一括してクローズ処理を行う
 * 
 */
private function allClose():void
{
	//プログレスバーを初期化。
	this.progressBar.setProgress(0, 100);
	try{
		this.downLoader.close();
 	}catch(e:Error){
 		trace(e);
 	}
 	try{
    	this.watchLoader.close();
 	}catch(e:Error){
 		trace(e);
 	}
 	try{
    	this.getLoader.close();
 	}catch(e:Error){
 		trace(e);
 	}
 	try{
 		this.listLoader.close();
 	}catch(e:Error){
 		trace(e);
 	}
}

/**
 * 終了するか次の読み込みを開始するかを判断するメソッド。
 * 
 */
private function qOrE():void
{
	//リトライのカウンタを初期化。
	this.retryCount = 0;
	this.isRetring = false;
	//プログレスバーをリセット
	this.progressBar.setProgress(0, 100);
	
	//1ファイルに対する読み込みの場合とそうでない場合
	if(!isNomalGet){
		//ランキングの読み込みの場合
		
		indexRanking++;
		
		//ランキングの読み込みが完了した
		if(indexRanking > urlList.length-1){
			this.startRankingGetButton.label = "ランキング取得開始";
			this.isAccessing = false;
		}else{
			//次のファイルを読みにいく
			trace(urlList[indexRanking]);
			watchLoader = this.watch(urlList[indexRanking][0], this.mailAddress, this.password);
		}
	} else {
		//1ファイルに対する読み込みが終了した。
		this.startButton.label = "開始";
		this.isAccessing = false;
		this.isNomalGetting = false;
		this.isRankingGetting = false;
	}
}

/**
 * リトライを行う。タイマーを使って実装されている。
 * 実際にアクセス制限は比較的長時間にわたって行われる為、リトライの意味はあまり無い。
 * ただ、ネットワークの切断などのアクセス制限以外の原因で接続が途切れた場合は有効かもしれない。
 * @param count
 * 
 */
private function reTry(count:int):void{
	
	this.progressBar.setProgress(0, 100);
	
	myTimer = new Timer(10000, count);
    myTimer.addEventListener("timer", timerHandler);
    myTimer.start();
	this.label_status.text = "[ " + urlList[indexRanking][1] + " ]のリトライの為待機中";
	this.label_status_2.text = count*10 + "秒後に再試行します。";
	
}

/**
 * タイマーハンドラ。
 * @param event
 * 
 */
private function timerHandler(event:TimerEvent):void
{
	
	if(retryCount == myTimer.currentCount){
		trace("ReTry:"+urlList[indexRanking]);
		this.isRetring = false;
		this.login();
	}else{
		this.label_status.text = "[ " + urlList[indexRanking][1] + " ]のリトライの為待機中";
		this.label_status_2.text = (retryCount-myTimer.currentCount)*10 + "秒後に再試行します。";
		trace((retryCount-myTimer.currentCount)*10 + "秒後に再試行");
	}
}


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

/**
 * 
 * @param evt
 * 
 */
private function securityErrorHandler(evt:SecurityErrorEvent):void{
	Alert.show(evt.text, "SecurityError");
	label_status.text = "停止";
	label_status_2.text = "";
	
	this.allClose();
	
	this.qOrE();
}

/**
 * IOエラーが発生したときのハンドラ。ここではニコニコ動画からアクセス制限を食らったときのリトライを行うかどうかの処理も行っている。
 * @param evt
 * 
 */
private function ioErrorHandler(evt:IOErrorEvent):void{
	//Alert.show(evt.text, "IOError");
	label_status.text = "エラー:[" + urlList[indexRanking][1] + "]のダウンロードに失敗。";
	label_status_2.text = (indexRanking+1) + "個目/" + urlList.length + "個中";
	
	if(this.isNomalGet){
		this.logArea.text = this.logArea.text.concat("次のファイルがダウンロードできませんでした。\n" + this.videoTitle + "\nErrorCode:" + evt + "\n\n");
	}else{
		this.logArea.text = this.logArea.text.concat("次のファイルがダウンロードできませんでした。\n" + (indexRanking+1) + "番目 : " + urlList[indexRanking][1] + "\nErrorCode:" + evt + "\n\n");
	}
	
	this.allClose();
	
	if(!this.isRetring){
		if(!this.checkBox_isReTry.selected){
			this.isRetring = false;
			this.qOrE();
			
		}else{
			
			this.isRetring = true;
			this.retryCount++;
			this.reTry(retryCount);
			
		}
	}
}

/**
 * 進捗状況を表示するハンドラ
 * @param evt
 * 
 */
private function progressHandler(evt:ProgressEvent):void{
	
	this.progressBar.setProgress(evt.bytesLoaded, evt.bytesTotal);
	
	if(this.isNomalGet){
		label_status.text = "ダウンロード中";
		label_status_2.text = evt.bytesLoaded + "Byte/" + evt.bytesTotal + "Byte";
	}else{
		label_status.text = "ダウンロード中 [" + urlList[indexRanking][1] + "]";
		label_status_2.text = (indexRanking+1) + "個目/" + urlList.length + "個中 : " + evt.bytesLoaded + "Byte/" + evt.bytesTotal + "Byte";
	}
	
	//trace(evt.bytesLoaded + "/" + evt.bytesTotal);
}