MALstreaming

Adds various anime and manga links to MAL

目前为 2018-10-06 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name MALstreaming
  3. // @namespace https://github.com/mattiadr/MALstreaming
  4. // @version 5.22
  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. // @supportURL https://github.com/mattiadr/MALstreaming/issues
  10. // @match https://myanimelist.net/animelist/*
  11. // @match https://myanimelist.net/ownlist/anime/*/edit*
  12. // @match https://myanimelist.net/ownlist/anime/add?selected_series_id=*
  13. // @match https://myanimelist.net/mangalist/*
  14. // @match https://myanimelist.net/ownlist/manga/*/edit*
  15. // @match https://myanimelist.net/ownlist/manga/add?selected_manga_id=*
  16. // @match http://anichart.net/airing
  17. // @match http://kissanime.ru/
  18. // @match http://kissmanga.com/
  19. // @require https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js
  20. // @grant GM_xmlhttpRequest
  21. // @grant GM_openInTab
  22. // @grant GM_setValue
  23. // @grant GM_getValue
  24. // @grant GM_addValueChangeListener
  25. // @grant window.close
  26. // ==/UserScript==
  27.  
  28. /*
  29. HOW TO ADD A NEW STREAMING SERVICE:
  30. - add a new object to the streamingServices array with attributes id (unique id, must be a valid identifier) and name (display name)
  31. - create a new function in getEplistUrl that will simply return the full url from the partial url (saved in comments)
  32. - create a new function in getEpisodes that will accept dataStream and url,
  33. the function needs to callback to putEpisodes(dataStream, episodes, timeMillis)
  34. url is the url of the episode list provoded by getEplistUrl
  35. episodes needs to be an array of object with text and href attributes
  36. timeMillis can optionally be the unix timestamp of the next episode
  37. - create a new function in search that will accept id and title
  38. the function needs to callback to putResults(id, results)
  39. results needs to be an array of object with title (display title), href (the url that will be put in the comments) attributes
  40. and epsiodes (optional number of episodes)
  41. - if other utility is needed, add it in the service section and if you need to run a script on specific pages add another object to the pages array
  42. */
  43.  
  44. /* generic */
  45. /*******************************************************************************************************************************************************************/
  46. // contains variable properties for anime/manga modes
  47. let properties = {};
  48. properties.anime = {
  49. mode: "anime",
  50. watching: ".list-unit.watching",
  51. colHeader: "<th class='header-title stream'>Watch</th>",
  52. commentsRegex: /Comments: ([\S ]+)(?=&nbsp;)/g,
  53. iconAdd: ".icon-add-episode",
  54. findProgress: ".data.progress",
  55. findAiring: "span.content-status:contains('Airing')",
  56. latest: "Latest ep is #",
  57. notAired: "Not Yet Aired",
  58. ep: "Ep.",
  59. editPageBox: "#add_anime_comments",
  60. };
  61. properties.manga = {
  62. mode: "manga",
  63. watching: ".list-unit.reading",
  64. colHeader: "<th class='header-title stream'>Read</th>",
  65. commentsRegex: /Comments: ([\S ]+)(?=\n)/g,
  66. iconAdd: ".icon-add-chapter",
  67. findProgress: ".data.chapter",
  68. findAiring: "span.content-status:contains('Publishing')",
  69. latest: "Latest ch is #",
  70. notAired: "Not Yet Published",
  71. ep: "Ch.",
  72. editPageBox: "#add_manga_comments",
  73. };
  74. // contains all functions to execute on page load
  75. const pageLoad = {};
  76. // contains all functions to get the episodes list from the streaming services
  77. // must callback to putEpisodes(dataStream, episodes, timeMillis)
  78. const getEpisodes = {};
  79. // contains all functions to get the episode list url from the partial url
  80. const getEplistUrl = {};
  81. // contains all functions to execute the search on the streaming services
  82. // must callback to putResults(results)
  83. const searchSite = {};
  84. // is an array of valid streaming services names
  85. const streamingServices = [
  86. // anime
  87. { id: "kissanime", type: "anime", name: "Kissanime", domain: "kissanime.ru" },
  88. { id: "nineanime", type: "anime", name: "9anime", domain: "www8.9anime.is" },
  89. { id: "masterani", type: "anime", name: "Masterani.me", domain: "www.masterani.me" },
  90. // manga
  91. { id: "kissmanga", type: "manga", name: "Kissmanga", domain: "kissmanga.com" },
  92. { id: "mangadex", type: "manga", name: "MangaDex", domain: "mangadex.org" },
  93. ];
  94.  
  95. // return an array that contains the streaming service and url relative to that service or false if comment is not valid
  96. function getUrlFromComment(comment) {
  97. let c = comment.split(" ");
  98. if (c.length < 2) return false;
  99. for (let i = 0; i < streamingServices.length; i++) {
  100. if (streamingServices[i].id == c[0]) return c;
  101. }
  102. return false;
  103. }
  104.  
  105. // estimate time before next chapter as min of last n chapters
  106. function estimateTimeMillis(episodes, n) {
  107. let prev = null;
  108. let min = undefined;
  109. for (let i = episodes.length - 1; i > Math.max(0, episodes.length - 1 - n); i--) {
  110. if (!episodes[i]) continue;
  111. if (prev && episodes[i].timestamp != prev) {
  112. let diff = prev - episodes[i].timestamp;
  113. if (!min || diff < min && diff > 0) min = diff;
  114. }
  115. prev = episodes[i].timestamp;
  116. }
  117. return episodes[episodes.length - 1].timestamp + min;
  118. }
  119.  
  120. // returns the domain for the streaming service or false if ss doesn't exist
  121. function getDomainById(id) {
  122. for (let i = 0; i < streamingServices.length; i++) {
  123. if (streamingServices[i].id == id) {
  124. return streamingServices[i].domain;
  125. }
  126. }
  127. return false;
  128. }
  129.  
  130. /* anichart */
  131. /*******************************************************************************************************************************************************************/
  132. const anichartUrl = "http://anichart.net/airing";
  133.  
  134. // puts timeMillis into dataStream, then calls back
  135. function anichart_setTimeMillis(dataStream, canReload) {
  136. let listitem = dataStream.parents(".list-item");
  137.  
  138. // anime is not airing, exit
  139. if (listitem.find(properties.findAiring).length == 0) return;
  140.  
  141. let times = GM_getValue("anichartTimes", false);
  142. // get anime id
  143. let id = listitem.find(".data.title > .link").attr("href").split("/")[2];
  144. let t = times ? times[id] : false;
  145.  
  146. if (times && t && Date.now() < t.timeMillis) {
  147. // time doesn't need to update
  148. // set timeMillis, this is used to check if anichart timer is referring to next episode
  149. dataStream.data("timeMillis", t);
  150. } else {
  151. // add value change listener
  152. let listenerId = GM_addValueChangeListener("anichartTimes", function(name, old_value, new_value, remote) {
  153. // reload, avoid infinite loops
  154. if (canReload) anichart_setTimeMillis(dataStream, false);
  155. // remove listener
  156. GM_removeValueChangeListener(listenerId);
  157. });
  158. // load times from anichart
  159. if (GM_getValue("anichartLoading", false) + 30*1000 < Date.now()) {
  160. // set value then open anichart
  161. GM_setValue("anichartLoading", Date.now());
  162. GM_openInTab(anichartUrl, true);
  163. }
  164. }
  165. }
  166.  
  167. // function to execute when script is run on anichart
  168. pageLoad["anichart"] = function() {
  169. // wait all items
  170. setTimeout(function() {
  171. // get items or cards
  172. let items = $(".item, .card");
  173. let times = {};
  174. if ($(items[0]).find(".title > a").attr("href").indexOf("myanimelist.net") != -1) {
  175. // check if using MAL urls
  176. items.each(function(i, e) {
  177. // get id from url
  178. let id = $(this).find(".title > a").attr("href").match(/\d+$/)[0];
  179. let ep = $(this).find(".airing > span:first-child").text().match(/\d+/)[0];
  180. // get time array days, hours, mins
  181. let time = $(this).find("timer").text().match(/\d+/g);
  182. let timeMillis = ((parseInt(time[0]) * 24 + parseInt(time[1])) * 60 + parseInt(time[2])) * 60 * 1000;
  183. // edge case 0d 0h 0m
  184. if (timeMillis == 0) {
  185. timeMillis = undefined;
  186. } else {
  187. timeMillis += Date.now();
  188. }
  189. // set time, ep is episode the timer is referring to
  190. times[id] = {
  191. ep: ep,
  192. timeMillis: timeMillis
  193. }
  194. });
  195. }
  196. // put times in GM value
  197. GM_setValue("anichartTimes", times);
  198. // finished loading, close only if opened by script
  199. if (GM_getValue("anichartLoading", false)) {
  200. GM_setValue("anichartLoading", false);
  201. window.close();
  202. }
  203. }, 500);
  204. }
  205.  
  206. /* kissanime */
  207. /*******************************************************************************************************************************************************************/
  208. const kissanime = {};
  209. kissanime.base = "http://kissanime.ru/";
  210. kissanime.anime = kissanime.base + "Anime/";
  211. kissanime.search = kissanime.base + "Search/SearchSuggestx";
  212. kissanime.server = "&s=rapidvideo";
  213. // blacklisted urls
  214. kissanime.epsBlacklist = [
  215. "/Anime/Macross/Bunny_Hat-Macross_Special_-4208D135?id=73054",
  216. "/Anime/Macross/Bunny_Hat_Raw-30th_Anniversary_Special_-0A1CD40E?id=73055",
  217. "/Anime/Macross/Episode-011-original?id=35423"
  218. ];
  219. // regexes
  220. kissanime.regexWhitelist = /episode|movie|special|OVA/i;
  221. kissanime.regexBlacklist = /\b_[a-z]+|recap|\.5/i;
  222. kissanime.regexCountdown = /\d+(?=\), function)/;
  223.  
  224. // loads kissanime cookies and then calls back
  225. function kissanime_loadCookies(callback) {
  226. if (GM_getValue("KAloadcookies", false) + 30*1000 < Date.now()) {
  227. GM_setValue("KAloadcookies", Date.now());
  228. GM_openInTab(kissanime.base, true);
  229. }
  230. if (callback) {
  231. setTimeout(function() {
  232. callback();
  233. }, 6000);
  234. }
  235. }
  236.  
  237. // function to execute when script is run on kissanime
  238. pageLoad["kissanime"] = function() {
  239. if (GM_getValue("KAloadcookies", false) && document.title != "Please wait 5 seconds...") {
  240. GM_setValue("KAloadcookies", false);
  241. window.close();
  242. }
  243. }
  244.  
  245. getEpisodes["kissanime"] = function(dataStream, url) {
  246. GM_xmlhttpRequest({
  247. method: "GET",
  248. url: kissanime.anime + url,
  249. onload: function(resp) {
  250. if (resp.status == 503) {
  251. // loading CF cookies
  252. kissanime_loadCookies(function() {
  253. getEpisodes["kissanime"](dataStream, url);
  254. });
  255. } else if (resp.status == 200) {
  256. // OK
  257. let jqPage = $(resp.response);
  258. let episodes = [];
  259. // get anchors for the episodes
  260. let as = jqPage.find(".listing").find("tr > td > a");
  261. // get series title to remove it from episode name
  262. let title = jqPage.find("#leftside > div:nth-child(1) > div.barContent > div:nth-child(2) > a").text();
  263. // filter and add to episodes array
  264. as.each(function() {
  265. // title must match regexWhitelist, must not match regexBlacklist and href must not be in epsBlacklist to be considered a valid episode
  266. if (kissanime.regexWhitelist.test(this.text) && !kissanime.regexBlacklist.test(this.text) && kissanime.epsBlacklist.indexOf(this.href) == -1) {
  267. // prepend new object to array
  268. episodes.unshift({
  269. text: this.text.split(title)[1].substring(1).replace(/ 0+(?=\d+)/, " "),
  270. href: kissanime.anime + this.href.split("/Anime/")[1] + kissanime.server
  271. });
  272. }
  273. });
  274. // get time until next episode
  275. let timeMillis = Date.now() + parseInt(kissanime.regexCountdown.exec(resp.responseText));
  276. // callback
  277. putEpisodes(dataStream, episodes, timeMillis);
  278. }
  279. }
  280. });
  281. }
  282.  
  283. getEplistUrl["kissanime"] = function(partialUrl) {
  284. return kissanime.anime + partialUrl;
  285. }
  286.  
  287. searchSite["kissanime"] = function(id, title) {
  288. GM_xmlhttpRequest({
  289. method: "POST",
  290. url: kissanime.search,
  291. data: "type=Anime" + "&keyword=" + title,
  292. headers: { "Content-Type": "application/x-www-form-urlencoded" },
  293. onload: function(resp) {
  294. if (resp.status == 503) {
  295. // loading CF cookies
  296. kissanime_loadCookies(function() {
  297. searchSite["kissanime"](id, title);
  298. });
  299. } else if (resp.status == 200) {
  300. // OK
  301. let results = [];
  302. let list = $(resp.responseText);
  303. list.each(function() {
  304. results.push({
  305. title: this.text,
  306. href: this.pathname.split("/")[2]
  307. });
  308. });
  309. // callback
  310. putResults(id, results);
  311. }
  312. }
  313. });
  314. }
  315.  
  316. /* 9anime */
  317. /*******************************************************************************************************************************************************************/
  318. const nineanime = {};
  319. nineanime.base = "https://9anime.is/";
  320. nineanime.anime = nineanime.base + "watch/";
  321. nineanime.servers = nineanime.base + "ajax/film/servers/";
  322. nineanime.search = nineanime.base + "search?keyword=";
  323. nineanime.regexBlacklist = /preview|special|trailer|CAM/i;
  324.  
  325. getEpisodes["nineanime"] = function(dataStream, url) {
  326. GM_xmlhttpRequest({
  327. method: "GET",
  328. url: nineanime.servers + url.match(/(?<=\.)\w+$/)[0],
  329. onload: function(resp) {
  330. if (resp.status == 200) {
  331. // OK
  332. // response is a json with only html attribute, parse and turn into jQuery object
  333. let jqPage = $(JSON.parse(resp.response).html);
  334. let episodes = [];
  335. // get servers
  336. let servers = jqPage.find("div.widget-body > .server");
  337. let as = null;
  338. // auto select server with the most videos
  339. servers.each(function() {
  340. let nas = $(this).find("li > a");
  341. if (!as || nas.length > as.length) {
  342. as = nas;
  343. }
  344. });
  345. if (as) {
  346. as.each(function() {
  347. // ignore blacklisted episodes
  348. if (!nineanime.regexBlacklist.test($(this).text())) {
  349. // push episode to array
  350. episodes.push({
  351. text: "Episode " + $(this).text().replace(/^0+(?=\d+)/, ""),
  352. href: nineanime.base + $(this).attr("href").substr(1),
  353. });
  354. }
  355. });
  356. }
  357. // get time if available
  358. GM_xmlhttpRequest({
  359. method: "GET",
  360. url: nineanime.anime + url,
  361. onload: function(resp) {
  362. if (resp.status == 200) {
  363. // OK
  364. let time = $(resp.response).find("#main > div > div.alert.alert-primary > i");
  365. let timeMillis = undefined;
  366. if (time.length !== 0) {
  367. // timer is present
  368. timeMillis = time.data("to") * 1000;
  369. }
  370. // callback
  371. putEpisodes(dataStream, episodes, timeMillis);
  372. } else {
  373. // not OK, callback
  374. putEpisodes(dataStream, episodes, undefined);
  375. }
  376. }
  377. });
  378. }
  379. }
  380. });
  381. }
  382.  
  383. getEplistUrl["nineanime"] = function(partialUrl) {
  384. return nineanime.anime + partialUrl;
  385. }
  386.  
  387. searchSite["nineanime"] = function(id, title) {
  388. GM_xmlhttpRequest({
  389. method: "GET",
  390. url: nineanime.search + encodeURI(title),
  391. onload: function(resp) {
  392. if (resp.status == 200) {
  393. // OK
  394. let jqPage = $(resp.response);
  395. let results = [];
  396. // get results from response
  397. let list = jqPage.find("#main > div > div:nth-child(1) > div.widget-body > div.film-list > .item");
  398. list = list.slice(0, 10);
  399. // add to results
  400. list.each(function() {
  401. // get anchor for text and href
  402. let a = $(this).find("a")[1];
  403. // get episode count
  404. let ep = $(this).find(".status > .ep").text().match(/(?<=\/)\d+/);
  405. results.push({
  406. title: a.text,
  407. href: a.href.split("/")[4],
  408. episodes: ep ? (ep[0] + " eps") : "1 ep"
  409. });
  410. });
  411. // callback
  412. putResults(id, results);
  413. }
  414. }
  415. });
  416. }
  417.  
  418. /* masterani */
  419. /*******************************************************************************************************************************************************************/
  420. const masterani = {};
  421. masterani.base = "https://www.masterani.me/";
  422. masterani.anime = masterani.base + "api/anime/";
  423. masterani.anime_suffix = "/detailed";
  424. masterani.anime_info = masterani.base + "anime/info/";
  425. masterani.anime_watch = masterani.base + "anime/watch/";
  426. masterani.search = masterani.base + "api/anime/filter?search=";
  427. masterani.search_suffix = "&order=relevance_desc&page=1";
  428.  
  429. getEpisodes["masterani"] = function(dataStream, url) {
  430. GM_xmlhttpRequest({
  431. method: "GET",
  432. url: masterani.anime + url + masterani.anime_suffix,
  433. onload: function(resp) {
  434. if (resp.status == 200) {
  435. // OK
  436. let res = JSON.parse(resp.response);
  437. let episodes = [];
  438. // get all episodes
  439. for (let i = 0; i < res.episodes.length; i++) {
  440. let ep = res.episodes[i].info.episode;
  441. // push episodes to array
  442. episodes.push({
  443. text: "Episode " + ep,
  444. href: masterani.anime_watch + url + "/" + ep,
  445. });
  446. }
  447. // callback
  448. putEpisodes(dataStream, episodes, undefined);
  449. }
  450. }
  451. });
  452. }
  453.  
  454. getEplistUrl["masterani"] = function(partialUrl) {
  455. return masterani.anime_info + partialUrl;
  456. }
  457.  
  458. searchSite["masterani"] = function(id, title) {
  459. GM_xmlhttpRequest({
  460. method: "GET",
  461. url: masterani.search + encodeURIComponent(title).slice(0, 60) + masterani.search_suffix, // maximum search length is 60 chars
  462. onload: function(resp) {
  463. if (resp.status == 200) {
  464. // OK
  465. let list = JSON.parse(resp.response).data;
  466. let results = [];
  467. if (list) {
  468. list = list.slice(0, 10);
  469. // add to results
  470. for (let i = 0; i < list.length; i++) {
  471. let r = list[i];
  472. results.push({
  473. title: r.title,
  474. href: r.slug,
  475. episodes: r.episode_count
  476. });
  477. }
  478. }
  479. // callback
  480. putResults(id, results);
  481. }
  482. }
  483. });
  484. }
  485.  
  486. /* kissmanga */
  487. /*******************************************************************************************************************************************************************/
  488. const kissmanga = {};
  489. kissmanga.base = "http://kissmanga.com/";
  490. kissmanga.manga = kissmanga.base + "Manga/";
  491. kissmanga.search = kissmanga.base + "Search/SearchSuggest";
  492. // regex
  493. kissmanga.regexVol = /(?<=vol).+?\d+/i;
  494.  
  495. // loads kissmanga cookies and then calls back
  496. function kissmanga_loadCookies(callback) {
  497. if (GM_getValue("KMloadcookies", false) + 30*1000 < Date.now()) {
  498. GM_setValue("KMloadcookies", Date.now());
  499. GM_openInTab(kissmanga.base, true);
  500. }
  501. if (callback) {
  502. setTimeout(function() {
  503. callback();
  504. }, 6000);
  505. }
  506. }
  507.  
  508. // function to execute when script is run on kissmanga
  509. pageLoad["kissmanga"] = function() {
  510. if (GM_getValue("KMloadcookies", false) && document.title != "Please wait 5 seconds...") {
  511. GM_setValue("KMloadcookies", false);
  512. window.close();
  513. }
  514. }
  515.  
  516. getEpisodes["kissmanga"] = function(dataStream, url) {
  517. GM_xmlhttpRequest({
  518. method: "GET",
  519. url: kissmanga.manga + url,
  520. onload: function(resp) {
  521. if (resp.status == 503) {
  522. // loading CF cookies
  523. kissmanga_loadCookies(function() {
  524. getEpisodes["kissmanga"](dataStream, url);
  525. });
  526. } else if (resp.status == 200) {
  527. // OK
  528. let jqPage = $(resp.response);
  529. let episodes = [];
  530. // get table rows for the episodes
  531. let trs = jqPage.find(".listing").find("tr");
  532. // get series title to remove it from chapter name
  533. let title = jqPage.find("#leftside > div:nth-child(1) > div.barContent > div:nth-child(2) > a").text();
  534. // filter and add to episodes array
  535. trs.each(function() {
  536. let a = $(this).find("td > a");
  537. if (a.length === 0) return;
  538. let t = a.text().split(title)[1].substring(1).replace(/ 0+(?=\d+)/, " ");
  539. // get all numbers in title
  540. let n = t.match(/\d+/g);
  541. // if vol is present then get second match else get first
  542. n = kissmanga.regexVol.test(t) ? n[1] : n[0];
  543. // chapter number - 1 is used as index
  544. n = parseInt(n) - 1;
  545. // add chapter to array
  546. episodes[n] = {
  547. text: t,
  548. href: kissmanga.manga + a.attr('href').split("/Manga/")[1],
  549. timestamp: Date.parse($(this).find("td:nth-child(2)").text()),
  550. }
  551. });
  552. // estimate timeMillis
  553. let timeMillis = estimateTimeMillis(episodes, 5);
  554. // callback
  555. putEpisodes(dataStream, episodes, timeMillis);
  556. }
  557. }
  558. });
  559. }
  560.  
  561. getEplistUrl["kissmanga"] = function(partialUrl) {
  562. return kissmanga.manga + partialUrl;
  563. }
  564.  
  565. searchSite["kissmanga"] = function(id, title) {
  566. GM_xmlhttpRequest({
  567. method: "POST",
  568. url: kissmanga.search,
  569. data: "type=Manga" + "&keyword=" + title,
  570. headers: { "Content-Type": "application/x-www-form-urlencoded" },
  571. onload: function(resp) {
  572. if (resp.status == 503) {
  573. // loading CF cookies
  574. kissmanga_loadCookies(function() {
  575. searchSite["kissmanga"](id, title);
  576. });
  577. } else if (resp.status == 200) {
  578. // OK
  579. let results = [];
  580. let list = $(resp.responseText);
  581. list.each(function() {
  582. results.push({
  583. title: this.text,
  584. href: this.pathname.split("/")[2]
  585. });
  586. });
  587. // callback
  588. putResults(id, results);
  589. }
  590. }
  591. });
  592. }
  593.  
  594. /* mangadex */
  595. /*******************************************************************************************************************************************************************/
  596. const mangadex = {};
  597. mangadex.base = "https://mangadex.org/";
  598. mangadex.manga = mangadex.base + "manga/";
  599. mangadex.manga_api = mangadex.base + "api/manga/";
  600. mangadex.chapter = mangadex.base + "chapter/";
  601. mangadex.lang_code = "gb";
  602. mangadex.search = mangadex.base + "quick_search/";
  603.  
  604. getEpisodes["mangadex"] = function(dataStream, url) {
  605. GM_xmlhttpRequest({
  606. method: "GET",
  607. url: mangadex.manga_api + url,
  608. onload: function(resp) {
  609. if (resp.status == 200) {
  610. // OK
  611. let res_ch = JSON.parse(resp.response).chapter;
  612. let episodes = [];
  613. // parse json
  614. for (let key in res_ch) {
  615. if (res_ch.hasOwnProperty(key)) {
  616. let ch = res_ch[key];
  617. // skip wrong language
  618. if (ch.lang_code != mangadex.lang_code) continue;
  619. // put into episodes array
  620. episodes[ch.chapter - 1] = {
  621. text: (ch.volume && `Vol. ${ch.volume}`) + `Ch. ${ch.chapter}`,
  622. href: mangadex.chapter + key,
  623. timestamp: ch.timestamp,
  624. }
  625. }
  626. }
  627. // estimate timeMillis
  628. let timeMillis = estimateTimeMillis(episodes, 5);
  629. // callback
  630. putEpisodes(dataStream, episodes, timeMillis);
  631. }
  632. }
  633. });
  634. }
  635.  
  636. getEplistUrl["mangadex"] = function(partialUrl) {
  637. return mangadex.manga + partialUrl;
  638. }
  639.  
  640. searchSite["mangadex"] = function(id, title) {
  641. GM_xmlhttpRequest({
  642. method: "GET",
  643. url: mangadex.search + encodeURI(title),
  644. onload: function(resp) {
  645. if (resp.status == 200) {
  646. // OK
  647. let results = [];
  648. // get title anchors
  649. let titles = $(resp.response).find("#search_manga").find("a.manga_title");
  650. titles.each(function() {
  651. results.push({
  652. title: this.title,
  653. href: this.pathname.split("/")[2]
  654. });
  655. });
  656. // callback
  657. putResults(id, results);
  658. }
  659. }
  660. });
  661. }
  662.  
  663. /* MAL list */
  664. /*******************************************************************************************************************************************************************/
  665. pageLoad["list"] = function() {
  666. // own list
  667. if ($(".header-menu.other").length !== 0) return;
  668. if ($(properties.watching).length !== 1) return;
  669.  
  670. // force hide more-info
  671. const styleSheet = document.createElement("style");
  672. styleSheet.innerHTML =`
  673. .list-table .more-info {
  674. display: none!important;
  675. }
  676. `;
  677. document.body.appendChild(styleSheet);
  678.  
  679. // expand more-info
  680. $(".more > a").each(function() {
  681. this.click();
  682. });
  683. // $(".more > a").click(); doesn't work for some reason
  684.  
  685. // add col to table
  686. $("#list-container").find("th.header-title.title").after(properties.colHeader);
  687. $(".list-item .data.title").after("<td class='data stream'></td>");
  688.  
  689. // style
  690. $(".data.stream").css("font-weight", "normal");
  691. $(".data.stream").css("line-height", "1.5em");
  692. $(".header-title.stream").css("min-width", "120px");
  693.  
  694. // wait
  695. setTimeout(function() {
  696. // collapse more-info
  697. $(".more-info").css("display", "none");
  698. // remove sheet
  699. document.body.removeChild(styleSheet);
  700.  
  701. // put comment into data("comment")
  702. $(".list-item").each(function() {
  703. let comment = $(this).find(".td1.borderRBL").html().match(properties.commentsRegex);
  704. if (comment) {
  705. // revome the first 10 characters to remove "Comments: " since js doesn't support lookbehinds
  706. comment = comment.toString().substring(10);
  707. } else {
  708. comment = null;
  709. }
  710.  
  711. $(this).find(".data.stream").data("comment", comment);
  712. });
  713.  
  714. // load links
  715. $(".header-title.stream").trigger("click");
  716. }, 1000);
  717.  
  718. // event listeners
  719. // column header
  720. $(".header-title.stream").on("click", function() {
  721. $(".data.stream").trigger("click");
  722. });
  723.  
  724. // table cell
  725. $(".data.stream").on("click", function() {
  726. updateList($(this), true, true);
  727. });
  728.  
  729. // complete one episode
  730. $(properties.iconAdd).on("click", function() {
  731. let dataStream = $(this).parents(".list-item").find(".data.stream");
  732. updateList(dataStream, false, true);
  733. });
  734.  
  735. // timer event
  736. $(".data.stream").on("update-time", function() {
  737. let dataStream = $(this);
  738. // get time object from dataStream
  739. let t = dataStream.data("timeMillis");
  740. // get next episode number
  741. let nextEp = parseInt(dataStream.parents(".list-item").find(properties.findProgress).find(".link").text()) + 1;
  742. let timeMillis;
  743. // if t.ep is set then it needs to be equal to nextEp, else we set timeMillis to false to display Not Yet Aired
  744. if (t && (t.ep ? t.ep == nextEp : true)) {
  745. timeMillis = t.timeMillis - Date.now();
  746. } else {
  747. timeMillis = false;
  748. }
  749.  
  750. let time;
  751. if (!timeMillis || isNaN(timeMillis) || timeMillis < 1000) {
  752. time = properties.notAired;
  753. } else {
  754. const d = Math.floor(timeMillis / (1000 * 60 * 60 * 24));
  755. const h = Math.floor((timeMillis % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
  756. const m = Math.floor((timeMillis % (1000 * 60 * 60)) / (1000 * 60));
  757. time = (h < 10 ? "0"+h : h) + "h:" + (m < 10 ? "0" + m : m) + "m";
  758. if (d > 0) {
  759. time = d + (d == 1 ? " day " : " days ") + time;
  760. }
  761. }
  762. if (dataStream.find(".nextep, .loading, .error").length > 0) {
  763. // do nothing if timer is not needed
  764. return;
  765. } else if (dataStream.find(".timer").length === 0) {
  766. // if timer doesn't exist create it
  767. dataStream.prepend("<div class='timer'>" + time + "<div>");
  768. } else {
  769. // update timer
  770. dataStream.find(".timer").html(time);
  771. }
  772. });
  773.  
  774. // update timer
  775. setInterval(function() {
  776. $(".data.stream").trigger("update-time");
  777. }, 1000);
  778. }
  779.  
  780. // updates dataStream cell
  781. function updateList(dataStream, forceReload, canReload) {
  782. // remove old divs
  783. dataStream.find(".error").remove();
  784. dataStream.find(".nextep").remove();
  785. dataStream.find(".loading").remove();
  786. dataStream.find(".timer").remove();
  787. // get episode list from data
  788. let episodeList = dataStream.data("episodeList");
  789. if (Array.isArray(episodeList) && !forceReload) {
  790. // episode list exists
  791. updateList_exists(dataStream);
  792. } else if (canReload) {
  793. // episode list doesn't exist or needs to be reloaded
  794. updateList_doesntExist(dataStream);
  795. } else {
  796. // broken link
  797. dataStream.prepend($("<div class='error'>Broken link<br></div>").css("color", "red"));
  798. }
  799. }
  800.  
  801. function updateList_exists(dataStream) {
  802. // listitem
  803. let listitem = dataStream.parents(".list-item");
  804. // get current episode number
  805. let currEp = parseInt(listitem.find(properties.findProgress).find(".link").text());
  806. if (isNaN(currEp)) currEp = 0;
  807. // get episodes from data
  808. let episodes = dataStream.data("episodeList");
  809. // create new nextep
  810. let nextep = $("<div class='nextep'></div>");
  811.  
  812. if (episodes.length > currEp) {
  813. // there are episodes available
  814. let isAiring = listitem.find(properties.findAiring).length !== 0;
  815. let t = episodes[currEp] ? episodes[currEp].text : ("Missing #" + (currEp + 1));
  816.  
  817. let a = $("<a></a>");
  818. a.text(t.length > 13 ? t.substr(0, 12) + "…" : t);
  819. if (t.length > 13) a.attr("title", t);
  820. a.attr("href", episodes[currEp] ? episodes[currEp].href : "#");
  821. a.attr("target", "_blank");
  822. a.attr("class", isAiring ? "airing" : "non-airing");
  823. a.css("color", isAiring ? "#2db039" : "#ff730a");
  824. nextep.append(a);
  825.  
  826. if (episodes.length - currEp > 1) {
  827. // if there is more than 1 new ep then put the amount in parenthesis
  828. nextep.append(" (" + (episodes.length - currEp) + ")");
  829. }
  830. // add new nextep
  831. dataStream.prepend(nextep);
  832. } else if (currEp > episodes.length) {
  833. // user has watched too many episodes
  834. nextep.append($("<div class='.ep-error'>" + properties.latest + episodes.length + "</div>").css("color", "red"));
  835. // add new nextep
  836. dataStream.prepend(nextep);
  837. } else {
  838. // there aren't episodes available, trigger timer
  839. dataStream.trigger("update-time");
  840. }
  841. }
  842.  
  843. function updateList_doesntExist(dataStream) {
  844. // check if comment exists and is correct
  845. let comment = dataStream.data("comment");
  846. if (comment) {
  847. // comment exists
  848. // url is and array that contains the streaming service and url relative to that service
  849. let url = getUrlFromComment(comment);
  850. if (url) {
  851. // comment valid
  852. // add loading
  853. dataStream.prepend("<div class='loading'>Loading...</div>");
  854. // add eplist and favicon to dataStream
  855. if (dataStream.find(".eplist").length === 0) {
  856. // add eplist
  857. let eplistUrl = getEplistUrl[url[0]](url[1]);
  858. dataStream.append("<a class='eplist' target='_blank' href='" + eplistUrl + "'>" + properties.ep + " list</a>");
  859. // add favicon
  860. let domain = getDomainById(url[0]);
  861. if (domain) {
  862. let src = "https://www.google.com/s2/favicons?domain=" + domain;
  863. dataStream.append("<img class='favicon' src='" + src + "' style='position: relative; top: 3px; padding-left: 4px'>");
  864. }
  865. }
  866. // executes getEpisodes relative to url[0] passing dataStream and url[1]
  867. getEpisodes[url[0]](dataStream, url[1]);
  868. } else {
  869. // comment invalid
  870. dataStream.append("<div class='error'>Invalid Link</div>")
  871. }
  872. } else {
  873. // comment doesn't extst
  874. dataStream.append("<div class='error'>No Link</div>");
  875. }
  876. }
  877.  
  878. // save episodeList and timeMillis inside .data.stream of listitem
  879. function putEpisodes(dataStream, episodes, timeMillis) {
  880. // add episodes to dataStream
  881. dataStream.data("episodeList", episodes);
  882. // add timeMillis to dataStream
  883. if (timeMillis) {
  884. // timeMillis is valid
  885. dataStream.data("timeMillis", { timeMillis: timeMillis });
  886. } else if (properties.mode == "anime") {
  887. // timeMillis doesn't exist, get time from anichart
  888. anichart_setTimeMillis(dataStream, true);
  889. }
  890. updateList(dataStream, false, false);
  891. }
  892.  
  893. /* MAL edit */
  894. /*******************************************************************************************************************************************************************/
  895. pageLoad["edit"] = function() {
  896. // get title
  897. const title = $("#main-form > table:nth-child(1) > tbody > tr:nth-child(1) > td:nth-child(2) > strong > a")[0].text;
  898. // add titleBox with default title
  899. let titleBox = $("<input type='text' value='" + title + "' size='36' style='font-size: 11px; padding: 3px;'>");
  900. // add #search div
  901. let search = $("<div id='search'><b style='font-size: 110%; line-height: 180%;'>Search: </b></div>");
  902. $(properties.editPageBox).after("<br>", titleBox, "<br>", search);
  903. // add streamingServices
  904. let first = true;
  905. streamingServices.forEach(function(ss) {
  906. if (ss.type != properties.mode) return;
  907. // don't append ", " before first ss
  908. if (first) {
  909. first = false;
  910. } else {
  911. search.append(", ");
  912. }
  913. // new anchor
  914. let a = $("<a></a>");
  915. a.text(ss.name);
  916. a.attr("href", "#");
  917. // on anchor click
  918. a.on("click", function() {
  919. // remove old results
  920. search.find(".site").remove();
  921. // add new result box
  922. search.append("<div class='site " + ss.id + "'><div id='searching'>Searching...</div></div>");
  923. // execute search
  924. searchSite[ss.id](ss.id, titleBox.val());
  925. // return
  926. return false;
  927. });
  928. search.append(a);
  929. });
  930. search.append("<br>");
  931. }
  932.  
  933. function putResults(id, results) {
  934. let siteDiv = $("#search").find("." + id);
  935. // if div with current id cant be found then don't add results
  936. if (siteDiv.length !== 0) {
  937. siteDiv.find("#searching").remove();
  938.  
  939. if (results.length === 0) {
  940. siteDiv.append("No Results. Try changing the title in the search box above.");
  941. return;
  942. }
  943. // add results
  944. for (let i = 0; i < results.length; i++) {
  945. let r = results[i];
  946. let a = $("<a href='#'>Select</a>");
  947. a.on("click", function() {
  948. $(properties.editPageBox).val(id + " " + r.href);
  949. return false;
  950. });
  951. siteDiv.append("(").append(a).append(") ").append("<a target='_blank' href='" + getEplistUrl[id](r.href) + "'>" + r.title + "</a>");
  952. if (r.episodes) {
  953. siteDiv.append(" (" + r.episodes + ")");
  954. }
  955. siteDiv.append("<br>");
  956. }
  957. }
  958. }
  959.  
  960. /* main */
  961. /*******************************************************************************************************************************************************************/
  962. // associates an url with properties and pageLoad function
  963. let pages = [
  964. { url: kissanime.base, prop: null, load: "kissanime" },
  965. { url: kissmanga.base, prop: null, load: "kissmanga" },
  966. { url: anichartUrl, prop: null, load: "anichart" },
  967. { url: "https://myanimelist.net/animelist/", prop: "anime", load: "list" },
  968. { url: "https://myanimelist.net/mangalist/", prop: "manga", load: "list" },
  969. { url: "https://myanimelist.net/ownlist/anime/", prop: "anime", load: "edit" },
  970. { url: "https://myanimelist.net/ownlist/manga/", prop: "manga", load: "edit" },
  971. ];
  972.  
  973. (function($) {
  974. for (let i = 0; i < pages.length; i++) {
  975. if (window.location.href.indexOf(pages[i].url) != -1) {
  976. properties = properties[pages[i].prop];
  977. pageLoad[pages[i].load]();
  978. break;
  979. }
  980. }
  981. })(jQuery);