Compare commits

...

5 commits

Author SHA1 Message Date
710ce264f8 update again
All checks were successful
Build / build (push) Successful in 22s
2026-06-09 05:35:54 +09:00
ffd23120fa change some filename rule
All checks were successful
Build / build (push) Successful in 23s
2026-06-09 05:32:17 +09:00
c11708281e ensure absolute path
All checks were successful
Build / build (push) Successful in 22s
2026-06-07 02:27:20 +09:00
a8f025e1b5 key cache
All checks were successful
Build / build (push) Successful in 23s
2026-06-07 02:22:44 +09:00
b56be38431 anti-devtools-detector
All checks were successful
Build / build (push) Successful in 25s
Co-authored-by: Copilot <copilot@github.com>
2026-06-07 01:59:52 +09:00
6 changed files with 107 additions and 59 deletions

View file

@ -1,8 +1,8 @@
// ==UserScript==
// @name M3U8 HLS Downloader
// @namespace http://tampermonkey.net/
// @version 1.0.3
// @description 자동 탐지 및 다운로드: HLS(m3u8) 스트림의 세그먼트를 병합하여 단일 파일로 저장
// @version 1.0.8
// @description 자동 탐지 및 다운로드: HLS(m3u8) 스트림의 세그먼트를 병합하여 단일 파일로 저장. 활성화된 동안 모든 트래픽을 모니터링하고 접근하기 때문에, 사용하지 않을때는 비활성화 필요.
// @author kyush
// @match *://*/*
// @grant GM_xmlhttpRequest

View file

@ -1,6 +1,6 @@
{
"name": "m3u8-monkey-script",
"version": "1.0.3",
"version": "1.0.8",
"description": "",
"main": "index.js",
"scripts": {

View file

@ -1,5 +1,5 @@
import { log } from './logger';
import { normalizeReferer } from './utils/uri';
import { normalizeReferer, resolveUri } from './utils/uri';
import { interceptXHR } from './detection/xhr-intercept';
import { interceptFetch } from './detection/fetch-intercept';
import { monitorDOM } from './detection/dom-monitor';
@ -252,13 +252,16 @@ async function downloadVttStream(url: string, entry: { url: string; type: 'vtt';
}
function onStreamDetected(url: string, headers?: Record<string, string>): void {
if (detectedStreams.has(url)) return;
const base = headers?.Referer || location.href;
const resolvedUrl = resolveUri(base, url);
const type = isVttUrl(url) ? 'vtt' : 'm3u8';
log.info(`onStreamDetected: ${url} type=${type} headers=${JSON.stringify(headers)}`);
detectedStreams.set(url, { url, type, headers });
if (detectedStreams.has(resolvedUrl)) return;
notifyTopFrame(url, headers);
const type = isVttUrl(resolvedUrl) ? 'vtt' : 'm3u8';
log.info(`onStreamDetected: ${resolvedUrl} type=${type} headers=${JSON.stringify(headers)}`);
detectedStreams.set(resolvedUrl, { url: resolvedUrl, type, headers });
notifyTopFrame(resolvedUrl, headers);
if (isTopFrame() && !panel && type === 'm3u8') {
panel = createPanel();
@ -275,62 +278,77 @@ function onStreamDetected(url: string, headers?: Record<string, string>): void {
}
if (isTopFrame() && panel) {
addStreamToUI(url);
addStreamToUI(resolvedUrl);
}
if (isTopFrame() && type === 'm3u8') {
parseM3U8(url, headers?.Referer, headers?.Origin).then(parsed => {
detectedStreams.get(url)!.parsed = parsed;
if (isTopFrame() && type === 'm3u8' && !detectedStreams.get(resolvedUrl)?.parsed) {
parseM3U8(resolvedUrl, headers?.Referer, headers?.Origin, false).then(parsed => {
detectedStreams.get(resolvedUrl)!.parsed = parsed;
if (panel) {
addStreamToUI(url, parsed);
addStreamToUI(resolvedUrl, parsed);
}
}).catch(e => {
log.error(`onStreamDetected: pre-parse failed for ${url}: ${e}`);
log.error(`onStreamDetected: pre-parse failed for ${resolvedUrl}: ${e}`);
});
} else if (!isTopFrame() && type === 'm3u8') {
log.info(`onStreamDetected: iframe detected ${url}, skipping parse (top frame will handle)`);
log.info(`onStreamDetected: iframe detected ${resolvedUrl}, skipping parse (top frame will handle)`);
}
}
if (typeof window === 'undefined') {
log.error('Script not running in a browser context');
} else {
// Anti-devtool: debugger statement 차단
// Anti-devtool: devtools-detector 우회
// Source: https://greasyfork.org/en/scripts/456011-%E5%8F%8D-devtools-detector-%E5%8F%8D%E8%B0%83%E8%AF%95/code
// Targets: https://github.com/AEPKILL/devtools-detector
// Mechanism: Hooks Object.prototype.launch setter to intercept devtoolsDetector.launch.
// When the detector sets its launch method (containing '_detectLoopDelay'), it is
// replaced with a no-op that deletes the prototype-level launch and restores the
// original, preventing the debugger loop from executing.
(function() {
const originalEval = window.eval;
window.eval = function(code: any) {
if (typeof code === 'string' && code.includes('debugger')) {
code = code.replace(/debugger/g, '');
}
return originalEval.call(window, code);
};
const noop = function() {};
const originalFunction = Function;
(window as any).Function = function(...args: any[]) {
if (args.length > 0) {
args[args.length - 1] = args[args.length - 1].replace(/debugger/g, '');
}
return originalFunction.apply(this, args);
};
(window as any).Function.prototype = originalFunction.prototype;
function disableLaunch(alt: () => void) {
log.info('anti-devtool: disabling devtoolsDetector.launch');
const originalLocation = Object.getOwnPropertyDescriptor(window, 'location');
let locationBlocked = false;
const map = new Map<any, any>();
window.addEventListener('beforeunload', function(e) {
if (locationBlocked) {
e.preventDefault();
const func = function() {
delete (Object as any).prototype.launch;
Object.getPrototypeOf(this).launch = alt;
this.launch();
};
Object.defineProperty(Object.prototype, 'launch', {
set(f: any) {
const checked = funcChecked(f);
map.set(this, checked ? func : f);
return true;
},
get() {
return getVal(this);
function getVal(obj: any): any {
return obj ? (map.has(obj) ? map.get(obj) : getVal(Object.getPrototypeOf(obj))) : undefined;
}
},
configurable: true,
enumerable: false,
});
function funcChecked(f: any): boolean {
if (f && typeof f.toString === 'function') {
const str = f.toString();
if (typeof str === 'string' && str.includes('_detectLoopDelay')) {
return true;
}
}
return false;
}
});
}
Object.defineProperty(window, 'location', {
get: function() { return originalLocation!.get!.call(window); },
set: function(val) {
locationBlocked = true;
},
configurable: true,
});
disableLaunch(noop);
log.info('anti-devtool: devtools-detector launch hook installed');
})();
log.info(`Script initialized in ${isTopFrame() ? 'top frame' : `iframe (${location.origin})`}`);

View file

@ -1,12 +1,24 @@
export const log = (() => {
let targetConsole: Console;
let prefix: string;
try {
const uW = (typeof unsafeWindow !== 'undefined' ? unsafeWindow : null) as any;
targetConsole = uW && uW.console ? uW.console : console;
} catch {
targetConsole = console;
// Collect all reachable consoles: page, unsafeWindow, top, parent, native
const consoles: Console[] = [];
const seen = new Set<any>();
function addConsole(c: any) {
if (c && c.log && consoles.length < 5 && !seen.has(c)) {
seen.add(c);
consoles.push(c);
}
}
try { addConsole(console); } catch { /* ignore */ }
try { addConsole((typeof unsafeWindow !== 'undefined' ? unsafeWindow : null)?.console); } catch { /* ignore */ }
try { addConsole(window.top?.console); } catch { /* ignore */ }
try { addConsole(window.parent?.console); } catch { /* ignore */ }
if (consoles.length === 0) {
try { addConsole(console); } catch { /* ignore */ }
}
try {
@ -16,9 +28,24 @@ export const log = (() => {
prefix = '[M3U8-DL]';
}
function formatMsg(level: string, ...args: unknown[]): string {
return `${prefix} [${level}] ${args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ')}`;
}
function teeLog(level: string, method: string, ...args: unknown[]) {
const msg = formatMsg(level, ...args);
for (const c of consoles) {
try {
if (c[method]) {
(c[method] as any)(msg);
}
} catch { /* console may be broken */ }
}
}
return {
info: (...args: unknown[]) => targetConsole.log(`${prefix} [INFO]`, ...args),
warn: (...args: unknown[]) => targetConsole.warn(`${prefix} [WARN]`, ...args),
error: (...args: unknown[]) => targetConsole.error(`${prefix} [ERROR]`, ...args),
info: (...args: unknown[]) => teeLog('INFO', 'log', ...args),
warn: (...args: unknown[]) => teeLog('WARN', 'warn', ...args),
error: (...args: unknown[]) => teeLog('ERROR', 'error', ...args),
};
})();

View file

@ -102,7 +102,7 @@ export async function parseM3U8(
}
const segments = manifest.segments || [];
const keyCache = new Map<string, Uint8Array>();
const keyCache = new Map<string, Uint8Array | undefined>();
const resolvedSegments: SegmentInfo[] = [];
const seen = new Set<string>();
@ -122,13 +122,16 @@ export async function parseM3U8(
let keyBytes: Uint8Array | null = null;
if (fetchKeys) {
keyBytes = keyCache.get(keyUri);
if (!keyBytes) {
const cached = keyCache.get(keyUri);
if (cached !== undefined) {
keyBytes = cached;
} else {
try {
keyBytes = await fetchKey(keyUri, referer, origin);
keyCache.set(keyUri, keyBytes);
} catch (e) {
log.error(`parseM3U8: failed to fetch key ${keyUri}: ${e}`);
log.warn(`parseM3U8: failed to fetch key ${keyUri}: ${e}`);
keyCache.set(keyUri, undefined);
}
}
}

View file

@ -28,7 +28,7 @@ export function guessFilename(url: string, mime?: string): string {
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/);
const match = title.match(/^(.+?)\s+😜\s+(\d+)\s+-\s+Ani\S+\s+-\s+Linkkf/);
if (match) {
return { name: match[1].trim(), volume: match[2].trim() };
}