AniHIDE - Hide Unrelated Episodes

Filter animes in the Home/New-Episodes pages to show only what you are watching or plan to watch based on your anime list on MAL or AL.

当前为 2024-01-03 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name AniHIDE - Hide Unrelated Episodes
  3. // @namespace https://gf.qytechs.cn/en/users/781076-jery-js
  4. // @version 1.3.9
  5. // @description Filter animes in the Home/New-Episodes pages to show only what you are watching or plan to watch based on your anime list on MAL or AL.
  6. // @icon https://image.myanimelist.net/ui/OK6W_koKDTOqqqLDbIoPAiC8a86sHufn_jOI-JGtoCQ
  7. // @author Jery
  8. // @license MIT
  9. // @match https://yugenanime.*/*
  10. // @match https://yugenanime.tv/*
  11. // @match https://anitaku.*/*
  12. // @match https://anitaku.to/*
  13. // @match https://gogoanime3.*/*
  14. // @match https://gogoanime3.net/*
  15. // @match https://animepahe.*/
  16. // @match https://animepahe.ru/
  17. // @match https://animesuge.to/*
  18. // @match https://animesuge.*/*
  19. // @match https://*animesuge.cc/*
  20. // @grant GM_registerMenuCommand
  21. // @grant GM_addStyle
  22. // @grant GM_getValue
  23. // @grant GM_setValue
  24. // @grant GM_notification
  25. // @require https://unpkg.com/axios/dist/axios.min.js
  26. // ==/UserScript==
  27.  
  28.  
  29. /**************************
  30. * Notify new Update
  31. ***************************/
  32. if (GM_getValue("version") != GM_info.script.version) {
  33. // refreshList();
  34. GM_setValue("version", GM_info.script.version);
  35. const msg = `
  36. ${GM_info.script.name}:\n
  37. This scipt has been updated!!\n
  38. What's new:
  39. -Fixed CORS Proxy issue with MAL [fix]
  40. `
  41. alert(msg);
  42. }
  43.  
  44. /* Preferred Format sample-
  45. What's new:
  46. -Added AnimePahe [website]
  47. -Added Timeout for certain sites [workaround]
  48. -Notification shown for list refresh [feature]
  49. -Bug Fixes + Code Cleanup`
  50. */
  51.  
  52. /**************************
  53. * CONSTANTS
  54. ***************************/
  55. const userSettingsKey = 'userSettings';
  56. const animeListKey = 'animeList';
  57. const manualListKey = 'manualList';
  58. const MALClientId = 'cfdd50f8037e9e8cf489992df497c761';
  59.  
  60.  
  61. /***************************************************************
  62. * ANIME SITES
  63. * -----------
  64. * the timeout variable is a workaround for sites like
  65. * AnimePahe which generate episodes page dynamically.
  66. ***************************************************************/
  67. const animeSites = [
  68. {
  69. name: 'yugenanime',
  70. url: ['yugenanime.tv'],
  71. item: '.ep-grid > li',
  72. title: '.ep-origin-name',
  73. thumbnail: '.ep-thumbnail > img',
  74. timeout: 0
  75. },
  76. {
  77. name: 'gogoanime',
  78. url: ['gogoanime3', 'gogoanimehd', 'gogoanime'],
  79. item: '.items > li',
  80. title: '.name > a',
  81. thumbnail: '.img > a > img',
  82. timeout: 0
  83. },
  84. {
  85. name: 'animepahe',
  86. url: ['animepahe.ru', 'animepahe.com', 'animepahe'],
  87. item: '.episode-wrap > .episode',
  88. title: '.episode-title > a',
  89. thumbnail: '.episode-snapshot > img',
  90. timeout: 500
  91. },
  92. {
  93. name: 'animesuge',
  94. url: ['animesuge.to'],
  95. item: '.item',
  96. title: '.name > a',
  97. thumbnail: '.poster img',
  98. timeout: 0
  99. },
  100. {
  101. name: 'animesuge',
  102. url: ['animesuge.cc'],
  103. item: '.itemlist > li',
  104. title: '.name a',
  105. thumbnail: '.poster > img',
  106. timeout: 0
  107. },
  108. {
  109. name: 'animesuge',
  110. url: ['animesuge.su'],
  111. item: '.bs',
  112. title: '.tt',
  113. thumbnail: 'img',
  114. timeout: 0
  115. }
  116. ];
  117.  
  118. const services = [
  119. {
  120. name: "MyAnimeList",
  121. icon: "https://image.myanimelist.net/ui/OK6W_koKDTOqqqLDbIoPAiC8a86sHufn_jOI-JGtoCQ",
  122. statuses: ['watching', 'plan_to_watch', 'on_hold', 'dropped', 'completed', ''],
  123. apiBaseUrl: 'https://api.myanimelist.net/v2/users',
  124. clientId: MALClientId,
  125. async getAnimeList(username, status) {
  126. const proxyUrl = 'https://test.cors.workers.dev/?'; //'https://corsproxy.io/?';
  127. const url = `${proxyUrl}${this.apiBaseUrl}/${username}/animelist?status=${status}&limit=1000`;
  128. const config = {
  129. headers: {
  130. 'X-MAL-CLIENT-ID': this.clientId
  131. }
  132. };
  133. const response = await axios.get(url, config);
  134. const data = response.data;
  135. return data.data.map(entry => new AnimeEntry(entry.node.title));
  136. }
  137. },
  138. {
  139. name: "AniList",
  140. icon: "https://anilist.co/img/icons/android-chrome-512x512.png",
  141. statuses: ['CURRENT', 'PLANNING', 'COMPLETED', 'DROPPED', 'PAUSED', ''],
  142. apiBaseUrl: 'https://graphql.anilist.co',
  143. async getAnimeList(username, status) {
  144. let page = 1, entries = [], hasNextPage = true;
  145. while (hasNextPage) {
  146. const query = `
  147. query {
  148. Page(page:${page}, perPage:50) {
  149. pageInfo { hasNextPage, currentPage },
  150. mediaList(userName:"${username}", type:ANIME, status:${status}) {
  151. media { title { romaji } }
  152. }
  153. }
  154. }
  155. `;
  156. const response = await axios.post(this.apiBaseUrl, { query });
  157. const data = response.data.data.Page;
  158. entries = entries.concat(data.mediaList);
  159. hasNextPage = data.pageInfo.hasNextPage;
  160. page = data.pageInfo.currentPage + 1;
  161. }
  162. return entries.map(entry => new AnimeEntry(entry.media.title.romaji));
  163. }
  164. },
  165. {
  166. name: "YugenAnime",
  167. icon: "https://yugenanime.tv/static/img/logo__light.png",
  168. statuses: [1, 2, 4, 5, 3],
  169. apiBaseUrl: 'https://yugenanime.tv/api/mylist',
  170. async getAnimeList(username, status) {
  171. const url = `${this.apiBaseUrl}/?list_status=${status}`;
  172. const response = await axios.get(url);
  173. const doc = new DOMParser().parseFromString(response.data.query, 'text/html');
  174. const list = Array.from(doc.querySelectorAll('.list-entry-row'), row => {
  175. return new AnimeEntry(row.querySelector('.list-entry-title a').textContent.trim());
  176. });
  177. return list;
  178. }
  179. }
  180. ];
  181.  
  182.  
  183. /***************************************************************
  184. * Classes for handling various data like settings, lists,
  185. * services and websites
  186. ***************************************************************/
  187. // User settings
  188. class UserSettings {
  189. constructor(usernames = {}) {
  190. this.usernames = usernames;
  191. }
  192. static load() {
  193. return GM_getValue(userSettingsKey, new UserSettings());
  194. }
  195. }
  196.  
  197. // Anime entry
  198. class AnimeEntry {
  199. constructor(title) {
  200. this.title = title;
  201. }
  202. }
  203.  
  204. // Anime list
  205. class AnimeList {
  206. constructor(key) {
  207. this.entries = GM_getValue(key, []);
  208. }
  209.  
  210. clear() {
  211. this.entries = [];
  212. }
  213.  
  214. removeEntry(entry) {
  215. this.entries = this.entries.filter(e => e.title !== entry.title);
  216. }
  217.  
  218. addEntry(entry) {
  219. this.entries.push(entry);
  220. }
  221.  
  222. // Use jaro-winkler algorithm to compare whether the given title is similar present in the animelist
  223. isEntryExist(title) {
  224. const threshold = 0.8;
  225. const a = title.toLowerCase();
  226. return this.entries.some(e => {
  227. const b = e.title.toLowerCase(), m = a.length, n = b.length;
  228. if (n === 0 || m === 0) return false;
  229. const max = Math.floor(Math.max(n, m) / 2) - 1, ma = Array(n).fill(false), mb = Array(m).fill(false);
  230. let mtc = 0;
  231. for (let i = 0; i < n; i++) {
  232. const s = Math.max(0, i - max), e = Math.min(m, i + max + 1);
  233. for (let j = s; j < e; j++) {
  234. if (!mb[j] && b[i] === a[j]) {
  235. ma[i] = true, mb[j] = true, mtc++;
  236. break;
  237. }
  238. }
  239. }
  240. if (mtc === 0) return false;
  241. let tr = 0, k = 0;
  242. for (let i = 0; i < n; i++) {
  243. if (ma[i]) {
  244. while (!mb[k]) k++;
  245. if (b[i] !== a[k]) tr++;
  246. k++;
  247. }
  248. }
  249. const sim = (mtc / n + mtc / m + (mtc - tr / 2) / mtc) / 3;
  250. if (sim >= 0.7) console.log(`jaro-winkler: ${b} - ${a} = ${sim}`);
  251. return sim >= threshold;
  252. });
  253. }
  254. }
  255.  
  256. // Website class
  257. class Website {
  258. constructor(site) {
  259. this.site = site;
  260.  
  261. // Apply initial CSS styles
  262. GM_addStyle(`
  263. /* Show eps on Hover */
  264. ${site.item} ${site.thumbnail}:hover {
  265. opacity: 1 !important;
  266. filter: brightness(1) !important;
  267. transition: .2s ease-in-out !important;
  268. }
  269. `);
  270. }
  271.  
  272. // Gets all the anime items on the page
  273. getAnimeItems() {
  274. return $(this.site.item);
  275. }
  276.  
  277. // Gets the anime title from the anime item
  278. getAnimeTitle(animeItem) {
  279. return $(animeItem).find(this.site.title).contents().filter(function() {
  280. return this.nodeType === Node.TEXT_NODE;
  281. }).text().trim();
  282. }
  283.  
  284. undarkenRelatedEps(animeList) {
  285. const animeItems = this.getAnimeItems();
  286. animeItems.each((_, animeItem) => {
  287. const animeTitle = this.getAnimeTitle(animeItem);
  288. const isRelated = animeList.isEntryExist(animeTitle);
  289. if (isRelated) {
  290. // console.log(`Anime "${animeTitle}" is related:`, isRelated);
  291. $(animeItem).find(this.site.thumbnail).css({
  292. opacity: '1',
  293. filter: 'brightness(1)',
  294. transition: '.2s ease-in-out'
  295. });
  296. } else {
  297. $(animeItem).find(this.site.thumbnail).css({
  298. opacity: '0.5',
  299. filter: 'brightness(0.3)',
  300. transition: '.4s ease-in-out'
  301. });
  302. }
  303. });
  304. }
  305. }
  306.  
  307.  
  308. /***************************************************************
  309. * Initialize all data and setup menu commands
  310. ***************************************************************/
  311. // User settings
  312. let userSettings = UserSettings.load();
  313.  
  314. // Anime list and manual list
  315. const animeList = new AnimeList(animeListKey);
  316. const manualList = new AnimeList(manualListKey);
  317.  
  318. // Service instance
  319. let service = null;
  320. chooseService(parseInt(GM_getValue('service', 1)));
  321.  
  322. // Register menu commands
  323. GM_registerMenuCommand('Show Options', showOptions);
  324.  
  325.  
  326. /***************************************************************
  327. * Functions for working of script
  328. ***************************************************************/
  329. // Show menu options as a prompt
  330. function showOptions() {
  331. let options = { 'Refresh Anime List': refreshList, 'Change Username': changeUsername, 'Manually Add/Remove Anime': modifyManualAnime, 'Choose Service': chooseService }
  332. let opt = prompt(
  333. `${GM_info.script.name}\n\nChoose an option:\n${Object.keys(options).map((key, i) => `${i + 1}. ${key}`).join('\n')}`, '1'
  334. )
  335. if (opt !== null) {
  336. let index = parseInt(opt) - 1
  337. let selectedOption = Object.values(options)[index]
  338. selectedOption()
  339. }
  340. }
  341.  
  342. // Refresh the anime list from MAL and store it using GM_setValue
  343. async function refreshList() {
  344. try {
  345. const username = userSettings.usernames[service.name]
  346. if (!username) {
  347. alert(`Please set your ${service.name} username to continue.`);
  348. changeUsername();
  349. return;
  350. }
  351. console.log(service);
  352.  
  353. GM_notification("Refreshing your list...", GM_info.script.name, service.icon)
  354.  
  355. const entriesWatching = await service.getAnimeList(username, service.statuses[0]);
  356. const entriesPlanned = await service.getAnimeList(username, service.statuses[1]);
  357. const entriesManual = manualList.entries;
  358.  
  359. const oldAnimeList = animeList.entries.map(entry => entry.title);
  360. animeList.clear();
  361. entriesWatching.forEach(entry => animeList.addEntry(entry));
  362. entriesPlanned.forEach(entry => animeList.addEntry(entry));
  363. entriesManual.forEach(entry => manualList.addEntry(entry));
  364. const newAnimeList = animeList.entries.map(entry => entry.title);
  365.  
  366. GM_setValue(animeListKey, animeList.entries);
  367.  
  368. const removedAnime = oldAnimeList.filter(anime => !newAnimeList.includes(anime));
  369. const addedAnime = newAnimeList.filter(anime => !oldAnimeList.includes(anime));
  370. const unchangedAnime = newAnimeList.filter(anime => oldAnimeList.includes(anime));
  371.  
  372. let output = '';
  373. if (removedAnime.length > 0) output += `-${removedAnime.join('\n-')}\n`;
  374. if (addedAnime.length > 0) output += `+${addedAnime.join('\n+')}\n`;
  375. output += `${unchangedAnime.join('\n')}`;
  376.  
  377. alert(`Anime list refreshed (${newAnimeList.length - oldAnimeList.length}/${newAnimeList.length}):\n\n${output}`);
  378. undarkenRelatedEps();
  379. } catch (error) {
  380. console.error('An error occurred while refreshing the anime list:', error);
  381. alert(`An error occurred while refreshing the anime list:\n\n${error}\n\n\nAlternatively, you can try to refresh the list from any other supported site and return here.\n\nSupported sites: ${animeSites.map(site => site.name).join(', ')}`);
  382. }
  383. }
  384.  
  385. // Change MAL username
  386. function changeUsername() {
  387. const newUsername = prompt(`Enter your ${service.name} username:`);
  388. if (newUsername) {
  389. userSettings.usernames[service.name] = newUsername;
  390. GM_setValue(userSettingsKey, userSettings);
  391. refreshList();
  392. }
  393. }
  394.  
  395. // Manually add anime
  396. function modifyManualAnime() {
  397. const animeTitle = prompt('This is a fallback mechanism to be used when the anime is not available on any service.\nFor both- Adding and Removing an anime, just enter the anime name.\n\nWith exact spelling, Enter the anime title:').trim();
  398. if (animeTitle == 'clear') { manualList.clear(); GM_setValue(manualListKey, manualList.entries); alert('Manual List Cleared'); return; }
  399. if (animeTitle) {
  400. const animeEntry = new AnimeEntry(animeTitle);
  401. if (manualList.isEntryExist(animeTitle)) {
  402. manualList.removeEntry(animeEntry);
  403. alert(`Anime Removed Successfully (reload page to see changes):\n\n${animeEntry.title}`);
  404. } else {
  405. manualList.addEntry(animeEntry);
  406. alert(`Anime Added Successfully:\n\n${animeEntry.title}`);
  407. }
  408. GM_setValue(manualListKey, manualList.entries);
  409. undarkenRelatedEps();
  410. }
  411. }
  412.  
  413. // Prompt the user to choose a service
  414. function chooseService(ch) {
  415. let choice = typeof ch == 'number' ? ch : parseInt(GM_getValue('service', 1));
  416.  
  417. if (typeof ch !== 'number') {
  418. const msg = `${GM_info.script.name}\n\nChoose a service:\n${services.map((s, i) => `${i + 1}. ${s.name}`).join('\n')}`;
  419. choice = prompt(msg, choice);
  420. }
  421. if (choice == null) { return } else choice = parseInt(choice);
  422. let newService = services[choice - 1];
  423.  
  424. if (!newService) {
  425. console.log('Invalid choice. Switch to a different service for now.');
  426. return chooseService(parseInt(GM_getValue('service', 1)));
  427. } else service = newService;
  428.  
  429. GM_setValue('service', choice);
  430.  
  431. if (typeof ch !== 'number') {
  432. GM_notification(`Switched to ${service.name} service.`, GM_info.script.name, service.icon);
  433. refreshList();
  434. }
  435.  
  436. console.log(`Switched to ${service.name} service.`);
  437. return service;
  438. }
  439.  
  440. // Undarken related eps based on the anime titles
  441. function undarkenRelatedEps() {
  442. const animeSite = getCurrentSite();
  443. const thisSite = new Website(animeSite);
  444. console.log('animeList', animeList)
  445. // Workaround for sites like AnimePahe which dynamically generate episodes page
  446. setTimeout(() => {
  447. const entriesList = Object.create(animeList);
  448. entriesList.entries = animeList.entries.concat(manualList.entries);
  449. console.log('entriesList', entriesList);
  450. if (!animeSite) console.error('No matching website found.');
  451. else thisSite.undarkenRelatedEps(entriesList);
  452. }, animeSite.timeout);
  453. }
  454.  
  455. // Get the current website based on the URL
  456. function getCurrentSite() {
  457. const currentUrl = window.location.href.toLowerCase();
  458. return animeSites.find(website => website.url.some(site => currentUrl.includes(site)));
  459. }
  460.  
  461. // Run the script
  462. undarkenRelatedEps();
  463.  
  464. // Refresh the anime list if it has been more than a week since the last refresh
  465. const lastRefreshTime = GM_getValue('lastRefreshTime', 0);
  466. const currentTime = new Date().getTime();
  467. const refreshInterval = 7 * 24 * 60 * 60 * 1000; // 7 days in milliseconds
  468. if (currentTime - lastRefreshTime > refreshInterval) {
  469. refreshList();
  470. GM_setValue('lastRefreshTime', currentTime);
  471. }

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址