mirror of
https://github.com/shafat-96/anilist-to-animepahe.git
synced 2026-04-17 15:51:45 +00:00
Add files via upload
This commit is contained in:
128
README.md
Normal file
128
README.md
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
# AniList to AnimePahe Mapper API
|
||||||
|
|
||||||
|
This is a Node.js API that maps anime data between AniList and AnimePahe. It provides endpoints to search for anime and retrieve detailed information including streaming sources.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Search anime across both AniList and AnimePahe
|
||||||
|
- Get detailed anime information from both sources
|
||||||
|
- Retrieve episode streaming sources from AnimePahe
|
||||||
|
- Title matching between services
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Clone the repository
|
||||||
|
2. Install dependencies:
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
3. Create a `.env` file with your configuration (see `.env.example`)
|
||||||
|
4. Start the server:
|
||||||
|
```bash
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
For development with auto-reload:
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### Search Anime
|
||||||
|
```
|
||||||
|
GET /api/search?query=<search_term>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Anime Details
|
||||||
|
```
|
||||||
|
GET /api/anime/:aniListId/:animePaheId
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Episode Sources
|
||||||
|
```
|
||||||
|
GET /api/episode/:episodeId
|
||||||
|
```
|
||||||
|
|
||||||
|
## Response Examples
|
||||||
|
|
||||||
|
### Search Response
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": {
|
||||||
|
"aniList": 123,
|
||||||
|
"animePahe": "456-anime-title"
|
||||||
|
},
|
||||||
|
"title": "Anime Title",
|
||||||
|
"alternativeTitles": {
|
||||||
|
"english": "English Title",
|
||||||
|
"native": "Native Title"
|
||||||
|
},
|
||||||
|
"coverImage": "https://example.com/image.jpg",
|
||||||
|
"episodes": {
|
||||||
|
"total": 12,
|
||||||
|
"available": 12
|
||||||
|
},
|
||||||
|
"status": "FINISHED"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Anime Details Response
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": {
|
||||||
|
"aniList": 123,
|
||||||
|
"animePahe": "456-anime-title"
|
||||||
|
},
|
||||||
|
"title": "Anime Title",
|
||||||
|
"alternativeTitles": {
|
||||||
|
"english": "English Title",
|
||||||
|
"native": "Native Title"
|
||||||
|
},
|
||||||
|
"coverImage": "https://example.com/image.jpg",
|
||||||
|
"description": "Anime description...",
|
||||||
|
"episodes": {
|
||||||
|
"total": 12,
|
||||||
|
"available": 12,
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"title": "Episode 1",
|
||||||
|
"episodeId": "session/episode-id",
|
||||||
|
"number": 1,
|
||||||
|
"image": "https://example.com/thumbnail.jpg"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"status": "FINISHED",
|
||||||
|
"genres": ["Action", "Adventure"],
|
||||||
|
"score": 8.5,
|
||||||
|
"season": {
|
||||||
|
"name": "SPRING",
|
||||||
|
"year": 2023
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Episode Sources Response
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"url": "https://example.com/video.mp4"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"multiSrc": [
|
||||||
|
{
|
||||||
|
"quality": "1080p",
|
||||||
|
"url": "https://example.com/video-1080p.mp4",
|
||||||
|
"referer": "https://kwik.cx"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Note
|
||||||
|
|
||||||
|
This API is for educational purposes only. Make sure to comply with the terms of service of both AniList and AnimePahe when using their services.
|
||||||
1553
package-lock.json
generated
Normal file
1553
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
35
package.json
Normal file
35
package.json
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "anilist-animepahe-mapper",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "A mapper and API between AniList and AnimePahe",
|
||||||
|
"main": "src/index.js",
|
||||||
|
"type": "module",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.x"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "node src/index.js",
|
||||||
|
"dev": "nodemon src/index.js",
|
||||||
|
"vercel-build": "echo hello"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"anime",
|
||||||
|
"anilist",
|
||||||
|
"animepahe",
|
||||||
|
"api"
|
||||||
|
],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^16.3.1",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"graphql": "^16.8.1",
|
||||||
|
"graphql-request": "^6.1.0",
|
||||||
|
"node-fetch": "^3.3.2",
|
||||||
|
"node-html-parser": "^7.0.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"nodemon": "^3.0.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
119
src/clients/anilist.js
Normal file
119
src/clients/anilist.js
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import { GraphQLClient } from 'graphql-request';
|
||||||
|
|
||||||
|
class AniListClient {
|
||||||
|
constructor() {
|
||||||
|
this.client = new GraphQLClient('https://graphql.anilist.co');
|
||||||
|
}
|
||||||
|
|
||||||
|
async searchAnime(query) {
|
||||||
|
const searchQuery = `
|
||||||
|
query ($search: String) {
|
||||||
|
Page(page: 1, perPage: 8) {
|
||||||
|
media(search: $search, type: ANIME) {
|
||||||
|
id
|
||||||
|
title {
|
||||||
|
romaji
|
||||||
|
english
|
||||||
|
native
|
||||||
|
}
|
||||||
|
coverImage {
|
||||||
|
large
|
||||||
|
}
|
||||||
|
episodes
|
||||||
|
status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this.client.request(searchQuery, { search: query });
|
||||||
|
return response.Page.media.map(anime => ({
|
||||||
|
id: anime.id,
|
||||||
|
title: anime.title.romaji || anime.title.english,
|
||||||
|
alternativeTitles: {
|
||||||
|
english: anime.title.english,
|
||||||
|
native: anime.title.native
|
||||||
|
},
|
||||||
|
coverImage: anime.coverImage.large,
|
||||||
|
episodes: anime.episodes,
|
||||||
|
status: anime.status
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('AniList search error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAnimeDetails(id) {
|
||||||
|
const detailsQuery = `
|
||||||
|
query ($id: Int) {
|
||||||
|
Media(id: $id, type: ANIME) {
|
||||||
|
id
|
||||||
|
title {
|
||||||
|
romaji
|
||||||
|
english
|
||||||
|
native
|
||||||
|
}
|
||||||
|
coverImage {
|
||||||
|
large
|
||||||
|
}
|
||||||
|
episodes
|
||||||
|
status
|
||||||
|
description
|
||||||
|
genres
|
||||||
|
averageScore
|
||||||
|
season
|
||||||
|
seasonYear
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this.client.request(detailsQuery, { id: parseInt(id) });
|
||||||
|
return {
|
||||||
|
id: response.Media.id,
|
||||||
|
title: response.Media.title.romaji || response.Media.title.english,
|
||||||
|
alternativeTitles: {
|
||||||
|
english: response.Media.title.english,
|
||||||
|
native: response.Media.title.native
|
||||||
|
},
|
||||||
|
coverImage: response.Media.coverImage.large,
|
||||||
|
episodes: response.Media.episodes,
|
||||||
|
status: response.Media.status,
|
||||||
|
description: response.Media.description,
|
||||||
|
genres: response.Media.genres,
|
||||||
|
score: response.Media.averageScore,
|
||||||
|
season: response.Media.season,
|
||||||
|
seasonYear: response.Media.seasonYear
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('AniList details error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAnimeTitle(id) {
|
||||||
|
const query = `
|
||||||
|
query ($id: Int) {
|
||||||
|
Media(id: $id, type: ANIME) {
|
||||||
|
title {
|
||||||
|
romaji
|
||||||
|
english
|
||||||
|
native
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this.client.request(query, { id: parseInt(id) });
|
||||||
|
return response.Media.title.romaji || response.Media.title.english || response.Media.title.native;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('AniList title fetch error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AniListClient;
|
||||||
214
src/clients/animepahe.js
Normal file
214
src/clients/animepahe.js
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
import fetch from 'node-fetch';
|
||||||
|
import { parse } from 'node-html-parser';
|
||||||
|
|
||||||
|
class AnimePaheClient {
|
||||||
|
constructor() {
|
||||||
|
this.baseUrl = 'https://animepahe.ru';
|
||||||
|
this.headers = {
|
||||||
|
'Cookie': '__ddg1_=;__ddg2_=;',
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async searchAnime(query) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this.baseUrl}/api?m=search&l=8&q=${encodeURIComponent(query)}`, {
|
||||||
|
headers: this.headers
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
return data.data.map(item => ({
|
||||||
|
name: item.title,
|
||||||
|
poster: item.poster,
|
||||||
|
id: `${item.id}-${item.title}`,
|
||||||
|
episodes: {
|
||||||
|
sub: item.episodes,
|
||||||
|
dub: '??'
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('AnimePahe search error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEpisodes(animeId) {
|
||||||
|
try {
|
||||||
|
const [id, title] = animeId.split('-');
|
||||||
|
const session = await this._getSession(title, id);
|
||||||
|
return this._fetchAllEpisodes(session);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('AnimePahe episodes error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _getSession(title, animeId) {
|
||||||
|
const response = await fetch(`${this.baseUrl}/api?m=search&q=${encodeURIComponent(title)}`, {
|
||||||
|
headers: this.headers
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
const session = data.data.find(
|
||||||
|
anime => anime.title === title
|
||||||
|
) || data.data[0];
|
||||||
|
|
||||||
|
return session.session;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _fetchAllEpisodes(session, page = 1, allEpisodes = []) {
|
||||||
|
const response = await fetch(
|
||||||
|
`${this.baseUrl}/api?m=release&id=${session}&sort=episode_desc&page=${page}`,
|
||||||
|
{ headers: this.headers }
|
||||||
|
);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const episodes = data.data.map(item => ({
|
||||||
|
title: `Episode ${item.episode}`,
|
||||||
|
episodeId: `${session}/${item.session}`,
|
||||||
|
number: item.episode,
|
||||||
|
image: item.snapshot
|
||||||
|
}));
|
||||||
|
|
||||||
|
allEpisodes.push(...episodes);
|
||||||
|
|
||||||
|
if (page < data.last_page) {
|
||||||
|
return this._fetchAllEpisodes(session, page + 1, allEpisodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch anime title
|
||||||
|
const animeResponse = await fetch(
|
||||||
|
`${this.baseUrl}/a/${data.data[0].anime_id}`,
|
||||||
|
{ headers: this.headers }
|
||||||
|
);
|
||||||
|
const html = await animeResponse.text();
|
||||||
|
const titleMatch = html.match(/<span class="title-wrapper">([^<]+)<\/span>/);
|
||||||
|
const animeTitle = titleMatch ? titleMatch[1].trim() : 'Could not fetch title';
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: animeTitle,
|
||||||
|
session: session,
|
||||||
|
totalEpisodes: data.total,
|
||||||
|
episodes: allEpisodes.reverse()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEpisodeSources(episodeUrl) {
|
||||||
|
try {
|
||||||
|
const [session, episodeSession] = episodeUrl.split('/');
|
||||||
|
const response = await fetch(`${this.baseUrl}/play/${session}/${episodeSession}`, {
|
||||||
|
headers: this.headers
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch episode: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = await response.text();
|
||||||
|
const root = parse(html);
|
||||||
|
const buttons = root.querySelectorAll('#resolutionMenu button');
|
||||||
|
|
||||||
|
const videoLinks = [];
|
||||||
|
for (const button of buttons) {
|
||||||
|
const quality = button.text.trim();
|
||||||
|
const kwikLink = button.getAttribute('data-src');
|
||||||
|
|
||||||
|
if (kwikLink) {
|
||||||
|
const videoUrl = await this._extractKwikVideo(kwikLink);
|
||||||
|
videoLinks.push({
|
||||||
|
quality: quality,
|
||||||
|
url: videoUrl,
|
||||||
|
referer: 'https://kwik.cx'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by quality
|
||||||
|
const qualityOrder = {
|
||||||
|
'1080p': 1,
|
||||||
|
'720p': 2,
|
||||||
|
'480p': 3,
|
||||||
|
'360p': 4
|
||||||
|
};
|
||||||
|
|
||||||
|
videoLinks.sort((a, b) => {
|
||||||
|
const qualityA = qualityOrder[a.quality.replace(/.*?(\d+p).*/, '$1')] || 999;
|
||||||
|
const qualityB = qualityOrder[b.quality.replace(/.*?(\d+p).*/, '$1')] || 999;
|
||||||
|
return qualityA - qualityB;
|
||||||
|
});
|
||||||
|
|
||||||
|
const sources = videoLinks.map(link => ({
|
||||||
|
url: link.url,
|
||||||
|
quality: link.quality,
|
||||||
|
referer: link.referer
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
sources: sources.length > 0 ? [{ url: sources[0].url }] : [],
|
||||||
|
multiSrc: sources
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting episode sources:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _extractKwikVideo(url) {
|
||||||
|
try {
|
||||||
|
// First request to get the Kwik page
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
...this.headers,
|
||||||
|
'Referer': this.baseUrl
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch Kwik page: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = await response.text();
|
||||||
|
|
||||||
|
// Extract and evaluate the obfuscated script using the correct regex
|
||||||
|
const scriptMatch = /(eval)(\(f.*?)(\n<\/script>)/s.exec(html);
|
||||||
|
if (!scriptMatch) {
|
||||||
|
throw new Error('Could not find obfuscated script');
|
||||||
|
}
|
||||||
|
|
||||||
|
const evalCode = scriptMatch[2].replace('eval', '');
|
||||||
|
const deobfuscated = eval(evalCode);
|
||||||
|
const m3u8Match = deobfuscated.match(/https.*?m3u8/);
|
||||||
|
|
||||||
|
if (m3u8Match && m3u8Match[0]) {
|
||||||
|
return m3u8Match[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error extracting Kwik video:', error);
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_organizeStreamLinks(links) {
|
||||||
|
const result = { sub: [], dub: [] };
|
||||||
|
const qualityOrder = ['1080p', '720p', '480p', '360p'];
|
||||||
|
|
||||||
|
for (const link of links) {
|
||||||
|
const isDub = link.quality.toLowerCase().includes('eng');
|
||||||
|
const targetList = isDub ? result.dub : result.sub;
|
||||||
|
targetList.push(link.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const type of ['sub', 'dub']) {
|
||||||
|
result[type].sort((a, b) => {
|
||||||
|
const qualityA = qualityOrder.indexOf(a.match(/\d+p/)?.[0] || '');
|
||||||
|
const qualityB = qualityOrder.indexOf(b.match(/\d+p/)?.[0] || '');
|
||||||
|
return qualityA - qualityB;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AnimePaheClient;
|
||||||
57
src/index.js
Normal file
57
src/index.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import cors from 'cors';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
import AnimeMapper from './mapper.js';
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
app.use(cors());
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
const mapper = new AnimeMapper();
|
||||||
|
|
||||||
|
// Health check endpoint
|
||||||
|
app.get('/', (req, res) => {
|
||||||
|
res.json({ status: 'ok', message: 'AniList AnimePahe Mapper API is running' });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get episodes from AniList ID
|
||||||
|
app.get('/api/:aniListId', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { aniListId } = req.params;
|
||||||
|
if (!aniListId) {
|
||||||
|
return res.status(400).json({ error: 'AniList ID is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const episodes = await mapper.getEpisodesFromAniListId(parseInt(aniListId));
|
||||||
|
res.json(episodes);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error.message);
|
||||||
|
res.status(error.message.includes('not found') ? 404 : 500)
|
||||||
|
.json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get episode sources
|
||||||
|
app.get('/api/episode/:episodeId(*)', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { episodeId } = req.params;
|
||||||
|
const sources = await mapper.getEpisodeSources(episodeId);
|
||||||
|
res.json(sources);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error.message);
|
||||||
|
res.status(500).json({ error: 'Failed to get episode sources' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// For local development
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
const port = process.env.PORT || 3000;
|
||||||
|
app.listen(port, () => {
|
||||||
|
console.log(`Server running on port ${port}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// For Vercel
|
||||||
|
export default app;
|
||||||
68
src/mapper.js
Normal file
68
src/mapper.js
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import AniListClient from './clients/anilist.js';
|
||||||
|
import AnimePaheClient from './clients/animepahe.js';
|
||||||
|
|
||||||
|
class AnimeMapper {
|
||||||
|
constructor() {
|
||||||
|
this.aniList = new AniListClient();
|
||||||
|
this.animePahe = new AnimePaheClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEpisodesFromAniListId(aniListId) {
|
||||||
|
try {
|
||||||
|
// Get anime title from AniList
|
||||||
|
const animeTitle = await this.aniList.getAnimeTitle(aniListId);
|
||||||
|
if (!animeTitle) {
|
||||||
|
throw new Error('Anime not found on AniList');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search AnimePahe for the anime
|
||||||
|
const searchResults = await this.animePahe.searchAnime(animeTitle);
|
||||||
|
if (!searchResults || searchResults.length === 0) {
|
||||||
|
throw new Error('Anime not found on AnimePahe');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the best match from search results
|
||||||
|
const bestMatch = this._findBestMatch(animeTitle, searchResults);
|
||||||
|
if (!bestMatch) {
|
||||||
|
throw new Error('No matching anime found on AnimePahe');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get episodes from AnimePahe
|
||||||
|
const episodes = await this.animePahe.getEpisodes(bestMatch.id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
aniListId: aniListId,
|
||||||
|
animePaheId: bestMatch.id,
|
||||||
|
title: episodes.title,
|
||||||
|
totalEpisodes: episodes.totalEpisodes,
|
||||||
|
episodes: episodes.episodes.map(ep => ({
|
||||||
|
number: ep.number,
|
||||||
|
id: ep.episodeId,
|
||||||
|
title: ep.title,
|
||||||
|
image: ep.image
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error mapping AniList to AnimePahe:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEpisodeSources(episodeId) {
|
||||||
|
try {
|
||||||
|
return await this.animePahe.getEpisodeSources(episodeId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting episode sources:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_findBestMatch(title, searchResults) {
|
||||||
|
const normalizedTitle = title.toLowerCase().trim();
|
||||||
|
return searchResults.find(result =>
|
||||||
|
result.name.toLowerCase().trim() === normalizedTitle
|
||||||
|
) || searchResults[0]; // Fallback to first result if no exact match
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AnimeMapper;
|
||||||
15
vercel.json
Normal file
15
vercel.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"version": 2,
|
||||||
|
"builds": [
|
||||||
|
{
|
||||||
|
"src": "src/index.js",
|
||||||
|
"use": "@vercel/node"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"src": "/(.*)",
|
||||||
|
"dest": "src/index.js"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user