mirror of
https://github.com/shafat-96/anime-mapper
synced 2026-04-17 15:51:45 +00:00
fix
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
node_modules
|
||||
10
src/index.js
10
src/index.js
@@ -1,10 +1,9 @@
|
||||
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 { getEpisodeServers, getEpisodeSources } from './providers/hianime-servers.js';
|
||||
import { cache } from './utils/cache.js';
|
||||
|
||||
const app = express();
|
||||
@@ -86,7 +85,7 @@ app.get('/hianime/servers/:animeId', cache('15 minutes'), async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Get streaming sources for HiAnime episode using AniWatch
|
||||
// Get streaming sources for HiAnime episode using local extractor
|
||||
app.get('/hianime/sources/:animeId', cache('15 minutes'), async (req, res) => {
|
||||
try {
|
||||
const { animeId } = req.params;
|
||||
@@ -99,9 +98,8 @@ app.get('/hianime/sources/:animeId', cache('15 minutes'), async (req, res) => {
|
||||
// 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);
|
||||
// Use our local extractor which supports MegaCloud
|
||||
const sources = await getEpisodeSources(episodeId, server, category);
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
|
||||
@@ -1,61 +1,373 @@
|
||||
import { ANIME } from '@consumet/extensions';
|
||||
import { load } from 'cheerio';
|
||||
import { client } from '../utils/client.js';
|
||||
|
||||
/**
|
||||
* AnimeKai provider class that wraps the Consumet library
|
||||
*/
|
||||
export class AnimeKai {
|
||||
constructor() {
|
||||
this.client = new ANIME.AnimeKai();
|
||||
}
|
||||
const DEFAULT_BASE = 'https://animekai.to';
|
||||
const KAISVA_URL = 'https://ilovekai.simplepostrequest.workers.dev'; // Cloudflare Worker decoder
|
||||
|
||||
/**
|
||||
* Search for anime on AnimeKai
|
||||
* @param {string} query - The search query
|
||||
* @returns {Promise<Object>} Search results
|
||||
*/
|
||||
async search(query) {
|
||||
function fixUrl(url, base = DEFAULT_BASE) {
|
||||
if (!url) return '';
|
||||
if (url.startsWith('http')) return url;
|
||||
return `${base.replace(/\/$/, '')}/${url.replace(/^\//, '')}`;
|
||||
}
|
||||
|
||||
async function requestWithRetry(url, config = {}, retries = 2, perRequestTimeoutMs = 60000) {
|
||||
let lastErr;
|
||||
for (let i = 0; i <= retries; i++) {
|
||||
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');
|
||||
const { data } = await client.get(url, { timeout: perRequestTimeoutMs, ...config });
|
||||
return data;
|
||||
} catch (e) {
|
||||
lastErr = e;
|
||||
if (i < retries) await new Promise(r => setTimeout(r, 500 * (i + 1)));
|
||||
}
|
||||
}
|
||||
throw lastErr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch anime information including episodes
|
||||
* @param {string} id - The anime ID
|
||||
* @returns {Promise<Object>} Anime info with episodes
|
||||
*/
|
||||
async fetchAnimeInfo(id) {
|
||||
// Optional: use Playwright headless browser to intercept .m3u8 and .vtt
|
||||
async function extractFromMegaUpHeadless(pageUrl, baseHeaders) {
|
||||
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');
|
||||
}
|
||||
}
|
||||
const { chromium } = await import('playwright').catch(() => ({ chromium: null }));
|
||||
if (!chromium) return null; // Playwright not installed
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
const ua = baseHeaders['User-Agent'] || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36';
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({
|
||||
userAgent: ua,
|
||||
extraHTTPHeaders: {
|
||||
Referer: baseHeaders.Referer || 'https://megaup.site',
|
||||
'Accept-Language': baseHeaders['Accept-Language'] || 'en-US,en;q=0.9',
|
||||
},
|
||||
});
|
||||
const page = await context.newPage();
|
||||
|
||||
const seenM3U8 = new Set();
|
||||
const seenVTT = new Set();
|
||||
|
||||
page.on('request', (req) => {
|
||||
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');
|
||||
const url = req.url();
|
||||
if (/\.m3u8(\?|$)/i.test(url)) seenM3U8.add(url);
|
||||
if (/\.vtt(\?|$)/i.test(url) && !/thumbnails/i.test(url)) seenVTT.add(url);
|
||||
} catch {}
|
||||
});
|
||||
|
||||
await page.goto(pageUrl, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
||||
// Try multiple common play selectors
|
||||
const playSelectors = ['button', '.vjs-big-play-button', '.plyr__control', '.jw-icon-playback'];
|
||||
for (const sel of playSelectors) {
|
||||
const btn = page.locator(sel).first();
|
||||
if (await btn.count().catch(() => 0)) {
|
||||
await btn.click({ timeout: 3000 }).catch(() => {});
|
||||
}
|
||||
}
|
||||
// Explicitly wait for an HLS request
|
||||
await Promise.race([
|
||||
page.waitForRequest(req => /\.m3u8(\?|$)/i.test(req.url()), { timeout: 10000 }).catch(() => null),
|
||||
page.waitForTimeout(7000),
|
||||
]);
|
||||
|
||||
const m3u8 = Array.from(seenM3U8)[0];
|
||||
const subtitles = Array.from(seenVTT).map((u) => ({ file: u, label: extractLangLabelFromUrl(u), kind: 'captions' }));
|
||||
|
||||
await context.close();
|
||||
await browser.close();
|
||||
|
||||
if (m3u8) {
|
||||
const pageUrlObj = new URL(pageUrl);
|
||||
const origin = `${pageUrlObj.protocol}//${pageUrlObj.host}`;
|
||||
return {
|
||||
headers: { Referer: origin, 'User-Agent': ua },
|
||||
sources: [ { url: m3u8, isM3U8: true } ],
|
||||
subtitles,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function decodeParam(value, mode = 'e') {
|
||||
// Always use worker-style: ilovefeet (encode) and ilovearmpits (decode)
|
||||
const url = new URL(KAISVA_URL);
|
||||
const paramName = mode === 'e' ? 'ilovefeet' : 'ilovearmpits';
|
||||
url.searchParams.set(paramName, value);
|
||||
return await requestWithRetry(url.toString(), { responseType: 'text' }, 2, 30000);
|
||||
}
|
||||
|
||||
async function getJson(url, params = {}, headers = {}) {
|
||||
return await requestWithRetry(url, { params, headers }, 2, 30000);
|
||||
}
|
||||
|
||||
function extractBackgroundUrl(style) {
|
||||
if (!style) return '';
|
||||
const m = style.match(/url\(([^)]+)\)/i);
|
||||
if (!m) return '';
|
||||
return m[1].replace(/^['"]|['"]$/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* AnimeKai provider implemented via scraping
|
||||
*/
|
||||
export class AnimeKai {
|
||||
constructor(baseUrl = DEFAULT_BASE) {
|
||||
this.baseUrl = baseUrl.replace(/\/$/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Search AnimeKai by keyword
|
||||
*/
|
||||
async search(query) {
|
||||
const url = `${this.baseUrl}/browser?keyword=${encodeURIComponent(query)}`;
|
||||
const { data: html } = await client.get(url, { responseType: 'text', headers: { Referer: this.baseUrl } });
|
||||
const $ = load(html);
|
||||
const results = $("div.aitem-wrapper div.aitem").map((_, el) => {
|
||||
const item = $(el);
|
||||
const href = fixUrl(item.find('a.poster').attr('href'), this.baseUrl);
|
||||
const title = item.find('a.title').text().trim();
|
||||
const subCount = parseInt(item.find('div.info span.sub').text().trim() || '0', 10) || 0;
|
||||
const dubCount = parseInt(item.find('div.info span.dub').text().trim() || '0', 10) || 0;
|
||||
const posterUrl = fixUrl(item.find('a.poster img').attr('data-src') || item.find('a.poster img').attr('src'), this.baseUrl);
|
||||
const type = (item.find('div.fd-infor > span.fdi-item').text().trim() || '').toLowerCase();
|
||||
return {
|
||||
id: href,
|
||||
url: href,
|
||||
title,
|
||||
image: posterUrl,
|
||||
type,
|
||||
subCount,
|
||||
dubCount,
|
||||
};
|
||||
}).get();
|
||||
return { results };
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch anime info and episodes from a show page URL
|
||||
*/
|
||||
async fetchAnimeInfo(idOrUrl) {
|
||||
const url = fixUrl(idOrUrl, this.baseUrl);
|
||||
const { data: html } = await client.get(url, { responseType: 'text', headers: { Referer: this.baseUrl } });
|
||||
const $ = load(html);
|
||||
|
||||
const title = $('h1.title').first().text().trim();
|
||||
const japaneseTitle = $('h1.title').first().attr('data-jp') || '';
|
||||
const animeId = $('div.rate-box').attr('data-id');
|
||||
const malId = $('div.watch-section').attr('data-mal-id') || null;
|
||||
const aniId = $('div.watch-section').attr('data-al-id') || null;
|
||||
const subCount = parseInt($('#main-entity div.info span.sub').text().trim() || '0', 10) || 0;
|
||||
const dubCount = parseInt($('#main-entity div.info span.dub').text().trim() || '0', 10) || 0;
|
||||
const bgStyle = $('div.watch-section-bg').attr('style') || '';
|
||||
const posterFromBg = extractBackgroundUrl(bgStyle);
|
||||
|
||||
const underscore = await decodeParam(animeId, 'e');
|
||||
const listJson = await getJson(`${this.baseUrl}/ajax/episodes/list`, { ani_id: animeId, _: underscore }, { Referer: url });
|
||||
const listHtml = listJson?.result || '';
|
||||
const $$ = load(listHtml);
|
||||
|
||||
const episodes = [];
|
||||
$$("div.eplist a").each((index, el) => {
|
||||
const a = $$(el);
|
||||
const token = a.attr('token');
|
||||
const name = a.find('span').text().trim();
|
||||
const numAttr = a.attr('num');
|
||||
const number = numAttr ? parseInt(numAttr, 10) : (index + 1);
|
||||
if (token) {
|
||||
episodes.push({ id: token, number, title: name });
|
||||
}
|
||||
});
|
||||
|
||||
// Optional enrichment from Ani.zip when MAL id is present
|
||||
let aniZip = null;
|
||||
if (malId) {
|
||||
try {
|
||||
const { data: aniZipData } = await client.get(`https://api.ani.zip/mappings`, { params: { mal_id: malId } });
|
||||
aniZip = aniZipData || null;
|
||||
} catch {
|
||||
aniZip = null;
|
||||
}
|
||||
if (aniZip && aniZip.episodes) {
|
||||
// Attach episode metadata when index matches
|
||||
episodes.forEach((ep) => {
|
||||
const meta = aniZip.episodes?.[String(ep.number)];
|
||||
if (meta) {
|
||||
ep.image = meta.image || undefined;
|
||||
ep.overview = meta.overview || undefined;
|
||||
const r = parseFloat(meta.rating || '0');
|
||||
ep.rating = Number.isFinite(r) ? Math.round(r * 10) : 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Genres
|
||||
const genres = $('div.detail a')
|
||||
.toArray()
|
||||
.map((el) => ({ href: $(el).attr('href') || '', text: $(el).text().trim() }))
|
||||
.filter((x) => x.href.includes('/genres/'))
|
||||
.map((x) => x.text);
|
||||
|
||||
// Status: avoid :containsOwn which isn't supported by css-select
|
||||
let statusText = undefined;
|
||||
const statusDiv = $('div').filter((_, el) => /\bstatus\b/i.test($(el).text()));
|
||||
if (statusDiv.length) {
|
||||
const spanTxt = statusDiv.first().find('span').first().text().trim();
|
||||
if (spanTxt) statusText = spanTxt;
|
||||
}
|
||||
|
||||
return {
|
||||
id: url,
|
||||
title,
|
||||
japaneseTitle,
|
||||
url,
|
||||
image: posterFromBg ? fixUrl(posterFromBg, this.baseUrl) : undefined,
|
||||
type: 'anime',
|
||||
totalEpisodes: episodes.length,
|
||||
episodes,
|
||||
hasSub: subCount > 0,
|
||||
hasDub: dubCount > 0,
|
||||
subOrDub: subCount && dubCount ? 'both' : (dubCount ? 'dub' : 'sub'),
|
||||
status: statusText,
|
||||
season: undefined,
|
||||
genres,
|
||||
malId: malId ? Number(malId) : undefined,
|
||||
anilistId: aniId ? Number(aniId) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch episode sources for a given episode token
|
||||
* @param {string} episodeToken
|
||||
* @param {string} serverName optional server display name filter
|
||||
* @param {boolean} dub fetch dubbed if true (also tries softsub when false)
|
||||
*/
|
||||
async fetchEpisodeSources(episodeToken, serverName = undefined, dub = false) {
|
||||
const underscoreToken = await decodeParam(episodeToken, 'e');
|
||||
const listJson = await getJson(`${this.baseUrl}/ajax/links/list`, { token: episodeToken, _: underscoreToken }, { Referer: this.baseUrl });
|
||||
const listHtml = listJson?.result || '';
|
||||
const $ = load(listHtml);
|
||||
|
||||
const preferredTypes = dub ? ['dub'] : ['sub', 'softsub'];
|
||||
const serverCandidates = [];
|
||||
preferredTypes.forEach((type) => {
|
||||
$(`div.server-items[data-id=${type}] span.server[data-lid]`).each((_, el) => {
|
||||
const span = $(el);
|
||||
serverCandidates.push({
|
||||
type,
|
||||
lid: span.attr('data-lid'),
|
||||
name: span.text().trim(),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
if (serverCandidates.length === 0) {
|
||||
throw new Error('No servers found for this episode');
|
||||
}
|
||||
|
||||
let chosen = serverCandidates[0];
|
||||
if (serverName) {
|
||||
const found = serverCandidates.find(s => s.name.toLowerCase() === serverName.toLowerCase());
|
||||
if (found) chosen = found;
|
||||
}
|
||||
|
||||
const underscoreLid = await decodeParam(chosen.lid, 'e');
|
||||
const viewJson = await getJson(`${this.baseUrl}/ajax/links/view`, { id: chosen.lid, _: underscoreLid }, { Referer: this.baseUrl });
|
||||
const result = viewJson?.result || '';
|
||||
|
||||
const decodedText = await decodeParam(result, 'd');
|
||||
let iframeUrl = '';
|
||||
try {
|
||||
const parsed = JSON.parse(decodedText);
|
||||
iframeUrl = parsed.url || '';
|
||||
} catch {
|
||||
const m = decodedText.match(/\"url\"\s*:\s*\"(.*?)\"/);
|
||||
if (m) iframeUrl = m[1].replace(/\\\//g, '/');
|
||||
}
|
||||
|
||||
if (!iframeUrl) {
|
||||
throw new Error('Failed to resolve iframe URL');
|
||||
}
|
||||
|
||||
// If MegaUp, try to extract direct m3u8 and subtitles
|
||||
if (/megaup\.(site|cc)/i.test(iframeUrl)) {
|
||||
const resolved = await extractFromMegaUp(iframeUrl);
|
||||
if (resolved) return resolved;
|
||||
}
|
||||
|
||||
return {
|
||||
headers: {
|
||||
Referer: this.baseUrl,
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
|
||||
},
|
||||
sources: [
|
||||
{ url: iframeUrl, isM3U8: /\.m3u8($|\?)/.test(iframeUrl) }
|
||||
],
|
||||
subtitles: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Try to resolve .m3u8 and .vtt subtitle links from MegaUp pages without a WebView
|
||||
async function extractFromMegaUp(pageUrl) {
|
||||
try {
|
||||
const pageUrlObj = new URL(pageUrl);
|
||||
const origin = `${pageUrlObj.protocol}//${pageUrlObj.host}`;
|
||||
const headers = {
|
||||
Referer: 'https://megaup.site',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:134.0) Gecko/20100101 Firefox/134.0',
|
||||
Accept: '*/*',
|
||||
'Accept-Language': 'en-US,en;q=0.9',
|
||||
Pragma: 'no-cache',
|
||||
'Cache-Control': 'no-cache',
|
||||
};
|
||||
const html = await requestWithRetry(pageUrl, { responseType: 'text', headers }, 1, 30000);
|
||||
|
||||
// Look for HLS URLs in HTML/inline scripts
|
||||
const m3u8Matches = String(html).match(/https?:[^\"'\s]+\.m3u8[^\"'\s]*/gi) || [];
|
||||
|
||||
// Collect subtitle .vtt links
|
||||
const vttMatches = String(html).match(/https?:[^\"'\s]+\.vtt[^\"'\s]*/gi) || [];
|
||||
const subtitles = vttMatches
|
||||
.filter((u) => !/thumbnails/i.test(u))
|
||||
.map((u) => ({
|
||||
file: u,
|
||||
label: extractLangLabelFromUrl(u),
|
||||
kind: 'captions',
|
||||
}));
|
||||
|
||||
if (m3u8Matches.length > 0) {
|
||||
const file = m3u8Matches[0];
|
||||
return {
|
||||
headers: { Referer: origin, 'User-Agent': headers['User-Agent'] },
|
||||
sources: [ { url: file, isM3U8: true } ],
|
||||
subtitles,
|
||||
};
|
||||
}
|
||||
|
||||
// If not found via static scraping, try headless interception if available
|
||||
const headless = await extractFromMegaUpHeadless(pageUrl, headers);
|
||||
if (headless) return headless;
|
||||
// If still not found, return null to fall back to iframe
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function extractLangLabelFromUrl(url) {
|
||||
try {
|
||||
const file = url.split('/').pop() || '';
|
||||
const code = (file.split('_')[0] || '').toLowerCase();
|
||||
const map = {
|
||||
eng: 'English', ger: 'German', deu: 'German', spa: 'Spanish', fre: 'French', fra: 'French',
|
||||
ita: 'Italian', jpn: 'Japanese', chi: 'Chinese', zho: 'Chinese', kor: 'Korean', rus: 'Russian',
|
||||
ara: 'Arabic', hin: 'Hindi', por: 'Portuguese', vie: 'Vietnamese', pol: 'Polish', ukr: 'Ukrainian',
|
||||
swe: 'Swedish', ron: 'Romanian', rum: 'Romanian', ell: 'Greek', gre: 'Greek', hun: 'Hungarian',
|
||||
fas: 'Persian', per: 'Persian', tha: 'Thai'
|
||||
};
|
||||
return map[code] || code.toUpperCase() || 'Subtitle';
|
||||
} catch { return 'Subtitle'; }
|
||||
}
|
||||
|
||||
export default AnimeKai;
|
||||
@@ -110,7 +110,13 @@ export async function getEpisodeSources(episodeId, serverName = 'vidstreaming',
|
||||
}
|
||||
);
|
||||
|
||||
// Return sources format similar to the AniWatch package
|
||||
// If the target is a MegaCloud embed, extract the direct source URL
|
||||
if (data?.link && /megacloud\./i.test(data.link)) {
|
||||
const extracted = await extractFromMegaCloud(data.link);
|
||||
return extracted;
|
||||
}
|
||||
|
||||
// Return sources format similar to the AniWatch package for other hosts
|
||||
return {
|
||||
headers: {
|
||||
Referer: data.link,
|
||||
@@ -119,7 +125,7 @@ export async function getEpisodeSources(episodeId, serverName = 'vidstreaming',
|
||||
sources: [
|
||||
{
|
||||
url: data.link,
|
||||
isM3U8: data.link.includes('.m3u8'),
|
||||
isM3U8: data.link?.includes('.m3u8') || false,
|
||||
}
|
||||
],
|
||||
subtitles: [],
|
||||
@@ -130,6 +136,108 @@ 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 {
|
||||
getEpisodeServers,
|
||||
getEpisodeSources
|
||||
|
||||
@@ -30,9 +30,17 @@ export function cache(duration) {
|
||||
|
||||
// Override the json method to cache the response
|
||||
res.json = function(data) {
|
||||
// Cache the data
|
||||
try {
|
||||
// Only cache successful responses (statusCode < 400)
|
||||
if ((res.statusCode || 200) < 400) {
|
||||
console.log(`Caching response for: ${key}`);
|
||||
apiCache.set(key, data);
|
||||
} else {
|
||||
console.log(`Skip caching error for: ${key} (status ${res.statusCode})`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`Cache middleware error: ${e?.message || e}`);
|
||||
}
|
||||
|
||||
// Call the original json method
|
||||
return originalJson.call(this, data);
|
||||
|
||||
@@ -6,7 +6,7 @@ export const client = axios.create({
|
||||
'Accept': 'application/json, text/plain, */*',
|
||||
'Accept-Language': 'en-US,en;q=0.9'
|
||||
},
|
||||
timeout: 10000
|
||||
timeout: 60000
|
||||
});
|
||||
|
||||
export default client;
|
||||
Reference in New Issue
Block a user