From a87ce6813e23988d39c94c36a8e7168c7f1d20ac Mon Sep 17 00:00:00 2001 From: giulivno <104376169+giulivno@users.noreply.github.com> Date: Mon, 17 Feb 2025 15:49:36 -0500 Subject: [PATCH 1/2] Implemented "Prevent playback looping of youtube shorts video" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⚬ PROBLEM: YouTube Shorts loop indefinitely, making it hard to stop playback. ⚬ SOLUTION: Detects when a Shorts video restarts and pauses it at the last frame to prevent looping. ⚬ RELEVANCE / SCOPE: Affects only Shorts videos. Doesn’t interfere with regular videos or ads. Works dynamically via MutationObserver. ⚬ "SIDE EFFECTS": May pause unintended seeks. Requires updates if YouTube changes behavior. Automatically toggled, menu toggle does not function properly (player settings) ⚬ CONTEXT: Uses chrome.storage.local.get, matching other features. Listens for setting changes dynamically. Integrates smoothly into ImprovedTube. --- js&css/web-accessible/core.js | 5 +- js&css/web-accessible/init.js | 12 + .../web-accessible/www.youtube.com/player.js | 251 +++++++++++++++++- .../www.youtube.com/settings.js | 16 ++ menu/skeleton-parts/general.js | 20 ++ menu/skeleton-parts/player.js | 13 + 6 files changed, 304 insertions(+), 13 deletions(-) diff --git a/js&css/web-accessible/core.js b/js&css/web-accessible/core.js index 36eb49f8a..42a060a9d 100644 --- a/js&css/web-accessible/core.js +++ b/js&css/web-accessible/core.js @@ -242,7 +242,10 @@ document.addEventListener('it-message-from-extension', function () { ImprovedTube.elements.player.querySelector('video').playbackRate = 1; } break - + + case 'preventShortsLooping': + ImprovedTube.preventShortsLooping(); + break; case 'theme': case 'themePrimaryColor': case 'themeTextColor': diff --git a/js&css/web-accessible/init.js b/js&css/web-accessible/init.js index f94dd692e..0b970cf0c 100644 --- a/js&css/web-accessible/init.js +++ b/js&css/web-accessible/init.js @@ -96,6 +96,13 @@ ImprovedTube.init = function () { this.YouTubeExperiments(); this.channelCompactTheme(); + + let preventShortsLooping = localStorage.getItem("prevent_shorts_looping") === "true"; + if (preventShortsLooping) { + console.log("Prevent Shorts Looping is active!"); + ImprovedTube.preventShortsLooping(); + } + if (ImprovedTube.elements.player && ImprovedTube.elements.player.setPlaybackRate) { ImprovedTube.videoPageUpdate(); ImprovedTube.initPlayer(); @@ -147,6 +154,11 @@ document.addEventListener('yt-navigate-finish', function () { } else if (document.documentElement.dataset.pageType === 'channel') { ImprovedTube.channelPlayAllButton(); } + + let preventShortsLooping = localStorage.getItem("prevent_shorts_looping") === "true"; + if (preventShortsLooping) { + ImprovedTube.preventShortsLooping(); + } }); window.addEventListener('load', function () { diff --git a/js&css/web-accessible/www.youtube.com/player.js b/js&css/web-accessible/www.youtube.com/player.js index 8491f7fe4..01c4ccae1 100644 --- a/js&css/web-accessible/www.youtube.com/player.js +++ b/js&css/web-accessible/www.youtube.com/player.js @@ -706,16 +706,19 @@ REPEAT ImprovedTube.playerRepeat = function () { setTimeout(function () { if (!/ad-showing/.test(ImprovedTube.elements.player.className)) { - ImprovedTube.elements.video.setAttribute('loop', ''); + // Prevent looping for Shorts videos + if (!location.href.includes("shorts/")) { + ImprovedTube.elements.video.setAttribute('loop', ''); + } } - //ImprovedTube.elements.buttons['it-repeat-styles'].style.opacity = '1'; //old class from version 3.x? that both repeat buttons could have - }, 200); -} + }, 200); +}; + /*------------------------------------------------------------------------------ REPEAT BUTTON ------------------------------------------------------------------------------*/ ImprovedTube.playerRepeatButton = function () { - if (this.storage.player_repeat_button === true) { + if (this.storage.player_repeat_button === true && !location.href.includes("shorts/")) { var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'), path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); svg.setAttributeNS(null, 'viewBox', '0 0 24 24'); @@ -1517,10 +1520,234 @@ ImprovedTube.pauseWhileTypingOnYoutube = function () { /*------------------------------------------------------------------------------ HIDE PROGRESS BAR PREVIEW ------------------------------------------------------------------------------*/ -ImprovedTube.playerHideProgressPreview = function () { - if (this.storage.player_hide_progress_preview === true) { - document.documentElement.setAttribute('it-hide-progress-preview', 'true'); - } else { - document.documentElement.removeAttribute('it-hide-progress-preview'); - } -}; + + + +/*------------------------------------------------------------------------------ +SHORTS LOOP PREVENTION & AD DETECTION +Prevent Shorts from looping indefinitely. +------------------------------------------------------------------------------*/ +(function () { + console.log("Initializing Shorts Loop Prevention"); + + let observer = null; + let lastVideoId = ""; + let currentVideo = null; + let preloadedVideos = new Map(); + let videoStartTime = new WeakMap(); + let userInteractedVideos = new WeakSet(); + let videoWasPausedByScript = new WeakSet(); + + function getShortsVideoElement() { + return document.querySelector("#shorts-player video"); + } + + function resetVideoListeners(video) { + if (!video) return; + console.log("Resetting event listeners for previous video"); + video.onended = null; + video.onseeked = null; + video.removeEventListener("timeupdate", handleTimeUpdate); + video.removeEventListener("play", trackUserPlay); + video.removeEventListener("pause", trackPauseEvent); + videoStartTime.delete(video); + userInteractedVideos.delete(video); + videoWasPausedByScript.delete(video); + } + + function handleTimeUpdate(event) { + let video = event.target; + + if (!userInteractedVideos.has(video) || video.currentTime < 1) { + return; + } + + if (video.currentTime < 0.5 && !video.paused && !videoWasPausedByScript.has(video)) { + console.log("Detected auto-restart! Checking if stopping is safe."); + if (!isNaN(video.duration) && video.duration !== Infinity && video.duration > 0) { + console.log("Stopping video."); + video.pause(); + video.currentTime = video.duration - 0.01; + videoWasPausedByScript.add(video); + } else { + console.warn("Skipped stopping video due to invalid duration:", video.duration); + } + } + } + + function trackUserPlay(event) { + let video = event.target; + console.log("User interacted. Allowing natural playback."); + userInteractedVideos.add(video); + video.removeEventListener("play", trackUserPlay); + } + + function trackPauseEvent(event) { + let video = event.target; + if (!videoWasPausedByScript.has(video)) { + console.log("User paused manually. Ignoring."); + videoWasPausedByScript.add(video); + } + } + + function preventShortsLoop() { + let video = getShortsVideoElement(); + if (!video) return; + + let videoId = new URLSearchParams(window.location.search).get("v") || document.location.href; + + if (preloadedVideos.has(video.src)) { + let preloadedData = preloadedVideos.get(video.src); + if (preloadedData.isAd) { + console.log("Ad detected from preload."); + resetVideoListeners(video); + return; + } + } + + if (videoId === lastVideoId) { + console.log("Already processed this video. Skipping."); + return; + } + + console.log("New Shorts video detected! Applying loop prevention."); + lastVideoId = videoId; + + resetVideoListeners(currentVideo); + currentVideo = video; + + videoStartTime.set(video, Date.now()); + + function handleLoadedMetadata() { + console.log("Video metadata loaded!"); + + video.onended = function () { + if (!this.paused) { + console.log("Video ended, pausing."); + this.pause(); + } + }; + + video.onseeked = function () { + let startTime = videoStartTime.get(video) || 0; + let timeElapsed = (Date.now() - startTime) / 1000; + + if (timeElapsed < 2 || videoWasPausedByScript.has(video)) { + console.log("Ignoring seek event (video just started or script-paused)"); + return; + } + + console.log("Blocked seek event!"); + this.pause(); + }; + + video.addEventListener("timeupdate", handleTimeUpdate); + video.addEventListener("pause", trackPauseEvent); + } + + video.addEventListener("play", trackUserPlay, { once: true }); + + if (video.readyState >= 2) { + handleLoadedMetadata(); + } else { + video.addEventListener("loadeddata", handleLoadedMetadata, { once: true }); + } + + preloadNextVideos(); + } + + function preloadNextVideos() { + console.log("Preloading metadata for upcoming Shorts..."); + let videos = document.querySelectorAll("#shorts-player video"); + + videos.forEach(video => { + if (!preloadedVideos.has(video.src)) { + video.addEventListener("loadedmetadata", function () { + let isAd = detectAdDuringPreload(video); + preloadedVideos.set(video.src, { duration: video.duration, isAd: isAd }); + + console.log(`Preloaded video: ${video.src}`); + console.log(` - Duration: ${video.duration}s`); + console.log(` - Detected as Ad? ${isAd}`); + }, { once: true }); + } + }); + } + + function detectAdDuringPreload(video) { + if (!video) return false; + + const player = video.closest('.html5-video-player') || video.closest('#movie_player'); + if (player && player.classList.contains('ad-showing')) { + console.warn("Ad detected during preload (ad-showing class)."); + return true; + } + + if (video.closest("ytd-ad-slot-renderer, ytd-in-feed-ad-layout-renderer")) { + console.warn("Shorts ad detected during preload (Ad container found)."); + return true; + } + + let adBadge = document.querySelector(".badge-shape-wiz_text"); + if (adBadge && adBadge.innerText.includes("Sponsored")) { + console.warn("Sponsored Shorts detected during preload."); + return true; + } + + let skipButton = document.querySelector('.ytp-ad-skip-button-modern.ytp-button,[class*="ytp-ad-skip-button"].ytp-button'); + if (skipButton) { + console.warn("Skippable Ad detected during preload."); + return true; + } + + if (isNaN(video.duration) || video.duration === Infinity || video.duration < 3) { + console.warn("Possible ad detected during preload (Invalid duration)."); + return true; + } + + return false; + } + + function observeShortsChanges() { + const shortsContainer = document.querySelector("#shorts-player"); + if (!shortsContainer) { + console.warn("Shorts container not found. Retrying..."); + setTimeout(observeShortsChanges, 1000); + return; + } + + if (observer) observer.disconnect(); + + observer = new MutationObserver(() => { + let video = getShortsVideoElement(); + if (video && video !== currentVideo) { + console.log("Shorts video changed! Reapplying loop prevention."); + preventShortsLoop(); + } + }); + + observer.observe(shortsContainer, { childList: true, subtree: false }); + } + + document.addEventListener("DOMContentLoaded", preventShortsLoop); + document.addEventListener("yt-navigate-finish", preventShortsLoop); +})(); + + + + + + + + + + + + + + + + + + + diff --git a/js&css/web-accessible/www.youtube.com/settings.js b/js&css/web-accessible/www.youtube.com/settings.js index 7771cfb3e..4b3bee731 100644 --- a/js&css/web-accessible/www.youtube.com/settings.js +++ b/js&css/web-accessible/www.youtube.com/settings.js @@ -180,3 +180,19 @@ ImprovedTube.youtubeLanguage = function () { } } }; + +/*------------------------------------------------------------------------------ +PREVENT SHORTS LOOPING +------------------------------------------------------------------------------*/ + +ImprovedTube.preventShortsLooping = function () { + let value = this.storage.prevent_shorts_looping; + + if (value === true) { + console.log("Prevent Shorts Looping: Enabled"); + localStorage.setItem("prevent_shorts_looping", "true"); + } else { + console.log("Prevent Shorts Looping: Disabled"); + localStorage.setItem("prevent_shorts_looping", "false"); + } +}; diff --git a/menu/skeleton-parts/general.js b/menu/skeleton-parts/general.js index 1fcd58e4a..f182975f7 100644 --- a/menu/skeleton-parts/general.js +++ b/menu/skeleton-parts/general.js @@ -11,6 +11,26 @@ extension.skeleton.main.layers.section.general = { section_1: { component: 'section', variant: 'card', + prevent_shorts_looping: { + component: 'switch', + text: 'Prevent Shorts Looping', + storage: 'prevent_shorts_looping', + tags: 'shorts looping auto-play', + + on: { + change: function (event) { + const isEnabled = event.target.checked; + console.log("🔄 Prevent Shorts Looping is now " + (isEnabled ? "ENABLED" : "DISABLED")); + + + ImprovedTube.storage.prevent_shorts_looping = isEnabled; + + if (isEnabled) { + preventShortsLoop(); + } + } + } + }, improvedtube_youtube_icon: { text: 'improvedtubeIconOnYoutube', component: 'select', diff --git a/menu/skeleton-parts/player.js b/menu/skeleton-parts/player.js index 5eac79849..2cb0846cc 100644 --- a/menu/skeleton-parts/player.js +++ b/menu/skeleton-parts/player.js @@ -45,6 +45,19 @@ extension.skeleton.main.layers.section.player.on.click = { section_1: { component: 'section', variant: 'card', + prevent_shorts_looping: { + component: 'switch', + text: 'Prevent Shorts Looping', + storage: 'player_prevent_shorts_looping', + id: 'player_prevent_shorts_looping', + on: { + click: function () { + let isEnabled = this.dataset.value === 'true'; + console.log(`🔁 Prevent Shorts Looping: ${isEnabled ? "Enabled" : "Disabled"}`); + chrome.storage.local.set({ "player_prevent_shorts_looping": isEnabled }); + } + } + }, autopause_when_switching_tabs: { component: 'switch', text: 'autopauseWhenSwitchingTabs', From 07533e4979388aef81b15f67a9e7f3d207b94f78 Mon Sep 17 00:00:00 2001 From: giulivno <104376169+giulivno@users.noreply.github.com> Date: Mon, 17 Feb 2025 20:34:21 -0500 Subject: [PATCH 2/2] Extension works in Incognito Mode Now Extension works in Incognito Mode Now --- js&css/web-accessible/init.js | 11 ++++++----- js&css/web-accessible/www.youtube.com/player.js | 10 ++++------ 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/js&css/web-accessible/init.js b/js&css/web-accessible/init.js index 0b970cf0c..c0ff2bfa3 100644 --- a/js&css/web-accessible/init.js +++ b/js&css/web-accessible/init.js @@ -97,11 +97,12 @@ ImprovedTube.init = function () { this.channelCompactTheme(); - let preventShortsLooping = localStorage.getItem("prevent_shorts_looping") === "true"; - if (preventShortsLooping) { - console.log("Prevent Shorts Looping is active!"); - ImprovedTube.preventShortsLooping(); - } + chrome.storage.local.get("prevent_shorts_looping", function (data) { + if (data.prevent_shorts_looping) { + console.log("Prevent Shorts Looping is active!"); + ImprovedTube.preventShortsLooping(); + } + }); if (ImprovedTube.elements.player && ImprovedTube.elements.player.setPlaybackRate) { ImprovedTube.videoPageUpdate(); diff --git a/js&css/web-accessible/www.youtube.com/player.js b/js&css/web-accessible/www.youtube.com/player.js index 01c4ccae1..4f9b173a5 100644 --- a/js&css/web-accessible/www.youtube.com/player.js +++ b/js&css/web-accessible/www.youtube.com/player.js @@ -706,13 +706,11 @@ REPEAT ImprovedTube.playerRepeat = function () { setTimeout(function () { if (!/ad-showing/.test(ImprovedTube.elements.player.className)) { - // Prevent looping for Shorts videos - if (!location.href.includes("shorts/")) { - ImprovedTube.elements.video.setAttribute('loop', ''); - } + ImprovedTube.elements.video.setAttribute('loop', ''); } - }, 200); -}; + //ImprovedTube.elements.buttons['it-repeat-styles'].style.opacity = '1'; //old class from version 3.x? that both repeat buttons could have + }, 200); +} /*------------------------------------------------------------------------------ REPEAT BUTTON