├── .gitignore ├── concat.sh ├── concat.bat ├── images ├── edit_page.png └── list_page.png ├── LICENSE ├── src ├── 99_main.js ├── 03_cookies.js ├── 13_animetwist.js.disabled ├── 02_anilist.js ├── 15_erairaws.js ├── 11_nineanime.js.disabled ├── 51_mangadex.js ├── 52_jaiminisbox.js.disabled ├── 00_header.js ├── 12_masterani.js.disabled ├── 91_MAL_edit.js ├── 14_horriblesubs.js.disabled ├── 50_kissmanga.js.disabled ├── 10_kissanime.js.disabled ├── 01_generic.js ├── 16_subsplease.js ├── 53_mangaplus.js └── 90_MAL_list.js ├── README.md └── MALstreaming.user.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.unused -------------------------------------------------------------------------------- /concat.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cat src/*.js > MALstreaming.user.js 4 | -------------------------------------------------------------------------------- /concat.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | >MALstreaming.user.js (for /r ".\src" %%F in (*.js) do type "%%F") -------------------------------------------------------------------------------- /images/edit_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattiadr/MALstreaming/HEAD/images/edit_page.png -------------------------------------------------------------------------------- /images/list_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattiadr/MALstreaming/HEAD/images/list_page.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Mattia De Rosa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/99_main.js: -------------------------------------------------------------------------------- 1 | /* main */ 2 | /*******************************************************************************************************************************************************************/ 3 | // associates an url with properties and pageLoad function 4 | let pages = [ 5 | { url: "https://myanimelist.net/animelist/", prop: "anime", load: "list" }, 6 | { url: "https://myanimelist.net/mangalist/", prop: "manga", load: "list" }, 7 | { url: "https://myanimelist.net/ownlist/anime/", prop: "anime", load: "edit" }, 8 | { url: "https://myanimelist.net/ownlist/manga/", prop: "manga", load: "edit" }, 9 | ]; 10 | 11 | (function($) { 12 | // check on which page we are 13 | for (let i = 0; i < pages.length; i++) { 14 | if (window.location.href.indexOf(pages[i].url) != -1) { 15 | properties = properties[pages[i].prop]; 16 | pageLoad[pages[i].load](); 17 | return; 18 | } 19 | } 20 | 21 | // check if we are on a load cookies page 22 | for (let i = 0; i < cookieServices.length; i++) { 23 | if (window.location.href.indexOf(cookieServices[i].url) != -1) { 24 | pageLoad["loadCookies"](cookieServices[i]); 25 | return; 26 | } 27 | } 28 | })(jQuery); 29 | -------------------------------------------------------------------------------- /src/03_cookies.js: -------------------------------------------------------------------------------- 1 | /* cookies */ 2 | /*******************************************************************************************************************************************************************/ 3 | // array with services that require cookies to make requests 4 | const cookieServices = [ 5 | // anime 6 | // manga 7 | ]; 8 | 9 | // checks if i need/can load cookies and returns the cookieService 10 | function needsCookies(id, status) { 11 | for (let i = 0; i < cookieServices.length; i++) { 12 | if (cookieServices[i].id == id && cookieServices[i].status == status) return cookieServices[i]; 13 | } 14 | return false; 15 | } 16 | 17 | // load cookies for specified service, then calls back 18 | function loadCookies(cookieService, callback) { 19 | let lc = GM_getValue("loadCookies", {}); 20 | if (lc[cookieService.id] === undefined || lc[cookieService.id] + 30*1000 < Date.now()) { 21 | lc[cookieService.id] = Date.now(); 22 | GM_setValue("loadCookies", lc); 23 | GM_openInTab(cookieService.url, true); 24 | } 25 | if (callback) { 26 | setTimeout(function() { 27 | callback(); 28 | }, cookieService.timeout); 29 | } 30 | } 31 | 32 | // function to execute when script is run on website to load cookies from 33 | pageLoad["loadCookies"] = function(cookieService) { 34 | let lc = GM_getValue("loadCookies", {}); 35 | if (lc[cookieService.id] && cookieService.loaded()) { 36 | lc[cookieService.id] = false; 37 | GM_setValue("loadCookies", lc); 38 | window.close(); 39 | } 40 | } 41 | 42 | -------------------------------------------------------------------------------- /src/13_animetwist.js.disabled: -------------------------------------------------------------------------------- 1 | /* animetwist */ 2 | /*******************************************************************************************************************************************************************/ 3 | const animetwist = {}; 4 | animetwist.base = "https://twist.moe/"; 5 | animetwist.anime = animetwist.base + "a/" 6 | animetwist.api = "https://api.twist.moe/api/anime"; 7 | 8 | getEpisodes["animetwist"] = function(dataStream, url) { 9 | GM_xmlhttpRequest({ 10 | method: "GET", 11 | url: `${animetwist.api}/${url}`, 12 | onload: function(resp) { 13 | if (resp.status == 200) { 14 | // OK 15 | let list = JSON.parse(resp.response).episodes; 16 | let episodes = []; 17 | // insert all episodes 18 | for (let i = 0; i < list.length; i++) { 19 | let n = list[i].number; 20 | episodes[n - 1] = { 21 | text: "Episode " + n, 22 | href: animetwist.anime + url + "/" + n, 23 | } 24 | } 25 | // callback 26 | putEpisodes(dataStream, episodes, undefined); 27 | } else { 28 | // error 29 | errorEpisodes(dataStream, "Anime Twist: " + resp.status); 30 | } 31 | } 32 | }); 33 | } 34 | 35 | getEplistUrl["animetwist"] = function(partialUrl) { 36 | return animetwist.anime + partialUrl; 37 | } 38 | 39 | searchSite["animetwist"] = function(id, title) { 40 | GM_xmlhttpRequest({ 41 | method: "GET", 42 | url: animetwist.api, 43 | onload: function(resp) { 44 | if (resp.status == 200) { 45 | // OK 46 | let list = JSON.parse(resp.response); 47 | if (!list) { 48 | // error 49 | return; 50 | } 51 | // map and filter list to results 52 | let results = list.map(item => ({ 53 | title: item.title, 54 | href: item.slug.slug, 55 | })).filter(item => matchResult(item, title)); 56 | // callback 57 | putResults(id, results); 58 | } else { 59 | // error 60 | errorResults(id, "Anime Twist: " + resp.status); 61 | } 62 | } 63 | }); 64 | } 65 | 66 | -------------------------------------------------------------------------------- /src/02_anilist.js: -------------------------------------------------------------------------------- 1 | /* anilist */ 2 | /*******************************************************************************************************************************************************************/ 3 | const anilist = {}; 4 | anilist.api = "https://graphql.anilist.co"; 5 | anilist.query = `\ 6 | query ($idMal: Int) { 7 | Media(type: ANIME, idMal: $idMal) { 8 | airingSchedule(notYetAired: true, perPage: 1) { 9 | nodes { 10 | episode 11 | airingAt 12 | } 13 | } 14 | } 15 | }`; 16 | 17 | // request time until next episode for the specified anime id 18 | function requestTime(id) { 19 | // prepare data 20 | let data = { 21 | query: anilist.query, 22 | variables: { idMal: id } 23 | }; 24 | // do request 25 | GM_xmlhttpRequest({ 26 | method: "POST", 27 | url: anilist.api, 28 | headers: { "Content-Type": "application/json" }, 29 | data: JSON.stringify(data), 30 | onload: function(resp) { 31 | let res = JSON.parse(resp.response); 32 | let times = GM_getValue("anilistTimes", {}); 33 | // get data from response 34 | let sched = res.data.Media.airingSchedule.nodes[0]; 35 | // if there is no episode then it means the last episode just notYetAired 36 | if (!sched || !sched.episode) return; 37 | let ep = sched.episode; 38 | let timeMillis = sched.airingAt * 1000; 39 | // set time, ep is episode the timer is referring to 40 | times[id] = { 41 | ep: ep, 42 | timeMillis: timeMillis 43 | }; 44 | // put times in GM value 45 | GM_setValue("anilistTimes", times); 46 | } 47 | }); 48 | } 49 | 50 | // puts timeMillis into dataStream, then calls back 51 | function anilist_setTimeMillis(dataStream, canReload) { 52 | let listitem = dataStream.parents(".list-item"); 53 | 54 | let times = GM_getValue("anilistTimes", false); 55 | // get anime id 56 | let id = listitem.find(".data.title > .link").attr("href").split("/")[2]; 57 | let t = times ? times[id] : false; 58 | 59 | if (times && t && Date.now() < t.timeMillis) { 60 | // time doesn't need to update 61 | // set timeMillis, this is used to check if anilist timer is referring to next episode 62 | putTimeMillis(dataStream, t.timeMillis, false, t.ep); 63 | } else if (canReload) { 64 | // add value change listener 65 | let listenerId = GM_addValueChangeListener("anilistTimes", function(name, old_value, new_value, remote) { 66 | // reload 67 | anilist_setTimeMillis(dataStream, false); 68 | // remove listener 69 | GM_removeValueChangeListener(listenerId); 70 | }); 71 | // api request to anilist 72 | requestTime(id); 73 | } 74 | } 75 | 76 | -------------------------------------------------------------------------------- /src/15_erairaws.js: -------------------------------------------------------------------------------- 1 | /* erai-raws */ 2 | /*******************************************************************************************************************************************************************/ 3 | const erairaws = {}; 4 | erairaws.base = "https://www.erai-raws.info/"; 5 | erairaws.anime = erairaws.base + "anime-list/"; 6 | erairaws.search = erairaws.base + "?s=" 7 | 8 | getEpisodes["erairaws"] = function(dataStream, url) { 9 | // request 10 | GM_xmlhttpRequest({ 11 | method: "POST", 12 | url: erairaws.anime + url, 13 | onload: function(resp) { 14 | if (resp.status == 200) { 15 | // OK 16 | let jqPage = $(resp.response); 17 | let episodes = []; 18 | 19 | jqPage.find("#menu0 > .table").each(function() { 20 | let tt = $(this).find(".tooltip2"); 21 | let type = tt.text(); 22 | let m = tt.next().text().match(/[\d\.]+/g); 23 | 24 | let release = $(this).find(".release-links").first(); 25 | let magnet = release.find(".load_more_links_buttons:contains(magnet)").attr("href"); 26 | 27 | if (type == "B") { 28 | // batch 29 | let first = parseInt(m[m.length - 2]); 30 | let last = parseInt(m[m.length - 1]); 31 | 32 | let obj = { 33 | text: `Batch ${first} ~ ${last}`, 34 | href: magnet, 35 | }; 36 | 37 | for (let i = first - 1; i < last; i++) { 38 | episodes[i] = obj; 39 | } 40 | } else if (type == "E" || type == "A" || type == "F") { 41 | // encoding || airing || final 42 | let ep = parseInt(m[m.length - 1]); 43 | let res = release.find("span").text().match(/^\w+/)[0]; 44 | 45 | if (!episodes[ep - 1]) { 46 | episodes[ep - 1] = { 47 | text: `Ep ${ep} (${res})`, 48 | href: magnet, 49 | } 50 | } 51 | } else { 52 | // unknown type 53 | return; 54 | } 55 | }); 56 | 57 | // callback 58 | putEpisodes(dataStream, episodes, undefined); 59 | } else { 60 | // error 61 | errorEpisodes(dataStream, "Erai-raws: " + resp.status); 62 | } 63 | } 64 | }); 65 | } 66 | 67 | getEplistUrl["erairaws"] = function(partialUrl) { 68 | return erairaws.anime + partialUrl; 69 | } 70 | 71 | searchSite["erairaws"] = function(id, title) { 72 | GM_xmlhttpRequest({ 73 | method: "GET", 74 | url: erairaws.search + title, 75 | onload: function(resp) { 76 | if (resp.status == 200) { 77 | // OK 78 | let jqPage = $(resp.response); 79 | let results = jqPage.find("#main .entry-title > a").map(function() { 80 | return { 81 | title: $(this).text().trim(), 82 | href: $(this).attr("href").split("/")[4], 83 | }; 84 | }); 85 | 86 | // callback 87 | putResults(id, results); 88 | } else { 89 | // error 90 | errorResults(id, "Erai-raws: " + resp.status); 91 | } 92 | } 93 | }); 94 | } 95 | 96 | -------------------------------------------------------------------------------- /src/11_nineanime.js.disabled: -------------------------------------------------------------------------------- 1 | /* 9anime */ 2 | /*******************************************************************************************************************************************************************/ 3 | const nineanime = {}; 4 | nineanime.base = "https://9anime.to/"; 5 | nineanime.anime = nineanime.base + "watch/"; 6 | nineanime.servers = nineanime.base + "ajax/anime/servers?id="; 7 | nineanime.search = nineanime.base + "search?keyword="; 8 | nineanime.regexBlacklist = /preview|special|trailer|CAM/i; 9 | 10 | getEpisodes["nineanime"] = function(dataStream, url) { 11 | GM_xmlhttpRequest({ 12 | method: "GET", 13 | url: nineanime.servers + url.match(/\.(\w+)$/)[1], 14 | onload: function(resp) { 15 | if (resp.status == 200) { 16 | // OK 17 | let res = JSON.parse(resp.response); 18 | let jqPage = $(res.html); 19 | let episodes = []; 20 | 21 | let list = jqPage.find(".episodes > li > a"); 22 | list.each(function() { 23 | // ignore blacklisted episodes 24 | if (!nineanime.regexBlacklist.test($(this).text())) { 25 | // push episode to array 26 | episodes.push({ 27 | text: "Episode " + $(this).text(), 28 | href: nineanime.base + $(this).attr("href").substr(1), 29 | }); 30 | } 31 | }); 32 | 33 | // callback 34 | putEpisodes(dataStream, episodes, undefined); 35 | } else { 36 | let cs = needsCookies("nineanime", resp.status); 37 | // error 38 | if (!cs) return errorEpisodes(dataStream, "9anime: " + resp.status); 39 | // load cookies 40 | loadCookies(cs, function() { 41 | getEpisodes["nineanime"](dataStream, url); 42 | }); 43 | } 44 | } 45 | }); 46 | } 47 | 48 | getEplistUrl["nineanime"] = function(partialUrl) { 49 | return nineanime.anime + partialUrl; 50 | } 51 | 52 | searchSite["nineanime"] = function(id, title) { 53 | GM_xmlhttpRequest({ 54 | method: "GET", 55 | url: nineanime.search + encodeURI(title), 56 | onload: function(resp) { 57 | if (resp.status == 200) { 58 | // OK 59 | let jqPage = $(resp.response); 60 | let results = []; 61 | // get results from response 62 | let list = jqPage.find("ul.anime-list > li"); 63 | list = list.slice(0, 10); 64 | // add to results 65 | list.each(function() { 66 | // get anchor for text and href 67 | let a = $(this).find("a")[1]; 68 | // get episode count 69 | let ep = $(this).find(".tag.ep").text().match(/\/(\d+)/); 70 | results.push({ 71 | title: a.text, 72 | href: a.href.split("/")[4], 73 | episodes: ep ? (ep[1] + " eps") : "1 ep" 74 | }); 75 | }); 76 | // callback 77 | putResults(id, results); 78 | } else { 79 | let cs = needsCookies("nineanime", resp.status); 80 | // error 81 | if (!cs) return errorResults(id, "9anime: " + resp.status); 82 | // load cookies 83 | loadCookies(cs, function() { 84 | searchSite["nineanime"](id, title); 85 | }); 86 | } 87 | } 88 | }); 89 | } 90 | 91 | -------------------------------------------------------------------------------- /src/51_mangadex.js: -------------------------------------------------------------------------------- 1 | /* mangadex */ 2 | /*******************************************************************************************************************************************************************/ 3 | const mangadex = {}; 4 | mangadex.base = "https://mangadex.org/"; 5 | mangadex.base_api = "https://api.mangadex.org/"; 6 | mangadex.manga = mangadex.base + "title/" 7 | mangadex.lang_code = "en"; 8 | mangadex.manga_api = mangadex.base_api + `manga/{0}/feed?limit=500&order[chapter]=asc&offset={1}&translatedLanguage[]=${mangadex.lang_code}`; 9 | mangadex.chapter = mangadex.base + "chapter/"; 10 | mangadex.search_api = mangadex.base_api + "manga?title="; 11 | 12 | getEpisodes["mangadex"] = function(dataStream, url, offset=0, episodes=[]) { 13 | GM_xmlhttpRequest({ 14 | method: "GET", 15 | url: mangadex.manga_api.formatUnicorn(url, offset), 16 | onload: function(resp) { 17 | if (resp.status == 200) { 18 | let res = JSON.parse(resp.response); 19 | if (res.result != "ok") { 20 | // error 21 | errorResults(id, "MangaDex: " + res.result); 22 | } 23 | // OK 24 | for (let i = 0; i < res.data.length; i++) { 25 | let chapter = res.data[i]; 26 | let n = chapter.attributes.chapter; 27 | let t = `Chapter ${n}`; 28 | if (chapter.attributes.title) t += `: ${chapter.attributes.title}`; 29 | episodes[n - 1] = { 30 | text: t, 31 | href: mangadex.chapter + chapter.id, 32 | timestamp: new Date(chapter.attributes.createdAt).getTime(), 33 | } 34 | } 35 | // check if we got all the episodes 36 | if (offset + 500 >= res.total) { 37 | // estimate timeMillis 38 | let timeMillis = estimateTimeMillis(episodes, 5); 39 | // callback 40 | putEpisodes(dataStream, episodes, timeMillis); 41 | } else { 42 | // request next 500 episodes 43 | getEpisodes["mangadex"](dataStream, url, offset + 500, episodes); 44 | } 45 | } else { 46 | // error 47 | errorEpisodes(dataStream, "MangaDex: " + resp.status); 48 | } 49 | } 50 | }); 51 | } 52 | 53 | getEplistUrl["mangadex"] = function(partialUrl) { 54 | return mangadex.manga + partialUrl; 55 | } 56 | 57 | searchSite["mangadex"] = function(id, title) { 58 | GM_xmlhttpRequest({ 59 | method: "GET", 60 | url: mangadex.search_api + encodeURI(title), 61 | onload: function(resp) { 62 | if (resp.status == 200) { 63 | let res = JSON.parse(resp.response); 64 | if (res.result != "ok") { 65 | // error 66 | errorResults(id, "MangaDex: " + res.result); 67 | } 68 | // OK 69 | let results = []; 70 | for (let i = 0; i < res.data.length; i++) { 71 | let manga = res.data[i]; 72 | results.push({ 73 | title: manga.attributes.title.en || manga.attributes.title.jp, 74 | href: manga.id, 75 | }); 76 | } 77 | // callback 78 | putResults(id, results); 79 | } else { 80 | // error 81 | errorResults(id, "MangaDex: " + resp.status); 82 | } 83 | } 84 | }); 85 | } 86 | 87 | -------------------------------------------------------------------------------- /src/52_jaiminisbox.js.disabled: -------------------------------------------------------------------------------- 1 | /* jaimini's box */ 2 | /*******************************************************************************************************************************************************************/ 3 | const jbox = {}; 4 | jbox.base = "https://jaiminisbox.com/"; 5 | jbox.manga = jbox.base + "reader/series/"; 6 | jbox.search = jbox.base + "reader/search/"; 7 | // regex 8 | jbox.dateRegex = /(\w+|[\d\.]+)(?= $)/; 9 | 10 | getEpisodes["jaiminisbox"] = function(dataStream, url) { 11 | GM_xmlhttpRequest({ 12 | method: "GET", 13 | url: jbox.manga + url, 14 | onload: function(resp) { 15 | if (resp.status == 200) { 16 | // OK 17 | let jqPage = $(resp.response); 18 | let episodes = []; 19 | // get chapter divs 20 | let divs = jqPage.find("#content > .panel > .list > .group > .element"); 21 | 22 | divs.each(function() { 23 | // get title, href and chapter number 24 | let a = $(this).find(".title > a"); 25 | let t = a.text(); 26 | let m = t.match(/\d+/); 27 | // skip if no chapter number found 28 | if (!m) return; 29 | // chapter number - 1 is used as index 30 | let n = parseInt(m[0]) - 1; 31 | // get date 32 | let date = $(this).find(".meta_r").text().match(jbox.dateRegex)[0]; 33 | if (date == "Today" || date == "Yesterday") { 34 | let d = new Date(); 35 | d.setHours(0); 36 | d.setMinutes(0); 37 | d.setSeconds(0); 38 | d.setMilliseconds(0); 39 | date = +d; 40 | // remove 24h if yesterday 41 | if (date == "Yesterday") date -= 24*60*60*1000; 42 | } else { 43 | date = Date.parse(date); 44 | } 45 | // add chapter to array 46 | episodes[n] = { 47 | text: t, 48 | href: a.attr("href"), 49 | timestamp: date, 50 | }; 51 | }); 52 | // estimate timeMillis 53 | let timeMillis = estimateTimeMillis(episodes, 5); 54 | // callback 55 | putEpisodes(dataStream, episodes, timeMillis); 56 | } else { 57 | // error 58 | errorEpisodes(dataStream, "Jaimini's Box: " + resp.status); 59 | } 60 | } 61 | }); 62 | } 63 | 64 | getEplistUrl["jaiminisbox"] = function(partialUrl) { 65 | return jbox.manga + partialUrl; 66 | } 67 | 68 | searchSite["jaiminisbox"] = function(id, title) { 69 | GM_xmlhttpRequest({ 70 | method: "POST", 71 | url: jbox.search, 72 | data: "search=" + encodeURIComponent(title), 73 | headers: { "Content-Type": "application/x-www-form-urlencoded" }, 74 | onload: function(resp) { 75 | if (resp.status == 200) { 76 | // OK 77 | let jqPage = $(resp.response); 78 | let results = []; 79 | 80 | let as = jqPage.find("#content > .panel > .list > .group > .title > a"); 81 | as.each(function() { 82 | results.push({ 83 | title: this.text, 84 | href: this.href.split("/")[5], 85 | }); 86 | }); 87 | // callback 88 | putResults(id, results); 89 | } else { 90 | // error 91 | errorResults(id, "Jaimini's Box: " + resp.status); 92 | } 93 | } 94 | }); 95 | } 96 | 97 | -------------------------------------------------------------------------------- /src/00_header.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name MALstreaming 3 | // @namespace https://github.com/mattiadr/MALstreaming 4 | // @version 5.89 5 | // @author https://github.com/mattiadr 6 | // @description Adds various anime and manga links to MAL 7 | // @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= 8 | // @run-at document-idle 9 | // @updateURL https://raw.githubusercontent.com/mattiadr/MALstreaming/master/MALstreaming.user.js 10 | // @downloadURL https://raw.githubusercontent.com/mattiadr/MALstreaming/master/MALstreaming.user.js 11 | // @supportURL https://github.com/mattiadr/MALstreaming/issues 12 | // @match https://myanimelist.net/animelist/* 13 | // @match https://myanimelist.net/ownlist/anime/*/edit* 14 | // @match https://myanimelist.net/ownlist/anime/add?selected_series_id=* 15 | // @match https://myanimelist.net/mangalist/* 16 | // @match https://myanimelist.net/ownlist/manga/*/edit* 17 | // @match https://myanimelist.net/ownlist/manga/add?selected_manga_id=* 18 | // @require https://code.jquery.com/jquery-3.7.1.min.js 19 | // @require https://cdn.rawgit.com/dcodeIO/protobuf.js/6.8.8/dist/protobuf.js 20 | // @grant GM_xmlhttpRequest 21 | // @grant GM_openInTab 22 | // @grant GM_setValue 23 | // @grant GM_getValue 24 | // @grant GM_deleteValue 25 | // @grant GM_addValueChangeListener 26 | // @grant GM_removeValueChangeListener 27 | // @grant window.close 28 | // @connect * 29 | // ==/UserScript== 30 | 31 | -------------------------------------------------------------------------------- /src/12_masterani.js.disabled: -------------------------------------------------------------------------------- 1 | /* masterani */ 2 | /*******************************************************************************************************************************************************************/ 3 | const masterani = {}; 4 | masterani.base = "https://www.masterani.me/"; 5 | masterani.anime = masterani.base + "api/anime/"; 6 | masterani.anime_suffix = "/detailed"; 7 | masterani.anime_info = masterani.base + "anime/info/"; 8 | masterani.anime_watch = masterani.base + "anime/watch/"; 9 | masterani.search = masterani.base + "api/anime/filter?search="; 10 | masterani.search_suffix = "&order=relevance_desc&page=1"; 11 | 12 | // loads cloudflare cookies and then calls back 13 | function masterani_loadCookies(callback) { 14 | if (GM_getValue("MAloadcookies", false) + 30*1000 < Date.now()) { 15 | GM_setValue("MAloadcookies", Date.now()); 16 | GM_openInTab(masterani.base, true); 17 | } 18 | if (callback) { 19 | setTimeout(function() { 20 | callback(); 21 | }, 6000); 22 | } 23 | } 24 | 25 | // function to execute when script is run on masteranime 26 | pageLoad["masterani"] = function() { 27 | if (GM_getValue("MAloadcookies", false) && document.title != "Just a moment...") { 28 | GM_setValue("MAloadcookies", false); 29 | window.close(); 30 | } 31 | } 32 | 33 | getEpisodes["masterani"] = function(dataStream, url) { 34 | GM_xmlhttpRequest({ 35 | method: "GET", 36 | url: masterani.anime + url + masterani.anime_suffix, 37 | onload: function(resp) { 38 | if (resp.status == 503) { 39 | // loading CF cookies 40 | masterani_loadCookies(function() { 41 | getEpisodes["masterani"](dataStream, url); 42 | }); 43 | } else if (resp.status == 200) { 44 | // OK 45 | let res = JSON.parse(resp.response); 46 | let episodes = []; 47 | // get all episodes 48 | for (let i = 0; i < res.episodes.length; i++) { 49 | let ep = res.episodes[i].info.episode; 50 | // push episodes to array 51 | episodes.push({ 52 | text: "Episode " + ep, 53 | href: masterani.anime_watch + url + "/" + ep, 54 | }); 55 | } 56 | // callback 57 | putEpisodes(dataStream, episodes, undefined); 58 | } 59 | } 60 | }); 61 | } 62 | 63 | getEplistUrl["masterani"] = function(partialUrl) { 64 | return masterani.anime_info + partialUrl; 65 | } 66 | 67 | searchSite["masterani"] = function(id, title) { 68 | GM_xmlhttpRequest({ 69 | method: "GET", 70 | url: masterani.search + encodeURIComponent(title).slice(0, 32) + masterani.search_suffix, // maximum search length is 32 chars 71 | onload: function(resp) { 72 | if (resp.status == 503) { 73 | // loading CF cookies 74 | masterani_loadCookies(function() { 75 | searchSite["masterani"](id, title); 76 | }); 77 | } else if (resp.status == 200) { 78 | // OK 79 | let list; 80 | try { 81 | list = JSON.parse(resp.response).data; 82 | } catch (e) { 83 | // error parsing JSON 84 | } 85 | let results = []; 86 | if (list) { 87 | list = list.slice(0, 10); 88 | // add to results 89 | for (let i = 0; i < list.length; i++) { 90 | let r = list[i]; 91 | let eps = r.episode_count; 92 | if (!eps) { 93 | eps = "? eps" 94 | } else { 95 | eps += ((eps > 1) ? " eps" : " ep") 96 | } 97 | results.push({ 98 | title: r.title, 99 | href: r.slug, 100 | episodes: eps, 101 | }); 102 | } 103 | } 104 | // callback 105 | putResults(id, results); 106 | } 107 | } 108 | }); 109 | } 110 | 111 | -------------------------------------------------------------------------------- /src/91_MAL_edit.js: -------------------------------------------------------------------------------- 1 | /* MAL edit */ 2 | /*******************************************************************************************************************************************************************/ 3 | pageLoad["edit"] = function() { 4 | // get title 5 | let title = $("#main-form > table:nth-child(1) > tbody > tr:nth-child(1) > td:nth-child(2) > strong > a")[0].text; 6 | // add titleBox with default title 7 | title = title.replace(/'/g, "'"); 8 | title = title.trim(); 9 | let titleBox = $(""); 10 | // add #search div 11 | let search = $(""); 12 | $(properties.editPageBox).after("
", titleBox, "
", search); 13 | // add streamingServices 14 | let first = true; 15 | streamingServices.forEach(function(ss) { 16 | if (ss.type != properties.mode) return; 17 | // don't append ", " before first ss 18 | if (first) { 19 | first = false; 20 | } else { 21 | search.append(", "); 22 | } 23 | // new anchor 24 | let a = $(""); 25 | a.text(ss.name); 26 | a.attr("href", "#"); 27 | // on anchor click 28 | a.on("click", function() { 29 | // remove old results 30 | search.find(".site").remove(); 31 | // add new result box 32 | search.append("
Searching...
"); 33 | // execute search 34 | searchSite[ss.id](ss.id, titleBox.val()); 35 | // return 36 | return false; 37 | }); 38 | search.append(a); 39 | }); 40 | search.append("
"); 41 | 42 | // offset textarea 43 | let offsetBox = $(""); 44 | let o = $(properties.editPageBox).val().split(" ")[2]; 45 | if (o) offsetBox.val(o); 46 | // Set Offset button 47 | let a = $("Set Offset"); 48 | a.attr("href", "#"); 49 | a.on("click", function() { 50 | // get offset from offsetBox 51 | let o = parseInt(offsetBox.val()); 52 | // replace or append to commentBox 53 | let val = $(properties.editPageBox).val().split(" "); 54 | if (!o || o == 0) { 55 | val[2] = undefined; 56 | } else { 57 | val[2] = o; 58 | } 59 | $(properties.editPageBox).val(val.join(" ")); 60 | return false; 61 | }); 62 | // offset div 63 | let offset = $("
"); 64 | offset.append(a, offsetBox); 65 | search.after(offset); 66 | } 67 | 68 | function putResults(id, results) { 69 | let siteDiv = $("#search").find("." + id); 70 | // if div with current id cant be found then don't add results 71 | if (siteDiv.length !== 0) { 72 | siteDiv.find("#searching").remove(); 73 | 74 | if (results.length === 0) { 75 | siteDiv.append("No Results. Try changing the title in the search box above."); 76 | return; 77 | } 78 | // add results 79 | for (let i = 0; i < results.length; i++) { 80 | let r = results[i]; 81 | let a = $("Select"); 82 | a.on("click", function() { 83 | $(properties.editPageBox).val(id + " " + r.href); 84 | return false; 85 | }); 86 | siteDiv.append("(").append(a).append(") ").append("" + r.title + ""); 87 | if (r.episodes) { 88 | siteDiv.append(" (" + r.episodes + ")"); 89 | } 90 | siteDiv.append("
"); 91 | } 92 | } 93 | } 94 | 95 | function errorResults(id, error) { 96 | let siteDiv = $("#search").find("." + id); 97 | // if div with current id cant be found then don't add error 98 | if (siteDiv.length !== 0) { 99 | siteDiv.find("#searching").remove(); 100 | siteDiv.append(error || mal.genericErrorRequest); 101 | } 102 | } 103 | 104 | -------------------------------------------------------------------------------- /src/14_horriblesubs.js.disabled: -------------------------------------------------------------------------------- 1 | /* horriblesubs */ 2 | /*******************************************************************************************************************************************************************/ 3 | const horriblesubs = {}; 4 | horriblesubs.base = "https://horriblesubs.info/"; 5 | horriblesubs.anime = horriblesubs.base + "shows/" 6 | horriblesubs.api = horriblesubs.base + "api.php?method=getshows&type=show&showid=" 7 | horriblesubs.nextid = "&nextid="; 8 | 9 | horriblesubs.regexID = /(?<=hs_showid = )\d+/; 10 | horriblesubs.resultsPerPage = 12; 11 | horriblesubs.loadPage = 2; 12 | 13 | function parseEpisodes(jqPage, episodes) { 14 | jqPage.each(function() { 15 | let ep = parseInt(this.id); 16 | let div = $(this).find(".rls-link").last(); 17 | let res = div.attr("id").split("-")[1]; 18 | let href = div.find(".hs-magnet-link > a").attr("href"); 19 | episodes[ep - 1] = { 20 | text: `Ep ${ep} (${res})`, 21 | href: href, 22 | } 23 | }); 24 | } 25 | 26 | getEpisodes["horriblesubs"] = function(dataStream, url) { 27 | // request id 28 | GM_xmlhttpRequest({ 29 | method: "GET", 30 | url: horriblesubs.anime + url, 31 | onload: function(resp) { 32 | if (resp.status == 200) { 33 | // OK 34 | let showid = resp.responseText.match(horriblesubs.regexID); 35 | 36 | // request first page of results 37 | GM_xmlhttpRequest({ 38 | method: "GET", 39 | url: horriblesubs.api + showid, 40 | onload: function(resp) { 41 | if (resp.status == 200) { 42 | // OK 43 | let jqPage = $(resp.responseText); 44 | let episodes = []; 45 | // parse first page of episodes 46 | parseEpisodes(jqPage, episodes); 47 | // put episodes, may be overridden by next requests 48 | putEpisodes(dataStream, episodes, undefined); 49 | // check if you need to download another page 50 | let latestEp = parseInt(jqPage.first().attr("id")); 51 | let nextEp = parseInt(dataStream.parents(".list-item").find(properties.findProgress).find(".link").text()) + 1; 52 | if (isNaN(nextEp)) nextEp = 0; 53 | 54 | let reqPage = Math.floor((latestEp - nextEp) / horriblesubs.resultsPerPage); 55 | 56 | // request n pages (avoids multiple requests to page 0) 57 | for (let i = 0; i < horriblesubs.loadPage && reqPage > 0; i++) { 58 | GM_xmlhttpRequest({ 59 | method: "GET", 60 | url: horriblesubs.api + showid + horriblesubs.nextid + reqPage, 61 | onload: function(resp) { 62 | if (resp.status == 200) { 63 | // OK 64 | parseEpisodes($(resp.responseText), episodes); 65 | // put episodes 66 | putEpisodes(dataStream, episodes, undefined); 67 | } 68 | } 69 | }); 70 | // next page 71 | reqPage--; 72 | } 73 | } 74 | } 75 | }); 76 | } else { 77 | // error 78 | errorEpisodes(dataStream, "HorribleSubs: " + resp.status); 79 | } 80 | } 81 | }); 82 | } 83 | 84 | getEplistUrl["horriblesubs"] = function(partialUrl) { 85 | return horriblesubs.anime + partialUrl; 86 | } 87 | 88 | searchSite["horriblesubs"] = function(id, title) { 89 | GM_xmlhttpRequest({ 90 | method: "GET", 91 | url: horriblesubs.anime, 92 | onload: function(resp) { 93 | if (resp.status == 200) { 94 | // OK 95 | let jqPage = $(resp.response); 96 | let list = []; 97 | 98 | let shows = jqPage.find(".ind-show > a"); 99 | shows.each(function() { 100 | list.push({ 101 | title: this.text, 102 | href: this.pathname.split("/")[2], 103 | }); 104 | }); 105 | // filter results 106 | let results = list.filter(item => matchResult(item, title)); 107 | // callback 108 | putResults(id, results); 109 | } else { 110 | // error 111 | errorResults(id, "HorribleSubs: " + resp.status); 112 | } 113 | } 114 | }); 115 | } 116 | 117 | -------------------------------------------------------------------------------- /src/50_kissmanga.js.disabled: -------------------------------------------------------------------------------- 1 | /* kissmanga */ 2 | /*******************************************************************************************************************************************************************/ 3 | const kissmanga = {}; 4 | kissmanga.base = "https://kissmanga.com/"; 5 | kissmanga.manga = kissmanga.base + "Manga/"; 6 | kissmanga.search = kissmanga.base + "Search/SearchSuggest"; 7 | // regex 8 | kissmanga.regexVol = /vol.+?\d+/i; 9 | 10 | // loads kissmanga cookies and then calls back 11 | function kissmanga_loadCookies(callback) { 12 | if (GM_getValue("KMloadcookies", false) + 30*1000 < Date.now()) { 13 | GM_setValue("KMloadcookies", Date.now()); 14 | GM_openInTab(kissmanga.base, true); 15 | } 16 | if (callback) { 17 | setTimeout(function() { 18 | callback(); 19 | }, 6000); 20 | } 21 | } 22 | 23 | // function to execute when script is run on kissmanga 24 | pageLoad["kissmanga"] = function() { 25 | if (GM_getValue("KMloadcookies", false) && document.title != "Just a moment...") { 26 | GM_setValue("KMloadcookies", false); 27 | window.close(); 28 | } 29 | } 30 | 31 | getEpisodes["kissmanga"] = function(dataStream, url) { 32 | GM_xmlhttpRequest({ 33 | method: "GET", 34 | url: kissmanga.manga + url, 35 | onload: function(resp) { 36 | if (resp.status == 503) { 37 | // loading CF cookies 38 | kissmanga_loadCookies(function() { 39 | getEpisodes["kissmanga"](dataStream, url); 40 | }); 41 | } else if (resp.status == 200) { 42 | // OK 43 | let jqPage = $(resp.response); 44 | let episodes = []; 45 | // get table rows for the episodes 46 | let trs = jqPage.find(".listing").find("tr"); 47 | // get series title to remove it from chapter name 48 | let title = jqPage.find("#leftside > div:nth-child(1) > div.barContent > div:nth-child(2) > a").text(); 49 | // filter and add to episodes array 50 | trs.each(function() { 51 | let a = $(this).find("td > a"); 52 | if (a.length === 0) return; 53 | let t = a.text().split(title)[1].substring(1).replace(/ 0+(?=\d+)/, " "); 54 | // get all numbers in title 55 | let n = t.match(/\d+/g); 56 | // if vol is present then get second match else get first 57 | n = kissmanga.regexVol.test(t) ? n[1] : n[0]; 58 | // chapter number - 1 is used as index 59 | n = parseInt(n) - 1; 60 | // add chapter to array 61 | episodes[n] = { 62 | text: t, 63 | href: kissmanga.manga + a.attr('href').split("/Manga/")[1], 64 | timestamp: Date.parse($(this).find("td:nth-child(2)").text()), 65 | }; 66 | }); 67 | // estimate timeMillis 68 | let timeMillis = estimateTimeMillis(episodes, 5); 69 | // callback 70 | putEpisodes(dataStream, episodes, timeMillis); 71 | } else { 72 | // error 73 | errorEpisodes(dataStream, "Kissmanga: " + resp.status); 74 | } 75 | } 76 | }); 77 | } 78 | 79 | getEplistUrl["kissmanga"] = function(partialUrl) { 80 | return kissmanga.manga + partialUrl; 81 | } 82 | 83 | searchSite["kissmanga"] = function(id, title) { 84 | GM_xmlhttpRequest({ 85 | method: "POST", 86 | url: kissmanga.search, 87 | data: "type=Manga" + "&keyword=" + title, 88 | headers: { "Content-Type": "application/x-www-form-urlencoded" }, 89 | onload: function(resp) { 90 | if (resp.status == 503) { 91 | // loading CF cookies 92 | kissmanga_loadCookies(function() { 93 | searchSite["kissmanga"](id, title); 94 | }); 95 | } else if (resp.status == 200) { 96 | // OK 97 | let results = []; 98 | 99 | let list = $(resp.responseText); 100 | list.each(function() { 101 | results.push({ 102 | title: this.text, 103 | href: this.pathname.split("/")[2], 104 | }); 105 | }); 106 | // callback 107 | putResults(id, results); 108 | } else { 109 | // error 110 | errorResults(id, "Kissmanga: " + resp.status); 111 | } 112 | } 113 | }); 114 | } 115 | 116 | -------------------------------------------------------------------------------- /src/10_kissanime.js.disabled: -------------------------------------------------------------------------------- 1 | /* kissanime */ 2 | /*******************************************************************************************************************************************************************/ 3 | const kissanime = {}; 4 | kissanime.base = "https://kissanime.ru/"; 5 | kissanime.anime = kissanime.base + "Anime/"; 6 | kissanime.search = kissanime.base + "Search/SearchSuggestx"; 7 | kissanime.server = "&s=rapidvideo"; 8 | // blacklisted urls 9 | kissanime.epsBlacklist = [ 10 | "/Anime/Macross/Bunny_Hat-Macross_Special_-4208D135?id=73054", 11 | "/Anime/Macross/Bunny_Hat_Raw-30th_Anniversary_Special_-0A1CD40E?id=73055", 12 | "/Anime/Macross/Episode-011-original?id=35423" 13 | ]; 14 | // regexes 15 | kissanime.regexWhitelist = /episode|movie|special|OVA/i; 16 | kissanime.regexBlacklist = /\b_[a-z]+|recap|\.5/i; 17 | kissanime.regexCountdown = /\d+(?=\), function)/; 18 | 19 | // loads kissanime cookies and then calls back 20 | function kissanime_loadCookies(callback) { 21 | if (GM_getValue("KAloadcookies", false) + 30*1000 < Date.now()) { 22 | GM_setValue("KAloadcookies", Date.now()); 23 | GM_openInTab(kissanime.base, true); 24 | } 25 | if (callback) { 26 | setTimeout(function() { 27 | callback(); 28 | }, 6000); 29 | } 30 | } 31 | 32 | // function to execute when script is run on kissanime 33 | pageLoad["kissanime"] = function() { 34 | if (GM_getValue("KAloadcookies", false) && document.title != "Just a moment...") { 35 | GM_setValue("KAloadcookies", false); 36 | window.close(); 37 | } 38 | } 39 | 40 | getEpisodes["kissanime"] = function(dataStream, url) { 41 | GM_xmlhttpRequest({ 42 | method: "GET", 43 | url: kissanime.anime + url, 44 | onload: function(resp) { 45 | if (resp.status == 503) { 46 | // loading CF cookies 47 | kissanime_loadCookies(function() { 48 | getEpisodes["kissanime"](dataStream, url); 49 | }); 50 | } else if (resp.status == 200) { 51 | // OK 52 | let jqPage = $(resp.response); 53 | let episodes = []; 54 | // get anchors for the episodes 55 | let as = jqPage.find(".listing").find("tr > td > a"); 56 | // get series title to remove it from episode name 57 | let title = jqPage.find("#leftside > div:nth-child(1) > div.barContent > div:nth-child(2) > a").text(); 58 | // filter and add to episodes array 59 | as.each(function() { 60 | // title must match regexWhitelist, must not match regexBlacklist and href must not be in epsBlacklist to be considered a valid episode 61 | if (kissanime.regexWhitelist.test(this.text) && !kissanime.regexBlacklist.test(this.text) && kissanime.epsBlacklist.indexOf(this.href) == -1) { 62 | // prepend new object to array 63 | episodes.unshift({ 64 | text: this.text.split(title)[1].substring(1).replace(/ 0+(?=\d+)/, " "), 65 | href: kissanime.anime + this.href.split("/Anime/")[1] + kissanime.server 66 | }); 67 | } 68 | }); 69 | // get time until next episode 70 | let timeMillis = Date.now() + parseInt(kissanime.regexCountdown.exec(resp.responseText)); 71 | // callback 72 | putEpisodes(dataStream, episodes, timeMillis); 73 | } else { 74 | // error 75 | errorEpisodes(dataStream, "Kissanime: " + resp.status); 76 | } 77 | } 78 | }); 79 | } 80 | 81 | getEplistUrl["kissanime"] = function(partialUrl) { 82 | return kissanime.anime + partialUrl; 83 | } 84 | 85 | searchSite["kissanime"] = function(id, title) { 86 | GM_xmlhttpRequest({ 87 | method: "POST", 88 | url: kissanime.search, 89 | data: "type=Anime" + "&keyword=" + title, 90 | headers: { "Content-Type": "application/x-www-form-urlencoded" }, 91 | onload: function(resp) { 92 | if (resp.status == 503) { 93 | // loading CF cookies 94 | kissanime_loadCookies(function() { 95 | searchSite["kissanime"](id, title); 96 | }); 97 | } else if (resp.status == 200) { 98 | // OK 99 | let results = []; 100 | 101 | let list = $(resp.responseText); 102 | list.each(function() { 103 | results.push({ 104 | title: this.text, 105 | href: this.pathname.split("/")[2] 106 | }); 107 | }); 108 | // callback 109 | putResults(id, results); 110 | } else { 111 | // error 112 | errorResults(id, "Kissanime: " + resp.status); 113 | } 114 | } 115 | }); 116 | } 117 | 118 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MALstreaming 2 | Adds anime streaming links to the next available episode to your Currently Watching page on [MAL](https://myanimelist.net/). 3 | 4 | Since version 5.0 it also adds next available manga chapter to your Currently Reading list. 5 | 6 | ## Preview 7 | ![Preview Image](images/list_page.png) 8 | 9 | Supported anime websites: 10 | - [x] [Erai-raws](https://www.erai-raws.info/) (torrent) 11 | - [x] [SubsPlease](https://subsplease.org/) (torrent) 12 | 13 | Supported manga websites: 14 | - [x] [MangaDex](https://mangadex.org/) 15 | - [x] [MANGA Plus](https://mangaplus.shueisha.co.jp/) 16 | 17 | Dead Websites: 18 | - [x] ~~[Kissanime](http://kissanime.ru/)~~ 19 | - [x] ~~[Masterani](https://www.masterani.me/)~~ 20 | - [x] ~~[HorribleSubs](https://horriblesubs.info/)~~ 21 | - [x] ~~[Kissmanga](http://kissmanga.com/)~~ 22 | - [x] ~~[Jaimini's Box](https://jaiminisbox.com/)~~ 23 | - [x] ~~[9anime](https://9anime.to/) (streaming)~~ 24 | - [x] ~~[Anime Twist](https://twist.moe/) (streaming)~~ 25 | 26 | ## Usage 27 | The usage is the same for both anime and manga. 28 | 29 | This script is only active if you are logged in and in your currently watching page. 30 | It provides the link to the next available episode and if there are multiple episodes their number will be in parenthesis. 31 | The link will be green for currently airing anime or orange for non airing. 32 | If there are no episodes available, a countdown will be shown. 33 | Ep. list will link to the full episode list. 34 | 35 | This script will work for both series and movies and will automatically skip episodes based on certain rules (usually recaps and special episodes are ignored). 36 | 37 | The script uses the Comments section on MAL to store the streaming website name and url for each anime you want to link to (this will also allow you to choose from the subbed or dubbed version if available), you can use the Tags freely. 38 | The Comment must contain the name of the website followed by the partial url for the anime (Example: "kissanime Boku-no-Hero-Academia-3rd-Season"). 39 | To easily set the comment, the script provides a search functionality in the edit page (shown below). 40 | 41 |
Show/Hide Image 42 | Edit Page 43 |
44 | 45 | ### Notes: 46 | - You can click on the "Watch" column to refresh all episodes or on the cell to refresh for a single anime. 47 | - You can ctrl-click after editing the anime to force a recheck on the comment, without needing to reload the page. 48 | - Some anime services do not give an estimate for the time until next episode, so the script uses [anilist](https://anilist.co/) to get the time remaining. 49 | - On MangaDex the timer estimate is often inaccurate, especially if a chapter is "locked" for the next days. 50 | - Some websites do not start from episode one for seasons after the first. To sync this script with the latest episode you can set the offset below the comment. The value must be the total number of episodes from the previous seasons that you want to exclude. 51 | - This script will work with most anime, but since there can be exceptions there might be some errors in the episode count. Please report them. 52 | 53 | ## How to Install 54 | 1. Install Tampermonkey for [Chrome](https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo), [Firefox](https://addons.mozilla.org/en-US/firefox/addon/tampermonkey/) or [other browsers](http://www.tampermonkey.net/) 55 | 2. Click [here to install the script](https://raw.githubusercontent.com/mattiadr/MALstreaming/master/MALstreaming.user.js) or visit the [GreasyFork page](https://greasyfork.org/en/scripts/369605-malstreaming) 56 | 57 | ## Post Install 58 | Since this script adds another column to the list, an wider style is recommended for the list. 59 | You can get mine [here](https://pastebin.com/NEnDujGY), the only things that have been modified are the width, added trasparency and a custom background image. 60 | You change the background image just by substituting the default `background-url` on the top of the stylesheet with a custom one (you need to link the image directly). 61 | 62 | If you want to report a bug or request a feature you can: 63 | - [Submit an issue on github](https://github.com/mattiadr/MALstreaming/issues) 64 | - [Start a discussion on GreasyFork](https://greasyfork.org/en/scripts/369605-malstreaming/feedback) 65 | - Message me directly on [reddit](https://www.reddit.com/user/mattiadr96/) or [MAL](https://myanimelist.net/profile/mattiadr) 66 | - Email me at `mattiadr96 (at) gmail (dot) com` 67 | -------------------------------------------------------------------------------- /src/01_generic.js: -------------------------------------------------------------------------------- 1 | /* generic */ 2 | /*******************************************************************************************************************************************************************/ 3 | // array of all streaming services 4 | const streamingServices = [ 5 | // anime 6 | { id: "erairaws", type: "anime", name: "Erai-raws", domain: "www.erai-raws.info" }, 7 | { id: "subsplease", type: "anime", name: "SubsPlease", domain: "subsplease.org" }, 8 | // manga 9 | { id: "mangadex", type: "manga", name: "MangaDex", domain: "mangadex.org" }, 10 | { id: "mangaplus", type: "manga", name: "MANGA Plus", domain: "mangaplus.shueisha.co.jp" }, 11 | ]; 12 | // contains variable properties for anime/manga modes 13 | let properties = {}; 14 | properties.anime = { 15 | mode: "anime", 16 | watching: ".list-unit.watching", 17 | colHeaderText: "Watch", 18 | commentsRegex: /Notes: ([\S ]+) /, 19 | iconAdd: ".icon-add-episode", 20 | findProgress: ".data.progress", 21 | findAiring: "span.content-status:contains('Airing')", 22 | latest: "Latest ep is #", 23 | notAired: "Not Yet Aired", 24 | ep: "Ep.", 25 | editPageBox: "#add_anime_comments", 26 | bulkTooltip: "Open %d episodes in bulk", 27 | }; 28 | properties.manga = { 29 | mode: "manga", 30 | watching: ".list-unit.reading", 31 | colHeaderText: "Read", 32 | commentsRegex: /Notes: ([\S ]+) /, 33 | iconAdd: ".icon-add-chapter", 34 | findProgress: ".data.chapter", 35 | findAiring: "span.content-status:contains('Publishing')", 36 | latest: "Latest ch is #", 37 | notAired: "Not Yet Published", 38 | ep: "Ch.", 39 | editPageBox: "#add_manga_comments", 40 | bulkTooltip: "Open %d chapters in bulk", 41 | }; 42 | // contains all functions to execute on page load 43 | const pageLoad = {}; 44 | // contains all functions to get the episodes list from the streaming services 45 | // must callback to putEpisodes(dataStream, episodes, timeMillis) 46 | const getEpisodes = {}; 47 | // contains queue settings for queuing requests to services (optional) 48 | // must contain `maxRequests` and `timout` 49 | const queueSettings = {}; 50 | queueSettings["default"] = { 51 | maxRequests: 1, 52 | timeout: 1000, 53 | } 54 | // contains all functions to get the episode list url from the partial url 55 | const getEplistUrl = {}; 56 | // contains all functions to execute the search on the streaming services 57 | // must callback to putResults(results) 58 | const searchSite = {}; 59 | 60 | // return an array that contains the streaming service and url relative to that service or false if comment is not valid 61 | function getUrlFromComment(comment) { 62 | let c = comment.split(" "); 63 | if (c.length < 2) return false; 64 | for (let i = 0; i < streamingServices.length; i++) { 65 | if (streamingServices[i].id == c[0]) return c; 66 | } 67 | return false; 68 | } 69 | 70 | // estimate time before next chapter as min of last n chapters 71 | function estimateTimeMillis(episodes, n) { 72 | if (episodes.length == 0) return undefined; 73 | let prev = null; 74 | let min = undefined; 75 | for (let i = episodes.length - 1; i > Math.max(0, episodes.length - 1 - n); i--) { 76 | if (!episodes[i]) continue; 77 | if (prev && episodes[i].timestamp != prev) { 78 | let diff = prev - episodes[i].timestamp; 79 | if (!min || diff < min && diff > 0) min = diff; 80 | } 81 | prev = episodes[i].timestamp; 82 | } 83 | return episodes[episodes.length - 1].timestamp + min; 84 | } 85 | 86 | // returns the domain for the streaming service or false if ss doesn't exist 87 | function getDomainById(id) { 88 | for (let i = 0; i < streamingServices.length; i++) { 89 | if (streamingServices[i].id == id) { 90 | return streamingServices[i].domain; 91 | } 92 | } 93 | return false; 94 | } 95 | 96 | // returns true if the result matches the title 97 | function matchResult(result, title) { 98 | // split title into tokens 99 | let split = title.split(/\W+/g); 100 | for (let i = 0; i < split.length; i++) { 101 | // result must contain all tokens 102 | if (!result.title.toLowerCase().includes(split[i].toLowerCase())) { 103 | return false; 104 | } 105 | } 106 | return true; 107 | } 108 | 109 | // stackexchange's string format utility 110 | String.prototype.formatUnicorn = function() { 111 | let e = this.toString(); 112 | if (!arguments.length) return e; 113 | let t = typeof arguments[0]; 114 | let n = "string" === t || "number" === t ? Array.prototype.slice.call(arguments) : arguments[0]; 115 | for (let i in n) { 116 | e = e.replace(new RegExp("\\{" + i + "\\}", "gi"), n[i]); 117 | } 118 | return e; 119 | } 120 | 121 | -------------------------------------------------------------------------------- /src/16_subsplease.js: -------------------------------------------------------------------------------- 1 | /* subsplease */ 2 | /*******************************************************************************************************************************************************************/ 3 | const subsplease = {}; 4 | subsplease.base = "https://subsplease.org/"; 5 | subsplease.anime = subsplease.base + "shows/"; 6 | subsplease.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone 7 | subsplease.api = subsplease.base + "api/?f=show&tz=" + subsplease.timezone + "&sid="; 8 | subsplease.schedule = subsplease.base + "api/?f=schedule&h=true&tz=" + subsplease.timezone 9 | 10 | getEpisodes["subsplease"] = function(dataStream, url) { 11 | let ids = GM_getValue("subspleaseIDS", {}); 12 | if (ids[url]) { 13 | // found id, request episodes 14 | subsplease_getEpisodesFromAPI(dataStream, ids[url], url); 15 | } else { 16 | // id not found, request id then episodes 17 | GM_xmlhttpRequest({ 18 | method: "GET", 19 | url: subsplease.anime + url, 20 | onload: function(resp) { 21 | if (resp.status == 200) { 22 | // OK 23 | let jqPage = $(resp.response); 24 | // get id 25 | let id = jqPage.find("#show-release-table").attr("sid"); 26 | // save id in GM values 27 | ids[url] = id; 28 | GM_setValue("subspleaseIDS", ids); 29 | // get episodes 30 | subsplease_getEpisodesFromAPI(dataStream, id, url); 31 | } else { 32 | // error 33 | errorEpisodes(dataStream, "SubsPlease: " + resp.status); 34 | } 35 | } 36 | }); 37 | } 38 | } 39 | 40 | function subsplease_getEpisodesFromAPI(dataStream, id, url) { 41 | GM_xmlhttpRequest({ 42 | method: "GET", 43 | url: subsplease.api + id, 44 | onload: function(resp) { 45 | if (resp.status == 200) { 46 | // OK 47 | let res = JSON.parse(resp.response); 48 | let episodes = []; 49 | // loop through values 50 | Object.values(res.episode).forEach(ep => { 51 | let dwn = ep.downloads.pop(); 52 | episodes[parseInt(ep.episode) - 1] = { 53 | text: `Ep ${ep.episode} (${dwn.res}p)`, 54 | href: dwn.magnet 55 | }; 56 | }); 57 | // callback 58 | putEpisodes(dataStream, episodes, undefined); 59 | subsplease_getAirTime(dataStream, url); 60 | } else { 61 | // error 62 | errorEpisodes(dataStream, "SubsPlease: " + resp.status); 63 | } 64 | } 65 | }); 66 | } 67 | 68 | function subsplease_getAirTime(dataStream, url) { 69 | let lastTs = GM_getValue("subspleaseScheduleDate", 0); 70 | let now = +new Date(); 71 | 72 | // request at most once every 5 minutes 73 | if (now > lastTs + 5 * 60 * 1000) { 74 | // we request schedule, invalidate the cache and set the date immediately to avoid other dataStream requesting it too 75 | GM_deleteValue("subspleaseSchedule") 76 | GM_setValue("subspleaseScheduleDate", now); 77 | // and we start the request for the schedule 78 | GM_xmlhttpRequest({ 79 | method: "GET", 80 | url: subsplease.schedule, 81 | onload: function(resp) { 82 | let timeMillis = undefined; 83 | if (resp.status == 200) { 84 | // OK 85 | let res = JSON.parse(resp.response); 86 | let schedule = {}; 87 | res.schedule.forEach(s => { 88 | if (!s.aired) { 89 | let airTime = new Date(); 90 | let t = s.time.split(":"); 91 | airTime.setHours(t[0], t[1], 0, 0); 92 | schedule[s.page] = +airTime; 93 | } 94 | }); 95 | // set time 96 | let time = schedule[url]; 97 | if (time) { 98 | putTimeMillis(dataStream, time, true); 99 | } 100 | // save schedule 101 | GM_setValue("subspleaseSchedule", schedule); 102 | } else { 103 | // error, remove date so we may retry the request 104 | GM_deleteValue("subspleaseScheduleDate"); 105 | } 106 | } 107 | }); 108 | } else { 109 | let schedule = GM_getValue("subspleaseSchedule", {}); 110 | let time = schedule[url]; 111 | if (time) { 112 | // time is valid, just callback 113 | putTimeMillis(dataStream, time, true); 114 | } else { 115 | // time is not available, can happen if we already sent a request from another dataStream and we are waiting for results 116 | // or if the time is actually not available (usually because it's the wrong day of week) 117 | // we set the listener in case we are waiting on another request 118 | let listenerId = GM_addValueChangeListener("subspleaseSchedule", function(name, old_value, new_value, remote) { 119 | let time = new_value[url]; 120 | if (time) { 121 | putTimeMillis(dataStream, time, true); 122 | } 123 | // remove listener 124 | GM_removeValueChangeListener(listenerId); 125 | }); 126 | } 127 | } 128 | } 129 | 130 | getEplistUrl["subsplease"] = function(partialUrl) { 131 | return subsplease.anime + partialUrl; 132 | } 133 | 134 | searchSite["subsplease"] = function(id, title) { 135 | GM_xmlhttpRequest({ 136 | method: "GET", 137 | url: subsplease.anime, 138 | onload: function(resp) { 139 | if (resp.status == 200) { 140 | // OK 141 | let jqPage = $(resp.response); 142 | let results = []; 143 | // get all anime as list 144 | let list = jqPage.find("#post-wrapper > div > div > .all-shows > .all-shows-link > a"); 145 | // map and filter list to results 146 | list.each(function() { 147 | results.push({ 148 | title: $(this).text().trim(), 149 | href: $(this).attr("href").split("/")[2] 150 | }); 151 | }); 152 | results = results.filter(item => matchResult(item, title)); 153 | // callback 154 | putResults(id, results); 155 | } else { 156 | // error 157 | errorResults(id, "SubsPlease: " + resp.status); 158 | } 159 | } 160 | }); 161 | } 162 | 163 | -------------------------------------------------------------------------------- /src/53_mangaplus.js: -------------------------------------------------------------------------------- 1 | /* manga plus */ 2 | /*******************************************************************************************************************************************************************/ 3 | const mangaplus = {} 4 | mangaplus.base = "https://mangaplus.shueisha.co.jp/"; 5 | mangaplus.manga = mangaplus.base + "titles/"; 6 | mangaplus.base_api = "https://jumpg-webapi.tokyo-cdn.com/api/"; 7 | mangaplus.manga_api = mangaplus.base_api + "title_detail?title_id="; 8 | mangaplus.chapter = mangaplus.base + "viewer/"; 9 | mangaplus.search = mangaplus.base_api + "title_list/all"; 10 | mangaplus.lang_table = { 11 | undefined: "english", 12 | 0: "english", 13 | 1: "spanish", 14 | 2: "french", 15 | 3: "indonesian", 16 | 4: "portuguese", 17 | 5: "russian", 18 | 6: "thai", 19 | } 20 | 21 | /* =============== *\ 22 | protobuf config 23 | \* =============== */ 24 | let Root = protobuf.Root; 25 | let Type = protobuf.Type; 26 | let Field = protobuf.Field; 27 | let Enum = protobuf.Enum; 28 | let OneOf = protobuf.OneOf; 29 | 30 | let Response = new Type("Response") 31 | .add(new OneOf("data") 32 | .add(new Field("success", 1, "SuccessResult")) 33 | .add(new Field("error", 2, "ErrorResult")) 34 | ); 35 | 36 | let ErrorResult = new Type("ErrorResult") 37 | .add(new Field("action", 1, "Action")) 38 | .add(new Field("englishPopup", 2, "Popup")) 39 | .add(new Field("spanishPopup", 3, "Popup")); 40 | 41 | let Action = new Enum("Action") 42 | .add("DEFAULT", 0) 43 | .add("UNAUTHORIZED", 1) 44 | .add("MAINTAINENCE", 2) 45 | .add("GEOIP_BLOCKING", 3); 46 | 47 | let Popup = new Type("Popup") 48 | .add(new Field("subject", 1, "string")) 49 | .add(new Field("body", 2, "string")); 50 | 51 | let SuccessResult = new Type("SuccessResult") 52 | .add(new Field("isFeaturedUpdated", 1, "bool")) 53 | .add(new OneOf("data") 54 | .add(new Field("allTitlesView", 5, "AllTitlesView")) 55 | .add(new Field("titleRankingView", 6, "TitleRankingView")) 56 | .add(new Field("titleDetailView", 8, "TitleDetailView")) 57 | .add(new Field("mangaViewer", 10, "MangaViewer")) 58 | .add(new Field("webHomeView", 11, "WebHomeView")) 59 | ); 60 | 61 | let TitleRankingView = new Type("TitleRankingView") 62 | .add(new Field("titles", 1, "Title", "repeated")); 63 | 64 | let AllTitlesView = new Type("AllTitlesView") 65 | .add(new Field("titles", 1, "Title", "repeated")); 66 | 67 | let WebHomeView = new Type("WebHomeView") 68 | .add(new Field("groups", 2, "UpdatedTitleGroup", "repeated")); 69 | 70 | let TitleDetailView = new Type("TitleDetailView") 71 | .add(new Field("title", 1, "Title")) 72 | .add(new Field("titleImageUrl", 2, "string")) 73 | .add(new Field("overview", 3, "string")) 74 | .add(new Field("backgroundImageUrl", 4, "string")) 75 | .add(new Field("nextTimeStamp", 5, "uint32")) 76 | .add(new Field("updateTiming", 6, "UpdateTiming")) 77 | .add(new Field("viewingPeriodDescription", 7, "string")) 78 | .add(new Field("firstChapterList", 9, "Chapter", "repeated")) 79 | .add(new Field("lastChapterList", 10, "Chapter", "repeated")) 80 | .add(new Field("isSimulReleased", 14, "bool")) 81 | .add(new Field("chaptersDescending", 17, "bool")); 82 | 83 | let UpdateTiming = new Enum("UpdateTiming") 84 | .add("NOT_REGULARLY", 0) 85 | .add("MONDAY", 1) 86 | .add("TUESDAY", 2) 87 | .add("WEDNESDAY", 3) 88 | .add("THURSDAY", 4) 89 | .add("FRIDAY", 5) 90 | .add("SATURDAY", 6) 91 | .add("SUNDAY", 7) 92 | .add("DAY", 8); 93 | 94 | let MangaViewer = new Type("MangaViewer") 95 | .add(new Field("pages", 1, "Page", "repeated")); 96 | 97 | let Title = new Type("Title") 98 | .add(new Field("titleId", 1, "uint32")) 99 | .add(new Field("name", 2, "string")) 100 | .add(new Field("author", 3, "string")) 101 | .add(new Field("portraitImageUrl", 4, "string")) 102 | .add(new Field("landscapeImageUrl", 5, "string")) 103 | .add(new Field("viewCount", 6, "uint32")) 104 | .add(new Field("language", 7, "Language", {"default": 0})); 105 | 106 | let Language = new Enum("Language") 107 | .add("ENGLISH", 0) 108 | .add("SPANISH", 1); 109 | 110 | let UpdatedTitleGroup = new Type("UpdatedTitleGroup") 111 | .add(new Field("groupName", 1, "string")) 112 | .add(new Field("titles", 2, "UpdatedTitle", "repeated")); 113 | 114 | let UpdatedTitle = new Type("UpdatedTitle") 115 | .add(new Field("title", 1, "Title")) 116 | .add(new Field("chapterId", 2, "uint32")) 117 | .add(new Field("chapterName", 3, "string")) 118 | .add(new Field("chapterSubtitle", 4, "string")); 119 | 120 | let Chapter = new Type("Chapter") 121 | .add(new Field("titleId", 1, "uint32")) 122 | .add(new Field("chapterId", 2, "uint32")) 123 | .add(new Field("name", 3, "string")) 124 | .add(new Field("subTitle", 4, "string", "optional")) 125 | .add(new Field("startTimeStamp", 6, "uint32")) 126 | .add(new Field("endTimeStamp", 7, "uint32")); 127 | 128 | let Page = new Type("Page") 129 | .add(new Field("page", 1, "MangaPage")); 130 | 131 | let MangaPage = new Type("MangaPage") 132 | .add(new Field("imageUrl", 1, "string")) 133 | .add(new Field("width", 2, "uint32")) 134 | .add(new Field("height", 3, "uint32")) 135 | .add(new Field("encryptionKey", 5, "string", "optional")); 136 | 137 | let root = new Root() 138 | .define("mangaplus") 139 | .add(Response) 140 | .add(ErrorResult) 141 | .add(Action) 142 | .add(Popup) 143 | .add(SuccessResult) 144 | .add(TitleRankingView) 145 | .add(AllTitlesView) 146 | .add(WebHomeView) 147 | .add(TitleDetailView) 148 | .add(UpdateTiming) 149 | .add(MangaViewer) 150 | .add(Title) 151 | .add(Language) 152 | .add(UpdatedTitleGroup) 153 | .add(UpdatedTitle) 154 | .add(Chapter) 155 | .add(Page) 156 | .add(MangaPage); 157 | 158 | /* =================== *\ 159 | protobuf config end 160 | \* =================== */ 161 | 162 | getEpisodes["mangaplus"] = function(dataStream, url) { 163 | GM_xmlhttpRequest({ 164 | method: "GET", 165 | url: mangaplus.manga_api + url, 166 | responseType: "arraybuffer", 167 | onload: function(resp) { 168 | if (resp.status == 200) { 169 | // OK 170 | // decode response 171 | let buf = resp.response; 172 | let message = Response.decode(new Uint8Array(buf)); 173 | let respJSON = Response.toObject(message); 174 | // check if response is valid 175 | if (!respJSON || !respJSON.success || !respJSON.success.titleDetailView) { 176 | // error 177 | errorEpisodes(dataStream, "MANGA Plus: Bad Response"); 178 | return; 179 | } 180 | 181 | let episodes = []; 182 | let titleDetailView = respJSON.success.titleDetailView; 183 | // insert episodes into list 184 | for (let i = 0; i < (titleDetailView.firstChapterList || []).length; i++) { 185 | let ch = titleDetailView.firstChapterList[i]; 186 | let n = parseInt(ch.name.slice(1) - 1); 187 | episodes[n] = { 188 | text: ch.subTitle, 189 | href: mangaplus.chapter + ch.chapterId, 190 | timestamp: ch.startTimeStamp * 1000, 191 | }; 192 | } 193 | for (let i = 0; i < (titleDetailView.lastChapterList || []).length; i++) { 194 | let ch = titleDetailView.lastChapterList[i]; 195 | let n = parseInt(ch.name.slice(1) - 1); 196 | episodes[n] = { 197 | text: ch.subTitle, 198 | href: mangaplus.chapter + ch.chapterId, 199 | timestamp: ch.startTimeStamp * 1000, 200 | }; 201 | } 202 | // get time of next episode 203 | let time = titleDetailView.nextTimeStamp * 1000; 204 | // callback 205 | putEpisodes(dataStream, episodes, time); 206 | } else { 207 | // error 208 | errorEpisodes(dataStream, "MANGA Plus: " + resp.status); 209 | } 210 | } 211 | }); 212 | } 213 | 214 | getEplistUrl["mangaplus"] = function(partialUrl) { 215 | return mangaplus.manga + partialUrl; 216 | } 217 | 218 | searchSite["mangaplus"] = function(id, title) { 219 | GM_xmlhttpRequest({ 220 | method: "GET", 221 | url: mangaplus.search, 222 | responseType: "arraybuffer", 223 | onload: function(resp) { 224 | if (resp.status == 200) { 225 | // OK 226 | // decode response 227 | let buf = resp.response; 228 | let message = Response.decode(new Uint8Array(buf)); 229 | let respJSON = Response.toObject(message); 230 | // check if response is valid 231 | if (!respJSON || !respJSON.success || !respJSON.success.allTitlesView) { 232 | // error 233 | return; 234 | } 235 | 236 | let titles = respJSON.success.allTitlesView.titles; 237 | let list = []; 238 | // insert results into list 239 | for (let i = 0; i < titles.length; i++) { 240 | let lang = mangaplus.lang_table[titles[i].language]; 241 | list.push({ 242 | title: titles[i].name + " (" + lang + ")", 243 | href: titles[i].titleId, 244 | }); 245 | } 246 | // filter results 247 | let results = list.filter(item => matchResult(item, title)); 248 | // callback 249 | putResults(id, results); 250 | } else { 251 | // error 252 | errorResults(id, "MANGA Plus: " + resp.status); 253 | } 254 | } 255 | }); 256 | } 257 | 258 | -------------------------------------------------------------------------------- /src/90_MAL_list.js: -------------------------------------------------------------------------------- 1 | /* MAL list */ 2 | /*******************************************************************************************************************************************************************/ 3 | const mal = {}; 4 | mal.timerRate = 15000; 5 | mal.recheckInterval = 4; // as a multiple of timerRate 6 | mal.loadRows = 25; 7 | mal.epStrLen = 14; 8 | mal.genericErrorRequest = "Error while performing request"; 9 | mal.userId = null; 10 | mal.CSRFToken = null; 11 | 12 | let onScrollQueue = []; 13 | let requestsQueues = {}; 14 | let timerEventCounter = 0; 15 | 16 | pageLoad["list"] = function() { 17 | // own list 18 | if ($(".header-menu.other").length !== 0) return; 19 | if ($(properties.watching).length !== 1) return; 20 | 21 | // add col header to table 22 | let colHeader = $(`${properties.colHeaderText}`); 23 | $("#list-container").find("th.header-title.title").after(colHeader); 24 | colHeader.css("min-width", "120px"); 25 | 26 | // column header listener 27 | colHeader.on("click", function() { 28 | $(".data.stream").each(function() { 29 | // update dataStream without skipping queue 30 | updateList($(this), true, false); 31 | }); 32 | }); 33 | 34 | // set id and token for more-info requests 35 | mal.userId = $(document.body).attr("data-owner-id"); 36 | mal.CSRFToken = $("meta[name=csrf_token]").attr("content"); 37 | 38 | // load first n rows, start from 1 to remove header 39 | loadRows(1, mal.loadRows + 1); 40 | 41 | // update timer 42 | setInterval(function() { 43 | $(".data.stream").trigger("update-time", [timerEventCounter++]); 44 | }, mal.timerRate); 45 | 46 | // check when an element comes into view 47 | $(window).scroll(function() { 48 | // get viewport 49 | let top = $(window).scrollTop(); 50 | let bottom = top + $(window).height(); 51 | // iterate scroll event queue 52 | let i = onScrollQueue.length; 53 | while (i--) { 54 | if (top < onScrollQueue[i].offset().top && bottom > onScrollQueue[i].offset().top) { 55 | onScrollQueue[i].trigger("intoView"); 56 | // remove element 57 | onScrollQueue.splice(i, 1); 58 | } 59 | } 60 | }); 61 | } 62 | 63 | // loads more-info and saves comment in dataStream 64 | function loadRows(start, end) { 65 | // get rows 66 | let rows = $("#list-container > div.list-block > div > table > tbody").slice(start, end); 67 | if (rows.length == 0) { 68 | return; 69 | } 70 | 71 | // iterate over rows to add dataStream and request more-info 72 | // stop if a row is not valid (usually happens only during first iteration) 73 | for (let i = 0; i < rows.length; i++) { 74 | let row = $(rows[i]); 75 | // if href does not exist then row is invalid 76 | let href = row.find(".list-table-data > .data.title > .link").attr("href"); 77 | if (!href) { 78 | // row was invalid, schedule retry and quit 79 | setTimeout(function() { 80 | loadRows(start, end); 81 | }, 100); 82 | return; 83 | } 84 | // add dataStream to row 85 | let dataStream = $(""); 86 | row.find(".list-table-data > .data.title").after(dataStream); 87 | // get id to make request 88 | let id = href.split("/")[2]; 89 | // finally request more info 90 | requestMoreInfo(id, dataStream); 91 | } 92 | 93 | let dataStreams = $(".data.stream"); 94 | 95 | // table cell listener 96 | dataStreams.on("click", function(e) { 97 | // if ctrl is pressed also reload more-info 98 | if (e.ctrlKey) { 99 | requestMoreInfo(null, $(this)); 100 | } else if (e.target.tagName != "A") { 101 | // avoid reloading if clicked on an anchor element 102 | updateList($(this), true, true); 103 | } 104 | }); 105 | 106 | // complete one episode listener 107 | rows.find(properties.iconAdd).on("click", function() { 108 | let dataStream = $(this).parents(".list-item").find(".data.stream"); 109 | // this timeout is needed, otherwise updateList could be called before the current episode number is updated 110 | setTimeout(function() { 111 | updateList(dataStream, false, false); 112 | }, 0); 113 | }); 114 | 115 | // timer event 116 | dataStreams.on("update-time", function(_, count) { 117 | let dataStream = $(this); 118 | if (dataStream.find(".nextep, .loading, .error").length > 0) { 119 | // do nothing if timer is not needed 120 | return; 121 | } 122 | // get time object from dataStream 123 | let t = dataStream.data("timeMillis"); 124 | // get next episode number 125 | let nextEp = parseInt(dataStream.parents(".list-item").find(properties.findProgress).find(".link").text()) + 1; 126 | if (isNaN(nextEp)) nextEp = 1; 127 | let timeMillis; 128 | // if there is an high priority time use that, otherwise check if episode matches and use low priority time 129 | if (t && t.highPriority) { 130 | timeMillis = t.highPriority - Date.now(); 131 | } else if (t && (t.ep ? t.ep == nextEp : true)) { 132 | timeMillis = t.lowPriority - Date.now(); 133 | } else { 134 | timeMillis = false; 135 | } 136 | 137 | let time; 138 | if (!timeMillis || isNaN(timeMillis) || timeMillis < 1000) { 139 | time = properties.notAired; 140 | if (count !== false && count % mal.recheckInterval === 0) { 141 | updateList(dataStream, true, false); 142 | // we don't need to add the timer again, since updateList will add it if needed, so we can safely return 143 | return; 144 | } 145 | } else { 146 | const d = Math.floor(timeMillis / (1000 * 60 * 60 * 24)); 147 | const h = Math.floor((timeMillis % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); 148 | const m = Math.floor((timeMillis % (1000 * 60 * 60)) / (1000 * 60)); 149 | time = (h < 10 ? "0"+h : h) + "h:" + (m < 10 ? "0" + m : m) + "m"; 150 | if (d > 0) { 151 | time = d + (d == 1 ? " day " : " days ") + time; 152 | } 153 | } 154 | if (dataStream.find(".timer").length === 0) { 155 | // if timer doesn't exist create it 156 | dataStream.prepend("
" + time + "
"); 157 | } else { 158 | // update timer 159 | dataStream.find(".timer").html(time); 160 | } 161 | }); 162 | 163 | // add last element to scroll event queue 164 | let last = rows.last(); 165 | last.on("intoView", function() { 166 | loadRows(end, end + mal.loadRows); 167 | }); 168 | onScrollQueue.push(last); 169 | } 170 | 171 | // request more-info and set data("comment") 172 | function requestMoreInfo(id, dataStream) { 173 | // if id is not set, get it from dataStream 174 | if (!id) { 175 | id = dataStream.parents(".list-item").find(".data.title > .link").attr("href").split("/")[2]; 176 | } 177 | // execute request 178 | GM_xmlhttpRequest({ 179 | method: "POST", 180 | url: "https://myanimelist.net/includes/ajax-no-auth.inc.php?t=6", 181 | headers: { 182 | "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", 183 | "Cookie": document.cookie, 184 | }, 185 | data: jQuery.param({ 186 | memId: mal.userId, 187 | csrf_token: mal.CSRFToken, 188 | type: properties.mode, 189 | id: id, 190 | }), 191 | onload: function(resp) { 192 | let comment = null; 193 | if (resp.status == 200) { 194 | // OK 195 | try { 196 | let respJSON = JSON.parse(resp.response); 197 | let m = respJSON.html.match(properties.commentsRegex); 198 | comment = m[1]; 199 | } catch (e) { 200 | // do nothing on error 201 | } 202 | } 203 | // set data 204 | dataStream.data("comment", comment); 205 | // remove old divs 206 | dataStream.find(".error").remove(); 207 | dataStream.find(".eplist").remove(); 208 | dataStream.find(".nextep").remove(); 209 | dataStream.find(".loading").remove(); 210 | dataStream.find(".timer").remove(); 211 | dataStream.find(".favicon").remove(); 212 | // check if comment exists and is correct 213 | if (comment) { 214 | // comment exists 215 | // url is an array that contains the streaming service and url relative to that service 216 | let url = getUrlFromComment(comment); 217 | if (url) { 218 | // add eplist 219 | let eplistUrl = getEplistUrl[url[0]](url[1]); 220 | dataStream.append("" + properties.ep + " list"); 221 | // add favicon 222 | let domain = getDomainById(url[0]); 223 | if (domain) { 224 | let src = "https://www.google.com/s2/favicons?domain=" + domain; 225 | dataStream.append(""); 226 | } 227 | // load links 228 | updateList(dataStream, true, true); 229 | } else { 230 | // comment invalid 231 | dataStream.append("
Invalid Link
"); 232 | } 233 | } else { 234 | // comment doesn't extst 235 | dataStream.append("
No Link
"); 236 | } 237 | } 238 | }); 239 | } 240 | 241 | // updates dataStream cell 242 | function updateList(dataStream, forceReload, skipQueue) { 243 | // remove old divs 244 | dataStream.find(".error").remove(); 245 | dataStream.find(".nextep").remove(); 246 | dataStream.find(".loading").remove(); 247 | dataStream.find(".timer").remove(); 248 | // get episode list from data 249 | let episodeList = dataStream.data("episodeList"); 250 | if (Array.isArray(episodeList) && !forceReload) { 251 | // episode list exists 252 | updateList_exists(dataStream); 253 | } else { 254 | // episode list doesn't exist or needs to be reloaded 255 | updateList_doesntExist(dataStream, skipQueue); 256 | } 257 | } 258 | 259 | function updateList_exists(dataStream) { 260 | // listitem 261 | let listitem = dataStream.parents(".list-item"); 262 | // get current episode number 263 | let currEp = parseInt(listitem.find(properties.findProgress).find(".link").text()); 264 | if (isNaN(currEp)) currEp = 0; 265 | // add offset to currEp 266 | currEp += parseInt(dataStream.data("offset")); 267 | // get episodes from data 268 | let episodes = dataStream.data("episodeList"); 269 | // create new nextep 270 | let nextep = $("
"); 271 | 272 | if (episodes.length > currEp) { 273 | // there are episodes available 274 | let isAiring = listitem.find(properties.findAiring).length !== 0; 275 | 276 | if (episodes[currEp]) { 277 | // episode is present 278 | let a = $(""); 279 | let t = episodes[currEp].text; 280 | a.text(t.length > mal.epStrLen ? t.substr(0, mal.epStrLen - 1) + "…" : t); 281 | if (t.length > mal.epStrLen) a.attr("title", t); 282 | a.attr("href", episodes[currEp].href); 283 | a.attr("class", isAiring ? "airing" : "non-airing"); 284 | a.css("color", isAiring ? "#2db039" : "#ff730a"); 285 | nextep.append(a); 286 | } else { 287 | // episode is missing 288 | let s = $(`Missing #${currEp + 1}`); 289 | s.css("color", "red"); 290 | nextep.append(s); 291 | } 292 | 293 | if (episodes.length - currEp > 1) { 294 | // if there is more than 1 new ep then put the amount in parenthesis 295 | let batchSize = episodes.length - currEp; 296 | let a = $(""); 297 | a.text(batchSize); 298 | a.attr("title", properties.bulkTooltip.replace("%d", batchSize)); 299 | a.css("cursor", "pointer"); 300 | a.on("click", () => { 301 | // open all episodes in non-focused tabs 302 | for (let ep of episodes.slice(currEp)) { 303 | if (new URL(ep.href).protocol == "magnet:") { 304 | window.open(ep.href, ep.text).blur(); 305 | } else { 306 | GM_openInTab(ep.href, true); 307 | } 308 | } 309 | window.focus(); 310 | }); 311 | nextep.append(" ("); 312 | nextep.append(a); 313 | nextep.append(")"); 314 | } 315 | // add new nextep 316 | dataStream.prepend(nextep); 317 | } else if (currEp > episodes.length && episodes.length > 0) { 318 | // user has watched too many episodes 319 | nextep.append($("
" + properties.latest + episodes.length + "
").css("color", "red")); 320 | // add new nextep 321 | dataStream.prepend(nextep); 322 | } else { 323 | // there aren't episodes available, trigger timer 324 | dataStream.trigger("update-time", [false]); 325 | } 326 | } 327 | 328 | function queueGetEpisodes(dataStream, service, url) { 329 | // get queue for specified service or create it 330 | let queue = requestsQueues[service]; 331 | if (!queue) { 332 | queue = []; 333 | queue.timers = 0; 334 | queue.maxRequests = (queueSettings[service] || queueSettings["default"]).maxRequests; 335 | queue.timeout = (queueSettings[service] || queueSettings["default"]).timeout; 336 | requestsQueues[service] = queue; 337 | } 338 | 339 | if (queue.timers < queue.maxRequests) { 340 | // if there are no active timers, set timer and do request 341 | queue.timers++ 342 | getEpisodes[service](dataStream, url); 343 | setTimeout(function() { 344 | dequeueGetEpisodes(service); 345 | }, queue.timeout); 346 | } else { 347 | // queue full, append to end 348 | queue.push({ 349 | dataStream: dataStream, 350 | url: url, 351 | }); 352 | } 353 | } 354 | 355 | function dequeueGetEpisodes(service) { 356 | let queue = requestsQueues[service]; 357 | 358 | if (queue.length > 0) { 359 | // if there are elements in queue, request the first and restart the timer 360 | let req = queue.shift(); 361 | getEpisodes[service](req.dataStream, req.url); 362 | setTimeout(function() { 363 | dequeueGetEpisodes(service); 364 | }, queue.timeout); 365 | } else { 366 | // queue empty, terminate timer 367 | queue.timers--; 368 | } 369 | } 370 | 371 | function updateList_doesntExist(dataStream, skipQueue) { 372 | // check if comment exists and is correct 373 | let comment = dataStream.data("comment"); 374 | if (comment) { 375 | // comment exists 376 | // url is an array that contains the streaming service and url relative to that service 377 | let url = getUrlFromComment(comment); 378 | if (url) { 379 | // comment valid 380 | // add loading 381 | dataStream.prepend("
Loading...
"); 382 | // set offset data 383 | dataStream.data("offset", url[2] ? url[2] : 0); 384 | // queue getEpisode if needed 385 | if (!skipQueue) { 386 | queueGetEpisodes(dataStream, url[0], url[1]); 387 | } else { 388 | getEpisodes[url[0]](dataStream, url[1]); 389 | } 390 | } else { 391 | // comment invalid 392 | dataStream.append("
Invalid Link
"); 393 | } 394 | } else { 395 | // comment doesn't extst 396 | dataStream.append("
No Link
"); 397 | } 398 | } 399 | 400 | // save episodeList in dataStream 401 | // timeMillis can be the unix timestamp of the next airing episode 402 | function putEpisodes(dataStream, episodes, timeMillis) { 403 | // add episodes to dataStream 404 | dataStream.data("episodeList", episodes); 405 | // add timeMillis to dataStream 406 | if (timeMillis) { 407 | // timeMillis is valid 408 | dataStream.data("timeMillis", { highPriority: timeMillis }); 409 | } else if (properties.mode == "anime") { 410 | // timeMillis doesn't exist, get time from anilist 411 | anilist_setTimeMillis(dataStream, true); 412 | } 413 | updateList(dataStream, false, false); 414 | } 415 | 416 | // save unix timestamp of next airing episode in dataStream 417 | function putTimeMillis(dataStream, timeMillis, highPriority, ep) { 418 | let t = dataStream.data("timeMillis") || {}; 419 | if (highPriority) { 420 | t.highPriority = timeMillis; 421 | } else { 422 | t.lowPriority = timeMillis; 423 | } 424 | if (ep) t.ep = ep; 425 | dataStream.data("timeMillis", t); 426 | dataStream.trigger("update-time"); 427 | updateList(dataStream, false, false); 428 | } 429 | 430 | // set error to dataStream 431 | function errorEpisodes(dataStream, error) { 432 | // remove old divs 433 | dataStream.find(".error").remove(); 434 | dataStream.find(".nextep").remove(); 435 | dataStream.find(".loading").remove(); 436 | dataStream.find(".timer").remove(); 437 | // create error div 438 | dataStream.prepend($(`
${error || mal.genericErrorRequest}
`).css("color", "red")); 439 | } 440 | 441 | -------------------------------------------------------------------------------- /MALstreaming.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name MALstreaming 3 | // @namespace https://github.com/mattiadr/MALstreaming 4 | // @version 5.89 5 | // @author https://github.com/mattiadr 6 | // @description Adds various anime and manga links to MAL 7 | // @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= 8 | // @run-at document-idle 9 | // @updateURL https://raw.githubusercontent.com/mattiadr/MALstreaming/master/MALstreaming.user.js 10 | // @downloadURL https://raw.githubusercontent.com/mattiadr/MALstreaming/master/MALstreaming.user.js 11 | // @supportURL https://github.com/mattiadr/MALstreaming/issues 12 | // @match https://myanimelist.net/animelist/* 13 | // @match https://myanimelist.net/ownlist/anime/*/edit* 14 | // @match https://myanimelist.net/ownlist/anime/add?selected_series_id=* 15 | // @match https://myanimelist.net/mangalist/* 16 | // @match https://myanimelist.net/ownlist/manga/*/edit* 17 | // @match https://myanimelist.net/ownlist/manga/add?selected_manga_id=* 18 | // @require https://code.jquery.com/jquery-3.7.1.min.js 19 | // @require https://cdn.rawgit.com/dcodeIO/protobuf.js/6.8.8/dist/protobuf.js 20 | // @grant GM_xmlhttpRequest 21 | // @grant GM_openInTab 22 | // @grant GM_setValue 23 | // @grant GM_getValue 24 | // @grant GM_deleteValue 25 | // @grant GM_addValueChangeListener 26 | // @grant GM_removeValueChangeListener 27 | // @grant window.close 28 | // @connect * 29 | // ==/UserScript== 30 | 31 | /* generic */ 32 | /*******************************************************************************************************************************************************************/ 33 | // array of all streaming services 34 | const streamingServices = [ 35 | // anime 36 | { id: "erairaws", type: "anime", name: "Erai-raws", domain: "www.erai-raws.info" }, 37 | { id: "subsplease", type: "anime", name: "SubsPlease", domain: "subsplease.org" }, 38 | // manga 39 | { id: "mangadex", type: "manga", name: "MangaDex", domain: "mangadex.org" }, 40 | { id: "mangaplus", type: "manga", name: "MANGA Plus", domain: "mangaplus.shueisha.co.jp" }, 41 | ]; 42 | // contains variable properties for anime/manga modes 43 | let properties = {}; 44 | properties.anime = { 45 | mode: "anime", 46 | watching: ".list-unit.watching", 47 | colHeaderText: "Watch", 48 | commentsRegex: /Notes: ([\S ]+) /, 49 | iconAdd: ".icon-add-episode", 50 | findProgress: ".data.progress", 51 | findAiring: "span.content-status:contains('Airing')", 52 | latest: "Latest ep is #", 53 | notAired: "Not Yet Aired", 54 | ep: "Ep.", 55 | editPageBox: "#add_anime_comments", 56 | bulkTooltip: "Open %d episodes in bulk", 57 | }; 58 | properties.manga = { 59 | mode: "manga", 60 | watching: ".list-unit.reading", 61 | colHeaderText: "Read", 62 | commentsRegex: /Notes: ([\S ]+) /, 63 | iconAdd: ".icon-add-chapter", 64 | findProgress: ".data.chapter", 65 | findAiring: "span.content-status:contains('Publishing')", 66 | latest: "Latest ch is #", 67 | notAired: "Not Yet Published", 68 | ep: "Ch.", 69 | editPageBox: "#add_manga_comments", 70 | bulkTooltip: "Open %d chapters in bulk", 71 | }; 72 | // contains all functions to execute on page load 73 | const pageLoad = {}; 74 | // contains all functions to get the episodes list from the streaming services 75 | // must callback to putEpisodes(dataStream, episodes, timeMillis) 76 | const getEpisodes = {}; 77 | // contains queue settings for queuing requests to services (optional) 78 | // must contain `maxRequests` and `timout` 79 | const queueSettings = {}; 80 | queueSettings["default"] = { 81 | maxRequests: 1, 82 | timeout: 1000, 83 | } 84 | // contains all functions to get the episode list url from the partial url 85 | const getEplistUrl = {}; 86 | // contains all functions to execute the search on the streaming services 87 | // must callback to putResults(results) 88 | const searchSite = {}; 89 | 90 | // return an array that contains the streaming service and url relative to that service or false if comment is not valid 91 | function getUrlFromComment(comment) { 92 | let c = comment.split(" "); 93 | if (c.length < 2) return false; 94 | for (let i = 0; i < streamingServices.length; i++) { 95 | if (streamingServices[i].id == c[0]) return c; 96 | } 97 | return false; 98 | } 99 | 100 | // estimate time before next chapter as min of last n chapters 101 | function estimateTimeMillis(episodes, n) { 102 | if (episodes.length == 0) return undefined; 103 | let prev = null; 104 | let min = undefined; 105 | for (let i = episodes.length - 1; i > Math.max(0, episodes.length - 1 - n); i--) { 106 | if (!episodes[i]) continue; 107 | if (prev && episodes[i].timestamp != prev) { 108 | let diff = prev - episodes[i].timestamp; 109 | if (!min || diff < min && diff > 0) min = diff; 110 | } 111 | prev = episodes[i].timestamp; 112 | } 113 | return episodes[episodes.length - 1].timestamp + min; 114 | } 115 | 116 | // returns the domain for the streaming service or false if ss doesn't exist 117 | function getDomainById(id) { 118 | for (let i = 0; i < streamingServices.length; i++) { 119 | if (streamingServices[i].id == id) { 120 | return streamingServices[i].domain; 121 | } 122 | } 123 | return false; 124 | } 125 | 126 | // returns true if the result matches the title 127 | function matchResult(result, title) { 128 | // split title into tokens 129 | let split = title.split(/\W+/g); 130 | for (let i = 0; i < split.length; i++) { 131 | // result must contain all tokens 132 | if (!result.title.toLowerCase().includes(split[i].toLowerCase())) { 133 | return false; 134 | } 135 | } 136 | return true; 137 | } 138 | 139 | // stackexchange's string format utility 140 | String.prototype.formatUnicorn = function() { 141 | let e = this.toString(); 142 | if (!arguments.length) return e; 143 | let t = typeof arguments[0]; 144 | let n = "string" === t || "number" === t ? Array.prototype.slice.call(arguments) : arguments[0]; 145 | for (let i in n) { 146 | e = e.replace(new RegExp("\\{" + i + "\\}", "gi"), n[i]); 147 | } 148 | return e; 149 | } 150 | 151 | /* anilist */ 152 | /*******************************************************************************************************************************************************************/ 153 | const anilist = {}; 154 | anilist.api = "https://graphql.anilist.co"; 155 | anilist.query = `\ 156 | query ($idMal: Int) { 157 | Media(type: ANIME, idMal: $idMal) { 158 | airingSchedule(notYetAired: true, perPage: 1) { 159 | nodes { 160 | episode 161 | airingAt 162 | } 163 | } 164 | } 165 | }`; 166 | 167 | // request time until next episode for the specified anime id 168 | function requestTime(id) { 169 | // prepare data 170 | let data = { 171 | query: anilist.query, 172 | variables: { idMal: id } 173 | }; 174 | // do request 175 | GM_xmlhttpRequest({ 176 | method: "POST", 177 | url: anilist.api, 178 | headers: { "Content-Type": "application/json" }, 179 | data: JSON.stringify(data), 180 | onload: function(resp) { 181 | let res = JSON.parse(resp.response); 182 | let times = GM_getValue("anilistTimes", {}); 183 | // get data from response 184 | let sched = res.data.Media.airingSchedule.nodes[0]; 185 | // if there is no episode then it means the last episode just notYetAired 186 | if (!sched || !sched.episode) return; 187 | let ep = sched.episode; 188 | let timeMillis = sched.airingAt * 1000; 189 | // set time, ep is episode the timer is referring to 190 | times[id] = { 191 | ep: ep, 192 | timeMillis: timeMillis 193 | }; 194 | // put times in GM value 195 | GM_setValue("anilistTimes", times); 196 | } 197 | }); 198 | } 199 | 200 | // puts timeMillis into dataStream, then calls back 201 | function anilist_setTimeMillis(dataStream, canReload) { 202 | let listitem = dataStream.parents(".list-item"); 203 | 204 | let times = GM_getValue("anilistTimes", false); 205 | // get anime id 206 | let id = listitem.find(".data.title > .link").attr("href").split("/")[2]; 207 | let t = times ? times[id] : false; 208 | 209 | if (times && t && Date.now() < t.timeMillis) { 210 | // time doesn't need to update 211 | // set timeMillis, this is used to check if anilist timer is referring to next episode 212 | putTimeMillis(dataStream, t.timeMillis, false, t.ep); 213 | } else if (canReload) { 214 | // add value change listener 215 | let listenerId = GM_addValueChangeListener("anilistTimes", function(name, old_value, new_value, remote) { 216 | // reload 217 | anilist_setTimeMillis(dataStream, false); 218 | // remove listener 219 | GM_removeValueChangeListener(listenerId); 220 | }); 221 | // api request to anilist 222 | requestTime(id); 223 | } 224 | } 225 | 226 | /* cookies */ 227 | /*******************************************************************************************************************************************************************/ 228 | // array with services that require cookies to make requests 229 | const cookieServices = [ 230 | // anime 231 | // manga 232 | ]; 233 | 234 | // checks if i need/can load cookies and returns the cookieService 235 | function needsCookies(id, status) { 236 | for (let i = 0; i < cookieServices.length; i++) { 237 | if (cookieServices[i].id == id && cookieServices[i].status == status) return cookieServices[i]; 238 | } 239 | return false; 240 | } 241 | 242 | // load cookies for specified service, then calls back 243 | function loadCookies(cookieService, callback) { 244 | let lc = GM_getValue("loadCookies", {}); 245 | if (lc[cookieService.id] === undefined || lc[cookieService.id] + 30*1000 < Date.now()) { 246 | lc[cookieService.id] = Date.now(); 247 | GM_setValue("loadCookies", lc); 248 | GM_openInTab(cookieService.url, true); 249 | } 250 | if (callback) { 251 | setTimeout(function() { 252 | callback(); 253 | }, cookieService.timeout); 254 | } 255 | } 256 | 257 | // function to execute when script is run on website to load cookies from 258 | pageLoad["loadCookies"] = function(cookieService) { 259 | let lc = GM_getValue("loadCookies", {}); 260 | if (lc[cookieService.id] && cookieService.loaded()) { 261 | lc[cookieService.id] = false; 262 | GM_setValue("loadCookies", lc); 263 | window.close(); 264 | } 265 | } 266 | 267 | /* erai-raws */ 268 | /*******************************************************************************************************************************************************************/ 269 | const erairaws = {}; 270 | erairaws.base = "https://www.erai-raws.info/"; 271 | erairaws.anime = erairaws.base + "anime-list/"; 272 | erairaws.search = erairaws.base + "?s=" 273 | 274 | getEpisodes["erairaws"] = function(dataStream, url) { 275 | // request 276 | GM_xmlhttpRequest({ 277 | method: "POST", 278 | url: erairaws.anime + url, 279 | onload: function(resp) { 280 | if (resp.status == 200) { 281 | // OK 282 | let jqPage = $(resp.response); 283 | let episodes = []; 284 | 285 | jqPage.find("#menu0 > .table").each(function() { 286 | let tt = $(this).find(".tooltip2"); 287 | let type = tt.text(); 288 | let m = tt.next().text().match(/[\d\.]+/g); 289 | 290 | let release = $(this).find(".release-links").first(); 291 | let magnet = release.find(".load_more_links_buttons:contains(magnet)").attr("href"); 292 | 293 | if (type == "B") { 294 | // batch 295 | let first = parseInt(m[m.length - 2]); 296 | let last = parseInt(m[m.length - 1]); 297 | 298 | let obj = { 299 | text: `Batch ${first} ~ ${last}`, 300 | href: magnet, 301 | }; 302 | 303 | for (let i = first - 1; i < last; i++) { 304 | episodes[i] = obj; 305 | } 306 | } else if (type == "E" || type == "A" || type == "F") { 307 | // encoding || airing || final 308 | let ep = parseInt(m[m.length - 1]); 309 | let res = release.find("span").text().match(/^\w+/)[0]; 310 | 311 | if (!episodes[ep - 1]) { 312 | episodes[ep - 1] = { 313 | text: `Ep ${ep} (${res})`, 314 | href: magnet, 315 | } 316 | } 317 | } else { 318 | // unknown type 319 | return; 320 | } 321 | }); 322 | 323 | // callback 324 | putEpisodes(dataStream, episodes, undefined); 325 | } else { 326 | // error 327 | errorEpisodes(dataStream, "Erai-raws: " + resp.status); 328 | } 329 | } 330 | }); 331 | } 332 | 333 | getEplistUrl["erairaws"] = function(partialUrl) { 334 | return erairaws.anime + partialUrl; 335 | } 336 | 337 | searchSite["erairaws"] = function(id, title) { 338 | GM_xmlhttpRequest({ 339 | method: "GET", 340 | url: erairaws.search + title, 341 | onload: function(resp) { 342 | if (resp.status == 200) { 343 | // OK 344 | let jqPage = $(resp.response); 345 | let results = jqPage.find("#main .entry-title > a").map(function() { 346 | return { 347 | title: $(this).text().trim(), 348 | href: $(this).attr("href").split("/")[4], 349 | }; 350 | }); 351 | 352 | // callback 353 | putResults(id, results); 354 | } else { 355 | // error 356 | errorResults(id, "Erai-raws: " + resp.status); 357 | } 358 | } 359 | }); 360 | } 361 | 362 | /* subsplease */ 363 | /*******************************************************************************************************************************************************************/ 364 | const subsplease = {}; 365 | subsplease.base = "https://subsplease.org/"; 366 | subsplease.anime = subsplease.base + "shows/"; 367 | subsplease.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone 368 | subsplease.api = subsplease.base + "api/?f=show&tz=" + subsplease.timezone + "&sid="; 369 | subsplease.schedule = subsplease.base + "api/?f=schedule&h=true&tz=" + subsplease.timezone 370 | 371 | getEpisodes["subsplease"] = function(dataStream, url) { 372 | let ids = GM_getValue("subspleaseIDS", {}); 373 | if (ids[url]) { 374 | // found id, request episodes 375 | subsplease_getEpisodesFromAPI(dataStream, ids[url], url); 376 | } else { 377 | // id not found, request id then episodes 378 | GM_xmlhttpRequest({ 379 | method: "GET", 380 | url: subsplease.anime + url, 381 | onload: function(resp) { 382 | if (resp.status == 200) { 383 | // OK 384 | let jqPage = $(resp.response); 385 | // get id 386 | let id = jqPage.find("#show-release-table").attr("sid"); 387 | // save id in GM values 388 | ids[url] = id; 389 | GM_setValue("subspleaseIDS", ids); 390 | // get episodes 391 | subsplease_getEpisodesFromAPI(dataStream, id, url); 392 | } else { 393 | // error 394 | errorEpisodes(dataStream, "SubsPlease: " + resp.status); 395 | } 396 | } 397 | }); 398 | } 399 | } 400 | 401 | function subsplease_getEpisodesFromAPI(dataStream, id, url) { 402 | GM_xmlhttpRequest({ 403 | method: "GET", 404 | url: subsplease.api + id, 405 | onload: function(resp) { 406 | if (resp.status == 200) { 407 | // OK 408 | let res = JSON.parse(resp.response); 409 | let episodes = []; 410 | // loop through values 411 | Object.values(res.episode).forEach(ep => { 412 | let dwn = ep.downloads.pop(); 413 | episodes[parseInt(ep.episode) - 1] = { 414 | text: `Ep ${ep.episode} (${dwn.res}p)`, 415 | href: dwn.magnet 416 | }; 417 | }); 418 | // callback 419 | putEpisodes(dataStream, episodes, undefined); 420 | subsplease_getAirTime(dataStream, url); 421 | } else { 422 | // error 423 | errorEpisodes(dataStream, "SubsPlease: " + resp.status); 424 | } 425 | } 426 | }); 427 | } 428 | 429 | function subsplease_getAirTime(dataStream, url) { 430 | let lastTs = GM_getValue("subspleaseScheduleDate", 0); 431 | let now = +new Date(); 432 | 433 | // request at most once every 5 minutes 434 | if (now > lastTs + 5 * 60 * 1000) { 435 | // we request schedule, invalidate the cache and set the date immediately to avoid other dataStream requesting it too 436 | GM_deleteValue("subspleaseSchedule") 437 | GM_setValue("subspleaseScheduleDate", now); 438 | // and we start the request for the schedule 439 | GM_xmlhttpRequest({ 440 | method: "GET", 441 | url: subsplease.schedule, 442 | onload: function(resp) { 443 | let timeMillis = undefined; 444 | if (resp.status == 200) { 445 | // OK 446 | let res = JSON.parse(resp.response); 447 | let schedule = {}; 448 | res.schedule.forEach(s => { 449 | if (!s.aired) { 450 | let airTime = new Date(); 451 | let t = s.time.split(":"); 452 | airTime.setHours(t[0], t[1], 0, 0); 453 | schedule[s.page] = +airTime; 454 | } 455 | }); 456 | // set time 457 | let time = schedule[url]; 458 | if (time) { 459 | putTimeMillis(dataStream, time, true); 460 | } 461 | // save schedule 462 | GM_setValue("subspleaseSchedule", schedule); 463 | } else { 464 | // error, remove date so we may retry the request 465 | GM_deleteValue("subspleaseScheduleDate"); 466 | } 467 | } 468 | }); 469 | } else { 470 | let schedule = GM_getValue("subspleaseSchedule", {}); 471 | let time = schedule[url]; 472 | if (time) { 473 | // time is valid, just callback 474 | putTimeMillis(dataStream, time, true); 475 | } else { 476 | // time is not available, can happen if we already sent a request from another dataStream and we are waiting for results 477 | // or if the time is actually not available (usually because it's the wrong day of week) 478 | // we set the listener in case we are waiting on another request 479 | let listenerId = GM_addValueChangeListener("subspleaseSchedule", function(name, old_value, new_value, remote) { 480 | let time = new_value[url]; 481 | if (time) { 482 | putTimeMillis(dataStream, time, true); 483 | } 484 | // remove listener 485 | GM_removeValueChangeListener(listenerId); 486 | }); 487 | } 488 | } 489 | } 490 | 491 | getEplistUrl["subsplease"] = function(partialUrl) { 492 | return subsplease.anime + partialUrl; 493 | } 494 | 495 | searchSite["subsplease"] = function(id, title) { 496 | GM_xmlhttpRequest({ 497 | method: "GET", 498 | url: subsplease.anime, 499 | onload: function(resp) { 500 | if (resp.status == 200) { 501 | // OK 502 | let jqPage = $(resp.response); 503 | let results = []; 504 | // get all anime as list 505 | let list = jqPage.find("#post-wrapper > div > div > .all-shows > .all-shows-link > a"); 506 | // map and filter list to results 507 | list.each(function() { 508 | results.push({ 509 | title: $(this).text().trim(), 510 | href: $(this).attr("href").split("/")[2] 511 | }); 512 | }); 513 | results = results.filter(item => matchResult(item, title)); 514 | // callback 515 | putResults(id, results); 516 | } else { 517 | // error 518 | errorResults(id, "SubsPlease: " + resp.status); 519 | } 520 | } 521 | }); 522 | } 523 | 524 | /* mangadex */ 525 | /*******************************************************************************************************************************************************************/ 526 | const mangadex = {}; 527 | mangadex.base = "https://mangadex.org/"; 528 | mangadex.base_api = "https://api.mangadex.org/"; 529 | mangadex.manga = mangadex.base + "title/" 530 | mangadex.lang_code = "en"; 531 | mangadex.manga_api = mangadex.base_api + `manga/{0}/feed?limit=500&order[chapter]=asc&offset={1}&translatedLanguage[]=${mangadex.lang_code}`; 532 | mangadex.chapter = mangadex.base + "chapter/"; 533 | mangadex.search_api = mangadex.base_api + "manga?title="; 534 | 535 | getEpisodes["mangadex"] = function(dataStream, url, offset=0, episodes=[]) { 536 | GM_xmlhttpRequest({ 537 | method: "GET", 538 | url: mangadex.manga_api.formatUnicorn(url, offset), 539 | onload: function(resp) { 540 | if (resp.status == 200) { 541 | let res = JSON.parse(resp.response); 542 | if (res.result != "ok") { 543 | // error 544 | errorResults(id, "MangaDex: " + res.result); 545 | } 546 | // OK 547 | for (let i = 0; i < res.data.length; i++) { 548 | let chapter = res.data[i]; 549 | let n = chapter.attributes.chapter; 550 | let t = `Chapter ${n}`; 551 | if (chapter.attributes.title) t += `: ${chapter.attributes.title}`; 552 | episodes[n - 1] = { 553 | text: t, 554 | href: mangadex.chapter + chapter.id, 555 | timestamp: new Date(chapter.attributes.createdAt).getTime(), 556 | } 557 | } 558 | // check if we got all the episodes 559 | if (offset + 500 >= res.total) { 560 | // estimate timeMillis 561 | let timeMillis = estimateTimeMillis(episodes, 5); 562 | // callback 563 | putEpisodes(dataStream, episodes, timeMillis); 564 | } else { 565 | // request next 500 episodes 566 | getEpisodes["mangadex"](dataStream, url, offset + 500, episodes); 567 | } 568 | } else { 569 | // error 570 | errorEpisodes(dataStream, "MangaDex: " + resp.status); 571 | } 572 | } 573 | }); 574 | } 575 | 576 | getEplistUrl["mangadex"] = function(partialUrl) { 577 | return mangadex.manga + partialUrl; 578 | } 579 | 580 | searchSite["mangadex"] = function(id, title) { 581 | GM_xmlhttpRequest({ 582 | method: "GET", 583 | url: mangadex.search_api + encodeURI(title), 584 | onload: function(resp) { 585 | if (resp.status == 200) { 586 | let res = JSON.parse(resp.response); 587 | if (res.result != "ok") { 588 | // error 589 | errorResults(id, "MangaDex: " + res.result); 590 | } 591 | // OK 592 | let results = []; 593 | for (let i = 0; i < res.data.length; i++) { 594 | let manga = res.data[i]; 595 | results.push({ 596 | title: manga.attributes.title.en || manga.attributes.title.jp, 597 | href: manga.id, 598 | }); 599 | } 600 | // callback 601 | putResults(id, results); 602 | } else { 603 | // error 604 | errorResults(id, "MangaDex: " + resp.status); 605 | } 606 | } 607 | }); 608 | } 609 | 610 | /* manga plus */ 611 | /*******************************************************************************************************************************************************************/ 612 | const mangaplus = {} 613 | mangaplus.base = "https://mangaplus.shueisha.co.jp/"; 614 | mangaplus.manga = mangaplus.base + "titles/"; 615 | mangaplus.base_api = "https://jumpg-webapi.tokyo-cdn.com/api/"; 616 | mangaplus.manga_api = mangaplus.base_api + "title_detail?title_id="; 617 | mangaplus.chapter = mangaplus.base + "viewer/"; 618 | mangaplus.search = mangaplus.base_api + "title_list/all"; 619 | mangaplus.lang_table = { 620 | undefined: "english", 621 | 0: "english", 622 | 1: "spanish", 623 | 2: "french", 624 | 3: "indonesian", 625 | 4: "portuguese", 626 | 5: "russian", 627 | 6: "thai", 628 | } 629 | 630 | /* =============== *\ 631 | protobuf config 632 | \* =============== */ 633 | let Root = protobuf.Root; 634 | let Type = protobuf.Type; 635 | let Field = protobuf.Field; 636 | let Enum = protobuf.Enum; 637 | let OneOf = protobuf.OneOf; 638 | 639 | let Response = new Type("Response") 640 | .add(new OneOf("data") 641 | .add(new Field("success", 1, "SuccessResult")) 642 | .add(new Field("error", 2, "ErrorResult")) 643 | ); 644 | 645 | let ErrorResult = new Type("ErrorResult") 646 | .add(new Field("action", 1, "Action")) 647 | .add(new Field("englishPopup", 2, "Popup")) 648 | .add(new Field("spanishPopup", 3, "Popup")); 649 | 650 | let Action = new Enum("Action") 651 | .add("DEFAULT", 0) 652 | .add("UNAUTHORIZED", 1) 653 | .add("MAINTAINENCE", 2) 654 | .add("GEOIP_BLOCKING", 3); 655 | 656 | let Popup = new Type("Popup") 657 | .add(new Field("subject", 1, "string")) 658 | .add(new Field("body", 2, "string")); 659 | 660 | let SuccessResult = new Type("SuccessResult") 661 | .add(new Field("isFeaturedUpdated", 1, "bool")) 662 | .add(new OneOf("data") 663 | .add(new Field("allTitlesView", 5, "AllTitlesView")) 664 | .add(new Field("titleRankingView", 6, "TitleRankingView")) 665 | .add(new Field("titleDetailView", 8, "TitleDetailView")) 666 | .add(new Field("mangaViewer", 10, "MangaViewer")) 667 | .add(new Field("webHomeView", 11, "WebHomeView")) 668 | ); 669 | 670 | let TitleRankingView = new Type("TitleRankingView") 671 | .add(new Field("titles", 1, "Title", "repeated")); 672 | 673 | let AllTitlesView = new Type("AllTitlesView") 674 | .add(new Field("titles", 1, "Title", "repeated")); 675 | 676 | let WebHomeView = new Type("WebHomeView") 677 | .add(new Field("groups", 2, "UpdatedTitleGroup", "repeated")); 678 | 679 | let TitleDetailView = new Type("TitleDetailView") 680 | .add(new Field("title", 1, "Title")) 681 | .add(new Field("titleImageUrl", 2, "string")) 682 | .add(new Field("overview", 3, "string")) 683 | .add(new Field("backgroundImageUrl", 4, "string")) 684 | .add(new Field("nextTimeStamp", 5, "uint32")) 685 | .add(new Field("updateTiming", 6, "UpdateTiming")) 686 | .add(new Field("viewingPeriodDescription", 7, "string")) 687 | .add(new Field("firstChapterList", 9, "Chapter", "repeated")) 688 | .add(new Field("lastChapterList", 10, "Chapter", "repeated")) 689 | .add(new Field("isSimulReleased", 14, "bool")) 690 | .add(new Field("chaptersDescending", 17, "bool")); 691 | 692 | let UpdateTiming = new Enum("UpdateTiming") 693 | .add("NOT_REGULARLY", 0) 694 | .add("MONDAY", 1) 695 | .add("TUESDAY", 2) 696 | .add("WEDNESDAY", 3) 697 | .add("THURSDAY", 4) 698 | .add("FRIDAY", 5) 699 | .add("SATURDAY", 6) 700 | .add("SUNDAY", 7) 701 | .add("DAY", 8); 702 | 703 | let MangaViewer = new Type("MangaViewer") 704 | .add(new Field("pages", 1, "Page", "repeated")); 705 | 706 | let Title = new Type("Title") 707 | .add(new Field("titleId", 1, "uint32")) 708 | .add(new Field("name", 2, "string")) 709 | .add(new Field("author", 3, "string")) 710 | .add(new Field("portraitImageUrl", 4, "string")) 711 | .add(new Field("landscapeImageUrl", 5, "string")) 712 | .add(new Field("viewCount", 6, "uint32")) 713 | .add(new Field("language", 7, "Language", {"default": 0})); 714 | 715 | let Language = new Enum("Language") 716 | .add("ENGLISH", 0) 717 | .add("SPANISH", 1); 718 | 719 | let UpdatedTitleGroup = new Type("UpdatedTitleGroup") 720 | .add(new Field("groupName", 1, "string")) 721 | .add(new Field("titles", 2, "UpdatedTitle", "repeated")); 722 | 723 | let UpdatedTitle = new Type("UpdatedTitle") 724 | .add(new Field("title", 1, "Title")) 725 | .add(new Field("chapterId", 2, "uint32")) 726 | .add(new Field("chapterName", 3, "string")) 727 | .add(new Field("chapterSubtitle", 4, "string")); 728 | 729 | let Chapter = new Type("Chapter") 730 | .add(new Field("titleId", 1, "uint32")) 731 | .add(new Field("chapterId", 2, "uint32")) 732 | .add(new Field("name", 3, "string")) 733 | .add(new Field("subTitle", 4, "string", "optional")) 734 | .add(new Field("startTimeStamp", 6, "uint32")) 735 | .add(new Field("endTimeStamp", 7, "uint32")); 736 | 737 | let Page = new Type("Page") 738 | .add(new Field("page", 1, "MangaPage")); 739 | 740 | let MangaPage = new Type("MangaPage") 741 | .add(new Field("imageUrl", 1, "string")) 742 | .add(new Field("width", 2, "uint32")) 743 | .add(new Field("height", 3, "uint32")) 744 | .add(new Field("encryptionKey", 5, "string", "optional")); 745 | 746 | let root = new Root() 747 | .define("mangaplus") 748 | .add(Response) 749 | .add(ErrorResult) 750 | .add(Action) 751 | .add(Popup) 752 | .add(SuccessResult) 753 | .add(TitleRankingView) 754 | .add(AllTitlesView) 755 | .add(WebHomeView) 756 | .add(TitleDetailView) 757 | .add(UpdateTiming) 758 | .add(MangaViewer) 759 | .add(Title) 760 | .add(Language) 761 | .add(UpdatedTitleGroup) 762 | .add(UpdatedTitle) 763 | .add(Chapter) 764 | .add(Page) 765 | .add(MangaPage); 766 | 767 | /* =================== *\ 768 | protobuf config end 769 | \* =================== */ 770 | 771 | getEpisodes["mangaplus"] = function(dataStream, url) { 772 | GM_xmlhttpRequest({ 773 | method: "GET", 774 | url: mangaplus.manga_api + url, 775 | responseType: "arraybuffer", 776 | onload: function(resp) { 777 | if (resp.status == 200) { 778 | // OK 779 | // decode response 780 | let buf = resp.response; 781 | let message = Response.decode(new Uint8Array(buf)); 782 | let respJSON = Response.toObject(message); 783 | // check if response is valid 784 | if (!respJSON || !respJSON.success || !respJSON.success.titleDetailView) { 785 | // error 786 | errorEpisodes(dataStream, "MANGA Plus: Bad Response"); 787 | return; 788 | } 789 | 790 | let episodes = []; 791 | let titleDetailView = respJSON.success.titleDetailView; 792 | // insert episodes into list 793 | for (let i = 0; i < (titleDetailView.firstChapterList || []).length; i++) { 794 | let ch = titleDetailView.firstChapterList[i]; 795 | let n = parseInt(ch.name.slice(1) - 1); 796 | episodes[n] = { 797 | text: ch.subTitle, 798 | href: mangaplus.chapter + ch.chapterId, 799 | timestamp: ch.startTimeStamp * 1000, 800 | }; 801 | } 802 | for (let i = 0; i < (titleDetailView.lastChapterList || []).length; i++) { 803 | let ch = titleDetailView.lastChapterList[i]; 804 | let n = parseInt(ch.name.slice(1) - 1); 805 | episodes[n] = { 806 | text: ch.subTitle, 807 | href: mangaplus.chapter + ch.chapterId, 808 | timestamp: ch.startTimeStamp * 1000, 809 | }; 810 | } 811 | // get time of next episode 812 | let time = titleDetailView.nextTimeStamp * 1000; 813 | // callback 814 | putEpisodes(dataStream, episodes, time); 815 | } else { 816 | // error 817 | errorEpisodes(dataStream, "MANGA Plus: " + resp.status); 818 | } 819 | } 820 | }); 821 | } 822 | 823 | getEplistUrl["mangaplus"] = function(partialUrl) { 824 | return mangaplus.manga + partialUrl; 825 | } 826 | 827 | searchSite["mangaplus"] = function(id, title) { 828 | GM_xmlhttpRequest({ 829 | method: "GET", 830 | url: mangaplus.search, 831 | responseType: "arraybuffer", 832 | onload: function(resp) { 833 | if (resp.status == 200) { 834 | // OK 835 | // decode response 836 | let buf = resp.response; 837 | let message = Response.decode(new Uint8Array(buf)); 838 | let respJSON = Response.toObject(message); 839 | // check if response is valid 840 | if (!respJSON || !respJSON.success || !respJSON.success.allTitlesView) { 841 | // error 842 | return; 843 | } 844 | 845 | let titles = respJSON.success.allTitlesView.titles; 846 | let list = []; 847 | // insert results into list 848 | for (let i = 0; i < titles.length; i++) { 849 | let lang = mangaplus.lang_table[titles[i].language]; 850 | list.push({ 851 | title: titles[i].name + " (" + lang + ")", 852 | href: titles[i].titleId, 853 | }); 854 | } 855 | // filter results 856 | let results = list.filter(item => matchResult(item, title)); 857 | // callback 858 | putResults(id, results); 859 | } else { 860 | // error 861 | errorResults(id, "MANGA Plus: " + resp.status); 862 | } 863 | } 864 | }); 865 | } 866 | 867 | /* MAL list */ 868 | /*******************************************************************************************************************************************************************/ 869 | const mal = {}; 870 | mal.timerRate = 15000; 871 | mal.recheckInterval = 4; // as a multiple of timerRate 872 | mal.loadRows = 25; 873 | mal.epStrLen = 14; 874 | mal.genericErrorRequest = "Error while performing request"; 875 | mal.userId = null; 876 | mal.CSRFToken = null; 877 | 878 | let onScrollQueue = []; 879 | let requestsQueues = {}; 880 | let timerEventCounter = 0; 881 | 882 | pageLoad["list"] = function() { 883 | // own list 884 | if ($(".header-menu.other").length !== 0) return; 885 | if ($(properties.watching).length !== 1) return; 886 | 887 | // add col header to table 888 | let colHeader = $(`${properties.colHeaderText}`); 889 | $("#list-container").find("th.header-title.title").after(colHeader); 890 | colHeader.css("min-width", "120px"); 891 | 892 | // column header listener 893 | colHeader.on("click", function() { 894 | $(".data.stream").each(function() { 895 | // update dataStream without skipping queue 896 | updateList($(this), true, false); 897 | }); 898 | }); 899 | 900 | // set id and token for more-info requests 901 | mal.userId = $(document.body).attr("data-owner-id"); 902 | mal.CSRFToken = $("meta[name=csrf_token]").attr("content"); 903 | 904 | // load first n rows, start from 1 to remove header 905 | loadRows(1, mal.loadRows + 1); 906 | 907 | // update timer 908 | setInterval(function() { 909 | $(".data.stream").trigger("update-time", [timerEventCounter++]); 910 | }, mal.timerRate); 911 | 912 | // check when an element comes into view 913 | $(window).scroll(function() { 914 | // get viewport 915 | let top = $(window).scrollTop(); 916 | let bottom = top + $(window).height(); 917 | // iterate scroll event queue 918 | let i = onScrollQueue.length; 919 | while (i--) { 920 | if (top < onScrollQueue[i].offset().top && bottom > onScrollQueue[i].offset().top) { 921 | onScrollQueue[i].trigger("intoView"); 922 | // remove element 923 | onScrollQueue.splice(i, 1); 924 | } 925 | } 926 | }); 927 | } 928 | 929 | // loads more-info and saves comment in dataStream 930 | function loadRows(start, end) { 931 | // get rows 932 | let rows = $("#list-container > div.list-block > div > table > tbody").slice(start, end); 933 | if (rows.length == 0) { 934 | return; 935 | } 936 | 937 | // iterate over rows to add dataStream and request more-info 938 | // stop if a row is not valid (usually happens only during first iteration) 939 | for (let i = 0; i < rows.length; i++) { 940 | let row = $(rows[i]); 941 | // if href does not exist then row is invalid 942 | let href = row.find(".list-table-data > .data.title > .link").attr("href"); 943 | if (!href) { 944 | // row was invalid, schedule retry and quit 945 | setTimeout(function() { 946 | loadRows(start, end); 947 | }, 100); 948 | return; 949 | } 950 | // add dataStream to row 951 | let dataStream = $(""); 952 | row.find(".list-table-data > .data.title").after(dataStream); 953 | // get id to make request 954 | let id = href.split("/")[2]; 955 | // finally request more info 956 | requestMoreInfo(id, dataStream); 957 | } 958 | 959 | let dataStreams = $(".data.stream"); 960 | 961 | // table cell listener 962 | dataStreams.on("click", function(e) { 963 | // if ctrl is pressed also reload more-info 964 | if (e.ctrlKey) { 965 | requestMoreInfo(null, $(this)); 966 | } else if (e.target.tagName != "A") { 967 | // avoid reloading if clicked on an anchor element 968 | updateList($(this), true, true); 969 | } 970 | }); 971 | 972 | // complete one episode listener 973 | rows.find(properties.iconAdd).on("click", function() { 974 | let dataStream = $(this).parents(".list-item").find(".data.stream"); 975 | // this timeout is needed, otherwise updateList could be called before the current episode number is updated 976 | setTimeout(function() { 977 | updateList(dataStream, false, false); 978 | }, 0); 979 | }); 980 | 981 | // timer event 982 | dataStreams.on("update-time", function(_, count) { 983 | let dataStream = $(this); 984 | if (dataStream.find(".nextep, .loading, .error").length > 0) { 985 | // do nothing if timer is not needed 986 | return; 987 | } 988 | // get time object from dataStream 989 | let t = dataStream.data("timeMillis"); 990 | // get next episode number 991 | let nextEp = parseInt(dataStream.parents(".list-item").find(properties.findProgress).find(".link").text()) + 1; 992 | if (isNaN(nextEp)) nextEp = 1; 993 | let timeMillis; 994 | // if there is an high priority time use that, otherwise check if episode matches and use low priority time 995 | if (t && t.highPriority) { 996 | timeMillis = t.highPriority - Date.now(); 997 | } else if (t && (t.ep ? t.ep == nextEp : true)) { 998 | timeMillis = t.lowPriority - Date.now(); 999 | } else { 1000 | timeMillis = false; 1001 | } 1002 | 1003 | let time; 1004 | if (!timeMillis || isNaN(timeMillis) || timeMillis < 1000) { 1005 | time = properties.notAired; 1006 | if (count !== false && count % mal.recheckInterval === 0) { 1007 | updateList(dataStream, true, false); 1008 | // we don't need to add the timer again, since updateList will add it if needed, so we can safely return 1009 | return; 1010 | } 1011 | } else { 1012 | const d = Math.floor(timeMillis / (1000 * 60 * 60 * 24)); 1013 | const h = Math.floor((timeMillis % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); 1014 | const m = Math.floor((timeMillis % (1000 * 60 * 60)) / (1000 * 60)); 1015 | time = (h < 10 ? "0"+h : h) + "h:" + (m < 10 ? "0" + m : m) + "m"; 1016 | if (d > 0) { 1017 | time = d + (d == 1 ? " day " : " days ") + time; 1018 | } 1019 | } 1020 | if (dataStream.find(".timer").length === 0) { 1021 | // if timer doesn't exist create it 1022 | dataStream.prepend("
" + time + "
"); 1023 | } else { 1024 | // update timer 1025 | dataStream.find(".timer").html(time); 1026 | } 1027 | }); 1028 | 1029 | // add last element to scroll event queue 1030 | let last = rows.last(); 1031 | last.on("intoView", function() { 1032 | loadRows(end, end + mal.loadRows); 1033 | }); 1034 | onScrollQueue.push(last); 1035 | } 1036 | 1037 | // request more-info and set data("comment") 1038 | function requestMoreInfo(id, dataStream) { 1039 | // if id is not set, get it from dataStream 1040 | if (!id) { 1041 | id = dataStream.parents(".list-item").find(".data.title > .link").attr("href").split("/")[2]; 1042 | } 1043 | // execute request 1044 | GM_xmlhttpRequest({ 1045 | method: "POST", 1046 | url: "https://myanimelist.net/includes/ajax-no-auth.inc.php?t=6", 1047 | headers: { 1048 | "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", 1049 | "Cookie": document.cookie, 1050 | }, 1051 | data: jQuery.param({ 1052 | memId: mal.userId, 1053 | csrf_token: mal.CSRFToken, 1054 | type: properties.mode, 1055 | id: id, 1056 | }), 1057 | onload: function(resp) { 1058 | let comment = null; 1059 | if (resp.status == 200) { 1060 | // OK 1061 | try { 1062 | let respJSON = JSON.parse(resp.response); 1063 | let m = respJSON.html.match(properties.commentsRegex); 1064 | comment = m[1]; 1065 | } catch (e) { 1066 | // do nothing on error 1067 | } 1068 | } 1069 | // set data 1070 | dataStream.data("comment", comment); 1071 | // remove old divs 1072 | dataStream.find(".error").remove(); 1073 | dataStream.find(".eplist").remove(); 1074 | dataStream.find(".nextep").remove(); 1075 | dataStream.find(".loading").remove(); 1076 | dataStream.find(".timer").remove(); 1077 | dataStream.find(".favicon").remove(); 1078 | // check if comment exists and is correct 1079 | if (comment) { 1080 | // comment exists 1081 | // url is an array that contains the streaming service and url relative to that service 1082 | let url = getUrlFromComment(comment); 1083 | if (url) { 1084 | // add eplist 1085 | let eplistUrl = getEplistUrl[url[0]](url[1]); 1086 | dataStream.append("" + properties.ep + " list"); 1087 | // add favicon 1088 | let domain = getDomainById(url[0]); 1089 | if (domain) { 1090 | let src = "https://www.google.com/s2/favicons?domain=" + domain; 1091 | dataStream.append(""); 1092 | } 1093 | // load links 1094 | updateList(dataStream, true, true); 1095 | } else { 1096 | // comment invalid 1097 | dataStream.append("
Invalid Link
"); 1098 | } 1099 | } else { 1100 | // comment doesn't extst 1101 | dataStream.append("
No Link
"); 1102 | } 1103 | } 1104 | }); 1105 | } 1106 | 1107 | // updates dataStream cell 1108 | function updateList(dataStream, forceReload, skipQueue) { 1109 | // remove old divs 1110 | dataStream.find(".error").remove(); 1111 | dataStream.find(".nextep").remove(); 1112 | dataStream.find(".loading").remove(); 1113 | dataStream.find(".timer").remove(); 1114 | // get episode list from data 1115 | let episodeList = dataStream.data("episodeList"); 1116 | if (Array.isArray(episodeList) && !forceReload) { 1117 | // episode list exists 1118 | updateList_exists(dataStream); 1119 | } else { 1120 | // episode list doesn't exist or needs to be reloaded 1121 | updateList_doesntExist(dataStream, skipQueue); 1122 | } 1123 | } 1124 | 1125 | function updateList_exists(dataStream) { 1126 | // listitem 1127 | let listitem = dataStream.parents(".list-item"); 1128 | // get current episode number 1129 | let currEp = parseInt(listitem.find(properties.findProgress).find(".link").text()); 1130 | if (isNaN(currEp)) currEp = 0; 1131 | // add offset to currEp 1132 | currEp += parseInt(dataStream.data("offset")); 1133 | // get episodes from data 1134 | let episodes = dataStream.data("episodeList"); 1135 | // create new nextep 1136 | let nextep = $("
"); 1137 | 1138 | if (episodes.length > currEp) { 1139 | // there are episodes available 1140 | let isAiring = listitem.find(properties.findAiring).length !== 0; 1141 | 1142 | if (episodes[currEp]) { 1143 | // episode is present 1144 | let a = $(""); 1145 | let t = episodes[currEp].text; 1146 | a.text(t.length > mal.epStrLen ? t.substr(0, mal.epStrLen - 1) + "…" : t); 1147 | if (t.length > mal.epStrLen) a.attr("title", t); 1148 | a.attr("href", episodes[currEp].href); 1149 | a.attr("class", isAiring ? "airing" : "non-airing"); 1150 | a.css("color", isAiring ? "#2db039" : "#ff730a"); 1151 | nextep.append(a); 1152 | } else { 1153 | // episode is missing 1154 | let s = $(`Missing #${currEp + 1}`); 1155 | s.css("color", "red"); 1156 | nextep.append(s); 1157 | } 1158 | 1159 | if (episodes.length - currEp > 1) { 1160 | // if there is more than 1 new ep then put the amount in parenthesis 1161 | let batchSize = episodes.length - currEp; 1162 | let a = $(""); 1163 | a.text(batchSize); 1164 | a.attr("title", properties.bulkTooltip.replace("%d", batchSize)); 1165 | a.css("cursor", "pointer"); 1166 | a.on("click", () => { 1167 | // open all episodes in non-focused tabs 1168 | for (let ep of episodes.slice(currEp)) { 1169 | if (new URL(ep.href).protocol == "magnet:") { 1170 | window.open(ep.href, ep.text).blur(); 1171 | } else { 1172 | GM_openInTab(ep.href, true); 1173 | } 1174 | } 1175 | window.focus(); 1176 | }); 1177 | nextep.append(" ("); 1178 | nextep.append(a); 1179 | nextep.append(")"); 1180 | } 1181 | // add new nextep 1182 | dataStream.prepend(nextep); 1183 | } else if (currEp > episodes.length && episodes.length > 0) { 1184 | // user has watched too many episodes 1185 | nextep.append($("
" + properties.latest + episodes.length + "
").css("color", "red")); 1186 | // add new nextep 1187 | dataStream.prepend(nextep); 1188 | } else { 1189 | // there aren't episodes available, trigger timer 1190 | dataStream.trigger("update-time", [false]); 1191 | } 1192 | } 1193 | 1194 | function queueGetEpisodes(dataStream, service, url) { 1195 | // get queue for specified service or create it 1196 | let queue = requestsQueues[service]; 1197 | if (!queue) { 1198 | queue = []; 1199 | queue.timers = 0; 1200 | queue.maxRequests = (queueSettings[service] || queueSettings["default"]).maxRequests; 1201 | queue.timeout = (queueSettings[service] || queueSettings["default"]).timeout; 1202 | requestsQueues[service] = queue; 1203 | } 1204 | 1205 | if (queue.timers < queue.maxRequests) { 1206 | // if there are no active timers, set timer and do request 1207 | queue.timers++ 1208 | getEpisodes[service](dataStream, url); 1209 | setTimeout(function() { 1210 | dequeueGetEpisodes(service); 1211 | }, queue.timeout); 1212 | } else { 1213 | // queue full, append to end 1214 | queue.push({ 1215 | dataStream: dataStream, 1216 | url: url, 1217 | }); 1218 | } 1219 | } 1220 | 1221 | function dequeueGetEpisodes(service) { 1222 | let queue = requestsQueues[service]; 1223 | 1224 | if (queue.length > 0) { 1225 | // if there are elements in queue, request the first and restart the timer 1226 | let req = queue.shift(); 1227 | getEpisodes[service](req.dataStream, req.url); 1228 | setTimeout(function() { 1229 | dequeueGetEpisodes(service); 1230 | }, queue.timeout); 1231 | } else { 1232 | // queue empty, terminate timer 1233 | queue.timers--; 1234 | } 1235 | } 1236 | 1237 | function updateList_doesntExist(dataStream, skipQueue) { 1238 | // check if comment exists and is correct 1239 | let comment = dataStream.data("comment"); 1240 | if (comment) { 1241 | // comment exists 1242 | // url is an array that contains the streaming service and url relative to that service 1243 | let url = getUrlFromComment(comment); 1244 | if (url) { 1245 | // comment valid 1246 | // add loading 1247 | dataStream.prepend("
Loading...
"); 1248 | // set offset data 1249 | dataStream.data("offset", url[2] ? url[2] : 0); 1250 | // queue getEpisode if needed 1251 | if (!skipQueue) { 1252 | queueGetEpisodes(dataStream, url[0], url[1]); 1253 | } else { 1254 | getEpisodes[url[0]](dataStream, url[1]); 1255 | } 1256 | } else { 1257 | // comment invalid 1258 | dataStream.append("
Invalid Link
"); 1259 | } 1260 | } else { 1261 | // comment doesn't extst 1262 | dataStream.append("
No Link
"); 1263 | } 1264 | } 1265 | 1266 | // save episodeList in dataStream 1267 | // timeMillis can be the unix timestamp of the next airing episode 1268 | function putEpisodes(dataStream, episodes, timeMillis) { 1269 | // add episodes to dataStream 1270 | dataStream.data("episodeList", episodes); 1271 | // add timeMillis to dataStream 1272 | if (timeMillis) { 1273 | // timeMillis is valid 1274 | dataStream.data("timeMillis", { highPriority: timeMillis }); 1275 | } else if (properties.mode == "anime") { 1276 | // timeMillis doesn't exist, get time from anilist 1277 | anilist_setTimeMillis(dataStream, true); 1278 | } 1279 | updateList(dataStream, false, false); 1280 | } 1281 | 1282 | // save unix timestamp of next airing episode in dataStream 1283 | function putTimeMillis(dataStream, timeMillis, highPriority, ep) { 1284 | let t = dataStream.data("timeMillis") || {}; 1285 | if (highPriority) { 1286 | t.highPriority = timeMillis; 1287 | } else { 1288 | t.lowPriority = timeMillis; 1289 | } 1290 | if (ep) t.ep = ep; 1291 | dataStream.data("timeMillis", t); 1292 | dataStream.trigger("update-time"); 1293 | updateList(dataStream, false, false); 1294 | } 1295 | 1296 | // set error to dataStream 1297 | function errorEpisodes(dataStream, error) { 1298 | // remove old divs 1299 | dataStream.find(".error").remove(); 1300 | dataStream.find(".nextep").remove(); 1301 | dataStream.find(".loading").remove(); 1302 | dataStream.find(".timer").remove(); 1303 | // create error div 1304 | dataStream.prepend($(`
${error || mal.genericErrorRequest}
`).css("color", "red")); 1305 | } 1306 | 1307 | /* MAL edit */ 1308 | /*******************************************************************************************************************************************************************/ 1309 | pageLoad["edit"] = function() { 1310 | // get title 1311 | let title = $("#main-form > table:nth-child(1) > tbody > tr:nth-child(1) > td:nth-child(2) > strong > a")[0].text; 1312 | // add titleBox with default title 1313 | title = title.replace(/'/g, "'"); 1314 | title = title.trim(); 1315 | let titleBox = $(""); 1316 | // add #search div 1317 | let search = $(""); 1318 | $(properties.editPageBox).after("
", titleBox, "
", search); 1319 | // add streamingServices 1320 | let first = true; 1321 | streamingServices.forEach(function(ss) { 1322 | if (ss.type != properties.mode) return; 1323 | // don't append ", " before first ss 1324 | if (first) { 1325 | first = false; 1326 | } else { 1327 | search.append(", "); 1328 | } 1329 | // new anchor 1330 | let a = $(""); 1331 | a.text(ss.name); 1332 | a.attr("href", "#"); 1333 | // on anchor click 1334 | a.on("click", function() { 1335 | // remove old results 1336 | search.find(".site").remove(); 1337 | // add new result box 1338 | search.append("
Searching...
"); 1339 | // execute search 1340 | searchSite[ss.id](ss.id, titleBox.val()); 1341 | // return 1342 | return false; 1343 | }); 1344 | search.append(a); 1345 | }); 1346 | search.append("
"); 1347 | 1348 | // offset textarea 1349 | let offsetBox = $(""); 1350 | let o = $(properties.editPageBox).val().split(" ")[2]; 1351 | if (o) offsetBox.val(o); 1352 | // Set Offset button 1353 | let a = $("Set Offset"); 1354 | a.attr("href", "#"); 1355 | a.on("click", function() { 1356 | // get offset from offsetBox 1357 | let o = parseInt(offsetBox.val()); 1358 | // replace or append to commentBox 1359 | let val = $(properties.editPageBox).val().split(" "); 1360 | if (!o || o == 0) { 1361 | val[2] = undefined; 1362 | } else { 1363 | val[2] = o; 1364 | } 1365 | $(properties.editPageBox).val(val.join(" ")); 1366 | return false; 1367 | }); 1368 | // offset div 1369 | let offset = $("
"); 1370 | offset.append(a, offsetBox); 1371 | search.after(offset); 1372 | } 1373 | 1374 | function putResults(id, results) { 1375 | let siteDiv = $("#search").find("." + id); 1376 | // if div with current id cant be found then don't add results 1377 | if (siteDiv.length !== 0) { 1378 | siteDiv.find("#searching").remove(); 1379 | 1380 | if (results.length === 0) { 1381 | siteDiv.append("No Results. Try changing the title in the search box above."); 1382 | return; 1383 | } 1384 | // add results 1385 | for (let i = 0; i < results.length; i++) { 1386 | let r = results[i]; 1387 | let a = $("Select"); 1388 | a.on("click", function() { 1389 | $(properties.editPageBox).val(id + " " + r.href); 1390 | return false; 1391 | }); 1392 | siteDiv.append("(").append(a).append(") ").append("" + r.title + ""); 1393 | if (r.episodes) { 1394 | siteDiv.append(" (" + r.episodes + ")"); 1395 | } 1396 | siteDiv.append("
"); 1397 | } 1398 | } 1399 | } 1400 | 1401 | function errorResults(id, error) { 1402 | let siteDiv = $("#search").find("." + id); 1403 | // if div with current id cant be found then don't add error 1404 | if (siteDiv.length !== 0) { 1405 | siteDiv.find("#searching").remove(); 1406 | siteDiv.append(error || mal.genericErrorRequest); 1407 | } 1408 | } 1409 | 1410 | /* main */ 1411 | /*******************************************************************************************************************************************************************/ 1412 | // associates an url with properties and pageLoad function 1413 | let pages = [ 1414 | { url: "https://myanimelist.net/animelist/", prop: "anime", load: "list" }, 1415 | { url: "https://myanimelist.net/mangalist/", prop: "manga", load: "list" }, 1416 | { url: "https://myanimelist.net/ownlist/anime/", prop: "anime", load: "edit" }, 1417 | { url: "https://myanimelist.net/ownlist/manga/", prop: "manga", load: "edit" }, 1418 | ]; 1419 | 1420 | (function($) { 1421 | // check on which page we are 1422 | for (let i = 0; i < pages.length; i++) { 1423 | if (window.location.href.indexOf(pages[i].url) != -1) { 1424 | properties = properties[pages[i].prop]; 1425 | pageLoad[pages[i].load](); 1426 | return; 1427 | } 1428 | } 1429 | 1430 | // check if we are on a load cookies page 1431 | for (let i = 0; i < cookieServices.length; i++) { 1432 | if (window.location.href.indexOf(cookieServices[i].url) != -1) { 1433 | pageLoad["loadCookies"](cookieServices[i]); 1434 | return; 1435 | } 1436 | } 1437 | })(jQuery); 1438 | --------------------------------------------------------------------------------