updates and new extensions
This commit is contained in:
266
anime/missav.js
Normal file
266
anime/missav.js
Normal 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;
|
||||
Reference in New Issue
Block a user