first commit
This commit is contained in:
372
src/routes.js
Normal file
372
src/routes.js
Normal file
@@ -0,0 +1,372 @@
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const axios = require('axios');
|
||||
const MovieBoxAPI = require('./api');
|
||||
const { DOWNLOAD_REQUEST_HEADERS, HOST_PROTOCOL, SELECTED_HOST } = require('./constants');
|
||||
|
||||
router.get('/home', async (req, res) => {
|
||||
try {
|
||||
const api = new MovieBoxAPI();
|
||||
const data = await api.getHomepage();
|
||||
res.json(data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/search', async (req, res) => {
|
||||
try {
|
||||
const { keyword, page = 1, perPage = 24, subjectType = 0 } = req.query;
|
||||
if (!keyword) {
|
||||
return res.status(400).json({ error: "Keyword is required" });
|
||||
}
|
||||
const api = new MovieBoxAPI();
|
||||
const data = await api.search(keyword, parseInt(page), parseInt(perPage), parseInt(subjectType));
|
||||
res.json(data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/trending', async (req, res) => {
|
||||
try {
|
||||
const { page = 0, perPage = 18 } = req.query;
|
||||
const api = new MovieBoxAPI();
|
||||
const data = await api.getTrending(parseInt(page), parseInt(perPage));
|
||||
res.json(data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/rank', async (req, res) => {
|
||||
try {
|
||||
const api = new MovieBoxAPI();
|
||||
const data = await api.getHotMovesAndSeries();
|
||||
res.json(data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/recommend', async (req, res) => {
|
||||
try {
|
||||
const { subjectId, page = 1, perPage = 24 } = req.query;
|
||||
if (!subjectId) {
|
||||
return res.status(400).json({ error: "subjectId is required" });
|
||||
}
|
||||
const api = new MovieBoxAPI();
|
||||
const data = await api.getRecommendation(subjectId, parseInt(page), parseInt(perPage));
|
||||
res.json(data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/detail', async (req, res) => {
|
||||
try {
|
||||
const { subjectId } = req.query;
|
||||
if (!subjectId) {
|
||||
return res.status(400).json({ error: "subjectId is required" });
|
||||
}
|
||||
const api = new MovieBoxAPI();
|
||||
const data = await api.getSubjectDetails(subjectId);
|
||||
res.json(data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/stream', async (req, res) => {
|
||||
try {
|
||||
const { subjectId, detailPath, season = 0, episode = 0 } = req.query;
|
||||
if (!subjectId || !detailPath) {
|
||||
return res.status(400).json({ error: "subjectId and detailPath are required" });
|
||||
}
|
||||
const api = new MovieBoxAPI();
|
||||
const data = await api.getStreamUrls(subjectId, detailPath, parseInt(season), parseInt(episode));
|
||||
res.json(data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/download', async (req, res) => {
|
||||
try {
|
||||
const { subjectId, season = 1, episode = 1 } = req.query;
|
||||
if (!subjectId) {
|
||||
return res.status(400).json({ error: "subjectId is required" });
|
||||
}
|
||||
|
||||
const api = new MovieBoxAPI();
|
||||
|
||||
// 1. Fetch Detail to get the correct detailPath (Slug) and warm up the session
|
||||
let detailPath = "video";
|
||||
let subjectType = 1;
|
||||
try {
|
||||
const apiDetail = await api.getSubjectDetails(subjectId);
|
||||
|
||||
if (apiDetail && apiDetail.subject && apiDetail.subject.detailPath) {
|
||||
detailPath = apiDetail.subject.detailPath;
|
||||
} else if (apiDetail && apiDetail.detailPath) {
|
||||
detailPath = apiDetail.detailPath;
|
||||
}
|
||||
|
||||
if (apiDetail && apiDetail.subject && apiDetail.subject.subjectType) {
|
||||
subjectType = apiDetail.subject.subjectType;
|
||||
}
|
||||
|
||||
// Construct the web URL for warming up the session - discovered all used /movies/ path
|
||||
const webUrl = `/movies/${detailPath}`;
|
||||
|
||||
await api.getItemDetails(webUrl);
|
||||
|
||||
} catch (e) {
|
||||
console.warn("[Download] Session warm-up failed:", e.message);
|
||||
}
|
||||
|
||||
// 2. Determine correct season/episode defaults
|
||||
let defaultVal = (parseInt(subjectType) === 1) ? 0 : 1;
|
||||
let finalSeason = req.query.season !== undefined ? parseInt(req.query.season) : defaultVal;
|
||||
let finalEpisode = req.query.episode !== undefined ? parseInt(req.query.episode) : defaultVal;
|
||||
|
||||
// 3. Try fetching Download URLs
|
||||
let data = await api.getDownloadUrls(subjectId, detailPath, finalSeason, finalEpisode, subjectType);
|
||||
|
||||
// 4. Fallback: If downloads empty, try Stream URLs
|
||||
if (!data.downloads || data.downloads.length === 0) {
|
||||
const streamData = await api.getStreamUrls(subjectId, detailPath, finalSeason, finalEpisode, subjectType);
|
||||
const list = streamData.list || streamData.streams || [];
|
||||
if (streamData && list.length > 0) {
|
||||
data.downloads = list.map(item => ({
|
||||
...item,
|
||||
url: item.url || item.path,
|
||||
resolution: item.quality || item.resolution || '720'
|
||||
}));
|
||||
if ((!data.captions || data.captions.length === 0) && streamData.captions) {
|
||||
data.captions = streamData.captions;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Transform for user requirement
|
||||
data.processedSources = [];
|
||||
if (data.downloads) {
|
||||
data.processedSources = data.downloads.map(item => ({
|
||||
id: item.id || `src_${Math.random()}`,
|
||||
quality: item.resolution || 'unknown',
|
||||
directUrl: item.url || "",
|
||||
size: item.size || "0",
|
||||
format: "mp4"
|
||||
}));
|
||||
}
|
||||
|
||||
res.json(data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/generate-stream-link', async (req, res) => {
|
||||
try {
|
||||
const { url } = req.query;
|
||||
if (!url) {
|
||||
return res.status(400).json({ success: false, message: "URL is required" });
|
||||
}
|
||||
|
||||
// Get file info using HEAD request
|
||||
const headRes = await axios.head(url, { headers: DOWNLOAD_REQUEST_HEADERS });
|
||||
const size = headRes.headers['content-length'];
|
||||
const contentType = headRes.headers['content-type'];
|
||||
|
||||
const sizeFormatted = size ? (parseInt(size) / (1024 * 1024)).toFixed(2) + " MB" : "Unknown";
|
||||
|
||||
// Construct proxy URL
|
||||
// Assuming the server is running on req.get('host') or configured host
|
||||
const protocol = req.protocol;
|
||||
const host = req.get('host');
|
||||
const proxyUrl = `${protocol}://${host}/api/proxy-stream?url=${encodeURIComponent(url)}`;
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "URL successfully validated! Open the link below to stream.",
|
||||
instruction: "Copy streamUrl and open in browser/player",
|
||||
streamUrl: proxyUrl,
|
||||
fileInfo: {
|
||||
size: parseInt(size) || 0,
|
||||
sizeFormatted: sizeFormatted,
|
||||
contentType: contentType
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/sources', async (req, res) => {
|
||||
try {
|
||||
const { subjectId, season = 1, episode = 1 } = req.query;
|
||||
if (!subjectId) {
|
||||
return res.status(400).json({ error: "subjectId is required" });
|
||||
}
|
||||
|
||||
const api = new MovieBoxAPI();
|
||||
|
||||
// 1. Fetch Detail to get the correct detailPath (Slug) and warm up the session
|
||||
let detailPath = "video";
|
||||
let subjectType = 1;
|
||||
try {
|
||||
const apiDetail = await api.getSubjectDetails(subjectId);
|
||||
|
||||
if (apiDetail && apiDetail.subject && apiDetail.subject.detailPath) {
|
||||
detailPath = apiDetail.subject.detailPath;
|
||||
} else if (apiDetail && apiDetail.detailPath) {
|
||||
detailPath = apiDetail.detailPath;
|
||||
}
|
||||
|
||||
if (apiDetail && apiDetail.subject && apiDetail.subject.subjectType) {
|
||||
subjectType = apiDetail.subject.subjectType;
|
||||
}
|
||||
|
||||
// Construct the web URL for warming up the session - discovered all used /movies/ path
|
||||
const webUrl = `/movies/${detailPath}`;
|
||||
|
||||
await api.getItemDetails(webUrl);
|
||||
|
||||
} catch (e) {
|
||||
console.warn("[Sources] Session warm-up failed:", e.message);
|
||||
}
|
||||
|
||||
// 2. Determine correct season/episode defaults
|
||||
let defaultVal = (parseInt(subjectType) === 1) ? 0 : 1;
|
||||
let finalSeason = req.query.season !== undefined ? parseInt(req.query.season) : defaultVal;
|
||||
let finalEpisode = req.query.episode !== undefined ? parseInt(req.query.episode) : defaultVal;
|
||||
|
||||
// 3. Try fetching Download URLs
|
||||
let data = await api.getDownloadUrls(subjectId, detailPath, finalSeason, finalEpisode, subjectType);
|
||||
|
||||
// 3. Fallback: If downloads empty, try Stream URLs
|
||||
if (!data.downloads || data.downloads.length === 0) {
|
||||
const streamData = await api.getStreamUrls(subjectId, detailPath, finalSeason, finalEpisode, subjectType);
|
||||
|
||||
// Map stream list (usually data.list or data.data.list) to downloads structure
|
||||
// API getStreamUrls returns data.data which has 'list' or similar?
|
||||
// verifying api.js: getStreamUrls returns data.data.
|
||||
// Let's assume streamData has 'list' or we map 'resources' if present
|
||||
|
||||
// Note: streamData structure might differ slightly (list vs downloads)
|
||||
// But usually keys are: { list: [], ... } or { items: [] }
|
||||
// Let's inspect streamData keys in previous log: ['downloads', 'captions'...] - actually NO, stream calls /play
|
||||
// /play response often has `list`
|
||||
|
||||
const list = streamData.list || streamData.streams || [];
|
||||
if (streamData && list.length > 0) {
|
||||
// Map stream list to downloads format to unify processing
|
||||
data.downloads = list.map(item => ({
|
||||
...item,
|
||||
// Stream items might have 'path' or 'url'
|
||||
url: item.url || item.path,
|
||||
resolution: item.quality || item.resolution || '720' // fallback
|
||||
}));
|
||||
// Merge captions if download didn't have them
|
||||
if ((!data.captions || data.captions.length === 0) && streamData.captions) {
|
||||
data.captions = streamData.captions;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Transform for user requirement
|
||||
data.processedSources = [];
|
||||
if (data.downloads) {
|
||||
data.processedSources = data.downloads.map(item => ({
|
||||
id: item.id || `src_${Math.random()}`,
|
||||
quality: item.resolution || 'unknown',
|
||||
directUrl: item.url || "",
|
||||
size: item.size || "0",
|
||||
format: "mp4" // valid assumption for array of mp4s
|
||||
}));
|
||||
}
|
||||
|
||||
res.json(data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/proxy-stream', async (req, res) => {
|
||||
try {
|
||||
const { url } = req.query;
|
||||
if (!url) {
|
||||
return res.status(400).send("URL is required");
|
||||
}
|
||||
|
||||
const range = req.headers.range;
|
||||
const headers = { ...DOWNLOAD_REQUEST_HEADERS };
|
||||
if (range) {
|
||||
headers['Range'] = range;
|
||||
}
|
||||
|
||||
const response = await axios({
|
||||
method: 'GET',
|
||||
url: url,
|
||||
headers: headers,
|
||||
responseType: 'stream',
|
||||
validateStatus: null // Capture all statuses (200, 206, etc)
|
||||
});
|
||||
|
||||
// Forward headers
|
||||
res.status(response.status);
|
||||
if (response.headers['content-range']) {
|
||||
res.setHeader('Content-Range', response.headers['content-range']);
|
||||
}
|
||||
if (response.headers['content-length']) {
|
||||
res.setHeader('Content-Length', response.headers['content-length']);
|
||||
}
|
||||
if (response.headers['content-type']) {
|
||||
res.setHeader('Content-Type', response.headers['content-type']);
|
||||
}
|
||||
if (response.headers['accept-ranges']) {
|
||||
res.setHeader('Accept-Ranges', response.headers['accept-ranges']);
|
||||
}
|
||||
|
||||
response.data.pipe(res);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Proxy Error:", error.message);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).send("Proxy Error");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
router.get('/apicheck', async (req, res) => {
|
||||
const startTime = Date.now();
|
||||
try {
|
||||
// Ping upstream to verify connectivity
|
||||
const api = new MovieBoxAPI();
|
||||
await api.getHomepage();
|
||||
const latency = Date.now() - startTime;
|
||||
|
||||
res.json({
|
||||
status: "Success",
|
||||
code: 200,
|
||||
message: "API is running smoothly",
|
||||
latency: `${latency}ms`,
|
||||
upstream: "OK"
|
||||
});
|
||||
} catch (error) {
|
||||
const latency = Date.now() - startTime;
|
||||
res.status(500).json({
|
||||
status: "Error",
|
||||
code: 500,
|
||||
message: "Upstream unreachable or Error",
|
||||
error: error.message,
|
||||
latency: `${latency}ms`
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user