AnimeWorld Smart Tracker & Colorizer

Tracker per il sito Animeworld

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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();
    }
})();