Files
project-kino/runtime-current/kino/app-src/browser-extension/kino-capture/service_worker.js

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;
});