Enhance video URL accessibility checks to validate content type and ensure it contains video data

This commit is contained in:
kuekhaoyang
2025-11-16 21:29:31 +08:00
parent d97a8b7aec
commit df8843aefb
2 changed files with 33 additions and 10 deletions
+17 -6
View File
@@ -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<boolean> {
if (!isValidUrlFormat(url)) {
@@ -30,21 +30,32 @@ async function checkVideoUrl(url: string, retries = MAX_RETRIES): Promise<boolea
const controller = new AbortController();
const timeoutId = setTimeout(() => 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
+16 -4
View File
@@ -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<ValidationResult> {
const startTime = Date.now();
@@ -23,22 +23,34 @@ async function checkUrlAccessibility(url: string): Promise<ValidationResult> {
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 {