Tracker per il sito Animeworld
// ==UserScript==
// @name AnimeWorld Smart Tracker & Colorizer
// @namespace aw-smart-tracker
// @version 2.1
// @description Tracker per il sito Animeworld
// @author Lollo
// @match *://*.animeworld.*/*
// @match *://*.animeworld.ac/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=animeworld.ac
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @run-at document-idle
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// ==========================================
// IMPOSTAZIONI DEL TEMPO (in secondi)
// ==========================================
const SOGLIA_INIZIO = 3 * 60; // 3 minuti (sotto questo tempo è considerato appena iniziato)
const SOGLIA_FINE = 19 * 60; // 19 minuti (sopra questo tempo diventa verde / completato)
// ==========================================
// COLORI BASE
// ==========================================
const COLORE_FINITO = 'rgba(76, 175, 80, 0.35)';
const COLORE_IN_CORSO = 'rgba(244, 67, 54, 0.4)';
const COLORE_DA_VEDERE = '';
const COLORE_FINITO_ATTIVO = 'rgba(76, 175, 80, 0.85)';
const COLORE_IN_CORSO_ATTIVO = 'rgba(244, 67, 54, 0.85)';
const STORAGE_PREFIX = 'aw-resume:';
// --- FUNZIONE HELPER ---
function getEpisodeId(url) {
const m = url.match(/\/play\/([^?#]+)/);
return m ? m[1] : null;
}
// ==========================================================
// SEZIONE 1: LOGICA PAGINA EPISODIO (/play/)
// ==========================================================
if (window.location.pathname.startsWith('/play/')) {
// --- A. Logica dei Colori ---
function coloraPulsanti() {
const pulsanti = document.querySelectorAll('a[data-episode-id][data-id]');
const currentUrlId = getEpisodeId(window.location.href);
let maxEpisodioFinito = -1;
pulsanti.forEach(btn => {
const href = btn.getAttribute('href');
if (!href) return;
const epId = getEpisodeId(href);
if (!epId) return;
const secondiVisti = parseFloat(localStorage.getItem(STORAGE_PREFIX + epId) || 0);
if (secondiVisti >= SOGLIA_FINE) {
const epNum = parseFloat(btn.getAttribute('data-num') || btn.textContent);
if (!isNaN(epNum) && epNum > maxEpisodioFinito) maxEpisodioFinito = epNum;
}
});
pulsanti.forEach(btn => {
const href = btn.getAttribute('href');
if (!href) return;
const epId = getEpisodeId(href);
if (!epId) return;
const epNum = parseFloat(btn.getAttribute('data-num') || btn.textContent);
const secondiVisti = parseFloat(localStorage.getItem(STORAGE_PREFIX + epId) || 0);
const isCurrentEpisode = (epId === currentUrlId);
if ((!isNaN(epNum) && epNum <= maxEpisodioFinito) || secondiVisti >= SOGLIA_FINE) {
btn.style.backgroundColor = isCurrentEpisode ? COLORE_FINITO_ATTIVO : COLORE_FINITO;
btn.style.borderColor = 'rgba(76, 175, 80, 0.9)';
} else if (secondiVisti >= SOGLIA_INIZIO) {
btn.style.backgroundColor = isCurrentEpisode ? COLORE_IN_CORSO_ATTIVO : COLORE_IN_CORSO;
btn.style.borderColor = 'rgba(244, 67, 54, 0.9)';
} else {
btn.style.backgroundColor = COLORE_DA_VEDERE;
btn.style.borderColor = '';
}
});
}
// --- B. Cerca l'episodio più avanzato (Il Cervello) ---
function aggiornaDatiTrackerGlobali() {
const titleEl = document.getElementById('anime-title');
if (!titleEl) return;
const title = titleEl.textContent.trim();
const pulsanti = Array.from(document.querySelectorAll('a[data-episode-id][data-id]'));
if (pulsanti.length === 0) return;
let maxEpFinitoNum = -1;
let epFinitoObj = null;
let maxEpInCorsoNum = -1;
let epInCorsoObj = null;
// Trova gli episodi col numero più alto tra quelli in corso e quelli finiti
pulsanti.forEach(btn => {
const href = btn.getAttribute('href');
if (!href) return;
const epId = getEpisodeId(href);
if (!epId) return;
const epNumStr = btn.getAttribute('data-num') || btn.textContent;
const epNum = parseFloat(epNumStr);
if (isNaN(epNum)) return;
const secondiVisti = parseFloat(localStorage.getItem(STORAGE_PREFIX + epId) || 0);
if (secondiVisti >= SOGLIA_FINE) {
if (epNum > maxEpFinitoNum) {
maxEpFinitoNum = epNum;
epFinitoObj = { btn, epNum, epId, epNumStr };
}
} else if (secondiVisti >= SOGLIA_INIZIO) {
if (epNum > maxEpInCorsoNum) {
maxEpInCorsoNum = epNum;
epInCorsoObj = { btn, epNum, epId, epNumStr };
}
}
});
let targetEpId = '';
let targetEpNum = '';
let targetUrl = '';
let targetNextUrl = null;
// Scegli quale episodio tracciare (sempre il più avanzato)
if (maxEpInCorsoNum > maxEpFinitoNum) {
targetEpId = epInCorsoObj.epId;
targetEpNum = epInCorsoObj.epNumStr;
targetUrl = epInCorsoObj.btn.getAttribute('href');
} else if (epFinitoObj) {
targetEpId = epFinitoObj.epId;
targetEpNum = epFinitoObj.epNumStr;
targetUrl = epFinitoObj.btn.getAttribute('href');
// Se l'ultimo guardato è finito, prepariamo il NextUrl
const currentLi = epFinitoObj.btn.closest('li.episode');
const nextLi = currentLi.nextElementSibling;
let nextBtn = null;
if (nextLi) {
nextBtn = nextLi.querySelector('a');
} else {
const currentUl = currentLi.closest('ul.episodes');
const nextUl = currentUl.nextElementSibling;
if (nextUl && nextUl.classList.contains('episodes')) {
nextBtn = nextUl.querySelector('li.episode a');
}
}
if (nextBtn) targetNextUrl = nextBtn.getAttribute('href');
}
// Fallback: se l'anime è immacolato (nessun ep > 3 min)
if (!targetUrl) {
const activeLink = document.querySelector('#animeId .episode a.active');
if (activeLink) {
const activeEpNumStr = activeLink.getAttribute('data-num') || activeLink.textContent;
const activeEpNum = parseFloat(activeEpNumStr);
let trackerData = GM_getValue('aw_tracker_data', {});
let oldData = trackerData[title];
// Evitiamo che aprire un ep vecchio sovrascriva i dati di un ep nuovo!
if (oldData && oldData.currentEp) {
const oldEpNum = parseFloat(oldData.currentEp);
if (!isNaN(oldEpNum) && !isNaN(activeEpNum) && activeEpNum < oldEpNum) {
return; // Blocca il salvataggio se è un episodio precedente
}
}
targetEpId = getEpisodeId(window.location.href);
targetEpNum = activeEpNumStr.trim();
targetUrl = activeLink.getAttribute('href');
}
}
if (!targetUrl) return;
if (targetUrl && !targetUrl.startsWith('http')) targetUrl = window.location.origin + targetUrl;
if (targetNextUrl && !targetNextUrl.startsWith('http')) targetNextUrl = window.location.origin + targetNextUrl;
// Salva nel Tracker
let trackerData = GM_getValue('aw_tracker_data', {});
trackerData[title] = {
currentEp: targetEpNum,
currentEpId: targetEpId,
currentUrl: targetUrl,
nextUrl: targetNextUrl,
lastUpdated: Date.now()
};
GM_setValue('aw_tracker_data', trackerData);
}
// --- C. Tracciamento Video in Tempo Reale ---
function tracciaTempoVideo() {
const video = document.querySelector('video');
const currentEpId = getEpisodeId(window.location.href);
if (!video || !currentEpId) return;
setInterval(() => {
if (!video.paused && video.currentTime > 5) {
localStorage.setItem(STORAGE_PREFIX + currentEpId, String(video.currentTime));
localStorage.setItem(STORAGE_PREFIX + currentEpId + ':ts', String(Date.now()));
// Ogni volta che si salva un po' di tempo, aggiorniamo l'interfaccia e il Tracker intelligente
coloraPulsanti();
aggiornaDatiTrackerGlobali();
}
}, 5000);
}
// Inizializzazione pagina /play/
setTimeout(() => {
coloraPulsanti();
aggiornaDatiTrackerGlobali(); // Mappa il progresso appena apri la pagina
}, 1500);
setTimeout(tracciaTempoVideo, 2000);
// Observer per i cambi dinamici dei server/episodi
const observer = new MutationObserver((mutations) => {
for (let mutation of mutations) {
if (mutation.addedNodes.length > 0) coloraPulsanti();
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
// ==========================================================
// SEZIONE 2: LOGICA HOMEPAGE (Sidebar Riprendi)
// ==========================================================
if (window.location.pathname === '/' || window.location.pathname === '/home') {
GM_addStyle(`
#aw-tracker-sidebar { position: fixed; top: 20%; right: -320px; width: 320px; background-color: #1a1a1a; border: 1px solid #333; border-right: none; border-radius: 10px 0 0 10px; color: #fff; z-index: 999999; transition: right 0.3s ease; box-shadow: -5px 0 15px rgba(0,0,0,0.5); font-family: sans-serif; }
#aw-tracker-sidebar.open { right: 0; }
#aw-tracker-toggle { position: absolute; left: -40px; top: 20px; width: 40px; height: 40px; background-color: #e50914; color: white; display: flex; align-items: center; justify-content: center; cursor: pointer; border-radius: 10px 0 0 10px; font-weight: bold; font-size: 20px; box-shadow: -2px 0 5px rgba(0,0,0,0.3); }
.aw-tracker-header { padding: 15px; background-color: #222; border-bottom: 1px solid #333; border-radius: 10px 0 0 0; text-align: center; font-weight: bold; font-size: 16px; }
.aw-tracker-list { max-height: 60vh; overflow-y: auto; padding: 10px; }
.aw-tracker-item { background: #2a2a2a; margin-bottom: 10px; padding: 12px; border-radius: 5px; display: flex; flex-direction: column; gap: 8px; border-left: 3px solid #555;}
.aw-tracker-item.in-corso { border-left-color: #f44336; }
.aw-tracker-item.finito { border-left-color: #4CAF50; }
.aw-tracker-title { font-size: 14px; font-weight: bold; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.aw-tracker-actions { display: flex; justify-content: space-between; align-items: center; margin-top: 5px;}
.aw-tracker-ep { font-size: 12px; color: #aaa; }
.aw-btn-resume { background-color: #e50914; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 12px; font-weight: bold; }
.aw-btn-resume:hover { background-color: #f6121d; }
.aw-btn-resume.btn-next { background-color: #4CAF50; }
.aw-btn-resume.btn-next:hover { background-color: #45a049; }
.aw-btn-delete { background: none; border: none; color: #666; cursor: pointer; font-size: 14px; }
.aw-btn-delete:hover { color: #ff4d4d; }
#aw-toast { visibility: hidden; min-width: 250px; background-color: #333; color: #fff; text-align: center; border-radius: 5px; padding: 16px; position: fixed; z-index: 9999999; left: 50%; bottom: 30px; transform: translateX(-50%); font-size: 15px; box-shadow: 0px 4px 10px rgba(0,0,0,0.5); }
#aw-toast.show { visibility: visible; animation: aw-fadein 0.5s, aw-fadeout 0.5s 2.5s; }
@keyframes aw-fadein { from {bottom: 0; opacity: 0;} to {bottom: 30px; opacity: 1;} }
@keyframes aw-fadeout { from {bottom: 30px; opacity: 1;} to {bottom: 0; opacity: 0;} }
.aw-tracker-list::-webkit-scrollbar { width: 6px; }
.aw-tracker-list::-webkit-scrollbar-track { background: #1a1a1a; }
.aw-tracker-list::-webkit-scrollbar-thumb { background: #555; border-radius: 3px; }
`);
const sidebarHTML = `
<div id="aw-tracker-sidebar">
<div id="aw-tracker-toggle">▶</div>
<div class="aw-tracker-header">I Tuoi Anime</div>
<div class="aw-tracker-list" id="aw-tracker-list"></div>
</div>
<div id="aw-toast">Sei in pari con l'anime! Nessun nuovo episodio.</div>
`;
document.body.insertAdjacentHTML('beforeend', sidebarHTML);
const sidebar = document.getElementById('aw-tracker-sidebar');
const toggleBtn = document.getElementById('aw-tracker-toggle');
const listContainer = document.getElementById('aw-tracker-list');
const toast = document.getElementById('aw-toast');
toggleBtn.addEventListener('click', () => {
sidebar.classList.toggle('open');
toggleBtn.textContent = sidebar.classList.contains('open') ? '◀' : '▶';
});
function showToast() {
toast.className = "show";
setTimeout(() => { toast.className = toast.className.replace("show", ""); }, 3000);
}
function renderList() {
const data = GM_getValue('aw_tracker_data', {});
listContainer.innerHTML = '';
const sortedAnime = Object.keys(data).sort((a, b) => data[b].lastUpdated - data[a].lastUpdated);
if (sortedAnime.length === 0) {
listContainer.innerHTML = '<div style="text-align:center; padding: 20px; color: #777; font-size:13px;">Nessun anime in corso.<br>Apri un episodio per iniziare!</div>';
return;
}
sortedAnime.forEach(title => {
const anime = data[title];
const savedTime = localStorage.getItem(STORAGE_PREFIX + anime.currentEpId);
const secondiVisti = savedTime ? parseFloat(savedTime) : 0;
let isFinished = secondiVisti >= SOGLIA_FINE;
let targetUrl = '';
let statusText = '';
let btnText = '';
let extraClassBtn = '';
let extraClassItem = '';
// Interpretazione Intelligente sulla Homepage
if (isFinished) {
targetUrl = anime.nextUrl || 'null';
statusText = `Finito: Ep. ${anime.currentEp}`;
btnText = "Prossimo Ep. ➔";
extraClassBtn = "btn-next";
extraClassItem = "finito";
} else {
targetUrl = anime.currentUrl || '#';
statusText = secondiVisti >= SOGLIA_INIZIO ? `In corso: Ep. ${anime.currentEp}` : `Nuovo: Ep. ${anime.currentEp}`;
btnText = "Riprendi Ep.";
extraClassItem = secondiVisti >= SOGLIA_INIZIO ? "in-corso" : "";
}
const itemHTML = `
<div class="aw-tracker-item ${extraClassItem}">
<div style="display:flex; justify-content:space-between; align-items:center;">
<span class="aw-tracker-title" title="${title}">${title}</span>
<button class="aw-btn-delete" data-title="${title}" title="Rimuovi dalla lista">✖</button>
</div>
<div class="aw-tracker-actions">
<span class="aw-tracker-ep">${statusText}</span>
<button class="aw-btn-resume ${extraClassBtn}" data-url="${targetUrl}">${btnText}</button>
</div>
</div>
`;
listContainer.insertAdjacentHTML('beforeend', itemHTML);
});
document.querySelectorAll('.aw-btn-resume').forEach(btn => {
btn.addEventListener('click', function() {
const url = this.getAttribute('data-url');
if (url && url !== 'null' && url !== '#') {
window.location.href = url;
} else {
showToast();
}
});
});
document.querySelectorAll('.aw-btn-delete').forEach(btn => {
btn.addEventListener('click', function() {
const titleToRemove = this.getAttribute('data-title');
const currentData = GM_getValue('aw_tracker_data', {});
delete currentData[titleToRemove];
GM_setValue('aw_tracker_data', currentData);
renderList();
});
});
}
renderList();
}
})();