diff --git a/hdrezkas.js b/hdrezkas.js deleted file mode 100644 index 77749ae0..00000000 --- a/hdrezkas.js +++ /dev/null @@ -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 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(); \ No newline at end of file diff --git a/scripts/test-hdrezka.js b/scripts/test-hdrezka.js deleted file mode 100644 index 1d190aa5..00000000 --- a/scripts/test-hdrezka.js +++ /dev/null @@ -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(); \ No newline at end of file diff --git a/src/components/home/FeaturedContent.tsx b/src/components/home/FeaturedContent.tsx index 64ebe7bc..46ee9390 100644 --- a/src/components/home/FeaturedContent.tsx +++ b/src/components/home/FeaturedContent.tsx @@ -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 ( diff --git a/src/components/player/modals/SourcesModal.tsx b/src/components/player/modals/SourcesModal.tsx index cd11cb4a..9533f6f3 100644 --- a/src/components/player/modals/SourcesModal.tsx +++ b/src/components/player/modals/SourcesModal.tsx @@ -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) { diff --git a/src/hooks/useFeaturedContent.ts b/src/hooks/useFeaturedContent.ts index 249fefef..5989d6a8 100644 --- a/src/hooks/useFeaturedContent.ts +++ b/src/hooks/useFeaturedContent.ts @@ -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; diff --git a/src/hooks/useMetadata.ts b/src/hooks/useMetadata.ts index 0c796c45..722ff176 100644 --- a/src/hooks/useMetadata.ts +++ b/src/hooks/useMetadata.ts @@ -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 diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts index 5f473285..87c66d44 100644 --- a/src/hooks/useSettings.ts +++ b/src/hooks/useSettings.ts @@ -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 }; diff --git a/src/modules/TorrentPlayer.ts b/src/modules/TorrentPlayer.ts deleted file mode 100644 index 0519ecba..00000000 --- a/src/modules/TorrentPlayer.ts +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx index 86d41b1f..a499fcb4 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -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> diff --git a/src/screens/InternalProvidersSettings.tsx b/src/screens/InternalProvidersSettings.tsx deleted file mode 100644 index 996d3d16..00000000 --- a/src/screens/InternalProvidersSettings.tsx +++ /dev/null @@ -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; \ No newline at end of file diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index 5ef3d627..059e1f41 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -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 diff --git a/src/screens/StreamsScreen.tsx b/src/screens/StreamsScreen.tsx index 6e22375f..aff29b11 100644 --- a/src/screens/StreamsScreen.tsx +++ b/src/screens/StreamsScreen.tsx @@ -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, diff --git a/src/screens/TraktSettingsScreen.tsx b/src/screens/TraktSettingsScreen.tsx index ba6bb44c..73a6fa16 100644 --- a/src/screens/TraktSettingsScreen.tsx +++ b/src/screens/TraktSettingsScreen.tsx @@ -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> ) : ( diff --git a/src/services/hdrezkaService.ts b/src/services/hdrezkaService.ts deleted file mode 100644 index 09459245..00000000 --- a/src/services/hdrezkaService.ts +++ /dev/null @@ -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(); \ No newline at end of file diff --git a/src/services/stremioService.ts b/src/services/stremioService.ts index bec80782..3afa6351 100644 --- a/src/services/stremioService.ts +++ b/src/services/stremioService.ts @@ -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()); diff --git a/src/services/torrentService.ts b/src/services/torrentService.ts deleted file mode 100644 index 7660b00d..00000000 --- a/src/services/torrentService.ts +++ /dev/null @@ -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(); \ No newline at end of file diff --git a/src/services/xprimeService.ts b/src/services/xprimeService.ts deleted file mode 100644 index 704982f3..00000000 --- a/src/services/xprimeService.ts +++ /dev/null @@ -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(); \ No newline at end of file diff --git a/src/testHDRezka.js b/src/testHDRezka.js deleted file mode 100644 index 19154c73..00000000 --- a/src/testHDRezka.js +++ /dev/null @@ -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); -}); \ No newline at end of file