feat: add HLS deep-probe scanning with refactored pipeline and cleanup

- Deep-probe HLS manifests via known player globals, inline scripts, config objects
- Scan main frame only (no nested iframes); pick largest visible video per tab
- Store mediaType/hlsManifest metadata through the pipeline
- CSP-safe interceptor injection via script.src (not inline textContent)
- Remove probeFn/scrapeFn from Vite build entries (non-existent files)
- Refactor: extract m3u8DeepProbeFn, scrapeFn, scanVideos, storeVideos,
  makeVideoEntry, filterValidTabs, probeHlsManifests, dedupManifests, scrapeTab
  to module-level functions (no inline closures recreated on every click)
- Clean up viewer.js: extract createVideoElement(), createPanel(),
  consolidate HLS_CONFIG, remove console.log noise
- Add DEBUG flag to gate console output in popup
- Unify url/src fields — prefer 'src' everywhere
- Remove dead code in renderList and setTimeout(100) hack
This commit is contained in:
robot committed 2026-08-12 00:20:00 +00:00
1 parent 7416ae8ff9
commit d86c1d1778
6 files changed
+646 -709

No files matched your search

+343 -457
View File
@@ -1,16 +1,19 @@
/**
* index.js — Popup logic for the Goon Plugin.
* Runs inside the popup; scans all tabs for videos and opens the viewer.
*
* Video detection strategy (per tab):
* 1. executeScript in main world: deeply query HLS player state for manifests
* 2. chrome.tabs.sendMessage to content script: get intercepted manifests + poster
* 3. executeScript DOM scan: fallback to <video> element attributes
* index.js - Popup logic for the Goon Plugin.
* Runs inside the popup; scans the current window's tabs for videos
* and stores the results for the viewer to consume.
*/
const STORAGE_KEY = 'goon_videos';
const TAGGED_KEY = 'goon_tagged';
// Set to true to enable verbose console output in the popup.
const DEBUG = false;
function log(...args) { if (DEBUG) console.log('[goon]', ...args); }
function warn(...args) { if (DEBUG) console.warn('[goon]', ...args); }
// UI references
const findBtn = document.getElementById('find-btn');
const clearBtn = document.getElementById('clear-btn');
const openBtn = document.getElementById('open-btn');
@@ -21,45 +24,47 @@ const btnRow = document.getElementById('btn-row');
listEl.style.wordBreak = 'break-word';
listEl.style.whiteSpace = 'pre-wrap';
/* ======================================================================== */
/* UI helpers */
/* ======================================================================== */
function setStatus(msg) {
statusEl.textContent = msg;
console.log('[goon]', msg);
log(msg);
}
/** Load the current video list and tags from storage. */
function loadVideos() {
return chrome.storage.local.get([STORAGE_KEY, TAGGED_KEY]).then(data => ({
urls: data[STORAGE_KEY] || [],
videos: data[STORAGE_KEY] || [],
tags: data[TAGGED_KEY] || [],
}));
}
/** Render the video list UI from stored {url, label} objects. */
function renderList(items) {
/** Render the video list UI from stored video entries. */
function renderList(videos) {
listEl.innerHTML = '';
if (!items || items.length === 0) {
if (!videos || videos.length === 0) {
listEl.style.display = 'none';
return;
}
listEl.style.display = 'block';
for (const item of items) {
for (const video of videos) {
const link = document.createElement('a');
link.className = 'video-item';
let url = item.url;
if (typeof url === 'string' && item.src) url = item.src;
if (typeof url === 'object' && url.src) url = url.src;
link.href = url;
link.textContent = item.label ? `${item.label}: ${url}` : url;
link.href = video.src || video.url || '';
link.textContent = video.label ? `${video.label}: ${link.href}` : link.href;
link.target = '_blank';
listEl.appendChild(link);
}
// Copy all URLs button
const copyBtn = document.createElement('button');
copyBtn.className = 'copy-btn';
copyBtn.textContent = 'Copy All URLs';
copyBtn.addEventListener('click', () => {
const urls = items.map(v => (typeof v === 'object' && v.src) ? v.src : (typeof v === 'object' ? v.url : v));
const urls = videos.map(v => v.src || v.url);
navigator.clipboard.writeText(urls.join('\n')).then(() => {
copyBtn.textContent = 'Copied!';
setTimeout(() => { copyBtn.textContent = 'Copy All URLs'; }, 1500);
@@ -68,482 +73,369 @@ function renderList(items) {
listEl.appendChild(copyBtn);
}
/** Show the main button row. */
/** Show the main button row and render saved videos. */
function showActions() {
btnRow.style.display = 'flex';
loadVideos().then(({ urls, tags }) => {
if (urls.length > 0) {
const items = urls.map((url, i) => {
const entry = { url: url.src || url, label: tags[i] };
if (typeof url === 'object' && url.src) entry.src = url.src;
return entry;
});
loadVideos().then(({ videos, tags }) => {
if (videos.length > 0) {
const items = videos.map((video, i) => ({
src: video.src || video.url || '',
label: tags[i],
}));
renderList(items);
setStatus(`Using ${urls.length} video(s) from last search.`);
setStatus(`Using ${videos.length} video(s) from last search.`);
} else {
setStatus('');
}
});
}
/* ====================================================================
* HLS DATA STRUCTURE QUERY — runs in the MAIN world via executeScript
*
* This is the primary detection strategy. We deeply probe the page's
* JavaScript state for HLS player instances, config objects, and
* playlist URLs. Works regardless of whether the page uses HLS.js,
* video.js, DASH.js, FLV.js, or a custom player.
* ==================================================================== */
/* ======================================================================== */
/* HLS deep probe - runs in target page context via executeScript */
/* ======================================================================== */
const hlsQueryFn = function () {
// Resolve relative URLs
function resolveUrl(url) {
if (!url) return null;
try {
if (url.startsWith('//')) return 'https:' + url;
if (url.startsWith('http')) return url;
return new URL(url, window.location.origin).href;
} catch (e) { return null; }
}
function isManifestUrl(url) {
if (!url || typeof url !== 'string') return false;
return url.includes('.m3u8') ||
url.includes('/playlist') ||
url.includes('/manifest') ||
url.includes('.mpd');
}
// Collect unique manifest URLs
/**
* Deep probe a page for HLS manifest URLs.
* Walks known player objects, video elements, inline scripts, and config globals.
* Returns an array of { url, player } objects.
*/
function m3u8DeepProbeFn() {
const manifests = [];
const manifestSet = new Set();
const seen = new Set();
function addManifest(url) {
const resolved = resolveUrl(url);
if (resolved && !manifestSet.has(resolved)) {
manifestSet.add(resolved);
manifests.push(resolved);
function walk(obj, path, depth) {
if (!obj || typeof obj !== 'object' || depth > 5) return;
if (typeof obj === 'string') {
const match = obj.match(/https?:\/\/[^"'\s`)]+\.m3u8[^"'\s`)]*/gi);
if (match) {
for (const u of match) {
if (!seen.has(u)) { seen.add(u); manifests.push({ url: u, player: path }); }
}
}
return;
}
for (const k of Object.keys(obj)) {
walk(obj[k], `${path}.${k}`, depth + 1);
}
}
// --- STRATEGY 1: Probe video elements for HLS player instances ---
try {
const videos = document.querySelectorAll('video');
const hlsProps = [
'hls', 'hlsInstance', '_hls', '__hls', 'player', '_player',
'videoPlayer', '_videoPlayer', 'mediaPlayer', '_mediaPlayer',
'flvjsPlayer', '_flvjsPlayer', 'dashPlayer', '_dashPlayer',
'__hlsPlayer', '__player', '__videoPlayer', 'phPlayer',
'_phPlayer', 'phVideoPlayer', '_phVideoPlayer'
];
for (const video of videos) {
for (const prop of hlsProps) {
const instance = video[prop];
if (!instance || typeof instance !== 'object') continue;
// HLS.js: .url, .opts.url, ._stream.url
if (instance.url) addManifest(instance.url);
if (instance.opts && instance.opts.url) addManifest(instance.opts.url);
if (instance._stream && instance._stream.url) addManifest(instance._stream.url);
// HLS.js levels (quality variants)
if (instance.levels) {
for (const level of instance.levels) {
if (level.uri) addManifest(level.uri);
if (level._uri) addManifest(level._uri);
}
}
// video.js: .currentSource_.src or .src_
if (instance.currentSource_ && instance.currentSource_.src) addManifest(instance.currentSource_.src);
if (instance.src_) addManifest(instance.src_);
// Generic: .playlist, .source, .mediaUrl
if (instance.playlist) addManifest(instance.playlist);
if (instance.source) addManifest(instance.source);
if (instance.mediaUrl) addManifest(instance.mediaUrl);
// 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',
];
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']) {
if (typeof obj[key] === 'string' && obj[key].includes('.m3u8')) {
if (!seen.has(obj[key])) { seen.add(obj[key]); manifests.push({ url: obj[key], player: `${name}.${key}` }); }
}
}
} catch (e) { /* DOM access error */ }
walk(obj, name, 0);
}
// --- STRATEGY 2: Probe window-level player variables ---
try {
const windowPlayerVars = [
'Hls', '_Hls', 'hls', 'FLV', 'flvjs', 'Video', 'Videojs',
'videojs', 'p7m', 'P7m', 'Player', 'player', '_player',
'PornhubPlayer', 'PHPlayer', '_PHPlayer', 'phVideoPlayer',
'phPlayer', '_phPlayer', 'videoPlayer', 'MediaPlayer',
'CustomPlayer', 'phVideo', 'ph_media', '_PH_VIDEO',
'_hls', '_dash', '_video', '__player', '__hls'
];
// Deep-probe an object for manifest URLs (up to depth 3)
function deepProbe(obj, depth, seen) {
if (!obj || typeof obj !== 'object' || depth <= 0) return;
if (seen.has(obj)) return;
seen.add(obj);
for (const key of Object.keys(obj)) {
const val = obj[key];
if (typeof val === 'string' && isManifestUrl(val)) {
addManifest(val);
} else if (typeof val === 'object' && val !== null) {
deepProbe(val, depth - 1, seen);
}
}
}
for (const varName of windowPlayerVars) {
const obj = window[varName];
if (obj && typeof obj === 'object') {
deepProbe(obj, 3, new Set());
// Also check prototype chain
const proto = Object.getPrototypeOf(obj);
if (proto && proto !== Object.prototype) {
deepProbe(proto, 2, new Set());
}
}
}
} catch (e) { /* Ignore */ }
// --- STRATEGY 3: Probe intercepted data (from content script interceptors) ---
try {
if (window.__goon_plugin_data && window.__goon_plugin_data.manifests) {
for (const url of window.__goon_plugin_data.manifests) {
addManifest(url);
}
}
} catch (e) { /* Ignore */ }
// --- STRATEGY 4: Extract from inline scripts ---
try {
const scripts = document.querySelectorAll('script');
for (const script of scripts) {
const text = script.textContent || '';
if (!text.includes('.m3u8') && !text.includes('manifest') &&
!text.includes('playlist') && !text.includes('/hls')) continue;
// m3u8 URLs in quotes
const m3u8Regex = /['"]([^'"]*\.m3u8[^'"]*)['"]/g;
let match;
while ((match = m3u8Regex.exec(text)) !== null) {
addManifest(match[1]);
}
// CDN-style HLS URLs (Pornhub CDN patterns)
const cdnRegex = /['"]([^'"]*(?:phncdn|pornhub|porn\.com|pornhubpremium)[^'"]*(?:m3u8|playlist|manifest|\/hls\/)[^'"]*)['"]/g;
while ((match = cdnRegex.exec(text)) !== null) {
addManifest(match[1]);
}
// Generic manifest/playlist URLs
const genericRegex = /['"]([^'"]*(?:\/manifest\/|\/playlist\/|\/hls\/)[^'"]*)['"]/g;
while ((match = genericRegex.exec(text)) !== null) {
addManifest(match[1]);
}
}
} catch (e) { /* Ignore */ }
// --- STRATEGY 5: Probe __dataLayer, __INITIAL_STATE__, and similar ---
try {
const dataKeys = Object.keys(window).filter(k =>
k.startsWith('__') && k !== '__goon_plugin_data' &&
k !== '__goon_plugin_intercepted'
);
const maxProbe = Math.min(dataKeys.length, 15);
for (let i = 0; i < maxProbe; i++) {
const key = dataKeys[i];
const obj = window[key];
if (!obj || typeof obj !== 'object') continue;
// Quick heuristic: only deep-probe if it looks media-related
try {
const peek = JSON.stringify(obj).substring(0, 300);
if (peek.includes('video') || peek.includes('m3u8') ||
peek.includes('stream') || peek.includes('playlist') ||
peek.includes('hls') || peek.includes('manifest') ||
peek.includes('phncdn') || peek.includes('pornhubpremium')) {
function deepProbeLimited(o, d) {
if (!o || typeof o !== 'object' || d <= 0) return;
for (const k of Object.keys(o)) {
const v = o[k];
if (typeof v === 'string' && isManifestUrl(v)) {
addManifest(v);
} else if (typeof v === 'object' && v !== null) {
deepProbeLimited(v, d - 1);
}
// Strategy B: Video element HLS instances
for (const v of document.querySelectorAll('video')) {
for (const key of Object.keys(v)) {
if (key.toLowerCase().includes('hls') || key === 'player') {
const hlsObj = v[key];
if (hlsObj && typeof hlsObj === 'object') {
for (const subKey of Object.keys(hlsObj)) {
if (typeof hlsObj[subKey] === 'string' && hlsObj[subKey].includes('.m3u8')) {
if (!seen.has(hlsObj[subKey])) { seen.add(hlsObj[subKey]); manifests.push({ url: hlsObj[subKey], player: `video.${key}.${subKey}` }); }
}
}
deepProbeLimited(obj, 3);
walk(hlsObj, `video.${key}`, 0);
}
} catch (e) { /* Ignore stringify errors */ }
}
} catch (e) { /* Ignore */ }
// --- STRATEGY 6: Query intercepted data from main world ---
try {
if (window.__goon_plugin_data && Array.isArray(window.__goon_plugin_data.manifests)) {
for (const m of window.__goon_plugin_data.manifests) {
addManifest(m);
}
}
} catch (e) {}
}
// Strategy C: Inline scripts
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' }); }
}
}
}
// Strategy D: Config globals
const configKeys = ['__dataLayer', '__INITIAL_STATE__', 'config', '__NEXT_DATA__', '__PRELOADED_STATE__', 'appData', 'pageData', '__data'];
for (const name of configKeys) {
const obj = window[name];
if (obj && typeof obj === 'object') walk(obj, name, 0);
}
return manifests;
}
/* ======================================================================== */
/* Video scraper - runs in target page context via executeScript */
/* ======================================================================== */
/**
* Scrape visible video elements from the main frame.
* Returns at most one video entry (the largest visible one).
* @param {Array} hlsManifests - manifests found by the deep probe
* @returns {{ videos: Array, videoCount: number, hlsManifest: string|null }}
*/
function scrapeFn(hlsManifests) {
const allVideos = Array.from(document.querySelectorAll('video'))
.filter(v => {
const style = window.getComputedStyle(v);
return style.display !== 'none' && v.clientWidth > 0 && v.clientHeight > 0;
})
.map(v => {
const area = v.clientWidth * v.clientHeight;
const rawSrc = v.currentSrc || v.src || '';
const poster = v.poster || v.getAttribute('data-poster') || null;
let hlsManifest = null;
let mediaType = 'video';
let src = rawSrc;
if (rawSrc.includes('.m3u8') && rawSrc.startsWith('http')) {
hlsManifest = rawSrc;
mediaType = 'hls';
} else if (rawSrc.startsWith('http') && !rawSrc.startsWith('blob:')) {
mediaType = 'video';
} else if (hlsManifests && hlsManifests.length > 0) {
hlsManifest = hlsManifests[0].url || hlsManifests[0];
mediaType = 'hls';
src = hlsManifest;
}
return { src, poster, area, width: v.clientWidth, height: v.clientHeight, mediaType, hlsManifest };
});
allVideos.sort((a, b) => b.area - a.area);
// Filter: only keep entries with a real playable source
const playable = allVideos.filter(v => {
const srcOk = v.src && (v.src.startsWith('http://') || v.src.startsWith('https://'));
const hlsOk = v.hlsManifest && (v.hlsManifest.startsWith('http://') || v.hlsManifest.startsWith('https://'));
return srcOk || hlsOk;
});
// Dedup: if multiple videos share the same HLS manifest, keep only the largest
const seen = new Map();
const unique = [];
for (const v of playable) {
const key = (v.hlsManifest || v.src || '') + '||' + (v.poster || '');
if (!seen.has(key)) {
seen.set(key, v);
unique.push(v);
}
}
const largest = unique.length > 0 ? unique[0] : null;
return {
manifests: manifests,
count: manifests.length,
videos: largest ? [largest] : [],
videoCount: largest ? 1 : 0,
hlsManifest: largest?.hlsManifest || null,
};
};
}
/* ======================================================================== */
/* Scan pipeline */
/* ======================================================================== */
/** URLs that should not be scanned. */
const SYSTEM_PREFIXES = ['chrome://', 'chrome-extension://', 'edge://', 'about:', 'view-source:'];
/** Filter out system/browser pages from the tab list. */
function filterValidTabs(tabs) {
return tabs.filter(t =>
t.url && !SYSTEM_PREFIXES.some(prefix => t.url.startsWith(prefix))
);
}
/**
* Probe all frames in a tab for HLS manifests.
* @param {number} tabId
* @returns {Promise<Array>} array of { url, player } or empty on error
*/
async function probeHlsManifests(tabId) {
try {
const results = await chrome.scripting.executeScript({
target: { tabId, allFrames: true },
func: m3u8DeepProbeFn,
});
return results.flatMap(r => r.result || []);
} catch {
return [];
}
}
/**
* Aggregate and deduplicate HLS manifests across frames for one tab.
* Prefers manifests from known players over inline-script scraping.
* @param {Array} allManifests
* @returns {Array}
*/
function dedupManifests(allManifests) {
const seenUrls = new Set();
const unique = allManifests.filter(m => {
if (seenUrls.has(m.url)) return false;
seenUrls.add(m.url);
return true;
});
unique.sort((a, b) => {
const aInline = a.player === 'inline-script' ? 1 : 0;
const bInline = b.player === 'inline-script' ? 1 : 0;
return aInline - bInline;
});
return unique;
}
/**
* Scrape video elements from the main frame of a tab.
* @param {number} tabId
* @param {Array} manifests - HLS manifests found by probing
* @returns {Promise<Object|null>} scrape result or null on error
*/
async function scrapeTab(tabId, manifests) {
try {
const results = await chrome.scripting.executeScript({
target: { tabId },
func: scrapeFn,
args: [manifests],
});
return results[0]?.result || null;
} catch {
warn('skipping tab', tabId);
return null;
}
}
/**
* Build a normalized video entry from a scraped video object.
* @param {{ src: string, poster: string|null, mediaType: string, hlsManifest: string|null }} video
* @param {string} tabTitle
* @returns {{ src: string, poster: string|null, tab: string, mediaType: string, hlsManifest?: string }}
*/
function makeVideoEntry(video, tabTitle) {
const entry = {
src: video.hlsManifest || video.src || '',
poster: video.poster || null,
tab: tabTitle,
mediaType: video.mediaType || 'video',
};
if (video.hlsManifest) entry.hlsManifest = video.hlsManifest;
return entry;
}
/**
* Run the full scan: probe HLS manifests, scrape video elements, dedup, store.
* @param {Array} tabsToScan - tabs returned by chrome.tabs.query
* @returns {Promise<{ items: Array, scanned: number, skipped: number }>}
*/
async function scanVideos(tabsToScan) {
// 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();
for (let i = 0; i < tabsToScan.length; i++) {
const unique = dedupManifests(allManifestResults[i]);
if (unique.length > 0) {
tabManifestMap.set(tabsToScan[i].id, unique);
}
}
// Step 2: Scrape video elements (main frame only)
const scrapePromises = tabsToScan.map(t =>
scrapeTab(t.id, tabManifestMap.get(t.id) || [])
);
const scrapeResults = await Promise.all(scrapePromises);
// Step 3: Collect results from tabs that found videos
const tabResults = tabsToScan.map((tab, i) => ({
tabTitle: tab.title || 'Unknown',
result: scrapeResults[i],
})).filter(g => g.result && g.result.videos?.length > 0);
// Step 4: Dedup across tabs
const seen = new Map();
for (const group of tabResults) {
for (const video of group.result.videos) {
const primarySrc = video.hlsManifest || video.src || '(empty)';
const key = primarySrc + '||' + (video.poster || '(none)');
if (!seen.has(key)) {
seen.set(key, makeVideoEntry(video, group.tabTitle));
}
}
}
const items = [...seen.values()];
const skipped = scrapeResults.filter(r => r === null).length;
return { items, scanned: tabResults.length, skipped };
}
/** Store video entries in chrome.storage.local. */
async function storeVideos(items) {
const stored = items.map(v => ({
src: v.src,
poster: v.poster,
tab: v.tab,
mediaType: v.mediaType || 'video',
hlsManifest: v.hlsManifest || undefined,
}));
await chrome.storage.local.set({
[STORAGE_KEY]: stored,
[TAGGED_KEY]: items.map(v => v.tab),
});
}
/* ======================================================================== */
/* Event handlers */
/* ======================================================================== */
/* === "Find Videos" — scan all tabs in the window === */
findBtn.addEventListener('click', async () => {
setStatus('Scanning tabs...');
// Get all tabs in the current window
const allTabs = await chrome.tabs.query({ currentWindow: true });
// Filter out chrome/system pages
const validTabs = allTabs.filter(t =>
t.url &&
!t.url.startsWith('chrome://') &&
!t.url.startsWith('chrome-extension://') &&
!t.url.startsWith('edge://') &&
!t.url.startsWith('about:') &&
!t.url.startsWith('view-source:')
);
const validTabs = filterValidTabs(allTabs);
if (validTabs.length === 0) {
setStatus('No browsable tabs found.');
return;
}
const tabsToScan = validTabs;
setStatus(`Scanning ${tabsToScan.length} tab(s)...`);
setStatus(`Scanning ${validTabs.length} tab(s)...`);
// DOM scrape function (runs in main world)
const scrapeFn = () => {
const dataKeys = ['src', 'video', 'mp4', 'webm', 'poster'];
try {
const { items, scanned, skipped } = await scanVideos(validTabs);
// Collect all visible video elements, measure by rendered pixel area
const allVideos = Array.from(document.querySelectorAll('video')).filter(v => {
const style = window.getComputedStyle(v);
return style.display !== 'none' && v.clientWidth > 0 && v.clientHeight > 0;
}).map(v => {
const area = v.clientWidth * v.clientHeight;
const src = dataKeys.map(k => v.getAttribute('data-' + k)).find(Boolean)
|| (v.currentSrc || v.src || '')
|| null;
const poster = (v.poster || '')
|| dataKeys.filter(k => k !== 'poster').map(k => v.getAttribute('data-' + k)).find(Boolean)
|| v.getAttribute('data-poster') || null;
return { src, poster, area, width: v.clientWidth, height: v.clientHeight };
});
allVideos.sort((a, b) => b.area - a.area);
const largest = allVideos.length > 0 ? allVideos[0] : null;
const sources = Array.from(document.querySelectorAll('video source[src]')).map(s => s.src);
const audios = Array.from(document.querySelectorAll('audio')).length;
await storeVideos(items);
return { videos: largest ? [largest] : [], videoCount: largest ? 1 : 0, sources, audios };
};
// Query content script for intercepted data + poster
async function queryContentScript(tabId) {
try {
return await chrome.tabs.sendMessage(tabId, { type: 'GOON_GET_VIDEO_DATA' });
} catch {
return null;
}
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';
} catch (e) {
setStatus(`Scan failed: ${e.message}`);
warn('executeScript failed:', e);
}
const results = await Promise.all(
tabsToScan.map(async t => {
try {
const [hlsQueryResults, scriptResult, contentData] = await Promise.all([
// STRATEGY 1: Deep query main-world HLS data structures
chrome.scripting.executeScript({
target: { tabId: t.id, allFrames: true },
func: hlsQueryFn,
}),
// STRATEGY 2: DOM scrape (existing behavior)
chrome.scripting.executeScript({
target: { tabId: t.id, allFrames: true },
func: scrapeFn,
}),
// STRATEGY 3: Content script intercepted manifests + poster
queryContentScript(t.id),
]);
return { hlsQueryResults: hlsQueryResults.filter(Boolean), scriptResult: scriptResult.filter(Boolean), contentData };
} catch {
console.warn('[goon] skipping tab', t.id, ':', t.url);
return null;
}
})
);
// Merge all results into unique video entries
const seen = new Map();
let totalFound = 0;
for (let ti = 0; ti < tabsToScan.length; ti++) {
const tab = tabsToScan[ti];
const res = results[ti];
if (!res) continue;
const tabTitle = tab.title || 'Unknown';
const contentPoster = (res.contentData && res.contentData.posterUrl) || null;
// --- PROCESS HLS MANIFESTS FROM MAIN-WORLD QUERY ---
const hlsManifests = [];
for (const frameResult of (res.hlsQueryResults || [])) {
const q = frameResult.result;
if (q && Array.isArray(q.manifests)) {
for (const m of q.manifests) {
if (m && m.startsWith('http')) {
hlsManifests.push(m);
}
}
}
}
// --- PROCESS INTERCEPTED MANIFESTS FROM CONTENT SCRIPT ---
const interceptedManifests = [];
if (res.contentData && Array.isArray(res.contentData.interceptedManifests)) {
for (const im of res.contentData.interceptedManifests) {
if (im.url && im.url.startsWith('http')) {
interceptedManifests.push(im.url);
}
}
}
// Merge all manifest sources
const allManifests = [...new Set([...hlsManifests, ...interceptedManifests])];
// --- PROCESS DOM VIDEO ELEMENTS ---
const domVideos = [];
for (const frameResult of (res.scriptResult || [])) {
const r = frameResult.result;
if (r && r.videos) {
for (const v of r.videos) {
if (v) domVideos.push(v);
}
}
}
// --- PROCESS CONTENT SCRIPT VIDEO ELEMENTS ---
const csVideos = [];
if (res.contentData && Array.isArray(res.contentData.videoElements)) {
for (const v of res.contentData.videoElements) {
csVideos.push(v);
}
}
// --- BUILD ENTRIES ---
// Priority 1: HLS manifests (per tab, only if present)
for (const manifestUrl of allManifests) {
totalFound++;
const key = 'hls||' + manifestUrl;
if (!seen.has(key)) {
const item = {
url: manifestUrl,
src: manifestUrl,
poster: contentPoster || null,
tab: tabTitle,
hlsManifest: manifestUrl,
mediaType: 'hls',
};
seen.set(key, item);
}
}
// Priority 2: DOM video elements (non-blob direct URLs)
for (const v of domVideos) {
// Skip blob: URLs — they're HLS-internal and can't be played directly
if (!v.src || v.src.startsWith('blob:')) continue;
totalFound++;
const key = (v.src || '(empty)') + '||' + (v.poster || '(none)');
if (!seen.has(key)) {
const item = {
url: v.src,
src: v.src,
poster: v.poster || contentPoster || null,
tab: tabTitle,
};
// If HLS is available for this tab, attach it
if (allManifests.length > 0) {
item.hlsManifest = allManifests[0];
item.mediaType = 'hls';
}
seen.set(key, item);
}
}
// Priority 3: Content script video elements
for (const v of csVideos) {
if (!v.src || v.src.startsWith('blob:')) continue;
totalFound++;
const key = (v.src || '(empty)') + '||' + (v.poster || '(none)');
if (!seen.has(key)) {
const item = {
url: v.src,
src: v.src,
poster: v.poster || contentPoster || null,
tab: tabTitle,
};
if (allManifests.length > 0) {
item.hlsManifest = allManifests[0];
item.mediaType = 'hls';
}
seen.set(key, item);
}
}
}
// Filter: keep only http/https URLs
const items = [...seen.values()].filter(v => {
const url = v.src || v.url || '';
return url.startsWith('http://') || url.startsWith('https://');
});
console.log('[goon] ===== Found', totalFound, 'video elements, unique:', items.length, 'across', tabsToScan.length, 'tabs');
// Store results
const storedItems = items.map(v => ({
url: v.url,
src: v.src || v.url,
poster: v.poster,
tab: v.tab,
hlsManifest: v.hlsManifest || null,
mediaType: v.mediaType || 'direct',
}));
await chrome.storage.local.set({
[STORAGE_KEY]: storedItems,
[TAGGED_KEY]: items.map(v => v.tab),
});
const skippedTabs = results.filter(r => r === null).length;
const statusMsg = `Found ${items.length} video(s) across ${tabsToScan.length} tab(s).`;
setStatus(skippedTabs > 0 ? `${statusMsg} ${skippedTabs} tab(s) skipped.` : statusMsg);
btnRow.style.display = 'flex';
});
/* === "Open Viewer" — launch the NxN grid === */
/* === "Open Viewer" - launch the NxN grid === */
openBtn.addEventListener('click', async () => {
const { urls, tags } = await loadVideos();
console.log('[goon] Open Viewer clicked, loaded from storage:', urls, 'tags:', tags);
const { videos, tags } = await loadVideos();
if (!urls || urls.length === 0) {
if (!videos || videos.length === 0) {
setStatus('No videos found. Click "Find Videos" first.');
return;
}
const videoUrls = urls;
const videoTags = tags || [];
const N = Math.ceil(Math.sqrt(videoUrls.length));
const N = Math.ceil(Math.sqrt(videos.length));
const PANEL_MIN_W = 420;
const PANEL_MIN_H = 340;
const GAP = 2;
@@ -551,20 +443,14 @@ openBtn.addEventListener('click', async () => {
const winH = Math.max(PANEL_MIN_H * N + GAP * (N - 1) + 40, 300);
const id = 'goonViewer_' + Date.now();
await chrome.storage.local.set({ [id]: videoUrls });
await chrome.storage.local.set({ [id]: videos });
const verify = await chrome.storage.local.get([id]);
console.log('[goon] viewer storage verified:', verify[id]?.length, 'entries');
await new Promise(r => setTimeout(r, 100));
const tagsParam = videoTags.length > 0 ? '&tags=' + encodeURIComponent(JSON.stringify(videoTags)) : '';
const gridParam = '&grid=' + N;
const viewerUrl = chrome.runtime.getURL('viewer/viewer.html') + '?id=' + id + gridParam + tagsParam;
const tagsParam = tags.length > 0 ? '&tags=' + encodeURIComponent(JSON.stringify(tags)) : '';
const viewerUrl = chrome.runtime.getURL('viewer/viewer.html') + '?id=' + id + '&grid=' + N + tagsParam;
chrome.windows.create({ url: viewerUrl, width: winW, height: winH });
});
/* === "Clear" — reset the stored list === */
/* === "Clear" - reset the stored list === */
clearBtn.addEventListener('click', async () => {
await chrome.storage.local.remove([STORAGE_KEY, TAGGED_KEY]);
listEl.style.display = 'none';
@@ -572,5 +458,5 @@ clearBtn.addEventListener('click', async () => {
setStatus('');
});
/* === Init — restore saved list === */
/* === Init - restore saved list === */
showActions();