From 47169a5f66944ba1f8b240daf0904e1c995207e3 Mon Sep 17 00:00:00 2001 From: lenafx Date: Tue, 2 Dec 2025 18:21:41 +0100 Subject: [PATCH] caching system + add extension entries to metadata pool --- README.md | 22 +-- server.js | 1 + src/anime/anime.controller.ts | 33 ++-- src/anime/anime.routes.ts | 1 + src/anime/anime.service.ts | 131 ++++++++++--- src/books/books.controller.ts | 31 ++- src/books/books.routes.ts | 1 + src/books/books.service.ts | 195 ++++++++++++------- src/scripts/anime/anime.js | 140 +++++++------- src/scripts/anime/animes.js | 76 +++++--- src/scripts/anime/player.js | 343 +++++++++++++++++----------------- src/scripts/books/book.js | 151 +++++++++------ src/scripts/books/books.js | 99 ++++++---- src/scripts/books/reader.js | 2 +- src/shared/database.js | 134 ++++++++++++- src/shared/extensions.js | 24 ++- src/types.ts | 3 + views/css/books/books.css | 22 +++ 18 files changed, 924 insertions(+), 485 deletions(-) diff --git a/README.md b/README.md index 76c28f9..1257596 100644 --- a/README.md +++ b/README.md @@ -3,21 +3,21 @@ The official recode repo, its private no one should know about this or have the # Things to get done: -| Task | Status | Notes | -| -----|------| ------ | -| Book Reader | ✅ | N/A | -| Multi book provider loading | ✅ | N/A | -| Better Code Organization | ✅ | N/A | +| Task | Status | Notes | +| -----|----------| ------ | +| Book Reader | ✅ | N/A | +| Multi book provider loading | ✅ | N/A | +| Better Code Organization | ✅ | N/A | | Mobile View | Not Done | N/A | -| Gallery | Not Done | N/A | -| Anime Schedule (Release Calendar for the week/month) | ✅ | N/A | +| Gallery | ✅ | N/A | +| Anime Schedule (Release Calendar for the week/month) | ✅ | N/A | | My List (Tracking) | Not Done | Persistent data would be in a data.db file in waifuboard directory | | Marketplace | Not Done | Uses the gitea repo | | Add to list / library | Not Done | Persistent data would be in data.db file in waifuboard directory| -| Gallery favorites | Not Done | Persistent in data.db like how it was previously | -| Change "StreamFlow" to "WaifuBoard" | ✅ | N/A | -| Change the cube icon next to "StreamFlow" to the current ico file | ✅ | Use the ico file from the current waifuboard ver | -| Favicon | ✅ | Use the ico file from the current waifuboard ver | +| Gallery favorites | ✅ | Persistent in data.db like how it was previously | +| Change "StreamFlow" to "WaifuBoard" | ✅ | N/A | +| Change the cube icon next to "StreamFlow" to the current ico file | ✅ | Use the ico file from the current waifuboard ver | +| Favicon | ✅ | Use the ico file from the current waifuboard ver | | Make video player more professional looking | Not Done | N/A | ***This stuff will get updated as time goes on, please check things off with a green checkmark as you complete them.*** \ No newline at end of file diff --git a/server.js b/server.js index 11f1933..e0f86b0 100644 --- a/server.js +++ b/server.js @@ -77,6 +77,7 @@ const start = async () => { try { initDatabase("anilist"); initDatabase("favorites"); + initDatabase("cache"); await loadExtensions(); await fastify.listen({ port: 3000, host: '0.0.0.0' }); diff --git a/src/anime/anime.controller.ts b/src/anime/anime.controller.ts index eec2193..f40aa8f 100644 --- a/src/anime/anime.controller.ts +++ b/src/anime/anime.controller.ts @@ -12,15 +12,8 @@ export async function getAnime(req: AnimeRequest, reply: FastifyReply) { if (source === 'anilist') { anime = await animeService.getAnimeById(id); } else { - const extensionName = source; - const ext = getExtension(extensionName); - - const results = await animeService.searchAnimeInExtension( - ext, - extensionName, - id.replace(/--/g, '\u0000').replace(/-/g, ' ').replace(new RegExp('\u0000', 'g'), '-').trim() - ); - anime = results[0] || null; + const ext = getExtension(source); + anime = await animeService.getAnimeInfoExtension(ext, id) } return anime; @@ -35,10 +28,10 @@ export async function getAnimeEpisodes(req: AnimeRequest, reply: FastifyReply) { const extensionName = req.query.ext || 'anilist'; const ext = getExtension(extensionName); - return await animeService.searchChaptersInExtension( + return await animeService.searchEpisodesInExtension( ext, extensionName, - id.replace(/--/g, '\u0000').replace(/-/g, ' ').replace(new RegExp('\u0000', 'g'), '-').trim() + id ); } catch (err) { return { error: "Database error" }; @@ -72,14 +65,26 @@ export async function search(req: SearchRequest, reply: FastifyReply) { return { results: results }; } - const extResults = await animeService.searchAnimeExtensions(query); - return { results: extResults }; - } catch (err) { return { results: [] }; } } +export async function searchInExtension(req: any, reply: FastifyReply) { + try { + const extensionName = req.params.extension; + const query = req.query.q; + + const ext = getExtension(extensionName); + if (!ext) return { results: [] }; + + const results = await animeService.searchAnimeInExtension(ext, extensionName, query); + return { results }; + } catch { + return { results: [] }; + } +} + export async function getWatchStream(req: WatchStreamRequest, reply: FastifyReply) { try { const { animeId, episode, server, category, ext } = req.query; diff --git a/src/anime/anime.routes.ts b/src/anime/anime.routes.ts index e9da5af..f23976d 100644 --- a/src/anime/anime.routes.ts +++ b/src/anime/anime.routes.ts @@ -7,6 +7,7 @@ async function animeRoutes(fastify: FastifyInstance) { fastify.get('/trending', controller.getTrending); fastify.get('/top-airing', controller.getTopAiring); fastify.get('/search', controller.search); + fastify.get('/search/:extension', controller.searchInExtension); fastify.get('/watch/stream', controller.getWatchStream); } diff --git a/src/anime/anime.service.ts b/src/anime/anime.service.ts index 07c8906..123be04 100644 --- a/src/anime/anime.service.ts +++ b/src/anime/anime.service.ts @@ -1,7 +1,8 @@ -import { queryOne, queryAll } from '../shared/database'; -import { getAnimeExtensionsMap } from '../shared/extensions'; +import {queryOne, queryAll, getCache, setCache, getCachedExtension, cacheExtension, getExtensionTitle } from '../shared/database'; import {Anime, Episode, Extension, StreamData} from '../types'; +const CACHE_TTL_MS = 24 * 60 * 60 * 1000; + export async function getAnimeById(id: string | number): Promise { const row = await queryOne("SELECT full_data FROM anime WHERE id = ?", [id]); @@ -47,6 +48,38 @@ export async function searchAnimeLocal(query: string): Promise { return cleanResults.slice(0, 10); } +export async function getAnimeInfoExtension(ext: Extension | null, id: string): Promise { + if (!ext) return { error: "not found" }; + + const extName = ext.constructor.name; + + const cached = await getCachedExtension(extName, id); + if (cached) { + try { + console.log(`[${extName}] Metadata cache hit for ID: ${id}`); + return JSON.parse(cached.metadata) as Anime; + } catch { + + } + } + + if ((ext.type === 'anime-board') && ext.getMetadata) { + try { + const match = await ext.getMetadata(id); + + if (match) { + + await cacheExtension(extName, id, match.title, match); + return match; + } + } catch (e) { + console.error(`Extension getMetadata failed:`, e); + } + } + + return { error: "not found" }; +} + export async function searchAnimeInExtension(ext: Extension | null, name: string, query: string): Promise { if (!ext) return []; @@ -71,7 +104,7 @@ export async function searchAnimeInExtension(ext: Extension | null, name: string averageScore: m.rating || m.score || null, format: 'ANIME', seasonYear: null, - isExtensionResult: true + isExtensionResult: true, })); } } catch (e) { @@ -82,35 +115,70 @@ export async function searchAnimeInExtension(ext: Extension | null, name: string return []; } -export async function searchChaptersInExtension(ext: Extension | null, name: string, query: string): Promise { +export async function searchEpisodesInExtension(ext: Extension | null, name: string, query: string): Promise { if (!ext) return []; + const cacheKey = `anime:episodes:${name}:${query}`; + const cached = await getCache(cacheKey); + + if (cached) { + const isExpired = Date.now() - cached.created_at > CACHE_TTL_MS; + + if (!isExpired) { + console.log(`[${name}] Episodes cache hit for: ${query}`); + try { + return JSON.parse(cached.result) as Episode[]; + } catch (e) { + console.error(`[${name}] Error parsing cached episodes:`, e); + } + } else { + console.log(`[${name}] Episodes cache expired for: ${query}`); + } + } + if (ext.type === "anime-board" && ext.search && typeof ext.findEpisodes === "function") { try { + const title = await getExtensionTitle(name, query); + let mediaId: string; - const matches = await ext.search({ - query, - media: { - romajiTitle: query, - englishTitle: query, - startDate: { year: 0, month: 0, day: 0 } - } - }); + if (title) { - if (!matches || matches.length === 0) return []; + const matches = await ext.search({ + query, + media: { + romajiTitle: query, + englishTitle: query, + startDate: { year: 0, month: 0, day: 0 } + } + }); - const res = matches[0]; - if (!res?.id) return []; + if (!matches || matches.length === 0) return []; + + const res = matches[0]; + if (!res?.id) return []; + + mediaId = res.id; + + } else { + + mediaId = query; + } + + const chapterList = await ext.findEpisodes(mediaId); - const chapterList = await ext.findEpisodes(res.id); if (!Array.isArray(chapterList)) return []; - return chapterList.map(ep => ({ + const result: Episode[] = chapterList.map(ep => ({ id: ep.id, number: ep.number, url: ep.url, title: ep.title })); + + await setCache(cacheKey, result, CACHE_TTL_MS); + + return result; + } catch (e) { console.error(`Extension search failed for ${name}:`, e); } @@ -119,18 +187,28 @@ export async function searchChaptersInExtension(ext: Extension | null, name: str return []; } -export async function searchAnimeExtensions(query: string): Promise { - const animeExtensions = getAnimeExtensionsMap(); +export async function getStreamData(extension: Extension, animeData: Anime, episode: string, server?: string, category?: string): Promise { + const providerName = extension.constructor.name; - for (const [name, ext] of animeExtensions) { - const results = await searchAnimeInExtension(ext, name, query); - if (results.length > 0) return results; + const cacheKey = `anime:stream:${providerName}:${animeData.id}:${episode}:${server || 'default'}:${category || 'sub'}`; + + const cached = await getCache(cacheKey); + + if (cached) { + const isExpired = Date.now() - cached.created_at > CACHE_TTL_MS; + + if (!isExpired) { + console.log(`[${providerName}] Stream data cache hit for episode ${episode}`); + try { + return JSON.parse(cached.result) as StreamData; + } catch (e) { + console.error(`[${providerName}] Error parsing cached stream data:`, e); + } + } else { + console.log(`[${providerName}] Stream data cache expired for episode ${episode}`); + } } - return []; -} - -export async function getStreamData(extension: Extension, animeData: Anime, episode: string, server?: string, category?: string): Promise { const searchOptions = { query: animeData.title.english || animeData.title.romaji, dub: category === 'dub', @@ -162,5 +240,6 @@ export async function getStreamData(extension: Extension, animeData: Anime, epis const serverName = server || "default"; const streamData = await extension.findEpisodeServer(targetEp, serverName); + await setCache(cacheKey, streamData, CACHE_TTL_MS); return streamData; } \ No newline at end of file diff --git a/src/books/books.controller.ts b/src/books/books.controller.ts index 8980e2c..3f6524e 100644 --- a/src/books/books.controller.ts +++ b/src/books/books.controller.ts @@ -1,7 +1,7 @@ import {FastifyReply, FastifyRequest} from 'fastify'; import * as booksService from './books.service'; -import { getExtension } from '../shared/extensions'; -import { BookRequest, SearchRequest, ChapterRequest } from '../types'; +import {getExtension} from '../shared/extensions'; +import {BookRequest, ChapterRequest, SearchRequest} from '../types'; export async function getBook(req: BookRequest, reply: FastifyReply) { try { @@ -12,11 +12,10 @@ export async function getBook(req: BookRequest, reply: FastifyReply) { if (source === 'anilist') { book = await booksService.getBookById(id); } else { - const extensionName = source; - const ext = getExtension(extensionName); + const ext = getExtension(source); - const results = await booksService.searchBooksInExtension(ext, extensionName, id.replace(/--/g, '\u0000').replace(/-/g, ' ').replace(new RegExp('\u0000', 'g'), '-').trim()); - book = results[0] || null; + const result = await booksService.getBookInfoExtension(ext, id); + book = result || null; } return book; @@ -60,8 +59,7 @@ export async function searchBooks(req: SearchRequest, reply: FastifyReply) { return { results: anilistResults }; } - const extResults = await booksService.searchBooksExtensions(query); - return { results: extResults }; + return { results: [] }; } catch (e) { const error = e as Error; @@ -70,6 +68,23 @@ export async function searchBooks(req: SearchRequest, reply: FastifyReply) { } } +export async function searchBooksInExtension(req: any, reply: FastifyReply) { + try { + const extensionName = req.params.extension; + const query = req.query.q; + + const ext = getExtension(extensionName); + if (!ext) return { results: [] }; + + const results = await booksService.searchBooksInExtension(ext, extensionName, query); + return { results }; + } catch (e) { + const error = e as Error; + console.error("Search Error:", error.message); + return { results: [] }; + } +} + export async function getChapters(req: BookRequest, reply: FastifyReply) { try { const { id } = req.params; diff --git a/src/books/books.routes.ts b/src/books/books.routes.ts index 6ab887c..8993d09 100644 --- a/src/books/books.routes.ts +++ b/src/books/books.routes.ts @@ -6,6 +6,7 @@ async function booksRoutes(fastify: FastifyInstance) { fastify.get('/books/trending', controller.getTrending); fastify.get('/books/popular', controller.getPopular); fastify.get('/search/books', controller.searchBooks); + fastify.get('/search/books/:extension', controller.searchBooksInExtension); fastify.get('/book/:id/chapters', controller.getChapters); fastify.get('/book/:bookId/:chapter/:provider', controller.getChapterContent); } diff --git a/src/books/books.service.ts b/src/books/books.service.ts index 55b6a30..cc67874 100644 --- a/src/books/books.service.ts +++ b/src/books/books.service.ts @@ -1,7 +1,9 @@ -import { queryOne, queryAll } from '../shared/database'; +import { queryOne, queryAll, getCachedExtension, cacheExtension, getCache, setCache, getExtensionTitle } from '../shared/database'; import { getAllExtensions, getBookExtensionsMap } from '../shared/extensions'; import { Book, Extension, ChapterWithProvider, ChapterContent } from '../types'; +const CACHE_TTL_MS = 24 * 60 * 60 * 1000; + export async function getBookById(id: string | number): Promise { const row = await queryOne("SELECT full_data FROM books WHERE id = ?", [id]); @@ -108,10 +110,39 @@ export async function searchBooksAniList(query: string): Promise { return []; } +export async function getBookInfoExtension(ext: Extension | null, id: string): Promise { + if (!ext) return []; + + const extName = ext.constructor.name; + + const cached = await getCachedExtension(extName, id); + if (cached) { + try { + return JSON.parse(cached.metadata); + } catch { + } + } + + if (ext.type === 'book-board' && ext.getMetadata) { + try { + const info = await ext.getMetadata(id); + + if (info) { + await cacheExtension(extName, id, info.title, info); + return info; + } + } catch (e) { + console.error(`Extension getInfo failed:`, e); + } + } + + return []; +} + export async function searchBooksInExtension(ext: Extension | null, name: string, query: string): Promise { if (!ext) return []; - if ((ext.type === 'book-board' || ext.type === 'manga-board') && ext.search) { + if ((ext.type === 'book-board') && ext.search) { try { console.log(`[${name}] Searching for book: ${query}`); const matches = await ext.search({ @@ -130,7 +161,7 @@ export async function searchBooksInExtension(ext: Extension | null, name: string title: { romaji: m.title, english: m.title, native: null }, coverImage: { large: m.image || '' }, averageScore: m.rating || m.score || null, - format: 'MANGA', + format: m.format, seasonYear: null, isExtensionResult: true })); @@ -143,17 +174,6 @@ export async function searchBooksInExtension(ext: Extension | null, name: string return []; } -export async function searchBooksExtensions(query: string): Promise { - const bookExtensions = getBookExtensionsMap(); - - for (const [name, ext] of bookExtensions) { - const results = await searchBooksInExtension(ext, name, query); - if (results.length > 0) return results; - } - - return []; -} - async function fetchBookMetadata(id: string): Promise { try { const query = `query ($id: Int) { @@ -177,44 +197,68 @@ async function fetchBookMetadata(id: string): Promise { } } -async function searchChaptersInExtension(ext: Extension, name: string, searchTitle: string, bookData: Book | null): Promise { +async function searchChaptersInExtension(ext: Extension, name: string, searchTitle: string, search: boolean): Promise { + const cacheKey = `chapters:${name}:${searchTitle}`; + const cached = await getCache(cacheKey); + + if (cached) { + const isExpired = Date.now() - cached.created_at > CACHE_TTL_MS; + + if (!isExpired) { + console.log(`[${name}] Chapters cache hit for: ${searchTitle}`); + try { + return JSON.parse(cached.result) as ChapterWithProvider[]; + } catch (e) { + console.error(`[${name}] Error parsing cached chapters:`, e); + } + } else { + console.log(`[${name}] Chapters cache expired for: ${searchTitle}`); + } + } + try { console.log(`[${name}] Searching chapters for: ${searchTitle}`); - const matches = await ext.search!({ - query: searchTitle, - media: bookData ? { - romajiTitle: bookData.title.romaji, - englishTitle: bookData.title.english || "", - startDate: bookData.startDate || { year: 0, month: 0, day: 0 } - } : { - romajiTitle: searchTitle, - englishTitle: searchTitle, - startDate: { year: 0, month: 0, day: 0 } - } - }); + let mediaId: string; + if (search) { + const matches = await ext.search!({ + query: searchTitle, + media: { + romajiTitle: searchTitle, + englishTitle: searchTitle, + startDate: { year: 0, month: 0, day: 0 } + } + }); - if (!matches?.length) { - console.log(`[${name}] No matches found for book.`); - return []; + const best = matches?.[0]; + + if (!best) { return [] } + + mediaId = best.id; + + } else { + const match = await ext.getMetadata(searchTitle); + mediaId = match.id; } - const best = matches[0]; - const chaps = await ext.findChapters!(best.id); + const chaps = await ext.findChapters!(mediaId); if (!chaps?.length) { return []; } console.log(`[${name}] Found ${chaps.length} chapters.`); - - return chaps.map((ch) => ({ + const result: ChapterWithProvider[] = chaps.map((ch) => ({ id: ch.id, number: parseFloat(ch.number.toString()), title: ch.title, date: ch.releaseDate, provider: name })); + + await setCache(cacheKey, result, CACHE_TTL_MS); + return result; + } catch (e) { const error = e as Error; console.error(`Failed to fetch chapters from ${name}:`, error.message); @@ -224,39 +268,40 @@ async function searchChaptersInExtension(ext: Extension, name: string, searchTit export async function getChaptersForBook(id: string): Promise<{ chapters: ChapterWithProvider[] }> { let bookData: Book | null = null; - let searchTitle: string | null = null; - - if (isNaN(Number(id))) { - searchTitle = id.replace(/--/g, '\u0000').replace(/-/g, ' ').replace(new RegExp('\u0000', 'g'), '-').trim(); - } else { - const result = await getBookById(id); - if (!('error' in result)) { - bookData = result; - } - - if (!bookData) { - bookData = await fetchBookMetadata(id); - } + let searchTitle: string = ""; + if (!isNaN(Number(id))) { + bookData = await fetchBookMetadata(id); if (!bookData) { return { chapters: [] }; } - const titles = [bookData.title.english, bookData.title.romaji].filter(Boolean) as string[]; searchTitle = titles[0]; } - - const allChapters: ChapterWithProvider[] = []; const bookExtensions = getBookExtensionsMap(); - const searchPromises = Array.from(bookExtensions.entries()) - .filter(([_, ext]) => ext.search && ext.findChapters) - .map(async ([name, ext]) => { - const chapters = await searchChaptersInExtension(ext, name, searchTitle!, bookData); - allChapters.push(...chapters); - }); + let extension; + if (!searchTitle) { + for (const [name, ext] of bookExtensions) { + const title = await getExtensionTitle(name, id) + if (title){ + searchTitle = title; + extension = name; + } + } + } - await Promise.all(searchPromises); + const allChapters: any[] = []; + + for (const [name, ext] of bookExtensions) { + if (name == extension) { + const chapters = await searchChaptersInExtension(ext, name, id, false); + allChapters.push(...chapters); + } else { + const chapters = await searchChaptersInExtension(ext, name, searchTitle, true); + allChapters.push(...chapters); + } + } return { chapters: allChapters.sort((a, b) => Number(a.number) - Number(b.number)) @@ -271,6 +316,25 @@ export async function getChapterContent(bookId: string, chapterIndex: string, pr throw new Error("Provider not found"); } + const contentCacheKey = `content:${providerName}:${bookId}:${chapterIndex}`; + const cachedContent = await getCache(contentCacheKey); + + if (cachedContent) { + const isExpired = Date.now() - cachedContent.created_at > CACHE_TTL_MS; + + if (!isExpired) { + console.log(`[${providerName}] Content cache hit for Book ID ${bookId}, Index ${chapterIndex}`); + try { + return JSON.parse(cachedContent.result) as ChapterContent; + } catch (e) { + console.error(`[${providerName}] Error parsing cached content:`, e); + + } + } else { + console.log(`[${providerName}] Content cache expired for Book ID ${bookId}, Index ${chapterIndex}`); + } + } + const chapterList = await getChaptersForBook(bookId); if (!chapterList?.chapters || chapterList.chapters.length === 0) { @@ -299,9 +363,11 @@ export async function getChapterContent(bookId: string, chapterIndex: string, pr throw new Error("Extension doesn't support findChapterPages"); } + let contentResult: ChapterContent; + if (ext.mediaType === "manga") { const pages = await ext.findChapterPages(chapterId); - return { + contentResult = { type: "manga", chapterId, title: chapterTitle, @@ -309,11 +375,9 @@ export async function getChapterContent(bookId: string, chapterIndex: string, pr provider: providerName, pages }; - } - - if (ext.mediaType === "ln") { + } else if (ext.mediaType === "ln") { const content = await ext.findChapterPages(chapterId); - return { + contentResult = { type: "ln", chapterId, title: chapterTitle, @@ -321,9 +385,14 @@ export async function getChapterContent(bookId: string, chapterIndex: string, pr provider: providerName, content }; + } else { + throw new Error("Unknown mediaType"); } - throw new Error("Unknown mediaType"); + await setCache(contentCacheKey, contentResult, CACHE_TTL_MS); + + return contentResult; + } catch (err) { const error = err as Error; console.error(`[Chapter] Error loading from ${providerName}:`, error.message); diff --git a/src/scripts/anime/anime.js b/src/scripts/anime/anime.js index ebc720c..5e13157 100644 --- a/src/scripts/anime/anime.js +++ b/src/scripts/anime/anime.js @@ -31,26 +31,44 @@ async function loadAnime() { const res = await fetch(fetchUrl); const data = await res.json(); - if(data.error) { + if (data.error) { document.getElementById('title').innerText = "Anime Not Found"; return; } - const title = data.title?.english || data.title?.romaji || "Unknown Title"; + const title = data.title?.english || data.title?.romaji || data.title || "Unknown Title"; document.title = `${title} | WaifuBoard`; document.getElementById('title').innerText = title; - if (data.coverImage?.extraLarge) { - document.getElementById('poster').src = data.coverImage.extraLarge; + + let posterUrl = ''; + + if (extensionName) { + posterUrl = data.image || ''; + + } else { + posterUrl = data.coverImage?.extraLarge || ''; } - const rawDesc = data.description || "No description available."; + if (posterUrl) { + document.getElementById('poster').src = posterUrl; + } + + const rawDesc = data.description || data.summary || "No description available."; handleDescription(rawDesc); - document.getElementById('score').innerText = (data.averageScore || '?') + '% Score'; - document.getElementById('year').innerText = data.seasonYear || data.startDate?.year || '????'; - document.getElementById('genres').innerText = data.genres?.length > 0 ? data.genres.slice(0, 3).join(' • ') : ''; + const score = extensionName ? (data.score ? data.score * 10 : '?') : data.averageScore; + document.getElementById('score').innerText = (score || '?') + '% Score'; + + document.getElementById('year').innerText = + extensionName ? (data.year || '????') : (data.seasonYear || data.startDate?.year || '????'); + + document.getElementById('genres').innerText = + data.genres?.length > 0 ? data.genres.slice(0, 3).join(' • ') : ''; + document.getElementById('format').innerText = data.format || 'TV'; + document.getElementById('status').innerText = data.status || 'Unknown'; + const extensionPill = document.getElementById('extension-pill'); if (extensionName && extensionPill) { extensionPill.textContent = `${extensionName.charAt(0).toUpperCase() + extensionName.slice(1).toLowerCase()}`; @@ -60,45 +78,53 @@ async function loadAnime() { } let seasonText = ''; - if (data.season && data.seasonYear) { - seasonText = `${data.season} ${data.seasonYear}`; - } else if (data.startDate?.year) { - const months = ['', 'Winter', 'Winter', 'Spring', 'Spring', 'Spring', 'Summer', 'Summer', 'Summer', 'Fall', 'Fall', 'Fall', 'Winter']; - const month = data.startDate.month || 1; - const estimatedSeason = months[month] || ''; - seasonText = `${estimatedSeason} ${data.startDate.year}`.trim(); + if (extensionName) { + seasonText = data.season || 'Unknown'; + } else { + if (data.season && data.seasonYear) { + seasonText = `${data.season} ${data.seasonYear}`; + } else if (data.startDate?.year) { + const months = ['', 'Winter', 'Winter', 'Spring', 'Spring', 'Spring', 'Summer', 'Summer', 'Summer', 'Fall', 'Fall', 'Fall', 'Winter']; + const month = data.startDate.month || 1; + const estimatedSeason = months[month] || ''; + seasonText = `${estimatedSeason} ${data.startDate.year}`.trim(); + } } document.getElementById('season').innerText = seasonText || 'Unknown'; - let studioName = 'Unknown Studio'; - if (data.studios?.nodes?.length > 0) { - studioName = data.studios.nodes[0].name; - } else if (data.studios?.edges?.length > 0) { - studioName = data.studios.edges[0]?.node?.name || 'Unknown Studio'; - } - document.getElementById('studio').innerText = studioName; + const studio = extensionName + ? data.studio || "Unknown" + : (data.studios?.nodes?.[0]?.name || + data.studios?.edges?.[0]?.node?.name || + 'Unknown Studio'); + + document.getElementById('studio').innerText = studio; const charContainer = document.getElementById('char-list'); charContainer.innerHTML = ''; let characters = []; - if (data.characters?.nodes?.length > 0) { - characters = data.characters.nodes.slice(0, 5); - } - else if (data.characters?.edges?.length > 0) { - characters = data.characters.edges - .filter(edge => edge?.node?.name?.full) - .slice(0, 5) - .map(edge => edge.node); + if (extensionName) { + characters = data.characters || []; + } else { + if (data.characters?.nodes?.length > 0) { + characters = data.characters.nodes.slice(0, 5); + } else if (data.characters?.edges?.length > 0) { + characters = data.characters.edges + .filter(edge => edge?.node?.name?.full) + .slice(0, 5) + .map(edge => edge.node); + } } if (characters.length > 0) { - characters.forEach(char => { - if (char?.name?.full) { + characters.slice(0, 5).forEach(char => { + const name = char?.name?.full || char?.name; + if (name) { charContainer.innerHTML += `
-
${char.name.full} +
${name}
`; } }); @@ -120,32 +146,27 @@ async function loadAnime() { width: '100%', videoId: data.trailer.id, playerVars: { - 'autoplay': 1, 'controls': 0, 'mute': 1, - 'loop': 1, 'playlist': data.trailer.id, - 'showinfo': 0, 'modestbranding': 1, 'disablekb': 1 + autoplay: 1, controls: 0, mute: 1, + loop: 1, playlist: data.trailer.id, + showinfo: 0, modestbranding: 1, disablekb: 1 }, - events: { 'onReady': (e) => e.target.playVideo() } + events: { onReady: (e) => e.target.playVideo() } }); }; } else { - const banner = data.bannerImage || data.coverImage?.extraLarge || ''; + const banner = extensionName + ? (data.image || '') + : (data.bannerImage || data.coverImage?.extraLarge || ''); + if (banner) { - document.querySelector('.video-background').innerHTML = ``; + document.querySelector('.video-background').innerHTML = + ``; } } - let extensionEpisodes = []; - if (extensionName) { - extensionEpisodes = await loadExtensionEpisodes(animeId, extensionName); - - if (extensionEpisodes.length > 0) { - totalEpisodes = extensionEpisodes.length; - } else { - totalEpisodes = 1; - } + totalEpisodes = data.episodes || 1; } else { - // MODO NORMAL (AniList) if (data.nextAiringEpisode?.episode) { totalEpisodes = data.nextAiringEpisode.episode - 1; } else if (data.episodes) { @@ -166,27 +187,6 @@ async function loadAnime() { } } -async function loadExtensionEpisodes(animeId, extName) { - try { - const url = `/api/anime/${animeId}/episodes?ext=${extName}`; - const res = await fetch(url); - const data = await res.json(); - - if (!Array.isArray(data)) return []; - - return data.map(ep => ({ - id: ep.id, - number: ep.number, - title: ep.title || `Episode ${ep.number}`, - url: ep.url - })); - } catch (err) { - console.error("Failed to fetch extension episodes:", err); - return []; - } -} - - function handleDescription(text) { const tmp = document.createElement("DIV"); tmp.innerHTML = text; diff --git a/src/scripts/anime/animes.js b/src/scripts/anime/animes.js index 637efd2..ff03d76 100644 --- a/src/scripts/anime/animes.js +++ b/src/scripts/anime/animes.js @@ -1,6 +1,7 @@ const searchInput = document.getElementById('search-input'); const searchResults = document.getElementById('search-results'); let searchTimeout; +let availableExtensions = []; searchInput.addEventListener('input', (e) => { const query = e.target.value; @@ -12,6 +13,7 @@ searchInput.addEventListener('input', (e) => { return; } searchTimeout = setTimeout(() => { + fetchSearh(query); }, 300); }); @@ -25,20 +27,48 @@ document.addEventListener('click', (e) => { async function fetchSearh(query) { try { - const res = await fetch(`/api/search?q=${encodeURIComponent(query.slice(0, 30))}`); - const data = await res.json(); - renderSearchResults(data.results); - } catch (err) { console.error("Search Error:", err); } -} + let apiUrl = `/api/search?q=${encodeURIComponent(query.slice(0, 30))}`; + let extensionName = null; + let finalQuery = query; -function createSlug(text) { - if (!text) return ''; - return text - .toLowerCase() - .trim() - .replace(/-/g, '--') - .replace(/\s+/g, '-') - .replace(/[^a-z0-9\-]/g, ''); + const parts = query.split(':'); + if (parts.length >= 2) { + const potentialExtension = parts[0].trim().toLowerCase(); + + const foundExtension = availableExtensions.find(ext => ext.toLowerCase() === potentialExtension); + + if (foundExtension) { + extensionName = foundExtension; + + finalQuery = parts.slice(1).join(':').trim(); + + if (finalQuery.length === 0) { + renderSearchResults([]); + return; + } + + apiUrl = `/api/search/${extensionName}?q=${encodeURIComponent(finalQuery.slice(0, 30))}`; + } + } + + const res = await fetch(apiUrl); + const data = await res.json(); + + const resultsWithExtension = (data.results || []).map(anime => { + if (extensionName) { + return { + ...anime, + isExtensionResult: true, + extensionName: extensionName + }; + } + return anime; + }); + + renderSearchResults(resultsWithExtension); + } catch (err) { + console.error("Search Error:", err); + } } function renderSearchResults(results) { @@ -54,20 +84,12 @@ function renderSearchResults(results) { const format = anime.format || 'TV'; let href; - if (anime.isExtensionResult) { - const titleSlug = createSlug(title); - console.log(title); - href = `/anime/${anime.extensionName}/${titleSlug}`; + href = `/anime/${anime.extensionName}/${anime.id}`; } else { href = `/anime/${anime.id}`; } - const extName = anime.extensionName?.charAt(0).toUpperCase() + anime.extensionName?.slice(1); - const extPill = anime.isExtensionResult - ? `• ${extName}` - : ''; - const item = document.createElement('a'); item.className = 'search-item'; item.href = href; @@ -79,7 +101,6 @@ function renderSearchResults(results) { ${rating} • ${year} • ${format} - ${extPill} `; @@ -118,6 +139,14 @@ function onYouTubeIframeAPIReady() { async function fetchContent(isUpdate = false) { try { + + const resExt = await fetch('/api/extensions/anime'); + const dataExt = await resExt.json(); + if (dataExt.extensions) { + availableExtensions = dataExt.extensions; + console.log("Extensiones de anime disponibles cargadas:", availableExtensions); + } + const trendingRes = await fetch('/api/trending'); const trendingData = await trendingRes.json(); @@ -220,6 +249,7 @@ function renderList(id, list) { const el = document.createElement('div'); el.className = 'card'; el.dataset.id = anime.id; + el.onclick = () => window.location.href = `/anime/${anime.id}`; el.innerHTML = `
diff --git a/src/scripts/anime/player.js b/src/scripts/anime/player.js index bbeeb81..b486fa3 100644 --- a/src/scripts/anime/player.js +++ b/src/scripts/anime/player.js @@ -23,138 +23,179 @@ document.getElementById('episode-label').innerText = `Episode ${currentEpisode}` async function loadMetadata() { try { const extQuery = extName ? `?ext=${extName}` : ""; - const res = await fetch(`/api/anime/${animeId}${extQuery}`); const data = await res.json(); - if (!data.error) { - const romajiTitle = data.title.romaji || data.title.english || 'Anime Title'; - - document.getElementById('anime-title-details').innerText = romajiTitle; - document.getElementById('anime-title-details2').innerText = romajiTitle; - - document.title = `Watching ${romajiTitle} - Ep ${currentEpisode}`; - - const tempDiv = document.createElement('div'); - tempDiv.innerHTML = data.description || 'No description available.'; - document.getElementById('detail-description').innerText = - tempDiv.textContent || tempDiv.innerText; - - document.getElementById('detail-format').innerText = data.format || '--'; - document.getElementById('detail-score').innerText = - data.averageScore ? `${data.averageScore}%` : '--'; - - const season = data.season - ? data.season.charAt(0) + data.season.slice(1).toLowerCase() - : ''; - document.getElementById('detail-season').innerText = - data.seasonYear ? `${season} ${data.seasonYear}` : '--'; - - document.getElementById('detail-cover-image').src = - data.coverImage.large || data.coverImage.medium || ''; - - if (data.characters && data.characters.edges && data.characters.edges.length > 0) { - populateCharacters(data.characters.edges); - } - - if (!extName) { - totalEpisodes = data.episodes || 0; - - if (totalEpisodes > 0) { - const simpleEpisodes = []; - for (let i = 1; i <= totalEpisodes; i++) { - simpleEpisodes.push({ - number: i, - title: null, - - thumbnail: null, - isDub: false - }); - } - populateEpisodeCarousel(simpleEpisodes); - } - - } else { - try { - const res2 = await fetch(`/api/anime/${animeId}/episodes${extQuery}`); - const data2 = await res2.json(); - totalEpisodes = Array.isArray(data2) ? data2.length : 0; - - if (Array.isArray(data2) && data2.length > 0) { - populateEpisodeCarousel(data2); - } - - } catch (e) { - console.error("Error cargando episodios por extensión:", e); - totalEpisodes = 0; - } - } - - if (currentEpisode >= totalEpisodes && totalEpisodes > 0) { - document.getElementById('next-btn').disabled = true; - } + if (data.error) { + console.error("Error from API:", data.error); + return; } + + const isAnilistFormat = data.title && (data.title.romaji || data.title.english); + + let title = ''; + let description = ''; + let coverImage = ''; + let averageScore = ''; + let format = ''; + let seasonYear = ''; + let season = ''; + let episodesCount = 0; + let characters = []; + + if (isAnilistFormat) { + + title = data.title.romaji || data.title.english || data.title.native || 'Anime Title'; + description = data.description || 'No description available.'; + coverImage = data.coverImage?.large || data.coverImage?.medium || ''; + averageScore = data.averageScore ? `${data.averageScore}%` : '--'; + format = data.format || '--'; + season = data.season ? data.season.charAt(0) + data.season.slice(1).toLowerCase() : ''; + seasonYear = data.seasonYear || ''; + episodesCount = data.episodes || 0; + characters = data.characters?.edges || []; + } else { + + title = data.title || 'Anime Title'; + description = data.summary || 'No description available.'; + coverImage = data.image || ''; + averageScore = data.score ? `${Math.round(data.score * 10)}%` : '--'; + format = '--'; + season = data.season || ''; + seasonYear = data.year || ''; + episodesCount = data.episodes || 0; + characters = data.characters || []; + } + + document.getElementById('anime-title-details').innerText = title; + document.getElementById('anime-title-details2').innerText = title; + document.title = `Watching ${title} - Ep ${currentEpisode}`; + + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = description; + document.getElementById('detail-description').innerText = tempDiv.textContent || tempDiv.innerText || 'No description available.'; + + document.getElementById('detail-format').innerText = format; + document.getElementById('detail-score').innerText = averageScore; + document.getElementById('detail-season').innerText = season && seasonYear ? `${season} ${seasonYear}` : (season || seasonYear || '--'); + document.getElementById('detail-cover-image').src = coverImage || '/default-cover.jpg'; + + if (Array.isArray(characters) && characters.length > 0) { + populateCharacters(characters, isAnilistFormat); + } + + if (!extName) { + totalEpisodes = episodesCount; + if (totalEpisodes > 0) { + const simpleEpisodes = []; + for (let i = 1; i <= totalEpisodes; i++) { + simpleEpisodes.push({ + number: i, + title: null, + thumbnail: null, + isDub: false + }); + } + populateEpisodeCarousel(simpleEpisodes); + } + } else { + + await loadExtensionEpisodes(); + } + + if (currentEpisode >= totalEpisodes && totalEpisodes > 0) { + document.getElementById('next-btn').disabled = true; + } + } catch (error) { console.error('Error loading metadata:', error); } } -function populateCharacters(characterEdges) { +async function loadExtensionEpisodes() { + try { + const extQuery = extName ? `?ext=${extName}` : ""; + const res = await fetch(`/api/anime/${animeId}/episodes${extQuery}`); + const data = await res.json(); + + totalEpisodes = Array.isArray(data) ? data.length : 0; + + if (Array.isArray(data) && data.length > 0) { + populateEpisodeCarousel(data); + } else { + + const fallback = []; + for (let i = 1; i <= totalEpisodes; i++) { + fallback.push({ number: i, title: null, thumbnail: null }); + } + populateEpisodeCarousel(fallback); + } + } catch (e) { + console.error("Error cargando episodios por extensión:", e); + totalEpisodes = 0; + } +} + +function populateCharacters(characters, isAnilistFormat) { const list = document.getElementById('characters-list'); list.classList.remove('characters-list'); list.classList.add('characters-carousel'); list.innerHTML = ''; - characterEdges.forEach(edge => { - const character = edge.node; - const voiceActor = edge.voiceActors ? edge.voiceActors.find(va => va.language === 'Japanese' || va.language === 'English') : null; + characters.forEach(item => { + let character, voiceActor; - if (character) { - const card = document.createElement('div'); - card.classList.add('character-card'); + if (isAnilistFormat) { + character = item.node; + voiceActor = item.voiceActors?.find(va => va.language === 'Japanese' || va.language === 'English'); + } else { + character = item; + voiceActor = null; - const img = document.createElement('img'); - img.classList.add('character-card-img'); - img.src = character.image.large || character.image.medium || ''; - img.alt = character.name.full || 'Character'; - - const vaName = voiceActor ? (voiceActor.name.full || voiceActor.name.userPreferred) : null; - const characterName = character.name.full || character.name.userPreferred || '--'; - - const details = document.createElement('div'); - details.classList.add('character-details'); - - const name = document.createElement('p'); - name.classList.add('character-name'); - name.innerText = characterName; - - const actor = document.createElement('p'); - actor.classList.add('actor-name'); - if (vaName) { - actor.innerText = `${vaName} (${voiceActor.language})`; - } else { - actor.innerText = 'Voice Actor: N/A'; - } - - details.appendChild(name); - details.appendChild(actor); - card.appendChild(img); - card.appendChild(details); - list.appendChild(card); } + + if (!character) return; + + const card = document.createElement('div'); + card.classList.add('character-card'); + + const img = document.createElement('img'); + img.classList.add('character-card-img'); + img.src = character.image?.large || character.image?.medium || character.image || ''; + img.alt = character.name?.full || 'Character'; + + const details = document.createElement('div'); + details.classList.add('character-details'); + + const name = document.createElement('p'); + name.classList.add('character-name'); + name.innerText = character.name?.full || character.name || '--'; + + const actor = document.createElement('p'); + actor.classList.add('actor-name'); + if (voiceActor?.name?.full) { + actor.innerText = `${voiceActor.name.full} (${voiceActor.language})`; + } else { + actor.innerText = 'Voice Actor: N/A'; + } + + details.appendChild(name); + details.appendChild(actor); + card.appendChild(img); + card.appendChild(details); + list.appendChild(card); }); } + function populateEpisodeCarousel(episodesData) { const carousel = document.getElementById('episode-carousel'); carousel.innerHTML = ''; episodesData.forEach((ep, index) => { const epNumber = ep.number || ep.episodeNumber || ep.id || (index + 1); - if (!epNumber) return; const extParam = extName ? `?${extName}` : ""; - const hasThumbnail = ep.thumbnail && ep.thumbnail.trim() !== ''; const link = document.createElement('a'); @@ -162,19 +203,13 @@ function populateEpisodeCarousel(episodesData) { link.classList.add('carousel-item'); link.dataset.episode = epNumber; - if (!hasThumbnail) { - link.classList.add('no-thumbnail'); - } - - if (parseInt(epNumber) === currentEpisode) { - link.classList.add('active-ep-carousel'); - } + if (!hasThumbnail) link.classList.add('no-thumbnail'); + if (parseInt(epNumber) === currentEpisode) link.classList.add('active-ep-carousel'); const imgContainer = document.createElement('div'); imgContainer.classList.add('carousel-item-img-container'); if (hasThumbnail) { - const img = document.createElement('img'); img.classList.add('carousel-item-img'); img.src = ep.thumbnail; @@ -188,11 +223,9 @@ function populateEpisodeCarousel(episodesData) { info.classList.add('carousel-item-info'); const title = document.createElement('p'); - title.innerText = `Ep ${epNumber}: ${ep.title || 'Untitled'}`; info.appendChild(title); - link.appendChild(info); carousel.appendChild(link); }); @@ -282,14 +315,8 @@ function setAudioMode(mode) { const dubOpt = document.getElementById('opt-dub'); toggle.setAttribute('data-state', mode); - - if (mode === 'sub') { - subOpt.classList.add('active'); - dubOpt.classList.remove('active'); - } else { - subOpt.classList.remove('active'); - dubOpt.classList.add('active'); - } + subOpt.classList.toggle('active', mode === 'sub'); + dubOpt.classList.toggle('active', mode === 'dub'); } async function loadStream() { @@ -323,7 +350,7 @@ async function loadStream() { if (headers['Origin']) proxyUrl += `&origin=${encodeURIComponent(headers['Origin'])}`; if (headers['User-Agent']) proxyUrl += `&userAgent=${encodeURIComponent(headers['User-Agent'])}`; - playVideo(proxyUrl, data.videoSources[0].subtitles); + playVideo(proxyUrl, data.videoSources[0].subtitles || data.subtitles); document.getElementById('loading-overlay').style.display = 'none'; } catch (error) { setLoading("Stream error. Check console."); @@ -331,17 +358,12 @@ async function loadStream() { } } -function playVideo(url, subtitles) { +function playVideo(url, subtitles = []) { const video = document.getElementById('player'); if (Hls.isSupported()) { if (hlsInstance) hlsInstance.destroy(); - - hlsInstance = new Hls({ - xhrSetup: (xhr, url) => { - xhr.withCredentials = false; - } - }); + hlsInstance = new Hls({ xhrSetup: (xhr) => xhr.withCredentials = false }); hlsInstance.loadSource(url); hlsInstance.attachMedia(video); } else if (video.canPlayType('application/vnd.apple.mpegurl')) { @@ -350,52 +372,28 @@ function playVideo(url, subtitles) { if (plyrInstance) plyrInstance.destroy(); - while (video.firstChild) { - video.removeChild(video.firstChild); + while (video.textTracks.length > 0) { + video.removeChild(video.textTracks[0]); } - if (subtitles && subtitles.length > 0) { - subtitles.forEach(sub => { - const track = document.createElement('track'); - track.kind = 'captions'; - track.label = sub.language; - track.srclang = sub.language.slice(0, 2).toLowerCase(); - track.src = sub.url; - - if (sub.default || sub.language.toLowerCase().includes('english')) { - track.default = true; - } - - video.appendChild(track); - }); - } + subtitles.forEach(sub => { + if (!sub.url) return; + const track = document.createElement('track'); + track.kind = 'captions'; + track.label = sub.language || 'Unknown'; + track.srclang = (sub.language || '').slice(0, 2).toLowerCase(); + track.src = sub.url; + if (sub.default || sub.language?.toLowerCase().includes('english')) track.default = true; + video.appendChild(track); + }); plyrInstance = new Plyr(video, { - captions: { - active: true, - update: true, - language: 'en' - }, - controls: [ - 'play-large', - 'play', - 'progress', - 'current-time', - 'duration', - 'mute', - 'volume', - 'captions', - 'settings', - 'pip', - 'airplay', - 'fullscreen' - ], + captions: { active: true, update: true, language: 'en' }, + controls: ['play-large', 'play', 'progress', 'current-time', 'duration', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', 'fullscreen'], settings: ['captions', 'quality', 'speed'] }); - video.play().catch(error => { - console.log("Autoplay blocked:", error); - }); + video.play().catch(() => console.log("Autoplay blocked")); } function setLoading(message) { @@ -414,7 +412,9 @@ document.getElementById('prev-btn').onclick = () => { }; document.getElementById('next-btn').onclick = () => { - window.location.href = `/watch/${animeId}/${currentEpisode + 1}${extParam}`; + if (currentEpisode < totalEpisodes || totalEpisodes === 0) { + window.location.href = `/watch/${animeId}/${currentEpisode + 1}${extParam}`; + } }; if (currentEpisode <= 1) { @@ -422,4 +422,5 @@ if (currentEpisode <= 1) { } loadMetadata(); -loadExtensions(); \ No newline at end of file +loadExtensions(); + diff --git a/src/scripts/books/book.js b/src/scripts/books/book.js index eece63a..1bf30b3 100644 --- a/src/scripts/books/book.js +++ b/src/scripts/books/book.js @@ -4,37 +4,77 @@ let filteredChapters = []; let currentPage = 1; const itemsPerPage = 12; let extensionName = null; +let bookSlug = null; async function init() { try { const path = window.location.pathname; const parts = path.split("/").filter(Boolean); - let bookId; + let currentBookId; if (parts.length === 3) { extensionName = parts[1]; - bookId = parts[2]; + bookSlug = parts[2]; + + currentBookId = bookSlug; } else { - bookId = parts[1]; + currentBookId = parts[1]; + } + const idForFetch = currentBookId; + const fetchUrl = extensionName - ? `/api/book/${bookId.slice(0,40)}?ext=${extensionName}` - : `/api/book/${bookId}`; + ? `/api/book/${idForFetch.slice(0,40)}?ext=${extensionName}` + : `/api/book/${idForFetch}`; const res = await fetch(fetchUrl); const data = await res.json(); console.log(data); - if (data.error) { + if (data.error || !data) { const titleEl = document.getElementById('title'); if (titleEl) titleEl.innerText = "Book Not Found"; return; } - const title = data.title.english || data.title.romaji; + let title, description, score, year, status, format, chapters, poster, banner, genres; + + if (extensionName) { + + title = data.title || data.name || "Unknown"; + description = data.summary || "No description available."; + score = data.score ? Math.round(data.score) : '?'; + + year = data.published || '????'; + + status = data.status || 'Unknown'; + format = data.format || 'LN'; + chapters = data.chapters || '?'; + poster = data.image || ''; + + banner = poster; + + genres = Array.isArray(data.genres) ? data.genres.slice(0, 3).join(' • ') : ''; + + } else { + + title = data.title.english || data.title.romaji || "Unknown"; + description = data.description || "No description available."; + score = data.averageScore || '?'; + + year = (data.startDate && data.startDate.year) ? data.startDate.year : '????'; + status = data.status || 'Unknown'; + format = data.format || 'MANGA'; + chapters = data.chapters || '?'; + poster = data.coverImage.extraLarge || data.coverImage.large || ''; + banner = data.bannerImage || poster; + genres = data.genres ? data.genres.slice(0, 3).join(' • ') : ''; + + } + document.title = `${title} | WaifuBoard Books`; - + const titleEl = document.getElementById('title'); if (titleEl) titleEl.innerText = title; @@ -47,71 +87,61 @@ async function init() { } const descEl = document.getElementById('description'); - if (descEl) descEl.innerHTML = data.description || "No description available."; + if (descEl) descEl.innerHTML = description; const scoreEl = document.getElementById('score'); - if (scoreEl) scoreEl.innerText = (data.averageScore || '?') + '% Score'; + if (scoreEl) scoreEl.innerText = score + (extensionName ? '' : '% Score'); const pubEl = document.getElementById('published-date'); - if (pubEl) { - if (data.startDate && data.startDate.year) { - const y = data.startDate.year; - const m = data.startDate.month ? `-${data.startDate.month.toString().padStart(2, '0')}` : ''; - const d = data.startDate.day ? `-${data.startDate.day.toString().padStart(2, '0')}` : ''; - pubEl.innerText = `${y}${m}${d}`; - } else { - pubEl.innerText = '????'; - } - } + if (pubEl) pubEl.innerText = year; const statusEl = document.getElementById('status'); - if (statusEl) statusEl.innerText = data.status || 'Unknown'; + if (statusEl) statusEl.innerText = status; const formatEl = document.getElementById('format'); - if (formatEl) formatEl.innerText = data.format || 'MANGA'; + if (formatEl) formatEl.innerText = format; const chaptersEl = document.getElementById('chapters'); - if (chaptersEl) chaptersEl.innerText = data.chapters || '?'; - + if (chaptersEl) chaptersEl.innerText = chapters; + const genresEl = document.getElementById('genres'); - if(genresEl && data.genres) { - genresEl.innerText = data.genres.slice(0, 3).join(' • '); + if(genresEl) { + genresEl.innerText = genres; } - const img = data.coverImage.extraLarge || data.coverImage.large; - const posterEl = document.getElementById('poster'); - if (posterEl) posterEl.src = img; + if (posterEl) posterEl.src = poster; const heroBgEl = document.getElementById('hero-bg'); - if (heroBgEl) heroBgEl.src = data.bannerImage || img; + if (heroBgEl) heroBgEl.src = banner; - loadChapters(); + loadChapters(idForFetch); } catch (err) { console.error("Metadata Error:", err); } } -async function loadChapters() { +async function loadChapters(idForFetch) { const tbody = document.getElementById('chapters-body'); if (!tbody) return; - + tbody.innerHTML = 'Searching extensions for chapters...'; try { + const fetchUrl = extensionName - ? `/api/book/${bookId.slice(0, 40)}/chapters` - : `/api/book/${bookId}/chapters`; + ? `/api/book/${idForFetch.slice(0, 40)}/chapters` + : `/api/book/${idForFetch}/chapters`; const res = await fetch(fetchUrl); const data = await res.json(); - + allChapters = data.chapters || []; filteredChapters = [...allChapters]; - + const totalEl = document.getElementById('total-chapters'); - + if (allChapters.length === 0) { tbody.innerHTML = 'No chapters found on loaded extensions.'; if (totalEl) totalEl.innerText = "0 Found"; @@ -119,15 +149,16 @@ async function loadChapters() { } if (totalEl) totalEl.innerText = `${allChapters.length} Found`; - + populateProviderFilter(); const readBtn = document.getElementById('read-start-btn'); if (readBtn && filteredChapters.length > 0) { - readBtn.onclick = () => openReader(filteredChapters[0].id); + + readBtn.onclick = () => openReader(idForFetch, filteredChapters[0].id, filteredChapters[0].provider); } - renderTable(); + renderTable(idForFetch); } catch (err) { tbody.innerHTML = 'Error loading chapters.'; @@ -140,12 +171,12 @@ function populateProviderFilter() { if (!select) return; const providers = [...new Set(allChapters.map(ch => ch.provider))]; - + if (providers.length > 0) { select.style.display = 'inline-block'; - + select.innerHTML = ''; - + providers.forEach(prov => { const opt = document.createElement('option'); opt.value = prov; @@ -153,6 +184,19 @@ function populateProviderFilter() { select.appendChild(opt); }); + if (extensionName) { + + const extensionProvider = providers.find(p => p.toLowerCase() === extensionName.toLowerCase()); + + if (extensionProvider) { + + select.value = extensionProvider; + + filteredChapters = allChapters.filter(ch => ch.provider === extensionProvider); + + } + } + select.onchange = (e) => { const selected = e.target.value; if (selected === 'all') { @@ -161,12 +205,13 @@ function populateProviderFilter() { filteredChapters = allChapters.filter(ch => ch.provider === selected); } currentPage = 1; - renderTable(); + const idForFetch = extensionName ? bookSlug : bookId; + renderTable(idForFetch); }; } } -function renderTable() { +function renderTable(idForFetch) { const tbody = document.getElementById('chapters-body'); if (!tbody) return; @@ -206,25 +251,27 @@ function renderTable() { function updatePagination() { const totalPages = Math.ceil(filteredChapters.length / itemsPerPage); const pagination = document.getElementById('pagination'); - + if (!pagination) return; if (totalPages <= 1) { pagination.style.display = 'none'; return; } - + pagination.style.display = 'flex'; document.getElementById('page-info').innerText = `Page ${currentPage} of ${totalPages}`; - + const prevBtn = document.getElementById('prev-page'); const nextBtn = document.getElementById('next-page'); - + prevBtn.disabled = currentPage === 1; nextBtn.disabled = currentPage >= totalPages; - prevBtn.onclick = () => { currentPage--; renderTable(); }; - nextBtn.onclick = () => { currentPage++; renderTable(); }; + const idForFetch = extensionName ? bookSlug : bookId; + + prevBtn.onclick = () => { currentPage--; renderTable(idForFetch); }; + nextBtn.onclick = () => { currentPage++; renderTable(idForFetch); }; } function openReader(bookId, chapterId, provider) { diff --git a/src/scripts/books/books.js b/src/scripts/books/books.js index 4f5d2f7..6ebc17d 100644 --- a/src/scripts/books/books.js +++ b/src/scripts/books/books.js @@ -1,6 +1,7 @@ let trendingBooks = []; let currentHeroIndex = 0; let heroInterval; +let availableExtensions = []; window.addEventListener('scroll', () => { const nav = document.getElementById('navbar'); @@ -15,7 +16,7 @@ let searchTimeout; searchInput.addEventListener('input', (e) => { const query = e.target.value; clearTimeout(searchTimeout); - + if (query.length < 2) { searchResults.classList.remove('active'); searchResults.innerHTML = ''; @@ -37,25 +38,51 @@ document.addEventListener('click', (e) => { async function fetchBookSearch(query) { try { - const res = await fetch(`/api/search/books?q=${encodeURIComponent(query)}`); + let apiUrl = `/api/search/books?q=${encodeURIComponent(query)}`; + let extensionName = null; + let finalQuery = query; + + const parts = query.split(':'); + if (parts.length >= 2) { + const potentialExtension = parts[0].trim().toLowerCase(); + + const foundExtension = availableExtensions.find(ext => ext.toLowerCase() === potentialExtension); + + if (foundExtension) { + extensionName = foundExtension; + + finalQuery = parts.slice(1).join(':').trim(); + + if (finalQuery.length === 0) { + renderSearchResults([]); + return; + } + + apiUrl = `/api/search/books/${extensionName}?q=${encodeURIComponent(finalQuery)}`; + } + } + + const res = await fetch(apiUrl); const data = await res.json(); - renderSearchResults(data.results || []); - } catch (err) { - console.error("Search Error:", err); + + const resultsWithExtension = data.results.map(book => { + if (extensionName) { + return { ...book, + isExtensionResult: true, + extensionName: extensionName + + }; + } + return book; + }); + + renderSearchResults(resultsWithExtension || []); + } catch (err) { + console.error("Search Error:", err); renderSearchResults([]); } } -function createSlug(text) { - if (!text) return ''; - return text - .toLowerCase() - .trim() - .replace(/-/g, '--') - .replace(/\s+/g, '-') - .replace(/[^a-z0-9\-]/g, ''); -} - function renderSearchResults(results) { searchResults.innerHTML = ''; @@ -65,25 +92,18 @@ function renderSearchResults(results) { results.forEach(book => { const title = book.title.english || book.title.romaji || "Unknown"; const img = (book.coverImage && (book.coverImage.medium || book.coverImage.large)) || ''; - const rating = book.averageScore ? `${book.averageScore}%` : 'N/A'; + const rating = Number.isInteger(book.averageScore) ? `${book.averageScore}%` : book.averageScore || 'N/A'; const year = book.seasonYear || (book.startDate ? book.startDate.year : '') || '????'; const format = book.format || 'MANGA'; - let href; - - if (book.isExtensionResult) { - const titleSlug = createSlug(title); - href = `/book/${book.extensionName}/${titleSlug}`; - } else { - href = `/book/${book.id}`; - } - - const extName = book.extensionName.charAt(0).toUpperCase() + book.extensionName.slice(1); - const extPill = book.isExtensionResult - ? `${extName}` - : ''; const item = document.createElement('a'); item.className = 'search-item'; + let href; + if (book.isExtensionResult) { + href = `/book/${book.extensionName}/${book.id}`; + } else { + href = `/book/${book.id}`; + } item.href = href; item.innerHTML = ` @@ -94,7 +114,6 @@ function renderSearchResults(results) { ${rating} • ${year} • ${format} - ${extPill} `; @@ -117,9 +136,17 @@ function scrollCarousel(id, direction) { async function init() { try { + + const resExt = await fetch('/api/extensions/book'); + const dataExt = await resExt.json(); + if (dataExt.extensions) { + availableExtensions = dataExt.extensions; + console.log("Extensiones disponibles cargadas:", availableExtensions); + } + const res = await fetch('/api/books/trending'); const data = await res.json(); - + if (data.results && data.results.length > 0) { trendingBooks = data.results; updateHeroUI(trendingBooks[0]); @@ -152,21 +179,22 @@ function updateHeroUI(book) { const desc = book.description || "No description available."; const poster = (book.coverImage && (book.coverImage.extraLarge || book.coverImage.large)) || ''; const banner = book.bannerImage || poster; - + document.getElementById('hero-title').innerText = title; document.getElementById('hero-desc').innerHTML = desc; document.getElementById('hero-score').innerText = (book.averageScore || '?') + '% Score'; document.getElementById('hero-year').innerText = (book.startDate && book.startDate.year) ? book.startDate.year : '????'; document.getElementById('hero-type').innerText = book.format || 'MANGA'; - + const heroPoster = document.getElementById('hero-poster'); if(heroPoster) heroPoster.src = poster; - + const bg = document.getElementById('hero-bg-media'); if(bg) bg.src = banner; const readBtn = document.getElementById('read-btn'); if (readBtn) { + readBtn.onclick = () => window.location.href = `/book/${book.id}`; } } @@ -174,7 +202,7 @@ function updateHeroUI(book) { function renderList(id, list) { const container = document.getElementById(id); container.innerHTML = ''; - + list.forEach(book => { const title = book.title.english || book.title.romaji; const cover = book.coverImage ? book.coverImage.large : ''; @@ -183,6 +211,7 @@ function renderList(id, list) { const el = document.createElement('div'); el.className = 'card'; + el.onclick = () => { window.location.href = `/book/${book.id}`; }; diff --git a/src/scripts/books/reader.js b/src/scripts/books/reader.js index 8387847..3fb980c 100644 --- a/src/scripts/books/reader.js +++ b/src/scripts/books/reader.js @@ -467,7 +467,7 @@ nextBtn.addEventListener('click', () => { function updateURL(newChapter) { chapter = newChapter; - const newUrl = `/reader/${provider}/${chapter}/${bookId}`; + const newUrl = `/read/${provider}/${chapter}/${bookId}`; window.history.pushState({}, '', newUrl); } diff --git a/src/shared/database.js b/src/shared/database.js index ee1d928..7a6625e 100644 --- a/src/shared/database.js +++ b/src/shared/database.js @@ -7,9 +7,46 @@ const databases = new Map(); const DEFAULT_PATHS = { anilist: path.join(__dirname, '..', 'metadata', 'anilist_anime.db'), - favorites: path.join(os.homedir(), "WaifuBoards", "favorites.db") + favorites: path.join(os.homedir(), "WaifuBoards", "favorites.db"), + + cache: path.join(os.homedir(), "WaifuBoards", "cache.db") }; +async function ensureExtensionsTable(db) { + return new Promise((resolve, reject) => { + db.exec(` + CREATE TABLE IF NOT EXISTS extension ( + ext_name TEXT NOT NULL, + id TEXT NOT NULL, + title TEXT NOT NULL, + metadata TEXT NOT NULL, + updated_at INTEGER NOT NULL, + + PRIMARY KEY(ext_name, id) + ); + `, (err) => { + if (err) reject(err); + else resolve(true); + }); + }); +} + +async function ensureCacheTable(db) { + return new Promise((resolve, reject) => { + db.exec(` + CREATE TABLE IF NOT EXISTS cache ( + key TEXT PRIMARY KEY, + result TEXT NOT NULL, + created_at INTEGER NOT NULL, + ttl_ms INTEGER NOT NULL + ); + `, (err) => { + if (err) reject(err); + else resolve(true); + }); + }); +} + function ensureFavoritesDB(dbPath) { const dir = path.dirname(dbPath); @@ -25,7 +62,6 @@ function ensureFavoritesDB(dbPath) { ); return new Promise((resolve, reject) => { - if (!exists) { const schema = ` CREATE TABLE IF NOT EXISTS favorites ( @@ -56,15 +92,11 @@ function ensureFavoritesDB(dbPath) { const queries = []; if (!hasHeaders) { - queries.push( - `ALTER TABLE favorites ADD COLUMN headers TEXT NOT NULL DEFAULT ""` - ); + queries.push(`ALTER TABLE favorites ADD COLUMN headers TEXT NOT NULL DEFAULT ""`); } if (!hasProvider) { - queries.push( - `ALTER TABLE favorites ADD COLUMN provider TEXT NOT NULL DEFAULT ""` - ); + queries.push(`ALTER TABLE favorites ADD COLUMN provider TEXT NOT NULL DEFAULT ""`); } if (queries.length === 0) { @@ -91,6 +123,13 @@ function initDatabase(name = 'anilist', dbPath = null, readOnly = false) { .catch(err => console.error("Error creando favorites:", err)); } + if (name === "cache") { + const dir = path.dirname(finalPath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + } + const mode = readOnly ? sqlite3.OPEN_READONLY : (sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE); const db = new sqlite3.Database(finalPath, mode, (err) => { @@ -102,12 +141,25 @@ function initDatabase(name = 'anilist', dbPath = null, readOnly = false) { }); databases.set(name, db); + + if (name === "anilist") { + ensureExtensionsTable(db) + .catch(err => console.error("Error creating extension table:", err)); + } + + if (name === "cache") { + ensureCacheTable(db) + .catch(err => console.error("Error creating cache table:", err)); + } + return db; } function getDatabase(name = 'anilist') { if (!databases.has(name)) { - return initDatabase(name, null, name === 'anilist'); + + const readOnly = (name === 'anilist'); + return initDatabase(name, null, readOnly); } return databases.get(name); } @@ -156,11 +208,73 @@ function closeDatabase(name = null) { } } +async function getCachedExtension(extName, id) { + return queryOne( + "SELECT metadata FROM extension WHERE ext_name = ? AND id = ?", + [extName, id] + ); +} + +async function cacheExtension(extName, id, title, metadata) { + return run( + ` + INSERT INTO extension (ext_name, id, title, metadata, updated_at) + VALUES (?, ?, ?, ?, ?) + ON CONFLICT(ext_name, id) + DO UPDATE SET + title = excluded.title, + metadata = excluded.metadata, + updated_at = ? + `, + [extName, id, title, JSON.stringify(metadata), Date.now(), Date.now()] + + ); +} + +async function getExtensionTitle(extName, id) { + + const sql = "SELECT title FROM extension WHERE ext_name = ? AND id = ?"; + + const row = await queryOne(sql, [extName, id], 'anilist'); + + return row ? row.title : null; +} + +async function deleteExtension(extName) { + return run( + "DELETE FROM extension WHERE ext_name = ?", + [extName] + ); +} + +async function getCache(key) { + return queryOne("SELECT result, created_at, ttl_ms FROM cache WHERE key = ?", [key], "cache"); +} + +async function setCache(key, result, ttl_ms) { + return run( + ` + INSERT INTO cache (key, result, created_at, ttl_ms) + VALUES (?, ?, ?, ?) + ON CONFLICT(key) + DO UPDATE SET result = excluded.result, created_at = excluded.created_at, ttl_ms = excluded.ttl_ms + `, + [key, JSON.stringify(result), Date.now(), ttl_ms], + "cache" + ); +} + module.exports = { initDatabase, getDatabase, queryOne, queryAll, run, - closeDatabase + getCachedExtension, + cacheExtension, + getExtensionTitle, + deleteExtension, + closeDatabase, + getCache, + setCache }; \ No newline at end of file diff --git a/src/shared/extensions.js b/src/shared/extensions.js index 1ca8cbc..774bce7 100644 --- a/src/shared/extensions.js +++ b/src/shared/extensions.js @@ -1,6 +1,7 @@ const fs = require('fs'); const path = require('path'); const os = require('os'); +const { queryAll, run } = require('./database'); const extensions = new Map(); @@ -24,11 +25,16 @@ async function loadExtensions() { delete require.cache[require.resolve(filePath)]; const ExtensionClass = require(filePath); + const instance = typeof ExtensionClass === 'function' ? new ExtensionClass() : (ExtensionClass.default ? new ExtensionClass.default() : null); - if (instance && (instance.type === "anime-board" || instance.type === "book-board" || instance.type === "image-board")) { + if (instance && + (instance.type === "anime-board" || + instance.type === "book-board" || + instance.type === "image-board")) { + const name = instance.constructor.name; extensions.set(name, instance); console.log(`📦 Loaded Extension: ${name}`); @@ -40,6 +46,22 @@ async function loadExtensions() { } console.log(`✅ Loaded ${extensions.size} extensions`); + + try { + const loaded = Array.from(extensions.keys()); + + const rows = await queryAll("SELECT DISTINCT ext_name FROM extension"); + + for (const row of rows) { + if (!loaded.includes(row.ext_name)) { + console.log(`🧹 Cleaning cached metadata for removed extension: ${row.ext_name}`); + await run("DELETE FROM extension WHERE ext_name = ?", [row.ext_name]); + } + } + } catch (err) { + console.error("❌ Error cleaning extension cache:", err); + } + } catch (err) { console.error("❌ Extension Scan Error:", err); } diff --git a/src/types.ts b/src/types.ts index 87b0f5c..e33742e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -61,6 +61,8 @@ export interface ExtensionSearchOptions { } export interface ExtensionSearchResult { + format: string; + headers: any; id: string; title: string; image?: string; @@ -88,6 +90,7 @@ export interface ChapterWithProvider extends Chapter { } export interface Extension { + getMetadata: any; type: 'anime-board' | 'book-board' | 'manga-board'; mediaType?: 'manga' | 'ln'; search?: (options: ExtensionSearchOptions) => Promise; diff --git a/views/css/books/books.css b/views/css/books/books.css index eaf45e8..83fc7ef 100644 --- a/views/css/books/books.css +++ b/views/css/books/books.css @@ -336,4 +336,26 @@ body { position: fixed; top: 120px; left: 0; right: 0; width: 100%; border-radius: 0; max-height: 60vh; border: none; z-index: 2001; } +} + +.adv-search-btn { + position: absolute; + top: 50%; /* Centrado verticalmente */ + right: 5px; /* Ajusta la distancia del borde derecho */ + transform: translateY(-50%); /* Ajuste fino de centrado */ + + /* Estilos para que parezca un botón de icono */ + background: transparent; + border: none; + cursor: pointer; + padding: 5px; /* Área de clic cómoda */ + line-height: 0; /* Elimina espacio extra */ + + /* Opcional: Dale un color de icono que combine */ + transition: color 0.2s; +} + +/* 3. Efecto al pasar el ratón (hover) */ +.adv-search-btn:hover { + color: var(--color-primary, #fff); /* Cambia de color al pasar el mouse */ } \ No newline at end of file