updates and new extensions

This commit is contained in:
2026-01-05 04:46:26 +01:00
parent 5ee2bde49a
commit 83c51a82da
9 changed files with 1500 additions and 129 deletions

437
anime/OppaiStream.js Normal file
View File

@@ -0,0 +1,437 @@
class OppaiStream {
constructor() {
this.baseUrl = "https://oppai.stream";
this.searchBaseUrl = "https://oppai.stream/actions/search.php?order=recent&page=1&limit=35&genres=&blacklist=&studio=&ibt=0&swa=1&text=";
this.type = "anime-board";
this.version = "1.0";
this.userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36";
this.ScoreWeight = {
Title: 3.6,
MaxScore: 10
};
}
getSettings() {
return {
episodeServers: ["oppai.stream"],
supportsDub: false,
};
}
async search(queryObj) {
let tempquery = queryObj.query;
while (tempquery !== "") {
try {
const url = this.searchBaseUrl + encodeURIComponent(tempquery);
const html = await this.GETText(url);
const $ = this.cheerio.load(html);
const movies = $("div.in-grid.episode-shown");
if (movies.length <= 0) {
if (tempquery.includes(" ")) {
tempquery = tempquery.split(/[\s:']+/).slice(0, -1).join(" ");
continue;
} else {
break;
}
}
const movieList = [];
movies.each((_, el) => {
const title = $(el).find(".title-ep").text().trim();
const href = $(el).find("a").attr("href");
const rawUrl = href ? href.replace("&for=search", "") : "";
if (title && rawUrl) {
movieList.push({ Title: title, Url: rawUrl });
}
});
const bestMovie = this.findBestTitle(movieList, queryObj.query);
if (!bestMovie) return [];
return [{
// Codificamos la URL para que sea un ID seguro para la URL de la app
id: encodeURIComponent(bestMovie.Url),
title: bestMovie.Title,
url: bestMovie.Url,
subOrDub: queryObj.dub ? "dub" : "sub",
}];
} catch (e) {
console.error(e);
return [];
}
}
return [];
}
async getMetadata(id) {
try {
// Decodificamos el ID para obtener la URL real de OppaiStream
const decodedUrl = decodeURIComponent(id);
const html = await this.GETText(decodedUrl);
const $ = this.cheerio.load(html);
const title = $("meta[property='og:title']").attr("content") || $("h1").text().trim();
const image = $("meta[property='og:image']").attr("content") || "";
const summary = $("meta[property='og:description']").attr("content") || $(".desc").text().trim();
return {
title: title,
summary: summary,
episodes: $("div.other-episodes.more-same-eps div.in-grid").length || 0,
image: image,
genres: [],
status: "Unknown"
};
} catch (e) {
console.error(e);
throw new Error("Failed to get metadata");
}
}
async findEpisodes(id) {
if (!id) return [];
try {
// Decodificamos el ID para obtener la URL real
const decodedUrl = decodeURIComponent(id);
const html = await this.GETText(decodedUrl);
const $ = this.cheerio.load(html);
const episodeDetails = [];
const eps = $("div.other-episodes.more-same-eps div.in-grid.episode-shown");
eps.each((_, el) => {
const elObj = $(el);
const idgt = elObj.attr("idgt");
if (idgt) {
const href = elObj.find("a").attr("href");
const rawEpUrl = href ? href.replace("&for=episode-more", "") : "";
const title = elObj.find("h5 .title").text().trim();
const epNum = parseInt(elObj.find("h5 .ep").text().trim(), 10);
episodeDetails.push({
// También codificamos el ID del episodio por seguridad
id: encodeURIComponent(rawEpUrl),
number: isNaN(epNum) ? 0 : epNum,
title: title || `Episode ${epNum}`,
url: rawEpUrl,
});
}
});
return episodeDetails;
} catch (e) {
console.error(e);
return [];
}
}
async findEpisodeServer(episode, serverStr) {
// Decodificamos el ID del episodio (que es la URL)
const serverUrl = decodeURIComponent(episode.id);
const videoSources = [];
if (serverUrl) {
const result = await this.HandleServerUrl(serverUrl);
if (Array.isArray(result)) {
videoSources.push(...result);
} else if (result) {
videoSources.push(result);
}
}
return {
server: serverStr || "oppai.stream",
headers: {
"Referer": this.baseUrl,
"User-Agent": this.userAgent
},
videoSources: videoSources
};
}
async HandleServerUrl(serverUrl) {
try {
const html = await this.GETText(serverUrl);
let unpacked = "";
const scriptContents = this.extractScripts(html);
for (const c of scriptContents) {
let c2 = c;
for (let j = 0; j < c.length; j += 900) {
c2 = c2.substring(0, j) + "\n" + c2.substring(j);
}
if (c.includes("eval(function(p,a,c,k,e,d)")) {
console.log("Packed script found.");
const fullRegex = /eval\(function\([^)]*\)\{[\s\S]*?\}\(\s*'([\s\S]*?)'\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*'([\s\S]*?)'\.split\('\|'\)/;
const match = c2.match(fullRegex);
if (match) {
const packed = match[1];
const base = parseInt(match[2], 10);
const count = parseInt(match[3], 10);
const dict = match[4].split('|');
unpacked = this.unpack(packed, base, count, dict);
unpacked = this.decodeUnpacked(unpacked);
}
}
}
const m3u8Videos = await this.findMediaUrls("m3u8", html, serverUrl, unpacked);
if (m3u8Videos) return m3u8Videos;
const mp4Videos = await this.findMediaUrls("mp4", html, serverUrl, unpacked);
if (mp4Videos) return mp4Videos;
return [];
} catch (e) {
console.error("Error handling server URL:", e);
return [];
}
}
async findMediaUrls(type, html, serverUrl, unpacked) {
const regex = new RegExp('https?:\\/\\/[^\'"]+\\.' + type + '(?:\\?[^\\s\'"]*)?(?:#[^\\s\'"]*)?', 'g');
const quotedRegex = new RegExp(`"([^"]+\\.${type})"`, "g");
let VideoMatch = html.match(regex)
|| (unpacked && unpacked.match(regex))
|| html.match(quotedRegex)
|| (unpacked && unpacked.match(quotedRegex));
if (VideoMatch) {
VideoMatch = VideoMatch.map(url => {
let clean = url.replace(/"/g, "");
if (!clean.startsWith("http")) {
const domain = serverUrl.split("/").slice(0, 3).join("/");
return `${domain}${clean}`;
}
return clean;
});
VideoMatch = [...new Set(VideoMatch)];
const mainUrl = VideoMatch[0];
console.log(`Found ${type} URL:`, mainUrl);
if (mainUrl.includes(`master.${type}`)) {
try {
const reqHtml = await this.GETText(mainUrl);
const videos = [];
let qual = "";
let url = "";
if (reqHtml.includes("#EXTM3U")) {
const lines = reqHtml.split("\n");
for (let line of lines) {
if (line.startsWith("#EXT-X-STREAM-INF")) {
qual = line.split("RESOLUTION=")[1]?.split(",")[0] || "unknown";
const h = parseInt(qual.split("x")[1]) || 0;
if (h >= 1080) qual = "1080p";
else if (h >= 720) qual = "720p";
else if (h >= 480) qual = "480p";
else if (h >= 360) qual = "360p";
} else if (line.trim().startsWith("http") || line.trim().endsWith(".m3u8")) {
url = line.trim();
if (!url.startsWith("http")) {
const baseUrl = mainUrl.substring(0, mainUrl.lastIndexOf('/') + 1);
url = baseUrl + url;
}
}
if (url && qual) {
videos.push({
url: url,
type: type,
quality: qual,
subtitles: []
});
url = "";
qual = "";
}
}
}
if (videos.length > 0) {
const subtitles = await this.findSubtitles(html, serverUrl, unpacked);
videos.forEach(v => v.subtitles = subtitles);
return videos;
}
} catch (e) {
console.warn("Failed to parse master playlist", e);
}
}
const resolutionRegex = /\/(\d{3,4})\//;
const resolutionMatch = mainUrl.match(resolutionRegex);
const quality = resolutionMatch ? `${resolutionMatch[1]}p` : "unknown";
return {
url: mainUrl,
quality: quality,
type: type,
subtitles: await this.findSubtitles(html, serverUrl, unpacked)
};
}
return undefined;
}
async findSubtitles(html, serverUrl, unpacked) {
let subtitles = [];
const subtitleRegex = /<track\s+[^>]*src=["']([^"']+\.vtt(?:\?[^"']*)?)["'][^>]*>/gi;
const extract = (text) => {
const matches = text.matchAll(subtitleRegex);
for (const match of matches) {
const src = match[1];
let url = src.startsWith("http") ? src : `${serverUrl.split("/").slice(0, 3).join("/")}${src}`;
const langMatch = match[0].match(/(?:label|srclang)=["']?([a-zA-Z\-]{2,})["']?/i);
const lang = langMatch?.[1]?.toLowerCase() || "unknown";
subtitles.push({
url,
language: lang,
type: "vtt"
});
}
};
if (html) extract(html);
if (subtitles.length === 0) {
const rawRegex = /https?:\/\/[^\s'"]+\.vtt(?:\?[^'"\s]*)?/g;
const matches = (html.match(rawRegex) || []).concat(unpacked ? (unpacked.match(rawRegex) || []) : []);
matches.forEach((url, idx) => {
if (!subtitles.some(s => s.url === url)) {
subtitles.push({
url: url,
language: "Unknown " + (idx + 1),
type: "vtt"
});
}
});
}
return subtitles;
}
extractScripts(str) {
const results = [];
const openTag = "<script type='text/javascript'>";
const closeTag = "</script>";
let pos = 0;
while (pos < str.length) {
const start = str.indexOf(openTag, pos);
if (start === -1) break;
const end = str.indexOf(closeTag, start);
if (end === -1) break;
results.push(str.substring(start + openTag.length, end));
pos = end + closeTag.length;
}
return results;
}
unpack(p, a, c, k) {
while (c--) if (k[c]) p = p.replace(new RegExp('\\b' + c.toString(a) + '\\b', 'g'), k[c]);
return p;
}
decodeUnpacked(str) {
return str.replace(/\\u([\d\w]{4})/gi, (_, grp) => String.fromCharCode(parseInt(grp, 16)))
.replace(/%3C/g, '<').replace(/%3E/g, '>')
.replace(/%3F/g, '?').replace(/%3A/g, ':')
.replace(/%2C/g, ',').replace(/%2F/g, '/')
.replace(/%2B/g, '+').replace(/%20/g, ' ')
.replace(/%21/g, '!').replace(/%22/g, '"')
.replace(/%27/g, "'").replace(/%28/g, '(')
.replace(/%29/g, ')').replace(/%3B/g, ';');
}
findBestTitle(movies, query) {
let bestScore = 0;
let bestMovie = undefined;
for (const movie of movies) {
let score = this.scoreStringMatch(2, movie.Title, query);
console.log(`Movie: ${movie.Title} - Score: ${score}`);
if (score > bestScore) {
bestScore = score;
bestMovie = movie;
}
}
return bestMovie;
}
scoreStringMatch(weight, text, query) {
if (!text || !query) return 0;
text = text.toLowerCase();
query = query.toLowerCase();
if (text === query) return this.ScoreWeight.MaxScore * weight;
const textWords = text.split(" ");
const queryWords = query.split(" ");
let score = 0;
for (const word of queryWords) {
if (textWords.includes(word)) {
score += this.ScoreWeight.MaxScore / textWords.length;
} else {
const similarity = this.getWordSimilarity(word, textWords);
score -= similarity * this.ScoreWeight.MaxScore / textWords.length;
}
}
return score * weight;
}
getWordSimilarity(word1, words) {
const word1Vector = this.getWordVector(word1);
let maxSimilarity = 0;
for (const word2 of words) {
const word2Vector = this.getWordVector(word2);
const similarity = this.cosineSimilarity(word1Vector, word2Vector);
maxSimilarity = Math.max(maxSimilarity, similarity);
}
return maxSimilarity;
}
getWordVector(word) {
return Array.from(word).map(char => char.charCodeAt(0));
}
cosineSimilarity(vec1, vec2) {
const dotProduct = vec1.reduce((sum, val, i) => sum + val * (vec2[i] || 0), 0);
const magnitude1 = Math.sqrt(vec1.reduce((sum, val) => sum + val * val, 0));
const magnitude2 = Math.sqrt(vec2.reduce((sum, val) => sum + val * val, 0));
return (magnitude1 && magnitude2) ? dotProduct / (magnitude1 * magnitude2) : 0;
}
async GETText(url) {
const response = await fetch(url, {
headers: {
"User-Agent": this.userAgent
}
});
if (!response.ok) throw new Error(`GETText failed: ${response.status}`);
return await response.text();
}
}
module.exports = OppaiStream;