// ==UserScript== // @name MALstreaming // @namespace https://github.com/mattiadr/MALstreaming // @version 4.0 // @author https://github.com/mattiadr // @description Adds various streaming 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 http://kissanime.ru/ // @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 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 time left until the next episode in milliseconds - create a new function in search that will accept id and title the function needs to callback to putResults(id, results, manualSearch) results needs to be an array of object with title (display title), href (the url that will be put in the comments) and fullhref (full url of page) attributes manualSearch needs to be an url to visit if search yields no results - if other utility is needed, add it in the service section and if you need to run a script on specific pages add another if in the "main" */ /* generic */ /*******************************************************************************************************************************************************************/ // 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 = [ {id:"kissanime", name:"Kissanime"}, {id:"nineanime", name:"9anime"} ]; // 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; } /* kissanime */ /*******************************************************************************************************************************************************************/ const kissanime = {}; kissanime.base = "http://kissanime.ru/"; kissanime.anime = kissanime.base + "Anime/"; kissanime.search = kissanime.base + "Search/Anime/"; 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, arg1, arg2) { if (!GM_getValue("KAloadcookies", false)) { GM_setValue("KAloadcookies", true); GM_openInTab(kissanime.base, true); } if (callback) { setTimeout(function() { callback(arg1, arg2); }, 6000); } } // function to execute when scrip 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(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"); // filter and add to episodes array as.each(function(i, e) { // 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(e.text) && !kissanime.regexBlacklist.test(e.text) && kissanime.epsBlacklist.indexOf(e.href) == -1) { // get tite to split episode name and leave only "Episode xx" let title = jqPage.find("#leftside > div:nth-child(1) > div.barContent > div:nth-child(2) > a").text(); let t = e.text.split(title)[1].substring(1).replace(/ 0+(?=\d+)/, " "); // prepend new object to array episodes.unshift({ text:t, href:kissanime.anime + e.href.split("/Anime/")[1] + kissanime.server }); } }); // get time until next episode let timeMillis = parseInt(kissanime.regexCountdown.exec(resp.responseText)); // callback to insert episodes in list putEpisodes(dataStream, episodes, timeMillis); } } }); } getEplistUrl["kissanime"] = function(partialUrl) { return kissanime.anime + partialUrl; } searchSite["kissanime"] = function(id, title) { GM_xmlhttpRequest({ method: "POST", data: "type=Anime" + "&keyword=" + title, url: kissanime.search, headers: { "Content-Type": "application/x-www-form-urlencoded" }, onload: function(resp) { if (resp.status == 503) { // loading CF cookies kissanime_loadCookies(search["kissanime"], title); } else if (resp.status == 200) { // OK let results = []; // if there is only one result, kissanime redirects to the only result page if (resp.finalUrl.indexOf(kissanime.search) == -1) { // only one result results.push({ title:title, href:resp.finalUrl.split("/")[4], fullhref:kissanime.anime + resp.finalUrl.split("/")[4] }); } else { // multiple results let list = $(resp.response).find("#leftside > div > div.barContent > div:nth-child(2) > table > tbody > tr").slice(2); list.each(function() { let a = $(this).find("a")[0]; results.push({ title:a.text.replace(/\n\s+/, ""), // regex is used to remove leading whitespace href:a.pathname.split("/")[2], fullhref:kissanime.anime + a.pathname.split("/")[2] }); }) } // callback putResults(id, results, kissanime.base); } } }); } /* 9anime */ /*******************************************************************************************************************************************************************/ const nineanime = {}; nineanime.base = "https://www5.9anime.is/"; nineanime.anime = nineanime.base + "watch/"; nineanime.search = nineanime.base + "search?keyword="; nineanime.server = "33"; // RapidVideo = 33, MyCloud = 28, Streamango = 34, OpenLoad = 24 getEpisodes["nineanime"] = function(dataStream, url) { GM_xmlhttpRequest({ method: "GET", url: nineanime.anime + url, onload: function(resp) { if (resp.status == 200) { // OK let jqPage = $(resp.response); let episodes = []; // get servers let servers = jqPage.find("#main > div > div.widget.servers > div.widget-body > .server"); let server = null; // if possibe use specified server servers.each(function() { if ($(this).attr("data-name") == nineanime.server) { server = $(this); } }); // else use first one if (!server) { server = servers.first(); } // get anchors let as = server.find("li > a"); as.each(function() { episodes.push({ text:"Episode " + $(this).text().replace(/^0+(?=\d+)/, ""), href:nineanime.base + $(this).attr("href").substr(1) }); }); // get time if available let time = jqPage.find("#main > div > div.alert.alert-primary > i"); let timeMillis; if (time.length !== 0) { // timer is present timeMillis = time.data("to") * 1000 - Date.now(); } else { // timer is not present, estimating based on latest episode let timeStr = as.last().data("title").replace("-", ""); timeMillis = Date.parse(timeStr) + 1000 * 60 * 60 * 24 * 7 - Date.now(); } // callback putEpisodes(dataStream, episodes, timeMillis); } } }); } 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() { let a = $(this).find("a")[1]; results.push({ title:a.text, href:a.href.split("/")[4], fullhref:a.href }); }); // callback putResults(id, results, nineanime.base); } } }); } /* MAL animelist */ /*******************************************************************************************************************************************************************/ pageLoad["list"] = function() { // own list if ($(".header-menu.other").length !== 0) return; // watching page if ($(".list-unit.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(i, e) { e.click(); }); // add col to table $("#list-container").find("th.header-title.title").after("