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-12-09 提交的版本,查看 最新版本

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

QingJ © 2025

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