mirror of
https://github.com/Inside4ndroid/TMDB-Embed-API.git
synced 2026-10-22 09:35:11 +00:00
Huge update see changelog for details.
This commit is contained in:
+155
-189
@@ -1,214 +1,180 @@
|
||||
/**
|
||||
* Vixsrc streaming provider integration (standalone)
|
||||
* React Native compatible variant - no external addon framework
|
||||
*/
|
||||
const axios = require('axios');
|
||||
|
||||
// Constants
|
||||
const TMDB_API_KEY = "68e094699525b18a70bab2f86b1fa706";
|
||||
const BASE_URL = 'https://vixsrc.to';
|
||||
let vixsrcUrl;
|
||||
let subtitleApiUrl;
|
||||
// Helper function to make HTTP requests with default headers
|
||||
function makeRequest(url, options = {}) {
|
||||
const defaultHeaders = {
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'Accept': 'application/json,*/*',
|
||||
'Accept-Language': 'en-US,en;q=0.5',
|
||||
'Accept-Encoding': 'gzip, deflate',
|
||||
'Connection': 'keep-alive',
|
||||
...options.headers
|
||||
};
|
||||
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
|
||||
};
|
||||
|
||||
return fetch(url, {
|
||||
method: options.method || 'GET',
|
||||
headers: defaultHeaders,
|
||||
...options
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
return response;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(`[Vixsrc] Request failed for ${url}: ${error.message}`);
|
||||
throw error;
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to get TMDB info
|
||||
function getTmdbInfo(tmdbId, mediaType) {
|
||||
const url = `https://api.themoviedb.org/3/${mediaType === 'tv' ? 'tv' : 'movie'}/${tmdbId}?api_key=${TMDB_API_KEY}`;
|
||||
// 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];
|
||||
|
||||
return makeRequest(url)
|
||||
.then(response => response.json())
|
||||
.then(data => ({ data }));
|
||||
}
|
||||
if (!token || !expires || !playlist) return null;
|
||||
|
||||
// Helper function to parse M3U8 playlist
|
||||
// Removed obsolete parseM3U8Playlist implementation
|
||||
|
||||
// Removed unused helpers getQualityFromResolution and resolveUrl
|
||||
|
||||
// Helper function to extract stream URL from Vixsrc page
|
||||
function extractStreamFromPage(url, contentType, contentId, seasonNum, episodeNum) {
|
||||
|
||||
if (contentType === 'movie') {
|
||||
vixsrcUrl = `${BASE_URL}/movie/${contentId}`;
|
||||
subtitleApiUrl = `https://sub.wyzie.ru/search?id=${contentId}`;
|
||||
} else {
|
||||
vixsrcUrl = `${BASE_URL}/tv/${contentId}/${seasonNum}/${episodeNum}`;
|
||||
subtitleApiUrl = `https://sub.wyzie.ru/search?id=${contentId}&season=${seasonNum}&episode=${episodeNum}`;
|
||||
// Reject expired tokens (with 60s grace period)
|
||||
if (parseInt(expires, 10) * 1000 - 60_000 < Date.now()) {
|
||||
console.log('[Vixsrc] Token is expired');
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(`[Vixsrc] Fetching: ${vixsrcUrl}`);
|
||||
return { token, expires, playlist };
|
||||
}
|
||||
|
||||
return makeRequest(vixsrcUrl, {
|
||||
// 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: {
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
|
||||
'Referer': pageApiUrl,
|
||||
'User-Agent': VIXSRC_HEADERS['User-Agent']
|
||||
}
|
||||
})
|
||||
.then(response => response.text())
|
||||
.then(html => {
|
||||
console.log(`[Vixsrc] HTML length: ${html.length} characters`);
|
||||
});
|
||||
|
||||
let masterPlaylistUrl = null;
|
||||
|
||||
// Method 1: Look for window.masterPlaylist (primary method)
|
||||
if (html.includes('window.masterPlaylist')) {
|
||||
console.log('[Vixsrc] Found window.masterPlaylist');
|
||||
|
||||
const urlMatch = html.match(/url:\s*['"]([^'"]+)['"]/);
|
||||
const tokenMatch = html.match(/['"]?token['"]?\s*:\s*['"]([^'"]+)['"]/);
|
||||
const expiresMatch = html.match(/['"]?expires['"]?\s*:\s*['"]([^'"]+)['"]/);
|
||||
|
||||
if (urlMatch && tokenMatch && expiresMatch) {
|
||||
const baseUrl = urlMatch[1];
|
||||
const token = tokenMatch[1];
|
||||
const expires = expiresMatch[1];
|
||||
|
||||
console.log('[Vixsrc] Extracted tokens:');
|
||||
console.log(` - Base URL: ${baseUrl}`);
|
||||
console.log(` - Token: ${token.substring(0, 20)}...`);
|
||||
console.log(` - Expires: ${expires}`);
|
||||
|
||||
// Construct the master playlist URL
|
||||
if (baseUrl.includes('?b=1')) {
|
||||
masterPlaylistUrl = `${baseUrl}&token=${token}&expires=${expires}&h=1&lang=en`;
|
||||
} else {
|
||||
masterPlaylistUrl = `${baseUrl}?token=${token}&expires=${expires}&h=1&lang=en`;
|
||||
}
|
||||
|
||||
console.log(`[Vixsrc] Constructed master playlist URL: ${masterPlaylistUrl}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Method 2: Look for direct .m3u8 URLs
|
||||
if (!masterPlaylistUrl) {
|
||||
const m3u8Match = html.match(/(https?:\/\/[^'"\s]+\.m3u8[^'"\s]*)/);
|
||||
if (m3u8Match) {
|
||||
masterPlaylistUrl = m3u8Match[1];
|
||||
console.log('[Vixsrc] Found direct .m3u8 URL:', masterPlaylistUrl);
|
||||
}
|
||||
}
|
||||
|
||||
// Method 3: Look for stream URLs in script tags
|
||||
if (!masterPlaylistUrl) {
|
||||
const scriptMatches = html.match(/<script[^>]*>(.*?)<\/script>/gs);
|
||||
if (scriptMatches) {
|
||||
for (const script of scriptMatches) {
|
||||
const streamMatch = script.match(/['"]?(https?:\/\/[^'"\s]+(?:\.m3u8|playlist)[^'"\s]*)/);
|
||||
if (streamMatch) {
|
||||
masterPlaylistUrl = streamMatch[1];
|
||||
console.log('[Vixsrc] Found stream in script:', masterPlaylistUrl);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!masterPlaylistUrl) {
|
||||
console.log('[Vixsrc] No master playlist URL found');
|
||||
return null;
|
||||
}
|
||||
|
||||
return { masterPlaylistUrl, subtitleApiUrl };
|
||||
});
|
||||
return { sources, subtitles };
|
||||
}
|
||||
|
||||
// Helper function to get subtitles
|
||||
function getSubtitles(subtitleApiUrl) {
|
||||
return makeRequest(subtitleApiUrl)
|
||||
.then(response => response.json())
|
||||
.then(subtitleData => {
|
||||
// Find English subtitle track (same logic as original)
|
||||
let subtitleTrack = subtitleData.find(track =>
|
||||
track.display.includes('English') && (track.encoding === 'ASCII' || track.encoding === 'UTF-8')
|
||||
);
|
||||
|
||||
if (!subtitleTrack) {
|
||||
subtitleTrack = subtitleData.find(track => track.display.includes('English') && track.encoding === 'CP1252');
|
||||
}
|
||||
|
||||
if (!subtitleTrack) {
|
||||
subtitleTrack = subtitleData.find(track => track.display.includes('English') && track.encoding === 'CP1250');
|
||||
}
|
||||
|
||||
if (!subtitleTrack) {
|
||||
subtitleTrack = subtitleData.find(track => track.display.includes('English') && track.encoding === 'CP850');
|
||||
}
|
||||
|
||||
const subtitles = subtitleTrack ? subtitleTrack.url : '';
|
||||
console.log(subtitles ? `[Vixsrc] Found subtitles: ${subtitles}` : '[Vixsrc] No English subtitles found');
|
||||
return subtitles;
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('[Vixsrc] Subtitle fetch failed:', error.message);
|
||||
return '';
|
||||
});
|
||||
}
|
||||
|
||||
// Main function to get streams
|
||||
function getVixsrcStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) {
|
||||
async function getVixsrcStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) {
|
||||
console.log(`[Vixsrc] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}`);
|
||||
|
||||
return getTmdbInfo(tmdbId, mediaType)
|
||||
.then(() => extractStreamFromPage(null, mediaType, tmdbId, seasonNum, episodeNum))
|
||||
.then(streamData => {
|
||||
if (!streamData) {
|
||||
console.log('[Vixsrc] No stream data found');
|
||||
return [];
|
||||
}
|
||||
// Build API URL
|
||||
let apiUrl;
|
||||
if (mediaType === 'movie') {
|
||||
apiUrl = `${BASE_URL}/api/movie/${tmdbId}`;
|
||||
} else {
|
||||
apiUrl = `${BASE_URL}/api/tv/${tmdbId}/${seasonNum}/${episodeNum}`;
|
||||
}
|
||||
|
||||
const { masterPlaylistUrl, subtitleApiUrl } = streamData;
|
||||
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}`);
|
||||
|
||||
// Return single master playlist with Auto quality
|
||||
console.log('[Vixsrc] Returning master playlist with Auto quality...');
|
||||
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`);
|
||||
|
||||
// Get subtitles
|
||||
return getSubtitles(subtitleApiUrl)
|
||||
.then(subtitles => {
|
||||
// Return single stream with master playlist
|
||||
const tmdbStreams = [{
|
||||
name: "Vixsrc",
|
||||
title: "Auto Quality Stream",
|
||||
url: masterPlaylistUrl,
|
||||
quality: '1080p',
|
||||
provider: 'Vidsrc',
|
||||
headers: {
|
||||
'Referer': vixsrcUrl,
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
|
||||
}
|
||||
}];
|
||||
const tokenData = extractTokenData(html);
|
||||
if (!tokenData) {
|
||||
console.log('[Vixsrc] Could not extract token/expires/playlist from embed HTML');
|
||||
return [];
|
||||
}
|
||||
|
||||
console.log('[Vixsrc] Successfully processed 1 stream with Auto quality');
|
||||
return tmdbStreams;
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(`[Vixsrc] Error in getVixsrcStreams: ${error.message}`);
|
||||
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 };
|
||||
|
||||
Reference in New Issue
Block a user