caching system + add extension entries to metadata pool

This commit is contained in:
2025-12-02 18:21:41 +01:00
parent af1e1d8098
commit 47169a5f66
18 changed files with 924 additions and 485 deletions

View File

@@ -4,17 +4,17 @@ The official recode repo, its private no one should know about this or have the
# Things to get done: # Things to get done:
| Task | Status | Notes | | Task | Status | Notes |
| -----|------| ------ | | -----|----------| ------ |
| Book Reader | ✅ | N/A | | Book Reader | ✅ | N/A |
| Multi book provider loading | ✅ | N/A | | Multi book provider loading | ✅ | N/A |
| Better Code Organization | ✅ | N/A | | Better Code Organization | ✅ | N/A |
| Mobile View | Not Done | N/A | | Mobile View | Not Done | N/A |
| Gallery | Not Done | N/A | | Gallery | | N/A |
| Anime Schedule (Release Calendar for the week/month) | ✅ | 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 | | My List (Tracking) | Not Done | Persistent data would be in a data.db file in waifuboard directory |
| Marketplace | Not Done | Uses the gitea repo | | Marketplace | Not Done | Uses the gitea repo |
| Add to list / library | Not Done | Persistent data would be in data.db file in waifuboard directory| | 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 | | Gallery favorites | | Persistent in data.db like how it was previously |
| Change "StreamFlow" to "WaifuBoard" | ✅ | N/A | | 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 | | 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 | | Favicon | ✅ | Use the ico file from the current waifuboard ver |

View File

@@ -77,6 +77,7 @@ const start = async () => {
try { try {
initDatabase("anilist"); initDatabase("anilist");
initDatabase("favorites"); initDatabase("favorites");
initDatabase("cache");
await loadExtensions(); await loadExtensions();
await fastify.listen({ port: 3000, host: '0.0.0.0' }); await fastify.listen({ port: 3000, host: '0.0.0.0' });

View File

@@ -12,15 +12,8 @@ export async function getAnime(req: AnimeRequest, reply: FastifyReply) {
if (source === 'anilist') { if (source === 'anilist') {
anime = await animeService.getAnimeById(id); anime = await animeService.getAnimeById(id);
} else { } else {
const extensionName = source; const ext = getExtension(source);
const ext = getExtension(extensionName); anime = await animeService.getAnimeInfoExtension(ext, id)
const results = await animeService.searchAnimeInExtension(
ext,
extensionName,
id.replace(/--/g, '\u0000').replace(/-/g, ' ').replace(new RegExp('\u0000', 'g'), '-').trim()
);
anime = results[0] || null;
} }
return anime; return anime;
@@ -35,10 +28,10 @@ export async function getAnimeEpisodes(req: AnimeRequest, reply: FastifyReply) {
const extensionName = req.query.ext || 'anilist'; const extensionName = req.query.ext || 'anilist';
const ext = getExtension(extensionName); const ext = getExtension(extensionName);
return await animeService.searchChaptersInExtension( return await animeService.searchEpisodesInExtension(
ext, ext,
extensionName, extensionName,
id.replace(/--/g, '\u0000').replace(/-/g, ' ').replace(new RegExp('\u0000', 'g'), '-').trim() id
); );
} catch (err) { } catch (err) {
return { error: "Database error" }; return { error: "Database error" };
@@ -72,14 +65,26 @@ export async function search(req: SearchRequest, reply: FastifyReply) {
return { results: results }; return { results: results };
} }
const extResults = await animeService.searchAnimeExtensions(query);
return { results: extResults };
} catch (err) { } catch (err) {
return { results: [] }; 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) { export async function getWatchStream(req: WatchStreamRequest, reply: FastifyReply) {
try { try {
const { animeId, episode, server, category, ext } = req.query; const { animeId, episode, server, category, ext } = req.query;

View File

@@ -7,6 +7,7 @@ async function animeRoutes(fastify: FastifyInstance) {
fastify.get('/trending', controller.getTrending); fastify.get('/trending', controller.getTrending);
fastify.get('/top-airing', controller.getTopAiring); fastify.get('/top-airing', controller.getTopAiring);
fastify.get('/search', controller.search); fastify.get('/search', controller.search);
fastify.get('/search/:extension', controller.searchInExtension);
fastify.get('/watch/stream', controller.getWatchStream); fastify.get('/watch/stream', controller.getWatchStream);
} }

View File

@@ -1,7 +1,8 @@
import { queryOne, queryAll } from '../shared/database'; import {queryOne, queryAll, getCache, setCache, getCachedExtension, cacheExtension, getExtensionTitle } from '../shared/database';
import { getAnimeExtensionsMap } from '../shared/extensions';
import {Anime, Episode, Extension, StreamData} from '../types'; import {Anime, Episode, Extension, StreamData} from '../types';
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
export async function getAnimeById(id: string | number): Promise<Anime | { error: string }> { export async function getAnimeById(id: string | number): Promise<Anime | { error: string }> {
const row = await queryOne("SELECT full_data FROM anime WHERE id = ?", [id]); const row = await queryOne("SELECT full_data FROM anime WHERE id = ?", [id]);
@@ -47,6 +48,38 @@ export async function searchAnimeLocal(query: string): Promise<Anime[]> {
return cleanResults.slice(0, 10); return cleanResults.slice(0, 10);
} }
export async function getAnimeInfoExtension(ext: Extension | null, id: string): Promise<Anime | { error: string }> {
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<Anime[]> { export async function searchAnimeInExtension(ext: Extension | null, name: string, query: string): Promise<Anime[]> {
if (!ext) return []; if (!ext) return [];
@@ -71,7 +104,7 @@ export async function searchAnimeInExtension(ext: Extension | null, name: string
averageScore: m.rating || m.score || null, averageScore: m.rating || m.score || null,
format: 'ANIME', format: 'ANIME',
seasonYear: null, seasonYear: null,
isExtensionResult: true isExtensionResult: true,
})); }));
} }
} catch (e) { } catch (e) {
@@ -82,11 +115,33 @@ export async function searchAnimeInExtension(ext: Extension | null, name: string
return []; return [];
} }
export async function searchChaptersInExtension(ext: Extension | null, name: string, query: string): Promise<Episode[]> { export async function searchEpisodesInExtension(ext: Extension | null, name: string, query: string): Promise<Episode[]> {
if (!ext) return []; 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") { if (ext.type === "anime-board" && ext.search && typeof ext.findEpisodes === "function") {
try { try {
const title = await getExtensionTitle(name, query);
let mediaId: string;
if (title) {
const matches = await ext.search({ const matches = await ext.search({
query, query,
@@ -102,15 +157,28 @@ export async function searchChaptersInExtension(ext: Extension | null, name: str
const res = matches[0]; const res = matches[0];
if (!res?.id) return []; if (!res?.id) return [];
const chapterList = await ext.findEpisodes(res.id); mediaId = res.id;
} else {
mediaId = query;
}
const chapterList = await ext.findEpisodes(mediaId);
if (!Array.isArray(chapterList)) return []; if (!Array.isArray(chapterList)) return [];
return chapterList.map(ep => ({ const result: Episode[] = chapterList.map(ep => ({
id: ep.id, id: ep.id,
number: ep.number, number: ep.number,
url: ep.url, url: ep.url,
title: ep.title title: ep.title
})); }));
await setCache(cacheKey, result, CACHE_TTL_MS);
return result;
} catch (e) { } catch (e) {
console.error(`Extension search failed for ${name}:`, e); console.error(`Extension search failed for ${name}:`, e);
} }
@@ -119,18 +187,28 @@ export async function searchChaptersInExtension(ext: Extension | null, name: str
return []; return [];
} }
export async function searchAnimeExtensions(query: string): Promise<Anime[]> { export async function getStreamData(extension: Extension, animeData: Anime, episode: string, server?: string, category?: string): Promise<StreamData> {
const animeExtensions = getAnimeExtensionsMap(); const providerName = extension.constructor.name;
for (const [name, ext] of animeExtensions) { const cacheKey = `anime:stream:${providerName}:${animeData.id}:${episode}:${server || 'default'}:${category || 'sub'}`;
const results = await searchAnimeInExtension(ext, name, query);
if (results.length > 0) return results; 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<StreamData> {
const searchOptions = { const searchOptions = {
query: animeData.title.english || animeData.title.romaji, query: animeData.title.english || animeData.title.romaji,
dub: category === 'dub', dub: category === 'dub',
@@ -162,5 +240,6 @@ export async function getStreamData(extension: Extension, animeData: Anime, epis
const serverName = server || "default"; const serverName = server || "default";
const streamData = await extension.findEpisodeServer(targetEp, serverName); const streamData = await extension.findEpisodeServer(targetEp, serverName);
await setCache(cacheKey, streamData, CACHE_TTL_MS);
return streamData; return streamData;
} }

View File

@@ -1,7 +1,7 @@
import {FastifyReply, FastifyRequest} from 'fastify'; import {FastifyReply, FastifyRequest} from 'fastify';
import * as booksService from './books.service'; import * as booksService from './books.service';
import { getExtension } from '../shared/extensions'; import {getExtension} from '../shared/extensions';
import { BookRequest, SearchRequest, ChapterRequest } from '../types'; import {BookRequest, ChapterRequest, SearchRequest} from '../types';
export async function getBook(req: BookRequest, reply: FastifyReply) { export async function getBook(req: BookRequest, reply: FastifyReply) {
try { try {
@@ -12,11 +12,10 @@ export async function getBook(req: BookRequest, reply: FastifyReply) {
if (source === 'anilist') { if (source === 'anilist') {
book = await booksService.getBookById(id); book = await booksService.getBookById(id);
} else { } else {
const extensionName = source; const ext = getExtension(source);
const ext = getExtension(extensionName);
const results = await booksService.searchBooksInExtension(ext, extensionName, id.replace(/--/g, '\u0000').replace(/-/g, ' ').replace(new RegExp('\u0000', 'g'), '-').trim()); const result = await booksService.getBookInfoExtension(ext, id);
book = results[0] || null; book = result || null;
} }
return book; return book;
@@ -60,8 +59,7 @@ export async function searchBooks(req: SearchRequest, reply: FastifyReply) {
return { results: anilistResults }; return { results: anilistResults };
} }
const extResults = await booksService.searchBooksExtensions(query); return { results: [] };
return { results: extResults };
} catch (e) { } catch (e) {
const error = e as Error; 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) { export async function getChapters(req: BookRequest, reply: FastifyReply) {
try { try {
const { id } = req.params; const { id } = req.params;

View File

@@ -6,6 +6,7 @@ async function booksRoutes(fastify: FastifyInstance) {
fastify.get('/books/trending', controller.getTrending); fastify.get('/books/trending', controller.getTrending);
fastify.get('/books/popular', controller.getPopular); fastify.get('/books/popular', controller.getPopular);
fastify.get('/search/books', controller.searchBooks); fastify.get('/search/books', controller.searchBooks);
fastify.get('/search/books/:extension', controller.searchBooksInExtension);
fastify.get('/book/:id/chapters', controller.getChapters); fastify.get('/book/:id/chapters', controller.getChapters);
fastify.get('/book/:bookId/:chapter/:provider', controller.getChapterContent); fastify.get('/book/:bookId/:chapter/:provider', controller.getChapterContent);
} }

View File

@@ -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 { getAllExtensions, getBookExtensionsMap } from '../shared/extensions';
import { Book, Extension, ChapterWithProvider, ChapterContent } from '../types'; import { Book, Extension, ChapterWithProvider, ChapterContent } from '../types';
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
export async function getBookById(id: string | number): Promise<Book | { error: string }> { export async function getBookById(id: string | number): Promise<Book | { error: string }> {
const row = await queryOne("SELECT full_data FROM books WHERE id = ?", [id]); const row = await queryOne("SELECT full_data FROM books WHERE id = ?", [id]);
@@ -108,10 +110,39 @@ export async function searchBooksAniList(query: string): Promise<Book[]> {
return []; return [];
} }
export async function getBookInfoExtension(ext: Extension | null, id: string): Promise<Book[]> {
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<Book[]> { export async function searchBooksInExtension(ext: Extension | null, name: string, query: string): Promise<Book[]> {
if (!ext) return []; if (!ext) return [];
if ((ext.type === 'book-board' || ext.type === 'manga-board') && ext.search) { if ((ext.type === 'book-board') && ext.search) {
try { try {
console.log(`[${name}] Searching for book: ${query}`); console.log(`[${name}] Searching for book: ${query}`);
const matches = await ext.search({ 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 }, title: { romaji: m.title, english: m.title, native: null },
coverImage: { large: m.image || '' }, coverImage: { large: m.image || '' },
averageScore: m.rating || m.score || null, averageScore: m.rating || m.score || null,
format: 'MANGA', format: m.format,
seasonYear: null, seasonYear: null,
isExtensionResult: true isExtensionResult: true
})); }));
@@ -143,17 +174,6 @@ export async function searchBooksInExtension(ext: Extension | null, name: string
return []; return [];
} }
export async function searchBooksExtensions(query: string): Promise<Book[]> {
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<Book | null> { async function fetchBookMetadata(id: string): Promise<Book | null> {
try { try {
const query = `query ($id: Int) { const query = `query ($id: Int) {
@@ -177,44 +197,68 @@ async function fetchBookMetadata(id: string): Promise<Book | null> {
} }
} }
async function searchChaptersInExtension(ext: Extension, name: string, searchTitle: string, bookData: Book | null): Promise<ChapterWithProvider[]> { async function searchChaptersInExtension(ext: Extension, name: string, searchTitle: string, search: boolean): Promise<ChapterWithProvider[]> {
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 { try {
console.log(`[${name}] Searching chapters for: ${searchTitle}`); console.log(`[${name}] Searching chapters for: ${searchTitle}`);
let mediaId: string;
if (search) {
const matches = await ext.search!({ const matches = await ext.search!({
query: searchTitle, query: searchTitle,
media: bookData ? { media: {
romajiTitle: bookData.title.romaji,
englishTitle: bookData.title.english || "",
startDate: bookData.startDate || { year: 0, month: 0, day: 0 }
} : {
romajiTitle: searchTitle, romajiTitle: searchTitle,
englishTitle: searchTitle, englishTitle: searchTitle,
startDate: { year: 0, month: 0, day: 0 } startDate: { year: 0, month: 0, day: 0 }
} }
}); });
if (!matches?.length) { const best = matches?.[0];
console.log(`[${name}] No matches found for book.`);
return []; 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!(mediaId);
const chaps = await ext.findChapters!(best.id);
if (!chaps?.length) { if (!chaps?.length) {
return []; return [];
} }
console.log(`[${name}] Found ${chaps.length} chapters.`); console.log(`[${name}] Found ${chaps.length} chapters.`);
const result: ChapterWithProvider[] = chaps.map((ch) => ({
return chaps.map((ch) => ({
id: ch.id, id: ch.id,
number: parseFloat(ch.number.toString()), number: parseFloat(ch.number.toString()),
title: ch.title, title: ch.title,
date: ch.releaseDate, date: ch.releaseDate,
provider: name provider: name
})); }));
await setCache(cacheKey, result, CACHE_TTL_MS);
return result;
} catch (e) { } catch (e) {
const error = e as Error; const error = e as Error;
console.error(`Failed to fetch chapters from ${name}:`, error.message); 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[] }> { export async function getChaptersForBook(id: string): Promise<{ chapters: ChapterWithProvider[] }> {
let bookData: Book | null = null; let bookData: Book | null = null;
let searchTitle: string | null = null; let searchTitle: string = "";
if (isNaN(Number(id))) { 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); bookData = await fetchBookMetadata(id);
}
if (!bookData) { if (!bookData) {
return { chapters: [] }; return { chapters: [] };
} }
const titles = [bookData.title.english, bookData.title.romaji].filter(Boolean) as string[]; const titles = [bookData.title.english, bookData.title.romaji].filter(Boolean) as string[];
searchTitle = titles[0]; searchTitle = titles[0];
} }
const allChapters: ChapterWithProvider[] = [];
const bookExtensions = getBookExtensionsMap(); const bookExtensions = getBookExtensionsMap();
const searchPromises = Array.from(bookExtensions.entries()) let extension;
.filter(([_, ext]) => ext.search && ext.findChapters) if (!searchTitle) {
.map(async ([name, ext]) => { for (const [name, ext] of bookExtensions) {
const chapters = await searchChaptersInExtension(ext, name, searchTitle!, bookData); const title = await getExtensionTitle(name, id)
allChapters.push(...chapters); 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 { return {
chapters: allChapters.sort((a, b) => Number(a.number) - Number(b.number)) 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"); 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); const chapterList = await getChaptersForBook(bookId);
if (!chapterList?.chapters || chapterList.chapters.length === 0) { 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"); throw new Error("Extension doesn't support findChapterPages");
} }
let contentResult: ChapterContent;
if (ext.mediaType === "manga") { if (ext.mediaType === "manga") {
const pages = await ext.findChapterPages(chapterId); const pages = await ext.findChapterPages(chapterId);
return { contentResult = {
type: "manga", type: "manga",
chapterId, chapterId,
title: chapterTitle, title: chapterTitle,
@@ -309,11 +375,9 @@ export async function getChapterContent(bookId: string, chapterIndex: string, pr
provider: providerName, provider: providerName,
pages pages
}; };
} } else if (ext.mediaType === "ln") {
if (ext.mediaType === "ln") {
const content = await ext.findChapterPages(chapterId); const content = await ext.findChapterPages(chapterId);
return { contentResult = {
type: "ln", type: "ln",
chapterId, chapterId,
title: chapterTitle, title: chapterTitle,
@@ -321,9 +385,14 @@ export async function getChapterContent(bookId: string, chapterIndex: string, pr
provider: providerName, provider: providerName,
content content
}; };
} else {
throw new Error("Unknown mediaType");
} }
throw new Error("Unknown mediaType"); await setCache(contentCacheKey, contentResult, CACHE_TTL_MS);
return contentResult;
} catch (err) { } catch (err) {
const error = err as Error; const error = err as Error;
console.error(`[Chapter] Error loading from ${providerName}:`, error.message); console.error(`[Chapter] Error loading from ${providerName}:`, error.message);

View File

@@ -31,26 +31,44 @@ async function loadAnime() {
const res = await fetch(fetchUrl); const res = await fetch(fetchUrl);
const data = await res.json(); const data = await res.json();
if(data.error) { if (data.error) {
document.getElementById('title').innerText = "Anime Not Found"; document.getElementById('title').innerText = "Anime Not Found";
return; 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.title = `${title} | WaifuBoard`;
document.getElementById('title').innerText = title; 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); handleDescription(rawDesc);
document.getElementById('score').innerText = (data.averageScore || '?') + '% Score'; const score = extensionName ? (data.score ? data.score * 10 : '?') : data.averageScore;
document.getElementById('year').innerText = data.seasonYear || data.startDate?.year || '????'; document.getElementById('score').innerText = (score || '?') + '% Score';
document.getElementById('genres').innerText = data.genres?.length > 0 ? data.genres.slice(0, 3).join(' • ') : '';
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('format').innerText = data.format || 'TV';
document.getElementById('status').innerText = data.status || 'Unknown'; document.getElementById('status').innerText = data.status || 'Unknown';
const extensionPill = document.getElementById('extension-pill'); const extensionPill = document.getElementById('extension-pill');
if (extensionName && extensionPill) { if (extensionName && extensionPill) {
extensionPill.textContent = `${extensionName.charAt(0).toUpperCase() + extensionName.slice(1).toLowerCase()}`; extensionPill.textContent = `${extensionName.charAt(0).toUpperCase() + extensionName.slice(1).toLowerCase()}`;
@@ -60,6 +78,9 @@ async function loadAnime() {
} }
let seasonText = ''; let seasonText = '';
if (extensionName) {
seasonText = data.season || 'Unknown';
} else {
if (data.season && data.seasonYear) { if (data.season && data.seasonYear) {
seasonText = `${data.season} ${data.seasonYear}`; seasonText = `${data.season} ${data.seasonYear}`;
} else if (data.startDate?.year) { } else if (data.startDate?.year) {
@@ -68,37 +89,42 @@ async function loadAnime() {
const estimatedSeason = months[month] || ''; const estimatedSeason = months[month] || '';
seasonText = `${estimatedSeason} ${data.startDate.year}`.trim(); seasonText = `${estimatedSeason} ${data.startDate.year}`.trim();
} }
}
document.getElementById('season').innerText = seasonText || 'Unknown'; document.getElementById('season').innerText = seasonText || 'Unknown';
let studioName = 'Unknown Studio'; const studio = extensionName
if (data.studios?.nodes?.length > 0) { ? data.studio || "Unknown"
studioName = data.studios.nodes[0].name; : (data.studios?.nodes?.[0]?.name ||
} else if (data.studios?.edges?.length > 0) { data.studios?.edges?.[0]?.node?.name ||
studioName = data.studios.edges[0]?.node?.name || 'Unknown Studio'; 'Unknown Studio');
}
document.getElementById('studio').innerText = studioName; document.getElementById('studio').innerText = studio;
const charContainer = document.getElementById('char-list'); const charContainer = document.getElementById('char-list');
charContainer.innerHTML = ''; charContainer.innerHTML = '';
let characters = []; let characters = [];
if (extensionName) {
characters = data.characters || [];
} else {
if (data.characters?.nodes?.length > 0) { if (data.characters?.nodes?.length > 0) {
characters = data.characters.nodes.slice(0, 5); characters = data.characters.nodes.slice(0, 5);
} } else if (data.characters?.edges?.length > 0) {
else if (data.characters?.edges?.length > 0) {
characters = data.characters.edges characters = data.characters.edges
.filter(edge => edge?.node?.name?.full) .filter(edge => edge?.node?.name?.full)
.slice(0, 5) .slice(0, 5)
.map(edge => edge.node); .map(edge => edge.node);
} }
}
if (characters.length > 0) { if (characters.length > 0) {
characters.forEach(char => { characters.slice(0, 5).forEach(char => {
if (char?.name?.full) { const name = char?.name?.full || char?.name;
if (name) {
charContainer.innerHTML += ` charContainer.innerHTML += `
<div class="character-item"> <div class="character-item">
<div class="char-dot"></div> ${char.name.full} <div class="char-dot"></div> ${name}
</div>`; </div>`;
} }
}); });
@@ -120,32 +146,27 @@ async function loadAnime() {
width: '100%', width: '100%',
videoId: data.trailer.id, videoId: data.trailer.id,
playerVars: { playerVars: {
'autoplay': 1, 'controls': 0, 'mute': 1, autoplay: 1, controls: 0, mute: 1,
'loop': 1, 'playlist': data.trailer.id, loop: 1, playlist: data.trailer.id,
'showinfo': 0, 'modestbranding': 1, 'disablekb': 1 showinfo: 0, modestbranding: 1, disablekb: 1
}, },
events: { 'onReady': (e) => e.target.playVideo() } events: { onReady: (e) => e.target.playVideo() }
}); });
}; };
} else { } else {
const banner = data.bannerImage || data.coverImage?.extraLarge || ''; const banner = extensionName
if (banner) { ? (data.image || '')
document.querySelector('.video-background').innerHTML = `<img src="${banner}" style="width:100%; height:100%; object-fit:cover;">`; : (data.bannerImage || data.coverImage?.extraLarge || '');
}
}
let extensionEpisodes = []; if (banner) {
document.querySelector('.video-background').innerHTML =
`<img src="${banner}" style="width:100%; height:100%; object-fit:cover;">`;
}
}
if (extensionName) { if (extensionName) {
extensionEpisodes = await loadExtensionEpisodes(animeId, extensionName); totalEpisodes = data.episodes || 1;
if (extensionEpisodes.length > 0) {
totalEpisodes = extensionEpisodes.length;
} else { } else {
totalEpisodes = 1;
}
} else {
// MODO NORMAL (AniList)
if (data.nextAiringEpisode?.episode) { if (data.nextAiringEpisode?.episode) {
totalEpisodes = data.nextAiringEpisode.episode - 1; totalEpisodes = data.nextAiringEpisode.episode - 1;
} else if (data.episodes) { } 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) { function handleDescription(text) {
const tmp = document.createElement("DIV"); const tmp = document.createElement("DIV");
tmp.innerHTML = text; tmp.innerHTML = text;

View File

@@ -1,6 +1,7 @@
const searchInput = document.getElementById('search-input'); const searchInput = document.getElementById('search-input');
const searchResults = document.getElementById('search-results'); const searchResults = document.getElementById('search-results');
let searchTimeout; let searchTimeout;
let availableExtensions = [];
searchInput.addEventListener('input', (e) => { searchInput.addEventListener('input', (e) => {
const query = e.target.value; const query = e.target.value;
@@ -12,6 +13,7 @@ searchInput.addEventListener('input', (e) => {
return; return;
} }
searchTimeout = setTimeout(() => { searchTimeout = setTimeout(() => {
fetchSearh(query); fetchSearh(query);
}, 300); }, 300);
}); });
@@ -25,20 +27,48 @@ document.addEventListener('click', (e) => {
async function fetchSearh(query) { async function fetchSearh(query) {
try { try {
const res = await fetch(`/api/search?q=${encodeURIComponent(query.slice(0, 30))}`); let apiUrl = `/api/search?q=${encodeURIComponent(query.slice(0, 30))}`;
const data = await res.json(); let extensionName = null;
renderSearchResults(data.results); let finalQuery = query;
} catch (err) { console.error("Search Error:", err); }
}
function createSlug(text) { const parts = query.split(':');
if (!text) return ''; if (parts.length >= 2) {
return text const potentialExtension = parts[0].trim().toLowerCase();
.toLowerCase()
.trim() const foundExtension = availableExtensions.find(ext => ext.toLowerCase() === potentialExtension);
.replace(/-/g, '--')
.replace(/\s+/g, '-') if (foundExtension) {
.replace(/[^a-z0-9\-]/g, ''); 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) { function renderSearchResults(results) {
@@ -54,20 +84,12 @@ function renderSearchResults(results) {
const format = anime.format || 'TV'; const format = anime.format || 'TV';
let href; let href;
if (anime.isExtensionResult) { if (anime.isExtensionResult) {
const titleSlug = createSlug(title); href = `/anime/${anime.extensionName}/${anime.id}`;
console.log(title);
href = `/anime/${anime.extensionName}/${titleSlug}`;
} else { } else {
href = `/anime/${anime.id}`; href = `/anime/${anime.id}`;
} }
const extName = anime.extensionName?.charAt(0).toUpperCase() + anime.extensionName?.slice(1);
const extPill = anime.isExtensionResult
? `<span>• ${extName}</span>`
: '';
const item = document.createElement('a'); const item = document.createElement('a');
item.className = 'search-item'; item.className = 'search-item';
item.href = href; item.href = href;
@@ -79,7 +101,6 @@ function renderSearchResults(results) {
<span class="rating-pill">${rating}</span> <span class="rating-pill">${rating}</span>
<span>• ${year}</span> <span>• ${year}</span>
<span>• ${format}</span> <span>• ${format}</span>
${extPill}
</div> </div>
</div> </div>
`; `;
@@ -118,6 +139,14 @@ function onYouTubeIframeAPIReady() {
async function fetchContent(isUpdate = false) { async function fetchContent(isUpdate = false) {
try { 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 trendingRes = await fetch('/api/trending');
const trendingData = await trendingRes.json(); const trendingData = await trendingRes.json();
@@ -220,6 +249,7 @@ function renderList(id, list) {
const el = document.createElement('div'); const el = document.createElement('div');
el.className = 'card'; el.className = 'card';
el.dataset.id = anime.id; el.dataset.id = anime.id;
el.onclick = () => window.location.href = `/anime/${anime.id}`; el.onclick = () => window.location.href = `/anime/${anime.id}`;
el.innerHTML = ` el.innerHTML = `
<div class="card-img-wrap"><img src="${cover}" loading="lazy"></div> <div class="card-img-wrap"><img src="${cover}" loading="lazy"></div>

View File

@@ -23,115 +23,158 @@ document.getElementById('episode-label').innerText = `Episode ${currentEpisode}`
async function loadMetadata() { async function loadMetadata() {
try { try {
const extQuery = extName ? `?ext=${extName}` : ""; const extQuery = extName ? `?ext=${extName}` : "";
const res = await fetch(`/api/anime/${animeId}${extQuery}`); const res = await fetch(`/api/anime/${animeId}${extQuery}`);
const data = await res.json(); const data = await res.json();
if (!data.error) { if (data.error) {
const romajiTitle = data.title.romaji || data.title.english || 'Anime Title'; console.error("Error from API:", data.error);
return;
}
document.getElementById('anime-title-details').innerText = romajiTitle; const isAnilistFormat = data.title && (data.title.romaji || data.title.english);
document.getElementById('anime-title-details2').innerText = romajiTitle;
document.title = `Watching ${romajiTitle} - Ep ${currentEpisode}`; 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'); const tempDiv = document.createElement('div');
tempDiv.innerHTML = data.description || 'No description available.'; tempDiv.innerHTML = description;
document.getElementById('detail-description').innerText = document.getElementById('detail-description').innerText = tempDiv.textContent || tempDiv.innerText || 'No description available.';
tempDiv.textContent || tempDiv.innerText;
document.getElementById('detail-format').innerText = data.format || '--'; document.getElementById('detail-format').innerText = format;
document.getElementById('detail-score').innerText = document.getElementById('detail-score').innerText = averageScore;
data.averageScore ? `${data.averageScore}%` : '--'; document.getElementById('detail-season').innerText = season && seasonYear ? `${season} ${seasonYear}` : (season || seasonYear || '--');
document.getElementById('detail-cover-image').src = coverImage || '/default-cover.jpg';
const season = data.season if (Array.isArray(characters) && characters.length > 0) {
? data.season.charAt(0) + data.season.slice(1).toLowerCase() populateCharacters(characters, isAnilistFormat);
: '';
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) { if (!extName) {
totalEpisodes = data.episodes || 0; totalEpisodes = episodesCount;
if (totalEpisodes > 0) { if (totalEpisodes > 0) {
const simpleEpisodes = []; const simpleEpisodes = [];
for (let i = 1; i <= totalEpisodes; i++) { for (let i = 1; i <= totalEpisodes; i++) {
simpleEpisodes.push({ simpleEpisodes.push({
number: i, number: i,
title: null, title: null,
thumbnail: null, thumbnail: null,
isDub: false isDub: false
}); });
} }
populateEpisodeCarousel(simpleEpisodes); populateEpisodeCarousel(simpleEpisodes);
} }
} else { } 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) { await loadExtensionEpisodes();
populateEpisodeCarousel(data2);
}
} catch (e) {
console.error("Error cargando episodios por extensión:", e);
totalEpisodes = 0;
}
} }
if (currentEpisode >= totalEpisodes && totalEpisodes > 0) { if (currentEpisode >= totalEpisodes && totalEpisodes > 0) {
document.getElementById('next-btn').disabled = true; document.getElementById('next-btn').disabled = true;
} }
}
} catch (error) { } catch (error) {
console.error('Error loading metadata:', 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'); const list = document.getElementById('characters-list');
list.classList.remove('characters-list'); list.classList.remove('characters-list');
list.classList.add('characters-carousel'); list.classList.add('characters-carousel');
list.innerHTML = ''; list.innerHTML = '';
characterEdges.forEach(edge => { characters.forEach(item => {
const character = edge.node; let character, voiceActor;
const voiceActor = edge.voiceActors ? edge.voiceActors.find(va => va.language === 'Japanese' || va.language === 'English') : null;
if (isAnilistFormat) {
character = item.node;
voiceActor = item.voiceActors?.find(va => va.language === 'Japanese' || va.language === 'English');
} else {
character = item;
voiceActor = null;
}
if (!character) return;
if (character) {
const card = document.createElement('div'); const card = document.createElement('div');
card.classList.add('character-card'); card.classList.add('character-card');
const img = document.createElement('img'); const img = document.createElement('img');
img.classList.add('character-card-img'); img.classList.add('character-card-img');
img.src = character.image.large || character.image.medium || ''; img.src = character.image?.large || character.image?.medium || character.image || '';
img.alt = character.name.full || 'Character'; 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'); const details = document.createElement('div');
details.classList.add('character-details'); details.classList.add('character-details');
const name = document.createElement('p'); const name = document.createElement('p');
name.classList.add('character-name'); name.classList.add('character-name');
name.innerText = characterName; name.innerText = character.name?.full || character.name || '--';
const actor = document.createElement('p'); const actor = document.createElement('p');
actor.classList.add('actor-name'); actor.classList.add('actor-name');
if (vaName) { if (voiceActor?.name?.full) {
actor.innerText = `${vaName} (${voiceActor.language})`; actor.innerText = `${voiceActor.name.full} (${voiceActor.language})`;
} else { } else {
actor.innerText = 'Voice Actor: N/A'; actor.innerText = 'Voice Actor: N/A';
} }
@@ -141,20 +184,18 @@ function populateCharacters(characterEdges) {
card.appendChild(img); card.appendChild(img);
card.appendChild(details); card.appendChild(details);
list.appendChild(card); list.appendChild(card);
}
}); });
} }
function populateEpisodeCarousel(episodesData) { function populateEpisodeCarousel(episodesData) {
const carousel = document.getElementById('episode-carousel'); const carousel = document.getElementById('episode-carousel');
carousel.innerHTML = ''; carousel.innerHTML = '';
episodesData.forEach((ep, index) => { episodesData.forEach((ep, index) => {
const epNumber = ep.number || ep.episodeNumber || ep.id || (index + 1); const epNumber = ep.number || ep.episodeNumber || ep.id || (index + 1);
if (!epNumber) return; if (!epNumber) return;
const extParam = extName ? `?${extName}` : ""; const extParam = extName ? `?${extName}` : "";
const hasThumbnail = ep.thumbnail && ep.thumbnail.trim() !== ''; const hasThumbnail = ep.thumbnail && ep.thumbnail.trim() !== '';
const link = document.createElement('a'); const link = document.createElement('a');
@@ -162,19 +203,13 @@ function populateEpisodeCarousel(episodesData) {
link.classList.add('carousel-item'); link.classList.add('carousel-item');
link.dataset.episode = epNumber; link.dataset.episode = epNumber;
if (!hasThumbnail) { if (!hasThumbnail) link.classList.add('no-thumbnail');
link.classList.add('no-thumbnail'); if (parseInt(epNumber) === currentEpisode) link.classList.add('active-ep-carousel');
}
if (parseInt(epNumber) === currentEpisode) {
link.classList.add('active-ep-carousel');
}
const imgContainer = document.createElement('div'); const imgContainer = document.createElement('div');
imgContainer.classList.add('carousel-item-img-container'); imgContainer.classList.add('carousel-item-img-container');
if (hasThumbnail) { if (hasThumbnail) {
const img = document.createElement('img'); const img = document.createElement('img');
img.classList.add('carousel-item-img'); img.classList.add('carousel-item-img');
img.src = ep.thumbnail; img.src = ep.thumbnail;
@@ -188,11 +223,9 @@ function populateEpisodeCarousel(episodesData) {
info.classList.add('carousel-item-info'); info.classList.add('carousel-item-info');
const title = document.createElement('p'); const title = document.createElement('p');
title.innerText = `Ep ${epNumber}: ${ep.title || 'Untitled'}`; title.innerText = `Ep ${epNumber}: ${ep.title || 'Untitled'}`;
info.appendChild(title); info.appendChild(title);
link.appendChild(info); link.appendChild(info);
carousel.appendChild(link); carousel.appendChild(link);
}); });
@@ -282,14 +315,8 @@ function setAudioMode(mode) {
const dubOpt = document.getElementById('opt-dub'); const dubOpt = document.getElementById('opt-dub');
toggle.setAttribute('data-state', mode); toggle.setAttribute('data-state', mode);
subOpt.classList.toggle('active', mode === 'sub');
if (mode === 'sub') { dubOpt.classList.toggle('active', mode === 'dub');
subOpt.classList.add('active');
dubOpt.classList.remove('active');
} else {
subOpt.classList.remove('active');
dubOpt.classList.add('active');
}
} }
async function loadStream() { async function loadStream() {
@@ -323,7 +350,7 @@ async function loadStream() {
if (headers['Origin']) proxyUrl += `&origin=${encodeURIComponent(headers['Origin'])}`; if (headers['Origin']) proxyUrl += `&origin=${encodeURIComponent(headers['Origin'])}`;
if (headers['User-Agent']) proxyUrl += `&userAgent=${encodeURIComponent(headers['User-Agent'])}`; 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'; document.getElementById('loading-overlay').style.display = 'none';
} catch (error) { } catch (error) {
setLoading("Stream error. Check console."); 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'); const video = document.getElementById('player');
if (Hls.isSupported()) { if (Hls.isSupported()) {
if (hlsInstance) hlsInstance.destroy(); if (hlsInstance) hlsInstance.destroy();
hlsInstance = new Hls({ xhrSetup: (xhr) => xhr.withCredentials = false });
hlsInstance = new Hls({
xhrSetup: (xhr, url) => {
xhr.withCredentials = false;
}
});
hlsInstance.loadSource(url); hlsInstance.loadSource(url);
hlsInstance.attachMedia(video); hlsInstance.attachMedia(video);
} else if (video.canPlayType('application/vnd.apple.mpegurl')) { } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
@@ -350,52 +372,28 @@ function playVideo(url, subtitles) {
if (plyrInstance) plyrInstance.destroy(); if (plyrInstance) plyrInstance.destroy();
while (video.firstChild) { while (video.textTracks.length > 0) {
video.removeChild(video.firstChild); video.removeChild(video.textTracks[0]);
} }
if (subtitles && subtitles.length > 0) {
subtitles.forEach(sub => { subtitles.forEach(sub => {
if (!sub.url) return;
const track = document.createElement('track'); const track = document.createElement('track');
track.kind = 'captions'; track.kind = 'captions';
track.label = sub.language; track.label = sub.language || 'Unknown';
track.srclang = sub.language.slice(0, 2).toLowerCase(); track.srclang = (sub.language || '').slice(0, 2).toLowerCase();
track.src = sub.url; track.src = sub.url;
if (sub.default || sub.language?.toLowerCase().includes('english')) track.default = true;
if (sub.default || sub.language.toLowerCase().includes('english')) {
track.default = true;
}
video.appendChild(track); video.appendChild(track);
}); });
}
plyrInstance = new Plyr(video, { plyrInstance = new Plyr(video, {
captions: { captions: { active: true, update: true, language: 'en' },
active: true, controls: ['play-large', 'play', 'progress', 'current-time', 'duration', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', 'fullscreen'],
update: true,
language: 'en'
},
controls: [
'play-large',
'play',
'progress',
'current-time',
'duration',
'mute',
'volume',
'captions',
'settings',
'pip',
'airplay',
'fullscreen'
],
settings: ['captions', 'quality', 'speed'] settings: ['captions', 'quality', 'speed']
}); });
video.play().catch(error => { video.play().catch(() => console.log("Autoplay blocked"));
console.log("Autoplay blocked:", error);
});
} }
function setLoading(message) { function setLoading(message) {
@@ -414,7 +412,9 @@ document.getElementById('prev-btn').onclick = () => {
}; };
document.getElementById('next-btn').onclick = () => { document.getElementById('next-btn').onclick = () => {
if (currentEpisode < totalEpisodes || totalEpisodes === 0) {
window.location.href = `/watch/${animeId}/${currentEpisode + 1}${extParam}`; window.location.href = `/watch/${animeId}/${currentEpisode + 1}${extParam}`;
}
}; };
if (currentEpisode <= 1) { if (currentEpisode <= 1) {
@@ -423,3 +423,4 @@ if (currentEpisode <= 1) {
loadMetadata(); loadMetadata();
loadExtensions(); loadExtensions();

View File

@@ -4,35 +4,75 @@ let filteredChapters = [];
let currentPage = 1; let currentPage = 1;
const itemsPerPage = 12; const itemsPerPage = 12;
let extensionName = null; let extensionName = null;
let bookSlug = null;
async function init() { async function init() {
try { try {
const path = window.location.pathname; const path = window.location.pathname;
const parts = path.split("/").filter(Boolean); const parts = path.split("/").filter(Boolean);
let bookId; let currentBookId;
if (parts.length === 3) { if (parts.length === 3) {
extensionName = parts[1]; extensionName = parts[1];
bookId = parts[2]; bookSlug = parts[2];
currentBookId = bookSlug;
} else { } else {
bookId = parts[1]; currentBookId = parts[1];
} }
const idForFetch = currentBookId;
const fetchUrl = extensionName const fetchUrl = extensionName
? `/api/book/${bookId.slice(0,40)}?ext=${extensionName}` ? `/api/book/${idForFetch.slice(0,40)}?ext=${extensionName}`
: `/api/book/${bookId}`; : `/api/book/${idForFetch}`;
const res = await fetch(fetchUrl); const res = await fetch(fetchUrl);
const data = await res.json(); const data = await res.json();
console.log(data); console.log(data);
if (data.error) { if (data.error || !data) {
const titleEl = document.getElementById('title'); const titleEl = document.getElementById('title');
if (titleEl) titleEl.innerText = "Book Not Found"; if (titleEl) titleEl.innerText = "Book Not Found";
return; 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`; document.title = `${title} | WaifuBoard Books`;
const titleEl = document.getElementById('title'); const titleEl = document.getElementById('title');
@@ -47,62 +87,52 @@ async function init() {
} }
const descEl = document.getElementById('description'); const descEl = document.getElementById('description');
if (descEl) descEl.innerHTML = data.description || "No description available."; if (descEl) descEl.innerHTML = description;
const scoreEl = document.getElementById('score'); 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'); const pubEl = document.getElementById('published-date');
if (pubEl) { if (pubEl) pubEl.innerText = year;
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 = '????';
}
}
const statusEl = document.getElementById('status'); const statusEl = document.getElementById('status');
if (statusEl) statusEl.innerText = data.status || 'Unknown'; if (statusEl) statusEl.innerText = status;
const formatEl = document.getElementById('format'); const formatEl = document.getElementById('format');
if (formatEl) formatEl.innerText = data.format || 'MANGA'; if (formatEl) formatEl.innerText = format;
const chaptersEl = document.getElementById('chapters'); const chaptersEl = document.getElementById('chapters');
if (chaptersEl) chaptersEl.innerText = data.chapters || '?'; if (chaptersEl) chaptersEl.innerText = chapters;
const genresEl = document.getElementById('genres'); const genresEl = document.getElementById('genres');
if(genresEl && data.genres) { if(genresEl) {
genresEl.innerText = data.genres.slice(0, 3).join(' • '); genresEl.innerText = genres;
} }
const img = data.coverImage.extraLarge || data.coverImage.large;
const posterEl = document.getElementById('poster'); const posterEl = document.getElementById('poster');
if (posterEl) posterEl.src = img; if (posterEl) posterEl.src = poster;
const heroBgEl = document.getElementById('hero-bg'); const heroBgEl = document.getElementById('hero-bg');
if (heroBgEl) heroBgEl.src = data.bannerImage || img; if (heroBgEl) heroBgEl.src = banner;
loadChapters(); loadChapters(idForFetch);
} catch (err) { } catch (err) {
console.error("Metadata Error:", err); console.error("Metadata Error:", err);
} }
} }
async function loadChapters() { async function loadChapters(idForFetch) {
const tbody = document.getElementById('chapters-body'); const tbody = document.getElementById('chapters-body');
if (!tbody) return; if (!tbody) return;
tbody.innerHTML = '<tr><td colspan="4" style="text-align:center; padding: 2rem;">Searching extensions for chapters...</td></tr>'; tbody.innerHTML = '<tr><td colspan="4" style="text-align:center; padding: 2rem;">Searching extensions for chapters...</td></tr>';
try { try {
const fetchUrl = extensionName const fetchUrl = extensionName
? `/api/book/${bookId.slice(0, 40)}/chapters` ? `/api/book/${idForFetch.slice(0, 40)}/chapters`
: `/api/book/${bookId}/chapters`; : `/api/book/${idForFetch}/chapters`;
const res = await fetch(fetchUrl); const res = await fetch(fetchUrl);
const data = await res.json(); const data = await res.json();
@@ -124,10 +154,11 @@ async function loadChapters() {
const readBtn = document.getElementById('read-start-btn'); const readBtn = document.getElementById('read-start-btn');
if (readBtn && filteredChapters.length > 0) { 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) { } catch (err) {
tbody.innerHTML = '<tr><td colspan="4" style="text-align:center; color: #ef4444;">Error loading chapters.</td></tr>'; tbody.innerHTML = '<tr><td colspan="4" style="text-align:center; color: #ef4444;">Error loading chapters.</td></tr>';
@@ -153,6 +184,19 @@ function populateProviderFilter() {
select.appendChild(opt); 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) => { select.onchange = (e) => {
const selected = e.target.value; const selected = e.target.value;
if (selected === 'all') { if (selected === 'all') {
@@ -161,12 +205,13 @@ function populateProviderFilter() {
filteredChapters = allChapters.filter(ch => ch.provider === selected); filteredChapters = allChapters.filter(ch => ch.provider === selected);
} }
currentPage = 1; currentPage = 1;
renderTable(); const idForFetch = extensionName ? bookSlug : bookId;
renderTable(idForFetch);
}; };
} }
} }
function renderTable() { function renderTable(idForFetch) {
const tbody = document.getElementById('chapters-body'); const tbody = document.getElementById('chapters-body');
if (!tbody) return; if (!tbody) return;
@@ -223,8 +268,10 @@ function updatePagination() {
prevBtn.disabled = currentPage === 1; prevBtn.disabled = currentPage === 1;
nextBtn.disabled = currentPage >= totalPages; nextBtn.disabled = currentPage >= totalPages;
prevBtn.onclick = () => { currentPage--; renderTable(); }; const idForFetch = extensionName ? bookSlug : bookId;
nextBtn.onclick = () => { currentPage++; renderTable(); };
prevBtn.onclick = () => { currentPage--; renderTable(idForFetch); };
nextBtn.onclick = () => { currentPage++; renderTable(idForFetch); };
} }
function openReader(bookId, chapterId, provider) { function openReader(bookId, chapterId, provider) {

View File

@@ -1,6 +1,7 @@
let trendingBooks = []; let trendingBooks = [];
let currentHeroIndex = 0; let currentHeroIndex = 0;
let heroInterval; let heroInterval;
let availableExtensions = [];
window.addEventListener('scroll', () => { window.addEventListener('scroll', () => {
const nav = document.getElementById('navbar'); const nav = document.getElementById('navbar');
@@ -37,25 +38,51 @@ document.addEventListener('click', (e) => {
async function fetchBookSearch(query) { async function fetchBookSearch(query) {
try { 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(); const data = await res.json();
renderSearchResults(data.results || []);
const resultsWithExtension = data.results.map(book => {
if (extensionName) {
return { ...book,
isExtensionResult: true,
extensionName: extensionName
};
}
return book;
});
renderSearchResults(resultsWithExtension || []);
} catch (err) { } catch (err) {
console.error("Search Error:", err); console.error("Search Error:", err);
renderSearchResults([]); renderSearchResults([]);
} }
} }
function createSlug(text) {
if (!text) return '';
return text
.toLowerCase()
.trim()
.replace(/-/g, '--')
.replace(/\s+/g, '-')
.replace(/[^a-z0-9\-]/g, '');
}
function renderSearchResults(results) { function renderSearchResults(results) {
searchResults.innerHTML = ''; searchResults.innerHTML = '';
@@ -65,25 +92,18 @@ function renderSearchResults(results) {
results.forEach(book => { results.forEach(book => {
const title = book.title.english || book.title.romaji || "Unknown"; const title = book.title.english || book.title.romaji || "Unknown";
const img = (book.coverImage && (book.coverImage.medium || book.coverImage.large)) || ''; 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 year = book.seasonYear || (book.startDate ? book.startDate.year : '') || '????';
const format = book.format || 'MANGA'; 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
? `<span>${extName}</span>`
: '';
const item = document.createElement('a'); const item = document.createElement('a');
item.className = 'search-item'; item.className = 'search-item';
let href;
if (book.isExtensionResult) {
href = `/book/${book.extensionName}/${book.id}`;
} else {
href = `/book/${book.id}`;
}
item.href = href; item.href = href;
item.innerHTML = ` item.innerHTML = `
@@ -94,7 +114,6 @@ function renderSearchResults(results) {
<span class="rating-pill">${rating}</span> <span class="rating-pill">${rating}</span>
<span>• ${year}</span> <span>• ${year}</span>
<span>• ${format}</span> <span>• ${format}</span>
${extPill}
</div> </div>
</div> </div>
`; `;
@@ -117,6 +136,14 @@ function scrollCarousel(id, direction) {
async function init() { async function init() {
try { 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 res = await fetch('/api/books/trending');
const data = await res.json(); const data = await res.json();
@@ -167,6 +194,7 @@ function updateHeroUI(book) {
const readBtn = document.getElementById('read-btn'); const readBtn = document.getElementById('read-btn');
if (readBtn) { if (readBtn) {
readBtn.onclick = () => window.location.href = `/book/${book.id}`; readBtn.onclick = () => window.location.href = `/book/${book.id}`;
} }
} }
@@ -183,6 +211,7 @@ function renderList(id, list) {
const el = document.createElement('div'); const el = document.createElement('div');
el.className = 'card'; el.className = 'card';
el.onclick = () => { el.onclick = () => {
window.location.href = `/book/${book.id}`; window.location.href = `/book/${book.id}`;
}; };

View File

@@ -467,7 +467,7 @@ nextBtn.addEventListener('click', () => {
function updateURL(newChapter) { function updateURL(newChapter) {
chapter = newChapter; chapter = newChapter;
const newUrl = `/reader/${provider}/${chapter}/${bookId}`; const newUrl = `/read/${provider}/${chapter}/${bookId}`;
window.history.pushState({}, '', newUrl); window.history.pushState({}, '', newUrl);
} }

View File

@@ -7,9 +7,46 @@ const databases = new Map();
const DEFAULT_PATHS = { const DEFAULT_PATHS = {
anilist: path.join(__dirname, '..', 'metadata', 'anilist_anime.db'), 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) { function ensureFavoritesDB(dbPath) {
const dir = path.dirname(dbPath); const dir = path.dirname(dbPath);
@@ -25,7 +62,6 @@ function ensureFavoritesDB(dbPath) {
); );
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (!exists) { if (!exists) {
const schema = ` const schema = `
CREATE TABLE IF NOT EXISTS favorites ( CREATE TABLE IF NOT EXISTS favorites (
@@ -56,15 +92,11 @@ function ensureFavoritesDB(dbPath) {
const queries = []; const queries = [];
if (!hasHeaders) { if (!hasHeaders) {
queries.push( queries.push(`ALTER TABLE favorites ADD COLUMN headers TEXT NOT NULL DEFAULT ""`);
`ALTER TABLE favorites ADD COLUMN headers TEXT NOT NULL DEFAULT ""`
);
} }
if (!hasProvider) { if (!hasProvider) {
queries.push( queries.push(`ALTER TABLE favorites ADD COLUMN provider TEXT NOT NULL DEFAULT ""`);
`ALTER TABLE favorites ADD COLUMN provider TEXT NOT NULL DEFAULT ""`
);
} }
if (queries.length === 0) { if (queries.length === 0) {
@@ -91,6 +123,13 @@ function initDatabase(name = 'anilist', dbPath = null, readOnly = false) {
.catch(err => console.error("Error creando favorites:", err)); .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 mode = readOnly ? sqlite3.OPEN_READONLY : (sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE);
const db = new sqlite3.Database(finalPath, mode, (err) => { const db = new sqlite3.Database(finalPath, mode, (err) => {
@@ -102,12 +141,25 @@ function initDatabase(name = 'anilist', dbPath = null, readOnly = false) {
}); });
databases.set(name, db); 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; return db;
} }
function getDatabase(name = 'anilist') { function getDatabase(name = 'anilist') {
if (!databases.has(name)) { if (!databases.has(name)) {
return initDatabase(name, null, name === 'anilist');
const readOnly = (name === 'anilist');
return initDatabase(name, null, readOnly);
} }
return databases.get(name); 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 = { module.exports = {
initDatabase, initDatabase,
getDatabase, getDatabase,
queryOne, queryOne,
queryAll, queryAll,
run, run,
closeDatabase getCachedExtension,
cacheExtension,
getExtensionTitle,
deleteExtension,
closeDatabase,
getCache,
setCache
}; };

View File

@@ -1,6 +1,7 @@
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const os = require('os'); const os = require('os');
const { queryAll, run } = require('./database');
const extensions = new Map(); const extensions = new Map();
@@ -24,11 +25,16 @@ async function loadExtensions() {
delete require.cache[require.resolve(filePath)]; delete require.cache[require.resolve(filePath)];
const ExtensionClass = require(filePath); const ExtensionClass = require(filePath);
const instance = typeof ExtensionClass === 'function' const instance = typeof ExtensionClass === 'function'
? new ExtensionClass() ? new ExtensionClass()
: (ExtensionClass.default ? new ExtensionClass.default() : null); : (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; const name = instance.constructor.name;
extensions.set(name, instance); extensions.set(name, instance);
console.log(`📦 Loaded Extension: ${name}`); console.log(`📦 Loaded Extension: ${name}`);
@@ -40,6 +46,22 @@ async function loadExtensions() {
} }
console.log(`✅ Loaded ${extensions.size} extensions`); 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) { } catch (err) {
console.error("❌ Extension Scan Error:", err); console.error("❌ Extension Scan Error:", err);
} }

View File

@@ -61,6 +61,8 @@ export interface ExtensionSearchOptions {
} }
export interface ExtensionSearchResult { export interface ExtensionSearchResult {
format: string;
headers: any;
id: string; id: string;
title: string; title: string;
image?: string; image?: string;
@@ -88,6 +90,7 @@ export interface ChapterWithProvider extends Chapter {
} }
export interface Extension { export interface Extension {
getMetadata: any;
type: 'anime-board' | 'book-board' | 'manga-board'; type: 'anime-board' | 'book-board' | 'manga-board';
mediaType?: 'manga' | 'ln'; mediaType?: 'manga' | 'ln';
search?: (options: ExtensionSearchOptions) => Promise<ExtensionSearchResult[]>; search?: (options: ExtensionSearchOptions) => Promise<ExtensionSearchResult[]>;

View File

@@ -337,3 +337,25 @@ body {
width: 100%; border-radius: 0; max-height: 60vh; border: none; z-index: 2001; 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 */
}