mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-18 02:03:43 +08:00
feat: Enhance M3U8 ad detection with proxy URL unwrapping and segment-level analysis, and implement proxy fallback for HLS playlist fetching.
This commit is contained in:
@@ -103,12 +103,30 @@ export function parseBlocks(lines: string[]): Block[] {
|
||||
return blocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unwrap proxied URL to get original URL
|
||||
*/
|
||||
function unwrapProxyUrl(url: string): string {
|
||||
if (url.includes('/api/proxy?url=')) {
|
||||
try {
|
||||
const match = url.match(/[?&]url=([^&]+)/);
|
||||
if (match && match[1]) {
|
||||
return decodeURIComponent(match[1]);
|
||||
}
|
||||
} catch {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract filename from URL (handles both relative and absolute URLs)
|
||||
*/
|
||||
function extractFilename(url: string): string {
|
||||
try {
|
||||
const path = url.includes('://') ? new URL(url).pathname : url;
|
||||
const unwrappedUrl = unwrapProxyUrl(url);
|
||||
const path = unwrappedUrl.includes('://') ? new URL(unwrappedUrl).pathname : unwrappedUrl;
|
||||
const parts = path.split('/');
|
||||
return parts[parts.length - 1] || '';
|
||||
} catch {
|
||||
@@ -143,7 +161,8 @@ function findCommonPrefix(strings: string[]): string {
|
||||
*/
|
||||
function extractPathPrefix(url: string): string {
|
||||
try {
|
||||
const path = url.includes('://') ? new URL(url).pathname : url;
|
||||
const unwrappedUrl = unwrapProxyUrl(url);
|
||||
const path = unwrappedUrl.includes('://') ? new URL(unwrappedUrl).pathname : unwrappedUrl;
|
||||
const lastSlash = path.lastIndexOf('/');
|
||||
return lastSlash >= 0 ? path.substring(0, lastSlash + 1) : '';
|
||||
} catch {
|
||||
@@ -234,7 +253,7 @@ export function scoreBlock(block: Block, mainPattern: MainPattern, extraKeywords
|
||||
|
||||
// **KEY FEATURE**: Check path prefix mismatch (e.g., different date/folder/bitrate)
|
||||
// This is the most reliable indicator for ads that come from different CDN paths
|
||||
if (mainPattern.pathPrefix && block.segments.length > 0) {
|
||||
if (mainPattern.pathPrefix !== undefined && block.segments.length > 0) {
|
||||
const pathMismatchCount = block.segments.filter(s => {
|
||||
const segmentPathPrefix = extractPathPrefix(s.url);
|
||||
return segmentPathPrefix !== mainPattern.pathPrefix;
|
||||
|
||||
+33
-3
@@ -25,10 +25,21 @@ export function filterM3u8Ad(content: string, baseUrl: string, mode: AdFilterMod
|
||||
// Use keywords passed from AdKeywordsWrapper (already loaded from env/file)
|
||||
const keywords = customKeywords;
|
||||
|
||||
const basePath = baseUrl.substring(0, baseUrl.lastIndexOf('/') + 1);
|
||||
// Unwrap baseUrl if it's a proxy URL to get correct basePath and origin
|
||||
let effectiveBaseUrl = baseUrl;
|
||||
if (baseUrl.includes('/api/proxy?url=')) {
|
||||
try {
|
||||
const urlMatch = baseUrl.match(/[?&]url=([^&]+)/);
|
||||
if (urlMatch && urlMatch[1]) {
|
||||
effectiveBaseUrl = decodeURIComponent(urlMatch[1]);
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
const basePath = effectiveBaseUrl.substring(0, effectiveBaseUrl.lastIndexOf('/') + 1);
|
||||
let origin = '';
|
||||
try {
|
||||
origin = new URL(baseUrl).origin;
|
||||
origin = new URL(effectiveBaseUrl).origin;
|
||||
} catch (e) { /* ignore */ }
|
||||
|
||||
// 2. Global Scan: Check if any ad keywords exist in the content
|
||||
@@ -42,18 +53,37 @@ export function filterM3u8Ad(content: string, baseUrl: string, mode: AdFilterMod
|
||||
if (!hasCueTag && (mode === 'heuristic' || mode === 'aggressive')) {
|
||||
// No obvious ad signals - run heuristic analysis
|
||||
const blocks = parseBlocks(lines);
|
||||
if (blocks.length > 1) {
|
||||
if (blocks.length > 0) {
|
||||
const mainPattern = learnMainPattern(blocks);
|
||||
for (const block of blocks) {
|
||||
// Pass all keywords (including custom ones) to heuristic scorer
|
||||
const score = scoreBlock(block, mainPattern, keywords);
|
||||
const threshold = mode === 'aggressive' ? 3.0 : 5.0;
|
||||
|
||||
if (shouldFilterBlock(score, threshold)) {
|
||||
// Mark all lines in this block for removal
|
||||
for (const segment of block.segments) {
|
||||
adLineIndices.add(segment.lineIndex);
|
||||
adLineIndices.add(segment.lineIndex - 1); // EXTINF line
|
||||
}
|
||||
} else if (block.segments.length > 0) {
|
||||
// Segment-level detection:
|
||||
// Even if the whole block didn't trigger, check segments individually
|
||||
// if it's a suspicious single-segment "block" (common for ads without discontinuity)
|
||||
for (const segment of block.segments) {
|
||||
const singleSegmentBlock = {
|
||||
segments: [segment],
|
||||
hasCueTag: false,
|
||||
startLineIndex: segment.lineIndex - 1,
|
||||
endLineIndex: segment.lineIndex
|
||||
};
|
||||
const segmentScore = scoreBlock(singleSegmentBlock, mainPattern, keywords);
|
||||
// Higher threshold for individual segments to avoid false positives
|
||||
if (segmentScore >= 4.0) {
|
||||
adLineIndices.add(segment.lineIndex);
|
||||
adLineIndices.add(segment.lineIndex - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user