MALstreaming

Adds various anime and manga links to MAL

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

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