mirror of
https://github.com/Inside4ndroid/TMDB-Embed-API.git
synced 2026-10-22 09:35:11 +00:00
181 lines
5.8 KiB
JavaScript
181 lines
5.8 KiB
JavaScript
const axios = require('axios');
|
|
|
|
const BASE_URL = 'https://vixsrc.to';
|
|
const VIXSRC_HEADERS = {
|
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150 Safari/537.36',
|
|
'Accept': 'application/json, text/javascript, */*; q=0.01',
|
|
'Accept-Language': 'en-US,en;q=0.9',
|
|
'Referer': BASE_URL,
|
|
'Origin': BASE_URL
|
|
};
|
|
|
|
// Step 1: GET /api/movie/{id} or /api/tv/{id}/{s}/{e} → { src: "/embed/..." }
|
|
async function fetchApi(url) {
|
|
try {
|
|
const response = await axios.get(url, { headers: VIXSRC_HEADERS, timeout: 10000 });
|
|
if (response.status !== 200 || !response.data) return null;
|
|
return response.data;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Step 2: GET BASE_URL + sublink.src → HTML embed page
|
|
async function fetchEmbedPage(suburl) {
|
|
try {
|
|
const response = await axios.get(BASE_URL + suburl, {
|
|
headers: { ...VIXSRC_HEADERS, Accept: 'text/html,application/xhtml+xml,*/*' },
|
|
timeout: 10000,
|
|
responseType: 'text'
|
|
});
|
|
if (response.status !== 200) return null;
|
|
return response.data;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Step 3: Extract token, expires, playlist URL from embed HTML
|
|
function extractTokenData(html) {
|
|
const token = html.match(/token["']\s*:\s*["']([^"']+)/)?.[1];
|
|
const expires = html.match(/expires["']\s*:\s*["']([^"']+)/)?.[1];
|
|
const playlist = html.match(/url\s*:\s*["']([^"']+)/)?.[1];
|
|
|
|
if (!token || !expires || !playlist) return null;
|
|
|
|
// Reject expired tokens (with 60s grace period)
|
|
if (parseInt(expires, 10) * 1000 - 60_000 < Date.now()) {
|
|
console.log('[Vixsrc] Token is expired');
|
|
return null;
|
|
}
|
|
|
|
return { token, expires, playlist };
|
|
}
|
|
|
|
// Step 4: Append token params to master URL
|
|
function buildMasterUrl(tokenData) {
|
|
const { token, expires, playlist } = tokenData;
|
|
const sep = playlist.includes('?') ? '&' : '?';
|
|
return `${playlist}${sep}token=${token}&expires=${expires}&h=1`;
|
|
}
|
|
|
|
// Step 5: Fetch the master HLS playlist
|
|
async function fetchPlaylist(masterUrl, pageApiUrl) {
|
|
try {
|
|
const response = await axios.get(masterUrl, {
|
|
headers: { ...VIXSRC_HEADERS, Referer: pageApiUrl },
|
|
timeout: 10000,
|
|
responseType: 'text'
|
|
});
|
|
if (response.status !== 200) return null;
|
|
return response.data;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Step 6: Parse HLS manifest for quality variants, audio tracks, subtitles
|
|
function parsePlaylist(content, masterUrl, pageApiUrl) {
|
|
const sources = [];
|
|
const subtitles = [];
|
|
const audioTracks = [];
|
|
|
|
const lines = content.split('\n');
|
|
|
|
// Audio tracks
|
|
for (const line of lines) {
|
|
if (!line.startsWith('#EXT-X-MEDIA:TYPE=AUDIO')) continue;
|
|
const language = line.match(/LANGUAGE="([^"]+)"/)?.[1] ?? 'unknown';
|
|
const label = line.match(/NAME="([^"]+)"/)?.[1] ?? 'Audio';
|
|
audioTracks.push({ language, label });
|
|
}
|
|
|
|
// Subtitles
|
|
for (const line of lines) {
|
|
if (!line.startsWith('#EXT-X-MEDIA:TYPE=SUBTITLES')) continue;
|
|
const url = line.match(/URI="([^"]+)"/)?.[1];
|
|
if (!url) continue;
|
|
const label = line.match(/NAME="([^"]+)"/)?.[1] ?? 'unknown';
|
|
subtitles.push({ url, label, format: 'vtt' });
|
|
}
|
|
|
|
// Quality variants — find the highest resolution
|
|
const variantRegex = /#EXT-X-STREAM-INF:[^\n]*RESOLUTION=\d+x(\d+)[^\n]*\n([^\n]+)/g;
|
|
let match;
|
|
let bestResolution = 0;
|
|
while ((match = variantRegex.exec(content)) !== null) {
|
|
const res = parseInt(match[1], 10);
|
|
if (res > bestResolution) bestResolution = res;
|
|
}
|
|
|
|
if (bestResolution === 0) return { sources: [], subtitles: [] };
|
|
|
|
sources.push({
|
|
name: `Vixsrc - ${bestResolution}p`,
|
|
title: `Vixsrc - ${bestResolution}p`,
|
|
url: masterUrl,
|
|
quality: `${bestResolution}p`,
|
|
provider: 'Vixsrc',
|
|
headers: {
|
|
'Referer': pageApiUrl,
|
|
'User-Agent': VIXSRC_HEADERS['User-Agent']
|
|
}
|
|
});
|
|
|
|
return { sources, subtitles };
|
|
}
|
|
|
|
async function getVixsrcStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) {
|
|
console.log(`[Vixsrc] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}`);
|
|
|
|
// Build API URL
|
|
let apiUrl;
|
|
if (mediaType === 'movie') {
|
|
apiUrl = `${BASE_URL}/api/movie/${tmdbId}`;
|
|
} else {
|
|
apiUrl = `${BASE_URL}/api/tv/${tmdbId}/${seasonNum}/${episodeNum}`;
|
|
}
|
|
|
|
console.log(`[Vixsrc] Step 1 - Calling API: ${apiUrl}`);
|
|
const apiData = await fetchApi(apiUrl);
|
|
if (!apiData || !apiData.src) {
|
|
console.log('[Vixsrc] No src returned from API');
|
|
return [];
|
|
}
|
|
console.log(`[Vixsrc] Step 2 - Fetching embed page: ${BASE_URL}${apiData.src}`);
|
|
|
|
const html = await fetchEmbedPage(apiData.src);
|
|
if (!html) {
|
|
console.log('[Vixsrc] Failed to fetch embed page');
|
|
return [];
|
|
}
|
|
console.log(`[Vixsrc] Embed HTML length: ${html.length} characters`);
|
|
|
|
const tokenData = extractTokenData(html);
|
|
if (!tokenData) {
|
|
console.log('[Vixsrc] Could not extract token/expires/playlist from embed HTML');
|
|
return [];
|
|
}
|
|
|
|
const masterUrl = buildMasterUrl(tokenData);
|
|
console.log(`[Vixsrc] Step 3 - Master URL: ${masterUrl}`);
|
|
|
|
const playlistContent = await fetchPlaylist(masterUrl, apiUrl);
|
|
if (!playlistContent) {
|
|
console.log('[Vixsrc] Failed to fetch HLS playlist');
|
|
return [];
|
|
}
|
|
|
|
const { sources, subtitles } = parsePlaylist(playlistContent, masterUrl, apiUrl);
|
|
|
|
if (sources.length === 0) {
|
|
console.log('[Vixsrc] No streams found in HLS playlist');
|
|
return [];
|
|
}
|
|
|
|
console.log(`[Vixsrc] Successfully extracted ${sources.length} stream(s). Subtitles: ${subtitles.length}`);
|
|
return sources;
|
|
}
|
|
|
|
module.exports = { getVixsrcStreams };
|