MyAnimeList (MAL) Track Missing Relations

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

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

// ==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      2.1.0
// @author       akarin
// @include      /^http\:\/\/myanimelist\.net\/editlist\.php\?type=anime$/
// @include      /^http\:\/\/myanimelist\.net\/panel\.php\?go=editmanga$/
// @grant        none
// @noframes
// ==/UserScript==

;(function() {	
	if ($('#malLogin').length > 0) {
		return;
	}

	const AJAX_DELAY = 300;
	const AJAX_TIMEOUT = 5000;
	
	const ANIME_T = 'anime', MANGA_T = 'manga';
	const LIST_TYPE = document.URL.match(/\?type=anime$/) ? ANIME_T : MANGA_T;
	
	const ANIME_CACHE = '2.0', MANGA_CACHE = '2.0';
	const LIST_CACHE = LIST_TYPE === ANIME_T ? ANIME_CACHE : MANGA_CACHE;
	
	const NICKNAME = $('ul#nav li:first > ul > li > a:contains(Profile)').prop('href').match(/(?!.*\/).*$/)[0];
	const MAL_PREFIX = 'http://myanimelist.net/';
	
	$.ajaxSetup({timeout: AJAX_TIMEOUT});
	checkCache(LIST_CACHE);
	applyCss();
	
	var total, calc = -1, fail = 0;
	var left = [], right = [], ignore = loadValue('ignoreList', '').split(';');
	
	for (var i = 0; i < ignore.length; ++i) {
		if (ignore[i].trim().length === 0) {
			ignore.splice(i--, 1);
		}
	}
	
	var $relList = $('<div id="mr_list"></div>');	
	var $relBody = $('<div id="mr_body"></div>')
		.append($('<h2 id="mr_body_title">Missing Relations</h2>')
			.append('&nbsp;')
			.append($('<span></span>')
				.append('<a href="http://greasyfork.org/scripts/9261" target="_blank">v' + GM_info.script.version + '</a>')
			)
		)
		.append($relList);
	
	var content = loadValue('relList', '');
	if (content.length > 0) {
		$relList.html(content);
		hideIgnoredRelations();
	}
	else {
		$relList.html('<br><small>No missing relations found.</small>');
	}
	
	$('<div style="display: none;"></div>').append($relBody).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,
			'titleShow': false,
			'transitionIn': 'none', 
			'transitionOut': 'none',
			'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($('#content'));
				}
			})
		))
		.append(')&nbsp;').append('<span id="mr_status_done" style="color: #3c2;"></span>')
		.append('&nbsp;').append('<span id="mr_status_fail" style="color: #c32;"></span>')
		.appendTo('#content > div:first');
		
	function checkCache(ver) {
		var v = loadValue('cache', null);
		if (v === null || v !== ver) {
			saveValue('cache', ver);
			saveValue('relList', '');
		}
	}
		
	function recalculate(context) {		
		if (calc > -1) {
			return;
		}
		
		var $links = $('strong + a', context);
		total = $links.length;
		
		if (total === 0) {
			$relList.html('<br><small>No missing relations found.</small>');
			return;
		}
		
		calc = total;
		fail = 0;
		$('span#mr_status_done').text('done: ' + (total - calc) + '/' + total);
		$('span#mr_status_fail').text('');
		$('span[id^="mr_id_"]').empty();
		left = [];
		right = [];
		
		$relList.html('<br><small>Calculating missing relations...</small>');
		
		$links.each(function(index) {
			var id = $(this).prop('href').match(/\d+$/)[0];
			var $span = $(this).parent().find('span[id="mr_' + id + '"]');
			if ($span.length === 0) {
				$('<span id="mr_id_' + id + '" style="float: right; margin: 0 7px;"></span>').appendTo($(this).parent());
			}
			left.push(parseInt(id));
			setTimeout(function() { 
				$.ajax('/' + LIST_TYPE + '/' + id)
					.done(function(data) {
						--calc;
						checkRelations(id, data);
						checkCorrectInfo(id, data);
						$('span[id="mr_id_' + id + '"]').html('<small style="color: green;">done</small>');
						$('span#mr_status_done').text('done: ' + (total - calc) + '/' + total);
						finalize();
					})
					.fail(function(data) {
						--calc;
						$('span[id="mr_id_' + id + '"]').html('<small style="color: red;">fail</small>');
						$('span#mr_status_fail').text('fail: ' + (++fail));
						finalize();
					});
			}, AJAX_DELAY * index);
		});
	}
	
	function checkRelations(id, data) {		
		var title = data.match(/<div id="contentWrapper">[\s\S]*?<h1><div [\s\S]*?<\/div>(.+?)</)[1].trim();
		var cnEx = LIST_TYPE === ANIME_T ? /<h2>Related Anime<\/h2>[\s\S]*?<h2>/ : /<h2>Related Manga<\/h2>[\s\S]*?<h2>/;
		var idEx = LIST_TYPE === ANIME_T ? /\/anime\/(\d+)\// : /\/manga\/(\d+)\//;
		
		$('a', '<context>' + data.match(cnEx) + '</context>').each(function() {
			var idData = $(this).prop('href').match(idEx);
			if (idData !== null && idData.length > 1) {
				right.push({
					lId: parseInt(id),
					lTitle: title,
					rId: parseInt(idData[1]),
					rTitle: $(this).text().trim()
				});
			}
		});
	}
	
	function checkCorrectInfo(id, data) {
		var correct = true;
		var title = data.match(/<div id="contentWrapper">[\s\S]*?<h1><div [\s\S]*?<\/div>(.+?)</)[1].trim();
		do {
			var status = data.match(/>Status:<\/span>([\s\S]*?)<\/div>/)[1].trim();
			var myStatus = data.match(/selected>(.*?)<\/option/)[1].trim();
			
			if (status === (LIST_TYPE === ANIME_T ? 'Not yet aired' : 'Not yet published')) {
				if (myStatus !== (LIST_TYPE === ANIME_T ? 'Plan to Watch' : 'Plan to Read')) {
					correct = false;
					break;
				}
			}
			else if (status === (LIST_TYPE === ANIME_T ? 'Currently Airing' : 'Publishing')) {
				if (myStatus === 'Completed') {
					correct = false;
					break;
				}
			}
			
			if (LIST_TYPE === ANIME_T) {
				var eps = data.match(/>Episodes:<\/span>([\s\S]*?)<\/div>/)[1].trim();
				var myEps = data.match(/name="myinfo_watchedeps"[\s\S]*?value="(\d*)"/)[1].trim();
				if (eps !== 'Unknown') {
					if (parseInt(myEps) > parseInt(eps) || (myStatus === 'Completed' && parseInt(myEps) !== parseInt(eps))) {
						correct = false;
						break;
					}
				}
			}
			else {
				var vols = data.match(/>Volumes:<\/span>([\s\S]*?)<\/div>/)[1].trim();
				var myVols = data.match(/id="myinfo_volumes"[\s\S]*?value="(\d*)"/)[1].trim();
				if (vols !== 'Unknown') {
					if (parseInt(myVols) > parseInt(vols) || (myStatus === 'Completed' && parseInt(myVols) !== parseInt(vols))) {
						correct = false;
						break;
					}
				}
				var chap = data.match(/>Chapters:<\/span>([\s\S]*?)<\/div>/)[1].trim();
				var myChap = data.match(/id="myinfo_chapters"[\s\S]*?value="(\d*)"/)[1].trim();
				if (chap !== 'Unknown') {
					if (parseInt(myChap) > parseInt(chap) || (myStatus === 'Completed' && parseInt(myChap) !== parseInt(chap))) {
						correct = false;
						break;
					}
				}
			}
		} while (false);
		
		if (correct === false) {
			right.push({ 
				lId: parseInt(id),	
				lTitle: title, 
				rId: -1,	
				rTitle: '' 
			});
		}
	}
	
	function finalize() {
		if (calc > 0) {
			return;
		}
		
		if (right.length === 0) {
			$relList.html('<br><small>No missing relations found.</small>');
			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>').append($('<td colspan="2"></td>')
			.append($('<p id="mr_undelete_msg" style="display: none;">There are <span id="mr_undelete_num" style="font-weight: bold;"></span> hidden relations. </p>').append($('<a href="javascript:void(0);" title="Show ignored relations" onclick="window.showIgnoredRelations();">Show them</a>')))
		).prependTo($table);
			
		right.sort(function(a, b) {
			return a.rId > b.rId;
		});
			
		var prev = -1;
		for (var i = 0; i < right.length; ++i) {
			if (right[i].rId < 0) {
				continue;
			}
			if (right[i].rId === prev || left.indexOf(right[i].rId) > -1) {
				right.splice(i--, 1);
			}
			else {
				prev = right[i].rId;
			}
		}
			
		right.sort(function(a, b) {
			if (a.lTitle === b.lTitle) {
				return b.rId < 0 || a.rTitle > b.rTitle;
			}
			return a.lTitle > b.lTitle;
		});
			
		var $ul = $('<ul></ul>');
		var $ulLeft = $ul.clone(), $ulRight = $ul.clone();
		
		$.each(right, function(index, rel) {
			var $liLeft = $('<li><a title="' + rel.lTitle + '"href="' + MAL_PREFIX + LIST_TYPE + '/' + rel.lId + '" target="_blank">' + rel.lTitle + '</a></li>');
			var $liRight = $('<li></li>');
			
			if (rel.rId < 0) {
				$liRight.html('<div class="mr_warning">Wrong status or ' + (LIST_TYPE === ANIME_T ? 'episode' : 'chapter') + ' count</div>');
			}
			else {
				$liRight.html('<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_PREFIX + LIST_TYPE + '/' + rel.rId + '" target="_blank">' + 
						rel.rTitle + '</a>');
			}
			
			if ($ulLeft.children().length === 0) {
				$ulLeft.append($liLeft);
			}
			$ulRight.append($liRight.prop('id', 'mr_li_' + rel.rId));
			
			if (index + 1 === right.length || 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.clone();
				$ulRight = $ul.clone();
			}
		});
			
		$relList.empty().append($table);
		saveValue('relList', $relList.html());	
		hideIgnoredRelations();
		calc = -1;
	}
	
	window.hideRelation = function(id) {
		hideRelation(id, true);
	};
	
	window.showIgnoredRelations = function() {
		showIgnoredRelations();
	};
	
	function hideRelation(id, save) {
		var $li = $('li[id="mr_li_' + id + '"]', $relList);
		if ($li.length === 0) {
			return;
		}
		
		$li.hide();
		var $tr = $li.closest('tr');
		
		var count = 0;
		$('td.mr_proposed li', $tr).each(function() {
			if ($(this).css('display') !== 'none') {
				++count;
			}
		});
		
		$tr.toggle(count > 0);
		
		if (save === true && ignore.indexOf(id) < 0) {
			ignore.push(id);
			saveValue('ignoreList', ignore.join(';'));
		}		
		
		$('#mr_undelete_num', $relList).text(ignore.length);
		$('#mr_undelete_msg', $relList).toggle(ignore.length > 0);
	}
	
	function hideIgnoredRelations() {
		$.each(ignore, function(index, id) {
			hideRelation(id, false);
		});
	}
	
	function showIgnoredRelations() {
		$('#mr_undelete_msg', $relList).hide();
		ignore = [];
		$('tr[id^="mr_tr_"]', $relList).show();
		$('li[id^="mr_li_"]', $relList).show();
		saveValue('ignoreList', '');
	}
	
	function applyCss() {	
		$('<style type="text/css" />').html('\
			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: 10px 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\
				{ color: #e43; font-weight: bold; }\
			div#mr_list #relTable td.mr_proposed div.mr_hide\
				{ position: absolute; right: 4px; color: #c32; }\
			div#mr_list #relTable td ul\
				{ list-style-type: none; margin: 0; padding: 0; }\
			div#mr_list #relTable td ul li\
				{ width: 285px; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; padding-right: 15px; position: relative; }\
			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');
	}
	
	function loadValue(name, defaultValue) {
		var value = localStorage.getItem(NICKNAME + '#' + LIST_TYPE + '#' + name);
		return value ? value : defaultValue;
	}

	function saveValue(name, value) {
		localStorage.setItem(NICKNAME + '#' + LIST_TYPE + '#' + name, value);
	}  
})();