// ==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.1
// @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; // ms
const AJAX_TIMEOUT = 5000; // ms
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(' ')
.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> | </span>')
.append($('<a href="#mr_body">Missing Relations</a>').fancybox({
'hideOnContentClick': false,
'hideOnOverlayClick': true,
'titleShow': false,
'transitionIn': 'none',
'transitionOut': 'none',
'scrolling': 'no'
}))
.append(' (')
.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(') ').append('<span id="mr_status_done" style="color: green;"></span>')
.append(' ').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('');
$('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) {
var lComp = a.lTitle.toLowerCase().localeCompare(b.lTitle.toLowerCase());
var rComp = a.rTitle.toLowerCase().localeCompare(b.rTitle.toLowerCase());
if (lComp === 0) {
return b.rId < 0 ? true : rComp;
}
return lComp;
});
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);
}
})();