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