139 lines
4.3 KiB
JavaScript
139 lines
4.3 KiB
JavaScript
const MEDIA_EXT_RE = /\.(mp4|m4v|webm|mkv|mov|m3u8|mpd)([?#]|$)/i;
|
|
const MEDIA_CONTENT_RE = /^(video\/|application\/(vnd\.apple\.mpegurl|x-mpegurl|dash\+xml)|audio\/mpegurl)/i;
|
|
const MAX_CANDIDATES_PER_TAB = 120;
|
|
|
|
const tabCandidates = new Map();
|
|
const tabPageUrls = new Map();
|
|
|
|
const actionApi = chrome.action || chrome.browserAction;
|
|
|
|
function ignoreChromeResult(result) {
|
|
if (result && typeof result.catch === 'function') result.catch(() => {});
|
|
}
|
|
|
|
function setBadgeTextSafe(tabId, text) {
|
|
if (actionApi?.setBadgeText) ignoreChromeResult(actionApi.setBadgeText({tabId, text}));
|
|
}
|
|
|
|
function setBadgeBackgroundColorSafe(tabId, color) {
|
|
if (actionApi?.setBadgeBackgroundColor) ignoreChromeResult(actionApi.setBadgeBackgroundColor({tabId, color}));
|
|
}
|
|
|
|
function candidateKey(candidate) {
|
|
return candidate.url;
|
|
}
|
|
|
|
function normalizeUrl(rawUrl) {
|
|
if (!rawUrl || typeof rawUrl !== 'string') return null;
|
|
try {
|
|
return new URL(rawUrl).href;
|
|
} catch (_) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function looksLikeMedia(url, contentType = '') {
|
|
return MEDIA_EXT_RE.test(url) || MEDIA_CONTENT_RE.test(String(contentType).split(';', 1)[0].trim());
|
|
}
|
|
|
|
function addCandidate(tabId, candidate) {
|
|
if (tabId < 0 || !candidate?.url) return;
|
|
const url = normalizeUrl(candidate.url);
|
|
if (!url) return;
|
|
const contentType = candidate.content_type || candidate.type || '';
|
|
if (!looksLikeMedia(url, contentType)) return;
|
|
|
|
const existing = tabCandidates.get(tabId) || [];
|
|
if (existing.some((item) => candidateKey(item) === url)) return;
|
|
existing.unshift({
|
|
url,
|
|
title: candidate.title || candidate.name || '',
|
|
content_type: contentType,
|
|
quality: candidate.quality || '',
|
|
source: candidate.source || 'webRequest',
|
|
ts: Date.now(),
|
|
});
|
|
tabCandidates.set(tabId, existing.slice(0, MAX_CANDIDATES_PER_TAB));
|
|
setBadgeTextSafe(tabId, String(Math.min(existing.length, 99)));
|
|
setBadgeBackgroundColorSafe(tabId, '#38bdf8');
|
|
}
|
|
|
|
chrome.webRequest.onBeforeRequest.addListener(
|
|
(details) => {
|
|
if (details.tabId < 0 || details.type === 'image') return;
|
|
if (looksLikeMedia(details.url)) {
|
|
addCandidate(details.tabId, {url: details.url, source: `request:${details.type}`});
|
|
}
|
|
},
|
|
{urls: ['<all_urls>']}
|
|
);
|
|
|
|
chrome.webRequest.onHeadersReceived.addListener(
|
|
(details) => {
|
|
if (details.tabId < 0) return;
|
|
const header = (details.responseHeaders || []).find((item) => item.name.toLowerCase() === 'content-type');
|
|
const contentType = header?.value || '';
|
|
if (looksLikeMedia(details.url, contentType)) {
|
|
addCandidate(details.tabId, {url: details.url, content_type: contentType, source: 'response'});
|
|
}
|
|
},
|
|
{urls: ['<all_urls>']},
|
|
['responseHeaders']
|
|
);
|
|
|
|
chrome.webNavigation?.onCommitted?.addListener?.((details) => {
|
|
if (details.frameId === 0) {
|
|
tabPageUrls.set(details.tabId, details.url);
|
|
tabCandidates.delete(details.tabId);
|
|
setBadgeTextSafe(details.tabId, '');
|
|
}
|
|
});
|
|
|
|
chrome.tabs.onRemoved.addListener((tabId) => {
|
|
tabCandidates.delete(tabId);
|
|
tabPageUrls.delete(tabId);
|
|
});
|
|
|
|
async function collectDomCandidates(tabId) {
|
|
try {
|
|
const responses = await chrome.tabs.sendMessage(tabId, {type: 'collect-media'});
|
|
return Array.isArray(responses) ? responses : responses?.candidates || [];
|
|
} catch (_) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
(async () => {
|
|
if (message?.type === 'candidate' && sender.tab?.id != null) {
|
|
addCandidate(sender.tab.id, message.candidate);
|
|
sendResponse({ok: true});
|
|
return;
|
|
}
|
|
|
|
if (message?.type === 'get-candidates') {
|
|
const tabId = message.tabId;
|
|
const domCandidates = await collectDomCandidates(tabId);
|
|
for (const candidate of domCandidates) addCandidate(tabId, {...candidate, source: candidate.source || 'dom'});
|
|
const tab = await chrome.tabs.get(tabId).catch(() => null);
|
|
sendResponse({
|
|
ok: true,
|
|
page_url: tab?.url || tabPageUrls.get(tabId) || '',
|
|
candidates: tabCandidates.get(tabId) || [],
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (message?.type === 'clear-candidates') {
|
|
const tabId = message.tabId;
|
|
tabCandidates.delete(tabId);
|
|
setBadgeTextSafe(tabId, '');
|
|
sendResponse({ok: true});
|
|
return;
|
|
}
|
|
|
|
sendResponse({ok: false, error: 'unknown message'});
|
|
})();
|
|
return true;
|
|
});
|