|
import readTorrent from 'read-torrent'; |
|
import { promisify } from 'util'; |
|
|
|
const readTorrentPromise = promisify(readTorrent); |
|
|
|
const SITE_CONFIG = { |
|
baseUrl: 'https://yourbittorrent.com', |
|
fallbackUrls: [ |
|
'https://yourbittorrent2.com' |
|
] |
|
}; |
|
|
|
async function getCinemetaMetadata(imdbId) { |
|
try { |
|
if (!imdbId.startsWith('tt')) { |
|
return null; |
|
} |
|
|
|
console.log(`\n🎬 Fetching Cinemeta data for ${imdbId}`); |
|
const response = await fetch(`https://v3-cinemeta.strem.io/meta/movie/${imdbId}.json`); |
|
if (!response.ok) throw new Error('Failed to fetch from Cinemeta'); |
|
const data = await response.json(); |
|
console.log('✅ Found:', data.meta.name); |
|
return data; |
|
} catch (error) { |
|
console.error('❌ Cinemeta error:', error); |
|
return null; |
|
} |
|
} |
|
|
|
async function fetchWithTimeout(url, options = {}, timeout = 30000) { |
|
const controller = new AbortController(); |
|
const timeoutId = setTimeout(() => controller.abort(), timeout); |
|
|
|
try { |
|
const response = await fetch(url, { |
|
...options, |
|
signal: controller.signal |
|
}); |
|
clearTimeout(timeoutId); |
|
return response; |
|
} catch (error) { |
|
clearTimeout(timeoutId); |
|
throw error; |
|
} |
|
} |
|
|
|
async function downloadAndParseTorrent(url) { |
|
try { |
|
console.log('Downloading torrent from:', url); |
|
|
|
const options = { |
|
headers: { |
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', |
|
'Accept': '*/*', |
|
} |
|
}; |
|
|
|
const torrentInfo = await readTorrentPromise(url, options); |
|
|
|
|
|
const hasVideoFiles = torrentInfo.files?.some(file => { |
|
const filePath = Array.isArray(file.path) ? file.path.join('/') : file.path; |
|
return /\.(mp4|mkv|avi|mov|wmv)$/i.test(filePath); |
|
}); |
|
|
|
if (!hasVideoFiles) { |
|
console.log('No video files found'); |
|
return null; |
|
} |
|
|
|
|
|
const videoFiles = torrentInfo.files.filter(file => { |
|
const filePath = Array.isArray(file.path) ? file.path.join('/') : file.path; |
|
return /\.(mp4|mkv|avi|mov|wmv)$/i.test(filePath); |
|
}).sort((a, b) => b.length - a.length); |
|
|
|
const mainFile = videoFiles[0]; |
|
const mainFilePath = Array.isArray(mainFile.path) ? mainFile.path.join('/') : mainFile.path; |
|
|
|
const magnetUri = `magnet:?xt=urn:btih:${torrentInfo.infoHash}` + |
|
`&dn=${encodeURIComponent(torrentInfo.name)}` + |
|
(torrentInfo.announce ? torrentInfo.announce.map(tr => `&tr=${encodeURIComponent(tr)}`).join('') : ''); |
|
|
|
return { |
|
magnetLink: magnetUri, |
|
filename: mainFilePath, |
|
torrentName: torrentInfo.name, |
|
infoHash: torrentInfo.infoHash, |
|
mainFileSize: mainFile.length |
|
}; |
|
|
|
} catch (error) { |
|
console.error('Error downloading/parsing torrent:', 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 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, |
|
/Season.?(\d{1,2}).?Episode.?(\d{1,2})/i |
|
]; |
|
|
|
for (const pattern of patterns) { |
|
const match = title.match(pattern); |
|
if (match) { |
|
const seasonNum = parseInt(match[1]); |
|
const 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 parseSearchResults(html) { |
|
const results = []; |
|
const rows = html.match(/<tr class="table-default">[\s\S]*?<\/tr>/g) || []; |
|
|
|
for (const row of rows) { |
|
try { |
|
const titleMatch = row.match(/href="\/torrent\/.*?">(.*?)<\/a>/); |
|
const sizeMatch = row.match(/<td.*?>\s*([\d.]+\s*[KMGT]B)\s*<\/td>/); |
|
const seedersMatch = row.match(/<td.*?>\s*(\d+)\s*<\/td>/); |
|
const leechersMatch = row.match(/<td.*?>\s*(\d+)\s*<\/td>/); |
|
const downloadIdMatch = row.match(/\/torrent\/(\d+)\//); |
|
|
|
if (titleMatch && downloadIdMatch) { |
|
const title = titleMatch[1].trim(); |
|
|
|
|
|
if (!title.match(/\.(mp4|mkv|avi|mov|wmv)$/i) && |
|
!title.match(/(1080p|720p|2160p|4k|uhd|web-?dl|bluray)/i)) { |
|
continue; |
|
} |
|
|
|
results.push({ |
|
title: title, |
|
size: sizeMatch?.[1] || 'Unknown', |
|
seeders: seedersMatch?.[1] || '0', |
|
leechers: leechersMatch?.[1] || '0', |
|
downloadId: downloadIdMatch[1], |
|
magnetLink: downloadIdMatch[1] ? |
|
`${SITE_CONFIG.baseUrl}/down/${downloadIdMatch[1]}.torrent` : |
|
null |
|
}); |
|
} |
|
} catch (error) { |
|
console.error('Error parsing result row:', error); |
|
} |
|
} |
|
|
|
return results; |
|
} |
|
|
|
async function searchTorrents(searchQuery, type = 'movie') { |
|
console.log('\n🔄 Searching YourBittorrent for:', searchQuery); |
|
|
|
try { |
|
let title = searchQuery; |
|
let year = ''; |
|
let searchTitle; |
|
|
|
if (type === 'movie' && searchQuery.startsWith('tt')) { |
|
const metadata = await getCinemetaMetadata(searchQuery); |
|
if (!metadata?.meta) { |
|
throw new Error('Failed to get movie metadata'); |
|
} |
|
title = metadata.meta.name; |
|
year = new Date(metadata.meta.released).getFullYear().toString(); |
|
searchTitle = title; |
|
} else if (type === 'series') { |
|
|
|
const showMatch = searchQuery.match(/(.+?)S\d{2}E\d{2}/i); |
|
searchTitle = showMatch ? showMatch[1].trim() : searchQuery; |
|
} |
|
|
|
const formattedQuery = searchTitle |
|
.replace(/[\\s]+/g, '-') |
|
.toLowerCase(); |
|
|
|
const url = `${SITE_CONFIG.baseUrl}/?v=&c=${type === 'movie' ? 'movies' : 'tv'}&q=${encodeURIComponent(formattedQuery)}`; |
|
console.log('Request URL:', url); |
|
|
|
const response = await fetchWithTimeout(url, { |
|
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' |
|
} |
|
}); |
|
|
|
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} raw results`); |
|
|
|
|
|
const streams = await Promise.all(results.map(async result => { |
|
try { |
|
if (!result.magnetLink) return null; |
|
|
|
const torrentInfo = await downloadAndParseTorrent(result.magnetLink); |
|
if (!torrentInfo) return null; |
|
|
|
return { |
|
magnetLink: torrentInfo.magnetLink, |
|
filename: torrentInfo.filename, |
|
websiteTitle: result.title, |
|
quality: extractQuality(result.title), |
|
size: result.size, |
|
source: 'YourBittorrent', |
|
seeders: parseInt(result.seeders) || 0, |
|
leechers: parseInt(result.leechers) || 0, |
|
mainFileSize: torrentInfo.mainFileSize |
|
}; |
|
} catch (error) { |
|
console.error('Error processing result:', error); |
|
return null; |
|
} |
|
})); |
|
|
|
const validStreams = streams.filter(Boolean); |
|
|
|
let filteredStreams; |
|
if (type === 'movie' && year) { |
|
const searchTerms = title.toLowerCase().split(' '); |
|
filteredStreams = validStreams.filter(stream => { |
|
const streamTitle = stream.websiteTitle.toLowerCase(); |
|
const hasYear = streamTitle.includes(year); |
|
const hasAllTerms = searchTerms.every(term => |
|
streamTitle.includes(term.toLowerCase()) |
|
); |
|
return hasYear && hasAllTerms; |
|
}); |
|
} else if (type === 'series') { |
|
|
|
filteredStreams = validStreams.filter(stream => |
|
isCorrectEpisode(stream.websiteTitle, searchQuery) |
|
); |
|
} else { |
|
filteredStreams = validStreams; |
|
} |
|
|
|
console.log(`Found ${filteredStreams.length} relevant streams after filtering`); |
|
|
|
if (filteredStreams.length > 0) { |
|
console.log('Sample matched stream:', { |
|
title: filteredStreams[0].websiteTitle, |
|
quality: filteredStreams[0].quality, |
|
size: filteredStreams[0].size |
|
}); |
|
} |
|
|
|
filteredStreams.sort((a, b) => { |
|
const qualityOrder = { '2160p': 4, '4k': 4, '1080p': 3, '720p': 2 }; |
|
const qualityDiff = (qualityOrder[b.quality] || 0) - (qualityOrder[a.quality] || 0); |
|
if (qualityDiff === 0) { |
|
return b.seeders - a.seeders; |
|
} |
|
return qualityDiff; |
|
}); |
|
|
|
return filteredStreams; |
|
|
|
} catch (error) { |
|
console.error('❌ Error searching YourBittorrent:', error); |
|
|
|
for (const fallbackUrl of SITE_CONFIG.fallbackUrls) { |
|
try { |
|
SITE_CONFIG.baseUrl = fallbackUrl; |
|
return await searchTorrents(searchQuery, type); |
|
} catch (fallbackError) { |
|
console.error(`Fallback ${fallbackUrl} also failed:`, fallbackError); |
|
} |
|
} |
|
|
|
return []; |
|
} |
|
} |
|
|
|
function extractQuality(title) { |
|
const qualityMatch = title.match(/\b(2160p|1080p|720p|4k|uhd)\b/i); |
|
return qualityMatch ? qualityMatch[1].toLowerCase() : ''; |
|
} |
|
|
|
export { searchTorrents }; |
|
|