feat: Strategy 8 — enhance phncdn.com network fallback detection
- Log ALL phncdn.com requests (not just .m3u8) to chrome.storage.local
- Reconstruct manifest URLs from segment URL patterns (/videos/{id}/playlist.m3u8)
- Add GOON_GET_PHNCDN_REQUESTS message handler for popup consumption
- Add queryPhncdnRequests() consumer in viewer/index.js
- Integrate reconstructed manifests into scan pipeline (step 1.6)
This commit is contained in:
1 parent
a1f4d2241b
commit
877feec731
2 files changed
+550
-61
No files matched your search
@@ -4,22 +4,178 @@
|
|||||||
* Uses Manifest V3 module-type service worker.
|
* Uses Manifest V3 module-type service worker.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Fired when the extension is first installed / updated / Chrome starts.
|
// === HLS manifest tracking via chrome.webRequest ===
|
||||||
chrome.runtime.onInstalled.addListener((details) => {
|
// Intercepts network requests at the browser level — no world/timing issues.
|
||||||
if (details.reason === 'install') {
|
// Catches .m3u8 manifest requests from ANY player (HLS.js, Video.js, custom).
|
||||||
console.log('[goon-plugin] Extension installed');
|
//
|
||||||
} else if (details.reason === 'update') {
|
// IMPORTANT: MV3 service workers are ephemeral — they are killed when idle and
|
||||||
console.log(`[goon-plugin] Extension updated from ${details.previousVersion}`);
|
// 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) => {
|
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case 'PING':
|
case 'PING':
|
||||||
sendResponse({ pong: true, timestamp: Date.now() });
|
sendResponse({ pong: true, timestamp: Date.now() });
|
||||||
return;
|
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:
|
default:
|
||||||
console.warn('[goon-plugin] Unknown message type:', message.type);
|
console.warn('[goon-plugin] Unknown message type:', message.type);
|
||||||
}
|
}
|
||||||
|
|||||||
+387
-54
@@ -8,11 +8,25 @@ const STORAGE_KEY = 'goon_videos';
|
|||||||
const TAGGED_KEY = 'goon_tagged';
|
const TAGGED_KEY = 'goon_tagged';
|
||||||
|
|
||||||
// Set to true to enable verbose console output in the popup.
|
// 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 log(...args) { if (DEBUG) console.log('[goon]', ...args); }
|
||||||
function warn(...args) { if (DEBUG) console.warn('[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
|
// UI references
|
||||||
const findBtn = document.getElementById('find-btn');
|
const findBtn = document.getElementById('find-btn');
|
||||||
const clearBtn = document.getElementById('clear-btn');
|
const clearBtn = document.getElementById('clear-btn');
|
||||||
@@ -104,6 +118,12 @@ function m3u8DeepProbeFn() {
|
|||||||
const manifests = [];
|
const manifests = [];
|
||||||
const seen = new Set();
|
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
|
// Helpers available in the MAIN world context
|
||||||
function resolveUrl(url) {
|
function resolveUrl(url) {
|
||||||
if (!url) return null;
|
if (!url) return null;
|
||||||
@@ -145,33 +165,8 @@ function m3u8DeepProbeFn() {
|
|||||||
} catch { /* avoid circular ref errors */ }
|
} catch { /* avoid circular ref errors */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strategy A: Known player globals (shallow + deep)
|
// Standard <video> element attributes — skip these when scanning for custom
|
||||||
const playerGlobals = [
|
// HLS player properties. Define here so it's available for Strategy A2 onward.
|
||||||
'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).
|
|
||||||
const STANDARD_VIDEO_ATTRS = new Set([
|
const STANDARD_VIDEO_ATTRS = new Set([
|
||||||
'src', 'srcObject', 'crossOrigin', 'preload', 'autoplay', 'muted',
|
'src', 'srcObject', 'crossOrigin', 'preload', 'autoplay', 'muted',
|
||||||
'defaultMuted', 'loop', 'controls', 'controlsList', 'disableRemotePlayback',
|
'defaultMuted', 'loop', 'controls', 'controlsList', 'disableRemotePlayback',
|
||||||
@@ -183,63 +178,205 @@ function m3u8DeepProbeFn() {
|
|||||||
'classList', 'style', 'attributes', 'dataset', 'innerHTML', 'textContent',
|
'classList', 'style', 'attributes', 'dataset', 'innerHTML', 'textContent',
|
||||||
'outerHTML', 'outerText', 'slot', 'accessKey', 'draggable', 'hidden',
|
'outerHTML', 'outerText', 'slot', 'accessKey', 'draggable', 'hidden',
|
||||||
'lang', 'spellcheck', 'tabIndex', 'title', 'translate',
|
'lang', 'spellcheck', 'tabIndex', 'title', 'translate',
|
||||||
'width', 'height', 'poster', 'preload', 'referrerPolicy',
|
'width', 'height', 'poster', 'referrerPolicy',
|
||||||
]);
|
]);
|
||||||
for (const v of document.querySelectorAll('video')) {
|
|
||||||
|
// 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)) {
|
for (const key of Object.keys(v)) {
|
||||||
if (STANDARD_VIDEO_ATTRS.has(key)) continue;
|
if (STANDARD_VIDEO_ATTRS.has(key)) continue;
|
||||||
const obj = v[key];
|
const obj = v[key];
|
||||||
if (!obj || typeof obj !== 'object') continue;
|
if (!obj || typeof obj !== 'object') continue;
|
||||||
// Check for common HLS instance patterns on video elements
|
// Check for common HLS instance patterns on video elements
|
||||||
if (typeof obj === 'object' && typeof obj.url === 'string' && isManifestUrl(obj.url)) {
|
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);
|
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 deep into player objects attached to video elements
|
||||||
walk(obj, `video.${key}`, 0);
|
walk(obj, `video.${key}`, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
probeLog('Strategy B1 (video custom props): found', strategyBCount, 'new manifests');
|
||||||
|
|
||||||
// B2: Check interceptor-set manifest annotation on video elements
|
// B2: Check interceptor-set manifest annotation on video elements
|
||||||
|
let strategyB2Count = 0;
|
||||||
for (const v of document.querySelectorAll('video')) {
|
for (const v of document.querySelectorAll('video')) {
|
||||||
if (v._goon_hls_manifest && !seen.has(v._goon_hls_manifest)) {
|
if (v._goon_hls_manifest && !seen.has(v._goon_hls_manifest)) {
|
||||||
seen.add(v._goon_hls_manifest);
|
seen.add(v._goon_hls_manifest);
|
||||||
manifests.push({ url: v._goon_hls_manifest, player: 'video._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
|
// Strategy C: Inline scripts
|
||||||
|
let strategyCCount = 0;
|
||||||
for (const script of document.querySelectorAll('script:not([src])')) {
|
for (const script of document.querySelectorAll('script:not([src])')) {
|
||||||
const text = script.textContent || '';
|
const text = script.textContent || '';
|
||||||
const matches = text.match(/https?:\/\/[^"'\s]+.(m3u8|hlsvod)[^"'\s]*/gi);
|
const matches = text.match(/https?:\/\/[^"'\s]+.(m3u8|hlsvod)[^"'\s]*/gi);
|
||||||
if (matches) {
|
if (matches) {
|
||||||
for (const u of 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
|
// Strategy D: Config globals
|
||||||
const configKeys = ['__dataLayer', '__INITIAL_STATE__', 'config', '__NEXT_DATA__', '__PRELOADED_STATE__', 'appData', 'pageData', '__data'];
|
const configKeys = ['__dataLayer', '__INITIAL_STATE__', 'config', '__NEXT_DATA__', '__PRELOADED_STATE__', 'appData', 'pageData', '__data'];
|
||||||
|
let preDCount = manifests.length;
|
||||||
for (const name of configKeys) {
|
for (const name of configKeys) {
|
||||||
const obj = window[name];
|
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.
|
// Strategy E: Interceptor-collected manifests — the most reliable source.
|
||||||
// The main-world interceptors (injected at document_start) capture .m3u8 URLs
|
// The main-world interceptors (injected at document_start) capture .m3u8 URLs
|
||||||
// from XHR, fetch, and Hls.prototype.loadSource/attachMedia calls, and store
|
// from XHR, fetch, and Hls.prototype.loadSource/attachMedia calls, and store
|
||||||
// them in window.__goon_plugin_data.manifests[]. This was the original
|
// them in window.__goon_plugin_data.manifests[]. This was the original
|
||||||
// Strategy 3/6 on master that was accidentally dropped during refactoring.
|
// 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)) {
|
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) {
|
for (const url of window.__goon_plugin_data.manifests) {
|
||||||
if (url && typeof url === 'string' && !seen.has(url)) {
|
if (url && typeof url === 'string' && !seen.has(url)) {
|
||||||
seen.add(url);
|
seen.add(url);
|
||||||
manifests.push({ url, player: '__goon_plugin_data' });
|
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;
|
return manifests;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,14 +524,21 @@ function filterValidTabs(tabs) {
|
|||||||
*/
|
*/
|
||||||
async function probeHlsManifests(tabId) {
|
async function probeHlsManifests(tabId) {
|
||||||
try {
|
try {
|
||||||
const results = await chrome.scripting.executeScript({
|
log('probeHlsManifests: executing deep probe on tab', tabId);
|
||||||
target: { tabId, allFrames: true, world: 'MAIN' },
|
const raw = await chrome.scripting.executeScript({
|
||||||
|
target: { tabId, allFrames: true },
|
||||||
func: m3u8DeepProbeFn,
|
func: m3u8DeepProbeFn,
|
||||||
});
|
});
|
||||||
// When allFrames is true, destroyed frames may return null entries.
|
// When allFrames is true, destroyed / sandboxed frames may return null entries.
|
||||||
// Filter them out before accessing .result to avoid TypeError.
|
// Guard: ensure raw is an array, then filter to objects with a defined .result.
|
||||||
return results.filter(r => r && typeof r === 'object').flatMap(r => r.result || []);
|
const manifests = Array.isArray(raw)
|
||||||
} catch {
|
? 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 [];
|
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.
|
* Scrape video elements from ALL frames in a tab.
|
||||||
* On sites like Pornhub, the video player lives inside an iframe — main-frame-only
|
* 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) {
|
async function scrapeTab(tabId, manifests) {
|
||||||
try {
|
try {
|
||||||
const results = await chrome.scripting.executeScript({
|
const raw = await chrome.scripting.executeScript({
|
||||||
target: { tabId, allFrames: true, world: 'MAIN' },
|
target: { tabId, allFrames: true },
|
||||||
func: scrapeFn,
|
func: scrapeFn,
|
||||||
args: [manifests],
|
args: [manifests],
|
||||||
});
|
});
|
||||||
// When allFrames is true, destroyed frames may return null entries.
|
// When allFrames is true, destroyed / sandboxed frames may return null entries.
|
||||||
// Filter them out before accessing .result to avoid TypeError.
|
// Guard: ensure raw is an array, then filter to objects with a defined .result.
|
||||||
const valid = results
|
const frameResults = Array.isArray(raw)
|
||||||
.filter(r => r && typeof r === 'object')
|
? raw.filter(r => r != null && typeof r === 'object' && 'result' in r)
|
||||||
.map(r => r.result)
|
.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) {
|
if (valid.length === 0) {
|
||||||
const firstValid = results.find(r => r && typeof r === 'object');
|
const firstWithResult = frameResults.find(r => r != null);
|
||||||
return firstValid?.result || null;
|
return firstWithResult || null;
|
||||||
}
|
}
|
||||||
valid.sort((a, b) => {
|
valid.sort((a, b) => {
|
||||||
const aArea = a.videos[0]?.area || 0;
|
const aArea = a.videos[0]?.area || 0;
|
||||||
@@ -478,8 +661,8 @@ async function scrapeTab(tabId, manifests) {
|
|||||||
return bArea - aArea;
|
return bArea - aArea;
|
||||||
});
|
});
|
||||||
return valid[0];
|
return valid[0];
|
||||||
} catch {
|
} catch(e) {
|
||||||
warn('skipping tab', tabId);
|
warn('scrapeTab: tab', tabId, 'error:', e && e.message ? e.message : e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -561,18 +744,70 @@ function makeVideoEntry(video, tabTitle, posterFromContent) {
|
|||||||
* @returns {Promise<{ items: Array, scanned: number, skipped: number }>}
|
* @returns {Promise<{ items: Array, scanned: number, skipped: number }>}
|
||||||
*/
|
*/
|
||||||
async function scanVideos(tabsToScan) {
|
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
|
// Step 1: Probe for HLS manifests in all frames
|
||||||
const manifestPromises = tabsToScan.map(t => probeHlsManifests(t.id));
|
const manifestPromises = tabsToScan.map(t => probeHlsManifests(t.id));
|
||||||
const allManifestResults = await Promise.all(manifestPromises);
|
const allManifestResults = await Promise.all(manifestPromises);
|
||||||
|
|
||||||
// Build per-tab manifest map (deduped)
|
// Build per-tab manifest map (deduped)
|
||||||
const tabManifestMap = new Map();
|
const tabManifestMap = new Map();
|
||||||
|
let totalManifests = 0;
|
||||||
for (let i = 0; i < tabsToScan.length; i++) {
|
for (let i = 0; i < tabsToScan.length; i++) {
|
||||||
const unique = dedupManifests(allManifestResults[i]);
|
const unique = dedupManifests(allManifestResults[i]);
|
||||||
if (unique.length > 0) {
|
const tabUrl = tabsToScan[i].url ? tabsToScan[i].url.substring(0, 80) : '(unknown)';
|
||||||
tabManifestMap.set(tabsToScan[i].id, unique);
|
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)
|
// Step 2: Scrape video elements (all frames) AND query content scripts (in parallel)
|
||||||
const scrapePromises = tabsToScan.map(t =>
|
const scrapePromises = tabsToScan.map(t =>
|
||||||
@@ -584,6 +819,24 @@ async function scanVideos(tabsToScan) {
|
|||||||
Promise.all(contentPromises),
|
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
|
// Step 3: Merge content-script data into tab results
|
||||||
// - Enrich video entries with poster URLs from the content script
|
// - Enrich video entries with poster URLs from the content script
|
||||||
// - Merge content-script intercepted manifests into the deep probe results
|
// - Merge content-script intercepted manifests into the deep probe results
|
||||||
@@ -622,6 +875,7 @@ async function scanVideos(tabsToScan) {
|
|||||||
const poster = contentData?.posterUrl || null;
|
const poster = contentData?.posterUrl || null;
|
||||||
const filteredManifests = filterManifests(tabManifests);
|
const filteredManifests = filterManifests(tabManifests);
|
||||||
if (filteredManifests.length > 0) {
|
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 => ({
|
const fallbackVideos = filteredManifests.map(m => ({
|
||||||
src: m.url,
|
src: m.url,
|
||||||
poster: poster,
|
poster: poster,
|
||||||
@@ -633,6 +887,7 @@ async function scanVideos(tabsToScan) {
|
|||||||
result: { videos: fallbackVideos },
|
result: { videos: fallbackVideos },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
log('scanVideos: tab', tab.id, 'has no videos and no manifests — skipping');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -640,7 +895,7 @@ async function scanVideos(tabsToScan) {
|
|||||||
tabTitle: tab.title || 'Unknown',
|
tabTitle: tab.title || 'Unknown',
|
||||||
result,
|
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
|
// Step 4: Dedup across tabs
|
||||||
const seen = new Map();
|
const seen = new Map();
|
||||||
@@ -657,6 +912,11 @@ async function scanVideos(tabsToScan) {
|
|||||||
const items = [...seen.values()];
|
const items = [...seen.values()];
|
||||||
const skipped = scrapeResults.filter(r => r === null).length;
|
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 };
|
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).`;
|
const msg = `Found ${items.length} video(s) across ${scanned} tab(s).`;
|
||||||
setStatus(skipped > 0 ? `${msg} ${skipped} tab(s) skipped (idle).` : msg);
|
setStatus(skipped > 0 ? `${msg} ${skipped} tab(s) skipped (idle).` : msg);
|
||||||
btnRow.style.display = 'flex';
|
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) {
|
} catch (e) {
|
||||||
setStatus(`Scan failed: ${e.message}`);
|
setStatus(`Scan failed: ${e.message}`);
|
||||||
warn('executeScript failed:', e);
|
warn('executeScript failed:', e);
|
||||||
|
|||||||
Reference in new issue
Block a user