MyAnimeList (MAL) Track Missing Relations

Allows to find missing relations and entries with wrong chapter/episode count.

当前为 2015-04-19 提交的版本,查看 最新版本

// ==UserScript==
// @name         MyAnimeList (MAL) Track Missing Relations
// @namespace    https://greasyfork.org/users/7517
// @description  Allows to find missing relations and entries with wrong chapter/episode count.
// @icon         http://i.imgur.com/b7Fw8oH.png
// @version      3.1.1
// @author       akarin
// @include      /^http\:\/\/myanimelist\.net\/editlist\.php\?type=anime$/
// @include      /^http\:\/\/myanimelist\.net\/panel\.php\?go=editmanga$/
// @grant        none
// @noframes
// ==/UserScript==

(function() {

var mal = {
	nickname: '',
	cache: '3.1',
	ajax: { delay: 300, timeout: 5000, url: 'http://myanimelist.net/' },
	list: { anime: 'anime', manga: 'manga', type: '' },
	entries: { total: 0, calc: -1, fail: 0, left: {}, right: [], ignore: {} },
	content: { body: null, list: null, done: null, fail: null }
};

function main() {
	if ($('#malLogin').length > 0) {
		return;
	}
	
	$.ajaxSetup({ timeout: mal.ajax.timeout });
	
	mal.nickname = $('ul#nav li:first > ul > li > a:contains(Profile)').prop('href').match(/(?!.*\/).*$/)[0];
	mal.list.type = document.URL.match(/\?type=anime$/) ? mal.list.anime : mal.list.manga;		
	
	if (loadValue('mal.cache', '') !== mal.cache) {
		saveData(); // save empty data: clear
	}
		
	mal.content.body = $('<div id="mr_body"></div>').html('<h2 id="mr_body_title">Missing Relations <span><a href="http://greasyfork.org/scripts/9261" target="_blank">v' + GM_info.script.version + '</a></span></h2>');
	mal.content.list = $('<div id="mr_list"></div>').appendTo($(mal.content.body));
	mal.content.done = $('<span id="mr_status_done" style="color: green;"></span>');
	mal.content.fail = $('<span id="mr_status_fail" style="color: #c32;"></span>');
	
	loadData();
	update(false);
	
	$('<div></div>').hide().append($(mal.content.body)).insertAfter('#content');
	$('<span id="mr_link"></span>')
		.append('<span>&nbsp;&nbsp;|&nbsp;&nbsp;</span>')
		.append($('<a href="#mr_body">Missing Relations</a>').fancybox({
			'hideOnContentClick': false, 
			'hideOnOverlayClick': true,
			'transitionIn': 'none', 
			'transitionOut': 'none',
			'titleShow': false,	
			'scrolling': 'no'
		}))
		.append('&nbsp;(')
		.append($('<small></small>').append($('<a href="javascript:void(0);">refresh</a>')
			.click(function() {
				if (true === confirm('Are you sure you want to recalculate missing relations?')) {
					recalculate();
				}
			})
		))
		.append(')&nbsp;').append($(mal.content.done))
		.append('&nbsp;').append($(mal.content.fail))
		.appendTo('#content > div:first');
}

function loadData() {
	mal.entries.calc = -1;
	mal.entries.left = loadValue('mal.entries.left', {});
	mal.entries.right = loadValue('mal.entries.right', []);
	mal.entries.ignore = loadValue('mal.entries.ignore', {});
}

function saveData() {
	saveValue('mal.cache', mal.cache);
	saveValue('mal.entries.left', mal.entries.left);
	saveValue('mal.entries.right', mal.entries.right);
	saveValue('mal.entries.ignore', mal.entries.ignore);
}

function recalculate() {	
	if (mal.entries.calc > -1) {
		return;
	}
	
	var $links = $('#content strong + a');
	
	mal.entries.total = $links.length;
	mal.entries.calc = mal.entries.total;
	mal.entries.fail = 0;
	mal.entries.left = {};
	mal.entries.right = [];
	
	$(mal.content.done).text('');
	$(mal.content.fail).text('');
	$('#content span[class^="mr_status_id_"]').empty();
	
	if ($links.length === 0) {
		update(true);
		return;
	}
	
	$links.each(function(index) {
		var id = $(this).prop('href').match(/\d+$/)[0];
		var $span = $(this).parent().find('span[class="mr_status_id_' + id + '"]');
		if ($span.length === 0) {
			$span = $('<span class="mr_status_id_' + id + '"></span>').appendTo($(this).parent());
		}
		mal.entries.left[id] = true;
		setTimeout(function() { 
			$.ajax(mal.ajax.url + mal.list.type + '/' + id)
				.done(function(data) {
					--mal.entries.calc;
					var title = data.match(/<div id="contentWrapper">[\s\S]*?<h1><div [\s\S]*?<\/div>(.+?)</)[1].trim();
					setRelations(id, title, data);
					if (false === checkCorrectInfo(id, data)) {
						mal.entries.right.push({ lId: id, lTitle: title, rId: '-1', rTitle: '' });
					}
					$span.html('<small style="color: green;">done</small>');
					$(mal.content.done).text('done: ' + (mal.entries.total - mal.entries.calc) + '/' + mal.entries.total);
					update(true);
				})
				.fail(function(data) {
					--mal.entries.calc;
					$span.html('<small style="color: red;">fail</small>');
					$(mal.content.fail).text('fail: ' + (++mal.entries.fail));
					update(true);
				});
		}, mal.ajax.delay * index);
	});
}
	
function setRelations(id, title, data) {	
	var cnRe = new RegExp('<h2>Related ' + (mal.list.type === mal.list.anime ? 'Anime' : 'Manga') + '</h2>[\\s\\S]*?<h2>');
	var idRe = new RegExp('/' + mal.list.type + '/(\\d+)/');	
	
	$('a', '<context>' + data.match(cnRe) + '</context>').each(function() {
		var idData = $(this).prop('href').match(idRe);
		if (idData !== null && idData.length > 1) {
			mal.entries.right.push({ lId: id, lTitle: title, rId: idData[1], rTitle: $(this).text().trim() });
		}
	});
}
	
function checkCorrectInfo(id, data) {
	var status = data.match(/>Status:<\/span>([\s\S]*?)<\/div>/)[1].trim();
	var myStatus = data.match(/selected>(.*?)<\/option/)[1].trim();
		
	if (mal.list.type === mal.list.anime) {
		if ((status === 'Not yet aired' && myStatus !== 'Plan to Watch') ||
			(status === 'Currently Airing' && myStatus === 'Completed')) {
			return false;
		}
		if (myStatus !== 'Completed') {
			return true;
		}
		var eps = data.match(/>Episodes:<\/span>([\s\S]*?)<\/div>/)[1].trim().replace('Unknown', '0');
		var myEps = data.match(/name="myinfo_watchedeps"[\s\S]*?value="(\d*)"/)[1].trim();
		if (parseInt(myEps) > parseInt(eps) || (myStatus === 'Completed' && parseInt(myEps) !== parseInt(eps))) {
			return false;
		}
	}
	else {
		if ((status === 'Not yet published' && myStatus !== 'Plan to Read') ||
			(status === 'Publishing' && myStatus === 'Completed')) {
			return false;
		}
		if (myStatus !== 'Completed') {
			return true;
		}
		var vols = data.match(/>Volumes:<\/span>([\s\S]*?)<\/div>/)[1].trim().replace('Unknown', '0');
		var myVols = data.match(/id="myinfo_volumes"[\s\S]*?value="(\d*)"/)[1].trim();
		if (parseInt(myVols) > parseInt(vols) || (myStatus === 'Completed' && parseInt(myVols) !== parseInt(vols))) {
			return false;
		}
		var chap = data.match(/>Chapters:<\/span>([\s\S]*?)<\/div>/)[1].trim().replace('Unknown', '0');
		var myChap = data.match(/id="myinfo_chapters"[\s\S]*?value="(\d*)"/)[1].trim();
		if (parseInt(myChap) > parseInt(chap) || (myStatus === 'Completed' && parseInt(myChap) !== parseInt(chap))) {
			return false;
		}
	}
	return true;
}

function update(save) {
	if (mal.entries.calc > 0) {
		return;
	}

	if (save === true) {
		saveData();
	}
	
	if (mal.entries.right.length === 0) {
		$(mal.content.list).html('<p>No missing relations found.</p>');
		return;
	}
		
	var $table = $('<table id="relTable" border="0" cellpadding="0" cellspacing="0" width="100%"><tr><td class="mr_table_header">You\'ve watched this…</td><td class="mr_table_header">…so you might want to check this:</td></tr></table>');
		
	$('<tr id="mr_tr_undelete"></tr>').html('<td colspan="2"><p id="mr_undelete_msg" style="display: none;">There are <span id="mr_undelete_num" style="font-weight: bold;"></span> hidden relations. <a href="javascript:void(0);" title="Show ignored relations" onclick="window.showIgnoredRelations();">Show them</a></p></td>').prependTo($table);
		
	var right = mal.entries.right.slice(0).sort(function(a, b) {
		return parseInt(a.rId) > parseInt(b.rId);
	});
		
	for (var i = 0, prevId = ''; i < right.length; ++i) {
		if (parseInt(right[i].rId) < 0) {
			continue;
		}
		if (right[i].rId === prevId || mal.entries.left.hasOwnProperty(right[i].rId)) {
			right.splice(i--, 1);
		}
		else {
			prevId = right[i].rId;
		}
	}
		
	right.sort(function(a, b) {
		var comp = a.lTitle.toLowerCase().localeCompare(b.lTitle.toLowerCase());
		if (parseInt(a.rId) < 0 && parseInt(b.rId) < 0) {
			return comp;
		}
		return parseInt(a.rId) < 0 ? false : 
			(comp !== 0 ? comp : a.rTitle.toLowerCase().localeCompare(b.rTitle.toLowerCase()));
	});
		
	var $ulLeft = $('<ul></ul>'), $ulRight = $('<ul></ul>');	
	$.each(right, function(index, rel) {
		var $liLeft = $('<li><a title="' + rel.lTitle + '"href="' + mal.ajax.url + mal.list.type + '/' + rel.lId + '" target="_blank">' + rel.lTitle + '</a></li>');
		
		if (parseInt(rel.rId) < 0) {			
			if ($ulRight.children().length === 0) {
				$('<li><div class="mr_warning">Wrong status or ' + (mal.list.type === mal.list.anime ? 'episode' : 'vol./chap.') + ' count</div></li>').appendTo($ulRight);
			}
			$ulLeft.append($liLeft);
		}
		else {				
			if ($ulLeft.children().length === 0) {
				$ulLeft.append($liLeft);
			}
			$('<li><div class="mr_hide"><small><a href="javascript:void(0);" title="Hide this relation" onclick="window.hideRelation(\'' + rel.rId + '\');">x</a></small></div>' + 
			'<a title="' + rel.rTitle + '" href="' + mal.ajax.url + mal.list.type + '/' + rel.rId + '" target="_blank">' + rel.rTitle + '</a></li>').prop('id', 'mr_li_' + rel.rId).appendTo($ulRight);
		}
		
		if (index + 1 === right.length || (parseInt(right[index + 1].rId) > -1 && rel.lId !== right[index + 1].lId)) {
			$('<tr id="mr_tr_' + rel.lId + '"></tr>')
				.append($('<td class="mr_subject"></td>').append($ulLeft))
				.append($('<td class="mr_proposed"></td>').append($ulRight))
				.appendTo($table);
				
			$ulLeft = $('<ul></ul>');
			$ulRight = $('<ul></ul>');
		}
	});
	
	updateIgnoredRelations(right);
	$(mal.content.list).empty().append($table);	
	hideIgnoredRelations();
	mal.entries.calc = -1;
}
	
function hideRelation(id, save) {
	var $li = $('li[id="mr_li_' + id + '"]', $(mal.content.list));
	if ($li.length === 0) {
		return;
	}
	
	var $tr = $li.hide().closest('tr');

	var count = 0;
	$('td.mr_proposed li', $tr).each(function() {
		if ($(this).css('display') !== 'none') {
			++count;
		}
	});
	
	$tr.toggle(count > 0);
	
	mal.entries.ignore[id] = true;
	if (save === true) {
		saveValue('mal.entries.ignore', mal.entries.ignore);
		count = Object.keys(mal.entries.ignore).length;
		$('#mr_undelete_num', $(mal.content.list)).text(count);
		$('#mr_undelete_msg', $(mal.content.list)).toggle(count > 0);
	}		
}
	
function hideIgnoredRelations(context) {
	$.each(mal.entries.ignore, function(id) {
		hideRelation(id, false);
	});
	var count = Object.keys(mal.entries.ignore).length;
	$('#mr_undelete_num', $(mal.content.list)).text(count);
	$('#mr_undelete_msg', $(mal.content.list)).toggle(count > 0);
}
	
function showIgnoredRelations() {
	$('#mr_undelete_msg', $(mal.content.list)).hide();
	$('tr[id^="mr_tr_"]', $(mal.content.list)).show();
	$('li[id^="mr_li_"]', $(mal.content.list)).show();
	saveValue('mal.entries.ignore', (mal.entries.ignore = {}));
}

function updateIgnoredRelations(right) {
	var seen = {};
	$.each(right, function(i, rel) {
		seen[rel.rId] = true;
	});
	$.each(mal.entries.ignore, function(id) {
		if (seen.hasOwnProperty(id) === false) {
			delete mal.entries.ignore[id];
		}
	});
	saveValue('mal.entries.ignore', mal.entries.ignore);
}

function loadValue(key, def) {
	try {
		return JSON.parse(localStorage.getItem(mal.nickname + '#' + mal.list.type + '#' + key));
	} catch (e) {
		return def;
	}
}

function saveValue(key, val) {
	localStorage.setItem(mal.nickname + '#' + mal.list.type + '#' + key, JSON.stringify(val));
}

window.hideRelation = function(id) {
	hideRelation(id, true);
};

window.showIgnoredRelations = function() {
	showIgnoredRelations();
};

$('<style type="text/css" />').html('\
	#content span[class^="mr_status_id_"] { float: right; padding: 0 7px; }\
	div#mr_body { text-align: center; width: 650px; height: auto; }\
	div#mr_body #mr_body_title { font-size: 1.5em; font-weight: normal; text-align: center; margin: 0 0 1em 0;	position: relative; border: 0; }\
	div#mr_body #mr_body_title span { font-size: 0.8em; font-weight: normal; }\
	div#mr_body #mr_body_title:after { position: absolute; bottom: -14px; left: 0; width: 100%; height: 8px;	border-top: 1px solid #eee; background: center bottom no-repeat radial-gradient(#f6f6f6, #fff 70%);	background-size: 100% 16px;	content: ""; }\
	div#mr_body p#mr_undelete_msg { margin: 0 0 10px; font-weight: normal; text-align: center; line-height: 1.6em; font-size: 1.1em; }\
	div#mr_list { width: auto; height: 680px; overflow-y: auto; margin: 18px auto 0; }\
	div#mr_list #relTable tr td { background-color: #fff; padding: 0.5em 0 0.5em 6px; font-weight: normal; text-align: left; box-shadow: 0px 1em 1em -1em #ddd inset; vertical-align: top; line-height: 1.6em; font-size: 1.1em; }\
	div#mr_list #relTable tr:not([id="mr_tr_undelete"]):hover td { background-color: #f5f5f5; }\
	div#mr_list #relTable tr td.mr_table_header { background-color: #f5f5f5; box-shadow: none; font-weight: bold; }\
	div#mr_list #relTable tr#mr_tr_undelete td { background-color: #fff; box-shadow: none; padding: 0; margin: 0; }\
	div#mr_list #relTable td.mr_proposed div.mr_warning { width: 285px; color: #e43; font-weight: bold; }\
	div#mr_list #relTable td.mr_proposed div.mr_hide { width: 15px; float: right; text-align: left; color: #c32; padding-left: 5px; }\
	div#mr_list #relTable td ul { list-style-type: none; margin: 0; padding: 0; }\
	div#mr_list #relTable td ul li { width: 100%; padding: 0; margin: 0; }\
	div#mr_list #relTable td ul li > a { display: block; width: 285px; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; }\
	div#mr_list a, div#mr_list a:visited { color: #1969cb; text-decoration: none; }\
	div#mr_list a:hover { color: #2d7de0; text-decoration: underline; }\
').appendTo('head');

main(); 

})();