/** * 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'); const statusEl = document.getElementById('status'); const listEl = document.getElementById('video-list'); const btnRow = document.getElementById('btn-row'); listEl.style.wordBreak = 'break-word'; listEl.style.whiteSpace = 'pre-wrap'; /* ======================================================================== */ /* UI helpers */ /* ======================================================================== */ function setStatus(msg) { statusEl.textContent = 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 => ({ videos: data[STORAGE_KEY] || [], tags: data[TAGGED_KEY] || [], })); } /** Render the video list UI from stored video entries. */ function renderList(videos) { listEl.innerHTML = ''; if (!videos || videos.length === 0) { listEl.style.display = 'none'; return; } listEl.style.display = 'block'; for (const video of videos) { const link = document.createElement('a'); link.className = 'video-item'; 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 = videos.map(v => v.src || v.url); navigator.clipboard.writeText(urls.join('\n')).then(() => { copyBtn.textContent = 'Copied!'; setTimeout(() => { copyBtn.textContent = 'Copy All URLs'; }, 1500); }); }); listEl.appendChild(copyBtn); } /** Show the main button row and render saved videos. */ function showActions() { btnRow.style.display = 'flex'; 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 ${videos.length} video(s) from last search.`); } else { setStatus(''); } }); } /* ======================================================================== */ /* HLS deep probe - runs in target page context via executeScript */ /* ======================================================================== */ /** * Deep probe a page for HLS manifest URLs. * Walks known player objects, video elements, inline scripts, config globals, * and interceptor-collected data. * Returns an array of { url, player } objects. */ function m3u8DeepProbeFn() { const manifests = []; const seen = new Set(); // Helpers available in the MAIN world context 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 { return null; } } function isManifestUrl(url) { if (!url) return false; const u = url.toLowerCase(); return u.includes('.m3u8') || u.includes('.hlsvod') || (u.includes('/hls/') && u.includes('http')) || (u.includes('/playlist') && u.includes('http')) || (u.includes('/manifest') && u.includes('http')); } function walk(obj, path, depth) { if (depth > 5) return; if (typeof obj === 'string') { const match = obj.match(/https?:\/\/[^"'`\s)]+?(?:\.m3u8|\.hlsvod|\/hls\/|\/playlist[^"'`\s]*|\/manifest[^"'`\s]*)[^"'`\s)]*/gi); if (match) { for (const u of match) { const resolved = resolveUrl(u); if (resolved && isManifestUrl(resolved) && !seen.has(resolved)) { seen.add(resolved); manifests.push({ url: resolved, player: path }); } } } return; } if (!obj || typeof obj !== 'object') return; try { for (const k of Object.keys(obj)) { walk(obj[k], `${path}.${k}`, depth + 1); } } 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