feat: full HLS.js support — detection via interceptors + deep probe, playback via hls.js in viewer #2
No files matched your search
@@ -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: ['<all_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);
|
||||
}
|
||||
|
||||
+384
-51
@@ -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 <video> element with no .url property. Real players store the
|
||||
// HLS instance directly on the video element as a custom property.
|
||||
// Only check non-standard properties (skip built-in <video> attributes).
|
||||
// Standard <video> element attributes — skip these when scanning for custom
|
||||
// HLS player properties. Define here so it's available for Strategy A2 onward.
|
||||
const STANDARD_VIDEO_ATTRS = new Set([
|
||||
'src', 'srcObject', 'crossOrigin', 'preload', 'autoplay', 'muted',
|
||||
'defaultMuted', 'loop', 'controls', 'controlsList', 'disableRemotePlayback',
|
||||
@@ -183,63 +178,205 @@ function m3u8DeepProbeFn() {
|
||||
'classList', 'style', 'attributes', 'dataset', 'innerHTML', 'textContent',
|
||||
'outerHTML', 'outerText', 'slot', 'accessKey', 'draggable', 'hidden',
|
||||
'lang', 'spellcheck', 'tabIndex', 'title', 'translate',
|
||||
'width', 'height', 'poster', 'preload', 'referrerPolicy',
|
||||
'width', 'height', 'poster', 'referrerPolicy',
|
||||
]);
|
||||
|
||||
// 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',
|
||||
];
|
||||
let strategyACount = 0;
|
||||
for (const name of playerGlobals) {
|
||||
const obj = window[name];
|
||||
if (!obj || typeof obj === 'function') continue;
|
||||
probeLog('Strategy A: found global', name, '(not function)');
|
||||
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}` }); strategyACount++; }
|
||||
}
|
||||
}
|
||||
walk(obj, name, 0);
|
||||
}
|
||||
probeLog('Strategy A (player globals): found', strategyACount, 'new manifests');
|
||||
|
||||
// Strategy A2: Hls instances — iterate all video elements and check if any
|
||||
// HLS player instance (created via new Hls()) is bound to them. Even if our
|
||||
// attachMedia patch missed the call (timing race), the player object still
|
||||
// exists on the video element with a .url property.
|
||||
let strategyA2Count = 0;
|
||||
try {
|
||||
for (const v of document.querySelectorAll('video')) {
|
||||
// Check all custom properties on the video for HLS-like instances
|
||||
for (const key of Object.keys(v)) {
|
||||
if (STANDARD_VIDEO_ATTRS.has(key)) continue;
|
||||
const obj = v[key];
|
||||
if (!obj || typeof obj !== 'object') continue;
|
||||
// HLS instances have .url, .media, .config, .networkControls, etc.
|
||||
if (typeof obj.url === 'string' && isManifestUrl(obj.url)) {
|
||||
const resolved = resolveUrl(obj.url);
|
||||
if (resolved && !seen.has(resolved)) {
|
||||
seen.add(resolved);
|
||||
manifests.push({ url: resolved, player: `video.${key}.url (Hls instance)` });
|
||||
strategyA2Count++;
|
||||
probeLog('Strategy A2: found Hls instance on video.', key, '- url:', obj.url.substring(0, 100));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch(e) { /* ignore */ }
|
||||
probeLog('Strategy A2 (Hls instances on video): found', strategyA2Count, 'new manifests');
|
||||
|
||||
// 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 <video> element with no .url property. Real players store the
|
||||
// HLS instance directly on the video element as a custom property.
|
||||
// Only check non-standard properties (skip built-in <video> attributes).
|
||||
const allVideos = document.querySelectorAll('video');
|
||||
probeLog('Strategy B: found', allVideos.length, '<video> elements');
|
||||
let strategyBCount = 0;
|
||||
for (const v of allVideos) {
|
||||
for (const key of Object.keys(v)) {
|
||||
if (STANDARD_VIDEO_ATTRS.has(key)) continue;
|
||||
const obj = v[key];
|
||||
if (!obj || typeof obj !== 'object') continue;
|
||||
// Check for common HLS instance patterns on video elements
|
||||
if (typeof obj === 'object' && typeof obj.url === 'string' && isManifestUrl(obj.url)) {
|
||||
probeLog('Strategy B1: found HLS instance on video.', key, '- url:', obj.url.substring(0, 100));
|
||||
const resolved = resolveUrl(obj.url);
|
||||
if (resolved && !seen.has(resolved)) { seen.add(resolved); manifests.push({ url: resolved, player: `video.${key}.url` }); }
|
||||
if (resolved && !seen.has(resolved)) { seen.add(resolved); manifests.push({ url: resolved, player: `video.${key}.url` }); strategyBCount++; }
|
||||
}
|
||||
// Walk deep into player objects attached to video elements
|
||||
walk(obj, `video.${key}`, 0);
|
||||
}
|
||||
}
|
||||
probeLog('Strategy B1 (video custom props): found', strategyBCount, 'new manifests');
|
||||
|
||||
// B2: Check interceptor-set manifest annotation on video elements
|
||||
let strategyB2Count = 0;
|
||||
for (const v of document.querySelectorAll('video')) {
|
||||
if (v._goon_hls_manifest && !seen.has(v._goon_hls_manifest)) {
|
||||
seen.add(v._goon_hls_manifest);
|
||||
manifests.push({ url: v._goon_hls_manifest, player: 'video._goon_hls_manifest' });
|
||||
strategyB2Count++;
|
||||
probeLog('Strategy B2: found _goon_hls_manifest on video:', v._goon_hls_manifest.substring(0, 100));
|
||||
}
|
||||
}
|
||||
probeLog('Strategy B2 (interceptor annotation): found', strategyB2Count, 'new manifests');
|
||||
|
||||
// Strategy C: Inline scripts
|
||||
let strategyCCount = 0;
|
||||
for (const script of document.querySelectorAll('script:not([src])')) {
|
||||
const text = script.textContent || '';
|
||||
const matches = text.match(/https?:\/\/[^"'\s]+.(m3u8|hlsvod)[^"'\s]*/gi);
|
||||
if (matches) {
|
||||
for (const u of matches) {
|
||||
if (!seen.has(u)) { seen.add(u); manifests.push({ url: u, player: 'inline-script' }); }
|
||||
if (!seen.has(u)) { seen.add(u); manifests.push({ url: u, player: 'inline-script' }); strategyCCount++; }
|
||||
}
|
||||
}
|
||||
}
|
||||
probeLog('Strategy C (inline scripts): found', strategyCCount, 'new manifests');
|
||||
|
||||
// Strategy D: Config globals
|
||||
const configKeys = ['__dataLayer', '__INITIAL_STATE__', 'config', '__NEXT_DATA__', '__PRELOADED_STATE__', 'appData', 'pageData', '__data'];
|
||||
let preDCount = manifests.length;
|
||||
for (const name of configKeys) {
|
||||
const obj = window[name];
|
||||
if (obj && typeof obj === 'object') walk(obj, name, 0);
|
||||
if (obj && typeof obj === 'object') {
|
||||
probeLog('Strategy D: walking config global', name);
|
||||
walk(obj, name, 0);
|
||||
}
|
||||
}
|
||||
probeLog('Strategy D (config globals): found', manifests.length - preDCount, 'new manifests');
|
||||
|
||||
// Strategy E: Interceptor-collected manifests — the most reliable source.
|
||||
// The main-world interceptors (injected at document_start) capture .m3u8 URLs
|
||||
// from XHR, fetch, and Hls.prototype.loadSource/attachMedia calls, and store
|
||||
// them in window.__goon_plugin_data.manifests[]. This was the original
|
||||
// Strategy 3/6 on master that was accidentally dropped during refactoring.
|
||||
// For dynamic playlists, the interceptors also poll Hls instances via Hls.cast()
|
||||
// and extract internal state (levels, url) — these URLs may not end in .m3u8.
|
||||
let preECount = manifests.length;
|
||||
if (window.__goon_plugin_data && Array.isArray(window.__goon_plugin_data.manifests)) {
|
||||
probeLog('Strategy E: __goon_plugin_data has', window.__goon_plugin_data.manifests.length, 'interceptor manifests');
|
||||
for (const url of window.__goon_plugin_data.manifests) {
|
||||
if (url && typeof url === 'string' && !seen.has(url)) {
|
||||
seen.add(url);
|
||||
manifests.push({ url, player: '__goon_plugin_data' });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
probeLog('Strategy E: __goon_plugin_data missing or has no manifests array', {
|
||||
hasData: !!window.__goon_plugin_data,
|
||||
hasArray: window.__goon_plugin_data ? Array.isArray(window.__goon_plugin_data.manifests) : false,
|
||||
});
|
||||
}
|
||||
probeLog('Strategy E (interceptor data): found', manifests.length - preECount, 'new manifests');
|
||||
|
||||
// Strategy F: Dynamic playlist fallback — scan video elements for Hls-like
|
||||
// instances and extract ANY URL, not just .m3u8. Sites like Pornhub use
|
||||
// HLS.js dynamic playlists where the "manifest" is generated in-memory,
|
||||
// so the URL might be a blob:, CDN endpoint, or data URI.
|
||||
let strategyFCount = 0;
|
||||
try {
|
||||
for (const v of document.querySelectorAll('video')) {
|
||||
for (const key of Object.keys(v)) {
|
||||
if (STANDARD_VIDEO_ATTRS.has(key)) continue;
|
||||
const obj = v[key];
|
||||
if (!obj || typeof obj !== 'object') continue;
|
||||
// Look for Hls-like objects: have .url, .levels, .attachMedia, etc.
|
||||
const isHlsLike = (typeof obj.url === 'string' && obj.url.length > 5) ||
|
||||
(obj.levels && Array.isArray(obj.levels)) ||
|
||||
(typeof obj.attachMedia === 'function') ||
|
||||
(typeof obj.loadSource === 'function');
|
||||
if (!isHlsLike) continue;
|
||||
// Extract URL without isManifestUrl filter
|
||||
if (typeof obj.url === 'string' && obj.url.length > 5 && !seen.has(obj.url)) {
|
||||
const resolved = resolveUrl(obj.url) || obj.url;
|
||||
// Skip obvious media segments
|
||||
const u = resolved.toLowerCase();
|
||||
if (u.includes('.ts') || u.includes('.m4s') || u.includes('.aac')) continue;
|
||||
seen.add(resolved);
|
||||
manifests.push({ url: resolved, player: `video.${key}.url (dynamic HLS)` });
|
||||
strategyFCount++;
|
||||
probeLog('Strategy F: found dynamic HLS URL on video.', key, ':', resolved.substring(0, 150));
|
||||
}
|
||||
// Extract level URIs from levels array
|
||||
if (obj.levels && Array.isArray(obj.levels)) {
|
||||
for (let li = 0; li < obj.levels.length; li++) {
|
||||
const lvl = obj.levels[li];
|
||||
if (lvl && lvl.uri && !seen.has(lvl.uri)) {
|
||||
const resolved = resolveUrl(lvl.uri) || lvl.uri;
|
||||
seen.add(resolved);
|
||||
manifests.push({ url: resolved, player: `video.${key}.levels[${li}].uri (dynamic)` });
|
||||
strategyFCount++;
|
||||
probeLog('Strategy F: found level URI:', resolved.substring(0, 120));
|
||||
}
|
||||
// Also check details.uri (populated after playlist load)
|
||||
if (lvl && lvl.details && lvl.details.uri && !seen.has(lvl.details.uri)) {
|
||||
const resolved = resolveUrl(lvl.details.uri) || lvl.details.uri;
|
||||
seen.add(resolved);
|
||||
manifests.push({ url: resolved, player: `video.${key}.levels[${li}].details.uri (dynamic)` });
|
||||
strategyFCount++;
|
||||
probeLog('Strategy F: found level details.uri:', resolved.substring(0, 120));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch(e) { /* ignore */ }
|
||||
probeLog('Strategy F (dynamic HLS fallback): found', strategyFCount, 'new manifests');
|
||||
|
||||
probeLog('Deep probe complete. Total unique manifests found:', manifests.length);
|
||||
if (manifests.length > 0) {
|
||||
probeLog('Manifest summary:', JSON.stringify(manifests.map(m => ({ url: m.url.substring(0, 100), player: m.player }))));
|
||||
}
|
||||
return manifests;
|
||||
}
|
||||
|
||||
@@ -387,14 +524,21 @@ function filterValidTabs(tabs) {
|
||||
*/
|
||||
async function probeHlsManifests(tabId) {
|
||||
try {
|
||||
const results = await chrome.scripting.executeScript({
|
||||
target: { tabId, allFrames: true, world: 'MAIN' },
|
||||
log('probeHlsManifests: executing deep probe on tab', tabId);
|
||||
const raw = await chrome.scripting.executeScript({
|
||||
target: { tabId, allFrames: true },
|
||||
func: m3u8DeepProbeFn,
|
||||
});
|
||||
// When allFrames is true, destroyed frames may return null entries.
|
||||
// Filter them out before accessing .result to avoid TypeError.
|
||||
return results.filter(r => r && typeof r === 'object').flatMap(r => r.result || []);
|
||||
} catch {
|
||||
// When allFrames is true, destroyed / sandboxed frames may return null entries.
|
||||
// Guard: ensure raw is an array, then filter to objects with a defined .result.
|
||||
const manifests = Array.isArray(raw)
|
||||
? raw.filter(r => r != null && typeof r === 'object' && 'result' in r)
|
||||
.flatMap(r => Array.isArray(r.result) ? r.result : [])
|
||||
: [];
|
||||
log('probeHlsManifests: tab', tabId, 'returned', manifests.length, 'manifests from', Array.isArray(raw) ? raw.length : '?', 'frame results');
|
||||
return manifests;
|
||||
} catch(e) {
|
||||
warn('probeHlsManifests: tab', tabId, 'error:', e && e.message ? e.message : e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -440,6 +584,44 @@ async function queryContentScript(tabId) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the service worker for webRequest-captured HLS manifests.
|
||||
* The service worker listens to chrome.webRequest.onBeforeRequest and
|
||||
* records all .m3u8 / HLS manifest URLs per tab. This works at the
|
||||
* network level — no world or timing issues.
|
||||
* @param {number} tabId
|
||||
* @returns {Promise<Array>} array of manifest URLs
|
||||
*/
|
||||
async function queryWebRequestManifests(tabId) {
|
||||
try {
|
||||
const resp = await chrome.runtime.sendMessage({
|
||||
type: 'GOON_GET_WEBREQUEST_MANIFESTS',
|
||||
tabId: tabId,
|
||||
});
|
||||
return resp?.manifests || [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the service worker for all phncdn.com requests captured by Strategy 8.
|
||||
* Returns segments + reconstructed manifest URLs from segment patterns.
|
||||
* @param {number} tabId
|
||||
* @returns {Promise<{segments: Array, reconstructedManifests: Array, totalRequests: number}>}
|
||||
*/
|
||||
async function queryPhncdnRequests(tabId) {
|
||||
try {
|
||||
const resp = await chrome.runtime.sendMessage({
|
||||
type: 'GOON_GET_PHNCDN_REQUESTS',
|
||||
tabId: tabId,
|
||||
});
|
||||
return resp || { segments: [], reconstructedManifests: [], totalRequests: 0 };
|
||||
} catch {
|
||||
return { segments: [], reconstructedManifests: [], totalRequests: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrape video elements from ALL frames in a tab.
|
||||
* On sites like Pornhub, the video player lives inside an iframe — main-frame-only
|
||||
@@ -457,20 +639,21 @@ async function queryContentScript(tabId) {
|
||||
*/
|
||||
async function scrapeTab(tabId, manifests) {
|
||||
try {
|
||||
const results = await chrome.scripting.executeScript({
|
||||
target: { tabId, allFrames: true, world: 'MAIN' },
|
||||
const raw = await chrome.scripting.executeScript({
|
||||
target: { tabId, allFrames: true },
|
||||
func: scrapeFn,
|
||||
args: [manifests],
|
||||
});
|
||||
// When allFrames is true, destroyed frames may return null entries.
|
||||
// Filter them out before accessing .result to avoid TypeError.
|
||||
const valid = results
|
||||
.filter(r => r && typeof r === 'object')
|
||||
// When allFrames is true, destroyed / sandboxed frames may return null entries.
|
||||
// Guard: ensure raw is an array, then filter to objects with a defined .result.
|
||||
const frameResults = Array.isArray(raw)
|
||||
? raw.filter(r => r != null && typeof r === 'object' && 'result' in r)
|
||||
.map(r => r.result)
|
||||
.filter(r => r && r.videos && r.videos.length > 0);
|
||||
: [];
|
||||
const valid = frameResults.filter(r => r && r.videos && r.videos.length > 0);
|
||||
if (valid.length === 0) {
|
||||
const firstValid = results.find(r => r && typeof r === 'object');
|
||||
return firstValid?.result || null;
|
||||
const firstWithResult = frameResults.find(r => r != null);
|
||||
return firstWithResult || null;
|
||||
}
|
||||
valid.sort((a, b) => {
|
||||
const aArea = a.videos[0]?.area || 0;
|
||||
@@ -478,8 +661,8 @@ async function scrapeTab(tabId, manifests) {
|
||||
return bArea - aArea;
|
||||
});
|
||||
return valid[0];
|
||||
} catch {
|
||||
warn('skipping tab', tabId);
|
||||
} catch(e) {
|
||||
warn('scrapeTab: tab', tabId, 'error:', e && e.message ? e.message : e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -561,18 +744,70 @@ function makeVideoEntry(video, tabTitle, posterFromContent) {
|
||||
* @returns {Promise<{ items: Array, scanned: number, skipped: number }>}
|
||||
*/
|
||||
async function scanVideos(tabsToScan) {
|
||||
log('scanVideos: starting scan of', tabsToScan.length, 'tabs');
|
||||
await logToPage(`scanVideos: starting scan of ${tabsToScan.length} tabs`);
|
||||
|
||||
// Step 1: Probe for HLS manifests in all frames
|
||||
const manifestPromises = tabsToScan.map(t => probeHlsManifests(t.id));
|
||||
const allManifestResults = await Promise.all(manifestPromises);
|
||||
|
||||
// Build per-tab manifest map (deduped)
|
||||
const tabManifestMap = new Map();
|
||||
let totalManifests = 0;
|
||||
for (let i = 0; i < tabsToScan.length; i++) {
|
||||
const unique = dedupManifests(allManifestResults[i]);
|
||||
if (unique.length > 0) {
|
||||
const tabUrl = tabsToScan[i].url ? tabsToScan[i].url.substring(0, 80) : '(unknown)';
|
||||
log('scanVideos step1: tab', tabsToScan[i].id, '(' + tabUrl + ') has', unique.length, 'unique manifests');
|
||||
totalManifests += unique.length;
|
||||
tabManifestMap.set(tabsToScan[i].id, unique);
|
||||
}
|
||||
await logToPage(`step1 (probe): found ${totalManifests} manifests across ${tabsToScan.length} tabs`);
|
||||
|
||||
// Step 1.5: Query service worker for webRequest-captured HLS manifests.
|
||||
// This catches manifests at the network level — no world or timing issues.
|
||||
// Works even when the player loads before our interceptors are active.
|
||||
const webRequestPromises = tabsToScan.map(t => queryWebRequestManifests(t.id));
|
||||
const webRequestResults = await Promise.all(webRequestPromises);
|
||||
let totalWebRequestManifests = 0;
|
||||
for (let i = 0; i < tabsToScan.length; i++) {
|
||||
const urls = webRequestResults[i];
|
||||
const existing = tabManifestMap.get(tabsToScan[i].id) || [];
|
||||
for (const url of urls) {
|
||||
if (!existing.some(m => m.url === url)) {
|
||||
existing.push({ url, player: 'webRequest' });
|
||||
}
|
||||
}
|
||||
tabManifestMap.set(tabsToScan[i].id, existing);
|
||||
const newCount = urls.length;
|
||||
totalWebRequestManifests += newCount;
|
||||
if (newCount > 0) {
|
||||
log('scanVideos webRequest: tab', tabsToScan[i].id, 'found', newCount, 'manifests via network interception');
|
||||
}
|
||||
}
|
||||
await logToPage(`step1.5 (webRequest): found ${totalWebRequestManifests} manifests via network interception`);
|
||||
|
||||
// Step 1.6: Strategy 8 — Query phncdn.com requests and reconstruct manifest URLs.
|
||||
// When the player uses in-memory dynamic HLS (zero .m3u8 network requests), we can
|
||||
// still detect it by recognizing segment URL patterns from phncdn.com and deriving
|
||||
// the parent manifest URL: /videos/{id}/playlist.m3u8
|
||||
const phncdnPromises = tabsToScan.map(t => queryPhncdnRequests(t.id));
|
||||
const phncdnResults = await Promise.all(phncdnPromises);
|
||||
let totalPhncdnManifests = 0;
|
||||
for (let i = 0; i < tabsToScan.length; i++) {
|
||||
const reconstructed = phncdnResults[i]?.reconstructedManifests || [];
|
||||
if (reconstructed.length > 0) {
|
||||
log('scanVideos phncdn: tab', tabsToScan[i].id, 'found', reconstructed.length, 'reconstructed manifests from segment URLs');
|
||||
}
|
||||
totalPhncdnManifests += reconstructed.length;
|
||||
const existing = tabManifestMap.get(tabsToScan[i].id) || [];
|
||||
for (const url of reconstructed) {
|
||||
if (!existing.some(m => m.url === url)) {
|
||||
existing.push({ url, player: 'phncdn_reconstructed' });
|
||||
}
|
||||
}
|
||||
tabManifestMap.set(tabsToScan[i].id, existing);
|
||||
}
|
||||
await logToPage(`step1.6 (phncdn): found ${totalPhncdnManifests} reconstructed manifests from segment URLs`);
|
||||
|
||||
// Step 2: Scrape video elements (all frames) AND query content scripts (in parallel)
|
||||
const scrapePromises = tabsToScan.map(t =>
|
||||
@@ -584,6 +819,24 @@ async function scanVideos(tabsToScan) {
|
||||
Promise.all(contentPromises),
|
||||
]);
|
||||
|
||||
// Log scrape + content script results for debugging
|
||||
let totalScrapedVideos = 0;
|
||||
let totalInterceptedManifests = 0;
|
||||
for (let i = 0; i < tabsToScan.length; i++) {
|
||||
const tabUrl = tabsToScan[i].url ? tabsToScan[i].url.substring(0, 80) : '(unknown)';
|
||||
const scrape = scrapeResults[i];
|
||||
const content = contentResults[i];
|
||||
const scrapeVideos = scrape ? (scrape.videos ? scrape.videos.length : 0) : 0;
|
||||
const intercepts = content ? (content.interceptedManifests ? content.interceptedManifests.length : 0) : 0;
|
||||
totalScrapedVideos += scrapeVideos;
|
||||
totalInterceptedManifests += intercepts;
|
||||
log('scanVideos step2: tab', tabsToScan[i].id, '(' + tabUrl + '):',
|
||||
'scrape=' + (scrape ? (scrape.videos ? scrape.videos.length + ' videos' : 'no videos') : 'null'),
|
||||
'content=' + (content ? ('poster=' + !!content.posterUrl + ', intercepts=' + intercepts + ', videoEls=' + (content.videoElements ? content.videoElements.length : 0)) : 'null'),
|
||||
);
|
||||
}
|
||||
await logToPage(`step2 (scrape+content): ${totalScrapedVideos} scraped videos, ${totalInterceptedManifests} intercepted manifests`);
|
||||
|
||||
// Step 3: Merge content-script data into tab results
|
||||
// - Enrich video entries with poster URLs from the content script
|
||||
// - Merge content-script intercepted manifests into the deep probe results
|
||||
@@ -622,6 +875,7 @@ async function scanVideos(tabsToScan) {
|
||||
const poster = contentData?.posterUrl || null;
|
||||
const filteredManifests = filterManifests(tabManifests);
|
||||
if (filteredManifests.length > 0) {
|
||||
log('scanVideos fallback: tab', tab.id, 'no visible videos but found', filteredManifests.length, 'manifests — creating fallback entries');
|
||||
const fallbackVideos = filteredManifests.map(m => ({
|
||||
src: m.url,
|
||||
poster: poster,
|
||||
@@ -633,6 +887,7 @@ async function scanVideos(tabsToScan) {
|
||||
result: { videos: fallbackVideos },
|
||||
};
|
||||
}
|
||||
log('scanVideos: tab', tab.id, 'has no videos and no manifests — skipping');
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -640,7 +895,7 @@ async function scanVideos(tabsToScan) {
|
||||
tabTitle: tab.title || 'Unknown',
|
||||
result,
|
||||
};
|
||||
}).filter(g => g.result && g.result.videos?.length > 0);
|
||||
}).filter(g => g != null && g.result && g.result.videos?.length > 0);
|
||||
|
||||
// Step 4: Dedup across tabs
|
||||
const seen = new Map();
|
||||
@@ -657,6 +912,11 @@ async function scanVideos(tabsToScan) {
|
||||
const items = [...seen.values()];
|
||||
const skipped = scrapeResults.filter(r => r === null).length;
|
||||
|
||||
log('scanVideos complete: scanned=', tabResults.length, 'skipped=', skipped, 'unique items=', items.length);
|
||||
if (items.length > 0) {
|
||||
log('Final items:', JSON.stringify(items.map(v => ({ src: (v.src || '').substring(0, 80), mediaType: v.mediaType, tab: v.tab }))));
|
||||
}
|
||||
await logToPage(`scan complete: ${items.length} unique items from ${tabResults.length} tabs (skipped: ${skipped})`);
|
||||
return { items, scanned: tabResults.length, skipped };
|
||||
}
|
||||
|
||||
@@ -700,6 +960,79 @@ findBtn.addEventListener('click', async () => {
|
||||
const msg = `Found ${items.length} video(s) across ${scanned} tab(s).`;
|
||||
setStatus(skipped > 0 ? `${msg} ${skipped} tab(s) skipped (idle).` : msg);
|
||||
btnRow.style.display = 'flex';
|
||||
|
||||
// If no videos found, run diagnostics in the active tab
|
||||
if (items.length === 0) {
|
||||
const activeTab = validTabs.find(t => t.active) || validTabs[0];
|
||||
if (activeTab) {
|
||||
try {
|
||||
await chrome.scripting.executeScript({
|
||||
target: { tabId: activeTab.id },
|
||||
func: () => {
|
||||
const diag = [];
|
||||
// Check interceptor data
|
||||
if (window.__goon_plugin_data) {
|
||||
diag.push(`__goon_plugin_data.manifests: ${window.__goon_plugin_data.manifests.length} URLs`);
|
||||
if (window.__goon_plugin_data.manifests.length > 0) {
|
||||
diag.push(` First: ${window.__goon_plugin_data.manifests[0].substring(0, 120)}`);
|
||||
}
|
||||
} else {
|
||||
diag.push('__goon_plugin_data: NOT FOUND — interceptors may not have loaded');
|
||||
}
|
||||
// Check Hls global
|
||||
if (typeof Hls !== 'undefined') {
|
||||
diag.push(`Hls global: defined`);
|
||||
if (typeof Hls.cast === 'function') {
|
||||
diag.push(`Hls.cast: available`);
|
||||
}
|
||||
} else {
|
||||
diag.push(`Hls global: NOT DEFINED`);
|
||||
}
|
||||
// Check video elements
|
||||
const videos = document.querySelectorAll('video');
|
||||
diag.push(`Video elements: ${videos.length}`);
|
||||
for (let vi = 0; vi < videos.length; vi++) {
|
||||
const v = videos[vi];
|
||||
const style = window.getComputedStyle(v);
|
||||
const isVisible = style.display !== 'none' && v.clientWidth > 0 && v.clientHeight > 0;
|
||||
const src = (v.currentSrc || v.src || '').substring(0, 120);
|
||||
diag.push(` Video[${vi}]: src="${src}", visible=${isVisible}, _goon_hls_manifest=${v._goon_hls_manifest || 'null'}`);
|
||||
// Check custom properties
|
||||
const customProps = [];
|
||||
for (const key of Object.keys(v)) {
|
||||
if (typeof v[key] === 'object' && v[key] && typeof v[key].url === 'string') {
|
||||
customProps.push(`${key}.url="${v[key].url.substring(0, 80)}"`);
|
||||
}
|
||||
}
|
||||
if (customProps.length > 0) {
|
||||
diag.push(` Custom props with .url: ${customProps.join(', ')}`);
|
||||
}
|
||||
}
|
||||
// Check known player globals
|
||||
const playerNames = ['hls', 'Hls', 'player', 'videoPlayer', 'mainPlayer', 'PHPlayer', '__phPlayer', '__ph', 'fhplayer', '__pj', 'bitgravity', 'bgPlayer', 'FG', 'Fhls'];
|
||||
for (const name of playerNames) {
|
||||
if (window[name] && typeof window[name] !== 'function') {
|
||||
const obj = window[name];
|
||||
const urlVal = obj.url || obj._url || obj.src || obj.manifestUrl || obj.hlsUrl;
|
||||
if (typeof urlVal === 'string' && urlVal) {
|
||||
diag.push(`Global ${name}.url-like: "${urlVal.substring(0, 120)}"`);
|
||||
} else {
|
||||
diag.push(`Global ${name}: exists but no obvious URL property. Keys: ${Object.keys(obj).slice(0, 20).join(', ')}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
console.group('[goon-diag] HLS diagnostics');
|
||||
for (const line of diag) {
|
||||
console.log(line);
|
||||
}
|
||||
console.groupEnd();
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('[goon-diag] Diagnostic failed:', e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
setStatus(`Scan failed: ${e.message}`);
|
||||
warn('executeScript failed:', e);
|
||||
|
||||
Reference in new issue
Block a user