From d127129b0ff5bccdf601322e046eb2039418552d Mon Sep 17 00:00:00 2001 From: Md Tahseen Hussain Date: Tue, 20 Jan 2026 00:24:28 +0530 Subject: [PATCH] Initial --- .gitignore | 8 + DEPLOY.md | 164 ++ README.md | 142 ++ START_HERE.txt | 71 + deploy.bat | 31 + index.js | 234 ++ lib/animepahe.js | 348 +++ lib/utils.js | 35 + package-lock.json | 5537 ++++++++++++++++++++++++++++++++++++++++++++ package.json | 36 + public/index.html | 88 + public/player.html | 228 ++ vercel.json | 15 + 13 files changed, 6937 insertions(+) create mode 100644 .gitignore create mode 100644 DEPLOY.md create mode 100644 README.md create mode 100644 START_HERE.txt create mode 100644 deploy.bat create mode 100644 index.js create mode 100644 lib/animepahe.js create mode 100644 lib/utils.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 public/index.html create mode 100644 public/player.html create mode 100644 vercel.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..62ed7fa --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +.vercel/ +coverage/ +*.log +.env +.DS_Store +*.tmp +*.temp diff --git a/DEPLOY.md b/DEPLOY.md new file mode 100644 index 0000000..9d54521 --- /dev/null +++ b/DEPLOY.md @@ -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! ๐Ÿš€ diff --git a/README.md b/README.md new file mode 100644 index 0000000..9eae2cc --- /dev/null +++ b/README.md @@ -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=` - Get episodes +- `GET /sources?anime_session=&episode_session=` - Get sources +- `GET /m3u8?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! + diff --git a/START_HERE.txt b/START_HERE.txt new file mode 100644 index 0000000..29372cf --- /dev/null +++ b/START_HERE.txt @@ -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 + +โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• diff --git a/deploy.bat b/deploy.bat new file mode 100644 index 0000000..98cec5c --- /dev/null +++ b/deploy.bat @@ -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 diff --git a/index.js b/index.js new file mode 100644 index 0000000..ee0e429 --- /dev/null +++ b/index.js @@ -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= 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=', + 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}`); + }); +} diff --git a/lib/animepahe.js b/lib/animepahe.js new file mode 100644 index 0000000..8397d5b --- /dev/null +++ b/lib/animepahe.js @@ -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 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 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 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 = /]+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} 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 = /]*>([\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 + + + +

๐ŸŽฌ Video Player (HLS.js)

+ +
+
+ โš ๏ธ Important: The CDN uses Cloudflare protection. The /proxy endpoint won't work. + This player uses HLS.js to play M3U8 URLs directly in your browser. +
+ +
+ How to use: +
    +
  1. Get the M3U8 URL from the /m3u8 endpoint
  2. +
  3. Paste it below
  4. +
  5. Click "Load Video" - it will play directly (no proxy needed!)
  6. +
+
+ + + + + + + + + +
+
+ + + + diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..964973f --- /dev/null +++ b/vercel.json @@ -0,0 +1,15 @@ +{ + "version": 2, + "builds": [ + { + "src": "index.js", + "use": "@vercel/node" + } + ], + "routes": [ + { + "src": "/(.*)", + "dest": "/index.js" + } + ] +}