Skip to content

Instantly share code, notes, and snippets.

@Ellivers
Last active September 2, 2024 00:49
Show Gist options
  • Save Ellivers/f7716b6b6895802058c367963f3a2c51 to your computer and use it in GitHub Desktop.
Save Ellivers/f7716b6b6895802058c367963f3a2c51 to your computer and use it in GitHub Desktop.
AnimePahe Improvements
// ==UserScript==
// @name AnimePahe Improvements
// @namespace https://gist.github.com/Ellivers/f7716b6b6895802058c367963f3a2c51
// @downloadURL https://gist.github.com/Ellivers/f7716b6b6895802058c367963f3a2c51/raw/anime-tracker.user.js
// @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.16
// @author Ellivers
// @description 2022-06-06, 18:04:32
// ==/UserScript==
/*
How to install:
* Get the Violentmonkey browser extension and then click the "Raw" button on this page
* 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 gradually.
* 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 "Autoplay" 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 {
linkList:[],
videoTimes:[],
bookmarks:[],
notifications: {
lastUpdated: Date.now(),
anime: [],
episodes: []
},
badCovers: [],
autoDelete:true,
hideThumbnails:false,
theatreMode:false,
bestQuality:true,
autoDownload:true,
autoPlay:false
};
}
function getStorage() {
const defa = getDefaultData();
const res = GM_getValue('anime-link-tracker', defa);
for (const key of Object.keys(defa)) {
if (res[key] !== undefined) continue;
res[key] = defa[key];
}
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) {
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;
$(`
<div style="width:100%;height:100%;background-color:rgba(0, 0, 0, 0.9);position:fixed;z-index:999;display:flex;justify-content:center;align-items:center;" id="anitrackerKwikDL">
<span style="color:white;font-size:3.5em;font-weight:bold;">[AnimePahe Improvements] Downloading...</span>
</div>`).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;
}
$(`
<div class="anitracker-loading plyr__control--overlaid" style="opacity: 1; border-radius: 10%;">
<span>Loading...</span>
</div>`).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]
};
}
function updateTime() {
const currentTime = player.currentTime;
const storage = getStorage();
if (player.duration - currentTime <= 20) {
const videoInfo = getVideoInfo();
for (const videoData of storage.videoTimes) {
if (videoData.animeName === videoInfo.animeName && videoData.episodeNum === videoInfo.episodeNum) {
const index = storage.videoTimes.indexOf(videoData);
storage.videoTimes.splice(index, 1);
}
}
saveData(storage);
return;
}
const vidInfo = getVideoInfo();
const storedVideoTime = getStoredTime(vidInfo.animeName, vidInfo.episodeNum, storage);
if (storedVideoTime === undefined) {
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;
saveData(storage);
}
if (initialStorage.videoTimes === undefined) {
initialStorage.videoTimes = [];
saveData(initialStorage);
}
// Messages received from main page
window.onmessage = function(e) {
const data = e.data;
const action = data.action;
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);
}
}
else {
storage.videoTimes.push({
videoUrls: [url],
time: 0,
animeName: getVideoInfo().animeName,
episodeNum: getVideoInfo().episodeNum
});
if (storage.videoTimes.length > 1000) {
storage.splice(0,1);
}
saveData(storage);
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);
});
function removeLoadingIndicators() {
$('.anitracker-loading').remove();
$('button.plyr__controls__item:nth-child(1)').show();
$('.plyr__progress__container').show();
}
player.addEventListener('timeupdate', function() {
if (player.currentTime % 10 < 0.5) {
updateTime();
}
});
player.addEventListener('pause', () => {
if (player.readyState <= 2) return;
updateTime();
});
player.addEventListener('seeked', () => {
updateTime();
removeLoadingIndicators();
});
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)) {
if (player.loop) {
showMessage('Loop: Off');
}
else {
showMessage('Loop: On');
}
player.loop = !player.loop;
return;
}
if (e.shiftKey && ['n','N'].includes(key)) {
window.top.postMessage({
action: "next"
}, '*');
return;
}
if (e.shiftKey && ['p','P'].includes(key)) {
window.top.postMessage({
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 = "";
}
window.top.postMessage({
action: "key",
key: key
}, '*');
});
// Ctrl+scrolling to change speed
$(`
<div class="anitracker-message" style="width:50%;height:10%;position:absolute;background-color:rgba(0,0,0,0.5);display:none;justify-content:center;align-items:center;margin-top:1.5%;border-radius:20px;">
<span style="color: white;font-size: 2.5em;">2.0x</span>
</div>`).appendTo($(player).parent().parent());
jQuery.event.special.wheel = {
setup: function( _, ns, handle ){
this.addEventListener("wheel", handle, { passive: false });
}
};
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;
})();
const defaultSpeeds = player.plyr.options.speed;
function changeSpeed(e, delta) {
if (!e.ctrlKey) return;
if (delta == 0) return;
const speedChange = e.shiftKey ? 0.05 : 0.1;
player.playbackRate += speedChange * (delta > 0 ? -1 : 1);
player.playbackRate = Math.round(player.playbackRate * 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');
$(`
<button type="button" role="menuitemradio" class="plyr__control anitracker-custom-speed-btn" aria-checked="true"><span>Custom</span></button>
`).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";
}
}
e.preventDefault();
}
$(`#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) {
console.log('[AnimePahe Improvements]', initialStorage);
if (initialStorage.hideThumbnails === true) {
hideThumbnails();
}
function windowOpen(url, target = '_blank') {
$(`<a href="${url}" target="${target}"></a>`)[0].click();
}
// -------- AnimePahe Improvements CSS ---------
$('style').remove(); // Removes a pre-existing style sheet that only has 2 rules
$("head").append('<style id="anitracker-style" type="text/css"></style>');
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: 666;
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:999;
}
#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 img {display: none;\n}
.anitracker-hide.anitracker-thumbnail {
border: 10px solid rgb(32, 32, 32);
aspect-ratio: 16/9;
}
.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 {
width: 24px;
height: 24px;
float: right;
margin-top: 6px;
margin-right: 8px;
}
.anitracker-modal-list-container {
background-color: rgb(40,40,40);
margin-bottom: 10px;
border-radius: 12px;
}
.anitracker-storage-data {
background-color: var(--gray);
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 h4 {display:inline-block;\n}
.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: -999;
}
.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-reverse-order-button {
height: 42px;
border-radius: 7px!important;
font-size: 2em;
color: #ddd!important;
margin-left: 10px!important;
}
.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;
opacity: .8;
color: #ff006c!important;
font-size: 2rem;
vertical-align: middle;
cursor: pointer;
padding: 0;
box-shadow: none!important;
}
.anitracker-title-icon:hover {
opacity: 1;
}
.anitracker-title-icon-check {
color: white;
margin-left: -.7rem;
font-size: 1rem;
vertical-align: super;
text-shadow: none;
}
.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;
}
@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: () => {
$('.anitracker-hide').removeClass('anitracker-hide');
$('#anitracker-hide-style').remove();
}
},
{
optionId: 'bestQuality',
switchId: 'best-quality',
value: initialStorage.bestQuality,
onEvent: bestVideoQuality
},
{
optionId: 'autoDownload',
switchId: 'auto-download',
value: initialStorage.autoDownload
},
{
optionId: 'autoPlay',
switchId: 'autoplay',
value: initialStorage.autoPlay
}];
// Things that update when focusing this tab
$(document).on('visibilitychange', () => {
if (document.hidden) return;
updateSwitches();
const storage = getStorage();
const data = (url.includes('/anime/') || isEpisode()) ? getAnimeData() : undefined;
if (data !== undefined) {
if (storage.bookmarks.find(a => a.id === data.id) === undefined) {
$('.anitracker-bookmark-toggle .anitracker-title-icon-check').hide();
}
if (storage.notifications.anime.find(a => a.id === data.id) === undefined) {
$('.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.animeName === $(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.removeClass('fa-eye');
eye.addClass('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() {
$(`
<div id="anitracker-modal" tabindex="-1">
<div id="anitracker-modal-content">
<i id="anitracker-modal-close" class="fa fa-close" title="Close modal">
</i>
<div id="anitracker-modal-body"></div>
</div>
</div>`).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').removeClass('fa-close');
$('#anitracker-modal-close').addClass('fa-arrow-left');
}
else {
$('#anitracker-modal-close').removeClass('fa-arrow-left');
$('#anitracker-modal-close').addClass('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() {
playAnimation($('#anitracker-modal'), 'fadeIn', 'reverse', 0.1).then(() => {
$('#anitracker-modal').hide();
});
}
function modalIsOpen() {
return $('#anitracker-modal').is(':visible');
}
// Main key events
$(document).on('keydown', (e) => {
if ($(e.target).is(':input')) return;
if (modalIsOpen() && e.key === 'Escape') {
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();
}
});
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 === 'key') {
if (data.key === 't') {
toggleTheatreMode();
}
}
else if (data === 'ended') {
const storage = getStorage();
if (storage.autoPlay !== 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 ({spring:0, summer:1, fall:2, winter:3})[season.toLowerCase()];
}
function getSeasonName(season) {
return ["spring","summer","fall","winter"][season];
}
function stringSimilarity(s1, s2) {
var longer = s1;
var shorter = s2;
if (s1.length < s2.length) {
longer = s2;
shorter = s1;
}
var 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();
var costs = new Array();
for (var i = 0; i <= s1.length; i++) {
var lastValue = i;
for (var j = 0; j <= s2.length; j++) {
if (i == 0)
costs[j] = j;
else {
if (j > 0) {
var 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) {
$(`
<li class="anitracker-collection" data-index="-1">
<a title="${toHtmlCodes(baseName + " - Collection")}" href="javascript:;">
<img src="${seriesList[0].poster.slice(0, -3) + 'th.jpg'}" referrerpolicy="no-referrer" style="pointer-events: all !important;max-width: 30px;">
<img src="${seriesList[1].poster.slice(0, -3) + 'th.jpg'}" referrerpolicy="no-referrer" style="pointer-events: all !important;max-width: 30px;left:30px;">
<div class="result-title">${baseName}</div>
<div class="result-status"><strong>Collection</strong> - ${seriesList.length} Entries</div>
</a>
</li>`).prependTo('.search-results');
function displayInModal() {
$('#anitracker-modal-body').empty();
$(`
<h4>Collection</h4>
<div class="anitracker-modal-list-container">
<div class="anitracker-modal-list" style="min-height: 100px;min-width: 200px;"></div>
</div>`).appendTo('#anitracker-modal-body');
for (const anime of seriesList) {
$(`
<div class="anitracker-big-list-item anitracker-collection-item">
<a href="/anime/${anime.session}" title="${toHtmlCodes(anime.title)}">
<img src="${anime.poster.slice(0, -3) + 'th.jpg'}" referrerpolicy="no-referrer" alt="[Thumbnail of ${anime.title}]">
<div class="anitracker-main-text">${anime.title}</div>
<div class="anitracker-subtext"><strong>${anime.type}</strong> - ${anime.episodes > 0 ? anime.episodes : '?'} Episode${anime.episodes === 1 ? '' : 's'} (${anime.status})</div>
<div class="anitracker-subtext">${anime.season} ${anime.year}</div>
</a>
</div>`).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');
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"}
],
"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 (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']
});
const matching = fuse.search(query);
const matches = [];
for (let i = 0; i < matching.length; i++) {
if (i >= limit) break;
const match = matching[i];
matches.push(match.item);
}
return matches;
}
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;
}
if (window.location.pathname.startsWith('/queue')) {
$(`
<span style="font-size:.6em;">&nbsp;&nbsp;&nbsp;(Incoming episodes)</span>
`).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 $('<div>').text(string).html().replace(/"/g, "&quot;").replace(/'/g, "&#039;");
}
// Bookmark & episode feed header buttons
$(`
<div style="margin-right:1%;display:flex;justify-content:space-evenly;flex-grow:0.1;">
<button class="anitracker-header-notifications anitracker-header-button" title="View episode feed">
<i class="fa fa-bell" aria-hidden="true"></i>
<i style="display:none;" aria-hidden="true" class="fa fa-circle anitracker-header-notifications-circle"></i>
</button>
<button class="anitracker-header-bookmark anitracker-header-button" title="View bookmarks"><i class="fa fa-bookmark" aria-hidden="true"></i></button>
</div>`).insertAfter('.navbar-nav');
let currentNotificationIndex = 0;
function openNotificationsModal() {
currentNotificationIndex = 0;
const oldStorage = getStorage();
$('#anitracker-modal-body').empty();
$(`
<h4>Episode Feed</h4>
<div class="btn-group" style="margin-bottom: 10px;">
<button class="btn btn-secondary anitracker-view-notif-animes">
Handle Feed...
</button>
</div>
<div class="anitracker-modal-list-container">
<div class="anitracker-modal-list" style="min-height: 100px;min-width: 200px;">
<div id="anitracker-notifications-list-spinner" style="display:flex;justify-content:center;">
<div class="spinner-border text-danger" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
</div>
</div>`).appendTo('#anitracker-modal-body');
$('.anitracker-view-notif-animes').on('click', () => {
$('#anitracker-modal-body').empty();
const storage = getStorage();
$(`
<h4>Handle Episode Feed</h4>
<div class="anitracker-modal-list-container">
<div class="anitracker-modal-list" style="min-height: 100px;min-width: 200px;"></div>
</div>
`).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");
$(`
<div class="anitracker-modal-list-entry" animename="${toHtmlCodes(g.name)}">
<a href="/a/${g.id}" target="_blank" title="${toHtmlCodes(g.name)}">
${g.name}
</a><br>
<span>
Latest episode: ${getDayName(latestEp.getDay())} ${latestEp.toLocaleTimeString()}
</span><br>
<button class="btn btn-danger" title="Remove this anime from the episode feed">
<i class="fa fa-trash" aria-hidden="true"></i>
&nbsp;Remove
</button>
</div>`).appendTo('#anitracker-modal-body .anitracker-modal-list')
});
if (storage.notifications.anime.length === 0) {
$("<span>Use the <i class=\"fa fa-bell\" title=\"bell\"></i> button on an ongoing anime to add it to the feed.</span>").appendTo('#anitracker-modal-body .anitracker-modal-list');
}
$('.anitracker-modal-list-entry button').on('click', (e) => {
const name = $(e.currentTarget).parent().attr('animename');
toggleNotifications(name);
const name2 = getAnimeName();
if (name2.length > 0 && name2 === name) {
$('.anitracker-notifications-toggle .anitracker-title-icon-check').hide();
}
$(e.currentTarget).parent().remove();
});
openModal(openNotificationsModal);
});
const animeData = {};
const queue = [...oldStorage.notifications.anime];
openModal().then(() => {
next();
});
async function next() {
const anime = queue.shift();
if (anime !== undefined) animeData[anime.name] = await updateNotifications(anime.name);
if (queue.length > 0 && $('#anitracker-notifications-list-spinner').length > 0) next();
else done();
}
function done() {
if ($('#anitracker-notifications-list-spinner').length === 0) return;
$('#anitracker-notifications-list-spinner').remove();
const storage = getStorage();
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) {
$("<span>Nothing here yet!</span>").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[ep.animeName];
if (data === undefined) return;
$(`
<div class="anitracker-big-list-item anitracker-notification-item${ep.watched ? "" : " anitracker-notification-item-unwatched"}" anime-data="${toHtmlCodes(data.title)}" episode-data="${ep.episode}">
<a href="/play/${data.session}/${ep.session}" target="_blank" title="${toHtmlCodes(data.title)}">
<img src="${data.poster.slice(0, -3) + 'th.jpg'}" referrerpolicy="no-referrer" alt="[Thumbnail of ${toHtmlCodes(data.title)}]"}>
<i class="fa ${ep.watched ? 'fa-eye-slash' : 'fa-eye'} anitracker-watched-toggle anitracker-temp" tabindex="0" aria-hidden="true" title="Mark this episode as ${ep.watched ? 'unwatched' : 'watched'}"></i>
<div class="anitracker-main-text">${data.title}</div>
<div class="anitracker-subtext"><strong>Episode ${ep.episode}</strong></div>
<div class="anitracker-subtext">${timeSince(new Date(ep.time + " UTC"))} ago</div>
</a>
</div>`).appendTo('#anitracker-modal-body .anitracker-modal-list');
if (i > index+num-1) break;
}
$('.anitracker-watched-toggle.anitracker-temp').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.animeName === parent.attr('anime-data') && a.episode === +parent.attr('episode-data'));
if (ep === undefined) return;
if (parent.hasClass('anitracker-notification-item-unwatched')) {
parent.removeClass('anitracker-notification-item-unwatched');
elem.removeClass('fa-eye');
elem.addClass('fa-eye-slash');
}
else {
parent.addClass('anitracker-notification-item-unwatched');
elem.addClass('fa-eye');
elem.removeClass('fa-eye-slash');
}
ep.watched = !ep.watched;
elem.attr('title', `Mark this episode as ${ep.watched ? 'unwatched' : 'watched'}`)
saveData(storage);
});
$('.anitracker-watched-toggle.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();
$(`
<h4>Bookmarks</h4>
<div class="anitracker-modal-list-container">
<div class="anitracker-modal-list" style="min-height: 100px;min-width: 200px;">
<div class="btn-group">
<input autocomplete="off" class="form-control anitracker-text-input-bar anitracker-modal-search" placeholder="Search">
<button dir="down" class="btn btn-secondary dropdown-toggle anitracker-reverse-order-button" title="Sort direction (down is default, and means newest first)"></button>
</div>
</div>
</div>
`).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 => {
$(`
<div class="anitracker-modal-list-entry" animeid="${g.id}">
<a href="/a/${g.id}" target="_blank" title="${toHtmlCodes(g.name)}">
${g.name}
</a><br>
<button class="btn btn-danger" title="Remove this bookmark">
<i class="fa fa-trash" aria-hidden="true"></i>
&nbsp;Remove
</button>
</div>`).appendTo('#anitracker-modal-body .anitracker-modal-list')
});
if (storage.bookmarks.length === 0) {
$(`<span style="display: block;">No bookmarks yet!</span>`).appendTo('#anitracker-modal-body .anitracker-modal-list');
}
applyDeleteEvents();
openModal();
});
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) {
const storage = getStorage();
const found = 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 !== name);
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);
if (data === undefined) return;
const episodes = await asyncGetResponseData('/api?m=release&sort=episode_desc&id=' + data.session);
if (episodes === undefined) return;
return new Promise((resolve, reject) => {
if (episodes.length === 0) resolve(undefined);
for (const anime of storage.notifications.anime) {
if (anime.name !== animeName) continue;
anime.latest_episode = episodes[0].created_at;
break;
}
for (const ep of episodes) {
const found = storage.notifications.episodes.find(a => a.episode === ep.episode && a.animeName === animeName);
if (found !== undefined) {
found.session = ep.session;
continue;
}
if (new Date(ep.created_at + " UTC").getTime() < storage.notifications.lastUpdated) {
continue;
}
storage.notifications.episodes.push({
session: ep.session,
animeName: animeName,
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);
});
}
// Search/index page
if (/^\/anime\/?$/.test(window.location.pathname)) {
$(`
<div id="anitracker" class="anitracker-index" style="margin-bottom: 10px;">
<button class="btn btn-dark" id="anitracker-random-anime" title="Open a random anime from within the selected filters">
<i class="fa fa-random" aria-hidden="true"></i>
&nbsp;Random Anime
</button>
<div class="anitracker-items-box" id="anitracker-genre-list" dropdown="genre">
<button default="and" title="Toggle filter logic">and</button>
<div>
<div class="anitracker-items-box-search" contenteditable="" spellcheck="false"><span class="anitracker-text-input"></span></div>
<span class="placeholder">Genre</span>
</div>
</div>
<div class="anitracker-items-box" id="anitracker-theme-list" dropdown="theme">
<button default="and" title="Toggle filter logic">and</button>
<div>
<div class="anitracker-items-box-search" contenteditable="" spellcheck="false"><span class="anitracker-text-input"></span></div>
<span class="placeholder">Theme</span>
</div>
</div>
<div class="anitracker-items-box" id="anitracker-type-list" dropdown="type">
<div>
<div class="anitracker-items-box-search" contenteditable="" spellcheck="false"><span class="anitracker-text-input"></span></div>
<span class="placeholder">Type (or)</span>
</div>
</div>
<div class="anitracker-items-box" id="anitracker-demographic-list" dropdown="demographic">
<div>
<div class="anitracker-items-box-search" contenteditable="" spellcheck="false"><span class="anitracker-text-input"></span></div>
<span class="placeholder">Demographic (or)</span>
</div>
</div>
<div class="btn-group">
<button class="btn dropdown-toggle btn-dark" id="anitracker-status-button" data-bs-toggle="dropdown" data-toggle="dropdown" title="Choose status">All</button>
</div>
<div class="btn-group">
<button class="btn btn-dark" id="anitracker-time-search-button" title="Set season filter">
<svg fill="#ffffff" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512" xml:space="preserve" aria-hidden="true">
<path d="M256,0C114.842,0,0,114.842,0,256s114.842,256,256,256s256-114.842,256-256S397.158,0,256,0z M374.821,283.546H256 c-15.148,0-27.429-12.283-27.429-27.429V137.295c0-15.148,12.281-27.429,27.429-27.429s27.429,12.281,27.429,27.429v91.394h91.392 c15.148,0,27.429,12.279,27.429,27.429C402.249,271.263,389.968,283.546,374.821,283.546z"/>
</svg>
</button>
</div>
</div>`).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();
$(`
<span style="display: block;margin-bottom: 10px;font-size: 1.2em;color:#ddd;" id="anitracker-filter-result-count">Filter results: <span>${animeList.length}</span></span>
`).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@6.6.2', 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 => $(`<button ref="${type}/${g.value}">${g.name}</button>`));
}
$(`<div id="anitracker-genre-dropdown" dropdown="genre" class="dropdown-menu anitracker-dropdown-content anitracker-items-dropdown">`).insertAfter('#anitracker-genre-list');
getDropdownButtons(filterValues.genre, 'genre').forEach(g => { g.appendTo('#anitracker-genre-dropdown') });
$(`<button ref="genre/none">(None)</button>`).appendTo('#anitracker-genre-dropdown');
$(`<div id="anitracker-theme-dropdown" dropdown="theme" class="dropdown-menu anitracker-dropdown-content anitracker-items-dropdown">`).insertAfter('#anitracker-theme-list');
getDropdownButtons(filterValues.theme, 'theme').forEach(g => { g.appendTo('#anitracker-theme-dropdown') });
$(`<button ref="theme/none">(None)</button>`).appendTo('#anitracker-theme-dropdown');
$(`<div id="anitracker-type-dropdown" dropdown="type" class="dropdown-menu anitracker-dropdown-content anitracker-items-dropdown">`).insertAfter('#anitracker-type-list');
getDropdownButtons(filterValues.type, 'type').forEach(g => { g.appendTo('#anitracker-type-dropdown') });
$(`<button ref="type/none">(None)</button>`).appendTo('#anitracker-type-dropdown');
$(`<div id="anitracker-demographic-dropdown" dropdown="demographic" class="dropdown-menu anitracker-dropdown-content anitracker-items-dropdown">`).insertAfter('#anitracker-demographic-list');
getDropdownButtons(filterValues.demographic, 'demographic').forEach(g => { g.appendTo('#anitracker-demographic-dropdown') });
$(`<button ref="demographic/none">(None)</button>`).appendTo('#anitracker-demographic-dropdown');
$(`<div id="anitracker-status-dropdown" dropdown="status" class="dropdown-menu anitracker-dropdown-content">`).insertAfter('#anitracker-status-button');
['all','airing','completed'].forEach(g => { $(`<button ref="${g}">${g[0].toUpperCase() + g.slice(1)}</button>`).appendTo('#anitracker-status-dropdown') });
$(`<button ref="none">(No status)</button>`).appendTo('#anitracker-status-dropdown');
const timeframeSettings = {
enabled: false
};
$('#anitracker-time-search-button').on('click', () => {
$('#anitracker-modal-body').empty();
$(`
<h5>Time interval</h5>
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="anitracker-settings-enable-switch">
<label class="custom-control-label" for="anitracker-settings-enable-switch">Enable</label>
</div>
<br>
<div class="anitracker-season-group" id="anitracker-season-from">
<span>From:</span>
<div class="btn-group">
<input autocomplete="off" class="form-control anitracker-text-input-bar anitracker-year-input" disabled placeholder="Year" type="number">
</div>
<div class="btn-group">
<button class="btn dropdown-toggle btn-secondary anitracker-season-dropdown-button" disabled data-bs-toggle="dropdown" data-toggle="dropdown" data-value="Spring">Spring</button>
</div>
<div class="btn-group">
<button class="btn btn-secondary" id="anitracker-season-copy-to-lower" style="color:white;margin-left:14px;" title="Copy the 'from' season to the 'to' season">
<i class="fa fa-arrow-circle-down" aria-hidden="true"></i>
</button>
</div>
</div>
<div class="anitracker-season-group" id="anitracker-season-to">
<span>To:</span>
<div class="btn-group">
<input autocomplete="off" class="form-control anitracker-text-input-bar anitracker-year-input" disabled placeholder="Year" type="number">
</div>
<div class="btn-group">
<button class="btn dropdown-toggle btn-secondary anitracker-season-dropdown-button" disabled data-bs-toggle="dropdown" data-toggle="dropdown" data-value="Spring">Spring</button>
</div>
</div>
<br>
<div>
<div class="btn-group">
<button class="btn btn-primary" id="anitracker-modal-confirm-button"><i class="fa fa-check" aria-hidden="true"></i>&nbsp;Done</button>
</div>
</div>`).appendTo('#anitracker-modal-body');
$('.anitracker-year-input').val(new Date().getFullYear());
$('#anitracker-settings-enable-switch').on('change', () => {
updateDisabled($('#anitracker-settings-enable-switch').is(':checked'));
});
$('#anitracker-settings-enable-switch').prop('checked', timeframeSettings.enabled);
updateDisabled(timeframeSettings.enabled);
function updateDisabled(enabled) {
$('.anitracker-season-group').find('input,button').prop('disabled', !enabled);
}
$('#anitracker-season-copy-to-lower').on('click', () => {
const seasonName = $('#anitracker-season-from .anitracker-season-dropdown-button').data('value');
$('#anitracker-season-to .anitracker-year-input').val($('#anitracker-season-from .anitracker-year-input').val());
$('#anitracker-season-to .anitracker-season-dropdown-button').data('value', seasonName);
$('#anitracker-season-to .anitracker-season-dropdown-button').text(seasonName);
});
$(`<div class="dropdown-menu anitracker-dropdown-content anitracker-season-dropdown">`).insertAfter('.anitracker-season-dropdown-button');
['Spring','Summer','Fall','Winter'].forEach(g => { $(`<button ref="${g.toLowerCase()}">${g}</button>`).appendTo('.anitracker-season-dropdown') });
$('.anitracker-season-dropdown button').on('click', (e) => {
const pressed = $(e.target)
const btn = pressed.parent().parent().find('.anitracker-season-dropdown-button');
btn.data('value', pressed.text());
btn.text(pressed.text());
});
if (timeframeSettings.from) {
$('#anitracker-season-from .anitracker-year-input').val(timeframeSettings.from.year.toString());
$('#anitracker-season-from .anitracker-season-dropdown button')[timeframeSettings.from.season].click();
}
if (timeframeSettings.to) {
$('#anitracker-season-to .anitracker-year-input').val(timeframeSettings.to.year.toString());
$('#anitracker-season-to .anitracker-season-dropdown button')[timeframeSettings.to.season].click();
}
$('#anitracker-modal-confirm-button').on('click', () => {
const from = {
year: +$('#anitracker-season-from .anitracker-year-input').val(),
season: getSeasonValue($('#anitracker-season-from').find('.anitracker-season-dropdown-button').data('value'))
}
const to = {
year: +$('#anitracker-season-to .anitracker-year-input').val(),
season: getSeasonValue($('#anitracker-season-to').find('.anitracker-season-dropdown-button').data('value'))
}
if ($('#anitracker-settings-enable-switch').is(':checked')) {
for (const input of $('.anitracker-year-input')) {
if (/^\d{4}$/.test($(input).val())) continue;
alert('[AnimePahe Improvements]\n\nYear values must both be 4 numbers.');
return;
}
if (to.year < from.year || (to.year === from.year && to.season < from.season)) {
alert('[AnimePahe Improvements]\n\nSeason times must be from oldest to newest.');
return;
}
if (to.year - from.year > 100) {
alert('[AnimePahe Improvements]\n\nYear interval cannot be more than 100 years.');
return;
}
removeSeasonsFromFilters();
appliedFilters.push(`season/${getSeasonName(from.season)}-${from.year.toString()}..${getSeasonName(to.season)}-${to.year.toString()}`);
$('#anitracker-time-search-button').addClass('anitracker-active');
}
else {
removeSeasonsFromFilters();
$('#anitracker-time-search-button').removeClass('anitracker-active');
}
timeframeSettings.enabled = $('#anitracker-settings-enable-switch').is(':checked');
timeframeSettings.from = from;
timeframeSettings.to = to;
closeModal();
refreshSearchPage(appliedFilters, true);
});
openModal();
});
function removeSeasonsFromFilters() {
const newFilters = [];
for (const filter of appliedFilters) {
if (filter.startsWith('season/')) continue;
newFilters.push(filter);
}
appliedFilters.length = 0;
appliedFilters.push(...newFilters);
}
const appliedFilters = [];
$('.anitracker-items-dropdown').on('click', (e) => {
const filterSearchBox = $(`#anitracker-${/^anitracker-([^\-]+)-dropdown$/.exec($(e.target).closest('.anitracker-dropdown-content').attr('id'))[1]}-list .anitracker-items-box-search`);
filterSearchBox.focus();
if (!$(e.target).is('button')) return;
const filter = $(e.target).attr('ref');
if (appliedFilters.includes(filter)) {
removeFilter(filter, filterSearchBox, e.target);
}
else {
addFilter(filter, filterSearchBox, e.target);
}
});
$('#anitracker-status-dropdown').on('click', (e) => {
if (!$(e.target).is('button')) return;
const filter = $(e.target).attr('ref');
addStatusFilter(filter);
refreshSearchPage(appliedFilters);
});
function addStatusFilter(filter) {
if (appliedFilters.includes(filter)) return;
for (const btn of $('#anitracker-status-dropdown button')) {
if ($(btn).attr('ref') !== filter) continue;
$('#anitracker-status-button').text($(btn).text());
}
if (filter !== 'all') $('#anitracker-status-button').addClass('anitracker-active');
else $('#anitracker-status-button').removeClass('anitracker-active');
for (const filter2 of appliedFilters) {
if (filter2.includes('/')) continue;
appliedFilters.splice(appliedFilters.indexOf(filter2), 1);
}
if (filter !== 'all') appliedFilters.push(filter);
}
function addFilter(name, filterBox, filterButton, refreshPage = true) {
const filterType = getFilterParts(name).type;
if (filterType !== '' && filterRules[filterType] === 'and') {
if (name.endsWith('/none')) {
for (const filter of appliedFilters.filter(a => a.startsWith(filterType))) {
if (filter.endsWith('/none')) continue;
removeFilter(filter, filterBox, (() => {
for (const btn of $(filterButton).parent().find('button')) {
if ($(btn).attr('ref') !== filter) continue;
return btn;
}
})(), false);
}
}
else if (appliedFilters.includes(filterType + '/none')) {
removeFilter(filterType + '/none', filterBox, (() => {
for (const btn of $(filterButton).parent().find('button')) {
if ($(btn).attr('ref') !== filterType + '/none') continue;
return btn;
}
})(), false);
}
}
$(filterBox).find('.anitracker-text-input').text('');
const basicText = $(filterBox).contents().filter(function(){return this.nodeType == Node.TEXT_NODE;})[0];
if (basicText !== undefined) basicText.nodeValue = '';
addFilterIcon($(filterBox)[0], name, $(filterButton).text());
$(filterButton).addClass('anitracker-active');
appliedFilters.push(name);
if (refreshPage) refreshSearchPage(appliedFilters);
updateFilterBox(filterBox);
}
function removeFilter(name, filterBox, filterButton, refreshPage = true) {
$(filterBox).find('.anitracker-text-input').text('');
const basicText = $(filterBox).contents().filter(function(){return this.nodeType == Node.TEXT_NODE;})[0];
if (basicText !== undefined) basicText.nodeValue = '';
removeFilterIcon($(filterBox)[0], name);
$(filterButton).removeClass('anitracker-active');
appliedFilters.splice(appliedFilters.indexOf(name), 1);
if (refreshPage) refreshSearchPage(appliedFilters);
updateFilterBox(filterBox);
}
function addFilterIcon(elem, filter, nameInput) {
const name = nameInput || getFilterParts(filter).value;
setIconEvent($(`
<span class="anitracker-filter-icon" data-name="${name}" data-filter="${filter}">${name}</span><span class="anitracker-text-input">&nbsp;</span>
`).after(' ').appendTo(elem));
}
function removeFilterIcon(elem, name) {
for (const f of $(elem).find('.anitracker-filter-icon')) {
if ($(f).text() === name) $(f).remove();
}
}
const searchQueue = [];
function refreshSearchPage(filtersInput, screenSpinner = false, fromQueue = false) {
const filters = JSON.parse(JSON.stringify(filtersInput));
if (!fromQueue) {
if (screenSpinner) {
$(`
<div style="width:100%;height:100%;background-color:rgba(0, 0, 0, 0.9);position:fixed;z-index:999;display:flex;justify-content:center;align-items:center;" class="anitracker-filter-spinner">
<div class="spinner-border" role="status" style="color:#d5015b;width:5rem;height:5rem;">
<span class="sr-only">Loading...</span>
</div>
<span style="position: absolute;font-weight: bold;">0%</span>
</div>`).prependTo(document.body);
}
else {
$(`
<div style="display: inline-flex;margin-left: 10px;justify-content: center;align-items: center;vertical-align: bottom;" class="anitracker-filter-spinner">
<div class="spinner-border" role="status" style="color:#d5015b;">
<span class="sr-only">Loading...</span>
</div>
<span style="position: absolute;font-size: .5em;font-weight: bold;">0%</span>
</div>`).appendTo('.page-index h1');
}
searchQueue.push(filters);
if (searchQueue.length > 1) return;
}
if (filters.length === 0) {
updateFilterResults([], true).then(() => {
animeList.length = 0;
animeList.push(...getAnimeList());
$('#anitracker-filter-result-count span').text(animeList.length.toString());
$($('.anitracker-filter-spinner')[0]).remove();
searchQueue.shift();
if (searchQueue.length > 0) {
refreshSearchPage(searchQueue[0], screenSpinner, true);
return;
}
if ($('#anitracker-anime-list-search').val() === '') return;
$('#anitracker-anime-list-search').trigger('anitracker:search');
});
return;
}
let filterTotal = 0;
for (const filter of filters) {
const parts = getFilterParts(filter);
if (noneFilterRegex.test(filter)) {
filterTotal += filterValues[parts.type].length;
continue;
}
if (seasonFilterRegex.test(filter)) {
const range = parts.value.split('..');
filterTotal += getSeasonTimeframe({
year: +range[0].split('-')[1],
season: getSeasonValue(range[0].split('-')[0])
},
{
year: +range[1].split('-')[1],
season: getSeasonValue(range[1].split('-')[0])
}).length;
continue;
}
filterTotal++;
}
getFilteredList(filters, filterTotal).then((finalList) => {
if (finalList === undefined) {
alert('[AnimePahe Improvements]\n\nSearch filter failed.');
$($('.anitracker-filter-spinner')[0]).remove();
searchQueue.length = 0;
refreshSearchPage([]);
return;
}
finalList.sort((a,b) => a.name > b.name ? 1 : -1);
updateFilterResults(finalList).then(() => {
animeList.length = 0;
animeList.push(...finalList);
$($('.anitracker-filter-spinner')[0]).remove();
updateParams(appliedFilters, $('.anitracker-items-box>button'));
searchQueue.shift();
if (searchQueue.length > 0) {
refreshSearchPage(searchQueue[0], screenSpinner, true);
return;
}
if ($('#anitracker-anime-list-search').val() === '') return;
$('#anitracker-anime-list-search').trigger('anitracker:search');
});
});
}
function updateFilterResults(list, noFilters = false) {
return new Promise((resolve, reject) => {
$('.anitracker-filter-result').remove();
$('#anitracker-filter-results').remove();
$('.nav-item').show();
if (noFilters) {
$('.index>').show();
$('.index>>>>div').show();
updateParams(appliedFilters);
resolve();
return;
}
$('#anitracker-filter-result-count span').text(list.length.toString());
$('.index>>>>div').hide();
if (list.length >= 100) {
$('.index>').show();
list.forEach(anime => {
const elem = $(`
<div class="anitracker-filter-result col-12 col-md-6">
${anime.html}
</div>`);
const matchLetter = (() => {
if (/^[A-Za-z]/.test(anime.name)) {
return anime.name[0].toUpperCase();
}
else {
return 'hash'
}
})();
for (const tab of $('.tab-content').children()) {
if (tab.id !== matchLetter) continue;
elem.appendTo($(tab).children()[0]);
}
});
for (const tab of $('.tab-content').children()) {
if ($(tab).find('.anitracker-filter-result').length > 0) continue;
const tabId = $(tab).attr('id');
for (const navLink of $('.nav-link')) {
if (($(navLink).attr('role') !== 'tab' || $(navLink).text() !== tabId) && !($(navLink).text() === '#' && tabId === 'hash')) continue;
$(navLink).parent().hide();
}
}
if ($('.nav-link.active').parent().css('display') === 'none') {
let visibleTabs = 0;
for (const navLink of $('.nav-link')) {
if ($(navLink).parent().css('display') === 'none' || $(navLink).text().length > 1) continue;
visibleTabs++;
}
for (const navLink of $('.nav-link')) {
if ($(navLink).parent().css('display') === 'none' || $(navLink).text().length > 1) continue;
if ($(navLink).text() === "#" && visibleTabs > 1) continue;
$(navLink).click();
break;
}
}
}
else {
$('.index>').hide();
$(`<div class="row" id="anitracker-filter-results"></div>`).prependTo('.index');
let matches = '';
list.forEach(anime => {
matches += `
<div class="col-12 col-md-6">
${anime.html}
</div>`;
});
if (list.length === 0) matches = `<div class="col-12 col-md-6">No results found.</div>`;
$(matches).appendTo('#anitracker-filter-results');
}
resolve();
});
}
function updateParams(filters, ruleButtons = []) {
window.history.replaceState({}, document.title, "/anime" + getParams(filters, ruleButtons));
}
function getParams(filters, ruleButtons = []) {
const filterArgs = textFromFilterList(filters);
let params = (filterArgs.length > 0 ? ('?' + filterArgs) : '');
if (ruleButtons.length > 0) {
for (const btn of ruleButtons) {
if ($(btn).text() === $(btn).attr('default')) continue;
params += '&' + $(btn).parent().attr('dropdown') + '-rule=' + $(btn).text();
}
}
return params;
}
$.getScript('https://cdn.jsdelivr.net/npm/fuse.js@6.6.2', function() {
$(`
<div class="btn-group">
<input id="anitracker-anime-list-search" autocomplete="off" class="form-control anitracker-text-input-bar" style="width: 150px;" placeholder="Search">
</div>`).appendTo('#anitracker');
let typingTimer;
$('#anitracker-anime-list-search').on('anitracker:search', function() {
animeListSearch();
});
$('#anitracker-anime-list-search').on('keyup', function() {
clearTimeout(typingTimer);
typingTimer = setTimeout(animeListSearch, 150);
});
$('#anitracker-anime-list-search').on('keydown', function() {
clearTimeout(typingTimer);
});
function animeListSearch() {
$('#anitracker-search-results').remove();
const value = $('#anitracker-anime-list-search').val();
if (value === '') {
$('.index>').show();
if (animeList.length < 100) $('.scrollable-ul').hide();
const newSearchParams = new URLSearchParams(window.location.search);
newSearchParams.delete('search');
window.history.replaceState({}, document.title, "/anime" + (Array.from(newSearchParams.entries()).length > 0 ? ('?' + newSearchParams.toString()) : ''));
}
else {
$('.index>').hide();
const matches = searchList(Fuse, animeList, value);
$(`<div class="row" id="anitracker-search-results"></div>`).prependTo('.index');
let elements = '';
matches.forEach(match => {
elements += `
<div class="col-12 col-md-6">
${match.html}
</div>`;
});
if (matches.length === 0) elements = `<div class="col-12 col-md-6">No results found.</div>`
$(elements).appendTo('#anitracker-search-results');
const newSearchParams = new URLSearchParams(window.location.search);
newSearchParams.set('search', value);
window.history.replaceState({}, document.title, "/anime?" + newSearchParams.toString());
}
}
const searchParams = new URLSearchParams(window.location.search);
if (searchParams.has('search')) {
$('#anitracker-anime-list-search').val(searchParams.get('search'));
animeListSearch();
}
}).fail(() => {
$(`
<div class="btn-group">
<span>Fuse.js failed to load.</span>
</div>`).appendTo('#anitracker');
});
const urlFilters = filterListFromParams(new URLSearchParams(window.location.search));
for (const filter of urlFilters) {
const parts = getFilterParts(filter);
const type = parts.type;
if (type === '') {
addStatusFilter(filter);
continue;
}
const searchBox = $(`#anitracker-${type}-list .anitracker-items-box-search`);
const dropdown = Array.from($(`#anitracker-${type}-dropdown`).children()).find(a=> $(a).attr('ref') === filter);
if (type.endsWith('-rule')) {
for (const btn of $('.anitracker-items-box>button')) {
const type2 = $(btn).parent().attr('dropdown');
if (type2 !== type.split('-')[0]) continue;
$(btn).text(parts.value);
}
continue;
}
if (type === 'season') {
if (!seasonFilterRegex.test(filter)) continue;
appliedFilters.push(filter);
$('#anitracker-time-search-button').addClass('anitracker-active');
const range = parts.value.split('..');
timeframeSettings.enabled = true;
timeframeSettings.from = {
year: +range[0].split('-')[1],
season: getSeasonValue(range[0].split('-')[0])
};
timeframeSettings.to = {
year: +range[1].split('-')[1],
season: getSeasonValue(range[1].split('-')[0])
};
continue;
}
if (searchBox.length === 0) {
appliedFilters.push(filter);
continue;
}
addFilter(filter, searchBox, dropdown, false);
continue;
}
if (urlFilters.length > 0) refreshSearchPage(appliedFilters, true);
return;
}
function filterListFromParams(params, allowRules = true) {
const filters = [];
for (const [key, values] of params.entries()) {
const key2 = (key === 'other' ? '' : key);
if (!filterRules[key2] && !key.endsWith('-rule')) continue;
if (key.endsWith('-rule')) {
filterRules[key.split('-')[0]] = values === 'and' ? 'and' : 'or';
if (!allowRules) continue;
}
decodeURIComponent(values).split(',').forEach(value => {
filters.push((key2 === '' ? '' : key2 + '/') + value);
});
}
return filters;
}
function textFromFilterList(filters) {
const filterTypes = {};
filters.forEach(filter => {
const parts = getFilterParts(filter);
let key = (() => {
if (parts.type === '') return 'other';
return parts.type;
})();
if (filterTypes[key] === undefined) filterTypes[key] = [];
filterTypes[key].push(parts.value);
});
const finishedList = [];
for (const [key, values] of Object.entries(filterTypes)) {
finishedList.push(key + '=' + encodeURIComponent(values.join(',')));
}
return finishedList.join('&');
}
function getAnimeList(page = $(document)) {
const animeList = [];
for (const anime of page.find('.col-12')) {
if (anime.children[0] === undefined || $(anime).hasClass('anitracker-filter-result') || $(anime).parent().attr('id') !== undefined) continue;
animeList.push({
name: $(anime.children[0]).text(),
link: anime.children[0].href,
html: $(anime).html()
});
}
return animeList;
}
function randint(min, max) { // min and max included
return Math.floor(Math.random() * (max - min + 1) + min)
}
(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());
});
};
})(jQuery);
function isEpisode(url = window.location.toString()) {
return url.includes('/play/');
}
function download(filename, text) {
var element = document.createElement('a');
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
element.setAttribute('download', filename);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
function deleteEpisodesFromTracker(exclude, nameInput) {
const storage = getStorage();
const animeName = nameInput || getAnimeName();
const linkData = getStoredLinkData(storage);
for (const episode of storage.linkList) {
if (episode.type === 'episode' && episode.animeName === animeName && episode.episodeNum !== exclude) {
const index = storage.linkList.indexOf(episode);
storage.linkList.splice(index, 1);
}
}
for (const timeData of storage.videoTimes) {
if (timeData.episodeNum !== exclude && stringSimilarity(timeData.animeName, animeName) > 0.81) {
const index = storage.videoTimes.indexOf(timeData);
storage.videoTimes.splice(index, 1);
}
}
saveData(storage);
}
function deleteEpisodeFromTracker(animeName, episodeNum) {
const storage = getStorage();
storage.linkList = storage.linkList.filter(a => !(a.type == 'episode' && a.animeName == animeName && a.episodeNum == episodeNum));
saveData(storage);
}
function getStoredLinkData(storage) {
if (isEpisode()) {
return storage.linkList.find(a => a.type == 'episode' && a.animeSession == animeSession && a.episodeSession == episodeSession);
}
return storage.linkList.find(a => a.type == 'anime' && a.animeSession == animeSession);
}
function getAnimeName() {
return isEpisode() ? /Watch (.*) - ([\d\.]+) Online/.exec($('.theatre-info h1').text())[1] : $($('.title-wrapper h1 span')[0]).text();
}
function getEpisodeNum() {
if (isEpisode()) return +(/Watch (.*) - ([\d\.]+) Online/.exec($('.theatre-info h1').text())[2]);
else return 0;
}
function sortAnimesChronologically(animeList) {
// Animes (plural)
animeList.sort((a, b) => {return getSeasonValue(a.season) > getSeasonValue(b.season) ? 1 : -1});
animeList.sort((a, b) => {return a.year > b.year ? 1 : -1});
return animeList;
}
function asyncGetResponseData(qurl) {
return new Promise((resolve, reject) => {
let req = new XMLHttpRequest();
req.open('GET', qurl, true);
req.onload = () => {
if (req.status === 200) {
resolve(JSON.parse(req.response).data);
return;
}
reject(undefined);
}
try {
req.send();
}
catch (err) {
console.error(err);
resolve(undefined);
}
});
}
function getResponseData(qurl) {
let req = new XMLHttpRequest();
req.open('GET', qurl, false);
try {
req.send();
}
catch (err) {
console.error(err);
return(undefined);
}
if (req.status === 200) {
return(JSON.parse(req.response).data);
}
return(undefined);
}
function getAnimeSessionFromUrl(url = window.location.toString()) {
return new RegExp('^(.*animepahe\.[a-z]+)?/(play|anime)/([^/?#]+)').exec(url)[3];
}
function getEpisodeSessionFromUrl(url = window.location.toString()) {
return new RegExp('^(.*animepahe\.[a-z]+)?/(play|anime)/([^/]+)/([^/?#]+)').exec(url)[4];
}
function getAnimeData(name = getAnimeName()) {
const response = getResponseData('/api?m=search&q=' + encodeURIComponent(name));
if (response === undefined) return response;
for (const anime of response) {
if (anime.title === name) {
return anime;
}
}
return undefined;
}
async function asyncGetAnimeData(name = getAnimeName()) {
const response = await getResponseData('/api?m=search&q=' + encodeURIComponent(name));
return new Promise((resolve, reject) => {
if (response === undefined) resolve(response);
for (const anime of response) {
if (anime.title === name) {
resolve(anime);
}
}
reject(undefined);
});
}
// For general animepahe pages that are not episode or anime pages
if (!url.includes('/play/') && !url.includes('/anime/') && !/anime[\/#]?[^\/]*([\?&][^=]+=[^\?^&])*$/.test(url)) {
$(`
<div id="anitracker">
</div>`).insertAfter('.notification-release');
addManageDataButton();
updateSwitches();
return;
}
let animeSession = getAnimeSessionFromUrl();
let episodeSession = '';
if (isEpisode()) {
episodeSession = getEpisodeSessionFromUrl();
}
function getEpisodeSession(aSession, episodeNum) {
const request = new XMLHttpRequest();
request.open('GET', '/api?m=release&id=' + aSession, false);
request.send();
if (request.status !== 200) return undefined;
const response = JSON.parse(request.response);
return (() => {
for (let i = 1; i <= response.last_page; i++) {
const episodes = getResponseData(`/api?m=release&sort=episode_asc&page=${i}&id=${aSession}`);
if (episodes === undefined) return undefined;
const episode = episodes.find(a => a.episode === episodeNum);
if (episode === undefined) continue;
return episode.session;
}
})();
}
function refreshSession(from404 = false) {
/* Return codes:
* 1: couldn't find stored session at 404 page
* 2: couldn't get anime data
* 3: couldn't get episode session
* 4: idk
*/
const storage = getStorage();
const bobj = getStoredLinkData(storage);
let name = '';
let episodeNum = 0;
if (bobj === undefined && from404) return 1;
if (bobj !== undefined) {
name = bobj.animeName;
episodeNum = bobj.episodeNum;
}
else {
name = getAnimeName();
episodeNum = getEpisodeNum();
}
if (isEpisode()) {
const animeData = getAnimeData(name);
if (animeData === undefined) return 2;
const episodeSession = getEpisodeSession(animeData.session, episodeNum);
if (episodeSession === undefined) return 3;
if (bobj !== undefined) {
for (const g of storage.linkList) {
if (g.type === 'episode' && g.animeSession === bobj.animeSession && g.episodeSession === bobj.episodeSession) {
storage.linkList.splice(storage.linkList.indexOf(g), 1);
}
}
}
saveData(storage);
window.location.replace('/play/' + animeData.session + '/' + episodeSession + window.location.search);
return 0;
}
else if (bobj !== undefined && bobj.animeId !== undefined) {
for (const g of storage.linkList) {
if (g.type === 'anime' && g.animeSession === bobj.animeSession) {
storage.linkList.splice(storage.linkList.indexOf(g), 1);
}
}
saveData(storage);
window.location.replace('/a/' + bobj.animeId);
return 0;
}
else {
let animeData = getAnimeData(name);
if (animeData === undefined) return 2;
window.location.replace('/a/' + animeData.id);
return 0;
}
return 4;
}
const obj = getStoredLinkData(initialStorage);
if (isEpisode() && !is404) $('#downloadMenu').changeElementType('button');
console.log('[AnimePahe Improvements]', obj, animeSession, episodeSession);
function setSessionData() {
const animeName = getAnimeName();
const storage = getStorage();
if (isEpisode()) {
storage.linkList.push({
animeSession: animeSession,
episodeSession: episodeSession,
type: 'episode',
animeName: animeName,
episodeNum: getEpisodeNum()
});
}
else {
storage.linkList.push({
animeId: getAnimeData(animeName)?.id,
animeSession: animeSession,
type: 'anime',
animeName: animeName
});
}
if (storage.linkList.length > 1000) {
storage.splice(0,1);
}
saveData(storage);
}
if (obj === undefined && !is404) {
if (!isRandomAnime()) setSessionData();
}
else if (obj !== undefined && is404) {
document.title = "Refreshing session... :: animepahe";
$('.text-center h1').text('Refreshing session, please wait...');
refreshSession(true);
return;
}
else if (obj === undefined && is404) {
if (document.referrer.length > 0) {
const bobj = (() => {
if (!/\/play\/.+/.test(document.referrer) && !/\/anime\/.+/.test(document.referrer)) {
return true;
}
const session = getAnimeSessionFromUrl(document.referrer);
if (isEpisode(document.referrer)) {
return initialStorage.linkList.find(a => a.type === 'episode' && a.animeSession === session && a.episodeSession === getEpisodeSessionFromUrl(document.referrer));
}
else {
return initialStorage.linkList.find(a => a.type === 'anime' && a.animeSession === session);
}
})();
if (bobj !== undefined) {
const prevUrl = new URL(document.referrer);
const params = new URLSearchParams(prevUrl);
params.set('ref','404');
prevUrl.search = params.toString();
windowOpen(prevUrl.toString(), '_self');
return;
}
}
$('.text-center h1').text('Cannot refresh session: Link not found in tracker.');
return;
}
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
$(`
<span style="display:block;width:100%;text-align:center;" class="anitracker-from-share-warning">
The current episode data for this anime was not replaced due to coming from a share link.
<br>Refresh this page to replace it.
<br><span class="anitracker-text-button" tabindex="0">Dismiss</span>
</span>`).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 getSubInfo(str) {
const match = /^\b([^·]+)·\s*(\d{2,4})p(.*)$/.exec(str);
return {
name: match[1],
quality: +match[2],
other: match[3]
}
}
// Set the quality to best automatically
function bestVideoQuality() {
if (!isEpisode()) return;
const currentSub = getStoredLinkData(getStorage()).subInfo || getSubInfo($('#resolutionMenu .active').text());
let index = -1;
for (let i = 0; i < $('#resolutionMenu').children().length; i++) {
const sub = $('#resolutionMenu').children()[i];
const subInfo = getSubInfo($(sub).text());
if (subInfo.name !== currentSub.name || subInfo.other !== currentSub.other) continue;
if (subInfo.quality >= currentSub.quality) index = i;
}
if (index === -1) {
return;
}
const newSub = $('#resolutionMenu').children()[index];
if (!["","Loading..."].includes($('#fansubMenu').text())) {
if ($(newSub).text() === $('#resolutionMenu .active').text()) return;
newSub.click();
return;
}
new MutationObserver(function(mutationList, observer) {
newSub.click();
observer.disconnect();
}).observe($('#fansubMenu')[0], { childList: true });
}
function setIframeUrl(url) {
$('.embed-responsive-item').remove();
$(`
<iframe class="embed-responsive-item" scrolling="no" allowfullscreen="" allowtransparency="" src="${url}"></iframe>
`).prependTo('.embed-responsive');
$('.embed-responsive-item')[0].contentWindow.focus();
}
// Fix the quality dropdown buttons
if (isEpisode()) {
new MutationObserver(function(mutationList, observer) {
$('.click-to-load').remove();
$('#resolutionMenu').off('click');
$('#resolutionMenu').on('click', (el) => {
const targ = $(el.target);
if (targ.data('src') === undefined) return;
setIframeUrl(targ.data('src'));
$('#resolutionMenu .active').removeClass('active');
targ.addClass('active');
$('#fansubMenu').html(targ.html());
const storage = getStorage();
const data = getStoredLinkData(storage);
data.subInfo = getSubInfo(targ.text());
saveData(storage);
$.cookie('res', targ.data('resolution'), {
expires: 365,
path: '/'
});
$.cookie('aud', targ.data('audio'), {
expires: 365,
path: '/'
});
$.cookie('av1', targ.data('av1'), {
expires: 365,
path: '/'
});
});
observer.disconnect();
}).observe($('#fansubMenu')[0], { childList: true });
if (initialStorage.bestQuality === true) {
bestVideoQuality();
}
else if (!["","Loading..."].includes($('#fansubMenu').text())) {
$('#resolutionMenu .active').click();
} else {
new MutationObserver(function(mutationList, observer) {
$('#resolutionMenu .active').click();
observer.disconnect();
}).observe($('#fansubMenu')[0], { childList: true });
}
const timeArg = paramArray.find(a => a[0] === 'time');
if (timeArg !== undefined) {
const time = timeArg[1];
function check() {
if ($('.embed-responsive-item').attr('src') !== undefined) done();
else setTimeout(check, 100);
}
setTimeout(check, 100);
function done() {
setIframeUrl(stripUrl($('.embed-responsive-item').attr('src')) + '?time=' + time);
window.history.replaceState({}, document.title, window.location.origin + window.location.pathname);
}
}
}
function getTrackerDiv() {
return $(`
<div id="anitracker">
<button class="btn btn-dark" id="anitracker-refresh-session" title="Refresh the session for the current page">
<i class="fa fa-refresh" aria-hidden="true"></i>
&nbsp;Refresh Session
</button>
</div>`);
}
function getRelationData(session, relationType) {
const request = new XMLHttpRequest();
request.open('GET', '/anime/' + session, false);
request.send();
const page = request.status === 200 ? $(request.response) : {};
if (Object.keys(page).length === 0) return undefined;
const relationDiv = (() => {
for (const div of page.find('.anime-relation .col-12')) {
if ($(div).find('h4 span').text() !== relationType) continue;
return $(div);
break;
}
return undefined;
})();
if (relationDiv === undefined) return undefined;
const relationSession = new RegExp('^.*animepahe\.[a-z]+/anime/([^/]+)').exec(relationDiv.find('a')[0].href)[1];
request.open('GET', '/api?m=release&id=' + relationSession, false);
request.send();
if (request.status !== 200) return undefined;
const episodeList = [];
const response = JSON.parse(request.response);
for (let i = 1; i <= response.last_page; i++) {
const episodes = getResponseData(`/api?m=release&sort=episode_asc&page=${i}&id=${relationSession}`);
if (episodes !== undefined) {
[].push.apply(episodeList, episodes);
}
}
if (episodeList.length === 0) return undefined;
return {
episodes: episodeList,
name: $(relationDiv.find('h5')[0]).text(),
poster: relationDiv.find('img').attr('data-src').replace('.th',''),
session: relationSession
};
}
function hideSpinner(t, parents = 1) {
$(t).parents(`:eq(${parents})`).find('.anitracker-download-spinner').hide();
}
if (isEpisode()) {
getTrackerDiv().appendTo('.anime-note');
$('.prequel,.sequel').addClass('anitracker-thumbnail');
$(`
<span relationType="Prequel" class="dropdown-item anitracker-relation-link" id="anitracker-prequel-link">
Previous Anime
</span>`).prependTo('.episode-menu #scrollArea');
$(`
<span relationType="Sequel" class="dropdown-item anitracker-relation-link" id="anitracker-sequel-link">
Next Anime
</span>`).appendTo('.episode-menu #scrollArea');
$('.anitracker-relation-link').on('click', function() {
if (this.href !== undefined) {
$(this).off();
return;
}
$(this).parents(':eq(2)').find('.anitracker-download-spinner').show();
const animeData = getAnimeData();
if (animeData === undefined) {
hideSpinner(this, 2);
return;
}
const relationType = $(this).attr('relationType');
const relationData = getRelationData(animeData.session, relationType);
if (relationData === undefined) {
hideSpinner(this, 2);
alert(`[AnimePahe Improvements]\n\nNo ${relationType.toLowerCase()} found for this anime.`)
$(this).remove();
return;
}
const episodeSession = relationType === 'Prequel' ? relationData.episodes[relationData.episodes.length-1].session : relationData.episodes[0].session;
windowOpen(`/play/${relationData.session}/${episodeSession}`, '_self');
hideSpinner(this, 2);
});
if ($('.prequel').length === 0) setPrequelPoster();
if ($('.sequel').length === 0) setSequelPoster();
} else {
getTrackerDiv().insertAfter('.anime-content');
}
async function setPrequelPoster() {
const relationData = getRelationData(animeSession, 'Prequel');
if (relationData === undefined) {
$('#anitracker-prequel-link').remove();
return;
}
const relationLink = `/play/${relationData.session}/${relationData.episodes[relationData.episodes.length-1].session}`;
$(`
<div class="prequel hidden-sm-down">
<a href="${relationLink}" title="${toHtmlCodes("Play Last Episode of" + relationData.name)}">
<img style="filter: none;" src="${relationData.poster}" data-src="${relationData.poster}" alt="">
</a>
<i class="fa fa-chevron-left" aria-hidden="true"></i>
</div>`).appendTo('.player');
$('#anitracker-prequel-link').attr('href', relationLink);
$('#anitracker-prequel-link').text(relationData.name);
$('#anitracker-prequel-link').changeElementType('a');
// If auto-clear is on, delete this prequel episode from the tracker
if (getStorage().autoDelete === true) {
deleteEpisodesFromTracker(undefined, relationData.name);
}
}
async function setSequelPoster() {
const relationData = getRelationData(animeSession, 'Sequel');
if (relationData === undefined) {
$('#anitracker-sequel-link').remove();
return;
}
const relationLink = `/play/${relationData.session}/${relationData.episodes[0].session}`;
$(`
<div class="sequel hidden-sm-down">
<a href="${relationLink}" title="${toHtmlCodes("Play First Episode of" + relationData.name)}">
<img style="filter: none;" src="${relationData.poster}" data-src="${relationData.poster}" alt="">
</a>
<i class="fa fa-chevron-right" aria-hidden="true"></i>
</div>`).appendTo('.player');
$('#anitracker-sequel-link').attr('href', relationLink);
$('#anitracker-sequel-link').text(relationData.name);
$('#anitracker-sequel-link').changeElementType('a');
}
if (!isEpisode() && $('#anitracker') != undefined) {
$('#anitracker').attr('style', "max-width: 1100px;margin-left: auto;margin-right: auto;margin-bottom: 20px;")
}
$('#anitracker-refresh-session').on('click', function(e) {
const elem = $('#anitracker-refresh-session');
let timeout = temporaryHtmlChange(elem, 2200, 'Waiting...');
const result = refreshSession();
if (result === 0) {
temporaryHtmlChange(elem, 2200, '<i class="fa fa-refresh" aria-hidden="true" style="animation: anitracker-spin 1s linear infinite;"></i>&nbsp;&nbsp;Refreshing...', timeout);
}
else if (result === 2 || result === 3) {
temporaryHtmlChange(elem, 2200, 'Failed: Couldn\'t find session', timeout);
}
else {
temporaryHtmlChange(elem, 2200, 'Failed.', timeout);
}
});
if (isEpisode()) {
// Replace the download buttons with better ones
if ($('#pickDownload a').length > 0) replaceDownloadButtons();
else {
new MutationObserver(function(mutationList, observer) {
replaceDownloadButtons();
observer.disconnect();
}).observe($('#pickDownload')[0], { childList: true });
}
$(document).on('blur', () => {
$('.dropdown-menu.show').removeClass('show');
});
(() => {
const storage = getStorage();
const foundNotifEpisode = storage.notifications.episodes.find(a => a.session === episodeSession);
if (foundNotifEpisode !== undefined) {
foundNotifEpisode.watched = true;
saveData(storage);
}
})();
}
function replaceDownloadButtons() {
for (const aTag of $('#pickDownload a')) {
$(aTag).changeElementType('span');
}
$('#pickDownload span').on('click', function(e) {
let request = new XMLHttpRequest();
//request.open('GET', `https://opsalar.000webhostapp.com/animepahe.php?url=${$(this).attr('href')}`, true);
request.open('GET', $(this).attr('href'), true);
try {
request.send();
$(this).parents(':eq(1)').find('.anitracker-download-spinner').show();
}
catch (err) {
windowOpen($(this).attr('href'));
}
const dlBtn = $(this);
request.onload = function(e) {
hideSpinner(dlBtn);
if (request.readyState !== 4 || request.status !== 200 ) {
windowOpen(dlBtn.attr('href'));
return;
}
const htmlText = request.response;
const link = /https:\/\/kwik.\w+\/f\/[^"]+/.exec(htmlText);
if (link) {
dlBtn.attr('href', link[0]);
dlBtn.off();
dlBtn.changeElementType('a');
windowOpen(link[0]);
}
else windowOpen(dlBtn.attr('href'));
}
});
}
function stripUrl(url) {
if (url === undefined) {
console.error('[AnimePahe Improvements] stripUrl was used with undefined URL')
return url;
}
const loc = new URL(url);
return loc.origin + loc.pathname;
}
function temporaryHtmlChange(elem, delay, html, timeout = undefined) {
if (timeout !== undefined) clearTimeout(timeout);
if ($(elem).attr('og-html') === undefined) {
$(elem).attr('og-html', $(elem).html());
}
elem.html(html);
return setTimeout(() => {
$(elem).html($(elem).attr('og-html'));
}, delay);
}
$(`
<button class="btn btn-dark" id="anitracker-clear-from-tracker" title="Remove this page from the session tracker">
<i class="fa fa-trash" aria-hidden="true"></i>
&nbsp;Clear from Tracker
</button>`).appendTo('#anitracker');
$('#anitracker-clear-from-tracker').on('click', function() {
const animeName = getAnimeName();
if (isEpisode()) {
deleteEpisodeFromTracker(animeName, getEpisodeNum());
if ($('.embed-responsive-item').length > 0) {
const storage = getStorage();
const videoUrl = stripUrl($('.embed-responsive-item').attr('src'));
for (const videoData of storage.videoTimes) {
if (!videoData.videoUrls.includes(videoUrl)) continue;
const index = storage.videoTimes.indexOf(videoData);
storage.videoTimes.splice(index, 1);
saveData(storage);
break;
}
}
}
else {
const storage = getStorage();
storage.linkList = storage.linkList.filter(a => !(a.type === 'anime' && a.animeName === animeName));
saveData(storage);
}
temporaryHtmlChange($('#anitracker-clear-from-tracker'), 1500, 'Cleared!');
});
function setCoverBlur(img) {
const cover = $('.anime-cover');
const ratio = cover.width()/img.width;
if (ratio <= 1) return;
cover.css('filter', `blur(${(ratio*Math.max((img.height/img.width)**2, 1))*1.6}px)`);
}
if (!isEpisode()) {
function improvePoster() {
if ($('.anime-poster .youtube-preview').length === 0) {
$('.anime-poster .poster-image').attr('target','_blank');
return;
}
$('.anime-poster .youtube-preview').removeAttr('href');
$(`
<a style="display:block;" target="_blank" href="${$('.anime-poster img').attr('src')}">
View full poster
</a>`).appendTo('.anime-poster');
}
if ($('.anime-poster img').attr('src') !== undefined) {
improvePoster();
}
else $('.anime-poster img').on('load', (e) => {
improvePoster();
$(e.target).off('load');
});
$(`
<button class="btn btn-dark" id="anitracker-clear-episodes-from-tracker" title="Clear all episodes from this anime from the session tracker">
<i class="fa fa-trash" aria-hidden="true"></i>
<i class="fa fa-window-maximize" aria-hidden="true"></i>
&nbsp;Clear Episodes from Tracker
</button>`).appendTo('#anitracker');
$('#anitracker-clear-episodes-from-tracker').on('click', function() {
deleteEpisodesFromTracker();
temporaryHtmlChange($('#anitracker-clear-episodes-from-tracker'), 1500, 'Cleared!');
});
const storedObj = getStoredLinkData(initialStorage);
if (storedObj === undefined || storedObj?.coverImg === undefined) updateAnimeCover();
else
{
new MutationObserver(function(mutationList, observer) {
$('.anime-cover').css('background-image', `url("${storedObj.coverImg}")`);
$('.anime-cover').addClass('anitracker-replaced-cover');
const img = new Image();
img.src = storedObj.coverImg;
img.onload = () => {
setCoverBlur(img);
}
observer.disconnect();
}).observe($('.anime-cover')[0], { attributes: true });
}
if (isRandomAnime()) {
const sourceParams = new URLSearchParams(window.location.search);
window.history.replaceState({}, document.title, "/anime/" + animeSession);
const storage = getStorage();
if (storage.cache) {
for (const [key, value] of Object.entries(storage.cache)) {
filterSearchCache[key] = value;
}
delete storage.cache;
saveData(storage);
}
$(`
<div style="margin-left: 240px;">
<div class="btn-group">
<button class="btn btn-dark" id="anitracker-reroll-button"><i class="fa fa-random" aria-hidden="true"></i>&nbsp;Reroll Anime</button>
</div>
<div class="btn-group">
<button class="btn btn-dark" id="anitracker-save-session"><i class="fa fa-floppy-o" aria-hidden="true"></i>&nbsp;Save Session</button>
</div>
</div>`).appendTo('.title-wrapper');
$('#anitracker-reroll-button').on('click', function() {
$(this).text('Rerolling...');
const sourceFilters = new URLSearchParams(sourceParams.toString());
getFilteredList(filterListFromParams(sourceFilters, false)).then((animeList) => {
storage.cache = filterSearchCache;
saveData(storage);
if (sourceParams.has('search')) {
$.getScript('https://cdn.jsdelivr.net/npm/fuse.js@6.6.2', function() {
getRandomAnime(searchList(Fuse, animeList, decodeURIComponent(sourceParams.get('search'))), '?' + sourceParams.toString(), '_self');
});
}
else {
getRandomAnime(animeList, '?' + sourceParams.toString(), '_self');
}
});
});
$('#anitracker-save-session').on('click', function() {
setSessionData();
$('#anitracker-save-session').off();
$(this).text('Saved!');
setTimeout(() => {
$(this).parent().remove();
}, 1500);
});
}
new MutationObserver(function(mutationList, observer) {
const pageNum = (() => {
const elem = $('.pagination');
if (elem.length == 0) return 1;
return +/^(\d+)/.exec($('.pagination').find('.page-item.active span').text())[0];
})();
const episodeSort = $('.episode-bar .btn-group-toggle .active').text().trim();
const episodes = getResponseData(`/api?m=release&sort=episode_${episodeSort}&page=${pageNum}&id=${animeSession}`);
if (episodes === undefined) return undefined;
const episodeElements = $('.episode-wrap');
for (let i = 0; i < episodeElements.length; i++) {
const elem = $(episodeElements[i]);
const date = new Date(episodes[i].created_at + " UTC");
$(`
<span style="margin-left:5%;pointer-events:auto;" title="${date.toDateString() + " " + date.toLocaleTimeString()}">${date.toLocaleDateString()}</span>
`).appendTo(elem.find('.episode-title-wrap'));
}
observer.disconnect();
setTimeout(observer.observe($('.episode-list-wrapper')[0], { childList: true, subtree: true }), 1)
}).observe($('.episode-list-wrapper')[0], { childList: true, subtree: true });
// Bookmark icon
const animename = getAnimeName();
const animeid = getAnimeData(animename).id;
$('h1 .fa').remove();
const notifIcon = (() => {
if (initialStorage.notifications.anime.find(a => a.name === animename) !== undefined) return true;
for (const info of $('.anime-info p>strong')) {
if (!$(info).text().startsWith('Status:')) continue;
return $(info).text().includes("Not yet aired") || $(info).find('a').text() === "Currently Airing";
}
return false;
})() ? `
<i title="Add to episode feed" class="fa fa-bell anitracker-title-icon anitracker-notifications-toggle">
<i style="display: none;" class="fa fa-check anitracker-title-icon-check" aria-hidden="true">
</i>
</i>` : '';
$(`
<i title="Bookmark this anime" class="fa fa-bookmark anitracker-title-icon anitracker-bookmark-toggle">
<i style="display: none;" class="fa fa-check anitracker-title-icon-check" aria-hidden="true">
</i>
</i>
${notifIcon}
<a href="/a/${animeid}" title="Get Link" class="fa fa-link btn anitracker-title-icon" data-toggle="modal" data-target="#modalBookmark"></a>
`).appendTo('.title-wrapper>h1');
if (initialStorage.bookmarks.find(g => g.id === animeid) !== undefined) {
$('.anitracker-bookmark-toggle .anitracker-title-icon-check').show();
}
if (initialStorage.notifications.anime.find(g => g.name === animename) !== undefined) {
$('.anitracker-notifications-toggle .anitracker-title-icon-check').show();
}
$('.anitracker-bookmark-toggle').on('click', (e) => {
const check = $(e.currentTarget).find('.anitracker-title-icon-check');
if (toggleBookmark(animeid, animename)) {
check.show();
return;
}
check.hide();
});
$('.anitracker-notifications-toggle').on('click', (e) => {
const check = $(e.currentTarget).find('.anitracker-title-icon-check');
if (toggleNotifications(animename)) {
check.show();
return;
}
check.hide();
});
}
function getRandomAnime(list, args, openType = '_blank') {
if (list.length === 0) {
alert("[AnimePahe Improvements]\n\nThere is no anime that matches the selected filters.");
return;
}
const random = randint(0, list.length-1);
windowOpen(list[random].link + args, openType);
}
function isRandomAnime() {
return new URLSearchParams(window.location.search).has('anitracker-random');
}
function getBadCovers() {
const storage = getStorage();
return ['https://s.pximg.net/www/images/pixiv_logo.png',
'https://st.deviantart.net/minish/main/logo/card_black_large.png',
'https://www.wcostream.com/wp-content/themes/animewp78712/images/logo.gif',
'https://s.pinimg.com/images/default_open_graph',
'https://share.redd.it/preview/post/',
'https://i.redd.it/o0h58lzmax6a1.png',
'https://ir.ebaystatic.com/cr/v/c1/ebay-logo',
'https://i.ebayimg.com/images/g/7WgAAOSwQ7haxTU1/s-l1600.jpg',
'https://www.rottentomatoes.com/assets/pizza-pie/head-assets/images/RT_TwitterCard',
'https://m.media-amazon.com/images/G/01/social_share/amazon_logo',
'https://zoro.to/images/capture.png',
'https://cdn.myanimelist.net/img/sp/icon/twitter-card.png',
'https://s2.bunnycdn.ru/assets/sites/animesuge/images/preview.jpg',
'https://s2.bunnycdn.ru/assets/sites/anix/preview.jpg',
'https://cdn.myanimelist.net/images/company_no_picture.png',
'https://myanimeshelf.com/eva2/handlers/generateSocialImage.php',
'https://cdn.myanimelist.net/img/sp/icon/apple-touch-icon',
...storage.badCovers];
}
async function updateAnimeCover() {
$(`<div id="anitracker-cover-spinner">
<div class="spinner-border text-danger" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>`).prependTo('.anime-cover');
const request = new XMLHttpRequest();
let beforeYear = 2022;
for (const info of $('.anime-info p')) {
if (!$(info).find('strong').html().startsWith('Season:')) continue;
const year = +/(\d+)$/.exec($(info).find('a').text())[0];
if (year >= beforeYear) beforeYear = year + 1;
}
request.open('GET', 'https://customsearch.googleapis.com/customsearch/v1?key=AIzaSyCzrHsVOqJ4vbjNLpGl8XZcxB49TGDGEFk&cx=913e33346cc3d42bf&tbs=isz:l&q=' + encodeURIComponent(getAnimeName()) + '%20anime%20hd%20wallpaper%20-phone%20-ai%20before:' + beforeYear, true);
request.onload = function() {
if (request.status !== 200) {
$('#anitracker-cover-spinner').remove();
return;
};
if ($('.anime-cover').css('background-image').length > 10) {
decideAnimeCover(request.response);
}
else {
new MutationObserver(function(mutationList, observer) {
if ($('.anime-cover').css('background-image').length <= 10) return;
decideAnimeCover(request.response);
observer.disconnect();
}).observe($('.anime-cover')[0], { attributes: true });
}
};
request.send();
}
function trimHttp(string) {
return string.replace(/^https?:\/\//,'');
}
async function setAnimeCover(src) {
return new Promise(resolve => {
$('.anime-cover').css('background-image', `url("${storedObj.coverImg}")`);
$('.anime-cover').addClass('anitracker-replaced-cover');
const img = new Image();
img.src = src;
img.onload = () => {
setCoverBlur(img);
}
$('.anime-cover').addClass('anitracker-replaced-cover');
$('.anime-cover').css('background-image', `url("${src}")`);
$('.anime-cover').attr('image', src);
$('#anitracker-replace-cover').remove();
$(`<button class="btn btn-dark" id="anitracker-replace-cover" title="Use another cover instead">
<i class="fa fa-refresh" aria-hidden="true"></i>
</button>`).appendTo('.anime-cover');
$('#anitracker-replace-cover').on('click', e => {
const storage = getStorage();
storage.badCovers.push($('.anime-cover').attr('image'));
saveData(storage);
updateAnimeCover();
$(e.target).off();
playAnimation($(e.target).find('i'), 'spin', 'infinite', 1);
});
setCoverBlur(image);
});
}
async function decideAnimeCover(response) {
const badCovers = getBadCovers();
const candidates = [];
let results = [];
try {
results = JSON.parse(response).items;
}
catch (e) {
return;
}
if (results === undefined) {
$('#anitracker-cover-spinner').remove();
return;
}
for (const result of results) {
let imgUrl = result['pagemap']?.['metatags']?.[0]?.['og:image'] ||
result['pagemap']?.['cse_image']?.[0]?.['src'] || result['pagemap']?.['webpage']?.[0]?.['image'] ||
result['pagemap']?.['metatags']?.[0]?.['twitter:image:src'];
const width = result['pagemap']?.['cse_thumbnail']?.[0]?.['width'];
const height = result['pagemap']?.['cse_thumbnail']?.[0]?.['height'];
if (imgUrl === undefined || height < 100 || badCovers.find(a=> trimHttp(imgUrl).startsWith(trimHttp(a))) !== undefined || imgUrl.endsWith('.gif')) continue;
if (imgUrl.startsWith('https://static.wikia.nocookie.net')) {
imgUrl = imgUrl.replace(/\/revision\/latest.*\?cb=\d+$/, '');
}
candidates.push({
src: imgUrl,
width: width,
height: height,
aspectRatio: width / height
});
}
if (candidates.length === 0) return;
candidates.sort((a, b) => {return a.aspectRatio < b.aspectRatio ? 1 : -1});
if (candidates[0].src.includes('"')) return;
const originalBg = $('.anime-cover').css('background-image');
function badImg() {
$('.anime-cover').css('background-image', originalBg);
const storage = getStorage();
for (const anime of storage.linkList) {
if (anime.type === 'anime' && anime.animeSession === animeSession) {
anime.coverImg = /^url\("?([^"]+)"?\)$/.exec(originalBg)[1];
break;
}
}
saveData(storage);
$('#anitracker-cover-spinner').remove();
}
const image = new Image();
image.onload = () => {
if (image.width >= 250) {
$('.anime-cover').addClass('anitracker-replaced-cover');
$('.anime-cover').css('background-image', `url("${candidates[0].src}")`);
$('.anime-cover').attr('image', candidates[0].src);
setCoverBlur(image);
const storage = getStorage();
for (const anime of storage.linkList) {
if (anime.type === 'anime' && anime.animeSession === animeSession) {
anime.coverImg = candidates[0].src;
break;
}
}
saveData(storage);
$('#anitracker-cover-spinner').remove();
}
else badImg();
};
image.addEventListener('error', function() {
badImg();
});
image.src = candidates[0].src;
}
function hideAnimePageThumbnails() {
$("head").append('<style id="anitracker-hide-style" type="text/css"></style>');
let sheet = $("#anitracker-hide-style")[0].sheet;
sheet.insertRule(`
.episode-snapshot img {
display: none;
}`, 0);
sheet.insertRule(`
.episode-snapshot {
border: 4px solid var(--dark);
}`, 1);
}
function hideThumbnails() {
if (isEpisode()) {
$('.anitracker-thumbnail').addClass('anitracker-hide');
}
else {
hideAnimePageThumbnails();
}
}
function addManageDataButton() {
$(`
<button class="btn btn-dark" id="anitracker-show-data" title="View and handle stored sessions and video progress">
<i class="fa fa-floppy-o" aria-hidden="true"></i>
&nbsp;Manage Data...
</button>
<button class="btn btn-dark" id="anitracker-settings" title="Settings">
<i class="fa fa-sliders" aria-hidden="true"></i>
&nbsp;Settings...
</button>`).appendTo('#anitracker');
$('#anitracker-settings').on('click', () => {
$('#anitracker-modal-body').empty();
addOptionSwitch('auto-delete', 'Auto-Clear Links', 'Auto-clearing means only one episode of a series is stored in the tracker at a time.', 'autoDelete');
addOptionSwitch('theatre-mode', 'Theatre Mode', 'Expand the video player for a better experience on bigger screens.', 'theatreMode');
addOptionSwitch('hide-thumbnails', 'Hide Thumbnails', 'Hide thumbnails and preview images.', 'hideThumbnails');
addOptionSwitch('best-quality', 'Default to Best Quality', 'Automatically select the best resolution quality available.', 'bestQuality');
addOptionSwitch('auto-download', 'Automatic Download', 'Automatically download the episode when visiting a download page.', 'autoDownload');
if (isEpisode()) {
$(`
<div class="btn-group">
<button class="btn btn-secondary" id="anitracker-reset-player" title="Reset the video player">
<i class="fa fa-rotate-right" aria-hidden="true"></i>
&nbsp;Reset player
</button></div>`).appendTo('#anitracker-modal-body');
$('#anitracker-reset-player').on('click', function() {
closeModal();
setIframeUrl(stripUrl($('.embed-responsive-item').attr('src')));
});
}
openModal();
});
function openShowDataModal() {
$('#anitracker-modal-body').empty();
$(`
<div class="anitracker-modal-list-container">
<div class="anitracker-storage-data" tabindex="0" key="linkList">
<h4>Session Data</h4>
</div>
<div class="anitracker-modal-list"></div>
</div>
<div class="anitracker-modal-list-container">
<div class="anitracker-storage-data" tabindex="0" key="videoTimes">
<h4>Video Progress</h4>
</div>
<div class="anitracker-modal-list"></div>
</div>
<div class="btn-group">
<button class="btn btn-danger" id="anitracker-reset-data" title="Remove stored data and reset all settings">
<i class="fa fa-undo" aria-hidden="true"></i>
&nbsp;Reset Data
</button>
</div>
<div class="btn-group">
<button class="btn btn-secondary" id="anitracker-raw-data" title="View data in JSON format">
<i class="fa fa-code" aria-hidden="true"></i>
&nbsp;Raw
</button>
</div>
<div class="btn-group">
<button class="btn btn-secondary" id="anitracker-export-data" title="Export and download the JSON data">
<i class="fa fa-download" aria-hidden="true"></i>
&nbsp;Export Data
</button>
</div>
<label class="btn btn-secondary" id="anitracker-import-data-label" tabindex="0" for="anitracker-import-data" style="margin-bottom:0;" title="Import a JSON file with AnimePahe Improvements data. This does not delete any existing data.">
<i class="fa fa-upload" aria-hidden="true"></i>
&nbsp;Import Data
</label>
<div class="btn-group">
<button class="btn btn-dark" id="anitracker-edit-data" title="Edit a key">
<i class="fa fa-pencil" aria-hidden="true"></i>
&nbsp;Edit...
</button>
</div>
<input type="file" id="anitracker-import-data" style="visibility: hidden;" accept=".json">
`).appendTo('#anitracker-modal-body');
const expandIcon = `<svg fill="#ffffff" height="800px" width="800px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 455 455" xml:space="preserve" class="anitracker-expand-data-icon" aria-hidden="true">
<polygon points="455,212.5 242.5,212.5 242.5,0 212.5,0 212.5,212.5 0,212.5 0,242.5 212.5,242.5 212.5,455 242.5,455 242.5,242.5 455,242.5 "/>
</svg>`;
const contractIcon = `<svg fill="#ffffff" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="800px" height="800px" viewBox="0 0 83 83" xml:space="preserve" class="anitracker-expand-data-icon" aria-hidden="true">
<path d="M81,36.166H2c-1.104,0-2,0.896-2,2v6.668c0,1.104,0.896,2,2,2h79c1.104,0,2-0.896,2-2v-6.668 C83,37.062,82.104,36.166,81,36.166z"/>
</svg>`;
$(expandIcon).appendTo('.anitracker-storage-data');
$('.anitracker-storage-data').on('click keydown', (e) => {
if (e.type === 'keydown' && e.key !== "Enter") return;
toggleExpandData($(e.currentTarget));
});
function toggleExpandData(elem) {
if (elem.hasClass('anitracker-expanded')) {
contractData(elem);
}
else {
expandData(elem);
}
}
$('#anitracker-reset-data').on('click', function() {
if (confirm('[AnimePahe Improvements]\n\nThis will remove all saved data and reset it to its default state.\nAre you sure?') === true) {
saveData(getDefaultData());
closeModal();
}
});
$('#anitracker-raw-data').on('click', function() {
const blob = new Blob([JSON.stringify(getStorage(), null, 2)], {type : 'application/json'});
windowOpen(URL.createObjectURL(blob));
});
$('#anitracker-edit-data').on('click', function() {
$('#anitracker-modal-body').empty();
$(`
<b>Warning: for developer use.<br>Back up your data before messing with this.</b>
<input autocomplete="off" class="form-control anitracker-text-input-bar anitracker-edit-data-key" placeholder="Key (Path)">
<input autocomplete="off" class="form-control anitracker-text-input-bar anitracker-edit-data-value" placeholder="Value (JSON)">
<p>Leave value empty to get the existing value</p>
<div class="btn-group">
<button class="btn dropdown-toggle btn-secondary anitracker-edit-mode-dropdown-button" data-bs-toggle="dropdown" data-toggle="dropdown" data-value="replace">Replace</button>
<div class="dropdown-menu anitracker-dropdown-content anitracker-edit-mode-dropdown"></div>
</div>
<div class="btn-group">
<button class="btn btn-primary anitracker-confirm-edit-button">Confirm</button>
</div>
`).appendTo('#anitracker-modal-body');
[{t:'Replace',i:'replace'},{t:'Append',i:'append'},{t:'Delete from list',i:'delList'}].forEach(g => { $(`<button ref="${g.i}">${g.t}</button>`).appendTo('.anitracker-edit-mode-dropdown') });
$('.anitracker-edit-mode-dropdown button').on('click', (e) => {
const pressed = $(e.target)
const btn = pressed.parent().parent().find('.anitracker-edit-mode-dropdown-button');
btn.data('value', pressed.attr('ref'));
btn.text(pressed.text());
});
$('.anitracker-confirm-edit-button').on('click', () => {
const storage = getStorage();
const key = $('.anitracker-edit-data-key').val();
let keyValue = undefined;
try {
keyValue = eval("storage." + key); // lots of evals here because I'm lazy
}
catch (e) {
console.error(e);
alert("Nope didn't work");
return;
}
if ($('.anitracker-edit-data-value').val() === '') {
alert(JSON.stringify(keyValue));
return;
}
if (keyValue === undefined) {
alert("Undefined");
return;
}
const mode = $('.anitracker-edit-mode-dropdown-button').data('value');
let value = undefined;
if (mode === 'delList') {
value = $('.anitracker-edit-data-value').val();
}
else {
try {
value = JSON.parse($('.anitracker-edit-data-value').val());
}
catch (e) {
console.error(e);
alert("Invalid JSON");
return;
}
}
const delFromListMessage = "Please enter a comparison in the 'value' field, with 'a' being the variable for the element.\neg. 'a.id === \"banana\"'\nWhichever elements that match this will be deleted.";
switch (mode) {
case 'replace':
eval(`storage.${key} = value`);
break;
case 'append':
if (keyValue.constructor.name !== 'Array') {
alert("Not a list");
return;
}
eval(`storage.${key}.push(value)`);
break;
case 'delList':
if (keyValue.constructor.name !== 'Array') {
alert("Not a list");
return;
}
try {
eval(`storage.${key} = storage.${key}.filter(a => !(${value}))`);
}
catch (e) {
console.error(e);
alert(delFromListMessage);
return;
}
break;
default:
alert("This message isn't supposed to show up. Uh...");
return;
}
if (JSON.stringify(storage) === JSON.stringify(getStorage())) {
alert("Nothing changed.");
if (mode === 'delList') {
alert(delFromListMessage);
}
return;
}
else alert("Probably worked!");
saveData(storage);
openShowDataModal();
});
openModal(openShowDataModal);
});
$('#anitracker-export-data').on('click', function() {
const storage = getStorage();
if (storage.cache) {
delete storage.cache;
saveData(storage);
}
download('anime-tracker-data-' + Date.now() + '.json', JSON.stringify(getStorage(), null, 2));
});
$('#anitracker-import-data-label').on('keydown', (e) => {
if (e.key === "Enter") $("#" + $(e.currentTarget).attr('for')).click();
});
$('#anitracker-import-data').on('change', function(event) {
const changed = {
linkListAdded: 0,
videoTimesAdded: 0,
videoTimesUpdated: 0,
bookmarksAdded: 0,
notificationsAdded: 0,
episodeFeedAdded: 0,
settingsUpdated: 0
}
const ignoredSettingKeys = [];
const file = this.files[0];
const fileReader = new FileReader();
$(fileReader).on('load', function() {
const storage = getStorage();
let newData = {};
try {
newData = JSON.parse(fileReader.result);
}
catch (err) {
alert('[AnimePahe Improvements]\n\nPlease input a valid JSON file.');
return;
}
for (const [key, value] of Object.entries(newData)) {
if (getDefaultData()[key] === undefined || ignoredSettingKeys.includes(key)) continue;
if (key === 'linkList') {
value.forEach(g => {
if ((g.type === 'episode' && storage.linkList.find(h => h.type === 'episode' && h.animeSession === g.animeSession && h.episodeSession === g.episodeSession) === undefined)
|| (g.type === 'anime' && storage.linkList.find(h => h.type === 'anime' && h.animeSession === g.animeSession) === undefined)) {
storage.linkList.push(g);
changed.linkListAdded++;
}
});
continue;
}
else if (key === 'videoTimes') {
value.forEach(g => {
const foundTime = storage.videoTimes.find(h => h.videoUrls.includes(g.videoUrls[0]));
if (foundTime === undefined) {
storage.videoTimes.push(g);
changed.videoTimesAdded++;
}
else if (foundTime.time < g.time) {
foundTime.time = g.time;
changed.videoTimesUpdated++;
}
});
continue;
}
else if (key === 'bookmarks') {
value.forEach(g => {
if (storage.bookmarks.find(h => h.id === g.id) !== undefined) return;
storage.bookmarks.push(g);
changed.bookmarksAdded++;
});
continue;
}
else if (key === 'notifications') {
value.anime.forEach(g => {
if (storage.notifications.anime.find(h => h.id === g.id) !== undefined) return;
storage.notifications.anime.push(g);
changed.notificationsAdded++;
});
value.episodes.forEach(g => {
if (storage.notifications.episodes.find(h => h.animeName === g.animeName && h.episode === g.episode) !== undefined) return;
storage.notifications.episodes.push(g);
changed.episodeFeedAdded++;
});
storage.notifications.episodes.sort((a,b) => a.time < b.time ? 1 : -1);
if (value.episodes.length > 0) {
storage.notifications.lastUpdated = new Date(value.episodes[0].time + " UTC").getTime();
}
continue;
}
if ((value !== true && value !== false) || storage[key] === undefined || storage[key] === value) continue;
changed.settingsUpdated++;
storage[key] = value;
}
saveData(storage);
let finishString = '';
if (changed.settingsUpdated > 0) updateSwitches();
let totalChanged = 0;
for (const [key, value] of Object.entries(changed)) {
totalChanged += value;
}
if (totalChanged > 0) {
finishString = 'Data imported!\n';
if (changed.linkListAdded > 0) finishString += `\nSession entries added: ${changed.linkListAdded}`;
if (changed.videoTimesAdded > 0) finishString += `\nVideo progress entries added: ${changed.videoTimesAdded}`;
if (changed.videoTimesUpdated > 0) finishString += `\nVideo progress times updated: ${changed.videoTimesUpdated}`;
if (changed.bookmarksAdded > 0) finishString += `\nBookmarks added: ${changed.bookmarksAdded}`;
if (changed.notificationsAdded > 0) finishString += `\nAnime added to episode feed: ${changed.notificationsAdded}`;
if (changed.episodeFeedAdded > 0) finishString += `\nEpisodes added to episode feed: ${changed.episodeFeedAdded}`;
if (changed.settingsUpdated > 0) finishString += `\nSettings updated: ${changed.settingsUpdated}`;
}
else finishString = 'No data was updated.';
alert('[AnimePahe Improvements]\n\n' + finishString);
if (totalChanged > 0) openShowDataModal();
});
fileReader.readAsText(file);
});
function expandData(elem) {
const storage = getStorage();
elem.find('.anitracker-expand-data-icon').replaceWith(contractIcon);
const dataEntries = elem.parent().find('.anitracker-modal-list');
$(`
<div class="btn-group anitracker-storage-filter">
<input autocomplete="off" class="form-control anitracker-text-input-bar anitracker-modal-search" placeholder="Search">
<button dir="down" class="btn btn-secondary dropdown-toggle anitracker-reverse-order-button" title="Sort direction (down is default, and means newest first)"></button>
</div>
`).appendTo(dataEntries);
elem.parent().find('.anitracker-modal-search').focus();
elem.parent().find('.anitracker-modal-search').on('input', (e) => {
setTimeout(() => {
const query = $(e.target).val();
for (const entry of elem.parent().find('.anitracker-modal-list-entry')) {
if ($($(entry).find('a,span')[0]).text().toLowerCase().includes(query)) {
$(entry).show();
continue;
}
$(entry).hide();
}
}, 10);
});
// When clicking the reverse order button
elem.parent().find('.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 elem.parent().find('.anitracker-modal-list-entry')) {
entries.push(entry.outerHTML);
}
entries.reverse();
elem.parent().find('.anitracker-modal-list-entry').remove();
for (const entry of entries) {
$(entry).appendTo(elem.parent().find('.anitracker-modal-list'));
}
applyDeleteEvents();
});
function applyDeleteEvents() {
$('.anitracker-modal-list-entry .anitracker-delete-session-button').on('click', function() {
const storage = getStorage();
const href = $(this).parent().find('a').attr('href');
const animeSession = getAnimeSessionFromUrl(href);
if (isEpisode(href)) {
const episodeSession = getEpisodeSessionFromUrl(href);
storage.linkList = storage.linkList.filter(g => !(g.type === 'episode' && g.animeSession === animeSession && g.episodeSession === episodeSession));
saveData(storage);
}
else {
storage.linkList = storage.linkList.filter(g => !(g.type === 'anime' && g.animeSession === animeSession));
saveData(storage);
}
$(this).parent().remove();
});
$('.anitracker-modal-list-entry .anitracker-delete-progress-button').on('click', function() {
const storage = getStorage();
storage.videoTimes = storage.videoTimes.filter(g => !g.videoUrls.includes(elem.attr('lookForUrl')))
saveData(storage);
$(this).parent().remove();
});
}
if (elem.attr('key') === 'linkList') {
[...storage.linkList].reverse().forEach(g => {
const name = g.animeName + (g.type === 'episode' ? (' - Episode ' + g.episodeNum) : '');
$(`
<div class="anitracker-modal-list-entry">
<a target="_blank" href="/${(g.type === 'episode' ? 'play/' : 'anime/') + g.animeSession + (g.type === 'episode' ? ('/' + g.episodeSession) : '')}" title="${toHtmlCodes(name)}">
${name}
</a><br>
<button class="btn btn-danger anitracker-delete-session-button" title="Delete this stored session">
<i class="fa fa-trash" aria-hidden="true"></i>
&nbsp;Delete
</button>
</div>`).appendTo(elem.parent().find('.anitracker-modal-list'));
});
applyDeleteEvents();
}
else if (elem.attr('key') === 'videoTimes') {
[...storage.videoTimes].reverse().forEach(g => {
$(`
<div class="anitracker-modal-list-entry">
<span>
${g.animeName} - Episode ${g.episodeNum}
</span><br>
<span>
Current time: ${secondsToHMS(g.time)}
</span><br>
<button class="btn btn-danger anitracker-delete-progress-button" lookForUrl="${g.videoUrls[0]}" title="Delete this video progress">
<i class="fa fa-trash" aria-hidden="true"></i>
&nbsp;Delete
</button>
</div>`).appendTo(elem.parent().find('.anitracker-modal-list'));
});
applyDeleteEvents();
}
elem.addClass('anitracker-expanded');
}
function contractData(elem) {
elem.find('.anitracker-expand-data-icon').replaceWith(expandIcon);
elem.parent().find('.anitracker-modal-list').empty();
elem.removeClass('anitracker-expanded');
elem.blur();
}
openModal();
}
$('#anitracker-show-data').on('click', openShowDataModal);
}
addManageDataButton();
if (isEpisode()) {
$(`
<span style="margin-left: 30px;"><i class="fa fa-files-o" aria-hidden="true"></i>&nbsp;Copy:</span>
<div class="btn-group">
<button class="btn btn-dark anitracker-copy-button" copy="link" data-placement="top" data-content="Copied!">Link</button>
</div>
<div class="btn-group" style="margin-right:30px;">
<button class="btn btn-dark anitracker-copy-button" copy="link-time" data-placement="top" data-content="Copied!">Link & Time</button>
</div>`).appendTo('#anitracker');
addOptionSwitch('autoplay','Auto-play','Automatically go to the next episode when the current one has ended.','autoPlay','#anitracker');
$('.anitracker-copy-button').on('click', (e) => {
const targ = $(e.currentTarget);
const type = targ.attr('copy');
const name = encodeURIComponent(getAnimeName());
const episode = getEpisodeNum();
if (type === 'link') {
navigator.clipboard.writeText(window.location.origin + '/customlink?a=' + name + '&e=' + episode);
}
else if (type === 'link-time') {
navigator.clipboard.writeText(window.location.origin + '/customlink?a=' + name + '&e=' + episode + '&t=' + currentEpisodeTime.toString());
}
targ.popover('show');
setTimeout(() => {
targ.popover('hide');
}, 1000);
});
}
if (initialStorage.autoDelete === true && isEpisode() && paramArray.find(a => a[0] === 'ref' && a[1] === 'customlink') === undefined) {
deleteEpisodesFromTracker(getEpisodeNum());
}
function updateSwitches() {
const storage = getStorage();
for (const s of optionSwitches) {
if (s.value !== storage[s.optionId]) {
s.value = storage[s.optionId];
}
if (s.value === true) {
if (s.onEvent !== undefined) s.onEvent();
}
else if (s.offEvent !== undefined) {
s.offEvent();
}
}
optionSwitches.forEach(s => {
$(`#anitracker-${s.switchId}-switch`).prop('checked', storage[s.optionId] === true);
$(`#anitracker-${s.switchId}-switch`).change();
});
}
updateSwitches();
function addOptionSwitch(id, name, desc = '', optionId, parent = '#anitracker-modal-body') {
const option = (() => {
for (const s of optionSwitches) {
if (s.optionId !== optionId) continue;
return s;
}
})();
$(`
<div class="custom-control custom-switch anitracker-switch" id="anitracker-${id}" title="${desc}">
<input type="checkbox" class="custom-control-input" id="anitracker-${id}-switch">
<label class="custom-control-label" for="anitracker-${id}-switch">${name}</label>
</div>`).appendTo(parent);
const switc = $(`#anitracker-${id}-switch`);
switc.prop('checked', option.value);
const events = [option.onEvent, option.offEvent];
switc.on('change', (e) => {
const checked = $(e.currentTarget).is(':checked');
const storage = getStorage();
if (checked !== storage[optionId]) {
storage[optionId] = checked;
option.value = checked;
saveData(storage);
}
if (checked) {
if (events[0] !== undefined) events[0]();
}
else if (events[1] !== undefined) events[1]();
});
}
$(`
<div class="anitracker-download-spinner" style="display: none;">
<div class="spinner-border text-danger" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>`).prependTo('#downloadMenu,#episodeMenu');
$('.prequel img,.sequel img').attr('loading','');
}
@striker222233333
Copy link

real?

@Ellivers
Copy link
Author

real?

Yes, very real. I've spent more than 2 years working on it now.

@jeryjs
Copy link

jeryjs commented Sep 1, 2024

Thats a really impressive script!! I wish I'd found it earlier.
I recommend publishing it on greasyfork since it's the most popular platform for sharing userscripts and also to help people better find your amazing work. (personally, i just wish I'd have found it much earlier if it were on greasyfork lol)

@jeryjs
Copy link

jeryjs commented Sep 1, 2024

btw, I'd also like to suggest saving/restoring the last selected player speed.
I think it would be best to keep it simple like -

  player.playbackRate = localStorage.getItem("playerSpeed") || 1;
  player.onratechange = () => localStorage.setItem("playerSpeed", player.playbackRate);

@Striker22223
Copy link

Bro it doesn't work u can't download it and use it ;-;

@Striker22223
Copy link

It say zip error

@Ellivers
Copy link
Author

Ellivers commented Sep 1, 2024

Are you following the instructions I put at the top of the file (beneath the "UserScript" stuff)? You don't have to download it.

@striker222233333
Copy link

sweet this is perfect i love this

@Striker22223
Copy link

Hi is there any way to get this for mobile

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment