mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
Add HDRezka support to metadata processing and update package dependencies
This commit is contained in:
parent
783948de38
commit
5751d755db
8 changed files with 1627 additions and 34 deletions
516
hdrezkas.js
Normal file
516
hdrezkas.js
Normal file
|
|
@ -0,0 +1,516 @@
|
|||
// Simplified standalone script to test hdrezka scraper flow
|
||||
import fetch from 'node-fetch';
|
||||
import readline from 'readline';
|
||||
|
||||
// Constants
|
||||
const rezkaBase = 'https://hdrezka.ag/';
|
||||
const baseHeaders = {
|
||||
'X-Hdrezka-Android-App': '1',
|
||||
'X-Hdrezka-Android-App-Version': '2.2.0',
|
||||
};
|
||||
|
||||
// Parse command line arguments
|
||||
const args = process.argv.slice(2);
|
||||
const argOptions = {
|
||||
title: null,
|
||||
type: null,
|
||||
year: null,
|
||||
season: null,
|
||||
episode: null
|
||||
};
|
||||
|
||||
// Process command line arguments
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === '--title' || args[i] === '-t') {
|
||||
argOptions.title = args[i + 1];
|
||||
i++;
|
||||
} else if (args[i] === '--type' || args[i] === '-m') {
|
||||
argOptions.type = args[i + 1].toLowerCase();
|
||||
i++;
|
||||
} else if (args[i] === '--year' || args[i] === '-y') {
|
||||
argOptions.year = parseInt(args[i + 1]);
|
||||
i++;
|
||||
} else if (args[i] === '--season' || args[i] === '-s') {
|
||||
argOptions.season = parseInt(args[i + 1]);
|
||||
i++;
|
||||
} else if (args[i] === '--episode' || args[i] === '-e') {
|
||||
argOptions.episode = parseInt(args[i + 1]);
|
||||
i++;
|
||||
} else if (args[i] === '--help' || args[i] === '-h') {
|
||||
console.log(`
|
||||
HDRezka Scraper Test Script
|
||||
|
||||
Usage:
|
||||
node hdrezka-test.js [options]
|
||||
|
||||
Options:
|
||||
--title, -t <title> Title to search for
|
||||
--type, -m <type> Media type (movie or show)
|
||||
--year, -y <year> Release year
|
||||
--season, -s <number> Season number (for shows)
|
||||
--episode, -e <number> Episode number (for shows)
|
||||
--help, -h Show this help message
|
||||
|
||||
Examples:
|
||||
node hdrezka-test.js --title "Breaking Bad" --type show --season 1 --episode 3
|
||||
node hdrezka-test.js --title "Inception" --type movie --year 2010
|
||||
node hdrezka-test.js (interactive mode)
|
||||
`);
|
||||
process.exit(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) {
|
||||
throw new Error('No video links found');
|
||||
}
|
||||
|
||||
console.log(`[PARSE] Parsing video links from stream URL data`);
|
||||
const linksArray = inputString.split(',');
|
||||
const result = {};
|
||||
|
||||
linksArray.forEach((link) => {
|
||||
// Handle different quality formats:
|
||||
// 1. Simple format: [360p]https://example.com/video.mp4
|
||||
// 2. HTML format: [<span class="pjs-registered-quality">1080p<img...>]https://example.com/video.mp4
|
||||
|
||||
// Try simple format first (non-HTML)
|
||||
let match = link.match(/\[([^<\]]+)\](https?:\/\/[^\s,]+\.mp4|null)/);
|
||||
|
||||
// If not found, try HTML format with more flexible pattern
|
||||
if (!match) {
|
||||
// Extract quality text from HTML span
|
||||
const qualityMatch = link.match(/\[<span[^>]*>([^<]+)/);
|
||||
// Extract URL separately
|
||||
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];
|
||||
|
||||
// Extract the quality value (e.g., "360p", "1080p Ultra")
|
||||
let quality = qualityText;
|
||||
|
||||
// Skip null URLs (premium content that requires login)
|
||||
if (mp4Url !== 'null') {
|
||||
result[quality] = { type: 'mp4', url: mp4Url };
|
||||
console.log(`[QUALITY] Found ${quality}: ${mp4Url}`);
|
||||
} else {
|
||||
console.log(`[QUALITY] Premium quality ${quality} 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', rezkaBase);
|
||||
fullUrl.searchParams.append('q', media.title);
|
||||
|
||||
console.log(`[REQUEST] Making search request to: ${fullUrl.toString()}`);
|
||||
const response = await fetch(fullUrl.toString(), {
|
||||
headers: baseHeaders
|
||||
});
|
||||
|
||||
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 : `${rezkaBase}${url.startsWith('/') ? url.substring(1) : url}`;
|
||||
console.log(`[REQUEST] Making request to: ${fullUrl}`);
|
||||
|
||||
const response = await fetch(fullUrl, {
|
||||
headers: baseHeaders,
|
||||
});
|
||||
|
||||
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 = `${rezkaBase}ajax/get_cdn_series/`;
|
||||
console.log(`[REQUEST] Making stream request to: ${fullUrl} with action=${media.type === 'show' ? 'get_stream' : 'get_movie'}`);
|
||||
|
||||
// Log the request details
|
||||
console.log('[HDRezka][FETCH DEBUG]', {
|
||||
url: fullUrl,
|
||||
method: 'POST',
|
||||
headers: baseHeaders,
|
||||
body: searchParams.toString()
|
||||
});
|
||||
|
||||
const response = await fetch(fullUrl, {
|
||||
method: 'POST',
|
||||
body: searchParams,
|
||||
headers: baseHeaders,
|
||||
});
|
||||
|
||||
// Log the response details
|
||||
let responseHeaders = {};
|
||||
if (response.headers && typeof response.headers.forEach === 'function') {
|
||||
response.headers.forEach((value, key) => {
|
||||
responseHeaders[key] = value;
|
||||
});
|
||||
} else if (response.headers && response.headers.entries) {
|
||||
for (const [key, value] of response.headers.entries()) {
|
||||
responseHeaders[key] = value;
|
||||
}
|
||||
}
|
||||
const responseText = await response.clone().text();
|
||||
console.log('[HDRezka][FETCH RESPONSE]', {
|
||||
status: response.status,
|
||||
headers: responseHeaders,
|
||||
text: responseText
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const rawText = await response.text();
|
||||
console.log(`[RESPONSE] Stream response length: ${rawText.length}`);
|
||||
|
||||
// Response content-type is text/html, but it's actually JSON
|
||||
try {
|
||||
const parsedResponse = JSON.parse(rawText);
|
||||
console.log(`[RESULT] Parsed response successfully`);
|
||||
|
||||
// Process video qualities and subtitles
|
||||
const qualities = parseVideoLinks(parsedResponse.url);
|
||||
const captions = parseSubtitles(parsedResponse.subtitle);
|
||||
|
||||
// Add the parsed data to the response
|
||||
parsedResponse.formattedQualities = qualities;
|
||||
parsedResponse.formattedCaptions = captions;
|
||||
|
||||
return parsedResponse;
|
||||
} catch (e) {
|
||||
console.error(`[ERROR] Failed to parse JSON response: ${e.message}`);
|
||||
console.log(`[ERROR] Raw response: ${rawText.substring(0, 200)}...`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Main execution
|
||||
async function main() {
|
||||
try {
|
||||
console.log('=== HDREZKA SCRAPER TEST ===');
|
||||
|
||||
let media;
|
||||
|
||||
// Check if we have command line arguments
|
||||
if (argOptions.title) {
|
||||
// Use command line arguments
|
||||
media = {
|
||||
type: argOptions.type || 'show',
|
||||
title: argOptions.title,
|
||||
releaseYear: argOptions.year || null
|
||||
};
|
||||
|
||||
// If it's a show, add season and episode
|
||||
if (media.type === 'show') {
|
||||
media.season = { number: argOptions.season || 1 };
|
||||
media.episode = { number: argOptions.episode || 1 };
|
||||
|
||||
console.log(`Testing scrape for ${media.type}: ${media.title} ${media.releaseYear ? `(${media.releaseYear})` : ''} S${media.season.number}E${media.episode.number}`);
|
||||
} else {
|
||||
console.log(`Testing scrape for ${media.type}: ${media.title} ${media.releaseYear ? `(${media.releaseYear})` : ''}`);
|
||||
}
|
||||
} else {
|
||||
// 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
|
||||
media = {
|
||||
type: mediaType,
|
||||
title: title,
|
||||
releaseYear: releaseYear
|
||||
};
|
||||
|
||||
// If it's a show, get season and episode
|
||||
if (mediaType === 'show') {
|
||||
const seasonNum = await prompt('Enter season number: ').then(num => parseInt(num) || 1);
|
||||
const episodeNum = await prompt('Enter episode number: ').then(num => parseInt(num) || 1);
|
||||
|
||||
media.season = { number: seasonNum };
|
||||
media.episode = { number: episodeNum };
|
||||
|
||||
console.log(`Testing scrape for ${media.type}: ${media.title} ${media.releaseYear ? `(${media.releaseYear})` : ''} S${media.season.number}E${media.episode.number}`);
|
||||
} else {
|
||||
console.log(`Testing scrape for ${media.type}: ${media.title} ${media.releaseYear ? `(${media.releaseYear})` : ''}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 1: Search and find media ID
|
||||
const result = await searchAndFindMediaId(media);
|
||||
if (!result || !result.id) {
|
||||
console.log('No result found, exiting');
|
||||
rl.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 2: Get translator ID
|
||||
const translatorId = await getTranslatorId(result.url, result.id, media);
|
||||
if (!translatorId) {
|
||||
console.log('No translator ID found, exiting');
|
||||
rl.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 3: Get stream
|
||||
const streamData = await getStream(result.id, translatorId, media);
|
||||
if (!streamData) {
|
||||
console.log('No stream data found, exiting');
|
||||
rl.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Format output in clean JSON similar to CLI output
|
||||
const formattedOutput = {
|
||||
embeds: [],
|
||||
stream: [
|
||||
{
|
||||
id: 'primary',
|
||||
type: 'file',
|
||||
flags: ['cors-allowed', 'ip-locked'],
|
||||
captions: streamData.formattedCaptions.map(caption => ({
|
||||
id: caption.url,
|
||||
language: caption.language === 'Русский' ? 'ru' :
|
||||
caption.language === 'Українська' ? 'uk' :
|
||||
caption.language === 'English' ? 'en' : caption.language.toLowerCase(),
|
||||
hasCorsRestrictions: false,
|
||||
type: 'vtt',
|
||||
url: caption.url
|
||||
})),
|
||||
qualities: Object.entries(streamData.formattedQualities).reduce((acc, [quality, data]) => {
|
||||
// Convert quality format to match CLI output
|
||||
// "360p" -> "360", "1080p Ultra" -> "1080" (or keep as is if needed)
|
||||
let qualityKey = quality;
|
||||
const numericMatch = quality.match(/^(\d+)p/);
|
||||
if (numericMatch) {
|
||||
qualityKey = numericMatch[1];
|
||||
}
|
||||
|
||||
acc[qualityKey] = {
|
||||
type: data.type,
|
||||
url: data.url
|
||||
};
|
||||
return acc;
|
||||
}, {})
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Display the formatted output
|
||||
console.log('✓ Done!');
|
||||
console.log(JSON.stringify(formattedOutput, null, 2).replace(/"([^"]+)":/g, '$1:'));
|
||||
|
||||
console.log('=== SCRAPING COMPLETE ===');
|
||||
} catch (error) {
|
||||
console.error(`Error: ${error.message}`);
|
||||
if (error.cause) {
|
||||
console.error(`Cause: ${error.cause.message}`);
|
||||
}
|
||||
} finally {
|
||||
rl.close();
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
1
package-lock.json
generated
1
package-lock.json
generated
|
|
@ -44,6 +44,7 @@
|
|||
"expo-system-ui": "^4.0.9",
|
||||
"expo-web-browser": "~14.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"node-fetch": "^2.6.7",
|
||||
"react": "18.3.1",
|
||||
"react-native": "0.76.9",
|
||||
"react-native-awesome-slider": "^2.9.0",
|
||||
|
|
|
|||
|
|
@ -63,7 +63,8 @@
|
|||
"react-native-video": "^6.12.0",
|
||||
"react-native-web": "~0.19.13",
|
||||
"react-native-wheel-color-picker": "^1.3.1",
|
||||
"subsrt": "^1.1.1"
|
||||
"subsrt": "^1.1.1",
|
||||
"node-fetch": "^2.6.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.25.2",
|
||||
|
|
|
|||
434
scripts/test-hdrezka.js
Normal file
434
scripts/test-hdrezka.js
Normal file
|
|
@ -0,0 +1,434 @@
|
|||
// 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();
|
||||
|
|
@ -3,6 +3,7 @@ import { StreamingContent } from '../services/catalogService';
|
|||
import { catalogService } from '../services/catalogService';
|
||||
import { stremioService } from '../services/stremioService';
|
||||
import { tmdbService } from '../services/tmdbService';
|
||||
import { hdrezkaService } from '../services/hdrezkaService';
|
||||
import { cacheService } from '../services/cacheService';
|
||||
import { Cast, Episode, GroupedEpisodes, GroupedStreams } from '../types/metadata';
|
||||
import { TMDBService } from '../services/tmdbService';
|
||||
|
|
@ -177,6 +178,43 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn =
|
|||
// Loading indicators should probably be managed based on callbacks completing.
|
||||
};
|
||||
|
||||
const processHDRezkaSource = async (type: string, id: string, season?: number, episode?: number, isEpisode = false) => {
|
||||
const sourceStartTime = Date.now();
|
||||
const logPrefix = isEpisode ? 'loadEpisodeStreams' : 'loadStreams';
|
||||
const sourceName = 'hdrezka';
|
||||
|
||||
logger.log(`🔍 [${logPrefix}:${sourceName}] Starting fetch`);
|
||||
|
||||
try {
|
||||
const streams = await hdrezkaService.getStreams(
|
||||
id,
|
||||
type,
|
||||
season,
|
||||
episode
|
||||
);
|
||||
|
||||
const processTime = Date.now() - sourceStartTime;
|
||||
|
||||
if (streams && streams.length > 0) {
|
||||
logger.log(`✅ [${logPrefix}:${sourceName}] Received ${streams.length} streams after ${processTime}ms`);
|
||||
|
||||
// Format response similar to Stremio format for the UI
|
||||
return {
|
||||
'hdrezka': {
|
||||
addonName: 'HDRezka',
|
||||
streams
|
||||
}
|
||||
};
|
||||
} else {
|
||||
logger.log(`⚠️ [${logPrefix}:${sourceName}] No streams found after ${processTime}ms`);
|
||||
return {};
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`❌ [${logPrefix}:${sourceName}] Error:`, error);
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
const processExternalSource = async (sourceType: string, promise: Promise<any>, isEpisode = false) => {
|
||||
const sourceStartTime = Date.now();
|
||||
const logPrefix = isEpisode ? 'loadEpisodeStreams' : 'loadStreams';
|
||||
|
|
@ -603,15 +641,18 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn =
|
|||
// Start Stremio request using the callback method
|
||||
processStremioSource(type, id, false);
|
||||
|
||||
// No external sources are used anymore
|
||||
const fetchPromises: Promise<any>[] = [];
|
||||
// Add HDRezka source
|
||||
const hdrezkaPromise = processExternalSource('hdrezka', processHDRezkaSource(type, id), false);
|
||||
|
||||
// Include HDRezka in fetchPromises array
|
||||
const fetchPromises: Promise<any>[] = [hdrezkaPromise];
|
||||
|
||||
// Wait only for external promises now (none in this case)
|
||||
// Wait only for external promises now
|
||||
const results = await Promise.allSettled(fetchPromises);
|
||||
const totalTime = Date.now() - startTime;
|
||||
console.log(`✅ [loadStreams] External source requests completed in ${totalTime}ms (Stremio continues in background)`);
|
||||
|
||||
const sourceTypes: string[] = []; // No external sources
|
||||
const sourceTypes: string[] = ['hdrezka'];
|
||||
results.forEach((result, index) => {
|
||||
const source = sourceTypes[Math.min(index, sourceTypes.length - 1)];
|
||||
console.log(`📊 [loadStreams:${source}] Status: ${result.status}`);
|
||||
|
|
@ -679,19 +720,25 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn =
|
|||
|
||||
console.log('🔄 [loadEpisodeStreams] Starting stream requests');
|
||||
|
||||
const fetchPromises: Promise<any>[] = [];
|
||||
|
||||
// Start Stremio request using the callback method
|
||||
processStremioSource('series', episodeId, true);
|
||||
|
||||
// Add HDRezka source for episodes
|
||||
const seasonNum = parseInt(season, 10);
|
||||
const episodeNum = parseInt(episode, 10);
|
||||
const hdrezkaPromise = processExternalSource('hdrezka',
|
||||
processHDRezkaSource('series', id, seasonNum, episodeNum, true),
|
||||
true
|
||||
);
|
||||
|
||||
const fetchPromises: Promise<any>[] = [hdrezkaPromise];
|
||||
|
||||
// No external sources are used anymore
|
||||
|
||||
// Wait only for external promises now (none in this case)
|
||||
// Wait only for external promises now
|
||||
const results = await Promise.allSettled(fetchPromises);
|
||||
const totalTime = Date.now() - startTime;
|
||||
console.log(`✅ [loadEpisodeStreams] External source requests completed in ${totalTime}ms (Stremio continues in background)`);
|
||||
|
||||
const sourceTypes: string[] = []; // No external sources
|
||||
const sourceTypes: string[] = ['hdrezka'];
|
||||
results.forEach((result, index) => {
|
||||
const source = sourceTypes[Math.min(index, sourceTypes.length - 1)];
|
||||
console.log(`📊 [loadEpisodeStreams:${source}] Status: ${result.status}`);
|
||||
|
|
@ -703,24 +750,15 @@ export const useMetadata = ({ id, type }: UseMetadataProps): UseMetadataReturn =
|
|||
console.log('🧮 [loadEpisodeStreams] Summary:');
|
||||
console.log(' Total time for external sources:', totalTime + 'ms');
|
||||
|
||||
// Log the final states - might not include all Stremio addons yet
|
||||
console.log('📦 [loadEpisodeStreams] Current combined streams count:',
|
||||
Object.keys(episodeStreams).length > 0 ?
|
||||
Object.values(episodeStreams).reduce((acc, group: any) => acc + group.streams.length, 0) :
|
||||
0
|
||||
);
|
||||
|
||||
// Cache the final streams state - Might be incomplete
|
||||
setEpisodeStreams(prev => {
|
||||
// Cache episode streams - maybe incrementally?
|
||||
setPreloadedEpisodeStreams(currentPreloaded => ({
|
||||
...currentPreloaded,
|
||||
[episodeId]: prev
|
||||
// Update preloaded episode streams for future use
|
||||
if (Object.keys(episodeStreams).length > 0) {
|
||||
setPreloadedEpisodeStreams(prev => ({
|
||||
...prev,
|
||||
[episodeId]: { ...episodeStreams }
|
||||
}));
|
||||
return prev;
|
||||
});
|
||||
}
|
||||
|
||||
// Add a delay before marking loading as complete to give Stremio addons more time
|
||||
// Add a delay before marking loading as complete to give addons more time
|
||||
setTimeout(() => {
|
||||
setLoadingEpisodeStreams(false);
|
||||
}, 10000); // 10 second delay to allow streams to load
|
||||
|
|
|
|||
|
|
@ -68,9 +68,13 @@ const StreamCard = ({ stream, onPress, index, isLoading, statusMessage, theme }:
|
|||
const isDolby = stream.title?.toLowerCase().includes('dolby') || stream.title?.includes('DV');
|
||||
const size = stream.title?.match(/💾\s*([\d.]+\s*[GM]B)/)?.[1];
|
||||
const isDebrid = stream.behaviorHints?.cached;
|
||||
|
||||
// Determine if this is a HDRezka stream
|
||||
const isHDRezka = stream.name === 'HDRezka';
|
||||
|
||||
const displayTitle = stream.name || stream.title || 'Unnamed Stream';
|
||||
const displayAddonName = stream.title || '';
|
||||
// For HDRezka streams, the title contains the quality information
|
||||
const displayTitle = isHDRezka ? `HDRezka ${stream.title}` : (stream.name || stream.title || 'Unnamed Stream');
|
||||
const displayAddonName = isHDRezka ? '' : (stream.title || '');
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
|
|
@ -126,6 +130,13 @@ const StreamCard = ({ stream, onPress, index, isLoading, statusMessage, theme }:
|
|||
<Text style={[styles.chipText, { color: theme.colors.white }]}>DEBRID</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Special badge for HDRezka streams */}
|
||||
{isHDRezka && (
|
||||
<View style={[styles.chip, { backgroundColor: theme.colors.accent }]}>
|
||||
<Text style={[styles.chipText, { color: theme.colors.white }]}>HDREZKA</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
|
|
@ -264,7 +275,7 @@ export const StreamsScreen = () => {
|
|||
setLoadStartTime(now);
|
||||
setProviderLoadTimes({});
|
||||
|
||||
// Reset provider status - only for stremio addons
|
||||
// Reset provider status - include HDRezka
|
||||
setProviderStatus({
|
||||
'stremio': {
|
||||
loading: true,
|
||||
|
|
@ -273,12 +284,21 @@ export const StreamsScreen = () => {
|
|||
message: 'Loading...',
|
||||
timeStarted: now,
|
||||
timeCompleted: 0
|
||||
},
|
||||
'hdrezka': {
|
||||
loading: true,
|
||||
success: false,
|
||||
error: false,
|
||||
message: 'Loading...',
|
||||
timeStarted: now,
|
||||
timeCompleted: 0
|
||||
}
|
||||
});
|
||||
|
||||
// Also update the simpler loading state - only for stremio
|
||||
// Also update the simpler loading state - include HDRezka
|
||||
setLoadingProviders({
|
||||
'stremio': true
|
||||
'stremio': true,
|
||||
'hdrezka': true
|
||||
});
|
||||
}
|
||||
}, [loadingStreams, loadingEpisodeStreams]);
|
||||
|
|
@ -287,14 +307,16 @@ export const StreamsScreen = () => {
|
|||
if (type === 'series' && episodeId) {
|
||||
logger.log(`🎬 Loading episode streams for: ${episodeId}`);
|
||||
setLoadingProviders({
|
||||
'stremio': true
|
||||
'stremio': true,
|
||||
'hdrezka': true
|
||||
});
|
||||
setSelectedEpisode(episodeId);
|
||||
loadEpisodeStreams(episodeId);
|
||||
} else if (type === 'movie') {
|
||||
logger.log(`🎬 Loading movie streams for: ${id}`);
|
||||
setLoadingProviders({
|
||||
'stremio': true
|
||||
'stremio': true,
|
||||
'hdrezka': true
|
||||
});
|
||||
loadStreams();
|
||||
}
|
||||
|
|
@ -550,6 +572,11 @@ export const StreamsScreen = () => {
|
|||
{ id: 'all', name: 'All Providers' },
|
||||
...Array.from(availableProviders)
|
||||
.sort((a, b) => {
|
||||
// Always put HDRezka at the top
|
||||
if (a === 'hdrezka') return -1;
|
||||
if (b === 'hdrezka') return 1;
|
||||
|
||||
// Then sort Stremio addons by installation order
|
||||
const indexA = installedAddons.findIndex(addon => addon.id === a);
|
||||
const indexB = installedAddons.findIndex(addon => addon.id === b);
|
||||
|
||||
|
|
@ -560,6 +587,13 @@ export const StreamsScreen = () => {
|
|||
})
|
||||
.map(provider => {
|
||||
const addonInfo = streams[provider];
|
||||
|
||||
// Special handling for HDRezka
|
||||
if (provider === 'hdrezka') {
|
||||
return { id: provider, name: 'HDRezka' };
|
||||
}
|
||||
|
||||
// Standard handling for Stremio addons
|
||||
const installedAddon = installedAddons.find(addon => addon.id === provider);
|
||||
|
||||
let displayName = provider;
|
||||
|
|
@ -586,6 +620,11 @@ export const StreamsScreen = () => {
|
|||
return addonId === selectedProvider;
|
||||
})
|
||||
.sort(([addonIdA], [addonIdB]) => {
|
||||
// Always put HDRezka at the top
|
||||
if (addonIdA === 'hdrezka') return -1;
|
||||
if (addonIdB === 'hdrezka') return 1;
|
||||
|
||||
// Then sort by Stremio addon installation order
|
||||
const indexA = installedAddons.findIndex(addon => addon.id === addonIdA);
|
||||
const indexB = installedAddons.findIndex(addon => addon.id === addonIdB);
|
||||
|
||||
|
|
@ -637,6 +676,10 @@ export const StreamsScreen = () => {
|
|||
const stream = item;
|
||||
const isLoading = loadingProviders[section.addonId];
|
||||
|
||||
// Special handling for HDRezka streams
|
||||
const quality = stream.title?.match(/(\d+)p/)?.[1] || null;
|
||||
const isHDRezka = section.addonId === 'hdrezka';
|
||||
|
||||
return (
|
||||
<StreamCard
|
||||
key={`${stream.url}-${index}`}
|
||||
|
|
|
|||
499
src/services/hdrezkaService.ts
Normal file
499
src/services/hdrezkaService.ts
Normal file
|
|
@ -0,0 +1,499 @@
|
|||
import { logger } from '../utils/logger';
|
||||
import { Stream } from '../types/metadata';
|
||||
import { tmdbService } from './tmdbService';
|
||||
import axios from 'axios';
|
||||
|
||||
// Use node-fetch if available, otherwise fallback to global fetch
|
||||
let fetchImpl: typeof fetch;
|
||||
try {
|
||||
// @ts-ignore
|
||||
fetchImpl = require('node-fetch');
|
||||
} catch {
|
||||
fetchImpl = fetch;
|
||||
}
|
||||
|
||||
// Constants
|
||||
const REZKA_BASE = 'https://hdrezka.ag/';
|
||||
const BASE_HEADERS = {
|
||||
'X-Hdrezka-Android-App': '1',
|
||||
'X-Hdrezka-Android-App-Version': '2.2.0',
|
||||
};
|
||||
|
||||
class HDRezkaService {
|
||||
private MAX_RETRIES = 3;
|
||||
private RETRY_DELAY = 1000; // 1 second
|
||||
|
||||
// No cookies/session logic needed for Android app API
|
||||
private getHeaders() {
|
||||
return {
|
||||
...BASE_HEADERS,
|
||||
'User-Agent': 'okhttp/4.9.0',
|
||||
};
|
||||
}
|
||||
|
||||
private generateRandomFavs(): string {
|
||||
const randomHex = () => Math.floor(Math.random() * 16).toString(16);
|
||||
const generateSegment = (length: number) => Array.from({ length }, () => randomHex()).join('');
|
||||
|
||||
return `${generateSegment(8)}-${generateSegment(4)}-${generateSegment(4)}-${generateSegment(4)}-${generateSegment(12)}`;
|
||||
}
|
||||
|
||||
private extractTitleAndYear(input: string): { title: string; year: number | null } | null {
|
||||
// Handle multiple formats
|
||||
|
||||
// Format 1: "Title, YEAR, Additional info"
|
||||
const regex1 = /^(.*?),.*?(\d{4})/;
|
||||
const match1 = input.match(regex1);
|
||||
if (match1) {
|
||||
const title = match1[1];
|
||||
const year = match1[2];
|
||||
return { title: title.trim(), year: year ? parseInt(year, 10) : null };
|
||||
}
|
||||
|
||||
// Format 2: "Title (YEAR)"
|
||||
const regex2 = /^(.*?)\s*\((\d{4})\)/;
|
||||
const match2 = input.match(regex2);
|
||||
if (match2) {
|
||||
const title = match2[1];
|
||||
const year = match2[2];
|
||||
return { title: title.trim(), year: year ? parseInt(year, 10) : null };
|
||||
}
|
||||
|
||||
// Format 3: Look for any 4-digit year in the string
|
||||
const yearMatch = input.match(/(\d{4})/);
|
||||
if (yearMatch) {
|
||||
const year = yearMatch[1];
|
||||
// Remove the year and any surrounding brackets/parentheses from the title
|
||||
let title = input.replace(/\s*\(\d{4}\)|\s*\[\d{4}\]|\s*\d{4}/, '');
|
||||
return { title: title.trim(), year: year ? parseInt(year, 10) : null };
|
||||
}
|
||||
|
||||
// If no year found but we have a title
|
||||
if (input.trim()) {
|
||||
return { title: input.trim(), year: null };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private parseVideoLinks(inputString: string | undefined): Record<string, { type: string; url: string }> {
|
||||
if (!inputString) {
|
||||
logger.log('[HDRezka] No video links found');
|
||||
return {};
|
||||
}
|
||||
|
||||
logger.log(`[HDRezka] Parsing video links from stream URL data`);
|
||||
const linksArray = inputString.split(',');
|
||||
const result: Record<string, { type: string; url: string }> = {};
|
||||
|
||||
linksArray.forEach((link) => {
|
||||
// Handle different quality formats:
|
||||
// 1. Simple format: [360p]https://example.com/video.mp4
|
||||
// 2. HTML format: [<span class="pjs-registered-quality">1080p<img...>]https://example.com/video.mp4
|
||||
|
||||
// Try simple format first (non-HTML)
|
||||
let match = link.match(/\[([^<\]]+)\](https?:\/\/[^\s,]+\.mp4|null)/);
|
||||
|
||||
// If not found, try HTML format with more flexible pattern
|
||||
if (!match) {
|
||||
// Extract quality text from HTML span
|
||||
const qualityMatch = link.match(/\[<span[^>]*>([^<]+)/);
|
||||
// Extract URL separately
|
||||
const urlMatch = link.match(/\][^[]*?(https?:\/\/[^\s,]+\.mp4|null)/);
|
||||
|
||||
if (qualityMatch && urlMatch) {
|
||||
match = [link, qualityMatch[1].trim(), urlMatch[1]] as RegExpMatchArray;
|
||||
}
|
||||
}
|
||||
|
||||
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 };
|
||||
logger.log(`[HDRezka] Found ${qualityText}: ${mp4Url}`);
|
||||
} else {
|
||||
logger.log(`[HDRezka] Premium quality ${qualityText} requires login (null URL)`);
|
||||
}
|
||||
} else {
|
||||
logger.log(`[HDRezka] Could not parse quality from: ${link}`);
|
||||
}
|
||||
});
|
||||
|
||||
logger.log(`[HDRezka] Found ${Object.keys(result).length} valid qualities: ${Object.keys(result).join(', ')}`);
|
||||
return result;
|
||||
}
|
||||
|
||||
private parseSubtitles(inputString: string | undefined): Array<{
|
||||
id: string;
|
||||
language: string;
|
||||
hasCorsRestrictions: boolean;
|
||||
type: string;
|
||||
url: string;
|
||||
}> {
|
||||
if (!inputString) {
|
||||
logger.log('[HDRezka] No subtitles found');
|
||||
return [];
|
||||
}
|
||||
|
||||
logger.log(`[HDRezka] Parsing subtitles data`);
|
||||
const linksArray = inputString.split(',');
|
||||
const captions: Array<{
|
||||
id: string;
|
||||
language: string;
|
||||
hasCorsRestrictions: boolean;
|
||||
type: string;
|
||||
url: string;
|
||||
}> = [];
|
||||
|
||||
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,
|
||||
});
|
||||
logger.log(`[HDRezka] Found subtitle ${language}: ${url}`);
|
||||
}
|
||||
});
|
||||
|
||||
logger.log(`[HDRezka] Found ${captions.length} subtitles`);
|
||||
return captions;
|
||||
}
|
||||
|
||||
async searchAndFindMediaId(media: { title: string; type: string; releaseYear?: number }): Promise<{
|
||||
id: string;
|
||||
year: number;
|
||||
type: string;
|
||||
url: string;
|
||||
title: string;
|
||||
} | null> {
|
||||
logger.log(`[HDRezka] 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);
|
||||
|
||||
logger.log(`[HDRezka] Making search request to: ${fullUrl.toString()}`);
|
||||
try {
|
||||
const response = await fetchImpl(fullUrl.toString(), {
|
||||
method: 'GET',
|
||||
headers: this.getHeaders()
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const searchData = await response.text();
|
||||
logger.log(`[HDRezka] Search response length: ${searchData.length}`);
|
||||
|
||||
const movieData: Array<{
|
||||
id: string;
|
||||
year: number;
|
||||
type: string;
|
||||
url: string;
|
||||
title: string;
|
||||
}> = [];
|
||||
|
||||
let match;
|
||||
|
||||
while ((match = itemRegexPattern.exec(searchData)) !== null) {
|
||||
const url = match[1];
|
||||
const titleAndYear = match[3];
|
||||
|
||||
const result = this.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]
|
||||
});
|
||||
logger.log(`[HDRezka] 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);
|
||||
logger.log(`[HDRezka] 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);
|
||||
logger.log(`[HDRezka] Items filtered by type ${media.type}: ${filteredItems.length}`);
|
||||
}
|
||||
|
||||
if (filteredItems.length > 0) {
|
||||
logger.log(`[HDRezka] Selected item: id=${filteredItems[0].id}, title=${filteredItems[0].title}`);
|
||||
return filteredItems[0];
|
||||
} else if (movieData.length > 0) {
|
||||
logger.log(`[HDRezka] No exact match, using first result: id=${movieData[0].id}, title=${movieData[0].title}`);
|
||||
return movieData[0];
|
||||
} else {
|
||||
logger.log(`[HDRezka] No matching items found`);
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`[HDRezka] Search request failed: ${error}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async getTranslatorId(url: string, id: string, mediaType: string): Promise<string | null> {
|
||||
logger.log(`[HDRezka] 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}`;
|
||||
logger.log(`[HDRezka] Making request to: ${fullUrl}`);
|
||||
|
||||
try {
|
||||
const response = await fetchImpl(fullUrl, {
|
||||
method: 'GET',
|
||||
headers: this.getHeaders(),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const responseText = await response.text();
|
||||
logger.log(`[HDRezka] Translator page response length: ${responseText.length}`);
|
||||
|
||||
// Translator ID 238 represents the Original + subtitles player.
|
||||
if (responseText.includes(`data-translator_id="238"`)) {
|
||||
logger.log(`[HDRezka] Found translator ID 238 (Original + subtitles)`);
|
||||
return '238';
|
||||
}
|
||||
|
||||
const functionName = mediaType === 'movie' ? 'initCDNMoviesEvents' : 'initCDNSeriesEvents';
|
||||
const regexPattern = new RegExp(`sof\\.tv\\.${functionName}\\(${id}, ([^,]+)`, 'i');
|
||||
const match = responseText.match(regexPattern);
|
||||
const translatorId = match ? match[1] : null;
|
||||
|
||||
logger.log(`[HDRezka] Extracted translator ID: ${translatorId}`);
|
||||
return translatorId;
|
||||
} catch (error) {
|
||||
logger.error(`[HDRezka] Failed to get translator ID: ${error}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async getStream(id: string, translatorId: string, media: {
|
||||
type: string;
|
||||
season?: { number: number };
|
||||
episode?: { number: number };
|
||||
}): Promise<any> {
|
||||
logger.log(`[HDRezka] Getting stream for id=${id}, translatorId=${translatorId}`);
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.append('id', id);
|
||||
searchParams.append('translator_id', translatorId);
|
||||
|
||||
if (media.type === 'show' && media.season && media.episode) {
|
||||
searchParams.append('season', media.season.number.toString());
|
||||
searchParams.append('episode', media.episode.number.toString());
|
||||
logger.log(`[HDRezka] Show params: season=${media.season.number}, episode=${media.episode.number}`);
|
||||
}
|
||||
|
||||
const randomFavs = this.generateRandomFavs();
|
||||
searchParams.append('favs', randomFavs);
|
||||
searchParams.append('action', media.type === 'show' ? 'get_stream' : 'get_movie');
|
||||
|
||||
const fullUrl = `${REZKA_BASE}ajax/get_cdn_series/`;
|
||||
logger.log(`[HDRezka] Making stream request to: ${fullUrl} with action=${media.type === 'show' ? 'get_stream' : 'get_movie'}`);
|
||||
|
||||
let attempts = 0;
|
||||
const maxAttempts = 3;
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
attempts++;
|
||||
try {
|
||||
// Log the request details
|
||||
logger.log('[HDRezka][AXIOS DEBUG]', {
|
||||
url: fullUrl,
|
||||
method: 'POST',
|
||||
headers: this.getHeaders(),
|
||||
data: searchParams.toString()
|
||||
});
|
||||
const axiosResponse = await axios.post(fullUrl, searchParams.toString(), {
|
||||
headers: {
|
||||
...this.getHeaders(),
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
validateStatus: () => true,
|
||||
});
|
||||
logger.log('[HDRezka][AXIOS RESPONSE]', {
|
||||
status: axiosResponse.status,
|
||||
headers: axiosResponse.headers,
|
||||
data: axiosResponse.data
|
||||
});
|
||||
if (axiosResponse.status !== 200) {
|
||||
throw new Error(`HTTP error! status: ${axiosResponse.status}`);
|
||||
}
|
||||
const responseText = typeof axiosResponse.data === 'string' ? axiosResponse.data : JSON.stringify(axiosResponse.data);
|
||||
logger.log(`[HDRezka] Stream response length: ${responseText.length}`);
|
||||
try {
|
||||
const parsedResponse = typeof axiosResponse.data === 'object' ? axiosResponse.data : JSON.parse(responseText);
|
||||
logger.log(`[HDRezka] Parsed response successfully: ${JSON.stringify(parsedResponse)}`);
|
||||
if (!parsedResponse.success && parsedResponse.message) {
|
||||
logger.error(`[HDRezka] Server returned error: ${parsedResponse.message}`);
|
||||
if (attempts < maxAttempts) {
|
||||
logger.log(`[HDRezka] Retrying stream request (attempt ${attempts + 1}/${maxAttempts})...`);
|
||||
continue;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const qualities = this.parseVideoLinks(parsedResponse.url);
|
||||
const captions = this.parseSubtitles(parsedResponse.subtitle);
|
||||
return {
|
||||
qualities,
|
||||
captions
|
||||
};
|
||||
} catch (e: unknown) {
|
||||
const error = e instanceof Error ? e.message : String(e);
|
||||
logger.error(`[HDRezka] Failed to parse JSON response: ${error}`);
|
||||
if (attempts < maxAttempts) {
|
||||
logger.log(`[HDRezka] Retrying stream request (attempt ${attempts + 1}/${maxAttempts})...`);
|
||||
continue;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`[HDRezka] Stream request failed: ${error}`);
|
||||
if (attempts < maxAttempts) {
|
||||
logger.log(`[HDRezka] Retrying stream request (attempt ${attempts + 1}/${maxAttempts})...`);
|
||||
continue;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
logger.error(`[HDRezka] All stream request attempts failed`);
|
||||
return null;
|
||||
}
|
||||
|
||||
async getStreams(mediaId: string, mediaType: string, season?: number, episode?: number): Promise<Stream[]> {
|
||||
try {
|
||||
logger.log(`[HDRezka] Getting streams for ${mediaType} with ID: ${mediaId}`);
|
||||
|
||||
// First, extract the actual title from TMDB if this is an ID
|
||||
let title = mediaId;
|
||||
let year: number | undefined = undefined;
|
||||
|
||||
if (mediaId.startsWith('tt') || mediaId.startsWith('tmdb:')) {
|
||||
let tmdbId: number | null = null;
|
||||
|
||||
// Handle IMDB IDs
|
||||
if (mediaId.startsWith('tt')) {
|
||||
logger.log(`[HDRezka] Converting IMDB ID to TMDB ID: ${mediaId}`);
|
||||
tmdbId = await tmdbService.findTMDBIdByIMDB(mediaId);
|
||||
}
|
||||
// Handle TMDB IDs
|
||||
else if (mediaId.startsWith('tmdb:')) {
|
||||
tmdbId = parseInt(mediaId.split(':')[1], 10);
|
||||
}
|
||||
|
||||
if (tmdbId) {
|
||||
// Fetch metadata from TMDB API
|
||||
if (mediaType === 'movie') {
|
||||
logger.log(`[HDRezka] Fetching movie details from TMDB for ID: ${tmdbId}`);
|
||||
const movieDetails = await tmdbService.getMovieDetails(tmdbId.toString());
|
||||
if (movieDetails) {
|
||||
title = movieDetails.title;
|
||||
year = movieDetails.release_date ? parseInt(movieDetails.release_date.substring(0, 4), 10) : undefined;
|
||||
logger.log(`[HDRezka] Using movie title "${title}" (${year}) for search`);
|
||||
}
|
||||
} else {
|
||||
logger.log(`[HDRezka] Fetching TV show details from TMDB for ID: ${tmdbId}`);
|
||||
const showDetails = await tmdbService.getTVShowDetails(tmdbId);
|
||||
if (showDetails) {
|
||||
title = showDetails.name;
|
||||
year = showDetails.first_air_date ? parseInt(showDetails.first_air_date.substring(0, 4), 10) : undefined;
|
||||
logger.log(`[HDRezka] Using TV show title "${title}" (${year}) for search`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const media = {
|
||||
title,
|
||||
type: mediaType === 'movie' ? 'movie' : 'show',
|
||||
releaseYear: year
|
||||
};
|
||||
|
||||
// Step 1: Search and find media ID
|
||||
const searchResult = await this.searchAndFindMediaId(media);
|
||||
if (!searchResult || !searchResult.id) {
|
||||
logger.log('[HDRezka] No search results found');
|
||||
return [];
|
||||
}
|
||||
|
||||
// Step 2: Get translator ID
|
||||
const translatorId = await this.getTranslatorId(
|
||||
searchResult.url,
|
||||
searchResult.id,
|
||||
media.type
|
||||
);
|
||||
|
||||
if (!translatorId) {
|
||||
logger.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 this.getStream(searchResult.id, translatorId, streamParams);
|
||||
if (!streamData) {
|
||||
logger.log('[HDRezka] No stream data found');
|
||||
return [];
|
||||
}
|
||||
|
||||
// Convert to Stream format
|
||||
const streams: Stream[] = [];
|
||||
|
||||
Object.entries(streamData.qualities).forEach(([quality, data]: [string, any]) => {
|
||||
streams.push({
|
||||
name: 'HDRezka',
|
||||
title: quality,
|
||||
url: data.url,
|
||||
behaviorHints: {
|
||||
notWebReady: false
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
logger.log(`[HDRezka] Found ${streams.length} streams`);
|
||||
return streams;
|
||||
} catch (error) {
|
||||
logger.error(`[HDRezka] Error getting streams: ${error}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const hdrezkaService = new HDRezkaService();
|
||||
61
src/testHDRezka.js
Normal file
61
src/testHDRezka.js
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
// Test script for HDRezka service
|
||||
const { hdrezkaService } = require('./services/hdrezkaService');
|
||||
|
||||
// Enable more detailed console logging
|
||||
const originalConsoleLog = console.log;
|
||||
console.log = function(...args) {
|
||||
const timestamp = new Date().toISOString();
|
||||
originalConsoleLog(`[${timestamp}]`, ...args);
|
||||
};
|
||||
|
||||
// Test function to get streams from HDRezka
|
||||
async function testHDRezka() {
|
||||
console.log('Testing HDRezka service...');
|
||||
|
||||
// Test a popular movie - "Deadpool & Wolverine" (2024)
|
||||
const movieId = 'tt6263850';
|
||||
console.log(`Testing movie ID: ${movieId}`);
|
||||
|
||||
try {
|
||||
const streams = await hdrezkaService.getStreams(movieId, 'movie');
|
||||
console.log('Streams found:', streams.length);
|
||||
if (streams.length > 0) {
|
||||
console.log('First stream:', {
|
||||
name: streams[0].name,
|
||||
title: streams[0].title,
|
||||
url: streams[0].url.substring(0, 100) + '...' // Only show part of the URL
|
||||
});
|
||||
} else {
|
||||
console.log('No streams found.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error testing HDRezka:', error);
|
||||
}
|
||||
|
||||
// Test a TV show - "House of the Dragon" with a specific episode
|
||||
const showId = 'tt11198330';
|
||||
console.log(`\nTesting TV show ID: ${showId}, Season 2 Episode 1`);
|
||||
|
||||
try {
|
||||
const streams = await hdrezkaService.getStreams(showId, 'series', 2, 1);
|
||||
console.log('Streams found:', streams.length);
|
||||
if (streams.length > 0) {
|
||||
console.log('First stream:', {
|
||||
name: streams[0].name,
|
||||
title: streams[0].title,
|
||||
url: streams[0].url.substring(0, 100) + '...' // Only show part of the URL
|
||||
});
|
||||
} else {
|
||||
console.log('No streams found.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error testing HDRezka TV show:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the test
|
||||
testHDRezka().then(() => {
|
||||
console.log('Test completed.');
|
||||
}).catch(error => {
|
||||
console.error('Test failed:', error);
|
||||
});
|
||||
Loading…
Reference in a new issue