こけめも

メモみたいなもの

(17/01/06追記)twitterで特定ユーザの画像を全部ダウンロードする(動画追加)

(17/01/06追記)

使っている内に、画像取得失敗するツイートがあることがわかりました。
どうも原因はこれのようです。
absg.hatenablog.com




var params = {
	screen_name : '@SCREEN_NAME',
	count : 200,
	max_id : maxId,
	exclude_replies : false,
	trim_user : true,
	include_rts : true
};



var params = {
	screen_name : '@SCREEN_NAME',
	count : 200,
	max_id : maxId,
	exclude_replies : false,
	trim_user : true,
	include_rts : true
        tweet_mode : "extended"
};


(追記以上)


今回はid:pppurple様のこの記事に対するリスペクト記事です。
pppurple.hatenablog.com



最初はオレオレ仕様のTwitterスクリプトを複数画像投稿対応しようと思ったんですが、ごちゃごちゃしてきたんで他人様に解決求めてしまいました。
これを実行するとともに、動画ダウンロード機能も追加したいと思います。



Node.js環境

上記記事のスクリプトはシンプルでいながら64bit整数演算のことなども考慮した素晴らしいものですが、Node.jsのスクリプトとして書かれているのでまずはその環境を整える必要あります。


当方Macなのでここを参考に。
qiita.com


バージョンがあるコマンドは、公式見ながら適宜読み替えました。
ただ、.bash_profileの編集はしなくてもパス通ってました。

スクリプト実行

Node.jsの導入と同にnpmも組み込まれてるので、これで各種ライブラリ追加できます。
そしたらローカルに作成したdlTwitterUserImages.jsをターミナルから実行してやれば、カレントディレクトリにファイルダウンロードされます。

動画について

Twitterでいつの間にか動画投稿できるようになってたんですね…
動画のURLも画像と同じようにextended_entitiesから取得可能です。


参考にさせていただいたのはこちら。
qiita.com


※以下、引用はTwitterの新機能である動画投稿のツイートからの動画URLをJSONから抽出し、動画をじゃぶじゃぶダウンロードする - Qiitaより。

画像か動画かの判定

mediaプロパティにはtypeプロパティがあり、typeがphotoの場合は写真、typeがvideoの場合はビデオ動画情報が格納されていることになります。


そこで、URL取得する前に画像か動画か判定することにします。

for(var j = 0; j < d.extended_entities.media.length; j++) {

	if(d.extended_entities.media[j].type == "photo"){
	//画像の場合
	
	} else if(d.extended_entities.media[j].type == "video"){
	//動画の場合

	}
}
動画のURL抽出

問題はここからです。

video_infoプロパティ詳細

media.type が 'video' の場合video_infoプロパティがmediaプロパティに存在します。
動画URLや動画形式はここから取得する形になります。
各プロパティの意味は以下のようなものです。

video_info プロパティ 意味
duration_millis 動画の長さをミリセカンドで表したもの
aspect_ratio 動画のアスペクト比
variants 投稿された動画を各種フォーマットに変換した情報配列


variantsプロパティの配列の各要素 意味
bitrate 動画のビットレート
content_type 動画フォーマット
url 動画URL


variantsの配列に動画のURLが格納されているのですが、ビットレートの異なる複数の動画URLが含まれています。
そこで、variantsの各要素のうち、content_typeが"video/mp4"で、かつbitrateが一番大きなもののURLを取得することにします。

} else if(d.extended_entities.media[j].type == "video"){
	//動画の場合
	var var_length = d.extended_entities.media[j].video_info.variants.length;
	var video_index = [];
	var video_bitrate = [];
	for (var k = 0; k < var_length; k++) {
		if(d.extended_entities.media[j].video_info.variants[k].content_type == "video/mp4") {
			//content_type がmp4の場合のみ	
			video_index.push(k);
			video_bitrate.push(d.extended_entities.media[j].video_info.variants[k].bitrate);
		}
	}
	//mp4のうち、ビットレートが最大となるvariantsインデックス	取得
	var max_index = video_index[video_bitrate.indexOf(Math.max.apply(null, video_bitrate))];
	console.log(cnt + " : " + d.extended_entities.media[j].video_info.variants[max_index].url);
	urls.push(d.extended_entities.media[j].video_info.variants[max_index].url);
}


この、「content_typeが"video/mp4"で、かつbitrateが一番大きな」variants配列のインデックスを得るための処理が配列2つ使ってカッコ悪いw
いい方法ありませんかね?

ダウンロード

オリジナルのスクリプトのダウンロード処理はこうなっています。

function download(urls) {
	for (var i = 0; i < urls.length; i++) {
		var url = urls[i];
		var pathname = URL.parse(url).pathname;
		var filename = pathname.replace(/[^a-zA-Z0-9\.]+/g, '_');
		request
			.get(url + ":large")
			.on('end', done(url))
			.on('error', function(err) {
				console.log(err);
			})
			.pipe(fs.createWriteStream(filename));
	}
}

改造点は2つ。
1. 「:large」→「:orig」に

画像を保存する時に「:large」で保存していますが、「:orig」だと元画像サイズで取れるらしいのでこちらを採用します。

参考
uasi.hatenablog.com


2.画像と動画のケース分け
と言っても簡単です。動画の場合はURLの後ろに何も付けません。
動画は、拡張子がmp4かどうかで判定しています。


以上を踏まえてダウンロード処理はこんな感じ。

function download(urls) {
	for (var i = 0; i < urls.length; i++) {
		var reg=/(.*)(?:\.([^.]+$))/;
		var url = urls[i];
		var pathname = URL.parse(url).pathname;
		var filename = pathname.replace(/[^a-zA-Z0-9\.]+/g, '_');
		if(filename.match(reg)[2] == "mp4") {
			request
				.get(url)
				.on('end', done(url))
				.on('error', function(err) {
					console.log(err);
				})
				.pipe(fs.createWriteStream(filename));

		}else {
			request
				.get(url + ":orig")
				.on('end', done(url))
				.on('error', function(err) {
					console.log(err);
				})
				.pipe(fs.createWriteStream(filename));
		}
	}
}

まとめ

最終的なコードはこうなります。

var twit    = require('twit');
var request = require('request');
var fs      = require('fs');
var Long    = require("long");
var URL     = require('url');

var T = new twit({
	consumer_key:         'YOUR CONSUMER KEY',
	consumer_secret:      'YOUR CONSUMER SECRET',
	access_token:         'YOUR ACCESS TOKEN',
	access_token_secret:  'YOUR ACCESS SECRET',
	app_only_auth:        true
});

var MAX_LOOP = 16;
var maxId;
var params = {
	screen_name : '@SCREEN_NAME',
	count : 200,
	max_id : maxId,
	exclude_replies : false,
	trim_user : true,
	include_rts : true
};

var urls = [];
var cnt  = 1;
getImage(0, download);

function getImage(loop, callback) {
	if (loop >= MAX_LOOP) {
		callback(urls);
		return;
	}
	T.get('statuses/user_timeline', params, function(err, data, response) {
		if (data.length === 0) {
			callback(urls);
			return;
		}
		var minId;
		for(var i = 0; i < data.length; i++) {
			var d = data[i];
			if (d.extended_entities) {
				for(var j = 0; j < d.extended_entities.media.length; j++) {
					if(d.extended_entities.media[j].type == "photo"){
						//画像の場合
						console.log(cnt + " : " + d.extended_entities.media[j].media_url);
						urls.push(d.extended_entities.media[j].media_url);
					} else if(d.extended_entities.media[j].type == "video"){
						//動画の場合
						var var_length = d.extended_entities.media[j].video_info.variants.length;
						var video_index = [];
						var video_bitrate = [];
						for (var k = 0; k < var_length; k++) {
							if(d.extended_entities.media[j].video_info.variants[k].content_type == "video/mp4") {
								//content_type がmp4の場合のみ	
								video_index.push(k);
								video_bitrate.push(d.extended_entities.media[j].video_info.variants[k].bitrate);
							}
						}
						//mp4のうち、ビットレートが最大となるvariantsインデックス	取得
						var max_index = video_index[video_bitrate.indexOf(Math.max.apply(null, video_bitrate))];
						console.log(cnt + " : " + d.extended_entities.media[j].video_info.variants[max_index].url);
						urls.push(d.extended_entities.media[j].video_info.variants[max_index].url);
					}
				}
			} else {
				console.log(cnt + " : no image");
			}
			minId = d.id_str;
			cnt++;
		}
		var longId = Long.fromString(minId);
		var longIdSub = longId.subtract(1);
		params.max_id = longIdSub.toString();
		getImage(loop + 1, callback);
	});
}

function download(urls) {
	for (var i = 0; i < urls.length; i++) {
		var reg=/(.*)(?:\.([^.]+$))/;
		var url = urls[i];
		var pathname = URL.parse(url).pathname;
		var filename = pathname.replace(/[^a-zA-Z0-9\.]+/g, '_');
		if(filename.match(reg)[2] == "mp4") {
			request
				.get(url)
				.on('end', done(url))
				.on('error', function(err) {
					console.log(err);
				})
				.pipe(fs.createWriteStream(filename));

		}else {
			request
				.get(url + ":orig")
				.on('end', done(url))
				.on('error', function(err) {
					console.log(err);
				})
				.pipe(fs.createWriteStream(filename));
		}
	}
}

function done(url) {
	return function() {
		console.log("downloaded : " + url);
	};
}


もしよかったらご参考までに〜