AnimePahe Improvements

Improvements and additions for the AnimePahe site

// ==UserScript==
// @name        AnimePahe Improvements
// @namespace   https://gist.github.com/Ellivers/f7716b6b6895802058c367963f3a2c51
// @match       https://animepahe.com/*
// @match       https://animepahe.org/*
// @match       https://animepahe.ru/*
// @match       https://kwik.*/e/*
// @match       https://kwik.*/f/*
// @grant       GM_getValue
// @grant       GM_setValue
// @version     4.1.0
// @author      Ellivers
// @license     MIT
// @description Improvements and additions for the AnimePahe site
// ==/UserScript==

/*
   How to install:
 * Get the Violentmonkey browser extension (Tampermonkey is largely untested, but seems to work as well).
 * For the GitHub Gist page, click the "Raw" button on this page.
 * For Greasy Fork, click "Install this script".
 * I highly suggest using an ad blocker (uBlock Origin is recommended)

   Feature list:

 * Automatically redirects to the correct session when a tab with an old session is loaded. No more having to search for the anime and find the episode again!
 * Saves your watch progress of each video, so you can resume right where you left off.
 * Bookmark anime and view it in a bookmark menu.
 * Add ongoing anime to an episode feed to easily check when new episodes are out.
 * Quickly visit the download page for a video, instead of having to wait 5 seconds when clicking the download link.
 * Find collections of anime series in the search results, with the series listed in release order.
 * Jump directly to the next anime's first episode from the previous anime's last episode, and the other way around.
 * Hide all episode thumbnails on the site, for those who are extra wary of spoilers (and for other reasons).
 * Saved data can be viewed and deleted in the "Manage Data" menu.
 * 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.
 * Automatically finds a relevant cover for the top of anime pages.
 * Adds points in the video player progress bar for opening, ending, and other highlights (only available for some anime).
 * Adds a button to skip openings and endings when they start (only available for some anime).
 * Frame-by-frame controls on videos, using ',' and '.'
 * Skip 10 seconds on videos at a time, using 'J' and 'L'
 * Changes the video 'loop' keybind to Shift + L
 * Press Shift + N to go to the next episode, and Shift + P to go to the previous one.
 * Speed up or slow down a video by holding Ctrl and:
    * Scrolling up/down
    * Pressing the up/down keys
    * You can also hold shift to make the speed change more gradual.
 * Remembers the selected speed for each anime.
 * Enables you to see images from the video while hovering over the progress bar.
 * Allows you to also use numpad number keys to seek through videos.
 * Theatre mode for a better non-fullscreen video experience on larger screens.
 * Instantly loads the video instead of having to click a button to load it.
 * Adds an "Auto-Play Video" option to automatically play the video (on some browsers, you may need to allow auto-playing for this to work).
 * Adds an "Auto-Play Next" option to automatically go to the next episode when the current one is finished.
 * Focuses on the video player when loading the page, so you don't have to click on it to use keyboard controls.
 * Adds an option to automatically choose the highest quality available when loading the video.
 * Adds a button (in the settings menu) to reset the video player.
 * Shows the dates of when episodes were added.
 * And more!
*/

const baseUrl = window.location.toString();
const initialStorage = getStorage();

function getDefaultData() {
  return {
    version: 2,
    linkList:[],
    videoTimes:[],
    bookmarks:[],
    notifications: {
      lastUpdated: Date.now(),
      anime: [],
      episodes: []
    },
    badCovers: [],
    settings: {
      autoDelete:true,
      hideThumbnails:false,
      theatreMode:false,
      bestQuality:true,
      autoDownload:true,
      autoPlayNext:false,
      autoPlayVideo:false,
      seekThumbnails:true,
      seekPoints:true,
      skipButton:true,
      reduceMotion:false
    },
    videoSpeed: []
  };
}

function upgradeData(data, fromver) {
  if (fromver === undefined) {
    fromver = 0;
  }
  const defaultVer = getDefaultData().version;
  if (fromver >= defaultVer) return;
  console.log(`[AnimePahe Improvements] Upgrading data from version ${fromver}`);
  /* Changes:
   * V1:
     * autoPlay -> autoPlayNext
   * v2:
     * autoDelete -> settings.autoDelete
     * hideThumbnails -> settings.hideThumbnails
     * theatreMode -> settings.theatreMode
     * bestQuality -> settings.bestQuality
     * autoDownload -> settings.autoDownload
     * autoPlayNext -> settings.autoPlayNext
     * autoPlayVideo -> settings.autoPlayVideo
     * +videoSpeed
   */
  const upgradeFunctions = [
    () => { // for V0
      data.autoPlayNext = data.autoPlay;
      delete data.autoPlay;
    },
    () => { // for V1
      const settings = {};
      settings.autoDelete = data.autoDelete;
      settings.hideThumbnails = data.hideThumbnails;
      settings.theatreMode = data.theatreMode;
      settings.bestQuality = data.bestQuality;
      settings.autoDownload = data.autoDownload;
      settings.autoPlayNext = data.autoPlayNext;
      settings.autoPlayVideo = data.autoPlayVideo;
      data.settings = settings;
      delete data.autoDelete;
      delete data.hideThumbnails;
      delete data.theatreMode;
      delete data.bestQuality;
      delete data.autoDownload;
      delete data.autoPlayNext;
      delete data.autoPlayVideo;
    }
  ]

  for (let i = fromver; i < defaultVer; i++) {
    const fn = upgradeFunctions[i];
    if (fn !== undefined) fn();
  }

  data.version = defaultVer;
}

function getStorage() {
  const defa = getDefaultData();
  const res = GM_getValue('anime-link-tracker', defa);

  const oldVersion = res.version;

  for (const key of Object.keys(defa)) {
    if (res[key] !== undefined) continue;
    res[key] = defa[key];
  }

  for (const key of Object.keys(defa.settings)) {
    if (res.settings[key] !== undefined) continue;
    res.settings[key] = defa.settings[key];
  }

  if (oldVersion !== defa.version) {
    upgradeData(res, oldVersion);
    saveData(res);
  }

  return res;
}

function saveData(data) {
  GM_setValue('anime-link-tracker', data);
}

function secondsToHMS(secs) {
  const mins = Math.floor(secs/60);
  const hrs = Math.floor(mins/60);
  const newSecs = Math.floor(secs % 60);
  return `${hrs > 0 ? hrs + ':' : ''}${mins % 60}:${newSecs.toString().length > 1 ? '' : '0'}${newSecs % 60}`;
}

function getStoredTime(name, ep, storage, id = undefined) {
  if (id !== undefined) {
    return storage.videoTimes.find(a => a.episodeNum === ep && a.animeId === id);
  }
  else return storage.videoTimes.find(a => a.animeName === name && a.episodeNum === ep);
}

function applyCssSheet(cssString) {
  $("head").append('<style id="anitracker-style" type="text/css"></style>');
  const sheet = $("#anitracker-style")[0].sheet;

  const rules = cssString.split(/^\}/mg).map(a => a.replace(/\n/gm,'') + '}');

  for (let i = 0; i < rules.length - 1; i++) {
    sheet.insertRule(rules[i], i);
  }
}

const kwikDLPageRegex = /^https:\/\/kwik\.\w+\/f\//;

// Video player improvements
if (/^https:\/\/kwik\.\w+/.test(baseUrl)) {
  if (typeof $ !== "undefined" && $() !== null) anitrackerKwikLoad(window.location.origin + window.location.pathname);
  else {
    const scriptElem = document.querySelector('head > link:nth-child(12)');
    if (scriptElem == null) {
      const h1 = document.querySelector('h1');
      // Some bug that the kwik DL page had before
      // (You're not actually blocked when this happens)
      if (!kwikDLPageRegex.test(baseUrl) && h1.textContent == "Sorry, you have been blocked") {
        h1.textContent = "Oops, page failed to load.";
        document.querySelector('h2').textContent = "This doesn't mean you're blocked. Try playing from another page instead.";
      }
      return;
    }
    scriptElem.onload(() => {anitrackerKwikLoad(window.location.origin + window.location.pathname)});
  }

  function anitrackerKwikLoad(url) {
  if (kwikDLPageRegex.test(url)) {
    if (initialStorage.settings.autoDownload === false) return;
    $(`
    <div style="width:100%;height:100%;background-color:rgba(0, 0, 0, 0.9);position:fixed;z-index:999;display:flex;justify-content:center;align-items:center;" id="anitrackerKwikDL">
      <span style="color:white;font-size:3.5em;font-weight:bold;">[AnimePahe Improvements] Downloading...</span>
    </div>`).prependTo(document.body);

    if ($('form').length > 0) {
      $('form').submit();
      setTimeout(() => {$('#anitrackerKwikDL').remove()}, 1500);
    }
    else new MutationObserver(function(mutationList, observer) {
      if ($('form').length > 0) {
        observer.disconnect();
        $('form').submit();
        setTimeout(() => {$('#anitrackerKwikDL').remove()}, 1500);
      }
    }).observe(document.body, { childList: true, subtree: true });

    return;
  }

// Needs to have this indentation
const _css = `
.anitracker-loading {
  background: none!important;
  border: 12px solid rgba(130,130,130,0.7);
  border-top-color: #00d1b2;
  border-radius: 50%;
  animation: spin 1.2s linear infinite;
  translate: -50% -50%;
  width: 80px;
  height: 80px;
}
.anitracker-message {
  width:50%;
  height:10%;
  position:absolute;
  background-color:rgba(0,0,0,0.5);
  justify-content:center;
  align-items:center;
  margin-top:1.5%;
  border-radius:20px;
}
.anitracker-message>span {
  color: white;
  font-size: 2.5em;
}
.anitracker-progress-tooltip {
  width: 219px;
  padding: 5px;
  opacity:0;
  position: absolute;
  left:0%;
  bottom: 100%;
  background-color: rgba(255,255,255,0.88);
  border-radius: 8px;
  transition: translate .2s ease .1s,scale .2s ease .1s,opacity .1s ease .05s;
  transform: translate(-50%,0);
  user-select: none;
  pointer-events: none;
  z-index: 2;
}
.anitracker-progress-image {
  height: 100%;
  width: 100%;
  background-color: gray;
  display:flex;
  flex-direction: column;
  align-items: center;
  overflow: hidden;
  border-radius: 5px;
}
.anitracker-progress-image>img {
  width: 100%;
}
.anitracker-progress-image>span {
  font-size: .9em;
  bottom: 5px;
  position: fixed;
  background-color: rgba(0,0,0,0.7);
  border-radius: 3px;
  padding: 0 4px 0 4px;
}
.anitracker-skip-button {
  position: absolute;
  left: 8%;
  bottom: 10%;
  color: white;
  background-color: rgba(100,100,100,0.6);
  z-index: 1;
  border: 3px solid white;
  border-radius: 8px;
  padding: 10px 24px;
  transition: .3s;
}
.anitracker-skip-button:hover, .anitracker-skip-button:focus-visible {
  background-color: rgba(0,0,0,0.75);
}
.anitracker-skip-button:focus-visible {
  outline: 3px dotted #00b3ff;
}
.anitracker-seek-points {
  width: 100%;
  bottom: 0;
  height: 100%;
  position: absolute;
  display: flex;
  align-items: center;
}
.anitracker-seek-points>i {
  position: absolute;
  width: 5px;
  height: 5px;
  border-radius: 2px;
  background-color: #1a9166;
  pointer-events: none;
  z-index: 2;
  translate: -50% 0;
}
.plyr--hide-controls>.anitracker-hide-control {
  opacity: 0!important;
  pointer-events: none!important;
}
@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}`;

  applyCssSheet(_css);

  if ($('.anitracker-message').length > 0) {
    console.log("[AnimePahe Improvements (Player)] Script was reloaded.");
    return;
  }

  $('button.plyr__controls__item:nth-child(1)').hide();
  $('.plyr__progress__container').hide();
  $('.plyr__control--overlaid').hide();

  $(`
  <div class="anitracker-loading plyr__control--overlaid">
      <span class="plyr__sr-only">Loading...</span>
  </div>`).appendTo('.plyr--video');

  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]
    };
  }

  $(`<div class="anitracker-seek-points"></div>`).appendTo('.plyr__progress');

  function setSeekPoints(seekPoints) {
    for (const p of seekPoints) {
      $(`<i style="left: ${p}%"></i>`).appendTo('.anitracker-seek-points');
    }
  }

  var timestamps = [];

  async function getAnidbIdFromTitle(title) {
    return new Promise((resolve) => {
      const req = new XMLHttpRequest();
      req.open('GET', 'https://raw.githubusercontent.com/c032/anidb-animetitles-archive/refs/heads/main/data/animetitles.json', true);
      req.onload = () => {
        if (req.status !== 200) {
          resolve(false);
          return
        };
        const data = req.response.split('\n');

        let anidbId = undefined;
        for (const anime of data) {
          const obj = JSON.parse(anime);
          if (obj.titles.find(a => a.title === title) === undefined) continue;
          anidbId = obj.id;
          break;
        }

        resolve(anidbId);
      };
      req.send();
    });
  }

  async function getTimestamps(anidbId, episode) {
    return new Promise((resolve) => {
      const req = new XMLHttpRequest();
      req.open('GET', 'https://raw.githubusercontent.com/Ellivers/open-anime-timestamps/refs/heads/master/timestamps.json', true); // Timestamp data
      req.onload = () => {
        if (req.status !== 200) {
          resolve(false);
          return
        };
        const data = JSON.parse(req.response)[anidbId];
        if (data === undefined) {
          resolve(false);
          return;
        }
        const episodeData = data.find(e => e.episode_number === episode);
        if (episodeData !== undefined) {
          console.log('[AnimePahe Improvements] Found timestamp data for episode.');
        }
        else {
          resolve(false);
          return;
        }

        const duration = player.duration;
        let timestampData = [
          {
            type: "recap",
            start: episodeData.recap.start,
            end: episodeData.recap.end
          },
          {
            type: "opening",
            start: episodeData.opening.start,
            end: episodeData.opening.end
          },
          {
            type: "ending",
            start: episodeData.ending.start,
            end: episodeData.ending.end
          },
          {
            type: "preview",
            start: episodeData.preview_start,
            end: duration
          }
        ];

        const seekPoints = [];

        for (const t of timestampData) {
          if (t.start === -1) continue;
          const percentage = (t.start / duration) * 100;
          seekPoints.push(percentage);
        }

        // Filter off unusable timestamps
        timestampData = timestampData.filter(t => t.start !== -1 && (t.end !== -1 || t.type === 'preview'));

        resolve({
          seekPoints: seekPoints,
          timestamps: timestampData
        });
      }
      req.send();
    });
  }

  function updateStoredPlaybackSpeed(speed) {
    const storage = getStorage();
    const vidInfo = getVideoInfo();
    const storedVideoTime = getStoredTime(vidInfo.animeName, vidInfo.episodeNum, storage);
    if (storedVideoTime === undefined) return;

    const storedPlaybackSpeed = storage.videoSpeed.find(a => a.animeId === storedVideoTime.animeId);

    if (speed === 1 && storedVideoTime.animeId !== undefined) {
      if (storedPlaybackSpeed === undefined) return;
      storage.videoSpeed = storage.videoSpeed.filter(a => a.animeId !== storedVideoTime.animeId);
      saveData(storage);
      return;
    }

    if (storedPlaybackSpeed === undefined) {
      storage.videoSpeed.push({
        animeId: storedVideoTime.animeId,
        animeName: vidInfo.animeName,
        speed: speed
      });
      if (storage.videoSpeed.length > 256) storage.videoSpeed.splice(0,1);
    }
    else storedPlaybackSpeed.speed = speed;
    saveData(storage);
  }

  function updateStoredTime() {
    const currentTime = player.currentTime;
    const storage = getStorage();

    // Delete the storage entry
    if (player.duration - currentTime <= 20) {
      const videoInfo = getVideoInfo();
      storage.videoTimes = storage.videoTimes.filter(a => !(a.animeName === videoInfo.animeName && a.episodeNum === videoInfo.episodeNum));
      saveData(storage);
      return;
    }
    if (waitingState.idRequest === 1) return;
    const vidInfo = getVideoInfo();
    const storedVideoTime = getStoredTime(vidInfo.animeName, vidInfo.episodeNum, storage);

    if (storedVideoTime === undefined) {
      if (![-1,0].includes(waitingState.idRequest)) { // If the video has loaded (>0) and getting the ID has not failed (-1)
        waitingState.idRequest = 1;
        sendMessage({action: "id_request"});
        setTimeout(() => {
          if (waitingState.idRequest === 1) {
            waitingState.idRequest = -1; // Failed to get the anime ID from the main page within 2 seconds
            updateStoredTime();
          }
        }, 2000);
        return;
      }
      const vidInfo = getVideoInfo();
      storage.videoTimes.push({
        videoUrls: [url],
        time: player.currentTime,
        animeName: vidInfo.animeName,
        episodeNum: vidInfo.episodeNum
      });
      if (storage.videoTimes.length > 1000) {
        storage.videoTimes.splice(0,1);
      }
      saveData(storage);
      return;
    }

    storedVideoTime.time = player.currentTime;
    saveData(storage);
  }

  if (initialStorage.videoTimes === undefined) {
    const storage = getStorage();
    storage.videoTimes = [];
    saveData(storage);
  }

  // For message requests from the main page
  // -1: failed
  // 0: hasn't started
  // 1: waiting
  // 2: succeeded
  const waitingState = {
    idRequest: 0,
    videoUrlRequest: 0,
    anidbIdRequest: 0
  };
  // Messages received from main page
  window.onmessage = function(e) {
    const storage = getStorage();
    const vidInfo = getVideoInfo();

    const data = e.data;
    const action = data.action;
    if (action === 'id_response' && waitingState.idRequest === 1) {
      storage.videoTimes.push({
        videoUrls: [url],
        time: 0,
        animeName: vidInfo.animeName,
        episodeNum: vidInfo.episodeNum,
        animeId: data.id
      });
      if (storage.videoTimes.length > 1000) {
        storage.videoTimes.splice(0,1);
      }
      saveData(storage);
      waitingState.idRequest = 2;

      const storedPlaybackSpeed = storage.videoSpeed.find(a => a.animeId === data.id);
      if (storedPlaybackSpeed !== undefined) {
        setSpeed(storedPlaybackSpeed.speed);
      }

      waitingState.anidbIdRequest = 1;
      sendMessage({action:"anidb_id_request",id:data.id});

      return;
    }
    else if (action === 'anidb_id_response' && waitingState.anidbIdRequest === 1) {
      waitingState.anidbIdRequest = 2;
      let anidbId = data.id;
      if (anidbId === undefined) {
        const episode = storage.linkList.find(e => e.type === 'episode' && e.animeId === data.originalId);
        if (episode === undefined) return;
        getAnidbIdFromTitle(episode.animeName).then(response => {
          anidbId = response;
        });
      }
      if (anidbId === undefined) return;
      getTimestamps(anidbId, vidInfo.episodeNum).then(response => {
        const storage = getStorage();
        const storedVideoTime = getStoredTime(vidInfo.animeName, vidInfo.episodeNum, storage);

        if (response === false) {
          storedVideoTime.hasTimestamps = false;
          saveData(storage);
          return;
        }

        if (storage.settings.seekPoints) setSeekPoints(response.seekPoints);
        if (storage.settings.skipButton) timestamps = response.timestamps;

        storedVideoTime.hasTimestamps = true;
        storedVideoTime.timestampData = response;
        saveData(storage);
      });
    }
    else if (action === 'video_url_response' && waitingState.videoUrlRequest === 1) {
      waitingState.videoUrlRequest = 2;
      const request = new XMLHttpRequest();
      request.open('GET', data.url, true);
      request.onload = () => {
        if (request.status !== 200) {
          console.error('[AnimePahe Improvements] Could not get kwik page for video source');
          return;
        }

        const pageElements = Array.from($(request.response)); // Elements that are not buried cannot be found with jQuery.find()
        const hostInfo = (() => {
          for (const link of pageElements.filter(a => a.tagName === 'LINK')) {
            const href = $(link).attr('href');
            if (!href.includes('vault')) continue;
            const result = /vault-(\d+)\.(\w+\.\w+)$/.exec(href);
            return {
              vaultId: result[1],
              hostName: result[2]
            }
            break;
          }
        })();

        const searchInfo = (() => {
          for (const script of pageElements.filter(a => a.tagName === 'SCRIPT')) {
            if ($(script).attr('url') !== undefined || !$(script).text().startsWith('eval')) continue;
            const result = /(\w{64})\|((?:\w+\|){4,5})https/.exec($(script).text());
            let extraNumber = undefined;
            result[2].split('|').forEach(a => {if (/\d{2}/.test(a)) extraNumber = a;}); // Some number that's needed for the url (doesn't always exist here)
            if (extraNumber === undefined) {
              const result2 = /q=\\'\w+:\/{2}\w+\-\w+\.\w+\.\w+\/((?:\w+\/)+)/.exec($(script).text());
              result2[1].split('/').forEach(a => {if (/\d{2}/.test(a) && a !== hostInfo.vaultId) extraNumber = a;});
            }
            if (extraNumber === undefined) {
              const result2 = /source\|(\d{2})\|ended/.exec($(script).text());
              if (result2 !== null) extraNumber = result2[1];
            }
            return {
              part1: extraNumber,
              part2: result[1]
            };
            break;
          }
        })();

        if (searchInfo.part1 === undefined) {
          console.error('[AnimePahe Improvements] Could not find "extraNumber" from ' + data.url);
          return;
        }

        setupSeekThumbnails(`https://vault-${hostInfo.vaultId}.${hostInfo.hostName}/stream/${hostInfo.vaultId}/${searchInfo.part1}/${searchInfo.part2}/uwu.m3u8`);
      };
      request.send();
    }
    else if (action === 'change_time') {
      if (data.time !== undefined) player.currentTime = data.time;
    }
    else if (action === 'key') {
      if ([' ','k'].includes(data.key)) {
        if (player.paused) player.play();
        else player.pause();
      }
      else if (data.key === 'ArrowLeft') {
        player.currentTime = Math.max(0, player.currentTime - 5);
        return;
      }
      else if (data.key === 'ArrowRight') {
        player.currentTime = Math.min(player.duration, player.currentTime + 5);
        return;
      }
      else if (/^\d$/.test(data.key)) {
        player.currentTime = (player.duration/10)*(+data.key);
        return;
      }
      else if (data.key === 'm') player.muted = !player.muted;
      else $(player).trigger('keydown', {
        key: data.key
      });
    }
    else if (action === 'setting_changed') {
      const storedVideoTime = getStoredTime(vidInfo.animeName, vidInfo.episodeNum, storage);
      if (data.type === 'seek_points' && storedVideoTime.hasTimestamps === true) {
        if (data.value === true && $('.anitracker-seek-points>i').length === 0) setSeekPoints(storedVideoTime.timestampData.seekPoints);
        else if (data.value === false) $('.anitracker-seek-points>i').remove();
      }
      else if (data.type === 'skip_button' && storedVideoTime.hasTimestamps === true) {
        if (data.value === true) {
          timestamps = storedVideoTime.timestampData.timestamps;
          checkActiveTimestamps();
        }
        else {
          setSkipBtnVisibility(false);
          timestamps = [];
        }
      }
    }
  };

  $('.plyr--full-ui').attr('tabindex','1');
  let skipBtnVisible = false;

  function setSkipBtnVisibility(on) {
    const elem = $('.anitracker-skip-button');
    if (on && !skipBtnVisible) {
      elem.css('opacity','1').css('pointer-events','').css('translate','');
      elem.attr('tabindex','2');
      skipBtnVisible = true;
    }
    else if (!on && skipBtnVisible) {
      elem.css('opacity','0').css('pointer-events','none').css('translate','-50%');
      elem.removeClass('anitracker-hide-control');
      elem.attr('tabindex','-1');
      elem.off('click');
      skipBtnVisible = false;
    }
  }

  const skipTexts = {
    'recap': 'Skip Recap',
    'opening': 'Skip Opening',
    'ending': 'Skip Ending',
    'preview': 'Skip to End'
  }

  function checkActiveTimestamps(time = player.currentTime) {
    if (timestamps.length === 0) return;
    let activeTimestamp;
    for (const t of timestamps) {
      if (time > t.start && time < (t.end - 2)) {
        activeTimestamp = t;
        break;
      }
    }
    if (activeTimestamp === undefined) {
      setSkipBtnVisibility(false);
      return;
    }
    const elem = $('.anitracker-skip-button');

    const text = skipTexts[activeTimestamp.type] || 'Skip Section';
    if (text === elem.text() && skipBtnVisible) {
      if (time - activeTimestamp.start > 4) {
        elem.addClass('anitracker-hide-control');
      }
      return;
    }

    elem.text(text);
    setSkipBtnVisibility(true);
    elem.off('click');
    elem.on('click', () => {
      player.currentTime = activeTimestamp.end - 2;
      setSkipBtnVisibility(false);
    });
  }

  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.hasTimestamps) {
        if (storage.settings.skipButton) timestamps = storedVideoTime.timestampData.timestamps;
        if (storage.settings.seekPoints) setSeekPoints(storedVideoTime.timestampData.seekPoints);
      }
      if (!storedVideoTime.videoUrls.includes(url)) {
        storedVideoTime.videoUrls.push(url);
        saveData(storage);
      }
      const storedPlaybackSpeed = storage.videoSpeed.find(a => a.animeId === storedVideoTime.animeId);
      if (storedPlaybackSpeed !== undefined) {
        setSpeed(storedPlaybackSpeed.speed);
      }
      else player.playbackRate = 1;
    }
    else {
      player.playbackRate = 1;
      waitingState.idRequest = 1;
      sendMessage({action: "id_request"});
      setTimeout(() => {
        if (waitingState.idRequest === 1) {
          waitingState.idRequest = -1; // Failed to get the anime ID from the main page within 2 seconds
          updateStoredTime();
        }
      }, 2000);
      finishedLoading();
    }

    const timeArg = Array.from(new URLSearchParams(window.location.search)).find(a => a[0] === 'time');
    if (timeArg !== undefined) {
      const newTime = +timeArg[1];
      if (storedVideoTime === undefined || (storedVideoTime !== undefined && Math.floor(storedVideoTime.time) === Math.floor(newTime)) || (storedVideoTime !== undefined &&
                                            confirm(`[AnimePahe Improvements]\n\nYou already have saved progress on this video (${secondsToHMS(storedVideoTime.time)}). Do you want to overwrite it and go to ${secondsToHMS(newTime)}?`))) {
        player.currentTime = Math.max(0, Math.min(newTime, player.duration));
      }
      window.history.replaceState({}, document.title, url);
    }

    player.removeEventListener('loadeddata', loadVideoData);

    // Set up events
    let lastTimeUpdate = 0;
    player.addEventListener('timeupdate', function() {
      const currentTime = player.currentTime;
      checkActiveTimestamps(currentTime);
      if (Math.trunc(currentTime) % 10 === 0 && player.currentTime - lastTimeUpdate > 9) {
        updateStoredTime();
        lastTimeUpdate = player.currentTime;
      }
    });

    player.addEventListener('pause', () => {
      updateStoredTime();
    });

    player.addEventListener('seeked', () => {
      updateStoredTime();
      checkActiveTimestamps();
      finishedLoading();
    });

    player.addEventListener('ratechange', () => {
      if (player.readyState > 2) updateStoredPlaybackSpeed(player.playbackRate);
    });
  });

  function getFrame(video, time, dimensions) {
    return new Promise((resolve) => {
      video.onseeked = () => {
        const canvas = document.createElement('canvas');
        canvas.height = dimensions.y;
        canvas.width = dimensions.x;
        const ctx = canvas.getContext('2d');
        ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
        resolve(canvas.toDataURL('image/png'));
      };
      try {
        video.currentTime = time;
      }
      catch (e) {
        console.error(time, e);
      }
    });
  }

  const settingsContainerId = (() => {
    for (const elem of $('.plyr__menu__container')) {
      const regex = /plyr\-settings\-(\d+)/.exec(elem.id);
      if (regex === null) continue;
      return regex[1];
    }
    return undefined;
  })();

  function setupSeekThumbnails(videoSource) {
    const resolution = 167;

    const bgVid = document.createElement('video');
    bgVid.height = resolution;
    bgVid.onloadeddata = () => {
      const fullDuration = bgVid.duration;
      const timeBetweenThumbnails = fullDuration/(24*6); // Just something arbitrary that seems good
      const thumbnails = [];
      const aspectRatio = bgVid.videoWidth / bgVid.videoHeight;

      const aspectRatioCss = `${bgVid.videoWidth} / ${bgVid.videoHeight}`;

      $('.plyr__progress .plyr__tooltip').remove();
      $(`
      <div class="anitracker-progress-tooltip" style="aspect-ratio: ${aspectRatioCss};">
        <div class="anitracker-progress-image">
          <img style="display: none; aspect-ratio: ${aspectRatioCss};">
          <span>0:00</span>
        </div>
      </div>`).insertAfter(`progress`);

      $('.anitracker-progress-tooltip img').on('load', () => {
        $('.anitracker-progress-tooltip img').css('display', 'block');
      });

      const toggleVisibility = (on) => {
        if (on) $('.anitracker-progress-tooltip').css('opacity', '1').css('scale','1').css('translate','');
        else $('.anitracker-progress-tooltip').css('opacity', '0').css('scale','0.75').css('translate','-12.5% 20px');
      };

      const elem = $('.anitracker-progress-tooltip');
      let currentTime = 0;
      new MutationObserver(function(mutationList, observer) {
        if ($('.plyr--full-ui').hasClass('plyr--hide-controls') || !$(`#plyr-seek-${settingsContainerId}`)[0].matches(':hover')) {
          toggleVisibility(false);
          return;
        }
        toggleVisibility(true);

        const seekValue = $(`#plyr-seek-${settingsContainerId}`).attr('seek-value');
        const time = seekValue !== undefined ? Math.min(Math.max(Math.trunc(fullDuration*(+seekValue/100)), 0), fullDuration) : Math.trunc(player.currentTime);
        const roundedTime = Math.trunc(time/timeBetweenThumbnails)*timeBetweenThumbnails;
        const timeSlot = Math.trunc(time/timeBetweenThumbnails);

        elem.find('span').text(secondsToHMS(time));
        elem.css('left', seekValue + '%');

        if (roundedTime === Math.trunc(currentTime/timeBetweenThumbnails)*timeBetweenThumbnails) return;

        const cached = thumbnails.find(a => a.time === timeSlot);
        if (cached !== undefined) {
          elem.find('img').attr('src', cached.data);
        }
        else {
          elem.find('img').css('display', 'none');
          getFrame(bgVid, roundedTime, {y: resolution, x: resolution*aspectRatio}).then((response) => {
            thumbnails.push({
              time: timeSlot,
              data: response
            });

            elem.find('img').css('display', 'none');
            elem.find('img').attr('src', response);
          });
        }
        currentTime = time;

      }).observe($(`#plyr-seek-${settingsContainerId}`)[0], { attributes: true });

      $(`#plyr-seek-${settingsContainerId}`).on('mouseleave', () => {
        toggleVisibility(false);
      });

    }

    const hls2 = new Hls({
      maxBufferLength: 0.1,
      backBufferLength: 0,
      capLevelToPlayerSize: true,
      maxAudioFramesDrift: Infinity
    });
    hls2.loadSource(videoSource);
    hls2.attachMedia(bgVid);
  }

  // Thumbnails when seeking
  if (Hls.isSupported() && initialStorage.settings.seekThumbnails !== false) {
    sendMessage({action:"video_url_request"});
    waitingState.videoUrlRequest = 1;
    setTimeout(() => {
      if (waitingState.videoUrlRequest === 2) return;

      waitingState.videoUrlRequest = -1;
      if (typeof hls !== "undefined") setupSeekThumbnails(hls.url);
    }, 500);
  }

  function finishedLoading() {
    if ($('.anitracker-loading').length === 0) return;
    $('.anitracker-loading').remove();
    $('button.plyr__controls__item:nth-child(1)').show();
    $('.plyr__progress__container').show();
    $('.plyr__control--overlaid').show();

    const storage = getStorage();
    if (storage.settings.autoPlayVideo === true) player.play();
  }

  let messageTimeout = undefined;

  function showMessage(text) {
    $('.anitracker-message span').text(text);
    $('.anitracker-message').css('display', 'flex');
    clearTimeout(messageTimeout);
    messageTimeout = setTimeout(() => {
      $('.anitracker-message').hide();
    }, 1000);
  }

  const frametime = 1 / 24;
  let funPitch = "";

  $(document).on('keydown', function(e, other = undefined) {
    const key = e.key || other.key;
    if (key === 'ArrowUp') {
      changeSpeed(e, -1); // The changeSpeed function only works if ctrl is being held
      return;
    }
    if (key === 'ArrowDown') {
      changeSpeed(e, 1);
      return;
    }
    if (e.shiftKey && ['l','L'].includes(key)) {
      showMessage('Loop: ' + (player.loop ? 'Off' : 'On'));
      player.loop = !player.loop;
      return;
    }
    if (e.shiftKey && ['n','N'].includes(key)) {
      sendMessage({action: "next"});
      return;
    }
    if (e.shiftKey && ['p','P'].includes(key)) {
      sendMessage({action: "previous"});
      return;
    }
    if (e.ctrlKey || e.altKey || e.shiftKey || e.metaKey) return; // Prevents special keys for the rest of the keybinds
    if (key === 'j') {
      player.currentTime = Math.max(0, player.currentTime - 10);
      return;
    }
    else if (key === 'l') {
      player.currentTime = Math.min(player.duration, player.currentTime + 10);
      setTimeout(() => {
        player.loop = false;
      }, 5);
      return;
    }
    else if (/^Numpad\d$/.test(e.code)) {
      player.currentTime = (player.duration/10)*(+e.code.replace('Numpad', ''));
      return;
    }
    if (!(player.currentTime > 0 && !player.paused && !player.ended && player.readyState > 2)) {
      if (key === ',') {
        player.currentTime = Math.max(0, player.currentTime - frametime);
        return;
      }
      else if (key === '.') {
        player.currentTime = Math.min(player.duration, player.currentTime + frametime);
        return;
      }
    }

    funPitch += key;
    if (funPitch === 'crazy') {
      player.preservesPitch = !player.preservesPitch;
      showMessage(player.preservesPitch ? 'Off' : 'Change speed ;D');
      funPitch = "";
      return;
    }
    if (!"crazy".startsWith(funPitch)) {
      funPitch = "";
    }

    sendMessage({
      action: "key",
      key: key
    });

  });

  // Ctrl+scrolling to change speed

  $(`
  <button class="anitracker-skip-button" tabindex="-1" style="opacity:0;pointer-events:none;translate:-50%;" aria-label="Skip section"><span>Skip Section</span></button>
  <div class="anitracker-message" style="display:none;">
    <span>2.0x</span>
  </div>`).appendTo($(player).parents().eq(1));

  jQuery.event.special.wheel = {
      setup: function( _, ns, handle ){
          this.addEventListener("wheel", handle, { passive: false });
      }
  };

  const defaultSpeeds = player.plyr.options.speed;

  function changeSpeed(e, delta) {
    if (!e.ctrlKey) return;
    e.preventDefault();
    if (delta == 0) return;

    const speedChange = e.shiftKey ? 0.05 : 0.1;

    setSpeed(player.playbackRate + speedChange * (delta > 0 ? -1 : 1));
  }

  function setSpeed(speed) {
    if (speed > 0) player.playbackRate = Math.round(speed * 100) / 100;
    showMessage(player.playbackRate + "x");

    if (defaultSpeeds.includes(player.playbackRate)) {
      $('.anitracker-custom-speed-btn').remove();
    }
    else if ($('.anitracker-custom-speed-btn').length === 0) {
      $(`#plyr-settings-${settingsContainerId}-speed>div>button`).attr('aria-checked','false');
      $(`
      <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";
      }
    }
  }

  $(`#plyr-settings-${settingsContainerId}-speed>div>button`).on('click', (e) => {
    $('.anitracker-custom-speed-btn').remove();
  });

  $(document).on('wheel', function(e) {
    changeSpeed(e, e.originalEvent.deltaY);
  });

  }

  return;
}

if ($() !== null) anitrackerLoad(window.location.origin + window.location.pathname + window.location.search);
else {
  document.querySelector('head > link:nth-child(10)').onload(() => {anitrackerLoad(window.location.origin + window.location.pathname + window.location.search)});
}

function anitrackerLoad(url) {

if ($('#anitracker-modal').length > 0) {
  console.log("[AnimePahe Improvements] Script was reloaded.");
  return;
}

if (initialStorage.settings.hideThumbnails === true) {
  hideThumbnails();
}

function windowOpen(url, target = '_blank') {
  $(`<a href="${url}" target="${target}"></a>`)[0].click();
}

(function($) {
  $.fn.changeElementType = function(newType) {
      let attrs = {};

      $.each(this[0].attributes, function(idx, attr) {
          attrs[attr.nodeName] = attr.nodeValue;
      });

      this.replaceWith(function() {
          return $("<" + newType + "/>", attrs).append($(this).contents());
      });
  };
  $.fn.replaceClass = function(oldClass, newClass) {
    this.removeClass(oldClass).addClass(newClass);
  };
})(jQuery);

// -------- AnimePahe Improvements CSS ---------

const animationTimes = {
  modalOpen: 0.2,
  fadeIn: 0.2
};

const _css = `
#anitracker {
  display: flex;
  flex-direction: row;
  gap: 15px 7px;
  align-items: center;
  flex-wrap: wrap;
}
.anitracker-index {
  align-items: end !important;
}
#anitracker>span {align-self: center;\n}
#anitracker-modal {
  position: fixed;
  width: 100%;
  height: 100%;
  background-color: rgba(0,0,0,0.6);
  z-index: 20;
  display: none;
}
#anitracker-modal-content {
  max-height: 90%;
  background-color: var(--dark);
  margin: auto auto auto auto;
  border-radius: 20px;
  display: flex;
  padding: 20px;
  z-index:50;
}
#anitracker-modal-close {
  font-size: 2.5em;
  margin: 3px 10px;
  cursor: pointer;
  height: 1em;
}
#anitracker-modal-close:hover,#anitracker-modal-close:focus-visible {
  color: rgb(255, 0, 108);
}
#anitracker-modal-body {
  padding: 10px;
  overflow-x: hidden;
}
#anitracker-modal-body .anitracker-switch {margin-bottom: 2px;\n}
.anitracker-big-list-item {
  list-style: none;
  border-radius: 10px;
  margin-top: 5px;
}
.anitracker-big-list-item>a {
  font-size: 0.875rem;
  display: block;
  padding: 5px 15px;
  color: rgb(238, 238, 238);
  text-decoration: none;
}
.anitracker-big-list-item img {
  margin: auto 0px;
  width: 50px;
  height: 50px;
  border-radius: 100%;
}
.anitracker-big-list-item .anitracker-main-text {
  font-weight: 700;
  color: rgb(238, 238, 238);
}
.anitracker-big-list-item .anitracker-subtext {
  font-size: 0.75rem;
  color: rgb(153, 153, 153);
}
.anitracker-big-list-item:hover .anitracker-main-text {
  color: rgb(238, 238, 238);
}
.anitracker-big-list-item:hover .anitracker-subtext {
  color: rgb(238, 238, 238);
}
.anitracker-big-list-item:hover {
  background-color: #111;
}
.anitracker-big-list-item:focus-within .anitracker-main-text {
  color: rgb(238, 238, 238);
}
.anitracker-big-list-item:focus-within .anitracker-subtext {
  color: rgb(238, 238, 238);
}
.anitracker-big-list-item:focus-within {
  background-color: #111;
}
.anitracker-hide-thumbnails .anitracker-thumbnail img {display: none;\n}
.anitracker-hide-thumbnails .anitracker-thumbnail {
  border: 10px solid rgb(32, 32, 32);
  aspect-ratio: 16/9;
}
.anitracker-hide-thumbnails .episode-snapshot img {
  display: none;
}
.anitracker-hide-thumbnails .episode-snapshot {
  border: 4px solid var(--dark);
}
.anitracker-download-spinner {display: inline;\n}
.anitracker-download-spinner .spinner-border {
  height: 0.875rem;
  width: 0.875rem;
}
.anitracker-dropdown-content {
  display: none;
  position: absolute;
  min-width: 100px;
  box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
  z-index: 1;
  max-height: 400px;
  overflow-y: auto;
  overflow-x: hidden;
  background-color: #171717;
}
.anitracker-dropdown-content button {
  color: white;
  padding: 12px 16px;
  text-decoration: none;
  display: block;
  width:100%;
  background-color: #171717;
  border: none;
  margin: 0;
}
.anitracker-dropdown-content button:hover, .anitracker-dropdown-content button:focus {background-color: black;\n}
.anitracker-active, .anitracker-active:hover, .anitracker-active:active {
  color: white!important;
  background-color: #d5015b!important;
}
.anitracker-dropdown-content a:hover {background-color: #ddd;\n}
.anitracker-dropdown:hover .anitracker-dropdown-content {display: block;\n}
.anitracker-dropdown:hover .anitracker-dropbtn {background-color: #bc0150;\n}
#pickDownload span, #scrollArea span {
  cursor: pointer;
  font-size: 0.875rem;
}
.anitracker-expand-data-icon {
  font-size: 24px;
  float: right;
  margin-top: 6px;
  margin-right: 8px;
}
.anitracker-modal-list-container {
  background-color: rgb(40,45,50);
  margin-bottom: 10px;
  border-radius: 12px;
}
.anitracker-storage-data {
  background-color: rgb(40,45,50);
  border-radius: 12px;
  cursor: pointer;
  position: relative;
  z-index: 1;
}
.anitracker-storage-data:focus {
  box-shadow: 0 0 0 .2rem rgb(255, 255, 255);
}
.anitracker-storage-data span {
  display:inline-block;
  font-size: 1.4em;
  font-weight: bold;
}
.anitracker-storage-data, .anitracker-modal-list {
  padding: 10px;
}
.anitracker-modal-list-entry {margin-top: 8px;\n}
.anitracker-modal-list-entry a {text-decoration: underline;\n}
.anitracker-modal-list-entry:hover {background-color: rgb(30,30,30);\n}
.anitracker-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-filter-input {
  width: 12.2rem;
  display: inline-block;
  cursor: text;
}
.anitracker-filter-input > div {
  height:56px;
  width:100%;
  border-bottom: 2px solid #454d54;
  overflow-y: auto;
}
.anitracker-filter-input.active > div {
  border-color: rgb(213, 1, 91);
}
.anitracker-filter-rules {
  background: black;
  border: 1px solid #bbb;
  color: #bbb;
  padding: 5px;
  float: right;
  border-radius: 5px;
  font-size: .8em;
  width: 2em;
  aspect-ratio: 1;
  margin-bottom: -10px;
  z-index: 1;
  position: relative;
  min-height: 0;
}
.anitracker-filter-rules>i {
  vertical-align: super;
}
.anitracker-filter-rules.anitracker-active {
  border-color: rgb(213, 1, 91);
}
.anitracker-filter-rules:hover, .anitracker-filter-rules:focus-visible {
  background: white;
  color: black;
  border-color: white;
}
.anitracker-filter-input-search {
  position: absolute;
  max-width: 150px;
  max-height: 45px;
  min-width: 150px;
  min-height: 45px;
  overflow-wrap: break-word;
  overflow-y: auto;
}
.anitracker-filter-input .placeholder {
  color: #999;
  position: absolute;
  z-index: -1;
}
.anitracker-filter-icon {
  padding: 0;
  padding-right: 4px;
  border-radius: 12px;
  display: inline-block;
  cursor: pointer;
  border: 2px solid white;
  margin-right: 5px;
  transition: background-color .3s, border-color .3s;
  vertical-align: text-top;
}
.anitracker-filter-icon>i {
  margin: 2px;
  font-size: .8em;
}
.anitracker-filter-icon.included {
  background-color: rgba(20, 113, 30, 0.64);
  border-color: rgb(62, 181, 62);
}
.anitracker-filter-icon.included>i {
  color: rgb(83, 255, 83);
}
.anitracker-filter-icon.excluded {
  background-color: rgba(187, 62, 62, 0.41);
  border-color: #d75a5a;
}
.anitracker-filter-icon.excluded>i {
  color: rgb(227, 96, 96);
}
.anitracker-filter-icon:hover {
  border-color: white;
}
#anitracker-settings-invert-switch:checked ~ .custom-control-label::before {
  border-color: red;
  background-color: red;
}
#anitracker-settings-invert-switch:checked[disabled=""] ~ .custom-control-label::before {
  border-color: #e88b8b;
  background-color: #e88b8b;
}
.anitracker-text-input {
  display: inline-block;
  height: 1em;
  line-break: anywhere;
  min-width: 50px;
}
.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-text-input-bar[disabled=""] {
  background: rgb(89, 89, 89);
  border-color: gray;
  cursor: not-allowed;
}
.anitracker-applied-filters {
  display: inline-block;
}
.anitracker-placeholder {
  color: gray;
}
.anitracker-filter-dropdown>button {
  transition: background-color .3s;
}
.anitracker-filter-dropdown>button.included {
  background-color: rgb(6, 130, 6);
}
.anitracker-filter-dropdown>button.included:focus {
  border: 2px dashed rgb(141, 234, 141);
}
.anitracker-filter-dropdown>button.excluded {
  background-color: rgb(117, 17, 17);
}
.anitracker-filter-dropdown>button.excluded:focus {
  border: 2px dashed rgb(215, 90, 90);
}
.anitracker-filter-dropdown>button.anitracker-active:focus {
  border: 2px dashed #ffd7eb;
}
#anitracker-season-copy-to-lower {
  color:white;
  margin-left:14px;
  border-radius:5px;
}
.anitracker-filter-spinner.small {
  display: inline-flex;
  margin-left: 10px;
  justify-content: center;
  align-items: center;
  vertical-align: bottom;
}
.anitracker-filter-spinner.screen {
  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;
}
.anitracker-filter-spinner .spinner-border {
  color:#d5015b;
}
.anitracker-filter-spinner.screen .spinner-border {
  width:5rem;
  height:5rem;
  border-width: 10px;
}
.anitracker-filter-spinner>span {
  position: absolute;
  font-weight: bold;
}
.anitracker-filter-spinner.small>span {
  font-size: .5em;
}
.anitracker-filter-rule-selection {
  margin-bottom: 2px;
  display: grid;
  grid-template-columns: 1.5em 32% auto;
  align-items: center;
  grid-gap: 5px;
  border-radius: 20px;
  padding: 5px;
}
.anitracker-filter-rule-selection[disabled=""]>* {
  opacity: 0.5;
  pointer-events: none;
}
.anitracker-filter-rule-selection>i {
  text-align: center;
  border-radius: 35%;
  padding: 2px;
  aspect-ratio: 1;
}
.anitracker-filter-rule-selection>i::before {
  vertical-align: middle;
}
.anitracker-filter-rule-selection>.fa-plus {
  color: rgb(72, 223, 58);
  background-color: #148214;
}
.anitracker-filter-rule-selection>.fa-minus {
  color: #ff0000;
  background-color: #911212;
}
.anitracker-filter-rule-selection button {
  padding: 0;
  width: 2.5em;
  height: 2.5em;
  background-color: var(--secondary);
  border: 3px solid var(--dark);
  border-radius: 35%;
  outline: rgb(94, 96, 100) solid 3px;
  margin: 5px;
  color: white;
}
.anitracker-filter-rule-selection button.anitracker-active {
  outline-color: rgb(213, 1, 91);
}
.anitracker-filter-rule-selection button:hover:not([disabled=""]), .anitracker-filter-rule-selection button:focus-visible:not([disabled=""]) {
  outline-color: white;
}
.anitracker-flat-button {
  padding-top: 0;
  padding-bottom: 0;
}
.anitracker-list-btn {
  height: 42px;
  border-radius: 7px!important;
  color: #ddd!important;
  margin-left: 10px!important;
}
.anitracker-reverse-order-button {
  font-size: 2em;
}
.anitracker-reverse-order-button::after {
  vertical-align: 20px;
}
.anitracker-reverse-order-button.anitracker-up::after {
  border-top: 0;
  border-bottom: .3em solid;
  vertical-align: 22px;
}
#anitracker-time-search-button {
  float: right;
}
#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;
}
.anitracker-season-group>span {
  align-self: center;
}
a.youtube-preview::before {
  -webkit-transition: opacity .2s linear!important;
  -moz-transition: opacity .2s linear!important;
  transition: opacity .2s linear!important;
}
.anitracker-replaced-cover {background-position-y: 25%;\n}
.anitracker-text-button {
  color:#d5015b;
  cursor:pointer;
  user-select:none;
}
.anitracker-text-button:hover {
  color:white;
}
.nav-search {
  float: left!important;
}
.anitracker-title-icon {
  margin-left: 1rem!important;
  opacity: .8!important;
  color: #ff006c!important;
  font-size: 2rem!important;
  vertical-align: middle;
  cursor: pointer;
  padding: 0;
  box-shadow: none!important;
}
.anitracker-title-icon:hover {
  opacity: 1!important;
}
.anitracker-title-icon-check {
  color: white;
  margin-left: -.7rem!important;
  font-size: 1rem!important;
  vertical-align: super;
  text-shadow: none;
  opacity: 1!important;
}
.anitracker-header {
  display: flex;
  justify-content: left;
  gap: 18px;
  flex-grow: 0.05;
}
.anitracker-header-button {
  color: white;
  background: none;
  border: 2px solid white;
  border-radius: 5px;
  width: 2rem;
}
.anitracker-header-button:hover {
  border-color: #ff006c;
  color: #ff006c;
}
.anitracker-header-button:focus {
  border-color: #ff006c;
  color: #ff006c;
}
.anitracker-header-notifications-circle {
  color: rgb(255, 0, 108);
  margin-left: -.3rem;
  font-size: 0.7rem;
  position: absolute;
}
.anitracker-notification-item .anitracker-main-text {
  color: rgb(153, 153, 153);
}
.anitracker-notification-item-unwatched {
  background-color: rgb(119, 62, 70);
}
.anitracker-notification-item-unwatched .anitracker-main-text {
  color: white!important;
}
.anitracker-notification-item-unwatched .anitracker-subtext {
  color: white!important;
}
.anitracker-watched-toggle {
  font-size: 1.7em;
  float: right;
  margin-right: 5px;
  margin-top: 5px;
  cursor: pointer;
  background-color: #592525;
  padding: 5px;
  border-radius: 5px;
}
.anitracker-watched-toggle:hover,.anitracker-watched-toggle:focus {
  box-shadow: 0 0 0 .2rem rgb(255, 255, 255);
}
#anitracker-replace-cover {
  z-index: 99;
  right: 10px;
  position: absolute;
  bottom: 6em;
}
header.main-header nav .main-nav li.nav-item > a:focus {
  color: #fff;
  background-color: #bc0150;
}
.theatre-settings .dropup .btn:focus {
  box-shadow: 0 0 0 .15rem rgb(100, 100, 100)!important;
}
.anitracker-episode-time {
  margin-left: 5%;
  font-size: 0.75rem!important;
  cursor: default!important;
}
.anitracker-episode-time:hover {
  text-decoration: none!important;
}
.index>* {
  width: 100%;
}
@media screen and (min-width: 1375px) {
  .theatre.anitracker-theatre-mode {
    margin-top: 10px!important;
  }
  .theatre.anitracker-theatre-mode>* {
    max-width: 81%!important;
  }
}
@keyframes anitracker-modalOpen {
  0% {
    transform: scale(0.5);
  }
  50% {
    transform: scale(1.07);
  }
  100% {
    transform: scale(1);
  }
}
@keyframes anitracker-fadeIn {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}
@keyframes anitracker-spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}
`;

applyCssSheet(_css);


const optionSwitches = [
  {
    optionId: 'autoDelete',
    switchId: 'auto-delete',
    value: initialStorage.settings.autoDelete
  },
  {
    optionId: 'theatreMode',
    switchId: 'theatre-mode',
    value: initialStorage.settings.theatreMode,
    onEvent: () => {
      theatreMode(true);
    },
    offEvent: () => {
      theatreMode(false);
    }
  },
  {
    optionId: 'hideThumbnails',
    switchId: 'hide-thumbnails',
    value: initialStorage.settings.hideThumbnails,
    onEvent: hideThumbnails,
    offEvent: () => {
      $('.main').removeClass('anitracker-hide-thumbnails');
    }
  },
  {
    optionId: 'bestQuality',
    switchId: 'best-quality',
    value: initialStorage.settings.bestQuality,
    onEvent: bestVideoQuality
  },
  {
    optionId: 'autoDownload',
    switchId: 'auto-download',
    value: initialStorage.settings.autoDownload
  },
  {
    optionId: 'autoPlayNext',
    switchId: 'autoplay-next',
    value: initialStorage.settings.autoPlayNext
  },
  {
    optionId: 'autoPlayVideo',
    switchId: 'autoplay-video',
    value: initialStorage.settings.autoPlayVideo
  },
  {
    optionId: 'seekThumbnails',
    switchId: 'seek-thumbnails',
    value: initialStorage.settings.seekThumbnails
  },
  {
    optionId: 'seekPoints',
    switchId: 'seek-points',
    value: initialStorage.settings.seekPoints,
    onEvent: () => {
      sendMessage({action:'setting_changed',type:'seek_points',value:true});
    },
    offEvent: () => {
      sendMessage({action:'setting_changed',type:'seek_points',value:false});
    }
  },
  {
    optionId: 'skipButton',
    switchId: 'skip-button',
    value: initialStorage.settings.skipButton,
    onEvent: () => {
      sendMessage({action:'setting_changed',type:'skip_button',value:true});
    },
    offEvent: () => {
      sendMessage({action:'setting_changed',type:'skip_button',value:false});
    }
  },
  {
    optionId: 'reduceMotion',
    switchId: 'reduced-motion',
    value: initialStorage.settings.reduceMotion
  }];

const cachedAnimeData = [];

// Things that update when focusing this tab
$(document).on('visibilitychange', () => {
  if (document.hidden) return;
  updatePage();
});

function updatePage() {
  updateSwitches();

  const storage = getStorage();
  const data = url.includes('/anime/') ? getAnimeData() : undefined;

  if (data !== undefined) {
    const isBookmarked = storage.bookmarks.find(a => a.id === data.id) !== undefined;
    if (isBookmarked) $('.anitracker-bookmark-toggle .anitracker-title-icon-check').show();
    else $('.anitracker-bookmark-toggle .anitracker-title-icon-check').hide();

    const hasNotifications = storage.notifications.anime.find(a => a.id === data.id) !== undefined;
    if (hasNotifications) $('.anitracker-notifications-toggle .anitracker-title-icon-check').show();
    else $('.anitracker-notifications-toggle .anitracker-title-icon-check').hide();
  }

  if (!modalIsOpen() || $('.anitracker-view-notif-animes').length === 0) return;

  for (const item of $('.anitracker-notification-item-unwatched')) {
    const entry = storage.notifications.episodes.find(a => a.animeId === +$(item).attr('anime-data') && a.episode === +$(item).attr('episode-data') && a.watched === true);
    if (entry === undefined) continue;
    $(item).removeClass('anitracker-notification-item-unwatched');
    const eye = $(item).find('.anitracker-watched-toggle');
    eye.replaceClass('fa-eye', 'fa-eye-slash');
  }
}

function theatreMode(on) {
  if (on) $('.theatre').addClass('anitracker-theatre-mode');
  else $('.theatre').removeClass('anitracker-theatre-mode');
}

function playAnimation(elem, anim, type = '', duration) {
  return new Promise(resolve => {
    elem.css('animation', `anitracker-${anim} ${duration || animationTimes[anim]}s forwards linear ${type}`);
    if (animationTimes[anim] === undefined) resolve();
    setTimeout(() => {
      elem.css('animation', '');
      resolve();
    }, animationTimes[anim] * 1000);
  });
}

let modalCloseFunction = closeModal;
// AnimePahe Improvements modal
function addModal() {
  $(`
  <div id="anitracker-modal" tabindex="-1">
    <div id="anitracker-modal-content">
      <i tabindex="0" id="anitracker-modal-close" class="fa fa-close" title="Close modal">
      </i>
      <div id="anitracker-modal-body"></div>
    </div>
  </div>`).insertBefore('.main-header');

  $('#anitracker-modal').on('click', (e) => {
    if (e.target !== e.currentTarget) return;
    modalCloseFunction();
  });

  $('#anitracker-modal-close').on('click keydown', (e) => {
    if (e.type === 'keydown' && e.key !== "Enter") return;
    modalCloseFunction();
  });
}
addModal();

function openModal(closeFunction = closeModal) {
  if (closeFunction !== closeModal) $('#anitracker-modal-close').replaceClass('fa-close', 'fa-arrow-left');
  else $('#anitracker-modal-close').replaceClass('fa-arrow-left', 'fa-close');

  const storage = getStorage();

  return new Promise(resolve => {
    if (storage.settings.reduceMotion !== true) {
      playAnimation($('#anitracker-modal-content'), 'modalOpen');
      playAnimation($('#anitracker-modal'), 'fadeIn').then(() => {
        $('#anitracker-modal').focus();
        resolve();
      });
    }
    else {
      $('#anitracker-modal').focus();
      resolve();
    }

    $('#anitracker-modal').css('display','flex');
    modalCloseFunction = closeFunction;
  });
}

function closeModal() {
  const storage = getStorage();
  if (storage.settings.reduceMotion === true || $('#anitracker-modal').css('animation') !== 'none') {
    $('#anitracker-modal').hide();
    return;
  }

  playAnimation($('#anitracker-modal'), 'fadeIn', 'reverse', 0.1).then(() => {
    $('#anitracker-modal').hide();
  });
}

function modalIsOpen() {
  return $('#anitracker-modal').is(':visible');
}

let currentEpisodeTime = 0;
// Messages received from iframe
if (isEpisode()) {
  window.onmessage = function(e) {
    const data = e.data;

    if (typeof(data) === 'number') {
      currentEpisodeTime = Math.trunc(data);
      return;
    }

    const action = data.action;
    if (action === 'id_request') {
      sendMessage({action:"id_response",id:getAnimeData().id});
    }
    else if (action === 'anidb_id_request') {
      getAnidbId(data.id).then(result => {
        sendMessage({action:"anidb_id_response",id:result,originalId:data.id});
      });
    }
    else if (action === 'video_url_request') {
      const selected = {
        src: undefined,
        res: undefined,
        audio: undefined
      }
      for (const btn of $('#resolutionMenu>button')) {
        const src = $(btn).data('src');
        const res = +$(btn).data('resolution');
        const audio = $(btn).data('audio');
        if (selected.src !== undefined && selected.res < res) continue;
        if (selected.audio !== undefined && audio === 'jp' && selected.res <= res) continue; // Prefer dubs, since they don't have subtitles
        selected.src = src;
        selected.res = res;
        selected.audio = audio;
      }
      if (selected.src === undefined) {
        console.error("[AnimePahe Improvements] Didn't find video URL");
        return;
      }
      console.log('[AnimePahe Improvements] Found lowest resolution URL ' + selected.src);
      sendMessage({action:"video_url_response", url:selected.src});
    }
    else if (action === 'key') {
      if (data.key === 't') {
        toggleTheatreMode();
      }
    }
    else if (data === 'ended') {
      const storage = getStorage();
      if (storage.settings.autoPlayNext !== true) return;
      const elem = $('.sequel a');
      if (elem.length > 0) elem[0].click();
    }
    else if (action === 'next') {
      const elem = $('.sequel a');
      if (elem.length > 0) elem[0].click();
    }
    else if (action === 'previous') {
      const elem = $('.prequel a');
      if (elem.length > 0) elem[0].click();
    }
  };
}

function sendMessage(message) {
  const iframe = $('.embed-responsive-item');
  if (iframe.length === 0) return;
  iframe[0].contentWindow.postMessage(message,'*');
}

function toggleTheatreMode() {
  const storage = getStorage();
  theatreMode(!storage.settings.theatreMode);

  storage.settings.theatreMode = !storage.settings.theatreMode;
  saveData(storage);
  updateSwitches();
}

async function getAnidbId(paheId) {
  return new Promise(resolve => {
    const req = new XMLHttpRequest();
    req.open('GET', `/a/${paheId}`, true);
    req.onload = () => {
      for (const link of $(req.response).find('.external-links a')) {
        const elem = $(link);
        if (elem.text() !== 'AniDB') continue;
        resolve(/\/\/anidb.net\/anime\/(\d+)/.exec(elem.attr('href'))[1]);
      }
      resolve(undefined);
    }
    req.send();
  })
}

function getSeasonValue(season) {
  return ({winter:0, spring:1, summer:2, fall:3})[season.toLowerCase()];
}

function getSeasonName(season) {
  return ["winter","spring","summer","fall"][season];
}

function stringSimilarity(s1, s2) {
  let longer = s1;
  let shorter = s2;
  if (s1.length < s2.length) {
    longer = s2;
    shorter = s1;
  }
  const longerLength = longer.length;
  if (longerLength == 0) {
    return 1.0;
  }
  return (longerLength - editDistance(longer, shorter)) / parseFloat(longerLength);
}

function editDistance(s1, s2) {
  s1 = s1.toLowerCase();
  s2 = s2.toLowerCase();
  const costs = [];
  for (let i = 0; i <= s1.length; i++) {
    let lastValue = i;
    for (let j = 0; j <= s2.length; j++) {
      if (i == 0)
        costs[j] = j;
      else {
        if (j > 0) {
          let newValue = costs[j - 1];
          if (s1.charAt(i - 1) != s2.charAt(j - 1))
            newValue = Math.min(Math.min(newValue, lastValue),
              costs[j]) + 1;
          costs[j - 1] = lastValue;
          lastValue = newValue;
        }
      }
    }
    if (i > 0)
      costs[s2.length] = lastValue;
  }
  return costs[s2.length];
}

function searchForCollections() {
  if ($('.search-results a').length === 0) return;

  const baseName = $($('.search-results .result-title')[0]).text();

  const request = new XMLHttpRequest();
  request.open('GET', '/api?m=search&q=' + encodeURIComponent(baseName), true);

  request.onload = () => {
    if (request.readyState !== 4 || request.status !== 200 ) return;

    response = JSON.parse(request.response).data;

    if (response == undefined) return;

    let seriesList = [];

    for (const anime of response) {
      if (stringSimilarity(baseName, anime.title) >= 0.42 || (anime.title.startsWith(baseName) && stringSimilarity(baseName, anime.title) >= 0.25)) {
        seriesList.push(anime);
      }
    }

    if (seriesList.length < 2) return;
    seriesList = sortAnimesChronologically(seriesList);

    displayCollection(baseName, seriesList);
  };

  request.send();
}

new MutationObserver(function(mutationList, observer) {
  if (!searchComplete()) return;
  searchForCollections();
}).observe($('.search-results-wrap')[0], { childList: true });

function searchComplete() {
  return $('.search-results').length !== 0 && $('.search-results a').length > 0;
}

function displayCollection(baseName, seriesList) {
  $(`
    <li class="anitracker-collection" data-index="-1">
      <a title="${toHtmlCodes(baseName + " - Collection")}" href="javascript:;">
        <img src="${seriesList[0].poster.slice(0, -3) + 'th.jpg'}" referrerpolicy="no-referrer" style="pointer-events: all !important;max-width: 30px;">
        <img src="${seriesList[1].poster.slice(0, -3) + 'th.jpg'}" referrerpolicy="no-referrer" style="pointer-events: all !important;max-width: 30px;left:30px;">
        <div class="result-title">${baseName}</div>
        <div class="result-status"><strong>Collection</strong> - ${seriesList.length} Entries</div>
      </a>
    </li>`).prependTo('.search-results');

  function displayInModal() {
    $('#anitracker-modal-body').empty();
    $(`
    <h4>Collection</h4>
    <div class="anitracker-modal-list-container">
      <div class="anitracker-modal-list" style="min-height: 100px;min-width: 200px;"></div>
    </div>`).appendTo('#anitracker-modal-body');

    for (const anime of seriesList) {
      $(`
      <div class="anitracker-big-list-item anitracker-collection-item">
        <a href="/anime/${anime.session}" title="${toHtmlCodes(anime.title)}">
          <img src="${anime.poster.slice(0, -3) + 'th.jpg'}" referrerpolicy="no-referrer" alt="[Thumbnail of ${anime.title}]">
          <div class="anitracker-main-text">${anime.title}</div>
          <div class="anitracker-subtext"><strong>${anime.type}</strong> - ${anime.episodes > 0 ? anime.episodes : '?'} Episode${anime.episodes === 1 ? '' : 's'} (${anime.status})</div>
          <div class="anitracker-subtext">${anime.season} ${anime.year}</div>
        </a>
      </div>`).appendTo('#anitracker-modal-body .anitracker-modal-list');
    }

    openModal();
  }

  $('.anitracker-collection').on('click', displayInModal);
  $('.input-search').on('keyup', (e) => {
    if (e.key === "Enter" && $('.anitracker-collection').hasClass('selected')) displayInModal();
  });
}

function getSeasonTimeframe(from, to) {
  const filters = [];
  for (let i = from.year; i <= to.year; i++) {
    const start = i === from.year ? from.season : 0;
    const end = i === to.year ? to.season : 3;
    for (let d = start; d <= end; d++) {
      filters.push({type: 'season_entry', value: {year: i, season: d}});
    }
  }
  return filters;
}

const is404 = $('h1').text().includes('404');

if (!isRandomAnime() && initialStorage.cache !== undefined) {
  const storage = getStorage();
  delete storage.cache;
  saveData(storage);
}

const filterSearchCache = {};

const filterValues = {
  "genre":[
    {"name":"Comedy","value":"comedy"},{"name":"Slice of Life","value":"slice-of-life"},{"name":"Romance","value":"romance"},{"name":"Ecchi","value":"ecchi"},{"name":"Drama","value":"drama"},
    {"name":"Supernatural","value":"supernatural"},{"name":"Sports","value":"sports"},{"name":"Horror","value":"horror"},{"name":"Sci-Fi","value":"sci-fi"},{"name":"Action","value":"action"},
    {"name":"Fantasy","value":"fantasy"},{"name":"Mystery","value":"mystery"},{"name":"Suspense","value":"suspense"},{"name":"Adventure","value":"adventure"},{"name":"Boys Love","value":"boys-love"},
    {"name":"Girls Love","value":"girls-love"},{"name":"Hentai","value":"hentai"},{"name":"Gourmet","value":"gourmet"},{"name":"Erotica","value":"erotica"},{"name":"Avant Garde","value":"avant-garde"},
    {"name":"Award Winning","value":"award-winning"}
  ],
  "theme":[
    {"name":"Adult Cast","value":"adult-cast"},{"name":"Anthropomorphic","value":"anthropomorphic"},{"name":"Detective","value":"detective"},{"name":"Love Polygon","value":"love-polygon"},
    {"name":"Mecha","value":"mecha"},{"name":"Music","value":"music"},{"name":"Psychological","value":"psychological"},{"name":"School","value":"school"},{"name":"Super Power","value":"super-power"},
    {"name":"Space","value":"space"},{"name":"CGDCT","value":"cgdct"},{"name":"Romantic Subtext","value":"romantic-subtext"},{"name":"Historical","value":"historical"},{"name":"Video Game","value":"video-game"},
    {"name":"Martial Arts","value":"martial-arts"},{"name":"Idols (Female)","value":"idols-female"},{"name":"Idols (Male)","value":"idols-male"},{"name":"Gag Humor","value":"gag-humor"},{"name":"Parody","value":"parody"},
    {"name":"Performing Arts","value":"performing-arts"},{"name":"Military","value":"military"},{"name":"Harem","value":"harem"},{"name":"Reverse Harem","value":"reverse-harem"},{"name":"Samurai","value":"samurai"},
    {"name":"Vampire","value":"vampire"},{"name":"Mythology","value":"mythology"},{"name":"High Stakes Game","value":"high-stakes-game"},{"name":"Strategy Game","value":"strategy-game"},
    {"name":"Magical Sex Shift","value":"magical-sex-shift"},{"name":"Racing","value":"racing"},{"name":"Isekai","value":"isekai"},{"name":"Workplace","value":"workplace"},{"name":"Iyashikei","value":"iyashikei"},
    {"name":"Time Travel","value":"time-travel"},{"name":"Gore","value":"gore"},{"name":"Educational","value":"educational"},{"name":"Delinquents","value":"delinquents"},{"name":"Organized Crime","value":"organized-crime"},
    {"name":"Otaku Culture","value":"otaku-culture"},{"name":"Medical","value":"medical"},{"name":"Survival","value":"survival"},{"name":"Reincarnation","value":"reincarnation"},{"name":"Showbiz","value":"showbiz"},
    {"name":"Team Sports","value":"team-sports"},{"name":"Mahou Shoujo","value":"mahou-shoujo"},{"name":"Combat Sports","value":"combat-sports"},{"name":"Crossdressing","value":"crossdressing"},
    {"name":"Visual Arts","value":"visual-arts"},{"name":"Childcare","value":"childcare"},{"name":"Pets","value":"pets"},{"name":"Love Status Quo","value":"love-status-quo"},{"name":"Urban Fantasy","value":"urban-fantasy"},
    {"name":"Villainess","value":"villainess"}
  ],
  "type":[
    {"name":"TV","value":"tv"},{"name":"Movie","value":"movie"},{"name":"OVA","value":"ova"},{"name":"ONA","value":"ona"},{"name":"Special","value":"special"},{"name":"Music","value":"music"}
  ],
  "demographic":[
    {"name":"Shounen","value":"shounen"},{"name":"Shoujo","value":"shoujo"},{"name":"Seinen","value":"seinen"},{"name":"Kids","value":"kids"},{"name":"Josei","value":"josei"}
  ],
  "status":[
    {"value":"airing"},{"value":"completed"}
  ]
};

const filterDefaultRules = {
  genre: {
    include: "and",
    exclude: "and"
  },
  theme: {
    include: "and",
    exclude: "and"
  },
  demographic: {
    include: "or",
    exclude: "and"
  },
  type: {
    include: "or",
    exclude: "and"
  },
  season: {
    include: "or",
    exclude: "and"
  },
  status: {
    include: "or"
  }
};

const filterRules = JSON.parse(JSON.stringify(filterDefaultRules));

function buildFilterString(type, value) {
  if (type === 'status') return value;
  if (type === 'season_entry') return `season/${getSeasonName(value.season)}-${value.year}`;

  return type + '/' + value;
}

const seasonFilterRegex = /^!?(spring|summer|winter|fall)-(\d{4})\.\.(spring|summer|winter|fall)-(\d{4})$/;

function getFilteredList(filtersInput) {
  let filtersChecked = 0;
  let filtersTotal = 0;

  function getPage(pageUrl) {
    return new Promise((resolve, reject) => {
      const cached = filterSearchCache[pageUrl];
      if (cached !== undefined) { // If cache exists
        if (cached === 'invalid') { // Not sure if it ever is 'invalid'
          resolve([]);
          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) {
          filterSearchCache[pageUrl] = [];
          resolve([]);
          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 = filter.type;
        if (filter.value === 'none') {
          filtersTotal += filterValues[filterType].length;

          getLists(filterValues[filterType].map(a => {return {type: filterType, value: a.value, exclude: false};})).then((filtered) => {
            getPage('/anime').then((unfiltered) => {
              const none = [];
              for (const entry of unfiltered) {
                const found = filtered.find(list => list.entries.find(a => a.name === entry.name));
                if (!filter.exclude && found !== undefined) continue;
                if (filter.exclude && found === undefined) continue;
                none.push(entry);
              }

              lists.push({
                type: filterType,
                excludedFilter: false,
                entries: none
              });

              check();
            });
          });
          return;
        }
        if (filterType === 'season') {
          const seasonFilters = getSeasonTimeframe(filter.value.from, filter.value.to);
          filtersTotal += seasonFilters.length;

          getLists(seasonFilters).then((filtered) => {
            const filtersResult = [];
            if (filter.exclude) getPage('/anime').then((unfiltered) => {
              for (const entry of unfiltered) {
                if (filtered.find(list => list.entries.find(a => a.name === entry.name)) !== undefined) continue;
                filtersResult.push(entry);
              }

              lists.push({
                type: 'season',
                excludedFilter: true,
                entries: filtersResult
              });

              check();
            });
            else {
              for (const list of filtered) {
                filtersResult.push(...list.entries);
              }

              lists.push({
                type: 'season',
                excludedFilter: false,
                entries: filtersResult
              });

              check();
            }
          });

          return;
        }
        if (filter.exclude) {
          getPage('/anime/' + buildFilterString(filterType, filter.value)).then((filtered) => {
            getPage('/anime').then((unfiltered) => {
              const included = [];
              for (const entry of unfiltered) {
                if (filtered.find(a => a.name === entry.name) !== undefined) continue;
                included.push(entry);
              }

              lists.push({
                type: filterType,
                excludedFilter: true,
                entries: included
              });

              check();
            });
          });
          return;
        }
        getPage('/anime/' + buildFilterString(filterType, filter.value)).then((result) => {
          if (result !== undefined) {
            lists.push({
              type: filterType,
              excludedFilter: false,
              entries: result
            });
          }
          if (filtersTotal > 0) {
            filtersChecked++;
            $($('.anitracker-filter-spinner>span')[0]).text(Math.floor((filtersChecked/filtersTotal) * 100).toString() + '%');
          }

          check();
        });
      }

      check();
    });
  }

  function combineLists(lists, rule) {
    if (lists.length === 0) return [];

    // Start with the first filter list result, then compare others to it
    let combinedList = lists[0];
    lists.splice(0,1); // Remove the first entry
    for (const list of lists) {
      // If the rule of this filter type is 'or,' start from the current list
      // Otherwise, start from an empty list
      const updatedList = rule === 'or' ? combinedList : [];
      if (rule === 'and') for (const anime of list) {
        // The anime has to exist in both the current and the checked list
        if (combinedList.find(a => a.name === anime.name) === undefined) continue;
        updatedList.push(anime);
      }
      else if (rule === 'or') for (const anime of list) {
        // The anime just has to not already exist in the current list
        if (combinedList.find(a => a.name === anime.name) !== undefined) continue;
        updatedList.push(anime);
      }
      combinedList = updatedList;
    }
    return combinedList;
  }

  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;
    }

    filtersTotal = filters.length;


    getLists(filters).then((listsInput) => {
      const lists = JSON.parse(JSON.stringify(listsInput));

      // groupedLists entries have the following format:
      /* {
            type, // the type of filter, eg. 'genre'
            includeLists: [
              <list of included anime>
            ],
            excludeLists: [
              <list of excluded anime>
            ]
         }
       */
      const groupedLists = [];
      for (const list of lists) {
        let foundGroup = groupedLists.find(a => a.type === list.type);
        if (foundGroup === undefined) {
          groupedLists.push({
            type: list.type,
            includeLists: [],
            excludeLists: []
          });
          foundGroup = groupedLists[groupedLists.length - 1];
        }

        if (list.excludedFilter) foundGroup.excludeLists.push(list.entries);
        else foundGroup.includeLists.push(list.entries);
      }

      let finalList;

      for (const group of groupedLists) {
        const includeList = combineLists(group.includeLists, filterRules[group.type].include);
        const excludeList = combineLists(group.excludeLists, filterRules[group.type].exclude);

        // Combine the include and exclude lists

        // If the exclude list exists, start from an empty list
        // Otherwise, just default to the include list
        let groupFinalList = [];
        if (excludeList.length > 0 && includeList.length > 0) {
          const combineRule = filterRules[group.type].combined;
          for (const entry of excludeList) {
            if (groupFinalList.find(a => a.name === entry.name) !== undefined) continue; // Don't include duplicates
            if (combineRule === 'or') {
              if (includeList.find(a => a.name === entry.name) !== undefined) continue;
              groupFinalList.push(entry);
              continue;
            }
            // Otherwise, the rule is 'and'
            if (includeList.find(a => a.name === entry.name) === undefined) continue;
            groupFinalList.push(entry);
          }
        }
        else if (excludeList.length === 0) groupFinalList = includeList;
        else if (includeList.length === 0) groupFinalList = excludeList;

        // If the current final list is undefined, just add the resulting list to it and continue
        if (finalList === undefined) {
          finalList = groupFinalList;
          continue;
        }

        const newFinalList = [];
        // Loop through the resulting list
        // Join together with 'and'
        for (const anime of groupFinalList) {
          if (finalList.find(a => a.name === anime.name) === undefined) continue;
          newFinalList.push(anime);
        }
        finalList = newFinalList;
      }

      resolve(finalList);
    });
  });
}

function searchList(fuseClass, list, query, limit = 80) {
  const fuse = new fuseClass(list, {
    keys: ['name'],
    findAllMatches: true
  });

  const matching = fuse.search(query);
  return matching.map(a => {return a.item}).splice(0,limit);
}

function timeSince(date) {
  const seconds = Math.floor((new Date() - date) / 1000);

  let interval = Math.floor(seconds / 31536000);

  if (interval >= 1) {
    return interval + " year" + (interval > 1 ? 's' : '');
  }
  interval = Math.floor(seconds / 2592000);
  if (interval >= 1) {
    return interval + " month" + (interval > 1 ? 's' : '');
  }
  interval = Math.floor(seconds / 86400);
  if (interval >= 1) {
    return interval + " day" + (interval > 1 ? 's' : '');
  }
  interval = Math.floor(seconds / 3600);
  if (interval >= 1) {
    return interval + " hour" + (interval > 1 ? 's' : '');
  }
  interval = Math.floor(seconds / 60);
  if (interval >= 1) {
    return interval + " minute" + (interval > 1 ? 's' : '');
  }
  return seconds + " second" + (seconds > 1 ? 's' : '');
}

if (window.location.pathname.startsWith('/customlink')) {
  const parts = {
    animeSession: '',
    episodeSession: '',
    time: -1
  };
  const entries = Array.from(new URLSearchParams(window.location.search).entries()).sort((a,b) => a[0] > b[0] ? 1 : -1);
  for (const entry of entries) {
    if (entry[0] === 'a') {
      parts.animeSession = getAnimeData(decodeURIComponent(entry[1])).session;
      continue;
    }
    if (entry[0] === 'e') {
      if (parts.animeSession === '') return;
      parts.episodeSession = getEpisodeSession(parts.animeSession, +entry[1]);
      continue;
    }
    if (entry[0] === 't') {
      if (parts.animeSession === '') return;
      if (parts.episodeSession === '') continue;

      parts.time = +entry[1];
      continue;
    }
  }

  const destination = (() => {
    if (parts.animeSession !== '' && parts.episodeSession === '' && parts.time === -1) {
      return '/anime/' + parts.animeSession + '?ref=customlink';
    }
    if (parts.animeSession !== '' && parts.episodeSession !== '' && parts.time === -1) {
      return '/play/' + parts.animeSession + '/' + parts.episodeSession + '?ref=customlink';
    }
    if (parts.animeSession !== '' && parts.episodeSession !== '' && parts.time >= 0) {
      return '/play/' + parts.animeSession + '/' + parts.episodeSession + '?time=' + parts.time + '&ref=customlink';
    }
    return undefined;
  })();

  if (destination !== undefined) {
    document.title = "Redirecting... :: animepahe";
    $('h1').text('Redirecting...');
    window.location.replace(destination);
  }

  return;
}

// Main key events
if (!is404) $(document).on('keydown', (e) => {
  if ($(e.target).is(':input')) return;

  if (modalIsOpen() && ['Escape','Backspace'].includes(e.key)) {
    modalCloseFunction();
    return;
  }
  if (!isEpisode() || modalIsOpen()) return;
  if (e.key === 't') {
    toggleTheatreMode();
  }
  else {
    sendMessage({action:"key",key:e.key});
    $('.embed-responsive-item')[0].contentWindow.focus();
    if ([" "].includes(e.key)) e.preventDefault();
  }
});

if (window.location.pathname.startsWith('/queue')) {
  $(`
  <span style="font-size:.6em;">&nbsp;&nbsp;&nbsp;(Incoming episodes)</span>
  `).appendTo('h2');
}

if (/^\/anime\/\w+(\/[\w\-\.]+)?$/.test(window.location.pathname)) {
  if (is404) return;

  const filter = /\/anime\/([^\/]+)\/?([^\/]+)?/.exec(window.location.pathname);

  if (filter[2] !== undefined) {
    if (filterRules[filter[1]] === undefined) return;
    if (filter[1] === 'season') {
      window.location.replace(`/anime?${filter[1]}=${filter[2]}..${filter[2]}`);
      return;
    }
    window.location.replace(`/anime?${filter[1]}=${filter[2]}`);
  }
  else {
    window.location.replace(`/anime?other=${filter[1]}`);
  }
  return;
}

function getDayName(day) {
  return [
    "Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"
  ][day];
}

function toHtmlCodes(string) {
  return $('<div>').text(string).html().replace(/"/g, "&quot;").replace(/'/g, "&#039;");
}

// Bookmark & episode feed header buttons
$(`
<div class="anitracker-header">
  <button class="anitracker-header-notifications anitracker-header-button" title="View episode feed">
    <i class="fa fa-bell" aria-hidden="true"></i>
    <i style="display:none;" aria-hidden="true" class="fa fa-circle anitracker-header-notifications-circle"></i>
  </button>
  <button class="anitracker-header-bookmark anitracker-header-button" title="View bookmarks"><i class="fa fa-bookmark" aria-hidden="true"></i></button>
</div>`).insertAfter('.navbar-nav');

let currentNotificationIndex = 0;

function openNotificationsModal() {
  currentNotificationIndex = 0;
  const oldStorage = getStorage();
  $('#anitracker-modal-body').empty();

  $(`
  <h4>Episode Feed</h4>
  <div class="btn-group" style="margin-bottom: 10px;">
    <button class="btn btn-secondary anitracker-view-notif-animes">
      Handle Feed...
    </button>
  </div>
  <div class="anitracker-modal-list-container">
    <div class="anitracker-modal-list" style="min-height: 100px;min-width: 200px;">
      <div id="anitracker-notifications-list-spinner" style="display:flex;justify-content:center;">
        <div class="spinner-border text-danger" role="status">
          <span class="sr-only">Loading...</span>
        </div>
      </div>
    </div>
  </div>`).appendTo('#anitracker-modal-body');

  $('.anitracker-view-notif-animes').on('click', () => {
    $('#anitracker-modal-body').empty();
    const storage = getStorage();
    $(`
    <h4>Handle Episode Feed</h4>
    <div class="anitracker-modal-list-container">
      <div class="anitracker-modal-list" style="min-height: 100px;min-width: 200px;"></div>
    </div>
    `).appendTo('#anitracker-modal-body');
    [...storage.notifications.anime].sort((a,b) => a.latest_episode > b.latest_episode ? 1 : -1).forEach(g => {
      const latestEp = new Date(g.latest_episode + " UTC");
      const latestEpString = g.latest_episode !== undefined ? `${getDayName(latestEp.getDay())} ${latestEp.toLocaleTimeString()}` : "None found";
      $(`
      <div class="anitracker-modal-list-entry" animeid="${g.id}" animename="${toHtmlCodes(g.name)}">
        <a href="/a/${g.id}" target="_blank" title="${toHtmlCodes(g.name)}">
          ${g.name}
        </a><br>
        <span>
          Latest episode: ${latestEpString}
        </span><br>
        <div class="btn-group">
          <button class="btn btn-danger anitracker-delete-button anitracker-flat-button" title="Remove this anime from the episode feed">
            <i class="fa fa-trash" aria-hidden="true"></i>
            &nbsp;Remove
          </button>
        </div>
        <div class="btn-group">
          <button class="btn btn-secondary anitracker-get-all-button anitracker-flat-button" title="Put all episodes in the feed" ${g.hasFirstEpisode ? 'disabled=""' : ''}>
            <i class="fa fa-rotate-right" aria-hidden="true"></i>
            &nbsp;Get All
          </button>
        </div>
      </div>`).appendTo('#anitracker-modal-body .anitracker-modal-list');
    });
    if (storage.notifications.anime.length === 0) {
      $("<span>Use the <i class=\"fa fa-bell\" title=\"bell\"></i> button on an ongoing anime to add it to the feed.</span>").appendTo('#anitracker-modal-body .anitracker-modal-list');
    }

    $('.anitracker-modal-list-entry .anitracker-get-all-button').on('click', (e) => {
      const elem = $(e.currentTarget);
      const id = +elem.parents().eq(1).attr('animeid');
      const storage = getStorage();

      const found = storage.notifications.anime.find(a => a.id === id);
      if (found === undefined) {
        console.error("[AnimePahe Improvements] Couldn't find feed for anime with id " + id);
        return;
      }

      found.hasFirstEpisode = true;
      found.updateFrom = 0;
      saveData(storage);

      elem.replaceClass("btn-secondary", "btn-primary");
      setTimeout(() => {
        elem.replaceClass("btn-primary", "btn-secondary");
        elem.prop('disabled', true);
      }, 200);
    });

    $('.anitracker-modal-list-entry .anitracker-delete-button').on('click', (e) => {
      const parent = $(e.currentTarget).parents().eq(1);
      const name = parent.attr('animename');
      toggleNotifications(name, +parent.attr('animeid'));

      const name2 = getAnimeName();
      if (name2.length > 0 && name2 === name) {
        $('.anitracker-notifications-toggle .anitracker-title-icon-check').hide();
      }

      parent.remove();
    });

    openModal(openNotificationsModal);
  });

  const animeData = [];
  const queue = [...oldStorage.notifications.anime];

  openModal().then(() => {
    if (queue.length > 0) next();
    else done();
  });

  async function next() {
    if (queue.length === 0) done();
    const anime = queue.shift();
    const data = await updateNotifications(anime.name);

    if (data === -1) {
      $("<span>An error occured.</span>").appendTo('#anitracker-modal-body .anitracker-modal-list');
      return;
    }
    animeData.push({
      id: anime.id,
      data: data
    });

    if (queue.length > 0 && $('#anitracker-notifications-list-spinner').length > 0) next();
    else done();
  }

  function done() {
    if ($('#anitracker-notifications-list-spinner').length === 0) return;
    const storage = getStorage();
    let removedAnime = 0;
    for (const anime of storage.notifications.anime) {
      if (anime.latest_episode === undefined || anime.dont_ask === true) continue;
      const time = Date.now() - new Date(anime.latest_episode + " UTC").getTime();
      if ((time / 1000 / 60 / 60 / 24 / 7) > 2) {
        const remove = confirm(`[AnimePahe Improvements]\n\nThe latest episode for ${anime.name} was more than 2 weeks ago. Remove it from the feed?\n\nThis prompt will not be shown again.`);
        if (remove === true) {
          toggleNotifications(anime.name, anime.id);
          removedAnime++;
        }
        else {
          anime.dont_ask = true;
          saveData(storage);
        }
      }
    }
    if (removedAnime > 0) {
      openNotificationsModal();
      return;
    }
    $('#anitracker-notifications-list-spinner').remove();
    storage.notifications.episodes.sort((a,b) => a.time < b.time ? 1 : -1);
    storage.notifications.lastUpdated = Date.now();
    saveData(storage);
    if (storage.notifications.episodes.length === 0) {
      $("<span>Nothing here yet!</span>").appendTo('#anitracker-modal-body .anitracker-modal-list');
    }
    else addToList(20);
  }

  function addToList(num) {
    const storage = getStorage();
    const index = currentNotificationIndex;
    for (let i = currentNotificationIndex; i < storage.notifications.episodes.length; i++) {
      const ep = storage.notifications.episodes[i];
      if (ep === undefined) break;
      currentNotificationIndex++;
      const data = animeData.find(a => a.id === ep.animeId)?.data;
      if (data === undefined) {
        console.error(`[AnimePahe Improvements] Could not find corresponding anime "${ep.animeName}" with ID ${ep.animeId} (episode ${ep.episode})`);
        continue;
      }

      const releaseTime = new Date(ep.time + " UTC");
      $(`
      <div class="anitracker-big-list-item anitracker-notification-item${ep.watched ? "" : " anitracker-notification-item-unwatched"} anitracker-temp" anime-data="${data.id}" episode-data="${ep.episode}">
        <a href="/play/${data.session}/${ep.session}" target="_blank" title="${toHtmlCodes(data.title)}">
          <img src="${data.poster.slice(0, -3) + 'th.jpg'}" referrerpolicy="no-referrer" alt="[Thumbnail of ${toHtmlCodes(data.title)}]"}>
          <i class="fa ${ep.watched ? 'fa-eye-slash' : 'fa-eye'} anitracker-watched-toggle" tabindex="0" aria-hidden="true" title="Mark this episode as ${ep.watched ? 'unwatched' : 'watched'}"></i>
          <div class="anitracker-main-text">${data.title}</div>
          <div class="anitracker-subtext"><strong>Episode ${ep.episode}</strong></div>
          <div class="anitracker-subtext">${timeSince(releaseTime)} ago (${releaseTime.toLocaleDateString()})</div>
        </a>
      </div>`).appendTo('#anitracker-modal-body .anitracker-modal-list');
      if (i > index+num-1) break;
    }

    $('.anitracker-notification-item.anitracker-temp').on('click', (e) => {
      $(e.currentTarget).find('a').blur();
    });

    $('.anitracker-notification-item.anitracker-temp .anitracker-watched-toggle').on('click keydown', (e) => {
      if (e.type === 'keydown' && e.key !== "Enter") return;
      e.preventDefault();
      const storage = getStorage();
      const elem = $(e.currentTarget);
      const parent = elem.parents().eq(1);
      const ep = storage.notifications.episodes.find(a => a.animeId === +parent.attr('anime-data') && a.episode === +parent.attr('episode-data'));
      if (ep === undefined) {
        console.error("[AnimePahe Improvements] couldn't mark episode as watched/unwatched");
        return;
      }
      parent.toggleClass('anitracker-notification-item-unwatched');
      elem.toggleClass('fa-eye').toggleClass('fa-eye-slash');

      if (e.type === 'click') elem.blur();

      ep.watched = !ep.watched;
      elem.attr('title', `Mark this episode as ${ep.watched ? 'unwatched' : 'watched'}`);

      saveData(storage);
    });

    $('.anitracker-notification-item.anitracker-temp').removeClass('anitracker-temp');

  }

  $('#anitracker-modal-body').on('scroll', () => {
    const elem = $('#anitracker-modal-body');
    if (elem.scrollTop() >= elem[0].scrollTopMax) {
      if ($('.anitracker-view-notif-animes').length === 0) return;
      addToList(20);
    }
  });
}

$('.anitracker-header-notifications').on('click', openNotificationsModal);

$('.anitracker-header-bookmark').on('click', () => {
  $('#anitracker-modal-body').empty();
  const storage = getStorage();
  $(`
  <h4>Bookmarks</h4>
  <div class="anitracker-modal-list-container">
    <div class="anitracker-modal-list" style="min-height: 100px;min-width: 200px;">
      <div class="btn-group">
        <input autocomplete="off" class="form-control anitracker-text-input-bar anitracker-modal-search" placeholder="Search">
        <button dir="down" class="btn btn-secondary dropdown-toggle anitracker-reverse-order-button anitracker-list-btn" title="Sort direction (down is default, and means newest first)"></button>
      </div>
    </div>
  </div>
  `).appendTo('#anitracker-modal-body');

  $('.anitracker-modal-search').on('input', (e) => {
    setTimeout(() => {
      const query = $(e.target).val();
      for (const entry of $('.anitracker-modal-list-entry')) {
        if ($($(entry).find('a,span')[0]).text().toLowerCase().includes(query)) {
          $(entry).show();
          continue;
        }
        $(entry).hide();
      }
    }, 10);
  });

  function applyDeleteEvents() {
    $('.anitracker-modal-list-entry button').on('click', (e) => {
      const id = $(e.currentTarget).parent().attr('animeid');
      toggleBookmark(id);

      const data = getAnimeData();
      if (data !== undefined && data.id === +id) {
        $('.anitracker-bookmark-toggle .anitracker-title-icon-check').hide();
      }

      $(e.currentTarget).parent().remove();
    });
  }

  // When clicking the reverse order button
  $('.anitracker-reverse-order-button').on('click', (e) => {
    const btn = $(e.target);
    if (btn.attr('dir') === 'down') {
      btn.attr('dir', 'up');
      btn.addClass('anitracker-up');
    }
    else {
      btn.attr('dir', 'down');
      btn.removeClass('anitracker-up');
    }

    const entries = [];
    for (const entry of $('.anitracker-modal-list-entry')) {
      entries.push(entry.outerHTML);
    }
    entries.reverse();
    $('.anitracker-modal-list-entry').remove();
    for (const entry of entries) {
      $(entry).appendTo($('.anitracker-modal-list'));
    }
    applyDeleteEvents();
  });

  [...storage.bookmarks].reverse().forEach(g => {
    $(`
    <div class="anitracker-modal-list-entry" animeid="${g.id}">
      <a href="/a/${g.id}" target="_blank" title="${toHtmlCodes(g.name)}">
        ${g.name}
      </a><br>
      <button class="btn btn-danger anitracker-flat-button" title="Remove this bookmark">
        <i class="fa fa-trash" aria-hidden="true"></i>
        &nbsp;Remove
      </button>
    </div>`).appendTo('#anitracker-modal-body .anitracker-modal-list')
  });
  if (storage.bookmarks.length === 0) {
    $(`<span style="display: block;">No bookmarks yet!</span>`).appendTo('#anitracker-modal-body .anitracker-modal-list');
  }

  applyDeleteEvents();
  openModal();
  $('#anitracker-modal-body')[0].scrollTop = 0;
});

function toggleBookmark(id, name=undefined) {
  const storage = getStorage();
  const found = storage.bookmarks.find(g => g.id === +id);

  if (found !== undefined) {
    const index = storage.bookmarks.indexOf(found);
    storage.bookmarks.splice(index, 1);

    saveData(storage);

    return false;
  }

  if (name === undefined) return false;

  storage.bookmarks.push({
    id: +id,
    name: name
  });
  saveData(storage);

  return true;
}

function toggleNotifications(name, id = undefined) {
  const storage = getStorage();
  const found = (() => {
    if (id !== undefined) return storage.notifications.anime.find(g => g.id === id);
    else return storage.notifications.anime.find(g => g.name === name);
  })();

  if (found !== undefined) {
    const index = storage.notifications.anime.indexOf(found);
    storage.notifications.anime.splice(index, 1);

    storage.notifications.episodes = storage.notifications.episodes.filter(a => a.animeName !== found.name); // Uses the name, because old data might not be updated to use IDs

    saveData(storage);

    return false;
  }

  const animeData = getAnimeData(name);

  storage.notifications.anime.push({
    name: name,
    id: animeData.id
  });
  saveData(storage);

  return true;
}

async function updateNotifications(animeName, storage = getStorage()) {
  const nobj = storage.notifications.anime.find(g => g.name === animeName);
  if (nobj === undefined) {
    toggleNotifications(animeName);
    return;
  }
  const data = await asyncGetAnimeData(animeName, nobj.id);
  if (data === undefined) return -1;
  const episodes = await asyncGetAllEpisodes(data.session, 'desc');
  if (episodes === undefined) return 0;

  return new Promise((resolve, reject) => {
    if (episodes.length === 0) resolve(undefined);

    nobj.latest_episode = episodes[0].created_at;

    if (nobj.name !== data.title) {
      for (const ep of storage.notifications.episodes) {
        if (ep.animeName !== nobj.name) continue;
        ep.animeName = data.title;
      }
      nobj.name = data.title;
    }

    const compareUpdateTime = nobj.updateFrom ?? storage.notifications.lastUpdated;
    if (nobj.updateFrom !== undefined) delete nobj.updateFrom;

    for (const ep of episodes) {
      const found = storage.notifications.episodes.find(a => a.episode === ep.episode && a.animeId === nobj.id) ?? storage.notifications.episodes.find(a => a.episode === ep.episode && a.animeName === data.title);
      if (found !== undefined) {
        found.session = ep.session;
        if (found.animeId === undefined) found.animeId = nobj.id;

        if (episodes.indexOf(ep) === episodes.length - 1) nobj.hasFirstEpisode = true;
        continue;
      }

      if (new Date(ep.created_at + " UTC").getTime() < compareUpdateTime) {
        continue;
      }

      storage.notifications.episodes.push({
        animeName: nobj.name,
        animeId: nobj.id,
        session: ep.session,
        episode: ep.episode,
        time: ep.created_at,
        watched: false
      });
    }

    const length = storage.notifications.episodes.length;
    if (length > 100) {
      storage.notifications.episodes = storage.notifications.episodes.slice(length - 100);
    }

    saveData(storage);

    resolve(data);
  });
}

const paramArray = Array.from(new URLSearchParams(window.location.search));

const refArg01 = paramArray.find(a => a[0] === 'ref');
if (refArg01 !==  undefined) {
  const ref = refArg01[1];
  if (ref === '404') {
    alert('[AnimePahe Improvements]\n\nThe session was outdated, and has been refreshed. Please try that link again.');
  }
  else if (ref === 'customlink' && isEpisode() && initialStorage.settings.autoDelete) {
    const name = getAnimeName();
    const num = getEpisodeNum();
    if (initialStorage.linkList.find(e => e.animeName === name && e.type === 'episode' && e.episodeNum !== num)) { // If another episode is already stored
      $(`
        <span style="display:block;width:100%;text-align:center;" class="anitracker-from-share-warning">
          The current episode data for this anime was not replaced due to coming from a share link.
          <br>Refresh this page to replace it.
          <br><span class="anitracker-text-button" tabindex="0">Dismiss</span>
        </span>`).prependTo('.content-wrapper');

      $('.anitracker-from-share-warning>span').on('click keydown', function(e) {
        if (e.type === 'keydown' && e.key !== "Enter") return;
        $(e.target).parent().remove();
      });
    }
  }

  window.history.replaceState({}, document.title, window.location.origin + window.location.pathname);
}

function getCurrentSeason() {
  const month = new Date().getMonth();
  return Math.trunc(month/3);
}

function getFiltersFromParams(params) {
  const filters = [];
  for (const [key, value] of params.entries()) {
    const inputFilters = value.split(',');  // Get all filters of this filter type
    for (const filter of inputFilters) {
      if (filterRules[key] === undefined) continue;

      const exclude = filter.startsWith('!');
      if (key === 'season' && seasonFilterRegex.test(filter)) {
        const parts = seasonFilterRegex.exec(filter);
        if (!parts.includes(undefined) && ![parseInt(parts[2]),parseInt(parts[4])].includes(NaN)) {
          filters.push({
            type: 'season',
            value: {
              from: { season: getSeasonValue(parts[1]), year: parseInt(parts[2]) },
              to: { season: getSeasonValue(parts[3]), year: parseInt(parts[4]) }
            },
            exclude: exclude
          });
        }
        continue;
      }

      filters.push({
        type: key,
        value: filter.replace(/^!/,''),
        exclude: exclude
      });
    }
  }
  return filters;
}

function loadIndexPage() {
  const animeList = getAnimeList();
  filterSearchCache['/anime'] = JSON.parse(JSON.stringify(animeList));

  $(`
  <div id="anitracker" class="anitracker-index" style="margin-bottom: 10px;">

    <div class="anitracker-filter-input" data-filter-type="genre">
      <button class="anitracker-filter-rules" title="Change filter logic" data-filter-type="genre"><i class="fa fa-sliders"></i></button>
      <div>
        <div data-filter-type="genre" class="anitracker-applied-filters"></div><span data-filter-type="genre" role="textbox" contenteditable="" spellcheck="false" class="anitracker-text-input"></span>
      </div>
    </div>

    <div class="anitracker-filter-input" data-filter-type="theme">
      <button class="anitracker-filter-rules" title="Change filter logic" data-filter-type="theme"><i class="fa fa-sliders"></i></button>
      <div>
        <div data-filter-type="theme" class="anitracker-applied-filters"></div><span data-filter-type="theme" role="textbox" contenteditable="" spellcheck="false" class="anitracker-text-input"></span>
      </div>
    </div>

    <div class="anitracker-filter-input" data-filter-type="type">
      <button class="anitracker-filter-rules" title="Change filter logic" data-filter-type="type"><i class="fa fa-sliders"></i></button>
      <div>
        <div data-filter-type="type" class="anitracker-applied-filters"></div><span data-filter-type="type" role="textbox" contenteditable="" spellcheck="false" class="anitracker-text-input"></span>
      </div>
    </div>

    <div class="anitracker-filter-input" data-filter-type="demographic">
      <button class="anitracker-filter-rules" title="Change filter logic" data-filter-type="demographic"><i class="fa fa-sliders"></i></button>
      <div>
        <div data-filter-type="demographic" class="anitracker-applied-filters"></div><span data-filter-type="demographic" role="textbox" contenteditable="" spellcheck="false" class="anitracker-text-input"></span>
      </div>
    </div>

    <div style="margin-left: auto;">
      <div class="btn-group">
        <button class="btn dropdown-toggle btn-dark" id="anitracker-status-button" data-bs-toggle="dropdown" data-toggle="dropdown" title="Choose status">All</button>
      </div>

      <div class="btn-group">
        <button class="btn btn-dark" id="anitracker-time-search-button" title="Set season filter">
          <svg fill="#ffffff" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512" xml:space="preserve" aria-hidden="true">
            <path d="M256,0C114.842,0,0,114.842,0,256s114.842,256,256,256s256-114.842,256-256S397.158,0,256,0z M374.821,283.546H256    c-15.148,0-27.429-12.283-27.429-27.429V137.295c0-15.148,12.281-27.429,27.429-27.429s27.429,12.281,27.429,27.429v91.394h91.392    c15.148,0,27.429,12.279,27.429,27.429C402.249,271.263,389.968,283.546,374.821,283.546z"/>
          </svg>
        </button>
      </div>
    </div>

  <div id="anitracker-filter-dropdown-container"></div>
  </div>
  <div id="anitracker-row-2">
    <span style="font-size: 1.2em;color:#ddd;" id="anitracker-filter-result-count">Filter results: <span>${animeList.length}</span></span>
    <div style="float: right; margin-right: 6px; margin-bottom: 2rem;">
      <div class="btn-group">
        <button class="btn btn-dark" id="anitracker-apply-filters" title="Apply selected filters"><i class="fa fa-check" aria-hidden="true"></i>&nbsp;&nbsp;Apply</button>
      </div>
      <div class="btn-group">
        <input id="anitracker-anime-list-search" disabled="" autocomplete="off" class="form-control anitracker-text-input-bar" style="width: 150px;" placeholder="Loading...">
      </div>
      <div class="btn-group">
        <button class="btn btn-dark" id="anitracker-random-anime" title="Open a random anime from within the selected filters">
          <i class="fa fa-random" aria-hidden="true"></i>
          &nbsp;Random Anime
        </button>
      </div>
    </div>
  </div>`).insertBefore('.index');

  function getDropdownButtons(filters, type) {
    return filters.sort((a,b) => a.name > b.name ? 1 : -1).concat({value: 'none', name: '(None)'}).map(g => $(`<button data-filter-type="${type}" data-filter-value="${g.value}">${g.name}</button>`));
  }

  $(`<div id="anitracker-genre-dropdown" tabindex="-1" data-filter-type="genre" class="dropdown-menu anitracker-dropdown-content anitracker-filter-dropdown">`).appendTo('#anitracker-filter-dropdown-container');
  getDropdownButtons(filterValues.genre, 'genre').forEach(g => { g.appendTo('#anitracker-genre-dropdown') });

  $(`<div id="anitracker-theme-dropdown" tabindex="-1" data-filter-type="theme" class="dropdown-menu anitracker-dropdown-content anitracker-filter-dropdown">`).appendTo('#anitracker-filter-dropdown-container');
  getDropdownButtons(filterValues.theme, 'theme').forEach(g => { g.appendTo('#anitracker-theme-dropdown') });

  $(`<div id="anitracker-type-dropdown" tabindex="-1" data-filter-type="type" class="dropdown-menu anitracker-dropdown-content anitracker-filter-dropdown">`).appendTo('#anitracker-filter-dropdown-container');
  getDropdownButtons(filterValues.type, 'type').forEach(g => { g.appendTo('#anitracker-type-dropdown') });

  $(`<div id="anitracker-demographic-dropdown" tabindex="-1" data-filter-type="demographic" class="dropdown-menu anitracker-dropdown-content anitracker-filter-dropdown">`).appendTo('#anitracker-filter-dropdown-container');
  getDropdownButtons(filterValues.demographic, 'demographic').forEach(g => { g.appendTo('#anitracker-demographic-dropdown') });

  $(`<div id="anitracker-status-dropdown" tabindex="-1" data-filter-type="status" class="dropdown-menu anitracker-dropdown-content anitracker-filter-dropdown special">`).insertAfter('#anitracker-status-button');
  ['all','airing','completed'].forEach(g => { $(`<button data-filter-type="status" data-filter-value="${g}">${g[0].toUpperCase() + g.slice(1)}</button>`).appendTo('#anitracker-status-dropdown') });
  $(`<button data-filter-type="status" data-filter-value="none">(No status)</button>`).appendTo('#anitracker-status-dropdown');

  const timeframeSettings = {
    enabled: false
  };

  const placeholderTexts = {
    'genre': 'Genre',
    'theme': 'Theme',
    'type': 'Type',
    'demographic': 'Demographic'
  }

  const selectedFilters = [];
  const appliedFilters = [];

  function getElemsFromFilterType(filterType) {
    const elems = {};
    if (filterType === undefined) return elems;
    for (const inp of $('.anitracker-filter-input')) {
      if ($(inp).data('filter-type') !== filterType) continue;
      elems.parent = $(inp);
      elems.filterIcons = Array.from($(inp).find('.anitracker-filter-icon'));
      elems.filterIconContainer = $(inp).find('.anitracker-applied-filters');
      elems.input = $(inp).find('.anitracker-text-input');
      elems.inputPlaceholder = $(inp).find('.anitracker-placeholder');
      elems.scrollingDiv = $(inp).find('>div');
      elems.filterRuleButton = $(inp).find('.anitracker-filter-rules');
      break;
    }
    for (const drop of $('.anitracker-filter-dropdown')) {
      if ($(drop).data('filter-type') !== filterType) continue;
      elems.dropdown = $(drop);
    }
    return elems;
  }

  function getFilterDataFromElem(jquery) {
    return {
      type: jquery.data('filter-type'),
      value: jquery.data('filter-value'),
      exclude: jquery.data('filter-exclude') === true
    }
  }

  function getInputText(elem) {
    return elem.contents().filter(function() {
      return this.nodeType === Node.TEXT_NODE;
    }).text().trim();
  }

  function clearPlaceholder(elem) {
    elem.find('.anitracker-placeholder').remove();
  }

  function addPlaceholder(elem, filterType) {
    if (getInputText(elem) !== '' || elem.find('.anitracker-placeholder').length > 0) return;
    $(`<span data-filter-type="${filterType}" class="anitracker-placeholder">${placeholderTexts[filterType]}</span>`).prependTo(elem);
  }

  function setChangesToApply(on) {
    const elem = $('#anitracker-apply-filters');
    if (on) elem.addClass('btn-primary').removeClass('btn-dark');
    else elem.removeClass('btn-primary').addClass('btn-dark');
  }

  function updateApplyButton() {
    setChangesToApply(JSON.stringify(selectedFilters) !== JSON.stringify(appliedFilters));
  }

  function showDropdown(elem, parentElem) {
    for (const type of Object.keys(filterRules)) {
      const elems = getElemsFromFilterType(type);
      if (elems.dropdown === undefined || elems.dropdown.length === 0 || elems.dropdown.hasClass('special')) continue;
      elems.dropdown.hide();
    }

    const top = $(parentElem).position().top + $(parentElem).outerHeight(true);
    const left = $(parentElem).position().left;
    elem.css('top',top).css('left',left);
    elem.show();
    elem.scrollTop(0);
  }

  function checkCloseDropdown(elems) {
    setTimeout(() => {
      if (elems.dropdown.is(':focus,:focus-within') || elems.input.is(':focus')) return;
      elems.dropdown.hide();
    }, 1);
  }

  function fixSelection(elem) {
    const sel = window.getSelection();
    if (!$(sel.anchorNode).is('div')) return;

    setSelection(elem);
  }

  function setSelection(elem) {
    const sel = window.getSelection();
    elem.focus();

    const index = elem.text().length - 1 - elem.find('.anitracker-placeholder').text().length - 1;
    const range = document.createRange();
    range.setStart(elem[0], index > 0 ? index : 0);
    range.collapse(true);

    sel.removeAllRanges();
    sel.addRange(range);
  }

  function scrollToBottom(elem) {
    elem.scrollTop(9999);
  }

  ['genre','theme','type','demographic'].forEach((type) => {
    const elems = getElemsFromFilterType(type);
    addPlaceholder(elems.input, type);
    elems.input.css('width','100%').css('height','100%');
  });

  function getActiveFilter(filter) {
    return selectedFilters.find(f => f.type === filter.type && f.value === filter.value && f.exclude === filter.exclude);
  }

  function refreshIconSymbol(elem) {
    const excluded = elem.data('filter-exclude');
    elem.find('i').remove();
    if (excluded === undefined) return;
    $(`<i class="fa fa-${excluded ? 'minus' : 'plus'}"></i>`).prependTo(elem);
  }

  function setStatusFilter(filter) {
    for (const filter of selectedFilters.filter(f => f.type === 'status')) {
      selectedFilters.splice(selectedFilters.indexOf(filter), 1);
    }

    for (const btn of $('#anitracker-status-dropdown>button')) {
      const elem = $(btn);
      const filterValue = elem.data('filter-value')
      if (filterValue !== filter.value) {
        elem.removeClass('anitracker-active');
        continue;
      }
      $('#anitracker-status-button').text(elem.text());
      if (filterValue !== 'all') elem.addClass('anitracker-active');
    }

    if (filter.value !== 'all') selectedFilters.push(filter);

    if (filter.value === 'all') $('#anitracker-status-button').removeClass('anitracker-active');
    else $('#anitracker-status-button').addClass('anitracker-active');
  }

  function addFilter(filter) {
    if (filter.type === 'season') {
      addSeasonFilter(filter);
      return;
    }
    if (filter.type === 'status') {
      setStatusFilter(filter);
      return;
    }

    const elems = getElemsFromFilterType(filter.type);
    elems.parent?.addClass('active');
    elems.input?.css('width','').css('height','');
    if (elems.input !== undefined) clearPlaceholder(elems.input);
    if (getActiveFilter(filter) !== undefined || filterValues[filter.type] === undefined) return;
    const filterEntry = filterValues[filter.type].find(f => f.value === filter.value);
    const name = (() => {
      if (filter.value === 'none') return '(None)';
      else return filterEntry !== undefined ? filterEntry.name : filter.value;
    })();
    const icon = $(`<span class="anitracker-filter-icon ${filter.exclude ? 'excluded' : 'included'}" data-filter-type="${filter.type}" data-filter-value="${filter.value}" data-filter-exclude="${filter.exclude}">${name}</span>`).appendTo(elems.filterIconContainer);
    refreshIconSymbol(icon);
    icon.on('click', (e) => {
      cycleFilter(getFilterDataFromElem($(e.currentTarget)));
    });

    for (const btn of elems.dropdown.find('button')) {
      const elem = $(btn);
      if (elem.data('filter-value') !== filter.value) continue;
      if (filter.exclude !== undefined) elem.data('filter-exclude', filter.exclude);

      if (filter.exclude) elem.addClass('excluded').removeClass('included');
      else elem.addClass('included').removeClass('excluded');
    }

    if (filter.exclude === undefined) filter.exclude = false;

    selectedFilters.push(filter);
  }

  function removeFilter(filter) {
    const elems = getElemsFromFilterType(filter.type);
    const activeFilter = getActiveFilter(filter);
    if (activeFilter === undefined) return;

    for (const icon of elems.filterIcons) {
      const elem = $(icon);
      if (elem.data('filter-value') !== filter.value) continue;
      elem.remove();
    }

    for (const btn of elems.dropdown.find('button')) {
      const elem = $(btn);
      if (elem.data('filter-value') !== filter.value) continue;
      elem.data('filter-exclude', '');

      elem.removeClass('excluded').removeClass('included');
    }

    selectedFilters.splice(selectedFilters.indexOf(activeFilter), 1);

    // Count remaining filters of the same type
    const remainingFilters = selectedFilters.filter(f => f.type === filter.type);
    if (remainingFilters.length === 0) {
      elems.parent?.removeClass('active');
      elems.input?.css('width','100%').css('height','100%');
      if (elems.input !== undefined) addPlaceholder(elems.input, filter.type);
    }
  }

  // Sets the filter to negative, doesn't actually invert it
  function invertFilter(filter) {
    const elems = getElemsFromFilterType(filter.type);
    const activeFilter = getActiveFilter(filter);
    if (activeFilter === undefined) return;

    for (const icon of elems.filterIcons) {
      const elem = $(icon);
      if (elem.data('filter-value') !== filter.value) continue;
      elem.removeClass('included').addClass('excluded');
      elem.data('filter-exclude', true);
      refreshIconSymbol(elem);
    }

    for (const btn of elems.dropdown.find('button')) {
      const elem = $(btn);
      if (elem.data('filter-value') !== filter.value) continue;

      elem.removeClass('included').addClass('excluded');
      elem.data('filter-exclude', true);
    }

    activeFilter.exclude = true;
  }

  function cycleFilter(filter) {
    if (getActiveFilter(filter) === undefined) addFilter(filter);
    else if (filter.exclude === false) invertFilter(filter);
    else if (filter.exclude === true) removeFilter(filter);
    updateApplyButton();
  }

  function removeSeasonFilters() {
    for (const filter of selectedFilters.filter(f => f.type === 'season')) {
      selectedFilters.splice(selectedFilters.indexOf(filter), 1);
    }
  }

  function addSeasonFilter(filter) {
    $('#anitracker-time-search-button').addClass('anitracker-active');
    timeframeSettings.enabled = true;
    timeframeSettings.inverted = filter.exclude === true;
    timeframeSettings.from = filter.value.from;
    timeframeSettings.to = filter.value.to;
    selectedFilters.push(filter);
  }

  const searchParams = new URLSearchParams(window.location.search);

  function setSearchParam(name, value) {
    if (value === undefined) searchParams.delete(name);
    else searchParams.set(name,value);
  }

  function getSearchParamsString(params) {
    if (Array.from(params.entries()).length === 0) return '';
    return '?' + decodeURIComponent(params.toString());
  }

  function updateSearchParams() {
    window.history.replaceState({}, document.title, "/anime" + getSearchParamsString(searchParams));
  }

  function layoutTabless(entries) { // Tabless = without tabs
    $('.index>').hide();
    $('#anitracker-search-results').remove();

    $(`<div class="row" id="anitracker-search-results"></div>`).prependTo('.index');

    let elements = entries.map(match => {
      return `
      <div class="col-12 col-md-6">
        ${match.html}
      </div>`;
    });

    if (entries.length === 0) elements = `<div class="col-12 col-md-6">No results found.</div>`;

    Array.from($(elements)).forEach(a => {$(a).appendTo('#anitracker-search-results');});
  }

  function layoutAnime(entries) {
    $('#anitracker-filter-result-count>span').text(entries.length);

    const tabs = $('.tab-content>div');
    tabs.find('.col-12').remove();
    $('.nav-link').show();
    $('.index>').show();
    $('#anitracker-search-results').remove();

    const sortedEntries = entries.sort((a,b) => a.name > b.name ? 1 : -1);
    if (entries.length < 100) {
      layoutTabless(sortedEntries);
      $('#anitracker-anime-list-search').trigger('anitracker:search');
      return;
    }

    for (const tab of tabs) {
      const id = $(tab).attr('id');
      const symbol = id.toLowerCase();
      const matchingAnime = (() => {
        if (symbol === 'hash') {
          return sortedEntries.filter(a => /^(?![A-Za-z])./.test(a.name.toLowerCase()));
        }
        else return sortedEntries.filter(a => a.name.toLowerCase().startsWith(symbol));
      })();
      if (matchingAnime.length === 0) {
        $(`.index .nav-link[href="#${id}"]`).hide();
        continue;
      }

      const row = $(tab).find('.row');
      for (const anime of matchingAnime) {
        $(`<div class="col-12 col-md-6">
            ${anime.html}
          </div>`).appendTo(row);
      }
    }

    if (!$('.index .nav-link.active').is(':visible')) {
      $('.index .nav-link:visible:not([href="#hash"])')[0].click();
    }
    $('#anitracker-anime-list-search').trigger('anitracker:search');
  }

  function updateAnimeEntries(entries) {
    animeList.length = 0;
    animeList.push(...entries);
  }

  function setSpinner(coverScreen) {
    const elem = $(`
    <div class="anitracker-filter-spinner ${coverScreen ? 'screen' : 'small'}">
      <div class="spinner-border" role="status">
        <span class="sr-only">Loading...</span>
      </div>
      <span>0%</span>
    </div>`);
    if (coverScreen) elem.prependTo(document.body);
    else elem.appendTo('.page-index h1');
  }

  function getSearchParams(filters, rules, inputParams = undefined) {
    const params = inputParams || new URLSearchParams();
    const values = [];
    for (const type of ['genre','theme','type','demographic','status','season']) {
      const foundFilters = filters.filter(f => f.type === type);
      if (foundFilters.length === 0) {
        params.delete(type);
        continue;
      }

      values.push({filters: foundFilters, type: type});
    }
    for (const entry of values) {
      if (entry.type === 'season') {
        const value = entry.filters[0].value;
        params.set('season', (entry.filters[0].exclude ? '!' : '') + `${getSeasonName(value.from.season)}-${value.from.year}..${getSeasonName(value.to.season)}-${value.to.year}`);
        continue;
      }

      params.set(entry.type, entry.filters.map(g => (g.exclude ? '!' : '') + g.value).join(','));
    }

    const existingRules = getRulesListFromParams(params);
    for (const rule of existingRules) {
      params.delete(`rule-${rule.filterType}-${rule.ruleType}`);
    }
    const changedRules = getChangedRulesList(rules);
    if (changedRules.length === 0) return params;
    for (const rule of changedRules) {
      params.set(`rule-${rule.filterType}-${rule.ruleType}`, rule.value);
    }

    return params;
  }

  function searchWithFilters(filters, screenSpinner) {
    if ($('.anitracker-filter-spinner').length > 0) return; // If already searching
    setSpinner(screenSpinner);

    appliedFilters.length = 0;
    appliedFilters.push(...JSON.parse(JSON.stringify(filters)));

    setChangesToApply(false);

    getFilteredList(filters).then(results => {
      updateAnimeEntries(results);
      layoutAnime(results);
      $('.anitracker-filter-spinner').remove();
      getSearchParams(filters, filterRules, searchParams); // Since a reference is passed, this will set the params
      updateSearchParams();
    });
  }

  const searchParamRuleRegex = /^rule\-(\w+)\-(include|exclude|combined)/;

  function getRulesListFromParams(params) {
    const rulesList = [];
    for (const [key, value] of params.entries()) {
      if (!searchParamRuleRegex.test(key) || !['any','or'].includes(value)) continue;
      const parts = searchParamRuleRegex.exec(key);
      if (filterRules[parts[1]] === undefined) continue;
      rulesList.push({
        filterType: parts[1],
        ruleType: parts[2],
        value: value
      });
    }
    return rulesList;
  }

  function applyRulesList(rulesList) {
    for (const rule of rulesList) {
      filterRules[rule.filterType][rule.ruleType] = rule.value;
    }
  }

  function getChangedRulesList(rules, type = undefined) {
    const changed = [];
    for (const [key, value] of Object.entries(rules)) {
      if (type !== undefined && key !== type) continue;

      if (value.include !== filterDefaultRules[key].include) {
        changed.push({filterType: key, ruleType: 'include', value: value.include});
      }
      if (value.exclude !== filterDefaultRules[key].exclude) {
        changed.push({filterType: key, ruleType: 'exclude', value: value.exclude});
      }
      if (![undefined,'and'].includes(value.combined)) {
        changed.push({filterType: key, ruleType: 'combined', value: value.combined});
      }
    }
    return changed;
  }

  function updateRuleButtons() {
    const changedRules = getChangedRulesList(filterRules);
    for (const type of Object.keys(filterRules)) {
      const elems = getElemsFromFilterType(type);
      const btn = elems.filterRuleButton;
      if (btn === undefined || btn.length === 0) continue;
      if (changedRules.find(r => r.filterType === type) === undefined) btn.removeClass('anitracker-active');
      else btn.addClass('anitracker-active');
    }
  }

  // Events

  $('.anitracker-text-input').on('focus', (e) => {
    const elem = $(e.currentTarget);
    const filterType = elem.data('filter-type');
    const elems = getElemsFromFilterType(filterType);
    showDropdown(elems.dropdown, elems.parent);
    clearPlaceholder(elems.input);
    elem.css('width','').css('height','');
    scrollToBottom(elems.scrollingDiv);
  })
    .on('blur', (e) => {
    const elem = $(e.currentTarget);
    const filterType = elem.data('filter-type');
    const elems = getElemsFromFilterType(filterType);
    checkCloseDropdown(elems);
    if (elems.filterIcons.length === 0) {
      addPlaceholder(elems.input, filterType);
      elem.css('width','100%').css('height','100%');
    }
  })
    .on('keydown', (e) => {
    const elem = $(e.currentTarget);
    const filterType = elem.data('filter-type');
    const elems = getElemsFromFilterType(filterType);

    if (e.key === 'Escape') {
      elem.blur();
      return;
    }
    if (e.key === 'ArrowDown') {
      e.preventDefault();
      elems.dropdown.find('button:visible')[0]?.focus();
      return;
    }
    const filterIcons = elems.filterIcons;
    if (e.key === 'Backspace' && getInputText(elem) === '' && filterIcons.length > 0) {
      removeFilter(getFilterDataFromElem($(filterIcons[filterIcons.length - 1])));
      updateApplyButton();
    }

    setTimeout(() => {
      const text = getInputText(elem).toLowerCase();

      for (const btn of elems.dropdown.find('button')) {
        const jqbtn = $(btn);
        if (jqbtn.text().toLowerCase().includes(text)) {
          jqbtn.show();
          continue;
        }
        jqbtn.hide();
      }
    }, 1);
  }).on('click', (e) => {
    fixSelection($(e.currentTarget));
  });

  $('.anitracker-filter-dropdown:not(.special)>button').on('blur', (e) => {
    const elem = $(e.currentTarget);
    const filterType = elem.data('filter-type');
    checkCloseDropdown(getElemsFromFilterType(filterType));
  }).on('click', (e) => {
    const elem = $(e.currentTarget);
    const filter = getFilterDataFromElem(elem);
    cycleFilter(filter);

    const elems = getElemsFromFilterType(elem.data('filter-type'));
    elems.input?.text('').keydown().blur();
    scrollToBottom(elems.scrollingDiv);
  });

  $('.anitracker-filter-dropdown>button').on('keydown', (e) => {
    const elem = $(e.currentTarget);
    const filterType = elem.data('filter-type');
    const elems = getElemsFromFilterType(filterType);

    if (e.key === 'Escape') {
      elem.blur();
      return;
    }

    const direction = {
      ArrowUp: -1,
      ArrowDown: 1
    }[e.key];
    if (direction === undefined) return;

    const activeButtons = elems.dropdown.find('button:visible');
    let activeIndex = 0;
    for (let i = 0; i < activeButtons.length; i++) {
      const btn = activeButtons[i];
      if (!$(btn).is(':focus')) continue;
      activeIndex = i;
      break;
    }
    const nextIndex = activeIndex + direction;
    if (activeButtons[nextIndex] !== undefined) {
      activeButtons[nextIndex].focus();
      return;
    }
    if (direction === -1 && activeIndex === 0) {
      elems.input?.focus();
      return;
    }
  });

  $('.anitracker-filter-input').on('click', (e) => {
    const elem = $(e.target);
    if (!elem.is('.anitracker-filter-input,.anitracker-applied-filters,.anitracker-filter-input>div')) return;

    const filterType = $(e.currentTarget).data('filter-type');
    const elems = getElemsFromFilterType(filterType);
    setSelection(elems.input);
  });

  $('#anitracker-status-button').on('keydown', (e) => {
    if (e.key !== 'ArrowDown') return;
    const elems = getElemsFromFilterType('status');
    elems.dropdown.find('button')[0]?.focus();
  });

  $('#anitracker-status-dropdown>button').on('click', (e) => {
    const elem = $(e.currentTarget);
    const filter = getFilterDataFromElem(elem);
    addFilter(filter);
    updateApplyButton();
  });

  $('#anitracker-apply-filters').on('click', () => {
    searchWithFilters(selectedFilters, false);
  });

  $('.anitracker-filter-rules').on('click', (e) => {
    const elem1 = $(e.currentTarget);
    const filterType = elem1.data('filter-type');

    const disableInclude = ['type','demographic'].includes(filterType) ? 'disabled' : '';

    $('#anitracker-modal-body').empty();

    $(`
    <p>Rules for ${filterType} filters</p>
    <div class="anitracker-filter-rule-selection" ${disableInclude} data-rule-type="include" style="background-color: #485057;">
      <i class="fa fa-plus" aria-hidden="true"></i>
      <span>Include:</span>
      <div class="btn-group"><button ${disableInclude} title="Select this rule type">and</button><button ${disableInclude} title="Select this rule type">or</button></div>
    </div>
    <div class="anitracker-filter-rule-selection" data-rule-type="combined" style="display: flex;justify-content: center;">
      <span>-</span>
      <div class="btn-group"><button title="Select this rule type">and</button><button title="Select this rule type">or</button></div>
      <span>-</span>
    </div>
    <div class="anitracker-filter-rule-selection" data-rule-type="exclude" style="background-color: #485057;">
      <i class="fa fa-minus" aria-hidden="true"></i>
      <span>Exclude:</span>
      <div class="btn-group"><button title="Select this rule type">and</button><button title="Select this rule type">or</button></div>
    </div>
    <div style="display: flex;justify-content: center; margin-top: 10px;"><button class="btn btn-secondary anitracker-flat-button" id="anitracker-reset-filter-rules" title="Reset to defaults">Reset</button></div>
    `).appendTo('#anitracker-modal-body');

    function refreshBtnStates() {
      const rules = filterRules[filterType];
      for (const selec of $('.anitracker-filter-rule-selection')) {
        const ruleType = $(selec).data('rule-type');
        const rule = rules[ruleType];

        const btns = $(selec).find('button').removeClass('anitracker-active');
        if (rule === 'or') $(btns[1]).addClass('anitracker-active');
        else $(btns[0]).addClass('anitracker-active');
      }
    }

    $('.anitracker-filter-rule-selection button').on('click', (e) => {
      const elem = $(e.currentTarget);
      const ruleType = elem.parents().eq(1).data('rule-type');
      const text = elem.text();
      if (!['and','or'].includes(text)) return;

      filterRules[filterType][ruleType] = text;

      elem.parent().find('button').removeClass('anitracker-active');
      elem.addClass('anitracker-active');
      updateRuleButtons();
    });

    $('#anitracker-reset-filter-rules').on('click', () => {
      filterRules[filterType] = JSON.parse(JSON.stringify(filterDefaultRules[filterType]));
      refreshBtnStates();
      updateRuleButtons();
    });

    refreshBtnStates();

    openModal();
  });

  $('#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" title="Enable timeframe settings">Enable</label>
    </div>
    <div class="custom-control custom-switch">
      <input type="checkbox" class="custom-control-input" id="anitracker-settings-invert-switch" disabled>
      <label class="custom-control-label" for="anitracker-settings-invert-switch" title="Invert time range">Invert</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>
        <button class="btn btn-secondary" id="anitracker-season-copy-to-lower" title="Copy the 'from' season to the 'to' season">
          <i class="fa fa-arrow-circle-down" aria-hidden="true"></i>
        </button>
      </div>
    </div>
    <div class="anitracker-season-group" id="anitracker-season-to">
      <span>To:</span>
      <div class="btn-group">
        <input autocomplete="off" class="form-control anitracker-text-input-bar anitracker-year-input" disabled placeholder="Year" type="number">
      </div>
      <div class="btn-group">
        <button class="btn dropdown-toggle btn-secondary anitracker-season-dropdown-button" disabled data-bs-toggle="dropdown" data-toggle="dropdown" data-value="Spring">Spring</button>
      </div>
    </div>
    <br>
    <div>
      <div class="btn-group">
        <button class="btn btn-primary" id="anitracker-modal-confirm-button">Save</button>
      </div>
    </div>`).appendTo('#anitracker-modal-body');

    $('.anitracker-year-input').val(new Date().getFullYear());

    $('#anitracker-settings-enable-switch').on('change', () => {
      const enabled = $('#anitracker-settings-enable-switch').is(':checked');
      $('.anitracker-season-group').find('input,button').prop('disabled', !enabled);
      $('#anitracker-settings-invert-switch').prop('disabled', !enabled);
    }).prop('checked', timeframeSettings.enabled).change();

    $('#anitracker-settings-invert-switch').prop('checked', timeframeSettings.inverted);

    $('#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');
    ['Winter','Spring','Summer','Fall'].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.parents().eq(1).find('.anitracker-season-dropdown-button');
      btn.data('value', pressed.text());
      btn.text(pressed.text());
    });

    const currentSeason = getCurrentSeason();
    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();
    }
    else $('#anitracker-season-from .anitracker-season-dropdown button')[currentSeason].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();
    }
    else $('#anitracker-season-to .anitracker-season-dropdown button')[currentSeason].click();

    $('#anitracker-modal-confirm-button').on('click', () => {
      const enabled = $('#anitracker-settings-enable-switch').is(':checked');
      const inverted = $('#anitracker-settings-invert-switch').is(':checked');
      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 (enabled) {
        for (const input of $('.anitracker-year-input')) {
          if (/^\d{4}$/.test($(input).val())) continue;
          alert('[AnimePahe Improvements]\n\nYear values must both be 4 numbers.');
          return;
        }
        if (to.year < from.year || (to.year === from.year && to.season < from.season)) {
          alert('[AnimePahe Improvements]\n\nSeason times must be from oldest to newest.' + (to.season === 0 ? '\n(Winter is the first quarter of the year)' : ''));
          return;
        }
        if (to.year - from.year > 100) {
          alert('[AnimePahe Improvements]\n\nYear interval cannot be more than 100 years.');
          return;
        }
        removeSeasonFilters(); // Put here so it doesn't remove existing filters if input is invalid
        addFilter({
          type: 'season',
          value: {
            from: from,
            to: to
          },
          exclude: inverted
        });
      }
      else {
        removeSeasonFilters();
        $('#anitracker-time-search-button').removeClass('anitracker-active');
      }
      updateApplyButton();
      timeframeSettings.enabled = enabled;
      timeframeSettings.inverted = inverted;
      closeModal();
    });

    openModal();
  });

  $('#anitracker-random-anime').on('click', function(e) {
    const elem = $(e.currentTarget);

    elem.find('i').removeClass('fa-random').addClass('fa-refresh').css('animation', 'anitracker-spin 1s linear infinite');

    getFilteredList(selectedFilters).then(results => {
      elem.find('i').removeClass('fa-refresh').addClass('fa-random').css('animation', '');

      const storage = getStorage();
      storage.cache = filterSearchCache;
      saveData(storage);

      const params = getSearchParams(selectedFilters, filterRules);
      params.set('anitracker-random', '1');

      getRandomAnime(results, getSearchParamsString(params));
    });
  });

  $.getScript('https://cdn.jsdelivr.net/npm/fuse.js@7.0.0', function() {
    let typingTimer;
    const elem = $('#anitracker-anime-list-search');
    elem.prop('disabled', false).attr('placeholder', 'Search');

    elem.on('anitracker:search', function() {
      if ($(this).val() !== '') animeListSearch();
    })
      .on('keyup', function() {
      clearTimeout(typingTimer);
      typingTimer = setTimeout(animeListSearch, 150);
    })
      .on('keydown', function() {
      clearTimeout(typingTimer);
    });

    function animeListSearch() {
      const value = elem.val();
      if (value === '') {
        layoutAnime(JSON.parse(JSON.stringify(animeList)));
        searchParams.delete('search');
      }
      else {
        const matches = searchList(Fuse, animeList, value);

        layoutTabless(matches);
        searchParams.set('search', encodeURIComponent(value));
      }
      updateSearchParams();
    }

    const loadedParams = new URLSearchParams(window.location.search);
    if (loadedParams.has('search')) {
      elem.val(decodeURIComponent(loadedParams.get('search')));
      animeListSearch();
    }
  }).fail(() => {
    console.error("[AnimePahe Improvements] Fuse.js failed to load");
  });

  // From parameters
  const paramRules = getRulesListFromParams(searchParams);
  applyRulesList(paramRules);
  updateRuleButtons();
  const paramFilters = getFiltersFromParams(searchParams);
  if (paramFilters.length === 0) return;
  for (const filter of paramFilters) {
    addFilter(filter);
  }
  searchWithFilters(selectedFilters, true);
}

// Search/index page
if (/^\/anime\/?$/.test(window.location.pathname)) {
  loadIndexPage();
  return;
}

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 isEpisode(url = window.location.toString()) {
  return url.includes('/play/');
}

function isAnime(url = window.location.pathname) {
  return /^\/anime\/[\d\w\-]+$/.test(url);
}

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, id = undefined) {
  const storage = getStorage();
  const animeName = nameInput || getAnimeName();
  const linkData = getStoredLinkData(storage);

  storage.linkList = (() => {
    if (id !== undefined) {
      const found = storage.linkList.filter(g => g.type === 'episode' && g.animeId === id && g.episodeNum !== exclude);
      if (found.length > 0) return storage.linkList.filter(g => !(g.type === 'episode' && g.animeId === id && g.episodeNum !== exclude));
    }

    return storage.linkList.filter(g => !(g.type === 'episode' && g.animeName === animeName && g.episodeNum !== exclude));
  })();

  storage.videoTimes = (() => {
    if (id !== undefined) {
      const found = storage.videoTimes.filter(g => g.animeId === id && g.episodeNum !== exclude);
      if (found.length > 0) return storage.videoTimes.filter(g => !(g.animeId === id && g.episodeNum !== exclude));
    }

    return storage.videoTimes.filter(g => !(g.episodeNum !== exclude && stringSimilarity(g.animeName, animeName) > 0.81));
  })();

  if (exclude === undefined && id !== undefined) {
    storage.videoSpeed = storage.videoSpeed.filter(g => g.animeId !== id);
  }

  saveData(storage);
}

function deleteEpisodeFromTracker(animeName, episodeNum, animeId = undefined) {
  const storage = getStorage();

  storage.linkList = (() => {
    if (animeId !== undefined) {
      const found = storage.linkList.find(g => g.type === 'episode' && g.animeId === animeId && g.episodeNum === episodeNum);
      if (found !== undefined) return storage.linkList.filter(g => !(g.type === 'episode' && g.animeId === animeId && g.episodeNum === episodeNum));
    }

    return storage.linkList.filter(g => !(g.type === 'episode' && g.animeName === animeName && g.episodeNum === episodeNum));
  })();

  storage.videoTimes = (() => {
    if (animeId !== undefined) {
      const found = storage.videoTimes.find(g => g.animeId === animeId && g.episodeNum === episodeNum);
      if (found !== undefined) return storage.videoTimes.filter(g => !(g.animeId === animeId && g.episodeNum === episodeNum));
    }

    return storage.videoTimes.filter(g => !(g.episodeNum === episodeNum && stringSimilarity(g.animeName, animeName) > 0.81));
  })();

  if (animeId !== undefined) {
    const episodesRemain = storage.videoTimes.find(g => g.animeId === animeId) !== undefined;
    if (!episodesRemain) {
      storage.videoSpeed = storage.videoSpeed.filter(g => g.animeId !== animeId);
    }
  }

  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\.]+)(?:\-[\d\.]+)? Online/.exec($('.theatre-info h1').text())[1] : $($('.title-wrapper h1 span')[0]).text();
}

function getEpisodeNum() {
  if (isEpisode()) return +(/Watch (.*) - ([\d\.]+)(?:\-[\d\.]+)? Online/.exec($('.theatre-info h1').text())[2]);
  else return 0;
}

function sortAnimesChronologically(animeList) {
  // Animes (plural)
  animeList.sort((a, b) => {return getSeasonValue(a.season) > getSeasonValue(b.season) ? 1 : -1});
  animeList.sort((a, b) => {return a.year > b.year ? 1 : -1});

  return animeList;
}

function asyncGetResponseData(qurl) {
  return new Promise((resolve, reject) => {
    let req = new XMLHttpRequest();
    req.open('GET', qurl, true);
    req.onload = () => {
      if (req.status === 200) {
        resolve(JSON.parse(req.response).data);
        return;
      }

      reject(undefined);
    };
    try {
      req.send();
    }
    catch (err) {
      console.error(err);
      resolve(undefined);
    }
  });
}

function getResponseData(qurl) {
  let req = new XMLHttpRequest();
  req.open('GET', qurl, false);
  try {
    req.send();
  }
  catch (err) {
    console.error(err);
    return(undefined);
  }

  if (req.status === 200) {
    return(JSON.parse(req.response).data);
  }

  return(undefined);
}

function getAnimeSessionFromUrl(url = window.location.toString()) {
  return new RegExp('^(.*animepahe\.[a-z]+)?/(play|anime)/([^/?#]+)').exec(url)[3];
}

function getEpisodeSessionFromUrl(url = window.location.toString()) {
  return new RegExp('^(.*animepahe\.[a-z]+)?/(play|anime)/([^/]+)/([^/?#]+)').exec(url)[4];
}

function makeSearchable(string) {
  return encodeURIComponent(string.replace(' -',' '));
}

function getAnimeData(name = getAnimeName(), id = undefined, guess = false) {
  const cached = (() => {
    if (id !== undefined) return cachedAnimeData.find(a => a.id === id);
    else return cachedAnimeData.find(a => a.title === name);
  })();
  if (cached !== undefined) {
    return cached;
  }

  if (name.length === 0) return undefined;
  const response = getResponseData('/api?m=search&q=' + makeSearchable(name));

  if (response === undefined) return response;

  for (const anime of response) {
    if (id === undefined && anime.title === name) {
      cachedAnimeData.push(anime);
      return anime;
    }
    if (id !== undefined && anime.id === id) {
      cachedAnimeData.push(anime);
      return anime;
    }
  }

  if (guess && response.length > 0) {
    cachedAnimeData.push(response[0]);
    return response[0];
  }

  return undefined;
}

async function asyncGetAnimeData(name = getAnimeName(), id) {
  const cached = cachedAnimeData.find(a => a.id === id);
  const response = cached === undefined ? await getResponseData('/api?m=search&q=' + makeSearchable(name)) : undefined;
  return new Promise((resolve, reject) => {
    if (cached !== undefined) {
      resolve(cached);
      return;
    }

    if (response === undefined) resolve(response);

    for (const anime of response) {
      if (anime.id === id) {
        cachedAnimeData.push(anime);
        resolve(anime);
      }
    }
    reject(`Anime "${name}" not found`);
  });
}

// 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');

  addGeneralButtons();
  updateSwitches();

  return;
}

let animeSession = getAnimeSessionFromUrl();
let episodeSession = '';
if (isEpisode()) {
  episodeSession = getEpisodeSessionFromUrl();
}

function getEpisodeSession(aSession, episodeNum) {
  const request = new XMLHttpRequest();
  request.open('GET', '/api?m=release&id=' + aSession, false);
  request.send();

  if (request.status !== 200) return undefined;

  const response = JSON.parse(request.response);

  return (() => {
    for (let i = 1; i <= response.last_page; i++) {
      const episodes = getResponseData(`/api?m=release&sort=episode_asc&page=${i}&id=${aSession}`);
      if (episodes === undefined) return undefined;
      const episode = episodes.find(a => a.episode === episodeNum);
      if (episode === undefined) continue;
      return episode.session;
    }
  })();
}

function refreshSession(from404 = false) {
  /* Return codes:
   * 0: ok!
   * 1: couldn't find stored session at 404 page
   * 2: couldn't get anime data
   * 3: couldn't get episode session
   * 4: idk
  */

  const storage = getStorage();
  const bobj = getStoredLinkData(storage);

  let name = '';
  let episodeNum = 0;

  if (bobj === undefined && from404) return 1;

  if (bobj !== undefined) {
    name = bobj.animeName;
    episodeNum = bobj.episodeNum;
  }
  else {
    name = getAnimeName();
    episodeNum = getEpisodeNum();
  }

  if (isEpisode()) {
    const animeData = getAnimeData(name, bobj?.animeId, true);

    if (animeData === undefined) return 2;

    if (bobj?.animeId === undefined && animeData.title !== name && !refreshGuessWarning(name, animeData.title)) {
      return 2;
    }

    const episodeSession = getEpisodeSession(animeData.session, episodeNum);

    if (episodeSession === undefined) return 3;

    if (bobj !== undefined) {
      storage.linkList = storage.linkList.filter(g => !(g.type === 'episode' && g.animeSession === bobj.animeSession && g.episodeSession === bobj.episodeSession));
    }

    saveData(storage);

    window.location.replace('/play/' + animeData.session + '/' + episodeSession + window.location.search);

    return 0;
  }
  else if (bobj !== undefined && bobj.animeId !== undefined) {
    storage.linkList = storage.linkList.filter(g => !(g.type === 'anime' && g.animeSession === bobj.animeSession));

    saveData(storage);

    window.location.replace('/a/' + bobj.animeId);
    return 0;
  }
  else {
    if (bobj !== undefined) {
      storage.linkList = storage.linkList.filter(g => !(g.type === 'anime' && g.animeSession === bobj.animeSession));
      saveData(storage);
    }

    let animeData = getAnimeData(name, undefined, true);

    if (animeData === undefined) return 2;

    if (animeData.title !== name && !refreshGuessWarning(name, animeData.title)) {
      return 2;
    }

    window.location.replace('/a/' + animeData.id);
    return 0;
  }

  return 4;
}

function refreshGuessWarning(name, title) {
  return confirm(`[AnimePahe Improvements]\n\nAn exact match with the anime name "${name}" couldn't be found. Go to "${title}" instead?`);
}

const obj = getStoredLinkData(initialStorage);

if (isEpisode() && !is404) {
  theatreMode(initialStorage.settings.theatreMode);
  $('#downloadMenu').changeElementType('button');
}

console.log('[AnimePahe Improvements]', obj, animeSession, episodeSession);

function setSessionData() {
  const animeName = getAnimeName();

  const storage = getStorage();
  if (isEpisode()) {
    storage.linkList.push({
      animeId: getAnimeData(animeName)?.id,
      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.linkList.splice(0,1);
  }

  saveData(storage);
}

if (obj === undefined && !is404) {
  if (!isRandomAnime()) setSessionData();
}
else if (obj !== undefined && is404) {
  document.title = "Refreshing session... :: animepahe";
  $('.text-center h1').text('Refreshing session, please wait...');
  const code = refreshSession(true);
  if (code === 1) {
    $('.text-center h1').text('Couldn\'t refresh session: Link not found in tracker');
  }
  else if (code === 2) {
    $('.text-center h1').text('Couldn\'t refresh session: Couldn\'t get anime data');
  }
  else if (code === 3) {
    $('.text-center h1').text('Couldn\'t refresh session: Couldn\'t get episode data');
  }
  else if (code !== 0) {
    $('.text-center h1').text('Couldn\'t refresh session: An unknown error occured');
  }

  if ([2,3].includes(code)) {
    if (obj.episodeNum !== undefined) {
      $(`<h3>
          Try finding the episode using the following info:
          <br>Anime name: ${obj.animeName}
          <br>Episode: ${obj.episodeNum}
        </h3>`).insertAfter('.text-center h1');
    }
    else {
      $(`<h3>
        Try finding the anime using the following info:
        <br>Anime name: ${obj.animeName}
      </h3>`).insertAfter('.text-center h1');
    }
  }
  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 stored 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.settings.bestQuality === true) {
    bestVideoQuality();
  }
  else if (!["","Loading..."].includes($('#fansubMenu').text())) {
      $('#resolutionMenu .active').click();
  } else {
    new MutationObserver(function(mutationList, observer) {
      $('#resolutionMenu .active').click();
      observer.disconnect();
    }).observe($('#fansubMenu')[0], { childList: true });
  }

  const timeArg = paramArray.find(a => a[0] === 'time');
  if (timeArg !== undefined) {
    applyTimeArg(timeArg);
  }
}

function applyTimeArg(timeArg) {
  const time = timeArg[1];

  function check() {
    if ($('.embed-responsive-item').attr('src') !== undefined) done();
    else setTimeout(check, 100);
  }
  setTimeout(check, 100);

  function done() {
    setIframeUrl(stripUrl($('.embed-responsive-item').attr('src')) + '?time=' + time);

    window.history.replaceState({}, document.title, window.location.origin + window.location.pathname);
  }
}


function getTrackerDiv() {
  return $(`
  <div id="anitracker">
    <button class="btn btn-dark" id="anitracker-refresh-session" title="Refresh the session for the current page">
      <i class="fa fa-refresh" aria-hidden="true"></i>
      &nbsp;Refresh Session
    </button>
  </div>`);
}

async function asyncGetAllEpisodes(session, sort = "asc") {
  const episodeList = [];
  const request = new XMLHttpRequest();
  request.open('GET', `/api?m=release&sort=episode_${sort}&id=` + session, true);

  return new Promise((resolve, reject) => {
    request.onload = () => {
      if (request.status !== 200) {
        reject("Received response code " + request.status);
        return;
      }

      const response = JSON.parse(request.response);
      if (response.current_page === response.last_page) {
        episodeList.push(...response.data);
      }
      else for (let i = 1; i <= response.last_page; i++) {
        asyncGetResponseData(`/api?m=release&sort=episode_${sort}&page=${i}&id=${session}`).then((episodes) => {
          if (episodes === undefined || episodes.length === 0) return;
          episodeList.push(...episodes);
        });
      }
      resolve(episodeList);
    };
    request.send();
  });
}

async 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];

  return new Promise(resolve => {
    const episodeList = [];
    asyncGetAllEpisodes(relationSession).then((episodes) => {
      episodeList.push(...episodes);

      if (episodeList.length === 0) {
        resolve(undefined);
        return;
      }

      resolve({
        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');
    getRelationData(animeData.session, relationType).then((relationData) => {
      if (relationData === undefined) {
        hideSpinner(this, 2);
        alert(`[AnimePahe Improvements]\n\nNo ${relationType.toLowerCase()} found for this anime.`);
        $(this).remove();
        return;
      }

      const episodeSession = relationType === 'Prequel' ? relationData.episodes[relationData.episodes.length-1].session : relationData.episodes[0].session;

      windowOpen(`/play/${relationData.session}/${episodeSession}`, '_self');
      hideSpinner(this, 2);
    });

  });

  if ($('.prequel').length === 0) setPrequelPoster();
  if ($('.sequel').length === 0) setSequelPoster();
} else {
  getTrackerDiv().insertAfter('.anime-content');
}

async function setPrequelPoster() {
  const relationData = await 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 anitracker-thumbnail">
    <a href="${relationLink}" title="${toHtmlCodes("Play Last Episode of " + relationData.name)}">
      <img style="filter: none;" src="${relationData.poster}" data-src="${relationData.poster}" alt="">
    </a>
    <i class="fa fa-chevron-left" aria-hidden="true"></i>
  </div>`).appendTo('.player');

  $('#anitracker-prequel-link').attr('href', relationLink);
  $('#anitracker-prequel-link').text(relationData.name);
  $('#anitracker-prequel-link').changeElementType('a');

  // If auto-clear is on, delete this prequel episode from the tracker
  if (getStorage().settings.autoDelete === true) {
    deleteEpisodesFromTracker(undefined, relationData.name);
  }
}

async function setSequelPoster() {
  const relationData = await 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 anitracker-thumbnail">
    <a href="${relationLink}" title="${toHtmlCodes("Play First Episode of " + relationData.name)}">
      <img style="filter: none;" src="${relationData.poster}" data-src="${relationData.poster}" alt="">
    </a>
    <i class="fa fa-chevron-right" aria-hidden="true"></i>
  </div>`).appendTo('.player');

  $('#anitracker-sequel-link').attr('href', relationLink);
  $('#anitracker-sequel-link').text(relationData.name);
  $('#anitracker-sequel-link').changeElementType('a');
}

if (!isEpisode() && $('#anitracker') != undefined) {
  $('#anitracker').attr('style', "max-width: 1100px;margin-left: auto;margin-right: auto;margin-bottom: 20px;");
}

$('#anitracker-refresh-session').on('click', function(e) {
  const elem = $('#anitracker-refresh-session');
  let timeout = temporaryHtmlChange(elem, 2200, 'Waiting...');

  const result = refreshSession();

  if (result === 0) {
    temporaryHtmlChange(elem, 2200, '<i class="fa fa-refresh" aria-hidden="true" style="animation: anitracker-spin 1s linear infinite;"></i>&nbsp;&nbsp;Refreshing...', timeout);
  }
  else if ([2,3].includes(result)) {
    temporaryHtmlChange(elem, 2200, 'Failed: Couldn\'t find session', timeout);
  }
  else {
    temporaryHtmlChange(elem, 2200, 'Failed.', timeout);
  }
});

if (isEpisode()) {
  // Replace the download buttons with better ones
  if ($('#pickDownload a').length > 0) replaceDownloadButtons();
  else {
    new MutationObserver(function(mutationList, observer) {
      replaceDownloadButtons();
      observer.disconnect();
    }).observe($('#pickDownload')[0], { childList: true });
  }


  $(document).on('blur', () => {
    $('.dropdown-menu.show').removeClass('show');
  });

  (() => {
    const storage = getStorage();
    const foundNotifEpisode = storage.notifications.episodes.find(a => a.session === episodeSession);
    if (foundNotifEpisode !== undefined) {
      foundNotifEpisode.watched = true;
      saveData(storage);
    }
  })();
}

function replaceDownloadButtons() {
  for (const aTag of $('#pickDownload a')) {
      $(aTag).changeElementType('span');
    }

    $('#pickDownload span').on('click', function(e) {

    let request = new XMLHttpRequest();
    //request.open('GET', `https://opsalar.000webhostapp.com/animepahe.php?url=${$(this).attr('href')}`, true);
    request.open('GET', $(this).attr('href'), true);
    try {
      request.send();
      $(this).parents(':eq(1)').find('.anitracker-download-spinner').show();
    }
    catch (err) {
      windowOpen($(this).attr('href'));
    }

    const dlBtn = $(this);

    request.onload = function(e) {
      hideSpinner(dlBtn);
      if (request.readyState !== 4 || request.status !== 200 ) {
        windowOpen(dlBtn.attr('href'));
        return;
      }

      const htmlText = request.response;
      const link = /https:\/\/kwik.\w+\/f\/[^"]+/.exec(htmlText);
      if (link) {
        dlBtn.attr('href', link[0]);
        dlBtn.off();
        dlBtn.changeElementType('a');
        windowOpen(link[0]);
      }
      else windowOpen(dlBtn.attr('href'));

    };
  });
}

function stripUrl(url) {
  if (url === undefined) {
    console.error('[AnimePahe Improvements] stripUrl was used with undefined URL');
    return url;
  }
  const loc = new URL(url);
  return loc.origin + loc.pathname;
}

function temporaryHtmlChange(elem, delay, html, timeout = undefined) {
  if (timeout !== undefined) clearTimeout(timeout);
  if ($(elem).attr('og-html') === undefined) {
    $(elem).attr('og-html', $(elem).html());
  }
  elem.html(html);
  return setTimeout(() => {
    $(elem).html($(elem).attr('og-html'));
  }, delay);
}

$(`
<button class="btn btn-dark" id="anitracker-clear-from-tracker" title="Remove this page from the session tracker">
  <i class="fa fa-trash" aria-hidden="true"></i>
  &nbsp;Clear from Tracker
</button>`).appendTo('#anitracker');

$('#anitracker-clear-from-tracker').on('click', function() {
  const animeName = getAnimeName();

  if (isEpisode()) {
    deleteEpisodeFromTracker(animeName, getEpisodeNum(), getAnimeData().id);

    if ($('.embed-responsive-item').length > 0) {
      const storage = getStorage();
      const videoUrl = stripUrl($('.embed-responsive-item').attr('src'));
      for (const videoData of storage.videoTimes) {
        if (!videoData.videoUrls.includes(videoUrl)) continue;
        const index = storage.videoTimes.indexOf(videoData);
        storage.videoTimes.splice(index, 1);
        saveData(storage);
        break;
      }
    }
  }
  else {
    const storage = getStorage();

    storage.linkList = storage.linkList.filter(a => !(a.type === 'anime' && a.animeName === animeName));

    saveData(storage);
  }

  temporaryHtmlChange($('#anitracker-clear-from-tracker'), 1500, 'Cleared!');
});

function setCoverBlur(img) {
  const cover = $('.anime-cover');
  const ratio = cover.width()/img.width;
  if (ratio <= 1) return;
  cover.css('filter', `blur(${(ratio*Math.max((img.height/img.width)**2, 1))*1.6}px)`);
}

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 (isAnime()) {
  if ($('.anime-poster img').attr('src') !== undefined) {
    improvePoster();
  }
  else $('.anime-poster img').on('load', (e) => {
    improvePoster();
    $(e.target).off('load');
  });

  $(`
  <button class="btn btn-dark" id="anitracker-clear-episodes-from-tracker" title="Clear all episodes from this anime from the session tracker">
    <i class="fa fa-trash" aria-hidden="true"></i>
    <i class="fa fa-window-maximize" aria-hidden="true"></i>
    &nbsp;Clear Episodes from Tracker
  </button>`).appendTo('#anitracker');

  $('#anitracker-clear-episodes-from-tracker').on('click', function() {
    const animeData = getAnimeData();
    deleteEpisodesFromTracker(undefined, animeData.title, animeData.id);

    temporaryHtmlChange($('#anitracker-clear-episodes-from-tracker'), 1500, 'Cleared!');
  });

  const storedObj = getStoredLinkData(initialStorage);

  if (storedObj === undefined || storedObj?.coverImg === undefined) updateAnimeCover();
  else
  {
    new MutationObserver(function(mutationList, observer) {
      $('.anime-cover').css('background-image', `url("${storedObj.coverImg}")`);
      $('.anime-cover').addClass('anitracker-replaced-cover');
      const img = new Image();
      img.src = storedObj.coverImg;
      img.onload = () => {
        setCoverBlur(img);
      };
      observer.disconnect();
    }).observe($('.anime-cover')[0], { attributes: true });
  }

  if (isRandomAnime()) {
    const sourceParams = new URLSearchParams(window.location.search);
    window.history.replaceState({}, document.title, "/anime/" + animeSession);

    const storage = getStorage();
    if (storage.cache) {
      for (const [key, value] of Object.entries(storage.cache)) {
        filterSearchCache[key] = value;
      }
      delete storage.cache;
      saveData(storage);
    }

    $(`
    <div style="margin-left: 240px;">
      <div class="btn-group">
        <button class="btn btn-dark" id="anitracker-reroll-button"><i class="fa fa-random" aria-hidden="true"></i>&nbsp;Reroll Anime</button>
      </div>
      <div class="btn-group">
        <button class="btn btn-dark" id="anitracker-save-session"><i class="fa fa-floppy-o" aria-hidden="true"></i>&nbsp;Save Session</button>
      </div>
    </div>`).appendTo('.title-wrapper');

    $('#anitracker-reroll-button').on('click', function() {
      $(this).text('Rerolling...');

      const sourceFilters = new URLSearchParams(sourceParams.toString());
      getFilteredList(getFiltersFromParams(sourceFilters)).then((animeList) => {
        const storage = getStorage();
        storage.cache = filterSearchCache;
        saveData(storage);

        getRandomAnime(animeList, '?' + sourceParams.toString(), '_self');
      });

    });

    $('#anitracker-save-session').on('click', function() {
      setSessionData();
      $('#anitracker-save-session').off();
      $(this).text('Saved!');

      setTimeout(() => {
        $(this).parent().remove();
      }, 1500);
    });
  }

  new MutationObserver(function(mutationList, observer) {
    const pageNum = (() => {
      const elem = $('.pagination');
      if (elem.length == 0) return 1;
      return +/^(\d+)/.exec($('.pagination').find('.page-item.active span').text())[0];
    })();

    const episodeSort = $('.episode-bar .btn-group-toggle .active').text().trim();

    const episodes = getResponseData(`/api?m=release&sort=episode_${episodeSort}&page=${pageNum}&id=${animeSession}`);
    if (episodes === undefined) return undefined;

    const episodeElements = $('.episode-wrap');

    for (let i = 0; i < episodeElements.length; i++) {
      const elem = $(episodeElements[i]);

      const date = new Date(episodes[i].created_at + " UTC");

      $(`
        <a class="anitracker-episode-time" href="${$(elem.find('a.play')).attr('href')}" tabindex="-1" title="${date.toDateString() + " " + date.toLocaleTimeString()}">${date.toLocaleDateString()}</a>
      `).appendTo(elem.find('.episode-title-wrap'));
    }
    observer.disconnect();
    setTimeout(observer.observe($('.episode-list-wrapper')[0], { childList: true, subtree: true }), 1);
  }).observe($('.episode-list-wrapper')[0], { childList: true, subtree: true });

  // Bookmark icon
  const animename = getAnimeName();
  const animeid = getAnimeData(animename).id;
  $('h1 .fa').remove();

  const notifIcon = (() => {
    if (initialStorage.notifications.anime.find(a => a.name === animename) !== undefined) return true;
    for (const info of $('.anime-info p>strong')) {
      if (!$(info).text().startsWith('Status:')) continue;
      return $(info).text().includes("Not yet aired") || $(info).find('a').text() === "Currently Airing";
    }
    return false;
  })() ?
  `<i title="Add to episode feed" class="fa fa-bell anitracker-title-icon anitracker-notifications-toggle">
    <i style="display: none;" class="fa fa-check anitracker-title-icon-check" aria-hidden="true"></i>
  </i>` : '';

  $(`
  <i title="Bookmark this anime" class="fa fa-bookmark anitracker-title-icon anitracker-bookmark-toggle">
    <i style="display: none;" class="fa fa-check anitracker-title-icon-check" aria-hidden="true"></i>
  </i>${notifIcon}<a href="/a/${animeid}" title="Get Link" class="fa fa-link btn anitracker-title-icon" data-toggle="modal" data-target="#modalBookmark"></a>
  `).appendTo('.title-wrapper>h1');

  if (initialStorage.bookmarks.find(g => g.id === animeid) !== undefined) {
    $('.anitracker-bookmark-toggle .anitracker-title-icon-check').show();
  }

  if (initialStorage.notifications.anime.find(g => g.id === animeid) !== undefined) {
    $('.anitracker-notifications-toggle .anitracker-title-icon-check').show();
  }

  $('.anitracker-bookmark-toggle').on('click', (e) => {
    const check = $(e.currentTarget).find('.anitracker-title-icon-check');

    if (toggleBookmark(animeid, animename)) {
      check.show();
      return;
    }
    check.hide();

  });

  $('.anitracker-notifications-toggle').on('click', (e) => {
    const check = $(e.currentTarget).find('.anitracker-title-icon-check');

    if (toggleNotifications(animename, animeid)) {
      check.show();
      return;
    }
    check.hide();

  });
}

function getRandomAnime(list, args, openType = '_blank') {
  if (list.length === 0) {
    alert("[AnimePahe Improvements]\n\nThere is no anime that matches the selected filters.");
    return;
  }
  const random = randint(0, list.length-1);
  windowOpen(list[random].link + args, openType);
}

function isRandomAnime() {
  return new URLSearchParams(window.location.search).has('anitracker-random');
}

function getBadCovers() {
  const storage = getStorage();
  return ['https://s.pximg.net/www/images/pixiv_logo.png',
          'https://st.deviantart.net/minish/main/logo/card_black_large.png',
          'https://www.wcostream.com/wp-content/themes/animewp78712/images/logo.gif',
          'https://s.pinimg.com/images/default_open_graph',
          'https://share.redd.it/preview/post/',
          'https://i.redd.it/o0h58lzmax6a1.png',
          'https://ir.ebaystatic.com/cr/v/c1/ebay-logo',
          'https://i.ebayimg.com/images/g/7WgAAOSwQ7haxTU1/s-l1600.jpg',
          'https://www.rottentomatoes.com/assets/pizza-pie/head-assets/images/RT_TwitterCard',
          'https://m.media-amazon.com/images/G/01/social_share/amazon_logo',
          'https://zoro.to/images/capture.png',
          'https://cdn.myanimelist.net/img/sp/icon/twitter-card.png',
          'https://s2.bunnycdn.ru/assets/sites/animesuge/images/preview.jpg',
          'https://s2.bunnycdn.ru/assets/sites/anix/preview.jpg',
          'https://cdn.myanimelist.net/images/company_no_picture.png',
          'https://myanimeshelf.com/eva2/handlers/generateSocialImage.php',
          'https://cdn.myanimelist.net/img/sp/icon/apple-touch-icon',
          'https://m.media-amazon.com/images/G/01/imdb/images/social',
          'https://forums.animeuknews.net/styles/default/',
          'https://honeysanime.com/wp-content/uploads/2016/12/facebook_cover_2016_851x315.jpg',
          'https://fi.somethingawful.com/images/logo.png',
          'https://static.hidive.com/misc/HIDIVE-Logo-White.png',
          ...storage.badCovers];
}

async function updateAnimeCover() {
  $(`<div id="anitracker-cover-spinner">
    <div class="spinner-border text-danger" role="status">
      <span class="sr-only">Loading...</span>
    </div>
  </div>`).prependTo('.anime-cover');

  const request = new XMLHttpRequest();
  let beforeYear = 2022;
  for (const info of $('.anime-info p')) {
    if (!$(info).find('strong').html().startsWith('Season:')) continue;
    const year = +/(\d+)$/.exec($(info).find('a').text())[0];
    if (year >= beforeYear) beforeYear = year + 1;
  }
  request.open('GET', 'https://customsearch.googleapis.com/customsearch/v1?key=AIzaSyCzrHsVOqJ4vbjNLpGl8XZcxB49TGDGEFk&cx=913e33346cc3d42bf&tbs=isz:l&q=' + encodeURIComponent(getAnimeName()) + '%20anime%20hd%20wallpaper%20-phone%20-ai%20before:' + beforeYear, true);
  request.onload = function() {
    if (request.status !== 200) {
      $('#anitracker-cover-spinner').remove();
      return;
    }
    if ($('.anime-cover').css('background-image').length > 10) {
      decideAnimeCover(request.response);
    }
    else {
      new MutationObserver(function(mutationList, observer) {
        if ($('.anime-cover').css('background-image').length <= 10) return;
        decideAnimeCover(request.response);
        observer.disconnect();
      }).observe($('.anime-cover')[0], { attributes: true });
    }
  };
  request.send();
}

function trimHttp(string) {
  return string.replace(/^https?:\/\//,'');
}

async function setAnimeCover(src) {
  return new Promise(resolve => {
    $('.anime-cover').css('background-image', `url("${storedObj.coverImg}")`);
    $('.anime-cover').addClass('anitracker-replaced-cover');
    const img = new Image();
    img.src = src;
    img.onload = () => {
      setCoverBlur(img);
    }

    $('.anime-cover').addClass('anitracker-replaced-cover');
    $('.anime-cover').css('background-image', `url("${src}")`);
    $('.anime-cover').attr('image', src);

    $('#anitracker-replace-cover').remove();
    $(`<button class="btn btn-dark" id="anitracker-replace-cover" title="Use another cover instead">
      <i class="fa fa-refresh" aria-hidden="true"></i>
    </button>`).appendTo('.anime-cover');

    $('#anitracker-replace-cover').on('click', e => {
      const storage = getStorage();
      storage.badCovers.push($('.anime-cover').attr('image'));
      saveData(storage);
      updateAnimeCover();
      $(e.target).off();
      playAnimation($(e.target).find('i'), 'spin', 'infinite', 1);
    });

    setCoverBlur(image);
  });
}

async function decideAnimeCover(response) {
  const badCovers = getBadCovers();
  const candidates = [];
  let results = [];
  try {
    results = JSON.parse(response).items;
  }
  catch (e) {
    return;
  }
  if (results === undefined) {
    $('#anitracker-cover-spinner').remove();
    return;
  }
  for (const result of results) {
    let imgUrl = result['pagemap']?.['metatags']?.[0]?.['og:image'] ||
          result['pagemap']?.['cse_image']?.[0]?.['src'] || result['pagemap']?.['webpage']?.[0]?.['image'] ||
          result['pagemap']?.['metatags']?.[0]?.['twitter:image:src'];


    const width = result['pagemap']?.['cse_thumbnail']?.[0]?.['width'];
    const height = result['pagemap']?.['cse_thumbnail']?.[0]?.['height'];

    if (imgUrl === undefined || height < 100 || badCovers.find(a=> trimHttp(imgUrl).startsWith(trimHttp(a))) !== undefined || imgUrl.endsWith('.gif')) continue;

    if (imgUrl.startsWith('https://static.wikia.nocookie.net')) {
      imgUrl = imgUrl.replace(/\/revision\/latest.*\?cb=\d+$/, '');
    }

    candidates.push({
      src: imgUrl,
      width: width,
      height: height,
      aspectRatio: width / height
    });
  }

  if (candidates.length === 0) return;

  candidates.sort((a, b) => {return a.aspectRatio < b.aspectRatio ? 1 : -1});

  if (candidates[0].src.includes('"')) return;

  const originalBg = $('.anime-cover').css('background-image');

  function badImg() {
    $('.anime-cover').css('background-image', originalBg);

    const storage = getStorage();
    for (const anime of storage.linkList) {
      if (anime.type === 'anime' && anime.animeSession === animeSession) {
        anime.coverImg = /^url\("?([^"]+)"?\)$/.exec(originalBg)[1];
        break;
      }
    }
    saveData(storage);

    $('#anitracker-cover-spinner').remove();
  }

  const image = new Image();
  image.onload = () => {
    if (image.width >= 250) {

      $('.anime-cover').addClass('anitracker-replaced-cover');
      $('.anime-cover').css('background-image', `url("${candidates[0].src}")`);
      $('.anime-cover').attr('image', candidates[0].src);
      setCoverBlur(image);
      const storage = getStorage();
      for (const anime of storage.linkList) {
        if (anime.type === 'anime' && anime.animeSession === animeSession) {
          anime.coverImg = candidates[0].src;
          break;
        }
      }
      saveData(storage);

      $('#anitracker-cover-spinner').remove();
    }
    else badImg();
  };

  image.addEventListener('error', function() {
    badImg();
  });

  image.src = candidates[0].src;
}

function hideThumbnails() {
  $('.main').addClass('anitracker-hide-thumbnails');
}

function resetPlayer() {
  setIframeUrl(stripUrl($('.embed-responsive-item').attr('src')));
}

function addGeneralButtons() {
  $(`
  <button class="btn btn-dark" id="anitracker-show-data" title="View and handle stored sessions and video progress">
    <i class="fa fa-floppy-o" aria-hidden="true"></i>
    &nbsp;Manage Data...
  </button>
  <button class="btn btn-dark" id="anitracker-settings" title="Settings">
    <i class="fa fa-sliders" aria-hidden="true"></i>
    &nbsp;Settings...
  </button>`).appendTo('#anitracker');

  $('#anitracker-settings').on('click', () => {
    $('#anitracker-modal-body').empty();
    $('<span style="display:block;">Video player:</span>').appendTo('#anitracker-modal-body');

    addOptionSwitch('autoPlayVideo', 'Auto-Play Video', 'Automatically play the video when it is loaded.');
    addOptionSwitch('theatreMode', 'Theatre Mode', 'Expand the video player for a better experience on bigger screens.');
    addOptionSwitch('bestQuality', 'Default to Best Quality', 'Automatically select the best resolution quality available.');
    addOptionSwitch('seekThumbnails', 'Seek Thumbnails', 'Show thumbnail images while seeking through the progress bar. May cause performance issues on weak systems.');
    addOptionSwitch('seekPoints', 'Seek Points', 'Show points on the progress bar.');
    addOptionSwitch('skipButton', 'Skip Button', 'Show a button to skip sections of episodes.');

    if (isEpisode()) {
      $(`
      <div class="btn-group">
      <button class="btn btn-secondary" id="anitracker-reset-player" title="Reset the video player">
        <i class="fa fa-rotate-right" aria-hidden="true"></i>
        &nbsp;Reset player
      </button></div>`).appendTo('#anitracker-modal-body');

      $('#anitracker-reset-player').on('click', function() {
        closeModal();
        resetPlayer();
      });
    }

    $('<span style="display:block;margin-top:10px;">Site:</span>').appendTo('#anitracker-modal-body');
    addOptionSwitch('hideThumbnails', 'Hide Thumbnails', 'Hide thumbnails and preview images.');
    addOptionSwitch('autoDelete', 'Auto-Clear Links', 'Auto-clearing means only one episode of a series is stored in the tracker at a time.');
    addOptionSwitch('autoDownload', 'Automatic Download', 'Automatically download the episode when visiting a download page.');
    addOptionSwitch('reduceMotion', 'Reduce Motion', 'Don\'t show animations for opening/closing modal menus.');

    openModal();
  });

  function openShowDataModal() {
    $('#anitracker-modal-body').empty();
    $(`
    <div class="anitracker-modal-list-container">
      <div class="anitracker-storage-data" tabindex="0" key="linkList">
        <span>Session Data</span>
      </div>
    </div>
    <div class="anitracker-modal-list-container">
      <div class="anitracker-storage-data" tabindex="0" key="videoTimes">
        <span>Video Progress</span>
      </div>
    </div>
    <div class="anitracker-modal-list-container">
      <div class="anitracker-storage-data" tabindex="0" key="videoSpeed">
        <span>Video Playback Speed</span>
      </div>
    </div>
    <div class="btn-group">
      <button class="btn btn-danger" id="anitracker-reset-data" title="Remove stored data and reset all settings">
        <i class="fa fa-undo" aria-hidden="true"></i>
        &nbsp;Reset Data
      </button>
    </div>
    <div class="btn-group">
      <button class="btn btn-secondary" id="anitracker-raw-data" title="View data in JSON format">
        <i class="fa fa-code" aria-hidden="true"></i>
        &nbsp;Raw
      </button>
    </div>
    <div class="btn-group">
      <button class="btn btn-secondary" id="anitracker-export-data" title="Export and download the JSON data">
        <i class="fa fa-download" aria-hidden="true"></i>
        &nbsp;Export Data
      </button>
    </div>
    <label class="btn btn-secondary" id="anitracker-import-data-label" tabindex="0" for="anitracker-import-data" style="margin-bottom:0;" title="Import a JSON file with AnimePahe Improvements data. This does not delete any existing data.">
      <i class="fa fa-upload" aria-hidden="true"></i>
      &nbsp;Import Data
    </label>
    <div class="btn-group">
      <button class="btn btn-dark" id="anitracker-edit-data" title="Edit a key">
        <i class="fa fa-pencil" aria-hidden="true"></i>
        &nbsp;Edit...
      </button>
    </div>
    <input type="file" id="anitracker-import-data" style="visibility: hidden; width: 0;" accept=".json">
    `).appendTo('#anitracker-modal-body');

    const expandIcon = `<i class="fa fa-plus anitracker-expand-data-icon" aria-hidden="true"></i>`;
    const contractIcon = `<i class="fa fa-minus anitracker-expand-data-icon" aria-hidden="true"></i>`;

    $(expandIcon).appendTo('.anitracker-storage-data');

    $('.anitracker-storage-data').on('click keydown', (e) => {
      if (e.type === 'keydown' && e.key !== "Enter") return;
      toggleExpandData($(e.currentTarget));
    });

    function toggleExpandData(elem) {
      if (elem.hasClass('anitracker-expanded')) {
        contractData(elem);
      }
      else {
        expandData(elem);
      }
    }

    $('#anitracker-reset-data').on('click', function() {
      if (confirm('[AnimePahe Improvements]\n\nThis will remove all saved data and reset it to its default state.\nAre you sure?') === true) {
        saveData(getDefaultData());
        updatePage();
        openShowDataModal();
      }
    });

    $('#anitracker-raw-data').on('click', function() {
      const blob = new Blob([JSON.stringify(getStorage())], {type : 'application/json'});
      windowOpen(URL.createObjectURL(blob));
    });

    $('#anitracker-edit-data').on('click', function() {
      $('#anitracker-modal-body').empty();
      $(`
        <b>Warning: for developer use.<br>Back up your data before messing with this.</b>
        <input autocomplete="off" class="form-control anitracker-text-input-bar anitracker-edit-data-key" placeholder="Key (Path)">
        <input autocomplete="off" class="form-control anitracker-text-input-bar anitracker-edit-data-value" placeholder="Value (JSON)">
        <p>Leave value empty to get the existing value</p>
        <div class="btn-group">
          <button class="btn dropdown-toggle btn-secondary anitracker-edit-mode-dropdown-button" data-bs-toggle="dropdown" data-toggle="dropdown" data-value="replace">Replace</button>
          <div class="dropdown-menu anitracker-dropdown-content anitracker-edit-mode-dropdown"></div>
        </div>
        <div class="btn-group">
          <button class="btn btn-primary anitracker-confirm-edit-button">Confirm</button>
        </div>
      `).appendTo('#anitracker-modal-body');

      [{t:'Replace',i:'replace'},{t:'Append',i:'append'},{t:'Delete from list',i:'delList'}].forEach(g => { $(`<button ref="${g.i}">${g.t}</button>`).appendTo('.anitracker-edit-mode-dropdown') });

      $('.anitracker-edit-mode-dropdown button').on('click', (e) => {
        const pressed = $(e.target)
        const btn = pressed.parents().eq(1).find('.anitracker-edit-mode-dropdown-button');
        btn.data('value', pressed.attr('ref'));
        btn.text(pressed.text());
      });

      $('.anitracker-confirm-edit-button').on('click', () => {
        const storage = getStorage();
        const key = $('.anitracker-edit-data-key').val();
        let keyValue = undefined;
        try {
          keyValue = eval("storage." + key); // lots of evals here because I'm lazy
        }
        catch (e) {
          console.error(e);
          alert("Nope didn't work");
          return;
        }

        if ($('.anitracker-edit-data-value').val() === '') {
          alert(JSON.stringify(keyValue));
          return;
        }

        if (keyValue === undefined) {
          alert("Undefined");
          return;
        }

        const mode = $('.anitracker-edit-mode-dropdown-button').data('value');

        let value = undefined;
        if (mode === 'delList') {
          value = $('.anitracker-edit-data-value').val();
        }
        else if ($('.anitracker-edit-data-value').val() !== "undefined") {
          try {
            value = JSON.parse($('.anitracker-edit-data-value').val());
          }
          catch (e) {
            console.error(e);
            alert("Invalid JSON");
            return;
          }
        }

        const delFromListMessage = "Please enter a comparison in the 'value' field, with 'a' being the variable for the element.\neg. 'a.id === \"apple\"'\nWhichever elements that match this will be deleted.";

        switch (mode) {
          case 'replace':
            eval(`storage.${key} = value`);
            break;
          case 'append':
            if (keyValue.constructor.name !== 'Array') {
              alert("Not a list");
              return;
            }
            eval(`storage.${key}.push(value)`);
            break;
          case 'delList':
            if (keyValue.constructor.name !== 'Array') {
              alert("Not a list");
              return;
            }
            try {
              eval(`storage.${key} = storage.${key}.filter(a => !(${value}))`);
            }
            catch (e) {
              console.error(e);
              alert(delFromListMessage);
              return;
            }
            break;
          default:
            alert("This message isn't supposed to show up. Uh...");
            return;
        }
        if (JSON.stringify(storage) === JSON.stringify(getStorage())) {
          alert("Nothing changed.");
          if (mode === 'delList') {
            alert(delFromListMessage);
          }
          return;
        }
        else alert("Probably worked!");

        saveData(storage);
      });

      openModal(openShowDataModal);
    });

    $('#anitracker-export-data').on('click', function() {
      const storage = getStorage();

      if (storage.cache) {
        delete storage.cache;
        saveData(storage);
      }
      download('animepahe-tracked-data-' + Date.now() + '.json', JSON.stringify(getStorage(), null, 2));
    });

    $('#anitracker-import-data-label').on('keydown', (e) => {
      if (e.key === "Enter") $("#" + $(e.currentTarget).attr('for')).click();
    });

    $('#anitracker-import-data').on('change', function(event) {
      const file = this.files[0];
      const fileReader = new FileReader();
      $(fileReader).on('load', function() {
        let newData = {};
        try {
          newData = JSON.parse(fileReader.result);
        }
        catch (err) {
          alert('[AnimePahe Improvements]\n\nPlease input a valid JSON file.');
          return;
        }

        const storage = getStorage();
        const diffBefore = importData(storage, newData, false);

        let totalChanged = 0;
        for (const [key, value] of Object.entries(diffBefore)) {
          totalChanged += value;
        }

        if (totalChanged === 0) {
          alert('[AnimePahe Improvements]\n\nThis file contains no changes to import.');
          return;
        }

        $('#anitracker-modal-body').empty();

        $(`
        <h4>Choose what to import</h4>
        <br>
        <div class="form-check">
          <input class="form-check-input anitracker-import-data-input" type="checkbox" value="" id="anitracker-link-list-check" ${diffBefore.linkListAdded > 0 ? "checked" : "disabled"}>
          <label class="form-check-label" for="anitracker-link-list-check">
            Session entries (${diffBefore.linkListAdded})
          </label>
        </div>
        <div class="form-check">
          <input class="form-check-input anitracker-import-data-input" type="checkbox" value="" id="anitracker-video-times-check" ${(diffBefore.videoTimesAdded + diffBefore.videoTimesUpdated) > 0 ? "checked" : "disabled"}>
          <label class="form-check-label" for="anitracker-video-times-check">
            Video progress times (${diffBefore.videoTimesAdded + diffBefore.videoTimesUpdated})
          </label>
        </div>
        <div class="form-check">
          <input class="form-check-input anitracker-import-data-input" type="checkbox" value="" id="anitracker-bookmarks-check" ${diffBefore.bookmarksAdded > 0 ? "checked" : "disabled"}>
          <label class="form-check-label" for="anitracker-bookmarks-check">
            Bookmarks (${diffBefore.bookmarksAdded})
          </label>
        </div>
        <div class="form-check">
          <input class="form-check-input anitracker-import-data-input" type="checkbox" value="" id="anitracker-notifications-check" ${(diffBefore.notificationsAdded + diffBefore.episodeFeedUpdated) > 0 ? "checked" : "disabled"}>
          <label class="form-check-label" for="anitracker-notifications-check">
            Episode feed entries (${diffBefore.notificationsAdded})
            <ul style="margin-bottom:0;margin-left:-24px;"><li>Episode feed entries updated: ${diffBefore.episodeFeedUpdated}</li></ul>
          </label>
        </div>
        <div class="form-check">
          <input class="form-check-input anitracker-import-data-input" type="checkbox" value="" id="anitracker-settings-check" ${diffBefore.settingsUpdated > 0 ? "checked" : "disabled"}>
          <label class="form-check-label" for="anitracker-settings-check">
            Settings (${diffBefore.settingsUpdated})
          </label>
        </div>
        <div class="btn-group" style="float: right;">
          <button class="btn btn-primary" id="anitracker-confirm-import" title="Confirm import">
            <i class="fa fa-upload" aria-hidden="true"></i>
            &nbsp;Import
          </button>
        </div>
        `).appendTo('#anitracker-modal-body');

        $('.anitracker-import-data-input').on('change', (e) => {
          let checksOn = 0;
          for (const elem of $('.anitracker-import-data-input')) {
            if ($(elem).prop('checked')) checksOn++;
          }
          if (checksOn === 0) {
            $('#anitracker-confirm-import').attr('disabled', true);
          }
          else {
            $('#anitracker-confirm-import').attr('disabled', false);
          }
        });

        $('#anitracker-confirm-import').on('click', () => {
          const diffAfter = importData(getStorage(), newData, true, {
            linkList: !$('#anitracker-link-list-check').prop('checked'),
            videoTimes: !$('#anitracker-video-times-check').prop('checked'),
            bookmarks: !$('#anitracker-bookmarks-check').prop('checked'),
            notifications: !$('#anitracker-notifications-check').prop('checked'),
            settings: !$('#anitracker-settings-check').prop('checked')
          });

          if ((diffAfter.bookmarksAdded + diffAfter.notificationsAdded + diffAfter.settingsUpdated) > 0) updatePage();
          if ((diffAfter.videoTimesUpdated + diffAfter.videoTimesAdded) > 0 && isEpisode()) {
            sendMessage({action:"change_time", time:getStorage().videoTimes.find(a => a.videoUrls.includes($('.embed-responsive-item')[0].src))?.time});
          }
          alert('[AnimePahe Improvements]\n\nImported!');
          openShowDataModal();
        });

        openModal(openShowDataModal);
      });
      fileReader.readAsText(file);
    });

    function importData(data, importedData, save = true, ignored = {settings:{}}) {
      const changed = {
        linkListAdded: 0, // Session entries added
        videoTimesAdded: 0, // Video progress entries added
        videoTimesUpdated: 0, // Video progress times updated
        bookmarksAdded: 0, // Bookmarks added
        notificationsAdded: 0, // Anime added to episode feed
        episodeFeedUpdated: 0, // Episodes either added to episode feed or that had their watched status updated
        settingsUpdated: 0 // Settings updated
      }

      const defaultData = getDefaultData();

      if (importedData.version !== defaultData.version) {
        upgradeData(importedData, importedData.version);
      }

      for (const [key, value] of Object.entries(importedData)) {
        if (defaultData[key] === undefined) continue;

        if (!ignored.linkList && key === 'linkList') {
          const added = [];
          value.forEach(g => {
            if ((g.type === 'episode' && data.linkList.find(h => h.type === 'episode' && h.animeSession === g.animeSession && h.episodeSession === g.episodeSession) === undefined)
               || (g.type === 'anime' && data.linkList.find(h => h.type === 'anime' && h.animeSession === g.animeSession) === undefined)) {
              added.push(g);
              changed.linkListAdded++;
            }
          });
          data.linkList.splice(0,0,...added);
          continue;
        }
        else if (!ignored.videoTimes && key === 'videoTimes') {
          const added = [];
          value.forEach(g => {
            const foundTime = data.videoTimes.find(h => h.videoUrls.includes(g.videoUrls[0]));
            if (foundTime === undefined) {
              added.push(g);
              changed.videoTimesAdded++;
            }
            else if (foundTime.time < g.time) {
              foundTime.time = g.time;
              changed.videoTimesUpdated++;
            }
          });
          data.videoTimes.splice(0,0,...added);
          continue;
        }
        else if (!ignored.bookmarks && key === 'bookmarks') {
          value.forEach(g => {
            if (data.bookmarks.find(h => h.id === g.id) !== undefined) return;
            data.bookmarks.push(g);
            changed.bookmarksAdded++;
          });
          continue;
        }
        else if (!ignored.notifications && key === 'notifications') {
          value.anime.forEach(g => {
            if (data.notifications.anime.find(h => h.id === g.id) !== undefined) return;
            data.notifications.anime.push(g);
            changed.notificationsAdded++;
          });

          // Checking if there exists any gap between the imported episodes and the existing ones
          if (save) data.notifications.anime.forEach(g => {
            const existingEpisodes = data.notifications.episodes.filter(a => (a.animeName === g.name || a.animeId === g.id));
            const addedEpisodes = value.episodes.filter(a => (a.animeName === g.name || a.animeId === g.id));
            if (existingEpisodes.length > 0 && (existingEpisodes[existingEpisodes.length-1].episode - addedEpisodes[0].episode > 1.5)) {
              g.updateFrom = new Date(addedEpisodes[0].time + " UTC").getTime();
            }
          });

          value.episodes.forEach(g => {
            const anime = (() => {
              if (g.animeId !== undefined) return data.notifications.anime.find(a => a.id === g.animeId);

              const fromNew = data.notifications.anime.find(a => a.name === g.animeName);
              if (fromNew !== undefined) return fromNew;
              const id = value.anime.find(a => a.name === g.animeName);
              return data.notifications.anime.find(a => a.id === id);
            })();
            if (anime === undefined) return;
            if (g.animeName !== anime.name) g.animeName = anime.name;
            if (g.animeId === undefined) g.animeId = anime.id;
            const foundEpisode = data.notifications.episodes.find(h => h.animeId === anime.id && h.episode === g.episode);
            if (foundEpisode !== undefined) {
              if (g.watched === true && !foundEpisode.watched) {
                foundEpisode.watched = true;
                changed.episodeFeedUpdated++;
              }
              return;
            }
            data.notifications.episodes.push(g);
            changed.episodeFeedUpdated++;
          });
          if (save) {
            data.notifications.episodes.sort((a,b) => a.time < b.time ? 1 : -1);
            if (value.episodes.length > 0) {
              data.notifications.lastUpdated = new Date(value.episodes[0].time + " UTC").getTime();
            }
          }
          continue;
        }
        else if (ignored.settings !== true && key === 'settings') {
          for (const [key, value2] of Object.entries(value)) {
            if (defaultData.settings[key] === undefined || ignored.settings[key] || ![true,false].includes(value2)) continue;
            if (data.settings[key] === value2) continue;
            data.settings[key] = value2;
            changed.settingsUpdated++;
          }
        }
      }

      if (save) saveData(data);

      return changed;
    }

    function getCleanType(type) {
      if (type === 'linkList') return "Clean up older duplicate entries";
      else if (type === 'videoTimes') return "Remove entries with no progress (0s)";
      else return "[Message not found]";
    }

    function expandData(elem) {
      const storage = getStorage();
      const dataType = elem.attr('key');

      elem.find('.anitracker-expand-data-icon').replaceWith(contractIcon);
      const dataEntries = $('<div class="anitracker-modal-list"></div>').appendTo(elem.parent());

      const cleanButton = ['linkList','videoTimes'].includes(dataType) ? `<button class="btn btn-secondary anitracker-clean-data-button anitracker-list-btn" style="text-wrap:nowrap;" title="${getCleanType(dataType)}">Clean up</button>` : '';
      $(`
      <div class="btn-group anitracker-storage-filter">
        <input autocomplete="off" class="form-control anitracker-text-input-bar anitracker-modal-search" placeholder="Search">
        <button dir="down" class="btn btn-secondary dropdown-toggle anitracker-reverse-order-button anitracker-list-btn" title="Sort direction (down is default, and means newest first)"></button>
        ${cleanButton}
      </div>
      `).appendTo(dataEntries);
      elem.parent().find('.anitracker-modal-search').focus();

      elem.parent().find('.anitracker-modal-search').on('input', (e) => {
        setTimeout(() => {
          const query = $(e.target).val();
          for (const entry of elem.parent().find('.anitracker-modal-list-entry')) {
            if ($($(entry).find('a,span')[0]).text().toLowerCase().includes(query)) {
              $(entry).show();
              continue;
            }
            $(entry).hide();
          }
        }, 10);
      });

      elem.parent().find('.anitracker-clean-data-button').on('click', () => {
        if (!confirm("[AnimePahe Improvements]\n\n" + getCleanType(dataType) + '?')) return;

        const updatedStorage = getStorage();

        const removed = [];
        if (dataType === 'linkList') {
          for (let i = 0; i < updatedStorage.linkList.length; i++) {
            const link = updatedStorage.linkList[i];

            const similar = updatedStorage.linkList.filter(a => a.animeName === link.animeName && a.episodeNum === link.episodeNum);
            if (similar[similar.length-1] !== link) {
              removed.push(link);
            }
          }
          updatedStorage.linkList = updatedStorage.linkList.filter(a => !removed.includes(a));
        }
        else if (dataType === 'videoTimes') {
          for (const timeEntry of updatedStorage.videoTimes) {
            if (timeEntry.time > 5) continue;
            removed.push(timeEntry);
          }
          updatedStorage.videoTimes = updatedStorage.videoTimes.filter(a => !removed.includes(a));
        }

        alert(`[AnimePahe Improvements]\n\nCleaned up ${removed.length} ${removed.length === 1 ? "entry" : "entries"}.`);

        saveData(updatedStorage);
        dataEntries.remove();
        expandData(elem);
      });

      // When clicking the reverse order button
      elem.parent().find('.anitracker-reverse-order-button').on('click', (e) => {
        const btn = $(e.target);
        if (btn.attr('dir') === 'down') {
          btn.attr('dir', 'up');
          btn.addClass('anitracker-up');
        }
        else {
          btn.attr('dir', 'down');
          btn.removeClass('anitracker-up');
        }

        const entries = [];
        for (const entry of elem.parent().find('.anitracker-modal-list-entry')) {
          entries.push(entry.outerHTML);
        }
        entries.reverse();
        elem.parent().find('.anitracker-modal-list-entry').remove();
        for (const entry of entries) {
          $(entry).appendTo(elem.parent().find('.anitracker-modal-list'));
        }
        applyDeleteEvents();
      });

      function applyDeleteEvents() {
        $('.anitracker-modal-list-entry .anitracker-delete-session-button').on('click', function() {
          const storage = getStorage();

          const href = $(this).parent().find('a').attr('href');
          const animeSession = getAnimeSessionFromUrl(href);

          if (isEpisode(href)) {
            const episodeSession = getEpisodeSessionFromUrl(href);
            storage.linkList = storage.linkList.filter(g => !(g.type === 'episode' && g.animeSession === animeSession && g.episodeSession === episodeSession));
            saveData(storage);
          }
          else {
            storage.linkList = storage.linkList.filter(g => !(g.type === 'anime' && g.animeSession === animeSession));
            saveData(storage);
          }

          $(this).parent().remove();
        });

        $('.anitracker-modal-list-entry .anitracker-delete-progress-button').on('click', function() {
          const storage = getStorage();
          storage.videoTimes = storage.videoTimes.filter(g => !g.videoUrls.includes($(this).attr('lookForUrl')));
          saveData(storage);

          $(this).parent().remove();
        });

        $('.anitracker-modal-list-entry .anitracker-delete-speed-entry-button').on('click', function() {
          const storage = getStorage();
          storage.videoSpeed = storage.videoSpeed.filter(g => g.animeId !== parseInt($(this).attr('animeId')));
          saveData(storage);

          $(this).parent().remove();
        });
      }

      if (dataType === 'linkList') {
        [...storage.linkList].reverse().forEach(g => {
          const name = g.animeName + (g.type === 'episode' ? (' - Episode ' + g.episodeNum) : '');
          $(`
          <div class="anitracker-modal-list-entry">
            <a target="_blank" href="/${(g.type === 'episode' ? 'play/' : 'anime/') + g.animeSession + (g.type === 'episode' ? ('/' + g.episodeSession) : '')}" title="${toHtmlCodes(name)}">
              ${name}
            </a><br>
            <button class="btn btn-danger anitracker-delete-session-button anitracker-flat-button" title="Delete this stored session">
              <i class="fa fa-trash" aria-hidden="true"></i>
              &nbsp;Delete
            </button>
          </div>`).appendTo(elem.parent().find('.anitracker-modal-list'));
        });

        applyDeleteEvents();
      }
      else if (dataType === 'videoTimes') {
        [...storage.videoTimes].reverse().forEach(g => {
          $(`
          <div class="anitracker-modal-list-entry">
            <span>
              ${g.animeName} - Episode ${g.episodeNum}
            </span><br>
            <span>
              Current time: ${secondsToHMS(g.time)}
            </span><br>
            <button class="btn btn-danger anitracker-delete-progress-button anitracker-flat-button" lookForUrl="${g.videoUrls[0]}" title="Delete this video progress">
              <i class="fa fa-trash" aria-hidden="true"></i>
              &nbsp;Delete
            </button>
          </div>`).appendTo(elem.parent().find('.anitracker-modal-list'));
        });

        applyDeleteEvents();
      }
      else if (dataType === 'videoSpeed') {
        [...storage.videoSpeed].reverse().forEach(g => {
          $(`
          <div class="anitracker-modal-list-entry">
            <span>
              ${g.animeName}
            </span><br>
            <span>
              Playback speed: ${g.speed}
            </span><br>
            <button class="btn btn-danger anitracker-delete-speed-entry-button anitracker-flat-button" animeId="${g.animeId}" title="Delete this video speed entry">
              <i class="fa fa-trash" aria-hidden="true"></i>
              &nbsp;Delete
            </button>
          </div>`).appendTo(elem.parent().find('.anitracker-modal-list'));
        });

        applyDeleteEvents();
      }

      elem.addClass('anitracker-expanded');
    }

    function contractData(elem) {
      elem.find('.anitracker-expand-data-icon').replaceWith(expandIcon);

      elem.parent().find('.anitracker-modal-list').remove();

      elem.removeClass('anitracker-expanded');
      elem.blur();
    }

    openModal();
  }

  $('#anitracker-show-data').on('click', openShowDataModal);
}

addGeneralButtons();
if (isEpisode()) {
  $(`
  <span style="margin-left: 30px;"><i class="fa fa-files-o" aria-hidden="true"></i>&nbsp;Copy:</span>
  <div class="btn-group">
    <button class="btn btn-dark anitracker-copy-button" copy="link" data-placement="top" data-content="Copied!">Link</button>
  </div>
  <div class="btn-group" style="margin-right:30px;">
    <button class="btn btn-dark anitracker-copy-button" copy="link-time" data-placement="top" data-content="Copied!">Link & Time</button>
  </div>`).appendTo('#anitracker');
  addOptionSwitch('autoPlayNext','Auto-Play Next','Automatically go to the next episode when the current one has ended.','#anitracker');

  $('.anitracker-copy-button').on('click', (e) => {
    const targ = $(e.currentTarget);
    const type = targ.attr('copy');
    const name = encodeURIComponent(getAnimeName());
    const episode = getEpisodeNum();
    if (['link','link-time'].includes(type)) {
      navigator.clipboard.writeText(window.location.origin + '/customlink?a=' + name + '&e=' + episode + (type !== 'link-time' ? '' : ('&t=' + currentEpisodeTime.toString())));
    }
    targ.popover('show');
    setTimeout(() => {
      targ.popover('hide');
    }, 1000);
  });
}

if (initialStorage.settings.autoDelete === true && isEpisode() && paramArray.find(a => a[0] === 'ref' && a[1] === 'customlink') === undefined) {
  const animeData = getAnimeData();
  deleteEpisodesFromTracker(getEpisodeNum(), animeData.title, animeData.id);
}

function updateSwitches() {
  const storage = getStorage();

  for (const s of optionSwitches) {
    const different = s.value !== storage.settings[s.optionId];
    if (!different) continue;

    s.value = storage.settings[s.optionId];
    $(`#anitracker-${s.switchId}-switch`).prop('checked', s.value === true);

    if (s.value === true) {
      if (s.onEvent !== undefined) s.onEvent();
    }
    else if (s.offEvent !== undefined) {
      s.offEvent();
    }
  }
}

updateSwitches();

function addOptionSwitch(optionId, name, desc = '', parent = '#anitracker-modal-body') {
  const option = optionSwitches.find(s => s.optionId === optionId);

  $(`
  <div class="custom-control custom-switch anitracker-switch" id="anitracker-${option.switchId}" title="${desc}">
    <input type="checkbox" class="custom-control-input" id="anitracker-${option.switchId}-switch">
    <label class="custom-control-label" for="anitracker-${option.switchId}-switch">${name}</label>
  </div>`).appendTo(parent);
  const switc = $(`#anitracker-${option.switchId}-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.settings[optionId]) {
      storage.settings[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','');
}