// ==UserScript== // @name Rumble Enhancement Suite // @namespace https://github.com/SysAdminDoc/RumbleEnhancementSuite // @version 11.0 // @description A premium suite of tools to enhance Rumble.com, featuring a data-driven, video downloader, privacy controls, advanced stats, live chat enhancements, a professional UI, and layout controls. // @author Matthew Parker // @match https://rumble.com/* // @exclude https://rumble.com/user/* // @icon https://www.google.com/s2/favicons?sz=64&domain=rumble.com // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @grant GM.xmlHttpRequest // @grant GM_xmlhttpRequest // @grant GM_setClipboard // @connect * // @require https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js // @updateURL https://github.com/SysAdminDoc/RumbleEnhancementSuite/raw/refs/heads/main/RumbleX.user.js // @downloadURL https://github.com/SysAdminDoc/RumbleEnhancementSuite/raw/refs/heads/main/RumbleX.user.js // @run-at document-start // ==/UserScript== /* globals $, GM_setValue, GM_getValue, GM_addStyle, GM_xmlHttpRequest, unsafeWindow, Hls, GM_setClipboard */ (function() { 'use strict'; // —————————————————————————————————————————————————————————————————————————— // 1. SETTINGS & STATE MANAGER // —————————————————————————————————————————————————————————————————————————— const settingsManager = { defaults: { // Theme & Appearance panelTheme: 'dark', siteTheme: 'system', // Navigation autoHideHeader: true, autoHideNavSidebar: true, logoLinksToSubscriptions: true, // Main Page Layout widenSearchBar: true, hideUploadIcon: false, hideHeaderAd: false, hideProfileBacksplash: false, hidePremiumVideos: true, hideFeaturedBanner: false, hideEditorPicks: false, hideTopLiveCategories: false, hidePremiumRow: false, hideHomepageAd: false, hideForYouRow: false, hideGamingRow: false, hideFinanceRow: false, hideLiveRow: false, hideFeaturedPlaylistsRow: false, hideSportsRow: false, hideViralRow: false, hidePodcastsRow: false, hideLeaderboardRow: false, hideVlogsRow: false, hideNewsRow: false, hideScienceRow: false, hideMusicRow: false, hideEntertainmentRow: false, hideCookingRow: false, hideFooter: false, // Video Page Layout adaptiveLiveLayout: true, hideRelatedOnLive: true, fullWidthPlayer: false, hideRelatedSidebar: true, widenContent: true, hideVideoDescription: false, hidePausedVideoAds: false, // Player Controls autoBestQuality: true, autoLike: false, hideRewindButton: false, hideFastForwardButton: false, hideCCButton: false, hideAutoplayButton: false, hideTheaterButton: false, hidePipButton: false, hideFullscreenButton: false, hidePlayerRumbleLogo: false, hidePlayerGradient: false, // Video Buttons hideLikeDislikeButton: false, hideShareButton: false, hideRepostButton: false, hideEmbedButton: false, hideSaveButton: false, hideCommentButton: false, hideReportButton: false, hidePremiumJoinButtons: false, // Video Comments commentBlocking: true, autoLoadComments: false, moveReplyButton: true, hideCommentReportLink: false, // Live Chat liveChatBlocking: true, cleanLiveChat: false, }, async load() { let savedSettings = await GM_getValue('rumbleSuiteSettings_v9', {}); // Keep v9 key for compatibility return { ...this.defaults, ...savedSettings }; }, async save(settings) { await GM_setValue('rumbleSuiteSettings_v9', settings); }, async getBlockedUsers(type = 'comment') { const key = type === 'livechat' ? 'rumbleSuiteLiveChatBlockedUsers' : 'rumbleSuiteBlockedUsers'; return await GM_getValue(key, []); }, async saveBlockedUsers(users, type = 'comment') { const key = type === 'livechat' ? 'rumbleSuiteLiveChatBlockedUsers' : 'rumbleSuiteBlockedUsers'; const uniqueUsers = [...new Set(users)]; await GM_setValue(key, uniqueUsers); return uniqueUsers; }, }; const appState = { videoData: null, commentBlockedUsers: [], liveChatBlockedUsers: [], hlsInstance: null, }; // —————————————————————————————————————————————————————————————————————————— // 2. DYNAMIC STYLE & UTILITY ENGINE // —————————————————————————————————————————————————————————————————————————— const styleManager = { _styles: new Map(), inject(id, css) { if (this._styles.has(id)) { this.remove(id); } const styleElement = GM_addStyle(css); this._styles.set(id, styleElement); }, remove(id) { const styleElement = this._styles.get(id); if (styleElement && styleElement.parentElement) { styleElement.parentElement.removeChild(styleElement); } this._styles.delete(id); }, }; function applyAllCssFeatures() { let cssRules = []; const pageType = location.pathname === '/' ? 'home' : (location.pathname.startsWith('/v') ? 'video' : (location.pathname.startsWith('/c/') ? 'profile' : 'other')); features.forEach(feature => { if (feature.css && appState.settings[feature.id]) { const appliesToPage = !feature.page || feature.page === 'all' || feature.page === pageType; if (appliesToPage) { cssRules.push(feature.css); } } }); styleManager.inject('master-css-rules', cssRules.join('\n')); } function waitForElement(selector, callback, timeout = 10000) { const intervalTime = 200; let elapsedTime = 0; const interval = setInterval(() => { const element = $(selector); if (element.length > 0) { clearInterval(interval); callback(element); } elapsedTime += intervalTime; if (elapsedTime >= timeout) { clearInterval(interval); } }, intervalTime); } function formatBytes(bytes, decimals = 2) { if (!+bytes) return '0 Bytes'; const k = 1024; const dm = decimals < 0 ? 0 : decimals; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`; } function formatSeconds(seconds) { if (isNaN(seconds) || seconds < 0) return "00:00"; const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); const s = Math.floor(seconds % 60); const pad = (num) => String(num).padStart(2, '0'); return h > 0 ? `${pad(h)}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`; } const ICONS = { cog: ``, close: ``, download: ``, spinner: ``, check: ``, trash: ``, plus: ``, chevronUp: ``, chevronDown: ``, move: ``, system: ``, dark: ``, light: ``, block: ``, copy: ``, mic: ``, closedCaptions: ``, image: ``, stream: ``, }; // —————————————————————————————————————————————————————————————————————————— // 3. CORE DATA ENGINE // —————————————————————————————————————————————————————————————————————————— const dataEngine = { init() { if (!location.pathname.startsWith('/v')) return; this.findAndParseVideoData(); this.findHlsInstance(); }, findAndParseVideoData() { if (appState.videoData) return; const scripts = document.querySelectorAll('script'); for (const script of scripts) { if (script.textContent.includes('player.load({') && script.textContent.includes('viewer_id')) { const match = script.textContent.match(/player\.load\((.*)\)/s); if (match && match[1]) { try { let jsonString = match[1].trim(); if (jsonString.endsWith(',')) { jsonString = jsonString.slice(0, -1); } appState.videoData = JSON.parse(jsonString); console.log('[Rumble Suite] Video data successfully parsed from script tag:', appState.videoData); $(document).trigger('res:videoDataLoaded'); return; } catch (e) { console.error("[Rumble Suite] Failed to parse video data JSON from script tag.", e); } } } } }, findHlsInstance() { if (appState.hlsInstance) return; const videoElement = document.querySelector('#videoPlayer video'); if (videoElement && videoElement.hls) { appState.hlsInstance = videoElement.hls; console.log('[Rumble Suite] HLS.js instance found:', appState.hlsInstance); $(document).trigger('res:hlsInstanceFound'); return; } // Fallback to MutationObserver if HLS is not immediately available const observer = new MutationObserver((mutations, obs) => { if (videoElement && videoElement.hls) { appState.hlsInstance = videoElement.hls; console.log('[Rumble Suite] HLS.js instance found via MutationObserver:', appState.hlsInstance); $(document).trigger('res:hlsInstanceFound'); obs.disconnect(); } }); waitForElement('.media-player-container', ($container) => { observer.observe($container[0], { childList: true, subtree: true }); }); } }; // —————————————————————————————————————————————————————————————————————————— // 4. FEATURE DEFINITIONS & LOGIC // —————————————————————————————————————————————————————————————————————————— const features = [ // --- THEME & APPEARANCE --- { id: 'siteTheme', name: 'Rumble Site Theme', description: 'Controls the appearance of the Rumble website itself, syncing with its native options.', newCategory: 'Theme & Appearance', isManagement: true, sync() { const activeTheme = $('a.main-menu-item.theme-option.main-menu-item--active').data('theme-option') || 'system'; if (appState.settings.siteTheme !== activeTheme) { appState.settings.siteTheme = activeTheme; settingsManager.save(appState.settings); } $(`.res-theme-button[data-theme-value="${activeTheme}"]`).prop('checked', true); }, init() { this.apply(appState.settings.siteTheme); const observer = new MutationObserver(() => this.sync()); waitForElement('.theme-option-group', ($el) => observer.observe($el[0], { attributes: true, subtree: true, attributeFilter: ['class'] })); }, apply(themeValue) { const $targetButton = $(`a.main-menu-item.theme-option[data-theme-option="${themeValue}"]`); if ($targetButton.length && !$targetButton.hasClass('main-menu-item--active')) { $targetButton[0].click(); } }, }, // --- NAVIGATION --- { id: 'autoHideHeader', name: 'Auto-hide Header', description: 'Fades the header out. It fades back in when you move your cursor to the top of the page.', newCategory: 'Navigation', init() { this.handler = (e) => { if (e.clientY < 80) { // Top trigger zone document.body.classList.add('res-header-visible'); } else if (!e.target.closest('header.header')) { document.body.classList.remove('res-header-visible'); } }; const css = ` body.res-autohide-header-active header.header { position: fixed; top: 0; left: 0; right: 0; z-index: 1001; opacity: 0; transition: opacity 0.3s ease-in-out; pointer-events: none; } body.res-autohide-header-active.res-header-visible header.header { opacity: 1; pointer-events: auto; } body.res-autohide-header-active { padding-top: 0 !important; } `; styleManager.inject(this.id, css); document.body.classList.add('res-autohide-header-active'); document.addEventListener('mousemove', this.handler); }, destroy() { if (this.handler) { document.removeEventListener('mousemove', this.handler); } styleManager.remove(this.id); document.body.classList.remove('res-autohide-header-active', 'res-header-visible'); } }, { id: 'autoHideNavSidebar', name: 'Auto-hide Navigation Sidebar', description: 'Hides the main navigation sidebar. It slides into view when you move your cursor to the left edge of the page.', newCategory: 'Navigation', init() { const css = ` body.res-autohide-nav-active nav.navs { position: fixed; top: 0; left: 0; transform: translateX(-100%); transition: transform 0.3s ease-in-out, opacity 0.3s ease-in-out; z-index: 1002; height: 100vh; opacity: 0.95; visibility: hidden; } body.res-autohide-nav-active main.nav--transition { margin-left: 0 !important; } #res-nav-sidebar-trigger { position: fixed; top: 80px; left: 0; width: 30px; height: calc(100% - 80px); z-index: 1001; } #res-nav-sidebar-trigger:hover + nav.navs, body.res-autohide-nav-active nav.navs:hover { transform: translateX(0); opacity: 1; visibility: visible; } `; styleManager.inject(this.id, css); $('body').addClass('res-autohide-nav-active'); if ($('#res-nav-sidebar-trigger').length === 0) { $('body').append('
'); } }, destroy() { styleManager.remove(this.id); $('body').removeClass('res-autohide-nav-active'); $('#res-nav-sidebar-trigger').remove(); } }, { id: 'logoLinksToSubscriptions', name: 'Logo Links to Subscriptions', description: 'Changes the main Rumble logo in the header to link to your subscriptions feed instead of the homepage.', newCategory: 'Navigation', init() { this.observer = new MutationObserver(() => { const $logo = $('a.header-logo'); if ($logo.length && $logo.attr('href') !== '/subscriptions') { $logo.attr('href', '/subscriptions'); } }); waitForElement('header.header', ($header) => { this.observer.observe($header[0], { childList: true, subtree: true }); }); }, destroy() { if (this.observer) this.observer.disconnect(); $('a.header-logo').attr('href', '/'); } }, // --- MAIN PAGE --- { id: 'widenSearchBar', name: 'Widen Search Bar', description: 'Expands the search bar to fill available header space.', newCategory: 'Main Page Layout', css: `.header .header-div { display: flex; align-items: center; gap: 1rem; padding-right: 1.5rem; box-sizing: border-box; } .header-search { flex-grow: 1; max-width: none !important; } .header-search .header-search-field { width: 100% !important; }` }, { id: 'hideUploadIcon', name: 'Hide Upload Icon', description: 'Hides the upload/stream live icon in the header.', newCategory: 'Main Page Layout', css: 'button.header-upload { display: none !important; }' }, { id: 'hideHeaderAd', name: 'Hide "Go Ad-Free" Button', description: 'Hides the "Go Ad-Free" button in the header.', newCategory: 'Main Page Layout', css: `span.hidden.lg\\:flex:has(button[hx-get*="premium-value-prop"]) { display: none !important; }` }, { id: 'hideProfileBacksplash', name: 'Hide Profile Backsplash', description: 'Hides the large header image on channel profiles.', newCategory: 'Main Page Layout', page: 'profile', css: `div.channel-header--backsplash { display: none; } html.main-menu-mode-permanent { margin-top: 30px !important; }` }, { id: 'hidePremiumVideos', name: 'Hide Premium Videos', description: 'Hides premium-only videos from subscription and channel feeds.', newCategory: 'Main Page Layout', init() { const hideRule = () => document.querySelectorAll('div.videostream:has(a[href="/premium"])').forEach(el => el.style.display = 'none'); this.observer = new MutationObserver(hideRule); waitForElement('main', ($main) => this.observer.observe($main[0], { childList: true, subtree: true })); hideRule(); }, destroy() { if (this.observer) this.observer.disconnect(); document.querySelectorAll('div.videostream:has(a[href="/premium"])').forEach(el => el.style.display = ''); } }, { id: 'hideFeaturedBanner', name: 'Hide Featured Banner', description: 'Hides the top category banner on the home page.', newCategory: 'Main Page Layout', css: 'div.homepage-featured { display: none !important; }', page: 'home' }, { id: 'hideEditorPicks', name: "Hide Editor Picks", description: "Hides the main 'Editor Picks' content row on the home page.", newCategory: 'Main Page Layout', css: '#section-editor-picks { display: none !important; }', page: 'home' }, { id: 'hideTopLiveCategories', name: "Hide 'Top Live' Row", description: "Hides the 'Top Live Categories' row on the home page.", newCategory: 'Main Page Layout', css: 'section#section-top-live { display: none !important; }', page: 'home' }, { id: 'hidePremiumRow', name: "Hide Premium Row", description: "Hides the Rumble Premium row on the home page.", newCategory: 'Main Page Layout', css: 'section#section-premium-videos { display: none !important; }', page: 'home' }, { id: 'hideHomepageAd', name: "Hide Ad Section", description: "Hides the ad container on the home page.", newCategory: 'Main Page Layout', css: 'section.homepage-section:has(.js-rac-desktop-container) { display: none !important; }', page: 'home' }, { id: 'hideForYouRow', name: "Hide 'For You' Row", description: "Hides 'For You' recommendations on the home page.", newCategory: 'Main Page Layout', css: 'section#section-personal-recommendations { display: none !important; }', page: 'home' }, { id: 'hideGamingRow', name: "Hide Gaming Row", description: "Hides the Gaming row on the home page.", newCategory: 'Main Page Layout', css: 'section#section-gaming { display: none !important; }', page: 'home' }, { id: 'hideFinanceRow', name: "Hide Finance & Crypto Row", description: "Hides the Finance & Crypto row on the home page.", newCategory: 'Main Page Layout', css: 'section#section-finance { display: none !important; }', page: 'home' }, { id: 'hideLiveRow', name: "Hide Live Row", description: "Hides the Live row on the home page.", newCategory: 'Main Page Layout', css: 'section#section-live-videos { display: none !important; }', page: 'home' }, { id: 'hideFeaturedPlaylistsRow', name: "Hide Featured Playlists", description: "Hides the Featured Playlists row on the home page.", newCategory: 'Main Page Layout', css: 'section#section-featured-playlists { display: none !important; }', page: 'home' }, { id: 'hideSportsRow', name: "Hide Sports Row", description: "Hides the Sports row on the home page.", newCategory: 'Main Page Layout', css: 'section#section-sports { display: none !important; }', page: 'home' }, { id: 'hideViralRow', name: "Hide Viral Row", description: "Hides the Viral row on the home page.", newCategory: 'Main Page Layout', css: 'section#section-viral { display: none !important; }', page: 'home' }, { id: 'hidePodcastsRow', name: "Hide Podcasts Row", description: "Hides the Podcasts row on the home page.", newCategory: 'Main Page Layout', css: 'section#section-podcasts { display: none !important; }', page: 'home' }, { id: 'hideLeaderboardRow', name: "Hide Leaderboard Row", description: "Hides the Leaderboard row on the home page.", newCategory: 'Main Page Layout', css: 'section#section-leaderboard { display: none !important; }', page: 'home' }, { id: 'hideVlogsRow', name: "Hide Vlogs Row", description: "Hides the Vlogs row on the home page.", newCategory: 'Main Page Layout', css: 'section#section-vlogs { display: none !important; }', page: 'home' }, { id: 'hideNewsRow', name: "Hide News Row", description: "Hides the News row on the home page.", newCategory: 'Main Page Layout', css: 'section#section-news { display: none !important; }', page: 'home' }, { id: 'hideScienceRow', name: "Hide Health & Science Row", description: "Hides the Health & Science row on the home page.", newCategory: 'Main Page Layout', css: 'section#section-science { display: none !important; }', page: 'home' }, { id: 'hideMusicRow', name: "Hide Music Row", description: "Hides the Music row on the home page.", newCategory: 'Main Page Layout', css: 'section#section-music { display: none !important; }', page: 'home' }, { id: 'hideEntertainmentRow', name: "Hide Entertainment Row", description: "Hides the Entertainment row on the home page.", newCategory: 'Main Page Layout', css: 'section#section-entertainment { display: none !important; }', page: 'home' }, { id: 'hideCookingRow', name: "Hide Cooking Row", description: "Hides the Cooking row on the home page.", newCategory: 'Main Page Layout', css: 'section#section-cooking { display: none !important; }', page: 'home' }, { id: 'hideFooter', name: 'Hide Footer', description: 'Removes the footer at the bottom of the page.', newCategory: 'Main Page Layout', css: 'footer.page__footer.foot.nav--transition { display: none !important; }' }, // --- VIDEO PAGE LAYOUT --- { id: 'adaptiveLiveLayout', name: 'Adaptive Live Video Layout', description: 'On live streams, expands the player to fill the space next to the live chat.', newCategory: 'Video Page Layout', init() { if (!document.querySelector('.video-header-live-info')) return; // Only run on live pages const chatSelector = 'aside.media-page-chat-aside-chat'; const applyStyles = (isChatVisible) => { const css = isChatVisible ? `body:not(.res-full-width-player):not(.res-live-two-col) .main-and-sidebar .main-content { width: calc(100% - 350px) !important; max-width: none !important; }` : ''; styleManager.inject('adaptive-live-css', css); }; this.observer = new MutationObserver((mutations) => { for (const m of mutations) { if (m.attributeName === 'style') { const chatIsVisible = $(m.target).css('display') !== 'none'; applyStyles(chatIsVisible); } } }); waitForElement(chatSelector, ($chat) => { this.observer.observe($chat[0], { attributes: true, attributeFilter: ['style'] }); applyStyles($chat.css('display') !== 'none'); // Initial check }); }, destroy() { if (this.observer) this.observer.disconnect(); styleManager.remove('adaptive-live-css'); } }, { id: 'hideRelatedOnLive', name: 'Hide Related Media on Live', description: 'Hides the "Related Media" section below the player on live streams.', newCategory: 'Video Page Layout', css: '.media-page-related-media-desktop-floating { display: none !important; }', page: 'video' }, { id: 'fullWidthPlayer', name: 'Full-Width Player / Live Layout', description: "Maximizes player width. Works with 'Auto-hide Header' for a full-screen experience. On live streams, it enables an optimized side-by-side view with chat.", newCategory: 'Video Page Layout', page: 'video', _liveObserver: null, _resizeListener: null, _standardCss: `body.res-full-width-player nav.navs, body.res-full-width-player aside.media-page-related-media-desktop-sidebar, body.res-full-width-player #player-spacer { display: none !important; } body.res-full-width-player main.nav--transition { margin-left: 0 !important; } body.res-full-width-player .main-and-sidebar { max-width: 100% !important; padding: 0 !important; margin: 0 !important; } body.res-full-width-player .main-content, body.res-full-width-player .media-container { width: 100% !important; max-width: 100% !important; } body.res-full-width-player .video-player, body.res-full-width-player [id^='vid_v'] { width: 100vw !important; height: calc(100vw * 9 / 16) !important; max-height: 100vh; } body.res-full-width-player #videoPlayer video { object-fit: contain !important; }`, _liveCss: ` /* Main grid container for the two-column layout */ body.res-live-two-col:not(.rumble-player--fullscreen) .main-and-sidebar { display: grid !important; grid-template-columns: minmax(0, 1fr) var(--res-chat-w, 360px); width: 100vw; max-width: 100vw; margin: 0; padding: 0; align-items: stretch; /* Make columns equal height */ } /* Make the video column and its children capable of filling the height */ body.res-live-two-col:not(.rumble-player--fullscreen) .main-and-sidebar .main-content { display: flex; flex-direction: column; } body.res-live-two-col:not(.rumble-player--fullscreen) .media-container { flex-grow: 1; /* Make this container fill the available vertical space */ } /* Chat column styles */ body.res-live-two-col:not(.rumble-player--fullscreen) aside.media-page-chat-aside-chat { width: var(--res-chat-w, 360px) !important; min-width: var(--res-chat-w, 360px) !important; max-width: clamp(320px, var(--res-chat-w, 360px), 480px) !important; position: relative; z-index: 1; } /* Video player fills its container completely */ body.res-live-two-col:not(.rumble-player--fullscreen) .video-player { margin-top: -30px; } body.res-live-two-col:not(.rumble-player--fullscreen) .video-player, body.res-live-two-col:not(.rumble-player--fullscreen) #videoPlayer, body.res-live-two-col:not(.rumble-player--fullscreen) #videoPlayer > div, body.res-live-two-col:not(.rumble-player--fullscreen) [id^='vid_v'] { width: 100% !important; height: 100% !important; max-height: none !important; background-color: #000; } /* The actual