first commit

This commit is contained in:
gotolombok
2026-01-31 17:32:07 +08:00
commit 0007660bd8
12 changed files with 1905 additions and 0 deletions

372
src/routes.js Normal file
View 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;