290 lines
9.7 KiB
JavaScript
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; |