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

267 lines
9.7 KiB
JavaScript

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 += `<h1>${title}</h1><br>`;
container.contents().each((_, el) => {
if (el.tagName === 'p') {
const text = $(el).text().trim();
if (text) content += `<p>${text}</p>`;
} else if (el.tagName === 'amp-img') {
const src = $(el).attr('src');
if (src) content += `<img src="${src}" style="max-width:100%; display:block; margin: 10px auto;">`;
}
});
return content;
}
}
module.exports = Wattpad;