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;