// ==UserScript==
// @name AnimePahe Improvements
// @namespace https://gist.github.com/Ellivers/f7716b6b6895802058c367963f3a2c51
// @match https://animepahe.com/*
// @match https://animepahe.org/*
// @match https://animepahe.ru/*
// @match https://kwik.*/e/*
// @match https://kwik.*/f/*
// @grant GM_getValue
// @grant GM_setValue
// @version 3.23.1
// @author Ellivers
// @license MIT
// @description Improvements and additions for the AnimePahe site
// @downloadURL none
// ==/UserScript==
/*
How to install:
* Get the Violentmonkey browser extension (Tampermonkey is largely untested, but seems to work as well).
* For the GitHub Gist page, click the "Raw" button on this page.
* For Greasy Fork, click "Install this script".
* I highly suggest using an ad blocker (uBlock Origin is recommended)
Feature list:
* Automatically redirects to the correct session when a tab with an old session is loaded. No more having to search for the anime and find the episode again!
* Saves your watch progress of each video, so you can resume right where you left off.
* The saved data for old sessions can be cleared and is fully viewable and editable.
* Bookmark anime and view it in a bookmark menu.
* Add ongoing anime to an episode feed to easily check when new episodes are out.
* Quickly visit the download page for a video, instead of having to wait 5 seconds when clicking the download link.
* Find collections of anime series in the search results, with the series listed in release order.
* Jump directly to the next anime's first episode from the previous anime's last episode, and the other way around.
* Hide all episode thumbnails on the site, for those who are extra wary of spoilers (and for other reasons).
* Reworked anime index page. You can now:
* Find anime with your desired genre, theme, type, demographic, status and season.
* Search among these filter results.
* Open a random anime within the specified filters and search query.
* Automatically finds a relevant cover for the top of anime pages.
* Frame-by-frame controls on videos, using ',' and '.'
* Skip 10 seconds on videos at a time, using 'j' and 'l'
* Changes the video 'loop' keybind to Shift + L
* Press Shift + N to go to the next episode, and Shift + P to go to the previous one.
* Speed up or slow down a video by holding Ctrl and:
* Scrolling up/down
* Pressing the up/down keys
* You can also hold shift to make the speed change more gradual.
* Enables you to see images from the video while hovering over the progress bar.
* Allows you to also use numpad number keys to seek through videos.
* Theatre mode for a better non-fullscreen video experience on larger screens.
* Instantly loads the video instead of having to click a button to load it.
* Adds an "Auto-Play Video" option to automatically play the video (on some browsers, you may need to allow auto-playing for this to work).
* Adds an "Auto-Play Next" option to automatically go to the next episode when the current one is finished.
* Focuses on the video player when loading the page, so you don't have to click on it to use keyboard controls.
* Adds an option to automatically choose the highest quality available when loading the video.
* Adds a button (in the settings menu) to reset the video player.
* Shows the dates of when episodes were added.
* And more!
*/
const baseUrl = window.location.toString();
const initialStorage = getStorage();
function getDefaultData() {
return {
version: 1,
linkList:[],
videoTimes:[],
bookmarks:[],
notifications: {
lastUpdated: Date.now(),
anime: [],
episodes: []
},
badCovers: [],
autoDelete:true,
hideThumbnails:false,
theatreMode:false,
bestQuality:true,
autoDownload:true,
autoPlayNext:false,
autoPlayVideo:false
};
}
function upgradeData(data, fromver) {
console.log(`[AnimePahe Improvements] Upgrading data from version ${fromver === undefined ? 0 : fromver}`);
/* Changes:
* V1:
* autoPlay -> autoPlayNext
*/
switch (fromver) {
case undefined:
data.autoPlayNext = data.autoPlay;
delete data.autoPlay;
break;
}
}
function getStorage() {
const defa = getDefaultData();
const res = GM_getValue('anime-link-tracker', defa);
const oldVersion = res.version;
for (const key of Object.keys(defa)) {
if (res[key] !== undefined) continue;
res[key] = defa[key];
}
if (oldVersion !== defa.version) {
upgradeData(res, oldVersion);
saveData(res);
}
return res;
}
function saveData(data) {
GM_setValue('anime-link-tracker', data);
}
function secondsToHMS(secs) {
const mins = Math.floor(secs/60);
const hrs = Math.floor(mins/60);
const newSecs = Math.floor(secs % 60);
return `${hrs > 0 ? hrs + ':' : ''}${mins % 60}:${newSecs.toString().length > 1 ? '' : '0'}${newSecs % 60}`;
}
function getStoredTime(name, ep, storage, id = undefined) {
if (id !== undefined) {
return storage.videoTimes.find(a => a.episodeNum === ep && a.animeId === id);
}
else return storage.videoTimes.find(a => a.animeName === name && a.episodeNum === ep);
}
const kwikDLPageRegex = /^https:\/\/kwik\.\w+\/f\//;
// Video player improvements
if (/^https:\/\/kwik\.\w+/.test(baseUrl)) {
if (typeof $ !== "undefined" && $() !== null) anitrackerKwikLoad(window.location.origin + window.location.pathname);
else {
const scriptElem = document.querySelector('head > link:nth-child(12)');
if (scriptElem == null) {
const h1 = document.querySelector('h1');
// Some bug that the kwik DL page had before
// (You're not actually blocked when this happens)
if (!kwikDLPageRegex.test(baseUrl) && h1.textContent == "Sorry, you have been blocked") {
h1.textContent = "Oops, page failed to load.";
document.querySelector('h2').textContent = "This doesn't mean you're blocked. Try playing from another page instead.";
}
return;
}
scriptElem.onload(() => {anitrackerKwikLoad(window.location.origin + window.location.pathname)});
}
function anitrackerKwikLoad(url) {
if (kwikDLPageRegex.test(url)) {
if (initialStorage.autoDownload === false) return;
$(`
`).appendTo('#anitracker-modal-body');
const expandIcon = ``;
const contractIcon = ``;
$(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());
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();
$(`
Warning: for developer use. Back up your data before messing with this.
Leave value empty to get the existing value
`).appendTo('#anitracker-modal-body');
[{t:'Replace',i:'replace'},{t:'Append',i:'append'},{t:'Delete from list',i:'delList'}].forEach(g => { $(``).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 === \"banana\"'\nWhichever elements that match this will be deleted.";
switch (mode) {
case 'replace':
eval(`storage.${key} = value`);
break;
case 'append':
if (keyValue.constructor.name !== 'Array') {
alert("Not a list");
return;
}
eval(`storage.${key}.push(value)`);
break;
case 'delList':
if (keyValue.constructor.name !== 'Array') {
alert("Not a list");
return;
}
try {
eval(`storage.${key} = storage.${key}.filter(a => !(${value}))`);
}
catch (e) {
console.error(e);
alert(delFromListMessage);
return;
}
break;
default:
alert("This message isn't supposed to show up. Uh...");
return;
}
if (JSON.stringify(storage) === JSON.stringify(getStorage())) {
alert("Nothing changed.");
if (mode === 'delList') {
alert(delFromListMessage);
}
return;
}
else alert("Probably worked!");
saveData(storage);
});
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();
$(`
Choose what to import
0 ? "checked" : "disabled"}>
0 ? "checked" : "disabled"}>
0 ? "checked" : "disabled"}>
0 ? "checked" : "disabled"}>
0 ? "checked" : "disabled"}>
`).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
}
for (const [key, value] of Object.entries(importedData)) {
if (getDefaultData()[key] === undefined || ignored.settings[key]) 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;
}
if ((value !== true && value !== false) || data[key] === undefined || data[key] === value || ignored.settings === true) continue;
data[key] = value;
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 = $('').appendTo(elem.parent());
$(`