// ==UserScript== // @name AnimePahe Improvements // @namespace https://gist.github.com/Ellivers/f7716b6b6895802058c367963f3a2c51 // @match https://animepahe.com/* // @match https://animepahe.org/* // @match https://animepahe.ru/* // @match https://kwik.*/e/* // @match https://kwik.*/f/* // @grant GM_getValue // @grant GM_setValue // @version 3.23.1 // @author Ellivers // @license MIT // @description Improvements and additions for the AnimePahe site // @downloadURL none // ==/UserScript== /* How to install: * Get the Violentmonkey browser extension (Tampermonkey is largely untested, but seems to work as well). * For the GitHub Gist page, click the "Raw" button on this page. * For Greasy Fork, click "Install this script". * I highly suggest using an ad blocker (uBlock Origin is recommended) Feature list: * Automatically redirects to the correct session when a tab with an old session is loaded. No more having to search for the anime and find the episode again! * Saves your watch progress of each video, so you can resume right where you left off. * The saved data for old sessions can be cleared and is fully viewable and editable. * Bookmark anime and view it in a bookmark menu. * Add ongoing anime to an episode feed to easily check when new episodes are out. * Quickly visit the download page for a video, instead of having to wait 5 seconds when clicking the download link. * Find collections of anime series in the search results, with the series listed in release order. * Jump directly to the next anime's first episode from the previous anime's last episode, and the other way around. * Hide all episode thumbnails on the site, for those who are extra wary of spoilers (and for other reasons). * Reworked anime index page. You can now: * Find anime with your desired genre, theme, type, demographic, status and season. * Search among these filter results. * Open a random anime within the specified filters and search query. * Automatically finds a relevant cover for the top of anime pages. * Frame-by-frame controls on videos, using ',' and '.' * Skip 10 seconds on videos at a time, using 'j' and 'l' * Changes the video 'loop' keybind to Shift + L * Press Shift + N to go to the next episode, and Shift + P to go to the previous one. * Speed up or slow down a video by holding Ctrl and: * Scrolling up/down * Pressing the up/down keys * You can also hold shift to make the speed change more gradual. * Enables you to see images from the video while hovering over the progress bar. * Allows you to also use numpad number keys to seek through videos. * Theatre mode for a better non-fullscreen video experience on larger screens. * Instantly loads the video instead of having to click a button to load it. * Adds an "Auto-Play Video" option to automatically play the video (on some browsers, you may need to allow auto-playing for this to work). * Adds an "Auto-Play Next" option to automatically go to the next episode when the current one is finished. * Focuses on the video player when loading the page, so you don't have to click on it to use keyboard controls. * Adds an option to automatically choose the highest quality available when loading the video. * Adds a button (in the settings menu) to reset the video player. * Shows the dates of when episodes were added. * And more! */ const baseUrl = window.location.toString(); const initialStorage = getStorage(); function getDefaultData() { return { version: 1, linkList:[], videoTimes:[], bookmarks:[], notifications: { lastUpdated: Date.now(), anime: [], episodes: [] }, badCovers: [], autoDelete:true, hideThumbnails:false, theatreMode:false, bestQuality:true, autoDownload:true, autoPlayNext:false, autoPlayVideo:false }; } function upgradeData(data, fromver) { console.log(`[AnimePahe Improvements] Upgrading data from version ${fromver === undefined ? 0 : fromver}`); /* Changes: * V1: * autoPlay -> autoPlayNext */ switch (fromver) { case undefined: data.autoPlayNext = data.autoPlay; delete data.autoPlay; break; } } function getStorage() { const defa = getDefaultData(); const res = GM_getValue('anime-link-tracker', defa); const oldVersion = res.version; for (const key of Object.keys(defa)) { if (res[key] !== undefined) continue; res[key] = defa[key]; } if (oldVersion !== defa.version) { upgradeData(res, oldVersion); saveData(res); } return res; } function saveData(data) { GM_setValue('anime-link-tracker', data); } function secondsToHMS(secs) { const mins = Math.floor(secs/60); const hrs = Math.floor(mins/60); const newSecs = Math.floor(secs % 60); return `${hrs > 0 ? hrs + ':' : ''}${mins % 60}:${newSecs.toString().length > 1 ? '' : '0'}${newSecs % 60}`; } function getStoredTime(name, ep, storage, id = undefined) { if (id !== undefined) { return storage.videoTimes.find(a => a.episodeNum === ep && a.animeId === id); } else return storage.videoTimes.find(a => a.animeName === name && a.episodeNum === ep); } const kwikDLPageRegex = /^https:\/\/kwik\.\w+\/f\//; // Video player improvements if (/^https:\/\/kwik\.\w+/.test(baseUrl)) { if (typeof $ !== "undefined" && $() !== null) anitrackerKwikLoad(window.location.origin + window.location.pathname); else { const scriptElem = document.querySelector('head > link:nth-child(12)'); if (scriptElem == null) { const h1 = document.querySelector('h1'); // Some bug that the kwik DL page had before // (You're not actually blocked when this happens) if (!kwikDLPageRegex.test(baseUrl) && h1.textContent == "Sorry, you have been blocked") { h1.textContent = "Oops, page failed to load."; document.querySelector('h2').textContent = "This doesn't mean you're blocked. Try playing from another page instead."; } return; } scriptElem.onload(() => {anitrackerKwikLoad(window.location.origin + window.location.pathname)}); } function anitrackerKwikLoad(url) { if (kwikDLPageRegex.test(url)) { if (initialStorage.autoDownload === false) return; $(`
[AnimePahe Improvements] Downloading...
`).prependTo(document.body); if ($('form').length > 0) { $('form').submit(); setTimeout(() => {$('#anitrackerKwikDL').remove()}, 1500); } else new MutationObserver(function(mutationList, observer) { if ($('form').length > 0) { observer.disconnect(); $('form').submit(); setTimeout(() => {$('#anitrackerKwikDL').remove()}, 1500); } }).observe(document.body, { childList: true, subtree: true }); return; } if ($('.anitracker-message').length > 0) { console.log("[AnimePahe Improvements (Player)] Script was reloaded."); return; } $(`
Loading...
`).appendTo('.plyr--video'); $('button.plyr__controls__item:nth-child(1)').hide(); $('.plyr__progress__container').hide(); const player = $('#kwikPlayer')[0]; function getVideoInfo() { const fileName = document.getElementsByClassName('ss-label')[0].textContent; const nameParts = fileName.split('_'); let name = ''; for (let i = 0; i < nameParts.length; i++) { const part = nameParts[i]; if (part.trim() === 'AnimePahe') { i ++; continue; } if (part === 'Dub' && i >= 1 && [2,3,4,5].includes(nameParts[i-1].length)) break; if (/\d{2}/.test(part) && i >= 1 && nameParts[i-1] === '-') break; name += nameParts[i-1] + ' '; } return { animeName: name.slice(0, -1), episodeNum: +/^AnimePahe_.+_-_([\d\.]{2,})/.exec(fileName)[1], resolution: +/^AnimePahe_.+_-_[\d\.]{2,}(?:_[A-Za-z]+)?_(\d+)p/.exec(fileName)[1] }; } async function handleTimestamps(title, episode) { const req = new XMLHttpRequest(); req.open('GET', 'https://raw.githubusercontent.com/c032/anidb-animetitles-archive/refs/heads/main/data/animetitles.json', true); req.onload = () => { if (req.status !== 200) return; const data = req.response.split('\n'); let anidbId = undefined; for (const anime of data) { const obj = JSON.parse(anime); if (obj.titles.find(a => a.title === title) === undefined) continue; anidbId = obj.id; break; } if (anidbId === undefined) return; const req2 = new XMLHttpRequest(); req2.open('GET', 'https://raw.githubusercontent.com/jonbarrow/open-anime-timestamps/refs/heads/master/timestamps.json', true); // Timestamp data req2.onload = () => { if (req.status !== 200) return; const data = JSON.parse(req2.response)[anidbId]; if (data === undefined) { console.log('[AnimePahe Improvements] Could not find timestamp data.'); return; } console.log(data); } req2.send(); } req.send(); } function updateTime() { const currentTime = player.currentTime; const storage = getStorage(); // Delete the storage entry if (player.duration - currentTime <= 20) { const videoInfo = getVideoInfo(); storage.videoTimes = storage.videoTimes.filter(a => !(a.animeName === videoInfo.animeName && a.episodeNum === videoInfo.episodeNum)); saveData(storage); return; } if (waitingState.idRequest === 1) return; const vidInfo = getVideoInfo(); const storedVideoTime = getStoredTime(vidInfo.animeName, vidInfo.episodeNum, storage); if (storedVideoTime === undefined) { if (![-1,0].includes(waitingState.idRequest)) { // If the video has loaded (>0) and getting the ID has not failed (-1) waitingState.idRequest = 1; sendMessage({action: "id_request"}); setTimeout(() => { if (waitingState.idRequest === 1) { waitingState.idRequest = -1; // Failed to get the anime ID from the main page within 2 seconds updateTime(); } }, 2000); return; } const vidInfo = getVideoInfo(); storage.videoTimes.push({ videoUrls: [url], time: player.currentTime, animeName: vidInfo.animeName, episodeNum: vidInfo.episodeNum }); if (storage.videoTimes.length > 1000) { storage.splice(0,1); } saveData(storage); return; } storedVideoTime.time = player.currentTime; if (storedVideoTime.playbackRate !== undefined || player.playbackRate !== 1) storedVideoTime.playbackRate = player.playbackRate; saveData(storage); } if (initialStorage.videoTimes === undefined) { const storage = getStorage(); storage.videoTimes = []; saveData(storage); } // For message requests from the main page // -1: failed // 0: hasn't started // 1: waiting // 2: succeeded const waitingState = { idRequest: 0, videoUrlRequest: 0 }; // Messages received from main page window.onmessage = function(e) { const data = e.data; const action = data.action; if (action === 'id_response' && waitingState.idRequest === 1) { const storage = getStorage(); storage.videoTimes.push({ videoUrls: [url], time: 0, animeName: getVideoInfo().animeName, episodeNum: getVideoInfo().episodeNum, animeId: data.id }); if (storage.videoTimes.length > 1000) { storage.splice(0,1); } saveData(storage); waitingState.idRequest = 2; /* WIP feature const episodeObj = storage.linkList.find(a => a.type === 'episode' && a.animeId === data.id); if (episodeObj === undefined) return; handleTimestamps(episodeObj.animeName, episodeObj.episodeNum);*/ return; } else if (action === 'video_url_response' && waitingState.videoUrlRequest === 1) { const request = new XMLHttpRequest(); request.open('GET', data.url, true); request.onload = () => { if (request.status !== 200) { console.error('[AnimePahe Improvements] Could not get kwik page for video source'); return; } const pageElements = Array.from($(request.response)); // Elements that are not buried cannot be found with jQuery.find() const hostInfo = (() => { for (const link of pageElements.filter(a => a.tagName === 'LINK')) { const href = $(link).attr('href'); if (!href.includes('vault')) continue; const result = /vault-(\d+)\.(\w+\.\w+)$/.exec(href); return { vaultId: result[1], hostName: result[2] } break; } })(); const searchInfo = (() => { for (const script of pageElements.filter(a => a.tagName === 'SCRIPT')) { if ($(script).attr('url') !== undefined || !$(script).text().startsWith('eval')) continue; const result = /(\w{64})\|((?:\w+\|){4,5})https/.exec($(script).text()); let extraNumber = undefined; result[2].split('|').forEach(a => {if (/\d{2}/.test(a)) extraNumber = a;}); // Some number that's needed for the url (doesn't always exist here) if (extraNumber === undefined) { const result2 = /q=\\'\w+:\/{2}\w+\-\w+\.\w+\.\w+\/((?:\w+\/)+)/.exec($(script).text()); result2[1].split('/').forEach(a => {if (/\d{2}/.test(a) && a !== hostInfo.vaultId) extraNumber = a;}); } return { part1: extraNumber, part2: result[1] }; break; } })(); if (searchInfo.part1 === undefined) { console.error('[AnimePahe Improvements] Could not find "extraNumber" from ' + data.url); return; } waitingState.videoUrlRequest = 2; setupSeekThumbnails(`https://vault-${hostInfo.vaultId}.${hostInfo.hostName}/stream/${hostInfo.vaultId}/${searchInfo.part1}/${searchInfo.part2}/uwu.m3u8`); }; request.send(); } else if (action === 'change_time') { if (data.time !== undefined) player.currentTime = data.time; } else if (action === 'key') { if ([' ','k'].includes(data.key)) { if (player.paused) player.play(); else player.pause(); } else if (data.key === 'ArrowLeft') { player.currentTime = Math.max(0, player.currentTime - 5); return; } else if (data.key === 'ArrowRight') { player.currentTime = Math.min(player.duration, player.currentTime + 5); return; } else if (/^\d$/.test(data.key)) { player.currentTime = (player.duration/10)*(+data.key); return; } else if (data.key === 'm') player.muted = !player.muted; else $(player).trigger('keydown', { key: data.key }); } }; player.addEventListener('loadeddata', function loadVideoData() { const storage = getStorage(); const vidInfo = getVideoInfo(); const storedVideoTime = getStoredTime(vidInfo.animeName, vidInfo.episodeNum, storage); if (storedVideoTime !== undefined) { player.currentTime = Math.max(0, Math.min(storedVideoTime.time, player.duration)); if (!storedVideoTime.videoUrls.includes(url)) { storedVideoTime.videoUrls.push(url); saveData(storage); } if (![undefined,1].includes(storedVideoTime.playbackRate)) { setSpeed(storedVideoTime.playbackRate); } else player.playbackRate = 1; } else { player.playbackRate = 1; waitingState.idRequest = 1; sendMessage({action: "id_request"}); setTimeout(() => { if (waitingState.idRequest === 1) { waitingState.idRequest = -1; // Failed to get the anime ID from the main page within 2 seconds updateTime(); } }, 2000); removeLoadingIndicators(); } const timeArg = Array.from(new URLSearchParams(window.location.search)).find(a => a[0] === 'time'); if (timeArg !== undefined) { const newTime = +timeArg[1]; if (storedVideoTime === undefined || (storedVideoTime !== undefined && Math.floor(storedVideoTime.time) === Math.floor(newTime)) || (storedVideoTime !== undefined && confirm(`[AnimePahe Improvements]\n\nYou already have saved progress on this video (${secondsToHMS(storedVideoTime.time)}). Do you want to overwrite it and go to ${secondsToHMS(newTime)}?`))) { player.currentTime = Math.max(0, Math.min(newTime, player.duration)); } window.history.replaceState({}, document.title, url); } player.removeEventListener('loadeddata', loadVideoData); // Set up events let lastTimeUpdate = 0; player.addEventListener('timeupdate', function() { if (Math.trunc(player.currentTime) % 10 === 0 && player.currentTime - lastTimeUpdate > 9) { updateTime(); lastTimeUpdate = player.currentTime; } }); player.addEventListener('pause', () => { updateTime(); }); player.addEventListener('seeked', () => { updateTime(); removeLoadingIndicators(); }); if (storage.autoPlayVideo === true) { player.play() } }); function getFrame(video, time, dimensions) { return new Promise((resolve) => { video.onseeked = () => { const canvas = document.createElement('canvas'); canvas.height = dimensions.y; canvas.width = dimensions.x; const ctx = canvas.getContext('2d'); ctx.drawImage(video, 0, 0, canvas.width, canvas.height); resolve(canvas.toDataURL('image/png')); }; try { video.currentTime = time; } catch (e) { console.error(time, e); } }); } const settingsContainerId = (() => { for (const elem of $('.plyr__menu__container')) { const regex = /plyr\-settings\-(\d+)/.exec(elem.id); if (regex === null) continue; return regex[1]; } return undefined; })(); function setupSeekThumbnails(videoSource) { const resolution = 167; const bgVid = document.createElement('video'); bgVid.height = resolution; bgVid.onloadeddata = () => { const fullDuration = bgVid.duration; const timeBetweenThumbnails = fullDuration/(24*6); // Just something arbitrary that seems good const thumbnails = []; const aspectRatio = bgVid.videoWidth / bgVid.videoHeight; const aspectRatioCss = `${bgVid.videoWidth} / ${bgVid.videoHeight}`; const mainStyles = [ "width: 219px", "aspect-ratio: " + aspectRatioCss, "padding: 5px", "opacity:0", "position: absolute", "left:0%", "bottom: 100%", "background-color: rgba(255,255,255,0.88)", "border-radius: 8px", "transition: translate .2s ease .1s,scale .2s ease .1s,opacity .1s ease .05s", "transform: translate(-50%,0)", "user-select: none", "pointer-events: none" ] $('.plyr__progress .plyr__tooltip').remove(); $(`
0:00
`).insertAfter(`progress`); $('.anitracker-progress-tooltip img').on('load', () => { $('.anitracker-progress-tooltip img').css('display', 'block'); }); const toggleVisibility = (on) => { if (on) $('.anitracker-progress-tooltip').css('opacity', '1').css('scale','1').css('translate',''); else $('.anitracker-progress-tooltip').css('opacity', '0').css('scale','0.75').css('translate','-12.5% 20px'); }; const elem = $('.anitracker-progress-tooltip'); let currentTime = 0; new MutationObserver(function(mutationList, observer) { if ($('.plyr--full-ui').hasClass('plyr--hide-controls') || !$(`#plyr-seek-${settingsContainerId}`)[0].matches(':hover')) { toggleVisibility(false); return; } toggleVisibility(true); const seekValue = $(`#plyr-seek-${settingsContainerId}`).attr('seek-value'); const time = seekValue !== undefined ? Math.min(Math.max(Math.trunc(fullDuration*(+seekValue/100)), 0), fullDuration) : Math.trunc(player.currentTime); const roundedTime = Math.trunc(time/timeBetweenThumbnails)*timeBetweenThumbnails; const timeSlot = Math.trunc(time/timeBetweenThumbnails); elem.find('span').text(secondsToHMS(time)); elem.css('left', seekValue + '%'); if (roundedTime === Math.trunc(currentTime/timeBetweenThumbnails)*timeBetweenThumbnails) return; const cached = thumbnails.find(a => a.time === timeSlot); if (cached !== undefined) { elem.find('img').attr('src', cached.data); } else { elem.find('img').css('display', 'none'); getFrame(bgVid, roundedTime, {y: resolution, x: resolution*aspectRatio}).then((response) => { thumbnails.push({ time: timeSlot, data: response }); elem.find('img').css('display', 'none'); elem.find('img').attr('src', response); }); } currentTime = time; }).observe($(`#plyr-seek-${settingsContainerId}`)[0], { attributes: true }); $(`#plyr-seek-${settingsContainerId}`).on('mouseleave', () => { toggleVisibility(false); }); } const hls2 = new Hls({ maxBufferLength: 0.1, backBufferLength: 0, capLevelToPlayerSize: true, maxAudioFramesDrift: Infinity }); hls2.loadSource(videoSource); hls2.attachMedia(bgVid); } // Thumbnails when seeking if (Hls.isSupported()) { sendMessage({action:"video_url_request"}); waitingState.videoUrlRequest = 1; setTimeout(() => { if (waitingState.videoUrlRequest === 2) return; waitingState.videoUrlRequest = -1; if (typeof hls !== "undefined") setupSeekThumbnails(hls.url); }, 500); } function removeLoadingIndicators() { $('.anitracker-loading').remove(); $('button.plyr__controls__item:nth-child(1)').show(); $('.plyr__progress__container').show(); } let messageTimeout = undefined; function showMessage(text) { $('.anitracker-message span').text(text); $('.anitracker-message').css('display', 'flex'); clearTimeout(messageTimeout); messageTimeout = setTimeout(() => { $('.anitracker-message').hide(); }, 1000); } const frametime = 1 / 24; let funPitch = ""; $(document).on('keydown', function(e, other = undefined) { const key = e.key || other.key; if (key === 'ArrowUp') { changeSpeed(e, -1); // The changeSpeed function only works if ctrl is being held return; } if (key === 'ArrowDown') { changeSpeed(e, 1); return; } if (e.shiftKey && ['l','L'].includes(key)) { showMessage('Loop: ' + (player.loop ? 'Off' : 'On')); player.loop = !player.loop; return; } if (e.shiftKey && ['n','N'].includes(key)) { sendMessage({action: "next"}); return; } if (e.shiftKey && ['p','P'].includes(key)) { sendMessage({action: "previous"}); return; } if (e.ctrlKey || e.altKey || e.shiftKey || e.metaKey) return; // Prevents special keys for the rest of the keybinds if (key === 'j') { player.currentTime = Math.max(0, player.currentTime - 10); return; } else if (key === 'l') { player.currentTime = Math.min(player.duration, player.currentTime + 10); setTimeout(() => { player.loop = false; }, 5); return; } else if (/^Numpad\d$/.test(e.code)) { player.currentTime = (player.duration/10)*(+e.code.replace('Numpad', '')); return; } if (!(player.currentTime > 0 && !player.paused && !player.ended && player.readyState > 2)) { if (key === ',') { player.currentTime = Math.max(0, player.currentTime - frametime); return; } else if (key === '.') { player.currentTime = Math.min(player.duration, player.currentTime + frametime); return; } } funPitch += key; if (funPitch === 'crazy') { player.preservesPitch = !player.preservesPitch; showMessage(player.preservesPitch ? 'Off' : 'Change speed ;D'); funPitch = ""; return; } if (!"crazy".startsWith(funPitch)) { funPitch = ""; } sendMessage({ action: "key", key: key }); }); // Ctrl+scrolling to change speed $(` `).appendTo($(player).parents().eq(1)); jQuery.event.special.wheel = { setup: function( _, ns, handle ){ this.addEventListener("wheel", handle, { passive: false }); } }; const defaultSpeeds = player.plyr.options.speed; function changeSpeed(e, delta) { if (!e.ctrlKey) return; e.preventDefault(); if (delta == 0) return; const speedChange = e.shiftKey ? 0.05 : 0.1; setSpeed(player.playbackRate + speedChange * (delta > 0 ? -1 : 1)); } function setSpeed(speed) { if (speed > 0) player.playbackRate = Math.round(speed * 100) / 100; showMessage(player.playbackRate + "x"); if (defaultSpeeds.includes(player.playbackRate)) { $('.anitracker-custom-speed-btn').remove(); } else if ($('.anitracker-custom-speed-btn').length === 0) { $(`#plyr-settings-${settingsContainerId}-speed>div>button`).attr('aria-checked','false'); $(` `).prependTo(`#plyr-settings-${settingsContainerId}-speed>div`); for (const elem of $(`#plyr-settings-${settingsContainerId}-home>div>`)) { if (!/^Speed/.test($(elem).children('span')[0].textContent)) continue; $(elem).find('span')[1].textContent = "Custom"; } } } $(`#plyr-settings-${settingsContainerId}-speed>div>button`).on('click', (e) => { $('.anitracker-custom-speed-btn').remove(); }); $(document).on('wheel', function(e) { changeSpeed(e, e.originalEvent.deltaY); }); } return; } if ($() !== null) anitrackerLoad(window.location.origin + window.location.pathname + window.location.search); else { document.querySelector('head > link:nth-child(10)').onload(() => {anitrackerLoad(window.location.origin + window.location.pathname + window.location.search)}); } function anitrackerLoad(url) { if ($('#anitracker-modal').length > 0) { console.log("[AnimePahe Improvements] Script was reloaded."); return; } if (initialStorage.hideThumbnails === true) { hideThumbnails(); } function windowOpen(url, target = '_blank') { $(``)[0].click(); } (function($) { $.fn.changeElementType = function(newType) { let attrs = {}; $.each(this[0].attributes, function(idx, attr) { attrs[attr.nodeName] = attr.nodeValue; }); this.replaceWith(function() { return $("<" + newType + "/>", attrs).append($(this).contents()); }); }; $.fn.replaceClass = function(oldClass, newClass) { this.removeClass(oldClass).addClass(newClass); }; })(jQuery); // -------- AnimePahe Improvements CSS --------- $("head").append(''); const sheet = $("#anitracker-style")[0].sheet; const animationTimes = { modalOpen: 0.2, fadeIn: 0.2 }; const rules = ` #anitracker { display: flex; flex-direction: row; gap: 15px 7px; align-items: center; flex-wrap: wrap; } .anitracker-index { align-items: end !important; } #anitracker>span {align-self: center;\n} #anitracker-modal { position: fixed; width: 100%; height: 100%; background-color: rgba(0,0,0,0.6); z-index: 20; display: none; } #anitracker-modal-content { max-height: 90%; background-color: var(--dark); margin: auto auto auto auto; border-radius: 20px; display: flex; padding: 20px; z-index:50; } #anitracker-modal-close { font-size: 2.5em; margin: 3px 10px; cursor: pointer; height: 1em; } #anitracker-modal-close:hover { color: rgb(255, 0, 108); } #anitracker-modal-body { padding: 10px; overflow-x: hidden; } #anitracker-modal-body .anitracker-switch {margin-bottom: 2px;\n} .anitracker-big-list-item { list-style: none; border-radius: 10px; margin-top: 5px; } .anitracker-big-list-item>a { font-size: 0.875rem; display: block; padding: 5px 15px; color: rgb(238, 238, 238); text-decoration: none; } .anitracker-big-list-item img { margin: auto 0px; width: 50px; height: 50px; border-radius: 100%; } .anitracker-big-list-item .anitracker-main-text { font-weight: 700; color: rgb(238, 238, 238); } .anitracker-big-list-item .anitracker-subtext { font-size: 0.75rem; color: rgb(153, 153, 153); } .anitracker-big-list-item:hover .anitracker-main-text { color: rgb(238, 238, 238); } .anitracker-big-list-item:hover .anitracker-subtext { color: rgb(238, 238, 238); } .anitracker-big-list-item:hover { background-color: #111; } .anitracker-big-list-item:focus-within .anitracker-main-text { color: rgb(238, 238, 238); } .anitracker-big-list-item:focus-within .anitracker-subtext { color: rgb(238, 238, 238); } .anitracker-big-list-item:focus-within { background-color: #111; } .anitracker-hide-thumbnails .anitracker-thumbnail img {display: none;\n} .anitracker-hide-thumbnails .anitracker-thumbnail { border: 10px solid rgb(32, 32, 32); aspect-ratio: 16/9; } .anitracker-hide-thumbnails .episode-snapshot img { display: none; } .anitracker-hide-thumbnails .episode-snapshot { border: 4px solid var(--dark); } .anitracker-download-spinner {display: inline;\n} .anitracker-download-spinner .spinner-border { height: 0.875rem; width: 0.875rem; } .anitracker-dropdown-content { display: none; position: absolute; min-width: 100px; box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); z-index: 1; max-height: 400px; overflow-y: auto; overflow-x: hidden; background-color: #171717; } .anitracker-dropdown-content button { color: white; padding: 12px 16px; text-decoration: none; display: block; width:100%; background-color: #171717; border: none; margin: 0; } .anitracker-dropdown-content button:hover, .anitracker-dropdown-content button:focus {background-color: black;\n} .anitracker-active, .anitracker-active:hover, .anitracker-active:active { color: white!important; background-color: #d5015b!important; } .anitracker-dropdown-content a:hover {background-color: #ddd;\n} .anitracker-dropdown:hover .anitracker-dropdown-content {display: block;\n} .anitracker-dropdown:hover .anitracker-dropbtn {background-color: #bc0150;\n} #pickDownload span, #scrollArea span { cursor: pointer; font-size: 0.875rem; } .anitracker-expand-data-icon { font-size: 24px; float: right; margin-top: 6px; margin-right: 8px; } .anitracker-modal-list-container { background-color: rgb(40,45,50); margin-bottom: 10px; border-radius: 12px; } .anitracker-storage-data { background-color: rgb(40,45,50); border-radius: 12px; cursor: pointer; position: relative; z-index: 1; } .anitracker-storage-data:focus { box-shadow: 0 0 0 .2rem rgb(255, 255, 255); } .anitracker-storage-data span { display:inline-block; font-size: 1.4em; font-weight: bold; } .anitracker-storage-data, .anitracker-modal-list { padding: 10px; } .anitracker-modal-list-entry {margin-top: 8px;\n} .anitracker-modal-list-entry a {text-decoration: underline;\n} .anitracker-modal-list-entry:hover {background-color: rgb(30,30,30);\n} .anitracker-modal-list-entry button { padding-top: 0; padding-bottom: 0; } .anitracker-relation-link { text-overflow: ellipsis; overflow: hidden; } #anitracker-cover-spinner .spinner-border { width:2rem; height:2rem; } .anime-cover { display: flex; justify-content: center; align-items: center; image-rendering: optimizequality; } .anitracker-items-box { width: 150px; display: inline-block; } .anitracker-items-box > div { height:45px; width:100%; border-bottom: 2px solid #454d54; } .anitracker-items-box > button { background: none; border: 1px solid #ccc; color: white; padding: 0; margin-left: 110px; vertical-align: bottom; border-radius: 5px; line-height: 1em; width: 2.5em; font-size: .8em; padding-bottom: .1em; margin-bottom: 2px; } .anitracker-items-box > button:hover { background: #ccc; color: black; } .anitracker-items-box-search { position: absolute; max-width: 150px; max-height: 45px; min-width: 150px; min-height: 45px; overflow-wrap: break-word; overflow-y: auto; } .anitracker-items-box .placeholder { color: #999; position: absolute; z-index: -1; } .anitracker-filter-icon { padding: 2px; background-color: #d5015b; border-radius: 5px; display: inline-block; cursor: pointer; } .anitracker-filter-icon:hover { border: 1px solid white; } .anitracker-text-input { display: inline-block; height: 1em; } .anitracker-text-input-bar { background: #333; box-shadow: none; color: #bbb; } .anitracker-text-input-bar:focus { border-color: #d5015b; background: none; box-shadow: none; color: #ddd; } .anitracker-list-btn { height: 42px; border-radius: 7px!important; color: #ddd!important; margin-left: 10px!important; } .anitracker-reverse-order-button { font-size: 2em; } .anitracker-reverse-order-button::after { vertical-align: 20px; } .anitracker-reverse-order-button.anitracker-up::after { border-top: 0; border-bottom: .3em solid; vertical-align: 22px; } #anitracker-time-search-button svg { width: 24px; vertical-align: bottom; } .anitracker-season-group { display: grid; grid-template-columns: 10% 30% 20% 10%; margin-bottom: 5px; } .anitracker-season-group .btn-group { margin-left: 5px; } a.youtube-preview::before { -webkit-transition: opacity .2s linear!important; -moz-transition: opacity .2s linear!important; transition: opacity .2s linear!important; } .anitracker-replaced-cover {background-position-y: 25%;\n} .anitracker-text-button { color:#d5015b; cursor:pointer; user-select:none; } .anitracker-text-button:hover { color:white; } .nav-search { float: left!important; } .anitracker-title-icon { margin-left: 1rem!important; opacity: .8!important; color: #ff006c!important; font-size: 2rem!important; vertical-align: middle; cursor: pointer; padding: 0; box-shadow: none!important; } .anitracker-title-icon:hover { opacity: 1!important; } .anitracker-title-icon-check { color: white; margin-left: -.7rem!important; font-size: 1rem!important; vertical-align: super; text-shadow: none; opacity: 1!important; } .anitracker-header { display: flex; justify-content: left; gap: 18px; flex-grow: 0.05; } .anitracker-header-button { color: white; background: none; border: 2px solid white; border-radius: 5px; width: 2rem; } .anitracker-header-button:hover { border-color: #ff006c; color: #ff006c; } .anitracker-header-button:focus { border-color: #ff006c; color: #ff006c; } .anitracker-header-notifications-circle { color: rgb(255, 0, 108); margin-left: -.3rem; font-size: 0.7rem; position: absolute; } .anitracker-notification-item .anitracker-main-text { color: rgb(153, 153, 153); } .anitracker-notification-item-unwatched { background-color: rgb(119, 62, 70); } .anitracker-notification-item-unwatched .anitracker-main-text { color: white!important; } .anitracker-notification-item-unwatched .anitracker-subtext { color: white!important; } .anitracker-watched-toggle { font-size: 1.7em; float: right; margin-right: 5px; margin-top: 5px; cursor: pointer; background-color: #592525; padding: 5px; border-radius: 5px; } .anitracker-watched-toggle:hover,.anitracker-watched-toggle:focus { box-shadow: 0 0 0 .2rem rgb(255, 255, 255); } #anitracker-replace-cover { z-index: 99; right: 10px; position: absolute; bottom: 6em; } header.main-header nav .main-nav li.nav-item > a:focus { color: #fff; background-color: #bc0150; } .theatre-settings .dropup .btn:focus { box-shadow: 0 0 0 .15rem rgb(100, 100, 100)!important; } .anitracker-episode-time { margin-left: 5%; font-size: 0.75rem!important; cursor: default!important; } .anitracker-episode-time:hover { text-decoration: none!important; } @media screen and (min-width: 1375px) { .anitracker-theatre-mode { max-width: 80%!important; } } @keyframes anitracker-modalOpen { 0% { transform: scale(0.5); } 20% { transform: scale(1.07); } 100% { transform: scale(1); } } @keyframes anitracker-fadeIn { from { opacity: 0; } to { opacity: 1; } } @keyframes anitracker-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } `.split(/^\}/mg).map(a => a.replace(/\n/gm,'') + '}'); for (let i = 0; i < rules.length - 1; i++) { sheet.insertRule(rules[i], i); } const optionSwitches = [ { optionId: 'autoDelete', switchId: 'auto-delete', value: initialStorage.autoDelete }, { optionId: 'theatreMode', switchId: 'theatre-mode', value: initialStorage.theatreMode, onEvent: () => { theatreMode(true); }, offEvent: () => { theatreMode(false); } }, { optionId: 'hideThumbnails', switchId: 'hide-thumbnails', value: initialStorage.hideThumbnails, onEvent: hideThumbnails, offEvent: () => { $('.main').removeClass('anitracker-hide-thumbnails'); } }, { optionId: 'bestQuality', switchId: 'best-quality', value: initialStorage.bestQuality, onEvent: bestVideoQuality }, { optionId: 'autoDownload', switchId: 'auto-download', value: initialStorage.autoDownload }, { optionId: 'autoPlayNext', switchId: 'autoplay-next', value: initialStorage.autoPlayNext }, { optionId: 'autoPlayVideo', switchId: 'autoplay-video', value: initialStorage.autoPlayVideo }]; const cachedAnimeData = []; // Things that update when focusing this tab $(document).on('visibilitychange', () => { if (document.hidden) return; updatePage(); }); function updatePage() { updateSwitches(); const storage = getStorage(); const data = url.includes('/anime/') ? getAnimeData() : undefined; if (data !== undefined) { const isBookmarked = storage.bookmarks.find(a => a.id === data.id) !== undefined; if (isBookmarked) $('.anitracker-bookmark-toggle .anitracker-title-icon-check').show(); else $('.anitracker-bookmark-toggle .anitracker-title-icon-check').hide(); const hasNotifications = storage.notifications.anime.find(a => a.id === data.id) !== undefined; if (hasNotifications) $('.anitracker-notifications-toggle .anitracker-title-icon-check').show(); else $('.anitracker-notifications-toggle .anitracker-title-icon-check').hide(); } if (!modalIsOpen() || $('.anitracker-view-notif-animes').length === 0) return; for (const item of $('.anitracker-notification-item-unwatched')) { const entry = storage.notifications.episodes.find(a => a.animeId === +$(item).attr('anime-data') && a.episode === +$(item).attr('episode-data') && a.watched === true); if (entry === undefined) continue; $(item).removeClass('anitracker-notification-item-unwatched'); const eye = $(item).find('.anitracker-watched-toggle'); eye.replaceClass('fa-eye', 'fa-eye-slash'); } } function theatreMode(on) { if (on) $('.theatre>').addClass('anitracker-theatre-mode'); else $('.theatre>').removeClass('anitracker-theatre-mode'); } function playAnimation(elem, anim, type = '', duration) { return new Promise(resolve => { elem.css('animation', `anitracker-${anim} ${duration || animationTimes[anim]}s forwards linear ${type}`); if (animationTimes[anim] === undefined) resolve(); setTimeout(() => { elem.css('animation', ''); resolve(); }, animationTimes[anim] * 1000); }); } let modalCloseFunction = closeModal; // AnimePahe Improvements modal function addModal() { $(`
`).insertBefore('.main-header'); $('#anitracker-modal').on('click', (e) => { if (e.target !== e.currentTarget) return; modalCloseFunction(); }); $('#anitracker-modal-close').on('click', () => { modalCloseFunction(); }); } addModal(); function openModal(closeFunction = closeModal) { if (closeFunction !== closeModal) $('#anitracker-modal-close').replaceClass('fa-close', 'fa-arrow-left'); else $('#anitracker-modal-close').replaceClass('fa-arrow-left', 'fa-close'); return new Promise(resolve => { playAnimation($('#anitracker-modal-content'), 'modalOpen'); playAnimation($('#anitracker-modal'), 'fadeIn').then(() => { $('#anitracker-modal').focus(); resolve(); }); $('#anitracker-modal').css('display','flex'); modalCloseFunction = closeFunction; }); } function closeModal() { if ($('#anitracker-modal').css('animation') !== 'none') { $('#anitracker-modal').hide(); return; } playAnimation($('#anitracker-modal'), 'fadeIn', 'reverse', 0.1).then(() => { $('#anitracker-modal').hide(); }); } function modalIsOpen() { return $('#anitracker-modal').is(':visible'); } let currentEpisodeTime = 0; // Messages received from iframe if (isEpisode()) { window.onmessage = function(e) { const data = e.data; if (typeof(data) === 'number') { currentEpisodeTime = Math.trunc(data); return; } const action = data.action; if (action === 'id_request') { sendMessage({action:"id_response",id:getAnimeData().id}); } else if (action === 'video_url_request') { const selected = { src: undefined, res: undefined, audio: undefined } for (const btn of $('#resolutionMenu>button')) { const src = $(btn).data('src'); const res = +$(btn).data('resolution'); const audio = $(btn).data('audio'); if (selected.src !== undefined && selected.res < res) continue; if (selected.audio !== undefined && audio === 'jp' && selected.res <= res) continue; // Prefer dubs, since they don't have subtitles selected.src = src; selected.res = res; selected.audio = audio; } if (selected.src === undefined) { console.error("[AnimePahe Improvements] Didn't find video URL"); return; } console.log('[AnimePahe Improvements] Found lowest resolution URL ' + selected.src); sendMessage({action:"video_url_response", url:selected.src}); } else if (action === 'key') { if (data.key === 't') { toggleTheatreMode(); } } else if (data === 'ended') { const storage = getStorage(); if (storage.autoPlayNext !== true) return; const elem = $('.sequel a'); if (elem.length > 0) elem[0].click(); } else if (action === 'next') { const elem = $('.sequel a'); if (elem.length > 0) elem[0].click(); } else if (action === 'previous') { const elem = $('.prequel a'); if (elem.length > 0) elem[0].click(); } }; } function sendMessage(message) { $('.embed-responsive-item')[0].contentWindow.postMessage(message,'*'); } function toggleTheatreMode() { const storage = getStorage(); theatreMode(!storage.theatreMode); storage.theatreMode = !storage.theatreMode; saveData(storage); updateSwitches(); } function getSeasonValue(season) { return ({winter:0, spring:1, summer:2, fall:3})[season.toLowerCase()]; } function getSeasonName(season) { return ["winter","spring","summer","fall"][season]; } function stringSimilarity(s1, s2) { let longer = s1; let shorter = s2; if (s1.length < s2.length) { longer = s2; shorter = s1; } const longerLength = longer.length; if (longerLength == 0) { return 1.0; } return (longerLength - editDistance(longer, shorter)) / parseFloat(longerLength); } function editDistance(s1, s2) { s1 = s1.toLowerCase(); s2 = s2.toLowerCase(); const costs = []; for (let i = 0; i <= s1.length; i++) { let lastValue = i; for (let j = 0; j <= s2.length; j++) { if (i == 0) costs[j] = j; else { if (j > 0) { let newValue = costs[j - 1]; if (s1.charAt(i - 1) != s2.charAt(j - 1)) newValue = Math.min(Math.min(newValue, lastValue), costs[j]) + 1; costs[j - 1] = lastValue; lastValue = newValue; } } } if (i > 0) costs[s2.length] = lastValue; } return costs[s2.length]; } function searchForCollections() { if ($('.search-results a').length === 0) return; const baseName = $($('.search-results .result-title')[0]).text(); const request = new XMLHttpRequest(); request.open('GET', '/api?m=search&q=' + encodeURIComponent(baseName), true); request.onload = () => { if (request.readyState !== 4 || request.status !== 200 ) return; response = JSON.parse(request.response).data; if (response == undefined) return; let seriesList = []; for (const anime of response) { if (stringSimilarity(baseName, anime.title) >= 0.42 || (anime.title.startsWith(baseName) && stringSimilarity(baseName, anime.title) >= 0.25)) { seriesList.push(anime); } } if (seriesList.length < 2) return; seriesList = sortAnimesChronologically(seriesList); displayCollection(baseName, seriesList); }; request.send(); } new MutationObserver(function(mutationList, observer) { if (!searchComplete()) return; searchForCollections(); }).observe($('.search-results-wrap')[0], { childList: true }); function searchComplete() { return $('.search-results').length !== 0 && $('.search-results a').length > 0; } function displayCollection(baseName, seriesList) { $(`
  • ${baseName}
    Collection - ${seriesList.length} Entries
  • `).prependTo('.search-results'); function displayInModal() { $('#anitracker-modal-body').empty(); $(`

    Collection

    `).appendTo('#anitracker-modal-body'); for (const anime of seriesList) { $(`
    [Thumbnail of ${anime.title}]
    ${anime.title}
    ${anime.type} - ${anime.episodes > 0 ? anime.episodes : '?'} Episode${anime.episodes === 1 ? '' : 's'} (${anime.status})
    ${anime.season} ${anime.year}
    `).appendTo('#anitracker-modal-body .anitracker-modal-list'); } openModal(); } $('.anitracker-collection').on('click', displayInModal); $('.input-search').on('keyup', (e) => { if (e.key === "Enter" && $('.anitracker-collection').hasClass('selected')) displayInModal(); }); } function getSeasonTimeframe(from, to) { const filters = []; for (let i = from.year; i <= to.year; i++) { const start = i === from.year ? from.season : 0; const end = i === to.year ? to.season : 3; for (let d = start; d <= end; d++) { filters.push(`season/${getSeasonName(d)}-${i.toString()}`); } } return filters; } const is404 = $('h1').text().includes('404'); if (!isRandomAnime() && initialStorage.cache !== undefined) { const storage = getStorage(); delete storage.cache; saveData(storage); } const filterSearchCache = {}; const filterValues = { "genre":[ {"name":"Comedy","value":"comedy"},{"name":"Slice of Life","value":"slice-of-life"},{"name":"Romance","value":"romance"},{"name":"Ecchi","value":"ecchi"},{"name":"Drama","value":"drama"}, {"name":"Supernatural","value":"supernatural"},{"name":"Sports","value":"sports"},{"name":"Horror","value":"horror"},{"name":"Sci-Fi","value":"sci-fi"},{"name":"Action","value":"action"}, {"name":"Fantasy","value":"fantasy"},{"name":"Mystery","value":"mystery"},{"name":"Suspense","value":"suspense"},{"name":"Adventure","value":"adventure"},{"name":"Boys Love","value":"boys-love"}, {"name":"Girls Love","value":"girls-love"},{"name":"Hentai","value":"hentai"},{"name":"Gourmet","value":"gourmet"},{"name":"Erotica","value":"erotica"},{"name":"Avant Garde","value":"avant-garde"}, {"name":"Award Winning","value":"award-winning"} ], "theme":[ {"name":"Adult Cast","value":"adult-cast"},{"name":"Anthropomorphic","value":"anthropomorphic"},{"name":"Detective","value":"detective"},{"name":"Love Polygon","value":"love-polygon"}, {"name":"Mecha","value":"mecha"},{"name":"Music","value":"music"},{"name":"Psychological","value":"psychological"},{"name":"School","value":"school"},{"name":"Super Power","value":"super-power"}, {"name":"Space","value":"space"},{"name":"CGDCT","value":"cgdct"},{"name":"Romantic Subtext","value":"romantic-subtext"},{"name":"Historical","value":"historical"},{"name":"Video Game","value":"video-game"}, {"name":"Martial Arts","value":"martial-arts"},{"name":"Idols (Female)","value":"idols-female"},{"name":"Idols (Male)","value":"idols-male"},{"name":"Gag Humor","value":"gag-humor"},{"name":"Parody","value":"parody"}, {"name":"Performing Arts","value":"performing-arts"},{"name":"Military","value":"military"},{"name":"Harem","value":"harem"},{"name":"Reverse Harem","value":"reverse-harem"},{"name":"Samurai","value":"samurai"}, {"name":"Vampire","value":"vampire"},{"name":"Mythology","value":"mythology"},{"name":"High Stakes Game","value":"high-stakes-game"},{"name":"Strategy Game","value":"strategy-game"}, {"name":"Magical Sex Shift","value":"magical-sex-shift"},{"name":"Racing","value":"racing"},{"name":"Isekai","value":"isekai"},{"name":"Workplace","value":"workplace"},{"name":"Iyashikei","value":"iyashikei"}, {"name":"Time Travel","value":"time-travel"},{"name":"Gore","value":"gore"},{"name":"Educational","value":"educational"},{"name":"Delinquents","value":"delinquents"},{"name":"Organized Crime","value":"organized-crime"}, {"name":"Otaku Culture","value":"otaku-culture"},{"name":"Medical","value":"medical"},{"name":"Survival","value":"survival"},{"name":"Reincarnation","value":"reincarnation"},{"name":"Showbiz","value":"showbiz"}, {"name":"Team Sports","value":"team-sports"},{"name":"Mahou Shoujo","value":"mahou-shoujo"},{"name":"Combat Sports","value":"combat-sports"},{"name":"Crossdressing","value":"crossdressing"}, {"name":"Visual Arts","value":"visual-arts"},{"name":"Childcare","value":"childcare"},{"name":"Pets","value":"pets"},{"name":"Love Status Quo","value":"love-status-quo"},{"name":"Urban Fantasy","value":"urban-fantasy"}, {"name":"Villainess","value":"villainess"} ], "type":[ {"name":"TV","value":"tv"},{"name":"Movie","value":"movie"},{"name":"OVA","value":"ova"},{"name":"ONA","value":"ona"},{"name":"Special","value":"special"},{"name":"Music","value":"music"} ], "demographic":[ {"name":"Shounen","value":"shounen"},{"name":"Shoujo","value":"shoujo"},{"name":"Seinen","value":"seinen"},{"name":"Kids","value":"kids"},{"name":"Josei","value":"josei"} ], "":[ {"value":"airing"},{"value":"completed"} ] }; const filterRules = { genre: "and", theme: "and", demographic: "or", type: "or", season: "or", "": "or" }; function getFilterParts(filter) { const regex = /^(?:([\w\-]+)(?:\/))?([\w\-\.]+)$/.exec(filter); return { type: regex[1] || '', value: regex[2] }; } function buildFilterString(type, value) { return (type === '' ? type : type + '/') + value; } const seasonFilterRegex = /^season\/(spring|summer|winter|fall)-\d{4}\.\.(spring|summer|winter|fall)-\d{4}$/; const noneFilterRegex = /^([\w\d\-]+\/)?none$/; function getFilteredList(filtersInput, filterTotal = 0) { let filterNum = 0; function getPage(pageUrl) { return new Promise((resolve, reject) => { const cached = filterSearchCache[pageUrl]; if (cached !== undefined) { // If cache exists if (cached === 'invalid') { resolve(undefined); return; } resolve(cached); return; } const req = new XMLHttpRequest(); req.open('GET', pageUrl, true); try { req.send(); } catch (err) { console.error(err); reject('A network error occured.'); return; } req.onload = () => { if (req.status !== 200) { resolve(undefined); return; } const animeList = getAnimeList($(req.response)); filterSearchCache[pageUrl] = animeList; resolve(animeList); }; }); } function getLists(filters) { const lists = []; return new Promise((resolve, reject) => { function check() { if (filters.length > 0) { repeat(filters.shift()); } else { resolve(lists); } } function repeat(filter) { const filterType = getFilterParts(filter).type; if (noneFilterRegex.test(filter)) { getLists(filterValues[filterType].map(a => buildFilterString(filterType, a.value))).then((filtered) => { getPage('/anime').then((unfiltered) => { const none = []; for (const entry of unfiltered) { if (filtered.find(list => list.entries.find(a => a.name === entry.name)) !== undefined) continue; none.push(entry); } lists.push({ type: filterType, entries: none }); check(); }); }); return; } getPage('/anime/' + filter).then((result) => { if (result !== undefined) { lists.push({ type: filterType, entries: result }); } if (filterTotal > 0) { filterNum++; $($('.anitracker-filter-spinner>span')[0]).text(Math.floor((filterNum/filterTotal) * 100).toString() + '%'); } check(); }); } check(); }); } return new Promise((resolve, reject) => { const filters = JSON.parse(JSON.stringify(filtersInput)); if (filters.length === 0) { getPage('/anime').then((response) => { if (response === undefined) { alert('Page loading failed.'); reject('Anime index page not reachable.'); return; } resolve(response); }); return; } const seasonFilter = filters.find(a => seasonFilterRegex.test(a)); if (seasonFilter !== undefined) { filters.splice(filters.indexOf(seasonFilter), 1); const range = getFilterParts(seasonFilter).value.split('..'); filters.push(...getSeasonTimeframe({ year: +range[0].split('-')[1], season: getSeasonValue(range[0].split('-')[0]) }, { year: +range[1].split('-')[1], season: getSeasonValue(range[1].split('-')[0]) })); } getLists(filters).then((listsInput) => { const lists = JSON.parse(JSON.stringify(listsInput)); const types = {}; for (const list of lists) { if (types[list.type]) continue; types[list.type] = list.entries; } lists.splice(0, 1); for (const list of lists) { const entries = list.entries; if (filterRules[list.type] === 'and') { const matches = []; for (const anime of types[list.type]) { if (entries.find(a => a.name === anime.name) === undefined) continue; matches.push(anime); } types[list.type] = matches; } else if (filterRules[list.type] === 'or') { for (const anime of list.entries) { if (types[list.type].find(a => a.name === anime.name) !== undefined) continue; types[list.type].push(anime); } } } const listOfTypes = Array.from(Object.values(types)); let finalList = listOfTypes[0]; listOfTypes.splice(0,1); for (const type of listOfTypes) { const matches = []; for (const anime of type) { if (finalList.find(a => a.name === anime.name) === undefined) continue; matches.push(anime); } finalList = matches; } resolve(finalList); }); }); } function searchList(fuseClass, list, query, limit = 80) { const fuse = new fuseClass(list, { keys: ['name'], findAllMatches: true }); const matching = fuse.search(query); return matching.map(a => {return a.item}).splice(0,limit); } function timeSince(date) { var seconds = Math.floor((new Date() - date) / 1000); var interval = Math.floor(seconds / 31536000); if (interval >= 1) { return interval + " year" + (interval > 1 ? 's' : ''); } interval = Math.floor(seconds / 2592000); if (interval >= 1) { return interval + " month" + (interval > 1 ? 's' : ''); } interval = Math.floor(seconds / 86400); if (interval >= 1) { return interval + " day" + (interval > 1 ? 's' : ''); } interval = Math.floor(seconds / 3600); if (interval >= 1) { return interval + " hour" + (interval > 1 ? 's' : ''); } interval = Math.floor(seconds / 60); if (interval >= 1) { return interval + " minute" + (interval > 1 ? 's' : ''); } return seconds + " second" + (seconds > 1 ? 's' : ''); } if (window.location.pathname.startsWith('/customlink')) { const parts = { animeSession: '', episodeSession: '', time: -1 }; const entries = Array.from(new URLSearchParams(window.location.search).entries()).sort((a,b) => a[0] > b[0] ? 1 : -1); for (const entry of entries) { if (entry[0] === 'a') { parts.animeSession = getAnimeData(decodeURIComponent(entry[1])).session; continue; } if (entry[0] === 'e') { if (parts.animeSession === '') return; parts.episodeSession = getEpisodeSession(parts.animeSession, +entry[1]); continue; } if (entry[0] === 't') { if (parts.animeSession === '') return; if (parts.episodeSession === '') continue; parts.time = +entry[1]; continue; } } const destination = (() => { if (parts.animeSession !== '' && parts.episodeSession === '' && parts.time === -1) { return '/anime/' + parts.animeSession + '?ref=customlink'; } if (parts.animeSession !== '' && parts.episodeSession !== '' && parts.time === -1) { return '/play/' + parts.animeSession + '/' + parts.episodeSession + '?ref=customlink'; } if (parts.animeSession !== '' && parts.episodeSession !== '' && parts.time >= 0) { return '/play/' + parts.animeSession + '/' + parts.episodeSession + '?time=' + parts.time + '&ref=customlink'; } return undefined; })(); if (destination !== undefined) { document.title = "Redirecting... :: animepahe"; $('h1').text('Redirecting...'); window.location.replace(destination); } return; } // Main key events if (!is404) $(document).on('keydown', (e) => { if ($(e.target).is(':input')) return; if (modalIsOpen() && ['Escape','Backspace'].includes(e.key)) { modalCloseFunction(); return; } if (!isEpisode() || modalIsOpen()) return; if (e.key === 't') { toggleTheatreMode(); } else { sendMessage({action:"key",key:e.key}); $('.embed-responsive-item')[0].contentWindow.focus(); if ([" "].includes(e.key)) e.preventDefault(); } }); if (window.location.pathname.startsWith('/queue')) { $(`    (Incoming episodes) `).appendTo('h2'); } if (/^\/anime\/\w+(\/[\w\-\.]+)?$/.test(window.location.pathname)) { if (is404) return; const filter = /\/anime\/([^\/]+)\/?([^\/]+)?/.exec(window.location.pathname); if (filter[2] !== undefined) { if (filterRules[filter[1]] === undefined) return; if (filter[1] === 'season') { window.location.replace(`/anime?${filter[1]}=${filter[2]}..${filter[2]}`); return; } window.location.replace(`/anime?${filter[1]}=${filter[2]}`); } else { window.location.replace(`/anime?other=${filter[1]}`); } return; } function getDayName(day) { return [ "Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday" ][day]; } function toHtmlCodes(string) { return $('
    ').text(string).html().replace(/"/g, """).replace(/'/g, "'"); } // Bookmark & episode feed header buttons $(`
    `).insertAfter('.navbar-nav'); let currentNotificationIndex = 0; function openNotificationsModal() { currentNotificationIndex = 0; const oldStorage = getStorage(); $('#anitracker-modal-body').empty(); $(`

    Episode Feed

    Loading...
    `).appendTo('#anitracker-modal-body'); $('.anitracker-view-notif-animes').on('click', () => { $('#anitracker-modal-body').empty(); const storage = getStorage(); $(`

    Handle Episode Feed

    `).appendTo('#anitracker-modal-body'); [...storage.notifications.anime].sort((a,b) => a.latest_episode > b.latest_episode ? 1 : -1).forEach(g => { const latestEp = new Date(g.latest_episode + " UTC"); const latestEpString = g.latest_episode !== undefined ? `${getDayName(latestEp.getDay())} ${latestEp.toLocaleTimeString()}` : "None found"; $(`
    ${g.name}
    Latest episode: ${latestEpString}
    `).appendTo('#anitracker-modal-body .anitracker-modal-list'); }); if (storage.notifications.anime.length === 0) { $("Use the button on an ongoing anime to add it to the feed.").appendTo('#anitracker-modal-body .anitracker-modal-list'); } $('.anitracker-modal-list-entry .anitracker-get-all-button').on('click', (e) => { const elem = $(e.currentTarget); const id = +elem.parents().eq(1).attr('animeid'); const storage = getStorage(); const found = storage.notifications.anime.find(a => a.id === id); if (found === undefined) { console.error("[AnimePahe Improvements] Couldn't find feed for anime with id " + id); return; } found.hasFirstEpisode = true; found.updateFrom = 0; saveData(storage); elem.replaceClass("btn-secondary", "btn-primary"); setTimeout(() => { elem.replaceClass("btn-primary", "btn-secondary"); elem.prop('disabled', true); }, 200); }); $('.anitracker-modal-list-entry .anitracker-delete-button').on('click', (e) => { const parent = $(e.currentTarget).parents().eq(1); const name = parent.attr('animename'); toggleNotifications(name, +parent.attr('animeid')); const name2 = getAnimeName(); if (name2.length > 0 && name2 === name) { $('.anitracker-notifications-toggle .anitracker-title-icon-check').hide(); } parent.remove(); }); openModal(openNotificationsModal); }); const animeData = []; const queue = [...oldStorage.notifications.anime]; openModal().then(() => { if (queue.length > 0) next(); else done(); }); async function next() { if (queue.length === 0) done(); const anime = queue.shift(); const data = await updateNotifications(anime.name); if (data === -1) { $("An error occured.").appendTo('#anitracker-modal-body .anitracker-modal-list'); return; } animeData.push({ id: anime.id, data: data }); if (queue.length > 0 && $('#anitracker-notifications-list-spinner').length > 0) next(); else done(); } function done() { if ($('#anitracker-notifications-list-spinner').length === 0) return; const storage = getStorage(); let removedAnime = 0; for (const anime of storage.notifications.anime) { if (anime.latest_episode === undefined || anime.dont_ask === true) continue; const time = Date.now() - new Date(anime.latest_episode + " UTC").getTime(); if ((time / 1000 / 60 / 60 / 24 / 7) > 2) { const remove = confirm(`[AnimePahe Improvements]\n\nThe latest episode for ${anime.name} was more than 2 weeks ago. Remove it from the feed?\n\nThis prompt will not be shown again.`); if (remove === true) { toggleNotifications(anime.name, anime.id); removedAnime++; } else { anime.dont_ask = true; saveData(storage); } } } if (removedAnime > 0) { openNotificationsModal(); return; } $('#anitracker-notifications-list-spinner').remove(); storage.notifications.episodes.sort((a,b) => a.time < b.time ? 1 : -1); storage.notifications.lastUpdated = Date.now(); saveData(storage); if (storage.notifications.episodes.length === 0) { $("Nothing here yet!").appendTo('#anitracker-modal-body .anitracker-modal-list'); } else addToList(20); } function addToList(num) { const storage = getStorage(); const index = currentNotificationIndex; for (let i = currentNotificationIndex; i < storage.notifications.episodes.length; i++) { const ep = storage.notifications.episodes[i]; if (ep === undefined) break; currentNotificationIndex++; const data = animeData.find(a => a.id === ep.animeId)?.data; if (data === undefined) { console.error(`[AnimePahe Improvements] Could not find corresponding anime "${ep.animeName}" with ID ${ep.animeId} (episode ${ep.episode})`); continue; } const releaseTime = new Date(ep.time + " UTC"); $(`
    [Thumbnail of ${toHtmlCodes(data.title)}]
    ${data.title}
    Episode ${ep.episode}
    ${timeSince(releaseTime)} ago (${releaseTime.toLocaleDateString()})
    `).appendTo('#anitracker-modal-body .anitracker-modal-list'); if (i > index+num-1) break; } $('.anitracker-notification-item.anitracker-temp').on('click', (e) => { $(e.currentTarget).find('a').blur(); }); $('.anitracker-notification-item.anitracker-temp .anitracker-watched-toggle').on('click keydown', (e) => { if (e.type === 'keydown' && e.key !== "Enter") return; e.preventDefault(); const storage = getStorage(); const elem = $(e.currentTarget); const parent = elem.parents().eq(1); const ep = storage.notifications.episodes.find(a => a.animeId === +parent.attr('anime-data') && a.episode === +parent.attr('episode-data')); if (ep === undefined) { console.error("[AnimePahe Improvements] couldn't mark episode as watched/unwatched"); return; } parent.toggleClass('anitracker-notification-item-unwatched'); elem.toggleClass('fa-eye').toggleClass('fa-eye-slash'); if (e.type === 'click') elem.blur(); ep.watched = !ep.watched; elem.attr('title', `Mark this episode as ${ep.watched ? 'unwatched' : 'watched'}`); saveData(storage); }); $('.anitracker-notification-item.anitracker-temp').removeClass('anitracker-temp'); } $('#anitracker-modal-body').on('scroll', () => { const elem = $('#anitracker-modal-body'); if (elem.scrollTop() >= elem[0].scrollTopMax) { if ($('.anitracker-view-notif-animes').length === 0) return; addToList(20); } }); } $('.anitracker-header-notifications').on('click', openNotificationsModal); $('.anitracker-header-bookmark').on('click', () => { $('#anitracker-modal-body').empty(); const storage = getStorage(); $(`

    Bookmarks

    `).appendTo('#anitracker-modal-body'); $('.anitracker-modal-search').on('input', (e) => { setTimeout(() => { const query = $(e.target).val(); for (const entry of $('.anitracker-modal-list-entry')) { if ($($(entry).find('a,span')[0]).text().toLowerCase().includes(query)) { $(entry).show(); continue; } $(entry).hide(); } }, 10); }); function applyDeleteEvents() { $('.anitracker-modal-list-entry button').on('click', (e) => { const id = $(e.currentTarget).parent().attr('animeid'); toggleBookmark(id); const data = getAnimeData(); if (data !== undefined && data.id === +id) { $('.anitracker-bookmark-toggle .anitracker-title-icon-check').hide(); } $(e.currentTarget).parent().remove(); }); } // When clicking the reverse order button $('.anitracker-reverse-order-button').on('click', (e) => { const btn = $(e.target); if (btn.attr('dir') === 'down') { btn.attr('dir', 'up'); btn.addClass('anitracker-up'); } else { btn.attr('dir', 'down'); btn.removeClass('anitracker-up'); } const entries = []; for (const entry of $('.anitracker-modal-list-entry')) { entries.push(entry.outerHTML); } entries.reverse(); $('.anitracker-modal-list-entry').remove(); for (const entry of entries) { $(entry).appendTo($('.anitracker-modal-list')); } applyDeleteEvents(); }); [...storage.bookmarks].reverse().forEach(g => { $(`
    ${g.name}
    `).appendTo('#anitracker-modal-body .anitracker-modal-list') }); if (storage.bookmarks.length === 0) { $(`No bookmarks yet!`).appendTo('#anitracker-modal-body .anitracker-modal-list'); } applyDeleteEvents(); openModal(); $('#anitracker-modal-body')[0].scrollTop = 0; }); function toggleBookmark(id, name=undefined) { const storage = getStorage(); const found = storage.bookmarks.find(g => g.id === +id); if (found !== undefined) { const index = storage.bookmarks.indexOf(found); storage.bookmarks.splice(index, 1); saveData(storage); return false; } if (name === undefined) return false; storage.bookmarks.push({ id: +id, name: name }); saveData(storage); return true; } function toggleNotifications(name, id = undefined) { const storage = getStorage(); const found = (() => { if (id !== undefined) return storage.notifications.anime.find(g => g.id === id); else return storage.notifications.anime.find(g => g.name === name); })(); if (found !== undefined) { const index = storage.notifications.anime.indexOf(found); storage.notifications.anime.splice(index, 1); storage.notifications.episodes = storage.notifications.episodes.filter(a => a.animeName !== found.name); // Uses the name, because old data might not be updated to use IDs saveData(storage); return false; } const animeData = getAnimeData(name); storage.notifications.anime.push({ name: name, id: animeData.id }); saveData(storage); return true; } async function updateNotifications(animeName, storage = getStorage()) { const nobj = storage.notifications.anime.find(g => g.name === animeName); if (nobj === undefined) { toggleNotifications(animeName); return; } const data = await asyncGetAnimeData(animeName, nobj.id); if (data === undefined) return -1; const episodes = await asyncGetAllEpisodes(data.session, 'desc'); if (episodes === undefined) return 0; return new Promise((resolve, reject) => { if (episodes.length === 0) resolve(undefined); nobj.latest_episode = episodes[0].created_at; if (nobj.name !== data.title) { for (const ep of storage.notifications.episodes) { if (ep.animeName !== nobj.name) continue; ep.animeName = data.title; } nobj.name = data.title; } const compareUpdateTime = nobj.updateFrom ?? storage.notifications.lastUpdated; if (nobj.updateFrom !== undefined) delete nobj.updateFrom; for (const ep of episodes) { const found = storage.notifications.episodes.find(a => a.episode === ep.episode && a.animeId === nobj.id) ?? storage.notifications.episodes.find(a => a.episode === ep.episode && a.animeName === data.title); if (found !== undefined) { found.session = ep.session; if (found.animeId === undefined) found.animeId = nobj.id; if (episodes.indexOf(ep) === episodes.length - 1) nobj.hasFirstEpisode = true; continue; } if (new Date(ep.created_at + " UTC").getTime() < compareUpdateTime) { continue; } storage.notifications.episodes.push({ animeName: nobj.name, animeId: nobj.id, session: ep.session, episode: ep.episode, time: ep.created_at, watched: false }); } const length = storage.notifications.episodes.length; if (length > 100) { storage.notifications.episodes = storage.notifications.episodes.slice(length - 100); } saveData(storage); resolve(data); }); } const paramArray = Array.from(new URLSearchParams(window.location.search)); const refArg01 = paramArray.find(a => a[0] === 'ref'); if (refArg01 !== undefined) { const ref = refArg01[1]; if (ref === '404') { alert('[AnimePahe Improvements]\n\nThe session was outdated, and has been refreshed. Please try that link again.'); } else if (ref === 'customlink' && isEpisode() && initialStorage.autoDelete) { const name = getAnimeName(); const num = getEpisodeNum(); if (initialStorage.linkList.find(e => e.animeName === name && e.type === 'episode' && e.episodeNum !== num)) { // If another episode is already stored $(` The current episode data for this anime was not replaced due to coming from a share link.
    Refresh this page to replace it.
    Dismiss
    `).prependTo('.content-wrapper'); $('.anitracker-from-share-warning>span').on('click keydown', function(e) { if (e.type === 'keydown' && e.key !== "Enter") return; $(e.target).parent().remove(); }); } } window.history.replaceState({}, document.title, window.location.origin + window.location.pathname); } function getCurrentSeason() { const month = new Date().getMonth(); return Math.trunc(month/3); } // Search/index page if (/^\/anime\/?$/.test(window.location.pathname)) { $(`
    Genre
    Theme
    Type (or)
    Demographic (or)
    `).insertBefore('.index'); $('.anitracker-items-box-search').on('focus click', (e) => { showDropdown(e.currentTarget); }); function showDropdown(elem) { $('.anitracker-dropdown-content').css('display', ''); const dropdown = $(`#anitracker-${$(elem).closest('.anitracker-items-box').attr('dropdown')}-dropdown`); dropdown.show(); dropdown.css('position', 'absolute'); const pos = $(elem).closest('.anitracker-items-box-search').position(); dropdown.css('left', pos.left); dropdown.css('top', pos.top + 40); } $('.anitracker-items-box-search').on('blur', (e) => { setTimeout(() => { const dropdown = $(`#anitracker-${$(e.target).parents().eq(1).attr('dropdown')}-dropdown`); if (dropdown.is(':active') || dropdown.is(':focus')) return; dropdown.hide(); }, 10); }); $('.anitracker-items-box-search').on('keydown', (e) => { setTimeout(() => { const targ =$(e.target); const type = targ.parents().eq(1).attr('dropdown'); const dropdown = $(`#anitracker-${type}-dropdown`); for (const icon of targ.find('.anitracker-filter-icon')) { (() => { if ($(icon).text() === $(icon).data('name')) return; const filter = $(icon).data('filter'); $(icon).remove(); for (const active of dropdown.find('.anitracker-active')) { if ($(active).attr('ref') !== filter) continue; removeFilter(filter, targ, $(active)); return; } removeFilter(filter, targ, undefined); })(); } if (dropdown.find('.anitracker-active').length > targ.find('.anitracker-filter-icon').length) { const filters = []; for (const icon of targ.find('.anitracker-filter-icon')) { filters.push($(icon).data('filter')); } let removedFilter = false; for (const active of dropdown.find('.anitracker-active')) { if (filters.includes($(active).attr('ref'))) continue; removedFilter = true; removeFilter($(active).attr('ref'), targ, $(active), false); } if (removedFilter) refreshSearchPage(appliedFilters); } for (const filter of appliedFilters) { // Special case for non-default filters (() => { const parts = getFilterParts(filter); if (parts.type !== type || filterValues[parts.type].includes(parts.value)) return; for (const icon of targ.find('.anitracker-filter-icon')) { if ($(icon).data('filter') === filter) return; } appliedFilters.splice(appliedFilters.indexOf(filter), 1); refreshSearchPage(appliedFilters); })(); } targ.find('br').remove(); updateFilterBox(targ[0]); }, 10); }); function setIconEvent(elem) { $(elem).on('click', (e) => { const targ = $(e.target); for (const btn of $(`#anitracker-${targ.closest('.anitracker-items-box').attr('dropdown')}-dropdown button`)) { if ($(btn).attr('ref') !== targ.data('filter')) continue; removeFilter(targ.data('filter'), targ.parent(), btn); return; } removeFilter(targ.data('filter'), targ.parent(), undefined); }); } function updateFilterBox(elem) { const targ = $(elem); for (const icon of targ.find('.anitracker-filter-icon')) { if (appliedFilters.includes($(icon).data('filter'))) continue; $(icon).remove(); } if (appliedFilters.length === 0) { for (const input of targ.find('.anitracker-text-input')) { if ($(input).text().trim() !== '') continue; $(input).text(''); } } const text = getFilterBoxText(targ[0]).trim(); const dropdownBtns = $(`#anitracker-${targ.parents().eq(1).attr('dropdown')}-dropdown button`); dropdownBtns.show(); if (text !== '') { for (const btn of dropdownBtns) { if ($(btn).text().toLowerCase().includes(text.toLowerCase())) continue; $(btn).hide(); } } if (targ.text().trim() === '') { targ.text(''); targ.parent().find('.placeholder').show(); return; } targ.parent().find('.placeholder').hide(); } function getFilterBoxText(elem) { const basicText = $(elem).contents().filter(function(){return this.nodeType == Node.TEXT_NODE;})[0] const spanText = $($(elem).find('.anitracker-text-input')[$(elem).find('.anitracker-text-input').length-1]).text() + ''; if (basicText === undefined) return spanText; return (basicText.nodeValue + spanText).trim(); } $('.anitracker-items-box>button').on('click', (e) => { const targ = $(e.target); const newRule = targ.text() === 'and' ? 'or' : 'and'; const type = targ.parent().attr('dropdown'); filterRules[type] = newRule; targ.text(newRule); const filterBox = targ.parent().find('.anitracker-items-box-search'); filterBox.focus(); const filterList = appliedFilters.filter(a => a.startsWith(type + '/')); if (newRule === 'and' && filterList.length > 1 && filterList.find(a => a.startsWith(type + '/none')) !== undefined) { for (const btn of $(`#anitracker-${type}-dropdown button`)) { if ($(btn).attr('ref') !== type + '/none' ) continue; removeFilter(type + '/none', filterBox, btn, false); break; } } if (filterList.length > 0) refreshSearchPage(appliedFilters); }); const animeList = getAnimeList(); $(` Filter results: ${animeList.length} `).insertAfter('#anitracker'); $('#anitracker-random-anime').on('click', function() { const storage = getStorage(); storage.cache = filterSearchCache; saveData(storage); const params = getParams(appliedFilters, $('.anitracker-items-box>button')); if ($('#anitracker-anime-list-search').length > 0 && $('#anitracker-anime-list-search').val() !== '') { $.getScript('https://cdn.jsdelivr.net/npm/fuse.js@7.0.0', function() { const query = $('#anitracker-anime-list-search').val(); getRandomAnime(searchList(Fuse, animeList, query), (params === '' ? '?anitracker-random=1' : params + '&anitracker-random=1') + '&search=' + encodeURIComponent(query)); }); } else { getRandomAnime(animeList, params === '' ? '?anitracker-random=1' : params + '&anitracker-random=1'); } }); function getDropdownButtons(filters, type) { return filters.sort((a,b) => a.name > b.name ? 1 : -1).map(g => $(``)); } $(`