|
import parseTorrent from 'parse-torrent'; |
|
|
|
const SITE_CONFIG = { |
|
baseUrl: 'https://1337x.to', |
|
fallbackUrls: [ |
|
'https://1337x.st', |
|
'https://x1337x.ws', |
|
'https://x1337x.eu', |
|
'https://x1337x.se', |
|
'https://x1337x.cc' |
|
] |
|
}; |
|
|
|
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 response = await fetch(url, { |
|
...options, |
|
headers: { |
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', |
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9', |
|
'Accept-Language': 'en-US,en;q=0.9', |
|
'Host': host, |
|
...options.headers |
|
}, |
|
signal: controller.signal |
|
}); |
|
clearTimeout(timeoutId); |
|
return response; |
|
} catch (error) { |
|
clearTimeout(timeoutId); |
|
throw error; |
|
} |
|
} |
|
|
|
function extractQuality(title) { |
|
const qualityMatch = title.match(/\b(2160p|1080p|720p|4k|uhd)\b/i); |
|
return qualityMatch ? qualityMatch[1].toLowerCase() : ''; |
|
} |
|
|
|
function formatSize(sizeStr) { |
|
if (!sizeStr) return 'Unknown'; |
|
return sizeStr.replace(/\s+/g, '').toUpperCase(); |
|
} |
|
|
|
function parseSearchResults(html) { |
|
const results = []; |
|
const rows = html.match(/<tr>\s*<td class="coll-1 name">[\s\S]*?<\/tr>/g) || []; |
|
|
|
for (const row of rows) { |
|
try { |
|
|
|
if (!row.includes('href="/torrent/')) continue; |
|
|
|
|
|
const titleMatch = row.match(/href="\/torrent\/\d+\/([^"]+)"/); |
|
if (!titleMatch) continue; |
|
const title = decodeURIComponent(titleMatch[1].replace(/\-/g, ' ')); |
|
|
|
|
|
const pathMatch = row.match(/href="(\/torrent\/\d+\/[^"]+)"/); |
|
const detailsPath = pathMatch ? pathMatch[1] : null; |
|
|
|
|
|
const sizeMatch = row.match(/<td class="coll-4[^"]*">([^<]+)<span/); |
|
const size = sizeMatch ? sizeMatch[1].trim() : 'Unknown'; |
|
|
|
|
|
const seedersMatch = row.match(/<td class="coll-2 seeds">(\d+)<\/td>/); |
|
const leechersMatch = row.match(/<td class="coll-3 leeches">(\d+)<\/td>/); |
|
const seeders = seedersMatch ? parseInt(seedersMatch[1]) : 0; |
|
const leechers = leechersMatch ? parseInt(leechersMatch[1]) : 0; |
|
|
|
|
|
const dateMatch = row.match(/<td class="coll-date">([^<]+)<\/td>/); |
|
const uploadDate = dateMatch ? dateMatch[1].trim() : ''; |
|
|
|
if (title && detailsPath) { |
|
results.push({ |
|
title, |
|
detailsPath, |
|
size, |
|
seeders, |
|
leechers, |
|
uploadDate |
|
}); |
|
} |
|
} catch (error) { |
|
console.error('Error parsing row:', error); |
|
} |
|
} |
|
|
|
return results; |
|
} |
|
|
|
async function getMagnetLink(detailsPath) { |
|
try { |
|
const url = `${SITE_CONFIG.baseUrl}${detailsPath}`; |
|
console.log('Fetching details:', url); |
|
|
|
const response = await fetchWithTimeout(url); |
|
if (!response.ok) throw new Error(`Failed to fetch details: ${response.status}`); |
|
|
|
const html = await response.text(); |
|
|
|
|
|
const magnetMatch = html.match(/href="(magnet:\?xt=urn:btih:[^"]+)"/); |
|
if (!magnetMatch) { |
|
console.log('No magnet link found in details page'); |
|
return null; |
|
} |
|
|
|
const magnetLink = magnetMatch[1]; |
|
|
|
|
|
const parsed = await parseTorrent(magnetLink); |
|
if (!parsed.files) { |
|
console.log('No file list in magnet link'); |
|
return magnetLink; |
|
} |
|
|
|
|
|
const hasRarFiles = parsed.files.some(file => |
|
/\.rar$|\.r\d+$|part\d+\.rar$/i.test(file.name) |
|
); |
|
|
|
if (hasRarFiles) { |
|
console.log('Skipping magnet containing RAR files'); |
|
return null; |
|
} |
|
|
|
const videoFiles = parsed.files.filter(file => |
|
/\.(mkv|mp4|avi|mov|wmv|m4v|ts)$/i.test(file.name) |
|
); |
|
|
|
if (videoFiles.length === 0) { |
|
console.log('No video files found in magnet'); |
|
return null; |
|
} |
|
|
|
console.log('Found video files:', videoFiles.map(f => f.name)); |
|
return magnetLink; |
|
|
|
} catch (error) { |
|
console.error('Error getting magnet link:', error); |
|
return null; |
|
} |
|
} |
|
|
|
function isCorrectEpisode(title, searchQuery) { |
|
if (!title || !searchQuery) return false; |
|
|
|
const queryMatch = searchQuery.match(/S(\d{2})E(\d{2})/i); |
|
if (!queryMatch) return true; |
|
|
|
const season = queryMatch[1]; |
|
const episode = queryMatch[2]; |
|
|
|
|
|
const patterns = [ |
|
new RegExp(`S${season}E${episode}`, 'i'), |
|
new RegExp(`${season}x${episode}`, 'i'), |
|
new RegExp(`Season.?${season}.?Episode.?${episode}`, 'i') |
|
]; |
|
|
|
return patterns.some(pattern => pattern.test(title)); |
|
} |
|
|
|
async function searchTorrents(searchQuery, type = 'movie') { |
|
console.log('\n🔄 Searching 1337x for:', searchQuery); |
|
|
|
try { |
|
let searchPath; |
|
if (type === 'series') { |
|
searchPath = `/category-search/${encodeURIComponent(searchQuery)}/TV/1/`; |
|
} else { |
|
searchPath = `/category-search/${encodeURIComponent(searchQuery)}/Movies/1/`; |
|
} |
|
|
|
const url = `${SITE_CONFIG.baseUrl}${searchPath}`; |
|
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(); |
|
const results = parseSearchResults(html); |
|
console.log(`Found ${results.length} initial results`); |
|
|
|
|
|
const filteredResults = type === 'series' |
|
? results.filter(r => isCorrectEpisode(r.title, searchQuery)) |
|
: results; |
|
|
|
console.log(`Filtered to ${filteredResults.length} matching results`); |
|
|
|
|
|
const streams = await Promise.all(filteredResults.map(async result => { |
|
try { |
|
const magnetLink = await getMagnetLink(result.detailsPath); |
|
if (!magnetLink) return null; |
|
|
|
return { |
|
magnetLink, |
|
filename: result.title, |
|
websiteTitle: result.title, |
|
quality: extractQuality(result.title), |
|
size: formatSize(result.size), |
|
source: '1337x', |
|
seeders: result.seeders, |
|
leechers: result.leechers |
|
}; |
|
} catch (error) { |
|
console.error('Error processing result:', error); |
|
return null; |
|
} |
|
})); |
|
|
|
const validStreams = streams.filter(Boolean); |
|
|
|
if (validStreams.length > 0) { |
|
console.log('\nSample stream:', { |
|
title: validStreams[0].websiteTitle, |
|
quality: validStreams[0].quality, |
|
size: validStreams[0].size, |
|
seeders: validStreams[0].seeders |
|
}); |
|
} |
|
|
|
|
|
validStreams.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 qualityDiff; |
|
return b.seeders - a.seeders; |
|
}); |
|
|
|
return validStreams; |
|
|
|
} catch (error) { |
|
console.error('❌ Error searching 1337x:', error); |
|
|
|
for (const fallbackUrl of SITE_CONFIG.fallbackUrls) { |
|
try { |
|
console.log(`Trying fallback URL: ${fallbackUrl}`); |
|
SITE_CONFIG.baseUrl = fallbackUrl; |
|
return await searchTorrents(searchQuery, type); |
|
} catch (fallbackError) { |
|
console.error(`Fallback ${fallbackUrl} also failed:`, fallbackError); |
|
} |
|
} |
|
|
|
return []; |
|
} |
|
} |
|
|
|
export { searchTorrents }; |
|
|