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'); // Common episode patterns const patterns = [ new RegExp(`S${seasonStr}E${episodeStr}`, 'i'), // S01E01 new RegExp(`${season}x${episodeStr}`, 'i'), // 1x01 new RegExp(`[. ]${season}${episodeStr}[. ]`), // .101. or ' 101 ' new RegExp(`Season ${season} Episode ${episode}`, 'i'), // Season 1 Episode 1 new RegExp(`Season.?${season}.?Episode.?${episode}`, 'i') // Season1Episode1 or Season.1.Episode.1 ]; 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') { // Extract season and episode info episodeInfo = extractSeasonEpisode(searchQuery); if (!episodeInfo) { console.log('No valid season/episode info found in query'); return []; } // Extract show name for search 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 { // For movies, use IMDB ID if available, otherwise use title 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); // Skip if no video files 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; } // For series, check if it matches the requested episode if (type === 'series' && episodeInfo) { if (!isCorrectEpisode(item.attributes.name, episodeInfo.season, episodeInfo.episode)) { console.log('Episode mismatch:', item.attributes.name); return null; } } // Find the main video file (largest video file) 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]; // Extract quality from name 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`); // Sort by quality and 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); 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 };