mirror of
https://github.com/Inside4ndroid/TMDB-Embed-API.git
synced 2026-10-22 17:45:11 +00:00
488 lines
19 KiB
JavaScript
488 lines
19 KiB
JavaScript
const axios = require('axios');
|
|
const FormData = require('form-data');
|
|
|
|
// Helper function to generate slug from title
|
|
function generateSlug(title) {
|
|
return title.toLowerCase()
|
|
.replace(/[^\w\s-]/g, '') // Remove special characters
|
|
.replace(/\s+/g, '-') // Replace spaces with hyphens
|
|
.replace(/-+/g, '-'); // Replace multiple hyphens with a single hyphen
|
|
}
|
|
|
|
// Process episode data from MP4Hydra response
|
|
function processEpisode(episode, baseServer, serverName, serverNumber) {
|
|
const videoUrl = `${baseServer}${episode.src}`;
|
|
const subtitles = episode.subs ? episode.subs.map(sub => ({
|
|
label: sub.label,
|
|
url: `${baseServer}${sub.src}`
|
|
})) : [];
|
|
|
|
return {
|
|
title: episode.show_title || episode.title,
|
|
episode: episode.title,
|
|
type: episode.type,
|
|
quality: episode.quality || episode.label,
|
|
videoUrl: videoUrl,
|
|
server: serverName,
|
|
serverNumber: serverNumber,
|
|
subtitles: subtitles
|
|
};
|
|
}
|
|
|
|
// Main function to get streams from MP4Hydra
|
|
async function getMP4HydraStreams(tmdbId, mediaType, seasonNum = null, episodeNum = null) {
|
|
try {
|
|
console.log(`[MP4Hydra] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}, Season: ${seasonNum}, Episode: ${episodeNum}`);
|
|
|
|
// Get TMDB details
|
|
const details = await getTMDBDetails(tmdbId, mediaType);
|
|
if (!details) {
|
|
console.log(`[MP4Hydra] Could not fetch details for TMDB ID: ${tmdbId}`);
|
|
return [];
|
|
}
|
|
|
|
console.log(`[MP4Hydra] Found title: ${details.title} (${details.year})`);
|
|
|
|
// Generate slug in the format "movie-name-year"
|
|
let slug = details.slug;
|
|
if (mediaType === 'movie' && details.year) {
|
|
slug = `${details.slug}-${details.year}`;
|
|
}
|
|
console.log(`[MP4Hydra] Using slug: ${slug}`);
|
|
|
|
// Create form data for multipart/form-data request
|
|
const formData = new FormData();
|
|
formData.append('v', '8');
|
|
formData.append('z', JSON.stringify([{
|
|
s: slug,
|
|
t: mediaType,
|
|
se: seasonNum,
|
|
ep: episodeNum
|
|
}]));
|
|
|
|
// Make request to MP4Hydra API
|
|
const response = await axios({
|
|
method: 'post',
|
|
url: 'https://mp4hydra.org/info2?v=8',
|
|
data: formData,
|
|
headers: {
|
|
...formData.getHeaders(),
|
|
'User-Agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Mobile Safari/537.36',
|
|
'Accept': '*/*',
|
|
'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8',
|
|
'Origin': 'https://mp4hydra.org',
|
|
'Referer': `https://mp4hydra.org/${mediaType}/${slug}`,
|
|
'Sec-Fetch-Dest': 'empty',
|
|
'Sec-Fetch-Mode': 'cors',
|
|
'Sec-Fetch-Site': 'same-origin'
|
|
},
|
|
timeout: 10000
|
|
});
|
|
|
|
// Process response
|
|
if (response.data && response.data.playlist && response.data.playlist.length > 0) {
|
|
const playlist = response.data.playlist;
|
|
const servers = response.data.servers;
|
|
|
|
console.log(`[MP4Hydra] Found ${playlist.length} videos`);
|
|
console.log(`[MP4Hydra] Available servers: ${Object.keys(servers).join(', ')}`);
|
|
|
|
// For TV shows, find the specific episode
|
|
if (mediaType === 'tv' && seasonNum && episodeNum) {
|
|
const paddedSeason = seasonNum.toString().padStart(2, '0');
|
|
const paddedEpisode = episodeNum.toString().padStart(2, '0');
|
|
const seasonEpisode = `S${paddedSeason}E${paddedEpisode}`;
|
|
|
|
const targetEpisode = playlist.find(item =>
|
|
item.title && item.title.toUpperCase() === seasonEpisode.toUpperCase()
|
|
);
|
|
|
|
if (!targetEpisode) {
|
|
console.log(`[MP4Hydra] Could not find ${seasonEpisode}`);
|
|
return [];
|
|
}
|
|
|
|
console.log(`[MP4Hydra] Found episode: ${targetEpisode.show_title || targetEpisode.title}`);
|
|
|
|
// Process streams from main servers
|
|
const streams = [];
|
|
const serverConfig = [
|
|
{ name: 'Beta', number: '#1' },
|
|
{ name: 'Beta#3', number: '#2' }
|
|
];
|
|
|
|
serverConfig.forEach(server => {
|
|
const serverName = server.name;
|
|
const serverNumber = server.number;
|
|
|
|
if (servers[serverName]) {
|
|
const baseServer = servers[serverName];
|
|
console.log(`[MP4Hydra] Processing server: ${serverName} (${baseServer})`);
|
|
|
|
const processedEpisode = processEpisode(targetEpisode, baseServer, serverName, serverNumber);
|
|
|
|
// Convert to standard stream format
|
|
streams.push({
|
|
title: `${details.title} - ${seasonEpisode} - ${processedEpisode.quality} [MP4Hydra ${serverNumber}]`,
|
|
url: processedEpisode.videoUrl,
|
|
quality: processedEpisode.quality,
|
|
provider: "mp4hydra",
|
|
headers: {
|
|
'Referer': 'https://mp4hydra.org/'
|
|
}
|
|
|
|
});
|
|
|
|
// Add subtitle tracks if available
|
|
if (processedEpisode.subtitles && processedEpisode.subtitles.length > 0) {
|
|
streams[streams.length - 1].subtitles = processedEpisode.subtitles.map(sub => ({
|
|
url: sub.url,
|
|
lang: sub.label
|
|
}));
|
|
}
|
|
}
|
|
});
|
|
|
|
return streams;
|
|
}
|
|
|
|
// For movies, process all videos
|
|
const streams = [];
|
|
const serverConfig = [
|
|
{ name: 'Beta', number: '#1' },
|
|
{ name: 'Beta#3', number: '#2' }
|
|
];
|
|
|
|
serverConfig.forEach(server => {
|
|
const serverName = server.name;
|
|
const serverNumber = server.number;
|
|
|
|
if (servers[serverName]) {
|
|
const baseServer = servers[serverName];
|
|
console.log(`[MP4Hydra] Processing server: ${serverName} (${baseServer})`);
|
|
|
|
playlist.forEach(item => {
|
|
const processedItem = processEpisode(item, baseServer, serverName, serverNumber);
|
|
|
|
// Convert to standard stream format
|
|
streams.push({
|
|
title: `${details.title} - ${processedItem.quality} [MP4Hydra ${serverNumber}]`,
|
|
url: processedItem.videoUrl,
|
|
quality: processedItem.quality,
|
|
provider: "mp4hydra",
|
|
headers: {
|
|
'Referer': 'https://mp4hydra.org/'
|
|
}
|
|
|
|
});
|
|
|
|
// Add subtitle tracks if available
|
|
if (processedItem.subtitles && processedItem.subtitles.length > 0) {
|
|
streams[streams.length - 1].subtitles = processedItem.subtitles.map(sub => ({
|
|
url: sub.url,
|
|
lang: sub.label
|
|
}));
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
return streams;
|
|
} else {
|
|
// Try with original title if the first attempt failed
|
|
if (details.title !== details.original_title) {
|
|
return await tryAlternativeTitle(details, mediaType, seasonNum, episodeNum);
|
|
}
|
|
|
|
// Try without year for movies
|
|
if (mediaType === 'movie' && details.year) {
|
|
return await tryWithoutYear(details, mediaType, seasonNum, episodeNum);
|
|
}
|
|
|
|
console.log('[MP4Hydra] No streaming links found');
|
|
return [];
|
|
}
|
|
} catch (error) {
|
|
console.error(`[MP4Hydra] Error fetching streams:`, error.message);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// Helper function to try with original title
|
|
async function tryAlternativeTitle(details, mediaType, seasonNum, episodeNum) {
|
|
try {
|
|
console.log(`[MP4Hydra] Retrying with original title: ${details.original_title}`);
|
|
const originalSlug = generateSlug(details.original_title);
|
|
let originalFullSlug = originalSlug;
|
|
|
|
if (mediaType === 'movie' && details.year) {
|
|
originalFullSlug = `${originalSlug}-${details.year}`;
|
|
}
|
|
|
|
// Create new form data with original title
|
|
const formData = new FormData();
|
|
formData.append('v', '8');
|
|
formData.append('z', JSON.stringify([{
|
|
s: originalFullSlug,
|
|
t: mediaType,
|
|
se: seasonNum,
|
|
ep: episodeNum
|
|
}]));
|
|
|
|
const response = await axios({
|
|
method: 'post',
|
|
url: 'https://mp4hydra.org/info2?v=8',
|
|
data: formData,
|
|
headers: {
|
|
...formData.getHeaders(),
|
|
'User-Agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Mobile Safari/537.36',
|
|
'Accept': '*/*',
|
|
'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8',
|
|
'Origin': 'https://mp4hydra.org',
|
|
'Referer': `https://mp4hydra.org/${mediaType}/${originalFullSlug}`,
|
|
'Sec-Fetch-Dest': 'empty',
|
|
'Sec-Fetch-Mode': 'cors',
|
|
'Sec-Fetch-Site': 'same-origin'
|
|
},
|
|
timeout: 10000
|
|
});
|
|
|
|
// Process response
|
|
if (response.data && response.data.playlist && response.data.playlist.length > 0) {
|
|
const playlist = response.data.playlist;
|
|
const servers = response.data.servers;
|
|
|
|
console.log(`[MP4Hydra] Found ${playlist.length} videos with original title`);
|
|
|
|
// For TV shows, find the specific episode
|
|
if (mediaType === 'tv' && seasonNum && episodeNum) {
|
|
const paddedSeason = seasonNum.toString().padStart(2, '0');
|
|
const paddedEpisode = episodeNum.toString().padStart(2, '0');
|
|
const seasonEpisode = `S${paddedSeason}E${paddedEpisode}`;
|
|
|
|
const targetEpisode = playlist.find(item =>
|
|
item.title && item.title.toUpperCase() === seasonEpisode.toUpperCase()
|
|
);
|
|
|
|
if (!targetEpisode) {
|
|
console.log(`[MP4Hydra] Could not find ${seasonEpisode} with original title`);
|
|
return [];
|
|
}
|
|
|
|
// Process streams from main servers
|
|
const streams = [];
|
|
const serverConfig = [
|
|
{ name: 'Beta', number: '#1' },
|
|
{ name: 'Beta#3', number: '#2' }
|
|
];
|
|
|
|
serverConfig.forEach(server => {
|
|
const serverName = server.name;
|
|
const serverNumber = server.number;
|
|
|
|
if (servers[serverName]) {
|
|
const baseServer = servers[serverName];
|
|
console.log(`[MP4Hydra] Processing server: ${serverName} (${baseServer})`);
|
|
|
|
const processedEpisode = processEpisode(targetEpisode, baseServer, serverName, serverNumber);
|
|
|
|
// Convert to standard stream format
|
|
streams.push({
|
|
title: `${details.original_title} - ${seasonEpisode} - ${processedEpisode.quality} [MP4Hydra ${serverNumber}]`,
|
|
url: processedEpisode.videoUrl,
|
|
quality: processedEpisode.quality,
|
|
provider: "mp4hydra",
|
|
headers: {
|
|
'Referer': 'https://mp4hydra.org/'
|
|
}
|
|
|
|
});
|
|
|
|
// Add subtitle tracks if available
|
|
if (processedEpisode.subtitles && processedEpisode.subtitles.length > 0) {
|
|
streams[streams.length - 1].subtitles = processedEpisode.subtitles.map(sub => ({
|
|
url: sub.url,
|
|
lang: sub.label
|
|
}));
|
|
}
|
|
}
|
|
});
|
|
|
|
return streams;
|
|
}
|
|
|
|
// For movies, process all videos
|
|
const streams = [];
|
|
const serverConfig = [
|
|
{ name: 'Beta', number: '#1' },
|
|
{ name: 'Beta#3', number: '#2' }
|
|
];
|
|
|
|
serverConfig.forEach(server => {
|
|
const serverName = server.name;
|
|
const serverNumber = server.number;
|
|
|
|
if (servers[serverName]) {
|
|
const baseServer = servers[serverName];
|
|
console.log(`[MP4Hydra] Processing server: ${serverName} (${baseServer})`);
|
|
|
|
playlist.forEach(item => {
|
|
const processedItem = processEpisode(item, baseServer, serverName, serverNumber);
|
|
|
|
// Convert to standard stream format
|
|
streams.push({
|
|
name: `${details.original_title}`,
|
|
title: `${details.original_title} - ${processedItem.quality} [MP4Hydra ${serverNumber}]`,
|
|
url: processedItem.videoUrl,
|
|
quality: processedItem.quality
|
|
});
|
|
|
|
// Add subtitle tracks if available
|
|
if (processedItem.subtitles && processedItem.subtitles.length > 0) {
|
|
streams[streams.length - 1].subtitles = processedItem.subtitles.map(sub => ({
|
|
url: sub.url,
|
|
lang: sub.label
|
|
}));
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
return streams;
|
|
}
|
|
|
|
return [];
|
|
} catch (error) {
|
|
console.error(`[MP4Hydra] Error with alternative title:`, error.message);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// Helper function to try without year
|
|
async function tryWithoutYear(details, mediaType, seasonNum, episodeNum) {
|
|
try {
|
|
console.log(`[MP4Hydra] Retrying with title only (without year): ${details.title}`);
|
|
const titleOnlySlug = details.slug;
|
|
|
|
// Create new form data with title only
|
|
const formData = new FormData();
|
|
formData.append('v', '8');
|
|
formData.append('z', JSON.stringify([{
|
|
s: titleOnlySlug,
|
|
t: mediaType,
|
|
se: seasonNum,
|
|
ep: episodeNum
|
|
}]));
|
|
|
|
const response = await axios({
|
|
method: 'post',
|
|
url: 'https://mp4hydra.org/info2?v=8',
|
|
data: formData,
|
|
headers: {
|
|
...formData.getHeaders(),
|
|
'User-Agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Mobile Safari/537.36',
|
|
'Accept': '*/*',
|
|
'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8',
|
|
'Origin': 'https://mp4hydra.org',
|
|
'Referer': `https://mp4hydra.org/${mediaType}/${titleOnlySlug}`,
|
|
'Sec-Fetch-Dest': 'empty',
|
|
'Sec-Fetch-Mode': 'cors',
|
|
'Sec-Fetch-Site': 'same-origin'
|
|
},
|
|
timeout: 10000
|
|
});
|
|
|
|
// Process response
|
|
if (response.data && response.data.playlist && response.data.playlist.length > 0) {
|
|
const playlist = response.data.playlist;
|
|
const servers = response.data.servers;
|
|
|
|
console.log(`[MP4Hydra] Found ${playlist.length} videos with title only`);
|
|
|
|
// For movies, process all videos
|
|
const streams = [];
|
|
const serverConfig = [
|
|
{ name: 'Beta', number: '#1' },
|
|
{ name: 'Beta#3', number: '#2' }
|
|
];
|
|
|
|
serverConfig.forEach(server => {
|
|
const serverName = server.name;
|
|
const serverNumber = server.number;
|
|
|
|
if (servers[serverName]) {
|
|
const baseServer = servers[serverName];
|
|
console.log(`[MP4Hydra] Processing server: ${serverName} (${baseServer})`);
|
|
|
|
playlist.forEach(item => {
|
|
const processedItem = processEpisode(item, baseServer, serverName, serverNumber);
|
|
|
|
// Convert to standard stream format
|
|
streams.push({
|
|
name: `${details.title}`,
|
|
title: `${details.title} - ${processedItem.quality} [MP4Hydra ${serverNumber}]`,
|
|
url: processedItem.videoUrl,
|
|
quality: processedItem.quality,
|
|
provider: "mp4hydra",
|
|
headers: {
|
|
'Referer': 'https://mp4hydra.org/'
|
|
}
|
|
|
|
});
|
|
|
|
// Add subtitle tracks if available
|
|
if (processedItem.subtitles && processedItem.subtitles.length > 0) {
|
|
streams[streams.length - 1].subtitles = processedItem.subtitles.map(sub => ({
|
|
url: sub.url,
|
|
lang: sub.label
|
|
}));
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
return streams;
|
|
}
|
|
|
|
return [];
|
|
} catch (error) {
|
|
console.error(`[MP4Hydra] Error with title-only:`, error.message);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// 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('[MP4Hydra] TMDB API key missing; skipping TMDB metadata fetch');
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
console.log(`[MP4Hydra] Fetching ${mediaType} details for TMDB ID: ${tmdbId}`);
|
|
const response = await axios.get(`https://api.themoviedb.org/3/${mediaType}/${tmdbId}?api_key=${TMDB_API_KEY}`);
|
|
|
|
if (mediaType === 'movie') {
|
|
return {
|
|
title: response.data.title,
|
|
original_title: response.data.original_title,
|
|
year: response.data.release_date ? response.data.release_date.split('-')[0] : null,
|
|
slug: generateSlug(response.data.title)
|
|
};
|
|
} else {
|
|
return {
|
|
title: response.data.name,
|
|
original_title: response.data.original_name,
|
|
year: response.data.first_air_date ? response.data.first_air_date.split('-')[0] : null,
|
|
slug: generateSlug(response.data.name)
|
|
};
|
|
}
|
|
} catch (error) {
|
|
console.error(`[MP4Hydra] Error fetching details from TMDB:`, error.message);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
module.exports = { getMP4HydraStreams };
|