Archives by date

You are browsing the site archives by date.

ニコニコ動画ダウンロード用のクラスを作った。

今更ながら必要だったので作成。

以下のページを参考にさせて頂きました。
http://istb16.wordpress.com/2011/09/11/niconico_download_for_php/

goto構文使ってるのでphp5.3以上じゃないと動作しません。

<?php 
/**
 * ニコニコ動画保存用クラス
 * 
 * $nico = new niconicoMovieDownloader();
 * $nico->email = 'ログインメールアドレス';
 * $nico->password = 'ログインパスワード';
 * $nico->saveDir = '動画保存用ディレクトリ'; // 末端DS無し
 * $nico->tmpDir = '作業用ディレクトリ'; // 末端DS無し
 * $res = $nico->download('sm*******');
 * 
 * @author Owner
 *
 */
class niconicoMovieDownloader {
	
	// ログイン用メールアドレス
	public $email = '';
	
	// ログインパスワード
	public $password = '';
	
	// 動画保存ディレクトリ(末端スラッシュ無し)
	public $saveDir = '';
	
	// 一時作業ディレクトリ(末端スラッシュ無し)
	public $tmpDir = '';
	
	// 動画ID(sm****)
	protected $mid = '';
	
	// 送信ヘッダーの入れ物
	protected $headers = array();
	
	// エラーメッセージの入れ物
	protected $errors = array();
	 
	// input要素の名前設定(メールアドレス欄)
	protected $formNameEmail = 'mail_tel';
	
	// input要素の名前設定(パスワード欄)
	protected $formNamePassword = 'password';
	
	// input要素の名前設定(移動先URL欄)
	protected $formNameNextUrl = 'next_url';
	
	// ニコニコ動画ログインポストURL。間違えると動画ページを取得できないので注意。
	protected $loginUrl = 'https://secure.nicovideo.jp/secure/login?site=niconico';
	
	// 動画ページのURL設定
	protected $watchUrl = 'http://www.nicovideo.jp/watch';
	
	// ダウンロードURLを割り出すAPIの設定
	protected $apiUrl = 'http://flapi.nicovideo.jp/api/getflv';
	
	/**
	 * インスタンス生成時、送信ヘッダーを設定
	 */
	public function __construct() {
		$this->headers[] = 'Connection: keep-alive';
		$this->headers[] = 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8';
		$this->headers[] = 'User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.95 Safari/537.36';
		$this->headers[] = 'Referer: http://www.nicovideo.jp/';
		$this->headers[] = 'Accept-Language: ja,en-US;q=0.8,en;q=0.6';
	}
	
	/**
	 * ダウンロードの実行
	 * @param string $mid
	 * @return multitype:|boolean
	 */
	public function download($mid = null) {
		$this->mid = $mid;
		$this->checkParams();
		if(!empty($this->errors)) return $this->errors;
		// cookieを持ちまわすためのファイルパスを指定
		$tmpCookiePath = $this->tmpDir . DIRECTORY_SEPARATOR . '_cookie';
		$tmpWorkPath = $this->tmpDir . DIRECTORY_SEPARATOR . md5(microtime());
		$headerFilePath = $this->tmpDir . DIRECTORY_SEPARATOR . 'h_' . $this->mid;
		self::tp($tmpCookiePath);
		self::tp($tmpWorkPath);
		self::tp($headerFilePath);
		
		$params = array(
				$this->formNameEmail => $this->email,
				$this->formNamePassword => $this->password,
				$this->formNameNextUrl => $this->watchUrl . DIRECTORY_SEPARATOR . $this->mid
		);
		$ch = curl_init();
		//curl_setopt($ch, CURLOPT_HTTPHEADER, $this->headers);
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
		curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
		curl_setopt($ch, CURLOPT_COOKIEJAR, $tmpCookiePath);
		//curl_setopt($ch, CURLOPT_COOKIEFILE, $tmpCookiePath);
		curl_setopt($ch, CURLOPT_URL, $this->loginUrl);
		curl_setopt($ch, CURLOPT_POST, true);
		curl_setopt($ch, CURLOPT_POSTFIELDS, $params);
		
		$output = curl_exec($ch);
		
		$params = array('v' => $this->mid);
		curl_setopt($ch, CURLOPT_URL, $this->apiUrl);
		curl_setopt($ch, CURLOPT_HTTPGET, true);
		curl_setopt($ch, CURLOPT_POSTFIELDS, $params);
		
		$output = curl_exec($ch);
		
		// DLリンクの解析
		preg_match("'url=(.*?)&link'", urldecode($output), $m);
		if(empty($m)) goto exception;
		
		curl_setopt($ch, CURLOPT_POSTFIELDS, array());
		curl_setopt($ch, CURLOPT_URL, $m[1]);
				
		// 動画DLセクション
		$execResult = false;
		$hdFp = fopen($headerFilePath, 'w');
		if($hdFp) {
			//プロセス実行中でも書き込んでくれるWRITEHEADERを利用
			curl_setopt($ch, CURLOPT_WRITEHEADER, $hdFp);
			$twFp = fopen($tmpWorkPath, 'wb');
			if($twFp) {
				curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
				curl_setopt($ch, CURLOPT_FILE, $twFp);
				$execResult = curl_exec($ch);
				curl_close($ch);
				fclose($twFp);
			}
			fclose($hdFp);
		}
		if(!$execResult) goto exception;
		
		// headerFileの解析(ファイルタイプ、拡張子を取得)
		$contentType = self::parseHeaderFile($headerFilePath);
		$ext = self::getFileExtension($contentType);
		
		// 実ファイルのパスを指定(お名前指定)
		$filePath = $this->saveDir . DIRECTORY_SEPARATOR . $this->mid . $ext;
		// ファイル移動
		rename($tmpWorkPath, $filePath);
		
		// 作業ファイル削除
		self::u($tmpCookiePath);
		self::u($tmpWorkPath);
		self::u($headerFilePath);
		return true;
		
		if(false) {
			exception:
			self::u($tmpCookiePath);
			self::u($tmpWorkPath);
			self::u($headerFilePath);
			curl_close($ch);
			return false;
		}	
	}
	
	/**
	 * ファイルの存在確認と作成、及び権限の付与
	 * @param unknown $path
	 */
	protected function tp($path) {
		if(!file_exists($path)) touch($path);
		chmod($path, 0777);
	}
	
	/**
	 * ファイルの存在確認と削除
	 * @param unknown $path
	 */
	protected function u($path) {
		if(file_exists($path)) unlink($path);
	}

	/**
	 * パラメーターチェック
	 */
	protected function checkParams() {
		if(!self::validateEmail($this->email)) array_push($this->errors, 'please check your email address.');
		if(empty($this->password)) array_push($this->errors, 'password is required but not set.');
		if(!is_dir($this->saveDir)) array_push($this->errors, 'saveDir must be a directory.');
		if(fileperms($this->saveDir) != '16895') array_push($this->errors, 'saveDir required to be permission 777.');
		if(!is_dir($this->tmpDir)) array_push($this->errors, 'tmpDir must be a directory.');
		if(fileperms($this->tmpDir) != '16895') array_push($this->errors, 'tmpDir required to be permission 777.');
		if(empty($this->mid)) array_push($this->errors, 'mid is required but not set.');
		if(!self::validateMid($this->mid)) array_push($this->errors, 'ID starting from nm is not allowed (sm****** only).');
	}
	
	/**
	 * メールアドレスのバリデーション
	 * @param unknown $check
	 * @return boolean
	 */
	protected function validateEmail($check) {
		$hostname = '(?:[_a-z0-9][-_a-z0-9]*\.)*(?:[a-z0-9][-a-z0-9]{0,62})\.(?:(?:[a-z]{2}\.)?[a-z]{2,})';
		$regex = '/^[a-z0-9!#$%&\'*+\/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&\'*+\/=?^_`{|}~-]+)*@' . $hostname . '$/i';
		if (preg_match($regex, $check)) return true;
		return false;
	}
	
	/**
	 * 動画IDの正規バリデーション
	 * @param unknown $check
	 * @return boolean
	 */
	protected function validateMid($check) {
		$regex = '/^(sm)[0-9]+$/';
		if (preg_match($regex, $check)) return true;
		return false;
	}
	
	/**
	 * ヘッダーファイルの情報を解析
	 * @param unknown $path
	 * @return Ambigous <NULL, string>
	 */
	protected function parseHeaderFile($path) {
		$hd = file_get_contents($path);
		$ct = null;
		$k = 'Content-Type:';
		$st = stripos($hd, $k);
		if($st !== false) {
			$st += strlen($k);
			$ed = strpos($hd, "\n", $st);
			if($ed === false) $ed = strlen($hd);
			$l = $ed - $st;
			$ct = strtolower(trim(substr($hd, $st, $l)));
		}
		return $ct;	
	}
	
	/**
	 * コンテントタイプより動画ファイルの拡張子を分析し返却
	 * @param unknown $ct
	 * @return string
	 */
	protected function getFileExtension($ct) {
		$e = '';
		switch($ct) {
			case 'video/3gpp':
				$e = '.3gp';
				return $e;
			case 'video/mp4':
				$e = '.mp4';
				return $e;
			case 'video/x-flv':
			default:
				$e = '.flv';
				return $e;
		}
	}
}

コメントアウトにもあるけど、使い方は以下の通り。

$nico = new niconicoMovieDownloader();
$nico->email = 'ログインメールアドレス';
$nico->password = 'ログインパスワード';
$nico->saveDir = '動画保存用ディレクトリ'; // 末端DS無し
$nico->tmpDir = '作業用ディレクトリ'; // 末端DS無し
$res = $nico->download('sm*******');

気が向いたら動画再生ページのパーサー実装しようかな。

ChromeでHTTPヘッダを確認する。

メモ。

1, Chromeのアドレスバーに以下を入力
「chrome://net-internals/」

2, 出てきた画面上部のプルダウンメニューから「Events」を選択

3, 該当のリクエストをクリックし、右側の詳細ログからHTTPヘッダ情報を探し出す。

他にも色々と情報が除ける模様。

PHPで行うファイル作成と書き込みの厳密な手順。

調べたのでメモ。

// 存在確認
file_exists()
↓
// ファイル作成
touch()
↓
// 権限変更
chmod()
↓
// ファイルオープン
fopen()
↓
// ファイルロック
flock()
↓
// 書き込み
fwrite()
↓
// ロック解除
flock()
↓
// ファイルクローズ
fclose()

いきなりfopen(‘/path’, ‘w’)とかやってもファイルが存在しなければ新規作成してくれちゃうけど、一応上記が厳密?な流れらしい。
ロックとアンロックは仲裁更新が起こりうる場面であればしたほうがいいけど、そうでなければしなくてもいい気はする。(適当

PHPのcURL自分用まとめ。

自分用メモ。

// postしたい場合は値を設定
$params = array(
	'id' => '',
	'password' => '',
);
// 初期化
$ch = curl_init();
// curl_exec() の返り値を 文字列で返す(通常はデータを直接出力)
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// Location: ヘッダの内容を再帰的に辿る
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
// サーバーから受け取ったクッキーを保存するファイル名
// 既存ファイルでないとエラーになる可能性があるので注意
curl_setopt($ch, CURLOPT_COOKIEJAR, '/cookie');
// サーバーに投げるクッキーのファイル名
curl_setopt($ch, CURLOPT_COOKIEFILE, '/cookie');
// リクエストを行うURL
curl_setopt($ch, CURLOPT_URL, '');
// HTTP POSTを行うか否か
curl_setopt($ch, CURLOPT_POST, true);
// POSTするパラメーター
curl_setopt($ch, CURLOPT_POSTFIELDS, $params);
// 実行
$output = curl_exec($ch);
// リクエストを閉じて終了
// 返り値は無い
curl_close($ch);

COOKIEJAR、COOKIEFILEのパスはどこでもいいが、名前が「cookie」でないと正常に動いてくれない模様
↑そんなこと無かった。「_cookie」で動作確認済み。
基本的なリクエストは上記のoptionで事足りるはず。
その他使用頻度が高そうなoptionは以下。

// HTTPのリクエスト形式をGETに戻す
curl_setopt($ch, CURLOPT_HTTPGET, true);
// リクエスト結果の詳細情報を出力するか否か
curl_setopt($ch, CURLOPT_VERBOSE, true);
// 上記VERBOSEの結果を書き込むファイルパス
// STDERRはプロセスが完了した後でないとファイル出力しないので注意
curl_setopt($ch, CURLOPT_STDERR, '/path/to/vb');
// WRITEHEADERはプロセス実行中でもファイルに出力してくれる
curl_setopt($ch, CURLOPT_WRITEHEADER, '/path/to/hd');

適宜追記予定。