class Wattpad { constructor() { this.baseUrl = "https://www.wattpad.com"; this.apiUrl = "https://www.wattpad.com/v4"; this.type = "book-board"; this.mediaType = "ln"; // Light Novel this.version = "1.1"; } getFilters() { return { sort: { label: "Sort By (Only for Explore)", type: "select", options: [ { value: "hot", label: "Hot (Trending)" }, { value: "new", label: "New (Latest)" }, { value: "paid", label: "Paid Stories" } ], default: "hot" }, updated: { label: "Last Updated (Search Only)", type: "select", options: [ { value: "", label: "Any time" }, { value: "24", label: "Today" }, { value: "168", label: "This Week" }, { value: "720", label: "This Month" }, { value: "8760", label: "This Year" } ], default: "" }, content: { label: "Content Filters", type: "multiselect", options: [ { value: "completed", label: "Completed stories only" }, { value: "paid", label: "Paid stories only (Hide free)" }, { value: "free", label: "Free stories only (Hide Paid)" }, { value: "mature", label: "Include mature content" } ] }, tags: { label: "Tags (comma separated)", type: "text", placeholder: "e.g. romance, vampire, magic" } }; } get headers() { return { "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-Language": "en-US,en;q=0.9", "Referer": "https://www.wattpad.com/" }; } async search({ query, page = 1, filters }) { const limit = 15; const offset = (page - 1) * limit; // 1. Preparar la Query (Texto + Etiquetas) let finalQuery = (query || "").trim(); if (filters?.tags) { const tags = String(filters.tags).split(","); tags.forEach(t => { const tag = t.trim(); if (tag) { // Wattpad requiere que los tags en la búsqueda lleven # finalQuery += ` #${tag.replace(/^#/, '')}`; } }); } finalQuery = finalQuery.trim(); let url; // 2. DECISIÓN: ¿Búsqueda o Exploración? if (finalQuery) { // CASO A: HAY BÚSQUEDA (Texto o Tags) -> Usar API de Search url = new URL(`${this.apiUrl}/search/stories/`); url.searchParams.set("query", finalQuery); // Filtros exclusivos de búsqueda if (filters?.updated) { url.searchParams.set("updateYoungerThan", filters.updated); } } else { // CASO B: NO HAY BÚSQUEDA (Default) -> Usar API de Stories (Explorar) // Esto cargará "Tendencias" o "Nuevos" cuando entres sin escribir nada. url = new URL(`${this.apiUrl}/stories/`); // Mapear el filtro 'sort' al parámetro 'filter' de la API // Por defecto usamos 'hot' (Tendencias) const filterVal = filters?.sort || "hot"; url.searchParams.set("filter", filterVal); } // 3. Filtros Comunes (Content) let isMature = false; if (filters?.content) { const contentOpts = Array.isArray(filters.content) ? filters.content : String(filters.content).split(','); if (contentOpts.includes("completed")) url.searchParams.set("completed", "1"); if (contentOpts.includes("paid")) url.searchParams.set("paid", "1"); if (contentOpts.includes("free")) url.searchParams.set("paid", "0"); if (contentOpts.includes("mature")) isMature = true; } // Wattpad requiere mature=1 explícito para mostrar contenido adulto url.searchParams.set("mature", isMature ? "1" : "0"); // 4. Parámetros Técnicos url.searchParams.set("limit", limit.toString()); url.searchParams.set("offset", offset.toString()); // Solicitar campos específicos para obtener portadas, autor y estado const fields = "stories(id,title,voteCount,readCount,commentCount,description,mature,completed,cover,url,numParts,isPaywalled,paidModel,length,language(id),user(name),lastPublishedPart(createDate),promoted,sponsor(name,avatar),tags),total,nextUrl"; url.searchParams.set("fields", fields); try { const res = await fetch(url.toString(), { headers: this.headers }); if (!res.ok) { console.error(`Wattpad API Error: ${res.status}`); return []; } const json = await res.json(); if (!json.stories) return []; return json.stories.map(story => ({ id: story.id, title: story.title, image: story.cover, type: "book", // Datos extra para la UI author: story.user?.name, status: story.completed ? "Completed" : "Ongoing", chapters: story.numParts, rating: story.voteCount // Opcional: usar votos como rating })); } catch (e) { console.error("Wattpad search failed:", e); return []; } } async getMetadata(id) { // Obtenemos metadatos básicos de la web para no depender solo de la API const res = await fetch(`${this.baseUrl}/story/${id}`, { headers: this.headers }); const html = await res.text(); const $ = this.cheerio.load(html); // Intentar extraer datos del script de hidratación (más fiable) const script = $('script') .map((_, el) => $(el).html()) .get() .find(t => t?.includes('window.__remixContext')); if (script) { const jsonText = script.match(/window\.__remixContext\s*=\s*({[\s\S]*?});/)?.[1]; if (jsonText) { try { const ctx = JSON.parse(jsonText); const route = ctx?.state?.loaderData?.["routes/story.$storyid"]; const story = route?.story; if (story) { return { id: story.id, title: story.title, format: "Novel", score: story.voteCount ?? 0, genres: story.tags || [], status: story.completed ? "Completed" : "Ongoing", published: story.createDate ? story.createDate.split("T")[0] : "???", summary: story.description || "", chapters: story.numParts || 0, image: story.cover || "", author: story.user?.name || "" }; } } catch (e) {} } } // Fallback clásico const title = $('h1').first().text().trim(); const image = $('.story-cover img').attr('src'); const summary = $('.description').text().trim(); return { id, title: title || "Unknown", format: "Novel", image: image || "", summary: summary || "", chapters: 0 }; } async findChapters(bookId) { const res = await fetch(`${this.baseUrl}/story/${bookId}`, { headers: this.headers }); const html = await res.text(); // Extraer estructura de capítulos del JSON const match = html.match(/window\.__remixContext\s*=\s*({[\s\S]*?});/); if (!match?.[1]) return []; try { const ctx = JSON.parse(match[1]); const story = ctx?.state?.loaderData?.["routes/story.$storyid"]?.story; if (!story?.parts) return []; return story.parts.map((part, i) => ({ id: String(part.id), title: part.title || `Chapter ${i + 1}`, number: i + 1, index: i, url: part.url })); } catch { return []; } } async findChapterPages(chapterId) { // Usamos la versión AMP para obtener el contenido limpio en una sola petición const url = `${this.baseUrl}/amp/${chapterId}`; const res = await fetch(url, { headers: this.headers }); const html = await res.text(); const $ = this.cheerio.load(html); const title = $('h2').first().text().trim(); const container = $('.story-body-type'); if (!container.length) return "Content not available or paid story."; // Limpieza de elementos basura container.find('[data-media-type="image"]').remove(); let content = ""; if (title) content += `
${text}
`; } else if (el.tagName === 'amp-img') { const src = $(el).attr('src'); if (src) content += `