267 lines
8.5 KiB
JavaScript
267 lines
8.5 KiB
JavaScript
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;
|