Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
320 changes: 81 additions & 239 deletions js/adBlocker.js
Original file line number Diff line number Diff line change
@@ -1,270 +1,112 @@
// Advanced YouTube Ad Blocker for Flow State App
// This module implements techniques similar to dedicated ad blockers
// Simplified YouTube Ad Blocker inspired by SkipCut script
// This module loads the YouTube IFrame API and attempts to skip video ads
// using injected code inside the player iframe.
// The API is used only when the ad blocker is enabled.

import storageService from './storage.js';

// Ad blocking state
let adBlockerEnabled = true;
let adSkipAttempts = 0;
const MAX_SKIP_ATTEMPTS = 5;
let apiReady = false;
let apiLoading = false;
const pendingIframes = new Set();

/**
* Initialize ad blocker for a YouTube iframe element
* @param {HTMLIFrameElement} ytPlayerElement - The YouTube iframe element
*/
export function initAdBlocker(ytPlayerElement) {
if (!ytPlayerElement || !adBlockerEnabled) return;

console.log('[AdBlocker] Initializing for YouTube player');

// Set better parameters for minimal ads
enhanceEmbedParameters(ytPlayerElement);

// Observe iframe load to inject ad-blocking script
ytPlayerElement.addEventListener('load', () => {
injectAdSkipper(ytPlayerElement);
});

// Handle messages from the iframe
window.addEventListener('message', handleYouTubeMessages);
// Load the YouTube IFrame API once
function loadYouTubeAPI() {
if (apiReady || apiLoading) return;
apiLoading = true;
const tag = document.createElement('script');
tag.src = 'https://www.youtube.com/iframe_api';
document.head.appendChild(tag);
}

/**
* Enhance YouTube embed parameters to minimize ads
* @param {HTMLIFrameElement} ytPlayerElement - The YouTube iframe element
*/
function enhanceEmbedParameters(ytPlayerElement) {
// Get current source
const currentSrc = ytPlayerElement.src;

// If already has our parameters, don't modify
if (currentSrc.includes('ad_blocking=true')) return;

// Create URL object for easy parameter manipulation
try {
const urlObj = new URL(currentSrc);

// Add our custom parameters
urlObj.searchParams.set('rel', '0'); // No related videos
urlObj.searchParams.set('controls', '1'); // Show controls
urlObj.searchParams.set('iv_load_policy', '3'); // No annotations
urlObj.searchParams.set('modestbranding', '1'); // Minimal branding
urlObj.searchParams.set('enablejsapi', '1'); // Enable API
urlObj.searchParams.set('origin', window.location.origin); // Security
urlObj.searchParams.set('ad_blocking', 'true'); // Our flag to prevent re-processing

// Update the player source
ytPlayerElement.src = urlObj.toString();

console.log('[AdBlocker] Enhanced YouTube embed parameters');
} catch (error) {
console.error('[AdBlocker] Error updating YouTube parameters:', error);
}
}
// Called by the IFrame API once it's ready
window.onYouTubeIframeAPIReady = () => {
apiReady = true;
pendingIframes.forEach(ifr => setupPlayer(ifr));
pendingIframes.clear();
};

/**
* Inject ad skipping script into the YouTube iframe
* @param {HTMLIFrameElement} ytPlayerElement - The YouTube iframe element
* Initialize ad blocker for a YouTube iframe element
* @param {HTMLIFrameElement} iframe
*/
function injectAdSkipper(ytPlayerElement) {
try {
// First check if we can access the iframe content (same-origin policy)
const iframeDoc = ytPlayerElement.contentDocument || ytPlayerElement.contentWindow?.document;

// If we can access the document, inject directly (only works for same-origin)
if (iframeDoc) {
const scriptTag = iframeDoc.createElement('script');
scriptTag.textContent = getAdSkipperCode();
iframeDoc.head.appendChild(scriptTag);
console.log('[AdBlocker] Directly injected ad skipper script');
return;
}

// If we can't access directly, use postMessage API
ytPlayerElement.contentWindow.postMessage(JSON.stringify({
event: 'command',
func: 'addEventListener',
args: ['onStateChange', 'flowStateAdCheck']
}), '*');

console.log('[AdBlocker] Registered ad detection via postMessage');
} catch (error) {
console.error('[AdBlocker] Error injecting ad skipper:', error);
export function initAdBlocker(iframe) {
if (!adBlockerEnabled || !iframe) return;
loadYouTubeAPI();
if (apiReady) {
setupPlayer(iframe);
} else {
pendingIframes.add(iframe);
}
}

/**
* Handle messages from the YouTube iframe
* @param {MessageEvent} event - The message event
*/
function handleYouTubeMessages(event) {
try {
// Validate message origin (should be from YouTube)
if (!event.origin.includes('youtube.com')) return;

// Parse the data
let data;
if (typeof event.data === 'string') {
try {
data = JSON.parse(event.data);
} catch {
// Not a JSON message we care about
return;
function setupPlayer(iframe) {
if (iframe._adBlockerPlayer) return; // already initialized
const player = new YT.Player(iframe, {
events: {
onStateChange: (e) => {
if (e.data === YT.PlayerState.PLAYING) {
injectSkipper(player);
}
}
} else {
data = event.data;
}

// Check for ad information
if (data.type === 'adStateChange' && data.isAd) {
console.log('[AdBlocker] Ad detected, attempting to skip');
skipAd(document.querySelector('iframe[src*="youtube.com"]'));
}
} catch (error) {
console.error('[AdBlocker] Error handling YouTube message:', error);
}
});
iframe._adBlockerPlayer = player;
}

/**
* Skip a detected ad
* @param {HTMLIFrameElement} ytPlayerElement - The YouTube iframe element
*/
function skipAd(ytPlayerElement) {
if (adSkipAttempts >= MAX_SKIP_ATTEMPTS) {
console.log('[AdBlocker] Maximum skip attempts reached, giving up');
return;
}

adSkipAttempts++;

// Inject ad skipping code into the player iframe
function injectSkipper(player) {
const iframe = player.getIframe();
if (!iframe) return;
try {
// Try various methods to skip the ad

// Method 1: Use the YouTube API to skip
ytPlayerElement.contentWindow.postMessage(JSON.stringify({
event: 'command',
func: 'seekTo',
args: [0, true] // Seek to start, then we'll seek to end of ad
}), '*');

// Method 2: After a small delay, try to skip to end of ad
setTimeout(() => {
ytPlayerElement.contentWindow.postMessage(JSON.stringify({
event: 'command',
func: 'seekTo',
args: [99999, true] // Seek far forward to try to skip ad
}), '*');
}, 500);

// Method 3: After another delay, try to mute and then resume normal
setTimeout(() => {
ytPlayerElement.contentWindow.postMessage(JSON.stringify({
event: 'command',
func: 'mute'
}), '*');

// Reset the skip attempts counter after a while
setTimeout(() => {
adSkipAttempts = 0;
}, 5000);
}, 1000);

console.log('[AdBlocker] Attempted to skip ad with multiple methods');
} catch (error) {
console.error('[AdBlocker] Error skipping ad:', error);
const doc = iframe.contentDocument || iframe.contentWindow.document;
if (!doc.getElementById('flow-ad-skipper')) {
const script = doc.createElement('script');
script.id = 'flow-ad-skipper';
script.textContent = getSkipperScript();
doc.head.appendChild(script);
}
} catch (e) {
// Fallback: use postMessage to execute code if cross-origin
try {
const code = getSkipperScript();
iframe.contentWindow.postMessage({
event: 'flowInject',
code
}, '*');
} catch {
// Ignore
}
}
}

/**
* Get the ad skipper code to inject into the YouTube iframe
* @returns {string} - JavaScript code as a string
*/
function getAdSkipperCode() {
return `
// YouTube ad detection and skipping code
(function() {
const adObserver = new MutationObserver(function(mutations) {
// Look for ad container elements
if (document.querySelector('.ad-showing') ||
document.querySelector('.ytp-ad-player-overlay') ||
document.querySelector('.ytp-ad-text')) {

// Notify the parent window
window.parent.postMessage(JSON.stringify({
type: 'adStateChange',
isAd: true
}), '*');

// Try to click the skip button if it exists
const skipButton = document.querySelector('.ytp-ad-skip-button') ||
document.querySelector('.ytp-ad-skip-button-modern');
if (skipButton) {
skipButton.click();
console.log('[YT-AdBlock] Clicked skip button');
}

// Try to mute the ad
const muteButton = document.querySelector('.ytp-mute-button');
if (muteButton && !muteButton.classList.contains('ytp-muted')) {
muteButton.click();
console.log('[YT-AdBlock] Muted ad');
}
}
});

// Start observing the entire document for changes
adObserver.observe(document.documentElement, {
childList: true,
subtree: true
});

// Define the callback function that YouTube API will call
window.flowStateAdCheck = function(state) {
// -1: unstarted, 0: ended, 1: playing, 2: paused, 3: buffering, 5: video cued
if (state === 1) {
// Check if this is an ad when playing starts
setTimeout(function() {
const adShowing = document.querySelector('.ad-showing') ||
document.querySelector('.ytp-ad-player-overlay');
if (adShowing) {
window.parent.postMessage(JSON.stringify({
type: 'adStateChange',
isAd: true
}), '*');
}
}, 500);
}
};

console.log('[YT-AdBlock] Ad detection initialized');
})();
`;
// Script that runs inside the YouTube iframe
function getSkipperScript() {
return `(() => {
if (window.__flowAdBlocker) return;
window.__flowAdBlocker = true;
const observer = new MutationObserver(() => {
const skipBtn = document.querySelector('.ytp-ad-skip-button, .ytp-ad-skip-button-modern');
if (skipBtn) skipBtn.click();
if (document.querySelector('.ad-showing')) {
const video = document.querySelector('video');
if (video) video.currentTime = video.duration;
}
});
observer.observe(document, {childList: true, subtree: true});
})();`;
}

/**
* Enable or disable the ad blocker
* @param {boolean} enabled - Whether to enable ad blocking
*/
export function setAdBlockerEnabled(enabled) {
adBlockerEnabled = enabled;
storageService.setItem('adBlockerEnabled', enabled);
console.log(`[AdBlocker] ${enabled ? 'Enabled' : 'Disabled'}`);
}

/**
* Get the current ad blocker enabled state
* @returns {Promise<boolean>} - Whether ad blocking is enabled
*/
export async function isAdBlockerEnabled() {
try {
const savedState = await storageService.getItem('adBlockerEnabled');
return savedState === null ? true : savedState === 'true';
} catch (error) {
console.error('[AdBlocker] Error getting state:', error);
return true; // Default to enabled
}
const saved = await storageService.getItem('adBlockerEnabled');
return saved === null ? true : saved === 'true';
}

// Initialize when the module loads
(async function() {
(async () => {
adBlockerEnabled = await isAdBlockerEnabled();
})();
})();
9 changes: 6 additions & 3 deletions js/music.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import {
musicLabels
} from './constants.js';
import storageService from './storage.js';
import { initAdBlocker } from './adBlocker.js'; // Import our new ad blocker
import { initAdBlocker } from './adBlocker.js'; // Import our ad blocker
import { initSponsorBlocker } from './sponsorBlocker.js';

// Music elements
let ytPlayer, customVidInput;
Expand Down Expand Up @@ -71,8 +72,9 @@ export async function initMusic() {
// Initialize YouTube player with the remembered video
ytPlayer.src = `https://www.youtube.com/embed/${currentVideoID}?autoplay=0&loop=1&playlist=${currentVideoID}&rel=0&controls=1&iv_load_policy=3&modestbranding=1&enablejsapi=1&origin=${window.location.origin}`;

// Initialize the ad blocker for the YouTube player
// Initialize ad and sponsor blockers for the YouTube player
initAdBlocker(ytPlayer);
initSponsorBlocker(ytPlayer);

// Update button labels
updateButtonLabels();
Expand All @@ -88,8 +90,9 @@ async function changeVideo(id) {
// Updated YouTube embed URL with ad-blocking parameters
ytPlayer.src = `https://www.youtube.com/embed/${id}?autoplay=1&loop=1&playlist=${id}&rel=0&controls=1&iv_load_policy=3&modestbranding=1&enablejsapi=1&origin=${window.location.origin}`;

// Re-initialize the ad blocker for the new video
// Re-initialize blockers for the new video
setTimeout(() => initAdBlocker(ytPlayer), 500);
setTimeout(() => initSponsorBlocker(ytPlayer), 500);

await saveLastVideoIDToStorage(id);
setCurrentVideo(id);
Expand Down
Loading