diff --git a/js/adBlocker.js b/js/adBlocker.js index d4cce9a..e8c8708 100644 --- a/js/adBlocker.js +++ b/js/adBlocker.js @@ -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} - 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(); -})(); \ No newline at end of file +})(); diff --git a/js/music.js b/js/music.js index cc3477e..66dabc1 100644 --- a/js/music.js +++ b/js/music.js @@ -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; @@ -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(); @@ -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); diff --git a/js/sponsorBlocker.js b/js/sponsorBlocker.js new file mode 100644 index 0000000..8aa1543 --- /dev/null +++ b/js/sponsorBlocker.js @@ -0,0 +1,134 @@ +// SponsorBlock integration for YouTube iframes +// This module fetches SponsorBlock segments for the current video +// and skips them during playback. + +import storageService from './storage.js'; + +let sponsorBlockEnabled = true; +let apiLoading = false; +let apiReady = false; +const pendingIframes = new Set(); +const players = new WeakMap(); + +const SEGMENT_CATEGORIES = [ + 'sponsor', + 'selfpromo', + 'interaction', + 'intro', + 'outro', + 'preview', + 'music_offtopic', + 'filler' +]; + +function loadYouTubeAPI() { + if (apiReady || apiLoading || window.YT) { + waitForAPI(); + return; + } + apiLoading = true; + const tag = document.createElement('script'); + tag.src = 'https://www.youtube.com/iframe_api'; + document.head.appendChild(tag); + waitForAPI(); +} + +function waitForAPI() { + if (window.YT && window.YT.Player) { + apiReady = true; + pendingIframes.forEach(ifr => setupPlayer(ifr)); + pendingIframes.clear(); + return; + } + setTimeout(waitForAPI, 100); +} + +export function initSponsorBlocker(iframe) { + if (!sponsorBlockEnabled || !iframe) return; + loadYouTubeAPI(); + if (apiReady && window.YT && window.YT.Player) { + setupPlayer(iframe); + } else { + pendingIframes.add(iframe); + } +} + +function setupPlayer(iframe) { + if (players.has(iframe)) return; + + const player = iframe._adBlockerPlayer || new YT.Player(iframe); + const data = { player, segments: [], checkInterval: null, currentId: null }; + players.set(iframe, data); + + player.addEventListener('onStateChange', () => onStateChange(iframe)); + + if (player.getPlayerState && player.getPlayerState() === YT.PlayerState.PLAYING) { + onStateChange(iframe); + } +} + +function onStateChange(iframe) { + const data = players.get(iframe); + if (!data) return; + const state = data.player.getPlayerState(); + if (state === YT.PlayerState.PLAYING) { + const vid = data.player.getVideoData().video_id; + if (vid && vid !== data.currentId) { + data.currentId = vid; + fetchSegments(vid).then(segments => { data.segments = segments; }); + } + startChecking(iframe); + } else { + stopChecking(iframe); + } +} + +function startChecking(iframe) { + const data = players.get(iframe); + if (!data || data.checkInterval) return; + data.checkInterval = setInterval(() => { + const time = data.player.getCurrentTime(); + for (const seg of data.segments) { + const [start, end] = seg.segment; + if (time >= start && time < end) { + data.player.seekTo(end, true); + break; + } + } + }, 500); +} + +function stopChecking(iframe) { + const data = players.get(iframe); + if (data && data.checkInterval) { + clearInterval(data.checkInterval); + data.checkInterval = null; + } +} + +async function fetchSegments(videoId) { + try { + const cats = encodeURIComponent(JSON.stringify(SEGMENT_CATEGORIES)); + const url = `https://sponsor.ajay.app/api/skipSegments?videoID=${videoId}&categories=${cats}&actionTypes=[\"skip\",\"mute\"]`; + const res = await fetch(url); + if (!res.ok) return []; + const segments = await res.json(); + return Array.isArray(segments) ? segments : []; + } catch { + return []; + } +} + +export function setSponsorBlockEnabled(enabled) { + sponsorBlockEnabled = enabled; + storageService.setItem('sponsorBlockEnabled', enabled); +} + +export async function isSponsorBlockEnabled() { + const saved = await storageService.getItem('sponsorBlockEnabled'); + return saved === null ? true : saved === 'true'; +} + +(async () => { + sponsorBlockEnabled = await isSponsorBlockEnabled(); +})();