source/mapple/mapple.js
2025-11-28 18:39:17 +00:00

449 lines
20 KiB
JavaScript

//Thanks ibro for the TMDB search!
async function searchResults(keyword) {
try {
let transformedResults = [];
const keywordGroups = {
trending: ["!trending", "!hot", "!tr", "!!"],
topRatedMovie: ["!top-rated-movie", "!topmovie", "!tm", "??"],
topRatedTV: ["!top-rated-tv", "!toptv", "!tt", "::"],
popularMovie: ["!popular-movie", "!popmovie", "!pm", ";;"],
popularTV: ["!popular-tv", "!poptv", "!pt", "++"],
};
const skipTitleFilter = Object.values(keywordGroups).flat();
const shouldFilter = !matchesKeyword(keyword, skipTitleFilter);
// --- TMDB Section ---
const encodedKeyword = encodeURIComponent(keyword);
let baseUrl = null;
if (matchesKeyword(keyword, keywordGroups.trending)) {
baseUrl = `https://api.themoviedb.org/3/trending/all/week?api_key=9801b6b0548ad57581d111ea690c85c8&include_adult=false&page=`;
} else if (matchesKeyword(keyword, keywordGroups.topRatedMovie)) {
baseUrl = `https://api.themoviedb.org/3/movie/top_rated?api_key=9801b6b0548ad57581d111ea690c85c8&include_adult=false&page=`;
} else if (matchesKeyword(keyword, keywordGroups.topRatedTV)) {
baseUrl = `https://api.themoviedb.org/3/tv/top_rated?api_key=9801b6b0548ad57581d111ea690c85c8&include_adult=false&page=`;
} else if (matchesKeyword(keyword, keywordGroups.popularMovie)) {
baseUrl = `https://api.themoviedb.org/3/movie/popular?api_key=9801b6b0548ad57581d111ea690c85c8&include_adult=false&page=`;
} else if (matchesKeyword(keyword, keywordGroups.popularTV)) {
baseUrl = `https://api.themoviedb.org/3/tv/popular?api_key=9801b6b0548ad57581d111ea690c85c8&include_adult=false&page=`;
} else {
baseUrl = `https://api.themoviedb.org/3/search/multi?api_key=9801b6b0548ad57581d111ea690c85c8&query=${encodedKeyword}&include_adult=false&page=`;
}
let dataResults = [];
if (baseUrl) {
const pagePromises = Array.from({ length: 5 }, (_, i) =>
soraFetch(baseUrl + (i + 1)).then(r => r.json())
);
const pages = await Promise.all(pagePromises);
dataResults = pages.flatMap(p => p.results || []);
}
if (dataResults.length > 0) {
function slugify(str) {
return (str || "")
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, "")
.replace(/\s+/g, "-")
.replace(/-+/g, "-");
}
transformedResults = transformedResults.concat(
dataResults
.map(result => {
if (result.media_type === "movie" || result.title) {
const title = result.title || result.name || result.original_title || result.original_name || "Untitled";
const slug = slugify(title);
return {
title,
image: result.poster_path ? `https://image.tmdb.org/t/p/w500${result.poster_path}` : "",
href: `movie/${result.id}-${slug}`,
};
} else if (result.media_type === "tv" || result.name) {
const title = result.name || result.title || result.original_name || result.original_title || "Untitled";
const slug = slugify(title);
return {
title,
image: result.poster_path ? `https://image.tmdb.org/t/p/w500${result.poster_path}` : "",
href: `tv/${result.id}-${slug}`,
};
}
})
.filter(Boolean)
.filter(result => result.title !== "Overflow")
.filter(result => result.title !== "My Marriage Partner Is My Student, a Cocky Troublemaker")
.filter(r => !shouldFilter || r.title.toLowerCase().includes(keyword.toLowerCase()))
);
}
console.log("Transformed Results: " + JSON.stringify(transformedResults));
return JSON.stringify(transformedResults);
} catch (error) {
console.log("Fetch error in searchResults: " + error);
return JSON.stringify([{ title: "Error", image: "", href: "" }]);
}
}
function matchesKeyword(keyword, commands) {
const lower = keyword.toLowerCase();
return commands.some(cmd => lower.startsWith(cmd.toLowerCase()));
}
async function extractDetails(url) {
try {
if(url.includes('movie')) {
const match = url.match(/movie\/([^\/]+)/);
if (!match) throw new Error("Invalid URL format");
const movieId = match[1];
const responseText = await soraFetch(`https://api.themoviedb.org/3/movie/${movieId}?api_key=ad301b7cc82ffe19273e55e4d4206885`);
const data = await responseText.json();
const transformedResults = [{
description: data.overview || 'No description available',
aliases: `If stream fails try again!`,
airdate: `If stream fails try again!`
}];
return JSON.stringify(transformedResults);
} else if(url.includes('tv')) {
const match = url.match(/tv\/([^\/]+)/);
if (!match) throw new Error("Invalid URL format");
const showId = match[1];
const responseText = await soraFetch(`https://api.themoviedb.org/3/tv/${showId}?api_key=ad301b7cc82ffe19273e55e4d4206885`);
const data = await responseText.json();
const transformedResults = [{
description: data.overview || 'No description available',
aliases: `If stream fails try again!`,
airdate: `If stream fails try again!`
}];
console.log(JSON.stringify(transformedResults));
return JSON.stringify(transformedResults);
} else {
throw new Error("Invalid URL format");
}
} catch (error) {
console.log('Details error: ' + error);
return JSON.stringify([{
description: 'Error loading description',
aliases: 'Duration: Unknown',
airdate: 'Aired/Released: Unknown'
}]);
}
}
async function extractEpisodes(url) {
try {
function slugify(str) {
return (str || "")
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, "")
.replace(/\s+/g, "-")
.replace(/-+/g, "-");
}
if(url.startsWith('movie/')) {
const match = url.match(/movie\/(\d+)-(.+)/);
if (!match) throw new Error("Invalid movie URL format");
const movieId = match[1];
const titleSlug = match[2];
const movie = [
{ href: `movie/${movieId}-${titleSlug}`, number: 1, title: "Full Movie" }
];
return JSON.stringify(movie);
} else if(url.startsWith('tv/')) {
const match = url.match(/tv\/(\d+)-(.+)/);
if (!match) throw new Error("Invalid TV URL format");
const showId = match[1];
const titleSlug = match[2];
const showResponseText = await soraFetch(`https://api.themoviedb.org/3/tv/${showId}?api_key=ad301b7cc82ffe19273e55e4d4206885`);
const showData = await showResponseText.json();
let allEpisodes = [];
for (const season of showData.seasons) {
const seasonNumber = season.season_number;
if(seasonNumber === 0) continue;
const seasonResponseText = await soraFetch(`https://api.themoviedb.org/3/tv/${showId}/season/${seasonNumber}?api_key=ad301b7cc82ffe19273e55e4d4206885`);
const seasonData = await seasonResponseText.json();
if (seasonData.episodes && seasonData.episodes.length) {
const episodes = seasonData.episodes.map(episode => ({
href: `tv/${showId}-${seasonNumber}-${episode.episode_number}-${titleSlug}`,
number: episode.episode_number,
title: episode.name || ""
}));
allEpisodes = allEpisodes.concat(episodes);
}
}
return JSON.stringify(allEpisodes);
} else {
throw new Error("Invalid URL format");
}
} catch (error) {
console.log('Fetch error in extractEpisodes: ' + error);
return JSON.stringify([]);
}
}
async function extractNextActionIdentifier(watchUrl) {
const htmlResponse = await soraFetch(watchUrl);
const htmlText = await htmlResponse.text();
const layoutMatch = htmlText.match(/<script[^>]*src="([^"]*app\/watch\/movie\/[^"]*layout-[^"]*\.js)"[^>]*><\/script><link rel="preload"/);
if (!layoutMatch || !layoutMatch[1]) {
throw new Error("error 1");
}
const beforeLayoutMatch = htmlText.match(/<script[^>]*src="([^"]*\.js)"[^>]*><\/script><script[^>]*src="[^"]*app\/watch\/(movie|tv)\/[^"]*layout-[^"]*\.js"/);
if (!beforeLayoutMatch || !beforeLayoutMatch[1]) {
throw new Error("error 2");
}
let targetUrl = beforeLayoutMatch[1];
if (targetUrl.startsWith('/_next/')) {
targetUrl = 'https://mapple.uk' + targetUrl;
} else if (!targetUrl.startsWith('http')) {
targetUrl = 'https://mapple.uk/' + targetUrl;
}
try {
const response = await soraFetch(targetUrl);
const text = await response.text();
let actionMatch = text.match(/createServerReference\)\("([a-f0-9]{40,})"[^"]*"getStreamUrl/);
if (!actionMatch) {
actionMatch = text.match(/createServerReference\)\("([a-f0-9]{40,})"/);
}
if (!actionMatch) {
actionMatch = text.match(/"([a-f0-9]{40,})"[^"]*"getStreamUrl/);
}
if (actionMatch && actionMatch[1]) {
return actionMatch[1];
}
} catch (e) {
throw new Error("error 3: " + e);
}
throw new Error("error 4");
}
async function extractStreamUrl(ID) {
const actionIdentifier = await extractNextActionIdentifier("https://mapple.uk/watch/movie/1061474-superman");
console.log("Action Identifier: " + actionIdentifier);
const sessionResponse = await soraFetch("https://enc-dec.app/api/enc-mapple");
const sessionData = await sessionResponse.json();
console.log("Session Data: " + JSON.stringify(sessionData));
const sessionID = sessionData.result.sessionId || "N/A";
if (ID.startsWith('/movie/') || ID.startsWith('movie/')) {
const match = ID.match(/movie\/(\d+)/);
const tmdbID = match ? match[1] : '';
console.log("Extracted TMDB ID: " + tmdbID);
const headers = {
"Content-Type": "text/plain",
"next-action": actionIdentifier
};
const postData = `[{"mediaId":${tmdbID},"mediaType":"movie","tv_slug":"","source":"mapple","useFallbackVideo":false,"sessionId":"${sessionID}"}]`;
const response = await fetchv2("https://mapple.uk/watch/"+ ID, headers, "POST", postData);
const data = await response.text();
console.log("Stream data: " + data);
const lines = data.split('\n');
let streamData = null;
for (const line of lines) {
if (line.includes('"success":true')) {
streamData = JSON.parse(line.substring(line.indexOf('{')));
break;
}
}
if (streamData && streamData.data && streamData.data.stream_url) {
const m3u8Response = await soraFetch(streamData.data.stream_url, {
headers: {
"referer": "https://mapple.uk/",
"origin": "https://mapple.uk"
}
});
const m3u8Text = await m3u8Response.text();
console.log("M3U8 Playlist: " + m3u8Text);
const streams = [];
const lines = m3u8Text.split('\n');
const resolutionNames = {
"3840x2160": "4K",
"1920x1080": "1080p",
"1280x720": "720p",
"640x360": "360p"
};
for (let i = 0; i < lines.length; i++) {
if (lines[i].startsWith('#EXT-X-STREAM-INF')) {
const resolutionMatch = lines[i].match(/RESOLUTION=(\d+x\d+)/);
const streamUrl = lines[i + 1];
if (resolutionMatch && streamUrl && streamUrl.trim()) {
const resolution = resolutionMatch[1];
const friendlyName = resolutionNames[resolution] || resolution;
streams.push({
title: friendlyName,
streamUrl: streamUrl.trim(),
headers: {
"referer": "https://mapple.uk/",
"origin": "https://mapple.uk"
}
});
}
}
}
let englishSubtitleUrl = "";
try {
const subResponse = await soraFetch(`https://mapple.uk/api/subtitles?id=${tmdbID}&mediaType=movie`);
const subData = await subResponse.json();
const englishSub = subData.find(sub => sub.language === "en");
if (englishSub) {
englishSubtitleUrl = englishSub.url;
}
} catch (e) {
englishSubtitleUrl = "";
}
console.log("English Subtitle URL: " + englishSubtitleUrl);
console.log("Extracted streams: " + JSON.stringify(streams));
return JSON.stringify({
streams: streams.length > 0 ? streams : [
{
title: "Mapple Server",
streamUrl: streamData.data.stream_url,
headers: {
"referer": "https://mapple.uk/",
"origin": "https://mapple.uk"
}
}
],
subtitle: englishSubtitleUrl || ""
});
} else {
throw new Error("Failed to extract stream URL");
}
} else if (ID.startsWith('/tv/') || ID.startsWith('tv/')) {
const match = ID.match(/tv\/(\d+)-(\d+)-(\d+)/);
if (!match) throw new Error("Invalid TV URL format");
const tmdbID = match[1];
const seasonNumber = match[2];
const episodeNumber = match[3];
const headers = {
"Content-Type": "text/plain",
"next-action": actionIdentifier
};
const postData = `[{"mediaId":${tmdbID},"mediaType":"tv","tv_slug":"${seasonNumber}-${episodeNumber}","source":"mapple","useFallbackVideo":false,"sessionId":"${sessionID}"}]`;
const response = await fetchv2("https://mapple.uk/watch/"+ ID, headers, "POST", postData);
const data = await response.text();
console.log("Stream data: " + data);
const lines = data.split('\n');
let streamData = null;
for (const line of lines) {
if (line.includes('"success":true')) {
streamData = JSON.parse(line.substring(line.indexOf('{')));
break;
}
}
if (streamData && streamData.data && streamData.data.stream_url) {
const m3u8Response = await soraFetch(streamData.data.stream_url, {
headers: {
"referer": "https://mapple.uk/",
"origin": "https://mapple.uk"
}
});
const m3u8Text = await m3u8Response.text();
console.log("M3U8 Playlist: " + m3u8Text);
const streams = [];
const lines = m3u8Text.split('\n');
const resolutionNames = {
"3840x2160": "4K",
"1920x1080": "1080p",
"1280x720": "720p",
"640x360": "360p"
};
for (let i = 0; i < lines.length; i++) {
if (lines[i].startsWith('#EXT-X-STREAM-INF')) {
const resolutionMatch = lines[i].match(/RESOLUTION=(\d+x\d+)/);
const streamUrl = lines[i + 1];
if (resolutionMatch && streamUrl && streamUrl.trim()) {
const resolution = resolutionMatch[1];
const friendlyName = resolutionNames[resolution] || resolution;
streams.push({
title: friendlyName,
streamUrl: streamUrl.trim(),
headers: {
"referer": "https://mapple.uk/",
"origin": "https://mapple.uk"
}
});
}
}
}
let englishSubtitleUrl = "";
try {
const subResponse = await soraFetch(`https://mapple.uk/api/subtitles?id=${tmdbID}&mediaType=tv&season=${seasonNumber}&episode=${episodeNumber}`);
const subData = await subResponse.json();
const englishSub = subData.find(sub => sub.language === "en");
if (englishSub) {
englishSubtitleUrl = englishSub.url;
}
} catch (e) {
englishSubtitleUrl = "";
}
console.log("English Subtitle URL: " + englishSubtitleUrl);
console.log("Extracted streams: " + JSON.stringify(streams));
return JSON.stringify({
streams: streams.length > 0 ? streams : [
{
title: "Mapple Server",
streamUrl: streamData.data.stream_url,
headers: {
"referer": "https://mapple.uk/",
"origin": "https://mapple.uk"
}
}
],
subtitle: englishSubtitleUrl || ""
});
} else {
throw new Error("Failed to extract stream URL");
}
}
}
async function soraFetch(url, options = { headers: {}, method: 'GET', body: null, encoding: 'utf-8' }) {
try {
return await fetchv2(
url,
options.headers ?? {},
options.method ?? 'GET',
options.body ?? null,
true,
options.encoding ?? 'utf-8'
);
} catch(e) {
try {
return await fetch(url, options);
} catch(error) {
return null;
}
}
}