mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-19 18:53:42 +08:00
Enhance video availability checks with improved accuracy and reduced concurrency; update source availability validation for better reliability
This commit is contained in:
@@ -71,8 +71,8 @@ export async function POST(request: NextRequest) {
|
||||
// Get all videos from all sources
|
||||
const allVideos = searchResults.flatMap(r => r.results);
|
||||
|
||||
// Check each video individually
|
||||
const availableVideos = await checkMultipleVideos(allVideos, 10);
|
||||
// Check each video individually with improved accuracy (reduced concurrency)
|
||||
const availableVideos = await checkMultipleVideos(allVideos, 8);
|
||||
|
||||
// Group available videos by source
|
||||
const videosBySource = new Map<string, any[]>();
|
||||
@@ -183,8 +183,8 @@ export async function GET(request: NextRequest) {
|
||||
// Get all videos from all sources
|
||||
const allVideos = searchResults.flatMap(r => r.results);
|
||||
|
||||
// Check each video individually
|
||||
const availableVideos = await checkMultipleVideos(allVideos, 10);
|
||||
// Check each video individually with improved accuracy (reduced concurrency)
|
||||
const availableVideos = await checkMultipleVideos(allVideos, 8);
|
||||
|
||||
// Group available videos by source
|
||||
const videosBySource = new Map<string, any[]>();
|
||||
|
||||
+159
-53
@@ -5,8 +5,9 @@
|
||||
|
||||
import { isValidUrlFormat } from './url-validator';
|
||||
|
||||
const CHECK_TIMEOUT = 3000; // 3 seconds per check
|
||||
const MAX_RETRIES = 2;
|
||||
const CHECK_TIMEOUT = 5000; // 5 seconds per check (increased for more reliability)
|
||||
const MAX_RETRIES = 1; // Reduced retries to speed up detection
|
||||
const MIN_CONTENT_LENGTH = 1024; // Minimum content size to consider valid video
|
||||
|
||||
export interface SourceCheckResult {
|
||||
sourceId: string;
|
||||
@@ -19,6 +20,7 @@ export interface SourceCheckResult {
|
||||
|
||||
/**
|
||||
* Check if a single video URL is accessible and actually contains video content
|
||||
* More accurate detection with multiple validation steps
|
||||
*/
|
||||
async function checkVideoUrl(url: string, retries = MAX_RETRIES): Promise<boolean> {
|
||||
if (!isValidUrlFormat(url)) {
|
||||
@@ -30,40 +32,82 @@ 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: '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
|
||||
},
|
||||
});
|
||||
// First, try HEAD request to check if resource exists without downloading
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(url, {
|
||||
method: 'HEAD',
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
|
||||
'Referer': new URL(url).origin,
|
||||
},
|
||||
});
|
||||
|
||||
// If HEAD request fails or returns bad status, try GET with Range
|
||||
if (!response.ok && response.status !== 206) {
|
||||
throw new Error('HEAD request failed');
|
||||
}
|
||||
} catch (headError) {
|
||||
// Fallback to GET with Range if HEAD fails
|
||||
response = await fetch(url, {
|
||||
method: 'GET',
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
|
||||
'Referer': new URL(url).origin,
|
||||
'Range': 'bytes=0-2048', // Fetch first 2KB to verify
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
// 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;
|
||||
}
|
||||
// Check response status
|
||||
// 200 OK: Full content
|
||||
// 206 Partial Content: Range request successful
|
||||
// 403 Forbidden: Not accessible (should fail)
|
||||
// 404 Not Found: Video doesn't exist (should fail)
|
||||
if (!response.ok && response.status !== 206) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Additional validation checks
|
||||
const contentType = response.headers.get('content-type');
|
||||
const contentLength = response.headers.get('content-length');
|
||||
const acceptRanges = response.headers.get('accept-ranges');
|
||||
|
||||
// Check 1: Content type validation
|
||||
const hasValidContentType = contentType && (
|
||||
contentType.includes('video') ||
|
||||
contentType.includes('mpegurl') ||
|
||||
contentType.includes('m3u8') ||
|
||||
contentType.includes('application/vnd.apple.mpegurl') ||
|
||||
contentType.includes('octet-stream')
|
||||
);
|
||||
|
||||
// Check 2: Content length validation (should have some content)
|
||||
// For m3u8 files, length can be small, so we're more lenient
|
||||
const hasValidLength = !contentLength ||
|
||||
parseInt(contentLength) >= MIN_CONTENT_LENGTH ||
|
||||
(contentType && (contentType.includes('m3u8') || contentType.includes('mpegurl')));
|
||||
|
||||
// Check 3: For video files, check if server supports range requests (good sign)
|
||||
const supportsRanges = acceptRanges === 'bytes' || response.status === 206;
|
||||
|
||||
// Video must pass content type check AND either have valid length OR support ranges
|
||||
if (hasValidContentType && (hasValidLength || supportsRanges)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
// If last attempt, return false
|
||||
if (attempt === retries) {
|
||||
return false;
|
||||
}
|
||||
// Wait before retry
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,22 +116,36 @@ async function checkVideoUrl(url: string, retries = MAX_RETRIES): Promise<boolea
|
||||
|
||||
/**
|
||||
* Extract first playable URL from video data
|
||||
* More robust parsing with better error handling
|
||||
*/
|
||||
function extractFirstVideoUrl(video: any): string | null {
|
||||
if (!video.vod_play_url) return null;
|
||||
|
||||
try {
|
||||
// Format: "Episode1$url1#Episode2$url2#..."
|
||||
const episodes = video.vod_play_url.split('#');
|
||||
// Format: "Episode1$url1#Episode2$url2#..." or sometimes "url1#url2#url3"
|
||||
const episodes = video.vod_play_url.split('#').filter((ep: string) => ep.trim());
|
||||
|
||||
for (const episode of episodes) {
|
||||
const [, url] = episode.split('$');
|
||||
if (url && isValidUrlFormat(url)) {
|
||||
return url;
|
||||
// Try different formats
|
||||
const parts = episode.split('$');
|
||||
|
||||
// Format 1: "Episode$url"
|
||||
if (parts.length >= 2) {
|
||||
const url = parts[1].trim();
|
||||
if (url && isValidUrlFormat(url)) {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
// Format 2: Just "url" without episode name
|
||||
else if (parts.length === 1) {
|
||||
const url = parts[0].trim();
|
||||
if (url && isValidUrlFormat(url)) {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Silent error
|
||||
console.error('Error extracting video URL:', error);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -95,54 +153,91 @@ function extractFirstVideoUrl(video: any): string | null {
|
||||
|
||||
/**
|
||||
* Check if a single video is playable
|
||||
* Returns false if URL is invalid or video fails validation
|
||||
*/
|
||||
export async function checkVideoAvailability(video: any): Promise<boolean> {
|
||||
// First check if video has required data
|
||||
if (!video || !video.vod_play_url) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const videoUrl = extractFirstVideoUrl(video);
|
||||
|
||||
if (!videoUrl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return await checkVideoUrl(videoUrl);
|
||||
// Perform thorough URL check
|
||||
const isAvailable = await checkVideoUrl(videoUrl);
|
||||
|
||||
// Log failed checks for debugging
|
||||
if (!isAvailable) {
|
||||
console.debug(`Video unavailable: ${video.vod_name || 'Unknown'} (${videoUrl.substring(0, 50)}...)`);
|
||||
}
|
||||
|
||||
return isAvailable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check multiple videos in parallel with concurrency limit
|
||||
* Improved error handling and progress reporting
|
||||
*/
|
||||
export async function checkMultipleVideos(
|
||||
videos: any[],
|
||||
concurrency: number = 10,
|
||||
concurrency: number = 8, // Reduced default concurrency for better accuracy
|
||||
onProgress?: (checked: number, total: number) => void
|
||||
): Promise<any[]> {
|
||||
if (!videos || videos.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const availableVideos: any[] = [];
|
||||
let checkedCount = 0;
|
||||
|
||||
// Process videos in batches to avoid overwhelming the system
|
||||
for (let i = 0; i < videos.length; i += concurrency) {
|
||||
const batch = videos.slice(i, i + concurrency);
|
||||
const results = await Promise.all(
|
||||
batch.map(async (video) => {
|
||||
const isAvailable = await checkVideoAvailability(video);
|
||||
checkedCount++;
|
||||
|
||||
// Report progress
|
||||
if (onProgress) {
|
||||
onProgress(checkedCount, videos.length);
|
||||
}
|
||||
|
||||
return isAvailable ? video : null;
|
||||
})
|
||||
);
|
||||
|
||||
// Add available videos to result
|
||||
availableVideos.push(...results.filter(v => v !== null));
|
||||
try {
|
||||
const results = await Promise.all(
|
||||
batch.map(async (video) => {
|
||||
try {
|
||||
const isAvailable = await checkVideoAvailability(video);
|
||||
checkedCount++;
|
||||
|
||||
// Report progress
|
||||
if (onProgress) {
|
||||
onProgress(checkedCount, videos.length);
|
||||
}
|
||||
|
||||
return isAvailable ? video : null;
|
||||
} catch (error) {
|
||||
// If individual check fails, mark as unavailable
|
||||
checkedCount++;
|
||||
if (onProgress) {
|
||||
onProgress(checkedCount, videos.length);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Add available videos to result
|
||||
availableVideos.push(...results.filter(v => v !== null));
|
||||
} catch (error) {
|
||||
console.error('Batch check error:', error);
|
||||
// Continue with next batch even if this one fails
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Checked ${videos.length} videos, ${availableVideos.length} available`);
|
||||
|
||||
return availableVideos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a source is available by testing a sample video
|
||||
* Now checks more videos for better accuracy
|
||||
*/
|
||||
export async function checkSourceAvailability(
|
||||
sourceId: string,
|
||||
@@ -162,15 +257,23 @@ export async function checkSourceAvailability(
|
||||
};
|
||||
}
|
||||
|
||||
// Try to find a video with a valid URL
|
||||
for (const video of sampleVideos.slice(0, 3)) { // Check up to 3 videos
|
||||
// Try to find at least one working video from up to 5 samples
|
||||
const samplesToCheck = Math.min(5, sampleVideos.length);
|
||||
let checkedCount = 0;
|
||||
|
||||
for (const video of sampleVideos.slice(0, samplesToCheck)) {
|
||||
checkedCount++;
|
||||
const videoUrl = extractFirstVideoUrl(video);
|
||||
|
||||
if (!videoUrl) continue;
|
||||
if (!videoUrl) {
|
||||
console.debug(`Source ${sourceName}: Video ${checkedCount} has no valid URL`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const isAvailable = await checkVideoUrl(videoUrl);
|
||||
|
||||
if (isAvailable) {
|
||||
console.log(`✓ Source ${sourceName} is available (verified with ${videoUrl.substring(0, 50)}...)`);
|
||||
return {
|
||||
sourceId,
|
||||
sourceName,
|
||||
@@ -178,14 +281,17 @@ export async function checkSourceAvailability(
|
||||
sampleUrl: videoUrl,
|
||||
checkedAt: Date.now(),
|
||||
};
|
||||
} else {
|
||||
console.debug(`Source ${sourceName}: Video ${checkedCount}/${samplesToCheck} unavailable`);
|
||||
}
|
||||
}
|
||||
|
||||
console.warn(`✗ Source ${sourceName} is unavailable (checked ${checkedCount} videos)`);
|
||||
return {
|
||||
sourceId,
|
||||
sourceName,
|
||||
isAvailable: false,
|
||||
error: 'All sample videos failed to load',
|
||||
error: `All ${checkedCount} sample videos failed to load`,
|
||||
checkedAt: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user