From e14846db29c5c6f17cd0ef539ff0c9a3f231a577 Mon Sep 17 00:00:00 2001 From: Arya Dasgupta Date: Fri, 6 Feb 2026 00:53:02 +0530 Subject: [PATCH 1/2] Feat: Implement Smart Speed feature using YouTube heatmap (#1463) --- _locales/en/messages.json | 3 + .../web-accessible/www.youtube.com/player.js | 189 ++++++++++++++++++ .../www.youtube.com/shortcuts.js | 22 ++ menu/skeleton-parts/player.js | 16 ++ 4 files changed, 230 insertions(+) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index a3808cfc6..c900cbb7b 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1369,6 +1369,9 @@ "Sidebar_simple_alternative": { "message": "Sidebar (simple alternative)" }, + "smartSpeed": { + "message": "Smart Speed (Skip Boring Parts)" + }, "softwareInformation": { "message": "Software information" }, diff --git a/js&css/web-accessible/www.youtube.com/player.js b/js&css/web-accessible/www.youtube.com/player.js index 27783a51a..ff59bf083 100644 --- a/js&css/web-accessible/www.youtube.com/player.js +++ b/js&css/web-accessible/www.youtube.com/player.js @@ -2347,3 +2347,192 @@ ImprovedTube.shortsAutoScroll = function () { } } }; + +/*------------------------------------------------------------------------------ +SMART SPEED & SKIP (HEATMAP ENGINE) +------------------------------------------------------------------------------*/ +ImprovedTube.heatmap = { + data: null, + segments: [], + isEnabled: false, + lastCheck: 0, + + init: function () { + this.isEnabled = ImprovedTube.storage.smart_speed === true; + if (!this.isEnabled) return; + + this.data = null; + this.segments = []; + this.getData(); + + const video = document.querySelector('video'); + if (video && !video.dataset.itSmartSpeedAttached) { + video.dataset.itSmartSpeedAttached = 'true'; + video.addEventListener('timeupdate', () => this.update(video)); + video.addEventListener('loadeddata', () => this.init()); + } + }, + + getData: function () { + console.log('[ImprovedTube] Hunting for Heatmap...'); + + // LEVEL 1: Active Player + try { + const playerResponse = document.getElementById('movie_player')?.getPlayerResponse(); + if (this.checkAndProcess(playerResponse, 'Active Player')) return; + } catch (e) {} + + // LEVEL 2: Global Variables + try { + if (this.checkAndProcess(window.ytInitialData, 'window.ytInitialData')) return; + if (this.checkAndProcess(window.ytInitialPlayerResponse, 'window.ytInitialPlayerResponse')) return; + } catch (e) {} + + // LEVEL 3: Background Fetch + const videoId = new URLSearchParams(window.location.search).get('v'); + if (videoId) { + console.log('[ImprovedTube] Level 1 and 2 failed. Fetching source...'); + fetch('https://www.youtube.com/watch?v=' + videoId) + .then(res => res.text()) + .then(text => { + // Extract and parse JSONs + const matchData = text.match(/var ytInitialData = ({.*?});/s); + const matchResp = text.match(/var ytInitialPlayerResponse = ({.*?});/s); + + if (matchData && this.checkAndProcess(JSON.parse(matchData[1]), 'Fetched ytInitialData')) return; + if (matchResp && this.checkAndProcess(JSON.parse(matchResp[1]), 'Fetched ytInitialPlayerResponse')) return; + + console.log('[ImprovedTube] Heatmap data not found in source.'); + }) + .catch(err => console.error('[ImprovedTube] Fetch error:', err)); + } + }, + + // Recursively searches JSON for the heatmap + findMarkers: function(obj) { + if (!obj || typeof obj !== 'object') return null; + + // Pattern 1: Standard MarkersList + if (obj.markerType === 'MARKER_TYPE_HEATMAP' && Array.isArray(obj.markers)) { + return obj.markers; + } + + // Pattern 2: MarkersMap (common in ytInitialData) + if (obj.key === 'MARKER_TYPE_HEATMAP' && obj.value && Array.isArray(obj.value.markers)) { + return obj.value.markers; + } + + // Recursive Search + for (let key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + const found = this.findMarkers(obj[key]); + if (found) return found; + } + } + return null; + }, + + checkAndProcess: function (rootObject, sourceName) { + if (!rootObject) return false; + // Use the recursive finder instead of hardcoded paths + const markers = this.findMarkers(rootObject); + + if (markers && markers.length > 0) { + console.log('[ImprovedTube] Found Heatmap via: ' + sourceName); + this.processSegments(markers); + return true; + } + return false; + }, + + processSegments: function (rawMarkers) { + if (!rawMarkers || rawMarkers.length === 0) return; + + this.segments = []; + let currentAction = 'PLAY'; + let currentSpeed = 1.0; + let startPct = 0; + + rawMarkers.forEach((marker, index) => { + const score = marker.intensityScoreNormalized; + if (typeof score !== 'number') return; + + let newAction = 'PLAY'; + let newSpeed = 1.0; + + // 1. SKIP LOGIC (Intro only: First 15%) + if (index < 15 && score < 0.15) { + newAction = 'SKIP'; + } + // 2. DYNAMIC SPEED LOGIC + else { + if (score <= 0.05) newSpeed = 2.0; // Extremely Boring + else if (score <= 0.12) newSpeed = 1.75; // Very Boring + else if (score <= 0.18) newSpeed = 1.5; // Boring + else if (score <= 0.25) newSpeed = 1.25; // Slightly Boring + else newSpeed = 1.0; // Normal + } + + // Create a new segment if the plan changes + if (newAction !== currentAction || newSpeed !== currentSpeed) { + this.segments.push({ + start: startPct, + end: index, + action: currentAction, + speed: currentSpeed + }); + currentAction = newAction; + currentSpeed = newSpeed; + startPct = index; + } + }); + // Push final segment + this.segments.push({ start: startPct, end: 100, action: currentAction, speed: currentSpeed }); + console.log('[ImprovedTube] Dynamic Segments ready:', this.segments.length); + }, + + update: function (video) { + if (!this.segments.length || video.paused) return; + + const now = Date.now(); + if (now - this.lastCheck < 500) return; + this.lastCheck = now; + + const duration = video.duration; + const currentPct = (video.currentTime / duration) * 100; + + const activeSegment = this.segments.find(s => currentPct >= s.start && currentPct < s.end); + + if (activeSegment) { + if (activeSegment.action === 'SKIP') { + const skipTo = (activeSegment.end / 100) * duration; + if (skipTo - video.currentTime > 5) { + console.log('[ImprovedTube] Auto-Skipping...'); + video.currentTime = skipTo; + } + } else { + // Apply Dynamic Speed with a small tolerance check to prevent log spam + // We check if current speed matches target speed (within 0.1 margin) + if (Math.abs(video.playbackRate - activeSegment.speed) > 0.1) { + video.playbackRate = activeSegment.speed; + } + } + } + } +}; + +/*------------------------------------------------------------------------------ +AUTO-START SMART SPEED +------------------------------------------------------------------------------*/ +setTimeout(function() { + if (ImprovedTube.storage.smart_speed) { + console.log('[ImprovedTube] Auto-Starting Smart Speed...'); + ImprovedTube.heatmap.init(); + } +}, 2000); + +window.addEventListener('yt-navigate-finish', function() { + if (ImprovedTube.storage.smart_speed) { + setTimeout(() => ImprovedTube.heatmap.init(), 1000); + } +}); \ No newline at end of file diff --git a/js&css/web-accessible/www.youtube.com/shortcuts.js b/js&css/web-accessible/www.youtube.com/shortcuts.js index 9494bd2b8..5d1876cfc 100644 --- a/js&css/web-accessible/www.youtube.com/shortcuts.js +++ b/js&css/web-accessible/www.youtube.com/shortcuts.js @@ -663,4 +663,26 @@ ImprovedTube.shortcutRefreshCategories = function () { } else { window.location.reload(); } +}; + +/*------------------------------------------------------------------------------ +4.7.33 SMART SPEED TOGGLE +------------------------------------------------------------------------------*/ +ImprovedTube.shortcutSmartSpeed = function () { + var currentState = ImprovedTube.storage.smart_speed; + var newState = !currentState; + + ImprovedTube.storage.smart_speed = newState; + + try { satus.storage.set('smart_speed', newState); } catch(e) {} + + if (newState) { + if (ImprovedTube.heatmap) ImprovedTube.heatmap.init(); + ImprovedTube.showStatus('Smart Speed: ON'); + } else { + if (ImprovedTube.heatmap) ImprovedTube.heatmap.isEnabled = false; + var video = document.querySelector('video'); + if (video) video.playbackRate = 1.0; + ImprovedTube.showStatus('Smart Speed: OFF'); + } }; \ No newline at end of file diff --git a/menu/skeleton-parts/player.js b/menu/skeleton-parts/player.js index 75814c8b7..dbe147b89 100644 --- a/menu/skeleton-parts/player.js +++ b/menu/skeleton-parts/player.js @@ -196,6 +196,22 @@ extension.skeleton.main.layers.section.player.on.click = { max: 3.17, step: .01 }, +smart_speed: { + component: 'switch', + text: 'smartSpeed', + storage: 'smart_speed', + id: 'smart_speed', + on: { + click: function () { + var isEnabled = this.dataset.value === 'false'; + if (isEnabled) { + ImprovedTube.messages.send({ action: 'eval', args: 'ImprovedTube.storage.smart_speed = true; if(ImprovedTube.heatmap) ImprovedTube.heatmap.init();' }); + } else { + ImprovedTube.messages.send({ action: 'eval', args: 'ImprovedTube.storage.smart_speed = false; if(ImprovedTube.heatmap) { ImprovedTube.heatmap.isEnabled = false; if(document.querySelector("video")) document.querySelector("video").playbackRate = 1.0; }' }); + } + } + } + }, autofullscreen: { component: 'switch', text: 'autoFullscreen', From ca51c1626acfea90b43813990a8eaff4f375cce6 Mon Sep 17 00:00:00 2001 From: ImprovedTube Date: Fri, 6 Feb 2026 03:33:51 +0100 Subject: [PATCH 2/2] Update shortcuts.js --- menu/skeleton-parts/shortcuts.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/menu/skeleton-parts/shortcuts.js b/menu/skeleton-parts/shortcuts.js index 23acee4d3..4bf324ff2 100644 --- a/menu/skeleton-parts/shortcuts.js +++ b/menu/skeleton-parts/shortcuts.js @@ -212,6 +212,10 @@ extension.skeleton.main.layers.section.shortcuts = { } } }, + shortcut_smart_speed: { + component: 'shortcut', + text: 'smartSpeed' + }, shortcut_play_pause: { component: 'shortcut', text: 'playPause',