ETI MAL Integration

Integrates MyAnimeList anime history into posts

// ==UserScript==
// @name           ETI MAL Integration
// @namespace      pendevin
// @description    Integrates MyAnimeList anime history into posts
// @include        http://endoftheinter.net/inboxthread.php*
// @include        http://boards.endoftheinter.net/postmsg.php*
// @include        http://boards.endoftheinter.net/showmessages.php*
// @include        https://endoftheinter.net/inboxthread.php*
// @include        https://boards.endoftheinter.net/postmsg.php*
// @include        https://boards.endoftheinter.net/showmessages.php*
// @require        http://code.jquery.com/jquery-2.1.3.min.js
// @version        3.3
// @grant          GM_xmlhttpRequest
// @grant          GM_setValue
// @grant          GM_getValue
// ==/UserScript==


//enter the characters you want to replace with your last watched show
//MAKE SURE THIS IS UNIQUE IN YOUR SIG
const EPISODE_REPLACER='/et';

//enter the characters you want to replace with your episode count for the day
//MAKE SURE THIS IS UNIQUE IN YOUR SIG
const COUNT_REPLACER='/ec';

//enter the url for your MyAnimeList history page
//if you only want to use anime or manga updates, add /anime or /manga to the end of the url
const HISTORY_URL='http://myanimelist.net/history/pendevin';

//ll breaks without noconflict jquery
this.$=this.jQuery=jQuery.noConflict(true);

//i got this from shoecream's userscript autoupdater at http://userscripts.org/scripts/show/45904
var XHR={
	// r.doc is the returned page
	// r.respose is the response element
	createDoc:function(response,callback,optional){
		var doc=document.implementation.createDocument('','',null);
		var html=document.createElement("html");
		html.innerHTML=response.responseText;
		doc.appendChild(html);
		var r={};
		r.response=response;
		r.doc=doc;
		callback(r,optional);
	},

	//sends the XHR request, callback is the function to call on the returned page
	get:function(url,callback,optional){
		if(optional==undefined)optional=null;
		GM_xmlhttpRequest({
			method:'GET',
			url:url,
			headers:{
				'User-Agent':navigator.userAgent,
				'Content-Type':'application/x-www-form-urlencoded',
				'Host':'myanimelist.net',
				'Accept':'image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, */*',
				'Pragma':'no-cache'
			},
			onload:function(r){XHR.createDoc(r,callback,optional);}
		});
	}
};

//finds the last index of a regular expression value
//takes a string and a regex object
//kinda slow :(
function reLastIndex(string,regex){
	var index=-1;
	//we're going backwards from the end and searching for the first occurrence we find
	for(var i=string.length-1;i>0;i++){
		//once we find it, we're outta here
		if(string.substring(i).search(regex)!=-1){
			index=i;
			break;
		}
	}
	return index;
}

function reEscape(str){
	var specials=new RegExp("[.*+?|()\\[\\]{}\\\\]","g"); // .*+?|()[]{}\
	return str.replace(specials,"\\$&");
}

//parse a mal date into a javascript date thingy
//takses a string, motherfucker, and returns a Date object
function parseMalDate(time){
	var now=new Date;
	var clock;
	//some date case
	if(time.match(/\w\w\w \d\d?,/)){
		clock=time.match(/(\w\w\w) (\d\d?), (\d\d?):(\d\d) ([AP]M)/);
		//adjust for am/pm
		clock[3]=parseInt(clock[3]!=12?clock[3]:0)+(clock[5]=='PM'?12:0);
		//gotta parse the month because ugh
		var monthKey={
			'Jan':0,
			'Feb':1,
			'Mar':2,
			'Apr':3,
			'May':4,
			'Jun':5,
			'Jul':6,
			'Aug':7,
			'Sep':8,
			'Oct':9,
			'Nov':10,
			'Dec':11
		};
		clock[1]=monthKey[clock[1]];
		//date object is (year (4 digits), month (0-11), date (1-31), hour (0-23), minute (0-59), second (0-59))
		return new Date(now.getFullYear(),clock[1],parseInt(clock[2],10),clock[3],parseInt(clock[4]),0);
	}
	//some time yesterday case
	else if(time.match(/Yesterday,/)){
		clock=time.match(/Yesterday, (\d\d?):(\d\d) ([AP]M)/);
		clock[1]=parseInt(clock[1]!=12?clock[1]:0)+(clock[3]=='PM'?12:0);
		return new Date(now.getFullYear(),now.getMonth(),now.getDate()-1,clock[1],parseInt(clock[2]),0);
	}
	//some time today case
	else if(time.match(/Today,/)){
		clock=time.match(/Today, (\d\d?):(\d\d) ([AP]M)/);
		clock[1]=parseInt(clock[1]!=12?clock[1]:0)+(clock[3]=='PM'?12:0);
		return new Date(now.getFullYear(),now.getMonth(),now.getDate(),clock[1],parseInt(clock[2]),0);
	}
	//hour(s) ago case
	else if(time.match(/hours? ago/)){
		return new Date(now.getTime()-parseInt(time.match(/\d\d?/))*3600000);
	}
	//minute(s) ago case
	else if(time.match(/minutes? ago/)){
		return new Date(now.getTime()-parseInt(time.match(/\d\d?/))*60000);
	}
	//second(s) ago case
	else if(time.match(/seconds? ago/)){
		return new Date(now.getTime()-parseInt(time.match(/\d\d?/))*1000);
	}
	return now;
}

//figure out how long ago something was, with expanding time scales
//takes a Date object!!, and tells you how long ago it was in the form ' (<time(s)> ago)''
//now returns an object with the numeral ago and the unit as attributes
function differenceEngine(then){
	var diff={
		diff:new Date().getTime()-then.getTime()
	};
	//days difference
	if(diff.diff>86400000){
		diff.numeral=(diff.diff-diff.diff%86400000)/86400000;
		diff.unit='day'+(diff.numeral>1?'s':'');
	}
	//hours difference
	else if(diff.diff>3600000){
		diff.numeral=(diff.diff-diff.diff%3600000)/3600000;
		diff.unit='hour'+(diff.numeral>1?'s':'');
	}
	//minutes difference
	else if(diff.diff>60000){
		diff.numeral=(diff.diff-diff.diff%60000)/60000;
		diff.unit='minute'+(diff.numeral>1?'s':'');
	}
	//seconds difference
	else if(diff.diff>1000){
		diff.numeral=(diff.diff-diff.diff%1000)/1000;
		diff.unit='second'+(diff.numeral>1?'s':'');
	}
	//guess it could have been less than a second ago but i can't imagine why
	else{
		diff.numeral=1;
		diff.unit='second';
	}
	return diff;
}

//when you get the history response, insert that info into the quickpost box
function malShit(r){
	var doc=$(r.doc);
	//do shit
	//make sure there's a history
	if(doc.find('#horiznav_nav').next().text()=='No history found'){
		return null;
	}
	//grab the rows and shit
	var episodes=doc.find('#horiznav_nav + div tr');
	//find most recent anime update
	var anime={
		raw:episodes.has('td:contains(" ep. ")').first()
	};
	//parse anime update
	if(anime.raw.length){
		anime.name=anime.raw.find('td:first-child>a').text();
		anime.link=anime.raw.find('td:first-child>a').attr('href');
		anime.episode=anime.raw.find('td:first-child>strong').text();
		anime.date=parseMalDate(anime.raw.find('td:last-child').text());
		anime.elapsed=differenceEngine(anime.date);
		anime.type='anime';
	}
	//find most recent manga update
	var manga={
		raw:episodes.has('td:contains(" chap. ")').first()
	};
	//parse manga update
	if(manga.raw.length){
		manga.name=manga.raw.find('td:first-child>a').text();
		manga.link=manga.raw.find('td:first-child>a').attr('href');
		manga.episode=manga.raw.find('td:first-child>strong').text();
		manga.date=parseMalDate(manga.raw.find('td:last-child').text());
		manga.elapsed=differenceEngine(manga.date);
		manga.type='manga';
	}
	//get today's ep count
	//make sure that it's actually today'
	var count={
		raw:episodes.find('div.normal_header:contains("Today") small'),
	};
	count.number=count.raw[0]?count.raw.text().slice(1,-1):"0";
	//which update is more recent and also really there
	//ugh this is ungainly
	var upd8=null;
	if(anime.raw[0]&&manga.raw[0]){
		if(anime.elapsed.diff<manga.elapsed.diff){
			upd8=anime;
		}
		else{
			upd8=manga;
		}
	}
	else if(anime.raw[0]){
		upd8=anime;
	}
	else if(manga.raw[0]){
		upd8=manga;
	}
	//now that we've extracted our data, check to see if it's different from what we've got
	if(upd8&&(cachedData.name!=upd8.name||cachedData.episode!=upd8.episode||count.number!=cachedData.count)){
		//if the episode changed
		if(upd8&&(cachedData.name!=upd8.name||cachedData.episode!=upd8.episode)){
			//cache our shit
			cachedData=upd8;
		}
		//i suck cocks
		cachedData.count=count.number;
		//make a display string i guess
		var display=cachedData.name+(cachedData.type=='anime'?' ep. ':' chap. ')+cachedData.episode+' ('+cachedData.elapsed.numeral+' '+cachedData.elapsed.unit+' ago)';
		//remember the quickpost box position
		var scrollPosition=quickpost.prop('scrollTop');
		var cursorStart=quickpost.prop('selectionStart');
		var cursorEnd=quickpost.prop('selectionEnd');
		//insert the shit
		//gotta do our stored shit because fucking cocks
		quickpost.val(quickpost.val().replace(rxCocks,'$1'+(quickpost.remember.replace(rxEpisode,'$1'+display+'$2').replace(rxCount,'$1'+cachedData.count+'$2'))));
		//restore the quickpost box position
		quickpost.prop('scrollTop',scrollPosition);
		quickpost.prop('selectionStart',cursorStart);
		quickpost.prop('selectionEnd',cursorEnd);
	}
	//save the data
	var gmCache={
		name:cachedData.name,
		link:cachedData.link,
		episode:cachedData.episode,
		date:cachedData.date.valueOf(),
		count:cachedData.count,
		type:cachedData.type
	}
	GM_setValue('cached',JSON.stringify(gmCache));
	return null;
}

//when the quickpost box opens, send off a request to mal for the history
function onFocus(e){
	//don't do this too often pls
	quickpost.off('focus.mal');
	//why the fuck doesn't this always work
	//maybe it has to do with gm hating event calls but not timeouts, which is whack
	if(!GM_getValue('test',true)){
		window.setTimeout(onFocus,500);
		return null;
	}
	//get a new elapsed time since now is different
	cachedData.elapsed=differenceEngine(new Date(cachedData.date));
	//make a display string i guess
	var display=cachedData.name!=''?
		cachedData.name+(cachedData.type=='anime'?' ep. ':' chap. ')+cachedData.episode+' ('+cachedData.elapsed.numeral+' '+cachedData.elapsed.unit+' ago)':
		HISTORY_URL
	;
	//save the position of the cursor and scrollbar
	var scrollPosition=quickpost.prop('scrollTop');
	var cursorStart=quickpost.prop('selectionStart');
	var cursorEnd=quickpost.prop('selectionEnd');
	//remember quickpost value because cocks
	quickpost.remember=quickpost.val().match(rxCocks)[2];
	//insert our data
	quickpost.val(quickpost.val().replace(rxEpisode,'$1'+display+'$2').replace(rxCount,'$1'+cachedData.count+'$2'));
	//restore the position of the cursor and scrollbar
	quickpost.prop('scrollTop',scrollPosition);
	quickpost.prop('selectionStart',cursorStart);
	quickpost.prop('selectionEnd',cursorEnd);
	//send off our get request to mal
	XHR.get(HISTORY_URL,malShit);
	return null;
}

//find the quickpost box and listen for it to open
//this gets the quickpost box and also the message box on postmsg.php
var quickpost=$('.quickpost textarea[name="message"], textarea#message');
//null data
var cachedData=JSON.parse(GM_getValue('cached','{"name":"","episode":"","date":0,"count":"0"}'));
//make a regexps
var rxEpisode=new RegExp('(---[\\w\\W]+)'+reEscape(EPISODE_REPLACER)+'([\\w\\W]*)');
var rxCount=new RegExp('(---[\\w\\W]+)'+reEscape(COUNT_REPLACER)+'([\\w\\W]*)');
var rxCocks=new RegExp('([\\w\\W]*)(---[\\w\\W]+)');
//listen for shit
quickpost.on('focus.mal',onFocus);
//reinitialize after posting
$('form.quickpost input[name="post"]').on('click.mal',function(){
	quickpost.on('focus.mal',onFocus);
});