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

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

QingJ © 2025

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