mirror of
https://github.com/shafat-96/anicrush-api.git
synced 2026-04-17 15:51:44 +00:00
Add files via upload
This commit is contained in:
127
README.md
Normal file
127
README.md
Normal file
@@ -0,0 +1,127 @@
|
||||
# Anime Sources API
|
||||
|
||||
A simple API wrapper for fetching anime sources from anicrush.to.
|
||||
|
||||
## Setup
|
||||
|
||||
1. Install dependencies:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
2. Start the server:
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
For development with auto-reload:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Map AniList ID to Anicrush
|
||||
|
||||
```
|
||||
GET /api/mapper/{anilistId}
|
||||
```
|
||||
|
||||
Maps an AniList ID to anicrush.to anime ID and episode information.
|
||||
|
||||
Example Request:
|
||||
```
|
||||
GET http://localhost:3000/api/mapper/21
|
||||
```
|
||||
|
||||
Example Response:
|
||||
```json
|
||||
{
|
||||
"anilist_id": "21",
|
||||
"anicrush_id": "vRPjMA",
|
||||
"titles": {
|
||||
"romaji": "One Piece",
|
||||
"english": "One Piece",
|
||||
"native": "ワンピース",
|
||||
"anicrush": "One Piece"
|
||||
},
|
||||
"total_episodes": 1000,
|
||||
"episodes": [
|
||||
{
|
||||
"number": 1,
|
||||
"id": "vRPjMA&episode=1"
|
||||
},
|
||||
// ... more episodes
|
||||
],
|
||||
"format": "TV",
|
||||
"status": "RELEASING"
|
||||
}
|
||||
```
|
||||
|
||||
### Search Anime
|
||||
|
||||
```
|
||||
GET /api/anime/search
|
||||
```
|
||||
|
||||
Query Parameters:
|
||||
- `keyword` (required): Search term
|
||||
- `page` (optional): Page number (default: 1)
|
||||
- `limit` (optional): Results per page (default: 24)
|
||||
|
||||
### Get Episode List
|
||||
|
||||
```
|
||||
GET /api/anime/episodes
|
||||
```
|
||||
|
||||
Query Parameters:
|
||||
- `movieId` (required): The ID of the movie/anime
|
||||
|
||||
### Get Servers
|
||||
|
||||
```
|
||||
GET /api/anime/servers
|
||||
```
|
||||
|
||||
Query Parameters:
|
||||
- `movieId` (required): The ID of the movie/anime
|
||||
- `episode` (optional): Episode number (default: 1)
|
||||
|
||||
### Get Sources
|
||||
|
||||
```
|
||||
GET /api/anime/sources
|
||||
```
|
||||
|
||||
Query Parameters:
|
||||
- `movieId` (required): The ID of the movie/anime (e.g., "vRPjMA")
|
||||
- `episode` (optional): Episode number (default: 1)
|
||||
- `server` (optional): Server number (default: 4)
|
||||
- `subOrDub` (optional): "sub" or "dub" (default: "sub")
|
||||
|
||||
Example Request:
|
||||
```
|
||||
GET http://localhost:3000/api/anime/sources?movieId=vRPjMA&episode=1&server=4&subOrDub=sub
|
||||
```
|
||||
|
||||
### Health Check
|
||||
|
||||
```
|
||||
GET /health
|
||||
```
|
||||
|
||||
Returns the API status.
|
||||
|
||||
## Error Handling
|
||||
|
||||
The API will return appropriate error messages with corresponding HTTP status codes:
|
||||
- 400: Bad Request (missing required parameters)
|
||||
- 404: Not Found (anime or episode not found)
|
||||
- 500: Internal Server Error (server-side issues)
|
||||
|
||||
## Notes
|
||||
|
||||
- The API includes necessary headers for authentication
|
||||
- CORS is enabled for cross-origin requests
|
||||
- The server runs on port 3000 by default (can be changed via PORT environment variable)
|
||||
110
embedHandler.js
Normal file
110
embedHandler.js
Normal file
@@ -0,0 +1,110 @@
|
||||
const { spawn } = require('child_process');
|
||||
const path = require('path');
|
||||
const fs = require('fs').promises;
|
||||
|
||||
class EmbedSource {
|
||||
constructor(file, sourceType) {
|
||||
this.file = file;
|
||||
this.type = sourceType;
|
||||
}
|
||||
}
|
||||
|
||||
class Track {
|
||||
constructor(file, label, kind, isDefault = false) {
|
||||
this.file = file;
|
||||
this.label = label;
|
||||
this.kind = kind;
|
||||
if (isDefault) {
|
||||
this.default = isDefault;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class EmbedSources {
|
||||
constructor(sources = [], tracks = [], t = 0, server = 1, intro = null, outro = null) {
|
||||
this.sources = sources;
|
||||
this.tracks = tracks;
|
||||
this.t = t;
|
||||
this.server = server;
|
||||
if (intro) this.intro = intro;
|
||||
if (outro) this.outro = outro;
|
||||
}
|
||||
}
|
||||
|
||||
const findRabbitScript = async () => {
|
||||
const possiblePaths = [
|
||||
path.join(__dirname, 'sources', 'rabbit.ts'),
|
||||
path.join(__dirname, 'sources', 'rabbit.js'),
|
||||
path.join(__dirname, 'rabbit.js'),
|
||||
path.join(process.cwd(), 'rabbit.js')
|
||||
];
|
||||
|
||||
for (const p of possiblePaths) {
|
||||
try {
|
||||
await fs.access(p);
|
||||
return p;
|
||||
} catch (error) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
throw new Error('rabbit.js not found in any expected locations');
|
||||
};
|
||||
|
||||
const handleEmbed = async (embedUrl, referrer) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const rabbitPath = await findRabbitScript();
|
||||
const childProcess = spawn('node', [
|
||||
rabbitPath,
|
||||
`--embed-url=${embedUrl}`,
|
||||
`--referrer=${referrer}`
|
||||
]);
|
||||
|
||||
let outputData = '';
|
||||
let errorData = '';
|
||||
|
||||
childProcess.stdout.on('data', (data) => {
|
||||
outputData += data.toString();
|
||||
});
|
||||
|
||||
childProcess.stderr.on('data', (data) => {
|
||||
errorData += data.toString();
|
||||
});
|
||||
|
||||
childProcess.on('close', (code) => {
|
||||
if (code !== 0) {
|
||||
reject(new Error(`Process exited with code ${code}: ${errorData}`));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedOutput = JSON.parse(outputData.trim());
|
||||
const embedSources = new EmbedSources(
|
||||
parsedOutput.sources.map(s => new EmbedSource(s.file, s.type)),
|
||||
parsedOutput.tracks.map(t => new Track(t.file, t.label, t.kind, t.default)),
|
||||
parsedOutput.t,
|
||||
parsedOutput.server,
|
||||
parsedOutput.intro,
|
||||
parsedOutput.outro
|
||||
);
|
||||
resolve(embedSources);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
childProcess.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
handleEmbed,
|
||||
EmbedSource,
|
||||
Track,
|
||||
EmbedSources
|
||||
};
|
||||
43
hls.js
Normal file
43
hls.js
Normal file
@@ -0,0 +1,43 @@
|
||||
const axios = require('axios');
|
||||
const { getCommonHeaders } = require('./mapper');
|
||||
const { handleEmbed } = require('./embedHandler');
|
||||
|
||||
// Function to get HLS link
|
||||
async function getHlsLink(embedUrl) {
|
||||
try {
|
||||
if (!embedUrl) {
|
||||
throw new Error('Embed URL is required');
|
||||
}
|
||||
|
||||
// Use rabbit.js to decode the embed URL and get sources
|
||||
const embedSources = await handleEmbed(embedUrl, 'https://anicrush.to');
|
||||
|
||||
if (!embedSources || !embedSources.sources || !embedSources.sources.length) {
|
||||
throw new Error('No sources found');
|
||||
}
|
||||
|
||||
// Return the complete response
|
||||
return {
|
||||
status: true,
|
||||
result: {
|
||||
sources: embedSources.sources,
|
||||
tracks: embedSources.tracks,
|
||||
t: embedSources.t,
|
||||
intro: embedSources.intro,
|
||||
outro: embedSources.outro,
|
||||
server: embedSources.server
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error getting HLS link:', error);
|
||||
return {
|
||||
status: false,
|
||||
error: error.message || 'Failed to get HLS link'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getHlsLink
|
||||
};
|
||||
255
index.js
Normal file
255
index.js
Normal file
@@ -0,0 +1,255 @@
|
||||
const express = require('express');
|
||||
const axios = require('axios');
|
||||
const cors = require('cors');
|
||||
const { mapAniListToAnicrush, getCommonHeaders } = require('./mapper');
|
||||
const { getHlsLink } = require('./hls');
|
||||
require('dotenv').config();
|
||||
|
||||
const app = express();
|
||||
|
||||
// Remove explicit port for Vercel
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
// CORS configuration for Vercel
|
||||
app.use(cors({
|
||||
origin: '*',
|
||||
methods: ['GET', 'POST', 'OPTIONS'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization']
|
||||
}));
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
// Endpoint to map AniList ID to anicrush
|
||||
app.get('/api/mapper/:anilistId', async (req, res) => {
|
||||
try {
|
||||
const { anilistId } = req.params;
|
||||
|
||||
if (!anilistId) {
|
||||
return res.status(400).json({ error: 'AniList ID is required' });
|
||||
}
|
||||
|
||||
const mappedData = await mapAniListToAnicrush(anilistId);
|
||||
res.json(mappedData);
|
||||
} catch (error) {
|
||||
console.error('Error in mapper:', error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to map AniList ID',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Endpoint to search for anime
|
||||
app.get('/api/anime/search', async (req, res) => {
|
||||
try {
|
||||
const { keyword, page = 1, limit = 24 } = req.query;
|
||||
|
||||
if (!keyword) {
|
||||
return res.status(400).json({ error: 'Search keyword is required' });
|
||||
}
|
||||
|
||||
const headers = getCommonHeaders();
|
||||
|
||||
const response = await axios({
|
||||
method: 'GET',
|
||||
url: `https://api.anicrush.to/shared/v2/movie/list`,
|
||||
params: {
|
||||
keyword,
|
||||
page,
|
||||
limit
|
||||
},
|
||||
headers
|
||||
});
|
||||
|
||||
res.json(response.data);
|
||||
} catch (error) {
|
||||
console.error('Error searching anime:', error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to search anime',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Endpoint to fetch episode list
|
||||
app.get('/api/anime/episodes', async (req, res) => {
|
||||
try {
|
||||
const { movieId } = req.query;
|
||||
|
||||
if (!movieId) {
|
||||
return res.status(400).json({ error: 'Movie ID is required' });
|
||||
}
|
||||
|
||||
const headers = getCommonHeaders();
|
||||
|
||||
const response = await axios({
|
||||
method: 'GET',
|
||||
url: `https://api.anicrush.to/shared/v2/episode/list`,
|
||||
params: {
|
||||
_movieId: movieId
|
||||
},
|
||||
headers
|
||||
});
|
||||
|
||||
res.json(response.data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching episode list:', error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to fetch episode list',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Endpoint to fetch servers for an episode
|
||||
app.get('/api/anime/servers', async (req, res) => {
|
||||
try {
|
||||
const { movieId, episode } = req.query;
|
||||
|
||||
if (!movieId) {
|
||||
return res.status(400).json({ error: 'Movie ID is required' });
|
||||
}
|
||||
|
||||
const headers = getCommonHeaders();
|
||||
|
||||
const response = await axios({
|
||||
method: 'GET',
|
||||
url: `https://api.anicrush.to/shared/v2/episode/servers`,
|
||||
params: {
|
||||
_movieId: movieId,
|
||||
ep: episode || 1
|
||||
},
|
||||
headers
|
||||
});
|
||||
|
||||
res.json(response.data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching servers:', error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to fetch servers',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Main endpoint to fetch anime sources
|
||||
app.get('/api/anime/sources', async (req, res) => {
|
||||
try {
|
||||
const { movieId, episode, server, subOrDub } = req.query;
|
||||
|
||||
if (!movieId) {
|
||||
return res.status(400).json({ error: 'Movie ID is required' });
|
||||
}
|
||||
|
||||
const headers = getCommonHeaders();
|
||||
|
||||
// First, check if the episode list exists
|
||||
const episodeListResponse = await axios({
|
||||
method: 'GET',
|
||||
url: `https://api.anicrush.to/shared/v2/episode/list`,
|
||||
params: {
|
||||
_movieId: movieId
|
||||
},
|
||||
headers
|
||||
});
|
||||
|
||||
if (!episodeListResponse.data || episodeListResponse.data.status === false) {
|
||||
return res.status(404).json({ error: 'Episode list not found' });
|
||||
}
|
||||
|
||||
// Then, get the servers for the episode
|
||||
const serversResponse = await axios({
|
||||
method: 'GET',
|
||||
url: `https://api.anicrush.to/shared/v2/episode/servers`,
|
||||
params: {
|
||||
_movieId: movieId,
|
||||
ep: episode || 1
|
||||
},
|
||||
headers
|
||||
});
|
||||
|
||||
if (!serversResponse.data || serversResponse.data.status === false) {
|
||||
return res.status(404).json({ error: 'Servers not found' });
|
||||
}
|
||||
|
||||
// Finally, get the sources
|
||||
const sourcesResponse = await axios({
|
||||
method: 'GET',
|
||||
url: `https://api.anicrush.to/shared/v2/episode/sources`,
|
||||
params: {
|
||||
_movieId: movieId,
|
||||
ep: episode || 1,
|
||||
sv: server || 4,
|
||||
sc: subOrDub || 'sub'
|
||||
},
|
||||
headers
|
||||
});
|
||||
|
||||
res.json(sourcesResponse.data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching anime sources:', error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to fetch anime sources',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Endpoint to get HLS link
|
||||
app.get('/api/anime/hls/:movieId', async (req, res) => {
|
||||
try {
|
||||
const { movieId } = req.params;
|
||||
const { episode = 1, server = 4, subOrDub = 'sub' } = req.query;
|
||||
|
||||
if (!movieId) {
|
||||
return res.status(400).json({ error: 'Movie ID is required' });
|
||||
}
|
||||
|
||||
const headers = getCommonHeaders();
|
||||
|
||||
// First get the embed link
|
||||
const embedResponse = await axios({
|
||||
method: 'GET',
|
||||
url: `https://api.anicrush.to/shared/v2/episode/sources`,
|
||||
params: {
|
||||
_movieId: movieId,
|
||||
ep: episode,
|
||||
sv: server,
|
||||
sc: subOrDub
|
||||
},
|
||||
headers
|
||||
});
|
||||
|
||||
if (!embedResponse.data || embedResponse.data.status === false) {
|
||||
return res.status(404).json({ error: 'Embed link not found' });
|
||||
}
|
||||
|
||||
const embedUrl = embedResponse.data.result.link;
|
||||
|
||||
// Get HLS link from embed URL
|
||||
const hlsData = await getHlsLink(embedUrl);
|
||||
res.json(hlsData);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching HLS link:', error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to fetch HLS link',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Health check endpoint
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({ status: 'OK' });
|
||||
});
|
||||
|
||||
// Only start the server if not in Vercel environment
|
||||
if (process.env.VERCEL !== '1') {
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server is running on port ${PORT}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Export the Express app for Vercel
|
||||
module.exports = app;
|
||||
345
mapper.js
Normal file
345
mapper.js
Normal file
@@ -0,0 +1,345 @@
|
||||
const axios = require('axios');
|
||||
|
||||
// Common headers for API requests
|
||||
const getCommonHeaders = () => ({
|
||||
'Accept': 'application/json, text/plain, */*',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36',
|
||||
'x-site': 'anicrush',
|
||||
'Referer': 'https://anicrush.to/',
|
||||
'Origin': 'https://anicrush.to',
|
||||
'sec-fetch-site': 'same-site',
|
||||
'sec-fetch-mode': 'cors',
|
||||
'sec-fetch-dest': 'empty'
|
||||
});
|
||||
|
||||
// GraphQL query for AniList with synonyms
|
||||
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
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
// Function to calculate string similarity using Levenshtein distance
|
||||
function calculateLevenshteinSimilarity(str1, str2) {
|
||||
if (!str1 || !str2) return 0;
|
||||
str1 = str1.toLowerCase();
|
||||
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;
|
||||
|
||||
// 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) {
|
||||
const similarity = calculateTitleSimilarity(
|
||||
normalizeTitle(title),
|
||||
normalizeTitle(resultTitle)
|
||||
);
|
||||
|
||||
// Add bonus for year match
|
||||
if (anilistData.seasonYear && result.aired_from) {
|
||||
const yearMatch = result.aired_from.includes(anilistData.seasonYear.toString());
|
||||
const currentSimilarity = similarity + (yearMatch ? 15 : 0);
|
||||
|
||||
if (currentSimilarity > highestSimilarity) {
|
||||
highestSimilarity = currentSimilarity;
|
||||
bestMatch = result;
|
||||
}
|
||||
} else if (similarity > highestSimilarity) {
|
||||
highestSimilarity = similarity;
|
||||
bestMatch = result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only return a match if similarity is above threshold
|
||||
console.log(`Best match found with similarity: ${highestSimilarity}%`);
|
||||
return highestSimilarity >= 60 ? bestMatch : null;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Main mapper function
|
||||
async function mapAniListToAnicrush(anilistId) {
|
||||
try {
|
||||
// Get AniList details
|
||||
const anilistData = await getAniListDetails(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
|
||||
const episodes = parsedEpisodes.map(ep => ({
|
||||
number: ep.number,
|
||||
name: ep.name,
|
||||
name_english: ep.name_english,
|
||||
is_filler: ep.is_filler,
|
||||
id: `${bestMatch.id}&episode=${ep.number}`
|
||||
}));
|
||||
|
||||
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
|
||||
},
|
||||
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
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Mapper error:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
mapAniListToAnicrush,
|
||||
getCommonHeaders
|
||||
};
|
||||
1354
package-lock.json
generated
Normal file
1354
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
package.json
Normal file
23
package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "anime-api",
|
||||
"version": "1.0.0",
|
||||
"description": "API for fetching anime sources",
|
||||
"main": "index.js",
|
||||
"engines": {
|
||||
"node": "18.x"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node index.js",
|
||||
"dev": "nodemon index.js",
|
||||
"vercel-build": "echo hello"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"axios": "^1.6.2",
|
||||
"dotenv": "^16.3.1",
|
||||
"cors": "^2.8.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.2"
|
||||
}
|
||||
}
|
||||
58292
sources/rabbit.js
Normal file
58292
sources/rabbit.js
Normal file
File diff suppressed because one or more lines are too long
15
vercel.json
Normal file
15
vercel.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"version": 2,
|
||||
"builds": [
|
||||
{
|
||||
"src": "index.js",
|
||||
"use": "@vercel/node"
|
||||
}
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"src": "/(.*)",
|
||||
"dest": "index.js"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user