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

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;