|
import https from 'https'; |
|
|
|
const API_CONFIG = { |
|
baseUrl: 'https://aither.cc', |
|
apiKey: 'dRkYjNbT0S4ObFArTH1FXUTLPGsFRV4SqpPwwwEIo0IAtzsKhfF3zkPpTpCOcyBzab5q068680zVeHPk644q9sshtqpMv5mglfou' |
|
}; |
|
|
|
const agent = new https.Agent({ |
|
rejectUnauthorized: true, |
|
timeout: 30000, |
|
keepAlive: true |
|
}); |
|
|
|
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, |
|
agent |
|
}); |
|
clearTimeout(timeoutId); |
|
return response; |
|
} catch (error) { |
|
clearTimeout(timeoutId); |
|
throw error; |
|
} |
|
} |
|
|
|
function isCorrectEpisode(title, season, episode) { |
|
if (!title) return false; |
|
|
|
const seasonStr = season.toString().padStart(2, '0'); |
|
const episodeStr = episode.toString().padStart(2, '0'); |
|
|
|
|
|
const patterns = [ |
|
new RegExp(`S${seasonStr}E${episodeStr}`, 'i'), |
|
new RegExp(`${season}x${episodeStr}`, 'i'), |
|
new RegExp(`[. ]${season}${episodeStr}[. ]`), |
|
new RegExp(`Season ${season} Episode ${episode}`, 'i'), |
|
new RegExp(`Season.?${season}.?Episode.?${episode}`, 'i') |
|
]; |
|
|
|
return patterns.some(pattern => pattern.test(title)); |
|
} |
|
|
|
function extractSeasonEpisode(query) { |
|
const match = query.match(/S(\d{1,2})E(\d{1,2})/i); |
|
if (match) { |
|
return { |
|
season: parseInt(match[1]), |
|
episode: parseInt(match[2]) |
|
}; |
|
} |
|
return null; |
|
} |
|
|
|
async function fetchTorrents(searchQuery, type = 'movie') { |
|
console.log('\n🔄 Fetching from Aither API for:', searchQuery); |
|
|
|
try { |
|
let params; |
|
let episodeInfo = null; |
|
|
|
if (type === 'series') { |
|
|
|
episodeInfo = extractSeasonEpisode(searchQuery); |
|
if (!episodeInfo) { |
|
console.log('No valid season/episode info found in query'); |
|
return []; |
|
} |
|
|
|
|
|
const showName = searchQuery.split(/S\d{2}E\d{2}/i)[0].trim(); |
|
console.log('Series search:', { showName, season: episodeInfo.season, episode: episodeInfo.episode }); |
|
|
|
params = new URLSearchParams({ |
|
name: showName, |
|
sortField: 'created_at', |
|
sortDirection: 'desc', |
|
perPage: '100' |
|
}); |
|
} else { |
|
|
|
const isImdbId = searchQuery.startsWith('tt'); |
|
params = new URLSearchParams({ |
|
[isImdbId ? 'imdbId' : 'name']: isImdbId ? searchQuery.replace('tt', '') : searchQuery, |
|
sortField: 'created_at', |
|
sortDirection: 'desc', |
|
perPage: '100' |
|
}); |
|
} |
|
|
|
const url = `${API_CONFIG.baseUrl}/api/torrents/filter?${params}`; |
|
console.log('Request URL:', url); |
|
|
|
const response = await fetchWithTimeout(url, { |
|
headers: { |
|
'Host': 'aither.cc', |
|
'Authorization': `Bearer ${API_CONFIG.apiKey}`, |
|
'Accept': '*/*', |
|
'Accept-Language': '*', |
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', |
|
'Accept-Encoding': 'gzip, deflate' |
|
}, |
|
method: 'GET', |
|
compress: true |
|
}); |
|
|
|
if (!response.ok) { |
|
throw new Error(`API request failed: ${response.status}`); |
|
} |
|
|
|
const data = await response.json(); |
|
if (!data.data || !Array.isArray(data.data)) { |
|
console.log('No torrents found'); |
|
return []; |
|
} |
|
|
|
console.log(`Found ${data.data.length} initial results`); |
|
|
|
const streams = await Promise.all(data.data.map(async (item, index) => { |
|
try { |
|
console.log(`\nProcessing item ${index + 1}/${data.data.length}:`, item.attributes.name); |
|
|
|
|
|
const hasVideoFiles = item.attributes.files.some(file => |
|
/\.(mp4|mkv|avi|mov|wmv)$/i.test(file.name) |
|
); |
|
|
|
if (!hasVideoFiles) { |
|
console.log('No video files found in:', item.attributes.name); |
|
return null; |
|
} |
|
|
|
|
|
if (type === 'series' && episodeInfo) { |
|
if (!isCorrectEpisode(item.attributes.name, episodeInfo.season, episodeInfo.episode)) { |
|
console.log('Episode mismatch:', item.attributes.name); |
|
return null; |
|
} |
|
} |
|
|
|
|
|
const videoFiles = item.attributes.files |
|
.filter(file => /\.(mp4|mkv|avi|mov|wmv)$/i.test(file.name)) |
|
.sort((a, b) => b.size - a.size); |
|
|
|
const mainFile = videoFiles[0]; |
|
|
|
|
|
const qualityMatch = item.attributes.name.match(/\b(2160p|1080p|720p|4k|uhd)\b/i); |
|
const quality = qualityMatch ? qualityMatch[1].toLowerCase() : ''; |
|
|
|
return { |
|
magnetLink: `magnet:?xt=urn:btih:${item.attributes.info_hash}`, |
|
filename: mainFile.name, |
|
websiteTitle: item.attributes.name, |
|
quality: quality, |
|
size: formatSize(item.attributes.size), |
|
source: 'Aither', |
|
infoHash: item.attributes.info_hash, |
|
mainFileSize: mainFile.size, |
|
seeders: item.attributes.seeders || 0, |
|
leechers: item.attributes.leechers || 0 |
|
}; |
|
} catch (error) { |
|
console.error(`Error processing item ${index + 1}:`, error); |
|
return null; |
|
} |
|
})); |
|
|
|
const validStreams = streams.filter(Boolean); |
|
console.log(`✅ Processed ${validStreams.length} valid streams`); |
|
|
|
|
|
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); |
|
return qualityDiff || b.seeders - a.seeders; |
|
}); |
|
|
|
if (validStreams.length > 0) { |
|
console.log('Sample stream:', { |
|
name: validStreams[0].websiteTitle, |
|
quality: validStreams[0].quality, |
|
size: validStreams[0].size |
|
}); |
|
} |
|
|
|
return validStreams; |
|
|
|
} catch (error) { |
|
console.error('❌ Error fetching from Aither:', error); |
|
console.error('Error details:', { |
|
message: error.message, |
|
cause: error.cause, |
|
code: error.code, |
|
syscall: error.syscall |
|
}); |
|
return []; |
|
} |
|
} |
|
|
|
function formatSize(bytes) { |
|
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; |
|
if (bytes === 0) return '0 Bytes'; |
|
const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024))); |
|
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i]; |
|
} |
|
|
|
export { fetchTorrents }; |
|
|