updates and new extensions

This commit is contained in:
2026-01-13 17:26:06 +01:00
parent 83c51a82da
commit e8d64174fd
15 changed files with 3516 additions and 468 deletions

266
anime/missav.js Normal file
View File

@@ -0,0 +1,266 @@
class MissAV {
constructor() {
this.type = "anime-board";
this.version = "1.0";
this.baseUrl = "https://missav.live";
}
getSettings() {
return {
supportsDub: false,
episodeServers: ["Default"],
};
}
/* ================= FILTERS ================= */
getFilters() {
return [
{
key: "sort",
name: "Sort by",
type: "select",
options: [
{ value: "", label: "Any" },
{ value: "released_at", label: "Release date" },
{ value: "published_at", label: "Recent update" },
{ value: "today_views", label: "Today views" },
{ value: "weekly_views", label: "Weekly views" },
{ value: "monthly_views", label: "Monthly views" },
{ value: "views", label: "Total views" },
],
},
{
key: "genre",
name: "Genres",
type: "select",
options: [
{ value: "", label: "<Select>" },
{ value: "en/uncensored-leak", label: "Uncensored Leak" },
{ value: "en/genres/Hd", label: "Hd" },
{ value: "en/genres/Exclusive", label: "Exclusive" },
{ value: "en/genres/Creampie", label: "Creampie" },
{ value: "en/genres/Big%20Breasts", label: "Big Breasts" },
{ value: "en/genres/Individual", label: "Individual" },
{ value: "en/genres/Wife", label: "Wife" },
{ value: "en/genres/Mature%20Woman", label: "Mature Woman" },
{ value: "en/genres/Pretty%20Girl", label: "Pretty Girl" },
{ value: "en/genres/Orgy", label: "Orgy" },
{ value: "en/genres/Lesbian", label: "Lesbian" },
{ value: "en/genres/Ntr", label: "NTR" },
{ value: "en/genres/Cosplay", label: "Cosplay" },
{ value: "en/genres/Uniform", label: "Uniform" },
{ value: "en/genres/Swimsuit", label: "Swimsuit" },
{ value: "en/genres/4K", label: "4K" },
{ value: "en/genres/Vr", label: "VR" },
],
},
{
type: "note",
text: "Genre filters ignored with text search!",
},
];
}
/* ================= SEARCH ================= */
async search(query) {
const filters = query?.filters || {};
const hasText = !!(query?.query && query.query.trim());
let url;
if (hasText) {
url = `${this.baseUrl}/en/search/${encodeURIComponent(query.query)}`;
} else if (filters.genre) {
url = `${this.baseUrl}/${filters.genre}`;
} else {
const params = new URLSearchParams();
if (filters.sort) params.set("sort", filters.sort);
url = `${this.baseUrl}/en?${params.toString()}`;
}
const { result, requests } = await this.scrape(
url,
async (page) => {
const html = await page.content();
const items = await page.$$eval(
'div.thumbnail',
nodes => nodes.map(n => {
const a = n.querySelector('a[href^="https://missav.live/en/"]');
if (!a) return null;
const href = a.getAttribute("href");
const img =
n.querySelector('img')?.getAttribute("data-src") ||
n.querySelector('img')?.getAttribute("src");
const title =
n.querySelector('div.text-sm a')?.textContent?.trim() ||
n.querySelector('a')?.textContent?.trim();
if (!href || !img || !title) return null;
return {
id: href.replace("https://missav.live", ""),
title,
image: img,
url: href,
};
}).filter(Boolean)
);
return items;
},
{
waitUntil: "domcontentloaded",
renderWaitTime: 1500,
scrollToBottom: true,
}
);
return result;
}
/* ================= METADATA ================= */
async getMetadata(animeId) {
const url = animeId.startsWith("http")
? animeId
: this.baseUrl + animeId;
console.log("[MissAV][meta] url =", url);
const { result, requests } = await this.scrape(
url,
async (page) => {
console.log("[MissAV][meta] page loaded");
const htmlSize = await page.content().then(h => h.length);
console.log("[MissAV][meta] html size =", htmlSize);
return await page.evaluate(() => {
const dbg = {};
const h1 = document.querySelector("h1");
dbg.hasH1 = !!h1;
const video = document.querySelector("video.player");
dbg.hasVideo = !!video;
const og = document.querySelector('meta[property="og:image"]');
dbg.hasOg = !!og;
const genreLinks = document.querySelectorAll('a[href^="/en/genres/"]');
dbg.genreCount = genreLinks.length;
const title =
document.querySelector("h1.text-base")?.textContent?.trim() ||
document.querySelector("h1")?.textContent?.trim() ||
"Unknown";
const poster =
video?.getAttribute("data-poster") ||
og?.content ||
null;
const description = "";
const genres = Array.from(genreLinks).map(a =>
a.textContent.trim()
);
return {
dbg,
title,
poster,
description,
genres,
};
});
},
{
waitUntil: "domcontentloaded",
timeout: 20000,
}
);
return {
title: result.title,
image: result.poster,
description: result.description,
genres: result.genres,
};
}
/* ================= EPISODES ================= */
async findEpisodes(animeId) {
// MissAV es 1 video = 1 episodio
return [
{
id: animeId,
number: 1,
title: "Video",
url: animeId.startsWith("http")
? animeId
: this.baseUrl + animeId,
},
];
}
/* ================= SERVERS ================= */
async findEpisodeServer(episode) {
const url = episode.url.startsWith("http")
? episode.url
: this.baseUrl + episode.url;
const { requests } = await this.scrape(
url,
async () => true,
{
waitUntil: "domcontentloaded",
timeout: 20000,
renderWaitTime: 1500,
}
);
const m3u8s = requests
.map(r => r.url)
.filter(u => u.includes(".m3u8"));
if (!m3u8s.length) throw new Error("No m3u8 in network");
// regla:
// - si existe .../playlist.m3u8 -> master
// - si no -> usar video.m3u8 (o el único que haya)
let finalUrl =
m3u8s.find(u => /\/playlist\.m3u8(\?|$)/.test(u)) ||
m3u8s.find(u => /\/video\.m3u8(\?|$)/.test(u)) ||
m3u8s[0];
return {
server: "Default",
videoSources: [
{
url: finalUrl,
type: "m3u8",
quality: "auto",
},
],
};
}
/* ================= UTILS ================= */
safeString(str) {
return typeof str === "string" ? str : "";
}
}
module.exports = MissAV;