updates and new extensions
This commit is contained in:
290
anime/rouvideo.js
Normal file
290
anime/rouvideo.js
Normal file
@@ -0,0 +1,290 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user