373 lines
13 KiB
JavaScript
373 lines
13 KiB
JavaScript
|
|
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;
|