Netflix Advanced Skipper

Script avanzato per Netflix: salta intro, recap, 'Are you still watching?', e passa automaticamente all'episodio successivo. Configurabile via menu, con storage persistente, logging opzionale, supporto multi-lingua, e monitoraggio robusto del player.

// ==UserScript==
// @name         Netflix Advanced Skipper
// @namespace    http://tampermonkey.net/
// @version      2.0
// @description  Script avanzato per Netflix: salta intro, recap, 'Are you still watching?', e passa automaticamente all'episodio successivo. Configurabile via menu, con storage persistente, logging opzionale, supporto multi-lingua, e monitoraggio robusto del player.
// @author       Boranga
// @match        https://www.netflix.com/watch/*
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_notification
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    // Configurazione predefinita
    const DEFAULT_SETTINGS = {
        skipIntro: true,
        skipRecap: true,
        skipStillWatching: true,
        autoNextEpisode: true,
        logging: false,
        introThreshold: 30, // Secondi iniziali per skip intro
        endThreshold: 0.95, // Percentuale fine video per next episode
        pollInterval: 1000 // Intervallo polling in ms
    };

    // Carica impostazioni da storage
    let settings = GM_getValue('netflixSkipperSettings', DEFAULT_SETTINGS);

    // Funzione per salvare impostazioni
    function saveSettings() {
        GM_setValue('netflixSkipperSettings', settings);
        GM_notification({ text: 'Impostazioni salvate', title: 'Netflix Skipper', timeout: 3000 });
    }

    // Funzione di logging condizionale
    function log(message) {
        if (settings.logging) {
            console.log('[Netflix Advanced Skipper] ' + message);
        }
    }

    // Selettori multi-lingua e robusti (basati su data-uia e aria-label)
    const SELECTORS = {
        skipIntro: '[data-uia="player-skip-intro"], button[aria-label*="Skip Intro"], button[aria-label*="Salta intro"], button[aria-label*="Skip Opening"], button[aria-label*="Salta apertura"]',
        skipRecap: '[data-uia="player-skip-recap"], button[aria-label*="Skip Recap"], button[aria-label*="Salta riassunto"], button[aria-label*="Skip Previously On"], button[aria-label*="Salta precedentemente"]',
        stillWatching: '[data-uia="interrupt-autoplay-continue"], button[aria-label*="Continue Watching"], button[aria-label*="Continua a guardare"], button[data-uia="continue-watching"]',
        nextEpisode: '[data-uia="next-episode-seamless-button"], button[aria-label*="Next Episode"], button[aria-label*="Prossimo episodio"], .next-episode-button, [data-uia="next-episode-button"]'
    };

    // Flag per prevenire click multipli
    let flags = {
        skippedIntro: false,
        skippedRecap: false,
        continuedWatching: false,
        nextEpisode: false
    };

    // Reset flag dopo timeout
    function resetFlag(flagName, timeout = 5000) {
        setTimeout(() => { flags[flagName] = false; }, timeout);
    }

    // Funzione generica per cliccare un bottone se presente e abilitato
    function clickButton(selector, settingKey, flagName, message) {
        if (!settings[settingKey]) return;
        const btn = document.querySelector(selector);
        if (btn && !flags[flagName]) {
            btn.click();
            flags[flagName] = true;
            log(message);
            resetFlag(flagName);
            return true;
        }
        return false;
    }

    // Funzione per monitorare il player video
    function monitorPlayer() {
        const video = document.querySelector('video');
        if (!video) return;

        const currentTime = video.currentTime;
        const duration = video.duration;

        // Skip intro se entro threshold iniziale
        if (settings.skipIntro && currentTime < settings.introThreshold) {
            clickButton(SELECTORS.skipIntro, 'skipIntro', 'skippedIntro', 'Intro saltata');
        }

        // Skip recap (solitamente dopo intro)
        if (settings.skipRecap && currentTime > 10 && currentTime < 120) { // Assumi recap entro primi 2 min
            clickButton(SELECTORS.skipRecap, 'skipRecap', 'skippedRecap', 'Recap saltato');
        }

        // Skip still watching (modal overlay)
        clickButton(SELECTORS.stillWatching, 'skipStillWatching', 'continuedWatching', 'Continua a guardare cliccato');

        // Auto-next episode se vicino alla fine
        if (settings.autoNextEpisode && duration && (currentTime / duration > settings.endThreshold)) {
            clickButton(SELECTORS.nextEpisode, 'autoNextEpisode', 'nextEpisode', 'Passato all\'episodio successivo');
        }
    }

    // Observer per cambiamenti DOM (Netflix è una SPA)
    let observer;
    function initObserver() {
        if (observer) return;
        observer = new MutationObserver(() => {
            monitorPlayer();
        });
        observer.observe(document.body, { childList: true, subtree: true, attributes: true });
        log('Observer DOM avviato');
    }

    // Hook eventi video
    function hookVideoEvents() {
        document.addEventListener('play', monitorPlayer, true);
        document.addEventListener('timeupdate', monitorPlayer, true);
        document.addEventListener('ended', () => {
            if (settings.autoNextEpisode) {
                clickButton(SELECTORS.nextEpisode, 'autoNextEpisode', 'nextEpisode', 'Episodio finito: next cliccato');
            }
        }, true);
    }

    // Setup menu comandi per configurazione
    function setupMenu() {
        GM_registerMenuCommand('Toggle Skip Intro', () => {
            settings.skipIntro = !settings.skipIntro;
            saveSettings();
            log(`Skip Intro: ${settings.skipIntro ? 'ON' : 'OFF'}`);
        });

        GM_registerMenuCommand('Toggle Skip Recap', () => {
            settings.skipRecap = !settings.skipRecap;
            saveSettings();
            log(`Skip Recap: ${settings.skipRecap ? 'ON' : 'OFF'}`);
        });

        GM_registerMenuCommand('Toggle Skip Still Watching', () => {
            settings.skipStillWatching = !settings.skipStillWatching;
            saveSettings();
            log(`Skip Still Watching: ${settings.skipStillWatching ? 'ON' : 'OFF'}`);
        });

        GM_registerMenuCommand('Toggle Auto Next Episode', () => {
            settings.autoNextEpisode = !settings.autoNextEpisode;
            saveSettings();
            log(`Auto Next Episode: ${settings.autoNextEpisode ? 'ON' : 'OFF'}`);
        });

        GM_registerMenuCommand('Toggle Logging', () => {
            settings.logging = !settings.logging;
            saveSettings();
            log(`Logging: ${settings.logging ? 'ON' : 'OFF'}`);
        });

        GM_registerMenuCommand('Reset Impostazioni', () => {
            settings = { ...DEFAULT_SETTINGS };
            saveSettings();
            log('Impostazioni resettate');
        });
    }

    // Inizializzazione
    window.addEventListener('load', () => {
        setupMenu();
        initObserver();
        hookVideoEvents();
        setInterval(monitorPlayer, settings.pollInterval); // Polling fallback
        log('Script avviato con impostazioni: ' + JSON.stringify(settings));
    });

    // Cleanup
    window.addEventListener('beforeunload', () => {
        if (observer) observer.disconnect();
    });

})();