caching system + add extension entries to metadata pool
This commit is contained in:
@@ -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 |
|
||||||
|
|||||||
@@ -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' });
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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}`;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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[]>;
|
||||||
|
|||||||
@@ -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 */
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user