diff --git a/src/background/background.js b/src/background/background.js index 35cf325..4789a18 100644 --- a/src/background/background.js +++ b/src/background/background.js @@ -4,22 +4,178 @@ * Uses Manifest V3 module-type service worker. */ -// Fired when the extension is first installed / updated / Chrome starts. -chrome.runtime.onInstalled.addListener((details) => { - if (details.reason === 'install') { - console.log('[goon-plugin] Extension installed'); - } else if (details.reason === 'update') { - console.log(`[goon-plugin] Extension updated from ${details.previousVersion}`); +// === HLS manifest tracking via chrome.webRequest === +// Intercepts network requests at the browser level — no world/timing issues. +// Catches .m3u8 manifest requests from ANY player (HLS.js, Video.js, custom). +// +// IMPORTANT: MV3 service workers are ephemeral — they are killed when idle and +// restarted on demand. In-memory state (Map, Set, etc.) is LOST on kill. +// Therefore, we persist captured manifests to chrome.storage.local so they +// survive service worker restarts. + +function isHlsManifest(url) { + const u = url.toLowerCase(); + // Exclude media segments + if (u.includes('.ts') || u.includes('.mp4') || u.includes('.webm') || + u.includes('.aac') || u.includes('.m4s')) return false; + return u.includes('.m3u8') || u.includes('.hlsvod') || + (u.includes('/hls/') && u.includes('.m3u')) || + u.includes('/playlist.m3u') || u.includes('/manifest'); +} + +// === STRATEGY 8: Network-level fallback — log ALL phncdn.com and video-related requests === +// Pornhub's es6player generates HLS manifests in-memory with zero .m3u8 network requests. +// However, the player still fetches video segments from phncdn.com. By logging all phncdn +// requests we can: (a) confirm HLS activity via segment patterns, (b) reconstruct manifest +// URLs from segment paths, (c) detect CDN response headers that reveal playlist endpoints. + +const PHNCDN_HOSTS = ['phncdn.com', 'pornhubpremium.com', 'cdn.pornhub.com']; +const VIDEO_PATH_PATTERNS = ['/hls/', '/dash/', '/video/', '/segment', '/media/', '/stream']; + +function isVideoRequest(url) { + const u = url.toLowerCase(); + // Already a known video/manifest extension + if (u.includes('.m3u8') || u.includes('.mpd') || u.includes('.hlsvod') || + u.includes('.ts') || u.includes('.m4s') || u.includes('.mp4')) return true; + // Path-based detection for in-memory playlists + return VIDEO_PATH_PATTERNS.some(p => u.includes(p)); +} + +// Persist all video requests per tab (survives SW restarts) +async function persistVideoRequest(tabId, url) { + const key = 'vr_requests_' + tabId; + try { + const stored = await chrome.storage.local.get(key); + const arr = stored[key] || []; + // Avoid duplicates — phncdn segments come in rapid succession + if (!arr.some(r => r === url)) { + arr.push(url); + // Keep last 200 requests per tab (enough to detect segment patterns) + if (arr.length > 200) arr.splice(0, arr.length - 200); + await chrome.storage.local.set({ [key]: arr }); + } + } catch(e) { + console.warn('[goon-bg] Failed to persist video request:', e && e.message ? e.message : e); } +} + +// Reconstruct manifest URL from segment URLs +// Segment: https://phncdn.com/videos/123456/seg-0-v1-a1.ts +// Manifest: https://phncdn.com/videos/123456/playlist.m3u8 +function reconstructManifestsFromSegments(segments) { + const seen = new Set(); + const manifests = []; + for (const url of segments) { + if (!url.includes('/videos/') || !url.includes('/seg-')) continue; + try { + const parsed = new URL(url); + const parts = parsed.pathname.split('/'); + // Find pattern: .../videos/{videoId}/seg-{n}... + const vidIdx = parts.findIndex(p => p === 'videos'); + if (vidIdx === -1 || vidIdx + 2 >= parts.length) continue; + const videoId = parts[vidIdx + 1]; + // Build manifest URL: same origin, /videos/{videoId}/playlist.m3u8 + const manifestUrl = `${parsed.protocol}//${parsed.host}/videos/${videoId}/playlist.m3u8`; + if (!seen.has(manifestUrl)) { + seen.add(manifestUrl); + manifests.push(manifestUrl); + } + } catch { /* skip malformed */ } + } + return manifests; +} + +chrome.webRequest.onBeforeRequest.addListener( + async (details) => { + if (details.tabId < 0) return; + + const u = details.url.toLowerCase(); + const isPhncdn = PHNCDN_HOSTS.some(host => details.url.includes(host)); + const isVideo = isVideoRequest(details.url); + + // Log ALL phncdn.com requests regardless of path + if (isPhncdn) { + console.log('[goon-bg] webRequest: phncdn request in tab', details.tabId, ':', details.url.substring(0, 180)); + await persistVideoRequest(details.tabId, details.url); + } + + // Log video-like requests from any host + if (isVideo && !isPhncdn) { + console.log('[goon-bg] webRequest: video request in tab', details.tabId, ':', details.url.substring(0, 150)); + } + + // Also capture HLS manifest URLs (original logic) + if (!isHlsManifest(details.url)) return; + + console.log('[goon-bg] webRequest: HLS manifest in tab', details.tabId, ':', details.url.substring(0, 120)); + + const key = 'wr_manifests_' + details.tabId; + try { + const stored = await chrome.storage.local.get(key); + const arr = stored[key] || []; + if (!arr.includes(details.url)) { + arr.push(details.url); + await chrome.storage.local.set({ [key]: arr }); + } + } catch(e) { + console.warn('[goon-bg] Failed to persist webRequest manifest:', e && e.message ? e.message : e); + } + }, + { urls: [''] } +); + +// Clean up stale entries when tabs are closed +chrome.tabs.onRemoved.addListener(async (tabId) => { + const keys = ['wr_manifests_' + tabId, 'vr_requests_' + tabId]; + try { + await chrome.storage.local.remove(keys); + } catch { /* ignore */ } }); -// Listen for messages from popup or content scripts. +// === Message handlers for popup/data consumers === + chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { switch (message.type) { case 'PING': sendResponse({ pong: true, timestamp: Date.now() }); return; + case 'GOON_GET_WEBREQUEST_MANIFESTS': { + (async () => { + const tabId = message.tabId; + const key = 'wr_manifests_' + tabId; + try { + const stored = await chrome.storage.local.get(key); + sendResponse({ manifests: stored[key] || [] }); + } catch(e) { + sendResponse({ manifests: [] }); + } + })(); + return true; + } + + case 'GOON_GET_PHNCDN_REQUESTS': { + // Strategy 8: Return all phncdn.com requests for a tab, including reconstructed manifests + (async () => { + const tabId = message.tabId; + const [segKey] = ['vr_requests_' + tabId]; + try { + const stored = await chrome.storage.local.get(segKey); + const segments = stored[segKey] || []; + // Also reconstruct manifest URLs from segment patterns + const reconstructed = reconstructManifestsFromSegments(segments); + sendResponse({ + segments: segments, + reconstructedManifests: reconstructed, + totalRequests: segments.length, + }); + } catch(e) { + sendResponse({ segments: [], reconstructedManifests: [], totalRequests: 0 }); + } + })(); + return true; + } + default: console.warn('[goon-plugin] Unknown message type:', message.type); } diff --git a/src/viewer/index.js b/src/viewer/index.js index 8003bb9..a6b4278 100644 --- a/src/viewer/index.js +++ b/src/viewer/index.js @@ -8,11 +8,25 @@ const STORAGE_KEY = 'goon_videos'; const TAGGED_KEY = 'goon_tagged'; // Set to true to enable verbose console output in the popup. -const DEBUG = false; +const DEBUG = true; // TEMP: enabled for debugging — revert to false when done function log(...args) { if (DEBUG) console.log('[goon]', ...args); } function warn(...args) { if (DEBUG) console.warn('[goon]', ...args); } +/** Log a message to the active tab's page console (visible to the user in DevTools). */ +async function logToPage(msg) { + if (!DEBUG) return; + try { + const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); + if (!tab) return; + await chrome.scripting.executeScript({ + target: { tabId: tab.id }, + func: (m) => { console.log('[goon-page]', m); }, + args: [msg], + }); + } catch { /* ignore — tab may have navigated away */ } +} + // UI references const findBtn = document.getElementById('find-btn'); const clearBtn = document.getElementById('clear-btn'); @@ -104,6 +118,12 @@ function m3u8DeepProbeFn() { const manifests = []; const seen = new Set(); + // DEBUG: Log probe activity to console + function probeLog(...args) { + try { console.log('[goon-probe]', ...args); } catch(e) {} + } + probeLog('Deep probe started on:', window.location.href, 'World:', typeof chrome !== 'undefined' ? 'ISOLATED' : 'MAIN'); + // Helpers available in the MAIN world context function resolveUrl(url) { if (!url) return null; @@ -145,33 +165,8 @@ function m3u8DeepProbeFn() { } catch { /* avoid circular ref errors */ } } - // Strategy A: Known player globals (shallow + deep) - const playerGlobals = [ - 'hls', 'Hls', 'videojs', 'Video', 'flowplayer', 'jwplayer', - '__hls', '__hls_player', 'player', 'videoPlayer', 'mainPlayer', - 'fhplayer', '__pj', 'playerInstance', - 'PHPlayer', 'fh', '__phPlayer', '__ph', 'phPlayer', 'phPlayerInstance', - 'bitgravity', 'bgPlayer', 'FG', 'Fhls', - ]; - for (const name of playerGlobals) { - const obj = window[name]; - if (!obj || typeof obj === 'function') continue; - for (const key of ['url', '_url', 'src', 'manifestUrl', 'hlsUrl', 'playlistUrl', 'hls_url', 'mp4_url']) { - if (typeof obj[key] === 'string' && isManifestUrl(obj[key])) { - const resolved = resolveUrl(obj[key]); - if (resolved && !seen.has(resolved)) { seen.add(resolved); manifests.push({ url: resolved, player: `${name}.${key}` }); } - } - } - walk(obj, name, 0); - } - - // Strategy B: Video element HLS instances - // B1: Scan video elements for HLS instance references stored by players. - // NOTE: Hls.cast() in hls.js is a TypeScript type-cast utility (identity function), - // NOT a reverse-lookup. It returns its input unchanged — Hls.cast(video) just - // returns the