+ // Buscamos texto "Estado:" para determinar el estado de la novela
+ const fullText = content.text();
+
+ // Estado
+ const estadoMatch = fullText.match(
+ /Estado\s*:\s*(Activa|Finaliz[ao]da|Hiatus|En\s*proceso)/i,
+ );
+ if (estadoMatch) {
+ const estado = estadoMatch[1].toLowerCase();
+ if (estado.includes('finaliz')) {
+ novel.status = NovelStatus.Completed;
+ } else if (estado === 'activa' || estado.includes('proceso')) {
+ novel.status = NovelStatus.Ongoing;
+ } else if (estado === 'hiatus') {
+ novel.status = NovelStatus.OnHiatus;
+ }
+ }
+
+ // Sinopsis: buscamos el párrafo después de "Sinopsis:"
+ const sinopsisMatch = fullText.match(
+ /Sinopsis\s*:([\s\S]*?)(?=\||\n\n\n|$)/i,
+ );
+ if (sinopsisMatch) {
+ novel.summary = sinopsisMatch[1]
+ .replace(/\s+/g, ' ')
+ .trim()
+ .substring(0, 2000); // limitar largo
+ }
+
+ // Géneros: las categorías del post están en el header del artículo
+ // Ejemplo: "/ Acción, Aventura, Comedia, Drama, Fantasía, Romántico /"
+ const categories: string[] = [];
+ $('a[rel="category tag"], .cat-links a').each((_i, el) => {
+ categories.push($(el).text().trim());
+ });
+ if (categories.length > 0) {
+ novel.genres = categories.join(', ');
+ }
+
+ // Autor/Traductor: el sitio muestra el autor del post (traductor)
+ const authorEl = $('a[rel="author"], .author a').first();
+ if (authorEl.length) {
+ novel.author = 'Trad: ' + authorEl.text().trim();
+ }
+
+ // ---- Parsear capítulos ----
+ const chapters: Plugin.ChapterItem[] = [];
+
+ /**
+ * Los capítulos están en una tabla HTML dentro del entry-content.
+ * Cada celda | puede contener:
+ * - Un con el nombre del capítulo (capítulo disponible)
+ * - Solo texto sin link (capítulo no publicado aún)
+ *
+ * Ejemplo:
+ * | 01 | 02 | 03 |
+ * | 04 | 05 | 06 |
+ */
+ content.find('table td a').each((i, el) => {
+ const chapterHref = $(el).attr('href') || '';
+ const chapterName = $(el).text().trim();
+
+ if (!chapterHref || !chapterName) return;
+
+ // Solo capítulos del mismo dominio
+ if (
+ !chapterHref.startsWith('https://imreadingabook.com') &&
+ !chapterHref.startsWith('https://www.imreadingabook.com')
+ ) {
+ return;
+ }
+
+ let chapterPath: string;
+ try {
+ chapterPath = new URL(chapterHref).pathname;
+ } catch {
+ return;
+ }
+
+ // Intentar extraer número de capítulo del texto
+ const numMatch = chapterName.match(/\d+/);
+ const chapterNumber = numMatch ? parseInt(numMatch[0], 10) : i + 1;
+
+ chapters.push({
+ name: chapterName,
+ path: chapterPath,
+ releaseTime: null,
+ chapterNumber,
+ });
+ });
+
+ // Ordenar por número de capítulo (ascendente)
+ chapters.sort((a, b) => (a.chapterNumber || 0) - (b.chapterNumber || 0));
+
+ novel.chapters = chapters;
+ return novel;
+ }
+
+ /**
+ * parseChapter: obtiene el texto de un capítulo.
+ *
+ * Los capítulos son posts de WordPress. El contenido está en .entry-content.
+ * Eliminamos: imágenes de portada al inicio, botones de navegación,
+ * widgets de likes, y cualquier script/iframe.
+ */
+ async parseChapter(chapterPath: string): Promise {
+ const url = this.site + chapterPath;
+ const result = await fetchApi(url);
+ const body = await result.text();
+ const $ = loadCheerio(body);
+
+ const content = $('.entry-content');
+
+ // Eliminar elementos no deseados dentro del contenido
+ content
+ .find('script, style, iframe, .sharedaddy, .jp-relatedposts')
+ .remove();
+ content.find('.wp-block-image, figure').first().remove(); // portada al inicio
+
+ // Limpiar enlaces internos de navegación (← Entrada anterior / siguiente →)
+ // Estos están fuera del entry-content normalmente, pero por si acaso:
+ $(
+ 'a:contains("Entrada anterior"), a:contains("Entrada siguiente")',
+ ).remove();
+
+ // Obtener el HTML limpio
+ const chapterText = content.html() || '';
+
+ return chapterText;
+ }
+
+ /**
+ * searchNovels: búsqueda usando el motor de WordPress.
+ *
+ * URL: https://www.imreadingabook.com/?s=término&page=N
+ *
+ * Los resultados son posts (capítulos y entradas de blog mezclados),
+ * así que filtramos para intentar devolver solo páginas de novelas.
+ * Esto es una limitación del sitio ya que no tiene búsqueda separada por tipo.
+ */
+ async searchNovels(
+ searchTerm: string,
+ pageNo: number,
+ ): Promise {
+ const encodedTerm = encodeURIComponent(searchTerm);
+ const url =
+ pageNo > 1
+ ? `${this.site}/page/${pageNo}/?s=${encodedTerm}`
+ : `${this.site}/?s=${encodedTerm}`;
+
+ const result = await fetchApi(url);
+ const body = await result.text();
+ const $ = loadCheerio(body);
+
+ const novels: Plugin.NovelItem[] = [];
+
+ $('article').each((_i, el) => {
+ const titleEl = $(el).find('h2 a, h1 a').first();
+ const name = titleEl.text().trim();
+ const href = titleEl.attr('href') || '';
+ const cover =
+ $(el).find('img').first().attr('src') ||
+ $(el).find('img').first().attr('data-src') ||
+ defaultCover;
+
+ if (!name || !href) return;
+
+ let path: string;
+ try {
+ path = new URL(href).pathname;
+ } catch {
+ return;
+ }
+
+ novels.push({ name, path, cover });
+ });
+
+ return novels;
+ }
+
+ /**
+ * resolveUrl: construye la URL completa desde un path relativo.
+ * LNReader la usa internamente en algunos contextos.
+ */
+ resolveUrl = (path: string, _isNovel?: boolean) => this.site + path;
+}
+
+export default new ImReadingABookPlugin();
diff --git a/public/static/src/es/imreadingabook/icon.png b/public/static/src/es/imreadingabook/icon.png
new file mode 100644
index 000000000..814c5d0d5
Binary files /dev/null and b/public/static/src/es/imreadingabook/icon.png differ
|