// ==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 // @downloadURL none // ==/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: "Watch", 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: "Read", 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; results.push({ title: r.title, href: r.slug, episodes: eps + ((eps > 1) ? " eps" : " ep"), }); } } // 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(""); // 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("
" + time + "
"); } 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($("
Broken link
").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 = $("
"); 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.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($("
" + properties.latest + episodes.length + "
").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("
Loading...
"); // add eplist and favicon to dataStream if (dataStream.find(".eplist").length === 0) { // add eplist let eplistUrl = getEplistUrl[url[0]](url[1]); dataStream.append("" + properties.ep + " list"); // add favicon let domain = getDomainById(url[0]); if (domain) { let src = "https://www.google.com/s2/favicons?domain=" + domain; dataStream.append(""); } } // executes getEpisodes relative to url[0] passing dataStream and url[1] getEpisodes[url[0]](dataStream, url[1]); } else { // comment invalid dataStream.append("
Invalid Link
"); } } else { // comment doesn't extst dataStream.append("
No Link
"); } } // 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 = $(""); // add #search div let search = $(""); $(properties.editPageBox).after("
", titleBox, "
", 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.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("
Searching...
"); // execute search searchSite[ss.id](ss.id, titleBox.val()); // return return false; }); search.append(a); }); search.append("
"); } 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 = $("Select"); a.on("click", function() { $(properties.editPageBox).val(id + " " + r.href); return false; }); siteDiv.append("(").append(a).append(") ").append("" + r.title + ""); if (r.episodes) { siteDiv.append(" (" + r.episodes + ")"); } siteDiv.append("
"); } } } /* 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);