|
import { promisify } from 'util'; |
|
|
|
const SITE_CONFIG = { |
|
baseUrl: 'https://eztvx.to', |
|
fallbackUrls: [ |
|
'https://eztv.wf', |
|
'https://eztv.tf', |
|
'https://eztv.yt', |
|
'https://eztv1.xyz' |
|
], |
|
headers: { |
|
'Host': 'eztvx.to', |
|
'Cookie': 'sort_no=100; q_filter=all; q_filter_web=on; q_filter_reality=on; q_filter_x265=on; layout=def_wlinks', |
|
'Accept': '*/*', |
|
'Accept-Language': '*', |
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36', |
|
'Accept-Encoding': 'gzip, deflate' |
|
} |
|
}; |
|
|
|
async function fetchWithTimeout(url, options = {}, timeout = 30000) { |
|
const controller = new AbortController(); |
|
const timeoutId = setTimeout(() => controller.abort(), timeout); |
|
|
|
try { |
|
const host = new URL(url).host; |
|
const headers = { |
|
...SITE_CONFIG.headers, |
|
'Host': host, |
|
...options.headers |
|
}; |
|
|
|
const response = await fetch(url, { |
|
...options, |
|
headers, |
|
signal: controller.signal |
|
}); |
|
clearTimeout(timeoutId); |
|
return response; |
|
} catch (error) { |
|
clearTimeout(timeoutId); |
|
throw error; |
|
} |
|
} |
|
|
|
function isCorrectEpisode(title, searchQuery) { |
|
if (!title || !searchQuery) return false; |
|
|
|
const queryMatch = searchQuery.match(/S(\d{2})E(\d{2})/i); |
|
if (!queryMatch) return false; |
|
|
|
const querySeasonNum = parseInt(queryMatch[1]); |
|
const queryEpisodeNum = parseInt(queryMatch[2]); |
|
|
|
const patterns = [ |
|
/S(\d{2})E(\d{2})/i, |
|
/(\d{1,2})x(\d{2})/i, |
|
/[. ](\d{1,2})(\d{2})[. ]/, |
|
/Season (\d{1,2}) Episode (\d{1,2})/i, |
|
/[. ]E(\d{2})[. ]/i, |
|
/(\d{1,2})[. ](\d{2})/ |
|
]; |
|
|
|
for (const pattern of patterns) { |
|
const match = title.match(pattern); |
|
if (match) { |
|
let seasonNum, episodeNum; |
|
if (pattern.toString().includes('[. ]E')) { |
|
seasonNum = querySeasonNum; |
|
episodeNum = parseInt(match[1]); |
|
} else { |
|
seasonNum = parseInt(match[1]); |
|
episodeNum = parseInt(match[2]); |
|
} |
|
|
|
if (seasonNum === querySeasonNum && episodeNum === queryEpisodeNum) { |
|
return true; |
|
} |
|
} |
|
} |
|
|
|
|
|
const combinedMatch = title.match(/[^0-9](\d)(\d{2})[^0-9]/); |
|
if (combinedMatch) { |
|
const seasonNum = parseInt(combinedMatch[1]); |
|
const episodeNum = parseInt(combinedMatch[2]); |
|
if (seasonNum === querySeasonNum && episodeNum === queryEpisodeNum) { |
|
return true; |
|
} |
|
} |
|
|
|
return false; |
|
} |
|
|
|
function parseSize(sizeStr) { |
|
if (!sizeStr) return 0; |
|
const match = sizeStr.match(/([\d.]+)\s*(KB|MB|GB|TB)/i); |
|
if (!match) return 0; |
|
|
|
const [, value, unit] = match; |
|
const size = parseFloat(value); |
|
|
|
switch (unit.toUpperCase()) { |
|
case 'TB': return size * 1024 * 1024 * 1024; |
|
case 'GB': return size * 1024 * 1024; |
|
case 'MB': return size * 1024; |
|
case 'KB': return size; |
|
default: return 0; |
|
} |
|
} |
|
|
|
function extractQuality(title) { |
|
const qualityMatch = title.match(/\b(2160p|1080p|720p|4k|uhd)\b/i); |
|
return qualityMatch ? qualityMatch[1].toLowerCase() : ''; |
|
} |
|
|
|
function cleanTitle(title) { |
|
if (!title) return ''; |
|
return title |
|
.replace(/\[eztv\]/i, '') |
|
.replace(/\[eztvx?\.to\]/i, '') |
|
.replace(/\[eztv\.(re|io)\]/i, '') |
|
.replace(/<img[^>]+>/g, '') |
|
.replace(/\[.*?\]/g, '') |
|
.replace(/\(.*?\)/g, '') |
|
.replace(/\.+/g, ' ') |
|
.replace(/\s+/g, ' ') |
|
.trim(); |
|
} |
|
|
|
async function searchTorrents(searchQuery, type = 'series') { |
|
if (type !== 'series') { |
|
console.log('EZTV only supports TV series searches'); |
|
return []; |
|
} |
|
|
|
console.log('\n🔄 Searching EZTV for:', searchQuery); |
|
|
|
try { |
|
const showMatch = searchQuery.match(/(.+?)S\d{2}E\d{2}/i); |
|
const searchTerm = showMatch ? showMatch[1].trim() : searchQuery; |
|
|
|
const formattedQuery = searchTerm |
|
.replace(/-/g, '') |
|
.replace(/\s+/g, '-') |
|
.replace(/&/g, '') |
|
.toLowerCase(); |
|
|
|
const url = `${SITE_CONFIG.baseUrl}/search/${encodeURIComponent(formattedQuery)}`; |
|
console.log('Request URL:', url); |
|
|
|
const response = await fetchWithTimeout(url); |
|
|
|
if (!response.ok) { |
|
throw new Error(`Search request failed: ${response.status}`); |
|
} |
|
|
|
const html = await response.text(); |
|
console.log('Response received, length:', html.length); |
|
|
|
const rows = html.match(/<tr[^>]*name="hover"[^>]*>[\s\S]*?<\/tr>/g) || []; |
|
console.log(`Found ${rows.length} raw results`); |
|
|
|
const streams = rows.map(row => { |
|
try { |
|
const titleLinkMatch = row.match(/<a href="\/ep\/\d+\/[^"]+\/"[^>]*class="epinfo">(.*?)<\/a>/); |
|
if (!titleLinkMatch) return null; |
|
let title = titleLinkMatch[1].replace(/<[^>]+>/g, '').trim(); |
|
|
|
const magnetMatch = row.match(/href="(magnet:\?xt=urn:btih:[^"]+)"/); |
|
if (!magnetMatch) return null; |
|
const magnetLink = decodeURIComponent(magnetMatch[1]); |
|
|
|
const sizeMatch = row.match(/<td[^>]*class="forum_thread_post"[^>]*>([^<]+(?:KB|MB|GB|TB))<\/td>/); |
|
const size = sizeMatch ? sizeMatch[1].trim() : '512 MB'; |
|
|
|
const seedersMatch = row.match(/<font color="green">(\d+)<\/font>/); |
|
const seeders = seedersMatch ? parseInt(seedersMatch[1]) : 0; |
|
|
|
title = cleanTitle(title); |
|
|
|
if (title.includes('COMPLETE') && searchQuery.match(/S\d{2}E\d{2}/i)) { |
|
console.log('Skipping complete season pack:', title); |
|
return null; |
|
} |
|
|
|
const mainFileSize = parseSize(size); |
|
if (mainFileSize < (100 * 1024)) { |
|
console.log('Skipping small file:', title, size); |
|
return null; |
|
} |
|
|
|
return { |
|
magnetLink, |
|
filename: title, |
|
websiteTitle: title, |
|
quality: extractQuality(title), |
|
size, |
|
source: 'EZTV', |
|
seeders, |
|
leechers: 0, |
|
mainFileSize |
|
}; |
|
} catch (error) { |
|
console.error('Error parsing row:', error); |
|
return null; |
|
} |
|
}).filter(Boolean); |
|
|
|
const filteredStreams = streams.filter(stream => |
|
isCorrectEpisode(stream.websiteTitle, searchQuery) |
|
); |
|
|
|
console.log(`Found ${filteredStreams.length} matching streams after filtering`); |
|
|
|
filteredStreams.sort((a, b) => { |
|
const qualityOrder = { '2160p': 4, '4k': 4, 'uhd': 4, '1080p': 3, '720p': 2 }; |
|
const qualityDiff = (qualityOrder[b.quality] || 0) - (qualityOrder[a.quality] || 0); |
|
if (qualityDiff === 0) { |
|
return b.mainFileSize - a.mainFileSize; |
|
} |
|
return qualityDiff; |
|
}); |
|
|
|
if (filteredStreams.length > 0) { |
|
console.log('Sample stream:', { |
|
title: filteredStreams[0].websiteTitle, |
|
quality: filteredStreams[0].quality, |
|
size: filteredStreams[0].size, |
|
seeders: filteredStreams[0].seeders |
|
}); |
|
} |
|
|
|
return filteredStreams; |
|
|
|
} catch (error) { |
|
console.error('❌ Error searching EZTV:', error); |
|
|
|
for (const fallbackUrl of SITE_CONFIG.fallbackUrls) { |
|
try { |
|
console.log(`Trying fallback URL: ${fallbackUrl}`); |
|
const originalBaseUrl = SITE_CONFIG.baseUrl; |
|
SITE_CONFIG.baseUrl = fallbackUrl; |
|
const results = await searchTorrents(searchQuery, type); |
|
if (results.length > 0) { |
|
return results; |
|
} |
|
SITE_CONFIG.baseUrl = originalBaseUrl; |
|
} catch (fallbackError) { |
|
console.error(`Fallback ${fallbackUrl} also failed:`, fallbackError); |
|
} |
|
} |
|
|
|
return []; |
|
} |
|
} |
|
|
|
export { searchTorrents }; |
|
|