MyAnimeList - Precise Scores

Enables you to set scores with one decimal place (9.6, 8.5, 7.4 etc.) on MAL.

// ==UserScript==
// @name         MyAnimeList - Precise Scores
// @namespace    https://github.com/DakuTree/userscripts
// @author       Daku (admin@codeanimu.net)
// @description  Enables you to set scores with one decimal place (9.6, 8.5, 7.4 etc.) on MAL.
// @homepageURL  https://github.com/DakuTree/userscripts
// @supportURL   https://github.com/DakuTree/userscripts/issues
// @include      /^http[s]?:\/\/myanimelist\.net\/(anime|manga)(list)?\/.*$/
// @include      /^http[s]?:\/\/myanimelist\.net\/(anime|manga)\.php\?id\=.*$/
// @include      /^http[s]?:\/\/myanimelist\.net\/panel\.php\?go\=(edit|add).*$/
// @include      /^http[s]?:\/\/myanimelist\.net\/editlist\.php\?type\=(anime|manga).*$/
// @updated      2016-08-24
// @version      2.2.6
// @require      https://ajax.googleapis.com/ajax/libs/jquery/2.2.1/jquery.min.js
// ==/UserScript==

var backend = "https://codeanimu.net/userscripts/myanimelist.net/backend/";
var fontsize = "9px"; //Change this if you are using a non-standard font-size (This font-size only applies to scores you have just changed).
var dev = false; //Enable if you want some dev stuff

$(document).ready(function() {
	if(dev === true){
		if(jQuery.fn.jquery !== '2.2.1'){
			alert('jQuery mismatch!\nRunning '+jQuery.fn.jquery+'.\nExpected 2.2.1.');
		}
		window.onerror = function(msg, url, linenumber) {
			alert('Error message: '+msg+'\nURL: '+url+'\nLine Number: '+linenumber);
			error = true;
			return true;
		};
	}

	var userid = (document.cookie.match('(^|; )(A|Y)=([^;]*)')||0)[3] || find_userid() || 0,
	    type;

	if(/myanimelist\.net\/(anime|manga)\/[0-9]+/.test(self.location.href) || /myanimelist\.net\/(anime|manga)\.php\?id\=.*/.test(self.location.href)){
		if(userid === 0) return false;
		type = self.location.pathname.split('/')[1].split('.')[0];

		//Create precise scores select
		var select = $('<select/>', {id: 'precise_score', class: 'inputtext', style: 'padding: 1px 0px 1px 0px'}).append(
						$('<option/>', {value: '0', selected: true, text: '0'})).append(
						$('<option/>', {value: '1', text: '1'})).append(
						$('<option/>', {value: '2', text: '2'})).append(
						$('<option/>', {value: '3', text: '3'})).append(
						$('<option/>', {value: '4', text: '4'})).append(
						$('<option/>', {value: '5', text: '5'})).append(
						$('<option/>', {value: '6', text: '6'})).append(
						$('<option/>', {value: '7', text: '7'})).append(
						$('<option/>', {value: '8', text: '8'})).append(
						$('<option/>', {value: '9', text: '9'}));

		var db_id = $('[name="aid"]').val() || $('[name="mid"]').val() || ((db_id = $('#addtolist a[href^="/panel.php?go=editmanga&id="]').attr('href')) ? db_id.match(/id=([0-9]+)$/)[1] : null); //Manga uses a different ID due to MAL being shit.
		if(db_id){
			//db_id should be set on 3/4 possible pages (anime add/update & manga update)

			//Check if series is already on list, if so update precise score
			if($('input[name=myinfo_submit]').val() == 'Update' || $('input[name=myinfo_submit]').val() == 'Submit'){
				$.getJSON(backend+"mp_index.php", {userid: userid, type: type, db_id: db_id}, function(data) {
					$(select).val(data['score_precise'].toString().split(".")[1] || "0");
				}).error(function(jqXHR, textStatus, errorThrown) {
					if(dev === true){
						alert("Error: "+textStatus+"\nIncoming Text: "+jqXHR.responseText);
					}
				});
			}
			$(select).insertAfter('#myinfo_score');
			$('#myinfo_score').after(' . ');
			$('#myinfo_score').css('padding', '1px 0px 1px 0px');

			$('[name=myinfo_submit]').click(function(){
				if($('#myinfo_score').val() !== 0){
					update_pscores(db_id, $('#myinfo_score').val()+'.'+$('#precise_score').val(), false);
				}
			});
		}else{
			//To make manga precise scores work properly with lists, we need to use a seperate id
			//Sadly this isn't set unless the actual has been already added to your own list
			//This means we need to jump through some hoops.

			$(select).insertAfter('#myinfo_score');
			$('#myinfo_score').after(' . ');
			$('#myinfo_score').css('padding', '1px 0px 1px 0px');

			$('[name=myinfo_submit]').attr('onclick', null);
			$('[name=myinfo_submit]').click(function(){
				$.post("/includes/ajax.inc.php?t=49", {mid: $('#myinfo_manga_id').val(), score: $('#myinfo_score').val(), status: $('#myinfo_status').val(), chapters: $('#myinfo_chapters').val(), volumes: $('#myinfo_volumes').val()},function(data) {
					$("#myinfoDisplay").html('');
					$("#addtolist").html(data);

					db_id = ((db_id = $(data.responseText).attr('href')) ? db_id.match(/id=([0-9]+)$/)[1] : null);
					if(typeof $('#myinfo_score').val() != 'undefined' && $('#myinfo_score').val() !== '0'){
						update_pscores(db_id, $('#myinfo_score').val()+'.'+$('#precise_score').val(), false);
					}
				});
			});
		}
	}
	else if(/myanimelist\.net\/(anime|manga)list\/.*/.test(self.location.href)){
		userid = $('#listUserId').val();
		type = $('#listType').val();

		//If custom CSS isn't loaded then use AJAX instead.
		if(/mal_style\/style\.php.*userid/.test($('style').eq(1).text()) === false){
			$.getJSON(backend+"mp_index.php", {userid: userid, type: type}, function(data) {
				$.each(data, function(){
					$('#score' + (type == "anime" ? "val" : "") + $(this)[0]['db_id']).text($(this)[0]['score_precise']);
				});
			});
		}

		$('input[type="button"]').click(function(e){
			e.preventDefault();
			update_pscores(
				$(this).closest('div').find('input[type=text]').attr('id').substr(9),
				$(this).closest('div').find('input[type=text]').val(),
				true
			);
		});

		$('input[id^="scoretext"]').bind("keypress", function(e) {
			var key =  e.keyCode || e.which;
			if (key == 13){
				update_pscores(
					$(this).attr('id').substr(9),
					$(this).val(),
					true
				);
			}else{
				return true;
			}
		});
	}
	else if(/myanimelist\.net\/panel\.php\?go\=(edit|add).*/.test(self.location.href) || /myanimelist\.net\/editlist\.php\?type\=(anime|manga).*/.test(self.location.href)){
		if(userid === 0) return false;
		type = $('#animeid, #mangaid').attr('id').substr(0, 5);

		//Create precise scores select
		var select = $('<select/>', {id: 'precise_score', class: 'inputtext', style: 'padding: 1px 0px 1px 0px'}).append(
						$('<option/>', {value: '0', selected: true, text: '0'})).append(
						$('<option/>', {value: '1', text: '1'})).append(
						$('<option/>', {value: '2', text: '2'})).append(
						$('<option/>', {value: '3', text: '3'})).append(
						$('<option/>', {value: '4', text: '4'})).append(
						$('<option/>', {value: '5', text: '5'})).append(
						$('<option/>', {value: '6', text: '6'})).append(
						$('<option/>', {value: '7', text: '7'})).append(
						$('<option/>', {value: '8', text: '8'})).append(
						$('<option/>', {value: '9', text: '9'}));

		var db_id = $('#animeid').val() || ((db_id = $('input[name=entry_id]').val()) !== "0" ? db_id : null) || null;
		if(db_id){
			//db_id should be set on 3/4 possible pages (anime add/update & manga update)
			if($('input[type=button][value^=Add], input[type=button][value^=Update]').val().split(" ")[0] == "Update"){
				//Modify precise score if exists
				$.getJSON(backend+"mp_index.php", {userid: userid, type: type, 'db_id': db_id}, function(data) {
					$(select).val(data['score_precise'].toString().split(".")[1]);
					$(select).insertAfter('select[name=score]');
					$('select[name=score]').after(' . ');
					$('select[name=score]').css('padding', '1px 0px 1px 0px');
				}).error(function(jqXHR, textStatus, errorThrown) {
					if(dev === true){
						alert("Error: "+textStatus+"\nIncoming Text: "+jqXHR.responseText);
					}
				});
			}
			$(select).insertAfter('select[name=score]');
			$('select[name=score]').after(' . ');
			$('select[name=score]').css('padding', '1px 0px 1px 0px');

			$('input[type=button][value^=Add], input[type=button][value^=Update]').click(function(){
				if($('form[name$=Form] select[name=status]').val() !== '0' && $('select[name=score]').val() !== '0'){
					update_pscores(db_id, parseInt($('select[name=score]').val())+'.'+$('#precise_score').val(), false);
				}
			});
		}else{
			$(select).insertAfter('select[name=score]');
			$('select[name=score]').after(' . ');
			$('select[name=score]').css('padding', '1px 0px 1px 0px');

			$('form[name=mangaForm] input[name=submitIt]').val(1);

			$('input[type=button][value^=Add]').attr('onclick', null);
			$('input[type=button][value^=Add]').click(function(){
				if($('form[name=mangaForm] select[name=status]').val() !== '0'){
					$.post(location.href, $('form[name=mangaForm]').serialize(), function(d){
						console.log(d);
						//Unlike the /manga/ page, we can't simply check the return data for the entry_id
						//Instead we need to send a second AJAX request. This is why I hate MAL.
						$.get("https://myanimelist.net/manga.php", {id: $('#mangaid').val()}, function(data){
							db_id = ((db_id = data.match(/panel\.php\?go=editmanga&id=([0-9]+)/)) ? db_id[1] : null);
							if(typeof $('#myinfo_score').val() != 'undefined' && $('#myinfo_score').val() !== '0'){
								update_pscores(db_id, parseInt($('select[name=score]').val())+'.'+$('#precise_score').val(), false);
							}
							location.href = 'https://myanimelist.net/manga/'+$('#mangaid').val();
						});
					});
				}else{
					alert("You must select a status (watching, completed, etc...) for this series.");
				}
			});
		}
	}

	function update_pscores(db_id, score, list){
		list = list || false;
		var newScore = Number(score);
		if (isNaN(newScore) || (newScore>10) || (newScore<0)){
			alert('Invalid score value, must be between 0 and 10 || '+newScore);
			return false;
		}else{
			var preciseScore = newScore.toFixed(1);
			var roundScore = Math.floor(newScore); //We use .floor here rather than .round as to avoid rounding up. As, for example, a 9.5+ should never equal a 10.

			if(list){
				var am = {'anime': ['63', 'id', 'scoreval'], 'manga': ['33', 'mid', 'score']};
				var pobj = {'score': roundScore};
				pobj[am[type][1]] = am[type][2];
				$.post("/includes/ajax.inc.php?t="+am[type][0], pobj, function(data){
					$('#scoretext'+db_id).attr('value', '');
					//This feels extremely ugly, but it has to be done so it plays nice with the custom MAL_Precise CSS.
					//We include content twice as a way to make it accessible via another JS script (since we can't access :after)
					$('<style/>').attr('rel', 'stylesheet').attr('type', 'text/css')
						.text("\
							#"+am[type][2]+db_id+" { font-size: 0; content: '"+preciseScore+"'; }\
							#"+am[type][2]+db_id+":after { content: '"+preciseScore+"'; font-size: "+fontsize+"; }\
						").appendTo('head');

					$.get(backend+"mp_update.php", {userid: userid, type: type, 'db_id': db_id, score_precise: preciseScore});
				});
			}else{
				$.get(backend+"mp_update.php", {userid: userid, type: type, 'db_id': db_id, score_precise: preciseScore});
			}
		}
	}

	function find_userid() {
		var userid,
		    unsafeWindow = this['unsafeWindow'] || window;

		//#1: Check localStorage to see if userid is already set?
		//    Also if expired, delete localStorage.
		if(unsafeWindow.localStorage.getItem('userid')) {
			var userid_data = JSON.parse(unsafeWindow.localStorage.getItem('userid'));
			if(new Date().getTime() > userid_data['timestamp']) {
				unsafeWindow.localStorage.removeItem('userid');
				console.log('localstorage found userid but has expired');
			}else{
				userid = userid_data['userid'];
				console.log('localstorage found userid');
			}
		}

		if(!userid) {
			//#2: localstorage does not exists, manually try and find userid

			//#2.1: Check if user is online, and if so, get username.
			var userName;
			$.get('https://myanimelist.net/panel.php', function(data) {
				userName = data.match(/\/profile\/(.*?)"/)[1];
				console.log(userName);
				if(userName) {
					//2.2: Check profile for userID. It may be possible for the userID to still not exist.
					$.get('https://myanimelist.net/profile/'+userName, function(data) {
						//since we're using an old version of jQuery, parsing the HTML is painful, so we're doing it the hacky way.
						userid = data.match(/name="profileMemId" value="([0-9]+)"/)[1];
						if(userid) {
							var expireTime = (14 * 24 * 60 * 60 * 1000); //2 weeks expire
							unsafeWindow.localStorage.setItem('userid', JSON.stringify({'userid': userid, 'timestamp': (new Date().getTime() + expireTime)}));
							console.log('userid found');
						}
					});
				}else{
					//User is not online, alert them?
				}
			});
		}

		return userid;
	}
});