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