mirror of
https://github.com/shafat-96/anime-mapper
synced 2026-04-17 15:51:45 +00:00
fix
This commit is contained in:
@@ -14,7 +14,7 @@ export const ANILIST_QUERY = `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
export const HIANIME_URL = 'https://hianimez.to';
|
export const HIANIME_URL = 'https://hianime.to';
|
||||||
export const ANIZIP_URL = 'https://api.ani.zip/mappings';
|
export const ANIZIP_URL = 'https://api.ani.zip/mappings';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
173
src/extractors/kwik.js
Normal file
173
src/extractors/kwik.js
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const kwikUserAgent = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Mobile Safari/537.36";
|
||||||
|
|
||||||
|
export async function extractKwik(kwikUrl, referer) {
|
||||||
|
if (!kwikUrl) {
|
||||||
|
throw new Error("missing kwik URL");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const urlObj = new URL(kwikUrl);
|
||||||
|
// Always use the origin of the kwik URL as Referer, regardless of passed-in value
|
||||||
|
// mimicking: if u, err := url.Parse(kwikURL); err == nil { referer = u.Scheme + "://" + u.Host + "/" }
|
||||||
|
const refinedReferer = `${urlObj.protocol}//${urlObj.host}/`;
|
||||||
|
|
||||||
|
const response = await axios.get(kwikUrl, {
|
||||||
|
headers: {
|
||||||
|
'User-Agent': kwikUserAgent,
|
||||||
|
'Referer': refinedReferer,
|
||||||
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const html = response.data;
|
||||||
|
|
||||||
|
// Find the packed eval JS - look for eval(...) containing m3u8
|
||||||
|
const jsMatch = html.match(/;(eval\(function\(p,a,c,k,e,d\).*?m3u8.*?\)\))/);
|
||||||
|
if (!jsMatch || jsMatch.length < 2) {
|
||||||
|
throw new Error("could not find eval JS pattern in Kwik page");
|
||||||
|
}
|
||||||
|
|
||||||
|
const jsCode = jsMatch[1];
|
||||||
|
|
||||||
|
const lastBraceIdx = jsCode.lastIndexOf("}(");
|
||||||
|
if (lastBraceIdx === -1) {
|
||||||
|
throw new Error("could not find argument start marker '}('");
|
||||||
|
}
|
||||||
|
|
||||||
|
const endIdx = jsCode.lastIndexOf("))");
|
||||||
|
if (endIdx === -1 || endIdx <= lastBraceIdx) {
|
||||||
|
throw new Error("could not find argument end marker '))'");
|
||||||
|
}
|
||||||
|
|
||||||
|
const stripped = jsCode.substring(lastBraceIdx + 2, endIdx);
|
||||||
|
|
||||||
|
const parts = parsePackedArgs(stripped);
|
||||||
|
if (parts.length < 4) {
|
||||||
|
throw new Error(`invalid packed data: expected at least 4 parts, got ${parts.length}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const p = parts[0];
|
||||||
|
const a = parseInt(parts[1], 10);
|
||||||
|
const c = parseInt(parts[2], 10);
|
||||||
|
|
||||||
|
let kStr = parts[3];
|
||||||
|
kStr = kStr.replace(/\.split\(['"]\|['"]\)$/, "");
|
||||||
|
const k = kStr.split("|");
|
||||||
|
|
||||||
|
let decoded = unpackKwik(p, a, c, k);
|
||||||
|
|
||||||
|
decoded = decoded.replace(/\\/g, "");
|
||||||
|
decoded = decoded.replace("https.split(://", "https://");
|
||||||
|
decoded = decoded.replace("http.split(://", "http://");
|
||||||
|
|
||||||
|
const srcMatch = decoded.match(/source=(https?:\/\/[^;]+)/);
|
||||||
|
if (!srcMatch || srcMatch.length < 2) {
|
||||||
|
throw new Error("could not find video URL in unpacked code");
|
||||||
|
}
|
||||||
|
|
||||||
|
const videoURL = cleanKwikURL(srcMatch[1]);
|
||||||
|
return {
|
||||||
|
url: videoURL,
|
||||||
|
isM3U8: videoURL.includes(".m3u8"),
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function unpackKwik(p, a, c, k) {
|
||||||
|
const digits = "0123456789abcdefghijklmnopqrstuvwxyz";
|
||||||
|
const dict = {};
|
||||||
|
|
||||||
|
function baseEncode(n) {
|
||||||
|
const rem = n % a;
|
||||||
|
let digit;
|
||||||
|
if (rem > 35) {
|
||||||
|
digit = String.fromCharCode(rem + 29);
|
||||||
|
} else {
|
||||||
|
digit = digits[rem];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (n < a) {
|
||||||
|
return digit;
|
||||||
|
}
|
||||||
|
return baseEncode(Math.floor(n / a)) + digit;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = c - 1; i >= 0; i--) {
|
||||||
|
const key = baseEncode(i);
|
||||||
|
if (i < k.length && k[i] !== "") {
|
||||||
|
dict[key] = k[i];
|
||||||
|
} else {
|
||||||
|
dict[key] = key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use regex to replace words
|
||||||
|
return p.replace(/\b\w+\b/g, (w) => {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(dict, w)) {
|
||||||
|
return dict[w];
|
||||||
|
}
|
||||||
|
return w;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePackedArgs(input) {
|
||||||
|
const result = [];
|
||||||
|
let inQuote = false;
|
||||||
|
let quoteChar = null;
|
||||||
|
let depth = 0;
|
||||||
|
let current = "";
|
||||||
|
|
||||||
|
for (let i = 0; i < input.length; i++) {
|
||||||
|
const r = input[i];
|
||||||
|
|
||||||
|
if (!inQuote) {
|
||||||
|
if (r === '\'' || r === '"') {
|
||||||
|
inQuote = true;
|
||||||
|
quoteChar = r;
|
||||||
|
// Don't add quote to current, mimicking Go logic 'continue'
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (r === ',' && depth === 0) {
|
||||||
|
result.push(current.trim());
|
||||||
|
current = "";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (r === '(' || r === '[' || r === '{') {
|
||||||
|
depth++;
|
||||||
|
} else if (r === ')' || r === ']' || r === '}') {
|
||||||
|
if (depth > 0) {
|
||||||
|
depth--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (r === quoteChar) {
|
||||||
|
inQuote = false;
|
||||||
|
// Don't add quote to current
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current += r;
|
||||||
|
}
|
||||||
|
if (current !== "") {
|
||||||
|
result.push(current.trim());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanKwikURL(u) {
|
||||||
|
u = u.replace(/\\\//g, "/");
|
||||||
|
u = u.replace(/^["']|["']$/g, ''); // Trim quotes
|
||||||
|
u = u.replace(/[\n\r\t ]/g, ''); // Trim whitespace chars
|
||||||
|
|
||||||
|
// Remove semicolon and anything after it
|
||||||
|
const idx = u.indexOf(";");
|
||||||
|
if (idx !== -1) {
|
||||||
|
u = u.substring(0, idx);
|
||||||
|
}
|
||||||
|
return u;
|
||||||
|
}
|
||||||
124
src/extractors/megacloud.js
Normal file
124
src/extractors/megacloud.js
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import { client } from '../utils/client.js';
|
||||||
|
|
||||||
|
class MegaCloudExtractor {
|
||||||
|
constructor() {
|
||||||
|
this.mainUrl = "https://megacloud.blog";
|
||||||
|
this.scriptUrl = "https://script.google.com/macros/s/AKfycbxHbYHbrGMXYD2-bC-C43D3njIbU-wGiYQuJL61H4vyy6YVXkybMNNEPJNPPuZrD1gRVA/exec";
|
||||||
|
this.keysUrl = "https://raw.githubusercontent.com/yogesh-hacker/MegacloudKeys/refs/heads/main/keys.json";
|
||||||
|
}
|
||||||
|
|
||||||
|
async extract(videoUrl) {
|
||||||
|
try {
|
||||||
|
const embedUrl = new URL(videoUrl);
|
||||||
|
const headers = {
|
||||||
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0",
|
||||||
|
"Accept": "*/*",
|
||||||
|
"Accept-Language": "en-US,en;q=0.5",
|
||||||
|
"Origin": this.mainUrl,
|
||||||
|
"Referer": `${this.mainUrl}/`,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 1. Fetch Embed Page
|
||||||
|
const { data: html } = await client.get(videoUrl, { headers });
|
||||||
|
|
||||||
|
// 2. Extract Nonce
|
||||||
|
let nonce = null;
|
||||||
|
const match1 = html.match(/\b[a-zA-Z0-9]{48}\b/);
|
||||||
|
if (match1) {
|
||||||
|
nonce = match1[0];
|
||||||
|
} else {
|
||||||
|
const match2 = html.match(/\b([a-zA-Z0-9]{16})\b.*?\b([a-zA-Z0-9]{16})\b.*?\b([a-zA-Z0-9]{16})\b/);
|
||||||
|
if (match2) {
|
||||||
|
nonce = match2[1] + match2[2] + match2[3];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nonce) throw new Error("Nonce not found");
|
||||||
|
|
||||||
|
// 3. Get Sources
|
||||||
|
// e.g. https://megacloud.blog/embed-2/e-1/VJq4nDSaJyzH?k=1 -> ID: VJq4nDSaJyzH
|
||||||
|
const id = embedUrl.pathname.split('/').pop();
|
||||||
|
|
||||||
|
const apiUrl = `${this.mainUrl}/embed-2/v3/e-1/getSources?id=${id}&_k=${nonce}`;
|
||||||
|
const { data: response } = await client.get(apiUrl, {
|
||||||
|
headers: {
|
||||||
|
...headers,
|
||||||
|
"X-Requested-With": "XMLHttpRequest",
|
||||||
|
"Referer": this.mainUrl
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.sources || response.sources.length === 0) {
|
||||||
|
throw new Error("No sources found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const encodedFile = response.sources[0].file;
|
||||||
|
let m3u8Url = "";
|
||||||
|
|
||||||
|
if (encodedFile.includes(".m3u8")) {
|
||||||
|
m3u8Url = encodedFile;
|
||||||
|
} else {
|
||||||
|
// 4. Decrypt via Google Script
|
||||||
|
const { data: keyData } = await axios.get(this.keysUrl);
|
||||||
|
const secret = keyData.mega;
|
||||||
|
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.append("encrypted_data", encodedFile);
|
||||||
|
params.append("nonce", nonce);
|
||||||
|
params.append("secret", secret);
|
||||||
|
|
||||||
|
const decryptUrl = `${this.scriptUrl}?${params.toString()}`;
|
||||||
|
|
||||||
|
// Fetch text response
|
||||||
|
const { data: decryptedResponse } = await axios.get(decryptUrl, { responseType: 'text' });
|
||||||
|
|
||||||
|
// Kotlin Regex: "\"file\":\"(.*?)\""
|
||||||
|
// Handling potentially weird JSON structure or escaped strings
|
||||||
|
const textContent = typeof decryptedResponse === 'string' ? decryptedResponse : JSON.stringify(decryptedResponse);
|
||||||
|
const fileMatch = textContent.match(/"file":"(.*?)"/);
|
||||||
|
|
||||||
|
if (fileMatch && fileMatch[1]) {
|
||||||
|
// Clean up URL if needed (remove escape slashes)
|
||||||
|
m3u8Url = fileMatch[1].replace(/\\/g, '');
|
||||||
|
} else {
|
||||||
|
throw new Error("Video URL not found in decrypted response");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Build Result
|
||||||
|
const tracks = [];
|
||||||
|
if (response.tracks) {
|
||||||
|
response.tracks.forEach(track => {
|
||||||
|
if (track.kind === "captions" || track.kind === "subtitles") {
|
||||||
|
tracks.push({
|
||||||
|
url: track.file,
|
||||||
|
lang: track.label || track.kind,
|
||||||
|
label: track.label
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
sources: [{
|
||||||
|
url: m3u8Url,
|
||||||
|
isM3U8: true
|
||||||
|
}],
|
||||||
|
tracks: tracks,
|
||||||
|
intro: response.intro || { start: 0, end: 0 },
|
||||||
|
outro: response.outro || { start: 0, end: 0 },
|
||||||
|
headers: {
|
||||||
|
Referer: this.mainUrl,
|
||||||
|
"User-Agent": headers["User-Agent"]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("MegaCloud extraction failed:", error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const megaCloudExtractor = new MegaCloudExtractor();
|
||||||
86
src/index.js
86
src/index.js
@@ -1,6 +1,6 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { ANIME } from '@consumet/extensions';
|
|
||||||
import { mapAnilistToAnimePahe, mapAnilistToHiAnime, mapAnilistToAnimeKai } from './mappers/index.js';
|
import { mapAnilistToAnimePahe, mapAnilistToHiAnime, mapAnilistToAnimeKai } from './mappers/index.js';
|
||||||
|
import { AnimePahe } from './providers/animepahe.js';
|
||||||
import { AniList } from './providers/anilist.js';
|
import { AniList } from './providers/anilist.js';
|
||||||
import { AnimeKai } from './providers/animekai.js';
|
import { AnimeKai } from './providers/animekai.js';
|
||||||
import { getEpisodeServers, getEpisodeSources } from './providers/hianime-servers.js';
|
import { getEpisodeServers, getEpisodeSources } from './providers/hianime-servers.js';
|
||||||
@@ -34,11 +34,11 @@ app.get('/', (req, res) => {
|
|||||||
app.get('/animepahe/map/:anilistId', cache('5 minutes'), async (req, res) => {
|
app.get('/animepahe/map/:anilistId', cache('5 minutes'), async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { anilistId } = req.params;
|
const { anilistId } = req.params;
|
||||||
|
|
||||||
if (!anilistId) {
|
if (!anilistId) {
|
||||||
return res.status(400).json({ error: 'AniList ID is required' });
|
return res.status(400).json({ error: 'AniList ID is required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const mappingResult = await mapAnilistToAnimePahe(anilistId);
|
const mappingResult = await mapAnilistToAnimePahe(anilistId);
|
||||||
return res.json(mappingResult);
|
return res.json(mappingResult);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -51,11 +51,11 @@ app.get('/animepahe/map/:anilistId', cache('5 minutes'), async (req, res) => {
|
|||||||
app.get('/hianime/:anilistId', cache('5 minutes'), async (req, res) => {
|
app.get('/hianime/:anilistId', cache('5 minutes'), async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { anilistId } = req.params;
|
const { anilistId } = req.params;
|
||||||
|
|
||||||
if (!anilistId) {
|
if (!anilistId) {
|
||||||
return res.status(400).json({ error: 'AniList ID is required' });
|
return res.status(400).json({ error: 'AniList ID is required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const episodes = await mapAnilistToHiAnime(anilistId);
|
const episodes = await mapAnilistToHiAnime(anilistId);
|
||||||
return res.json(episodes);
|
return res.json(episodes);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -69,14 +69,14 @@ app.get('/hianime/servers/:animeId', cache('15 minutes'), async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const { animeId } = req.params;
|
const { animeId } = req.params;
|
||||||
const { ep } = req.query;
|
const { ep } = req.query;
|
||||||
|
|
||||||
if (!animeId) {
|
if (!animeId) {
|
||||||
return res.status(400).json({ error: 'Anime ID is required' });
|
return res.status(400).json({ error: 'Anime ID is required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Combine animeId and ep to form the expected episodeId format
|
// Combine animeId and ep to form the expected episodeId format
|
||||||
const episodeId = ep ? `${animeId}?ep=${ep}` : animeId;
|
const episodeId = ep ? `${animeId}?ep=${ep}` : animeId;
|
||||||
|
|
||||||
const servers = await getEpisodeServers(episodeId);
|
const servers = await getEpisodeServers(episodeId);
|
||||||
return res.json(servers);
|
return res.json(servers);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -90,26 +90,26 @@ app.get('/hianime/sources/:animeId', cache('15 minutes'), async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const { animeId } = req.params;
|
const { animeId } = req.params;
|
||||||
const { ep, server = 'vidstreaming', category = 'sub' } = req.query;
|
const { ep, server = 'vidstreaming', category = 'sub' } = req.query;
|
||||||
|
|
||||||
if (!animeId || !ep) {
|
if (!animeId || !ep) {
|
||||||
return res.status(400).json({ error: 'Both anime ID and episode number (ep) are required' });
|
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
|
// Combine animeId and ep to form the expected episodeId format
|
||||||
const episodeId = `${animeId}?ep=${ep}`;
|
const episodeId = `${animeId}?ep=${ep}`;
|
||||||
|
|
||||||
// Use our local extractor which supports MegaCloud
|
// Use our local extractor which supports MegaCloud
|
||||||
const sources = await getEpisodeSources(episodeId, server, category);
|
const sources = await getEpisodeSources(episodeId, server, category);
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: sources
|
data: sources
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('HiAnime sources error:', error.message);
|
console.error('HiAnime sources error:', error.message);
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: error.message
|
error: error.message
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -118,11 +118,11 @@ app.get('/hianime/sources/:animeId', cache('15 minutes'), async (req, res) => {
|
|||||||
app.get('/animekai/map/:anilistId', cache('5 minutes'), async (req, res) => {
|
app.get('/animekai/map/:anilistId', cache('5 minutes'), async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { anilistId } = req.params;
|
const { anilistId } = req.params;
|
||||||
|
|
||||||
if (!anilistId) {
|
if (!anilistId) {
|
||||||
return res.status(400).json({ error: 'AniList ID is required' });
|
return res.status(400).json({ error: 'AniList ID is required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const mappingResult = await mapAnilistToAnimeKai(anilistId);
|
const mappingResult = await mapAnilistToAnimeKai(anilistId);
|
||||||
return res.json(mappingResult);
|
return res.json(mappingResult);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -136,11 +136,11 @@ app.get('/animekai/sources/:episodeId', cache('15 minutes'), async (req, res) =>
|
|||||||
try {
|
try {
|
||||||
const { episodeId } = req.params;
|
const { episodeId } = req.params;
|
||||||
const { server, dub } = req.query;
|
const { server, dub } = req.query;
|
||||||
|
|
||||||
if (!episodeId) {
|
if (!episodeId) {
|
||||||
return res.status(400).json({ error: 'Episode ID is required' });
|
return res.status(400).json({ error: 'Episode ID is required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const animeKai = new AnimeKai();
|
const animeKai = new AnimeKai();
|
||||||
const isDub = dub === 'true' || dub === '1';
|
const isDub = dub === 'true' || dub === '1';
|
||||||
const sources = await animeKai.fetchEpisodeSources(episodeId, server, isDub);
|
const sources = await animeKai.fetchEpisodeSources(episodeId, server, isDub);
|
||||||
@@ -156,20 +156,20 @@ app.get('/animepahe/sources/:session/:episodeId', cache('15 minutes'), async (re
|
|||||||
try {
|
try {
|
||||||
const { session, episodeId } = req.params;
|
const { session, episodeId } = req.params;
|
||||||
const fullEpisodeId = `${session}/${episodeId}`;
|
const fullEpisodeId = `${session}/${episodeId}`;
|
||||||
|
|
||||||
// Initialize a new AnimePahe instance each time
|
// Initialize a new AnimePahe instance each time
|
||||||
const consumetAnimePahe = new ANIME.AnimePahe();
|
const animePahe = new AnimePahe();
|
||||||
|
|
||||||
// Directly fetch and return the sources without modification
|
// Directly fetch and return the sources without modification
|
||||||
const sources = await consumetAnimePahe.fetchEpisodeSources(fullEpisodeId);
|
const sources = await animePahe.fetchEpisodeSources(fullEpisodeId);
|
||||||
|
|
||||||
// Simply return the sources directly as provided by Consumet
|
// Simply return the sources directly as provided by Consumet
|
||||||
return res.status(200).json(sources);
|
return res.status(200).json(sources);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching episode sources:', error.message);
|
console.error('Error fetching episode sources:', error.message);
|
||||||
|
|
||||||
// Keep error handling simple
|
// Keep error handling simple
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
error: error.message,
|
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.'
|
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.'
|
||||||
});
|
});
|
||||||
@@ -180,24 +180,24 @@ app.get('/animepahe/sources/:session/:episodeId', cache('15 minutes'), async (re
|
|||||||
app.get('/animepahe/sources/:id', cache('15 minutes'), async (req, res) => {
|
app.get('/animepahe/sources/:id', cache('15 minutes'), async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const episodeId = req.params.id;
|
const episodeId = req.params.id;
|
||||||
|
|
||||||
if (!episodeId) {
|
if (!episodeId) {
|
||||||
return res.status(400).json({ error: 'Episode ID is required' });
|
return res.status(400).json({ error: 'Episode ID is required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize a new AnimePahe instance each time
|
// Initialize a new AnimePahe instance each time
|
||||||
const consumetAnimePahe = new ANIME.AnimePahe();
|
const animePahe = new AnimePahe();
|
||||||
|
|
||||||
// Directly fetch and return the sources without modification
|
// Directly fetch and return the sources without modification
|
||||||
const sources = await consumetAnimePahe.fetchEpisodeSources(episodeId);
|
const sources = await animePahe.fetchEpisodeSources(episodeId);
|
||||||
|
|
||||||
// Simply return the sources directly as provided by Consumet
|
// Simply return the sources directly as provided by Consumet
|
||||||
return res.status(200).json(sources);
|
return res.status(200).json(sources);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching episode sources:', error.message);
|
console.error('Error fetching episode sources:', error.message);
|
||||||
|
|
||||||
// Keep error handling simple
|
// Keep error handling simple
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
error: error.message,
|
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.'
|
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.'
|
||||||
});
|
});
|
||||||
@@ -208,18 +208,18 @@ app.get('/animepahe/sources/:id', cache('15 minutes'), async (req, res) => {
|
|||||||
app.get('/animepahe/hls/:anilistId/:episode', cache('15 minutes'), async (req, res) => {
|
app.get('/animepahe/hls/:anilistId/:episode', cache('15 minutes'), async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { anilistId, episode } = req.params;
|
const { anilistId, episode } = req.params;
|
||||||
|
|
||||||
if (!anilistId || !episode) {
|
if (!anilistId || !episode) {
|
||||||
return res.status(400).json({ error: 'Both AniList ID and episode number are required' });
|
return res.status(400).json({ error: 'Both AniList ID and episode number are required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// First, get the mapping from Anilist to AnimePahe
|
// First, get the mapping from Anilist to AnimePahe
|
||||||
const mappingResult = await mapAnilistToAnimePahe(anilistId);
|
const mappingResult = await mapAnilistToAnimePahe(anilistId);
|
||||||
|
|
||||||
if (!mappingResult.animepahe || !mappingResult.animepahe.episodes || mappingResult.animepahe.episodes.length === 0) {
|
if (!mappingResult.animepahe || !mappingResult.animepahe.episodes || mappingResult.animepahe.episodes.length === 0) {
|
||||||
return res.status(404).json({ error: 'No episodes found for this anime on AnimePahe' });
|
return res.status(404).json({ error: 'No episodes found for this anime on AnimePahe' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to find the episode with the exact number first (e.g., AnimePahe episode numbers)
|
// Try to find the episode with the exact number first (e.g., AnimePahe episode numbers)
|
||||||
let targetEpisode = mappingResult.animepahe.episodes.find(
|
let targetEpisode = mappingResult.animepahe.episodes.find(
|
||||||
ep => ep.number === parseInt(episode, 10)
|
ep => ep.number === parseInt(episode, 10)
|
||||||
@@ -237,11 +237,11 @@ app.get('/animepahe/hls/:anilistId/:episode', cache('15 minutes'), async (req, r
|
|||||||
if (!targetEpisode) {
|
if (!targetEpisode) {
|
||||||
return res.status(404).json({ error: `Episode ${episode} not found on AnimePahe` });
|
return res.status(404).json({ error: `Episode ${episode} not found on AnimePahe` });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now fetch the sources for this episode
|
// Now fetch the sources for this episode
|
||||||
const consumetAnimePahe = new ANIME.AnimePahe();
|
const animePahe = new AnimePahe();
|
||||||
const sources = await consumetAnimePahe.fetchEpisodeSources(targetEpisode.episodeId);
|
const sources = await animePahe.fetchEpisodeSources(targetEpisode.episodeId);
|
||||||
|
|
||||||
// Return the sources directly
|
// Return the sources directly
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
sources: sources,
|
sources: sources,
|
||||||
@@ -249,7 +249,7 @@ app.get('/animepahe/hls/:anilistId/:episode', cache('15 minutes'), async (req, r
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching HLS sources:', error.message);
|
console.error('Error fetching HLS sources:', error.message);
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
error: error.message,
|
error: error.message,
|
||||||
message: 'Failed to fetch HLS sources. If you receive a 403 error when accessing streaming URLs, add a Referer: "https://kwik.cx/" header to your requests.'
|
message: 'Failed to fetch HLS sources. If you receive a 403 error when accessing streaming URLs, add a Referer: "https://kwik.cx/" header to your requests.'
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,44 +1,27 @@
|
|||||||
import { AniList } from '../providers/anilist.js';
|
import { AniList } from '../providers/anilist.js';
|
||||||
import { AnimeKai } from '../providers/animekai.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) {
|
export async function mapAnilistToAnimeKai(anilistId) {
|
||||||
const mapper = new AnimeKaiMapper();
|
const mapper = new AnimeKaiMapper();
|
||||||
return await mapper.mapAnilistToAnimeKai(anilistId);
|
return await mapper.mapAnilistToAnimeKai(anilistId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Mapper class that provides mapping between Anilist and AnimeKai
|
|
||||||
*/
|
|
||||||
export class AnimeKaiMapper {
|
export class AnimeKaiMapper {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.anilist = new AniList();
|
this.anilist = new AniList();
|
||||||
this.animeKai = new AnimeKai();
|
this.animeKai = new AnimeKai();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Maps an Anilist anime to AnimeKai content
|
|
||||||
* @param {string|number} anilistId - The AniList ID to map
|
|
||||||
*/
|
|
||||||
async mapAnilistToAnimeKai(anilistId) {
|
async mapAnilistToAnimeKai(anilistId) {
|
||||||
try {
|
try {
|
||||||
// Get anime info from AniList
|
|
||||||
const animeInfo = await this.anilist.getAnimeInfo(parseInt(anilistId));
|
const animeInfo = await this.anilist.getAnimeInfo(parseInt(anilistId));
|
||||||
|
|
||||||
if (!animeInfo) {
|
if (!animeInfo) {
|
||||||
throw new Error(`Anime with id ${anilistId} not found on AniList`);
|
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;
|
const searchTitle = animeInfo.title.english || animeInfo.title.romaji || animeInfo.title.userPreferred;
|
||||||
if (!searchTitle) {
|
if (!searchTitle) {
|
||||||
throw new Error('No title available for the anime');
|
throw new Error('No title available for the anime');
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchResults = await this.animeKai.search(searchTitle);
|
const searchResults = await this.animeKai.search(searchTitle);
|
||||||
if (!searchResults || !searchResults.results || searchResults.results.length === 0) {
|
if (!searchResults || !searchResults.results || searchResults.results.length === 0) {
|
||||||
return {
|
return {
|
||||||
@@ -47,8 +30,6 @@ export class AnimeKaiMapper {
|
|||||||
animekai: null
|
animekai: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the best match from search results
|
|
||||||
const bestMatch = this.findBestMatch(searchTitle, animeInfo, searchResults.results);
|
const bestMatch = this.findBestMatch(searchTitle, animeInfo, searchResults.results);
|
||||||
if (!bestMatch) {
|
if (!bestMatch) {
|
||||||
return {
|
return {
|
||||||
@@ -57,10 +38,8 @@ export class AnimeKaiMapper {
|
|||||||
animekai: null
|
animekai: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get detailed info for the best match
|
|
||||||
const animeDetails = await this.animeKai.fetchAnimeInfo(bestMatch.id);
|
const animeDetails = await this.animeKai.fetchAnimeInfo(bestMatch.id);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: animeInfo.id,
|
id: animeInfo.id,
|
||||||
title: searchTitle,
|
title: searchTitle,
|
||||||
@@ -86,57 +65,37 @@ export class AnimeKaiMapper {
|
|||||||
throw 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) {
|
findBestMatch(searchTitle, animeInfo, results) {
|
||||||
if (!results || results.length === 0) return null;
|
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 normalizeTitle = title => title.toLowerCase().replace(/[^\w\s]/g, '').replace(/\s+/g, ' ').trim();
|
||||||
const normalizedSearch = normalizeTitle(searchTitle);
|
const normalizedSearch = normalizeTitle(searchTitle);
|
||||||
|
|
||||||
// Extract year from AniList title if present
|
|
||||||
let year = null;
|
let year = null;
|
||||||
if (animeInfo.startDate && animeInfo.startDate.year) {
|
if (animeInfo.startDate && animeInfo.startDate.year) {
|
||||||
year = animeInfo.startDate.year;
|
year = animeInfo.startDate.year;
|
||||||
} else if (animeInfo.seasonYear) {
|
} else if (animeInfo.seasonYear) {
|
||||||
year = animeInfo.seasonYear;
|
year = animeInfo.seasonYear;
|
||||||
}
|
}
|
||||||
|
|
||||||
// First try: find exact title match
|
|
||||||
for (const result of results) {
|
for (const result of results) {
|
||||||
const resultTitle = normalizeTitle(result.title);
|
const resultTitle = normalizeTitle(result.title);
|
||||||
const japaneseTitle = result.japaneseTitle ? normalizeTitle(result.japaneseTitle) : '';
|
const japaneseTitle = result.japaneseTitle ? normalizeTitle(result.japaneseTitle) : '';
|
||||||
|
|
||||||
if (resultTitle === normalizedSearch || japaneseTitle === normalizedSearch) {
|
if (resultTitle === normalizedSearch || japaneseTitle === normalizedSearch) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Second try: find partial match with proper episode count match
|
|
||||||
const expectedEpisodes = animeInfo.episodes || 0;
|
const expectedEpisodes = animeInfo.episodes || 0;
|
||||||
for (const result of results) {
|
for (const result of results) {
|
||||||
const resultTitle = normalizeTitle(result.title);
|
const resultTitle = normalizeTitle(result.title);
|
||||||
const japaneseTitle = result.japaneseTitle ? normalizeTitle(result.japaneseTitle) : '';
|
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 (result.episodes === expectedEpisodes && expectedEpisodes > 0) {
|
||||||
if (resultTitle.includes(normalizedSearch) ||
|
if (resultTitle.includes(normalizedSearch) ||
|
||||||
normalizedSearch.includes(resultTitle) ||
|
normalizedSearch.includes(resultTitle) ||
|
||||||
japaneseTitle.includes(normalizedSearch) ||
|
japaneseTitle.includes(normalizedSearch) ||
|
||||||
normalizedSearch.includes(japaneseTitle)) {
|
normalizedSearch.includes(japaneseTitle)) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Final fallback: just return the first result
|
|
||||||
return results[0];
|
return results[0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,49 +1,36 @@
|
|||||||
import { AniList } from '../providers/anilist.js';
|
import { AniList } from '../providers/anilist.js';
|
||||||
import { AnimePahe } from '../providers/animepahe.js';
|
import { AnimePahe } from '../providers/animepahe.js';
|
||||||
|
|
||||||
/**
|
|
||||||
* Maps an Anilist anime to AnimePahe content
|
|
||||||
*/
|
|
||||||
export async function mapAnilistToAnimePahe(anilistId) {
|
export async function mapAnilistToAnimePahe(anilistId) {
|
||||||
const mapper = new AnimepaheMapper();
|
const mapper = new AnimepaheMapper();
|
||||||
return await mapper.mapAnilistToAnimePahe(anilistId);
|
return await mapper.mapAnilistToAnimePahe(anilistId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Mapper class that provides mapping between Anilist and AnimePahe
|
|
||||||
*/
|
|
||||||
export class AnimepaheMapper {
|
export class AnimepaheMapper {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.anilist = new AniList();
|
this.anilist = new AniList();
|
||||||
this.animePahe = new AnimePahe();
|
this.animePahe = new AnimePahe();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Maps an Anilist anime to AnimePahe content
|
|
||||||
*/
|
|
||||||
async mapAnilistToAnimePahe(anilistId) {
|
async mapAnilistToAnimePahe(anilistId) {
|
||||||
try {
|
try {
|
||||||
// Get anime info from AniList
|
|
||||||
const animeInfo = await this.anilist.getAnimeInfo(parseInt(anilistId));
|
const animeInfo = await this.anilist.getAnimeInfo(parseInt(anilistId));
|
||||||
|
|
||||||
if (!animeInfo) {
|
if (!animeInfo) {
|
||||||
throw new Error(`Anime with id ${anilistId} not found on AniList`);
|
throw new Error(`Anime with id ${anilistId} not found on AniList`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to find matching content on AnimePahe
|
|
||||||
const bestMatch = await this.findAnimePaheMatch(animeInfo);
|
const bestMatch = await this.findAnimePaheMatch(animeInfo);
|
||||||
|
|
||||||
if (!bestMatch) {
|
if (!bestMatch) {
|
||||||
return {
|
return {
|
||||||
id: animeInfo.id,
|
id: animeInfo.id,
|
||||||
animepahe: null
|
animepahe: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get episode data for the matched anime
|
|
||||||
const episodeData = await this.getAnimePaheEpisodes(bestMatch);
|
const episodeData = await this.getAnimePaheEpisodes(bestMatch);
|
||||||
|
|
||||||
// Return the mapped result
|
|
||||||
return {
|
return {
|
||||||
id: animeInfo.id,
|
id: animeInfo.id,
|
||||||
animepahe: {
|
animepahe: {
|
||||||
@@ -65,20 +52,12 @@ export class AnimepaheMapper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds the matching AnimePahe content for an AniList anime
|
|
||||||
*/
|
|
||||||
async findAnimePaheMatch(animeInfo) {
|
async findAnimePaheMatch(animeInfo) {
|
||||||
// Only use one primary title to reduce API calls
|
|
||||||
let bestTitle = animeInfo.title.romaji || animeInfo.title.english || animeInfo.title.userPreferred;
|
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);
|
const searchResults = await this.animePahe.scrapeSearchResults(bestTitle);
|
||||||
|
|
||||||
// Process results if we found any
|
|
||||||
if (searchResults && searchResults.length > 0) {
|
if (searchResults && searchResults.length > 0) {
|
||||||
// First try direct ID match (fastest path)
|
|
||||||
const rawId = animeInfo.id.toString();
|
const rawId = animeInfo.id.toString();
|
||||||
for (const result of searchResults) {
|
for (const result of searchResults) {
|
||||||
const resultId = (result.id || '').split('-')[0];
|
const resultId = (result.id || '').split('-')[0];
|
||||||
@@ -86,52 +65,40 @@ export class AnimepaheMapper {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no direct ID match, find the best match with our algorithm
|
|
||||||
return this.findBestMatchFromResults(animeInfo, searchResults);
|
return this.findBestMatchFromResults(animeInfo, searchResults);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no results found, try a fallback search with a more generic title
|
|
||||||
const genericTitle = this.getGenericTitle(animeInfo);
|
const genericTitle = this.getGenericTitle(animeInfo);
|
||||||
|
|
||||||
if (genericTitle && genericTitle !== bestTitle) {
|
if (genericTitle && genericTitle !== bestTitle) {
|
||||||
const fallbackResults = await this.animePahe.scrapeSearchResults(genericTitle);
|
const fallbackResults = await this.animePahe.scrapeSearchResults(genericTitle);
|
||||||
|
|
||||||
if (fallbackResults && fallbackResults.length > 0) {
|
if (fallbackResults && fallbackResults.length > 0) {
|
||||||
return this.findBestMatchFromResults(animeInfo, fallbackResults);
|
return this.findBestMatchFromResults(animeInfo, fallbackResults);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Find the best match from available search results
|
|
||||||
*/
|
|
||||||
findBestMatchFromResults(animeInfo, results) {
|
findBestMatchFromResults(animeInfo, results) {
|
||||||
if (!results || results.length === 0) return null;
|
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 normalizeTitle = t => t.toLowerCase().replace(/[^\w\s]/g, '').replace(/\s+/g, ' ').trim();
|
||||||
const anilistTitles = [
|
const anilistTitles = [
|
||||||
animeInfo.title.romaji,
|
animeInfo.title.romaji,
|
||||||
animeInfo.title.english,
|
animeInfo.title.english,
|
||||||
animeInfo.title.userPreferred
|
animeInfo.title.userPreferred
|
||||||
].filter(Boolean).map(normalizeTitle);
|
].filter(Boolean).map(normalizeTitle);
|
||||||
|
|
||||||
// Prepare year information
|
const anilistYear =
|
||||||
const anilistYear =
|
(animeInfo.startDate && animeInfo.startDate.year) ?
|
||||||
(animeInfo.startDate && animeInfo.startDate.year) ?
|
animeInfo.startDate.year : animeInfo.seasonYear;
|
||||||
animeInfo.startDate.year : animeInfo.seasonYear;
|
|
||||||
|
|
||||||
const animeYear = anilistYear || this.extractYearFromTitle(animeInfo);
|
const animeYear = anilistYear || this.extractYearFromTitle(animeInfo);
|
||||||
|
|
||||||
// Process matches sequentially with early returns
|
|
||||||
let bestMatch = null;
|
let bestMatch = null;
|
||||||
|
|
||||||
// Try exact title match with year (highest priority)
|
|
||||||
if (animeYear) {
|
if (animeYear) {
|
||||||
// Find matches with exact year
|
|
||||||
const yearMatches = [];
|
const yearMatches = [];
|
||||||
for (const result of results) {
|
for (const result of results) {
|
||||||
const resultYear = result.year ? parseInt(result.year) : this.extractYearFromTitle(result);
|
const resultYear = result.year ? parseInt(result.year) : this.extractYearFromTitle(result);
|
||||||
@@ -139,80 +106,69 @@ export class AnimepaheMapper {
|
|||||||
yearMatches.push(result);
|
yearMatches.push(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we have year matches, try to find the best title match among them
|
|
||||||
if (yearMatches.length > 0) {
|
if (yearMatches.length > 0) {
|
||||||
for (const match of yearMatches) {
|
for (const match of yearMatches) {
|
||||||
const resultTitle = normalizeTitle(match.title || match.name);
|
const resultTitle = normalizeTitle(match.title || match.name);
|
||||||
|
|
||||||
// First try: exact title match with year
|
|
||||||
for (const title of anilistTitles) {
|
for (const title of anilistTitles) {
|
||||||
if (!title) continue;
|
if (!title) continue;
|
||||||
|
|
||||||
if (resultTitle === title ||
|
if (resultTitle === title ||
|
||||||
(resultTitle.includes(title) && title.length > 7) ||
|
(resultTitle.includes(title) && title.length > 7) ||
|
||||||
(title.includes(resultTitle) && resultTitle.length > 7)) {
|
(title.includes(resultTitle) && resultTitle.length > 7)) {
|
||||||
return match; // Early return for best match
|
return match;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Second try: high similarity title match with year
|
|
||||||
for (const title of anilistTitles) {
|
for (const title of anilistTitles) {
|
||||||
if (!title) continue;
|
if (!title) continue;
|
||||||
|
|
||||||
const similarity = this.calculateTitleSimilarity(title, resultTitle);
|
const similarity = this.calculateTitleSimilarity(title, resultTitle);
|
||||||
if (similarity > 0.5) {
|
if (similarity > 0.5) {
|
||||||
bestMatch = match;
|
bestMatch = match;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bestMatch) break;
|
if (bestMatch) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we found a title similarity match with year, return it
|
|
||||||
if (bestMatch) return bestMatch;
|
if (bestMatch) return bestMatch;
|
||||||
|
|
||||||
// Otherwise use the first year match as a fallback
|
|
||||||
return yearMatches[0];
|
return yearMatches[0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try exact title match
|
|
||||||
for (const result of results) {
|
for (const result of results) {
|
||||||
const resultTitle = normalizeTitle(result.title || result.name);
|
const resultTitle = normalizeTitle(result.title || result.name);
|
||||||
|
|
||||||
for (const title of anilistTitles) {
|
for (const title of anilistTitles) {
|
||||||
if (!title) continue;
|
if (!title) continue;
|
||||||
|
|
||||||
if (resultTitle === title) {
|
if (resultTitle === title) {
|
||||||
return result; // Early return for exact title match
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try high similarity title match
|
|
||||||
bestMatch = this.findBestSimilarityMatch(anilistTitles, results);
|
bestMatch = this.findBestSimilarityMatch(anilistTitles, results);
|
||||||
if (bestMatch) return bestMatch;
|
if (bestMatch) return bestMatch;
|
||||||
|
|
||||||
// Just use the first result as a fallback
|
|
||||||
return results[0];
|
return results[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Find the best match based on title similarity
|
|
||||||
*/
|
|
||||||
findBestSimilarityMatch(titles, results) {
|
findBestSimilarityMatch(titles, results) {
|
||||||
const normalizeTitle = t => t.toLowerCase().replace(/[^\w\s]/g, '').replace(/\s+/g, ' ').trim();
|
const normalizeTitle = t => t.toLowerCase().replace(/[^\w\s]/g, '').replace(/\s+/g, ' ').trim();
|
||||||
let bestMatch = null;
|
let bestMatch = null;
|
||||||
let highestSimilarity = 0;
|
let highestSimilarity = 0;
|
||||||
|
|
||||||
for (const result of results) {
|
for (const result of results) {
|
||||||
const resultTitle = normalizeTitle(result.title || result.name);
|
const resultTitle = normalizeTitle(result.title || result.name);
|
||||||
|
|
||||||
for (const title of titles) {
|
for (const title of titles) {
|
||||||
if (!title) continue;
|
if (!title) continue;
|
||||||
|
|
||||||
const similarity = this.calculateTitleSimilarity(title, resultTitle);
|
const similarity = this.calculateTitleSimilarity(title, resultTitle);
|
||||||
if (similarity > highestSimilarity) {
|
if (similarity > highestSimilarity) {
|
||||||
highestSimilarity = similarity;
|
highestSimilarity = similarity;
|
||||||
@@ -220,14 +176,10 @@ export class AnimepaheMapper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only return if we have a reasonably good match
|
|
||||||
return highestSimilarity > 0.6 ? bestMatch : null;
|
return highestSimilarity > 0.6 ? bestMatch : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the AnimePahe episodes for a match
|
|
||||||
*/
|
|
||||||
async getAnimePaheEpisodes(match) {
|
async getAnimePaheEpisodes(match) {
|
||||||
try {
|
try {
|
||||||
const episodeData = await this.animePahe.scrapeEpisodes(match.id);
|
const episodeData = await this.animePahe.scrapeEpisodes(match.id);
|
||||||
@@ -240,82 +192,62 @@ export class AnimepaheMapper {
|
|||||||
return { totalEpisodes: 0, episodes: [] };
|
return { totalEpisodes: 0, episodes: [] };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate similarity between two titles
|
|
||||||
*/
|
|
||||||
calculateTitleSimilarity(title1, title2) {
|
calculateTitleSimilarity(title1, title2) {
|
||||||
if (!title1 || !title2) return 0;
|
if (!title1 || !title2) return 0;
|
||||||
|
|
||||||
// Normalize both titles
|
|
||||||
const norm1 = title1.toLowerCase().replace(/[^\w\s]/g, '').replace(/\s+/g, ' ').trim();
|
const norm1 = title1.toLowerCase().replace(/[^\w\s]/g, '').replace(/\s+/g, ' ').trim();
|
||||||
const norm2 = title2.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;
|
if (norm1 === norm2) return 1;
|
||||||
|
|
||||||
// Split into words
|
|
||||||
const words1 = norm1.split(' ').filter(Boolean);
|
const words1 = norm1.split(' ').filter(Boolean);
|
||||||
const words2 = norm2.split(' ').filter(Boolean);
|
const words2 = norm2.split(' ').filter(Boolean);
|
||||||
|
|
||||||
// Count common words
|
|
||||||
const commonCount = words1.filter(w => words2.includes(w)).length;
|
const commonCount = words1.filter(w => words2.includes(w)).length;
|
||||||
|
|
||||||
// Weight by percentage of common words
|
|
||||||
return commonCount * 2 / (words1.length + words2.length);
|
return commonCount * 2 / (words1.length + words2.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract year from title (e.g., "JoJo's Bizarre Adventure (2012)" -> 2012)
|
|
||||||
*/
|
|
||||||
extractYearFromTitle(item) {
|
extractYearFromTitle(item) {
|
||||||
if (!item) return null;
|
if (!item) return null;
|
||||||
|
|
||||||
// Extract the title string based on the input type
|
|
||||||
let titleStr = '';
|
let titleStr = '';
|
||||||
if (typeof item === 'string') {
|
if (typeof item === 'string') {
|
||||||
titleStr = item;
|
titleStr = item;
|
||||||
} else if (typeof item === 'object') {
|
} else if (typeof item === 'object') {
|
||||||
// Handle both anime objects and result objects
|
|
||||||
if (item.title) {
|
if (item.title) {
|
||||||
if (typeof item.title === 'string') {
|
if (typeof item.title === 'string') {
|
||||||
titleStr = item.title;
|
titleStr = item.title;
|
||||||
} else if (typeof item.title === 'object') {
|
} else if (typeof item.title === 'object') {
|
||||||
// AniList title object
|
|
||||||
titleStr = item.title.userPreferred || item.title.english || item.title.romaji || '';
|
titleStr = item.title.userPreferred || item.title.english || item.title.romaji || '';
|
||||||
}
|
}
|
||||||
} else if (item.name) {
|
} else if (item.name) {
|
||||||
titleStr = item.name;
|
titleStr = item.name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!titleStr) return null;
|
if (!titleStr) return null;
|
||||||
|
|
||||||
// Look for year pattern in parentheses or brackets
|
|
||||||
const yearMatches = titleStr.match(/[\(\[](\d{4})[\)\]]/);
|
const yearMatches = titleStr.match(/[\(\[](\d{4})[\)\]]/);
|
||||||
|
|
||||||
if (yearMatches && yearMatches[1]) {
|
if (yearMatches && yearMatches[1]) {
|
||||||
const year = parseInt(yearMatches[1]);
|
const year = parseInt(yearMatches[1]);
|
||||||
if (!isNaN(year) && year > 1950 && year <= new Date().getFullYear()) {
|
if (!isNaN(year) && year > 1950 && year <= new Date().getFullYear()) {
|
||||||
return year;
|
return year;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a generic title by removing year information and other specific identifiers
|
|
||||||
*/
|
|
||||||
getGenericTitle(animeInfo) {
|
getGenericTitle(animeInfo) {
|
||||||
if (!animeInfo || !animeInfo.title) return null;
|
if (!animeInfo || !animeInfo.title) return null;
|
||||||
|
|
||||||
const title = animeInfo.title.english || animeInfo.title.romaji || animeInfo.title.userPreferred;
|
const title = animeInfo.title.english || animeInfo.title.romaji || animeInfo.title.userPreferred;
|
||||||
if (!title) return null;
|
if (!title) return null;
|
||||||
|
|
||||||
// Remove year information and common specifiers
|
|
||||||
return title.replace(/\([^)]*\d{4}[^)]*\)/g, '').replace(/\[[^\]]*\d{4}[^\]]*\]/g, '').trim();
|
return title.replace(/\([^)]*\d{4}[^)]*\)/g, '').replace(/\[[^\]]*\d{4}[^\]]*\]/g, '').trim();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default mapAnilistToAnimePahe;
|
export default mapAnilistToAnimePahe;
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import * as cheerio from 'cheerio';
|
import * as cheerio from 'cheerio';
|
||||||
|
import { extractKwik } from '../extractors/kwik.js';
|
||||||
|
|
||||||
export class AnimePahe {
|
export class AnimePahe {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.baseUrl = "https://animepahe.ru";
|
this.baseUrl = "https://animepahe.si";
|
||||||
this.sourceName = 'AnimePahe';
|
this.sourceName = 'AnimePahe';
|
||||||
this.isMulti = false;
|
this.isMulti = false;
|
||||||
}
|
}
|
||||||
@@ -15,7 +16,7 @@ export class AnimePahe {
|
|||||||
'Cookie': "__ddg1_=;__ddg2_=;",
|
'Cookie': "__ddg1_=;__ddg2_=;",
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const jsonResult = response.data;
|
const jsonResult = response.data;
|
||||||
const searchResults = [];
|
const searchResults = [];
|
||||||
|
|
||||||
@@ -36,9 +37,9 @@ export class AnimePahe {
|
|||||||
score: item.score || 0,
|
score: item.score || 0,
|
||||||
poster: item.poster,
|
poster: item.poster,
|
||||||
session: item.session,
|
session: item.session,
|
||||||
episodes: {
|
episodes: {
|
||||||
sub: item.episodes || null,
|
sub: item.episodes || null,
|
||||||
dub: '??'
|
dub: '??'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -54,16 +55,16 @@ export class AnimePahe {
|
|||||||
try {
|
try {
|
||||||
const title = url.split('-')[1];
|
const title = url.split('-')[1];
|
||||||
const id = url.split('-')[0];
|
const id = url.split('-')[0];
|
||||||
|
|
||||||
const session = await this._getSession(title, id);
|
const session = await this._getSession(title, id);
|
||||||
const epUrl = `${this.baseUrl}/api?m=release&id=${session}&sort=episode_desc&page=1`;
|
const epUrl = `${this.baseUrl}/api?m=release&id=${session}&sort=episode_desc&page=1`;
|
||||||
|
|
||||||
const response = await axios.get(epUrl, {
|
const response = await axios.get(epUrl, {
|
||||||
headers: {
|
headers: {
|
||||||
'Cookie': "__ddg1_=;__ddg2_=;",
|
'Cookie': "__ddg1_=;__ddg2_=;",
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return await this._recursiveFetchEpisodes(epUrl, JSON.stringify(response.data), session);
|
return await this._recursiveFetchEpisodes(epUrl, JSON.stringify(response.data), session);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching episodes:', error.message);
|
console.error('Error fetching episodes:', error.message);
|
||||||
@@ -102,38 +103,34 @@ export class AnimePahe {
|
|||||||
'Cookie': "__ddg1_=;__ddg2_=;",
|
'Cookie': "__ddg1_=;__ddg2_=;",
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const moreEpisodes = await this._recursiveFetchEpisodes(newUrl, JSON.stringify(newResponse.data), session);
|
const moreEpisodes = await this._recursiveFetchEpisodes(newUrl, JSON.stringify(newResponse.data), session);
|
||||||
episodes = [...episodes, ...moreEpisodes.episodes];
|
episodes = [...episodes, ...moreEpisodes.episodes];
|
||||||
animeTitle = moreEpisodes.title;
|
animeTitle = moreEpisodes.title;
|
||||||
animeDetails = moreEpisodes.details || animeDetails;
|
animeDetails = moreEpisodes.details || animeDetails;
|
||||||
} else {
|
} else {
|
||||||
const detailUrl = `https://animepahe.ru/a/${jsonResult.data[0].anime_id}`;
|
const detailUrl = `https://animepahe.si/a/${jsonResult.data[0].anime_id}`;
|
||||||
const newResponse = await axios.get(detailUrl, {
|
const newResponse = await axios.get(detailUrl, {
|
||||||
headers: {
|
headers: {
|
||||||
'Cookie': "__ddg1_=;__ddg2_=;",
|
'Cookie': "__ddg1_=;__ddg2_=;",
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (newResponse.status === 200) {
|
if (newResponse.status === 200) {
|
||||||
const $ = cheerio.load(newResponse.data);
|
const $ = cheerio.load(newResponse.data);
|
||||||
animeTitle = $('.title-wrapper span').text().trim() || 'Could not fetch title';
|
animeTitle = $('.title-wrapper span').text().trim() || 'Could not fetch title';
|
||||||
|
|
||||||
// Try to extract additional information
|
|
||||||
try {
|
try {
|
||||||
// Parse type
|
|
||||||
const typeText = $('.col-sm-4.anime-info p:contains("Type")').text();
|
const typeText = $('.col-sm-4.anime-info p:contains("Type")').text();
|
||||||
if (typeText) {
|
if (typeText) {
|
||||||
animeDetails.type = typeText.replace('Type:', '').trim();
|
animeDetails.type = typeText.replace('Type:', '').trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse status
|
|
||||||
const statusText = $('.col-sm-4.anime-info p:contains("Status")').text();
|
const statusText = $('.col-sm-4.anime-info p:contains("Status")').text();
|
||||||
if (statusText) {
|
if (statusText) {
|
||||||
animeDetails.status = statusText.replace('Status:', '').trim();
|
animeDetails.status = statusText.replace('Status:', '').trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse season and year
|
|
||||||
const seasonText = $('.col-sm-4.anime-info p:contains("Season")').text();
|
const seasonText = $('.col-sm-4.anime-info p:contains("Season")').text();
|
||||||
if (seasonText) {
|
if (seasonText) {
|
||||||
const seasonMatch = seasonText.match(/Season:\s+(\w+)\s+(\d{4})/);
|
const seasonMatch = seasonText.match(/Season:\s+(\w+)\s+(\d{4})/);
|
||||||
@@ -142,8 +139,7 @@ export class AnimePahe {
|
|||||||
animeDetails.year = parseInt(seasonMatch[2]);
|
animeDetails.year = parseInt(seasonMatch[2]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse score
|
|
||||||
const scoreText = $('.col-sm-4.anime-info p:contains("Score")').text();
|
const scoreText = $('.col-sm-4.anime-info p:contains("Score")').text();
|
||||||
if (scoreText) {
|
if (scoreText) {
|
||||||
const scoreMatch = scoreText.match(/Score:\s+([\d.]+)/);
|
const scoreMatch = scoreText.match(/Score:\s+([\d.]+)/);
|
||||||
@@ -157,7 +153,6 @@ export class AnimePahe {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always sort episodes by number in ascending order, regardless of how the API returns them
|
|
||||||
const sortedEpisodes = [...episodes].sort((a, b) => a.number - b.number);
|
const sortedEpisodes = [...episodes].sort((a, b) => a.number - b.number);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -165,7 +160,7 @@ export class AnimePahe {
|
|||||||
session: session,
|
session: session,
|
||||||
totalEpisodes: jsonResult.total,
|
totalEpisodes: jsonResult.total,
|
||||||
details: animeDetails,
|
details: animeDetails,
|
||||||
episodes: sortedEpisodes, // Return sorted episodes, always in ascending order
|
episodes: sortedEpisodes,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error recursively fetching episodes:', error.message);
|
console.error('Error recursively fetching episodes:', error.message);
|
||||||
@@ -173,14 +168,18 @@ export class AnimePahe {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async scrapeEpisodesSrcs(episodeUrl, { category, lang } = {}) {
|
async fetchEpisodeSources(episodeId, options = {}) {
|
||||||
|
return this.scrapeEpisodesSrcs(episodeId, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async scrapeEpisodesSrcs(episodeId, { category, lang } = {}) {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`${this.baseUrl}/play/${episodeUrl}`, {
|
const response = await axios.get(`${this.baseUrl}/play/${episodeId}`, {
|
||||||
headers: {
|
headers: {
|
||||||
'Cookie': "__ddg1_=;__ddg2_=;",
|
'Cookie': "__ddg1_=;__ddg2_=;",
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const $ = cheerio.load(response.data);
|
const $ = cheerio.load(response.data);
|
||||||
const buttons = $('#resolutionMenu > button');
|
const buttons = $('#resolutionMenu > button');
|
||||||
const videoLinks = [];
|
const videoLinks = [];
|
||||||
@@ -189,21 +188,27 @@ export class AnimePahe {
|
|||||||
const btn = buttons[i];
|
const btn = buttons[i];
|
||||||
const kwikLink = $(btn).attr('data-src');
|
const kwikLink = $(btn).attr('data-src');
|
||||||
const quality = $(btn).text();
|
const quality = $(btn).text();
|
||||||
|
|
||||||
// Instead of extracting, just return the link directly
|
try {
|
||||||
videoLinks.push({
|
const extraction = await extractKwik(kwikLink, response.config.url);
|
||||||
quality: quality,
|
if (extraction && extraction.url) {
|
||||||
url: kwikLink,
|
videoLinks.push({
|
||||||
referer: "https://kwik.cx",
|
quality: quality,
|
||||||
});
|
url: extraction.url,
|
||||||
|
isM3U8: extraction.isM3U8,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Error extracting Kwik for ${quality}:`, e.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = {
|
return {
|
||||||
sources: videoLinks.length > 0 ? [{ url: videoLinks[0].url }] : [],
|
headers: {
|
||||||
multiSrc: videoLinks,
|
Referer: "https://kwik.cx/"
|
||||||
|
},
|
||||||
|
sources: videoLinks
|
||||||
};
|
};
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching episode sources:', error.message);
|
console.error('Error fetching episode sources:', error.message);
|
||||||
throw new Error('Failed to fetch episode sources');
|
throw new Error('Failed to fetch episode sources');
|
||||||
@@ -217,62 +222,55 @@ export class AnimePahe {
|
|||||||
'Cookie': "__ddg1_=;__ddg2_=;",
|
'Cookie': "__ddg1_=;__ddg2_=;",
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const resBody = response.data;
|
const resBody = response.data;
|
||||||
if (!resBody.data || resBody.data.length === 0) {
|
if (!resBody.data || resBody.data.length === 0) {
|
||||||
throw new Error(`No results found for title: ${title}`);
|
throw new Error(`No results found for title: ${title}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// First try: Direct ID match if provided and valid
|
|
||||||
if (animeId) {
|
if (animeId) {
|
||||||
const animeIdMatch = resBody.data.find(anime => String(anime.id) === String(animeId));
|
const animeIdMatch = resBody.data.find(anime => String(anime.id) === String(animeId));
|
||||||
if (animeIdMatch) {
|
if (animeIdMatch) {
|
||||||
return animeIdMatch.session;
|
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 normalizeTitle = t => t.toLowerCase().replace(/[^\w\s]/g, '').replace(/\s+/g, ' ').trim();
|
||||||
const normalizedSearchTitle = normalizeTitle(title);
|
const normalizedSearchTitle = normalizeTitle(title);
|
||||||
|
|
||||||
let bestMatch = null;
|
let bestMatch = null;
|
||||||
let highestSimilarity = 0;
|
let highestSimilarity = 0;
|
||||||
|
|
||||||
for (const anime of resBody.data) {
|
for (const anime of resBody.data) {
|
||||||
const normalizedAnimeTitle = normalizeTitle(anime.title);
|
const normalizedAnimeTitle = normalizeTitle(anime.title);
|
||||||
// Calculate simple similarity (more sophisticated than exact match)
|
|
||||||
let similarity = 0;
|
let similarity = 0;
|
||||||
|
|
||||||
// Exact match
|
|
||||||
if (normalizedAnimeTitle === normalizedSearchTitle) {
|
if (normalizedAnimeTitle === normalizedSearchTitle) {
|
||||||
similarity = 1;
|
similarity = 1;
|
||||||
}
|
}
|
||||||
// Contains match
|
else if (normalizedAnimeTitle.includes(normalizedSearchTitle) ||
|
||||||
else if (normalizedAnimeTitle.includes(normalizedSearchTitle) ||
|
normalizedSearchTitle.includes(normalizedAnimeTitle)) {
|
||||||
normalizedSearchTitle.includes(normalizedAnimeTitle)) {
|
const lengthRatio = Math.min(normalizedAnimeTitle.length, normalizedSearchTitle.length) /
|
||||||
const lengthRatio = Math.min(normalizedAnimeTitle.length, normalizedSearchTitle.length) /
|
Math.max(normalizedAnimeTitle.length, normalizedSearchTitle.length);
|
||||||
Math.max(normalizedAnimeTitle.length, normalizedSearchTitle.length);
|
|
||||||
similarity = 0.8 * lengthRatio;
|
similarity = 0.8 * lengthRatio;
|
||||||
}
|
}
|
||||||
// Word match
|
|
||||||
else {
|
else {
|
||||||
const searchWords = normalizedSearchTitle.split(' ');
|
const searchWords = normalizedSearchTitle.split(' ');
|
||||||
const animeWords = normalizedAnimeTitle.split(' ');
|
const animeWords = normalizedAnimeTitle.split(' ');
|
||||||
const commonWords = searchWords.filter(word => animeWords.includes(word));
|
const commonWords = searchWords.filter(word => animeWords.includes(word));
|
||||||
similarity = commonWords.length / Math.max(searchWords.length, animeWords.length);
|
similarity = commonWords.length / Math.max(searchWords.length, animeWords.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (similarity > highestSimilarity) {
|
if (similarity > highestSimilarity) {
|
||||||
highestSimilarity = similarity;
|
highestSimilarity = similarity;
|
||||||
bestMatch = anime;
|
bestMatch = anime;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bestMatch && highestSimilarity > 0.5) {
|
if (bestMatch && highestSimilarity > 0.5) {
|
||||||
return bestMatch.session;
|
return bestMatch.session;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default to first result if no good match found
|
|
||||||
return resBody.data[0].session;
|
return resBody.data[0].session;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting session:', error.message);
|
console.error('Error getting session:', error.message);
|
||||||
@@ -281,4 +279,4 @@ export class AnimePahe {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AnimePahe;
|
export default AnimePahe;
|
||||||
@@ -1,12 +1,9 @@
|
|||||||
import { load } from 'cheerio';
|
import { load } from 'cheerio';
|
||||||
import { client } from '../utils/client.js';
|
import { client } from '../utils/client.js';
|
||||||
import { HIANIME_URL } from '../constants/api-constants.js';
|
import { HIANIME_URL } from '../constants/api-constants.js';
|
||||||
|
import { megaCloudExtractor } from '../extractors/megacloud.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) {
|
export async function getEpisodeServers(episodeId) {
|
||||||
const result = {
|
const result = {
|
||||||
sub: [],
|
sub: [],
|
||||||
@@ -23,7 +20,7 @@ export async function getEpisodeServers(episodeId) {
|
|||||||
|
|
||||||
const epId = episodeId.split("?ep=")[1];
|
const epId = episodeId.split("?ep=")[1];
|
||||||
const ajaxUrl = `${HIANIME_URL}/ajax/v2/episode/servers?episodeId=${epId}`;
|
const ajaxUrl = `${HIANIME_URL}/ajax/v2/episode/servers?episodeId=${epId}`;
|
||||||
|
|
||||||
const { data } = await client.get(ajaxUrl, {
|
const { data } = await client.get(ajaxUrl, {
|
||||||
headers: {
|
headers: {
|
||||||
"X-Requested-With": "XMLHttpRequest",
|
"X-Requested-With": "XMLHttpRequest",
|
||||||
@@ -71,14 +68,6 @@ export async function getEpisodeServers(episodeId) {
|
|||||||
throw error;
|
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') {
|
export async function getEpisodeSources(episodeId, serverName = 'vidstreaming', category = 'sub') {
|
||||||
try {
|
try {
|
||||||
if (!episodeId || episodeId.trim() === "" || episodeId.indexOf("?ep=") === -1) {
|
if (!episodeId || episodeId.trim() === "" || episodeId.indexOf("?ep=") === -1) {
|
||||||
@@ -87,18 +76,18 @@ export async function getEpisodeSources(episodeId, serverName = 'vidstreaming',
|
|||||||
|
|
||||||
// First get available servers
|
// First get available servers
|
||||||
const servers = await getEpisodeServers(episodeId);
|
const servers = await getEpisodeServers(episodeId);
|
||||||
|
|
||||||
// Find the requested server
|
// Find the requested server
|
||||||
const serverList = servers[category] || [];
|
const serverList = servers[category] || [];
|
||||||
const server = serverList.find(s => s.serverName.toLowerCase() === serverName.toLowerCase());
|
const server = serverList.find(s => s.serverName.toLowerCase() === serverName.toLowerCase());
|
||||||
|
|
||||||
if (!server) {
|
if (!server) {
|
||||||
throw new Error(`Server '${serverName}' not found for category '${category}'`);
|
throw new Error(`Server '${serverName}' not found for category '${category}'`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const epId = episodeId.split("?ep=")[1];
|
const epId = episodeId.split("?ep=")[1];
|
||||||
const serverId = server.serverId;
|
const serverId = server.serverId;
|
||||||
|
|
||||||
// Fetch the source URL
|
// Fetch the source URL
|
||||||
const { data } = await client.get(
|
const { data } = await client.get(
|
||||||
`${HIANIME_URL}/ajax/v2/episode/sources?id=${epId}&server=${serverId}`,
|
`${HIANIME_URL}/ajax/v2/episode/sources?id=${epId}&server=${serverId}`,
|
||||||
@@ -112,15 +101,18 @@ export async function getEpisodeSources(episodeId, serverName = 'vidstreaming',
|
|||||||
|
|
||||||
// If the target is a MegaCloud embed, extract the direct source URL
|
// If the target is a MegaCloud embed, extract the direct source URL
|
||||||
if (data?.link && /megacloud\./i.test(data.link)) {
|
if (data?.link && /megacloud\./i.test(data.link)) {
|
||||||
const extracted = await extractFromMegaCloud(data.link);
|
try {
|
||||||
return extracted;
|
const extracted = await megaCloudExtractor.extract(data.link);
|
||||||
|
return extracted;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`MegaCloud extraction failed for ${data.link}:`, e.message);
|
||||||
|
// Fallback to returning the link as is
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return sources format similar to the AniWatch package for other hosts
|
|
||||||
return {
|
return {
|
||||||
headers: {
|
headers: {
|
||||||
Referer: data.link,
|
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: [
|
sources: [
|
||||||
{
|
{
|
||||||
@@ -136,108 +128,6 @@ export async function getEpisodeSources(episodeId, serverName = 'vidstreaming',
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Helpers ---
|
|
||||||
async function extractFromMegaCloud(embedUrl) {
|
|
||||||
// Parse domain for Referer
|
|
||||||
const urlObj = new URL(embedUrl);
|
|
||||||
const defaultDomain = `${urlObj.protocol}//${urlObj.host}`;
|
|
||||||
|
|
||||||
// Use a mobile UA to match site expectations
|
|
||||||
const mobileUA = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Mobile Safari/537.36";
|
|
||||||
|
|
||||||
// Load embed page HTML
|
|
||||||
const { data: html } = await client.get(embedUrl, {
|
|
||||||
responseType: 'text',
|
|
||||||
headers: {
|
|
||||||
Accept: '*/*',
|
|
||||||
'X-Requested-With': 'XMLHttpRequest',
|
|
||||||
Referer: defaultDomain,
|
|
||||||
'User-Agent': mobileUA,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const $ = load(html);
|
|
||||||
|
|
||||||
// Get file id from #megacloud-player
|
|
||||||
const videoTag = $('#megacloud-player');
|
|
||||||
const fileId = videoTag?.attr('data-id');
|
|
||||||
if (!fileId) {
|
|
||||||
throw new Error('MegaCloud: missing file id (possibly expired URL)');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract nonce - either 48 chars or 3x16 concatenated
|
|
||||||
let nonce = null;
|
|
||||||
const nonceRegex48 = /\b[a-zA-Z0-9]{48}\b/;
|
|
||||||
const match48 = html.match(nonceRegex48);
|
|
||||||
if (match48) {
|
|
||||||
nonce = match48[0];
|
|
||||||
} else {
|
|
||||||
const match3x16 = html.match(/\b([a-zA-Z0-9]{16})\b[\s\S]*?\b([a-zA-Z0-9]{16})\b[\s\S]*?\b([a-zA-Z0-9]{16})\b/);
|
|
||||||
if (match3x16) nonce = `${match3x16[1]}${match3x16[2]}${match3x16[3]}`;
|
|
||||||
}
|
|
||||||
if (!nonce) {
|
|
||||||
throw new Error('MegaCloud: failed to capture nonce');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get decryption key from public repo
|
|
||||||
const { data: keyJson } = await client.get('https://raw.githubusercontent.com/yogesh-hacker/MegacloudKeys/refs/heads/main/keys.json', {
|
|
||||||
headers: { 'User-Agent': mobileUA }
|
|
||||||
});
|
|
||||||
const secret = keyJson?.mega;
|
|
||||||
|
|
||||||
// Try to get sources JSON
|
|
||||||
const { data: sourcesResp } = await client.get(`${defaultDomain}/embed-2/v3/e-1/getSources`, {
|
|
||||||
params: { id: fileId, _k: nonce },
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json, text/plain, */*',
|
|
||||||
Referer: defaultDomain,
|
|
||||||
'User-Agent': mobileUA,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let fileUrl = null;
|
|
||||||
if (Array.isArray(sourcesResp?.sources) && sourcesResp.sources[0]?.file) {
|
|
||||||
fileUrl = sourcesResp.sources[0].file;
|
|
||||||
} else if (sourcesResp?.sources) {
|
|
||||||
// Encrypted payload; use remote decoder
|
|
||||||
const decodeBase = 'https://script.google.com/macros/s/AKfycbxHbYHbrGMXYD2-bC-C43D3njIbU-wGiYQuJL61H4vyy6YVXkybMNNEPJNPPuZrD1gRVA/exec';
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
encrypted_data: String(sourcesResp.sources),
|
|
||||||
nonce: nonce, // keep for compatibility if server expects this key
|
|
||||||
secret: String(secret || ''),
|
|
||||||
});
|
|
||||||
// Some servers expect 'nonce' as '_k' or 'nonce'; try both key names
|
|
||||||
if (!params.has('_k')) params.append('_k', nonce);
|
|
||||||
|
|
||||||
const { data: decodedText } = await client.get(`${decodeBase}?${params.toString()}`, {
|
|
||||||
responseType: 'text',
|
|
||||||
headers: { 'User-Agent': mobileUA }
|
|
||||||
});
|
|
||||||
const match = /\"file\":\"(.*?)\"/.exec(decodedText);
|
|
||||||
if (match) fileUrl = match[1].replace(/\\\//g, '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fileUrl) {
|
|
||||||
throw new Error('MegaCloud: failed to extract file URL');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
headers: {
|
|
||||||
Referer: defaultDomain,
|
|
||||||
'User-Agent': mobileUA,
|
|
||||||
},
|
|
||||||
tracks: [],
|
|
||||||
intro: { start: 0, end: 0 },
|
|
||||||
outro: { start: 0, end: 0 },
|
|
||||||
sources: [
|
|
||||||
{
|
|
||||||
url: fileUrl,
|
|
||||||
isM3U8: /\.m3u8($|\?)/.test(fileUrl),
|
|
||||||
}
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
getEpisodeServers,
|
getEpisodeServers,
|
||||||
getEpisodeSources
|
getEpisodeSources
|
||||||
|
|||||||
Reference in New Issue
Block a user