Initial commit

This commit is contained in:
shafat420
2025-04-25 12:18:41 +06:00
commit 72eda2bf5c
18 changed files with 3865 additions and 0 deletions

287
README.md Normal file
View File

@@ -0,0 +1,287 @@
# Anilist to AnimePahe Mapper API
A specialized Node.js API that maps anime data between Anilist and streaming platforms (AnimePahe, HiAnime, AnimeKai) using advanced string similarity algorithms.
## Features
- Map Anilist anime IDs to AnimePahe, HiAnime, and AnimeKai content
- Advanced string similarity analysis with multiple algorithms
- Season/year matching for multi-season anime series
- Title variation detection across platforms
- Get streaming links with proper headers
- Support for both subbed and dubbed anime (AnimeKai)
## Installation
```bash
# Clone the repository
git clone https://github.com/yourusername/anilist-animepahe-mapper.git
cd anilist-animepahe-mapper
# Install dependencies
npm install
# Start the server
npm start
```
## API Endpoints
### AnimePahe Endpoints
#### Map Anilist ID to AnimePahe
```
GET /animepahe/map/:anilistId
```
Maps an Anilist anime ID to its corresponding AnimePahe content.
#### Get AnimePahe Streaming Links
```
GET /animepahe/sources/:session/:episodeId
```
Returns streaming links for a specific AnimePahe episode.
Alternative format:
```
GET /animepahe/sources/:id
```
### HiAnime Endpoints
#### Map Anilist ID to HiAnime
```
GET /hianime/:anilistId
```
Maps an Anilist anime ID to its corresponding HiAnime content.
#### Get HiAnime Servers
```
GET /hianime/servers/:episodeId
```
Get available servers for a HiAnime episode.
Parameters:
- `ep` (optional): Episode number
Example:
```
GET /hianime/servers/one-piece-100?ep=2142
```
#### Get HiAnime Streaming Sources
```
GET /hianime/sources/:episodeId
```
Returns streaming links for a specific HiAnime episode.
Parameters:
- `ep` (required): Episode number
- `server` (optional): Server name (default: vidstreaming)
- `category` (optional): Content type (sub, dub, raw) (default: sub)
Example:
```
GET /hianime/sources/one-piece-100?ep=2142&server=vidstreaming&category=sub
```
### AnimeKai Endpoints
#### Map Anilist ID to AnimeKai
```
GET /animekai/map/:anilistId
```
Maps an Anilist anime ID to its corresponding AnimeKai content.
#### Get AnimeKai Streaming Links
```
GET /animekai/sources/:episodeId
```
Returns streaming links for a specific AnimeKai episode.
Parameters:
- `server` (optional): Specify a streaming server
- `dub` (optional): Set to `true` or `1` to get dubbed sources instead of subbed
Example:
```
GET /animekai/sources/episode-id-here?dub=true
```
## Handling 403 Errors
When accessing the streaming URLs (not the API endpoint), you will encounter 403 Forbidden errors unless you include the proper Referer header. This is a requirement from the underlying streaming provider.
### Required Headers for Streaming
```
Referer: https://kwik.cx/
```
### Example Implementation
```javascript
// Javascript fetch example
fetch('https://streaming-url-from-response.m3u8', {
headers: {
'Referer': 'https://kwik.cx/'
}
})
// Using axios
axios.get('https://streaming-url-from-response.m3u8', {
headers: {
'Referer': 'https://kwik.cx/'
}
})
```
### Video Player Examples
```javascript
// Video.js player
const player = videojs('my-player', {
html5: {
hls: {
overrideNative: true,
xhr: {
beforeRequest: function(options) {
options.headers = {
...options.headers,
'Referer': 'https://kwik.cx/'
};
return options;
}
}
}
}
});
// HLS.js player
const hls = new Hls({
xhrSetup: function(xhr, url) {
xhr.setRequestHeader('Referer', 'https://kwik.cx/');
}
});
hls.loadSource('https://streaming-url-from-response.m3u8');
hls.attachMedia(document.getElementById('video'));
```
## Mapping Approach
The API uses several techniques to find the best match between Anilist and streaming platforms:
1. Tries multiple possible titles (romaji, english, native, userPreferred, synonyms)
2. Uses multiple string similarity algorithms to find the best match
3. Ranks matches based on similarity score
4. Uses year and season information to match the correct season of a series
5. Extracts season numbers from titles for better matching
## Response Format Examples
### AnimePahe Mapping Response
```json
{
"id": 131681,
"animepahe": {
"id": "5646-Re:ZERO -Starting Life in Another World- Season 3",
"title": "Re:ZERO -Starting Life in Another World- Season 3",
"type": "TV",
"status": "Finished Airing",
"season": "Fall",
"year": 2024,
"score": 8.5,
"posterImage": "https://i.animepahe.ru/posters/016b3cda2c47fb5167e238a3f4e97f03e0d1bd3d5e8ffb079ad6d8665fb92455.jpg",
"episodes": {
"count": 16,
"data": [...]
}
}
}
```
### AnimeKai Mapping Response
```json
{
"id": 131681,
"animekai": {
"id": "re-zero-starting-life-in-another-world-season-3",
"title": "Re:ZERO -Starting Life in Another World- Season 3",
"malId": 51194,
"posterImage": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx131681-OFQfZ5v67VYq.jpg",
"episodes": [...]
}
}
```
### Episode Sources Response
```json
{
"headers": { "Referer": "https://kwik.cx/" },
"sources": [
{
"url": "https://na-191.files.nextcdn.org/hls/01/b49063a1225cf4350deb46d79b42a7572e323274d1c9d63f3b067cc4df09986a/uwu.m3u8",
"isM3U8": true,
"quality": "360",
"size": 44617958
},
{
"url": "https://na-191.files.nextcdn.org/hls/01/c32da1b1975a5106dcee7e7182219f9b4dbef836fb782d7939003a8cde8f057f/uwu.m3u8",
"isM3U8": true,
"quality": "720",
"size": 78630133
},
{
"url": "https://na-191.files.nextcdn.org/hls/01/b85d4450908232aa32b71bc67c80e8aedcc4f32a282e5df9ad82e4662786e9d8/uwu.m3u8",
"isM3U8": true,
"quality": "1080",
"size": 118025148
}
]
}
```
## Notes
- This API is for educational purposes only
- Respect the terms of service of all providers (Anilist, AnimePahe, HiAnime, AnimeKai)
- Optimized for simplicity and focused exclusively on mapping functionality
## Dependencies
- Express.js - Web framework
- @consumet/extensions - For AnimePahe integration
- AniWatch (HiAnime) - For HiAnime integration
- Node-cache - For response caching
## Example Usage
Map by Anilist ID:
```
GET /animepahe/map/21
GET /hianime/21
GET /animekai/map/21
```
Get streaming links for an episode:
```
GET /animepahe/sources/session-id/episode-id
GET /animekai/sources/episode-id
GET /animekai/sources/episode-id?dub=true
```

1784
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

24
package.json Normal file
View File

@@ -0,0 +1,24 @@
{
"name": "anilist-animepahe-mapper",
"version": "1.0.0",
"description": "API to map Anilist to AnimePahe",
"main": "src/index.js",
"type": "module",
"scripts": {
"start": "node src/index.js",
"dev": "nodemon src/index.js"
},
"dependencies": {
"@consumet/extensions": "github:consumet/consumet.ts",
"aniwatch": "^2.21.2",
"axios": "^1.8.4",
"cheerio": "^1.0.0",
"express": "^4.21.2",
"node-cache": "^5.1.2",
"node-fetch": "^3.3.2",
"string-similarity-js": "^2.1.4"
},
"devDependencies": {
"nodemon": "^3.0.1"
}
}

View File

@@ -0,0 +1,25 @@
export const ANILIST_URL = 'https://graphql.anilist.co';
export const ANILIST_QUERY = `
query ($id: Int) {
Media(id: $id, type: ANIME) {
id
title {
romaji
english
native
userPreferred
}
episodes
synonyms
}
}
`;
export const HIANIME_URL = 'https://hianimez.to';
export const ANIZIP_URL = 'https://api.ani.zip/mappings';
export default {
ANILIST_URL,
ANILIST_QUERY,
HIANIME_URL,
ANIZIP_URL
};

210
src/index.js Normal file
View File

@@ -0,0 +1,210 @@
import express from 'express';
import { ANIME } from '@consumet/extensions';
import { HiAnime } from 'aniwatch';
import { mapAnilistToAnimePahe, mapAnilistToHiAnime, mapAnilistToAnimeKai } from './mappers/index.js';
import { AniList } from './providers/anilist.js';
import { AnimeKai } from './providers/animekai.js';
import { getEpisodeServers } from './providers/hianime-servers.js';
import { cache } from './utils/cache.js';
const app = express();
const PORT = process.env.PORT || 3000;
app.use(express.json());
// Return server information
app.get('/', (req, res) => {
res.json({
status: 'ok',
message: 'Anilist to AnimePahe Mapper API',
routes: [
'/animepahe/map/:anilistId',
'/animepahe/sources/:session/:episodeId',
'/animepahe/sources/:id',
'/hianime/:anilistId',
'/hianime/servers/:episodeId - For example: /hianime/servers/one-piece-100?ep=2142',
'/hianime/sources/:episodeId - optional params: ?ep=episodeId&server=serverName&category=sub|dub|raw',
'/animekai/map/:anilistId',
'/animekai/sources/:episodeId - supports ?server and ?dub=true params'
],
});
});
// Map Anilist ID to AnimePahe
app.get('/animepahe/map/:anilistId', cache('5 minutes'), async (req, res) => {
try {
const { anilistId } = req.params;
if (!anilistId) {
return res.status(400).json({ error: 'AniList ID is required' });
}
const mappingResult = await mapAnilistToAnimePahe(anilistId);
return res.json(mappingResult);
} catch (error) {
console.error('Mapping error:', error.message);
return res.status(500).json({ error: error.message });
}
});
// Map Anilist ID to HiAnime
app.get('/hianime/:anilistId', cache('5 minutes'), async (req, res) => {
try {
const { anilistId } = req.params;
if (!anilistId) {
return res.status(400).json({ error: 'AniList ID is required' });
}
const episodes = await mapAnilistToHiAnime(anilistId);
return res.json(episodes);
} catch (error) {
console.error('HiAnime mapping error:', error.message);
return res.status(500).json({ error: error.message });
}
});
// Get available servers for HiAnime episode
app.get('/hianime/servers/:animeId', cache('15 minutes'), async (req, res) => {
try {
const { animeId } = req.params;
const { ep } = req.query;
if (!animeId) {
return res.status(400).json({ error: 'Anime ID is required' });
}
// Combine animeId and ep to form the expected episodeId format
const episodeId = ep ? `${animeId}?ep=${ep}` : animeId;
const servers = await getEpisodeServers(episodeId);
return res.json(servers);
} catch (error) {
console.error('HiAnime servers error:', error.message);
return res.status(500).json({ error: error.message });
}
});
// Get streaming sources for HiAnime episode using AniWatch
app.get('/hianime/sources/:animeId', cache('15 minutes'), async (req, res) => {
try {
const { animeId } = req.params;
const { ep, server = 'vidstreaming', category = 'sub' } = req.query;
if (!animeId || !ep) {
return res.status(400).json({ error: 'Both anime ID and episode number (ep) are required' });
}
// Combine animeId and ep to form the expected episodeId format
const episodeId = `${animeId}?ep=${ep}`;
// Use the AniWatch library directly
const hianime = new HiAnime.Scraper();
const sources = await hianime.getEpisodeSources(episodeId, server, category);
return res.json({
success: true,
data: sources
});
} catch (error) {
console.error('HiAnime sources error:', error.message);
return res.status(500).json({
success: false,
error: error.message
});
}
});
// Map Anilist ID to AnimeKai
app.get('/animekai/map/:anilistId', cache('5 minutes'), async (req, res) => {
try {
const { anilistId } = req.params;
if (!anilistId) {
return res.status(400).json({ error: 'AniList ID is required' });
}
const mappingResult = await mapAnilistToAnimeKai(anilistId);
return res.json(mappingResult);
} catch (error) {
console.error('AnimeKai mapping error:', error.message);
return res.status(500).json({ error: error.message });
}
});
// Get episode sources from AnimeKai
app.get('/animekai/sources/:episodeId', cache('15 minutes'), async (req, res) => {
try {
const { episodeId } = req.params;
const { server, dub } = req.query;
if (!episodeId) {
return res.status(400).json({ error: 'Episode ID is required' });
}
const animeKai = new AnimeKai();
const isDub = dub === 'true' || dub === '1';
const sources = await animeKai.fetchEpisodeSources(episodeId, server, isDub);
return res.json(sources);
} catch (error) {
console.error('AnimeKai sources error:', error.message);
return res.status(500).json({ error: error.message });
}
});
// Get HLS streaming links for an episode using path parameter that may contain slashes
app.get('/animepahe/sources/:session/:episodeId', cache('15 minutes'), async (req, res) => {
try {
const { session, episodeId } = req.params;
const fullEpisodeId = `${session}/${episodeId}`;
// Initialize a new AnimePahe instance each time
const consumetAnimePahe = new ANIME.AnimePahe();
// Directly fetch and return the sources without modification
const sources = await consumetAnimePahe.fetchEpisodeSources(fullEpisodeId);
// Simply return the sources directly as provided by Consumet
return res.status(200).json(sources);
} catch (error) {
console.error('Error fetching episode sources:', error.message);
// Keep error handling simple
return res.status(500).json({
error: error.message,
message: 'Failed to fetch episode sources. If you receive a 403 error when accessing streaming URLs, add a Referer: "https://kwik.cx/" header to your requests.'
});
}
});
// Backward compatibility for the single parameter version
app.get('/animepahe/sources/:id', cache('15 minutes'), async (req, res) => {
try {
const episodeId = req.params.id;
if (!episodeId) {
return res.status(400).json({ error: 'Episode ID is required' });
}
// Initialize a new AnimePahe instance each time
const consumetAnimePahe = new ANIME.AnimePahe();
// Directly fetch and return the sources without modification
const sources = await consumetAnimePahe.fetchEpisodeSources(episodeId);
// Simply return the sources directly as provided by Consumet
return res.status(200).json(sources);
} catch (error) {
console.error('Error fetching episode sources:', error.message);
// Keep error handling simple
return res.status(500).json({
error: error.message,
message: 'Failed to fetch episode sources. If you receive a 403 error when accessing streaming URLs, add a Referer: "https://kwik.cx/" header to your requests.'
});
}
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});

View File

@@ -0,0 +1,144 @@
import { AniList } from '../providers/anilist.js';
import { AnimeKai } from '../providers/animekai.js';
/**
* Maps an Anilist anime to AnimeKai
* @param {string|number} anilistId - The AniList ID to map
* @returns {Promise<Object>} The mapping result with episodes
*/
export async function mapAnilistToAnimeKai(anilistId) {
const mapper = new AnimeKaiMapper();
return await mapper.mapAnilistToAnimeKai(anilistId);
}
/**
* Mapper class that provides mapping between Anilist and AnimeKai
*/
export class AnimeKaiMapper {
constructor() {
this.anilist = new AniList();
this.animeKai = new AnimeKai();
}
/**
* Maps an Anilist anime to AnimeKai content
* @param {string|number} anilistId - The AniList ID to map
*/
async mapAnilistToAnimeKai(anilistId) {
try {
// Get anime info from AniList
const animeInfo = await this.anilist.getAnimeInfo(parseInt(anilistId));
if (!animeInfo) {
throw new Error(`Anime with id ${anilistId} not found on AniList`);
}
// Search for the anime on AnimeKai using the title
const searchTitle = animeInfo.title.english || animeInfo.title.romaji || animeInfo.title.userPreferred;
if (!searchTitle) {
throw new Error('No title available for the anime');
}
const searchResults = await this.animeKai.search(searchTitle);
if (!searchResults || !searchResults.results || searchResults.results.length === 0) {
return {
id: animeInfo.id,
title: searchTitle,
animekai: null
};
}
// Find the best match from search results
const bestMatch = this.findBestMatch(searchTitle, animeInfo, searchResults.results);
if (!bestMatch) {
return {
id: animeInfo.id,
title: searchTitle,
animekai: null
};
}
// Get detailed info for the best match
const animeDetails = await this.animeKai.fetchAnimeInfo(bestMatch.id);
return {
id: animeInfo.id,
title: searchTitle,
animekai: {
id: bestMatch.id,
title: bestMatch.title,
japaneseTitle: bestMatch.japaneseTitle,
url: bestMatch.url,
image: bestMatch.image,
type: bestMatch.type,
episodes: animeDetails.totalEpisodes,
episodesList: animeDetails.episodes,
hasSub: animeDetails.hasSub,
hasDub: animeDetails.hasDub,
subOrDub: animeDetails.subOrDub,
status: animeDetails.status,
season: animeDetails.season,
genres: animeDetails.genres
}
};
} catch (error) {
console.error('Error mapping AniList to AnimeKai:', error);
throw error;
}
}
/**
* Find the best match from search results
* @param {string} searchTitle - The search title
* @param {Object} animeInfo - The AniList anime info
* @param {Array} results - The search results
* @returns {Object|null} The best match or null if no good match found
*/
findBestMatch(searchTitle, animeInfo, results) {
if (!results || results.length === 0) return null;
// Normalize titles for comparison
const normalizeTitle = title => title.toLowerCase().replace(/[^\w\s]/g, '').replace(/\s+/g, ' ').trim();
const normalizedSearch = normalizeTitle(searchTitle);
// Extract year from AniList title if present
let year = null;
if (animeInfo.startDate && animeInfo.startDate.year) {
year = animeInfo.startDate.year;
} else if (animeInfo.seasonYear) {
year = animeInfo.seasonYear;
}
// First try: find exact title match
for (const result of results) {
const resultTitle = normalizeTitle(result.title);
const japaneseTitle = result.japaneseTitle ? normalizeTitle(result.japaneseTitle) : '';
if (resultTitle === normalizedSearch || japaneseTitle === normalizedSearch) {
return result;
}
}
// Second try: find partial match with proper episode count match
const expectedEpisodes = animeInfo.episodes || 0;
for (const result of results) {
const resultTitle = normalizeTitle(result.title);
const japaneseTitle = result.japaneseTitle ? normalizeTitle(result.japaneseTitle) : '';
// Check if this is likely the right anime by comparing episode count
if (result.episodes === expectedEpisodes && expectedEpisodes > 0) {
if (resultTitle.includes(normalizedSearch) ||
normalizedSearch.includes(resultTitle) ||
japaneseTitle.includes(normalizedSearch) ||
normalizedSearch.includes(japaneseTitle)) {
return result;
}
}
}
// Final fallback: just return the first result
return results[0];
}
}
export default mapAnilistToAnimeKai;

View File

@@ -0,0 +1,321 @@
import { AniList } from '../providers/anilist.js';
import { AnimePahe } from '../providers/animepahe.js';
/**
* Maps an Anilist anime to AnimePahe content
*/
export async function mapAnilistToAnimePahe(anilistId) {
const mapper = new AnimepaheMapper();
return await mapper.mapAnilistToAnimePahe(anilistId);
}
/**
* Mapper class that provides mapping between Anilist and AnimePahe
*/
export class AnimepaheMapper {
constructor() {
this.anilist = new AniList();
this.animePahe = new AnimePahe();
}
/**
* Maps an Anilist anime to AnimePahe content
*/
async mapAnilistToAnimePahe(anilistId) {
try {
// Get anime info from AniList
const animeInfo = await this.anilist.getAnimeInfo(parseInt(anilistId));
if (!animeInfo) {
throw new Error(`Anime with id ${anilistId} not found on AniList`);
}
// Try to find matching content on AnimePahe
const bestMatch = await this.findAnimePaheMatch(animeInfo);
if (!bestMatch) {
return {
id: animeInfo.id,
animepahe: null
};
}
// Get episode data for the matched anime
const episodeData = await this.getAnimePaheEpisodes(bestMatch);
// Return the mapped result
return {
id: animeInfo.id,
animepahe: {
id: bestMatch.id,
title: bestMatch.title || bestMatch.name,
episodes: episodeData.episodes,
type: bestMatch.type,
status: bestMatch.status,
season: bestMatch.season,
year: bestMatch.year,
score: bestMatch.score,
posterImage: bestMatch.poster,
session: bestMatch.session
}
};
} catch (error) {
console.error('Error mapping AniList to AnimePahe:', error.message);
throw new Error('Failed to map AniList to AnimePahe: ' + error.message);
}
}
/**
* Finds the matching AnimePahe content for an AniList anime
*/
async findAnimePaheMatch(animeInfo) {
// Only use one primary title to reduce API calls
let bestTitle = animeInfo.title.romaji || animeInfo.title.english || animeInfo.title.userPreferred;
const titleType = animeInfo.title.romaji ? 'romaji' : (animeInfo.title.english ? 'english' : 'userPreferred');
// First search attempt
const searchResults = await this.animePahe.scrapeSearchResults(bestTitle);
// Process results if we found any
if (searchResults && searchResults.length > 0) {
// First try direct ID match (fastest path)
const rawId = animeInfo.id.toString();
for (const result of searchResults) {
const resultId = (result.id || '').split('-')[0];
if (resultId && resultId === rawId) {
return result;
}
}
// If no direct ID match, find the best match with our algorithm
return this.findBestMatchFromResults(animeInfo, searchResults);
}
// If no results found, try a fallback search with a more generic title
const genericTitle = this.getGenericTitle(animeInfo);
if (genericTitle && genericTitle !== bestTitle) {
const fallbackResults = await this.animePahe.scrapeSearchResults(genericTitle);
if (fallbackResults && fallbackResults.length > 0) {
return this.findBestMatchFromResults(animeInfo, fallbackResults);
}
}
return null;
}
/**
* Find the best match from available search results
*/
findBestMatchFromResults(animeInfo, results) {
if (!results || results.length === 0) return null;
// Normalize titles just once to avoid repeating work
const normalizeTitle = t => t.toLowerCase().replace(/[^\w\s]/g, '').replace(/\s+/g, ' ').trim();
const anilistTitles = [
animeInfo.title.romaji,
animeInfo.title.english,
animeInfo.title.userPreferred
].filter(Boolean).map(normalizeTitle);
// Prepare year information
const anilistYear =
(animeInfo.startDate && animeInfo.startDate.year) ?
animeInfo.startDate.year : animeInfo.seasonYear;
const animeYear = anilistYear || this.extractYearFromTitle(animeInfo);
// Process matches sequentially with early returns
let bestMatch = null;
// Try exact title match with year (highest priority)
if (animeYear) {
// Find matches with exact year
const yearMatches = [];
for (const result of results) {
const resultYear = result.year ? parseInt(result.year) : this.extractYearFromTitle(result);
if (resultYear === animeYear) {
yearMatches.push(result);
}
}
// If we have year matches, try to find the best title match among them
if (yearMatches.length > 0) {
for (const match of yearMatches) {
const resultTitle = normalizeTitle(match.title || match.name);
// First try: exact title match with year
for (const title of anilistTitles) {
if (!title) continue;
if (resultTitle === title ||
(resultTitle.includes(title) && title.length > 7) ||
(title.includes(resultTitle) && resultTitle.length > 7)) {
return match; // Early return for best match
}
}
// Second try: high similarity title match with year
for (const title of anilistTitles) {
if (!title) continue;
const similarity = this.calculateTitleSimilarity(title, resultTitle);
if (similarity > 0.5) {
bestMatch = match;
break;
}
}
if (bestMatch) break;
}
// If we found a title similarity match with year, return it
if (bestMatch) return bestMatch;
// Otherwise use the first year match as a fallback
return yearMatches[0];
}
}
// Try exact title match
for (const result of results) {
const resultTitle = normalizeTitle(result.title || result.name);
for (const title of anilistTitles) {
if (!title) continue;
if (resultTitle === title) {
return result; // Early return for exact title match
}
}
}
// Try high similarity title match
bestMatch = this.findBestSimilarityMatch(anilistTitles, results);
if (bestMatch) return bestMatch;
// Just use the first result as a fallback
return results[0];
}
/**
* Find the best match based on title similarity
*/
findBestSimilarityMatch(titles, results) {
const normalizeTitle = t => t.toLowerCase().replace(/[^\w\s]/g, '').replace(/\s+/g, ' ').trim();
let bestMatch = null;
let highestSimilarity = 0;
for (const result of results) {
const resultTitle = normalizeTitle(result.title || result.name);
for (const title of titles) {
if (!title) continue;
const similarity = this.calculateTitleSimilarity(title, resultTitle);
if (similarity > highestSimilarity) {
highestSimilarity = similarity;
bestMatch = result;
}
}
}
// Only return if we have a reasonably good match
return highestSimilarity > 0.6 ? bestMatch : null;
}
/**
* Get the AnimePahe episodes for a match
*/
async getAnimePaheEpisodes(match) {
try {
const episodeData = await this.animePahe.scrapeEpisodes(match.id);
return {
totalEpisodes: episodeData.totalEpisodes || 0,
episodes: episodeData.episodes || []
};
} catch (error) {
console.error('Error getting AnimePahe episodes:', error.message);
return { totalEpisodes: 0, episodes: [] };
}
}
/**
* Calculate similarity between two titles
*/
calculateTitleSimilarity(title1, title2) {
if (!title1 || !title2) return 0;
// Normalize both titles
const norm1 = title1.toLowerCase().replace(/[^\w\s]/g, '').replace(/\s+/g, ' ').trim();
const norm2 = title2.toLowerCase().replace(/[^\w\s]/g, '').replace(/\s+/g, ' ').trim();
// Exact match is best
if (norm1 === norm2) return 1;
// Split into words
const words1 = norm1.split(' ').filter(Boolean);
const words2 = norm2.split(' ').filter(Boolean);
// Count common words
const commonCount = words1.filter(w => words2.includes(w)).length;
// Weight by percentage of common words
return commonCount * 2 / (words1.length + words2.length);
}
/**
* Extract year from title (e.g., "JoJo's Bizarre Adventure (2012)" -> 2012)
*/
extractYearFromTitle(item) {
if (!item) return null;
// Extract the title string based on the input type
let titleStr = '';
if (typeof item === 'string') {
titleStr = item;
} else if (typeof item === 'object') {
// Handle both anime objects and result objects
if (item.title) {
if (typeof item.title === 'string') {
titleStr = item.title;
} else if (typeof item.title === 'object') {
// AniList title object
titleStr = item.title.userPreferred || item.title.english || item.title.romaji || '';
}
} else if (item.name) {
titleStr = item.name;
}
}
if (!titleStr) return null;
// Look for year pattern in parentheses or brackets
const yearMatches = titleStr.match(/[\(\[](\d{4})[\)\]]/);
if (yearMatches && yearMatches[1]) {
const year = parseInt(yearMatches[1]);
if (!isNaN(year) && year > 1950 && year <= new Date().getFullYear()) {
return year;
}
}
return null;
}
/**
* Get a generic title by removing year information and other specific identifiers
*/
getGenericTitle(animeInfo) {
if (!animeInfo || !animeInfo.title) return null;
const title = animeInfo.title.english || animeInfo.title.romaji || animeInfo.title.userPreferred;
if (!title) return null;
// Remove year information and common specifiers
return title.replace(/\([^)]*\d{4}[^)]*\)/g, '').replace(/\[[^\]]*\d{4}[^\]]*\]/g, '').trim();
}
}
export default mapAnilistToAnimePahe;

View File

@@ -0,0 +1,18 @@
import { getEpisodesForAnime } from '../providers/hianime.js';
/**
* Maps an Anilist anime to HiAnime episodes
* @param {string|number} anilistId - The AniList ID to map
* @returns {Promise<Object>} The mapping result with episodes
*/
export async function mapAnilistToHiAnime(anilistId) {
try {
const episodes = await getEpisodesForAnime(anilistId);
return episodes;
} catch (error) {
console.error('Error mapping Anilist to HiAnime:', error);
throw error;
}
}
export default mapAnilistToHiAnime;

9
src/mappers/index.js Normal file
View File

@@ -0,0 +1,9 @@
import mapAnilistToAnimePahe from './animepahe-mapper.js';
import mapAnilistToHiAnime from './hianime-mapper.js';
import mapAnilistToAnimeKai from './animekai-mapper.js';
export {
mapAnilistToAnimePahe,
mapAnilistToHiAnime,
mapAnilistToAnimeKai
};

103
src/providers/anilist.js Normal file
View File

@@ -0,0 +1,103 @@
import axios from 'axios';
import { ANILIST_URL } from '../constants/api-constants.js';
export class AniList {
constructor() {
this.baseUrl = ANILIST_URL;
}
async getAnimeInfo(id) {
try {
const query = `
query ($id: Int) {
Media(id: $id, type: ANIME) {
id
title {
romaji
english
native
userPreferred
}
description
coverImage {
large
medium
}
bannerImage
episodes
status
season
seasonYear
startDate {
year
month
day
}
endDate {
year
month
day
}
genres
source
averageScore
synonyms
isAdult
format
type
}
}
`;
const response = await axios.post(this.baseUrl, {
query,
variables: { id }
});
return response.data.data.Media;
} catch (error) {
console.error('Error fetching anime info from AniList:', error.message);
throw new Error('Failed to fetch anime info from AniList');
}
}
async searchAnime(query) {
try {
const gqlQuery = `
query ($search: String) {
Page(page: 1, perPage: 10) {
media(search: $search, type: ANIME) {
id
title {
romaji
english
native
}
description
coverImage {
large
medium
}
episodes
status
genres
averageScore
}
}
}
`;
const response = await axios.post(this.baseUrl, {
query: gqlQuery,
variables: { search: query }
});
return response.data.data.Page.media;
} catch (error) {
console.error('Error searching anime on AniList:', error.message);
throw new Error('Failed to search anime on AniList');
}
}
}
export default AniList;

61
src/providers/animekai.js Normal file
View File

@@ -0,0 +1,61 @@
import { ANIME } from '@consumet/extensions';
/**
* AnimeKai provider class that wraps the Consumet library
*/
export class AnimeKai {
constructor() {
this.client = new ANIME.AnimeKai();
}
/**
* Search for anime on AnimeKai
* @param {string} query - The search query
* @returns {Promise<Object>} Search results
*/
async search(query) {
try {
const results = await this.client.search(query);
return results;
} catch (error) {
console.error('Error searching AnimeKai:', error);
throw new Error('Failed to search AnimeKai');
}
}
/**
* Fetch anime information including episodes
* @param {string} id - The anime ID
* @returns {Promise<Object>} Anime info with episodes
*/
async fetchAnimeInfo(id) {
try {
const info = await this.client.fetchAnimeInfo(id);
return info;
} catch (error) {
console.error('Error fetching anime info from AnimeKai:', error);
throw new Error('Failed to fetch anime info from AnimeKai');
}
}
/**
* Fetch episode streaming sources
* @param {string} episodeId - The episode ID
* @param {string} server - Optional streaming server
* @param {boolean} dub - Whether to fetch dubbed sources (true) or subbed (false)
* @returns {Promise<Object>} Streaming sources
*/
async fetchEpisodeSources(episodeId, server = undefined, dub = false) {
try {
// Use the SubOrSub enum from Consumet if dub is true
const subOrDub = dub ? 'dub' : 'sub';
const sources = await this.client.fetchEpisodeSources(episodeId, server, subOrDub);
return sources;
} catch (error) {
console.error('Error fetching episode sources from AnimeKai:', error);
throw new Error('Failed to fetch episode sources from AnimeKai');
}
}
}
export default AnimeKai;

281
src/providers/animepahe.js Normal file
View File

@@ -0,0 +1,281 @@
import axios from 'axios';
import * as cheerio from 'cheerio';
export class AnimePahe {
constructor() {
this.baseUrl = "https://animepahe.ru";
this.sourceName = 'AnimePahe';
this.isMulti = false;
}
async scrapeSearchResults(query) {
try {
const response = await axios.get(`${this.baseUrl}/api?m=search&l=8&q=${query}`, {
headers: {
'Cookie': "__ddg1_=;__ddg2_=;",
}
});
const jsonResult = response.data;
const searchResults = [];
if (!jsonResult.data || !jsonResult.data.length) {
return searchResults;
}
for (const item of jsonResult.data) {
searchResults.push({
id: `${item.id}-${item.title}`,
title: item.title,
name: item.title,
type: item.type || 'TV',
episodes: item.episodes || 0,
status: item.status || 'Unknown',
season: item.season || 'Unknown',
year: item.year || 0,
score: item.score || 0,
poster: item.poster,
session: item.session,
episodes: {
sub: item.episodes || null,
dub: '??'
}
});
}
return searchResults;
} catch (error) {
console.error('Error searching AnimePahe:', error.message);
throw new Error('Failed to search AnimePahe');
}
}
async scrapeEpisodes(url) {
try {
const title = url.split('-')[1];
const id = url.split('-')[0];
const session = await this._getSession(title, id);
const epUrl = `${this.baseUrl}/api?m=release&id=${session}&sort=episode_desc&page=1`;
const response = await axios.get(epUrl, {
headers: {
'Cookie': "__ddg1_=;__ddg2_=;",
}
});
return await this._recursiveFetchEpisodes(epUrl, JSON.stringify(response.data), session);
} catch (error) {
console.error('Error fetching episodes:', error.message);
throw new Error('Failed to fetch episodes');
}
}
async _recursiveFetchEpisodes(url, responseData, session) {
try {
const jsonResult = JSON.parse(responseData);
const page = jsonResult.current_page;
const hasNextPage = page < jsonResult.last_page;
let animeTitle = 'Could not fetch title';
let episodes = [];
let animeDetails = {
type: 'TV',
status: 'Unknown',
season: 'Unknown',
year: 0,
score: 0
};
for (const item of jsonResult.data) {
episodes.push({
title: `Episode ${item.episode}`,
episodeId: `${session}/${item.session}`,
number: item.episode,
image: item.snapshot,
});
}
if (hasNextPage) {
const newUrl = `${url.split("&page=")[0]}&page=${page + 1}`;
const newResponse = await axios.get(newUrl, {
headers: {
'Cookie': "__ddg1_=;__ddg2_=;",
}
});
const moreEpisodes = await this._recursiveFetchEpisodes(newUrl, JSON.stringify(newResponse.data), session);
episodes = [...episodes, ...moreEpisodes.episodes];
animeTitle = moreEpisodes.title;
animeDetails = moreEpisodes.details || animeDetails;
} else {
const detailUrl = `https://animepahe.ru/a/${jsonResult.data[0].anime_id}`;
const newResponse = await axios.get(detailUrl, {
headers: {
'Cookie': "__ddg1_=;__ddg2_=;",
}
});
if (newResponse.status === 200) {
const $ = cheerio.load(newResponse.data);
animeTitle = $('.title-wrapper span').text().trim() || 'Could not fetch title';
// Try to extract additional information
try {
// Parse type
const typeText = $('.col-sm-4.anime-info p:contains("Type")').text();
if (typeText) {
animeDetails.type = typeText.replace('Type:', '').trim();
}
// Parse status
const statusText = $('.col-sm-4.anime-info p:contains("Status")').text();
if (statusText) {
animeDetails.status = statusText.replace('Status:', '').trim();
}
// Parse season and year
const seasonText = $('.col-sm-4.anime-info p:contains("Season")').text();
if (seasonText) {
const seasonMatch = seasonText.match(/Season:\s+(\w+)\s+(\d{4})/);
if (seasonMatch) {
animeDetails.season = seasonMatch[1];
animeDetails.year = parseInt(seasonMatch[2]);
}
}
// Parse score
const scoreText = $('.col-sm-4.anime-info p:contains("Score")').text();
if (scoreText) {
const scoreMatch = scoreText.match(/Score:\s+([\d.]+)/);
if (scoreMatch) {
animeDetails.score = parseFloat(scoreMatch[1]);
}
}
} catch (err) {
console.error('Error parsing anime details:', err.message);
}
}
}
return {
title: animeTitle,
session: session,
totalEpisodes: jsonResult.total,
details: animeDetails,
episodes: hasNextPage ? episodes : episodes.reverse(),
};
} catch (error) {
console.error('Error recursively fetching episodes:', error.message);
throw new Error('Failed to fetch episodes recursively');
}
}
async scrapeEpisodesSrcs(episodeUrl, { category, lang } = {}) {
try {
const response = await axios.get(`${this.baseUrl}/play/${episodeUrl}`, {
headers: {
'Cookie': "__ddg1_=;__ddg2_=;",
}
});
const $ = cheerio.load(response.data);
const buttons = $('#resolutionMenu > button');
const videoLinks = [];
for (let i = 0; i < buttons.length; i++) {
const btn = buttons[i];
const kwikLink = $(btn).attr('data-src');
const quality = $(btn).text();
// Instead of extracting, just return the link directly
videoLinks.push({
quality: quality,
url: kwikLink,
referer: "https://kwik.cx",
});
}
const result = {
sources: videoLinks.length > 0 ? [{ url: videoLinks[0].url }] : [],
multiSrc: videoLinks,
};
return result;
} catch (error) {
console.error('Error fetching episode sources:', error.message);
throw new Error('Failed to fetch episode sources');
}
}
async _getSession(title, animeId) {
try {
const response = await axios.get(`${this.baseUrl}/api?m=search&q=${title}`, {
headers: {
'Cookie': "__ddg1_=;__ddg2_=;",
}
});
const resBody = response.data;
if (!resBody.data || resBody.data.length === 0) {
throw new Error(`No results found for title: ${title}`);
}
// First try: Direct ID match if provided and valid
if (animeId) {
const animeIdMatch = resBody.data.find(anime => String(anime.id) === String(animeId));
if (animeIdMatch) {
return animeIdMatch.session;
}
}
// Second try: Normalize titles and find best match
const normalizeTitle = t => t.toLowerCase().replace(/[^\w\s]/g, '').replace(/\s+/g, ' ').trim();
const normalizedSearchTitle = normalizeTitle(title);
let bestMatch = null;
let highestSimilarity = 0;
for (const anime of resBody.data) {
const normalizedAnimeTitle = normalizeTitle(anime.title);
// Calculate simple similarity (more sophisticated than exact match)
let similarity = 0;
// Exact match
if (normalizedAnimeTitle === normalizedSearchTitle) {
similarity = 1;
}
// Contains match
else if (normalizedAnimeTitle.includes(normalizedSearchTitle) ||
normalizedSearchTitle.includes(normalizedAnimeTitle)) {
const lengthRatio = Math.min(normalizedAnimeTitle.length, normalizedSearchTitle.length) /
Math.max(normalizedAnimeTitle.length, normalizedSearchTitle.length);
similarity = 0.8 * lengthRatio;
}
// Word match
else {
const searchWords = normalizedSearchTitle.split(' ');
const animeWords = normalizedAnimeTitle.split(' ');
const commonWords = searchWords.filter(word => animeWords.includes(word));
similarity = commonWords.length / Math.max(searchWords.length, animeWords.length);
}
if (similarity > highestSimilarity) {
highestSimilarity = similarity;
bestMatch = anime;
}
}
if (bestMatch && highestSimilarity > 0.5) {
return bestMatch.session;
}
// Default to first result if no good match found
return resBody.data[0].session;
} catch (error) {
console.error('Error getting session:', error.message);
throw new Error('Failed to get session');
}
}
}
export default AnimePahe;

View File

@@ -0,0 +1,136 @@
import { load } from 'cheerio';
import { client } from '../utils/client.js';
import { HIANIME_URL } from '../constants/api-constants.js';
/**
* Get all available servers for a HiAnime episode
* @param {string} episodeId - Episode ID in format "anime-title-123?ep=456"
* @returns {Promise<Object>} Object containing sub, dub, and raw server lists
*/
export async function getEpisodeServers(episodeId) {
const result = {
sub: [],
dub: [],
raw: [],
episodeId,
episodeNo: 0,
};
try {
if (!episodeId || episodeId.trim() === "" || episodeId.indexOf("?ep=") === -1) {
throw new Error("Invalid anime episode ID");
}
const epId = episodeId.split("?ep=")[1];
const ajaxUrl = `${HIANIME_URL}/ajax/v2/episode/servers?episodeId=${epId}`;
const { data } = await client.get(ajaxUrl, {
headers: {
"X-Requested-With": "XMLHttpRequest",
"Referer": `${HIANIME_URL}/watch/${episodeId}`
}
});
if (!data.html) {
throw new Error("No server data found");
}
const $ = load(data.html);
// Extract episode number
const epNoSelector = ".server-notice strong";
result.episodeNo = Number($(epNoSelector).text().split(" ").pop()) || 0;
// Extract SUB servers
$(`.ps_-block.ps_-block-sub.servers-sub .ps__-list .server-item`).each((_, el) => {
result.sub.push({
serverName: $(el).find("a").text().toLowerCase().trim(),
serverId: Number($(el)?.attr("data-server-id")?.trim()) || null,
});
});
// Extract DUB servers
$(`.ps_-block.ps_-block-sub.servers-dub .ps__-list .server-item`).each((_, el) => {
result.dub.push({
serverName: $(el).find("a").text().toLowerCase().trim(),
serverId: Number($(el)?.attr("data-server-id")?.trim()) || null,
});
});
// Extract RAW servers
$(`.ps_-block.ps_-block-sub.servers-raw .ps__-list .server-item`).each((_, el) => {
result.raw.push({
serverName: $(el).find("a").text().toLowerCase().trim(),
serverId: Number($(el)?.attr("data-server-id")?.trim()) || null,
});
});
return result;
} catch (error) {
console.error('Error fetching episode servers:', error.message);
throw error;
}
}
/**
* Get streaming sources for a HiAnime episode
* @param {string} episodeId - Episode ID in format "anime-title-123?ep=456"
* @param {string} serverName - Name of the server to get sources from
* @param {string} category - Type of episode: 'sub', 'dub', or 'raw'
* @returns {Promise<Object>} Object containing sources and related metadata
*/
export async function getEpisodeSources(episodeId, serverName = 'vidstreaming', category = 'sub') {
try {
if (!episodeId || episodeId.trim() === "" || episodeId.indexOf("?ep=") === -1) {
throw new Error("Invalid anime episode ID");
}
// First get available servers
const servers = await getEpisodeServers(episodeId);
// Find the requested server
const serverList = servers[category] || [];
const server = serverList.find(s => s.serverName.toLowerCase() === serverName.toLowerCase());
if (!server) {
throw new Error(`Server '${serverName}' not found for category '${category}'`);
}
const epId = episodeId.split("?ep=")[1];
const serverId = server.serverId;
// Fetch the source URL
const { data } = await client.get(
`${HIANIME_URL}/ajax/v2/episode/sources?id=${epId}&server=${serverId}`,
{
headers: {
"X-Requested-With": "XMLHttpRequest",
"Referer": `${HIANIME_URL}/watch/${episodeId}`
}
}
);
// Return sources format similar to the AniWatch package
return {
headers: {
Referer: data.link,
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36"
},
sources: [
{
url: data.link,
isM3U8: data.link.includes('.m3u8'),
}
],
subtitles: [],
};
} catch (error) {
console.error('Error fetching episode sources:', error.message);
throw error;
}
}
export default {
getEpisodeServers,
getEpisodeSources
};

381
src/providers/hianime.js Normal file
View File

@@ -0,0 +1,381 @@
import { load } from 'cheerio';
import * as stringSimilarity from 'string-similarity-js';
import { client } from '../utils/client.js';
import { ANILIST_URL, ANILIST_QUERY, HIANIME_URL, ANIZIP_URL } from '../constants/api-constants.js';
// Common word replacements in anime titles - moved outside to avoid recreation
const TITLE_REPLACEMENTS = {
'season': ['s', 'sz'],
's': ['season', 'sz'],
'sz': ['season', 's'],
'two': ['2', 'ii'],
'three': ['3', 'iii'],
'four': ['4', 'iv'],
'part': ['pt', 'p'],
'episode': ['ep'],
'chapters': ['ch'],
'chapter': ['ch'],
'first': ['1', 'i'],
'second': ['2', 'ii'],
'third': ['3', 'iii'],
'fourth': ['4', 'iv']
};
// Cache for word variations to avoid recalculating
const wordVariationsCache = new Map();
// Helper function to normalize text for comparison
const normalizeText = (text) => {
return text.toLowerCase()
.replace(/[^\w\s]/g, '')
.replace(/\s+/g, ' ')
.trim();
};
// Get word variations with caching
const getWordVariations = (word) => {
const cacheKey = word.toLowerCase();
if (wordVariationsCache.has(cacheKey)) {
return wordVariationsCache.get(cacheKey);
}
const variations = new Set([word]);
const normalized = normalizeText(word);
variations.add(normalized);
const withoutNumbers = word.replace(/\d+/g, '').trim();
if (withoutNumbers !== word) variations.add(withoutNumbers);
for (const [key, values] of Object.entries(TITLE_REPLACEMENTS)) {
if (normalized === key) {
values.forEach(v => variations.add(v));
} else if (values.includes(normalized)) {
variations.add(key);
values.forEach(v => variations.add(v));
}
}
const result = [...variations];
wordVariationsCache.set(cacheKey, result);
return result;
};
// Fetch anime info from Anilist
async function getAnimeInfo(anilistId) {
try {
const response = await client.post(ANILIST_URL, {
query: ANILIST_QUERY,
variables: { id: anilistId }
});
const animeData = response.data.data.Media;
if (!animeData) return null;
// Get all possible titles and synonyms at once
const allTitles = new Set([
...(animeData.synonyms || []),
animeData.title.english,
animeData.title.romaji
].filter(Boolean) // Remove nulls/undefined
.filter(t => !(/[\u4E00-\u9FFF]/.test(t)))); // Remove Chinese titles
return {
id: animeData.id,
title: animeData.title,
episodes: animeData.episodes,
synonyms: [...allTitles]
};
} catch (error) {
console.error('Error fetching anime info:', error);
return null;
}
}
// Calculate similarity score between two titles
const calculateTitleScore = (searchTitle, hianimeTitle) => {
const normalizedSearch = normalizeText(searchTitle);
const normalizedTitle = normalizeText(hianimeTitle);
// Quick exact match check
if (normalizedSearch === normalizedTitle) {
return 1;
}
const searchWords = normalizedSearch.split(' ');
const titleWords = normalizedTitle.split(' ');
// Pre-calculate word variations for both titles
const searchVariations = searchWords.map(w => getWordVariations(w));
const titleVariations = titleWords.map(w => getWordVariations(w));
let matches = 0;
let partialMatches = 0;
for (let i = 0; i < searchVariations.length; i++) {
let bestWordMatch = 0;
for (let j = 0; j < titleVariations.length; j++) {
for (const searchVar of searchVariations[i]) {
for (const titleVar of titleVariations[j]) {
if (searchVar === titleVar) {
bestWordMatch = 1;
break;
}
if (searchVar.includes(titleVar) || titleVar.includes(searchVar)) {
const matchLength = Math.min(searchVar.length, titleVar.length);
const maxLength = Math.max(searchVar.length, titleVar.length);
bestWordMatch = Math.max(bestWordMatch, matchLength / maxLength);
}
}
if (bestWordMatch === 1) break;
}
if (bestWordMatch === 1) break;
}
if (bestWordMatch === 1) {
matches++;
} else if (bestWordMatch > 0) {
partialMatches += bestWordMatch;
}
}
const wordMatchScore = (matches + (partialMatches * 0.5)) / searchWords.length;
const similarity = stringSimilarity.stringSimilarity(normalizedSearch, normalizedTitle);
return (wordMatchScore * 0.7) + (similarity * 0.3);
};
// Search anime on Hianime and get the most similar match
async function searchAnime(title, animeInfo) {
try {
let bestMatch = { score: 0, id: null };
let seriesMatches = [];
let year = null;
// Extract year from title if present (e.g., 2011 from "Hunter x Hunter (2011)")
const yearMatch = title.match(/\((\d{4})\)/);
if (yearMatch && yearMatch[1]) {
year = yearMatch[1];
}
// Try each title in order of priority
const titlesToTry = [
animeInfo.title.english,
animeInfo.title.romaji,
...animeInfo.synonyms
].filter(Boolean)
.filter((t, i, arr) => arr.indexOf(t) === i);
for (const searchTitle of titlesToTry) {
const searchUrl = `${HIANIME_URL}/search?keyword=${encodeURIComponent(searchTitle)}`;
const response = await client.get(searchUrl);
const $ = load(response.data);
$('.film_list-wrap > .flw-item').each((_, item) => {
const el = $(item).find('.film-detail .film-name a');
const hianimeTitle = el.text().trim();
const hianimeId = el.attr('href')?.split('/').pop()?.split('?')[0];
// Check the data-jname attribute which may contain year information
const jname = el.attr('data-jname')?.trim();
// Check if this is a TV series
const isTV = $(item).find('.fd-infor .fdi-item').first().text().trim() === 'TV';
// Check episodes count to match with expected count from Anilist
const episodesText = $(item).find('.tick-item.tick-eps').text().trim();
const episodesCount = episodesText ? parseInt(episodesText, 10) : 0;
if (hianimeId) {
// Calculate base similarity score
let score = calculateTitleScore(searchTitle, hianimeTitle);
// Boost score for TV series when searching for TV series
if (isTV && animeInfo.episodes > 12) {
score += 0.1;
}
// Boost score if episodes count matches Anilist data
if (animeInfo.episodes && episodesCount === animeInfo.episodes) {
score += 0.2;
}
// Special handling for year matching
if (year) {
// Check if jname contains the year
if (jname && jname.includes(year)) {
score += 0.3;
}
}
// Penalize movies when looking for series with episodes
const isMovie = $(item).find('.fd-infor .fdi-item').first().text().trim() === 'Movie';
if (isMovie && animeInfo.episodes > 1) {
score -= 0.3;
}
if (score > 0.5) {
seriesMatches.push({
title: hianimeTitle,
id: hianimeId,
score,
isMovie,
isTV,
episodes: episodesCount,
jname
});
}
if (score > bestMatch.score) {
bestMatch = { score, id: hianimeId };
}
}
});
// Stop if we found a very good match
if (bestMatch.score > 0.85) {
return bestMatch.id;
}
}
// Special handling for Hunter x Hunter 2011 version
const hxh2011Matches = seriesMatches.filter(match =>
match.isTV &&
match.episodes >= 100 &&
(match.jname?.includes('2011') || match.title.toLowerCase().includes('hunter x hunter'))
);
if (hxh2011Matches.length > 0 && year === '2011') {
const bestHxH = hxh2011Matches.sort((a, b) => b.score - a.score)[0];
return bestHxH.id;
}
// If we have multiple matches, prioritize non-movies and TV series
if (seriesMatches.length > 0) {
// First prioritize by TV series with matching episode count
const exactEpisodeMatches = seriesMatches.filter(m =>
m.isTV && animeInfo.episodes && m.episodes === animeInfo.episodes
);
if (exactEpisodeMatches.length > 0) {
const bestExactMatch = exactEpisodeMatches.sort((a, b) => b.score - a.score)[0];
return bestExactMatch.id;
}
// Sort by score descending
seriesMatches.sort((a, b) => b.score - a.score);
// Prioritize TV series over others if scores are close
const bestTV = seriesMatches.filter(m => m.isTV)[0];
const bestOverall = seriesMatches[0];
if (bestTV && bestOverall.score - bestTV.score < 0.2) {
return bestTV.id;
}
// Otherwise return best match
return bestOverall.id;
}
return bestMatch.score > 0.4 ? bestMatch.id : null;
} catch (error) {
console.error('Error searching Hianime:', error);
return null;
}
}
// Get episode IDs for an anime
async function getEpisodeIds(animeId, anilistId) {
try {
const episodeUrl = `${HIANIME_URL}/ajax/v2/episode/list/${animeId.split('-').pop()}`;
// Fetch additional metadata from ani.zip
const anizipUrl = `${ANIZIP_URL}?anilist_id=${anilistId}`;
const [episodeResponse, anizipResponse] = await Promise.all([
client.get(episodeUrl, {
headers: {
'Referer': `${HIANIME_URL}/watch/${animeId}`,
'X-Requested-With': 'XMLHttpRequest'
}
}),
client.get(anizipUrl)
]);
if (!episodeResponse.data.html) {
return { totalEpisodes: 0, episodes: [] };
}
const $ = load(episodeResponse.data.html);
const episodes = [];
const anizipData = anizipResponse.data;
$('#detail-ss-list div.ss-list a').each((i, el) => {
const $el = $(el);
const href = $el.attr('href');
if (!href) return;
const fullPath = href.split('/').pop();
const episodeNumber = i + 1;
const anizipEpisode = anizipData?.episodes?.[episodeNumber];
if (fullPath) {
episodes.push({
episodeId: `${animeId}?ep=${fullPath.split('?ep=')[1]}`,
title: anizipEpisode?.title?.en || $el.attr('title') || '',
number: episodeNumber,
image: anizipEpisode?.image || null,
overview: anizipEpisode?.overview || null,
airDate: anizipEpisode?.airDate || null,
runtime: anizipEpisode?.runtime || null
});
}
});
return {
totalEpisodes: episodes.length,
episodes,
titles: anizipData?.titles || null,
images: anizipData?.images || null,
mappings: anizipData?.mappings || null
};
} catch (error) {
console.error('Error fetching episodes:', error);
return { totalEpisodes: 0, episodes: [] };
}
}
// Main function to get episodes for an Anilist ID
export async function getEpisodesForAnime(anilistId) {
try {
const animeInfo = await getAnimeInfo(anilistId);
if (!animeInfo) {
throw new Error('Could not fetch anime info from Anilist');
}
const title = animeInfo.title.english || animeInfo.title.romaji;
if (!title) {
throw new Error('No English or romaji title found');
}
const hianimeId = await searchAnime(title, animeInfo);
if (!hianimeId) {
throw new Error('Could not find anime on Hianime');
}
const episodes = await getEpisodeIds(hianimeId, anilistId);
if (!episodes || episodes.totalEpisodes === 0) {
throw new Error('Could not fetch episodes');
}
return { anilistId, hianimeId, title, ...episodes };
} catch (error) {
console.error('Error in getEpisodesForAnime:', error);
throw error;
}
}
export default {
getEpisodesForAnime
};

11
src/providers/index.js Normal file
View File

@@ -0,0 +1,11 @@
import AniList from './anilist.js';
import AnimePahe from './animepahe.js';
import HiAnime from './hianime.js';
import AnimeKai from './animekai.js';
export {
AniList,
AnimePahe,
HiAnime,
AnimeKai
};

43
src/utils/cache.js Normal file
View File

@@ -0,0 +1,43 @@
import NodeCache from 'node-cache';
// Create a new cache instance
const apiCache = new NodeCache({ stdTTL: 300 }); // Default TTL 5 minutes
/**
* Middleware for caching API responses
* @param {string} duration Cache duration in format: '5 minutes', '1 hour', etc.
*/
export function cache(duration) {
return (req, res, next) => {
// Skip caching for non-GET requests
if (req.method !== 'GET') {
return next();
}
// Create a unique cache key from the request URL
const key = req.originalUrl || req.url;
// Check if we have a cached response for this request
const cachedResponse = apiCache.get(key);
if (cachedResponse) {
console.log(`Cache hit for: ${key}`);
return res.json(cachedResponse);
}
// Store the original json method
const originalJson = res.json;
// Override the json method to cache the response
res.json = function(data) {
// Cache the data
console.log(`Caching response for: ${key}`);
apiCache.set(key, data);
// Call the original json method
return originalJson.call(this, data);
};
next();
};
}

12
src/utils/client.js Normal file
View File

@@ -0,0 +1,12 @@
import axios from 'axios';
export const client = axios.create({
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'en-US,en;q=0.9'
},
timeout: 10000
});
export default client;

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"
}
]
}