diff --git a/mapper.js b/mapper.js index fd33d46..582ca57 100644 --- a/mapper.js +++ b/mapper.js @@ -1,6 +1,9 @@ const axios = require('axios'); const stringSimilarity = require('string-similarity'); +const PROXY_PRO = process.env.PROXY_PRO || ""; +const PROXY_PRO2 = process.env.PROXY_PRO2; + // Common headers for API requests const getCommonHeaders = () => ({ 'Accept': 'application/json, text/plain, */*', @@ -13,7 +16,7 @@ const getCommonHeaders = () => ({ 'sec-fetch-dest': 'empty' }); -// Minimal GraphQL query for AniList (title, format, year only) +// Minimal GraphQL query for AniList const ANILIST_QUERY = ` query ($id: Int) { Media(id: $id, type: ANIME) { @@ -29,7 +32,7 @@ query ($id: Int) { } }`; -// Function to normalize title for comparison +// Normalize title function normalizeTitle(title) { if (!title) return ''; return title.toLowerCase() @@ -38,112 +41,103 @@ function normalizeTitle(title) { .trim(); } -// Function to get anime details from AniList +// 🔥 UPDATED FUNCTION WITH PROPER LOGGING async function getAniListDetails(anilistId) { + const targetUrl = 'https://graphql.anilist.co'; + try { + console.log("⚡ Trying DIRECT AniList request..."); + const response = await axios({ - url: 'https://graphql.anilist.co', + url: targetUrl, method: 'POST', data: { query: ANILIST_QUERY, - variables: { - id: parseInt(anilistId) - } + variables: { id: parseInt(anilistId) } + }, + timeout: 5000, + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'User-Agent': 'Mozilla/5.0' } }); if (!response.data?.data?.Media) { - throw new Error('Anime not found on AniList'); + throw new Error('Anime not found Direct'); } + console.log("✅ DIRECT REQUEST SUCCESS"); return response.data.data.Media; + } catch (error) { - console.error('Error fetching from AniList:', error.message); - throw new Error('Failed to fetch anime details from AniList'); + console.error("❌ DIRECT REQUEST FAILED"); + console.error("Status:", error.response?.status || "No response"); + console.error("Reason:", error.response?.data || error.message); + + // 🔁 Fallback to Proxy + try { + const proxyBase = PROXY_PRO2 || PROXY_PRO; + const sep = proxyBase.includes('?') ? '&' : '?'; + const proxyUrl = `${proxyBase}${sep}url=${encodeURIComponent(targetUrl)}`; + + console.log("🌐 Trying PROXY request..."); + console.log("Proxy URL:", proxyUrl); + + const response = await axios.post(proxyUrl, { + query: ANILIST_QUERY, + variables: { id: parseInt(anilistId) } + }, { + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'User-Agent': 'Mozilla/5.0' + }, + timeout: 10000 + }); + + if (!response.data?.data?.Media) { + throw new Error('Anime not found via Proxy'); + } + + console.log("✅ PROXY REQUEST SUCCESS"); + return response.data.data.Media; + + } catch (err2) { + console.error("❌ PROXY REQUEST ALSO FAILED"); + console.error("Status:", err2.response?.status || "No response"); + console.error("Reason:", err2.response?.data || err2.message); + + throw new Error('Failed to fetch anime details from AniList'); + } } } -// Function to search anime on anicrush +// Search anime on anicrush async function searchAnicrush(title) { - if (!title) { - throw new Error('Search title is required'); - } + const headers = getCommonHeaders(); - try { - const headers = getCommonHeaders(); - const response = await axios({ - method: 'GET', - url: 'https://api.anicrush.to/shared/v2/movie/list', - params: { - keyword: title, - page: 1, - limit: 24 - }, - headers - }); + const response = await axios({ + method: 'GET', + url: 'https://api.anicrush.to/shared/v2/movie/list', + params: { + keyword: title, + page: 1, + limit: 24 + }, + headers + }); - if (response.data?.status === false) { - throw new Error(response.data.message || 'Search failed'); - } - - return response.data; - } catch (error) { - if (error.response) { - console.error('Search API error:', error.response.data); - throw new Error(error.response.data.message || 'Search request failed'); - } else if (error.request) { - console.error('No response received:', error.request); - throw new Error('No response from search API'); - } else { - console.error('Search error:', error.message); - throw new Error('Failed to search anime'); - } - } + return response.data; } -// Function to get episode list from anicrush -async function getEpisodeList(movieId) { - if (!movieId) { - throw new Error('Movie ID is required'); - } - - try { - const headers = getCommonHeaders(); - const response = await axios({ - method: 'GET', - url: 'https://api.anicrush.to/shared/v2/episode/list', - params: { - _movieId: movieId - }, - headers - }); - - if (response.data?.status === false) { - throw new Error(response.data.message || 'Failed to fetch episode list'); - } - - return response.data; - } catch (error) { - if (error.response) { - console.error('Episode list API error:', error.response.data); - throw new Error(error.response.data.message || 'Episode list request failed'); - } else if (error.request) { - console.error('No response received:', error.request); - throw new Error('No response from episode list API'); - } else { - console.error('Episode list error:', error.message); - throw new Error('Failed to fetch episode list'); - } - } -} - -// Function to calculate similarity between titles using string-similarity library +// Similarity function calculateTitleSimilarity(title1, title2) { if (!title1 || !title2) return 0; return stringSimilarity.compareTwoStrings( - title1.toLowerCase(), + title1.toLowerCase(), title2.toLowerCase() - ) * 100; // Convert to percentage + ) * 100; } function extractSeasonNumber(title) { @@ -151,21 +145,15 @@ function extractSeasonNumber(title) { const lower = title.toLowerCase(); let match = lower.match(/(?:season|cour|part)\s*(\d{1,2})/i); - if (match && match[1]) { - const num = parseInt(match[1], 10); - if (!Number.isNaN(num)) return num; - } + if (match && match[1]) return parseInt(match[1], 10); match = lower.match(/(\d{1,2})\s*$/); - if (match && match[1]) { - const num = parseInt(match[1], 10); - if (!Number.isNaN(num) && num < 10) return num; - } + if (match && match[1] && parseInt(match[1], 10) < 10) + return parseInt(match[1], 10); return null; } -// Function to find best match between AniList and anicrush results function findBestMatch(anilistData, anicrushResults) { if (!anicrushResults?.result?.movies?.length) return null; @@ -176,141 +164,53 @@ function findBestMatch(anilistData, anicrushResults) { ...(anilistData.synonyms || []) ].filter(Boolean).map(normalizeTitle); - const primaryAniListTitle = normalizeTitle( - anilistData.title.romaji || anilistData.title.english || anilistData.title.native - ); - const anilistSeason = extractSeasonNumber(primaryAniListTitle); - let bestMatch = null; let highestScore = 0; - const formatTypeMap = { - TV: 'TV', - TV_SHORT: 'TV', - MOVIE: 'MOVIE', - SPECIAL: 'SPECIAL', - OVA: 'OVA', - ONA: 'ONA', - MUSIC: 'MUSIC' - }; - const expectedType = formatTypeMap[anilistData.format] || null; - for (const result of anicrushResults.result.movies) { - let typePenalty = 0; - if (expectedType && result.type && expectedType !== result.type) { - typePenalty = 15; // small penalty instead of skip - } - - const resultTitles = [result.name, result.name_english].filter(Boolean); - for (const aTitle of anilistTitles) { - for (const rTitle of resultTitles) { - const similarity = calculateTitleSimilarity(aTitle, rTitle); - let score = Math.max(0, similarity - typePenalty); - - if (anilistSeason !== null) { - const resultSeason = extractSeasonNumber(rTitle); - if (resultSeason !== null) { - if (resultSeason === anilistSeason) { - score += 25; - } else { - score -= 30; - } - } - } - - if (score > highestScore) { - highestScore = score; - bestMatch = result; - } + const similarity = calculateTitleSimilarity(aTitle, result.name); + if (similarity > highestScore) { + highestScore = similarity; + bestMatch = result; } } } - return highestScore >= 60 ? bestMatch : null; // Increased threshold to 60% for better accuracy + return highestScore >= 60 ? bestMatch : null; } -// Alias for compatibility -const findBestMatchFuzzy = findBestMatch; - -// Function to parse episode list response -function parseEpisodeList(episodeList) { - if (!episodeList?.result) return []; - - const episodes = []; - for (const [key, value] of Object.entries(episodeList.result)) { - if (Array.isArray(value)) { - value.forEach(ep => { - episodes.push({ - number: ep.number, - name: ep.name, - name_english: ep.name_english, - is_filler: ep.is_filler - }); - }); - } - } - return episodes.sort((a, b) => a.number - b.number); -} - -// Function to fetch ani.zip mappings -async function getAniZipMappings(anilistId) { - try { - const response = await axios({ - method: 'GET', - url: `https://api.ani.zip/mappings?anilist_id=${anilistId}`, - headers: { - 'Accept': 'application/json' - } - }); - - return response.data; - } catch (error) { - console.error('Error fetching ani.zip mappings:', error.message); - return null; - } -} - -// Main mapper function +// Main mapper async function mapAniListToAnicrush(anilistId) { - try { - const anilistData = await getAniListDetails(anilistId); + const anilistData = await getAniListDetails(anilistId); - const titlesToTry = [ - anilistData.title.romaji, - anilistData.title.english, - anilistData.title.native - ].filter(Boolean); + const titlesToTry = [ + anilistData.title.romaji, + anilistData.title.english, + anilistData.title.native + ].filter(Boolean); - let bestMatch = null; - for (const title of titlesToTry) { - const searchResults = await searchAnicrush(title); - bestMatch = findBestMatchFuzzy(anilistData, searchResults); - if (bestMatch) break; - } + let bestMatch = null; - if (!bestMatch) throw new Error('No matching anime found on anicrush'); - - return { - anilist_id: anilistId, - anicrush_id: bestMatch.id, - title: { - romaji: anilistData.title.romaji, - english: anilistData.title.english, - native: anilistData.title.native, - anicrush: bestMatch.name, - anicrush_english: bestMatch.name_english - }, - type: bestMatch.type, - year: anilistData.seasonYear - }; - } catch (error) { - console.error('Mapper error:', error.message); - throw error; + for (const title of titlesToTry) { + const searchResults = await searchAnicrush(title); + bestMatch = findBestMatch(anilistData, searchResults); + if (bestMatch) break; } + + if (!bestMatch) + throw new Error('No matching anime found on anicrush'); + + return { + anilist_id: anilistId, + anicrush_id: bestMatch.id, + title: bestMatch.name, + type: bestMatch.type, + year: anilistData.seasonYear + }; } - + module.exports = { mapAniListToAnicrush, getCommonHeaders -}; +};