Add files via upload

This commit is contained in:
shafat-96
2025-02-23 13:48:48 +06:00
committed by GitHub
commit 373f793300
8 changed files with 2189 additions and 0 deletions

128
README.md Normal file
View 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

File diff suppressed because it is too large Load Diff

35
package.json Normal file
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,15 @@
{
"version": 2,
"builds": [
{
"src": "src/index.js",
"use": "@vercel/node"
}
],
"routes": [
{
"src": "/(.*)",
"dest": "src/index.js"
}
]
}