// ==UserScript== // @name MALstreaming // @namespace https://github.com/mattiadr/MALstreaming // @version 5.36 // @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://kissanime.ru/ // @match https://kissmanga.com/ // @match https://www1.9anime.nl/ // @match https://twist.moe/ // @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 // @connect * // @downloadURL none // ==/UserScript== /* generic */ /*******************************************************************************************************************************************************************/ // array of all streaming services 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: "animetwist", type: "anime", name: "Anime Twist", domain: "https://twist.moe/" }, // manga { id: "kissmanga", type: "manga", name: "Kissmanga", domain: "https://kissmanga.com/" }, { id: "mangadex", type: "manga", name: "MangaDex", domain: "https://mangadex.org/" }, { id: "jaiminisbox", type: "manga", name: "Jaimini's Box", domain: "https://jaiminisbox.com/" }, ]; // 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 = {}; // 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; } /* anilist */ /*******************************************************************************************************************************************************************/ const anilist = {}; anilist.api = "https://graphql.anilist.co"; anilist.query = `\ query ($idMal: Int) { Media(type: ANIME, idMal: $idMal) { airingSchedule(notYetAired: true, perPage: 1) { nodes { episode airingAt } } } }`; // request time until next episode for the specified anime id function requestTime(id) { // prepare data let data = { query: anilist.query, variables: { idMal: id } }; // do request GM_xmlhttpRequest({ method: "POST", url: anilist.api, headers: { "Content-Type": "application/json" }, data: JSON.stringify(data), onload: function(resp) { let res = JSON.parse(resp.response); let times = GM_getValue("anilistTimes", {}); // get data from response let sched = res.data.Media.airingSchedule.nodes[0]; let ep = sched.episode; let timeMillis = sched.airingAt * 1000; // set time, ep is episode the timer is referring to times[id] = { ep: ep, timeMillis: timeMillis }; // put times in GM value GM_setValue("anilistTimes", times); } }); } // puts timeMillis into dataStream, then calls back function anilist_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("anilistTimes", 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 anilist timer is referring to next episode dataStream.data("timeMillis", t); } else { // add value change listener let listenerId = GM_addValueChangeListener("anilistTimes", function(name, old_value, new_value, remote) { // reload, avoid infinite loops if (canReload) anilist_setTimeMillis(dataStream, false); // remove listener GM_removeValueChangeListener(listenerId); }); // api request to anilist requestTime(id); } } /* 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 != "Just a moment...") { 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://www1.9anime.nl/"; 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; // open captcha page function nineanime_openCaptcha() { if (GM_getValue("NAcaptcha", false) + 30*1000 < Date.now()) { GM_setValue("NAcaptcha", Date.now()); GM_openInTab(nineanime.base, false); } } // function to execute when script is run on nineanime pageLoad["nineanime"] = function() { // close window if opended by script if (GM_getValue("NAcaptcha", false) && document.title != "WAF") { GM_setValue("NAcaptcha", false); window.close(); } } getEpisodes["nineanime"] = function(dataStream, url) { GM_xmlhttpRequest({ method: "GET", url: nineanime.servers + url.match(/\.(\w+)$/)[1], onload: function(resp) { if (resp.status == 200) { // successful response is a json with only html attribute, parse it let json = null; try { json = JSON.parse(resp.response); } catch (e) { // solving captcha nineanime_openCaptcha(); return; } // OK let jqPage = $(json.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[1] + " eps") : "1 ep" }); }); // callback putResults(id, results); } } }); } /* animetwist */ /*******************************************************************************************************************************************************************/ const animetwist = {}; animetwist.base = "https://twist.moe/"; animetwist.anime = animetwist.base + "a/"; animetwist.anime_suffix = "/last"; animetwist.dataRegex = /