Strategy G — Independent non-enumerable + Symbol property detection on video elements. Uses Object.getOwnPropertyNames/Symbols to catch properties hidden by enumerable:false or Symbol keys. Walks nested objects for URL/levels/config signatures matching HLS instance heuristics. Strategy H — Full window global enumeration with player-like key filtering. Catches previously-unknown globals the player wrapper exposes internally. Content script bridge — merge content-level intercepted manifests into window.__goon_plugin_data so both interceptors and executeScript share findings. Add phncdn_reconstructed to dedup priority ranking.
188 lines
7.0 KiB
JavaScript
188 lines
7.0 KiB
JavaScript
/**
|
|
* content.js - Content script injected into matching pages at document_start.
|
|
* Runs in the ISOLATED world.
|
|
*
|
|
* Strategy: Inject XHR/fetch interceptors into the MAIN world at document_start
|
|
* to capture HLS manifest URLs as they load. Also listen for postMessage events
|
|
* from intercepted URLs.
|
|
*
|
|
* 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 () {
|
|
'use strict';
|
|
|
|
// Collected intercepted HLS manifest URLs from XHR/fetch interception
|
|
const interceptedManifests = [];
|
|
const interceptedSources = new Set();
|
|
|
|
/**
|
|
* Inject interceptors into the MAIN world at document_start.
|
|
*
|
|
* Uses script.src = chrome.runtime.getURL('interceptors.js') instead of
|
|
* script.textContent to bypass strict page CSP. Sites like Pornhub enforce
|
|
* 'script-src' that blocks inline execution (textContent). Chrome treats
|
|
* extension-originated <script src="..."> as content scripts — not as
|
|
* page inline code — so the page's CSP does not apply.
|
|
*
|
|
* IMPORTANT: Remove the script element ONLY after the load event fires.
|
|
* Removing it synchronously can prevent the browser from starting the fetch,
|
|
* especially in Edge/Chrome with strict timing.
|
|
*
|
|
* Runs BEFORE any page JS (document_start), so we catch all HLS activity.
|
|
*/
|
|
function injectInterceptors() {
|
|
const script = document.createElement('script');
|
|
script.src = chrome.runtime.getURL('interceptors.js');
|
|
script.setAttribute('type', 'text/javascript');
|
|
script.onload = function () {
|
|
// Script loaded and executed — safe to remove the element now
|
|
if (script.parentNode) {
|
|
script.parentNode.removeChild(script);
|
|
}
|
|
};
|
|
(document.head || document.documentElement).appendChild(script);
|
|
// Keep the element in the DOM — onload will remove it after execution
|
|
}
|
|
|
|
// Inject immediately at document_start
|
|
injectInterceptors();
|
|
|
|
// Listen for intercepted HLS manifest URLs from the main world
|
|
window.addEventListener('message', function(event) {
|
|
if (event.source !== window) return;
|
|
if (event.data && event.data.type === 'GOON_PLUGIN_HLS_INTERCEPT') {
|
|
const url = event.data.url;
|
|
const source = event.data.source;
|
|
if (!interceptedSources.has(url)) {
|
|
interceptedSources.add(url);
|
|
interceptedManifests.push({
|
|
url: url,
|
|
source: source,
|
|
timestamp: event.data.timestamp
|
|
});
|
|
console.log('[goon-plugin] Intercepted HLS manifest via', source, ':', url);
|
|
console.log('[goon-plugin] Total intercepted manifests so far:', interceptedManifests.length);
|
|
}
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Extract poster URL from the page (content script CAN access DOM).
|
|
*/
|
|
function extractPoster() {
|
|
const posterSelectors = [
|
|
'meta[property="og:image"]',
|
|
'meta[name="twitter:image"]',
|
|
'meta[itemprop="image"]',
|
|
'img.poster, img.thumbnail, .video-poster img, .thumbnail img, .preview img',
|
|
'[data-poster]',
|
|
];
|
|
|
|
for (const selector of posterSelectors) {
|
|
try {
|
|
const el = document.querySelector(selector);
|
|
if (el) {
|
|
const url = el.getAttribute('content') || el.getAttribute('src') ||
|
|
el.getAttribute('data-src') || el.getAttribute('data-poster');
|
|
if (url && url.startsWith('http')) {
|
|
return url;
|
|
}
|
|
}
|
|
} catch {}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get video data: intercepted manifests + DOM scan + poster + Hls.cast probe.
|
|
* Called by the popup via chrome.tabs.sendMessage.
|
|
*/
|
|
function getVideoData() {
|
|
const poster = extractPoster();
|
|
console.log('[goon-content] getVideoData called. Poster:', poster);
|
|
console.log('[goon-content] Intercepted manifests count:', interceptedManifests.length);
|
|
if (interceptedManifests.length > 0) {
|
|
console.log('[goon-content] Manifest details:', JSON.stringify(interceptedManifests.map(m => ({ url: m.url, source: m.source }))));
|
|
}
|
|
|
|
// DOM video scan (isolated world — can access DOM but not page JS)
|
|
const videoElements = [];
|
|
try {
|
|
const videos = document.querySelectorAll('video');
|
|
console.log('[goon-content] Found', videos.length, '<video> elements in DOM');
|
|
for (const video of videos) {
|
|
const style = window.getComputedStyle(video);
|
|
const isVisible = style.display !== 'none' && video.clientWidth > 0 && video.clientHeight > 0;
|
|
console.log('[goon-content] Video:', {
|
|
src: (video.currentSrc || video.src || '').substring(0, 120),
|
|
visible: isVisible,
|
|
dimensions: video.clientWidth + 'x' + video.clientHeight,
|
|
networkState: video.networkState,
|
|
readyState: video.readyState,
|
|
});
|
|
if (isVisible) {
|
|
// Extract buffered ranges as serializable arrays
|
|
const bufferedRanges = [];
|
|
if (video.buffered && video.buffered.length > 0) {
|
|
for (let b = 0; b < video.buffered.length; b++) {
|
|
bufferedRanges.push({ start: video.buffered.start(b), end: video.buffered.end(b) });
|
|
}
|
|
}
|
|
|
|
videoElements.push({
|
|
src: video.currentSrc || video.src || '',
|
|
poster: video.poster || '',
|
|
width: video.clientWidth,
|
|
height: video.clientHeight,
|
|
duration: video.duration || 0,
|
|
buffered: bufferedRanges,
|
|
});
|
|
}
|
|
}
|
|
} catch(e) { /* ignore DOM access errors */
|
|
console.warn('[goon-content] DOM scan error:', e && e.message ? e.message : e);
|
|
}
|
|
|
|
console.log('[goon-content] Visible video elements:', videoElements.length);
|
|
// Expose interceptor data globally so the popup's deep probe can read it.
|
|
window.__goon_plugin_data = window.__goon_plugin_data || {
|
|
manifests: [],
|
|
sources: [],
|
|
_contentType: 'content-script',
|
|
};
|
|
// Merge our intercepted manifests into the shared data store (the interceptors
|
|
// write their own URLs here; we add content-script-level XHR intercepts).
|
|
for (const m of interceptedManifests) {
|
|
if (!window.__goon_plugin_data.manifests.includes(m.url)) {
|
|
window.__goon_plugin_data.manifests.push(m.url);
|
|
window.__goon_plugin_data.sources.push(m.source);
|
|
}
|
|
}
|
|
|
|
return {
|
|
posterUrl: poster,
|
|
interceptedManifests: interceptedManifests,
|
|
videoElements: videoElements,
|
|
timestamp: Date.now(),
|
|
};
|
|
}
|
|
|
|
// Listen for messages from the popup
|
|
chrome.runtime.onMessage.addListener(function(message, _sender, sendResponse) {
|
|
if (message.type === 'GOON_GET_VIDEO_DATA') {
|
|
sendResponse(getVideoData());
|
|
return false;
|
|
}
|
|
return false;
|
|
});
|
|
|
|
console.log('[goon-plugin] Content script loaded. Interceptors injected at document_start.');
|
|
})();
|