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

69 lines
2.5 KiB
JavaScript

const MEDIA_EXT_RE = /\.(mp4|m4v|webm|mkv|mov|m3u8|mpd)([?#]|$)/i;
const ABSOLUTE_MEDIA_RE = /(?:https?:\/\/|https?:\\\/\\\/|\/\/|\\\/\\\/)[^\s"'<>]+?\.(?:mp4|m4v|webm|mkv|mov|m3u8|mpd)(?=$|[?&#\s"'<>])(?:[?&][^\s"'<>]*)?/gi;
function ignoreChromeResult(result) {
if (result && typeof result.catch === 'function') result.catch(() => {});
}
function normalizeMediaUrl(rawUrl, baseUrl = location.href) {
if (!rawUrl || typeof rawUrl !== 'string') return null;
try {
const cleaned = rawUrl
.trim()
.replaceAll('\\/', '/')
.replaceAll('\\u002F', '/')
.replaceAll('\\u002f', '/')
.replace(/[;,)}\]]+$/, '');
return new URL(cleaned, baseUrl).href;
} catch (_) {
return null;
}
}
function pushCandidate(candidates, rawUrl, title = document.title, contentType = '', source = 'dom') {
const url = normalizeMediaUrl(rawUrl);
if (!url) return;
const cleanType = String(contentType || '').split(';', 1)[0].trim();
if (MEDIA_EXT_RE.test(url) || cleanType.startsWith('video/') || /mpegurl|dash\+xml/i.test(cleanType)) {
candidates.push({url, title: title || document.title, content_type: contentType || '', source});
}
}
function collectCandidates() {
const candidates = [];
document.querySelectorAll('video,source,a').forEach((el) => {
pushCandidate(candidates, el.currentSrc || el.src || el.href, el.title || el.textContent || document.title, el.type || '', 'dom');
});
performance.getEntriesByType('resource').forEach((entry) => {
pushCandidate(candidates, entry.name, document.title, entry.initiatorType === 'video' ? 'video/unknown' : '', `performance:${entry.initiatorType || 'resource'}`);
});
const html = document.documentElement?.outerHTML || '';
for (const match of html.matchAll(ABSOLUTE_MEDIA_RE)) {
pushCandidate(candidates, match[0], document.title, '', 'html-js');
}
const seen = new Set();
return candidates.filter((candidate) => {
if (seen.has(candidate.url)) return false;
seen.add(candidate.url);
return true;
}).slice(0, 120);
}
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (message?.type === 'collect-media') {
sendResponse({candidates: collectCandidates()});
return true;
}
return false;
});
// Opportunistisch nach dem Laden melden, damit das Popup sofort Daten hat.
setTimeout(() => {
for (const candidate of collectCandidates()) {
ignoreChromeResult(chrome.runtime.sendMessage({type: 'candidate', candidate}));
}
}, 1500);