// ==UserScript==
// @name MALstreaming
// @namespace https://github.com/mattiadr/MALstreaming
// @version 5.26
// @author https://github.com/mattiadr
// @description Adds various anime and manga links to MAL
// @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JQAAgIMAAPn/AACA6QAAdTAAAOpgAAA6mAAAF2+SX8VGAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3wQRDic4ysC1kQAAA+lJREFUWMPtlk1sVFUUx3/n3vvmvU6nnXbESkTCR9DYCCQSFqQiMdEY4zeJuiBhwUISAyaIHzHGaDTxKyzEr6ULNboiRonRhQrRCMhGiDFGA+WjhQ4NVKbtzJuP9969Lt4wlGnBxk03vZv3cu495/7u/5x7cmX1xk8dczjUXG4+DzAPMA8AYNoNIunXudnZ2+enrvkvn2kADkhiiwM8o6YEEuLE4pxDK0GakZUIoiCOHXFiW2uNEqyjZdNaIbMB0Ero7gwQ4OJEDa0VSoR6lNDT5eMZRaUa0YgSjFZU6zG1ekK+y6er00eJECWWchiRMYp8VwBAOYyw1l0dQIlQrcfcvKSHT968j+5chg+/OMoHnx9FCdwzsIRdz24gGxhe2v0Le74/htaKFYvzbNm4knWrF3J9IYtSQq0e8+C2r+jwDXvefYjEWja98B2DQyU6fINty8cVCigl9HYHiMCOzWs4/HuR4XNl3n5mPbmsB0DgGyYrDR69ewXvvXgXgW+oNxLOX6ySJJaebp/+ZQWOD5fIZT2cS5WddRGCw9oU5rVtA1SqEfmcTxRZPE8RxZbe7oBXnlpH4BtGx0Ke2PkNt624jte3DzBWqjF4ZhzP6GYBOtw1qtC07Y2I0IgTisUKtyztBaB4voLWQl8hS1iLuL2/j0V9OQC+/fkkx4ZK3L9hGQt6Oyj0BCiR1qZpwV5dgRn7gBLh1Y8OcmpkAoDndv3E6IUQgCRx9BWy6b91bH64n7P7tvL8lrU4l/pOi6dSRZWSaShmJgDPKIbPTfLy+wdYfEMXB46M0JXLNE8ElWoEQK0e8/fJi8SJpa+QZemi7hmiOSphxESlQRRb/IzGKMHNBOCaJwTI53wOHhnBM5pCPqDRSFIHrTh1drzls/2Nffx18h+efGwV7+y8kyi2l+O5VKW1KxeycEEn2Q6PPwfHKE3WMVpwrg1AAK1TkaxzBBlDEGiSxLXsgW84cWacE2fGWX5TnnsHlnB8qEQ2SG+J1qnM0lTLaMVbO+5AJL2ijzy9l7FSDaMV4FIAh0MpoRxGfL1vECRtHiK0Gsj+w8OcHpmkeKFCWIv54dAQWx9fxfo1N/Lxl38wVJzgx1+HCGsx1XoMwN79gy1VfU9zujjB2dFJfE9dLtKpb0JrHeUwzW8u66Gm3N9yGJEkls6sR5I4+pcX2PTArez+7DcmK+lcWIsRgc5mzyhXoivSq5W0+klL9fZH6SWpL9VCy64ERLDW4lyaorAaE2Q0xihE0kqnmfepsaZSJPYanXCmjVt265rnaAKJkM9lsM7hXLPg2nyvFuuaALMdjumn+T9jzh8k8wDzAPMAcw7wLz7iq04ifbsDAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE1LTA0LTE3VDE0OjM5OjU2LTA0OjAw6I0f5AAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNS0wNC0xN1QxNDozOTo1Ni0wNDowMJnQp1gAAAAASUVORK5CYII=
// @run-at document-idle
// @supportURL https://github.com/mattiadr/MALstreaming/issues
// @match https://myanimelist.net/animelist/*
// @match https://myanimelist.net/ownlist/anime/*/edit*
// @match https://myanimelist.net/ownlist/anime/add?selected_series_id=*
// @match https://myanimelist.net/mangalist/*
// @match https://myanimelist.net/ownlist/manga/*/edit*
// @match https://myanimelist.net/ownlist/manga/add?selected_manga_id=*
// @match http://anichart.net/airing
// @match http://kissanime.ru/
// @match https://kissmanga.com/
// @match https://www.masterani.me/
// @require https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js
// @grant GM_xmlhttpRequest
// @grant GM_openInTab
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addValueChangeListener
// @grant window.close
// ==/UserScript==
/*
HOW TO ADD A NEW STREAMING SERVICE:
- add a new object to the streamingServices array with attributes id (unique id, must be a valid identifier) and name (display name)
- create a new function in getEplistUrl that will simply return the full url from the partial url (saved in comments)
- create a new function in getEpisodes that will accept dataStream and url,
the function needs to callback to putEpisodes(dataStream, episodes, timeMillis)
url is the url of the episode list provoded by getEplistUrl
episodes needs to be an array of object with text and href attributes
timeMillis can optionally be the unix timestamp of the next episode
- create a new function in search that will accept id and title
the function needs to callback to putResults(id, results)
results needs to be an array of object with title (display title), href (the url that will be put in the comments) attributes
and epsiodes (optional number of episodes)
- if other utility is needed, add it in the service section and if you need to run a script on specific pages add another object to the pages array
*/
/* generic */
/*******************************************************************************************************************************************************************/
// contains variable properties for anime/manga modes
let properties = {};
properties.anime = {
mode: "anime",
watching: ".list-unit.watching",
colHeader: "<th class='header-title stream'>Watch</th>",
commentsRegex: /Comments: ([\S ]+)(?= )/g,
iconAdd: ".icon-add-episode",
findProgress: ".data.progress",
findAiring: "span.content-status:contains('Airing')",
latest: "Latest ep is #",
notAired: "Not Yet Aired",
ep: "Ep.",
editPageBox: "#add_anime_comments",
};
properties.manga = {
mode: "manga",
watching: ".list-unit.reading",
colHeader: "<th class='header-title stream'>Read</th>",
commentsRegex: /Comments: ([\S ]+)(?=\n)/g,
iconAdd: ".icon-add-chapter",
findProgress: ".data.chapter",
findAiring: "span.content-status:contains('Publishing')",
latest: "Latest ch is #",
notAired: "Not Yet Published",
ep: "Ch.",
editPageBox: "#add_manga_comments",
};
// contains all functions to execute on page load
const pageLoad = {};
// contains all functions to get the episodes list from the streaming services
// must callback to putEpisodes(dataStream, episodes, timeMillis)
const getEpisodes = {};
// contains all functions to get the episode list url from the partial url
const getEplistUrl = {};
// contains all functions to execute the search on the streaming services
// must callback to putResults(results)
const searchSite = {};
// is an array of valid streaming services names
const streamingServices = [
// anime
{ id: "kissanime", type: "anime", name: "Kissanime", domain: "http://kissanime.ru/" },
{ id: "nineanime", type: "anime", name: "9anime", domain: "https://www1.9anime.to/" },
{ id: "masterani", type: "anime", name: "Masterani.me", domain: "https://www.masterani.me/" },
// manga
{ id: "kissmanga", type: "manga", name: "Kissmanga", domain: "https://kissmanga.com/" },
{ id: "mangadex", type: "manga", name: "MangaDex", domain: "https://mangadex.org/" },
];
// return an array that contains the streaming service and url relative to that service or false if comment is not valid
function getUrlFromComment(comment) {
let c = comment.split(" ");
if (c.length < 2) return false;
for (let i = 0; i < streamingServices.length; i++) {
if (streamingServices[i].id == c[0]) return c;
}
return false;
}
// estimate time before next chapter as min of last n chapters
function estimateTimeMillis(episodes, n) {
let prev = null;
let min = undefined;
for (let i = episodes.length - 1; i > Math.max(0, episodes.length - 1 - n); i--) {
if (!episodes[i]) continue;
if (prev && episodes[i].timestamp != prev) {
let diff = prev - episodes[i].timestamp;
if (!min || diff < min && diff > 0) min = diff;
}
prev = episodes[i].timestamp;
}
return episodes[episodes.length - 1].timestamp + min;
}
// returns the domain for the streaming service or false if ss doesn't exist
function getDomainById(id) {
for (let i = 0; i < streamingServices.length; i++) {
if (streamingServices[i].id == id) {
return streamingServices[i].domain;
}
}
return false;
}
/* anichart */
/*******************************************************************************************************************************************************************/
const anichartUrl = "http://anichart.net/airing";
// puts timeMillis into dataStream, then calls back
function anichart_setTimeMillis(dataStream, canReload) {
let listitem = dataStream.parents(".list-item");
// anime is not airing, exit
if (listitem.find(properties.findAiring).length == 0) return;
let times = GM_getValue("anichartTimes", false);
// get anime id
let id = listitem.find(".data.title > .link").attr("href").split("/")[2];
let t = times ? times[id] : false;
if (times && t && Date.now() < t.timeMillis) {
// time doesn't need to update
// set timeMillis, this is used to check if anichart timer is referring to next episode
dataStream.data("timeMillis", t);
} else {
// add value change listener
let listenerId = GM_addValueChangeListener("anichartTimes", function(name, old_value, new_value, remote) {
// reload, avoid infinite loops
if (canReload) anichart_setTimeMillis(dataStream, false);
// remove listener
GM_removeValueChangeListener(listenerId);
});
// load times from anichart
if (GM_getValue("anichartLoading", false) + 30*1000 < Date.now()) {
// set value then open anichart
GM_setValue("anichartLoading", Date.now());
GM_openInTab(anichartUrl, true);
}
}
}
// function to execute when script is run on anichart
pageLoad["anichart"] = function() {
// get xsrf token from cookies
let xsrf_tok = document.cookie.match(/(?<=XSRF-TOKEN=)\w+/)[0];
// request data
GM_xmlhttpRequest({
method: "GET",
url: "http://anichart.net/api/airing",
headers: { "X-CSRF-TOKEN": xsrf_tok },
onload: function(resp) {
// parse response
let res = JSON.parse(resp.response);
let times = {};
// iterate over day of week
for (let day in res) {
if (res.hasOwnProperty(day)) {
// iterate over array
for (let i = 0; i < res[day].length; i++) {
let entry = res[day][i];
// get id from mal_link
let id = entry.mal_link.match(/\d+$/)[0];
let ep = entry.airing.next_episode;
let timeMillis = entry.airing.time * 1000;
// set time, ep is episode the timer is referring to
times[id] = {
ep: ep,
timeMillis: timeMillis
}
}
}
}
// put times in GM value
GM_setValue("anichartTimes", times);
// finished loading, close only if opened by script
if (GM_getValue("anichartLoading", false)) {
GM_setValue("anichartLoading", false);
window.close();
}
}
});
}
/* kissanime */
/*******************************************************************************************************************************************************************/
const kissanime = {};
kissanime.base = "http://kissanime.ru/";
kissanime.anime = kissanime.base + "Anime/";
kissanime.search = kissanime.base + "Search/SearchSuggestx";
kissanime.server = "&s=rapidvideo";
// blacklisted urls
kissanime.epsBlacklist = [
"/Anime/Macross/Bunny_Hat-Macross_Special_-4208D135?id=73054",
"/Anime/Macross/Bunny_Hat_Raw-30th_Anniversary_Special_-0A1CD40E?id=73055",
"/Anime/Macross/Episode-011-original?id=35423"
];
// regexes
kissanime.regexWhitelist = /episode|movie|special|OVA/i;
kissanime.regexBlacklist = /\b_[a-z]+|recap|\.5/i;
kissanime.regexCountdown = /\d+(?=\), function)/;
// loads kissanime cookies and then calls back
function kissanime_loadCookies(callback) {
if (GM_getValue("KAloadcookies", false) + 30*1000 < Date.now()) {
GM_setValue("KAloadcookies", Date.now());
GM_openInTab(kissanime.base, true);
}
if (callback) {
setTimeout(function() {
callback();
}, 6000);
}
}
// function to execute when script is run on kissanime
pageLoad["kissanime"] = function() {
if (GM_getValue("KAloadcookies", false) && document.title != "Please wait 5 seconds...") {
GM_setValue("KAloadcookies", false);
window.close();
}
}
getEpisodes["kissanime"] = function(dataStream, url) {
GM_xmlhttpRequest({
method: "GET",
url: kissanime.anime + url,
onload: function(resp) {
if (resp.status == 503) {
// loading CF cookies
kissanime_loadCookies(function() {
getEpisodes["kissanime"](dataStream, url);
});
} else if (resp.status == 200) {
// OK
let jqPage = $(resp.response);
let episodes = [];
// get anchors for the episodes
let as = jqPage.find(".listing").find("tr > td > a");
// get series title to remove it from episode name
let title = jqPage.find("#leftside > div:nth-child(1) > div.barContent > div:nth-child(2) > a").text();
// filter and add to episodes array
as.each(function() {
// title must match regexWhitelist, must not match regexBlacklist and href must not be in epsBlacklist to be considered a valid episode
if (kissanime.regexWhitelist.test(this.text) && !kissanime.regexBlacklist.test(this.text) && kissanime.epsBlacklist.indexOf(this.href) == -1) {
// prepend new object to array
episodes.unshift({
text: this.text.split(title)[1].substring(1).replace(/ 0+(?=\d+)/, " "),
href: kissanime.anime + this.href.split("/Anime/")[1] + kissanime.server
});
}
});
// get time until next episode
let timeMillis = Date.now() + parseInt(kissanime.regexCountdown.exec(resp.responseText));
// callback
putEpisodes(dataStream, episodes, timeMillis);
}
}
});
}
getEplistUrl["kissanime"] = function(partialUrl) {
return kissanime.anime + partialUrl;
}
searchSite["kissanime"] = function(id, title) {
GM_xmlhttpRequest({
method: "POST",
url: kissanime.search,
data: "type=Anime" + "&keyword=" + title,
headers: { "Content-Type": "application/x-www-form-urlencoded" },
onload: function(resp) {
if (resp.status == 503) {
// loading CF cookies
kissanime_loadCookies(function() {
searchSite["kissanime"](id, title);
});
} else if (resp.status == 200) {
// OK
let results = [];
let list = $(resp.responseText);
list.each(function() {
results.push({
title: this.text,
href: this.pathname.split("/")[2]
});
});
// callback
putResults(id, results);
}
}
});
}
/* 9anime */
/*******************************************************************************************************************************************************************/
const nineanime = {};
nineanime.base = "https://9anime.to/";
nineanime.anime = nineanime.base + "watch/";
nineanime.servers = nineanime.base + "ajax/film/servers/";
nineanime.search = nineanime.base + "search?keyword=";
nineanime.regexBlacklist = /preview|special|trailer|CAM/i;
getEpisodes["nineanime"] = function(dataStream, url) {
GM_xmlhttpRequest({
method: "GET",
url: nineanime.servers + url.match(/(?<=\.)\w+$/)[0],
onload: function(resp) {
if (resp.status == 200) {
// OK
// response is a json with only html attribute, parse and turn into jQuery object
let jqPage = $(JSON.parse(resp.response).html);
let episodes = [];
// get servers
let servers = jqPage.find("div.widget-body > .server");
let as = null;
// auto select server with the most videos
servers.each(function() {
let nas = $(this).find("li > a");
if (!as || nas.length > as.length) {
as = nas;
}
});
if (as) {
as.each(function() {
// ignore blacklisted episodes
if (!nineanime.regexBlacklist.test($(this).text())) {
// push episode to array
episodes.push({
text: "Episode " + $(this).text().replace(/^0+(?=\d+)/, ""),
href: nineanime.base + $(this).attr("href").substr(1),
});
}
});
}
// get time if available
GM_xmlhttpRequest({
method: "GET",
url: nineanime.anime + url,
onload: function(resp) {
if (resp.status == 200) {
// OK
let time = $(resp.response).find("#main > div > div.alert.alert-primary > i");
let timeMillis = undefined;
if (time.length !== 0) {
// timer is present
timeMillis = time.data("to") * 1000;
}
// callback
putEpisodes(dataStream, episodes, timeMillis);
} else {
// not OK, callback
putEpisodes(dataStream, episodes, undefined);
}
}
});
}
}
});
}
getEplistUrl["nineanime"] = function(partialUrl) {
return nineanime.anime + partialUrl;
}
searchSite["nineanime"] = function(id, title) {
GM_xmlhttpRequest({
method: "GET",
url: nineanime.search + encodeURI(title),
onload: function(resp) {
if (resp.status == 200) {
// OK
let jqPage = $(resp.response);
let results = [];
// get results from response
let list = jqPage.find("#main > div > div:nth-child(1) > div.widget-body > div.film-list > .item");
list = list.slice(0, 10);
// add to results
list.each(function() {
// get anchor for text and href
let a = $(this).find("a")[1];
// get episode count
let ep = $(this).find(".status > .ep").text().match(/(?<=\/)\d+/);
results.push({
title: a.text,
href: a.href.split("/")[4],
episodes: ep ? (ep[0] + " eps") : "1 ep"
});
});
// callback
putResults(id, results);
}
}
});
}
/* masterani */
/*******************************************************************************************************************************************************************/
const masterani = {};
masterani.base = "https://www.masterani.me/";
masterani.anime = masterani.base + "api/anime/";
masterani.anime_suffix = "/detailed";
masterani.anime_info = masterani.base + "anime/info/";
masterani.anime_watch = masterani.base + "anime/watch/";
masterani.search = masterani.base + "api/anime/filter?search=";
masterani.search_suffix = "&order=relevance_desc&page=1";
// loads cloudflare cookies and then calls back
function masterani_loadCookies(callback) {
if (GM_getValue("MAloadcookies", false) + 30*1000 < Date.now()) {
GM_setValue("MAloadcookies", Date.now());
GM_openInTab(masterani.base, true);
}
if (callback) {
setTimeout(function() {
callback();
}, 6000);
}
}
// function to execute when script is run on masteranime
pageLoad["masterani"] = function() {
if (GM_getValue("MAloadcookies", false) && document.title != "Just a moment...") {
GM_setValue("MAloadcookies", false);
window.close();
}
}
getEpisodes["masterani"] = function(dataStream, url) {
GM_xmlhttpRequest({
method: "GET",
url: masterani.anime + url + masterani.anime_suffix,
onload: function(resp) {
if (resp.status == 503) {
// loading CF cookies
masterani_loadCookies(function() {
getEpisodes["masterani"](dataStream, url);
});
} else if (resp.status == 200) {
// OK
let res = JSON.parse(resp.response);
let episodes = [];
// get all episodes
for (let i = 0; i < res.episodes.length; i++) {
let ep = res.episodes[i].info.episode;
// push episodes to array
episodes.push({
text: "Episode " + ep,
href: masterani.anime_watch + url + "/" + ep,
});
}
// callback
putEpisodes(dataStream, episodes, undefined);
}
}
});
}
getEplistUrl["masterani"] = function(partialUrl) {
return masterani.anime_info + partialUrl;
}
searchSite["masterani"] = function(id, title) {
GM_xmlhttpRequest({
method: "GET",
url: masterani.search + encodeURIComponent(title).slice(0, 60) + masterani.search_suffix, // maximum search length is 60 chars
onload: function(resp) {
if (resp.status == 503) {
// loading CF cookies
masterani_loadCookies(function() {
searchSite["masterani"](id, title);
});
} else if (resp.status == 200) {
// OK
let list = JSON.parse(resp.response).data;
let results = [];
if (list) {
list = list.slice(0, 10);
// add to results
for (let i = 0; i < list.length; i++) {
let r = list[i];
let eps = r.episode_count;
if (!eps) {
eps = "? eps"
} else {
eps += ((eps > 1) ? " eps" : " ep")
}
results.push({
title: r.title,
href: r.slug,
episodes: eps,
});
}
}
// callback
putResults(id, results);
}
}
});
}
/* kissmanga */
/*******************************************************************************************************************************************************************/
const kissmanga = {};
kissmanga.base = "https://kissmanga.com/";
kissmanga.manga = kissmanga.base + "Manga/";
kissmanga.search = kissmanga.base + "Search/SearchSuggest";
// regex
kissmanga.regexVol = /(?<=vol).+?\d+/i;
// loads kissmanga cookies and then calls back
function kissmanga_loadCookies(callback) {
if (GM_getValue("KMloadcookies", false) + 30*1000 < Date.now()) {
GM_setValue("KMloadcookies", Date.now());
GM_openInTab(kissmanga.base, true);
}
if (callback) {
setTimeout(function() {
callback();
}, 6000);
}
}
// function to execute when script is run on kissmanga
pageLoad["kissmanga"] = function() {
if (GM_getValue("KMloadcookies", false) && document.title != "Please wait 5 seconds...") {
GM_setValue("KMloadcookies", false);
window.close();
}
}
getEpisodes["kissmanga"] = function(dataStream, url) {
GM_xmlhttpRequest({
method: "GET",
url: kissmanga.manga + url,
onload: function(resp) {
if (resp.status == 503) {
// loading CF cookies
kissmanga_loadCookies(function() {
getEpisodes["kissmanga"](dataStream, url);
});
} else if (resp.status == 200) {
// OK
let jqPage = $(resp.response);
let episodes = [];
// get table rows for the episodes
let trs = jqPage.find(".listing").find("tr");
// get series title to remove it from chapter name
let title = jqPage.find("#leftside > div:nth-child(1) > div.barContent > div:nth-child(2) > a").text();
// filter and add to episodes array
trs.each(function() {
let a = $(this).find("td > a");
if (a.length === 0) return;
let t = a.text().split(title)[1].substring(1).replace(/ 0+(?=\d+)/, " ");
// get all numbers in title
let n = t.match(/\d+/g);
// if vol is present then get second match else get first
n = kissmanga.regexVol.test(t) ? n[1] : n[0];
// chapter number - 1 is used as index
n = parseInt(n) - 1;
// add chapter to array
episodes[n] = {
text: t,
href: kissmanga.manga + a.attr('href').split("/Manga/")[1],
timestamp: Date.parse($(this).find("td:nth-child(2)").text()),
}
});
// estimate timeMillis
let timeMillis = estimateTimeMillis(episodes, 5);
// callback
putEpisodes(dataStream, episodes, timeMillis);
}
}
});
}
getEplistUrl["kissmanga"] = function(partialUrl) {
return kissmanga.manga + partialUrl;
}
searchSite["kissmanga"] = function(id, title) {
GM_xmlhttpRequest({
method: "POST",
url: kissmanga.search,
data: "type=Manga" + "&keyword=" + title,
headers: { "Content-Type": "application/x-www-form-urlencoded" },
onload: function(resp) {
if (resp.status == 503) {
// loading CF cookies
kissmanga_loadCookies(function() {
searchSite["kissmanga"](id, title);
});
} else if (resp.status == 200) {
// OK
let results = [];
let list = $(resp.responseText);
list.each(function() {
results.push({
title: this.text,
href: this.pathname.split("/")[2]
});
});
// callback
putResults(id, results);
}
}
});
}
/* mangadex */
/*******************************************************************************************************************************************************************/
const mangadex = {};
mangadex.base = "https://mangadex.org/";
mangadex.manga = mangadex.base + "manga/";
mangadex.manga_api = mangadex.base + "api/manga/";
mangadex.chapter = mangadex.base + "chapter/";
mangadex.lang_code = "gb";
mangadex.search = mangadex.base + "quick_search/";
getEpisodes["mangadex"] = function(dataStream, url) {
GM_xmlhttpRequest({
method: "GET",
url: mangadex.manga_api + url,
onload: function(resp) {
if (resp.status == 200) {
// OK
let res_ch = JSON.parse(resp.response).chapter;
let episodes = [];
// parse json
for (let key in res_ch) {
if (res_ch.hasOwnProperty(key)) {
let ch = res_ch[key];
// skip wrong language
if (ch.lang_code != mangadex.lang_code) continue;
// put into episodes array
episodes[ch.chapter - 1] = {
text: (ch.volume && `Vol. ${ch.volume} `) + `Ch. ${ch.chapter}`,
href: mangadex.chapter + key,
timestamp: ch.timestamp,
}
}
}
// estimate timeMillis
let timeMillis = estimateTimeMillis(episodes, 5);
// callback
putEpisodes(dataStream, episodes, timeMillis);
}
}
});
}
getEplistUrl["mangadex"] = function(partialUrl) {
return mangadex.manga + partialUrl;
}
searchSite["mangadex"] = function(id, title) {
GM_xmlhttpRequest({
method: "GET",
url: mangadex.search + encodeURI(title),
onload: function(resp) {
if (resp.status == 200) {
// OK
let results = [];
// get title anchors
let titles = $(resp.response).find("#search_manga").find("a.manga_title");
titles.each(function() {
results.push({
title: this.title,
href: this.pathname.split("/")[2]
});
});
// callback
putResults(id, results);
}
}
});
}
/* MAL list */
/*******************************************************************************************************************************************************************/
pageLoad["list"] = function() {
// own list
if ($(".header-menu.other").length !== 0) return;
if ($(properties.watching).length !== 1) return;
// force hide more-info
const styleSheet = document.createElement("style");
styleSheet.innerHTML =`
.list-table .more-info {
display: none!important;
}
`;
document.body.appendChild(styleSheet);
// expand more-info
$(".more > a").each(function() {
this.click();
});
// $(".more > a").click(); doesn't work for some reason
// add col to table
$("#list-container").find("th.header-title.title").after(properties.colHeader);
$(".list-item .data.title").after("<td class='data stream'></td>");
// style
$(".data.stream").css("font-weight", "normal");
$(".data.stream").css("line-height", "1.5em");
$(".header-title.stream").css("min-width", "120px");
// wait
let interval = setInterval(function() {
let done = true;
// put comment into data("comment")
$(".list-item").each(function() {
let td = $(this).find(".td1.borderRBL");
// if not loaded yet then check later
if (td.length == 0) {
done = false;
return
}
let comment = td.html().match(properties.commentsRegex);
if (comment) {
// revome the first 10 characters to remove "Comments: " since js doesn't support lookbehinds
comment = comment.toString().substring(10);
} else {
comment = null;
}
$(this).find(".data.stream").data("comment", comment);
});
if (done) {
// collapse more-info
$(".more-info").css("display", "none");
// remove sheet
document.body.removeChild(styleSheet);
// load links
$(".header-title.stream").trigger("click");
// stop interval
clearInterval(interval);
}
}, 100);
// event listeners
// column header
$(".header-title.stream").on("click", function() {
$(".data.stream").trigger("click");
});
// table cell
$(".data.stream").on("click", function() {
updateList($(this), true, true);
});
// complete one episode
$(properties.iconAdd).on("click", function() {
let dataStream = $(this).parents(".list-item").find(".data.stream");
updateList(dataStream, false, true);
});
// timer event
$(".data.stream").on("update-time", function() {
let dataStream = $(this);
// get time object from dataStream
let t = dataStream.data("timeMillis");
// get next episode number
let nextEp = parseInt(dataStream.parents(".list-item").find(properties.findProgress).find(".link").text()) + 1;
let timeMillis;
// if t.ep is set then it needs to be equal to nextEp, else we set timeMillis to false to display Not Yet Aired
if (t && (t.ep ? t.ep == nextEp : true)) {
timeMillis = t.timeMillis - Date.now();
} else {
timeMillis = false;
}
let time;
if (!timeMillis || isNaN(timeMillis) || timeMillis < 1000) {
time = properties.notAired;
} else {
const d = Math.floor(timeMillis / (1000 * 60 * 60 * 24));
const h = Math.floor((timeMillis % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const m = Math.floor((timeMillis % (1000 * 60 * 60)) / (1000 * 60));
time = (h < 10 ? "0"+h : h) + "h:" + (m < 10 ? "0" + m : m) + "m";
if (d > 0) {
time = d + (d == 1 ? " day " : " days ") + time;
}
}
if (dataStream.find(".nextep, .loading, .error").length > 0) {
// do nothing if timer is not needed
return;
} else if (dataStream.find(".timer").length === 0) {
// if timer doesn't exist create it
dataStream.prepend("<div class='timer'>" + time + "<div>");
} else {
// update timer
dataStream.find(".timer").html(time);
}
});
// update timer
setInterval(function() {
$(".data.stream").trigger("update-time");
}, 1000);
}
// updates dataStream cell
function updateList(dataStream, forceReload, canReload) {
// remove old divs
dataStream.find(".error").remove();
dataStream.find(".nextep").remove();
dataStream.find(".loading").remove();
dataStream.find(".timer").remove();
// get episode list from data
let episodeList = dataStream.data("episodeList");
if (Array.isArray(episodeList) && !forceReload) {
// episode list exists
updateList_exists(dataStream);
} else if (canReload) {
// episode list doesn't exist or needs to be reloaded
updateList_doesntExist(dataStream);
} else {
// broken link
dataStream.prepend($("<div class='error'>Broken link<br></div>").css("color", "red"));
}
}
function updateList_exists(dataStream) {
// listitem
let listitem = dataStream.parents(".list-item");
// get current episode number
let currEp = parseInt(listitem.find(properties.findProgress).find(".link").text());
if (isNaN(currEp)) currEp = 0;
// get episodes from data
let episodes = dataStream.data("episodeList");
// create new nextep
let nextep = $("<div class='nextep'></div>");
if (episodes.length > currEp) {
// there are episodes available
let isAiring = listitem.find(properties.findAiring).length !== 0;
let t = episodes[currEp] ? episodes[currEp].text : ("Missing #" + (currEp + 1));
let a = $("<a></a>");
a.text(t.length > 13 ? t.substr(0, 12) + "…" : t);
if (t.length > 13) a.attr("title", t);
a.attr("href", episodes[currEp] ? episodes[currEp].href : "#");
a.attr("target", "_blank");
a.attr("class", isAiring ? "airing" : "non-airing");
a.css("color", isAiring ? "#2db039" : "#ff730a");
nextep.append(a);
if (episodes.length - currEp > 1) {
// if there is more than 1 new ep then put the amount in parenthesis
nextep.append(" (" + (episodes.length - currEp) + ")");
}
// add new nextep
dataStream.prepend(nextep);
} else if (currEp > episodes.length) {
// user has watched too many episodes
nextep.append($("<div class='.ep-error'>" + properties.latest + episodes.length + "</div>").css("color", "red"));
// add new nextep
dataStream.prepend(nextep);
} else {
// there aren't episodes available, trigger timer
dataStream.trigger("update-time");
}
}
function updateList_doesntExist(dataStream) {
// check if comment exists and is correct
let comment = dataStream.data("comment");
if (comment) {
// comment exists
// url is and array that contains the streaming service and url relative to that service
let url = getUrlFromComment(comment);
if (url) {
// comment valid
// add loading
dataStream.prepend("<div class='loading'>Loading...</div>");
// add eplist and favicon to dataStream
if (dataStream.find(".eplist").length === 0) {
// add eplist
let eplistUrl = getEplistUrl[url[0]](url[1]);
dataStream.append("<a class='eplist' target='_blank' href='" + eplistUrl + "'>" + properties.ep + " list</a>");
// add favicon
let domain = getDomainById(url[0]);
if (domain) {
let src = "https://www.google.com/s2/favicons?domain=" + domain;
dataStream.append("<img class='favicon' src='" + src + "' style='position: relative; top: 3px; padding-left: 4px'>");
}
}
// executes getEpisodes relative to url[0] passing dataStream and url[1]
getEpisodes[url[0]](dataStream, url[1]);
} else {
// comment invalid
dataStream.append("<div class='error'>Invalid Link</div>");
}
} else {
// comment doesn't extst
dataStream.append("<div class='error'>No Link</div>");
}
}
// save episodeList and timeMillis inside .data.stream of listitem
function putEpisodes(dataStream, episodes, timeMillis) {
// add episodes to dataStream
dataStream.data("episodeList", episodes);
// add timeMillis to dataStream
if (timeMillis) {
// timeMillis is valid
dataStream.data("timeMillis", { timeMillis: timeMillis });
} else if (properties.mode == "anime") {
// timeMillis doesn't exist, get time from anichart
anichart_setTimeMillis(dataStream, true);
}
updateList(dataStream, false, false);
}
/* MAL edit */
/*******************************************************************************************************************************************************************/
pageLoad["edit"] = function() {
// get title
const title = $("#main-form > table:nth-child(1) > tbody > tr:nth-child(1) > td:nth-child(2) > strong > a")[0].text;
// add titleBox with default title
let titleBox = $("<input type='text' value='" + title + "' size='36' style='font-size: 11px; padding: 3px;'>");
// add #search div
let search = $("<div id='search'><b style='font-size: 110%; line-height: 180%;'>Search: </b></div>");
$(properties.editPageBox).after("<br>", titleBox, "<br>", search);
// add streamingServices
let first = true;
streamingServices.forEach(function(ss) {
if (ss.type != properties.mode) return;
// don't append ", " before first ss
if (first) {
first = false;
} else {
search.append(", ");
}
// new anchor
let a = $("<a></a>");
a.text(ss.name);
a.attr("href", "#");
// on anchor click
a.on("click", function() {
// remove old results
search.find(".site").remove();
// add new result box
search.append("<div class='site " + ss.id + "'><div id='searching'>Searching...</div></div>");
// execute search
searchSite[ss.id](ss.id, titleBox.val());
// return
return false;
});
search.append(a);
});
search.append("<br>");
}
function putResults(id, results) {
let siteDiv = $("#search").find("." + id);
// if div with current id cant be found then don't add results
if (siteDiv.length !== 0) {
siteDiv.find("#searching").remove();
if (results.length === 0) {
siteDiv.append("No Results. Try changing the title in the search box above.");
return;
}
// add results
for (let i = 0; i < results.length; i++) {
let r = results[i];
let a = $("<a href='#'>Select</a>");
a.on("click", function() {
$(properties.editPageBox).val(id + " " + r.href);
return false;
});
siteDiv.append("(").append(a).append(") ").append("<a target='_blank' href='" + getEplistUrl[id](r.href) + "'>" + r.title + "</a>");
if (r.episodes) {
siteDiv.append(" (" + r.episodes + ")");
}
siteDiv.append("<br>");
}
}
}
/* main */
/*******************************************************************************************************************************************************************/
// associates an url with properties and pageLoad function
let pages = [
{ url: kissanime.base, prop: null, load: "kissanime" },
{ url: kissmanga.base, prop: null, load: "kissmanga" },
{ url: masterani.base, prop: null, load: "masterani" },
{ url: anichartUrl, prop: null, load: "anichart" },
{ url: "https://myanimelist.net/animelist/", prop: "anime", load: "list" },
{ url: "https://myanimelist.net/mangalist/", prop: "manga", load: "list" },
{ url: "https://myanimelist.net/ownlist/anime/", prop: "anime", load: "edit" },
{ url: "https://myanimelist.net/ownlist/manga/", prop: "manga", load: "edit" },
];
(function($) {
for (let i = 0; i < pages.length; i++) {
if (window.location.href.indexOf(pages[i].url) != -1) {
properties = properties[pages[i].prop];
pageLoad[pages[i].load]();
break;
}
}
})(jQuery);