Inline Thumbnail

Expand to view thumbnails in the row.

// ==UserScript==
// @name Inline Thumbnail
// @namespace inline_thumbnail
// @description Expand to view thumbnails in the row.
// @run-at document-end
// @include http://hootsuite.com/*
// @include https://hootsuite.com/*
// @include http://twitter.com/*
// @include https://twitter.com/*
// @include https://mobile.twitter.com/*
// @include http://www.crowy.net/*
// @include http://twipple.jp/*
// @include https://tweetdeck.twitter.com/*
// @version 1.7.3
// ==/UserScript==

(function() {
function source() { // source code

var VERSION = '1.7.3';
var USE_LOCAL_STORAGE_ENABLE = true;
var MAX_URL_CACHE = 400;

function isSupportXhr2() { return 'withCredentials' in $.ajaxSettings.xhr(); }
function ajax(arg) {
	var done = 0;
	var option = {
		url: arg.url, data: arg.data, dataType: arg.dataType, type: 'GET', cache: true,
		success: function(data, status, xhr) { done++ || arg.success(data, status, xhr); },
		error: function(xhr, status, errorThrown) { done++ || arg.error(xhr, status, errorThrown); },
	};
	if (arg.dataType == 'jsonp') {
		setTimeout(function() { option.error(); }, 15 * 1000);
	}
	$.ajax(option);
}
function loadJQuery() {
	var id = 'dy-load-jq';
	if (document.getElementById(id)) {
		return;
	}
	var script = document.createElement('script');
	script.id = id;
	script.type = 'text/javascript';
	script.src = '//ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js';
	document.body.appendChild(script);
}

function main(EXT_SERV_HOST_URL) { // main logic

var STORE_PREFIX = 'inlineThumbnail.';
var EXPANDED_URL_CACHE_KEY = STORE_PREFIX + 'expandedUrls';
var THUMBNAIL_URL_CACHE_KEY = STORE_PREFIX + 'thumbnailUrls';
var EXT_SERV_ENDS = (function(base) {
	var ret = {
		expandurl: 'expandurl',
		amazon: 'amazon',
		flickr: 'flickr',
		nicovideo: 'nicovideo',
		videosurf: 'videosurf',
		tinami: 'tinami',
		ustream: 'ustream',
		tumblr: 'tumblr',
		rakuten: 'rakuten',
		ogmedia: 'ogmedia',
		myspacevideo: 'myspacevideo',
		pictwitter: 'pictwitter',
		itunes: 'itunes',
		loctouch: 'loctouch',
		i500px: '500px',
		facebook: 'facebook',
		twil: 'proxy/twil'
	};
	$.each(ret, function(key, val) { ret[key] = base + '/' + val; });
	return ret;
})(EXT_SERV_HOST_URL);

var HTTPS_SUPPORT_SITE_REGEX = (function() {
	var fragment = (
		'pbs.twimg.com twitpic.com img.ly yfrog.com p.twipple.jp farm\\d.static.flickr.com miil.me' + ' ' +
		''
	).replace(/\./g,'\\.').replace(/ /g, '|');
	return new RegExp('^https?://(?:' + fragment + ')/');
})();
function modPrt(origUrl) {
	if (HTTPS_SUPPORT_SITE_REGEX.test(origUrl)) {
		return origUrl.replace(/^https?:/, '');	
	}
	return origUrl;
}

var ACCESS_CONTROL_SUPPORT_DOMAIN_REGEX = new RegExp('^' + EXT_SERV_HOST_URL + '/');
var SUPPORT_XHR2_CROSS_DOMAIN = isSupportXhr2();

// Utils
function $E(tagName, attributes) {
	var e = $(document.createElement(tagName));
	attributes && e.attr(attributes);
	return e;
}

function callWebApi(endpoint, sendData, complete) {
	var dataType = SUPPORT_XHR2_CROSS_DOMAIN && ACCESS_CONTROL_SUPPORT_DOMAIN_REGEX.test(endpoint) ? 'json' : 'jsonp';
	ajax({
		url: endpoint, data: sendData, dataType: dataType,
		success: function(data, status, xhr) {
			complete(data, xhr && xhr.status);
		},
		error: function(xhr, status, errorThrown) {
			complete(null, xhr && xhr.status);
		}
	});
}

function baseDecode(letters, snipcode) {
	var ret = 0;
	for (var i = snipcode.length, m = 1; i; i--, m *= letters.length) {
		ret += letters.indexOf(snipcode.charAt(i-1)) * m;
	}
	return ret;
}
function base58decode(snipcode) {
	return baseDecode('123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ', snipcode);
}
function base36decode(snipcode) {
	return baseDecode('0123456789abcdefghijklmnopqrstuvwxyz', snipcode);
}

function unique(array, prop) {
	if (array == null) {
		return null;
	}
	var uniqueSet = {};
	return $.grep(array, function(value) {
		var key = value[prop];
		var result = !(key in uniqueSet);
		if (result) {
			uniqueSet[key] = true;
		}
		return result;
	});
}
function objectToArrayWithKey(obj) {
	var arry = [];
	for (var k in obj) {
		arry.push([k, obj[k]]);
	}
	return arry;
}
// /Utils

// Cache
var Cache = (function() {
	var storage = null;
	if (USE_LOCAL_STORAGE_ENABLE && window.localStorage) {
		storage = localStorage;
	} else if (window.sessionStorage) {
		storage = sessionStorage;
	}

	function constructor(cacheSizeMax, storeKey) {
		this.cacheSizeMax = cacheSizeMax;
		this.storeKey = storeKey;
		this.interval = 20 * 1000;
		this.init();
	}

	function genCache() {
		return { version: VERSION, size: 0 };
	}

	constructor.prototype = {
		init: function() {
			this.modified = false;
			this.caches = [];
			if (storage && this.storeKey) {
				var json = storage[this.storeKey];
				if (json) {
					var cache = JSON.parse(json);
					if (cache.version == VERSION) {
						this.caches.push(cache);
					}
				}
				var self = this;
				setInterval(function() {
					if (self.modified) {
						storage[self.storeKey] = JSON.stringify(self.cache());
						self.modified = false;
					}
				}, this.interval);
			}
			if (this.caches.length == 0) {
				this.caches.push(genCache());
			}
		},
		get: function(key) {
			for(var i = 0; i < this.caches.length; i++) {
				var val = this.caches[i][key];
				if (val) {
					return val;
				}
			}
			return null;
		},
		put: function(key, value) {
			this.shift();
			var cache = this.cache();
			if (!(key in cache)) {
				cache.size += 1;
			}
			cache[key] = value;
			this.modified = true;
		},
		shift: function() {
			if (this.cache().size > this.cacheSizeMax) {
				if (this.caches.unshift(genCache()) > 3) {
					this.caches.pop();
				}
				this.modified = true;
			}
		},
		cache: function() { return this.caches[0]; }
	};
	return constructor;
})();

var ExpandedUrlCache = new Cache(MAX_URL_CACHE, EXPANDED_URL_CACHE_KEY);
ExpandedUrlCache.put_orig = ExpandedUrlCache.put;
ExpandedUrlCache.put = function(key, value) {
	if (key == value) {
		return;
	}
	this.put_orig(key, value);
};
var ThumbnailUrlCache = new Cache(MAX_URL_CACHE, THUMBNAIL_URL_CACHE_KEY);
// /Cache

// WebService
function expandShortUrl(shortUrl, complete) {
	var cached = ExpandedUrlCache.get(shortUrl);
	if (cached) {
		complete(cached);
		return;
	}
	callWebApi(EXT_SERV_ENDS.expandurl, { url: shortUrl }, function(data, statusCode) {
		var longUrl = null;
		if (data) {
			longUrl = data['long-url'];
			ExpandedUrlCache.put(shortUrl, longUrl);
		}
		complete(longUrl);
	});
}

var twitcasting = {
	fetchUserImage: function(userId, complete) {
		callWebApi('http://api.twitcasting.tv/api/userstatus', { user: userId }, function(data) {
			complete(data && data.image, true);
		});
	},
	fetchMovieThumbnail: function(movieId, userId, complete) {
		var self = this;
		callWebApi('http://api.twitcasting.tv/api/moviestatus', { movieid: movieId }, function(movieData) {
			if (movieData) {
				if (movieData.thumbnailsmall) {
					complete(movieData.thumbnailsmall, true);
				} else {
					self.fetchUserImage(userId, complete);
				}
			} else {
				complete();
			}
		});
	},
	fetchLiveThumbnail: function(userId, complete) {
		var completeNoCache = function(thumbUrl) { complete(thumbUrl); }; // ignore cache
		var self = this;
		callWebApi('http://api.twitcasting.tv/api/livestatus', { user: userId }, function(liveData) {
				if (liveData) {
					if (liveData.islive) {
						completeNoCache('http://twitcasting.tv/' + userId + '/thumbstream/liveshot-1');
					} else if (liveData.movieid) {
						self.fetchMovieThumbnail(liveData.movieid, userId, completeNoCache);
					} else {
						self.fetchUserImage(userId, completeNoCache);
					}
				} else {
					completeNoCache();
				}
		});
	}
};

function fetchUstreamThumbnail(subject, uid, complete) {
	callWebApi('http://api.ustream.tv/json/' + subject + '/' + uid + '/getValueOf/imageUrl', {}, function(data) {
		complete(data && data.small, true);
	});
}

function resolvOgmediaDefault(matched, complete) {
	callWebApi(EXT_SERV_ENDS.ogmedia, { url: matched[0] }, function(data) {
		complete(data && data.url, true);
	});
}
// /WebService

// ShortUrlRegexs
var SHORT_URL_REGEX = (function() {
	var fragment = (
		'bit.ly j.mp t.co htn.to ux.nu ustre.am am6.jp ow.ly ht.ly fb.me lnk.ms dai.ly a.r10.to' + ' ' +
		'cot.ag p.tl amzn.to dlvr.it goo.gl moi.st dailybooth.com/u tinyurl.com sgp.cm pbckt.com' + ' ' +
		'fon.gs mysp.ac itun.es is.gd v.gd r.sm3.jp tou.ch po.st post.ly pocket.co'
	).replace(/\./g,'\\.').replace(/ /g, '|');
	return new RegExp('^http://(?:' + fragment + ')/[\\w\\-:\\.]+');
})();
// /ShortUrlRegexs

// ThumbnailResolvers
var THUMB_RESOLVERS = {
	allimg: {
		priority: -1,
		regex: /^https?:\/\/[^?]+?\.(?:jpe?g|png|gif|bmp|JPE?G|PNG|GIF|BMP)(?=\?|$)/,
		resolv: function(matched, complete) {
			complete(matched[0]);
		}
	},
	twitpic: {
		priority: 2,
		regex: /^http:\/\/twitpic\.com\/(\w+)/,
		regexThumb: /^http:\/\/twitpic\.com\/show\/thumb\/\w+/,
		resolv: function(matched, complete) {
			complete('http://twitpic.com/show/thumb/' + matched[1]);
		}
	},
	imgly: {
		regex: /^http:\/\/img\.ly\/(\w+)/,
		regexThumb: /^http:\/\/img\.ly\/show\/thumb\/\w+/,
		resolv: function(matched, complete) {
			complete('http://img.ly/show/thumb/' + matched[1]);
		}
	},
	twitgoo: {
		regex: /^http:\/\/twitgoo\.com\/(\w+)/,
		regexThumb: /^http:\/\/twitgoo\.com\/show\/thumb\/\w+/,
		resolv: function(matched, complete) {
			complete('http://twitgoo.com/show/thumb/' + matched[1]);
		}
	},
	picim: {
		regex: /^http:\/\/pic\.im\/(\w+)/,
		regexThumb: /^http:\/\/pic\.im\/website\/thumbnail\/\w+/,
		resolv: function(matched, complete) {
			complete('http://pic.im/website/thumbnail/' + matched[1]);
		}
	},
	youtube: {
		regex: /^(?:http:\/\/www\.youtube\.com\/watch\/?\?v=([\w\-]+))|(?:http:\/\/youtu\.be\/([\w\-]+))/,
		resolv: function(matched, complete) {
			var id = matched[2] || matched[1];
			complete('http://i.ytimg.com/vi/' + id + '/default.jpg');
		}
	},
	imgur: {
		regex: /^http:\/\/imgur\.com\/(?:r\/pics\/)?(\w+)(?!\/)/,
		resolv: function(matched, complete) {
			complete('http://i.imgur.com/' + matched[1] + 't.jpg');
		}
	},
	owly: {
		regex: /^http:\/\/ow\.ly\/i\/(\w+)/,
		resolv: function(matched, complete) {
			complete('http://static.ow.ly/photos/thumb/' + matched[1] + '.jpg');
		}
	},
	movapic: {
		regex: /^http:\/\/movapic\.com\/pic\/(\w+)/,
		resolv: function(matched, complete) {
			complete('http://image.movapic.com/pic/s_' + matched[1] + '.jpeg');
		}
	},
	hatena: {
		regex: /^http:\/\/f\.hatena\.ne\.jp\/([\w\-]+)\/(\d+)/,
		resolv: function(matched, complete) {
			complete(['http://cdn-ak.f.st-hatena.com/images/fotolife', matched[1].charAt(0), matched[1], matched[2].substr(0, 8), matched[2]].join('/') + '_120.jpg');
		}
	},
	moby: {
		regex: /^http:\/\/moby\.to\/(\w+)/,
		resolv: function(matched, complete) {
			complete(matched[0] + ':square');
		}
	},
	yfrog: {
		regex: /^http:\/\/yfrog\.com\/\w+/,
		resolv: function(matched, complete) {
			complete(matched[0] + ':small');
		}
	},
	plixi: {
		regex: /^http:\/\/plixi\.com\/p\/(\w+)/,
		regexThumb: /^http:\/\/api\.plixi\.com\/api\/TPAPI\.svc\/imagefromurl\?size=thumbnail&url=http:\/\/plixi\.com\/p\/\w+/,
		resolv: function(matched, complete) {
			complete('http://api.plixi.com/api/TPAPI.svc/imagefromurl?size=thumbnail&url=' + matched[0]);
		}
	},
	nicovideo: {
		regex: /^(?:http:\/\/www\.nicovideo\.jp\/watch\/(?:sm|nm)(\d+))|(?:http:\/\/nico\.ms\/(?:sm|nm)(\d+))/,
		regexThumb: /^http:\/\/tn-skr\d?\.smilevideo\.jp\/smile\?i=\d+/,
		resolv: function(matched, complete) {
			var id = matched[2] || matched[1];
			complete('http://tn-skr.smilevideo.jp/smile?i=' + id);
		}
	},
	nicovideoapi: {
		regex: /^(?:http:\/\/(?:www|live)\.nicovideo\.jp\/(?:watch|gate)\/((?:lv)?\d+))|(?:http:\/\/nico\.ms\/((?:lv)?\d+))/,
		resolv: function(matched, complete) {
			var id = matched[2] || matched[1];
			callWebApi(EXT_SERV_ENDS.nicovideo, { id: id }, function(data) {
				complete(data && data.url, true);
			});
		}
	},
	nicoseiga: {
		regex: /^(?:http:\/\/seiga\.nicovideo\.jp\/seiga\/im(\d+))|(?:http:\/\/nico\.ms\/im(\d+))/,
		regexThumb: /^http:\/\/lohas\.nicoseiga\.jp\/thumb\/\d+[qi]/,
		resolv: function(matched, complete) {
			var id = matched[2] || matched[1];
			complete('http://lohas.nicoseiga.jp/thumb/' + id + 'q');
		}
	},
	nicocomu: {
		regex: /^http:\/\/com\.nicovideo\.jp\/community\/co(\d+)/,
		resolv: function(matched, complete) {
			complete('http://icon.nimg.jp/community/co' + matched[1] + '.jpg');
		}
	},
	twipple: {
		regex: /^http:\/\/p\.twipple\.jp\/(\w+)$/,
		regexThumb: /^http:\/\/p\.twpl\.jp\/show\/thumb\/\w+/,
		resolv: function(matched, complete) {
			complete('http://p.twpl.jp/show/thumb/' + matched[1]);
		}
	},
	photozou: {
		regex: /^http:\/\/photozou\.jp\/photo\/show\/\d+\/(\d+)/,
		regexThumb: /^http:\/\/photozou\.jp\/p\/thumb\/\d+/,
		resolv: function(matched, complete) {
			complete('http://photozou.jp/p/thumb/' + matched[1]);
		}
	},
	ustream: {
		regex: /^http:\/\/(?:www\.)?ustream\.tv\/(channel|recorded)\/(?:id\/)?([\w\-%]+)/,
		subjects: { 'channel': 'channel', 'recorded': 'video' },
		resolv: function(matched, complete) {
			fetchUstreamThumbnail(this.subjects[matched[1]], matched[2], complete);
		}
	},
	lockerz: {
		regex: /^http:\/\/lockerz\.com\/s\/\d+/,
		regexThumb: /^http:\/\/api\.plixi\.com\/api\/tpapi\.svc\/imagefromurl\?size=thumbnail&url=http:\/\/lockerz\.com\/s\/\d+/,
		resolv: function(matched, complete) {
			complete('http://api.plixi.com/api/tpapi.svc/imagefromurl?size=thumbnail&url=' + matched[0]);
		}
	},
	jcomi: {
		regex: /^http:\/\/(?:www\.|vw\.)?j-comi\.jp\/(?:book|murasame)\/(?:comic|detail|view)\/\d+/,
		resolv: resolvOgmediaDefault
	},
	picplz: {
		regex: /^http:\/\/picplz\.com\/(?:user\/\w+\/pic\/(\w+)|(\w+))/,
		resolv: function(matched, complete) {
			var param = {};
			if (matched[1]) {
				param.longurl_id = matched[1];
			} else {
				param.shorturl_id = matched[2];
			}
			callWebApi('http://api.picplz.com/api/v2/pic.json', param, function(data) {
				var imgUrl = null;
				if (data && data.result == 'ok') {
					imgUrl = data.value.pics[0].pic_files['100sh'].img_url;
				};
				complete(imgUrl, true);
			});
		}
	},
	kabegami: {
		regex: /^http:\/\/www\.kabegami\.com\/[\w\-]+\/\w+\/show\/id\/(PHOT(\d{10})(\w\w)(\w\w)\w\w)/,
		resolv: function(matched, complete) {
			complete(['http://www.kabegami.com/content_image/phot', matched[2], matched[3], matched[4], matched[1]].join('/') + '_s90.jpg');
		}
	},
	instagram: {
		regex: /^http:\/\/(?:instagr\.am|instagram\.com)\/p\/[\w\-]+/,
		resolv: function(matched, complete) {
			complete(matched[0] + '/media/?size=t');
		}
	},
	mypix: {
		regex: /^http:\/\/www\.mypix\.jp\/app\.php\/picture\/\d+/,
		resolv: function(matched, complete) {
			complete(matched[0] + '/thumbx.jpg');
		}
	},
	fotolog: {
		regex: /^http:\/\/fotolog\.cc\/(\w+)/,
		regexThumb: /^http:\/\/fotolog\.cc\/image\/\w+\/mini/,
		resolv: function(matched, complete) {
			complete('http://fotolog.cc/image/' + matched[1] + '/mini');
		}
	},
	vimeo: {
		regex: /^http:\/\/(?:www\.)?vimeo\.com\/(\d+)/,
		resolv: function(matched, complete) {
			callWebApi('http://vimeo.com/api/v2/video/' + matched[1] + '.json', {}, function(data) {
				complete(data && data[0].thumbnail_small, true);
			});
		}
	},
	dailybooth: {
		regex: /^http:\/\/dailybooth\.com\/[\w\-]{2,}\/(\d+)/,
		resolv: function(matched, complete) {
			callWebApi('http://api.dailybooth.com/v2/pictures/' + matched[1] + '.json', {}, function(data) {
				complete(data && data.urls.small, true);
			});
		}
	},
	twitcasting: {
		regex: /^http:\/\/twitcasting\.tv\/([\w\-]+)\/movie\/(\d+)/,
		resolv: function(matched, complete) {
			var userId = matched[1];
			var movieId = matched[2];
			twitcasting.fetchMovieThumbnail(movieId, userId, complete);
		}
	},
	twitcastinglive: {
		regex: /^http:\/\/twitcasting\.tv\/([\w\-]+)\/?$/,
		resolv: function(matched, complete) {
			var userId = matched[1];
			twitcasting.fetchLiveThumbnail(userId, complete);
		}
	},
	metacafe: {
		regex: /^http:\/\/www\.metacafe\.com\/(?:w|watch)\/([\w\-]+)/,
		resolv: function(matched, complete) {
			complete('http://www.metacafe.com/thumb/' + matched[1] + '.jpg');
		}
	},
	dailymotion: {
		regex: /^http:\/\/(?:www|touch)\.dailymotion\.com\/(?:#\/)?video\/([\w\-]+)/,
		resolv: function(matched, complete) {
			callWebApi('https://api.dailymotion.com/video/' + matched[1], { fields: 'thumbnail_medium_url' }, function(data) {
				complete(data && data.thumbnail_medium_url, true);
			});
		}
	},
	stickamjpprof: {
		regex: /^http:\/\/(?:www\.stickam\.jp\/profile|stick\.am\/p)\/(\w+)/,
		resolv: function(matched, complete) {
			callWebApi('http://api.stickam.jp/api/user/' + matched[1] + '/profile', { mime: 'json' }, function(data) {
				complete(data && data.profile_image, true);
			});
		}
	},
	stickamjpmedia: {
		regex: /^http:\/\/www\.stickam\.jp\/video\/(\d+)/,
		resolv: function(matched, complete) {
			callWebApi('http://api.stickam.jp/api/media/' + matched[1], { mime: 'json' }, function(data) {
				complete(data && data.media && data.media.thumb, true);
			});
		}
	},
	photobucket: {
		regex: /^http:\/\/[is]\d+\.photobucket\.com\/albums\/.+/,
		resolv: function(matched, complete) {
			callWebApi('http://photobucket.com/oembed', { url: matched[0] }, function(data) {
				complete(data && data.thumbnail_url, true);
			});
		}
	},
	pixiv: {
		regex: /^http:\/\/(?:www\.pixiv\.net\/member_illust\.php\/?\?(?:mode=medium&)?illust_id=|p\.tl\/i\/)(\d+)/,
		resolv: function (matched, complete) {
			callWebApi(EXT_SERV_ENDS.ogmedia, { url: 'http://www.pixiv.net/member_illust.php?mode=medium&illust_id=' + matched[1] }, function(data) {
				complete(data && data.url, true);
			});
		}
	},
	flickr: {
		regex: /^http:\/\/(?:www\.flickr\.com\/photos\/[\w@\-]+\/(\d+))|(?:flic\.kr\/p\/(\w+))/,
		resolv: function(matched, complete) {
			var id = matched[2] ? base58decode(matched[2]) : matched[1];
			callWebApi(EXT_SERV_ENDS.flickr, { photo_id: id }, function(data) {
				complete(data && data.url, true);
			});
		}
	},
	videosurf: {
		regex: /^http:\/\/www\.videosurf\.com\/video\/-?(?:[a-zA-Z0-9%]+-)*(\d+)\b/,
		resolv: function(matched, complete) {
			callWebApi(EXT_SERV_ENDS.videosurf, { id: matched[1] }, function(data) {
				complete(data && data.url, true);
			});
		}
	},
	tinami: {
		regex: /^http:\/\/(?:www\.tinami\.com\/view\/(\w+)|tinami\.jp\/(\w+))/,
		resolv: function(matched, complete) {
			var id = matched[2] ? base36decode(matched[2]) : matched[1];
			callWebApi(EXT_SERV_ENDS.tinami, { id: id }, function(data) {
				complete(data && data.url, true);
			});
		}
	},
	akibablog: {
		regex: /^http:\/\/blog\.livedoor\.jp\/geek\/archives\/\d+\.html/,
		resolv: resolvOgmediaDefault
	},
	photobucketmedia: {
		regex: /^http:\/\/media\.photobucket\.com\/.+/,
		resolv: resolvOgmediaDefault
	},
	ustreamsp: {
		regex: /^http:\/\/(?:www\.)?ustream\.tv\/[\w\-%]+(?=\/?$)/,
		resolv: function(matched, complete) {
			callWebApi(EXT_SERV_ENDS.ustream, { url: matched[0] }, function(data) {
				if (data) {
					fetchUstreamThumbnail('channel', data.channelId, complete);
				} else {
					complete();
				}
			});
		}
	},
	tumblr: {
		regex: /^http:\/\/([\w\-]+\.tumblr\.com)\/post\/(\d+)/,
		resolv: function(matched, complete) {
			callWebApi(EXT_SERV_ENDS.tumblr, { base_hostname: matched[1], post_id: matched[2] }, function(data) {
					complete(data && data.url, true);
			});
		}
	},
	tumblrs: {
		regex: /^http:\/\/(?:tumblr\.com|tmblr\.co)\/[\w\-]+/,
		resolv: function(matched, complete) {
			expandShortUrl(matched[0], function(longUrl) {
				var regex = /^http:\/\/((?:\.?[\w\-]+)+)\/post\/(\d+)/;
				if (regex.test(longUrl)) {
					THUMB_RESOLVERS.tumblr.resolv(longUrl.match(regex), complete);
				} else {
					complete();
				}
			});
		}
	},
	rakutenbooks: {
		regex: /^http:\/\/books\.rakuten\.co\.jp\/rb\/[\w%\-]+\-(\d+)\//,
		resolv: function(matched, complete) {
			callWebApi(EXT_SERV_ENDS.rakuten, { type: 'book', id: matched[1] }, function(data) {
				complete(data && data.url, true);
			});
		}
	},
	rakutenitem: {
		regex: /^http:\/\/item\.rakuten\.co\.jp\/([\w\-]+\/[\w\-]+)/,
		resolv: function(matched, complete) {
			callWebApi(EXT_SERV_ENDS.rakuten, { type: 'item', id: matched[1] }, function(data) {
				complete(data && data.url, true);
			});
		}
	},
	twil: {
		regex: /^http:\/\/shlink\.st\/[\w\-]+/,
		resolv: function(matched, complete) {
			callWebApi(EXT_SERV_ENDS.twil, { url: matched[0] }, function(data) {
				complete(data && data.thumbnail_url, true);
			});
		}
	},
	twitrpix: {
		regex: /^http:\/\/twitrpix\.com\/(\w+)/,
		regexThumb: /^http:\/\/img\.twitrpix\.com\/thumb\/\w+/,
		resolv: function(matched, complete) {
			complete('http://img.twitrpix.com/thumb/' + matched[1]);
		}
	},
	pikchur: {
		regex: /^http:\/\/(?:pikchur\.com|pk\.gd)\/(\w+)/,
		resolv: function(matched, complete) {
			complete(' http://img.pikchur.com/pic_' + matched[1] + '_s.jpg');
		}
	},
	twitxr: {
		regex: /^http:\/\/twitxr\.com\/(\w+)\/updates\/(\d+)/,
		regexThumb: /^http:\/\/twitxr\.com\/image\/\d+\/th/,
		resolv: function(matched, complete) {
			complete('http://twitxr.com/image/' + matched[2] + '/th');
		}
	},
	myspacevideo: {
		regex: /^http:\/\/(?:www\.)?myspace\.com\/video\/(?:(vid|rid)|[\w-]+\/[\w-]+)\/(\d+)/,
		resolv: function(matched, complete) {
			var id = (matched[1] || 'vid') + '/' + matched[2];
			callWebApi(EXT_SERV_ENDS.myspacevideo, { id: id }, function(data) {
				complete(data && data.url, true);
			});
		}
	},
	myspacevideovids: {
		regex: /^http:\/\/(?:vids\.|www\.)?myspace\.com\/index\.cfm\?fuseaction=vids\.individual&videoid=(\d+)/,
		resolv: function(matched, complete) {
			var id = 'vid/' + matched[1];
			callWebApi(EXT_SERV_ENDS.myspacevideo, { id: id }, function(data) {
				complete(data && data.url, true);
			});
		}
	},
	twitvideo: {
		regex: /^http:\/\/twitvideo\.jp\/(\w+)/,
		regexThumb: /^http:\/\/twitvideo\.jp\/img\/thumb\/\w+/,
		resolv: function(matched, complete) {
			complete('http://twitvideo.jp/img/thumb/' + matched[1]);
		}
	},
	mycom: {
		regex: /^http:\/\/(?:if\.|s\.)?journal\.mycom\.co\.jp\/\w+\/\d+\/\d+\/\d+\/\w+\//,
		resolv: function(matched, complete) {
			complete(matched[0] + 'index.top.jpg');
		}
	},
	twitvid: {
		regex: /^http:\/\/(?:www\.)?twitvid\.com\/(?!videos)(\w+)/,
		resolv: function(matched, complete) {
			var id = matched[1];
			complete(['http://llphotos.twitvid.com/twitvidthumbnails', id.charAt(0), id.charAt(1), id.charAt(2), id].join('/') + '_med.jpg');
		}
	},
	pckles: {
		regex: /^http:\/\/(?:pckl\.es|pckles\.com)\/\w+\/\w+/,
		resolv: function(matched, complete) {
			complete(matched[0] + '.thumb.jpg');
		}
	},
	itunes: {
		regex: /^http:\/\/(?:c\.)?itunes\.apple\.com\/.*\/id\w+(?:\?i=\d+)?/,
		resolv: function(matched, complete) {
			callWebApi(EXT_SERV_ENDS.itunes, { url: matched[0] }, function(data) {
				complete(data && data.url, true);
			});
		}
	},
	loctouch: {
		regex: /^http:\/\/tou\.ch\/spot\/\d+\/(\w+)\/(\d+)/,
		resolv: function(matched, complete) {
			callWebApi(EXT_SERV_ENDS.loctouch, { type: matched[1], id: matched[2] }, function(data) {
				complete(data && data.url, true);
			});
		}
	},
	miil: {
		regex: /^http:\/\/miil\.me\/p\/(\w+)/,
		resolv: function(matched, complete) {
			complete(matched[0] + '.jpeg?size=150');
		}
	},
	i500px: {
		regex: /^https?:\/\/(?:www\.)?500px\.com\/photo\/(\d+)/,
		resolv: function(matched, complete) {
			callWebApi(EXT_SERV_ENDS.i500px, { id: matched[1] }, function(data) {
				complete(data && data.url, true);
			});
		}
	},
	hulu: {
		regex: /^http:\/\/(?:www\.hulu\.com\/watch|hulu\.com\/w)\/.*/,
		resolv: function(matched, complete) {
			callWebApi('http://www.hulu.com/api/oembed', { url: matched[0] }, function(data) {
				complete(data && data.thumbnail_url, true);
			});
		}
	},
	facebook: {
		regex: /^https?:\/\/www\.facebook\.com\/photo\.php\?fbid=(\w+)/,
		resolv: function(matched, complete) {
			callWebApi(EXT_SERV_ENDS.facebook, { id: matched[1] }, function(data) {
				complete(data && data.url, true);
			});
		}
	},
	immio: {
		regex: /^http:\/\/imm\.io\/\w+/,
		resolv: resolvOgmediaDefault
	},
	comicdash: {
		regex: /^http:\/\/ckworks\.jp\/comicdash\/series\/\d+/,
		resolv: resolvOgmediaDefault
	},
	gyazo: {
		regex: /^http:\/\/gyazo\.com\/\w+/,
		resolv: function(matched, complete) {
			complete(matched[0] + '.png');
		}
	},
	viame: {
		regex: /^http:\/\/via\.me\/-\w+/,
		resolv: resolvOgmediaDefault
	},
	posterous: {
		regex: /^http:\/\/[\w\-]+\.posterous\.com\/[\w\-]+/,
		resolv: resolvOgmediaDefault
	},
	bookmetercmt: {
		regex: /^http:\/\/book\.akahoshitakuya\.com\/cmt\/\w+/,
		resolv: resolvOgmediaDefault
	},
	booklogp: {
		regex: /^http:\/\/p\.booklog\.jp\/book\/\d+/,
		resolv: function(matched, complete) {
			callWebApi(EXT_SERV_ENDS.ogmedia, { url: matched[0] }, function(data) {
				var imgUrl = null;
				if (data && data.url) {
					imgUrl = data.url.replace(/_s\.jpg$/, '_m.jpg');
				}
				complete(imgUrl, true);
			});
		}
	},
	booklogitem: {
		regex: /^http:\/\/booklog\.jp\/item\/3\/\d+/,
		resolv: resolvOgmediaDefault
	},
	mlkshk: {
		regex: /^http:\/\/mlkshk\.com\/\w\/(\w+)/,
		resolv: function(matched, complete) {
			complete('http://mlkshk.com/r/' + matched[1]);
		}
	},
	twitdraw: {
		regex: /^http:\/\/twitdraw\.com\/(\w+)/,
		resolv: function(matched, complete) {
			complete('http://td-images.s3.amazonaws.com/' + matched[1] + '.png');
		}
	},
	tegaki: {
		regex: /^http:\/\/tegaki\.pipa\.jp\/\d+\/(\d+)\.html/,
		resolv: function(matched, complete) {
			complete('http://tegaki.pipa.jp/CGetBlogImgS.jsp?TD=' + matched[1]);
		}
	},
	gizmodo: {
		regex: /^http:\/\/(?:www|us)\.gizmodo\.(?:jp|co\.uk|de|com)\/\d+\/(?:\d+\/)?[\w\.\-]+/,
		resolv: resolvOgmediaDefault
	},
	piapro: {
		regex: /^http:\/\/piapro\.jp\/t\/[\w\-]+/,
		resolv: resolvOgmediaDefault
	},
	tweetvite: {
		regex: /^http:\/\/(?:tweetvite\.com\/event|twvt\.us)\/([\w\-]+)/,
		resolv: function resolvOgmediaDefault(matched, complete) {
			var url = 'http://tweetvite.com/event/' + matched[1];
			callWebApi(EXT_SERV_ENDS.ogmedia, { url: url }, function(data) {
				complete(data && data.url, true);
			});
		}
	},
	twitter: {
		priority: 1,
		regex: /^https?:\/\/twitter\.com\/(?:#!\/)?[\w\-]+\/status\/(\d+)\/photo\/1/,
		resolv: function(matched, complete) {
			callWebApi(EXT_SERV_ENDS.pictwitter, { id: matched[1] }, function(data) {
				complete(data && data.url, true);
			});
		}
	},
	eyeem: {
		regex: /^http:\/\/www\.eyeem\.com\/p\/\d+/,
		resolv: resolvOgmediaDefault
	},
	vine: {
		regex: /^https?:\/\/vine\.co\/v\/\w+/,
		resolv: resolvOgmediaDefault
	}
};
// amazon
(function(resolvers) {
	var fetchAmazonThumbnail = function(sendData, complete) {
		callWebApi(EXT_SERV_ENDS.amazon, sendData, function(data) {
			complete(data && data.url, true);
		});
	};
	var resolvAmazonThumbnailJpDefault = function(matched, complete) {
		fetchAmazonThumbnail({ tld: 'jp', asin: matched[1] }, complete);
	};

	resolvers.amazon = {
		regex: /^http:\/\/(?:www\.)?amazon(?:\.co)?\.(jp|com|ca|cn|de|fr|it|uk)\/(?:(?:(?:gp|dp)\/product(?:-\w+)?|o\/ASIN|exec\/obidos\/ASIN)\/(\w+)|(?:[^\/]+\/)?dp\/(\w+))/,
		resolv: function(matched, complete) {
			fetchAmazonThumbnail({ tld: matched[1], asin: matched[3] || matched[2] }, complete);
		}
	};
	resolvers.bookmeter = {
		regex: /^http:\/\/book\.akahoshitakuya\.com\/b\/(\w+)/,
		resolv: resolvAmazonThumbnailJpDefault
	};
	resolvers.mediamarker = {
		regex: /^http:\/\/mediamarker\.net\/\w\/[\w\-]+\/\?asin=(\w+)/,
		resolv: resolvAmazonThumbnailJpDefault
	};
	resolvers.booklog = {
		regex: /^http:\/\/booklog\.jp\/(?:users\/[\w\-]+\/archives|asin|item)\/1\/(\w+)/,
		resolv: resolvAmazonThumbnailJpDefault
	};
	resolvers.sociallibrary = {
		regex: /^http:\/\/www\.sociallibrary\.jp\/entry\/(\w+)/,
		resolv: resolvAmazonThumbnailJpDefault
	};
})(THUMB_RESOLVERS);
// /amazon

// /ThumbnailResolvers

// Process
var Process = function(args) {
	this.waitCount = 0;
	this.results = [];
	this.split = args.split;
	this.execute = args.execute;
	this.complete = args.complete;
	this.queue = args.queue;
};
Process.prototype = {
	run: function(input) {
		var subInputs = this.split(input);
		var inputLength = subInputs ? subInputs.length : 0;
		this.waitCount = inputLength;
		this.internalComplete();
		for (var i = 0; i < inputLength; i++) {
			this.execute(subInputs[i]);
		}
	},
	emitComplete: function(result) {
		this.waitCount--;
		if (result) {
			this.results.push(result);
		}
		this.internalComplete();
	},
	internalComplete: function() {
		if (this.waitCount == 0) {
			if (this.complete) {
				this.complete(this.results);
			}
			if (this.queue.length != 0) {
				this.queue.shift()();
			}
		}
	}
};
// /Process

// ExpandThumbnailMainProcess
var thumbResolverWithKeyArry = objectToArrayWithKey(THUMB_RESOLVERS);
thumbResolverWithKeyArry.sort(function(a, b) {
	var a_p = 'priority' in a[1] ? a[1].priority : 0;
	var b_p = 'priority' in b[1] ? b[1].priority : 0;
	if (a_p < b_p) return 1;
	if (a_p > b_p) return -1;
	return 0;
});
function resolvThumbnailUrl(existsOfficalThumbnails, contentUrl, complete) {
	var cached = ThumbnailUrlCache.get(contentUrl);
	if (cached) {
		complete(cached);
		return true;
	}
	var match = false;
	$.each(thumbResolverWithKeyArry, function(index, resolverWithKey) {
		var resolver = resolverWithKey[1];
		if (resolver.regex.test(contentUrl)) {
			if (existsOfficalThumbnails && domainEnv.supportedThumbnails && resolverWithKey[0] in domainEnv.supportedThumbnails) {
				return false;
			}
			resolver.resolv(contentUrl.match(resolver.regex), function(thumbnailUrl, cache) {
				if (thumbnailUrl && cache) {
					ThumbnailUrlCache.put(contentUrl, thumbnailUrl);
				}
				complete(thumbnailUrl);
			});
			match = true;
			return false;
		} else if (resolver.regexThumb && resolver.regexThumb.test(contentUrl)) {
			complete(contentUrl.match(resolver.regexThumb)[0]);
			match = true;
			return false;
		}
	});
	return match;
}

function createThumbnailElement(urlEntries) {
	var ul = $E('ul', { 'class': 'ithumb-ul' });
	for (var i = 0; i < urlEntries.length; i++) {
		var urlEntry = urlEntries[i];
		ul.append(
			$E('li', { 'class':'ithumb-li' }).append(
				$E('a', { 'class':'ithumb-a', 'target':'_blank', 'href':urlEntry.url, 'rel':'url' }).append(
					$E('img', { 'src': modPrt(urlEntry.thumbUrl), 'class':'ithumb-img' })
						.error(function() {
							var img = $(this);
							var tryload = img.data('tryload') || 0;
							if (tryload < 2) {
								setTimeout(function() {
									img.data('tryload', tryload + 1).attr('src', img.attr('src'));
								}, 7000);
							} else {
								img.hide();
							}
						})
				)
			)
		);
	}
	var thumb = $E('div', { 'class':'ithumb-container' }).append(ul);
	if (domainEnv.customizeThumbnailElement) {
		domainEnv.customizeThumbnailElement(thumb);
	}
	return thumb;
}

function expandThumbnail(contentElement, existsOfficalThumbnails) {
	var urlStack = [];
	var processQueue = [];

	/* <thumbnailExpandProcess> */
	var urlExtractProcess = new Process({
		queue: processQueue,
		split: function(element) {
			return element.find('a').map(function() {
				var anchor = $(this);
				var url = anchor.attr('href');
				return (url && (/^https?:\/\/(?!twitter\.com\/#!\/)/).test(url)) ? anchor : null;
			});
		},
		execute: function(anchor) {
			var urlEntry = { thumbUrl: null, expandCount: 0 };
			var expandedUrl = null;
			if (domainEnv.getExpandedUrl) {
				expandedUrl = domainEnv.getExpandedUrl(anchor);
			}
			urlEntry.url = expandedUrl || anchor.attr('href');
			this.emitComplete(urlEntry);
		},
		complete: function(urlEntries) {
			urlStack = unique(urlEntries, 'url');
		}
	});

	var thumbnailUrlResolveProcess = new Process({
		queue: processQueue,
		split: function(urlEntries) {
			return urlEntries;
		},
		execute: function (urlEntry) {
			var self = this;
			var match = resolvThumbnailUrl(existsOfficalThumbnails, urlEntry.url, function(thumbUrl) {
				urlEntry.thumbUrl = thumbUrl;
				self.emitComplete();
			});
			if (!match) {
				if (urlEntry.expandCount < 5 && SHORT_URL_REGEX.test(urlEntry.url)) {
					expandShortUrl(urlEntry.url, function(longUrl) {
						if (urlEntry.url != longUrl) {
							urlEntry.expandCount += 1;
							urlEntry.url = longUrl;
							self.execute(urlEntry);
						} else {
							self.emitComplete();
						}
					});
				} else {
					self.emitComplete();
				}
			}
		},
	});
	/* </thumbnailExpandProcess> */

	processQueue.push(function() { thumbnailUrlResolveProcess.run(urlStack); });
	processQueue.push(function() {
		var urlEntries = unique(
												$.grep(urlStack, function(val) { return val.thumbUrl; }),
												'thumbUrl');
		if (urlEntries.length != 0) {
			domainEnv.appendThumbnail(contentElement, createThumbnailElement(urlEntries));
		}
	});

	urlExtractProcess.run(contentElement);
}
// /ExpandThumbnailMainProcess

var CSS_URL_DEFAULT = EXT_SERV_HOST_URL + '/stylesheets/inlinethumbnail/1.1.0/default.css';
var APPEND_THUMBNAIL_DEFAULT = function(contentElement, thumbnailElement) { contentElement.append(thumbnailElement); };
var DOMAIN_ENVS = {
	hootsuite: {
		hostname: 'hootsuite.com',
		select: function(context) {
			return $('._baseTweetText:not([expanded-img])', context);
		},
		appendThumbnail: APPEND_THUMBNAIL_DEFAULT,
		cssUrl: CSS_URL_DEFAULT
	},
	twitter: {
		hostname: 'twitter.com',
		select: function(context) {
			return $('.tweet:not(.simple-tweet) > .content > p:not([expanded-img])', context);
		},
		appendThumbnail: APPEND_THUMBNAIL_DEFAULT,
		getExpandedUrl: function(anchor) { return anchor.data('expanded-url'); },
		cssUrl: EXT_SERV_HOST_URL + '/stylesheets/inlinethumbnail/1.6.2/twittercom.css',
		existsOfficalThumbnails: function(contentElement) {
			return contentElement.nextAll('.cards-media-container').length > 0;
		},
		supportedThumbnails: {
			'twitter' : true
		}
	},
	twitter_mobile: {
		hostname: 'mobile.twitter.com',
		select: function(context) {
			return $('.tweet-text:not([expanded-img])', context);
		},
		appendThumbnail: APPEND_THUMBNAIL_DEFAULT,
		getExpandedUrl: function(anchor) { return anchor.data('url'); },
		cssUrl: CSS_URL_DEFAULT
	},
	crowy: {
		hostname: 'www.crowy.net',
		select: function(context) {
			return $('.message-text:not([expanded-img])', context);
		},
		appendThumbnail: APPEND_THUMBNAIL_DEFAULT,
		cssUrl: CSS_URL_DEFAULT,
		existsOfficalThumbnails: function(contentElement) {
			return contentElement.nextAll('.images').length > 0;
		},
		supportedThumbnails: {
			'twitter' : true,
			'twitpic' : true,
			'youtube' : true,
			'amazon' : true
		}
	},
	twipple: {
		hostname: 'twipple.jp',
		select: function(context) {
			return $('.tweetBox:not([expanded-img])', context.parentNode);
		},
		appendThumbnail: APPEND_THUMBNAIL_DEFAULT,
		cssUrl: CSS_URL_DEFAULT,
		existsOfficalThumbnails: function(contentElement) {
			return contentElement.find('.thumbBoxImage').length > 0;
		},
		supportedThumbnails: {
			'twitter' : true,
			'twitpic' : true,
			'youtube' : true
		}
	},
	tweetdeck: {
		hostname: 'tweetdeck.twitter.com',
		select: function(context) {
			return $('.tweet-body:not([expanded-img])', context.parentNode);
		},
		appendThumbnail: function(contentElement, thumbnailElement) { contentElement.after(thumbnailElement); },
		getExpandedUrl: function(anchor) { return anchor.data('full-url'); },
		cssUrl: CSS_URL_DEFAULT,
		existsOfficalThumbnails: function(contentElement) {
			return contentElement.children('.media-preview').length > 0;
		},
		supportedThumbnails: {
			'twitter' : true,
			'twitpic' : true,
			'youtube' : true
		}
	}
};

var domainEnv = null;
$.each(DOMAIN_ENVS, function() {
	if (this.hostname == location.hostname) {
		if (this.match) {
			if (this.match()) {
				domainEnv = this;
				return false;
			}
		} else {
			domainEnv = this;
			return false;
		}
	}
});

function applyElements(context) {
	domainEnv.select(context).each(function() {
		var contentElement = $(this);
		expandThumbnail(
			contentElement.attr('expanded-img', 'expanded-img'),
			domainEnv.existsOfficalThumbnails && domainEnv.existsOfficalThumbnails(contentElement));
	});
}

$(document.getElementsByTagName('head')[0]).append($E('link', { 'rel':'stylesheet', 'type':'text/css', 'media':'screen', 'href':domainEnv.cssUrl }));

var ApplyQueue = {
	timeoutId: null,
	queue: [],
	apply: function() {
		var targets = this.queue.splice(0, this.queue.length);
		applyElements(targets); 
	},
	push: function(elem) {
		if (this.timeoutId) {
			clearTimeout(this.timeoutId);
			this.timeoutId = null;
		}
		this.queue.push(elem);

		var self = this;
		this.timeoutId = setTimeout(function() {
			self.apply();
		}, 1000);
	}
};

// initial apply
ApplyQueue.push(document);

$(document).bind('DOMNodeInserted', function(e) {
	ApplyQueue.push(e.target);
});

} // /main logic

// load
(function mainloader(tryCount, loaded) {
	if (tryCount < 30 && !(window.jQuery)) {
		setTimeout(function() { mainloader(tryCount + 1, loaded); }, 60);
		return;
	}
	if (!loaded && !(window.jQuery)) {
		loadJQuery();
		setTimeout(function() { mainloader(0, true); }, 60);
		return;
	}
	var hostname = 'thumbnailurlconv.appspot.com';
	var endpoint = '//' + hostname + '/endpoint';
	var dataType = isSupportXhr2() ? 'json' : 'jsonp';
	ajax({ url: endpoint, dataType: dataType,
		success: function(data) { main('//' + (data ? data.hostname : hostname)); },
		error: function() { main('//' + hostname); }
	});
})(0);

} // /source code

var script = document.createElement('script');
script.type = 'text/javascript';
script.innerHTML = '(' + source.toString() + ')();';
document.body.appendChild(script);

})();