MALstreaming

Adds various anime and manga links to MAL

当前为 2018-08-09 提交的版本,查看 最新版本

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