add vtt support
This commit is contained in:
parent
2f9fa8f76e
commit
22b8ee2fa0
5 changed files with 169 additions and 40 deletions
|
|
@ -6,6 +6,15 @@ function isM3U8Url(url: string): boolean {
|
||||||
lower.includes('application/x-mpegurl') || lower.includes('application/vnd.apple.mpegurl');
|
lower.includes('application/x-mpegurl') || lower.includes('application/vnd.apple.mpegurl');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isVttUrl(url: string): boolean {
|
||||||
|
const lower = url.toLowerCase();
|
||||||
|
return lower.includes('.vtt');
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTargetUrl(url: string): boolean {
|
||||||
|
return isM3U8Url(url) || isVttUrl(url);
|
||||||
|
}
|
||||||
|
|
||||||
export function monitorDOM(onDetected: (url: string, headers?: Record<string, string>) => void): void {
|
export function monitorDOM(onDetected: (url: string, headers?: Record<string, string>) => void): void {
|
||||||
const target = (typeof unsafeWindow !== 'undefined' ? unsafeWindow : window) as any;
|
const target = (typeof unsafeWindow !== 'undefined' ? unsafeWindow : window) as any;
|
||||||
const doc = target.document || document;
|
const doc = target.document || document;
|
||||||
|
|
@ -28,8 +37,8 @@ export function monitorDOM(onDetected: (url: string, headers?: Record<string, st
|
||||||
|
|
||||||
const checkSource = (el: HTMLMediaElement) => {
|
const checkSource = (el: HTMLMediaElement) => {
|
||||||
const src = el.currentSrc || el.src || '';
|
const src = el.currentSrc || el.src || '';
|
||||||
if (src && isM3U8Url(src)) {
|
if (src && isTargetUrl(src)) {
|
||||||
log.info(`DOM monitor: found m3u8 in <${el.tagName.toLowerCase()}> src: ${src}`);
|
log.info(`DOM monitor: found in <${el.tagName.toLowerCase()}> src: ${src}`);
|
||||||
const referer = getReferer(src);
|
const referer = getReferer(src);
|
||||||
onDetected(src, referer ? { Referer: referer } : undefined);
|
onDetected(src, referer ? { Referer: referer } : undefined);
|
||||||
}
|
}
|
||||||
|
|
@ -38,8 +47,8 @@ export function monitorDOM(onDetected: (url: string, headers?: Record<string, st
|
||||||
for (let i = 0; i < el.children.length; i++) {
|
for (let i = 0; i < el.children.length; i++) {
|
||||||
const source = el.children[i] as HTMLSourceElement;
|
const source = el.children[i] as HTMLSourceElement;
|
||||||
const sourceSrc = source.src || '';
|
const sourceSrc = source.src || '';
|
||||||
if (sourceSrc && isM3U8Url(sourceSrc)) {
|
if (sourceSrc && isTargetUrl(sourceSrc)) {
|
||||||
log.info(`DOM monitor: found m3u8 in <source> src: ${sourceSrc}`);
|
log.info(`DOM monitor: found in <source> src: ${sourceSrc}`);
|
||||||
const referer = getReferer(sourceSrc);
|
const referer = getReferer(sourceSrc);
|
||||||
onDetected(sourceSrc, referer ? { Referer: referer } : undefined);
|
onDetected(sourceSrc, referer ? { Referer: referer } : undefined);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,18 @@
|
||||||
import { log } from '../logger';
|
import { log } from '../logger';
|
||||||
|
|
||||||
const M3U8_PATTERNS = ['.m3u8', '.m3u', 'application/x-mpegurl', 'application/vnd.apple.mpegurl'];
|
const M3U8_PATTERNS = ['.m3u8', '.m3u', 'application/x-mpegurl', 'application/vnd.apple.mpegurl'];
|
||||||
|
const VTT_PATTERNS = ['.vtt'];
|
||||||
|
|
||||||
function isM3U8Url(url: string): boolean {
|
function isM3U8Url(url: string): boolean {
|
||||||
const lower = url.toLowerCase();
|
const lower = url.toLowerCase();
|
||||||
return M3U8_PATTERNS.some(p => lower.includes(p));
|
return M3U8_PATTERNS.some(p => lower.includes(p));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isVttUrl(url: string): boolean {
|
||||||
|
const lower = url.toLowerCase();
|
||||||
|
return VTT_PATTERNS.some(p => lower.includes(p));
|
||||||
|
}
|
||||||
|
|
||||||
export function interceptFetch(onDetected: (url: string, headers?: Record<string, string>) => void): void {
|
export function interceptFetch(onDetected: (url: string, headers?: Record<string, string>) => void): void {
|
||||||
const target = (typeof unsafeWindow !== 'undefined' ? unsafeWindow : window) as any;
|
const target = (typeof unsafeWindow !== 'undefined' ? unsafeWindow : window) as any;
|
||||||
const origFetch = target.fetch;
|
const origFetch = target.fetch;
|
||||||
|
|
@ -14,19 +20,21 @@ export function interceptFetch(onDetected: (url: string, headers?: Record<string
|
||||||
|
|
||||||
target.fetch = function(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
|
target.fetch = function(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
|
||||||
const url = typeof input === 'string' ? input : (input as Request).url;
|
const url = typeof input === 'string' ? input : (input as Request).url;
|
||||||
|
const isTarget = isM3U8Url(url) || isVttUrl(url);
|
||||||
|
|
||||||
if (isM3U8Url(url)) {
|
if (isTarget) {
|
||||||
log.info(`Fetch request: ${url}`);
|
log.info(`Fetch request: ${url}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return origFetch.apply(this, arguments as [RequestInfo | URL, RequestInit?]).then((response: Response) => {
|
return origFetch.apply(this, arguments as [RequestInfo | URL, RequestInit?]).then((response: Response) => {
|
||||||
if (isM3U8Url(url)) {
|
if (isTarget) {
|
||||||
const contentType = response.headers.get('Content-Type') || '';
|
const contentType = response.headers.get('Content-Type') || '';
|
||||||
log.info(`Fetch response: ${url} status=${response.status} contentType=${contentType}`);
|
log.info(`Fetch response: ${url} status=${response.status} contentType=${contentType}`);
|
||||||
|
|
||||||
if (response.status >= 200 && response.status < 300) {
|
if (response.status >= 200 && response.status < 300) {
|
||||||
const isM3U8Response = isM3U8Url(url) || contentType.toLowerCase().includes('mpegurl');
|
const isM3U8Response = isM3U8Url(url) || contentType.toLowerCase().includes('mpegurl');
|
||||||
if (isM3U8Response) {
|
const isVttResponse = isVttUrl(url) || contentType.toLowerCase().includes('vtt') || contentType.toLowerCase().includes('text/vtt');
|
||||||
|
if (isM3U8Response || isVttResponse) {
|
||||||
try {
|
try {
|
||||||
let referer = location.href;
|
let referer = location.href;
|
||||||
const urlObj = new URL(url);
|
const urlObj = new URL(url);
|
||||||
|
|
@ -40,7 +48,7 @@ export function interceptFetch(onDetected: (url: string, headers?: Record<string
|
||||||
}
|
}
|
||||||
return response;
|
return response;
|
||||||
}).catch((error: unknown) => {
|
}).catch((error: unknown) => {
|
||||||
if (isM3U8Url(url)) {
|
if (isTarget) {
|
||||||
log.error(`Fetch error: ${url} - ${error}`);
|
log.error(`Fetch error: ${url} - ${error}`);
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,18 @@
|
||||||
import { log } from '../logger';
|
import { log } from '../logger';
|
||||||
|
|
||||||
const M3U8_PATTERNS = ['.m3u8', '.m3u', 'application/x-mpegurl', 'application/vnd.apple.mpegurl'];
|
const M3U8_PATTERNS = ['.m3u8', '.m3u', 'application/x-mpegurl', 'application/vnd.apple.mpegurl'];
|
||||||
|
const VTT_PATTERNS = ['.vtt'];
|
||||||
|
|
||||||
function isM3U8Url(url: string): boolean {
|
function isM3U8Url(url: string): boolean {
|
||||||
const lower = url.toLowerCase();
|
const lower = url.toLowerCase();
|
||||||
return M3U8_PATTERNS.some(p => lower.includes(p));
|
return M3U8_PATTERNS.some(p => lower.includes(p));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isVttUrl(url: string): boolean {
|
||||||
|
const lower = url.toLowerCase();
|
||||||
|
return VTT_PATTERNS.some(p => lower.includes(p));
|
||||||
|
}
|
||||||
|
|
||||||
export function interceptXHR(onDetected: (url: string, headers?: Record<string, string>) => void): void {
|
export function interceptXHR(onDetected: (url: string, headers?: Record<string, string>) => void): void {
|
||||||
const target = (typeof unsafeWindow !== 'undefined' ? unsafeWindow : window) as any;
|
const target = (typeof unsafeWindow !== 'undefined' ? unsafeWindow : window) as any;
|
||||||
const XHR = target.XMLHttpRequest;
|
const XHR = target.XMLHttpRequest;
|
||||||
|
|
@ -19,7 +25,8 @@ export function interceptXHR(onDetected: (url: string, headers?: Record<string,
|
||||||
XHR.prototype.open = function(method: string, url: string, ...rest: unknown[]): void {
|
XHR.prototype.open = function(method: string, url: string, ...rest: unknown[]): void {
|
||||||
const requestUrl = String(url);
|
const requestUrl = String(url);
|
||||||
const headers: Record<string, string> = {};
|
const headers: Record<string, string> = {};
|
||||||
if (isM3U8Url(requestUrl)) {
|
const isTarget = isM3U8Url(requestUrl) || isVttUrl(requestUrl);
|
||||||
|
if (isTarget) {
|
||||||
log.info(`XHR open: ${method} ${requestUrl}`);
|
log.info(`XHR open: ${method} ${requestUrl}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -32,7 +39,7 @@ export function interceptXHR(onDetected: (url: string, headers?: Record<string,
|
||||||
|
|
||||||
const origSendFn = origSend.bind(this);
|
const origSendFn = origSend.bind(this);
|
||||||
this.send = function(body?: unknown): void {
|
this.send = function(body?: unknown): void {
|
||||||
if (isM3U8Url(requestUrl)) {
|
if (isTarget) {
|
||||||
this.addEventListener('load', function() {
|
this.addEventListener('load', function() {
|
||||||
try {
|
try {
|
||||||
const status = (this as any).status;
|
const status = (this as any).status;
|
||||||
|
|
@ -41,7 +48,8 @@ export function interceptXHR(onDetected: (url: string, headers?: Record<string,
|
||||||
log.info(`XHR load: ${requestUrl} status=${status} contentType=${contentType} bodyLen=${responseText.length}`);
|
log.info(`XHR load: ${requestUrl} status=${status} contentType=${contentType} bodyLen=${responseText.length}`);
|
||||||
if (status >= 200 && status < 300) {
|
if (status >= 200 && status < 300) {
|
||||||
const isM3U8Response = isM3U8Url(requestUrl) || contentType.toLowerCase().includes('mpegurl') || responseText.startsWith('#EXTM3U');
|
const isM3U8Response = isM3U8Url(requestUrl) || contentType.toLowerCase().includes('mpegurl') || responseText.startsWith('#EXTM3U');
|
||||||
if (isM3U8Response) {
|
const isVttResponse = isVttUrl(requestUrl) || contentType.toLowerCase().includes('vtt') || contentType.toLowerCase().includes('text/vtt');
|
||||||
|
if (isM3U8Response || isVttResponse) {
|
||||||
const capturedHeaders: Record<string, string> = {};
|
const capturedHeaders: Record<string, string> = {};
|
||||||
for (const [k, v] of Object.entries(headers)) {
|
for (const [k, v] of Object.entries(headers)) {
|
||||||
if (k.toLowerCase() !== 'referer') {
|
if (k.toLowerCase() !== 'referer') {
|
||||||
|
|
|
||||||
132
src/index.ts
132
src/index.ts
|
|
@ -10,8 +10,9 @@ import { createPanel } from './ui/panel';
|
||||||
import { showStatus, hideProgress, resetProgress } from './ui/progress';
|
import { showStatus, hideProgress, resetProgress } from './ui/progress';
|
||||||
import { showPlaylistSelector } from './ui/selector';
|
import { showPlaylistSelector } from './ui/selector';
|
||||||
import { DownloadProgress } from './downloader/segment-fetch';
|
import { DownloadProgress } from './downloader/segment-fetch';
|
||||||
|
import { guessFilename } from './utils/filename';
|
||||||
|
|
||||||
const detectedM3U8s = new Map<string, { url: string; headers?: Record<string, string>; parsed?: ParsedPlaylist }>();
|
const detectedStreams = new Map<string, { url: string; type: 'm3u8' | 'vtt'; headers?: Record<string, string>; parsed?: ParsedPlaylist }>();
|
||||||
let panel: HTMLDivElement | null = null;
|
let panel: HTMLDivElement | null = null;
|
||||||
let isDownloading = false;
|
let isDownloading = false;
|
||||||
let downloadAborted = false;
|
let downloadAborted = false;
|
||||||
|
|
@ -24,6 +25,10 @@ function isTopFrame(): boolean {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isVttUrl(url: string): boolean {
|
||||||
|
return url.toLowerCase().includes('.vtt');
|
||||||
|
}
|
||||||
|
|
||||||
function notifyTopFrame(url: string, headers?: Record<string, string>): void {
|
function notifyTopFrame(url: string, headers?: Record<string, string>): void {
|
||||||
if (!isTopFrame()) {
|
if (!isTopFrame()) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -58,16 +63,28 @@ function addStreamToUI(url: string, parsed?: ParsedPlaylist): void {
|
||||||
item.className = 'm3u8-dl-stream-item';
|
item.className = 'm3u8-dl-stream-item';
|
||||||
item.setAttribute('data-url', url);
|
item.setAttribute('data-url', url);
|
||||||
|
|
||||||
const segmentCount = parsed && !parsed.isMaster ? parsed.segments.length : '--';
|
const entry = detectedStreams.get(url);
|
||||||
const encrypted = parsed && !parsed.isMaster ? parsed.segments.some(s => s.key !== null) : 'unknown';
|
const isVtt = entry?.type === 'vtt';
|
||||||
|
|
||||||
item.innerHTML = `
|
if (isVtt) {
|
||||||
<div class="m3u8-dl-stream-url">${url}</div>
|
item.innerHTML = `
|
||||||
<div class="m3u8-dl-stream-meta">Segments: ${segmentCount} | Encrypted: ${encrypted ? 'AES-128' : 'No'}</div>
|
<div class="m3u8-dl-stream-url">${url}</div>
|
||||||
<div class="m3u8-dl-stream-actions">
|
<div class="m3u8-dl-stream-meta">Subtitle (VTT)</div>
|
||||||
<button class="m3u8-dl-download-btn" data-url="${url}">Download</button>
|
<div class="m3u8-dl-stream-actions">
|
||||||
</div>
|
<button class="m3u8-dl-download-btn" data-url="${url}">Download</button>
|
||||||
`;
|
</div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
const segmentCount = parsed && !parsed.isMaster ? parsed.segments.length : '--';
|
||||||
|
const encrypted = parsed && !parsed.isMaster ? parsed.segments.some(s => s.key !== null) : 'unknown';
|
||||||
|
item.innerHTML = `
|
||||||
|
<div class="m3u8-dl-stream-url">${url}</div>
|
||||||
|
<div class="m3u8-dl-stream-meta">Segments: ${segmentCount} | Encrypted: ${encrypted ? 'AES-128' : 'No'}</div>
|
||||||
|
<div class="m3u8-dl-stream-actions">
|
||||||
|
<button class="m3u8-dl-download-btn" data-url="${url}">Download</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
streamsContainer.appendChild(item);
|
streamsContainer.appendChild(item);
|
||||||
|
|
||||||
|
|
@ -78,6 +95,26 @@ function addStreamToUI(url: string, parsed?: ParsedPlaylist): void {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function downloadVtt(blob: Blob, url: string): void {
|
||||||
|
const baseName = guessFilename(url, 'text/vtt');
|
||||||
|
const filename = baseName.replace(/\.(mp4|ts|m4a)$/, '.vtt');
|
||||||
|
log.info(`downloadVtt: ${filename} (${blob.size} bytes)`);
|
||||||
|
const urlObj = URL.createObjectURL(blob);
|
||||||
|
const target = (typeof unsafeWindow !== 'undefined' ? unsafeWindow : window) as any;
|
||||||
|
const doc = target.document || document;
|
||||||
|
const a = doc.createElement('a');
|
||||||
|
a.href = urlObj;
|
||||||
|
a.download = filename;
|
||||||
|
doc.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
doc.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(urlObj);
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
async function startDownload(url: string): Promise<void> {
|
async function startDownload(url: string): Promise<void> {
|
||||||
if (isDownloading) {
|
if (isDownloading) {
|
||||||
log.warn('startDownload: already downloading');
|
log.warn('startDownload: already downloading');
|
||||||
|
|
@ -87,8 +124,14 @@ async function startDownload(url: string): Promise<void> {
|
||||||
isDownloading = true;
|
isDownloading = true;
|
||||||
downloadAborted = false;
|
downloadAborted = false;
|
||||||
|
|
||||||
const entry = detectedM3U8s.get(url);
|
const entry = detectedStreams.get(url);
|
||||||
log.info(`startDownload: ${url} entryHeaders=${JSON.stringify(entry?.headers)}`);
|
log.info(`startDownload: ${url} type=${entry?.type} entryHeaders=${JSON.stringify(entry?.headers)}`);
|
||||||
|
|
||||||
|
if (entry?.type === 'vtt') {
|
||||||
|
await downloadVttStream(url, entry);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let parsed = entry?.parsed;
|
let parsed = entry?.parsed;
|
||||||
|
|
||||||
if (!parsed) {
|
if (!parsed) {
|
||||||
|
|
@ -97,6 +140,7 @@ async function startDownload(url: string): Promise<void> {
|
||||||
if (entry) entry.parsed = parsed;
|
if (entry) entry.parsed = parsed;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.error(`startDownload: parse failed for ${url}: ${e}`);
|
log.error(`startDownload: parse failed for ${url}: ${e}`);
|
||||||
|
isDownloading = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -160,11 +204,47 @@ async function startDownload(url: string): Promise<void> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onM3U8Detected(url: string, headers?: Record<string, string>): void {
|
async function downloadVttStream(url: string, entry: { url: string; type: 'vtt'; headers?: Record<string, string> }): Promise<void> {
|
||||||
if (detectedM3U8s.has(url)) return;
|
log.info(`downloadVttStream: ${url}`);
|
||||||
|
isDownloading = true;
|
||||||
|
try {
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
(GM_xmlhttpRequest as any)({
|
||||||
|
method: 'GET',
|
||||||
|
url: url,
|
||||||
|
responseType: 'text',
|
||||||
|
headers: {
|
||||||
|
'Referer': entry.headers?.Referer || document.location.href,
|
||||||
|
'Origin': entry.headers?.Origin || new URL(document.location.href).origin,
|
||||||
|
},
|
||||||
|
onload: (response: any) => {
|
||||||
|
if (response.status >= 200 && response.status < 300) {
|
||||||
|
const text = response.responseText || response.response || '';
|
||||||
|
const blob = new Blob([text], { type: 'text/vtt' });
|
||||||
|
downloadVtt(blob, url);
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
log.error(`downloadVttStream: HTTP ${response.status} for ${url}`);
|
||||||
|
reject(new Error(`VTT download failed: HTTP ${response.status}`));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onerror: (error: any) => {
|
||||||
|
log.error(`downloadVttStream: error for ${url}: ${error.error}`);
|
||||||
|
reject(new Error(`VTT download error: ${error.error}`));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
isDownloading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
log.info(`onM3U8Detected: ${url} headers=${JSON.stringify(headers)}`);
|
function onStreamDetected(url: string, headers?: Record<string, string>): void {
|
||||||
detectedM3U8s.set(url, { url, headers });
|
if (detectedStreams.has(url)) return;
|
||||||
|
|
||||||
|
const type = isVttUrl(url) ? 'vtt' : 'm3u8';
|
||||||
|
log.info(`onStreamDetected: ${url} type=${type} headers=${JSON.stringify(headers)}`);
|
||||||
|
detectedStreams.set(url, { url, type, headers });
|
||||||
|
|
||||||
notifyTopFrame(url, headers);
|
notifyTopFrame(url, headers);
|
||||||
|
|
||||||
|
|
@ -172,14 +252,14 @@ function onM3U8Detected(url: string, headers?: Record<string, string>): void {
|
||||||
addStreamToUI(url);
|
addStreamToUI(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isTopFrame()) {
|
if (isTopFrame() && type === 'm3u8') {
|
||||||
parseM3U8(url, headers?.Referer, headers?.Origin).then(parsed => {
|
parseM3U8(url, headers?.Referer, headers?.Origin).then(parsed => {
|
||||||
detectedM3U8s.get(url)!.parsed = parsed;
|
detectedStreams.get(url)!.parsed = parsed;
|
||||||
if (panel) {
|
if (panel) {
|
||||||
addStreamToUI(url, parsed);
|
addStreamToUI(url, parsed);
|
||||||
}
|
}
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
log.error(`onM3U8Detected: pre-parse failed for ${url}: ${e}`);
|
log.error(`onStreamDetected: pre-parse failed for ${url}: ${e}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -189,9 +269,9 @@ if (typeof window === 'undefined') {
|
||||||
} else {
|
} else {
|
||||||
log.info(`Script initialized in ${isTopFrame() ? 'top frame' : `iframe (${location.origin})`}`);
|
log.info(`Script initialized in ${isTopFrame() ? 'top frame' : `iframe (${location.origin})`}`);
|
||||||
|
|
||||||
interceptXHR(onM3U8Detected);
|
interceptXHR(onStreamDetected);
|
||||||
interceptFetch(onM3U8Detected);
|
interceptFetch(onStreamDetected);
|
||||||
monitorDOM(onM3U8Detected);
|
monitorDOM(onStreamDetected);
|
||||||
|
|
||||||
if (isTopFrame()) {
|
if (isTopFrame()) {
|
||||||
window.addEventListener('message', (e: MessageEvent) => {
|
window.addEventListener('message', (e: MessageEvent) => {
|
||||||
|
|
@ -201,7 +281,7 @@ if (typeof window === 'undefined') {
|
||||||
const headers: Record<string, string> = {};
|
const headers: Record<string, string> = {};
|
||||||
if (referer) headers['Referer'] = referer;
|
if (referer) headers['Referer'] = referer;
|
||||||
if (origin) headers['Origin'] = origin;
|
if (origin) headers['Origin'] = origin;
|
||||||
onM3U8Detected(e.data.url as string, Object.keys(headers).length ? headers : undefined);
|
onStreamDetected(e.data.url as string, Object.keys(headers).length ? headers : undefined);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -224,7 +304,7 @@ if (typeof window === 'undefined') {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const entry of detectedM3U8s.values()) {
|
for (const entry of detectedStreams.values()) {
|
||||||
addStreamToUI(entry.url, entry.parsed);
|
addStreamToUI(entry.url, entry.parsed);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -239,10 +319,10 @@ if (typeof window === 'undefined') {
|
||||||
if (iframeWin && iframeWin.location.origin === window.location.origin) {
|
if (iframeWin && iframeWin.location.origin === window.location.origin) {
|
||||||
log.info(`iframe observer: injecting hooks into same-origin iframe`);
|
log.info(`iframe observer: injecting hooks into same-origin iframe`);
|
||||||
interceptXHR.call(null, (url: string) => {
|
interceptXHR.call(null, (url: string) => {
|
||||||
onM3U8Detected(url);
|
onStreamDetected(url);
|
||||||
});
|
});
|
||||||
interceptFetch.call(null, (url: string) => {
|
interceptFetch.call(null, (url: string) => {
|
||||||
onM3U8Detected(url);
|
onStreamDetected(url);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,14 @@
|
||||||
export function guessFilename(url: string, mime?: string): string {
|
export function guessFilename(url: string, mime?: string): string {
|
||||||
|
const titleMatch = extractTitleParts();
|
||||||
|
const ext = mime ? mimeToExt(mime) : null;
|
||||||
|
|
||||||
|
if (titleMatch) {
|
||||||
|
if (ext === '.vtt' || mime?.includes('vtt')) {
|
||||||
|
return `${titleMatch.name} ${titleMatch.volume}.vtt`;
|
||||||
|
}
|
||||||
|
return `${titleMatch.name} ${titleMatch.volume}.mp4`;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const urlObj = new URL(url);
|
const urlObj = new URL(url);
|
||||||
let name = urlObj.pathname.replace(/[^\/\\]+$/, '') || '/';
|
let name = urlObj.pathname.replace(/[^\/\\]+$/, '') || '/';
|
||||||
|
|
@ -8,13 +18,24 @@ export function guessFilename(url: string, mime?: string): string {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ext = mime ? mimeToExt(mime) : '.mp4';
|
const fallbackExt = mime ? mimeToExt(mime) : '.mp4';
|
||||||
return name + ext;
|
return name + fallbackExt;
|
||||||
} catch {
|
} catch {
|
||||||
return 'download' + (mime === 'video/mp4' || mime?.includes('mp4') ? '.mp4' : '.ts');
|
return 'download' + (mime === 'video/mp4' || mime?.includes('mp4') ? '.mp4' : '.ts');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractTitleParts(): { name: string; volume: string } | null {
|
||||||
|
try {
|
||||||
|
const title = document.title || '';
|
||||||
|
const match = title.match(/^(.+?)\s+😜\s+(\d+)\s+-\s+Anime\s+-\s+Linkkf/);
|
||||||
|
if (match) {
|
||||||
|
return { name: match[1].trim(), volume: match[2].trim() };
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function mimeToExt(mime: string): string {
|
function mimeToExt(mime: string): string {
|
||||||
switch (mime) {
|
switch (mime) {
|
||||||
case 'video/mp4':
|
case 'video/mp4':
|
||||||
|
|
@ -25,6 +46,9 @@ function mimeToExt(mime: string): string {
|
||||||
return '.ts';
|
return '.ts';
|
||||||
case 'audio/mp4':
|
case 'audio/mp4':
|
||||||
return '.m4a';
|
return '.m4a';
|
||||||
|
case 'text/vtt':
|
||||||
|
case 'text/plain':
|
||||||
|
return '.vtt';
|
||||||
default:
|
default:
|
||||||
return '.ts';
|
return '.ts';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue