mirror of
https://github.com/shafat-96/anicrush-api.git
synced 2026-04-17 15:51:44 +00:00
fix
This commit is contained in:
318
mapper.js
318
mapper.js
@@ -1,6 +1,9 @@
|
|||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
const stringSimilarity = require('string-similarity');
|
const stringSimilarity = require('string-similarity');
|
||||||
|
|
||||||
|
const PROXY_PRO = process.env.PROXY_PRO || "";
|
||||||
|
const PROXY_PRO2 = process.env.PROXY_PRO2;
|
||||||
|
|
||||||
// Common headers for API requests
|
// Common headers for API requests
|
||||||
const getCommonHeaders = () => ({
|
const getCommonHeaders = () => ({
|
||||||
'Accept': 'application/json, text/plain, */*',
|
'Accept': 'application/json, text/plain, */*',
|
||||||
@@ -13,7 +16,7 @@ const getCommonHeaders = () => ({
|
|||||||
'sec-fetch-dest': 'empty'
|
'sec-fetch-dest': 'empty'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Minimal GraphQL query for AniList (title, format, year only)
|
// Minimal GraphQL query for AniList
|
||||||
const ANILIST_QUERY = `
|
const ANILIST_QUERY = `
|
||||||
query ($id: Int) {
|
query ($id: Int) {
|
||||||
Media(id: $id, type: ANIME) {
|
Media(id: $id, type: ANIME) {
|
||||||
@@ -29,7 +32,7 @@ query ($id: Int) {
|
|||||||
}
|
}
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
// Function to normalize title for comparison
|
// Normalize title
|
||||||
function normalizeTitle(title) {
|
function normalizeTitle(title) {
|
||||||
if (!title) return '';
|
if (!title) return '';
|
||||||
return title.toLowerCase()
|
return title.toLowerCase()
|
||||||
@@ -38,112 +41,103 @@ function normalizeTitle(title) {
|
|||||||
.trim();
|
.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to get anime details from AniList
|
// 🔥 UPDATED FUNCTION WITH PROPER LOGGING
|
||||||
async function getAniListDetails(anilistId) {
|
async function getAniListDetails(anilistId) {
|
||||||
|
const targetUrl = 'https://graphql.anilist.co';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
console.log("⚡ Trying DIRECT AniList request...");
|
||||||
|
|
||||||
const response = await axios({
|
const response = await axios({
|
||||||
url: 'https://graphql.anilist.co',
|
url: targetUrl,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: {
|
data: {
|
||||||
query: ANILIST_QUERY,
|
query: ANILIST_QUERY,
|
||||||
variables: {
|
variables: { id: parseInt(anilistId) }
|
||||||
id: parseInt(anilistId)
|
},
|
||||||
}
|
timeout: 5000,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'User-Agent': 'Mozilla/5.0'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.data?.data?.Media) {
|
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;
|
return response.data.data.Media;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching from AniList:', error.message);
|
console.error("❌ DIRECT REQUEST FAILED");
|
||||||
throw new Error('Failed to fetch anime details from AniList');
|
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) {
|
async function searchAnicrush(title) {
|
||||||
if (!title) {
|
const headers = getCommonHeaders();
|
||||||
throw new Error('Search title is required');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
const response = await axios({
|
||||||
const headers = getCommonHeaders();
|
method: 'GET',
|
||||||
const response = await axios({
|
url: 'https://api.anicrush.to/shared/v2/movie/list',
|
||||||
method: 'GET',
|
params: {
|
||||||
url: 'https://api.anicrush.to/shared/v2/movie/list',
|
keyword: title,
|
||||||
params: {
|
page: 1,
|
||||||
keyword: title,
|
limit: 24
|
||||||
page: 1,
|
},
|
||||||
limit: 24
|
headers
|
||||||
},
|
});
|
||||||
headers
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.data?.status === false) {
|
return response.data;
|
||||||
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
|
// Similarity
|
||||||
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
|
|
||||||
function calculateTitleSimilarity(title1, title2) {
|
function calculateTitleSimilarity(title1, title2) {
|
||||||
if (!title1 || !title2) return 0;
|
if (!title1 || !title2) return 0;
|
||||||
return stringSimilarity.compareTwoStrings(
|
return stringSimilarity.compareTwoStrings(
|
||||||
title1.toLowerCase(),
|
title1.toLowerCase(),
|
||||||
title2.toLowerCase()
|
title2.toLowerCase()
|
||||||
) * 100; // Convert to percentage
|
) * 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractSeasonNumber(title) {
|
function extractSeasonNumber(title) {
|
||||||
@@ -151,21 +145,15 @@ function extractSeasonNumber(title) {
|
|||||||
const lower = title.toLowerCase();
|
const lower = title.toLowerCase();
|
||||||
|
|
||||||
let match = lower.match(/(?:season|cour|part)\s*(\d{1,2})/i);
|
let match = lower.match(/(?:season|cour|part)\s*(\d{1,2})/i);
|
||||||
if (match && match[1]) {
|
if (match && match[1]) return parseInt(match[1], 10);
|
||||||
const num = parseInt(match[1], 10);
|
|
||||||
if (!Number.isNaN(num)) return num;
|
|
||||||
}
|
|
||||||
|
|
||||||
match = lower.match(/(\d{1,2})\s*$/);
|
match = lower.match(/(\d{1,2})\s*$/);
|
||||||
if (match && match[1]) {
|
if (match && match[1] && parseInt(match[1], 10) < 10)
|
||||||
const num = parseInt(match[1], 10);
|
return parseInt(match[1], 10);
|
||||||
if (!Number.isNaN(num) && num < 10) return num;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to find best match between AniList and anicrush results
|
|
||||||
function findBestMatch(anilistData, anicrushResults) {
|
function findBestMatch(anilistData, anicrushResults) {
|
||||||
if (!anicrushResults?.result?.movies?.length) return null;
|
if (!anicrushResults?.result?.movies?.length) return null;
|
||||||
|
|
||||||
@@ -176,141 +164,53 @@ function findBestMatch(anilistData, anicrushResults) {
|
|||||||
...(anilistData.synonyms || [])
|
...(anilistData.synonyms || [])
|
||||||
].filter(Boolean).map(normalizeTitle);
|
].filter(Boolean).map(normalizeTitle);
|
||||||
|
|
||||||
const primaryAniListTitle = normalizeTitle(
|
|
||||||
anilistData.title.romaji || anilistData.title.english || anilistData.title.native
|
|
||||||
);
|
|
||||||
const anilistSeason = extractSeasonNumber(primaryAniListTitle);
|
|
||||||
|
|
||||||
let bestMatch = null;
|
let bestMatch = null;
|
||||||
let highestScore = 0;
|
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) {
|
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 aTitle of anilistTitles) {
|
||||||
for (const rTitle of resultTitles) {
|
const similarity = calculateTitleSimilarity(aTitle, result.name);
|
||||||
const similarity = calculateTitleSimilarity(aTitle, rTitle);
|
if (similarity > highestScore) {
|
||||||
let score = Math.max(0, similarity - typePenalty);
|
highestScore = similarity;
|
||||||
|
bestMatch = result;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return highestScore >= 60 ? bestMatch : null; // Increased threshold to 60% for better accuracy
|
return highestScore >= 60 ? bestMatch : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Alias for compatibility
|
// Main mapper
|
||||||
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) {
|
async function mapAniListToAnicrush(anilistId) {
|
||||||
try {
|
const anilistData = await getAniListDetails(anilistId);
|
||||||
const anilistData = await getAniListDetails(anilistId);
|
|
||||||
|
|
||||||
const titlesToTry = [
|
const titlesToTry = [
|
||||||
anilistData.title.romaji,
|
anilistData.title.romaji,
|
||||||
anilistData.title.english,
|
anilistData.title.english,
|
||||||
anilistData.title.native
|
anilistData.title.native
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
|
|
||||||
let bestMatch = null;
|
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');
|
for (const title of titlesToTry) {
|
||||||
|
const searchResults = await searchAnicrush(title);
|
||||||
return {
|
bestMatch = findBestMatch(anilistData, searchResults);
|
||||||
anilist_id: anilistId,
|
if (bestMatch) break;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 = {
|
module.exports = {
|
||||||
mapAniListToAnicrush,
|
mapAniListToAnicrush,
|
||||||
getCommonHeaders
|
getCommonHeaders
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user