feat: add HLS playback support with hls.js #1
No files matched your search
+166
-215
@@ -1,227 +1,171 @@
|
|||||||
/**
|
/**
|
||||||
* content.js — Content script injected into matching pages.
|
* content.js - Content script injected into matching pages at document_start.
|
||||||
* Runs in the ISOLATED world. Coordinates scanning by injecting a script
|
* Runs in the ISOLATED world.
|
||||||
* into the MAIN world (which CAN intercept XHR/fetch), then collects results
|
|
||||||
* via postMessage.
|
|
||||||
*
|
*
|
||||||
* Architecture:
|
* Strategy: Inject XHR/fetch interceptors into the MAIN world at document_start
|
||||||
* 1. Inject interceptor.js into MAIN world (intercepts network + DOM)
|
* to capture HLS manifest URLs as they load. Also listen for postMessage events
|
||||||
* 2. Interceptor posts results back via window.postMessage
|
* from intercepted URLs.
|
||||||
* 3. This script collects results and forwards to the popup
|
*
|
||||||
|
* When the popup sends GOON_GET_VIDEO_DATA, we respond with:
|
||||||
|
* - posterUrl: extracted poster/thumbnail
|
||||||
|
* - interceptedManifests: manifests caught by XHR/fetch interceptors
|
||||||
|
* - videoElements: DOM video elements with basic info
|
||||||
|
*
|
||||||
|
* The popup separately executes main-world scripts via chrome.scripting.executeScript
|
||||||
|
* to deeply query HLS player state — this content script handles interception only.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// Collect results from injected scripts
|
// Collected intercepted HLS manifest URLs from XHR/fetch interception
|
||||||
let collectedData = {
|
const interceptedManifests = [];
|
||||||
hlsManifests: new Set(),
|
const interceptedSources = new Set();
|
||||||
dashManifests: new Set(),
|
|
||||||
videoSources: [],
|
|
||||||
posterUrl: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
let pollingActive = false;
|
|
||||||
let pollTimer = null;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inject the interceptor script into the MAIN world to intercept XHR/fetch.
|
* Inject interceptors into the MAIN world at document_start.
|
||||||
|
* Runs BEFORE any page JS, so we catch all HLS activity.
|
||||||
*/
|
*/
|
||||||
function injectInterceptor() {
|
function injectInterceptors() {
|
||||||
const interceptorCode = `
|
const interceptCode = `
|
||||||
(function() {
|
(function() {
|
||||||
// Already injected
|
if (window.__goon_plugin_intercepted) return;
|
||||||
if (window.__goonInterceptor) return;
|
window.__goon_plugin_intercepted = true;
|
||||||
window.__goonInterceptor = true;
|
|
||||||
|
|
||||||
const manifests = { hls: new Set(), dash: new Set() };
|
window.__goon_plugin_data = window.__goon_plugin_data || {
|
||||||
let scanCount = 0;
|
manifests: [],
|
||||||
|
sources: []
|
||||||
// --- XHR interception ---
|
|
||||||
const origOpen = XMLHttpRequest.prototype.open;
|
|
||||||
const origSend = XMLHttpRequest.prototype.send;
|
|
||||||
|
|
||||||
XMLHttpRequest.prototype.open = function(method, url) {
|
|
||||||
this.__goonUrl = typeof url === 'string' ? url : url.toString();
|
|
||||||
return origOpen.apply(this, arguments);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
XMLHttpRequest.prototype.send = function() {
|
// Helper: resolve relative URLs
|
||||||
const url = this.__goonUrl || '';
|
function resolveUrl(url) {
|
||||||
if (url.includes('.m3u8') || url.includes('/hls/')) {
|
if (!url) return null;
|
||||||
manifests.hls.add(url);
|
|
||||||
}
|
|
||||||
if (url.includes('.mpd') || url.includes('/dash/')) {
|
|
||||||
manifests.dash.add(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.addEventListener('load', function() {
|
|
||||||
try {
|
try {
|
||||||
const resp = this.responseText;
|
if (url.startsWith('//')) return 'https:' + url;
|
||||||
if (resp && typeof resp === 'string') {
|
if (url.startsWith('http')) return url;
|
||||||
const m3u8Re = /https?:[^"'\\s]+\\.m3u8[^"'\\s]*/gi;
|
return new URL(url, window.location.origin).href;
|
||||||
let m;
|
} catch { return null; }
|
||||||
while ((m = m3u8Re.exec(resp)) !== null) {
|
|
||||||
const cleaned = m[0].replace(/[;,]$/, '');
|
|
||||||
if (!cleaned.includes('/subtitle') && !cleaned.includes('/captions') && !cleaned.includes('.m3u8x')) {
|
|
||||||
manifests.hls.add(cleaned);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch(e) {}
|
|
||||||
});
|
|
||||||
|
|
||||||
return origSend.apply(this, arguments);
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- Fetch interception ---
|
|
||||||
const origFetch = window.fetch;
|
|
||||||
window.fetch = function(input, init) {
|
|
||||||
const url = typeof input === 'string' ? input : (input && input.url ? input.url.toString() : '');
|
|
||||||
if (url.includes('.m3u8') || url.includes('/hls/')) manifests.hls.add(url);
|
|
||||||
if (url.includes('.mpd') || url.includes('/dash/')) manifests.dash.add(url);
|
|
||||||
|
|
||||||
return origFetch.apply(this, arguments).then(function(resp) {
|
|
||||||
if (resp && resp.clone) {
|
|
||||||
resp.clone().text().then(function(text) {
|
|
||||||
if (text && typeof text === 'string') {
|
|
||||||
const m3u8Re = /https?:[^"'\\s]+\\.m3u8[^"'\\s]*/gi;
|
|
||||||
let m;
|
|
||||||
while ((m = m3u8Re.exec(text)) !== null) {
|
|
||||||
const cleaned = m[0].replace(/[;,]$/, '');
|
|
||||||
if (!cleaned.includes('/subtitle') && !cleaned.includes('/captions') && !cleaned.includes('.m3u8x')) {
|
|
||||||
manifests.hls.add(cleaned);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).catch(function(){});
|
|
||||||
}
|
|
||||||
return resp;
|
|
||||||
}).catch(function(e) { return origFetch(input, init); });
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- Scan for HLS instances on video elements ---
|
|
||||||
function scanVideos() {
|
|
||||||
scanCount++;
|
|
||||||
const videos = document.querySelectorAll('video');
|
|
||||||
const sources = [];
|
|
||||||
const hlsProps = ['hls', 'hlsInstance', '_hls', '__hls', 'player', '_player', '__player__'];
|
|
||||||
|
|
||||||
for (const v of videos) {
|
|
||||||
let src = v.currentSrc || v.src || '';
|
|
||||||
let poster = v.poster || '';
|
|
||||||
let hlsUrl = null;
|
|
||||||
|
|
||||||
// Check HLS instances
|
|
||||||
for (const prop of hlsProps) {
|
|
||||||
if (v[prop] && typeof v[prop] === 'object') {
|
|
||||||
const obj = v[prop];
|
|
||||||
if (obj.url) hlsUrl = obj.url;
|
|
||||||
else if (obj.opts && obj.opts.url) hlsUrl = obj.opts.url;
|
|
||||||
else if (obj.config && obj.config.url) hlsUrl = obj.config.url;
|
|
||||||
else if (obj.levels && obj.levels.length > 0) hlsUrl = obj.levels[0].uri;
|
|
||||||
else if (obj.sourceLevels && obj.sourceLevels.length > 0) hlsUrl = obj.sourceLevels[0].uri;
|
|
||||||
if (hlsUrl) break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hlsUrl) manifests.hls.add(hlsUrl);
|
// Helper: report manifest to content script via postMessage
|
||||||
|
function reportManifest(url, source) {
|
||||||
|
var resolved = resolveUrl(url);
|
||||||
|
if (!resolved) return;
|
||||||
|
if (window.__goon_plugin_data.manifests.includes(resolved)) return;
|
||||||
|
window.__goon_plugin_data.manifests.push(resolved);
|
||||||
|
|
||||||
sources.push({
|
|
||||||
src: src,
|
|
||||||
poster: poster,
|
|
||||||
width: v.clientWidth || 0,
|
|
||||||
height: v.clientHeight || 0,
|
|
||||||
isBlob: src.startsWith('blob:'),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Scan inline scripts for embedded URLs ---
|
|
||||||
const m3u8Re = /https?:[^"'\\s]+\\.m3u8[^"'\\s]*/gi;
|
|
||||||
for (const script of document.querySelectorAll('script')) {
|
|
||||||
if (script.src) continue;
|
|
||||||
const text = script.textContent;
|
|
||||||
if (!text) continue;
|
|
||||||
let m;
|
|
||||||
while ((m = m3u8Re.exec(text)) !== null) {
|
|
||||||
const cleaned = m[0].replace(/[;,]$/, '');
|
|
||||||
if (!cleaned.includes('/subtitle') && !cleaned.includes('/captions') && !cleaned.includes('.m3u8x')) {
|
|
||||||
manifests.hls.add(cleaned);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Scan global config objects ---
|
|
||||||
const configVars = ['hlsConfig', 'hls_player', 'hlsOptions', 'playerConfig', 'streamConfig',
|
|
||||||
'videoConfig', 'playerOpts', 'phPlayer', 'playerData', 'videoData', 'streamData',
|
|
||||||
'cloudflareConfig', 'cfPlayer', 'cfStream', 'videoSource', '__videojs__', 'videojs'];
|
|
||||||
for (const vname of configVars) {
|
|
||||||
try {
|
|
||||||
const obj = window[vname];
|
|
||||||
if (obj && typeof obj === 'object') {
|
|
||||||
function deepFind(o, depth) {
|
|
||||||
if (!o || typeof o !== 'object' || depth > 4) return;
|
|
||||||
for (const k of ['src', 'url', 'source', 'manifest', 'hls', 'm3u8', 'file', 'stream', 'video', 'data']) {
|
|
||||||
if (o[k]) {
|
|
||||||
if (typeof o[k] === 'string') {
|
|
||||||
if (o[k].includes('.m3u8')) manifests.hls.add(o[k]);
|
|
||||||
if (o[k].includes('.mpd')) manifests.dash.add(o[k]);
|
|
||||||
} else {
|
|
||||||
deepFind(o[k], depth + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
deepFind(obj, 0);
|
|
||||||
}
|
|
||||||
} catch(e) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Post results to content script via postMessage
|
|
||||||
window.postMessage({
|
window.postMessage({
|
||||||
type: '__GOON_SCAN_RESULT__',
|
type: 'GOON_PLUGIN_HLS_INTERCEPT',
|
||||||
hlsManifests: Array.from(manifests.hls),
|
url: resolved,
|
||||||
dashManifests: Array.from(manifests.dash),
|
source: source,
|
||||||
videoSources: sources,
|
timestamp: Date.now()
|
||||||
scanCount: scanCount,
|
|
||||||
}, '*');
|
}, '*');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run initial scan + periodic rescans
|
// === INTERCEPTORS ===
|
||||||
scanVideos();
|
|
||||||
setInterval(scanVideos, 1500);
|
// 1. Intercept XMLHttpRequest for manifest-like requests
|
||||||
|
var originalOpen = XMLHttpRequest.prototype.open;
|
||||||
|
var originalSend = XMLHttpRequest.prototype.send;
|
||||||
|
XMLHttpRequest.prototype.open = function(method, url) {
|
||||||
|
this._goon_url = url;
|
||||||
|
return originalOpen.apply(this, arguments);
|
||||||
|
};
|
||||||
|
XMLHttpRequest.prototype.send = function(body) {
|
||||||
|
if (this._goon_url && typeof this._goon_url === 'string') {
|
||||||
|
var resolved = resolveUrl(this._goon_url);
|
||||||
|
if (resolved && (resolved.includes('.m3u8') ||
|
||||||
|
resolved.includes('/playlist') ||
|
||||||
|
resolved.includes('/manifest'))) {
|
||||||
|
reportManifest(resolved, 'xhr');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return originalSend.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. Intercept fetch() for manifest-like requests
|
||||||
|
var originalFetch = window.fetch;
|
||||||
|
window.fetch = function(resource, init) {
|
||||||
|
var url = typeof resource === 'string' ? resource : (resource && resource.url);
|
||||||
|
if (url && typeof url === 'string') {
|
||||||
|
var resolved = resolveUrl(url);
|
||||||
|
if (resolved && (resolved.includes('.m3u8') ||
|
||||||
|
resolved.includes('/playlist') ||
|
||||||
|
resolved.includes('/manifest'))) {
|
||||||
|
reportManifest(resolved, 'fetch');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return originalFetch.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. Intercept HLS.js loadSource (catches HLS.js instances)
|
||||||
|
function patchHlsPrototype() {
|
||||||
|
if (typeof Hls === 'undefined') return;
|
||||||
|
var originalLoadSource = Hls.prototype.loadSource;
|
||||||
|
if (!originalLoadSource) return;
|
||||||
|
|
||||||
|
Hls.prototype.loadSource = function(url) {
|
||||||
|
reportManifest(url, 'hls_loadSource');
|
||||||
|
return originalLoadSource.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
var originalAttachMedia = Hls.prototype.attachMedia;
|
||||||
|
if (originalAttachMedia) {
|
||||||
|
Hls.prototype.attachMedia = function(media) {
|
||||||
|
if (media && media.tagName === 'VIDEO') {
|
||||||
|
media._goon_hls_manifest = this.url;
|
||||||
|
reportManifest(this.url, 'hls_attachMedia');
|
||||||
|
}
|
||||||
|
return originalAttachMedia.apply(this, arguments);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patch immediately if HLS is already defined
|
||||||
|
patchHlsPrototype();
|
||||||
|
|
||||||
|
// Watch for HLS.js to be dynamically defined later
|
||||||
|
var originalDefineProperty = Object.defineProperty;
|
||||||
|
var hlsPatched = false;
|
||||||
|
Object.defineProperty = function(obj, prop, descriptor) {
|
||||||
|
var result = originalDefineProperty.apply(this, arguments);
|
||||||
|
if (obj === window && prop === 'Hls' && !hlsPatched) {
|
||||||
|
hlsPatched = true;
|
||||||
|
patchHlsPrototype();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
})();
|
})();
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Inject as a script element into the page (runs in MAIN world)
|
|
||||||
const script = document.createElement('script');
|
const script = document.createElement('script');
|
||||||
script.textContent = interceptorCode;
|
script.textContent = interceptCode;
|
||||||
script.setAttribute('type', 'text/javascript');
|
script.setAttribute('type', 'text/javascript');
|
||||||
(document.head || document.documentElement).appendChild(script);
|
(document.head || document.documentElement).appendChild(script);
|
||||||
|
// Remove the script element after execution but keep interceptors active
|
||||||
script.parentNode.removeChild(script);
|
script.parentNode.removeChild(script);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Inject immediately at document_start
|
||||||
* Listen for results from the injected interceptor.
|
injectInterceptors();
|
||||||
*/
|
|
||||||
|
// Listen for intercepted HLS manifest URLs from the main world
|
||||||
window.addEventListener('message', function(event) {
|
window.addEventListener('message', function(event) {
|
||||||
if (event.source !== window) return;
|
if (event.source !== window) return;
|
||||||
if (!event.data || event.data.type !== '__GOON_SCAN_RESULT__') return;
|
if (event.data && event.data.type === 'GOON_PLUGIN_HLS_INTERCEPT') {
|
||||||
|
const url = event.data.url;
|
||||||
const data = event.data;
|
const source = event.data.source;
|
||||||
if (data.hlsManifests) {
|
if (!interceptedSources.has(url)) {
|
||||||
for (const url of data.hlsManifests) {
|
interceptedSources.add(url);
|
||||||
collectedData.hlsManifests.add(url);
|
interceptedManifests.push({
|
||||||
|
url: url,
|
||||||
|
source: source,
|
||||||
|
timestamp: event.data.timestamp
|
||||||
|
});
|
||||||
|
console.log('[goon-plugin] Intercepted HLS manifest via', source, ':', url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (data.dashManifests) {
|
|
||||||
for (const url of data.dashManifests) {
|
|
||||||
collectedData.dashManifests.add(url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (data.videoSources && data.videoSources.length > 0) {
|
|
||||||
collectedData.videoSources = data.videoSources;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -237,36 +181,48 @@
|
|||||||
];
|
];
|
||||||
|
|
||||||
for (const selector of posterSelectors) {
|
for (const selector of posterSelectors) {
|
||||||
|
try {
|
||||||
const el = document.querySelector(selector);
|
const el = document.querySelector(selector);
|
||||||
if (el) {
|
if (el) {
|
||||||
const url = el.getAttribute('content') || el.getAttribute('src') || el.getAttribute('data-src') || el.getAttribute('data-poster');
|
const url = el.getAttribute('content') || el.getAttribute('src') ||
|
||||||
|
el.getAttribute('data-src') || el.getAttribute('data-poster');
|
||||||
if (url && url.startsWith('http')) {
|
if (url && url.startsWith('http')) {
|
||||||
collectedData.posterUrl = url;
|
return url;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch(e) {}
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inject the interceptor on load
|
|
||||||
injectInterceptor();
|
|
||||||
|
|
||||||
// Extract poster from the content script's DOM view
|
|
||||||
extractPoster();
|
|
||||||
// Re-extract after a delay to catch dynamic content
|
|
||||||
setTimeout(extractPoster, 3000);
|
|
||||||
setTimeout(extractPoster, 6000);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get collected data as a serializable object.
|
* Get video data: intercepted manifests + DOM scan + poster.
|
||||||
|
* Called by the popup via chrome.tabs.sendMessage.
|
||||||
*/
|
*/
|
||||||
function getData() {
|
function getVideoData() {
|
||||||
extractPoster(); // Fresh scan
|
const poster = extractPoster();
|
||||||
|
|
||||||
|
// DOM video scan (isolated world — can access DOM but not page JS)
|
||||||
|
const videoElements = [];
|
||||||
|
try {
|
||||||
|
const videos = document.querySelectorAll('video');
|
||||||
|
for (const video of videos) {
|
||||||
|
const style = window.getComputedStyle(video);
|
||||||
|
if (style.display !== 'none' && video.clientWidth > 0 && video.clientHeight > 0) {
|
||||||
|
videoElements.push({
|
||||||
|
src: video.currentSrc || video.src || '',
|
||||||
|
poster: video.poster || '',
|
||||||
|
width: video.clientWidth,
|
||||||
|
height: video.clientHeight,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(e) { /* ignore DOM access errors */ }
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hlsManifests: Array.from(collectedData.hlsManifests),
|
posterUrl: poster,
|
||||||
dashManifests: Array.from(collectedData.dashManifests),
|
interceptedManifests: interceptedManifests,
|
||||||
videoSources: collectedData.videoSources,
|
videoElements: videoElements,
|
||||||
posterUrl: collectedData.posterUrl,
|
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -274,16 +230,11 @@
|
|||||||
// Listen for messages from the popup
|
// Listen for messages from the popup
|
||||||
chrome.runtime.onMessage.addListener(function(message, _sender, sendResponse) {
|
chrome.runtime.onMessage.addListener(function(message, _sender, sendResponse) {
|
||||||
if (message.type === 'GOON_GET_VIDEO_DATA') {
|
if (message.type === 'GOON_GET_VIDEO_DATA') {
|
||||||
sendResponse(getData());
|
sendResponse(getVideoData());
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (message.type === 'GOON_INJECT') {
|
|
||||||
injectInterceptor();
|
|
||||||
sendResponse({ injected: true });
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('[goon-plugin] Content script loaded (isolated world). Interceptor injected.');
|
console.log('[goon-plugin] Content script loaded. Interceptors injected at document_start.');
|
||||||
})();
|
})();
|
||||||
+333
-76
@@ -1,6 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
* index.js — Popup logic for the Goon Plugin.
|
* index.js — Popup logic for the Goon Plugin.
|
||||||
* Runs inside the popup; scans the current tab for videos and opens the viewer.
|
* 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
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const STORAGE_KEY = 'goon_videos';
|
const STORAGE_KEY = 'goon_videos';
|
||||||
@@ -50,7 +55,6 @@ function renderList(items) {
|
|||||||
listEl.appendChild(link);
|
listEl.appendChild(link);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy all URLs button
|
|
||||||
const copyBtn = document.createElement('button');
|
const copyBtn = document.createElement('button');
|
||||||
copyBtn.className = 'copy-btn';
|
copyBtn.className = 'copy-btn';
|
||||||
copyBtn.textContent = 'Copy All URLs';
|
copyBtn.textContent = 'Copy All URLs';
|
||||||
@@ -82,9 +86,222 @@ function showActions() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ====================================================================
|
||||||
|
* 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.
|
||||||
|
* ==================================================================== */
|
||||||
|
|
||||||
|
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
|
||||||
|
const manifests = [];
|
||||||
|
const manifestSet = new Set();
|
||||||
|
|
||||||
|
function addManifest(url) {
|
||||||
|
const resolved = resolveUrl(url);
|
||||||
|
if (resolved && !manifestSet.has(resolved)) {
|
||||||
|
manifestSet.add(resolved);
|
||||||
|
manifests.push(resolved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) { /* DOM access error */ }
|
||||||
|
|
||||||
|
// --- 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
deepProbeLimited(obj, 3);
|
||||||
|
}
|
||||||
|
} 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) {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
manifests: manifests,
|
||||||
|
count: manifests.length,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
/* === "Find Videos" — scan all tabs in the window === */
|
/* === "Find Videos" — scan all tabs in the window === */
|
||||||
findBtn.addEventListener('click', async () => {
|
findBtn.addEventListener('click', async () => {
|
||||||
setStatus('Scanning tabs…');
|
setStatus('Scanning tabs...');
|
||||||
|
|
||||||
// Get all tabs in the current window
|
// Get all tabs in the current window
|
||||||
const allTabs = await chrome.tabs.query({ currentWindow: true });
|
const allTabs = await chrome.tabs.query({ currentWindow: true });
|
||||||
@@ -105,10 +322,9 @@ findBtn.addEventListener('click', async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tabsToScan = validTabs;
|
const tabsToScan = validTabs;
|
||||||
setStatus(`Scanning ${tabsToScan.length} tab(s)…`);
|
setStatus(`Scanning ${tabsToScan.length} tab(s)...`);
|
||||||
|
|
||||||
try {
|
// DOM scrape function (runs in main world)
|
||||||
// Execute scraping on all tabs in parallel
|
|
||||||
const scrapeFn = () => {
|
const scrapeFn = () => {
|
||||||
const dataKeys = ['src', 'video', 'mp4', 'webm', 'poster'];
|
const dataKeys = ['src', 'video', 'mp4', 'webm', 'poster'];
|
||||||
|
|
||||||
@@ -126,7 +342,6 @@ findBtn.addEventListener('click', async () => {
|
|||||||
|| v.getAttribute('data-poster') || null;
|
|| v.getAttribute('data-poster') || null;
|
||||||
return { src, poster, area, width: v.clientWidth, height: v.clientHeight };
|
return { src, poster, area, width: v.clientWidth, height: v.clientHeight };
|
||||||
});
|
});
|
||||||
// Sort by displayed size descending, pick the largest
|
|
||||||
allVideos.sort((a, b) => b.area - a.area);
|
allVideos.sort((a, b) => b.area - a.area);
|
||||||
const largest = allVideos.length > 0 ? allVideos[0] : null;
|
const largest = allVideos.length > 0 ? allVideos[0] : null;
|
||||||
const sources = Array.from(document.querySelectorAll('video source[src]')).map(s => s.src);
|
const sources = Array.from(document.querySelectorAll('video source[src]')).map(s => s.src);
|
||||||
@@ -135,12 +350,11 @@ findBtn.addEventListener('click', async () => {
|
|||||||
return { videos: largest ? [largest] : [], videoCount: largest ? 1 : 0, sources, audios };
|
return { videos: largest ? [largest] : [], videoCount: largest ? 1 : 0, sources, audios };
|
||||||
};
|
};
|
||||||
|
|
||||||
// Query content scripts for real-time HLS interception data
|
// Query content script for intercepted data + poster
|
||||||
async function queryContentScript(tabId) {
|
async function queryContentScript(tabId) {
|
||||||
try {
|
try {
|
||||||
return await chrome.tabs.sendMessage(tabId, { type: 'GOON_GET_VIDEO_DATA' });
|
return await chrome.tabs.sendMessage(tabId, { type: 'GOON_GET_VIDEO_DATA' });
|
||||||
} catch {
|
} catch {
|
||||||
// Content script not available (e.g., chrome:// pages)
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -148,14 +362,21 @@ findBtn.addEventListener('click', async () => {
|
|||||||
const results = await Promise.all(
|
const results = await Promise.all(
|
||||||
tabsToScan.map(async t => {
|
tabsToScan.map(async t => {
|
||||||
try {
|
try {
|
||||||
const [scriptResult, contentData] = await Promise.all([
|
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({
|
chrome.scripting.executeScript({
|
||||||
target: { tabId: t.id, allFrames: true },
|
target: { tabId: t.id, allFrames: true },
|
||||||
func: scrapeFn,
|
func: scrapeFn,
|
||||||
}),
|
}),
|
||||||
|
// STRATEGY 3: Content script intercepted manifests + poster
|
||||||
queryContentScript(t.id),
|
queryContentScript(t.id),
|
||||||
]);
|
]);
|
||||||
return { scriptResult: scriptResult.filter(Boolean), contentData };
|
return { hlsQueryResults: hlsQueryResults.filter(Boolean), scriptResult: scriptResult.filter(Boolean), contentData };
|
||||||
} catch {
|
} catch {
|
||||||
console.warn('[goon] skipping tab', t.id, ':', t.url);
|
console.warn('[goon] skipping tab', t.id, ':', t.url);
|
||||||
return null;
|
return null;
|
||||||
@@ -163,90 +384,133 @@ findBtn.addEventListener('click', async () => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Build tab results — merge DOM snapshot with content script interception data
|
// Merge all results into unique video entries
|
||||||
const tabResults = tabsToScan.map((tab, i) => {
|
|
||||||
const res = results[i];
|
|
||||||
if (!res) return null;
|
|
||||||
return {
|
|
||||||
tabTitle: tab.title || 'Unknown',
|
|
||||||
frames: res.scriptResult || [],
|
|
||||||
contentData: res.contentData || null,
|
|
||||||
};
|
|
||||||
}).filter(Boolean);
|
|
||||||
const scanned = tabResults.length;
|
|
||||||
|
|
||||||
const seen = new Map();
|
const seen = new Map();
|
||||||
let totalFound = 0;
|
let totalFound = 0;
|
||||||
for (const group of tabResults) {
|
|
||||||
// Collect HLS manifests from the content script (real-time interception)
|
|
||||||
const hlsManifests = (group.contentData && group.contentData.hlsManifests) || [];
|
|
||||||
const contentPoster = (group.contentData && group.contentData.posterUrl) || null;
|
|
||||||
|
|
||||||
// Process DOM snapshot frames
|
for (let ti = 0; ti < tabsToScan.length; ti++) {
|
||||||
for (const r of group.frames) {
|
const tab = tabsToScan[ti];
|
||||||
if (!r.result) continue;
|
const res = results[ti];
|
||||||
for (const v of r.result.videos) {
|
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++;
|
totalFound++;
|
||||||
const key = (v.src || '(empty)') + '||' + (v.poster || '(none)');
|
const key = (v.src || '(empty)') + '||' + (v.poster || '(none)');
|
||||||
if (!seen.has(key)) {
|
if (!seen.has(key)) {
|
||||||
const item = { url: v.src || v.poster || '', poster: v.poster || null, tab: group.tabTitle };
|
const item = {
|
||||||
if (v.src) item.src = v.src;
|
url: v.src,
|
||||||
|
src: v.src,
|
||||||
// Attach HLS data from content script interception
|
poster: v.poster || contentPoster || null,
|
||||||
if (hlsManifests.length > 0) {
|
tab: tabTitle,
|
||||||
item.hlsManifest = hlsManifests[0];
|
};
|
||||||
|
// If HLS is available for this tab, attach it
|
||||||
|
if (allManifests.length > 0) {
|
||||||
|
item.hlsManifest = allManifests[0];
|
||||||
item.mediaType = 'hls';
|
item.mediaType = 'hls';
|
||||||
// When HLS is present, prefer the manifest URL as the primary URL
|
|
||||||
if (!v.src || v.src.startsWith('blob:')) {
|
|
||||||
item.url = hlsManifests[0];
|
|
||||||
item.src = hlsManifests[0];
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Use poster from content script if available and better
|
|
||||||
if (contentPoster && !item.poster) {
|
|
||||||
item.poster = contentPoster;
|
|
||||||
}
|
|
||||||
|
|
||||||
seen.set(key, item);
|
seen.set(key, item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// If no videos found in DOM but content script found HLS manifests + video sources,
|
// Priority 3: Content script video elements
|
||||||
// create entries from the intercepted data
|
for (const v of csVideos) {
|
||||||
if (!group.frames.length && group.contentData) {
|
if (!v.src || v.src.startsWith('blob:')) continue;
|
||||||
const contentVideos = group.contentData.videoSources || [];
|
|
||||||
for (const cv of contentVideos) {
|
|
||||||
if (!cv.src && !hlsManifests.length) continue;
|
|
||||||
totalFound++;
|
totalFound++;
|
||||||
const effectiveSrc = hlsManifests[0] || cv.src;
|
const key = (v.src || '(empty)') + '||' + (v.poster || '(none)');
|
||||||
const key = effectiveSrc + '||' + (cv.poster || '(none)');
|
|
||||||
if (!seen.has(key)) {
|
if (!seen.has(key)) {
|
||||||
const item = {
|
const item = {
|
||||||
url: effectiveSrc,
|
url: v.src,
|
||||||
src: effectiveSrc,
|
src: v.src,
|
||||||
poster: cv.poster || contentPoster || null,
|
poster: v.poster || contentPoster || null,
|
||||||
tab: group.tabTitle,
|
tab: tabTitle,
|
||||||
};
|
};
|
||||||
if (hlsManifests.length > 0) {
|
if (allManifests.length > 0) {
|
||||||
item.hlsManifest = hlsManifests[0];
|
item.hlsManifest = allManifests[0];
|
||||||
item.mediaType = 'hls';
|
item.mediaType = 'hls';
|
||||||
}
|
}
|
||||||
seen.set(key, item);
|
seen.set(key, item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
// Filter: keep only http/https URLs
|
||||||
const items = [...seen.values()].filter(v => {
|
const items = [...seen.values()].filter(v => {
|
||||||
const url = v.src || v.url || '';
|
const url = v.src || v.url || '';
|
||||||
return url.startsWith('http://') || url.startsWith('https://');
|
return url.startsWith('http://') || url.startsWith('https://');
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('[goon] ===== Found', totalFound, 'video elements across', tabsToScan.length, 'tabs, unique:', items.length);
|
console.log('[goon] ===== Found', totalFound, 'video elements, unique:', items.length, 'across', tabsToScan.length, 'tabs');
|
||||||
|
|
||||||
// Replace the entire list (fresh scan) — preserve hlsManifest and mediaType
|
// Store results
|
||||||
const storedItems = items.map(v => ({
|
const storedItems = items.map(v => ({
|
||||||
url: v.url,
|
url: v.url,
|
||||||
src: v.src || v.url,
|
src: v.src || v.url,
|
||||||
@@ -261,16 +525,12 @@ findBtn.addEventListener('click', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const skippedTabs = results.filter(r => r === null).length;
|
const skippedTabs = results.filter(r => r === null).length;
|
||||||
const statusMsg = `Found ${items.length} video(s) across ${scanned} tab(s).`;
|
const statusMsg = `Found ${items.length} video(s) across ${tabsToScan.length} tab(s).`;
|
||||||
setStatus(skippedTabs > 0 ? `${statusMsg} ${skippedTabs} tab(s) skipped (idle).` : statusMsg);
|
setStatus(skippedTabs > 0 ? `${statusMsg} ${skippedTabs} tab(s) skipped.` : statusMsg);
|
||||||
btnRow.style.display = 'flex';
|
btnRow.style.display = 'flex';
|
||||||
} catch (e) {
|
|
||||||
setStatus(`Scan failed: ${e.message}`);
|
|
||||||
console.warn('[goon] executeScript failed:', e);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/* === "Open Viewer" — launch the 2x2 grid === */
|
/* === "Open Viewer" — launch the NxN grid === */
|
||||||
openBtn.addEventListener('click', async () => {
|
openBtn.addEventListener('click', async () => {
|
||||||
const { urls, tags } = await loadVideos();
|
const { urls, tags } = await loadVideos();
|
||||||
console.log('[goon] Open Viewer clicked, loaded from storage:', urls, 'tags:', tags);
|
console.log('[goon] Open Viewer clicked, loaded from storage:', urls, 'tags:', tags);
|
||||||
@@ -280,11 +540,9 @@ openBtn.addEventListener('click', async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dedup by index to preserve tag alignment
|
|
||||||
const videoUrls = urls;
|
const videoUrls = urls;
|
||||||
const videoTags = tags || [];
|
const videoTags = tags || [];
|
||||||
|
|
||||||
// Compute NxN grid
|
|
||||||
const N = Math.ceil(Math.sqrt(videoUrls.length));
|
const N = Math.ceil(Math.sqrt(videoUrls.length));
|
||||||
const PANEL_MIN_W = 420;
|
const PANEL_MIN_W = 420;
|
||||||
const PANEL_MIN_H = 340;
|
const PANEL_MIN_H = 340;
|
||||||
@@ -295,7 +553,6 @@ openBtn.addEventListener('click', async () => {
|
|||||||
const id = 'goonViewer_' + Date.now();
|
const id = 'goonViewer_' + Date.now();
|
||||||
await chrome.storage.local.set({ [id]: videoUrls });
|
await chrome.storage.local.set({ [id]: videoUrls });
|
||||||
|
|
||||||
// Verify write
|
|
||||||
const verify = await chrome.storage.local.get([id]);
|
const verify = await chrome.storage.local.get([id]);
|
||||||
console.log('[goon] viewer storage verified:', verify[id]?.length, 'entries');
|
console.log('[goon] viewer storage verified:', verify[id]?.length, 'entries');
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user