// ==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.5
// @author akarin
// @include /^http:\/\/myanimelist\.net\/panel\.php(\?go=editmanga)?$/
// @include /^http:\/\/myanimelist\.net\/editlist\.php\?type=anime$/
// @grant none
// @noframes
// ==/UserScript==
(function($) {
var mal = {
nickname: '',
version: '',
cache: '3.1',
page: {},
type: document.URL.match(/\?type=anime$/) ? 'anime' : 'manga',
ajax: { delay: 300, timeout: 5000, url: 'http://myanimelist.net/' },
content: { body: $(), list: $(), done: $(), fail: $() },
entries: { done: 0, fail: 0, total: 0, left: {}, right: [], ignore: {} }
};
mal.main = function(version) {
if ($('#malLogin').length > 0) {
return;
}
mal.nickname = $('ul#nav li:first > ul > li > a:contains(Profile)').prop('href').match(/(?!.*\/).*$/)[0];
mal.version = version;
mal.content.body = $('<div id="mr_body"><h2 id="mr_body_title">Missing Relations <span><a href="http://greasyfork.org/scripts/9261" target="_blank">v' + mal.version + '</a></span></h2></div>');
mal.content.list = $('<div id="mr_list"></div>').appendTo(mal.content.body);
$('<div></div>').hide().append(mal.content.body).insertAfter('#contentWrapper');
if (document.URL.match(/^http:\/\/myanimelist\.net\/panel\.php$/)) {
mal.page.panel();
}
else {
mal.page.list();
}
};
mal.page.panel = function() {
var animeLink = $('<a href="#mr_body" style="font-size: 11px; font-weight: normal;">Missing Anime Relations</a>')
.fancybox({
'hideOnContentClick': false,
'hideOnOverlayClick': true,
'transitionIn': 'none',
'transitionOut': 'none',
'titleShow': false,
'scrolling': 'no',
'onStart': function() {
mal.type = 'anime';
if (mal.loadValue('mal.cache', '') === mal.cache) {
mal.entries.load();
}
mal.content.update(false);
}
});
var mangaLink = $('<a href="#mr_body" style="font-size: 11px; font-weight: normal;">Missing ' + (mal.type === 'anime' ? 'Anime' : 'Manga') + ' Relations</a>')
.fancybox({
'hideOnContentClick': false,
'hideOnOverlayClick': true,
'transitionIn': 'none',
'transitionOut': 'none',
'titleShow': false,
'scrolling': 'no',
'onStart': function() {
mal.type = 'manga';
if (mal.loadValue('mal.cache', '') === mal.cache) {
mal.entries.load();
}
mal.content.update(false);
}
});
$('<div class="floatRightHeader"></div>')
.append(animeLink)
.append(' - ')
.append(mangaLink)
.appendTo('#panel_left > .container > div.bgColor1:first-of-type + .spaceit');
};
mal.page.list = function() {
$.ajaxSetup({ timeout: mal.ajax.timeout });
if (mal.loadValue('mal.cache', '') !== mal.cache) {
mal.entries.save(); // save empty data: clear
}
mal.content.done = $('<span id="mr_status_done" style="color: green;"></span>');
mal.content.fail = $('<span id="mr_status_fail" style="color: #c32;"></span>');
mal.entries.load();
mal.content.update(false);
$('<span id="mr_link"></span>')
.append('<span> | </span>')
.append($('<a href="#mr_body">Missing Relations</a>').fancybox({
'hideOnContentClick': false,
'hideOnOverlayClick': true,
'transitionIn': 'none',
'transitionOut': 'none',
'titleShow': false,
'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?')) {
mal.entries.update();
}
})
))
.append(') ').append(mal.content.done)
.append(' ').append(mal.content.fail)
.appendTo('#content > div:first');
};
mal.entries.updating = function() {
return (mal.entries.done + mal.entries.fail) !== mal.entries.total ? true : false;
};
mal.entries.load = function() {
mal.entries.clear();
mal.entries.left = mal.loadValue('mal.entries.left', mal.entries.left);
mal.entries.right = mal.loadValue('mal.entries.right', mal.entries.right);
mal.entries.ignore = mal.loadValue('mal.entries.ignore', mal.entries.ignore);
};
mal.entries.save = function() {
mal.saveValue('mal.cache', mal.cache);
mal.saveValue('mal.entries.left', mal.entries.left);
mal.saveValue('mal.entries.right', mal.entries.right);
mal.saveValue('mal.entries.ignore', mal.entries.ignore);
};
mal.entries.clear = function() {
mal.entries.done = 0;
mal.entries.fail = 0;
mal.entries.total = 0;
mal.entries.left = {};
mal.entries.right = [];
//mal.entries.ignore = {};
};
mal.entries.update = function() {
if (mal.entries.updating()) {
return;
}
var links = $('#content strong + a');
mal.entries.clear();
mal.entries.total = links.length;
mal.content.done.text('');
mal.content.fail.text('');
$('#content span[class^="mr_status_id_"]').empty();
if (links.length === 0) {
mal.content.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.type + '/' + id)
.done(function(data) {
var title = data.match(/<div id="contentWrapper">[\s\S]*?<h1><div [\s\S]*?<\/div>(.+?)</)[1].trim();
mal.setRelations(id, title, data);
if (!mal.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.done) + '/' + mal.entries.total);
mal.content.update(true);
})
.fail(function(data) {
span.html('<small style="color: red;">fail</small>');
mal.content.fail.text('fail: ' + (++mal.entries.fail));
mal.content.update(true);
});
}, mal.ajax.delay * index);
});
};
mal.setRelations = function(id, title, data) {
var cnRe = />Related (Anime|Manga)<\/h2>[\s\S]*?<h2>/;
var idRe = new RegExp('/' + mal.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() });
}
});
};
mal.checkCorrectInfo = function(id, data) {
var myStatusData = data.match(/selected>(.*?)<\/option/);
if (myStatusData === null) {
return false;
}
var status = data.match(/>Status:<\/span>([\s\S]*?)<\/div>/)[1].trim();
var myStatus = myStatusData[1].trim();
if (mal.type === '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;
};
mal.content.update = function(save) {
if (mal.entries.updating()) {
return;
}
if (save === true) {
mal.entries.save();
}
var title = $('#mr_body_title', mal.content.body);
title.html(title.html().replace(/Missing (.*?)Relations/, 'Missing ' + (mal.type === 'anime' ? 'Anime' : 'Manga') + ' Relations'));
if (mal.entries.right.length === 0) {
var p = $('<p>No missing relations found.</p>');
if (document.URL.match(/^http:\/\/myanimelist\.net\/panel\.php$/)) {
p.append(' ').append('<a href="' + (mal.type === 'anime' ? '/editlist.php?type=anime' : '/panel.php?go=editmanga') + '">Refresh</a>');
}
mal.content.list.empty().append(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>');
var undel = $('<div id="mr_undelete"></div>').html('<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>');
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>'),
lcount = 0, rcount = 0;
$.each(right, function(index, rel) {
// red relation
if (parseInt(rel.rId) < 0) {
// add left
$('<li></li>')
.append(mal.getLineCount(++lcount))
.append(mal.getEntryLink(rel.lId, rel.lTitle))
.appendTo(ulLeft);
// add right
if (ulRight.children().length === 0) {
$('<li></li>')
.prop('id', 'mr_li_red')
.append(mal.getEntryWarning())
.appendTo(ulRight);
}
}
// normal relation
else {
// add left
if (ulLeft.children().length === 0) {
$('<li></li>')
.append(mal.getHideButton('window.hideRow(\'' + rel.lId + '\');', 'Hide all relations'))
.append(mal.getLineCount(++lcount))
.append(mal.getEntryLink(rel.lId, rel.lTitle))
.appendTo(ulLeft);
}
// add right
$('<li></li>')
.prop('id', 'mr_li_' + rel.rId)
.append(mal.getHideButton('window.hideRelation(\'' + rel.rId + '\');', 'Hide this relation'))
.append(mal.getLineCount(++rcount))
.append(mal.getEntryLink(rel.rId, rel.rTitle))
.appendTo(ulRight);
}
// if this is a last relation
if (index + 1 === right.length ||
// or this is a last relation in a group
(parseInt(right[index + 1].rId) > -1 && (rel.rId < 0 || rel.lId !== right[index + 1].lId))) {
$('<tr id="' + (rel.rId > 0 ? ('mr_tr_' + rel.lId) : 'mr_tr_red') + '"></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>');
}
});
mal.updateIgnoredRelations(right);
mal.content.list.empty().append(undel).append(table);
mal.hideIgnoredRelations();
mal.updateLineCount();
};
mal.getEntryLink = function(id, title) {
return $('<a title="' + title + '"href="' + mal.ajax.url + mal.type + '/' + id + '" target="_blank">' + title + '</a>');
};
mal.getEntryWarning = function() {
return $('<div class="mr_warning"><span>Wrong status or ' + (mal.type === 'anime' ? 'episode' : 'vol./chap.') + ' count</span></div>');
};
mal.getHideButton = function(func, title) {
return $('<div class="mr_hide"><span><a href="javascript:void(0);" title="' + title + '" onclick="' + func + '">x</a></span></div>');
};
mal.getLineCount = function(index) {
return $('<div class="mr_count"><span>' + index + '</span></div>');
};
mal.hideRow = function(id, save) {
var tr = $('tr[id="mr_tr_' + id + '"]', mal.content.list);
if (tr.length === 0) {
return;
}
$('td.mr_proposed li', tr).hide().each(function() {
var rId = $(this).prop('id').match(/\d+/)[0];
mal.entries.ignore[rId] = true;
});
tr.hide();
if (save === true) {
mal.saveValue('mal.entries.ignore', mal.entries.ignore);
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);
}
};
mal.hideRelation = function(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 = $('td.mr_proposed li:not([style*="display: none;"])', tr).length;
tr.toggle(count > 0);
mal.entries.ignore[id] = true;
if (save === true) {
mal.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);
}
};
mal.hideIgnoredRelations = function() {
$.each(mal.entries.ignore, function(id) {
mal.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);
};
mal.showIgnoredRelations = function() {
$('#mr_undelete_msg', mal.content.list).hide();
$('tr[id^="mr_tr_"]', mal.content.list).show();
$('li[id^="mr_li_"]', mal.content.list).show();
mal.saveValue('mal.entries.ignore', (mal.entries.ignore = {}));
};
mal.updateIgnoredRelations = function(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];
}
});
mal.saveValue('mal.entries.ignore', mal.entries.ignore);
};
mal.updateLineCount = function() {
var lcount = 0, rcount = 0;
$('#relTable tr:not([style*="display: none"])', mal.content.list).each(function() {
$('td.mr_subject .mr_count span', this).each(function() {
$(this).text(++lcount);
});
$('td.mr_proposed .mr_count span', this).each(function() {
$(this).text(++rcount);
});
});
};
mal.loadValue = function(key, value) {
try {
value = JSON.parse(localStorage.getItem(mal.nickname + '#' + mal.type + '#' + key)) || value;
} catch (e) {}
return value;
};
mal.saveValue = function(key, value) {
localStorage.setItem(mal.nickname + '#' + mal.type + '#' + key, JSON.stringify(value));
};
window.hideRow = function(id) {
mal.hideRow(id, true);
mal.updateLineCount();
};
window.hideRelation = function(id) {
mal.hideRelation(id, true);
mal.updateLineCount();
};
window.showIgnoredRelations = function() {
mal.showIgnoredRelations();
mal.updateLineCount();
};
$('<style type="text/css" />').html(
'#content span[class^="mr_status_id_"] { float: right; padding: 0 7px; }' +
'div#mr_body { text-align: center; width: 670px; height: auto; }' +
'div#mr_body a, div#mr_body a:visited { color: #1969cb; text-decoration: none; }' +
'div#mr_body a:hover { color: #2d7de0; text-decoration: underline; }' +
'div#mr_body #mr_body_title { font-size: 16px; font-weight: normal; text-align: center; margin: 0 0 8px 0; position: relative; border: 0; }' +
'div#mr_body #mr_body_title span { font-size: 12px; font-weight: normal; }' +
'div#mr_body #mr_body_title:after { content: ""; 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; }' +
'div#mr_body div#mr_undelete { background-color: #fff; padding: 0; margin: 0; }' +
'div#mr_body p#mr_undelete_msg { margin: 0 0 10px; font-weight: normal; text-align: center; line-height: 20px; font-size: 13px; }' +
'div#mr_list { width: auto; height: 680px; overflow-y: auto; margin: 18px auto 0; }' +
'div#mr_list #relTable { border: 1px solid #f5f5f5; }' +
'div#mr_list #relTable tr td { background-color: #fff; width: 50%; padding: 5px 0; font-weight: normal; text-align: left; box-shadow: 0px 1em 1em -1em #ddd inset; vertical-align: top; line-height: 20px !important; font-size: 13px; }' +
'div#mr_list #relTable tr:hover td { background-color: #f5f5f5; }' +
'div#mr_list #relTable td.mr_subject { border-right: 1px solid #f5f5f5; }' +
'div#mr_list #relTable td.mr_table_header { background-color: #f5f5f5; font-size: 12px; font-weight: bold; padding-left: 6px; box-shadow: none; }' +
'div#mr_list #relTable td .mr_warning { width: 260px; color: #e43; font-size: 12px; font-weight: bold; text-align: left; padding-left: 35px; }' +
'div#mr_list #relTable td .mr_hide { display: inline-block !important; width: 15px; float: right; text-align: left; font-size: 11px; }' +
'div#mr_list #relTable td .mr_hide a { color: #56c; line-height: 20px !important; font-style: normal !important; }' +
'div#mr_list #relTable td .mr_count { display: inline-block !important; width: 35px; float: left; text-align: center; font-size: 11px; color: #666; }' +
'div#mr_list #relTable td div span { line-height: 20px !important; }' +
'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: 265px !important; line-height: 20px !important; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; }'
).appendTo('head');
mal.main(GM_info.script.version);
})(jQuery);