// ==UserScript== // @name Disney Plus Enchantments // @namespace http://tampermonkey.net/ // @version 0.6.2 // @description Enhancements for Disney Plus video player: auto fullscreen, skip intro, skip credits, and more. // @author JJJ // @match https://www.disneyplus.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=disneyplus.com // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @license MIT // @downloadURL none // ==/UserScript== (function () { 'use strict'; const CONFIG = { enableAutoFullscreen: GM_getValue('enableAutoFullscreen', true), enableSkipIntro: GM_getValue('enableSkipIntro', true), enableAutoPlayNext: GM_getValue('enableAutoPlayNext', false) }; const SELECTORS = { skipIntroButton: 'button.skip__button:not([class*="overlay_upnextlite"])', autoPlayButton: 'button, *[data-testid="up-next-play-button"]', fullscreenButton: 'button.fullscreen-icon' }; const CONSTANTS = { CLICK_DELAY: 5000, BUTTON_TRACKING_TIMEOUT: 30000 // Polling settings for dynamic appearance of up-next button }; const AUTOPLAY_POLL = { INTERVAL_MS: 500, MAX_RETRIES: 20 }; let lastSkipClickTime = 0; const clickedButtons = new Set(); let autoPlayPollTimer = null; let autoPlayPollRetries = 0; function createSettingsDialog() { const dialogHTML = `

Disney Plus Enchantments

${createToggle('enableAutoFullscreen', 'Auto Fullscreen', 'Automatically enter fullscreen mode')} ${createToggle('enableSkipIntro', 'Skip Intro', 'Automatically skip the intro of episodes')} ${createToggle('enableAutoPlayNext', 'Auto Play Next Episode', 'Automatically play the next episode')}
`; const styleSheet = ` `; const dialogWrapper = document.createElement('div'); dialogWrapper.innerHTML = styleSheet + dialogHTML; document.body.appendChild(dialogWrapper); document.getElementById('saveSettingsButton').addEventListener('click', saveAndCloseDialog); document.getElementById('cancelSettingsButton').addEventListener('click', closeDialog); } function createToggle(id, label, title) { return `
`; } function saveAndCloseDialog() { Object.keys(CONFIG).forEach(key => { CONFIG[key] = document.getElementById(key).checked; GM_setValue(key, CONFIG[key]); }); closeDialog(); } function closeDialog() { const dialog = document.getElementById('disneyPlusEnchantmentsDialog'); if (dialog) { dialog.remove(); if (document.fullscreenElement) { document.exitFullscreen(); } } } function isElementVisible(element) { if (!element) return false; const rect = element.getBoundingClientRect(); return ( element.offsetParent !== null && rect.width > 0 && rect.height > 0 && rect.top >= 0 && rect.left >= 0 ); } function findAutoPlayButton() { const candidates = Array.from(document.querySelectorAll(SELECTORS.autoPlayButton)); // common class-name patterns observed on Disney+ up-next buttons (use regex to catch variants) const classPatterns = [ /r3t2ih[\w-]*/i, /_14aj777[\w-]*/i, /_8mbuv9[\w-]*/i, /fl2b6o4/i, /_1055dze3/i, /overlay_upnextlite/i ]; for (const el of candidates) { if (!isElementVisible(el)) continue; // prefer class-based detection on the element itself or its ancestors let classMatch = false; let node = el; for (let depth = 0; node && depth < 4; depth++, node = node.parentElement) { const className = (node.className || '').toString(); if (!className) continue; if (classPatterns.some(rx => rx.test(className))) { classMatch = true; break; } } if (classMatch) return el; // fallback: use aria/text or data-testid if no class match const aria = (el.getAttribute && (el.getAttribute('aria-label') || '')).toLowerCase(); const text = (el.textContent || '').toLowerCase(); if ( aria.includes('próximo') || aria.includes('proximo') || aria.includes('next') || text.includes('ver próximo') || text.includes('ver proximo') || text.includes('next') || (el.dataset && el.dataset.testid === 'up-next-play-button') ) { return el; } } return null; } function clickButton(selector) { if (selector === SELECTORS.autoPlayButton) { const button = findAutoPlayButton(); if (button) button.click(); return; } const button = document.querySelector(selector); if (button && isElementVisible(button)) { if (selector === SELECTORS.skipIntroButton) { handleSkipIntroButton(button); } } } function handleSkipIntroButton(button) { const currentTime = Date.now(); if (currentTime - lastSkipClickTime < CONSTANTS.CLICK_DELAY) return; const buttonText = button.textContent.toLowerCase(); if (isValidSkipButton(buttonText) && !clickedButtons.has(buttonText)) { button.click(); lastSkipClickTime = currentTime; clickedButtons.add(buttonText); setTimeout(() => clickedButtons.delete(buttonText), CONSTANTS.BUTTON_TRACKING_TIMEOUT); } } function isValidSkipButton(buttonText) { return (buttonText.includes('skip') || buttonText.includes('saltar')) && !buttonText.includes('next') && !buttonText.includes('próximo'); } function enterFullscreen() { if (!document.fullscreenElement) { document.documentElement.requestFullscreen(); } } function exitFullscreen() { if (document.fullscreenElement) { document.exitFullscreen(); } } function maintainFullscreen() { const fullscreenButton = document.querySelector(SELECTORS.fullscreenButton); if (fullscreenButton && !document.fullscreenElement) { fullscreenButton.click(); } } function attemptAutoPlay() { // If disabled, abort if (!CONFIG.enableAutoPlayNext) return; // If a poll is already running, don't start another if (autoPlayPollTimer) return; const tryClick = () => { const btn = findAutoPlayButton(); if (btn) { btn.click(); clearInterval(autoPlayPollTimer); autoPlayPollTimer = null; autoPlayPollRetries = 0; return; } autoPlayPollRetries++; if (autoPlayPollRetries >= AUTOPLAY_POLL.MAX_RETRIES) { clearInterval(autoPlayPollTimer); autoPlayPollTimer = null; autoPlayPollRetries = 0; } }; // Try immediate first, then schedule polling tryClick(); if (!autoPlayPollTimer && !findAutoPlayButton()) { autoPlayPollTimer = setInterval(tryClick, AUTOPLAY_POLL.INTERVAL_MS); } } function handleEnhancements() { try { if (CONFIG.enableAutoFullscreen) { enterFullscreen(); maintainFullscreen(); } if (CONFIG.enableSkipIntro) { clickButton(SELECTORS.skipIntroButton); } // use attemptAutoPlay so we retry until the dynamic button appears if (CONFIG.enableAutoPlayNext) { attemptAutoPlay(); } } catch (error) { console.error('Disney Plus Enchantments error:', error); } } // Detect SPA navigation (pushState/replaceState/popstate) and re-run enhancements (function patchHistoryEvents() { const wrap = (orig) => function () { const ret = orig.apply(this, arguments); handleEnhancements(); return ret; }; if (history.pushState) history.pushState = wrap(history.pushState); if (history.replaceState) history.replaceState = wrap(history.replaceState); window.addEventListener('popstate', handleEnhancements); })(); const observer = new MutationObserver(handleEnhancements); observer.observe(document.body, { childList: true, subtree: true }); GM_registerMenuCommand('Disney Plus Enchantments Settings', createSettingsDialog); let isSettingsDialogOpen = false; function toggleSettingsDialog() { if (isSettingsDialogOpen) { closeDialog(); isSettingsDialogOpen = false; } else { createSettingsDialog(); isSettingsDialogOpen = true; } } document.addEventListener('keyup', (event) => { if (event.key === 'F2') { toggleSettingsDialog(); } else if (event.key === 'Escape') { exitFullscreen(); closeDialog(); } }); })();