// ==UserScript==
// @name MAL (MyAnimeList) Dubs
// @namespace https://github.com/MAL-Dubs
// @version 0.9.03
// @description Labels English dubbed titles on MyAnimeList.net and adds dub only filtering to search, seasonal and top anime pages.
// @author MAL Dubs
// @supportURL https://github.com/MAL-Dubs/MAL-Dubs/issues
// @match http://myanimelist.net/*
// @match https://myanimelist.net/*
// @match http://*.myanimelist.net/*
// @match https://*.myanimelist.net/*
// @iconURL https://raw.githubusercontent.com/MAL-Dubs/MAL-Dubs/main/images/icon.png
// @license GPL version 3.0; http://www.gnu.org/licenses/gpl-3.0.txt
// @resource CSS https://raw.githubusercontent.com/MAL-Dubs/MAL-Dubs/main/css/style.css
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @grant GM_getResourceText
// @connect githubusercontent.com
// @connect github.com
// @run-at document-end
// @noframes
// ==/UserScript==
(function() {
const dubbedLinks = document.querySelectorAll("p.title-text>a,content-result .information>a:first-child,.list>.information>a:first-child,table.anime_detail_related_anime a[href^='/anime'],p.data a.title,td.data.title a:first-child,.list td:nth-child(2)>a.hoverinfo_trigger,#content>table>tbody>tr>td>table>tbody>tr>td.borderClass>a[href*='myanimelist.net/anime/'],#content>div>div>table>tbody>tr>td>a[href*='/anime'],#content>table>tbody>tr>td:nth-child(2)>div.js-scrollfix-bottom-rel>div>table>tbody>tr>td:nth-child(2)>div:nth-child(2)>a:nth-child(1),.news-container h2 a[href*='/anime/'],li.ranking-unit>div>h3>a,tr.ranking-list>td:nth-child(2)>div>div>h3>a,div.borderClass>a,#content>table>tbody>tr>td:nth-child(1)>a:nth-child(1),[id^='#revAreaItemTrigger'],.news-container a[href^='https://myanimelist.net/anime/'],.animeography>.title>a,#content>div:nth-child(3)>a"),
dubbedRecs = document.querySelectorAll("div#anime_recommendation>div.anime-slide-outer>.anime-slide>li.btn-anime>a.link:not([href*='suggestion'])"),
dubbedThumbs = "div.auto-recommendations>div.items>a.item,div.recommendations div.items>a.item,div#widget-seasonal-video li.btn-anime>a.link,div#anime_recommendation li.btn-anime.auto>a.link,.js-seasonal-anime>.image>a:nth-child(1)",
rgx = /^(https?:\/\/myanimelist\.net)?\/?anime\/(\d+)\/?.*/,
recrgx = /^(https?:\/\/myanimelist\.net)?\/recommendations\/anime\/(\d+\-\d+)\/?.*/,
filteruri = /.*\/(((anime\.php\?(?!id).+|topanime\.php.*))|anime\/(genre|producer|season)\/?.*)/;
const IDURL = `https://raw.githubusercontent.com/MAL-Dubs/MAL-Dubs/main/data/dubIDs.json`,
incompleteDubs = [122,170,235,250,516,687,738,918,966,967,1486,2280,7674,8687,10033,40010];
GM_addStyle(GM_getResourceText('CSS'));
var dubbedIDs = JSON.parse(localStorage.getItem('dubIDs'));
if(dubbedIDs === null){
GM_xmlhttpRequest({
method: "GET",
nocache: true,
url: IDURL,
onload: function(response) {
dubbedIDs = JSON.parse(response.responseText);
localStorage.setItem('dubIDs',response.responseText);
onComplete();
localStorage.setItem('dubCacheDate',Date.now());
}
});
} else {
onComplete();
dubCache();
}
function onComplete() {
if (document.location.href.match(/.*\/animelist\/.*/)) {
setTimeout(()=>parseList(),0);
} else {
parseSite();
if (document.location.href.match(filteruri)) {searchFilter();}
if (document.location.href.match(rgx)) {animePages();}
setTimeout(()=>labelThumbnails(),400);
}
}
function dubCache() {
if (localStorage.getItem('dubCacheDate') === null){localStorage.setItem('dubCacheDate',Date.now());}
if(parseInt(localStorage.getItem('dubCacheDate'))+600000<Date.now()){
GM_xmlhttpRequest({
method: "GET",
url: IDURL,
nocache: true,
revalidate: true,
onload: function(response) {
localStorage.setItem('dubIDs',response.responseText);
localStorage.setItem('dubCacheDate',Date.now());
}
});
}
}
function labelDub(anime) {
if (rgx.test(anime.href)) {
var linkID = parseInt(anime.href.match(/\/(\d+)\//)[1]);
if (dubbedIDs.includes(linkID)) {
anime.title = "Dubbed";
if (incompleteDubs.includes(linkID)) {anime.title = "Incomplete Dub";}
} else {anime.title = "Undubbed";}
}
}
function parseSite() {
labelThumbnails();
dubbedLinks.forEach (e => {labelDub(e);});
}
function animePages() {
var thispage = rgx.exec(document.location.href)[2];
if (dubbedIDs.includes(parseInt(thispage))) {
var pagetitle = document.querySelectorAll("h1.title-name")[0];
pagetitle.title = "Dubbed";
if (incompleteDubs.includes(parseInt(thispage))) {pagetitle.title = "Incomplete Dub";}
}
dubbedRecs.foreach ( e => {
var recID = parseInt(recrgx.exec(e)[2].replace(thispage+"-", "").replace("-"+thispage,""));
if (dubbedIDs.includes(recID)) {
e.title = "Dubbed";
e.classList.add("imagelink");
if (incompleteDubs.includes(recID)) {e.title = "Incomplete Dub";}
}
});
}
function scanList() {
let listEntries = document.querySelectorAll('#list-container>div.list-block>div>table>tbody[class=list-item]>tr.list-table-data>td.data.title>a.link,div#list_surround>table>tbody>tr>td>a.animetitle');
listEntries.forEach( e => {
if (e.classList.contains('checked')) {
return true;
} else {
labelDub(e);
e.classList.add('checked');
}
});
}
function addScrollListener() {
document.addEventListener("scroll", function scroll(event) {
event.currentTarget.removeEventListener(event.type, scroll);
scanList();
setTimeout(() => addScrollListener(), 1000);
});
}
function parseList() {
setTimeout(function() {
scanList();
addScrollListener();
}, 50);
}
function labelThumbnails() {
document.querySelectorAll(dubbedThumbs).forEach(e => {
labelDub(e);
e.classList.add("imagelink");
});
}
function hideUndubbed(links) {
links.forEach(e => {
if(document.location.href.match(/.*\/topanime\.php.*/)){
e.parentNode.parentNode.parentNode.parentNode.parentNode.classList.add("hidden");
} else {e.parentNode.parentNode.classList.add("hidden");}
});
}
function showUndubbed(links) {
links.forEach(e => {
if(document.location.href.match(/.*\/topanime\.php.*/)){
e.parentNode.parentNode.parentNode.parentNode.parentNode.classList.remove("hidden");
} else {e.parentNode.parentNode.classList.remove("hidden");}
});
}
function searchFilter() {
let undubbed = document.querySelectorAll('[title="Undubbed"]'),
filterTarget = document.querySelector('.js-search-filter-block>div.fl-r.di-ib.mt4.mr12,div.horiznav-nav-seasonal>span[data-id="sort"],h2.top-rank-header2>span.fs10.fw-n.ff-Verdana.di-ib.ml16,.normal_header.js-search-filter-block>.di-ib,.normal_header>div.fl-r.di-ib.fs11.fw-n'),
filterCheckbox = document.createElement('input'),
label = document.createElement('label');
if(filterTarget !== null){
filterCheckbox.type = 'checkbox';
filterCheckbox.id = 'undubbed-filter';
label.setAttribute('for','undubbed-filter');
label.className = 'fs11 fl-r btn-show-undubbed mr12 fw-n';
label.appendChild(document.createTextNode('Dub Only'));
filterTarget.after(filterCheckbox);
filterCheckbox.after(label);
}
let filter = true;
let filterUndubbed = (localStorage.getItem('dubOnlySearch') === "true");
if(filterUndubbed === null){
filterUndubbed = false;
localStorage.setItem('dubOnlySearch', filterUndubbed);
filterCheckbox.checked = filterUndubbed;
filter = false;
}
if (filter) {
filterCheckbox.checked = filterUndubbed;
if(filterCheckbox.checked === true) {hideUndubbed(undubbed);}
}
filterCheckbox.addEventListener('change',function () {
if(filterCheckbox.checked === true){hideUndubbed(undubbed);}
if(filterCheckbox.checked === false){showUndubbed(undubbed);}
localStorage.setItem('dubOnlySearch', filterCheckbox.checked);
},false);
}
})();