From df8843aefbbb753a9a210e34d242f31fdeb9eed4 Mon Sep 17 00:00:00 2001 From: kuekhaoyang Date: Sun, 16 Nov 2025 21:29:31 +0800 Subject: [PATCH] Enhance video URL accessibility checks to validate content type and ensure it contains video data --- lib/utils/source-checker.ts | 23 +++++++++++++++++------ lib/utils/url-validator.ts | 20 ++++++++++++++++---- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/lib/utils/source-checker.ts b/lib/utils/source-checker.ts index 3f5fd5a..7b3865f 100644 --- a/lib/utils/source-checker.ts +++ b/lib/utils/source-checker.ts @@ -18,7 +18,7 @@ export interface SourceCheckResult { } /** - * Check if a single video URL is accessible + * Check if a single video URL is accessible and actually contains video content */ async function checkVideoUrl(url: string, retries = MAX_RETRIES): Promise { if (!isValidUrlFormat(url)) { @@ -30,21 +30,32 @@ async function checkVideoUrl(url: string, retries = MAX_RETRIES): Promise controller.abort(), CHECK_TIMEOUT); + // Use GET with Range header to check if it's actually a video const response = await fetch(url, { - method: 'HEAD', + method: 'GET', signal: controller.signal, headers: { 'User-Agent': 'Mozilla/5.0', 'Referer': new URL(url).origin, + 'Range': 'bytes=0-1024', // Only fetch first 1KB to check }, }); clearTimeout(timeoutId); - // Consider 200, 206, and even 403 as "available" - // (some sources block HEAD but work with actual playback) - if (response.ok || response.status === 206 || response.status === 403) { - return true; + // Only accept successful responses (200 OK or 206 Partial Content) + // Reject 403 Forbidden as it means we can't actually access the video + if (response.ok || response.status === 206) { + // Check content-type to ensure it's actually a video + const contentType = response.headers.get('content-type'); + if (contentType && ( + contentType.includes('video') || + contentType.includes('mpegurl') || + contentType.includes('m3u8') || + contentType.includes('octet-stream') + )) { + return true; + } } } catch (error) { // If last attempt, return false diff --git a/lib/utils/url-validator.ts b/lib/utils/url-validator.ts index 3d4bf95..79e464a 100644 --- a/lib/utils/url-validator.ts +++ b/lib/utils/url-validator.ts @@ -14,7 +14,7 @@ export interface ValidationResult { } /** - * Check if a URL is accessible with HEAD request + * Check if a URL is accessible and contains video content */ async function checkUrlAccessibility(url: string): Promise { const startTime = Date.now(); @@ -23,22 +23,34 @@ async function checkUrlAccessibility(url: string): Promise { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), VALIDATION_TIMEOUT); + // Use GET with Range header to actually check video content const response = await fetch(url, { - method: 'HEAD', + method: 'GET', signal: controller.signal, headers: { 'User-Agent': 'Mozilla/5.0', 'Referer': new URL(url).origin, + 'Range': 'bytes=0-1024', // Only fetch first 1KB }, }); clearTimeout(timeoutId); + // Check if response is successful and contains video content + const isSuccess = response.ok || response.status === 206; + const contentType = response.headers.get('content-type'); + const isVideoContent = contentType && ( + contentType.includes('video') || + contentType.includes('mpegurl') || + contentType.includes('m3u8') || + contentType.includes('octet-stream') + ); + return { url, - isValid: response.ok || response.status === 403, // Some sources block HEAD but work with GET + isValid: isSuccess && !!isVideoContent, responseTime: Date.now() - startTime, - error: !response.ok && response.status !== 403 ? `HTTP ${response.status}` : undefined, + error: !isSuccess ? `HTTP ${response.status}` : (!isVideoContent ? 'Not video content' : undefined), }; } catch (error) { return {