cross origin communication with postMessage

This commit is contained in:
Kyush 2026-06-06 20:16:38 +09:00
commit 10d5c46ec3
3 changed files with 67 additions and 22 deletions

13
metadata.user.js Normal file
View file

@ -0,0 +1,13 @@
// ==UserScript==
// @name M3U8 HLS Downloader
// @namespace http://tampermonkey.net/
// @version 1.0.0
// @description 자동 탐지 및 다운로드: HLS(m3u8) 스트림의 세그먼트를 병합하여 단일 파일로 저장
// @author kyush
// @match *://*/*
// @grant GM_xmlhttpRequest
// @grant unsafeWindow
// @grant GM_registerMenuCommand
// @run-at document-start
// @all-frames true
// ==/UserScript==

View file

@ -42,7 +42,13 @@ export function interceptXHR(onDetected: (url: string, headers?: Record<string,
if (status >= 200 && status < 300) {
const isM3U8Response = isM3U8Url(requestUrl) || contentType.toLowerCase().includes('mpegurl') || responseText.startsWith('#EXTM3U');
if (isM3U8Response) {
onDetected(requestUrl, Object.keys(headers).length ? headers : undefined);
const capturedHeaders = Object.keys(headers).length ? headers : {};
if (!capturedHeaders.Referer) {
try {
capturedHeaders.Referer = location.href;
} catch { /* cross-origin */ }
}
onDetected(requestUrl, capturedHeaders);
}
}
} catch (e) {

View file

@ -23,14 +23,18 @@ function isTopFrame(): boolean {
}
}
function notifyTopFrame(url: string): void {
function notifyTopFrame(url: string, headers?: Record<string, string>): void {
if (!isTopFrame()) {
try {
if (window.top && (window.top as any)._m3u8Detected) {
(window.top as any)._m3u8Detected(url);
}
window.top.postMessage({
__m3u8dl: true,
url,
referer: headers?.Referer || location.href,
origin: location.origin,
}, '*');
log.info(`notifyTopFrame: sent via postMessage: ${url}`);
} catch (e) {
log.info('notifyTopFrame: cross-origin iframe cannot notify top (expected)');
log.warn(`notifyTopFrame: postMessage failed: ${e}`);
}
}
}
@ -85,7 +89,7 @@ async function startDownload(url: string): Promise<void> {
if (!parsed) {
try {
parsed = await parseM3U8(url);
parsed = await parseM3U8(url, entry?.headers?.Referer);
if (entry) entry.parsed = parsed;
} catch (e) {
log.error(`startDownload: parse failed for ${url}: ${e}`);
@ -103,7 +107,7 @@ async function startDownload(url: string): Promise<void> {
}
targetUrl = selected;
try {
parsed = await parseM3U8(targetUrl);
parsed = await parseM3U8(targetUrl, entry?.headers?.Referer);
if (entry) entry.parsed = parsed;
} catch (e) {
log.error(`startDownload: parse selected playlist failed: ${e}`);
@ -156,14 +160,18 @@ function onM3U8Detected(url: string, headers?: Record<string, string>): void {
log.info(`onM3U8Detected: ${url}`);
detectedM3U8s.set(url, { url, headers });
notifyTopFrame(url);
notifyTopFrame(url, headers);
if (isTopFrame() && panel) {
addStreamToUI(url);
}
if (isTopFrame()) {
addStreamToUI(url);
parseM3U8(url, headers?.Referer).then(parsed => {
detectedM3U8s.get(url)!.parsed = parsed;
addStreamToUI(url, parsed);
if (panel) {
addStreamToUI(url, parsed);
}
}).catch(e => {
log.error(`onM3U8Detected: pre-parse failed for ${url}: ${e}`);
});
@ -180,19 +188,37 @@ if (typeof window === 'undefined') {
monitorDOM(onM3U8Detected);
if (isTopFrame()) {
panel = createPanel();
window.addEventListener('message', (e: MessageEvent) => {
if (e.data && typeof e.data === 'object' && e.data.__m3u8dl === true && e.data.url) {
const referer = typeof e.data.referer === 'string' ? e.data.referer : '';
onM3U8Detected(e.data.url as string, referer ? { Referer: referer } : undefined);
}
});
const cancelBtn = panel.querySelector('.m3u8-dl-cancel-btn') as HTMLButtonElement;
if (cancelBtn) {
cancelBtn.addEventListener('click', () => {
downloadAborted = true;
log.info('Download cancelled by user');
});
}
GM_registerMenuCommand('M3U8 Downloader', () => {
if (panel) {
const isVisible = panel.style.display !== 'none';
panel.style.display = isVisible ? 'none' : 'block';
const hideBtn = panel.querySelector('#m3u8-dl-hide') as HTMLButtonElement;
if (hideBtn) hideBtn.textContent = isVisible ? 'Show' : 'Hide';
return;
}
(window as any)._m3u8Detected = onM3U8Detected;
panel = createPanel();
const cancelBtn = panel.querySelector('.m3u8-dl-cancel-btn') as HTMLButtonElement;
if (cancelBtn) {
cancelBtn.addEventListener('click', () => {
downloadAborted = true;
log.info('Download cancelled by user');
});
}
for (const entry of detectedM3U8s.values()) {
addStreamToUI(entry.url, entry.parsed);
}
});
const target = (typeof unsafeWindow !== 'undefined' ? unsafeWindow : window) as any;
const iframeObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {