// ==UserScript== // @name Netflix Marathon (Pausable) // @name:zh-CN 网飞马拉松赛(可暫停) // @name:zh-TW 网飞马拉松赛(可暫停) // @name:ja Netflix Marathon(一時停止できます) // @name:ko Netflix Marathon (일시 중지 가능) // @name:ar ماراثون Netflix (يمكن إيقافه مؤقتًا) // @name:he מרתון נטפליקס (ניתן להשהות) // @name:hi नेटफ्लिक्स मैराथन (पॉज़ेबल) // @name:ru Netflix Марафон (можно приостановить) // @namespace https://github.com/aminomancer // @version 4.3.7 // @description A configurable userscript that automatically skips recaps, intros, credits, and ads, and clicks "next episode" prompts on Netflix and Amazon Prime Video. Customizable hotkey to pause/resume the auto-skipping functionality. // @description:zh-CN 一种可配置的用户脚本,该脚本会自动跳过介绍,字幕和广告,并单击网飞和亚马孙Prime Video上的“下一集”按钮。包括一个可自定义的热键,以暂停/恢复自动跳过功能。 // @description:zh-TW 一种可配置的用户脚本,该脚本会自动跳过介绍,字幕和广告,并单击网飞和亚马孙Prime Video上的“下一集”按钮。包括一个可自定义的热键,以暂停/恢复自动跳过功能。 // @description:ja イントロ、クレジット、広告を自動的にスキップし、NetflixとAmazon PrimeVideoの「次のエピソード」ボタンをクリックする構成可能なユーザースクリプト。自動スキップ機能を一時停止/再開するためのカスタマイズ可能なホットキーが含まれています。 // @description:ko 인트로, 크레딧 및 광고를 자동으로 건너 뛰고 Netflix 및 Amazon Prime Video에서 "다음 에피소드"버튼을 클릭하는 구성 가능한 사용자 스크립트입니다.자동 건너 뛰기 기능을 일시 중지 / 재개하기위한 사용자 지정 가능한 핫키가 포함되어 있습니다. // @description:ar جافا سكريبت قابل للتكوين يتخطى تلقائيًا المقدمات والاعتمادات والإعلانات وينقر على أزرار "الحلقة التالية" على Netflix و Amazon Prime Video.يتضمن مفتاح اختصار قابل للتخصيص لإيقاف / استئناف وظيفة التخطي التلقائي. // @description:he סקריפט משתמש הניתן להגדרה שדלג אוטומטית על מבוא, זיכויים ומודעות ולחץ על כפתורי "הפרק הבא" ב- Netflix וב- Amazon Prime Video.כולל מקש קיצור הניתן להתאמה אישית כדי להשהות / לחדש את הפונקציונליות של דילוג אוטומטי. // @description:hi एक कॉन्फ़िगर करने योग्य उपयोगकर्तास्क्रिप्ट जो स्वचालित रूप से इंट्रो, क्रेडिट और विज्ञापनों को बायपास करता है, और नेटफ्लिक्स और अमेज़ॅन प्राइम वीडियो पर "अगले एपिसोड" बटन पर क्लिक करता है। इसमें एक कॉन्फ़िगर करने योग्य हॉटकी शामिल है जो लंघन को रोक देता है या फिर से शुरू करता है। // @description:ru Настраиваемый сценарий, который автоматически пропускает вступления, титры и рекламу, а также нажимает кнопки «следующий выпуск» на Netflix и Amazon Prime Video.Включает настраиваемую горячую клавишу для приостановки / возобновления функции автоматического пропуска. // @author aminomancer // @homepageURL https://github.com/aminomancer/Netflix-Marathon-Pausable // @supportURL https://github.com/aminomancer/Netflix-Marathon-Pausable/issues // @icon data:image/svg+xml;utf8, // @include https://www.netflix.com/* // @include https://*.amazon.com/* // @include https://*.primevideo.com/* // @require http://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant GM_setValue // @grant GM_getValue // @grant GM.setValue // @grant GM.getValue // @downloadURL none // ==/UserScript== // You can customize the websites, hotkey, interval rate, and popup settings. ***Don't change the values below*** These are only the default settings. open netflix or amazon once so they'll initialize, and then in your userscript extension, go to the script's page and change the settings in the values/storage page. (e.g. in violentmonkey, at the top there's a code tab, settings, and values. click the values tab) This ensures that you keep your settings even if the script is updated. I don't recommend greasemonkey but if you need to use it for some reason, there is no UI to change stored settings, and I don't want to add a UI to such a simple script, so your only option is to edit the default options below. They will be reset when the script is updated though, so you will need to turn auto update off. const defaultOptions = { rate: 300, // integer: interval rate in milliseconds. (how often to check for the elements we want to click) increase if you're running this on a mega-potato? amazon: true, // boolean: whether to bother checking for amazon elements netflix: true, // boolean: whether to check for netflix elements hotkey: true, // boolean: whether to use a hotkey at all code: "F7", // string: physical key, e.g. KeyF for the F key. code, NOT keyCode. see the list here: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code/code_values ctrlKey: true, // boolean: modifier keys. if you don't want to use a modifier key, set these to false. if you want to use multiple, set them to true. don't delete these lines though. altKey: false, shiftKey: false, metaKey: false, pop: true, // boolean: whether to show pause/resume popups at all popDur: 3000, // integer: how long to leave the popup open for font: "Source Sans Pro", // string: font to use for the popup. if it's not locally installed on your PC, then it must be available on https://fonts.google.com/ and webfont must be true (see below) fontSize: "24px", // string: font size in pixels, followed by px, in quotes. fontWeight: "300", // string: font weight, in multiples of 100 between 100 and 900, surrounded by quotes. italic: false, // boolean: whether the font should be italic or not webfont: true, // boolean: whether to grab the font from google fonts }; const options = {}, // don't edit this. the script fills it with your stored settings. GMObj = typeof GM === "object" && GM !== null && typeof GM.getValue === "function", // ensure the GM object exists so we can use the right GM API functions GM4 = GMObj && GM.info.scriptHandler === "Greasemonkey" && GM.info.version >= 4, // check if the script handler is GM4, since if it is, we can't add a menu command site = test("netflix") ? "netflix" : "amazon"; let marathon = { count: 0, results: null, nDrain: "[data-uia='next-episode-seamless-button-draining']", nReady: "[data-uia='next-episode-seamless-button']", /** * getElementsByClassName * @param {string} s (class name) */ $c(s) { return document.getElementsByClassName(s); }, /** * getElementsByTagName * @param {string} s (tag name) */ $t(s) { return document.getElementsByTagName(s); }, /** * getElementById * @param {string} s (element id) */ $i(s) { return document.getElementById(s); }, /** * querySelector * @param {string} s (CSS selector e.g. ".class") */ $q(s) { return document.querySelector(s); }, /** * querySelectorAll * @param {string} s (CSS selector) */ $qa(s) { return document.querySelectorAll(s); }, /** * document.evaluate * @param {string} s (node's text content) * @param {string} n (node's tag name. if not passed, then accept any tag) * @param {string} p (node's parent's tag name. this is like saying button>div. if not passed, then just use div, ignoring the node's parent) */ $ev(s, n = "*", p) { let exp = p ? `//${p}/child::${n}[text()="${s}"]` : `//${n}[text()="${s}"]`; return document.evaluate( exp, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, this.results ).singleNodeValue; }, /** * :contains * @param {string} s (node's text content) */ $cnt(s) { return $(`div *:contains('${s}')`); }, /** * click every element with the given text content * @param {string} s (node's text content) */ $click(s) { let divs = this.$cnt(s); for (let i = 0; i < divs.length; i++) { if (divs[i].innerText == s) { divs[i].click(); this.count = 5; } } }, /** * find react property * @param {object} d (DOM node) */ findReact(d) { for (const k in d) { if (k.startsWith("__reactInternalInstance$")) { return d[k]?.child; } } return null; }, /** * get a node's react children * @param {string} s (CSS selector) */ getReact(s) { const el = this.$qa(s); return el.length > 0 ? this.findReact(el[0])?.memoizedProps.children : null; }, /** * determine if an element is visible (namely the amazon player) * @param {string} s (element id) */ isVis(s) { return this.$i(s)?.offsetParent ? true : false; }, // searches for elements that skip stuff. repeated every 300ms. change "rate" in the options if you want to make this more or less frequent. async amazon() { if (this.count === 0) { // console.log(this.count); if (this.isVis("dv-web-player")) { if (this.$c("atvwebplayersdk-nextupcard-button").length) { // console.log('Found Amazon video next.'); setTimeout(() => { this.$c("atvwebplayersdk-nextupcard-button")[0]?.click(); }, 700); this.count = 5; } else if (this.$c("atvwebplayersdk-skipelement-button").length) { this.$c("atvwebplayersdk-skipelement-button")[0]?.click(); this.count = 5; } else if (this.$c("adSkipButton").length) { // console.log('Found Amazon skip ad.'); this.$c("adSkipButton")[0].click(); this.count = 5; } else if (this.$c("skipElement").length) { // console.log('Found Amazon skip intro.'); this.$c("skipElement")[0].click(); this.count = 5; } else if (this.$ev("Skip Intro")) { // console.log('Found Amazon skip intro.'); this.$ev("Skip Intro").click(); this.count = 5; } else if (this.$cnt("Skip").length) { // amazon trailers this.$click("Skip"); this.count = 5; } else if (this.$cnt("Skip Intro").length) { // amazon intro this.$click("Skip Intro"); this.count = 5; } else if (this.$cnt("Skip Recap").length) { // amazon recap this.$click("Skip Recap"); this.count = 5; } else { // console.log('404 keep looking.'); } } } else { this.count--; } }, async netflix() { if (this.count === 0) { if (this.$c("skip-credits").length && this.$c("skip-credits-hidden").length == 0) { // console.log('Found credits.'); await sleep(200); this.$c("skip-credits")[0].firstElementChild.click(); await sleep(200); this.$q(".button-nfplayerPlay").click(); this.count = 80; // console.log('Found credits. +4s'); } else if (this.$q(this.nDrain)) { // console.log('Netflix next episode draining button skipped'); this.getReact(this.nDrain)._owner.memoizedProps.handlePress(); this.count = 5; } else if (this.$q(this.nReady)) { // console.log('Netflix next episode button skipped'); this.getReact( this.nReady ).props.children._owner.memoizedProps.onClickWatchNextEpisode(); this.count = 5; } else if (this.$c("postplay-still-container").length) { // console.log('Found autoplay.'); this.$c("postplay-still-container")[0].click(); this.count = 5; } else if (this.$c("WatchNext-still-container").length) { // console.log('Found autoplay.'); this.$c("WatchNext-still-container")[0].click(); this.count = 5; } else { // console.log('404 keep looking.'); } } else { this.count--; } }, }; /** * pause execution for ms milliseconds * @param {int} ms (milliseconds) */ function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * @param {string} u (a string to test the URL against) */ function test(u) { return window.location.href.includes(u); } // an interval constructor that you can pause and resume, and which opens a brief popup when you do so. class PauseUtil { /** * pausable interval utility * @param {func} callback (the stuff you want to execute periodically, in this case marathon.netflix or marathon.amazon) * @param {int} int (how often to repeat the callback) */ constructor(callback, int) { this.callback = callback; this.int = int; this.popup = options.pop ? document.createElement("div") : null; // if popup is disabled, create nothing this.text = options.pop ? document.createTextNode("Marathon: Paused") : null; // if popup is disabled, create nothing this.remainder = 0; // how much time is remaining on the interval when we pause it this.fading; // 3 second timeout (by default), after which the popup fades this.pauseState = 0; // 0: idle, 1: running, 2: paused, 3: resumed this.register("Pause Marathon", true); // initial creation of the menu command // if popup is enabled in options, style it if (options.pop) { document.body.insertBefore(this.popup, document.body.firstElementChild); this.popup.appendChild(this.text); this.popup.style.cssText = `position:fixed;top:50%;right:3%;transform:translateY(-50%);z-index:2147483646;background:hsla(0, 0%, 8%, 0.7);color:hsla(0, 0%, 97%, 0.95);max-width:-moz-fit-content;padding:17px 19px;border-radius:5px;pointer-events:none;letter-spacing:1px;transition:opacity 0.2s ease-in-out;opacity:0;`; this.popup.style.fontFamily = options.font; this.popup.style.fontSize = options.fontSize; this.popup.style.fontWeight = options.fontWeight; this.popup.style.fontStyle = options.italic ? "italic" : ""; } this.time = new Date(); this.timer = window.setInterval(this.callback, this.int); this.pauseState = 1; } // returns false if we're on a valid site but not actually in the video player (e.g. we're only browsing videos). get playing() { return site === "netflix" ? test("netflix.com/watch/") : marathon.isVis("dv-web-player"); } // pause the interval pause() { if (this.pauseState !== 1) { return; } this.remainder = this.int - (new Date() - this.time); window.clearInterval(this.timer); this.pauseState = 2; this.register("Resume Marathon"); // update the menu command label this.openPopup(false); } // resume the interval async resume() { if (this.pauseState !== 2) { return; } this.pauseState = 3; this.register("Pause Marathon"); this.openPopup(true); await sleep(this.remainder); this.run(); } // when we pause, there's usually still time left on the interval. resume() calls this after waiting for the remaining duration. so this is what actually resumes the interval. run() { if (this.pauseState !== 3) { return; } this.callback(); this.time = new Date(); this.timer = window.setInterval(this.callback, this.int); this.pauseState = 1; } // toggle the interval on/off. toggle() { switch (this.pauseState) { case 1: return this.pause(); case 2: return this.resume(); default: return; } } /** * opens the popup and schedules it to close * @param {bool} state (whether the popup should say "Resumed" or "Paused") */ openPopup(state) { // if popup is disabled in options, do nothing if (!options.pop) { return; } // if window is netflix or amazon but there's no video player, (e.g. we're browsing titles) do nothing but ensure the popup is hidden. if (!this.playing) { this.popup.style.transitionDuration = "1s"; return (this.popup.style.opacity = "0"); } let string = state ? "Resumed" : "Paused"; this.popup.textContent = `Marathon: ${string}`; this.popup.style.transitionDuration = "0.2s"; this.popup.style.opacity = "1"; window.clearTimeout(this.fading); // clear any existing timeout since we're about to set a new one // schedule the popup to fade into oblivion this.fading = window.setTimeout(() => { this.popup.style.transitionDuration = "1s"; this.popup.style.opacity = "0"; }, options.popDur); } /** * register or change the label of the menu command * @param {string} cap (intended caption to display on the menu command) * @param {bool} firstRun (we call this function at startup and every time we pause/unpause. we don't need to register a menu command if this is the startup call, since none exists yet) */ register(cap, firstRun = false) { if (GM4) { return; // don't register a menu command if the script manager is greasemonkey 4.0+ since the function doesn't exist } if (!firstRun) { GM_unregisterMenuCommand(this.caption); } GM_registerMenuCommand(cap, this.toggle.bind(this)); this.caption = cap; } } // initial setup function marathonSetUp() { if (!options[site]) { return; // if the site we're on is disabled in options, then don't bother setting up } let search = marathon[site].bind(marathon), // use the correct callback searchInterval = new PauseUtil(search, options.rate), // create the interval with our rate setting wf = options.webfont ? document.createElement("script") : null, first = document.scripts[0], ital = options.italic ? "ital," : ""; /** * what to do when you press the hotkey. * @param {object} e (event) */ function onKeyDown(e) { if (e.code == options.code && modTest(e)) { e.stopPropagation(); searchInterval.toggle(); e.preventDefault(); } } /** * check that the modifier keys match those defined in user settings * @param {object} e (event) */ function modTest(e) { let ctrl = options.ctrlKey, alt = options.altKey, shift = options.shiftKey, meta = options.metaKey; return e.ctrlKey == ctrl && e.altKey == alt && e.shiftKey == shift && e.metaKey == meta; } // start listening to key events function startCapturing() { window.addEventListener("keydown", onKeyDown, true); } // stop listening to key events (currently unused) function stopCapturing() { window.removeEventListener("keydown", onKeyDown, true); } WebFontConfig = { classes: false, // don't bother changing the DOM at all, we aren't listening for it events: false, // no need for events, not worth the execution google: { families: [`${options.font}:${ital}wght@1,${options.fontWeight}`], // e.g. "Source Sans Pro:wght@1,300" or "Lobster Two:ital,wght@1,700" display: "swap", // not really necessary since the popup doesn't appear until you press a button. but whatever }, }; // load web font if enabled if (options.webfont) { wf.src = "https://cdn.jsdelivr.net/npm/webfontloader@latest/webfontloader.js"; wf.async = true; first.parentNode.insertBefore(wf, first); } // if hotkey is enabled in options, start listening to keyboard events if (options.hotkey) { startCapturing(); } return { searchInterval, startCapturing, stopCapturing, }; } // get settings from *monkey storage, and if any are missing, set them to defaults. then create properties in options (the js object) based on the stored settings. async function settings() { // use the correct get/set functions for user's script handler let getVal = GMObj ? GM.getValue : GM_getValue, setVal = GMObj ? GM.setValue : GM_setValue; // for each key, either get or set for (const key in defaultOptions) { let stored = await getVal(`${key}`); if (stored != undefined) { options[key] = stored; } else { await setVal(`${key}`, defaultOptions[key]); options[key] = defaultOptions[key]; } } } async function start() { await settings(); marathonSetUp(); } start();