Files
WaifuBoard-Extensions/anime/rouvideo.js
2026-01-13 17:26:06 +01:00

290 lines
9.7 KiB
JavaScript

class RouVideo {
constructor() {
this.baseUrl = "https://rou.video";
this.apiUrl = "https://rou.video/api";
this.type = "anime-board";
this.version = "1.0";
}
getFilters() {
return {
sort: {
label: "Ordenar por",
type: "select",
options: [
{ value: "createdAt", label: "Recent" },
{ value: "viewCount", label: "Most viewed" },
{ value: "likeCount", label: "Most liked" }
],
default: "createdAt"
},
category: {
label: "Categoría",
type: "select",
options: [
{ value: "all", label: "Todos los videos" },
{ value: "featured", label: "Destacados" },
{ value: "watching", label: "Viendo ahora" },
{ value: "國產AV", label: "Chinese AV" },
{ value: "中文字幕", label: "Chinese Sub" },
{ value: "麻豆傳媒", label: "Madou Media" },
{ value: "自拍流出", label: "Selfie Leaked" },
{ value: "探花", label: "Tanhua" },
{ value: "OnlyFans", label: "OnlyFans" },
{ value: "日本", label: "JAV" }
],
default: "all"
}
};
}
getSettings() {
return {
episodeServers: ["RouVideo"],
supportsDub: false,
};
}
async search(queryObj) {
const { query, filters, page } = queryObj;
const pageNum = page || 1;
const sort = filters?.sort || "createdAt";
const category = filters?.category || "all";
let url;
if (query && query.trim().length > 0) {
url = `${this.baseUrl}/search?q=${encodeURIComponent(query.trim())}&page=${pageNum}`;
if (category !== "all" && category !== "featured" && category !== "watching") {
url += `&t=${encodeURIComponent(category)}`;
}
} else {
if (category === "watching") {
url = `${this.apiUrl}/v/watching`;
} else if (category === "featured") {
url = `${this.baseUrl}/home`;
} else if (category !== "all") {
url = `${this.baseUrl}/t/${encodeURIComponent(category)}?page=${pageNum}&order=${sort}`;
} else {
url = `${this.baseUrl}/v?page=${pageNum}&order=${sort}`;
}
}
try {
if (category === "watching" && !query) {
const response = await this.requestApi(url);
const json = JSON.parse(response);
return json.map(this.parseVideoItem);
}
const response = await this.request(url);
const $ = this.cheerio.load(response);
const nextData = this.extractNextData($);
if (!nextData || !nextData.props || !nextData.props.pageProps) {
return [];
}
const props = nextData.props.pageProps;
let videos = [];
if (props.videos) {
videos = props.videos;
} else if (props.hotSearches && query) {
videos = props.videos || [];
} else if (category === "featured" || url.includes("/home")) {
videos = [
...(props.latestVideos || []),
...(props.hotCNAV || []),
...(props.hot91 || []),
...(props.hotSelfie || [])
];
}
return videos.map(this.parseVideoItem);
} catch (error) {
console.error("Error en search:", error);
return [];
}
}
async getMetadata(id) {
try {
const url = `${this.baseUrl}/v/${id}`;
const response = await this.request(url);
const $ = this.cheerio.load(response);
const nextData = this.extractNextData($);
if (!nextData) return { id, title: "Unknown" };
const video = nextData.props.pageProps.video;
if (!video) return { id, title: "Unknown" };
let descText = "";
if (video.sources && video.sources.length > 0) {
descText += `Resolution: ${video.sources[0].resolution}p\n`;
}
descText += `Duration: ${this.formatDuration(video.duration)}\n`;
descText += `View: ${video.viewCount}`;
if (video.likeCount) descText += ` - Like: ${video.likeCount}`;
if (video.ref) descText += `\nRef: ${video.ref}`;
if (video.description) descText += `\n\n${video.description}`;
return {
id: video.id,
title: video.name,
cover: video.coverImageUrl,
description: descText,
genres: video.tags || [],
author: video.tags?.[0] || "",
status: "Completed",
url: url
};
} catch (error) {
console.error("Error en getMetadata:", error);
return {};
}
}
async findEpisodes(id) {
try {
return [{
id: id,
number: 1,
title: "Movie"
}];
} catch (error) {
return [];
}
}
async findEpisodeServer(episodeInput, server, category = "sub") {
let cleanId = "";
if (typeof episodeInput === 'object' && episodeInput !== null) {
cleanId = episodeInput.id;
} else {
cleanId = episodeInput;
}
if (String(cleanId).includes('/')) {
cleanId = String(cleanId).split('/').pop();
}
console.log(`[RouVideo] Buscando servidor para ID: ${cleanId}`);
const apiUrl = `${this.apiUrl}/v/${cleanId}`;
try {
const req = await fetch(apiUrl, {
method: 'GET',
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept': 'application/json, text/plain, */*',
'Host': 'rou.video',
'Origin': 'https://rou.video',
'Referer': 'https://rou.video/'
}
});
if (!req.ok) {
console.error(`[RouVideo] Error HTTP: ${req.status}`);
return { videoSources: [] };
}
const text = await req.text();
if (text.trim().startsWith("<")) {
console.error("[RouVideo] Error: La API devolvió HTML");
return { videoSources: [] };
}
const json = JSON.parse(text);
if (json?.video?.videoUrl) {
console.log("[RouVideo] Video URL encontrado:", json.video.videoUrl);
// Headers necesarios para reproducir el stream
const streamHeaders = {
"Referer": "https://rou.video/",
"Origin": "https://rou.video",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
};
return {
headers: streamHeaders,
videoSources: [{
server: "RouVideo",
url: json.video.videoUrl,
type: "m3u8",
quality: "Auto",
headers: streamHeaders
}]
};
} else {
console.warn("[RouVideo] JSON válido pero sin videoUrl");
return { videoSources: [] };
}
} catch (error) {
console.error("[RouVideo] Error fatal:", error);
return { videoSources: [] };
}
}
extractNextData($) {
try {
const scriptContent = $('#__NEXT_DATA__').html();
if (scriptContent) {
return JSON.parse(scriptContent);
}
} catch (e) {
console.error("Error parsing __NEXT_DATA__", e);
}
return null;
}
parseVideoItem(video) {
return {
id: video.id,
title: video.name,
image: video.coverImageUrl,
};
}
formatDuration(seconds) {
if (!seconds) return "0:00";
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
const mStr = m < 10 && h > 0 ? `0${m}` : m;
const sStr = s < 10 ? `0${s}` : s;
return h > 0 ? `${h}:${mStr}:${sStr}` : `${mStr}:${sStr}`;
}
async request(url) {
const req = await fetch(url, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Referer': `${this.baseUrl}/`,
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8'
}
});
return await req.text();
}
async requestApi(url) {
const req = await fetch(url, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Referer': `${this.baseUrl}/`,
'Origin': this.baseUrl,
'Accept': 'application/json, text/plain, */*'
}
});
return await req.text();
}
}
module.exports = RouVideo;