Skip Netflix Intro

Automatically press "Skip Intro" button on Netflix

// ==UserScript==
// @name         Skip Netflix Intro
// @namespace    http://tampermonkey.net/
// @version      0.7
// @description  Automatically press "Skip Intro" button on Netflix
// @author       You
// @match        https://www.netflix.com/*
// @grant        GM.setValue
// @grant        GM.getValue
// ==/UserScript==

"use strict";

class NetSkip {
    constructor(skipList) {
        this.observer = new MutationObserver(this.onMutations.bind(this));
        this.lastSkip = 0;
        this.skipList = skipList;
    }
    onMutations(mutations) {
        let skipButton = null;
        let videoContainer = null;
        let titleContainer = null;
        let video = null;
        for (const mutation of mutations) {
            if (videoContainer = this.isPlayerChange(mutation)) {
                const videoDetails = videoContainer.querySelector(".video-title div");
                const video = videoContainer.querySelector("video");
                if (videoDetails)
                    this.episode = this.getEpisodeDetails(videoDetails);
                if (video)
                    this.updatePlayer(video);
            }
            else if (video = this.isVideoChange(mutation)) {
                this.updatePlayer(video);
            }
            else if (titleContainer = this.isTitleChange(mutation)) {
                this.episode = this.getEpisodeDetails(titleContainer);
            }
            else if (skipButton = this.isSkip(mutation)) {
                if (this.shouldSkip()) {
                    this.lastSkip = Date.now();
                    skipButton.click();
                }
                else {
                    this.rememberSkip();
                }
            }
            else if (skipButton = this.isNextButton(mutation) || this.isNextEpisode(mutation)) {
                if (this.shouldSkip()) {
                    if (!skipButton)
                        return;
                    this.lastSkip = Date.now();
                    skipButton.click();
                }
            }
        }
    }
    //** utility methods to help me find specific mutations */
    getSelector(el) {
        const classes = Array.from(el.classList);
        return el.tagName.toLowerCase()
            + (classes.length ? "." + classes.join(".") : "")
            + (el.id.length ? "#" + el.id : "");
    }
    findInjection(mutation, selector) {
        const target = mutation.target;
        if (target.matches(selector)) {
            if (mutation.type === "attributes") {
                console.log(`%c[${selector}].${mutation.attributeName}` +
                    ` = %c"${mutation.oldValue}"` +
                    ` -> %c"${target.getAttribute(mutation.attributeName || "")}"`, 'font-weight: bold', 'font-weight: normal; color: red;', 'color: green;');
            }
            else {
                console.log(`%c[${selector}] >`, 'font-weight: bold;', mutation);
            }
        }
        else if (mutation.type === "childList") {
            for (let node of mutation.addedNodes) {
                if (node.nodeType !== Node.ELEMENT_NODE)
                    continue;
                if (node.matches(selector)) {
                    console.log(`<${this.getSelector(target)}>.addedNodes = [...,` +
                        `%c<${selector}>%c, ...]`, 'color: green;', mutation);
                }
                else if (node.querySelector(selector)) {
                    console.log(`<${this.getSelector(target)}>.addedNodes = [...,` +
                        `%c<${this.getSelector(node)}>%c.querySelector("%c${selector}"%c),...]`, 'color: green;', '', 'color: green; font-weight: bold;', mutation);
                }
            }
            for (let node of mutation.removedNodes) {
                if (node.nodeType !== Node.ELEMENT_NODE)
                    continue;
                if (node.matches(selector)) {
                    console.log(`<${this.getSelector(target)}>.removedNodes = [..., <%c${selector}%c>, ...]`, 'color: red');
                }
                else if (node.querySelector(selector)) {
                    console.log(`<${this.getSelector(target)}>.removedNodes = [..., ` +
                        `%c<${this.getSelector(node)}>.querySelector("${selector}"), ...] `, 'color: red');
                }
            }
        }
    }
    //** end of utility functions */
    isTitleChange(mutation) {
        const target = mutation.target;
        if (mutation.type === "childList"
            && target.className === "ellipsize-text"
            && target.parentElement
            && target.parentElement.classList.contains("video-title"))
            return target;
    }
    isVideoChange(mutation) {
        if (mutation.type !== "childList")
            return;
        const target = mutation.target;
        if (target.className === "VideoContainer") {
            return target.querySelector("video");
        }
    }
    isPlayerChange(mutation) {
        if (mutation.type !== "childList")
            return;
        const target = mutation.target;
        if (target.className === "sizing-wrapper") {
            for (const node of mutation.addedNodes) {
                if (node.className === "nfp AkiraPlayer") {
                    return node;
                }
            }
        }
    }
    isNextEpisode(mutation) {
        if (mutation.type !== "childList")
            return;
        const target = mutation.target;
        if (target.className === "nfp AkiraPlayer") {
            for (const node of mutation.addedNodes) {
                if (node.nodeType !== Node.ELEMENT_NODE)
                    continue;
                if (node.classList.contains("ptrack-container")) {
                    return node.querySelector(".WatchNext-still-container");
                }
            }
        }
    }
    isNextButton(mutation) {
        if (mutation.type !== "childList")
            return;
        const target = mutation.target;
        if (target.classList.contains("PlayerControlsNeo__all-controls")) {
            for (const node of mutation.addedNodes) {
                if (node.nodeType !== Node.ELEMENT_NODE)
                    continue;
                if (node.classList.contains("main-hitzone-element-container")) {
                    return node.querySelector("[data-uia=next-episode-seamless-button]");
                }
            }
        }
    }
    isSkip(mutation) {
        if (mutation.type === "attributes" && this.classXor(mutation.target, "skip-credits", "skip-credits-hidden")) {
            return mutation.target.querySelector("a");
        }
        else if (mutation.type === "childList") {
            for (const node of mutation.addedNodes) {
                if (node.nodeType !== Node.ELEMENT_NODE)
                    continue;
                if (this.classXor(node, "skip-credits", "skip-credits-hidden")) {
                    return node.querySelector("a");
                }
            }
        }
    }
    updatePlayer(video) {
        if (!video.dataset.skipEventAdded) {
            // console.log('NetSkip: Adding event listener to', video);
            video.addEventListener("pause", () => {
                const lastSkipped = Date.now() - this.lastSkip;
                // console.log("NetSkip: Detected pause")
                if (lastSkipped < 2000) {
                    // console.log("NetSkip: Attempting playback resume");
                    const playButton = document.querySelector(".button-nfplayerPlay");
                    const int = window.setInterval(() => playButton ? playButton.click() : video.play(), 400);
                    this.resumedPlayback(video).then(() => window.clearInterval(int)); //.then(() => console.log("NetSkip: Detected resumed playback"))
                }
            });
            video.dataset.skipEventAdded = "true";
            video.dispatchEvent(new Event("pause"));
        }
    }
    resumedPlayback(video) {
        return new Promise(resolve => {
            const playing = () => {
                video.removeEventListener("playing", playing);
                resolve();
            };
            video.addEventListener("playing", playing);
        });
    }
    getItem(name, season) {
        return this.skipList.findIndex(show => show.name === name && show.season === season);
    }
    shouldSkip() {
        if (!this.episode)
            return false;
        return this.getItem(this.episode.name, this.episode.season) !== -1;
    }
    rememberSkip() {
        if (!this.episode)
            return;
        this.skipList.push({ name: this.episode.name, season: this.episode.season });
        GM.setValue("skipList", JSON.stringify(this.skipList));
    }
    getEpisodeDetails(playerLabel) {
        if (!playerLabel) {
            console.error("Trouble getting episode label");
            return;
        }
        const [nameEl, episodeEl, episodeNameEl] = playerLabel.children;
        if (!nameEl.textContent || !episodeEl.textContent || !episodeNameEl.textContent) {
            console.error("Trouble getting episode details");
            return;
        }
        const [season, episode] = episodeEl.textContent.split(":").map(i => ~~i.substring(1)); // converts "S2:E6" to [2, 6]
        return {
            name: nameEl.textContent.trim(),
            season: season,
            episode: episode,
            episodeTitle: episodeNameEl.textContent.trim()
        };
    }
    classXor(node, requiredClass, absentClass) {
        return node.classList.contains(requiredClass) && !node.classList.contains(absentClass);
    }
    start() {
        this.observer.observe(document.body, {
            attributes: true,
            subtree: true,
            childList: true,
            attributeFilter: ["class"]
        });
    }
}
GM.getValue("skipList", "[]")
    .then((list) => list ? JSON.parse(list) : [])
    .then((list) => {
    const netflix = new NetSkip(list);
    netflix.start();
    console.log(netflix);
});