Files
TMDB-Embed-API/providers/4khdhub.js
T

1801 lines
77 KiB
JavaScript

const https = require('https');
const http = require('http');
const { URL } = require('url');
const cheerio = require('cheerio');
const fs = require('fs').promises;
const path = require('path');
// Configuration
const DOMAINS_URL = 'https://raw.githubusercontent.com/phisher98/TVVVV/refs/heads/main/domains.json';
let cachedDomains = null;
// --- Caching Configuration ---
const CACHE_ENABLED = process.env.DISABLE_CACHE !== 'true';
console.log(`[4KHDHub] Internal cache is ${CACHE_ENABLED ? 'enabled' : 'disabled'}.`);
const CACHE_DIR = process.env.VERCEL ? path.join('/tmp', '.4khdhub_cache') : path.join(__dirname, '.cache', '4khdhub');
// Caching layer simplified: currently inert placeholders (extend for FS persistence if desired)
// --- Caching Helper Functions ---
const ensureCacheDir = async () => {
if (!CACHE_ENABLED) return;
try {
await fs.mkdir(CACHE_DIR, { recursive: true });
} catch (error) {
if (error.code !== 'EEXIST') {
console.error(`[4KHDHub Cache] Error creating cache directory: ${error.message}`);
}
}
};
const getFromCache = async (key) => {
if (!CACHE_ENABLED) return null;
return null; // no-op
};
const saveToCache = async (key, data) => {
if (!CACHE_ENABLED) return;
// no-op (placeholder for future FS caching)
};
// Initialize cache directory on startup
ensureCacheDir();
// Utility functions
function base64Decode(str) {
return Buffer.from(str, 'base64').toString('utf-8');
}
function rot13(str) {
return str.replace(/[A-Za-z]/g, function (char) {
const start = char <= 'Z' ? 65 : 97;
return String.fromCharCode(((char.charCodeAt(0) - start + 13) % 26) + start);
});
}
function validateUrl(url) {
return new Promise((resolve) => {
try {
const urlObj = new URL(url);
// Skip validation for known reliable hosting services
const trustedHosts = [
'pixeldrain.dev',
'r2.dev'
];
const isTrustedHost = trustedHosts.some(host => urlObj.hostname.includes(host));
if (isTrustedHost) {
console.log(`[4KHDHub] Skipping validation for trusted host: ${urlObj.hostname}`);
resolve(true);
return;
}
const protocol = urlObj.protocol === 'https:' ? https : http;
const options = {
method: 'HEAD',
timeout: 15000,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
};
const req = protocol.request(url, options, (res) => {
// Consider 2xx and 3xx status codes as valid, including 206 (Partial Content)
const isValid = res.statusCode >= 200 && res.statusCode < 400;
console.log(`[4KHDHub] URL validation for ${url}: ${res.statusCode} - ${isValid ? 'VALID' : 'INVALID'}`);
res.destroy(); // Close connection immediately
resolve(isValid);
});
req.on('error', (err) => {
console.log(`[4KHDHub] URL validation error for ${url}: ${err.message}`);
resolve(false);
});
req.on('timeout', () => {
console.log(`[4KHDHub] URL validation timeout for ${url}`);
req.destroy();
resolve(false);
});
req.setTimeout(15000);
req.end();
} catch (error) {
console.log(`[4KHDHub] URL validation parse error for ${url}: ${error.message}`);
resolve(false);
}
});
}
function makeRequest(url, options = {}) {
return new Promise((resolve, reject) => {
const urlObj = new URL(url);
const isHttps = urlObj.protocol === 'https:';
const httpModule = isHttps ? https : http;
const requestOptions = {
hostname: urlObj.hostname,
port: urlObj.port || (isHttps ? 443 : 80),
path: urlObj.pathname + urlObj.search,
method: options.method || 'GET',
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
...options.headers
},
timeout: 30000
};
const req = httpModule.request(requestOptions, (res) => {
if (options.allowRedirects === false && (res.statusCode === 301 || res.statusCode === 302 || res.statusCode === 303 || res.statusCode === 307 || res.statusCode === 308)) {
resolve({ statusCode: res.statusCode, headers: res.headers });
return;
}
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
if (options.parseHTML && data) {
const $ = cheerio.load(data);
resolve({ $: $, body: data, statusCode: res.statusCode, headers: res.headers });
} else {
resolve({ body: data, statusCode: res.statusCode, headers: res.headers });
}
});
});
req.on('error', reject);
req.on('timeout', () => reject(new Error('Request timeout')));
req.end();
});
}
// Helper function to decode URL-encoded filenames and make them human-readable
function decodeFilename(filename) {
if (!filename) return filename;
try {
// Handle UTF-8 prefix and decode URL encoding
let decoded = filename;
// Remove UTF-8 prefix if present
if (decoded.startsWith('UTF-8')) {
decoded = decoded.substring(5);
}
// Decode URL encoding (%20 -> space, etc.)
decoded = decodeURIComponent(decoded);
return decoded;
} catch (error) {
console.log(`[4KHDHub] Error decoding filename: ${error.message}`);
return filename; // Return original if decoding fails
}
}
function getFilenameFromUrl(url) {
return new Promise((resolve) => {
try {
const urlObj = new URL(url);
const isHttps = urlObj.protocol === 'https:';
const httpModule = isHttps ? https : http;
const requestOptions = {
hostname: urlObj.hostname,
port: urlObj.port || (isHttps ? 443 : 80),
path: urlObj.pathname + urlObj.search,
method: 'HEAD',
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
},
timeout: 10000
};
const req = httpModule.request(requestOptions, (res) => {
const contentDisposition = res.headers['content-disposition'];
let filename = null;
if (contentDisposition) {
const filenameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/i);
if (filenameMatch && filenameMatch[1]) {
filename = filenameMatch[1].replace(/["']/g, '');
}
}
if (!filename) {
// Extract from URL path
const pathParts = urlObj.pathname.split('/');
filename = pathParts[pathParts.length - 1];
if (filename && filename.includes('.')) {
filename = filename.replace(/\.[^.]+$/, ''); // Remove extension
}
}
// Decode the filename to make it human-readable
const decodedFilename = decodeFilename(filename);
resolve(decodedFilename || null);
});
req.on('error', () => resolve(null));
req.on('timeout', () => resolve(null));
req.end();
} catch (error) {
resolve(null);
}
});
}
function getDomains() {
if (cachedDomains) {
return Promise.resolve(cachedDomains);
}
return makeRequest(DOMAINS_URL)
.then(response => {
cachedDomains = JSON.parse(response.body);
return cachedDomains;
})
.catch(error => {
console.error('[4KHDHub] Failed to fetch domains:', error.message);
return null;
});
}
function getRedirectLinks(url) {
return makeRequest(url)
.then(response => {
const doc = response.body;
const regex = /s\('o','([A-Za-z0-9+/=]+)'|ck\('_wp_http_\d+','([^']+)'/g;
let combinedString = '';
let match;
while ((match = regex.exec(doc)) !== null) {
const extractedValue = match[1] || match[2];
if (extractedValue) {
combinedString += extractedValue;
}
}
try {
const decodedString = base64Decode(rot13(base64Decode(base64Decode(combinedString))));
const jsonObject = JSON.parse(decodedString);
const encodedurl = base64Decode(jsonObject.o || '').trim();
const data = base64Decode(jsonObject.data || '').trim();
const wphttp1 = (jsonObject.blog_url || '').trim();
if (encodedurl) {
return Promise.resolve(encodedurl);
}
if (wphttp1 && data) {
return makeRequest(`${wphttp1}?re=${data}`, { parseHTML: true })
.then(resp => resp.document.body.textContent.trim())
.catch(() => '');
}
return Promise.resolve('');
} catch (e) {
console.error('[4KHDHub] Error processing links:', e.message);
return Promise.resolve('');
}
})
.catch(error => {
console.error('[4KHDHub] Error fetching redirect links:', error.message);
return Promise.resolve('');
});
}
function getIndexQuality(str) {
const match = (str || '').match(/(\d{3,4})[pP]/);
return match ? parseInt(match[1]) : 2160;
}
function getBaseUrl(url) {
try {
const urlObj = new URL(url);
return `${urlObj.protocol}//${urlObj.host}`;
} catch (e) {
return '';
}
}
function cleanTitle(title) {
// Decode URL-encoded title first
const decodedTitle = decodeFilename(title);
const parts = decodedTitle.split(/[.\-_]/);
const qualityTags = ['WEBRip', 'WEB-DL', 'WEB', 'BluRay', 'HDRip', 'DVDRip', 'HDTV', 'CAM', 'TS', 'R5', 'DVDScr', 'BRRip', 'BDRip', 'DVD', 'PDTV', 'HD'];
const audioTags = ['AAC', 'AC3', 'DTS', 'MP3', 'FLAC', 'DD5', 'EAC3', 'Atmos'];
const subTags = ['ESub', 'ESubs', 'Subs', 'MultiSub', 'NoSub', 'EnglishSub', 'HindiSub'];
const codecTags = ['x264', 'x265', 'H264', 'HEVC', 'AVC'];
const startIndex = parts.findIndex(part =>
qualityTags.some(tag => part.toLowerCase().includes(tag.toLowerCase()))
);
const endIndex = parts.map((part, index) => {
const hasTag = [...subTags, ...audioTags, ...codecTags].some(tag =>
part.toLowerCase().includes(tag.toLowerCase())
);
return hasTag ? index : -1;
}).filter(index => index !== -1).pop() || -1;
if (startIndex !== -1 && endIndex !== -1 && endIndex >= startIndex) {
return parts.slice(startIndex, endIndex + 1).join('.');
} else if (startIndex !== -1) {
return parts.slice(startIndex).join('.');
} else {
return parts.slice(-3).join('.');
}
}
// Enhanced title normalization with better handling of special cases
function normalizeTitle(title) {
return title
.toLowerCase()
// Handle common title variations
.replace(/&/g, 'and') // & -> and
.replace(/\b(the|a|an)\b/g, '') // Remove articles
// Handle common abbreviations and expansions
.replace(/\bvs\b/g, 'versus')
.replace(/\bversus\b/g, 'vs')
.replace(/\bdr\b/g, 'doctor')
.replace(/\bdoctor\b/g, 'dr')
.replace(/\bmr\b/g, 'mister')
.replace(/\bmister\b/g, 'mr')
.replace(/\bst\b/g, 'saint')
.replace(/\bsaint\b/g, 'st')
.replace(/\bmt\b/g, 'mount')
.replace(/\bmount\b/g, 'mt')
.replace(/[^a-z0-9\s]/g, ' ') // Remove special characters
.replace(/\s+/g, ' ') // Normalize whitespace
.trim();
}
// Extract year from title if present
function extractYear(title) {
const yearMatch = title.match(/\((19|20)\d{2}\)/);
return yearMatch ? parseInt(yearMatch[0].replace(/[()]/g, '')) : null;
}
// Remove year from title for cleaner comparison
function removeYear(title) {
return title.replace(/\s*\((19|20)\d{2}\)\s*/g, ' ').trim();
}
// Generate alternative search queries for better matching
function generateAlternativeQueries(title, originalTitle = null) {
const queries = new Set();
// Add the original title
queries.add(title);
// Add original title if different
if (originalTitle && originalTitle !== title) {
queries.add(originalTitle);
}
// Remove year and try again
const titleWithoutYear = removeYear(title);
if (titleWithoutYear !== title) {
queries.add(titleWithoutYear);
}
// Remove colons and other punctuation
queries.add(title.replace(/:/g, ''));
queries.add(title.replace(/[^a-zA-Z0-9\s]/g, ' ').replace(/\s+/g, ' ').trim());
// Handle common title variations
const variations = [
title.replace(/\bPart\s+(\d+)\b/gi, 'Part $1'),
title.replace(/\bPart\s+(\d+)\b/gi, '$1'),
title.replace(/\b(\d+)\b/g, match => {
const num = parseInt(match);
const romans = ['', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X'];
return romans[num] || match;
}),
title.replace(/\b(I{1,3}|IV|V|VI{0,3}|IX|X)\b/g, match => {
const romans = {
'I': '1', 'II': '2', 'III': '3', 'IV': '4', 'V': '5',
'VI': '6', 'VII': '7', 'VIII': '8', 'IX': '9', 'X': '10'
};
return romans[match] || match;
})
];
variations.forEach(v => {
if (v && v !== title) queries.add(v);
});
// Remove duplicates and filter out empty strings
return Array.from(queries).filter(q => q && q.trim().length > 0);
}
// Enhanced similarity calculation with multiple algorithms
function calculateSimilarity(str1, str2) {
const s1 = normalizeTitle(str1);
const s2 = normalizeTitle(str2);
if (s1 === s2) return 1.0;
// Levenshtein distance
const levenshtein = calculateLevenshteinSimilarity(s1, s2);
// Jaccard similarity (word-based)
const jaccard = calculateJaccardSimilarity(s1, s2);
// Longest common subsequence
const lcs = calculateLCSSimilarity(s1, s2);
// Weighted combination of different similarity measures
return (levenshtein * 0.4) + (jaccard * 0.4) + (lcs * 0.2);
}
// Levenshtein distance similarity
function calculateLevenshteinSimilarity(s1, s2) {
const len1 = s1.length;
const len2 = s2.length;
if (len1 === 0) return len2 === 0 ? 1.0 : 0.0;
if (len2 === 0) return 0.0;
const matrix = Array(len1 + 1).fill().map(() => Array(len2 + 1).fill(0));
for (let i = 0; i <= len1; i++) matrix[i][0] = i;
for (let j = 0; j <= len2; j++) matrix[0][j] = j;
for (let i = 1; i <= len1; i++) {
for (let j = 1; j <= len2; j++) {
const cost = s1[i - 1] === s2[j - 1] ? 0 : 1;
matrix[i][j] = Math.min(
matrix[i - 1][j] + 1, // deletion
matrix[i][j - 1] + 1, // insertion
matrix[i - 1][j - 1] + cost // substitution
);
}
}
const maxLen = Math.max(len1, len2);
return (maxLen - matrix[len1][len2]) / maxLen;
}
// Jaccard similarity for word-based comparison
function calculateJaccardSimilarity(s1, s2) {
const words1 = new Set(s1.split(' ').filter(w => w.length > 0));
const words2 = new Set(s2.split(' ').filter(w => w.length > 0));
const intersection = new Set([...words1].filter(w => words2.has(w)));
const union = new Set([...words1, ...words2]);
return union.size === 0 ? 0 : intersection.size / union.size;
}
// Longest Common Subsequence similarity
function calculateLCSSimilarity(s1, s2) {
const len1 = s1.length;
const len2 = s2.length;
if (len1 === 0 || len2 === 0) return 0;
const dp = Array(len1 + 1).fill().map(() => Array(len2 + 1).fill(0));
for (let i = 1; i <= len1; i++) {
for (let j = 1; j <= len2; j++) {
if (s1[i - 1] === s2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
const maxLen = Math.max(len1, len2);
return dp[len1][len2] / maxLen;
}
// Enhanced word containment check with fuzzy matching
function containsWords(title, query) {
const titleWords = normalizeTitle(title).split(' ').filter(w => w.length > 1);
const queryWords = normalizeTitle(query).split(' ').filter(w => w.length > 1);
let matchedWords = 0;
for (const queryWord of queryWords) {
const found = titleWords.some(titleWord => {
// Exact match
if (titleWord === queryWord) return true;
// Substring match
if (titleWord.includes(queryWord) || queryWord.includes(titleWord)) return true;
// Fuzzy match for longer words (allow 1 character difference)
if (queryWord.length > 3 && titleWord.length > 3) {
const similarity = calculateLevenshteinSimilarity(titleWord, queryWord);
return similarity > 0.8;
}
return false;
});
if (found) matchedWords++;
}
// Require at least 70% of query words to be matched
return matchedWords / queryWords.length >= 0.7;
}
// Enhanced best match finder with improved scoring
function findBestMatch(results, query, tmdbYear = null) {
if (results.length === 0) return null;
if (results.length === 1) return results[0];
console.log(`[4KHDHub] Finding best match for: "${query}" (Year: ${tmdbYear || 'N/A'})`);
// Strict year gating: if TMDB year is known, only consider exact matches first,
// then allow unknown-year entries as fallback. Reject clear mismatches.
let candidateResults = results;
if (tmdbYear) {
const tmdbYearInt = parseInt(tmdbYear);
const exactYearMatches = results.filter(r => {
const y = r.year || extractYear(r.title);
return y && parseInt(y) === tmdbYearInt;
});
if (exactYearMatches.length > 0) {
candidateResults = exactYearMatches;
console.log(`[4KHDHub] Year filter: using ${exactYearMatches.length} exact year matches for ${tmdbYearInt}`);
} else {
const unknownYear = results.filter(r => {
const y = r.year || extractYear(r.title);
return !y;
});
if (unknownYear.length > 0) {
candidateResults = unknownYear;
console.log(`[4KHDHub] Year filter: no exact matches; falling back to ${unknownYear.length} unknown-year results`);
} else {
console.log(`[4KHDHub] Year filter: no exact or unknown-year matches for ${tmdbYearInt}; rejecting results`);
return null;
}
}
}
// Score each result
const scoredResults = candidateResults.map(result => {
let score = 0;
// Use year from search result metadata if available, otherwise extract from title
const resultYear = result.year || extractYear(result.title);
const queryWithoutYear = removeYear(query);
const resultWithoutYear = removeYear(result.title);
// Exact match gets highest score (without year)
if (normalizeTitle(resultWithoutYear) === normalizeTitle(queryWithoutYear)) {
score += 100;
}
// Enhanced similarity score (0-60 points)
const similarity = calculateSimilarity(resultWithoutYear, queryWithoutYear);
score += similarity * 60;
// Word containment bonus (0-25 points)
if (containsWords(result.title, query)) {
score += 25;
}
// Year matching bonus (strict): only reward exact match when TMDB year is known
if (tmdbYear && resultYear) {
if (parseInt(tmdbYear) === parseInt(resultYear)) {
score += 30; // Strong reward for exact year
}
} else if (resultYear && !tmdbYear) {
score += 5; // Slight bonus for having year info when TMDB year unknown
}
// Length similarity bonus (0-10 points)
const lengthDiff = Math.abs(resultWithoutYear.length - queryWithoutYear.length);
score += Math.max(0, 10 - lengthDiff / 3);
// Prefer results with quality indicators
if (result.title.match(/\b(1080p|720p|4K|2160p|BluRay|WEB-DL)\b/i)) {
score += 3;
}
// Penalty for results with too many extra words
const queryWordCount = queryWithoutYear.split(' ').filter(w => w.length > 0).length;
const resultWordCount = resultWithoutYear.split(' ').filter(w => w.length > 0).length;
if (resultWordCount > queryWordCount * 2) {
score -= 10;
}
return { ...result, score, similarity, resultYear };
});
// Sort by score (highest first)
scoredResults.sort((a, b) => b.score - a.score);
console.log('[4KHDHub] Title matching scores:');
scoredResults.slice(0, 5).forEach((result, index) => {
console.log(`${index + 1}. ${result.title} (Score: ${result.score.toFixed(1)}, Similarity: ${(result.similarity * 100).toFixed(1)}%, Year: ${result.resultYear || 'N/A'})`);
});
// Additional validation: ensure the best match has a reasonable score
const bestResult = scoredResults[0];
// Final guard: if TMDB year is known and best has a conflicting year, reject
if (tmdbYear && bestResult.resultYear && parseInt(bestResult.resultYear) !== parseInt(tmdbYear)) {
console.log(`[4KHDHub] Best match year mismatch (best=${bestResult.resultYear}, tmdb=${tmdbYear}), rejecting`);
return null;
}
if (bestResult.score < 30) {
console.log(`[4KHDHub] Best match score too low (${bestResult.score.toFixed(1)}), rejecting`);
return null;
}
return bestResult;
}
function extractHubCloudLinks(url, referer) {
console.log(`[4KHDHub] Starting HubCloud extraction for: ${url}`);
const baseUrl = getBaseUrl(url);
return makeRequest(url, { parseHTML: true })
.then(response => {
const $ = response.$;
console.log(`[4KHDHub] Got HubCloud page, looking for download element...`);
// Check if this is already a hubcloud.php URL
let href;
if (url.includes('hubcloud.php')) {
href = url;
console.log(`[4KHDHub] Already a hubcloud.php URL: ${href}`);
} else {
const downloadElement = $('#download');
if (downloadElement.length === 0) {
console.log('[4KHDHub] Download element #download not found, trying alternatives...');
// Try alternative selectors
const alternatives = ['a[href*="hubcloud.php"]', '.download-btn', 'a[href*="download"]'];
let found = false;
for (const selector of alternatives) {
const altElement = $(selector).first();
if (altElement.length > 0) {
const rawHref = altElement.attr('href');
if (rawHref) {
href = rawHref.startsWith('http') ? rawHref : `${baseUrl.replace(/\/$/, '')}/${rawHref.replace(/^\//, '')}`;
console.log(`[4KHDHub] Found download link with selector ${selector}: ${href}`);
found = true;
break;
}
}
}
if (!found) {
throw new Error('Download element not found with any selector');
}
} else {
const rawHref = downloadElement.attr('href');
if (!rawHref) {
throw new Error('Download href not found');
}
href = rawHref.startsWith('http') ? rawHref : `${baseUrl.replace(/\/$/, '')}/${rawHref.replace(/^\//, '')}`;
console.log(`[4KHDHub] Found download href: ${href}`);
}
}
console.log(`[4KHDHub] Making request to HubCloud download page: ${href}`);
return makeRequest(href, { parseHTML: true });
})
.then(response => {
const $ = response.$;
console.log(`[4KHDHub] Processing HubCloud download page...`);
// Extract quality and size information
const size = $('i#size').text() || '';
const header = $('div.card-header').text() || '';
const quality = getIndexQuality(header);
const headerDetails = cleanTitle(header);
console.log(`[4KHDHub] Extracted info - Size: ${size}, Header: ${header}, Quality: ${quality}, HeaderDetails: ${headerDetails}`);
// Extract just the quality for clean naming
const qualityLabel = quality ? ` - ${quality}p` : '';
// We'll build the title format later after getting actual filename from HEAD request
// Find download buttons
const downloadButtons = $('div.card-body h2 a.btn');
console.log(`[4KHDHub] Found ${downloadButtons.length} download buttons`);
if (downloadButtons.length === 0) {
// Try alternative selectors for download buttons
const altSelectors = ['a.btn', '.btn', 'a[href]'];
for (const selector of altSelectors) {
const altButtons = $(selector);
if (altButtons.length > 0) {
console.log(`[4KHDHub] Found ${altButtons.length} buttons with alternative selector: ${selector}`);
altButtons.each((index, btn) => {
const $btn = $(btn);
const link = $btn.attr('href');
const text = $btn.text();
console.log(`[4KHDHub] Button ${index + 1}: ${text} -> ${link}`);
});
break;
}
}
}
const promises = downloadButtons.get().map((button, index) => {
return new Promise((resolve) => {
const $button = $(button);
const link = $button.attr('href');
const text = $button.text();
console.log(`[4KHDHub] Processing button ${index + 1}: "${text}" -> ${link}`);
if (!link) {
console.log(`[4KHDHub] Button ${index + 1} has no link`);
resolve(null);
return;
}
const buttonBaseUrl = getBaseUrl(link);
if (text.includes('FSL Server')) {
console.log(`[4KHDHub] Button ${index + 1} is FSL Server`);
// Get actual filename from HEAD request
getFilenameFromUrl(link)
.then(actualFilename => {
const displayFilename = actualFilename || headerDetails || 'Unknown';
const titleParts = [];
if (displayFilename) titleParts.push(displayFilename);
if (size) titleParts.push(size);
const finalTitle = titleParts.join('\n');
resolve({
name: `4KHDHub - FSL Server${qualityLabel}`,
title: finalTitle,
url: link,
quality: quality,
provider: '4khdhub',
headers: {}
});
})
.catch(() => {
const displayFilename = headerDetails || 'Unknown';
const titleParts = [];
if (displayFilename) titleParts.push(displayFilename);
if (size) titleParts.push(size);
const finalTitle = titleParts.join('\n');
resolve({
name: `4KHDHub - FSL Server${qualityLabel}`,
title: finalTitle,
url: link,
quality: quality,
provider: '4khdhub',
headers: {}
});
});
} else if (text.includes('Download File')) {
console.log(`[4KHDHub] Button ${index + 1} is Download File`);
// Get actual filename from HEAD request
getFilenameFromUrl(link)
.then(actualFilename => {
const displayFilename = actualFilename || headerDetails || 'Unknown';
const titleParts = [];
if (displayFilename) titleParts.push(displayFilename);
if (size) titleParts.push(size);
const finalTitle = titleParts.join('\n');
resolve({
name: `4KHDHub - HubCloud${qualityLabel}`,
title: finalTitle,
url: link,
quality: quality,
provider: '4khdhub',
headers: {}
});
})
.catch(() => {
const displayFilename = headerDetails || 'Unknown';
const titleParts = [];
if (displayFilename) titleParts.push(displayFilename);
if (size) titleParts.push(size);
const finalTitle = titleParts.join('\n');
resolve({
name: `4KHDHub - HubCloud${qualityLabel}`,
title: finalTitle,
url: link,
quality: quality,
provider: '4khdhub',
headers: {}
});
});
} else if (text.includes('BuzzServer')) {
console.log(`[4KHDHub] Button ${index + 1} is BuzzServer, following redirect...`);
// Handle BuzzServer redirect
makeRequest(`${link}/download`, {
parseHTML: false,
allowRedirects: false,
headers: { 'Referer': link }
})
.then(response => {
const redirectUrl = response.headers['hx-redirect'] || response.headers['location'];
if (redirectUrl) {
console.log(`[4KHDHub] BuzzServer redirect found: ${redirectUrl}`);
const finalUrl = buttonBaseUrl + redirectUrl;
// Get actual filename from HEAD request
getFilenameFromUrl(finalUrl)
.then(actualFilename => {
const displayFilename = actualFilename || headerDetails || 'Unknown';
const titleParts = [];
if (displayFilename) titleParts.push(displayFilename);
if (size) titleParts.push(size);
const finalTitle = titleParts.join('\n');
resolve({
name: `4KHDHub - BuzzServer${qualityLabel}`,
title: finalTitle,
url: finalUrl,
quality: quality,
provider: '4khdhub',
headers: {}
});
})
.catch(() => {
const displayFilename = headerDetails || 'Unknown';
const titleParts = [];
if (displayFilename) titleParts.push(displayFilename);
if (size) titleParts.push(size);
const finalTitle = titleParts.join('\n');
resolve({
name: `4KHDHub - BuzzServer${qualityLabel}`,
title: finalTitle,
url: finalUrl,
quality: quality,
provider: '4khdhub',
headers: {}
});
});
} else {
console.log(`[4KHDHub] BuzzServer redirect not found`);
resolve(null);
}
})
.catch(err => {
console.log(`[4KHDHub] BuzzServer redirect failed: ${err.message}`);
resolve(null);
});
} else if (link.includes('pixeldra')) {
console.log(`[4KHDHub] Button ${index + 1} is Pixeldrain`);
// Convert pixeldrain.net/u/ID format to pixeldrain.net/api/file/ID format
let convertedLink = link;
const pixeldrainMatch = link.match(/pixeldrain\.net\/u\/([a-zA-Z0-9]+)/);
if (pixeldrainMatch) {
const fileId = pixeldrainMatch[1];
convertedLink = `https://pixeldrain.net/api/file/${fileId}`;
console.log(`[4KHDHub] Converted Pixeldrain URL from ${link} to ${convertedLink}`);
}
// Get actual filename from HEAD request
getFilenameFromUrl(convertedLink)
.then(actualFilename => {
const displayFilename = actualFilename || headerDetails || 'Unknown';
const titleParts = [];
if (displayFilename) titleParts.push(displayFilename);
if (size) titleParts.push(size);
const finalTitle = titleParts.join('\n');
resolve({
name: `4KHDHub - Pixeldrain${qualityLabel}`,
title: finalTitle,
url: convertedLink,
quality: quality,
provider: '4khdhub',
headers: {}
});
})
.catch(() => {
const displayFilename = headerDetails || 'Unknown';
const titleParts = [];
if (displayFilename) titleParts.push(displayFilename);
if (size) titleParts.push(size);
const finalTitle = titleParts.join('\n');
resolve({
name: `4KHDHub - Pixeldrain${qualityLabel}`,
title: finalTitle,
url: convertedLink,
quality: quality,
provider: '4khdhub',
headers: {}
});
});
} else if (text.includes('S3 Server')) {
console.log(`[4KHDHub] Button ${index + 1} is S3 Server`);
// Get actual filename from HEAD request
getFilenameFromUrl(link)
.then(actualFilename => {
const displayFilename = actualFilename || headerDetails || 'Unknown';
const titleParts = [];
if (displayFilename) titleParts.push(displayFilename);
if (size) titleParts.push(size);
const finalTitle = titleParts.join('\n');
resolve({
name: `4KHDHub - S3 Server${qualityLabel}`,
title: finalTitle,
url: link,
quality: quality,
provider: '4khdhub',
headers: {}
});
})
.catch(() => {
const displayFilename = headerDetails || 'Unknown';
const titleParts = [];
if (displayFilename) titleParts.push(displayFilename);
if (size) titleParts.push(size);
const finalTitle = titleParts.join('\n');
resolve({
name: `4KHDHub - S3 Server${qualityLabel}`,
title: finalTitle,
url: link,
quality: quality,
provider: '4khdhub',
headers: {}
});
});
} else if (text.includes('10Gbps')) {
console.log(`[4KHDHub] Button ${index + 1} is 10Gbps server, following redirects...`);
// Handle 10Gbps server with multiple redirects
let currentLink = link;
const followRedirects = () => {
return makeRequest(currentLink, {
parseHTML: false,
allowRedirects: false
})
.then(response => {
const redirectUrl = response.headers['location'];
if (!redirectUrl) {
throw new Error('No redirect found');
}
console.log(`[4KHDHub] 10Gbps redirect: ${redirectUrl}`);
if (redirectUrl.includes('id=')) {
// Final redirect, extract the link parameter
const finalLink = redirectUrl.split('link=')[1];
if (finalLink) {
console.log(`[4KHDHub] 10Gbps final link: ${finalLink}`);
const decodedUrl = decodeURIComponent(finalLink);
// Get actual filename from HEAD request
return getFilenameFromUrl(decodedUrl)
.then(actualFilename => {
const displayFilename = actualFilename || headerDetails || 'Unknown';
const titleParts = [];
if (displayFilename) titleParts.push(displayFilename);
if (size) titleParts.push(size);
const finalTitle = titleParts.join('\n');
return {
name: `4KHDHub - 10Gbps Server${qualityLabel}`,
title: finalTitle,
url: decodedUrl,
quality: quality,
provider: '4khdhub',
headers: {}
};
})
.catch(() => {
const displayFilename = headerDetails || 'Unknown';
const titleParts = [];
if (displayFilename) titleParts.push(displayFilename);
if (size) titleParts.push(size);
const finalTitle = titleParts.join('\n');
return {
name: `4KHDHub - 10Gbps Server${qualityLabel}`,
title: finalTitle,
url: decodedUrl,
quality: quality,
provider: '4khdhub',
headers: {}
};
});
}
throw new Error('Final link not found');
} else {
currentLink = redirectUrl;
return followRedirects();
}
});
};
followRedirects()
.then(result => {
console.log(`[4KHDHub] 10Gbps processing completed`);
resolve(result);
})
.catch(err => {
console.log(`[4KHDHub] 10Gbps processing failed: ${err.message}`);
resolve(null);
});
} else {
console.log(`[4KHDHub] Button ${index + 1} is generic link`);
// Generic link - Get actual filename from HEAD request
getFilenameFromUrl(link)
.then(actualFilename => {
const displayFilename = actualFilename || headerDetails || 'Unknown';
const titleParts = [];
if (displayFilename) titleParts.push(displayFilename);
if (size) titleParts.push(size);
const finalTitle = titleParts.join('\n');
resolve({
name: `4KHDHub - HubCloud${qualityLabel}`,
title: finalTitle,
url: link,
quality: quality,
provider: '4khdhub',
headers: {}
});
})
.catch(() => {
const displayFilename = headerDetails || 'Unknown';
const titleParts = [];
if (displayFilename) titleParts.push(displayFilename);
if (size) titleParts.push(size);
const finalTitle = titleParts.join('\n');
resolve({
name: `4KHDHub - HubCloud${qualityLabel}`,
title: finalTitle,
url: link,
quality: quality,
provider: '4khdhub',
headers: {}
});
});
}
});
});
return Promise.all(promises)
.then(results => {
const validResults = results.filter(result => result !== null);
console.log(`[4KHDHub] HubCloud extraction completed, found ${validResults.length} valid links`);
return validResults;
});
})
.catch(error => {
console.error(`[4KHDHub] HubCloud extraction error for ${url}:`, error.message);
return [];
});
}
function searchContent(query) {
return getDomains()
.then(domains => {
if (!domains || !domains['4khdhub']) {
throw new Error('Failed to get domain information');
}
const baseUrl = domains['4khdhub'];
const searchUrl = `${baseUrl}/?s=${encodeURIComponent(query)}`;
return makeRequest(searchUrl, { parseHTML: true })
.then(response => ({ response, baseUrl }));
})
.then(({ response, baseUrl }) => {
const $ = response.$;
const results = [];
$('div.card-grid a').each((index, card) => {
const $card = $(card);
const title = $card.find('h3').text();
const href = $card.attr('href');
const posterUrl = $card.find('img').attr('src');
// Extract year from movie-card-meta element
const metaElement = $card.find('.movie-card-meta');
let year = null;
if (metaElement.length > 0) {
const metaText = metaElement.text().trim();
const yearMatch = metaText.match(/(19|20)\d{2}/);
if (yearMatch) {
year = parseInt(yearMatch[0]);
}
}
if (title && href) {
// Convert relative URLs to absolute URLs
const absoluteUrl = href.startsWith('http') ? href : `${baseUrl}${href.startsWith('/') ? '' : '/'}${href}`;
results.push({
title: title.trim(),
url: absoluteUrl,
poster: posterUrl || '',
year: year
});
}
});
return results;
});
}
function loadContent(url) {
return makeRequest(url, { parseHTML: true })
.then(response => {
const $ = response.$;
const title = $('h1.page-title').text().split('(')[0].trim() || '';
const poster = $('meta[property="og:image"]').attr('content') || '';
const tags = $('div.mt-2 span.badge').map((i, el) => $(el).text()).get();
const year = parseInt($('div.mt-2 span').text()) || null;
const description = $('div.content-section p.mt-4').text().trim() || '';
const trailer = $('#trailer-btn').attr('data-trailer-url') || '';
const isMovie = tags.includes('Movies');
// Try multiple selectors to find download links
let hrefs = [];
const selectors = [
'div.download-item a',
'.download-item a',
'a[href*="hubdrive"]',
'a[href*="hubcloud"]',
'a[href*="drive"]',
'.btn[href]',
'a.btn'
];
for (const selector of selectors) {
const links = $(selector)
.map((i, a) => $(a).attr('href'))
.get()
.filter(href => href && href.trim());
if (links.length > 0) {
hrefs = links;
console.log(`[4KHDHub] Found ${links.length} links using selector: ${selector}`);
break;
}
}
if (hrefs.length === 0) {
console.log('[4KHDHub] No download links found. Available links on page:');
const allLinks = $('a[href]')
.map((i, a) => $(a).attr('href'))
.get()
.filter(href => href && href.includes('http'))
.slice(0, 10); // Show first 10 links
console.log(allLinks);
}
const content = {
title,
poster,
tags,
year,
description,
trailer,
type: isMovie ? 'movie' : 'series'
};
if (isMovie) {
content.downloadLinks = hrefs;
return Promise.resolve(content);
} else {
// Handle TV series episodes
const episodesMap = new Map(); // used to dedupe episodes below
console.log(`[4KHDHub] Looking for episode structure...`);
const seasonItems = $('div.episodes-list div.season-item');
console.log(`[4KHDHub] Found ${seasonItems.length} season items`);
if (seasonItems.length === 0) {
// Try alternative episode structure selectors
const altSelectors = [
'div.season-item',
'.episode-item',
'.episode-download',
'div[class*="episode"]',
'div[class*="season"]'
];
for (const selector of altSelectors) {
const items = $(selector);
if (items.length > 0) {
console.log(`[4KHDHub] Found ${items.length} items with selector: ${selector}`);
break;
}
}
// If no episode structure found, treat all found links as general series links
if (hrefs.length > 0) {
console.log(`[4KHDHub] No episode structure found, using general links for series`);
// Create a single episode entry with all links
content.episodes = [{
season: 1,
episode: 1,
downloadLinks: hrefs
}];
} else {
content.episodes = [];
}
} else {
seasonItems.each((i, seasonElement) => {
const $seasonElement = $(seasonElement);
const seasonText = $seasonElement.find('div.episode-number').text() || '';
const seasonMatch = seasonText.match(/S?([1-9][0-9]*)/);
const season = seasonMatch ? parseInt(seasonMatch[1]) : null;
const episodeItems = $seasonElement.find('div.episode-download-item');
episodeItems.each((j, episodeItem) => {
const $episodeItem = $(episodeItem);
const episodeText = $episodeItem.find('div.episode-file-info span.badge-psa').text() || '';
const episodeMatch = episodeText.match(/Episode-0*([1-9][0-9]*)/);
const episode = episodeMatch ? parseInt(episodeMatch[1]) : null;
const episodeHrefs = $episodeItem.find('a')
.map((k, a) => $(a).attr('href'))
.get()
.filter(href => href && href.trim());
if (season && episode && episodeHrefs.length > 0) {
const key = `${season}-${episode}`;
if (!episodesMap.has(key)) {
episodesMap.set(key, {
season,
episode,
downloadLinks: []
});
}
episodesMap.get(key).downloadLinks.push(...episodeHrefs);
}
});
});
content.episodes = Array.from(episodesMap.values()).map(ep => ({
...ep,
downloadLinks: [...new Set(ep.downloadLinks)] // Remove duplicates
}));
}
console.log(`[4KHDHub] Found ${content.episodes.length} episodes with links`);
return Promise.resolve(content);
}
});
}
function extractStreamingLinks(downloadLinks) {
console.log(`[4KHDHub] Processing ${downloadLinks.length} download links...`);
const promises = downloadLinks.map((link, index) => {
return new Promise((resolve) => {
console.log(`[4KHDHub] Processing link ${index + 1}: ${link}`);
// Check if link needs redirect processing
if (link.toLowerCase().includes('id=')) {
console.log(`[4KHDHub] Link ${index + 1} needs redirect processing`);
getRedirectLinks(link)
.then(resolvedLink => {
if (resolvedLink) {
console.log(`[4KHDHub] Link ${index + 1} resolved to: ${resolvedLink}`);
processExtractorLink(resolvedLink, resolve, index + 1);
} else {
console.log(`[4KHDHub] Link ${index + 1} redirect resolution failed`);
resolve(null);
}
})
.catch(err => {
console.error(`[4KHDHub] Redirect failed for link ${index + 1} (${link}):`, err.message);
resolve(null);
});
} else {
processExtractorLink(link, resolve, index + 1);
}
});
});
return Promise.all(promises)
.then(results => {
const validResults = results.filter(result => result !== null);
const flatResults = validResults.flat();
// Filter out .zip files
const filteredResults = flatResults.filter(link => {
return link && link.url && !link.url.toLowerCase().endsWith('.zip');
});
// Note: Link count will be logged after validation completes
return filteredResults;
});
}
function extractHubDriveLinks(url, referer) {
console.log(`[4KHDHub] Starting HubDrive extraction for: ${url}`);
return makeRequest(url, { parseHTML: true })
.then(response => {
const $ = response.$;
console.log(`[4KHDHub] Got HubDrive page, looking for download button...`);
// Extract filename and size information
const size = $('i#size').text() || '';
const header = $('div.card-header').text() || '';
const quality = getIndexQuality(header);
const headerDetails = cleanTitle(header);
console.log(`[4KHDHub] HubDrive extracted info - Size: ${size}, Header: ${header}, Quality: ${quality}, HeaderDetails: ${headerDetails}`);
// Extract filename from header for title display
let filename = headerDetails || header || 'Unknown';
// Clean up the filename by removing common prefixes and file extensions
filename = filename.replace(/^4kHDHub\.com\s*[-_]?\s*/i, '')
.replace(/\.[a-z0-9]{2,4}$/i, '')
.replace(/[._]/g, ' ')
.trim();
// Use the exact selector from Kotlin code
const downloadBtn = $('.btn.btn-primary.btn-user.btn-success1.m-1').first();
if (downloadBtn.length === 0) {
console.log('[4KHDHub] Primary download button not found, trying alternative selectors...');
// Try alternative selectors
const alternatives = [
'a.btn.btn-primary',
'.btn-primary',
'a[href*="download"]',
'a.btn'
];
let foundBtn = null;
for (const selector of alternatives) {
foundBtn = $(selector).first();
if (foundBtn.length > 0) {
console.log(`[4KHDHub] Found download button with selector: ${selector}`);
break;
}
}
if (!foundBtn || foundBtn.length === 0) {
throw new Error('Download button not found with any selector');
}
const href = foundBtn.attr('href');
if (!href) {
throw new Error('Download link not found');
}
console.log(`[4KHDHub] Found HubDrive download link: ${href}`);
return processHubDriveLink(href, referer, filename, size, quality);
}
const href = downloadBtn.attr('href');
if (!href) {
throw new Error('Download link not found');
}
console.log(`[4KHDHub] Found HubDrive download link: ${href}`);
return processHubDriveLink(href, referer, filename, size, quality);
})
.catch(error => {
console.error(`[4KHDHub] Error extracting HubDrive links for ${url}:`, error.message);
return [];
});
}
function processHubDriveLink(href, referer, filename = 'Unknown', size = '', quality = 1080) {
// Check if it's a HubCloud link
if (href.toLowerCase().includes('hubcloud')) {
console.log('[4KHDHub] HubDrive link redirects to HubCloud, processing...');
return extractHubCloudLinks(href, '4KHDHub');
} else {
console.log('[4KHDHub] HubDrive direct link found');
// Direct link or other extractor
const qualityLabel = quality ? ` - ${quality}p` : '';
// Build labelExtras like the original extractor
const labelExtras = [];
if (filename && filename !== 'Unknown') labelExtras.push(`[${filename}]`);
if (size) labelExtras.push(`[${size}]`);
// labelExtra removed (not used)
// Get actual filename from HEAD request
return getFilenameFromUrl(href)
.then(actualFilename => {
const displayFilename = actualFilename || filename || 'Unknown';
const titleParts = [];
if (displayFilename) titleParts.push(displayFilename);
if (size) titleParts.push(size);
const finalTitle = titleParts.join('\n');
return [{
name: `4KHDHub - HubDrive${qualityLabel}`,
title: finalTitle,
url: href,
quality: quality,
provider: '4khdhub',
headers: {}
}];
})
.catch(() => {
const displayFilename = filename || 'Unknown';
const titleParts = [];
if (displayFilename) titleParts.push(displayFilename);
if (size) titleParts.push(size);
const finalTitle = titleParts.join('\n');
return [{
name: `4KHDHub - HubDrive${qualityLabel}`,
title: finalTitle,
url: href,
quality: quality,
provider: '4khdhub',
headers: {}
}];
});
}
}
function processExtractorLink(link, resolve, linkNumber) {
const linkLower = link.toLowerCase();
console.log(`[4KHDHub] Checking extractors for link ${linkNumber}: ${link}`);
if (linkLower.includes('hubdrive')) {
console.log(`[4KHDHub] Link ${linkNumber} matched HubDrive extractor`);
extractHubDriveLinks(link, '4KHDHub')
.then(links => {
console.log(`[4KHDHub] HubDrive extraction completed for link ${linkNumber}:`, links);
resolve(links);
})
.catch(err => {
console.error(`[4KHDHub] HubDrive extraction failed for link ${linkNumber} (${link}):`, err.message);
resolve(null);
});
} else if (linkLower.includes('hubcloud')) {
console.log(`[4KHDHub] Link ${linkNumber} matched HubCloud extractor`);
extractHubCloudLinks(link, '4KHDHub')
.then(links => {
console.log(`[4KHDHub] HubCloud extraction completed for link ${linkNumber}:`, links);
resolve(links);
})
.catch(err => {
console.error(`[4KHDHub] HubCloud extraction failed for link ${linkNumber} (${link}):`, err.message);
resolve(null);
});
} else {
console.log(`[4KHDHub] No extractor matched for link ${linkNumber}: ${link}`);
// Try to extract any direct streaming URLs from the link
if (link.includes('http') && (link.includes('.mp4') || link.includes('.mkv') || link.includes('.avi'))) {
console.log(`[4KHDHub] Link ${linkNumber} appears to be a direct video link`);
// Extract filename from URL
const urlParts = link.split('/');
const filename = urlParts[urlParts.length - 1].replace(/\.[^/.]+$/, '').replace(/[._]/g, ' ');
// Build labelExtras like the original extractor
const labelExtras = [];
if (filename) labelExtras.push(`[${filename}]`);
labelExtras.push('[Direct Link]');
// labelExtra removed (not used)
// Get actual filename from HEAD request
getFilenameFromUrl(link)
.then(actualFilename => {
const displayFilename = actualFilename || filename || 'Unknown';
const titleParts = [];
if (displayFilename) titleParts.push(displayFilename);
titleParts.push('[Direct Link]');
const finalTitle = titleParts.join('\n');
resolve([{
name: '4KHDHub Direct Link',
title: finalTitle,
url: link,
quality: 1080,
provider: "4khdhub",
headers: {}
}]);
})
.catch(() => {
const displayFilename = filename || 'Unknown';
const titleParts = [];
if (displayFilename) titleParts.push(displayFilename);
titleParts.push('[Direct Link]');
const finalTitle = titleParts.join('\n');
resolve([{
name: '4KHDHub Direct Link',
title: finalTitle,
url: link,
quality: 1080,
provider: "4khdhub",
headers: {}
}]);
});
} else {
resolve(null);
}
}
}
// Helper function to get TMDB details
async function getTMDBDetails(tmdbId, mediaType) {
const { getTmdbApiKey } = require('../utils/tmdbKey');
const TMDB_API_KEY = getTmdbApiKey();
if (!TMDB_API_KEY) {
console.warn('[4KHDHub] TMDB API key missing; skipping TMDB metadata fetch');
return null;
}
try {
console.log(`[4KHDHub] Fetching ${mediaType} details for TMDB ID: ${tmdbId}`);
const response = await makeRequest(`https://api.themoviedb.org/3/${mediaType}/${tmdbId}?api_key=${TMDB_API_KEY}`);
const data = JSON.parse(response.body);
if (mediaType === 'movie') {
return {
title: data.title,
original_title: data.original_title,
year: data.release_date ? data.release_date.split('-')[0] : null
};
} else {
return {
title: data.name,
original_title: data.original_name,
year: data.first_air_date ? data.first_air_date.split('-')[0] : null
};
}
} catch (error) {
console.error(`[4KHDHub] Error fetching details from TMDB:`, error.message);
return null;
}
}
// Main function to get streams for the addon
async function get4KHDHubStreams(tmdbId, type, season = null, episode = null) {
try {
console.log(`[4KHDHub] Starting search for TMDB ID: ${tmdbId}, Type: ${type}${season ? `, Season: ${season}` : ''}${episode ? `, Episode: ${episode}` : ''}`);
// Create cache key for resolved file hosting URLs
const cacheKey = `4khdhub_resolved_urls_v3_${tmdbId}_${type}${season ? `_s${season}e${episode}` : ''}`;
let streamingLinks = [];
// 1. Check cache for resolved file hosting URLs first
const cachedResolvedUrls = await getFromCache(cacheKey);
if (cachedResolvedUrls && cachedResolvedUrls.length > 0) {
console.log(`[4KHDHub] Cache HIT for ${cacheKey}. Using ${cachedResolvedUrls.length} cached resolved URLs.`);
// Process cached resolved URLs directly to final streaming links
console.log(`[4KHDHub] Processing ${cachedResolvedUrls.length} cached resolved URLs to get streaming links.`);
streamingLinks = await extractStreamingLinks(cachedResolvedUrls);
} else {
if (cachedResolvedUrls && cachedResolvedUrls.length === 0) {
console.log(`[4KHDHub] Cache contains empty data for ${cacheKey}. Refetching from source.`);
} else {
console.log(`[4KHDHub] Cache MISS for ${cacheKey}. Fetching from source.`);
}
// Map type to TMDB API format
const tmdbType = type === 'series' ? 'tv' : type;
// Get TMDB details to get the actual title
const tmdbDetails = await getTMDBDetails(tmdbId, tmdbType);
if (!tmdbDetails || !tmdbDetails.title) {
console.log(`[4KHDHub] Could not fetch TMDB details for ID: ${tmdbId}`);
return [];
}
console.log(`[4KHDHub] TMDB Details: ${tmdbDetails.title} (${tmdbDetails.year || 'N/A'})`);
// Enhanced search with fallback strategies
let searchResults = [];
let bestMatch = null;
// Primary search using the actual title
const searchQuery = tmdbDetails.title;
searchResults = await searchContent(searchQuery);
console.log(`[4KHDHub] Primary search found ${searchResults.length} results`);
if (searchResults.length > 0) {
bestMatch = findBestMatch(searchResults, tmdbDetails.title, tmdbDetails.year);
}
// Fallback search strategies if no good match found
if (!bestMatch && searchResults.length > 0) {
console.log(`[4KHDHub] No good match from primary search, trying fallback strategies...`);
// Try search without year
const titleWithoutYear = removeYear(tmdbDetails.title);
if (titleWithoutYear !== tmdbDetails.title) {
console.log(`[4KHDHub] Trying search without year: "${titleWithoutYear}"`);
const fallbackResults = await searchContent(titleWithoutYear);
if (fallbackResults.length > 0) {
const fallbackMatch = findBestMatch(fallbackResults, tmdbDetails.title, tmdbDetails.year);
if (fallbackMatch && (!bestMatch || fallbackMatch.score > bestMatch.score)) {
bestMatch = fallbackMatch;
searchResults = fallbackResults;
}
}
}
// Try search with comprehensive alternative title formats
if (!bestMatch) {
const alternativeQueries = generateAlternativeQueries(
tmdbDetails.title,
tmdbDetails.original_title
).filter(query => query !== tmdbDetails.title); // Exclude the original title we already tried
for (const altQuery of alternativeQueries) {
console.log(`[4KHDHub] Trying alternative search: "${altQuery}"`);
const altResults = await searchContent(altQuery);
if (altResults.length > 0) {
const altMatch = findBestMatch(altResults, tmdbDetails.title, tmdbDetails.year);
if (altMatch && (!bestMatch || altMatch.score > bestMatch.score)) {
bestMatch = altMatch;
searchResults = altResults;
console.log(`[4KHDHub] Found better match with query: "${altQuery}" (score: ${altMatch.score})`);
break;
}
}
}
}
}
if (searchResults.length === 0) {
console.log(`[4KHDHub] No search results found for any query variation`);
return [];
}
if (!bestMatch) {
console.log(`[4KHDHub] No suitable match found for: ${tmdbDetails.title}`);
return [];
}
console.log(`[4KHDHub] Using best match: ${bestMatch.title}`);
const content = await loadContent(bestMatch.url);
let downloadLinks = [];
if (type === 'movie') {
downloadLinks = content.downloadLinks || [];
} else if ((type === 'series' || type === 'tv') && season && episode) {
console.log(`[4KHDHub] Looking for Season ${season}, Episode ${episode}`);
console.log(`[4KHDHub] Available episodes:`, content.episodes?.map(ep => `S${ep.season}E${ep.episode} (${ep.downloadLinks?.length || 0} links)`));
const targetEpisode = content.episodes?.find(ep =>
ep.season === parseInt(season) && ep.episode === parseInt(episode)
);
if (targetEpisode) {
console.log(`[4KHDHub] Found target episode S${targetEpisode.season}E${targetEpisode.episode} with ${targetEpisode.downloadLinks?.length || 0} links`);
downloadLinks = targetEpisode.downloadLinks || [];
} else {
console.log(`[4KHDHub] Target episode S${season}E${episode} not found`);
}
}
if (downloadLinks.length === 0) {
console.log(`[4KHDHub] No download links found`);
return [];
}
// Resolve redirect URLs to actual file hosting URLs
console.log(`[4KHDHub] Resolving ${downloadLinks.length} redirect URLs to file hosting URLs...`);
const resolvedUrls = [];
for (let i = 0; i < downloadLinks.length; i++) {
const link = downloadLinks[i];
console.log(`[4KHDHub] Resolving link ${i + 1}/${downloadLinks.length}: ${link}`);
try {
if (link.toLowerCase().includes('id=')) {
// This is a redirect URL, resolve it
const resolvedUrl = await getRedirectLinks(link);
if (resolvedUrl && resolvedUrl.trim()) {
console.log(`[4KHDHub] Link ${i + 1} resolved to: ${resolvedUrl}`);
resolvedUrls.push(resolvedUrl);
} else {
console.log(`[4KHDHub] Link ${i + 1} resolution failed or returned empty`);
}
} else {
// Direct URL, use as-is
console.log(`[4KHDHub] Link ${i + 1} is direct URL: ${link}`);
resolvedUrls.push(link);
}
} catch (error) {
console.error(`[4KHDHub] Error resolving link ${i + 1} (${link}):`, error.message);
}
}
if (resolvedUrls.length === 0) {
console.log(`[4KHDHub] No URLs resolved successfully`);
return [];
}
// Cache the resolved file hosting URLs
console.log(`[4KHDHub] Caching ${resolvedUrls.length} resolved URLs for key: ${cacheKey}`);
await saveToCache(cacheKey, resolvedUrls);
// Process resolved URLs to get final streaming links
console.log(`[4KHDHub] Processing ${resolvedUrls.length} resolved URLs to get streaming links.`);
streamingLinks = await extractStreamingLinks(resolvedUrls);
}
// Filter out suspicious AMP/redirect URLs
const filteredLinks = streamingLinks.filter(link => {
const url = link.url.toLowerCase();
const suspiciousPatterns = [
'www-google-com.cdn.ampproject.org',
'bloggingvector.shop',
'cdn.ampproject.org'
];
const isSuspicious = suspiciousPatterns.some(pattern => url.includes(pattern));
if (isSuspicious) {
console.log(`[4KHDHub] Filtered out suspicious URL: ${link.url}`);
return false;
}
return true;
});
// Remove duplicates based on URL
const uniqueLinks = [];
const seenUrls = new Set();
for (const link of filteredLinks) {
if (!seenUrls.has(link.url)) {
seenUrls.add(link.url);
uniqueLinks.push(link);
}
}
console.log(`[4KHDHub] Processing ${uniqueLinks.length} unique links (${streamingLinks.length - filteredLinks.length} suspicious URLs filtered, ${filteredLinks.length - uniqueLinks.length} duplicates removed)`);
// Validate URLs if DISABLE_4KHDHUB_URL_VALIDATION is false
let validatedLinks = uniqueLinks;
const disableValidation = process.env.DISABLE_4KHDHUB_URL_VALIDATION === 'true';
if (!disableValidation) {
console.log(`[4KHDHub] URL validation enabled, validating ${uniqueLinks.length} links...`);
const validationPromises = uniqueLinks.map(async (link) => {
const isValid = await validateUrl(link.url);
return isValid ? link : null;
});
const validationResults = await Promise.all(validationPromises);
validatedLinks = validationResults.filter(link => link !== null);
console.log(`[4KHDHub] URL validation complete: ${validatedLinks.length}/${uniqueLinks.length} links are valid`);
} else {
console.log(`[4KHDHub] URL validation disabled, skipping validation`);
}
// Normalize to generic stream object format
const streams = validatedLinks.map(link => ({
name: link.name, // Don't add prefix since it's already included
title: link.title || link.name,
url: link.url,
quality: `${link.quality}p`,
provider: "4khdhub",
headers: {}
}));
console.log(`[4KHDHub] Returning ${streams.length} streams`);
return streams;
} catch (error) {
console.error(`[4KHDHub] Error getting streams:`, error.message);
return [];
}
}
module.exports = {
get4KHDHubStreams
};