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