mirror of
https://github.com/mdtahseen7/AnimepaheApi.git
synced 2026-04-17 16:11:44 +00:00
Initial
This commit is contained in:
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
node_modules/
|
||||
.vercel/
|
||||
coverage/
|
||||
*.log
|
||||
.env
|
||||
.DS_Store
|
||||
*.tmp
|
||||
*.temp
|
||||
164
DEPLOY.md
Normal file
164
DEPLOY.md
Normal 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
142
README.md
Normal 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
71
START_HERE.txt
Normal 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
31
deploy.bat
Normal 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
234
index.js
Normal 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
348
lib/animepahe.js
Normal 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
35
lib/utils.js
Normal 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
5537
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
36
package.json
Normal file
36
package.json
Normal 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
88
public/index.html
Normal 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=<m3u8-url></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
228
public/player.html
Normal 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
15
vercel.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"version": 2,
|
||||
"builds": [
|
||||
{
|
||||
"src": "index.js",
|
||||
"use": "@vercel/node"
|
||||
}
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"src": "/(.*)",
|
||||
"dest": "/index.js"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user