Huge update see changelog for details.

This commit is contained in:
Inside4ndroid
2026-05-16 12:29:45 +01:00
parent 35b95809de
commit d0419321a4
23 changed files with 1328 additions and 3758 deletions
+155 -189
View File
@@ -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 };