diff --git a/index.js b/index.js index 83df1b8..a7a8608 100644 --- a/index.js +++ b/index.js @@ -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=&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}`); }); -} - - - - - + } diff --git a/lib/animepahe.js b/lib/animepahe.js index e383825..433d1e9 100644 --- a/lib/animepahe.js +++ b/lib/animepahe.js @@ -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 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;