// ==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