diff --git a/app/api/search/route.ts b/app/api/search/route.ts index 4961e0b..17ca6c1 100644 --- a/app/api/search/route.ts +++ b/app/api/search/route.ts @@ -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(); @@ -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(); diff --git a/lib/utils/source-checker.ts b/lib/utils/source-checker.ts index 7b3865f..e39a1c3 100644 --- a/lib/utils/source-checker.ts +++ b/lib/utils/source-checker.ts @@ -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 { if (!isValidUrlFormat(url)) { @@ -30,40 +32,82 @@ 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: '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 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 { + // 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 { + 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(), }; }