// ==UserScript==
// @name AnimeWorld Skipper
// @namespace https://github.com/pizidavi
// @icon https://static.animeworld.so/assets/images/favicon/android-icon-192x192.png
// @description Salta anime intro in AnimeWorld
// @author pizidavi
// @version 1.1
// @copyright 2023, PIZIDAVI
// @license MIT
// @require https://cdn.jsdelivr.net/gh/soufianesakhi/node-creation-observer-js@edabdee1caaee6af701333a527a0afd95240aa3b/release/node-creation-observer-latest.min.js
// @require https://cdn.jsdelivr.net/gh/sizzlemctwizzle/GM_config@2207c5c1322ebb56e401f03c2e581719f909762a/gm_config.min.js
// @require https://greasyfork.org/scripts/457460-aniskip/code/AniSkip.js
// @require https://greasyfork.org/scripts/401626-notify-library/code/Notify%20Library.js
// @match https://www.animeworld.so/play/*
// @connect api.aniskip.com
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_xmlhttpRequest
// @grant GM_registerMenuCommand
// @grant GM_getResourceText
// @run-at document-body
// ==/UserScript==
/* global GM_config, NodeCreationObserver, AniSkip, Notify */
(function () {
'use strict';
//* GM_config
GM_config.init({
id: GM.info.script.name.toLowerCase().replace(/\s/g, '-'),
title: `${GM.info.script.name} v${GM.info.script.version} Settings`,
fields: {
AniSkipUserId: {
label: 'AniSkip User Id',
section: ['AniSkip', 'Ottieni su: https://www.uuidgenerator.net/'],
labelPos: 'left',
type: 'text',
title: 'Il tuo AniSkip userId',
size: 70,
default: ''
},
ManualSeekTime: {
label: 'Secondi saltati con Salto Manuale (s)',
section: ['Settings', 'Script settings'],
type: 'int',
default: 90
},
SeekWheel: {
label: 'Millisecondi saltati con rotella del mouse su input nel Menu (ms)',
type: 'int',
default: 50
},
SubmittedSkip: {
label: 'Times Skip inviati',
section: ['Stats', '(solo lettura)'],
type: 'int',
default: 0,
save: false
}
},
events: {
init: () => { },
open: () => {
GM_config.set('SubmittedSkip', GM_getValue('SubmittedSkip', 0));
},
save: () => {
alert('Impostazioni salvate');
GM_config.close()
setTimeout(window.location.reload(false), 500);
}
}
});
GM_registerMenuCommand('Configure', () => GM_config.open());
//* Const
const CSS = `
/* Skip Button */
#player { display: flex; }
.aws-btn {
display: none;
position: absolute;
bottom: 6em;
right: 2em;
padding: .5em 1em .4em;
font-size: 14px;
border: 1px solid #bdc3c7;
border-radius: 2px;
color: #bdc3c7;
background-color: rgba(0, 0, 0, .7);
opacity: .35;
z-index: 2147483648;
}
.aws-btn:hover {
opacity: .75;
}
.aws-btn.active {
display: block;
}
/* Menu */
.aws-container {
position: absolute;
top: 1em;
right: 1em;
border: none;
}
.aws-container summary {
cursor: pointer;
color: white;
background-color: rgba(35, 35, 35, 0.5);
border-radius: 5px;
padding: 1px 5px;
user-select: none;
opacity: 0.5;
transition: opacity 0.3s;
}
.aws-container[open] summary,
.aws-container summary:hover {
opacity: 0.8;
}
.aws-container summary::-webkit-details-marker {
display: none;
}
.aws-menu {
position: absolute;
right: 0;
padding: 10px;
border-radius: 3px;
color: white;
background-color: rgba(20, 20, 20, 0.95);
z-index: 2;
min-width: 300px;
}
.aws-menu h4,
.aws-menu h5 {
margin-top: 0;
margin-bottom: 3px;
}
.aws-menu h5 {
margin-bottom: 0;
}
.aws-menu hr {
margin: 10px 0;
padding: 0;
}
#aws-message {
text-align: center;
margin-bottom: 0;
}
#aws-reload-list {
fill: white;
width: 11px;
margin-right: 2px;
}
.aws-skip-list {
overflow-y: auto;
max-height: 250px;
}
.aws-skip-list .skip-item {
display: flex;
align-items: center;
padding: 5px;
border-bottom: 1px solid #555;
}
.aws-skip-list .skip-item:last-child {
border-bottom: none;
}
.aws-skip-list .skip-item .icon {
display: block;
height: 25px;
width: 25px;
margin-right: 10px;
float: left;
background-color: #bbb;
border: 1px solid white;
border-radius: 50%;
}
.aws-skip-list .skip-item .icon.op {
background-color: green
}
.aws-skip-list .skip-item .icon.ed {
background-color: gold
}
.aws-skip-list .skip-item .icon.mixed-op {
background-color: #90ee90
}
.aws-skip-list .skip-item .icon.mixed-ed {
background-color: #ff0
}
.aws-skip-list .skip-item .icon.recap {
background-color: #add8e6
}
.aws-skip-list .skip-item .info .name {
font-weight: 400;
font-size: 1.1rem;
}
.aws-skip-list .skip-item .info p {
margin: 0;
font-size: .9rem;
}
.aws-skip-list .skip-item .action {
display: flex;
margin-left: auto;
}
.aws-skip-list .skip-item .action button {
display: flex;
justify-content: center;
align-items: center;
width: 25px;
aspect-ratio: 1;
margin-right: 4px;
padding: 0;
border-radius: 25%;
}
.aws-skip-list .skip-item .action button:last-child {
margin-right: 0;
}
.aws-skip-list .skip-item .action button svg {
width: 12px;
}
`;
const MENU = `
<details id="aws" class="aws-container">
<summary title="Apri/Chiudi menu" role="button">AW Skipper</summary>
<div class="aws-menu">
<h4>AnimeWorld Skipper</h4>
<hr />
<!-- Form -->
<form>
<h5>Add Skip Time</h5>
<select class="form-control" name="aws-skip-type" style="margin-bottom:5px" required>
<option value hidden selected>Seleziona una tipologia</option>
<option value="op">Opening</option>
<option value="ed">Ending</option>
<option value="mixed-op">Mixed Epening</option>
<option value="mixed-ed">Mixed Ending</option>
<option value="recap">Recap</option>
</select>
<div class="row">
<div class="col-sm-4" style="padding-right:0;">
<input
type="number"
class="form-control"
name="aws-start"
value="0.000"
placeholder="Start time"
step="0.001"
min="0"
required
data-aws-wheel
>
<small data-aws-goTo="now" role="button">+Ora</small>
</div>
<div class="col-sm-4" style="padding-right:0;">
<input
type="number"
class="form-control"
name="aws-end"
value="0.000"
placeholder="End time"
step="0.001"
min="0"
required
data-aws-wheel
>
<small data-aws-goTo="now" role="button">+Ora</small>
<small data-aws-goTo="+90" role="button">+90s</small>
<small data-aws-goTo="end" style="margin-left:auto;" role="button">+Fine</small>
</div>
<div class="col-sm-4">
<button
class="btn btn-block btn-primary"
type="submit"
disabled
>Salva</button>
</div>
</div>
</form>
<!-- End form -->
<hr />
<!-- Skip list -->
<h5>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" id="aws-reload-list" role="button">
<path d="M463.5 224H472c13.3 0 24-10.7 24-24V72c0-9.7-5.8-18.5-14.8-22.2s-19.3-1.7-26.2 5.2L413.4 96.6c-87.6-86.5-228.7-86.2-315.8 1c-87.5 87.5-87.5 229.3 0 316.8s229.3 87.5 316.8 0c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0c-62.5 62.5-163.8 62.5-226.3 0s-62.5-163.8 0-226.3c62.2-62.2 162.7-62.5 225.3-1L327 183c-6.9 6.9-8.9 17.2-5.2 26.2s12.5 14.8 22.2 14.8H463.5z" />
</svg>
Skip Times
</h5>
<div id="aws-skip-list" class="aws-skip-list"><!-- Items --></div>
<p id="aws-message" style="text-align:center;">Waiting for video metadata</p>
<!-- End skip list -->
</div>
</details>
`;
const TEMPLATE_ITEM = `
<div class="skip-item">
<span class="icon"></span>
<div class="info">
<span class="name"></span>
<p class="times">
<span class="time-start" role="button" title="Go to start"></span>
-
<span class="time-end" role="button" title="Go to end"></span>
</p>
</div>
<div class="action">
<button class="btn btn-dark" data-aws-vote="upvote">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path d="M313.4 32.9c26 5.2 42.9 30.5 37.7 56.5l-2.3 11.4c-5.3 26.7-15.1 52.1-28.8 75.2H464c26.5 0 48 21.5 48 48c0 25.3-19.5 46-44.3 47.9c7.7 8.5 12.3 19.8 12.3 32.1c0 23.4-16.8 42.9-38.9 47.1c4.4 7.2 6.9 15.8 6.9 24.9c0 21.3-13.9 39.4-33.1 45.6c.7 3.3 1.1 6.8 1.1 10.4c0 26.5-21.5 48-48 48H294.5c-19 0-37.5-5.6-53.3-16.1l-38.5-25.7C176 420.4 160 390.4 160 358.3V320 272 247.1c0-29.2 13.3-56.7 36-75l7.4-5.9c26.5-21.2 44.6-51 51.2-84.2l2.3-11.4c5.2-26 30.5-42.9 56.5-37.7zM32 192H96c17.7 0 32 14.3 32 32V448c0 17.7-14.3 32-32 32H32c-17.7 0-32-14.3-32-32V224c0-17.7 14.3-32 32-32z" fill="black"></path>
</svg>
</button>
<button class="btn" data-aws-vote="downvote">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path d="M313.4 479.1c26-5.2 42.9-30.5 37.7-56.5l-2.3-11.4c-5.3-26.7-15.1-52.1-28.8-75.2H464c26.5 0 48-21.5 48-48c0-25.3-19.5-46-44.3-47.9c7.7-8.5 12.3-19.8 12.3-32.1c0-23.4-16.8-42.9-38.9-47.1c4.4-7.3 6.9-15.8 6.9-24.9c0-21.3-13.9-39.4-33.1-45.6c.7-3.3 1.1-6.8 1.1-10.4c0-26.5-21.5-48-48-48H294.5c-19 0-37.5 5.6-53.3 16.1L202.7 73.8C176 91.6 160 121.6 160 153.7V192v48 24.9c0 29.2 13.3 56.7 36 75l7.4 5.9c26.5 21.2 44.6 51 51.2 84.2l2.3 11.4c5.2 26 30.5 42.9 56.5 37.7zM32 320H96c17.7 0 32-14.3 32-32V64c0-17.7-14.3-32-32-32H32C14.3 32 0 46.3 0 64V288c0 17.7 14.3 32 32 32z"/>
</svg>
</button>
</div>
</div>
`;
const aniskip = new AniSkip({
userId: GM_config.get('AniSkipUserId'),
providerName: 'AnimeWorld'
});
const seekWheel = GM_config.get('SeekWheel', 50) / 1000;
//* Script
NodeCreationObserver.init('AnimeWorldSkipper');
NodeCreationObserver.onCreation('#player iframe', function (iframe) {
const malId = document.querySelector('#mal-button').getAttribute('href').split('/').at(-1);
const episodeNumber = document.querySelector('div.server ul a.active').getAttribute('data-base').split('-')[0];
iframe.addEventListener('load', function () {
const content = this.contentDocument;
const video = content?.querySelector('video');
if (!video) return;
injectStyle(CSS, content.head);
const menu = injectHTML(MENU, video.parentNode);
menu.querySelector('form [name="aws-start"]').onchange = function () {
this.value = (parseFloat(this.value) || 0).toFixed(3);
video.currentTime = this.value;
};
menu.querySelector('form [name="aws-end"]').onchange = function () {
this.value = (parseFloat(this.value) || 0).toFixed(3);
video.currentTime = this.value;
};
const loadskipdata = () => {
const videoLength = video.duration;
content.querySelectorAll('.aws-btn').forEach(element => {
element.remove();
});
menu.querySelector('#aws-message').textContent = 'Loading...';
menu.querySelector('#aws-skip-list').innerHTML = '';
menu.querySelector('#aws-reload-list').onclick = () => {
video.dispatchEvent(new Event('loadskipdata'));
};
menu.querySelector('form [name="aws-start"]').max = videoLength.toFixed(3);
menu.querySelector('form [name="aws-end"]').max = videoLength.toFixed(3);
menu.querySelector('form button[type="submit"]').disabled = false;
menu.querySelectorAll('form [data-aws-goTo]').forEach(element => {
const action = element.getAttribute('data-aws-goTo');
element.onclick = function () {
const value = (action === 'now' ? video.currentTime : action === '+90' ? video.currentTime+90 : videoLength).toFixed(3);
this.parentNode.querySelector('input').value = value;
this.parentNode.querySelector('input').dispatchEvent(new Event('change'));
};
});
menu.querySelectorAll('form [data-aws-wheel]').forEach(element => {
element.onwheel = function (e) {
e.preventDefault();
e.stopPropagation();
let newValue = (parseFloat(this.value) || 0) + (e.deltaY >= 0 ? -seekWheel : seekWheel);
if (newValue < 0)
newValue = 0;
else if (newValue > videoLength)
newValue = videoLength;
if (this.value !== newValue.toFixed(3)) {
this.value = newValue.toFixed(3);
this.dispatchEvent(new Event('change'));
}
};
});
aniskip.getSkipTimes(malId, episodeNumber, videoLength)
.then(data => {
console.log(data)
menu.querySelector('#aws-message').textContent = '';
data.forEach(item => {
// Add skip-btn to video
const skipButton = createSkipButton({
id: item.skipId,
text: 'Salta ' + aniskip.CategoryTitle[item.skipType]
});
skipButton.onclick = () => {
video.currentTime = item.interval.endTime;
video.focus();
};
skipButton.setAttribute('data-time-start', item.interval.startTime);
skipButton.setAttribute('data-time-end', item.interval.endTime);
video.parentNode.appendChild(skipButton);
// Add skip-item to menu
const skipItem = injectHTML(TEMPLATE_ITEM, menu.querySelector('#aws-skip-list'));
skipItem.querySelector('.icon').classList.add(item.skipType);
skipItem.querySelector('.name').textContent = aniskip.CategoryTitle[item.skipType];
skipItem.querySelector('.times .time-start').textContent = item.interval.startTime.toFixed(1);
skipItem.querySelector('.times .time-start').onclick = () => {
video.currentTime = item.interval.startTime;
};
skipItem.querySelector('.times .time-end').textContent = item.interval.endTime.toFixed(1);
skipItem.querySelector('.times .time-end').onclick = () => {
video.currentTime = item.interval.endTime;
};
skipItem.querySelectorAll('.action [data-aws-vote]').forEach(element => {
const action = element.getAttribute('data-aws-vote');
element.onclick = function () {
this.disabled = true;
aniskip.vote(action, item.skipId)
.then(data => {
new Notify({
text: data.message || 'Votato!',
type: 'success'
}).show();
})
.catch(data => {
console.error(data)
new Notify({
text: data.message || 'Errore',
type: 'error'
}).show();
})
.finally(() => {
video.dispatchEvent(new Event('loadskipdata'));
});
}
});
});
})
.catch(response => {
console.log(response)
menu.querySelector('#aws-message').textContent = response.message || 'No skip time';
// Add skip-btn to video
const skipButton = createSkipButton({
id: 'manual-skip',
text: 'Salto Manuale',
class: 'aws-btn manual active',
});
skipButton.onclick = function () {
const seekTime = GM_config.get('ManualSeekTime', 90);
const form = menu.querySelector('form');
form.querySelector('[name="aws-start"]').value = video.currentTime.toFixed(3);
form.querySelector('[name="aws-end"]').value = (video.currentTime + seekTime).toFixed(3);
menu.setAttribute('open', true);
video.pause();
video.currentTime += seekTime;
video.focus();
this.remove();
};
video.parentNode.appendChild(skipButton);
});
menu.querySelector('form').onsubmit = function (e) {
e.preventDefault();
e.stopPropagation();
const form = this;
const skipType = form.querySelector('[name="aws-skip-type"]').value;
const timeStart = parseFloat(form.querySelector('[name="aws-start"]').value);
const timeEnd = parseFloat(form.querySelector('[name="aws-end"]').value);
if (
!skipType ||
(!timeStart && timeStart !== 0) ||
(!timeEnd && timeEnd !== 0) ||
timeStart >= timeEnd ||
timeStart < 0 ||
timeEnd > videoLength
) {
alert('Campi non validi');
return;
}
form.querySelector('button[type="submit"]').disabled = true;
aniskip.createSkipTime(
malId,
episodeNumber,
{
skipType: skipType,
startTime: timeStart,
endTime: timeEnd,
episodeLength: videoLength
}
).then(data => {
form.querySelector('button[type="submit"]').disabled = false;
form.reset();
GM_setValue('SubmittedSkip', GM_getValue('SubmittedSkip', 0) + 1);
video.dispatchEvent(new Event('loadskipdata'));
}).catch(error => console.error(error));
};
};
video.addEventListener('loadskipdata', loadskipdata);
if (video.readyState > 0) {
loadskipdata();
video.play();
} else
video.onloadedmetadata = loadskipdata;
// Show/Hide skipButton base of currentTime
video.ontimeupdate = () => {
content.querySelectorAll('.aws-btn:not(.manual)').forEach(element => {
const startTime = element.getAttribute('data-time-start');
const endTime = element.getAttribute('data-time-end');
const active = video.currentTime >= startTime && video.currentTime < endTime;
element.classList.toggle('active', active);
});
if (video.currentTime > video.duration / 3)
content.querySelector('#manual-skip')?.remove();
};
video.oncanplay = function () {
this.play();
this.focus();
this.oncanplay = null; // only at video start
};
video.onmouseenter = function () {
if (!this.paused && !this.ended)
this.focus();
};
content.onkeydown = e => {
if (e.keyCode === 13) {
const btn = content.querySelector('.aws-btn.active');
if (btn) {
e.preventDefault();
btn.click();
}
}
};
}); // end iframe.addEventListener
}); // end NodeCreationObserver.onCreation
injectMeta('AnimeWorldSkipper');
//* Functions
function createSkipButton(options = {}) {
return createElement('button', {
text: options.text ?? 'Salta',
class: options.class ?? 'aws-btn',
...options
});
}
function createElement(tagName, options) {
const element = document.createElement(tagName);
element.textContent = options?.text;
element.id = options?.id ?? '';
element.classList = options?.class ?? '';
element.title = options?.title ?? '';
element.value = options?.value ?? '';
element.style = options?.style ?? '';
element.innerHTML = options?.html ?? element.innerHTML;
return element;
}
function injectHTML(html, parent = document.body) {
const tag = document.createElement('div');
tag.innerHTML = html;
return parent.appendChild(tag.childElementCount === 1 ? tag.firstElementChild : tag);
}
function injectStyle(style, parent = document.head) {
const tag = document.createElement('style');
tag.innerText = style;
parent.appendChild(tag);
}
function injectMeta(name, content = '', parent = document.head) {
const tag = document.createElement('meta');
tag.name = name;
tag.content = content;
parent.appendChild(tag);
}
})();