Merge pull request #3 from N-SUDY/main

Added latest endpoint
This commit is contained in:
Tahseen
2026-04-26 12:21:00 +05:30
committed by GitHub
2 changed files with 91 additions and 18 deletions
+24 -17
View File
@@ -30,6 +30,7 @@ app.get('/', (req, res) => {
endpoints: {
search: '/search?q=naruto',
episodes: '/episodes?session=anime-session-id',
latest: '/latest?page=1',
sources: '/sources?anime_session=xxx&episode_session=yyy',
ids: '/ids?session=anime-session-id (returns AniList and MyAnimeList IDs)',
m3u8: '/m3u8?url=kwik-url (returns m3u8 URL with required referer)',
@@ -77,12 +78,23 @@ app.get('/episodes', async (req, res) => {
}
});
app.get('/latest', async (req, res) => {
try {
const page = parseInt(req.query.page, 10) || 1;
const latest = await pahe.getLatest(page);
res.json(latest);
} catch (error) {
console.error('Latest error:', error);
res.status(mapErrorToStatusCode(error.message)).json({ error: error.message });
}
});
app.get('/sources', async (req, res) => {
try {
const { anime_session, episode_session } = req.query;
if (!anime_session || !episode_session) {
return res.status(400).json({
error: 'Query parameters "anime_session" and "episode_session" are required'
return res.status(400).json({
error: 'Query parameters "anime_session" and "episode_session" are required'
});
}
const sources = await pahe.getSources(anime_session, episode_session);
@@ -114,7 +126,7 @@ app.get('/m3u8', async (req, res) => {
return res.status(400).json({ error: 'Query parameter "url" is required' });
}
const result = await pahe.resolveKwikWithNode(url);
// Return m3u8 URL along with required referer for CORS bypass
res.json({
m3u8: result.m3u8,
@@ -135,7 +147,7 @@ app.get('/proxy', async (req, res) => {
try {
const { url, referer: customReferer } = req.query;
if (!url) {
return res.status(400).json({
return res.status(400).json({
error: 'Query parameter "url" is required',
usage: 'GET /proxy?url=<m3u8-or-ts-url>&referer=<optional-referer>',
example: '/proxy?url=https://example.com/video.m3u8&referer=https://kwik.si/'
@@ -157,7 +169,7 @@ app.get('/proxy', async (req, res) => {
const axios = require('axios');
const urlObj = new URL(url);
const referer = customReferer || `${urlObj.protocol}//${urlObj.host}/`;
// Fetch the content with proper headers
const response = await axios.get(url, {
headers: {
@@ -181,14 +193,14 @@ app.get('/proxy', async (req, res) => {
});
if (response.status === 403) {
return res.status(403).json({
return res.status(403).json({
error: 'Access forbidden - CDN blocked the request',
url: url
});
}
const contentType = response.headers['content-type'] ||
(url.includes('.m3u8') ? 'application/vnd.apple.mpegurl' :
const contentType = response.headers['content-type'] ||
(url.includes('.m3u8') ? 'application/vnd.apple.mpegurl' :
url.includes('.ts') ? 'video/mp2t' : 'application/octet-stream');
if (contentType.includes('mpegurl') || url.includes('.m3u8')) {
@@ -229,7 +241,7 @@ app.get('/proxy', async (req, res) => {
res.setHeader('Accept-Ranges', 'bytes');
if (response.headers['content-length']) res.setHeader('Content-Length', response.headers['content-length']);
if (response.headers['content-range']) res.setHeader('Content-Range', response.headers['content-range']);
res.status(response.status);
response.data.pipe(res);
}
@@ -252,9 +264,9 @@ app.options('/proxy', (req, res) => {
// Global error handler
app.use((err, req, res, next) => {
console.error('Unhandled error:', err);
res.status(500).json({
res.status(500).json({
error: 'Internal server error',
message: err.message
message: err.message
});
});
@@ -266,9 +278,4 @@ if (require.main === module) {
app.listen(PORT, () => {
console.log(`Animepahe API server running on port ${PORT}`);
});
}
}
+67 -1
View File
@@ -126,6 +126,9 @@ class AnimePahe {
if (context === 'ids') {
return 'Anime session not found. Use /search first to get a valid session id.';
}
if (context === 'latest') {
return 'Unable to fetch latest episodes right now.';
}
}
if (statusCode === 403 && /ddos-guard|checking your browser|cloudflare/i.test(rawMessage)) {
@@ -260,6 +263,70 @@ class AnimePahe {
}
}
/**
* Get latest airing episodes
* @param {number} page - Page number
* @returns {Promise<Array>} Array of latest airing episodes
*/
async getLatest(page = 1) {
try {
const latestUrl = `${this.base}/api?m=airing&page=${page}`;
const response = await cloudscraper.get(latestUrl, {
headers: this.getHeaders()
});
const data = this._parseJsonResponse(response);
const latestRows = this._extractArray(data, ['data', 'results', 'items', 'episodes', 'list']);
const results = [];
for (const item of latestRows) {
const animeSession =
item.anime_session ||
item.session ||
item.slug ||
item.anime?.session ||
item.anime?.slug ||
null;
const episodeSession =
item.episode_session ||
item.release_session ||
item.episode?.session ||
item.session ||
null;
const title =
item.anime_title ||
item.title ||
item.anime?.title ||
item.anime?.name ||
null;
const urlValue =
item.url ||
(animeSession ? `${this.base}/anime/${animeSession}` : null);
results.push({
id: item.id || item.release_id || item.episode_id || null,
title,
url: urlValue,
episode: Number(item.episode ?? item.number ?? item.ep ?? item.ep_num ?? null),
episode_title:
item.episode_title ||
item.title ||
`Episode ${item.episode ?? item.number ?? ''}`.trim(),
snapshot: item.snapshot || item.thumbnail || item.image || null,
anime_session: animeSession,
episode_session: episodeSession
});
}
return results;
} catch (error) {
throw new Error(`Failed to get latest episodes: ${this._formatUpstreamError('latest', error)}`);
}
}
/**
* Get streaming sources for an episode
* @param {string} animeSession - Anime session ID
@@ -501,7 +568,6 @@ ${transformedScript}
});
});
}
}
module.exports = AnimePahe;