cross origin communication with postMessage
This commit is contained in:
parent
91a367f7ce
commit
10d5c46ec3
3 changed files with 67 additions and 22 deletions
13
metadata.user.js
Normal file
13
metadata.user.js
Normal 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==
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
68
src/index.ts
68
src/index.ts
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue