feat: Prepare for App Store submission

BREAKING CHANGE: Removes all internal providers, torrenting functionality, and default addons to comply with App Store guidelines. The app now starts with a clean slate, requiring users to manually install addons.
This commit is contained in:
tapframe 2025-06-30 12:52:10 +05:30
parent c41f5b881f
commit ba94a515c8
18 changed files with 143 additions and 2945 deletions

View file

@ -1,516 +0,0 @@
// 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();

View file

@ -1,434 +0,0 @@
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();

View file

@ -44,6 +44,80 @@ const imageCache: Record<string, boolean> = {};
const { width, height } = Dimensions.get('window');
const NoFeaturedContent = () => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { currentTheme } = useTheme();
const styles = StyleSheet.create({
noContentContainer: {
height: height * 0.55,
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: 40,
backgroundColor: currentTheme.colors.elevation1,
borderRadius: 12,
marginBottom: 12,
},
noContentTitle: {
fontSize: 22,
fontWeight: 'bold',
color: currentTheme.colors.highEmphasis,
marginTop: 16,
marginBottom: 8,
textAlign: 'center',
},
noContentText: {
fontSize: 16,
color: currentTheme.colors.mediumEmphasis,
textAlign: 'center',
marginBottom: 24,
},
noContentButtons: {
flexDirection: 'row',
justifyContent: 'center',
gap: 16,
width: '100%',
},
noContentButton: {
paddingVertical: 12,
paddingHorizontal: 20,
borderRadius: 30,
backgroundColor: currentTheme.colors.elevation3,
alignItems: 'center',
justifyContent: 'center'
},
noContentButtonText: {
color: currentTheme.colors.highEmphasis,
fontWeight: '600',
fontSize: 14,
}
});
return (
<View style={styles.noContentContainer}>
<MaterialIcons name="theaters" size={48} color={currentTheme.colors.mediumEmphasis} />
<Text style={styles.noContentTitle}>No Featured Content</Text>
<Text style={styles.noContentText}>
Install addons with catalogs or change the content source in your settings.
</Text>
<View style={styles.noContentButtons}>
<TouchableOpacity
style={[styles.noContentButton, { backgroundColor: currentTheme.colors.primary }]}
onPress={() => navigation.navigate('Addons')}
>
<Text style={[styles.noContentButtonText, { color: currentTheme.colors.white }]}>Install Addons</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.noContentButton}
onPress={() => navigation.navigate('HomeScreenSettings')}
>
<Text style={styles.noContentButtonText}>Settings</Text>
</TouchableOpacity>
</View>
</View>
);
};
const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: FeaturedContentProps) => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { currentTheme } = useTheme();
@ -408,7 +482,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat
};
if (!featuredContent) {
return <SkeletonFeatured />;
return <NoFeaturedContent />;
}
return (

View file

@ -155,11 +155,7 @@ const SourcesModal: React.FC<SourcesModalProps> = ({
if (!showSourcesModal) return null;
const sortedProviders = Object.entries(availableStreams).sort(([a], [b]) => {
if (a === 'hdrezka') return -1;
if (b === 'hdrezka') return 1;
return 0;
});
const sortedProviders = Object.entries(availableStreams);
const handleStreamSelect = (stream: Stream) => {
if (stream.url !== currentStreamUrl && !isChangingSource) {

View file

@ -140,24 +140,29 @@ export function useFeaturedContent() {
if (signal.aborted) return;
// Filter catalogs based on user selection if any catalogs are selected
const filteredCatalogs = selectedCatalogs && selectedCatalogs.length > 0
? catalogs.filter(catalog => {
const catalogId = `${catalog.addon}:${catalog.type}:${catalog.id}`;
return selectedCatalogs.includes(catalogId);
})
: catalogs; // Use all catalogs if none specifically selected
// If no catalogs are installed, stop loading and return.
if (catalogs.length === 0) {
formattedContent = [];
} else {
// Filter catalogs based on user selection if any catalogs are selected
const filteredCatalogs = selectedCatalogs && selectedCatalogs.length > 0
? catalogs.filter(catalog => {
const catalogId = `${catalog.addon}:${catalog.type}:${catalog.id}`;
return selectedCatalogs.includes(catalogId);
})
: catalogs; // Use all catalogs if none specifically selected
// Flatten all catalog items into a single array, filter out items without posters
const allItems = filteredCatalogs.flatMap(catalog => catalog.items)
.filter(item => item.poster)
.filter((item, index, self) =>
// Remove duplicates based on ID
index === self.findIndex(t => t.id === item.id)
);
// Flatten all catalog items into a single array, filter out items without posters
const allItems = filteredCatalogs.flatMap(catalog => catalog.items)
.filter(item => item.poster)
.filter((item, index, self) =>
// Remove duplicates based on ID
index === self.findIndex(t => t.id === item.id)
);
// Sort by popular, newest, etc. (possibly enhanced later)
formattedContent = allItems.sort(() => Math.random() - 0.5).slice(0, 10);
// Sort by popular, newest, etc. (possibly enhanced later)
formattedContent = allItems.sort(() => Math.random() - 0.5).slice(0, 10);
}
}
if (signal.aborted) return;

View file

@ -3,7 +3,6 @@ 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';
@ -184,85 +183,9 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
// 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) => {
try {
const startTime = Date.now();
const result = await promise;
const processingTime = Date.now() - startTime;
if (result && Object.keys(result).length > 0) {
// Update the appropriate state based on whether this is for an episode or not
const updateState = (prevState: GroupedStreams) => {
const newState = { ...prevState };
// Merge in the new streams
Object.entries(result).forEach(([provider, data]: [string, any]) => {
newState[provider] = data;
});
return newState;
};
if (isEpisode) {
setEpisodeStreams(updateState);
} else {
setGroupedStreams(updateState);
}
console.log(`✅ [processExternalSource:${sourceType}] Processed in ${processingTime}ms, found streams:`,
Object.values(result).reduce((acc: number, curr: any) => acc + (curr.streams?.length || 0), 0)
);
// Return the result for the promise chain
return result;
} else {
console.log(`⚠️ [processExternalSource:${sourceType}] No streams found after ${processingTime}ms`);
return {};
}
} catch (error) {
console.error(`❌ [processExternalSource:${sourceType}] Error:`, error);
return {};
}
};
const loadCast = async () => {
if (!metadata || !metadata.id) return;
setLoadingCast(true);
try {
// Handle TMDB IDs
@ -794,44 +717,6 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
console.log('🎬 [loadStreams] Using ID for Stremio addons:', stremioId);
processStremioSource(type, stremioId, false);
// 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
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[] = ['hdrezka'];
results.forEach((result, index) => {
const source = sourceTypes[Math.min(index, sourceTypes.length - 1)];
console.log(`📊 [loadStreams:${source}] Status: ${result.status}`);
if (result.status === 'rejected') {
console.error(`❌ [loadStreams:${source}] Error:`, result.reason);
}
});
console.log('🧮 [loadStreams] Summary:');
console.log(' Total time for external sources:', totalTime + 'ms');
// Log the final states - this might not include all Stremio addons yet
console.log('📦 [loadStreams] Current combined streams count:',
Object.keys(groupedStreams).length > 0 ?
Object.values(groupedStreams).reduce((acc, group: any) => acc + group.streams.length, 0) :
0
);
// Cache the final streams state - Note: This might be incomplete if Stremio addons are slow
setGroupedStreams(prev => {
// We might want to reconsider when exactly to cache or mark loading as fully complete
// cacheService.setStreams(id, type, prev); // Maybe cache incrementally in callback?
setPreloadedStreams(prev);
return prev;
});
// Add a delay before marking loading as complete to give Stremio addons more time
setTimeout(() => {
setLoadingStreams(false);
@ -906,40 +791,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
console.log('🎬 [loadEpisodeStreams] Using episode ID for Stremio addons:', stremioEpisodeId);
processStremioSource('series', stremioEpisodeId, true);
// Add HDRezka source for episodes
const hdrezkaEpisodePromise = processExternalSource('hdrezka',
processHDRezkaSource('series', id, parseInt(season), parseInt(episode), true),
true
);
const fetchPromises: Promise<any>[] = [hdrezkaEpisodePromise];
// 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[] = ['hdrezka'];
results.forEach((result, index) => {
const source = sourceTypes[Math.min(index, sourceTypes.length - 1)];
console.log(`📊 [loadEpisodeStreams:${source}] Status: ${result.status}`);
if (result.status === 'rejected') {
console.error(`❌ [loadEpisodeStreams:${source}] Error:`, result.reason);
}
});
console.log('🧮 [loadEpisodeStreams] Summary:');
console.log(' Total time for external sources:', totalTime + 'ms');
// Update preloaded episode streams for future use
if (Object.keys(episodeStreams).length > 0) {
setPreloadedEpisodeStreams(prev => ({
...prev,
[episodeId]: { ...episodeStreams }
}));
}
// Add a delay before marking loading as complete to give addons more time
// Add a delay before marking loading as complete to give Stremio addons more time
setTimeout(() => {
setLoadingEpisodeStreams(false);
}, 10000); // 10 second delay to allow streams to load

View file

@ -34,7 +34,6 @@ export interface AppSettings {
selectedHeroCatalogs: string[]; // Array of catalog IDs to display in hero section
logoSourcePreference: 'metahub' | 'tmdb'; // Preferred source for title logos
tmdbLanguagePreference: string; // Preferred language for TMDB logos (ISO 639-1 code)
enableInternalProviders: boolean; // Toggle for internal providers like HDRezka
episodeLayoutStyle: 'vertical' | 'horizontal'; // Layout style for episode cards
autoplayBestStream: boolean; // Automatically play the best available stream
}
@ -49,11 +48,10 @@ export const DEFAULT_SETTINGS: AppSettings = {
useExternalPlayer: false,
preferredPlayer: 'internal',
showHeroSection: true,
featuredContentSource: 'tmdb',
featuredContentSource: 'catalogs',
selectedHeroCatalogs: [], // Empty array means all catalogs are selected
logoSourcePreference: 'metahub', // Default to Metahub as first source
tmdbLanguagePreference: 'en', // Default to English
enableInternalProviders: true, // Enable internal providers by default
episodeLayoutStyle: 'horizontal', // Default to the new horizontal layout
autoplayBestStream: false, // Disabled by default for user choice
};

View file

@ -1 +0,0 @@

View file

@ -39,7 +39,6 @@ import PlayerSettingsScreen from '../screens/PlayerSettingsScreen';
import LogoSourceSettings from '../screens/LogoSourceSettings';
import ThemeScreen from '../screens/ThemeScreen';
import ProfilesScreen from '../screens/ProfilesScreen';
import InternalProvidersSettings from '../screens/InternalProvidersSettings';
// Stack navigator types
export type RootStackParamList = {
@ -102,7 +101,6 @@ export type RootStackParamList = {
LogoSourceSettings: undefined;
ThemeSettings: undefined;
ProfilesSettings: undefined;
InternalProvidersSettings: undefined;
};
export type RootStackNavigationProp = NativeStackNavigationProp<RootStackParamList>;
@ -1039,21 +1037,6 @@ const AppNavigator = () => {
},
}}
/>
<Stack.Screen
name="InternalProvidersSettings"
component={InternalProvidersSettings}
options={{
animation: Platform.OS === 'android' ? 'slide_from_right' : 'fade',
animationDuration: Platform.OS === 'android' ? 250 : 200,
presentation: 'card',
gestureEnabled: true,
gestureDirection: 'horizontal',
headerShown: false,
contentStyle: {
backgroundColor: currentTheme.colors.darkBackground,
},
}}
/>
</Stack.Navigator>
</View>
</PaperProvider>

View file

@ -1,491 +0,0 @@
import React, { useState, useCallback, useEffect } from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
SafeAreaView,
Platform,
TouchableOpacity,
StatusBar,
Switch,
Alert,
} from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { useSettings } from '../hooks/useSettings';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
import { useTheme } from '../contexts/ThemeContext';
import AsyncStorage from '@react-native-async-storage/async-storage';
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
interface SettingItemProps {
title: string;
description?: string;
icon: string;
value: boolean;
onValueChange: (value: boolean) => void;
isLast?: boolean;
badge?: string;
}
const SettingItem: React.FC<SettingItemProps> = ({
title,
description,
icon,
value,
onValueChange,
isLast,
badge,
}) => {
const { currentTheme } = useTheme();
return (
<View
style={[
styles.settingItem,
!isLast && styles.settingItemBorder,
{ borderBottomColor: 'rgba(255,255,255,0.08)' },
]}
>
<View style={styles.settingContent}>
<View style={[
styles.settingIconContainer,
{ backgroundColor: 'rgba(255,255,255,0.1)' }
]}>
<MaterialIcons
name={icon}
size={20}
color={currentTheme.colors.primary}
/>
</View>
<View style={styles.settingText}>
<View style={styles.titleRow}>
<Text
style={[
styles.settingTitle,
{ color: currentTheme.colors.text },
]}
>
{title}
</Text>
{badge && (
<View style={[styles.badge, { backgroundColor: currentTheme.colors.primary }]}>
<Text style={styles.badgeText}>{badge}</Text>
</View>
)}
</View>
{description && (
<Text
style={[
styles.settingDescription,
{ color: currentTheme.colors.textMuted },
]}
>
{description}
</Text>
)}
</View>
<Switch
value={value}
onValueChange={onValueChange}
trackColor={{ false: 'rgba(255,255,255,0.1)', true: currentTheme.colors.primary }}
thumbColor={Platform.OS === 'android' ? (value ? currentTheme.colors.white : currentTheme.colors.white) : ''}
ios_backgroundColor={'rgba(255,255,255,0.1)'}
/>
</View>
</View>
);
};
const InternalProvidersSettings: React.FC = () => {
const { settings, updateSetting } = useSettings();
const { currentTheme } = useTheme();
const navigation = useNavigation();
// Individual provider states
const [hdrezkaEnabled, setHdrezkaEnabled] = useState(true);
// Load individual provider settings
useEffect(() => {
const loadProviderSettings = async () => {
try {
const hdrezkaSettings = await AsyncStorage.getItem('hdrezka_settings');
if (hdrezkaSettings) {
const parsed = JSON.parse(hdrezkaSettings);
setHdrezkaEnabled(parsed.enabled !== false);
}
} catch (error) {
console.error('Error loading provider settings:', error);
}
};
loadProviderSettings();
}, []);
const handleBack = () => {
navigation.goBack();
};
const handleMasterToggle = useCallback((enabled: boolean) => {
if (!enabled) {
Alert.alert(
'Disable Internal Providers',
'This will disable all built-in streaming providers (HDRezka). You can still use external Stremio addons.',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Disable',
style: 'destructive',
onPress: () => {
updateSetting('enableInternalProviders', false);
}
}
]
);
} else {
updateSetting('enableInternalProviders', true);
}
}, [updateSetting]);
const handleHdrezkaToggle = useCallback(async (enabled: boolean) => {
setHdrezkaEnabled(enabled);
try {
await AsyncStorage.setItem('hdrezka_settings', JSON.stringify({ enabled }));
} catch (error) {
console.error('Error saving HDRezka settings:', error);
}
}, []);
return (
<SafeAreaView
style={[
styles.container,
{ backgroundColor: currentTheme.colors.darkBackground },
]}
>
<StatusBar
translucent
backgroundColor="transparent"
barStyle="light-content"
/>
<View style={styles.header}>
<TouchableOpacity
onPress={handleBack}
style={styles.backButton}
activeOpacity={0.7}
>
<MaterialIcons
name="arrow-back"
size={24}
color={currentTheme.colors.text}
/>
</TouchableOpacity>
<Text
style={[
styles.headerTitle,
{ color: currentTheme.colors.text },
]}
>
Internal Providers
</Text>
</View>
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
>
{/* Master Toggle Section */}
<View style={styles.section}>
<Text
style={[
styles.sectionTitle,
{ color: currentTheme.colors.textMuted },
]}
>
MASTER CONTROL
</Text>
<View
style={[
styles.card,
{ backgroundColor: currentTheme.colors.elevation2 },
]}
>
<SettingItem
title="Enable Internal Providers"
description="Toggle all built-in streaming providers on/off"
icon="toggle-on"
value={settings.enableInternalProviders}
onValueChange={handleMasterToggle}
isLast={true}
/>
</View>
</View>
{/* Individual Providers Section */}
{settings.enableInternalProviders && (
<View style={styles.section}>
<Text
style={[
styles.sectionTitle,
{ color: currentTheme.colors.textMuted },
]}
>
INDIVIDUAL PROVIDERS
</Text>
<View
style={[
styles.card,
{ backgroundColor: currentTheme.colors.elevation2 },
]}
>
<SettingItem
title="HDRezka"
description="Popular streaming service with multiple quality options"
icon="hd"
value={hdrezkaEnabled}
onValueChange={handleHdrezkaToggle}
isLast={true}
/>
</View>
</View>
)}
{/* Information Section */}
<View style={styles.section}>
<Text
style={[
styles.sectionTitle,
{ color: currentTheme.colors.textMuted },
]}
>
INFORMATION
</Text>
<View
style={[
styles.infoCard,
{
backgroundColor: currentTheme.colors.elevation2,
borderColor: `${currentTheme.colors.primary}30`
},
]}
>
<MaterialIcons
name="info-outline"
size={24}
color={currentTheme.colors.primary}
style={styles.infoIcon}
/>
<View style={styles.infoContent}>
<Text
style={[
styles.infoTitle,
{ color: currentTheme.colors.text },
]}
>
About Internal Providers
</Text>
<Text
style={[
styles.infoDescription,
{ color: currentTheme.colors.textMuted },
]}
>
Internal providers are built directly into the app and don't require separate addon installation. They complement your Stremio addons by providing additional streaming sources.
</Text>
<View style={styles.featureList}>
<View style={styles.featureItem}>
<MaterialIcons
name="check-circle"
size={16}
color={currentTheme.colors.primary}
/>
<Text
style={[
styles.featureText,
{ color: currentTheme.colors.textMuted },
]}
>
No addon installation required
</Text>
</View>
<View style={styles.featureItem}>
<MaterialIcons
name="check-circle"
size={16}
color={currentTheme.colors.primary}
/>
<Text
style={[
styles.featureText,
{ color: currentTheme.colors.textMuted },
]}
>
Multiple quality options
</Text>
</View>
<View style={styles.featureItem}>
<MaterialIcons
name="check-circle"
size={16}
color={currentTheme.colors.primary}
/>
<Text
style={[
styles.featureText,
{ color: currentTheme.colors.textMuted },
]}
>
Fast and reliable streaming
</Text>
</View>
</View>
</View>
</View>
</View>
</ScrollView>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
header: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 16,
paddingTop: Platform.OS === 'android' ? ANDROID_STATUSBAR_HEIGHT + 16 : 16,
paddingBottom: 16,
},
backButton: {
padding: 8,
marginRight: 8,
},
headerTitle: {
fontSize: 24,
fontWeight: '700',
flex: 1,
},
scrollView: {
flex: 1,
},
scrollContent: {
padding: 16,
paddingBottom: 100,
},
section: {
marginBottom: 24,
},
sectionTitle: {
fontSize: 13,
fontWeight: '600',
letterSpacing: 0.8,
marginBottom: 8,
paddingHorizontal: 4,
},
card: {
borderRadius: 16,
overflow: 'hidden',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
settingItem: {
padding: 16,
borderBottomWidth: 0.5,
},
settingItemBorder: {
borderBottomWidth: 0.5,
},
settingContent: {
flexDirection: 'row',
alignItems: 'center',
},
settingIconContainer: {
marginRight: 16,
width: 36,
height: 36,
borderRadius: 10,
alignItems: 'center',
justifyContent: 'center',
},
settingText: {
flex: 1,
},
titleRow: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 4,
},
settingTitle: {
fontSize: 16,
fontWeight: '500',
},
settingDescription: {
fontSize: 14,
opacity: 0.8,
lineHeight: 20,
},
badge: {
height: 18,
minWidth: 18,
borderRadius: 9,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 6,
marginLeft: 8,
},
badgeText: {
color: 'white',
fontSize: 10,
fontWeight: '600',
},
infoCard: {
borderRadius: 16,
padding: 16,
flexDirection: 'row',
borderWidth: 1,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
infoIcon: {
marginRight: 12,
marginTop: 2,
},
infoContent: {
flex: 1,
},
infoTitle: {
fontSize: 16,
fontWeight: '600',
marginBottom: 8,
},
infoDescription: {
fontSize: 14,
lineHeight: 20,
marginBottom: 12,
},
featureList: {
gap: 6,
},
featureItem: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
featureText: {
fontSize: 14,
flex: 1,
},
});
export default InternalProvidersSettings;

View file

@ -12,7 +12,8 @@ import {
Platform,
Dimensions,
Image,
Button
Button,
Linking
} from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useNavigation } from '@react-navigation/native';
@ -477,11 +478,11 @@ const SettingsScreen: React.FC = () => {
badge={catalogCount}
/>
<SettingItem
title="Internal Providers"
description="Enable or disable built-in providers like HDRezka"
icon="source"
renderControl={ChevronRight}
onPress={() => navigation.navigate('InternalProvidersSettings')}
title="Trakt"
icon="sync"
onPress={() => navigation.navigate('TraktSettings')}
renderControl={() => <ChevronRight />}
badge={isAuthenticated ? `Logged in as ${userProfile?.username}`: "Not Logged In"}
/>
<SettingItem
title="Home Screen"
@ -602,6 +603,17 @@ const SettingsScreen: React.FC = () => {
</SettingsCard>
)}
<SettingsCard title="About">
<SettingItem
title="Privacy Policy"
description="Information about data collection and usage"
icon="policy"
renderControl={ChevronRight}
onPress={() => Linking.openURL('https://github.com/Stremio/stremio-expo/blob/main/PRIVACY_POLICY.md').catch(err => console.error("Couldn't load page", err))}
isLast={true}
/>
</SettingsCard>
<View style={styles.versionContainer}>
<Text style={[styles.versionText, {color: currentTheme.colors.mediumEmphasis}]}>
Version 1.0.0

View file

@ -72,13 +72,6 @@ const StreamCard = ({ stream, onPress, index, isLoading, statusMessage, theme }:
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';
// 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 || '');
// Animation delay based on index - stagger effect
const enterDelay = 100 + (index * 30);
@ -100,11 +93,11 @@ const StreamCard = ({ stream, onPress, index, isLoading, statusMessage, theme }:
<View style={styles.streamNameRow}>
<View style={styles.streamTitleContainer}>
<Text style={[styles.streamName, { color: theme.colors.highEmphasis }]}>
{displayTitle}
{stream.name || stream.title || 'Unnamed Stream'}
</Text>
{displayAddonName && displayAddonName !== displayTitle && (
{stream.title && stream.title !== stream.name && (
<Text style={[styles.streamAddonName, { color: theme.colors.mediumEmphasis }]}>
{displayAddonName}
{stream.title}
</Text>
)}
</View>
@ -140,13 +133,6 @@ 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>
@ -332,7 +318,7 @@ export const StreamsScreen = () => {
}
// Update loading states for individual providers
const expectedProviders = ['stremio', 'hdrezka'];
const expectedProviders = ['stremio'];
const now = Date.now();
setLoadingProviders(prevLoading => {
@ -359,9 +345,9 @@ export const StreamsScreen = () => {
// Update useEffect to check for sources
useEffect(() => {
const checkProviders = async () => {
// Check for both Stremio addons and if the internal provider is enabled
// Check for Stremio addons
const hasStremioProviders = await stremioService.hasStreamProviders();
const hasProviders = hasStremioProviders || settings.enableInternalProviders;
const hasProviders = hasStremioProviders;
if (!isMounted.current) return;
@ -377,8 +363,7 @@ export const StreamsScreen = () => {
if (type === 'series' && episodeId) {
logger.log(`🎬 Loading episode streams for: ${episodeId}`);
setLoadingProviders({
'stremio': true,
'hdrezka': true
'stremio': true
});
setSelectedEpisode(episodeId);
loadEpisodeStreams(episodeId);
@ -399,7 +384,7 @@ export const StreamsScreen = () => {
};
checkProviders();
}, [type, id, episodeId, settings.autoplayBestStream, settings.enableInternalProviders]);
}, [type, id, episodeId, settings.autoplayBestStream]);
React.useEffect(() => {
// Trigger entrance animations
@ -474,8 +459,6 @@ export const StreamsScreen = () => {
// Provider priority (higher number = higher priority)
const getProviderPriority = (addonId: string): number => {
if (addonId === 'hdrezka') return 100; // HDRezka highest priority
// Get Stremio addon installation order (earlier = higher priority)
const installedAddons = stremioService.getInstalledAddons();
const addonIndex = installedAddons.findIndex(addon => addon.id === addonId);
@ -564,8 +547,7 @@ export const StreamsScreen = () => {
const streamsToPass = type === 'series' ? episodeStreams : groupedStreams;
// Determine the stream name using the same logic as StreamCard
const isHDRezka = stream.name === 'HDRezka';
const streamName = isHDRezka ? `HDRezka ${stream.title}` : (stream.name || stream.title || 'Unnamed Stream');
const streamName = stream.name || stream.title || 'Unnamed Stream';
navigation.navigate('Player', {
uri: stream.url,
@ -805,11 +787,7 @@ export const StreamsScreen = () => {
{ id: 'all', name: 'All Providers' },
...Array.from(allProviders)
.sort((a, b) => {
// Put HDRezka first
if (a === 'hdrezka') return -1;
if (b === 'hdrezka') return 1;
// Then sort by Stremio addon installation order
// Sort by Stremio addon installation order
const indexA = installedAddons.findIndex(addon => addon.id === a);
const indexB = installedAddons.findIndex(addon => addon.id === b);
@ -821,11 +799,6 @@ 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);
@ -895,11 +868,7 @@ export const StreamsScreen = () => {
return addonId === selectedProvider;
})
.sort(([addonIdA], [addonIdB]) => {
// Put HDRezka first
if (addonIdA === 'hdrezka') return -1;
if (addonIdB === 'hdrezka') return 1;
// Then sort by Stremio addon installation order
// Sort by Stremio addon installation order
const indexA = installedAddons.findIndex(addon => addon.id === addonIdA);
const indexB = installedAddons.findIndex(addon => addon.id === addonIdB);
@ -909,21 +878,13 @@ export const StreamsScreen = () => {
return 0;
})
.map(([addonId, { addonName, streams: providerStreams }]) => {
let sortedProviderStreams = providerStreams;
if (addonId === 'hdrezka') {
sortedProviderStreams = [...providerStreams].sort((a, b) => {
const qualityA = getQualityNumeric(a.title);
const qualityB = getQualityNumeric(b.title);
return qualityB - qualityA; // Sort descending (e.g., 1080p before 720p)
});
} else {
// Sort other streams by quality if possible
sortedProviderStreams = [...providerStreams].sort((a, b) => {
const qualityA = getQualityNumeric(a.name || a.title);
const qualityB = getQualityNumeric(b.name || b.title);
return qualityB - qualityA; // Sort descending
});
}
// Sort streams by quality if possible
const sortedProviderStreams = [...providerStreams].sort((a, b) => {
const qualityA = getQualityNumeric(a.name || a.title);
const qualityB = getQualityNumeric(b.name || b.title);
return qualityB - qualityA; // Sort descending
});
return {
title: addonName,
addonId,

View file

@ -248,6 +248,7 @@ const TraktSettingsScreen: React.FC = () => {
</Text>
{userProfile.vip && (
<View style={styles.vipBadge}>
<MaterialIcons name="star" size={14} color="#FFF" />
<Text style={styles.vipText}>VIP</Text>
</View>
)}
@ -267,13 +268,11 @@ const TraktSettingsScreen: React.FC = () => {
style={[
styles.button,
styles.signOutButton,
{ backgroundColor: isDarkMode ? 'rgba(255,59,48,0.1)' : 'rgba(255,59,48,0.08)' }
{ backgroundColor: currentTheme.colors.error }
]}
onPress={handleSignOut}
>
<Text style={[styles.buttonText, { color: '#FF3B30' }]}>
Sign Out
</Text>
<Text style={styles.buttonText}>Sign Out</Text>
</TouchableOpacity>
</View>
) : (

View file

@ -1,543 +0,0 @@
import { logger } from '../utils/logger';
import { Stream } from '../types/metadata';
import { tmdbService } from './tmdbService';
import axios from 'axios';
import { settingsEmitter } from '../hooks/useSettings';
import AsyncStorage from '@react-native-async-storage/async-storage';
// 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}`);
// 1. Check for "Original + Subtitles" specific ID (often ID 238)
if (responseText.includes(`data-translator_id="238"`)) {
logger.log(`[HDRezka] Found specific translator ID 238 (Original + subtitles)`);
return '238';
}
// 2. Try to extract from the main CDN init function (e.g., initCDNMoviesEvents, initCDNSeriesEvents)
const functionName = mediaType === 'movie' ? 'initCDNMoviesEvents' : 'initCDNSeriesEvents';
const cdnEventsRegex = new RegExp(`sof\.tv\.${functionName}\(${id}, ([^,]+)`, 'i');
const cdnEventsMatch = responseText.match(cdnEventsRegex);
if (cdnEventsMatch && cdnEventsMatch[1]) {
const translatorIdFromCdn = cdnEventsMatch[1].trim().replace(/['"]/g, ''); // Remove potential quotes
if (translatorIdFromCdn && translatorIdFromCdn !== 'false' && translatorIdFromCdn !== 'null') {
logger.log(`[HDRezka] Extracted translator ID from CDN init: ${translatorIdFromCdn}`);
return translatorIdFromCdn;
}
}
logger.log(`[HDRezka] CDN init function did not yield a valid translator ID.`);
// 3. Fallback: Try to find any other data-translator_id attribute in the HTML
// This regex looks for data-translator_id="<digits>"
const anyTranslatorRegex = /data-translator_id="(\d+)"/;
const anyTranslatorMatch = responseText.match(anyTranslatorRegex);
if (anyTranslatorMatch && anyTranslatorMatch[1]) {
const fallbackTranslatorId = anyTranslatorMatch[1].trim();
logger.log(`[HDRezka] Found fallback translator ID from data attribute: ${fallbackTranslatorId}`);
return fallbackTranslatorId;
}
logger.log(`[HDRezka] No fallback data-translator_id found.`);
// If all attempts fail
logger.log(`[HDRezka] Could not find any translator ID for id ${id} on page ${fullUrl}`);
return null;
} 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}`);
// Check if internal providers are enabled globally
const appSettingsJson = await AsyncStorage.getItem('app_settings');
if (appSettingsJson) {
const appSettings = JSON.parse(appSettingsJson);
if (appSettings.enableInternalProviders === false) {
logger.log('[HDRezka] Internal providers are disabled in settings, skipping HDRezka');
return [];
}
}
// Check if HDRezka specifically is enabled
const hdrezkaSettingsJson = await AsyncStorage.getItem('hdrezka_settings');
if (hdrezkaSettingsJson) {
const hdrezkaSettings = JSON.parse(hdrezkaSettingsJson);
if (hdrezkaSettings.enabled === false) {
logger.log('[HDRezka] HDRezka provider is disabled in settings, skipping HDRezka');
return [];
}
}
// 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();

View file

@ -175,10 +175,6 @@ class StremioService {
private addonOrder: string[] = [];
private readonly STORAGE_KEY = 'stremio-addons';
private readonly ADDON_ORDER_KEY = 'stremio-addon-order';
private readonly DEFAULT_ADDONS = [
'https://v3-cinemeta.strem.io/manifest.json',
'https://opensubtitles-v3.strem.io/manifest.json'
];
private readonly MAX_CONCURRENT_REQUESTS = 3;
private readonly DEFAULT_PAGE_SIZE = 50;
private initialized: boolean = false;
@ -227,19 +223,15 @@ class StremioService {
const missingIds = installedIds.filter(id => !this.addonOrder.includes(id));
this.addonOrder = [...this.addonOrder, ...missingIds];
// If no addons, install defaults
if (this.installedAddons.size === 0) {
await this.installDefaultAddons();
}
// Ensure order is saved
await this.saveAddonOrder();
this.initialized = true;
} catch (error) {
logger.error('Failed to initialize addons:', error);
// Install defaults as fallback
await this.installDefaultAddons();
// Initialize with empty state on error
this.installedAddons = new Map();
this.addonOrder = [];
this.initialized = true;
}
}
@ -275,20 +267,6 @@ class StremioService {
throw lastError;
}
private async installDefaultAddons(): Promise<void> {
try {
for (const url of this.DEFAULT_ADDONS) {
const manifest = await this.getManifest(url);
if (manifest) {
this.installedAddons.set(manifest.id, manifest);
}
}
await this.saveInstalledAddons();
} catch (error) {
logger.error('Failed to install default addons:', error);
}
}
private async saveInstalledAddons(): Promise<void> {
try {
const addonsArray = Array.from(this.installedAddons.values());

View file

@ -1,316 +0,0 @@
import { NativeModules, NativeEventEmitter, EmitterSubscription, Platform } from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { logger } from '../utils/logger';
// Mock implementation for Expo environment
const MockTorrentStreamModule = {
TORRENT_PROGRESS_EVENT: 'torrentProgress',
startStream: async (magnetUri: string): Promise<string> => {
logger.log('[MockTorrentService] Starting mock stream for:', magnetUri);
// Return a fake URL that would look like a file path
return `https://mock-torrent-stream.com/${magnetUri.substring(0, 10)}.mp4`;
},
stopStream: () => {
logger.log('[MockTorrentService] Stopping mock stream');
},
fileExists: async (path: string): Promise<boolean> => {
logger.log('[MockTorrentService] Checking if file exists:', path);
return false;
},
// Add these methods to satisfy NativeModule interface
addListener: () => {},
removeListeners: () => {}
};
// Create an EventEmitter that doesn't rely on native modules
class MockEventEmitter {
private listeners: Map<string, Function[]> = new Map();
addListener(eventName: string, callback: Function): { remove: () => void } {
if (!this.listeners.has(eventName)) {
this.listeners.set(eventName, []);
}
this.listeners.get(eventName)?.push(callback);
return {
remove: () => {
const eventListeners = this.listeners.get(eventName);
if (eventListeners) {
const index = eventListeners.indexOf(callback);
if (index !== -1) {
eventListeners.splice(index, 1);
}
}
}
};
}
emit(eventName: string, ...args: any[]) {
const eventListeners = this.listeners.get(eventName);
if (eventListeners) {
eventListeners.forEach(listener => listener(...args));
}
}
removeAllListeners(eventName: string) {
this.listeners.delete(eventName);
}
}
// Use the mock module and event emitter since we're in Expo
const TorrentStreamModule = Platform.OS === 'web' ? null : MockTorrentStreamModule;
const mockEmitter = new MockEventEmitter();
const CACHE_KEY = '@torrent_cache_mapping';
export interface TorrentProgress {
bufferProgress: number;
downloadSpeed: number;
progress: number;
seeds: number;
}
export interface TorrentStreamEvents {
onProgress?: (progress: TorrentProgress) => void;
}
class TorrentService {
private eventEmitter: NativeEventEmitter | MockEventEmitter;
private progressListener: EmitterSubscription | { remove: () => void } | null = null;
private static TORRENT_PROGRESS_EVENT = TorrentStreamModule?.TORRENT_PROGRESS_EVENT || 'torrentProgress';
private cachedTorrents: Map<string, string> = new Map(); // Map of magnet URI to cached file path
private initialized: boolean = false;
private mockProgressInterval: NodeJS.Timeout | null = null;
constructor() {
// Use mock event emitter since we're in Expo
this.eventEmitter = mockEmitter;
this.loadCache();
}
private async loadCache() {
try {
const cacheData = await AsyncStorage.getItem(CACHE_KEY);
if (cacheData) {
const cacheMap = JSON.parse(cacheData);
this.cachedTorrents = new Map(Object.entries(cacheMap));
logger.log('[TorrentService] Loaded cache mapping:', this.cachedTorrents);
}
this.initialized = true;
} catch (error) {
logger.error('[TorrentService] Error loading cache:', error);
this.initialized = true;
}
}
private async saveCache() {
try {
const cacheData = Object.fromEntries(this.cachedTorrents);
await AsyncStorage.setItem(CACHE_KEY, JSON.stringify(cacheData));
logger.log('[TorrentService] Saved cache mapping');
} catch (error) {
logger.error('[TorrentService] Error saving cache:', error);
}
}
public async startStream(magnetUri: string, events?: TorrentStreamEvents): Promise<string> {
// Wait for cache to be loaded
while (!this.initialized) {
await new Promise(resolve => setTimeout(resolve, 50));
}
try {
// First check if we have this torrent cached
const cachedPath = this.cachedTorrents.get(magnetUri);
if (cachedPath) {
logger.log('[TorrentService] Found cached torrent file:', cachedPath);
// In mock mode, we'll always use the cached path if available
if (!TorrentStreamModule) {
// Still set up progress listeners for cached content
this.setupProgressListener(events);
// Simulate progress for cached content too
if (events?.onProgress) {
this.startMockProgressUpdates(events.onProgress);
}
return cachedPath;
}
// For native implementations, verify the file still exists
try {
const exists = await TorrentStreamModule.fileExists(cachedPath);
if (exists) {
logger.log('[TorrentService] Using cached torrent file');
// Setup progress listener if callback provided
this.setupProgressListener(events);
// Start the stream in cached mode
await TorrentStreamModule.startStream(magnetUri);
return cachedPath;
} else {
logger.log('[TorrentService] Cached file not found, removing from cache');
this.cachedTorrents.delete(magnetUri);
await this.saveCache();
}
} catch (error) {
logger.error('[TorrentService] Error checking cached file:', error);
// Continue to download again if there's an error
}
}
// First stop any existing stream
await this.stopStreamAndWait();
// Setup progress listener if callback provided
this.setupProgressListener(events);
// If we're in mock mode (Expo), simulate progress
if (!TorrentStreamModule) {
logger.log('[TorrentService] Using mock implementation');
const mockUrl = `https://mock-torrent-stream.com/${magnetUri.substring(0, 10)}.mp4`;
// Save to cache
this.cachedTorrents.set(magnetUri, mockUrl);
await this.saveCache();
// Start mock progress updates if events callback provided
if (events?.onProgress) {
this.startMockProgressUpdates(events.onProgress);
}
// Return immediately with mock URL
return mockUrl;
}
// Start the actual stream if native module is available
logger.log('[TorrentService] Starting torrent stream');
const filePath = await TorrentStreamModule.startStream(magnetUri);
// Save to cache
if (filePath) {
logger.log('[TorrentService] Adding path to cache:', filePath);
this.cachedTorrents.set(magnetUri, filePath);
await this.saveCache();
}
return filePath;
} catch (error) {
logger.error('[TorrentService] Error starting torrent stream:', error);
this.cleanup(); // Clean up on error
throw error;
}
}
private setupProgressListener(events?: TorrentStreamEvents) {
if (events?.onProgress) {
logger.log('[TorrentService] Setting up progress listener');
this.progressListener = this.eventEmitter.addListener(
TorrentService.TORRENT_PROGRESS_EVENT,
(progress) => {
logger.log('[TorrentService] Progress event received:', progress);
if (events.onProgress) {
events.onProgress(progress);
}
}
);
} else {
logger.log('[TorrentService] No progress callback provided');
}
}
private startMockProgressUpdates(onProgress: (progress: TorrentProgress) => void) {
// Clear any existing interval
if (this.mockProgressInterval) {
clearInterval(this.mockProgressInterval);
}
// Start at 0% progress
let mockProgress = 0;
// Update every second
this.mockProgressInterval = setInterval(() => {
// Increase by 10% each time
mockProgress += 10;
// Create mock progress object
const progress: TorrentProgress = {
bufferProgress: mockProgress,
downloadSpeed: 1024 * 1024 * (1 + Math.random()), // Random speed around 1MB/s
progress: mockProgress,
seeds: Math.floor(5 + Math.random() * 20), // Random seed count between 5-25
};
// Emit the event instead of directly calling callback
if (this.eventEmitter instanceof MockEventEmitter) {
(this.eventEmitter as MockEventEmitter).emit(TorrentService.TORRENT_PROGRESS_EVENT, progress);
} else {
// Fallback to direct callback if needed
onProgress(progress);
}
// If we reach 100%, clear the interval
if (mockProgress >= 100) {
if (this.mockProgressInterval) {
clearInterval(this.mockProgressInterval);
this.mockProgressInterval = null;
}
}
}, 1000);
}
public async stopStreamAndWait(): Promise<void> {
logger.log('[TorrentService] Stopping stream and waiting for cleanup');
this.cleanup();
if (TorrentStreamModule) {
try {
TorrentStreamModule.stopStream();
// Wait a moment to ensure native side has cleaned up
await new Promise(resolve => setTimeout(resolve, 500));
} catch (error) {
logger.error('[TorrentService] Error stopping torrent stream:', error);
}
}
}
public stopStream(): void {
try {
logger.log('[TorrentService] Stopping stream and cleaning up');
this.cleanup();
if (TorrentStreamModule) {
TorrentStreamModule.stopStream();
}
} catch (error) {
logger.error('[TorrentService] Error stopping torrent stream:', error);
// Still attempt cleanup even if stop fails
this.cleanup();
}
}
private cleanup(): void {
logger.log('[TorrentService] Cleaning up event listeners and intervals');
// Clean up progress listener
if (this.progressListener) {
try {
this.progressListener.remove();
} catch (error) {
logger.error('[TorrentService] Error removing progress listener:', error);
} finally {
this.progressListener = null;
}
}
// Clean up mock progress interval
if (this.mockProgressInterval) {
clearInterval(this.mockProgressInterval);
this.mockProgressInterval = null;
}
}
}
export const torrentService = new TorrentService();

View file

@ -1,298 +0,0 @@
import { logger } from '../utils/logger';
import { Stream } from '../types/metadata';
import { tmdbService } from './tmdbService';
import axios from 'axios';
import AsyncStorage from '@react-native-async-storage/async-storage';
import * as FileSystem from 'expo-file-system';
import * as Crypto from 'expo-crypto';
// 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 MAX_RETRIES_XPRIME = 3;
const RETRY_DELAY_MS_XPRIME = 1000;
// Use app's cache directory for React Native
const CACHE_DIR = `${FileSystem.cacheDirectory}xprime/`;
const BROWSER_HEADERS_XPRIME = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36',
'Accept': '*/*',
'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8',
'Cache-Control': 'no-cache',
'Pragma': 'no-cache',
'Sec-Ch-Ua': '"Chromium";v="136", "Google Chrome";v="136", "Not.A/Brand";v="99"',
'Sec-Ch-Ua-Mobile': '?0',
'Sec-Ch-Ua-Platform': '"Windows"',
'Connection': 'keep-alive'
};
interface XprimeStream {
url: string;
quality: string;
title: string;
provider: string;
codecs: string[];
size: string;
}
class XprimeService {
private MAX_RETRIES = 3;
private RETRY_DELAY = 1000; // 1 second
// Ensure cache directories exist
private async ensureCacheDir(dirPath: string) {
try {
const dirInfo = await FileSystem.getInfoAsync(dirPath);
if (!dirInfo.exists) {
await FileSystem.makeDirectoryAsync(dirPath, { intermediates: true });
}
} catch (error) {
logger.error(`[XPRIME] Warning: Could not create cache directory ${dirPath}:`, error);
}
}
// Cache helpers
private async getFromCache(cacheKey: string, subDir: string = ''): Promise<any> {
try {
const fullPath = `${CACHE_DIR}${subDir}/${cacheKey}`;
const fileInfo = await FileSystem.getInfoAsync(fullPath);
if (fileInfo.exists) {
const data = await FileSystem.readAsStringAsync(fullPath);
logger.log(`[XPRIME] CACHE HIT for: ${subDir}/${cacheKey}`);
try {
return JSON.parse(data);
} catch (e) {
return data;
}
}
return null;
} catch (error) {
logger.error(`[XPRIME] CACHE READ ERROR for ${cacheKey}:`, error);
return null;
}
}
private async saveToCache(cacheKey: string, content: any, subDir: string = '') {
try {
const fullSubDir = `${CACHE_DIR}${subDir}/`;
await this.ensureCacheDir(fullSubDir);
const fullPath = `${fullSubDir}${cacheKey}`;
const dataToSave = typeof content === 'string' ? content : JSON.stringify(content, null, 2);
await FileSystem.writeAsStringAsync(fullPath, dataToSave);
logger.log(`[XPRIME] SAVED TO CACHE: ${subDir}/${cacheKey}`);
} catch (error) {
logger.error(`[XPRIME] CACHE WRITE ERROR for ${cacheKey}:`, error);
}
}
// Helper function to fetch stream size using a HEAD request
private async fetchStreamSize(url: string): Promise<string> {
const cacheSubDir = 'xprime_stream_sizes';
// Create a hash of the URL to use as the cache key
const urlHash = await Crypto.digestStringAsync(
Crypto.CryptoDigestAlgorithm.MD5,
url,
{ encoding: Crypto.CryptoEncoding.HEX }
);
const urlCacheKey = `${urlHash}.txt`;
const cachedSize = await this.getFromCache(urlCacheKey, cacheSubDir);
if (cachedSize !== null) {
return cachedSize;
}
try {
// For m3u8, Content-Length is for the playlist file, not the stream segments
if (url.toLowerCase().includes('.m3u8')) {
await this.saveToCache(urlCacheKey, 'Playlist (size N/A)', cacheSubDir);
return 'Playlist (size N/A)';
}
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5-second timeout
try {
const response = await fetchImpl(url, {
method: 'HEAD',
signal: controller.signal
});
clearTimeout(timeoutId);
const contentLength = response.headers.get('content-length');
if (contentLength) {
const sizeInBytes = parseInt(contentLength, 10);
if (!isNaN(sizeInBytes)) {
let formattedSize;
if (sizeInBytes < 1024) formattedSize = `${sizeInBytes} B`;
else if (sizeInBytes < 1024 * 1024) formattedSize = `${(sizeInBytes / 1024).toFixed(2)} KB`;
else if (sizeInBytes < 1024 * 1024 * 1024) formattedSize = `${(sizeInBytes / (1024 * 1024)).toFixed(2)} MB`;
else formattedSize = `${(sizeInBytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
await this.saveToCache(urlCacheKey, formattedSize, cacheSubDir);
return formattedSize;
}
}
await this.saveToCache(urlCacheKey, 'Unknown size', cacheSubDir);
return 'Unknown size';
} finally {
clearTimeout(timeoutId);
}
} catch (error) {
logger.error(`[XPRIME] Could not fetch size for ${url.substring(0, 50)}...`, error);
await this.saveToCache(urlCacheKey, 'Unknown size', cacheSubDir);
return 'Unknown size';
}
}
private async fetchWithRetry(url: string, options: any, maxRetries: number = MAX_RETRIES_XPRIME) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetchImpl(url, options);
if (!response.ok) {
let errorBody = '';
try {
errorBody = await response.text();
} catch (e) {
// ignore
}
const httpError = new Error(`HTTP error! Status: ${response.status} ${response.statusText}. Body: ${errorBody.substring(0, 200)}`);
(httpError as any).status = response.status;
throw httpError;
}
return response;
} catch (error: any) {
lastError = error;
logger.error(`[XPRIME] Fetch attempt ${attempt}/${maxRetries} failed for ${url}:`, error);
// If it's a 403 error, stop retrying immediately
if (error.status === 403) {
logger.log(`[XPRIME] Encountered 403 Forbidden for ${url}. Halting retries.`);
throw lastError;
}
if (attempt < maxRetries) {
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS_XPRIME * Math.pow(2, attempt - 1)));
}
}
}
logger.error(`[XPRIME] All fetch attempts failed for ${url}. Last error:`, lastError);
if (lastError) throw lastError;
else throw new Error(`[XPRIME] All fetch attempts failed for ${url} without a specific error captured.`);
}
async getStreams(mediaId: string, mediaType: string, season?: number, episode?: number): Promise<Stream[]> {
// XPRIME service has been removed from internal providers
logger.log('[XPRIME] Service has been removed from internal providers');
return [];
}
private async getXprimeStreams(title: string, year: number, type: string, seasonNum?: number, episodeNum?: number): Promise<XprimeStream[]> {
let rawXprimeStreams: XprimeStream[] = [];
try {
logger.log(`[XPRIME] Fetch attempt for '${title}' (${year}). Type: ${type}, S: ${seasonNum}, E: ${episodeNum}`);
const xprimeName = encodeURIComponent(title);
let xprimeApiUrl: string;
// type here is tmdbTypeFromId which is 'movie' or 'tv'/'series'
if (type === 'movie') {
xprimeApiUrl = `https://backend.xprime.tv/primebox?name=${xprimeName}&year=${year}&fallback_year=${year}`;
} else if (type === 'tv' || type === 'series') { // Accept both 'tv' and 'series' for compatibility
if (seasonNum !== null && seasonNum !== undefined && episodeNum !== null && episodeNum !== undefined) {
xprimeApiUrl = `https://backend.xprime.tv/primebox?name=${xprimeName}&year=${year}&fallback_year=${year}&season=${seasonNum}&episode=${episodeNum}`;
} else {
logger.log('[XPRIME] Skipping series request: missing season/episode numbers.');
return [];
}
} else {
logger.log(`[XPRIME] Skipping request: unknown type '${type}'.`);
return [];
}
let xprimeResult: any;
// Direct fetch only
logger.log(`[XPRIME] Fetching directly: ${xprimeApiUrl}`);
const xprimeResponse = await this.fetchWithRetry(xprimeApiUrl, {
headers: {
...BROWSER_HEADERS_XPRIME,
'Origin': 'https://pstream.org',
'Referer': 'https://pstream.org/',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'cross-site',
'Sec-Fetch-Dest': 'empty'
}
});
xprimeResult = await xprimeResponse.json();
// Process the result
this.processXprimeResult(xprimeResult, rawXprimeStreams, title, type, seasonNum, episodeNum);
// Fetch stream sizes concurrently for all Xprime streams
if (rawXprimeStreams.length > 0) {
logger.log('[XPRIME] Fetching stream sizes...');
const sizePromises = rawXprimeStreams.map(async (stream) => {
stream.size = await this.fetchStreamSize(stream.url);
return stream;
});
await Promise.all(sizePromises);
logger.log(`[XPRIME] Found ${rawXprimeStreams.length} streams with sizes.`);
}
return rawXprimeStreams;
} catch (xprimeError) {
logger.error('[XPRIME] Error fetching or processing streams:', xprimeError);
return [];
}
}
// Helper function to process Xprime API response
private processXprimeResult(xprimeResult: any, rawXprimeStreams: XprimeStream[], title: string, type: string, seasonNum?: number, episodeNum?: number) {
const processXprimeItem = (item: any) => {
if (item && typeof item === 'object' && !item.error && item.streams && typeof item.streams === 'object') {
Object.entries(item.streams).forEach(([quality, fileUrl]) => {
if (fileUrl && typeof fileUrl === 'string') {
rawXprimeStreams.push({
url: fileUrl,
quality: quality || 'Unknown',
title: `${title} - ${(type === 'tv' || type === 'series') ? `S${String(seasonNum).padStart(2,'0')}E${String(episodeNum).padStart(2,'0')} ` : ''}${quality}`,
provider: 'XPRIME',
codecs: [],
size: 'Unknown size'
});
}
});
} else {
logger.log('[XPRIME] Skipping item due to missing/invalid streams or an error was reported by Xprime API:', item && item.error);
}
};
if (Array.isArray(xprimeResult)) {
xprimeResult.forEach(processXprimeItem);
} else if (xprimeResult) {
processXprimeItem(xprimeResult);
} else {
logger.log('[XPRIME] No result from Xprime API to process.');
}
}
}
export const xprimeService = new XprimeService();

View file

@ -1,61 +0,0 @@
// 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);
});