ImageBoard Downloader

The original fullsize images downloader for gelbooru, rule34, yande.re, and donmai imageboards

Από την 26/10/2017. Δείτε την τελευταία έκδοση.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey, το Greasemonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Userscripts για να εγκαταστήσετε αυτόν τον κώδικα.

You will need to install an extension such as Tampermonkey to install this script.

Θα χρειαστεί να εγκαταστήσετε μια επέκταση διαχείρισης κώδικα χρήστη για να εγκαταστήσετε αυτόν τον κώδικα.

(Έχω ήδη έναν διαχειριστή κώδικα χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Έχω ήδη έναν διαχειριστή στυλ χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

// ==UserScript==
// @name        ImageBoard Downloader
// @description The original fullsize images downloader for gelbooru, rule34, yande.re, and donmai imageboards
// @namespace   https://greasyfork.org/users/155308
// @include     https://gelbooru.com*
// @include     https://rule34.xxx*
// @include     https://yande.re*
// @include     *://*.donmai.us*
// @version     0.0.7
// @grant       GM_xmlhttpRequest
// ==/UserScript==

/*
 0.0.7
	+ *.donmai.us downloader
		- safebooru.donmai.us
		- danbooru.donmai.us
		- sonohara.donmai.us
		- hijiribe.donmai.us
 0.0.6
	+ yande.re downloader
	+ download jpeg image on yande.re [false]
 0.0.5
	+ rule34 downloader
	+ add the imageboard name to the image name [true]
 0.0.3
	+ gelbooru downloader
	+ maximum tags in the image name [10]
	+ tags delimeter in the image name ['-']
*/
if( window.self !== window.top )
	return;
var RANDOM = '1681238';//Math.floor(Math.random()*1e6 + 1e6);
console.log('start..');
(function(){
	function blank(){}
	var clog = console.log;
	clog = blank;
	var userOptions = initOptions();
	var methodsObject = initMethodsObject();
	userOptions.set({
		'maxTagsInName': 10,
		'tagsDelim': '-',
		'addImgBrdName': true,// add the imageboard name to the image name
		'downloadJPEG': false,// download jpeg image on yande.re when exists
	});
	var imageBoard = initImageBoard();
	newCssClasses();
	
	//------------------------------------------------------------------------------------//
	//------------------------------------- SITE LIST ------------------------------------//
	function initSiteList()
	{
		var retVal = {
			get current(){return this.currentObj.val;},
			get gelbooru(){return this.data.gelbooru.val;},
			get rule34(){return this.data.rule34.val;},
			get yandere(){return this.data['yande.re'].val;},
			data: {
				'gelbooru': {
					val: null,
					regexp: /gelbooru/,
					get needXHR(){return this.val.needXHR;},
					style: {
						color: '#fff',
						width: '180px',
						background: '#0773fb',
						backgroundHover: '#fbb307',
						colorHover: '#fff',
					},
					init: function(){
						this.val = this.val || initGelbooruObject();
					},
					name: 'gelbooru',
				},
				'rule34': {
					val: null,
					regexp: /rule34/,
					get needXHR(){return this.val.needXHR;},
					style: {
						color: '#fff',
						width: '180px',
						background: '#84AE83',
						backgroundHover: '#A4CEA3',
						colorHover: '#fff',
					},
					init: function(){
						this.val = this.val || initRule34Object();
					},
					name: 'rule34',
				},
				'yande.re': {
					val: null,
					regexp: /yande\.re/,
					get needXHR(){return this.val.needXHR;},
					style: {
						color: '#ee8887',
						width: '180px',
						background: '#222',
						backgroundHover: '#444',
						colorHover: '#ee8887',
					},
					init: function(){
						this.val = this.val || initYandereObject();
					},
					name: 'yande.re',
				},
				'donmai': {
					val: null,
					regexp: /donmai/,
					get needXHR(){return this.val.needXHR;},
					style: {
						color: '#0073ff',
						width: '180px',
						background: '#f5f5ff',
						backgroundHover: '#f5f5ff',
						colorHover: '#80b9ff',
					},
					init: function(){
						this.val = this.val || initDonmaiObject();// TODO
					},
					name: 'donmai',
				},
			},
			get: function( type, prop1, prop2 ){
				var obj = (type === undefined || type === null) ? this.currentObj : this.data[type];
				return nodeWalk.call( obj, prop1, prop2 );
			},
			style: function(type){
				return this.get( type, 'style' );
			},
			val: function(type){
				return this.get( type, 'val' );
			},
			needXHR: function(type){
				return this.get( type, 'needXHR' );
			},
			init: function(type){
				if( type === undefined || type === null )
					this.initCurrent();
				else if( this.data[type] )
					this.data[type].init();
			},
			getSiteType: function(url){
				url = url || window.location.href;
				for( var key in this.data )
				{
					if( this.data[key].regexp.test(url) )
						return key;
				}
				console.error("no site object found for this host");
				return null;
			},
			initCurrent: function(){
				if( !this.currentObj )
				{
					var type = this.getSiteType();
					if( !type )
						return;
					this.currentObj = this.data[type];
				}
				this.currentObj.init();
			},
		};
		retVal.init();
		clog("siteList.current: ", retVal.current);
		return retVal;
	}
	//------------------------------------- SITE LIST ------------------------------------//
	//------------------------------------------------------------------------------------//
	//------------------------------------ IMAGE BOARD -----------------------------------//
	function initImageBoard( d )
	{
		var imgBrdCl = initImageBoardClasses(d),
			imgBrdDt = initImageBoardDataset(d),
			siteList = initSiteList(),
			imgBrdDw = initImageBoardDownloader(d),
			imgBrdId = 'image-board-div-' + RANDOM;
		var retVal = {
			get siteList(){return siteList;},
			get imgBrdCl(){return imgBrdCl;},
			get imgBrdDt(){return imgBrdDt;},
			get imgBrdId(){return imgBrdId;},
			get imgBrdDw(){return imgBrdDw;},
			get images(){return this.data.images;},
			get downloader(){return this.data.downloader;},
			data: {
				'images': {
					list: null,
					init: function( doc, type ){
						clog("init");
						siteList.init(type);
						imgBrdDt.init(doc);
						imgBrdCl.init(doc);
						this.list = this.list || [];
						this.doc = doc || document;
						var siteObj = siteList.val(type),
							isPost = siteObj.isPost(),
							imgD;
						function addNewImage( list, img, isPost )
						{
							list.push({});
							var imgD = last(list);
							imgD.state = 'empty';
							imgD.index = list.length - 1;
							imgD.type = siteObj.name;
							if( isPost )
							{
								imgD.postId = siteObj.getPostId();
								imgD.postUrl = window.location.href;
								siteObj.setImageDataDoc(imgD);
							}else
								siteObj.setImageDataThumb( imgD, img );
							imgBrdDt.val( img, 'index', imgD.index);
							imgBrdCl.addClass( img, 'counted' );
							if( imgD.state === 'ready' )
							{
								siteObj.createDiv( imgBrdId, this.doc);
								imgBrdDw.init(imgBrdId, this.doc);
								setReadyImage( imgD, imgBrdCl, imgBrdDt, imgBrdDw );
							}
							return imgD;
						}
						if( isPost )
						{
							var img = siteObj.getPostImage();
							if( img && !imgBrdCl.hasClass( img, 'counted') )
								addNewImage( this.list, img, true );
						}
						var thumbs = this.doc.querySelectorAll('img.preview');
						if( thumbs && thumbs.length === 0 )
							thumbs = this.doc.querySelectorAll('img[itemprop="thumbnailUrl"]');// donmai
						clog("thumbs.length: ", thumbs.length);
						for( var i = 0, len = thumbs.length, thumb; i < len; ++i )
						{
							thumb = thumbs[i];
							if( imgBrdCl.hasClass( thumb, 'counted' ) )
								continue;
							addNewImage( this.list, thumb );
						}
					},
					getEmpty: function(){
						var empty = [];
						for( var i = 0; i < this.list.length; ++i )
						{
							if( this.list[i].state === 'empty' )
								empty.push(i);
						}
						return empty;
					},
					fix: function()
					{
						var empty = this.getEmpty();
						for( var i = 0, idx, imgD; i < empty.length; ++i )
						{
							idx = empty[i];
							imgD = this.list[idx];
							imgD.state = 'busy';
							this.getImageData(imgD);
						}
					},
					getImageData: function(imgD)
					{
						if( siteList.needXHR(imgD.type) )
						{
							GM_xmlhttpRequest({
								url: imgD.postUrl,
								method: 'GET',
								context: {
									'index': imgD.index,
								},
								onload: xhrImageData,
							});
						}else{
							console.log("TODO =D");
							var siteObj = siteList.val(imgD.type);
							//siteObj.setImageDataFull(imgD);// yande.re, donmai
						}
					},
				},
				'downloader': {
					init: function(doc, type){
						clog("downloader init..");
						siteList.init(type);
						var siteObj = siteList.val(type);
						siteObj.createDiv( imgBrdId, doc);
						imgBrdDw.init(imgBrdId, doc);
					},
					isActive: function(){
						return imgBrdDw && imgBrdDw.isActive() || false;
					},
					activate: function(doc){
						clog("activate");
						doc = doc || document;
						imgBrdCl.init(doc);
						var thumbs = imgBrdCl.queryAll('counted');
						for( var i = 0, len = thumbs.length, thumb, a; i < len; ++i )
						{
							thumb = thumbs[i];
							a = thumb.parentNode;
							if( !imgBrdCl.hasClass(thumb, 'ready' ) )
								continue;
							else if( !imgBrdCl.hasClass( a, 'downloadAttach' ) )
							{
								a.addEventListener('click', handleDownloadEvent, false);
								imgBrdCl.addClass( a, 'downloadAttach' );
							}
							imgBrdCl.addClass( a, 'downloadActive' );
						}
						imgBrdDw.downloadOn();
					},
					deactivate: function(doc){
						clog("deactivate");
						doc = doc || document;
						imgBrdCl.init(doc);
						var activ = imgBrdCl.queryAll('downloadActive');
						clog("active.length: ", activ.length);
						for( var i = 0, len = activ.length; i < len; ++i )
							imgBrdCl.removeClass( activ[i], 'downloadActive' );
						imgBrdDw.downloadOff();
					},
					downloadAll: function(){
						imgBrdDw.downloadAll.click();// =)
					},
				},
			},
			init: function(doc){
				for( var key in this.data )
					this.data[key].init(doc);
			},
			fix: function(){
				this.data.images.fix();
			},
		};
		retVal.init(d);
		retVal.fix();
		return retVal;
	}
	//------------------------------------ IMAGE BOARD -----------------------------------//
	//------------------------------------------------------------------------------------//
	//----------------------------------- XRH IMAGE DATA ---------------------------------//
	function xhrImageData(xhr)
	{
		var imgD = imageBoard.images.list[xhr.context.index];
		if( xhr.status !== 200 )
		{
			console.error("xhr.status: ", xhr.status, xhr.statusText );
			console.error("index: ", xhr.context ? xhr.context.index : null);
			console.error("postUrl: ", this.url );
			if( imgD.state !== 'ready' )
				imgD.state = 'empty';
			return;
		}
		if( !imgD || imgD.state === 'ready' )
		{
			console.error("invalid context: ", imgD);
			return;
		}
		var siteObj = imageBoard.siteList.val(imgD.type);
		if( !siteObj )
		{
			console.error("invalid site type: ", imgD.type);
			return;
		}
		var doc = document.implementation.createHTMLDocument("");
		doc.documentElement.innerHTML = xhr.response;
		siteObj.setImageDataDoc(imgD, doc);
		clog("xhrImageData[" + imgD.index + "].state : " + imgD.state);
		if( imgD.state === 'ready' )
		{
			setReadyImage( imgD );
		}
	}
	function setReadyImage( imgD, imgBrdCl, imgBrdDt, imgBrdDw )
	{
		if( (!imgBrdCl || !imgBrdDt || !imgBrdDw) && imageBoard )
		{
			imgBrdCl = imageBoard.imgBrdCl;
			imgBrdDt = imageBoard.imgBrdDt;
			imgBrdDw = imageBoard.imgBrdDw;
		}
		var thumb = imgBrdDt.query('index', imgD.index + '');
		imgBrdCl.addClass( thumb, 'ready' );
		imgBrdDt.val( thumb, 'source', imgD.source );
		if( imgD.bytes ) imgBrdDt.val( thumb, 'bytes', imgD.bytes );
		imgBrdDw.total += 1;
		if( imageBoard && imageBoard.downloader.isActive() )
		{
			imageBoard.downloader.activate();// TODO: activate only current image
		}
	}
	//----------------------------------- XRH IMAGE DATA ---------------------------------//
	//------------------------------------------------------------------------------------//
	//-------------------------------------- GELBOORU ------------------------------------//
	function initGelbooruObject()
	{
		var retVal = {
			data: {
				name: 'gelbooru',
				hostname: 'gelbooru.com',
				imageDir: '/images',
				imageUri: 'simg3.gelbooru.com//images/',// TODO
				//imageSubDomain: 'simg3',
				postDivInsertionPlace: 'h4 > a*showCommentBox',
				divInsertionPlace: '.contain-push > div',
			},
			get postDivInsertionPlace(){return this.data.postDivInsertionPlace;},
			get divInsertionPlace(){return this.data.divInsertionPlace;},
			get name(){ return this.data.name; },
			get hostname(){return this.data.hostname; },
			get imageDir(){return this.data.imageDir; },
			get needXHR(){return true;},
			methodsMap: {
				// href
				isPost: 'booru',
				getPostId: 'booru',
				getPostUrl: 'booru',
				// thumb
				setImageDataThumb: 'booru',
				// post page
				getPostImage: 'booru',
				setImageOriginalResolution: 'booru',
				setImageDataSize: 'booru',
				setImageDataSourceLowres: 'booru',
				setImageDataSourceHighres: 'booru',
				setImageDataTags: 'booru',
				setImageDataName: 'general',
				setImageDataExtension: 'general',
				setImageDataBytes: null,
				setImageDataDoc: 'general',
				// button insertion div
				getPostDivInsertionPlace: 'booru',
				getDivInsertionPlace: 'booru',
				keyboardDiv: 'booru',
				createDiv: 'booru',
			},
			init: function(){
				var name, type;
				for( name in this.methodsMap )
				{
					type = this.methodsMap[name];
					if( type )
						this[name] = methodsObject.method(type, name);
				}
			}
		};
		retVal.init();
		return retVal;
	}
	//-------------------------------------- GELBOORU ------------------------------------//
	//------------------------------------------------------------------------------------//
	//--------------------------------------- RULE34 -------------------------------------//
	function initRule34Object()
	{
		var retVal = {
			get needXHR(){return true;},
			data: {
				name: 'rule34',
				hostname: 'rule34.xxx',
				imageDir: '/images',
				imageUrl: 'img.rule34.xxx//images/',
				//imageSubDomain: 'img',
				postDivInsertionPlace: 'h4 > a',
				divInsertionPlace: 'div#top',
			},
			get name(){ return this.data.name; },
			get hostname(){return this.data.hostname; },
			get imageDir(){return this.data.imageDir; },
			get postDivInsertionPlace(){return this.data.postDivInsertionPlace;},
			get divInsertionPlace(){return this.data.divInsertionPlace;},
			
			methodsMap: {
				// href
				isPost: 'booru',
				getPostId: 'booru',
				getPostUrl: 'booru',
				// method of thumbnail data grabbing (thumbSource, )
				setImageDataThumb: 'booru',
				// post page
				getPostImage: 'booru',
				setImageOriginalResolution: 'booru',
				setImageDataSize: 'booru',
				setImageDataSourceLowres: 'booru',
				setImageDataSourceHighres: 'booru',
				setImageDataTags: 'booru',
				setImageDataName: 'general',
				setImageDataExtension: 'general',
				setImageDataBytes: null,
				setImageDataDoc: 'general',
				//
				getPostDivInsertionPlace: 'booru',
				getDivInsertionPlace: 'booru',
				keyboardDiv: 'booru',
				createDiv: 'booru',
			},
			init: function(){
				var name, type;
				for( name in this.methodsMap )
				{
					type = this.methodsMap[name];
					if( type )
						this[name] = methodsObject.method(type, name);
				}
			}
		};
		retVal.init();
		return retVal;
	}
	//--------------------------------------- RULE34 -------------------------------------//
	//------------------------------------------------------------------------------------//
	//------------------------------------- YANDE.RE -------------------------------------//
	function initYandereObject()
	{
		var retVal = {
			//get needXHR(){return false;},
			get needXHR(){return true;},
			data: {
				name: 'yande.re',
				hostname: 'yande.re',
				imageDir: 'image',
				//imageUri: 'files.yande.re/image/',
				postDivInsertionPlace: 'h4 > a',
				//postDivInsertionPlace: 'a.js-posts-show-comments-tab',
				divInsertionPlace: '#post-list-posts',
			},
			get name(){ return this.data.name; },
			get hostname(){return this.data.hostname; },
			get imageDir(){return this.data.imageDir; },
			get postDivInsertionPlace(){return this.data.postDivInsertionPlace;},
			get divInsertionPlace(){return this.data.divInsertionPlace;},
			methodsMap: {
				// href methods
				isPost: 'yande.re',// if getPostId success then true else false
				getPostId: 'yande.re',// get post id from href
				getPostUrl: 'yande.re',// get post url by postId
				// method of thumbnail data grabbing (thumbSource, )
				setImageDataThumb: 'booru',
				// methods of image data getting from image post page
				getPostImage: 'booru',
				setImageOriginalResolution: 'booru',
				setImageDataSize: 'booru',
				setImageDataSourceLowres: 'booru',
				setImageDataSourceHighres: 'booru',
				setImageDataTags: 'booru',
				setImageDataName: 'general',
				setImageDataExtension: 'general',
				setImageDataBytes: null,
				setImageDataDoc: 'general',
				// create place for buttons insertion
				getPostDivInsertionPlace: 'yande.re',
				getDivInsertionPlace: 'yande.re',
				keyboardDiv: 'booru',
				createDiv: 'booru',
			},
			init: function(){
				var name, type;
				for( name in this.methodsMap )
				{
					type = this.methodsMap[name];
					if( type )
						this[name] = methodsObject.method(type, name);
				}
			},
		};
		retVal.init();
		return retVal;
	}
	//------------------------------------- YANDE.RE -------------------------------------//
	//------------------------------------------------------------------------------------//
	//-------------------------------------- DONMAI --------------------------------------//
	function initDonmaiObject()
	{
		var retVal = {
			//get needXHR(){return false;},
			get needXHR(){return true;},
			data: {
				name: 'donmai',
				get hostname(){return this.sub_domains[this.sub_index] + '.donmai.us';},
				imageDir: 'data',
				postDivInsertionPlace: '#post-sections > li > a',
				divInsertionPlace: '#posts',
				get sub_domains(){return this.sub.domains;},
				get sub_index(){return this.sub.index;},
				sub: {
					domains: ['safebooru', 'danbooru', 'sonohara', 'hijiribe'],
					index: null,
					init: function( sub_dom ){
						var p;
						if( sub_dom !== undefined )
						{
							p = this.domains.indexOf(sub_dom);
							if( p != -1 )
								return (this.index = p);
						}
						var host = window.location.hostname;
						p = host.indexOf('donmai.us');
						if( p == -1 )
							return -1;
						for( var i = 0; i < this.domains.length; ++i )
						{
							p = host.indexOf(this.domains[i]);
							if( p != -1 )
								return (this.index = p);
						}
						return -1;
					},
				},
			},
			get name(){ return this.data.name; },
			get hostname(){return this.data.hostname; },
			get imageDir(){return this.data.imageDir; },
			get postDivInsertionPlace(){return this.data.postDivInsertionPlace;},
			get divInsertionPlace(){return this.data.divInsertionPlace;},
			methodsMap: {
				// href methods
				isPost: 'donmai',// if getPostId success then true else false
				getPostId: 'donmai',// get post id from href
				getPostUrl: 'donmai',// get post url by postId
				// method of thumbnail data grabbing (thumbSource, )
				setImageDataThumb: 'booru',
				// methods of image data getting from image post page
				getPostImage: 'booru',
				setImageOriginalResolution: 'booru',
				setImageDataSize: 'booru',
				setImageDataSourceLowres: 'booru',
				setImageDataSourceHighres: 'booru',
				setImageDataTags: 'booru',
				setImageDataName: 'general',
				setImageDataExtension: 'general',
				setImageDataBytes: null,
				setImageDataDoc: 'general',
				// create place for buttons insertion
				getPostDivInsertionPlace: 'yande.re',
				getDivInsertionPlace: 'yande.re',
				keyboardDiv: 'booru',
				createDiv: 'booru',
			},
			sub_init: function(domain){
				return this.data.sub.init(domain);
			},
			init: function(){
				this.sub_init();
				var name, type;
				for( name in this.methodsMap )
				{
					type = this.methodsMap[name];
					if( type )
						this[name] = methodsObject.method(type, name);
				}
			},
		};
		retVal.init();
		return retVal;
	}
	//-------------------------------------- DONMAI --------------------------------------//
	//------------------------------------------------------------------------------------//
	//----------------------------------- METHODS OBJECT ---------------------------------//
	function initMethodsObject()
	{
		var retVal = {
			data: {
				'booru': {
					val: null,
					init: function(){
						this.val = this.val || getBooruMethodsObject();
					},
				},
				'yande.re': {
					val: null,
					init: function(){
						this.val = this.val || getYandereMethodsObject();
					},
				},
				'general': {
					val: null,
					init: function(){
						this.val = this.val || getGeneralMethodsObject();
					},
				},
				'donmai': {
					val: null,
					init: function(){
						this.val = this.val || getDonmaiMethodsObject();
					},
				},
			},
			init: function(){
				for( var type in this.data )
					this.data[type].init();
			},
			method: function( type, name ){
				if( this.data[type] )
				{
					if( name )
						return this.data[type].val[name];
					return this.data[type].val;
				}
				return null;
			},
		};
		retVal.init();
		return retVal;
	}
	//----------------------------------- METHODS OBJECT ---------------------------------//
	//------------------------------------------------------------------------------------//
	//-------------------------------- BOORU METHODS OBJECT ------------------------------//
	function getBooruMethodsObject()
	{
		var retVal = {
			getPostImage: function(doc){
				return (doc || document).querySelector('#image');
			},
			setImageDataThumb: function( imgD, thumb ){
				if( thumb && imgD )
				{
					if( thumb.dataset && thumb.dataset.original )
						imgD.thumbSource = thumb.dataset.original;
					else
						imgD.thumbSource = thumb.src;
					imgD.postUrl = thumb.parentNode.href;
					if( thumb.parentNode.id )
						imgD.postId = thumb.parentNode.id.slice(1);
					else
						imgD.postId = this.getPostId( imgD.postUrl );
				}
			},
			setImageDataSourceLowres: function( imgD, doc ){
				var img = this.getPostImage(doc);
				if( img )
					imgD.lowresSource = img.src;
				else
					return 1;
				return 0;
			},
			setImageOriginalResolution: function( imgD, img ){
				if( !img )
					return false;
				var width, height;
				width = img.getAttribute('large_width');
				height = img.getAttribute('large_height');
				if( !width || !height )
				{
					width = img.getAttribute('data-original-width');
					height = img.getAttribute('data-original-height');
				}
				if( width && height )
				{
					imgD.width = width;
					imgD.height = height;
					return true;
				}
				return false;
			},
			setImageDataSize: function( imgD, doc ){
				doc = doc || document;
				var img = this.getPostImage(doc), res;
				if( this.setImageOriginalResolution )
					res = this.setImageOriginalResolution( imgD, img );
				if( res )
					return;
				var lis = doc.querySelectorAll('li'), i, li, len = lis.length;
				for( i = 0; i < len; ++i )
				{
					li = lis[i];
					if( li.innerHTML.indexOf('Size:') != -1 )
						break;
				}
				var match = li.innerHTML.match(/(\d+)x(\d+)/);
				if( i < len && match )
				{
					imgD.width = match[1];
					imgD.height = match[2];
				}else
					console.error("[setImageDataSize] can't find image size (width x height)");
			},
			setImageDataSourceHighres: function( imgD, doc ){
				doc = doc || document;
				var link = doc.querySelector('li > a[href*="' + this.hostname + '/' + this.imageDir + '/"]');
				if( !link && this.name === 'donmai' )
					link = doc.querySelector('li > a[href*="/' + this.imageDir + '/"]');
				//var link = doc.querySelector('li > a[href*="' + this.imageUri + '"]');
				if( link )
					imgD.source = link.href;
				else if( imgD.lowresSource )
					imgD.source = imgD.lowresSource;
				else{
					console.error("[setImageDataSourceHighres] no image source found");
					return 1;
				}
				// + yande.re:
				var jpeg = doc.querySelector('li > a[href*="' + this.hostname + '/jpeg/"]');
				if( jpeg )
					imgD.jpegSource = jpeg.href;
				return 0;
			},
			setImageDataTags: function( imgD, doc ){
				doc = doc || document;
				var getTagName = function( tagElm )
				{
					return last(tagElm.querySelectorAll('a')).innerText.trim().replace(/\s+/g, '_');
				};
				var tagsType = ['character', 'copyright', 'artist', 'circle', 'general', 'faults'],
					tagsMap = ['4', '3', '1', '-1', '0', '-1'];// donmai
				imgD.tags = imgD.tags || [];
				imgD.tags.length = 0;
				for( var i = 0, tags; i < tagsType.length; ++i )
				{
					tags = doc.querySelectorAll('li.tag-type-' + tagsType[i] );
					if( tags.length === 0 )
						tags = doc.querySelectorAll('li.category-' + tagsMap[i]);// donmai
					for( var k = 0; tags && k < tags.length; ++k )
						imgD.tags.push( getTagName(tags[k]) );
				}
			},
			isPost: function( url ){
				url = url || window.location.href;
				if( this.getPostId(url) )
					return true;
				return false;
			},
			getPostId: function( postUrl ){
				postUrl = postUrl || window.location.href;
				var srch = getLocation( postUrl, 'search' ),
					keys = getSearchObject( srch );
				if( keys.s === 'view' && keys.page === 'post' )
					return keys.id;
				else
					return null;
			},
			getPostUrl: function( postId ){
				return 'https://' + this.hostname + '/index.php?page=post&s=view&id=' + postId;
			},
			getPostDivInsertionPlace: function(doc){
				doc = doc || document;
				var insertPlace = doc.querySelector( this.postDivInsertionPlace );
				if( insertPlace )
					return insertPlace.parentNode;
				return null;
			},
			getDivInsertionPlace: function(doc){
				doc = doc || document;
				var insertPlace = doc.querySelector( this.divInsertionPlace );
				if( insertPlace )
					return insertPlace.nextSibling;
				return null;
			},
			createDiv: function(id, doc){
				doc = doc || document;
				var div = doc.querySelector('#' + id);
				if( div )
					return div;
				div = document.createElement('div');
				var insertPlace;
				if( this.isPost() )
					insertPlace = this.getPostDivInsertionPlace(doc);
				else
					insertPlace = this.getDivInsertionPlace(doc);
				if( !insertPlace )
					return null;
				div.setAttribute('id', id);
				div = insertPlace.parentNode.insertBefore( div, insertPlace);
				if( typeof this.keyboardDiv === 'function' )
					this.keyboardDiv( id, doc );
				return div;
			},
			keyboardDiv: function( id, doc ){
				/*
				clog("keyboard init..");
				doc = doc || document;
				var kbId = 'keyboard-div-' + RANDOM,
					div = doc.querySelector('#' + kbId);
				if( div )
					return div;
				var insPlace = doc.querySelector('#' + id);
				if( !insPlace )
				{
					console.error("can't create keyboard div");
					return;
				}
				div = document.createElement('div');
				//div = document.createElement('button');
				div.setAttribute('id', kbId);
				div.setAttribute('class', 'keyboar-inactive');
				div.innerHTML = '<button class="keyboard-inactive">Keyboard</button>';
				//div.innerHTML = 'Keyboard';
				div = insPlace.appendChild(div);
				div.addEventListener('click', handleKeyboardBtnEvent, false);
				clog("keyboard: ", div);
				return div;
				*/
			},
		};
		return retVal;
	}
	/*
	function handleKeyboardBtnEvent(event)
	{
		var t = event.target;
		if( t.tagName !== 'BUTTON' )
			return;
		else if( hasClass( t, 'keyboard-inactive') )
		{
			toggleClass( t, 'keyboard-active', 'keyboard-inactive');
			activateKeyboard();
		}
		else if( hasClass( t, 'keyboard-active' ) )
		{
			toggleClass( t, 'keyboard-inactive', 'keyboard-active');
			deactivateKeyboard();
		}
	}
	*/
	//-------------------------------- BOORU METHODS OBJECT ------------------------------//
	//------------------------------------------------------------------------------------//
	//------------------------------- GENERAL METHODS OBJECT -----------------------------//
	function getGeneralMethodsObject()
	{
		var retVal = {
			setImageDataDoc: function( imgD, doc ){
				if( !imgD || imgD.state === 'ready' )
					return 1;
				doc = doc || document;
				// size
				this.setImageDataSize( imgD, doc );
				// lowres
				var errN = this.setImageDataSourceLowres( imgD, doc );
				// highres
				errN += this.setImageDataSourceHighres( imgD, doc );
				if( errN > 1 )
					return errN;
				if( !imgD.lowresSource )
				{
					imgD.lowresSource = imgD.source;
					//imgD.lowresSource = (imgD.jpegSource || imgD.source);
				}
				// tags
				this.setImageDataTags( imgD, doc );
				// name
				this.setImageDataName( imgD );
				// extension
				this.setImageDataExtension( imgD );
				imgD.state = 'ready';
				//clog("imgD[" + imgD.index + "]: ", imgD.source, imgD.state);
				return 0;
			},
			setImageDataName: function( imgD ){
				var tagsLen = imgD.tags.length,
					uLen = userOptions.val('maxTagsInName'),
					tagsDelim = userOptions.val('tagsDelim');
				imgD.name = '';
				for( var i = 0; i < tagsLen && i < uLen; ++i )
					imgD.name += imgD.tags[i] + tagsDelim;
				if( userOptions.val('addImgBrdName') )
					imgD.name += this.name + tagsDelim;
				imgD.name += imgD.postId;
			},
			setImageDataExtension: function( imgD ){
				var pathname = getLocation( imgD.source, 'pathname' );
				imgD.extension = pathname.match(/\.([a-z\d]+)$/)[1];
				if( imgD.jpegSource )
				{
					pathname = getLocation( imgD.jpegSource, 'pathname' );
					imgD.jpegExtension = pathname.match(/\.([a-z\d]+)$/)[1];
				}
			},
		};
		return retVal;
	}
	//------------------------------- GENERAL METHODS OBJECT -----------------------------//
	//------------------------------------------------------------------------------------//
	//------------------------------- YANDERE METHODS OBJECT -----------------------------//
	function getYandereMethodsObject()
	{
		var retVal = {
			isPost: function(url){
				url = url || window.location.href;
				return /\/post\/show\/\d+/.test(url);
			},
			getPostId: function(url){
				if( this.isPost(url) )
					return getLocation(url, 'pathname').match(/\d+/)[0];
				return null;
			},
			getPostUrl: function(postId){
				return 'https://' + this.hostname + '/post/show/' + postId;
			},
			getPostDivInsertionPlace: function(doc){
				doc = doc || document;
				var insertPlace = doc.querySelector(this.postDivInsertionPlace);
				if( insertPlace )
					return insertPlace.parentNode.parentNode;
				return null;
			},
			getDivInsertionPlace: function(doc){
				doc = doc || document;
				var insertPlace = doc.querySelector(this.divInsertionPlace);
				if( insertPlace )
					return insertPlace;
				return null;
			},
		};
		return retVal;
	}
	//------------------------------- YANDERE METHODS OBJECT -----------------------------//
	//------------------------------------------------------------------------------------//
	//-------------------------------- DONMAI METHODS OBJECT -----------------------------//
	function getDonmaiMethodsObject()
	{
		var retVal = {
			isPost: function(url){
				url = url || window.location.href;
				return /\/posts\/\d+/.test(url);
			},
			getPostId: function(url){
				if( this.isPost(url) )
					return getLocation(url, 'pathname').match(/(\/posts\/)?(\d+)?/)[2];
				return null;
			},
			getPostUrl: function(postId){
				return 'https://' + this.hostname + '/posts/' + postId;
			},
		};
		return retVal;
	}
	//-------------------------------- DONMAI METHODS OBJECT -----------------------------//
	//------------------------------------------------------------------------------------//
	//-------------------------------------- DATASET -------------------------------------//
	function initImageBoardDataset(d)
	{
		var retVal = {
			data: {
				source: 'data-image-board-source',
				index: 'data-image-board-index',
				extension: 'data-image-board-extension',
				bytes: 'data-image-board-bytes',
			},
			val: function(elm, propName, v){
				if( this.data[propName] )
				{
					if( v !== undefined )
						elm.setAttribute(this.data[propName], v);
					else
						return elm.getAttribute(this.data[propName]);
				}
				return null;
			},
			init: function(doc){
				this.doc = doc || document;
			},
			getSelector: function(propName, v){
				var sel = this.data[propName];
				if( sel )
				{
					if( v !== undefined )
					{
						var pos = v.indexOf('=');
						if( pos > -1 && pos < 2 )
							sel += v;
						else
							sel += '="' + v + '"';
					}
					return '[' + sel + ']';
				}
				return null;
			},
			query: function(propName, v){
				var sel = this.getSelector(propName, v);
				if( sel )
					return this.doc.querySelector(sel);
				return null;
			},
			queryAll: function(propName, v){
				var sel = this.getSelector(propName, v);
				if( sel )
					return this.doc.querySelectorAll(sel);
				return null;
			},
		};
		retVal.init(d);
		return retVal;
	}
	//-------------------------------------- DATASET -------------------------------------//
	//------------------------------------------------------------------------------------//
	//-------------------------------------- CLASSES -------------------------------------//
	function initImageBoardClasses(d)
	{
		var retVal = {
			get counted(){return this.data.counted;},
			get viewActive(){return this.data.viewActive;},
			get viewAttach(){return this.data.viewAttach;},
			get ready(){return this.data.ready;},
			get downloaded(){return this.data.downloaded;},
			get downloadAttach(){ return this.data.downloadAttach;},
			get downloadActive(){ return this.data.downloadActive;},
			data: {
				counted: 'image-board-counted',
				viewAttach: 'image-board-attach-view-event',
				viewActive: 'image-board-active-for-view',
				ready: 'image-board-has-original-source',
				downloaded: 'image-board-downloaded-original',
				downloadAttach: 'image-board-attach-download-event',
				downloadActive: 'image-board-active-for-download',
			},
			hasClass: function(elm, propName){
				if( this.data[propName] )
					return hasClass(elm, this.data[propName]);
				return false;
			},
			addClass: function(elm, propName){
				if( this.data[propName] )
					addClass(elm, this.data[propName]);
			},
			removeClass: function(elm, propName){
				if( this.data[propName] )
					removeClass(elm, this.data[propName]);
			},
			toggleClass: function(elm, newPropName, oldPropName){
				if( oldPropName && !this.data[oldPropName] )
					return;
				else if( !newPropName || !this.data[newPropName] )
					return;
				toggleClass( elm, this.data[newPropName], this.data[oldPropName] );
			},
			queryAll: function(propName){
				if( this.data[propName] )
					return this.doc.querySelectorAll('.' + this.data[propName]);
				return null;
			},
			init: function(doc){this.doc = doc || document;},
		};
		retVal.init(d);
		return retVal;
	}
	//-------------------------------------- CLASSES -------------------------------------//
	//------------------------------------------------------------------------------------//
	//------------------------------------- DOWNLOADER -----------------------------------//
	function initImageBoardDownloader(d)
	{
		var iter = {
			total : 0,
			done: 0,
		};
		var retVal = {
			get total(){return iter.total;},
			set total(n){
				iter.total = n;
				this.downloadAllHtml(iter.total, iter.done);
			},
			get done(){return iter.done;},
			set done(n){
				iter.done = n;
				this.downloadAllHtml(iter.total, iter.done);
			},
			id: {
				downloader: 'image-board-downloader-' + RANDOM,
				downloadAll: 'image-board-download-all-' + RANDOM,
				downloadSwitch: 'image-board-download-switch-' + RANDOM,
			},
			'class': {
				on: 'image-board-downloader-on',
				off: 'image-board-downloader-off',
				active: 'image-board-downloader-active',
			},
			init: function(id, doc){
				doc = doc || document;
				clog("imgBrdDw.init, doc: ", doc);
				var div = doc.querySelector('div#' + id);
				clog("div: ", div, id);
				if( !div )
				{
					console.error("[initImageBoardDownloader] can't init div");
					return;
				}
				this.val = div.querySelector('#' + this.id.downloader );
				if( !this.val )
				{
					this.val = document.createElement('div');
					this.val.setAttribute('id', this.id.downloader);
					var html = '' +
					'<button id="' + this.id.downloadSwitch + '" class="' + this.class.off + '" ' +
					'title="Press \'Shift+D\' to switch download mode on/off">Download Mode</button>' +
					'<button id="' + this.id.downloadAll + '" class="">Download All (0)</button>' +
					'';
					this.val.innerHTML = html;
					this.val = div.appendChild(this.val);
				}
				this.downloadAll = this.val.querySelector('#' + this.id.downloadAll);
				if( !this.downloadAll.classList.contains(this.class.active) )
				{
					this.downloadAll.addEventListener('click', handleDownloadAllEvent, false);
					this.downloadAll.classList.add(this.class.active);
				}
				this.downloadSwitch = this.val.querySelector('#' + this.id.downloadSwitch);
				if( !this.downloadSwitch.classList.contains(this.class.active) )
				{
					activateKeyboard();
					this.downloadSwitch.addEventListener('click', handleDownloadSwitchEvent, false);
					this.downloadSwitch.classList.add(this.class.active);
				}
				return this.val;
			},
			downloadAllHtml: function( total, loaded ){
				if( !this.val )
					return;
				this.downloadAll.innerHTML = '' +
				'Download All (' + (loaded ? loaded + ' / ': '') + (total ? total : 0) + ')';
			},
			downloadOn: function(){
				toggleClass( this.downloadSwitch, this.class.on, this.class.off );
			},
			downloadOff: function(){
				toggleClass( this.downloadSwitch, this.class.off, this.class.on );
			},
			isActive: function(){
				if( this.downloadSwitch )
					return this.downloadSwitch.classList.contains(this.class.on);
				return false;
			},
		};
		return retVal;
	}
	//------------------------------------- DOWNLOADER -----------------------------------//
	//------------------------------------------------------------------------------------//
	//------------------------------------ DOWNLOADER-2 ----------------------------------//
	function handleDownloadEvent(event)
	{
		if( !imageBoard.imgBrdCl.hasClass( this, 'downloadActive' ) )
			return;
		event.preventDefault();
		var thumb = event.target,
			index = imageBoard.imgBrdDt.val( thumb, 'index' ),
			imgD = imageBoard.images.list[index];
		downloadFile( imgD );
	}
	function handleDownloadAllEvent(event)
	{
		var list = imageBoard.images.list;
		for( var i = 0, len = list.length, imgD; i < len; ++i )
		{
			imgD = list[i];
			downloadFile( imgD );
		}
	}
	function fileDownloader( name, resource )
	{
		var a = document.createElement('a'),
			body = document.body || document.getElementsByTagName('body')[0];
		a.setAttribute('download', name);
		a.href = resource;
		body.appendChild(a);
		a.click();
		body.removeChild(a);
	}
	function downloadFile( imgD )
	{
		if( imgD.state !== 'ready' || imgD.downloadState === 'downloaded' || imgD.downloadState === 'inProgress' )
			return;
		imgD.downloadState = 'inProgress';
		var hostname = getLocation(imgD.source, 'hostname'), source;
		if( userOptions.val('downloadJPEG') && imgD.jpegSource )
			source = imgD.jpegSource;
		else
			source = imgD.source;
		if( hostname === window.location.hostname )
		{
			imageBoardDownloader( imgD, source );
			return;
		}
		GM_xmlhttpRequest({
			url: source,
			method: 'GET',
			context: {
				'index': imgD.index,
			},
			responseType: 'blob',
			onload: blibBlobDownloader,
		});
	}
	function blibBlobDownloader( xhr )
	{
		if( xhr.status !== 200 )
		{
			console.error("xhr.status: ", xhr.status, xhr.statusText);
			console.erorr("url: " + this.url);
			return;
		}
		var wndURL = window.webkitURL || window.URL,
			resource = wndURL.createObjectURL(xhr.response),
			imgD = imageBoard.images.list[xhr.context.index];
		imageBoardDownloader( imgD, resource );
		wndURL.revokeObjectURL( resource );
	}
	function imageBoardDownloader( imgD, resource )
	{
		var name = imgD.name + '.' + imgD.extension;
		fileDownloader( name, resource );
		var thumb = imageBoard.imgBrdDt.query('index', imgD.index + '');
		imageBoard.imgBrdCl.addClass( thumb, 'downloaded' );
		imgD.downloadState = 'downloaded';
		imageBoard.imgBrdDw.done += 1;
	}
	function handleDownloadSwitchEvent(event)
	{
		if( imageBoard.imgBrdDw.isActive() )
			imageBoard.downloader.deactivate();
		else
			imageBoard.downloader.activate();
	}
	function activateKeyboard()
	{
		window.addEventListener('keydown', handleKeyboardEvent, false);
	}
	function deactivateKeyboard()
	{
		window.removeEventListener('keydown', handleKeyboardEvent, false);
	}
	function handleKeyboardEvent(event)
	{
		var charCode = event.keyCode || event.which,
			str = String.fromCharCode(charCode).toLowerCase();
		if( !event.shiftKey || event.ctrlKey || event.altKey )
			return;
		else if( str === 'd' )
		{
			handleDownloadSwitchEvent();
		}
		/*
		else if( str === 'v' )
		{
			handleViewerSwitchEvent();// dummy function
		}
		else if( str === 's' )
		{
			if( imageBoard )
			{
				imageBoard.init();
				imageBoard.fix();
			}
		}
		*/
	}
	//------------------------------------ DOWNLOADER-2 ----------------------------------//
	//------------------------------------------------------------------------------------//
	function handleViewerSwitchEvent(event){}// TODO
	//------------------------------------------------------------------------------------//
	//------------------------------------ USER OPTIONS ----------------------------------//
	function initOptions()
	{
		function _setDef(){this.val = this.def;}
		var retVal = {
			data: {
				'maxTagsInName': {
					val: null,
					def: 10,
					setDef: _setDef,
				},
				'tagsDelim': {
					val: null,
					def: '-',
					setDef: _setDef,
				},
				'addImgBrdName': {
					val: null,
					def: true,
					setDef: _setDef,
				},
				'downloadJPEG': {
					val: null,
					def: false,
					setDef: _setDef,
				},
				/*
				'autoRunSource': {
					val: null,
					def: true,
					setDef: _setDef,
				},
				'autoRunDownloadOn': {
					val: null,
					def: false,
					setDef: _setDef,
				},
				*/
			},
			val: function( opt, v )
			{
				if( this.data[opt] )
				{
					if( v === undefined )
						return this.data[opt].val;
					else
						this.data[opt].val = v;
				}else
					return null;
			},
			setDefs: function(){
				for( var key in this.data )
					this.data[key].setDef();
			},
			set: function(obj){
				for( var key in obj )
					this.val(key, obj[key]);
			},
		};
		retVal.setDefs();
		return retVal;
	}
	//------------------------------------ USER OPTIONS ----------------------------------//
	//------------------------------------------------------------------------------------//
	function newCssClasses()
	{
		addCssClass(`
			#image-board-downloader-${RANDOM} {
				position: relative;
				//float: right;
				text-align: right;
				top: 2px;
				right: 10px;
				bottom: 2px;
				//display: inline-block;
			}
			#keyboard-div-${RANDOM} {
				position: relative;
				display: inline-block;
			}
			#image-board-div-${RANDOM} button {
				margin: 3px 10px;
				color: ${imageBoard.siteList.style().color};
				font-weight: bold;
				width: 180px;
				border: 0px;
				padding: 5px;
				background: ${imageBoard.siteList.style().background};
			}
			#image-board-div-${RANDOM} button:hover {
				background: ${imageBoard.siteList.style().backgroundHover};
				color: ${imageBoard.siteList.style().colorHover};
			}
			.keyboard-inactive::after,
			.image-board-downloader-off::after {
				content: " [off]";
			}
			.keyboard-active::after,
			.image-board-downloader-on::after {
				content: " [on]";
			}
			img.image-board-has-original-source {
				border-bottom: 5px solid green !important;
			}
			.image-board-active-for-download {
				cursor: default;
			}
		`);
	}
	function addCssClass(cssClass)
	{
		var style = document.createElement('style'),
			head = document.querySelector('head');
		style.type = 'text/css';
		if( style.styleSheets )
			style.styleSheets.cssText = cssClass;
		else
			style.appendChild(document.createTextNode(cssClass));
		return head.appendChild(style);
	}
	function addClass( elm, name )
	{
		if( elm && name )
			elm.classList.add(name);
	}
	function removeClass( elm, name )
	{
		if( elm && name )
			elm.classList.remove(name);
	}
	function hasClass( elm, name )
	{
		if( elm && name )
			return elm.classList.contains(name);
		return false;
	}
	function toggleClass( elm, newClass, oldClass )
	{
		if( !elm || !newClass )
			return;
		if( oldClass )
		{
			elm.classList.remove(oldClass);
			elm.classList.add(newClass);
		}
		else if( elm.classList.contains(newClass) )
			elm.classList.remove(newClass);
		else
			elm.classList.add(newClass);
	}
	function getLocation( url, attr )
	{
		if( !attr )
			return null;
		url = url || window.location.href;
		this.link = this.link || document.createElement('a');
		this.link.href = url;
		return this.link[attr];
	}
	function getSearchObject( search )
	{
		var keys = {}
		if( search )
		{
			search = search.replace(/^\?/, '');
			search.split('&').forEach(function(item){
				item = item.split('=');
				keys[item[0]] = item[1];
			});
		}
		return keys;
	}
	function last( arr )
	{
		if( arr && arr.length > 0 )
			return arr[arr.length-1];
		return null;
	}
	function nodeWalk()
	{
		var len = arguments.length, obj = this, i, arg;
		for( i = 0; i < len; ++i )
		{
			arg = arguments[i];
			if( arg === undefined )
				return obj;
			else if( obj[arg] === undefined )
				return null;
			obj = obj[arg];
		}
		return obj;
	}
})();