diff --git a/mapple/mapple.js b/mapple/mapple.js new file mode 100644 index 0000000..ad23324 --- /dev/null +++ b/mapple/mapple.js @@ -0,0 +1,323 @@ +//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: `Duration: ${data.runtime ? data.runtime + " minutes" : 'Unknown'}`, + airdate: `Released: ${data.release_date ? data.release_date : 'Unknown'}` + }]; + + 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: `Duration: ${data.episode_run_time && data.episode_run_time.length ? data.episode_run_time.join(', ') + " minutes" : 'Unknown'}`, + airdate: `Aired: ${data.first_air_date ? data.first_air_date : 'Unknown'}` + }]; + + 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 extractStreamUrl(ID) { + 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/')) { + const match = ID.match(/movie\/(\d+)-/); + const tmdbID = match ? match[1] : ''; + const headers = { + "Content-Type": "text/plain", + "next-action": "4067783bff09863112a9a148953f77cd8fa3d8d72a" + }; + 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) { + 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); + return JSON.stringify({ + 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/')) { + 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": "4067783bff09863112a9a148953f77cd8fa3d8d72a" + }; + 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) { + 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); + return JSON.stringify({ + 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; + } + } +}