Skip to content

Instantly share code, notes, and snippets.

@Ellivers
Last active May 19, 2024 20:52
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.14
// @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.
* 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'
* Speed up or slow down a video by holding Ctrl and:
* Scrolling up/down
* Pressing the up/down keys
* You can also hold shift to make the speed change more gradual.
* 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.
* Focuses on the video player when loading the page, so you don't have to click on it to use keyboard controls.
* Automatically chooses the highest quality available when loading the video.
* Shows the dates of when episodes were added.
* And more!
*/
const baseUrl = window.location.toString();
const initialStorage = getStorage();
function getDefaultData() {
return {linkList:[], videoTimes:[], bookmarks:[], autoDelete:true, hideThumbnails:false, theatreMode:false, bestQuality:true};
}
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 currently has
// (You're not actually blocked)
if (!kwikDLPageRegex.test(baseUrl) && h1.textContent == "Sorry, you have been blocked") {
h1.textContent = "Oops, page failed to load.";
document.querySelector('h2').textContent = "Try playing from another page instead.";
}
return;
}
scriptElem.onload(() => {anitrackerKwikLoad(window.location.origin + window.location.pathname)});
}
function anitrackerKwikLoad(url) {
if (kwikDLPageRegex.test(url)) {
$(`
<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;">[Anime Tracker] 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);
}
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(`[Anime Tracker]\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', updateTime);
player.addEventListener('seeked', () => {
updateTime();
removeLoadingIndicators();
});
const frametime = 1 / 24;
$(document).on('keydown', function(e) {
if (e.key === 'ArrowUp') changeSpeed(e, -1);
if (e.key === 'ArrowDown') changeSpeed(e, 1);
if (e.ctrlKey || e.altKey || e.shiftKey || e.metaKey) return;
if (e.key === 'j') {
player.currentTime = Math.max(0, player.currentTime - 10);
}
else if (e.key === 'l') {
player.currentTime = Math.min(player.duration, player.currentTime + 10);
}
else if (/^Numpad\d$/.test(e.code)) {
player.currentTime = (player.duration/10)*(+e.code.replace('Numpad', ''));
}
if (player.currentTime > 0 && !player.paused && !player.ended && player.readyState > 2) return;
if (e.key === ',') {
player.currentTime = Math.max(0, player.currentTime - frametime);
}
else if (e.key === '.') {
player.currentTime = Math.min(player.duration, player.currentTime + frametime);
}
});
// Ctrl+scrolling to change speed
$(`
<div class="anitracker-video-speed" 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 });
}
};
let showSpeedChange = undefined;
let settingsContainerId = undefined;
for (const elem of $('.plyr__menu__container')) {
regex = /plyr\-settings\-(\d+)/.exec(elem.id)
if (regex === null) continue;
settingsContainerId = regex[1];
}
const defaultSpeeds = Array.from($(`#plyr-settings-${settingsContainerId}-speed>div>button`)).map(a => +$(a).attr('value'));
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;
$('.anitracker-video-speed span').text(player.playbackRate + "x");
$('.anitracker-video-speed').css('display', 'flex');
clearTimeout(showSpeedChange);
showSpeedChange = setTimeout(() => {
$('.anitracker-video-speed').hide();
}, 1000);
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('[Anime Tracker]', initialStorage);
if (initialStorage.hideThumbnails === true) {
hideThumbnails();
}
function windowOpen(url, target = '_blank') {
$(`<a href="${url}" target="${target}"></a>`)[0].click();
}
// -------- Anime Tracker 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: end;
flex-wrap: wrap;
}
#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 {
width: 24px;
height: 24px;
margin: 20px 20px;
cursor: pointer;
}
#anitracker-modal-body {
padding: 10px;
overflow-x: hidden;
}
#anitracker-modal-body .anitracker-switch {margin-bottom: 2px;\n}
.anitracker-collection-item {list-style: none;\n}
.anitracker-collection-item a {
font-size: 0.875rem;
display: block;
padding: 5px 15px;
color: rgb(238, 238, 238);
text-decoration: none;
}
.anitracker-collection-item img {
margin: auto 0px;
width: 50px;
height: 50px;
border-radius: 100%;
}
.anitracker-collection-item .anitracker-main-text {
font-weight: 700;
color: rgb(238, 238, 238);
}
.anitracker-collection-item .anitracker-subtext {
font-size: 0.75rem;
color: rgb(153, 153, 153);
}
.anitracker-collection-item:hover .anitracker-subtext {
color: rgb(238, 238, 238);
}
.anitracker-collection-item:hover {
background-color: var(--pink);
}
.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 {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 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-storage-filter button {
height: 42px;
border-radius: 7px!important;
font-size: 2em;
color: #ddd!important;
margin-left: 10px!important;
}
.anitracker-storage-filter button::after {
vertical-align: 20px;
}
.anitracker-storage-filter 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-bookmark-check {
color: white;
margin-left: -.7rem;
font-size: 1rem;
vertical-align: super;
text-shadow: none;
}
.anitracker-header-bookmark {
margin-right: 1%;
margin-left: 1%;
color: white;
background: none;
border: 2px solid white;
border-radius: 5px;
width: 2rem;
}
.anitracker-header-bookmark:hover {
border-color: #ff006c;
color: #ff006c;
}
@keyframes anitracker-modalOpen {
0% {
transform: scale(0.5);
}
20% {
transform: scale(1.07);
}
100% {
transform: scale(1);
}
}
@keyframes anitracker-fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
`.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: () => {
if (window.innerWidth <= 1375) return;
$('.theatre>').css('max-width', '80%');
},
offEvent: () => {
$('.theatre>').css('max-width', '');
}
},
{
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
}];
$(document).on('visibilitychange', () => {
if (document.hidden) return;
updateSwitches();
});
function playAnimation(elem, anim, type = '', duration) {
return new Promise(resolve => {
elem.css('animation', `anitracker-${anim} ${duration || animationTimes[anim]}s forwards ${type}`);
setTimeout(() => {
elem.css('animation', '');
resolve();
}, animationTimes[anim] * 1000);
});
}
// Anime Tracker modal
function addModal() {
$(`
<div id="anitracker-modal">
<div id="anitracker-modal-content">
<svg id="anitracker-modal-close" fill="#ffffff" height="800px" width="800px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 460.775 460.775" xml:space="preserve">
<path d="M285.08,230.397L456.218,59.27c6.076-6.077,6.076-15.911,0-21.986L423.511,4.565c-2.913-2.911-6.866-4.55-10.992-4.55 c-4.127,0-8.08,1.639-10.993,4.55l-171.138,171.14L59.25,4.565c-2.913-2.911-6.866-4.55-10.993-4.55 c-4.126,0-8.08,1.639-10.992,4.55L4.558,37.284c-6.077,6.075-6.077,15.909,0,21.986l171.138,171.128L4.575,401.505
c-6.074,6.077-6.074,15.911,0,21.986l32.709,32.719c2.911,2.911,6.865,4.55,10.992,4.55c4.127,0,8.08-1.639,10.994-4.55 l171.117-171.12l171.118,171.12c2.913,2.911,6.866,4.55,10.993,4.55c4.128,0,8.081-1.639,10.992-4.55l32.709-32.719 c6.074-6.075,6.074-15.909,0-21.986L285.08,230.397z"></path>
</svg>
<div id="anitracker-modal-body"></div>
</div>
</div>`).insertBefore('.main-header');
$('#anitracker-modal').on('click', (e) => {
if (e.target !== e.currentTarget) return;
closeModal();
});
$('#anitracker-modal-close').on('click', () => {
closeModal();
});
$(document).on('keydown', (e) => {
if (modalIsOpen() && e.key === 'Escape') {
closeModal();
}
});
}
addModal();
function openModal() {
playAnimation($('#anitracker-modal-content'), 'modalOpen');
playAnimation($('#anitracker-modal'), 'fadeIn');
$('#anitracker-modal').css('display','flex');
}
function closeModal() {
playAnimation($('#anitracker-modal'), 'fadeIn', 'reverse', 0.1).then(() => {
$('#anitracker-modal').hide();
});
}
function modalIsOpen() {
return $('#anitracker-modal').is(':visible');
}
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="${baseName} - Collection">
<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');
$('.anitracker-collection').on('click', function() {
$('#anitracker-modal-body').empty();
for (const anime of seriesList) {
$(`
<div class="anitracker-collection-item">
<a href="/anime/${anime.session}" title="${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');
}
openModal();
});
}
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 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;
}
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 + '?from=customlink';
}
if (parts.animeSession !== '' && parts.episodeSession !== '' && parts.time === -1) {
return '/play/' + parts.animeSession + '/' + parts.episodeSession + '?from=customlink';
}
if (parts.animeSession !== '' && parts.episodeSession !== '' && parts.time >= 0) {
return '/play/' + parts.animeSession + '/' + parts.episodeSession + '?time=' + parts.time + '&from=customlink';
}
return undefined;
})();
if (destination !== undefined) {
$('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 ($($('h1')[0]).text().includes('404')) 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;
}
// Bookmark header button
$(`
<button class="anitracker-header-bookmark" title="View bookmarks"><i class="fa fa-bookmark"></i></button>
`).insertAfter('.navbar-nav');
$('.anitracker-header-bookmark').on('click', () => {
$('#anitracker-modal-body').empty();
const storage = getStorage();
$("<h4>Bookmarks</h4>").appendTo('#anitracker-modal-body');
$(`
<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.bookmarks.forEach(g => {
$(`
<div class="anitracker-modal-list-entry" animeid="${g.id}">
<a href="/a/${g.id}">
${g.name}
</a><br>
<button class="btn btn-danger">
<i class="fa fa-trash" aria-hidden="true"></i>
&nbsp;Delete
</button>
</div>`).appendTo('#anitracker-modal-body .anitracker-modal-list')});
if (storage.bookmarks.length === 0) {
$("<span>No bookmarks yet!</span>").appendTo('#anitracker-modal-body .anitracker-modal-list');
}
$('.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-check').hide();
}
$(e.currentTarget).parent().remove();
});
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;
}
// Search/index page
if (/^\/anime\/?$/.test(window.location.pathname)) {
$(`
<div id="anitracker" style="margin-bottom: 10px;">
<button class="btn btn-dark" id="anitracker-random-anime">
<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">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">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">All</button>
</div>
<div class="btn-group">
<button class="btn btn-dark" id="anitracker-time-search-button">
<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">
<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')) 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');
if (newRule === 'and' && appliedFilters.filter(a => a.startsWith(type + '/')).length > 1 && appliedFilters.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;
}
}
filterBox.focus();
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" 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" 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" class="dropdown-menu anitracker-dropdown-content anitracker-items-dropdown">`).insertAfter('#anitracker-type-list');
getDropdownButtons(filterValues.type, 'type').forEach(g => { g.appendTo('#anitracker-type-dropdown') });
$(`<div id="anitracker-demographic-dropdown" 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" 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:12px;">
<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('[Anime Tracker]\n\nYear values must both be 4 numbers.');
return;
}
if (to.year < from.year || (to.year === from.year && to.season < from.season)) {
alert('[Anime Tracker]\n\nSeason times must be from oldest to newest.');
return;
}
if (to.year - from.year > 100) {
alert('[Anime Tracker]\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('[Anime Tracker] Search 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) {
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 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;
}
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('[Anime Tracker]\n\nThe session was outdated, and has been refreshed. Please try that link again.')
}
window.history.replaceState({}, document.title, window.location.origin + window.location.pathname);
}
// 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) {
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 2;
}
const obj = getStoredLinkData(initialStorage);
const is404 = $('h1').text().includes('404');
if (isEpisode() && !is404) $('#downloadMenu').changeElementType('button');
console.log('[Anime Tracker]', 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) {
$('.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;
}
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 ($('#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);
}
}
}
const linkFromArg = paramArray.find(a => a[0] === 'from');
if (linkFromArg !== undefined) {
if (isEpisode() && initialStorage.autoDelete && obj !== undefined) {
$(`
<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">Dismiss</span>
</span>`).prependTo('.content-wrapper');
$('.anitracker-from-share-warning>span').on('click', function(e) {
$(e.target).parent().remove();
});
}
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">
<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(`[Anime Tracker]\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);
});
const animeData = getAnimeData();
if (animeData !== undefined) {
const animeSession = animeData.session;
const firstEpisodes = getResponseData('/api?m=release&sort=episode_asc&id=' + animeSession);
const lastEpisodes = getResponseData('/api?m=release&sort=episode_desc&id=' + animeSession);
if (firstEpisodes !== undefined && lastEpisodes !== undefined && firstEpisodes.length > 0) {
let episode = getEpisodeNum();
if (episode === firstEpisodes[0].episode) {
setPrequelPoster();
}
if (episode === lastEpisodes[0].episode) {
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="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="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() {
$('#anitracker-refresh-session').text('Waiting...');
const result = refreshSession();
if (result === 0) {
$('#anitracker-refresh-session').text('Refreshing...');
}
else if (result === 1) {
$('#anitracker-refresh-session').text('Failed: Link not found in tracker');
setTimeout(() => {
$('#anitracker-refresh-session').text('Refresh Session');
}, 2200);
}
else {
$('#anitracker-refresh-session').text('Failed.');
setTimeout(() => {
$('#anitracker-refresh-session').text('Refresh Session');
}, 2200);
}
});
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');
});
}
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) {
const loc = new URL(url);
return loc.origin + loc.pathname;
}
$(`
<button class="btn btn-dark" id="anitracker-clear-from-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);
}
$('#anitracker-clear-from-tracker').text('Cleared!');
setTimeout(() => {
$('#anitracker-clear-from-tracker').text('Clear from Tracker');
}, 1500);
});
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">
<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();
$('#anitracker-clear-episodes-from-tracker').text('Cleared!');
setTimeout(() => {
$('#anitracker-clear-episodes-from-tracker').text('Clear Episodes from Tracker');
}, 1500);
});
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();
$(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]);
$(`
<span style="margin-left:5%;pointer-events:auto;" title="Upload date">${new Date(episodes[i].created_at).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();
[$(`
<i title="Bookmark ${animename}" class="fa fa-bookmark anitracker-title-icon anitracker-bookmark-toggle">
<i style="display: none;" class="fa fa-check anitracker-bookmark-check">
</i>
</i>`),
$(`
<a href="/a/${animeid}" title="Get Link" class="fa fa-link btn anitracker-title-icon" data-toggle="modal" data-target="#modalBookmark"></a>
`)].forEach(a => a.appendTo('.title-wrapper>h1'));
if (initialStorage.bookmarks !== undefined && initialStorage.bookmarks.find(g => g.id === animeid)) {
$('.anitracker-bookmark-check').show();
}
$('.anitracker-bookmark-toggle').on('click', (e) => {
const check = $(e.currentTarget).find('.anitracker-bookmark-check');
if (toggleBookmark(animeid, animename)) {
check.show();
return;
}
check.hide();
});
}
function getRandomAnime(list, args, openType = '_blank') {
const random = randint(0, list.length-1);
windowOpen(list[random].link + args, openType);
}
function isRandomAnime() {
return new URLSearchParams(window.location.search).has('anitracker-random');
}
const badCovers = ['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'];
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) {
setAnimeCover(request.response);
}
else {
new MutationObserver(function(mutationList, observer) {
if ($('.anime-cover').css('background-image').length <= 10) return;
setAnimeCover(request.response);
observer.disconnect();
}).observe($('.anime-cover')[0], { attributes: true });
}
};
request.send();
}
function trimHttp(string) {
return string.replace(/^https?:\/\//,'');
}
function setAnimeCover(response) {
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) 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}")`);
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 mergeData(newData, ignoredKeys = []) {
const storage = getStorage();
const changed = {
linkListAdded: 0,
videoTimesAdded: 0,
videoTimesUpdated: 0,
bookmarksAdded: 0,
settingsUpdated: 0
}
for (const [key, value] of Object.entries(newData)) {
if (getDefaultData()[key] === undefined || ignoredKeys.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;
}
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;
}
if (key === 'bookmarks') {
value.forEach(g => {
if (storage.bookmarks.find(h => h.id === g.id) !== undefined) return;
storage.bookmarks.push(g);
changed.bookmarksAdded++;
});
continue;
}
if ((value !== true && value !== false) || storage[key] === undefined || storage[key] === value) continue;
changed.settingsUpdated++;
storage[key] = value;
}
saveData(storage);
if (changed.settingsUpdated > 0) updateSwitches();
let totalChanged = 0;
for (const [key, value] of Object.entries(changed)) {
totalChanged += value;
}
changed.total = totalChanged;
return changed;
}
function addManageDataButton() {
$(`
<button class="btn btn-dark" id="anitracker-show-data">
<i class="fa fa-floppy-o" aria-hidden="true"></i>
&nbsp;Manage Data...
</button>
<button class="btn btn-dark" id="anitracker-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');
openModal();
});
$('#anitracker-show-data').on('click', function() {
$('#anitracker-modal-body').empty();
$(`
<div class="anitracker-modal-list-container">
<div class="anitracker-storage-data" 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" 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">
<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">
<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">
<i class="fa fa-download" aria-hidden="true"></i>
&nbsp;Export Data
</button>
</div>
<label class="btn btn-secondary" for="anitracker-import-data" style="margin-bottom:0;">
<i class="fa fa-upload" aria-hidden="true"></i>
&nbsp;Import Data
</label>
<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">
<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">
<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', expandData);
$('#anitracker-reset-data').on('click', function() {
if (confirm('[Anime Tracker]\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-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').on('change', function(event) {
const file = this.files[0];
const fileReader = new FileReader();
$(fileReader).on('load', function() {
let newData = {};
try {
newData = JSON.parse(fileReader.result);
}
catch (err) {
alert('[Anime Tracker]\n\nPlease input a valid JSON file.');
return;
}
const changed = mergeData(newData);
let finishString = '';
if (changed.total > 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.settingsUpdated > 0) finishString += `\nSettings updated: ${changed.settingsUpdated}`;
}
else finishString = 'No data was updated.';
alert('[Anime Tracker]\n\n' + finishString);
if (changed.total > 0) closeModal();
});
fileReader.readAsText(file);
});
function expandData() {
const storage = getStorage();
$(this).find('.anitracker-expand-data-icon').replaceWith(contractIcon);
const dataEntries = $(this).parent().find('.anitracker-modal-list');
$(`
<div class="btn-group anitracker-storage-filter">
<input autocomplete="off" class="form-control anitracker-text-input-bar anitracker-storage-data-search" placeholder="Search">
<button dir="down" class="btn btn-secondary dropdown-toggle"></button>
</div>
`).appendTo(dataEntries);
$(this).parent().find('.anitracker-storage-data-search').focus();
$(this).parent().find('.anitracker-storage-data-search').on('input', (e) => {
setTimeout(() => {
const query = $(e.target).val();
for (const entry of $(this).parent().find('.anitracker-modal-list-entry')) {
if ($($(entry).find('a,span')[0]).text().toLowerCase().includes(query)) {
$(entry).show();
continue;
}
$(entry).hide();
}
}, 10);
});
$(this).parent().find('.anitracker-storage-filter 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 $(this).parent().find('.anitracker-modal-list-entry')) {
entries.push(entry.outerHTML);
}
entries.reverse();
$(this).parent().find('.anitracker-modal-list-entry').remove();
for (const entry of entries) {
$(entry).appendTo($(this).parent().find('.anitracker-modal-list'));
}
applyDeleteEvents();
});
function applyDeleteEvents() {
$('.anitracker-modal-list-entry 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();
});
}
if ($(this).attr('key') === 'linkList') {
storage.linkList.forEach(g => {
$(`
<div class="anitracker-modal-list-entry">
<a target="_blank" href="/${(g.type === 'episode' ? 'play/' : 'anime/') + g.animeSession + (g.type === 'episode' ? ('/' + g.episodeSession) : '')}">
${g.animeName}${g.type === 'episode' ? (' - Episode ' + g.episodeNum) : ''}
</a><br>
<button class="btn btn-danger">
<i class="fa fa-trash" aria-hidden="true"></i>
&nbsp;Delete
</button>
</div>`).appendTo($(this).parent().find('.anitracker-modal-list'));
});
applyDeleteEvents();
}
else if ($(this).attr('key') === 'videoTimes') {
storage.videoTimes.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" lookForUrl="${g.videoUrls[0]}">
<i class="fa fa-trash" aria-hidden="true"></i>
&nbsp;Delete
</button>
</div>`).appendTo($(this).parent().find('.anitracker-modal-list'));
});
$('.anitracker-modal-list-entry button').on('click', function() {
const storage = getStorage();
storage.videoTimes = storage.videoTimes.filter(g => !g.videoUrls.includes($(this).attr('lookForUrl')))
saveData(storage);
$(this).parent().remove();
});
}
$(this).off('click', expandData);
$(this).on('click', contractData);
}
function contractData() {
$(this).find('.anitracker-expand-data-icon').replaceWith(expandIcon);
$(this).parent().find('.anitracker-modal-list').empty();
$(this).off('click', contractData);
$(this).on('click', expandData);
}
openModal();
});
}
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">
<button class="btn btn-dark anitracker-copy-button" copy="link-time" data-placement="top" data-content="Copied!">Link & Time</button>
</div>`).appendTo('#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') {
const videoUrl = stripUrl($('.embed-responsive-item').attr('src'));
const time = (() => {
for (const time of getStorage().videoTimes) {
if (!time.videoUrls.includes(videoUrl)) continue;
return time.time;
}
})();
navigator.clipboard.writeText(window.location.origin + '/customlink?a=' + name + '&e=' + episode + '&t=' + Math.floor(time).toString());
}
targ.popover('show');
setTimeout(() => {
targ.popover('hide');
}, 1000);
});
}
if (initialStorage.autoDelete === true && isEpisode() && linkFromArg == 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();
}
}
if (modalIsOpen()) {
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) {
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('#anitracker-modal-body');
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','');
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment