mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
This update removes the XPRIME provider from internal settings, streamlining the provider management process. The HDRezka provider is now prioritized in the UI, improving user experience. Additionally, various components have been optimized for better performance, including enhancements to the VideoPlayer for improved buffering and seeking behavior on Android devices. The app's theme has been updated to a dark mode, and several dependencies have been upgraded for better stability and performance.
434 lines
No EOL
14 KiB
JavaScript
434 lines
No EOL
14 KiB
JavaScript
d// Test script for HDRezka service
|
|
// Run with: node scripts/test-hdrezka.js
|
|
|
|
const fetch = require('node-fetch');
|
|
const readline = require('readline');
|
|
|
|
// Constants
|
|
const REZKA_BASE = 'https://hdrezka.ag/';
|
|
const BASE_HEADERS = {
|
|
'X-Hdrezka-Android-App': '1',
|
|
'X-Hdrezka-Android-App-Version': '2.2.0',
|
|
};
|
|
|
|
// Create readline interface for user input
|
|
const rl = readline.createInterface({
|
|
input: process.stdin,
|
|
output: process.stdout
|
|
});
|
|
|
|
// Function to prompt user for input
|
|
function prompt(question) {
|
|
return new Promise((resolve) => {
|
|
rl.question(question, (answer) => {
|
|
resolve(answer);
|
|
});
|
|
});
|
|
}
|
|
|
|
// Helper functions
|
|
function generateRandomFavs() {
|
|
const randomHex = () => Math.floor(Math.random() * 16).toString(16);
|
|
const generateSegment = (length) => Array.from({ length }, randomHex).join('');
|
|
|
|
return `${generateSegment(8)}-${generateSegment(4)}-${generateSegment(4)}-${generateSegment(4)}-${generateSegment(12)}`;
|
|
}
|
|
|
|
function extractTitleAndYear(input) {
|
|
const regex = /^(.*?),.*?(\d{4})/;
|
|
const match = input.match(regex);
|
|
|
|
if (match) {
|
|
const title = match[1];
|
|
const year = match[2];
|
|
return { title: title.trim(), year: year ? parseInt(year, 10) : null };
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function parseVideoLinks(inputString) {
|
|
if (!inputString) {
|
|
console.warn('No video links found');
|
|
return {};
|
|
}
|
|
|
|
console.log(`[PARSE] Parsing video links from stream URL data`);
|
|
const linksArray = inputString.split(',');
|
|
const result = {};
|
|
|
|
linksArray.forEach((link) => {
|
|
// Handle different quality formats
|
|
let match = link.match(/\[([^<\]]+)\](https?:\/\/[^\s,]+\.mp4|null)/);
|
|
|
|
// If not found, try HTML format with more flexible pattern
|
|
if (!match) {
|
|
const qualityMatch = link.match(/\[<span[^>]*>([^<]+)/);
|
|
const urlMatch = link.match(/\][^[]*?(https?:\/\/[^\s,]+\.mp4|null)/);
|
|
|
|
if (qualityMatch && urlMatch) {
|
|
match = [null, qualityMatch[1].trim(), urlMatch[1]];
|
|
}
|
|
}
|
|
|
|
if (match) {
|
|
const qualityText = match[1].trim();
|
|
const mp4Url = match[2];
|
|
|
|
// Skip null URLs (premium content that requires login)
|
|
if (mp4Url !== 'null') {
|
|
result[qualityText] = { type: 'mp4', url: mp4Url };
|
|
console.log(`[QUALITY] Found ${qualityText}: ${mp4Url}`);
|
|
} else {
|
|
console.log(`[QUALITY] Premium quality ${qualityText} requires login (null URL)`);
|
|
}
|
|
} else {
|
|
console.log(`[WARNING] Could not parse quality from: ${link}`);
|
|
}
|
|
});
|
|
|
|
console.log(`[PARSE] Found ${Object.keys(result).length} valid qualities: ${Object.keys(result).join(', ')}`);
|
|
return result;
|
|
}
|
|
|
|
function parseSubtitles(inputString) {
|
|
if (!inputString) {
|
|
console.log('[SUBTITLES] No subtitles found');
|
|
return [];
|
|
}
|
|
|
|
console.log(`[PARSE] Parsing subtitles data`);
|
|
const linksArray = inputString.split(',');
|
|
const captions = [];
|
|
|
|
linksArray.forEach((link) => {
|
|
const match = link.match(/\[([^\]]+)\](https?:\/\/\S+?)(?=,\[|$)/);
|
|
|
|
if (match) {
|
|
const language = match[1];
|
|
const url = match[2];
|
|
|
|
captions.push({
|
|
id: url,
|
|
language,
|
|
hasCorsRestrictions: false,
|
|
type: 'vtt',
|
|
url: url,
|
|
});
|
|
console.log(`[SUBTITLE] Found ${language}: ${url}`);
|
|
}
|
|
});
|
|
|
|
console.log(`[PARSE] Found ${captions.length} subtitles`);
|
|
return captions;
|
|
}
|
|
|
|
// Main scraper functions
|
|
async function searchAndFindMediaId(media) {
|
|
console.log(`[STEP 1] Searching for title: ${media.title}, type: ${media.type}, year: ${media.releaseYear || 'any'}`);
|
|
|
|
const itemRegexPattern = /<a href="([^"]+)"><span class="enty">([^<]+)<\/span> \(([^)]+)\)/g;
|
|
const idRegexPattern = /\/(\d+)-[^/]+\.html$/;
|
|
|
|
const fullUrl = new URL('/engine/ajax/search.php', REZKA_BASE);
|
|
fullUrl.searchParams.append('q', media.title);
|
|
|
|
console.log(`[REQUEST] Making search request to: ${fullUrl.toString()}`);
|
|
const response = await fetch(fullUrl.toString(), {
|
|
headers: BASE_HEADERS
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const searchData = await response.text();
|
|
console.log(`[RESPONSE] Search response length: ${searchData.length}`);
|
|
|
|
const movieData = [];
|
|
let match;
|
|
|
|
while ((match = itemRegexPattern.exec(searchData)) !== null) {
|
|
const url = match[1];
|
|
const titleAndYear = match[3];
|
|
|
|
const result = extractTitleAndYear(titleAndYear);
|
|
if (result !== null) {
|
|
const id = url.match(idRegexPattern)?.[1] || null;
|
|
const isMovie = url.includes('/films/');
|
|
const isShow = url.includes('/series/');
|
|
const type = isMovie ? 'movie' : isShow ? 'show' : 'unknown';
|
|
|
|
movieData.push({
|
|
id: id ?? '',
|
|
year: result.year ?? 0,
|
|
type,
|
|
url,
|
|
title: match[2]
|
|
});
|
|
console.log(`[MATCH] Found: id=${id}, title=${match[2]}, type=${type}, year=${result.year}`);
|
|
}
|
|
}
|
|
|
|
// If year is provided, filter by year
|
|
let filteredItems = movieData;
|
|
if (media.releaseYear) {
|
|
filteredItems = movieData.filter(item => item.year === media.releaseYear);
|
|
console.log(`[FILTER] Items filtered by year ${media.releaseYear}: ${filteredItems.length}`);
|
|
}
|
|
|
|
// If type is provided, filter by type
|
|
if (media.type) {
|
|
filteredItems = filteredItems.filter(item => item.type === media.type);
|
|
console.log(`[FILTER] Items filtered by type ${media.type}: ${filteredItems.length}`);
|
|
}
|
|
|
|
if (filteredItems.length === 0 && movieData.length > 0) {
|
|
console.log(`[WARNING] No items match the exact criteria. Showing all results:`);
|
|
movieData.forEach((item, index) => {
|
|
console.log(` ${index + 1}. ${item.title} (${item.year}) - ${item.type}`);
|
|
});
|
|
|
|
// Let user select from results
|
|
const selection = await prompt("Enter the number of the item you want to select (or press Enter to use the first result): ");
|
|
const selectedIndex = parseInt(selection) - 1;
|
|
|
|
if (!isNaN(selectedIndex) && selectedIndex >= 0 && selectedIndex < movieData.length) {
|
|
console.log(`[RESULT] Selected item: id=${movieData[selectedIndex].id}, title=${movieData[selectedIndex].title}`);
|
|
return movieData[selectedIndex];
|
|
} else if (movieData.length > 0) {
|
|
console.log(`[RESULT] Using first result: id=${movieData[0].id}, title=${movieData[0].title}`);
|
|
return movieData[0];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
if (filteredItems.length > 0) {
|
|
console.log(`[RESULT] Selected item: id=${filteredItems[0].id}, title=${filteredItems[0].title}`);
|
|
return filteredItems[0];
|
|
} else {
|
|
console.log(`[ERROR] No matching items found`);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function getTranslatorId(url, id, media) {
|
|
console.log(`[STEP 2] Getting translator ID for url=${url}, id=${id}`);
|
|
|
|
// Make sure the URL is absolute
|
|
const fullUrl = url.startsWith('http') ? url : `${REZKA_BASE}${url.startsWith('/') ? url.substring(1) : url}`;
|
|
console.log(`[REQUEST] Making request to: ${fullUrl}`);
|
|
|
|
const response = await fetch(fullUrl, {
|
|
headers: BASE_HEADERS,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const responseText = await response.text();
|
|
console.log(`[RESPONSE] Translator page response length: ${responseText.length}`);
|
|
|
|
// Translator ID 238 represents the Original + subtitles player.
|
|
if (responseText.includes(`data-translator_id="238"`)) {
|
|
console.log(`[RESULT] Found translator ID 238 (Original + subtitles)`);
|
|
return '238';
|
|
}
|
|
|
|
const functionName = media.type === 'movie' ? 'initCDNMoviesEvents' : 'initCDNSeriesEvents';
|
|
const regexPattern = new RegExp(`sof\\.tv\\.${functionName}\\(${id}, ([^,]+)`, 'i');
|
|
const match = responseText.match(regexPattern);
|
|
const translatorId = match ? match[1] : null;
|
|
|
|
console.log(`[RESULT] Extracted translator ID: ${translatorId}`);
|
|
return translatorId;
|
|
}
|
|
|
|
async function getStream(id, translatorId, media) {
|
|
console.log(`[STEP 3] Getting stream for id=${id}, translatorId=${translatorId}`);
|
|
|
|
const searchParams = new URLSearchParams();
|
|
searchParams.append('id', id);
|
|
searchParams.append('translator_id', translatorId);
|
|
|
|
if (media.type === 'show') {
|
|
searchParams.append('season', media.season.number.toString());
|
|
searchParams.append('episode', media.episode.number.toString());
|
|
console.log(`[PARAMS] Show params: season=${media.season.number}, episode=${media.episode.number}`);
|
|
}
|
|
|
|
const randomFavs = generateRandomFavs();
|
|
searchParams.append('favs', randomFavs);
|
|
searchParams.append('action', media.type === 'show' ? 'get_stream' : 'get_movie');
|
|
|
|
const fullUrl = `${REZKA_BASE}ajax/get_cdn_series/`;
|
|
console.log(`[REQUEST] Making stream request to: ${fullUrl} with action=${media.type === 'show' ? 'get_stream' : 'get_movie'}`);
|
|
|
|
const response = await fetch(fullUrl, {
|
|
method: 'POST',
|
|
body: searchParams,
|
|
headers: BASE_HEADERS,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const responseText = await response.text();
|
|
console.log(`[RESPONSE] Stream response length: ${responseText.length}`);
|
|
|
|
// Response content-type is text/html, but it's actually JSON
|
|
try {
|
|
const parsedResponse = JSON.parse(responseText);
|
|
console.log(`[RESULT] Parsed response successfully`);
|
|
|
|
// Process video qualities and subtitles
|
|
const qualities = parseVideoLinks(parsedResponse.url);
|
|
const captions = parseSubtitles(parsedResponse.subtitle);
|
|
|
|
return {
|
|
qualities,
|
|
captions
|
|
};
|
|
} catch (e) {
|
|
console.error(`[ERROR] Failed to parse JSON response: ${e.message}`);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function getStreams(mediaId, mediaType, season, episode) {
|
|
try {
|
|
console.log(`[HDRezka] Getting streams for ${mediaType} with ID: ${mediaId}`);
|
|
|
|
// Check if the mediaId appears to be an ID rather than a title
|
|
let title = mediaId;
|
|
let year;
|
|
|
|
// If it's an ID format (starts with 'tt' for IMDB or contains ':' like TMDB IDs)
|
|
// For testing, we'll replace it with an example title instead of implementing full TMDB API calls
|
|
if (mediaId.startsWith('tt') || mediaId.includes(':')) {
|
|
console.log(`[HDRezka] ID format detected for "${mediaId}". Using title search instead.`);
|
|
|
|
// For demo purposes only - you would actually get this from TMDB API in real implementation
|
|
if (mediaType === 'movie') {
|
|
title = "Inception"; // Example movie
|
|
year = 2010;
|
|
} else {
|
|
title = "Breaking Bad"; // Example show
|
|
year = 2008;
|
|
}
|
|
|
|
console.log(`[HDRezka] Using title "${title}" (${year}) for search instead of ID`);
|
|
}
|
|
|
|
const media = {
|
|
title,
|
|
type: mediaType === 'movie' ? 'movie' : 'show',
|
|
releaseYear: year
|
|
};
|
|
|
|
// Step 1: Search and find media ID
|
|
const searchResult = await searchAndFindMediaId(media);
|
|
if (!searchResult || !searchResult.id) {
|
|
console.log('[HDRezka] No search results found');
|
|
return [];
|
|
}
|
|
|
|
// Step 2: Get translator ID
|
|
const translatorId = await getTranslatorId(
|
|
searchResult.url,
|
|
searchResult.id,
|
|
media
|
|
);
|
|
|
|
if (!translatorId) {
|
|
console.log('[HDRezka] No translator ID found');
|
|
return [];
|
|
}
|
|
|
|
// Step 3: Get stream
|
|
const streamParams = {
|
|
type: media.type,
|
|
season: season ? { number: season } : undefined,
|
|
episode: episode ? { number: episode } : undefined
|
|
};
|
|
|
|
const streamData = await getStream(searchResult.id, translatorId, streamParams);
|
|
if (!streamData) {
|
|
console.log('[HDRezka] No stream data found');
|
|
return [];
|
|
}
|
|
|
|
// Convert to Stream format
|
|
const streams = [];
|
|
|
|
Object.entries(streamData.qualities).forEach(([quality, data]) => {
|
|
streams.push({
|
|
name: 'HDRezka',
|
|
title: quality,
|
|
url: data.url,
|
|
behaviorHints: {
|
|
notWebReady: false
|
|
}
|
|
});
|
|
});
|
|
|
|
console.log(`[HDRezka] Found ${streams.length} streams`);
|
|
return streams;
|
|
} catch (error) {
|
|
console.error(`[HDRezka] Error getting streams: ${error}`);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// Main execution
|
|
async function main() {
|
|
try {
|
|
console.log('=== HDREZKA SCRAPER TEST ===');
|
|
|
|
// Get user input interactively
|
|
const title = await prompt('Enter title to search: ');
|
|
const mediaType = await prompt('Enter media type (movie/show): ').then(type =>
|
|
type.toLowerCase() === 'movie' || type.toLowerCase() === 'show' ? type.toLowerCase() : 'show'
|
|
);
|
|
const releaseYear = await prompt('Enter release year (optional): ').then(year =>
|
|
year ? parseInt(year) : null
|
|
);
|
|
|
|
// Create media object
|
|
let media = {
|
|
title,
|
|
type: mediaType,
|
|
releaseYear
|
|
};
|
|
|
|
let seasonNum, episodeNum;
|
|
|
|
// If it's a show, get season and episode
|
|
if (mediaType === 'show') {
|
|
seasonNum = await prompt('Enter season number: ').then(num => parseInt(num) || 1);
|
|
episodeNum = await prompt('Enter episode number: ').then(num => parseInt(num) || 1);
|
|
|
|
console.log(`Testing scrape for ${media.type}: ${media.title} ${media.releaseYear ? `(${media.releaseYear})` : ''} S${seasonNum}E${episodeNum}`);
|
|
} else {
|
|
console.log(`Testing scrape for ${media.type}: ${media.title} ${media.releaseYear ? `(${media.releaseYear})` : ''}`);
|
|
}
|
|
|
|
const streams = await getStreams(title, mediaType, seasonNum, episodeNum);
|
|
|
|
if (streams && streams.length > 0) {
|
|
console.log('✓ Found streams:');
|
|
console.log(JSON.stringify(streams, null, 2));
|
|
} else {
|
|
console.log('✗ No streams found');
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error(`Error: ${error.message}`);
|
|
} finally {
|
|
rl.close();
|
|
}
|
|
}
|
|
|
|
main();
|