diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b512c09 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/embedHandler.js b/embedHandler.js index d309b43..bf292f1 100644 --- a/embedHandler.js +++ b/embedHandler.js @@ -1,107 +1,126 @@ const axios = require("axios"); -const crypto = require("crypto"); +const cheerio = require("cheerio"); -const MEGACLOUD_URL = 'https://megacloud.blog'; +const MEGACLOUD_URL = "https://megacloud.blog"; const KEY_URL = "https://raw.githubusercontent.com/yogesh-hacker/MegacloudKeys/refs/heads/main/keys.json"; const DECODE_URL = "https://script.google.com/macros/s/AKfycbx-yHTwupis_JD0lNzoOnxYcEYeXmJZrg7JeMxYnEZnLBy5V0--UxEvP-y9txHyy1TX9Q/exec"; -const UA = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Mobile Safari/537.36"; +const UA = + "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Mobile Safari/537.36"; let cachedKey = null; +/** 🔹 Extract nonce from HTML */ function extractNonce(html) { - const match1 = html.match(/\b[a-zA-Z0-9]{48}\b/); - if (match1) return match1[0]; - - const match2 = html.match(/\b([a-zA-Z0-9]{16})\b.*?\b([a-zA-Z0-9]{16})\b.*?\b([a-zA-Z0-9]{16})\b/); - if (match2) return match2.groupValues ? match2.groupValues[1] + match2.groupValues[2] + match2.groupValues[3] : match2[1] + match2[2] + match2[3]; - + const match48 = html.match(/\b[a-zA-Z0-9]{48}\b/); + if (match48) return match48[0]; + + const match3x16 = html.match( + /\b([a-zA-Z0-9]{16})\b.*?\b([a-zA-Z0-9]{16})\b.*?\b([a-zA-Z0-9]{16})\b/ + ); + if (match3x16) return match3x16.slice(1, 4).join(""); + return null; } +/** 🔹 Cache + fetch decryption key */ async function fetchKey() { if (cachedKey) return cachedKey; - try { const { data } = await axios.get(KEY_URL, { headers: { "User-Agent": UA } }); cachedKey = data?.mega; + if (!cachedKey) throw new Error("Missing key in JSON"); return cachedKey; - } catch (error) { - console.error("Failed to fetch key:", error.message); + } catch (err) { + console.error("Failed to fetch key:", err.message); return null; } } +/** 🔹 Call remote Google Script to decrypt */ async function decryptWithGoogleScript(encryptedData, nonce, key) { - const fullUrl = `${DECODE_URL}?encrypted_data=${encodeURIComponent(encryptedData)}&nonce=${encodeURIComponent(nonce)}&secret=${encodeURIComponent(key)}`; - + const fullUrl = `${DECODE_URL}?encrypted_data=${encodeURIComponent( + typeof encryptedData === "string" ? encryptedData : JSON.stringify(encryptedData) + )}&nonce=${encodeURIComponent(nonce)}&secret=${encodeURIComponent(key)}`; + const { data } = await axios.get(fullUrl); - const match = data.match(/"file":"(.*?)"/); - if (!match) throw new Error("Video URL not found in decrypted response"); - - return match[1]; + const match = data.match(/"file":"(.*?)"/); + if (!match) throw new Error("Decrypted file not found"); + return match[1].replace(/\\\//g, "/"); } +/** 🔹 Main extractor */ async function extract(embedUrl) { try { const headers = { - "Accept": "*/*", + Accept: "*/*", "X-Requested-With": "XMLHttpRequest", - "Referer": MEGACLOUD_URL, - "User-Agent": UA + // Use domain as referer for proper CORS + Referer: `${new URL(embedUrl).origin}/`, + "User-Agent": UA, }; - const id = embedUrl.split("/").pop().split("?")[0]; - + // Fetch embed HTML const { data: html } = await axios.get(embedUrl, { headers }); + const $ = cheerio.load(html); + + // Extract file ID + const fileId = $("#megacloud-player").attr("data-id"); + if (!fileId) throw new Error("data-id not found"); + + // Extract nonce const nonce = extractNonce(html); - - if (!nonce) { - throw new Error("Could not extract nonce from embed page"); - } + if (!nonce) throw new Error("Nonce not found"); - const apiUrl = `${MEGACLOUD_URL}/embed-2/v3/e-1/getSources?id=${id}&_k=${nonce}`; + // Build API URL dynamically + const urlParts = new URL(embedUrl).pathname.split("/").filter(Boolean); + // Always use v3 endpoint as it returns correct JSON even when embed URL contains v2 + const apiUrl = `${MEGACLOUD_URL}/embed-2/v3/e-1/getSources?id=${fileId}&_k=${nonce}`; + + // Fetch JSON const { data: response } = await axios.get(apiUrl, { headers }); - - if (!response || !response.sources) { - throw new Error("No sources found in API response"); - } + if (!response || !response.sources) + throw new Error("No sources in API response"); - const encoded = response.sources; + let sources = response.sources; + const tracks = response.tracks || []; let m3u8Url; - if (encoded.includes(".m3u8")) { - m3u8Url = encoded; - } else { + // 🔸 If sources is array and contains HLS directly + if (Array.isArray(sources) && sources.length && sources[0].file) { + m3u8Url = sources[0].file; + } + // 🔸 Encrypted string or empty array + else { const key = await fetchKey(); - if (!key) { - throw new Error("Could not fetch decryption key"); - } + if (!key) throw new Error("No key available"); - m3u8Url = await decryptWithGoogleScript(encoded, nonce, key); + m3u8Url = await decryptWithGoogleScript(sources, nonce, key); } return { sources: [{ file: m3u8Url, type: "hls" }], - tracks: response.tracks || [], + tracks: tracks.filter((t) => + ["captions", "subtitles"].includes((t.kind || "").toLowerCase()) + ), t: response.t || 0, server: response.server || 0, intro: response.intro || null, - outro: response.outro || null + outro: response.outro || null, }; - - } catch (error) { - console.error("MegaCloud extraction failed:", error.message); + } catch (err) { + console.error("❌ MegaCloud extraction failed:", err.message); return { sources: [], tracks: [], t: 0, - server: 0 + server: 0, }; } } -async function handleEmbed(embedUrl, referrer) { +/** 🔹 Wrapper */ +async function handleEmbed(embedUrl) { return await extract(embedUrl); } -module.exports = { extract, handleEmbed }; +module.exports = { extract, handleEmbed }; diff --git a/index.js b/index.js index 4c0279e..5649ac7 100644 --- a/index.js +++ b/index.js @@ -284,13 +284,13 @@ app.get('/api/anime/:anilistId/:episodeNum', async (req, res) => { // Find the specific episode data const episodeNumber = parseInt(episodeNum) || 1; - const episodeData = mappedData.episodes.find(ep => ep.number === episodeNumber) || {}; + const episodeData = (mappedData.episodes || []).find(ep => ep.number === episodeNumber) || {}; // Add metadata from the mapped data const response = { ...hlsData, metadata: { - title: mappedData.titles?.english || mappedData.titles?.romaji, + title: mappedData.title?.english || mappedData.title?.romaji, anilistId: parseInt(anilistId), movieId: movieId, episode: episodeNumber, diff --git a/mapper.js b/mapper.js index bb348a4..fb1d706 100644 --- a/mapper.js +++ b/mapper.js @@ -12,28 +12,20 @@ const getCommonHeaders = () => ({ 'sec-fetch-dest': 'empty' }); -// GraphQL query for AniList with synonyms +// Minimal GraphQL query for AniList (title, format, year only) const ANILIST_QUERY = ` query ($id: Int) { - Media(id: $id, type: ANIME) { - id - title { - romaji - english - native - } - synonyms - episodes - format - status - countryOfOrigin - seasonYear - description - genres - tags { - name - } + Media(id: $id, type: ANIME) { + id + title { + romaji + english + native } + synonyms + format + seasonYear + } }`; // Function to calculate string similarity using Levenshtein distance @@ -43,376 +35,285 @@ function calculateLevenshteinSimilarity(str1, str2) { str2 = str2.toLowerCase(); const matrix = Array(str2.length + 1).fill(null) - .map(() => Array(str1.length + 1).fill(null)); - - for (let i = 0; i <= str1.length; i++) matrix[0][i] = i; - for (let j = 0; j <= str2.length; j++) matrix[j][0] = j; - - for (let j = 1; j <= str2.length; j++) { - for (let i = 1; i <= str1.length; i++) { - const indicator = str1[i - 1] === str2[j - 1] ? 0 : 1; - matrix[j][i] = Math.min( - matrix[j][i - 1] + 1, - matrix[j - 1][i] + 1, - matrix[j - 1][i - 1] + indicator - ); - } - } - - const maxLength = Math.max(str1.length, str2.length); - if (maxLength === 0) return 100; - return ((maxLength - matrix[str2.length][str1.length]) / maxLength) * 100; -} - -// Function to calculate word-based similarity -function calculateWordSimilarity(str1, str2) { - if (!str1 || !str2) return 0; - - const words1 = str1.toLowerCase().split(/\s+/).filter(Boolean); - const words2 = str2.toLowerCase().split(/\s+/).filter(Boolean); - - const commonWords = words1.filter(word => words2.includes(word)); - const totalUniqueWords = new Set([...words1, ...words2]).size; - - return (commonWords.length / totalUniqueWords) * 100; -} - -// Function to normalize title for comparison -function normalizeTitle(title) { - if (!title) return ''; - return title.toLowerCase() - .replace(/[^a-z0-9\u3000-\u303f\u3040-\u309f\u30a0-\u30ff\u4e00-\u9faf\uff00-\uff9f]/g, ' ') - .replace(/\s+/g, ' ') - .trim(); -} - -// Function to get anime details from AniList -async function getAniListDetails(anilistId) { - try { - const response = await axios({ - url: 'https://graphql.anilist.co', - method: 'POST', - data: { - query: ANILIST_QUERY, - variables: { - id: parseInt(anilistId) - } - } - }); - - if (!response.data?.data?.Media) { - throw new Error('Anime not found on AniList'); - } - - 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'); - } -} - -// Function to search anime on anicrush -async function searchAnicrush(title) { - if (!title) { - throw new Error('Search title is required'); - } - - 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 - }); - - 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'); - } - } -} - -// 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 overall similarity between titles -function calculateTitleSimilarity(title1, title2) { - const levenshteinSim = calculateLevenshteinSimilarity(title1, title2); - const wordSim = calculateWordSimilarity(title1, title2); - - // Weight the similarities (favoring word-based matching for titles) - return (levenshteinSim * 0.4) + (wordSim * 0.6); -} - -// Function to find best match between AniList and anicrush results -function findBestMatch(anilistData, anicrushResults) { - if (!anicrushResults?.result?.movies || !anicrushResults.result.movies.length) { - return null; - } - - // Prepare all possible titles from AniList - const titles = [ - anilistData.title.romaji, - anilistData.title.english, - anilistData.title.native, - ...(anilistData.synonyms || []) - ].filter(Boolean); - - let bestMatch = null; - let highestSimilarity = 0; - - // Map AniList format to Anicrush type - const formatTypeMap = { - 'TV': 'TV', - 'TV_SHORT': 'TV', - 'MOVIE': 'MOVIE', - 'SPECIAL': 'SPECIAL', - 'OVA': 'OVA', - 'ONA': 'ONA', - 'MUSIC': 'MUSIC' - }; - - const expectedType = formatTypeMap[anilistData.format] || null; - - // Check each result from anicrush - for (const result of anicrushResults.result.movies) { - const resultTitles = [ - result.name, - result.name_english - ].filter(Boolean); - - for (const resultTitle of resultTitles) { - // Try each possible title combination - for (const title of titles) { - // Remove common words that might cause false matches - const cleanTitle1 = normalizeTitle(title).replace(/\b(season|2nd|3rd|4th|5th|6th|7th|8th|9th|10th|11th|12th|13th|14th|15th|16th|17th|18th|19th|20th|21st|22nd|23rd|24th|25th|26th|27th|28th|29th|30th|31st|32nd|33rd|34th|35th|36th|37th|38th|39th|40th|41st|42nd|43rd|44th|45th|46th|47th|48th|49th|50th)\b/gi, ''); - const cleanTitle2 = normalizeTitle(resultTitle).replace(/\b(season|2nd|3rd|4th|5th|6th|7th|8th|9th|10th|11th|12th|13th|14th|15th|16th|17th|18th|19th|20th|21st|22nd|23rd|24th|25th|26th|27th|28th|29th|30th|31st|32nd|33rd|34th|35th|36th|37th|38th|39th|40th|41st|42nd|43rd|44th|45th|46th|47th|48th|49th|50th)\b/gi, ''); - - const similarity = calculateTitleSimilarity(cleanTitle1, cleanTitle2); - - // Add bonus for year match - let currentSimilarity = similarity; - if (anilistData.seasonYear && result.aired_from) { - const yearMatch = result.aired_from.includes(anilistData.seasonYear.toString()); - if (yearMatch) currentSimilarity += 10; // Reduced from 15 to 10 - } - - // Add bonus for type match - if (expectedType && result.type) { - if (expectedType === result.type) { - currentSimilarity += 25; // Increased from 10 to 25 - } else { - currentSimilarity -= 30; // Add significant penalty for type mismatch - } - } - - // Add penalty for episode count mismatch (if available) - if (anilistData.episodes && result.episodes_count) { - const episodeDiff = Math.abs(anilistData.episodes - result.episodes_count); - if (episodeDiff > 2) { // Allow small differences - currentSimilarity -= (episodeDiff * 2); - } - } - - // Add penalty for length difference - const lengthDiff = Math.abs(cleanTitle1.length - cleanTitle2.length); - if (lengthDiff > 10) { - currentSimilarity -= (lengthDiff * 0.5); - } - - // Add penalty for completely different first words - const firstWord1 = cleanTitle1.split(' ')[0]; - const firstWord2 = cleanTitle2.split(' ')[0]; - if (firstWord1 && firstWord2 && firstWord1 !== firstWord2) { - currentSimilarity -= 20; - } - - if (currentSimilarity > highestSimilarity) { - highestSimilarity = currentSimilarity; - bestMatch = result; - } - } - } - } - - // Only return a match if similarity is above threshold - console.log(`Best match found with similarity: ${highestSimilarity}%`); - return highestSimilarity >= 70 ? bestMatch : null; // Increased threshold from 60 to 70 -} - -// 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 -async function mapAniListToAnicrush(anilistId) { - try { - // Get AniList details - const anilistData = await getAniListDetails(anilistId); - - // Get ani.zip mappings for episode images - const aniZipData = await getAniZipMappings(anilistId); - - // Try all possible titles for search - const titlesToTry = [ - anilistData.title.romaji, - anilistData.title.english, - anilistData.title.native, - ...(anilistData.synonyms || []) - ].filter(Boolean); - - let searchResults = null; - let bestMatch = null; - - // Try each title until we find a match - for (const title of titlesToTry) { - console.log(`Trying title: ${title}`); - searchResults = await searchAnicrush(title); - bestMatch = findBestMatch(anilistData, searchResults); - if (bestMatch) break; - } - - if (!bestMatch) { - throw new Error('No matching anime found on anicrush'); - } - - // Get episode list - const episodeList = await getEpisodeList(bestMatch.id); - const parsedEpisodes = parseEpisodeList(episodeList); - - // Create episode mapping with images from ani.zip - const episodes = parsedEpisodes.map(ep => { - const aniZipEpisode = aniZipData?.episodes?.[ep.number] || {}; - return { - number: ep.number, - name: ep.name, - name_english: ep.name_english, - is_filler: ep.is_filler, - id: `${bestMatch.id}?episode=${ep.number}`, - image: aniZipEpisode.image || null, - overview: aniZipEpisode.overview || null, - airDate: aniZipEpisode.airDate || null, - runtime: aniZipEpisode.runtime || null - }; - }); - - return { - anilist_id: anilistId, - anicrush_id: bestMatch.id, - titles: { - romaji: anilistData.title.romaji, - english: anilistData.title.english, - native: anilistData.title.native, - synonyms: anilistData.synonyms, - anicrush: bestMatch.name, - anicrush_english: bestMatch.name_english, - additional: aniZipData?.titles || {} - }, - type: bestMatch.type, - total_episodes: episodes.length, - episodes: episodes, - format: anilistData.format, - status: anilistData.status, - mal_score: bestMatch.mal_score, - genres: bestMatch.genres, - country_of_origin: anilistData.countryOfOrigin, - year: anilistData.seasonYear, - description: anilistData.description, - images: aniZipData?.images || [] - }; - - } catch (error) { - console.error('Mapper error:', error.message); - throw error; - } -} - -module.exports = { - mapAniListToAnicrush, - getCommonHeaders, - getAniZipMappings -}; + .map(() => Array(str1.length + 1).fill(null)); + + for (let i = 0; i <= str1.length; i++) matrix[0][i] = i; + for (let j = 0; j <= str2.length; j++) matrix[j][0] = j; + + for (let j = 1; j <= str2.length; j++) { + for (let i = 1; i <= str1.length; i++) { + const indicator = str1[i - 1] === str2[j - 1] ? 0 : 1; + matrix[j][i] = Math.min( + matrix[j][i - 1] + 1, + matrix[j - 1][i] + 1, + matrix[j - 1][i - 1] + indicator + ); + } + } + + const maxLength = Math.max(str1.length, str2.length); + if (maxLength === 0) return 100; + return ((maxLength - matrix[str2.length][str1.length]) / maxLength) * 100; +} + +// Function to calculate word-based similarity +function calculateWordSimilarity(str1, str2) { + if (!str1 || !str2) return 0; + + const words1 = str1.toLowerCase().split(/\s+/).filter(Boolean); + const words2 = str2.toLowerCase().split(/\s+/).filter(Boolean); + + const commonWords = words1.filter(word => words2.includes(word)); + const totalUniqueWords = new Set([...words1, ...words2]).size; + + return (commonWords.length / totalUniqueWords) * 100; +} + +// Function to normalize title for comparison +function normalizeTitle(title) { + if (!title) return ''; + return title.toLowerCase() + .replace(/[^a-z0-9\u3000-\u303f\u3040-\u309f\u30a0-\u30ff\u4e00-\u9faf\uff00-\uff9f]/g, ' ') + .replace(/\s+/g, ' ') + .trim(); +} + +// Function to get anime details from AniList +async function getAniListDetails(anilistId) { + try { + const response = await axios({ + url: 'https://graphql.anilist.co', + method: 'POST', + data: { + query: ANILIST_QUERY, + variables: { + id: parseInt(anilistId) + } + } + }); + + if (!response.data?.data?.Media) { + throw new Error('Anime not found on AniList'); + } + + 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'); + } +} + +// Function to search anime on anicrush +async function searchAnicrush(title) { + if (!title) { + throw new Error('Search title is required'); + } + + 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 + }); + + 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'); + } + } +} + +// 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 overall similarity between titles +function calculateTitleSimilarity(title1, title2) { + const levenshteinSim = calculateLevenshteinSimilarity(title1, title2); + const wordSim = calculateWordSimilarity(title1, title2); + + // Weight the similarities (favoring word-based matching for titles) + return (levenshteinSim * 0.4) + (wordSim * 0.6); +} + +// Function to find best match between AniList and anicrush results +function findBestMatch(anilistData, anicrushResults) { + if (!anicrushResults?.result?.movies?.length) return null; + + const anilistTitles = [ + anilistData.title.romaji, + anilistData.title.english, + anilistData.title.native, + ...(anilistData.synonyms || []) + ].filter(Boolean).map(normalizeTitle); + + 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).map(normalizeTitle); + + for (const aTitle of anilistTitles) { + for (const rTitle of resultTitles) { + const score = calculateTitleSimilarity(aTitle, rTitle); + if (score > highestScore) { + highestScore = score; + bestMatch = result; + } + } + } + } + + return highestScore >= 25 ? bestMatch : null; // accept if 25%+ similarity +} + +// 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 +async function mapAniListToAnicrush(anilistId) { + try { + const anilistData = await getAniListDetails(anilistId); + + 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; + } + + 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; + } +} + +module.exports = { + mapAniListToAnicrush, + getCommonHeaders +}; diff --git a/package-lock.json b/package-lock.json index bccbd3e..dffa399 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "dependencies": { "axios": "^1.6.2", + "cheerio": "^1.1.2", "cors": "^2.8.5", "crypto": "^1.0.1", "dotenv": "^16.3.1", @@ -115,6 +116,12 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -177,6 +184,48 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/cheerio": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.1.2.tgz", + "integrity": "sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.0.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.12.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -277,6 +326,34 @@ "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.", "license": "ISC" }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -314,6 +391,61 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dotenv": { "version": "16.4.7", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", @@ -355,6 +487,43 @@ "node": ">= 0.8" } }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/encoding-sniffer/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -680,6 +849,37 @@ "node": ">= 0.4" } }, + "node_modules/htmlparser2": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", + "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.1", + "entities": "^6.0.0" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -937,6 +1137,18 @@ "node": ">=0.10.0" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -970,6 +1182,55 @@ "node": ">= 0.8" } }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -1334,6 +1595,15 @@ "dev": true, "license": "MIT" }, + "node_modules/undici": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz", + "integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -1360,6 +1630,39 @@ "engines": { "node": ">= 0.8" } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "engines": { + "node": ">=18" + } } } } diff --git a/package.json b/package.json index ffce7f6..c5565c8 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ }, "dependencies": { "axios": "^1.6.2", + "cheerio": "^1.1.2", "cors": "^2.8.5", "crypto": "^1.0.1", "dotenv": "^16.3.1",