This commit is contained in:
Md Tahseen Hussain
2026-01-20 00:24:28 +05:30
commit d127129b0f
13 changed files with 6937 additions and 0 deletions

8
.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
node_modules/
.vercel/
coverage/
*.log
.env
.DS_Store
*.tmp
*.temp

164
DEPLOY.md Normal file
View File

@@ -0,0 +1,164 @@
# 🚀 Deployment Checklist
## Pre-Deployment
- [x] All files copied to `vercel-deploy` folder
- [x] `vercel.json` configuration ready
- [x] `package.json` with correct dependencies
- [ ] Test locally before deploying
## Test Locally
```bash
cd vercel-deploy
npm install
npm start
```
Visit `http://localhost:3000` and test:
- [ ] `/health` endpoint works
- [ ] `/search?q=naruto` returns results
- [ ] `/player.html` loads correctly
## Deploy to Vercel
### Step 1: Install Vercel CLI
```bash
npm i -g vercel
```
### Step 2: Login to Vercel
```bash
vercel login
```
### Step 3: Deploy
```bash
cd vercel-deploy
vercel
```
Answer the prompts:
- **Set up and deploy?** → Y
- **Which scope?** → (select your account)
- **Link to existing project?** → N
- **Project name?** → (press Enter or type a name)
- **Directory?** → (press Enter)
- **Override settings?** → N
### Step 4: Test Deployment
After deployment, you'll get a URL. Test it:
```bash
# Replace with your actual URL
curl https://your-project.vercel.app/health
```
Expected response:
```json
{"status":"ok","message":"Animepahe API is alive!"}
```
## Post-Deployment Testing
Test all endpoints with your deployed URL:
```bash
# Set your URL
URL="https://your-project.vercel.app"
# Health check
curl "$URL/health"
# Search
curl "$URL/search?q=naruto"
# Visit player
# Open in browser: $URL/player.html
```
## Production Deploy
For production deployment:
```bash
vercel --prod
```
This will deploy to your production domain.
## Troubleshooting
### Issue: "Command not found: vercel"
**Solution:** Install Vercel CLI globally
```bash
npm i -g vercel
```
### Issue: "No package.json found"
**Solution:** Make sure you're in the `vercel-deploy` folder
```bash
cd vercel-deploy
```
### Issue: Deployment timeout
**Solution:** Vercel free tier has 10-second timeout. This is sufficient for all API calls. If you see timeouts, check:
- AnimePahe website is accessible
- Your internet connection is stable
### Issue: 404 on routes
**Solution:** Check `vercel.json` is present and correctly configured
## Environment Variables
No environment variables needed! The app works without any configuration.
## Custom Domain
To add a custom domain:
1. Go to your project on vercel.com
2. Click "Settings" → "Domains"
3. Add your domain
4. Update DNS records as instructed
## Monitoring
View logs and analytics:
1. Go to vercel.com
2. Select your project
3. Click "Deployments" to see logs
4. Click "Analytics" to see usage
## Updates
To update your deployment:
```bash
cd vercel-deploy
vercel
```
Vercel will automatically detect changes and redeploy.
## Rollback
To rollback to a previous deployment:
1. Go to vercel.com
2. Select your project
3. Click "Deployments"
4. Find the working deployment
5. Click "..." → "Promote to Production"
---
**Ready to deploy?** Just run `vercel` in the `vercel-deploy` folder! 🚀

142
README.md Normal file
View File

@@ -0,0 +1,142 @@
# AnimePahe Scraper API
## Features
- Search anime by query
- Get all episodes for a given anime session
- Retrieve source links for a specific episode
- Resolve `.m3u8` URLs from Kwik or embedded players
- FastAPI backend for easy integration with frontends or other tools
- Async, efficient, and capable of bypassing Cloudflare restrictions
---
## 📡 API Endpoints
Once deployed, your API will have these endpoints:
- `GET /` - API information
- `GET /health` - Health check
- `GET /search?q=naruto` - Search anime
- `GET /episodes?session=<session>` - Get episodes
- `GET /sources?anime_session=<session>&episode_session=<session>` - Get sources
- `GET /m3u8?url=<kwik-url>` - Resolve M3U8 URL
- `GET /player.html` - Video player demo
## 📝 Environment Variables
No environment variables needed! The app works out of the box.
**Note:** M3U8 resolution takes 2-3 seconds, well within the timeout.
## Example JSON
```http
GET /search?q=the%20fragrant%20flower%20blooms%20with%20dignity
```
Output:
```json
[
{
"id": 6234,
"title": "The Fragrant Flower Blooms with Dignity",
"url": "https://animepahe.si/anime/27a95751-0311-47ed-dbce-7f0680d5074a",
"year": 2025,
"poster": "https://i.animepahe.si/uploads/posters/7103371364ff1310373c89cf444ffc3e6de0b757694a0936ae80e65cfae400b5.jpg",
"type": "TV",
"session": "27a95751-0311-47ed-dbce-7f0680d5074a"
},
{
"id": 279,
"title": "The iDOLM@STER",
"url": "https://animepahe.si/anime/b6b777aa-6827-3626-2b30-f0eaea4dbc29",
"year": 2011,
"poster": "https://i.animepahe.si/posters/c121185f6dadbe63ef45560032b41d2b5186e2ca39edfd0b2796c3cecaa552b0.jpg",
"type": "TV",
"session": "b6b777aa-6827-3626-2b30-f0eaea4dbc29"
}
]
```
---
```http
GET /episodes?session=27a95751-0311-47ed-dbce-7f0680d5074a
```
Output:
```json
[
{
"id": 70730,
"number": 1,
"title": "Episode 1",
"snapshot": "https://i.animepahe.si/uploads/snapshots/22c034f704a286b5ce17cc33a3dccf9258cc83038e5bafbcc5a196b2584c3454.jpg",
"session": "800a1f7d29d6ebb94d2bfd320b2001b95d00decff4aaecaa6fbef5916379a762"
},
{
"id": 70823,
"number": 2,
"title": "Episode 2",
"snapshot": "https://i.animepahe.si/uploads/snapshots/baf28a9ea1fecf9bbee49844cf3b782632e487ff49d3ba5c93b56241719fab05.jpg",
"session": "4dd535ded2d2773bb3285881839d018c5787619de262dffb801ab4f78cf20123"
}
]
```
---
```http
GET /sources?anime_session=27a95751-0311-47ed-dbce-7f0680d5074a&episode_session=800a1f7d29d6ebb94d2bfd320b2001b95d00decff4aaecaa6fbef5916379a762
```
Output:
```json
[
{
"url": "https://kwik.si/e/KcfYGhr86Ww2",
"quality": "1080p",
"fansub": "KawaSubs",
"audio": "jpn"
},
{
"url": "https://kwik.si/e/Sr2gRRoVz6wy",
"quality": "720p",
"fansub": "KawaSubs",
"audio": "jpn"
},
{
"url": "https://kwik.si/e/J7jBHBSJhTEv",
"quality": "360p",
"fansub": "KawaSubs",
"audio": "jpn"
}
]
```
---
```http
GET /m3u8?url=https://kwik.si/e/uEPQKLMzFpaz
```
Output:
```json
{
"m3u8": "https://vault-12.owocdn.top/stream/12/12/1478df0e98f767de547ac36d33bc92b73b9a5b7318fe3f3e81328fa31fc1eac3/uwu.m3u8"
}
```
## 🔗 After Deployment
1. You'll get a URL like: `https://your-project.vercel.app`
## 🎉 You're Ready!

71
START_HERE.txt Normal file
View File

@@ -0,0 +1,71 @@
╔════════════════════════════════════════════════════════════════╗
║ ║
║ AnimePahe API - Vercel Deployment Package ║
║ ║
╚════════════════════════════════════════════════════════════════╝
📦 This folder contains everything needed for Vercel deployment!
🚀 QUICK START (3 steps):
1. Install Vercel CLI:
npm i -g vercel
2. Navigate to this folder:
cd vercel-deploy
3. Deploy:
vercel
That's it! Follow the prompts and you're deployed! 🎉
📁 FILES INCLUDED:
✅ index.js - Main server
✅ lib/animepahe.js - Scraper logic
✅ lib/utils.js - Utilities
✅ public/player.html - Video player
✅ public/index.html - Landing page
✅ package.json - Dependencies
✅ vercel.json - Vercel config
✅ README.md - Full documentation
✅ DEPLOY.md - Deployment checklist
🧪 TEST LOCALLY FIRST:
cd vercel-deploy
npm install
npm start
Then visit: http://localhost:3000
📚 DOCUMENTATION:
- README.md → Full deployment guide
- DEPLOY.md → Step-by-step checklist
🎬 AFTER DEPLOYMENT:
Your API will be live at: https://your-project.vercel.app
Test it:
- https://your-project.vercel.app/health
- https://your-project.vercel.app/search?q=naruto
- https://your-project.vercel.app/player.html
💡 TIPS:
- No environment variables needed
- Free tier is sufficient
- 10-second timeout (enough for all API calls)
- Unlimited requests on free tier
🆘 NEED HELP?
Check DEPLOY.md for troubleshooting and detailed instructions.
═══════════════════════════════════════════════════════════════
Ready to deploy? Run: vercel
═══════════════════════════════════════════════════════════════

31
deploy.bat Normal file
View File

@@ -0,0 +1,31 @@
@echo off
echo ========================================
echo AnimePahe API - Vercel Deployment
echo ========================================
echo.
echo [1/3] Installing dependencies...
call npm install
if %errorlevel% neq 0 (
echo ERROR: Failed to install dependencies
pause
exit /b 1
)
echo.
echo [2/3] Testing locally...
echo Starting server on http://localhost:3000
echo Press Ctrl+C to stop and continue to deployment
echo.
call npm start
echo.
echo [3/3] Ready to deploy!
echo.
echo Run this command to deploy:
echo vercel
echo.
echo Or for production:
echo vercel --prod
echo.
pause

234
index.js Normal file
View File

@@ -0,0 +1,234 @@
const express = require('express');
const cors = require('cors');
const AnimePahe = require('./lib/animepahe');
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware
app.use(cors());
app.use(express.json());
// Serve static files from public directory
app.use(express.static('public'));
// Create AnimePahe instance
const pahe = new AnimePahe();
// Routes
app.get('/', (req, res) => {
res.json({
message: 'Welcome to Animepahe API',
endpoints: {
search: '/search?q=naruto',
episodes: '/episodes?session=anime-session-id',
sources: '/sources?anime_session=xxx&episode_session=yyy',
m3u8: '/m3u8?url=kwik-url',
proxy: '/proxy?url=m3u8-or-ts-url (Use this to play videos)',
health: '/health'
},
usage: {
note: 'Use /proxy endpoint to stream videos through the server to bypass CORS and referrer restrictions',
example: 'Get M3U8 URL from /m3u8, then use /proxy?url=<m3u8-url> in your video player'
}
});
});
app.get('/health', (req, res) => {
res.json({ status: 'ok', message: 'Animepahe API is alive!' });
});
app.get('/search', async (req, res) => {
try {
const { q } = req.query;
if (!q) {
return res.status(400).json({ error: 'Query parameter "q" is required' });
}
const results = await pahe.search(q);
res.json(results);
} catch (error) {
console.error('Search error:', error);
res.status(500).json({ error: error.message });
}
});
app.get('/episodes', async (req, res) => {
try {
const { session } = req.query;
if (!session) {
return res.status(400).json({ error: 'Query parameter "session" is required' });
}
const episodes = await pahe.getEpisodes(session);
res.json(episodes);
} catch (error) {
console.error('Episodes error:', error);
res.status(500).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'
});
}
const sources = await pahe.getSources(anime_session, episode_session);
res.json(sources);
} catch (error) {
console.error('Sources error:', error);
res.status(500).json({ error: error.message });
}
});
app.get('/m3u8', async (req, res) => {
try {
const { url } = req.query;
if (!url) {
return res.status(400).json({ error: 'Query parameter "url" is required' });
}
const m3u8 = await pahe.resolveKwikWithNode(url);
res.json({ m3u8 });
} catch (error) {
console.error('M3U8 resolution error:', error);
res.status(500).json({ error: error.message });
}
});
app.get('/proxy', async (req, res) => {
try {
const { url } = req.query;
if (!url) {
return res.status(400).json({
error: 'Query parameter "url" is required',
usage: 'GET /proxy?url=<m3u8-or-ts-url>',
example: '/proxy?url=https://example.com/video.m3u8'
});
}
const axios = require('axios');
// Extract domain from URL for referer
const urlObj = new URL(url);
const referer = `${urlObj.protocol}//${urlObj.host}/`;
// Fetch the content with proper headers
const response = await axios.get(url, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Referer': referer,
'Origin': referer.slice(0, -1),
'Accept': '*/*',
'Accept-Language': 'en-US,en;q=0.9',
'Accept-Encoding': 'gzip, deflate, br',
'Connection': 'keep-alive',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site'
},
responseType: 'arraybuffer',
timeout: 30000,
maxRedirects: 5,
validateStatus: function (status) {
return status >= 200 && status < 500; // Accept 4xx errors to handle them
}
});
// Check if we got blocked
if (response.status === 403) {
return res.status(403).json({
error: 'Access forbidden - CDN blocked the request',
suggestion: 'The video CDN is blocking server requests. Try using a browser extension or different source.',
url: url
});
}
// Determine content type
const contentType = response.headers['content-type'] ||
(url.includes('.m3u8') ? 'application/vnd.apple.mpegurl' :
url.includes('.ts') ? 'video/mp2t' : 'application/octet-stream');
// If it's an m3u8 playlist, modify URLs to go through proxy
if (contentType.includes('mpegurl') || url.includes('.m3u8')) {
let content = response.data.toString('utf-8');
// Replace relative URLs with proxied absolute URLs
const baseUrl = url.substring(0, url.lastIndexOf('/') + 1);
content = content.split('\n').map(line => {
line = line.trim();
if (line && !line.startsWith('#') && !line.startsWith('http')) {
// Relative URL - make it absolute and proxy it
const absoluteUrl = baseUrl + line;
return `/proxy?url=${encodeURIComponent(absoluteUrl)}`;
} else if (line.startsWith('http')) {
// Absolute URL - proxy it
return `/proxy?url=${encodeURIComponent(line)}`;
}
return line;
}).join('\n');
res.setHeader('Content-Type', contentType);
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Range, Content-Type');
res.send(content);
} else {
// Video segment or other binary content
res.setHeader('Content-Type', contentType);
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Range, Content-Type');
res.setHeader('Accept-Ranges', 'bytes');
if (response.headers['content-length']) {
res.setHeader('Content-Length', response.headers['content-length']);
}
res.send(Buffer.from(response.data));
}
} catch (error) {
console.error('Proxy error:', error.message);
if (error.response && error.response.status === 403) {
return res.status(403).json({
error: 'Access forbidden - CDN blocked the request',
suggestion: 'The video CDN has Cloudflare protection. You may need to use a CORS proxy service or browser extension.',
url: req.query.url
});
}
res.status(500).json({
error: error.message,
url: req.query.url,
suggestion: 'Try accessing the M3U8 URL directly in your browser or use a CORS proxy service'
});
}
});
// Handle OPTIONS for CORS preflight
app.options('/proxy', (req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Range, Content-Type');
res.sendStatus(200);
});
// Global error handler
app.use((err, req, res, next) => {
console.error('Unhandled error:', err);
res.status(500).json({
error: 'Internal server error',
message: err.message
});
});
// Export for Vercel
module.exports = app;
// Start server if not in Vercel environment
if (require.main === module) {
app.listen(PORT, () => {
console.log(`Animepahe API server running on port ${PORT}`);
});
}

348
lib/animepahe.js Normal file
View File

@@ -0,0 +1,348 @@
const cloudscraper = require('cloudscraper');
const cheerio = require('cheerio');
const { randomUserAgent, extractM3U8FromText } = require('./utils');
const { spawn } = require('child_process');
const fs = require('fs').promises;
const os = require('os');
const path = require('path');
/**
* AnimePahe scraper class
*/
class AnimePahe {
constructor() {
this.base = 'https://animepahe.si';
this.headers = {
'User-Agent': randomUserAgent(),
'Cookie': '__ddg1_=;__ddg2_=',
'Referer': 'https://animepahe.si/'
};
}
/**
* Get headers with a fresh user agent
* @returns {Object} Headers object
*/
getHeaders() {
return {
...this.headers,
'User-Agent': randomUserAgent()
};
}
/**
* Search for anime by query
* @param {string} query - Search query
* @returns {Promise<Array>} Array of anime results
*/
async search(query) {
const url = `${this.base}/api?m=search&q=${encodeURIComponent(query)}`;
try {
const response = await cloudscraper.get(url, {
headers: this.getHeaders()
});
const data = typeof response === 'string' ? JSON.parse(response) : response;
const results = [];
for (const anime of (data.data || [])) {
results.push({
id: anime.id,
title: anime.title,
url: `${this.base}/anime/${anime.session}`,
year: anime.year,
poster: anime.poster,
type: anime.type,
session: anime.session
});
}
return results;
} catch (error) {
throw new Error(`Search failed: ${error.message}`);
}
}
/**
* Get episodes for an anime
* @param {string} animeSession - Anime session ID
* @returns {Promise<Array>} Array of episodes
*/
async getEpisodes(animeSession) {
try {
// Fetch anime page to get internal ID
const animePageUrl = `${this.base}/anime/${animeSession}`;
const html = await cloudscraper.get(animePageUrl, {
headers: this.getHeaders()
});
// Parse HTML to extract meta tag
const $ = cheerio.load(html);
const metaTag = $('meta[property="og:url"]');
if (!metaTag.length) {
throw new Error('Could not find session ID in meta tag');
}
const metaContent = metaTag.attr('content');
const tempId = metaContent.split('/').pop();
// Fetch first page to get pagination info
const firstPageUrl = `${this.base}/api?m=release&id=${tempId}&sort=episode_asc&page=1`;
const firstPageResponse = await cloudscraper.get(firstPageUrl, {
headers: this.getHeaders()
});
const firstPageData = typeof firstPageResponse === 'string'
? JSON.parse(firstPageResponse)
: firstPageResponse;
let episodes = firstPageData.data || [];
const lastPage = firstPageData.last_page || 1;
// Fetch remaining pages concurrently
if (lastPage > 1) {
const pagePromises = [];
for (let page = 2; page <= lastPage; page++) {
const pageUrl = `${this.base}/api?m=release&id=${tempId}&sort=episode_asc&page=${page}`;
pagePromises.push(
cloudscraper.get(pageUrl, { headers: this.getHeaders() })
.then(response => {
const data = typeof response === 'string' ? JSON.parse(response) : response;
return data.data || [];
})
);
}
const additionalPages = await Promise.all(pagePromises);
for (const pageData of additionalPages) {
episodes = episodes.concat(pageData);
}
}
// Transform to Episode format
const formattedEpisodes = episodes.map(ep => ({
id: ep.id,
number: ep.episode,
title: ep.title || `Episode ${ep.episode}`,
snapshot: ep.snapshot,
session: ep.session
}));
// Sort by episode number ascending
formattedEpisodes.sort((a, b) => a.number - b.number);
return formattedEpisodes;
} catch (error) {
throw new Error(`Failed to get episodes: ${error.message}`);
}
}
/**
* Get streaming sources for an episode
* @param {string} animeSession - Anime session ID
* @param {string} episodeSession - Episode session ID
* @returns {Promise<Array>} Array of streaming sources
*/
async getSources(animeSession, episodeSession) {
try {
const playUrl = `${this.base}/play/${animeSession}/${episodeSession}`;
const html = await cloudscraper.get(playUrl, {
headers: this.getHeaders()
});
// Extract button data attributes using regex
const buttonPattern = /<button[^>]+data-src="([^"]+)"[^>]+data-fansub="([^"]+)"[^>]+data-resolution="([^"]+)"[^>]+data-audio="([^"]+)"[^>]*>/g;
const sources = [];
let match;
while ((match = buttonPattern.exec(html)) !== null) {
const [, src, fansub, resolution, audio] = match;
if (src.startsWith('https://kwik.')) {
sources.push({
url: src,
quality: `${resolution}p`,
fansub: fansub,
audio: audio
});
}
}
// Fallback: extract kwik links directly
if (sources.length === 0) {
const kwikPattern = /https:\/\/kwik\.(si|cx|link)\/e\/\w+/g;
let kwikMatch;
while ((kwikMatch = kwikPattern.exec(html)) !== null) {
sources.push({
url: kwikMatch[0],
quality: null,
fansub: null,
audio: null
});
}
}
if (sources.length === 0) {
throw new Error('No kwik links found on play page');
}
// Deduplicate sources by URL
const uniqueSourcesMap = new Map();
for (const source of sources) {
if (!uniqueSourcesMap.has(source.url)) {
uniqueSourcesMap.set(source.url, source);
}
}
const uniqueSources = Array.from(uniqueSourcesMap.values());
// Sort by resolution descending
uniqueSources.sort((a, b) => {
const getResolution = (source) => {
if (!source.quality) return 0;
const match = source.quality.match(/(\d+)p/);
return match ? parseInt(match[1]) : 0;
};
return getResolution(b) - getResolution(a);
});
return uniqueSources;
} catch (error) {
throw new Error(`Failed to get sources: ${error.message}`);
}
}
/**
* Resolve Kwik URL to M3U8 streaming URL
* @param {string} kwikUrl - Kwik page URL
* @returns {Promise<string>} M3U8 streaming URL
*/
async resolveKwikWithNode(kwikUrl) {
try {
// Fetch Kwik page
const html = await cloudscraper.get(kwikUrl, {
headers: this.getHeaders(),
timeout: 20000
});
// Check for direct M3U8 URL in HTML
const directM3u8 = extractM3U8FromText(html);
if (directM3u8) {
return directM3u8;
}
// Extract script blocks containing eval()
const scriptPattern = /<script[^>]*>([\s\S]*?)<\/script>/gi;
const scripts = [];
let scriptMatch;
while ((scriptMatch = scriptPattern.exec(html)) !== null) {
scripts.push(scriptMatch[1]);
}
// Find the best candidate script
let scriptBlock = null;
let largestEvalScript = null;
let maxLen = 0;
for (const script of scripts) {
if (script.includes('eval(')) {
if (script.includes('source') || script.includes('.m3u8') || script.includes('Plyr')) {
scriptBlock = script;
break;
}
if (script.length > maxLen) {
maxLen = script.length;
largestEvalScript = script;
}
}
}
if (!scriptBlock) {
scriptBlock = largestEvalScript;
}
if (!scriptBlock) {
// Try data-src attribute as fallback
const dataSrcPattern = /data-src="([^"]+\.m3u8[^"]*)"/;
const dataSrcMatch = html.match(dataSrcPattern);
if (dataSrcMatch) {
return dataSrcMatch[1];
}
throw new Error('No candidate <script> block found to evaluate');
}
// Transform script for Node.js execution
let transformedScript = scriptBlock.replace(/\bdocument\b/g, 'DOC_STUB');
transformedScript = transformedScript.replace(/^(var|const|let|j)\s*q\s*=/gm, 'window.q = ');
transformedScript += '\ntry { console.log(window.q); } catch(e) { console.log("Variable q not found"); }';
// Create temporary file
const tmpDir = os.tmpdir();
const tmpFile = path.join(tmpDir, `kwik-${Date.now()}-${Math.random().toString(36).substr(2, 9)}.js`);
const wrapperCode = `
globalThis.window = { location: {} };
globalThis.document = { cookie: '' };
const DOC_STUB = globalThis.document;
globalThis.navigator = { userAgent: 'mozilla' };
${transformedScript}
`;
await fs.writeFile(tmpFile, wrapperCode, 'utf8');
// Execute with Node.js
const nodeOutput = await this._executeNodeScript(tmpFile);
// Clean up temp file
try {
await fs.unlink(tmpFile);
} catch (e) {
// Ignore cleanup errors
}
// Extract M3U8 from output
const m3u8FromOutput = extractM3U8FromText(nodeOutput);
if (m3u8FromOutput) {
return m3u8FromOutput;
}
throw new Error(`Could not resolve .m3u8. Node output (first 2000 chars):\n${nodeOutput.substring(0, 2000)}`);
} catch (error) {
throw new Error(`Failed to resolve Kwik URL: ${error.message}`);
}
}
/**
* Execute a Node.js script and capture output
* @param {string} scriptPath - Path to script file
* @returns {Promise<string>} Script output
* @private
*/
async _executeNodeScript(scriptPath) {
return new Promise((resolve, reject) => {
const nodeProcess = spawn('node', [scriptPath]);
let stdout = '';
let stderr = '';
nodeProcess.stdout.on('data', (data) => {
stdout += data.toString();
});
nodeProcess.stderr.on('data', (data) => {
stderr += data.toString();
});
nodeProcess.on('close', (code) => {
const output = stdout + (stderr ? '\n[stderr]\n' + stderr : '');
resolve(output);
});
nodeProcess.on('error', (error) => {
reject(error);
});
});
}
}
module.exports = AnimePahe;

35
lib/utils.js Normal file
View File

@@ -0,0 +1,35 @@
/**
* Utility functions for the AnimePahe scraper
*/
/**
* Returns a random user agent string from a predefined list
* @returns {string} Random user agent string
*/
function randomUserAgent() {
const agents = [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " +
"(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 13_0) AppleWebKit/605.1.15 " +
"(KHTML, like Gecko) Version/16.1 Safari/605.1.15",
"Mozilla/5.0 (Linux; Android 12; SM-G998B) AppleWebKit/537.36 " +
"(KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36",
];
return agents[Math.floor(Math.random() * agents.length)];
}
/**
* Extracts M3U8 URL from text content using regex
* @param {string} text - Text content to search
* @returns {string|null} M3U8 URL if found, null otherwise
*/
function extractM3U8FromText(text) {
const m3u8Pattern = /https?:\/\/[^'"\s<>]+\.m3u8[^\s'")<]*/;
const match = text.match(m3u8Pattern);
return match ? match[0] : null;
}
module.exports = {
randomUserAgent,
extractM3U8FromText
};

5537
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

36
package.json Normal file
View File

@@ -0,0 +1,36 @@
{
"name": "animepahe-scraper-nodejs",
"version": "1.0.0",
"description": "Node.js Express-based web scraper for AnimePahe",
"main": "index.js",
"scripts": {
"start": "node index.js",
"dev": "node index.js",
"test": "jest",
"test:watch": "jest --watch"
},
"keywords": [
"anime",
"scraper",
"animepahe",
"express",
"api"
],
"author": "",
"license": "MIT",
"dependencies": {
"axios": "^1.13.2",
"cheerio": "^1.0.0-rc.12",
"cloudscraper": "^4.6.0",
"cors": "^2.8.5",
"express": "^4.18.2"
},
"devDependencies": {
"fast-check": "^3.15.0",
"jest": "^29.7.0",
"nock": "^13.5.0"
},
"engines": {
"node": ">=18.0.0"
}
}

88
public/index.html Normal file
View File

@@ -0,0 +1,88 @@
<!DOCTYPE html>
<html>
<head>
<title>Animepahe API - Node.js</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
background-color: #f5f5f5;
}
h1 {
color: #333;
}
.endpoint {
background: white;
padding: 15px;
margin: 10px 0;
border-radius: 5px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
code {
background: #f0f0f0;
padding: 2px 6px;
border-radius: 3px;
font-family: 'Courier New', monospace;
}
a {
color: #0066cc;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<h1>🎬 Animepahe API (Node.js)</h1>
<p>Welcome to the Node.js version of the Animepahe scraper API!</p>
<div class="endpoint">
<h3>Health Check</h3>
<p><a href="/health"><code>GET /health</code></a></p>
<p>Check if the API is running</p>
</div>
<div class="endpoint">
<h3>Search Anime</h3>
<p><a href="/search?q=naruto"><code>GET /search?q=naruto</code></a></p>
<p>Search for anime by title</p>
</div>
<div class="endpoint">
<h3>Get Episodes</h3>
<p><code>GET /episodes?session=anime-session-id</code></p>
<p>Get all episodes for an anime (requires session ID from search)</p>
</div>
<div class="endpoint">
<h3>Get Sources</h3>
<p><code>GET /sources?anime_session=xxx&episode_session=yyy</code></p>
<p>Get streaming sources for an episode</p>
</div>
<div class="endpoint">
<h3>Resolve M3U8</h3>
<p><code>GET /m3u8?url=kwik-url</code></p>
<p>Resolve Kwik URL to M3U8 streaming URL</p>
</div>
<div class="endpoint">
<h3>🎥 Proxy Stream (Use this for video playback!)</h3>
<p><code>GET /proxy?url=m3u8-or-ts-url</code></p>
<p><strong>Important:</strong> Use this endpoint to stream videos through the server. This bypasses CORS and referrer restrictions.</p>
<p><strong>Usage:</strong></p>
<ol>
<li>Get M3U8 URL from <code>/m3u8</code> endpoint</li>
<li>Use <code>/proxy?url=&lt;m3u8-url&gt;</code> in your video player</li>
<li>The proxy will handle all video segments automatically</li>
</ol>
</div>
<hr>
<p><strong>🎥 <a href="/player.html">Try the Video Player Demo</a></strong></p>
<p><small>Powered by Node.js + Express</small></p>
</body>
</html>

228
public/player.html Normal file
View File

@@ -0,0 +1,228 @@
<!DOCTYPE html>
<html>
<head>
<title>Video Player Example</title>
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
<style>
body {
font-family: Arial, sans-serif;
max-width: 900px;
margin: 50px auto;
padding: 20px;
background-color: #f5f5f5;
}
h1 {
color: #333;
}
.container {
background: white;
padding: 20px;
border-radius: 5px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.info {
background: #e7f3ff;
padding: 15px;
border-radius: 3px;
margin: 20px 0;
}
.warning {
background: #fff3cd;
border-left: 4px solid #ffc107;
padding: 15px;
margin: 20px 0;
}
input {
width: 100%;
padding: 10px;
margin: 10px 0;
border: 1px solid #ddd;
border-radius: 3px;
box-sizing: border-box;
}
button {
background: #0066cc;
color: white;
padding: 10px 20px;
border: none;
border-radius: 3px;
cursor: pointer;
font-size: 16px;
margin-right: 10px;
}
button:hover {
background: #0052a3;
}
button.secondary {
background: #6c757d;
}
button.secondary:hover {
background: #5a6268;
}
video {
width: 100%;
max-width: 800px;
margin-top: 20px;
background: #000;
}
code {
background: #f0f0f0;
padding: 2px 6px;
border-radius: 3px;
font-family: 'Courier New', monospace;
}
#status {
margin-top: 10px;
padding: 10px;
border-radius: 3px;
}
.success { background: #d4edda; color: #155724; }
.error { background: #f8d7da; color: #721c24; }
.loading { background: #d1ecf1; color: #0c5460; }
</style>
</head>
<body>
<h1>🎬 Video Player (HLS.js)</h1>
<div class="container">
<div class="warning">
<strong>⚠️ Important:</strong> The CDN uses Cloudflare protection. The <code>/proxy</code> endpoint won't work.
This player uses <strong>HLS.js</strong> to play M3U8 URLs directly in your browser.
</div>
<div class="info">
<strong>How to use:</strong>
<ol>
<li>Get the M3U8 URL from the <code>/m3u8</code> endpoint</li>
<li>Paste it below</li>
<li>Click "Load Video" - it will play directly (no proxy needed!)</li>
</ol>
</div>
<label for="m3u8Url"><strong>M3U8 URL:</strong></label>
<input
type="text"
id="m3u8Url"
placeholder="https://example.com/video.m3u8"
/>
<button onclick="loadVideo()">Load Video (Direct)</button>
<button class="secondary" onclick="loadWithProxy()">Try with Proxy (may fail)</button>
<video id="videoPlayer" controls></video>
<div id="status"></div>
</div>
<script>
let hls = null;
function showStatus(message, type) {
const status = document.getElementById('status');
status.textContent = message;
status.className = type;
}
function loadVideo() {
const m3u8Url = document.getElementById('m3u8Url').value;
const video = document.getElementById('videoPlayer');
if (!m3u8Url) {
showStatus('Please enter an M3U8 URL', 'error');
return;
}
showStatus('Loading video with HLS.js...', 'loading');
// Clean up previous instance
if (hls) {
hls.destroy();
}
if (Hls.isSupported()) {
hls = new Hls({
debug: false,
enableWorker: true,
lowLatencyMode: true,
backBufferLength: 90
});
hls.loadSource(m3u8Url);
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, function() {
showStatus('✅ Video loaded successfully! Click play to watch.', 'success');
video.play().catch(e => {
showStatus('Video loaded. Click play button to start.', 'success');
});
});
hls.on(Hls.Events.ERROR, function(event, data) {
if (data.fatal) {
showStatus(`❌ Error: ${data.type} - ${data.details}. Try using a CORS browser extension.`, 'error');
console.error('HLS error:', data);
}
});
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
// Native HLS support (Safari)
video.src = m3u8Url;
video.addEventListener('loadedmetadata', function() {
showStatus('✅ Video loaded successfully!', 'success');
});
video.addEventListener('error', function() {
showStatus('❌ Error loading video. Try using a CORS browser extension.', 'error');
});
} else {
showStatus('❌ Your browser does not support HLS playback.', 'error');
}
}
function loadWithProxy() {
const m3u8Url = document.getElementById('m3u8Url').value;
const video = document.getElementById('videoPlayer');
if (!m3u8Url) {
showStatus('Please enter an M3U8 URL', 'error');
return;
}
showStatus('Trying with proxy (this may fail with Cloudflare-protected CDNs)...', 'loading');
// Clean up previous instance
if (hls) {
hls.destroy();
hls = null;
}
const proxiedUrl = `/proxy?url=${encodeURIComponent(m3u8Url)}`;
if (Hls.isSupported()) {
hls = new Hls();
hls.loadSource(proxiedUrl);
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, function() {
showStatus('✅ Proxy worked! Video loaded.', 'success');
});
hls.on(Hls.Events.ERROR, function(event, data) {
if (data.fatal) {
showStatus(`❌ Proxy failed: ${data.details}. Use "Load Video (Direct)" instead.`, 'error');
}
});
} else {
video.src = proxiedUrl;
video.onloadeddata = () => showStatus('✅ Video loaded!', 'success');
video.onerror = () => showStatus('❌ Proxy failed. Use "Load Video (Direct)" instead.', 'error');
}
}
// Allow Enter key to load video
document.getElementById('m3u8Url').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
loadVideo();
}
});
</script>
</body>
</html>

15
vercel.json Normal file
View File

@@ -0,0 +1,15 @@
{
"version": 2,
"builds": [
{
"src": "index.js",
"use": "@vercel/node"
}
],
"routes": [
{
"src": "/(.*)",
"dest": "/index.js"
}
]
}