From ca2ed4187ea441bec66784585fde9086d1d618a6 Mon Sep 17 00:00:00 2001 From: lenafx Date: Sun, 30 Nov 2025 21:18:19 +0100 Subject: [PATCH] added gallery section --- server.js | 2 + src/extensions/extensions.controller.ts | 7 +- src/extensions/extensions.routes.ts | 1 + src/gallery/gallery.controller.ts | 59 ++++++ src/gallery/gallery.routes.ts | 10 + src/gallery/gallery.service.ts | 104 ++++++++++ src/scripts/gallery/gallery.js | 251 ++++++++++++++++++++++++ src/scripts/gallery/image.js | 139 +++++++++++++ src/shared/extensions.js | 15 +- src/types.ts | 55 +++++- src/views/views.routes.ts | 10 + views/books.html | 2 +- views/css/gallery/gallery.css | 139 +++++++++++++ views/css/gallery/image.css | 145 ++++++++++++++ views/gallery-image.html | 60 ++++++ views/gallery.html | 77 ++++++++ views/index.html | 2 +- 17 files changed, 1072 insertions(+), 6 deletions(-) create mode 100644 src/gallery/gallery.controller.ts create mode 100644 src/gallery/gallery.routes.ts create mode 100644 src/gallery/gallery.service.ts create mode 100644 src/scripts/gallery/gallery.js create mode 100644 src/scripts/gallery/image.js create mode 100644 views/css/gallery/gallery.css create mode 100644 views/css/gallery/image.css create mode 100644 views/gallery-image.html create mode 100644 views/gallery.html diff --git a/server.js b/server.js index 3d232cb..cc2ed4d 100644 --- a/server.js +++ b/server.js @@ -11,6 +11,7 @@ const animeRoutes = require('./src/anime/anime.routes'); const booksRoutes = require('./src/books/books.routes'); const proxyRoutes = require('./src/shared/proxy/proxy.routes'); const extensionsRoutes = require('./src/extensions/extensions.routes'); +const galleryRoutes = require('./src/gallery/gallery.routes'); fastify.register(require('@fastify/static'), { root: path.join(__dirname, 'public'), @@ -35,6 +36,7 @@ fastify.register(animeRoutes, { prefix: '/api' }); fastify.register(booksRoutes, { prefix: '/api' }); fastify.register(proxyRoutes, { prefix: '/api' }); fastify.register(extensionsRoutes, { prefix: '/api' }); +fastify.register(galleryRoutes, { prefix: '/api' }); function startCppScraper() { const exePath = path.join( diff --git a/src/extensions/extensions.controller.ts b/src/extensions/extensions.controller.ts index 3f5500c..2d85fd4 100644 --- a/src/extensions/extensions.controller.ts +++ b/src/extensions/extensions.controller.ts @@ -1,5 +1,5 @@ import { FastifyReply, FastifyRequest } from 'fastify'; -import { getExtension, getExtensionsList, getAllExtensions, getBookExtensionsMap, getAnimeExtensionsMap } from '../shared/extensions'; +import { getExtension, getExtensionsList, getGalleryExtensionsMap, getBookExtensionsMap, getAnimeExtensionsMap } from '../shared/extensions'; import { ExtensionNameRequest } from '../types'; export async function getExtensions(req: FastifyRequest, reply: FastifyReply) { @@ -16,6 +16,11 @@ export async function getBookExtensions(req: FastifyRequest, reply: FastifyReply return { extensions: Array.from(bookExtensions.keys()) }; } +export async function getGalleryExtensions(req: FastifyRequest, reply: FastifyReply) { + const galleryExtensions = getGalleryExtensionsMap(); + return { extensions: Array.from(galleryExtensions.keys()) }; +} + export async function getExtensionSettings(req: ExtensionNameRequest, reply: FastifyReply) { const { name } = req.params; const ext = getExtension(name); diff --git a/src/extensions/extensions.routes.ts b/src/extensions/extensions.routes.ts index 44dc385..6a065fe 100644 --- a/src/extensions/extensions.routes.ts +++ b/src/extensions/extensions.routes.ts @@ -5,6 +5,7 @@ async function extensionsRoutes(fastify: FastifyInstance) { fastify.get('/extensions', controller.getExtensions); fastify.get('/extensions/anime', controller.getAnimeExtensions); fastify.get('/extensions/book', controller.getBookExtensions); + fastify.get('/extensions/gallery', controller.getGalleryExtensions); fastify.get('/extensions/:name/settings', controller.getExtensionSettings); } diff --git a/src/gallery/gallery.controller.ts b/src/gallery/gallery.controller.ts new file mode 100644 index 0000000..e61fe53 --- /dev/null +++ b/src/gallery/gallery.controller.ts @@ -0,0 +1,59 @@ +import {FastifyReply} from 'fastify'; +import * as galleryService from './gallery.service'; + +export async function search(req: any, reply: FastifyReply) { + try { + const query = req.query.q || ''; + const page = parseInt(req.query.page as string) || 1; + const perPage = parseInt(req.query.perPage as string) || 48; + + return await galleryService.searchGallery(query, page, perPage); + } catch (err) { + const error = err as Error; + console.error("Gallery Search Error:", error.message); + return { + results: [], + total: 0, + page: 1, + hasNextPage: false + }; + } +} + +export async function searchInExtension(req: any, reply: FastifyReply) { + try { + const provider = req.query.provider; + const query = req.query.q || ''; + const page = parseInt(req.query.page as string) || 1; + const perPage = parseInt(req.query.perPage as string) || 48; + + if (!provider) { + return reply.code(400).send({ error: "Missing provider" }); + } + + return await galleryService.searchInExtension(provider, query, page, perPage); + + } catch (err) { + console.error("Gallery SearchInExtension Error:", (err as Error).message); + + return { + results: [], + total: 0, + page: 1, + hasNextPage: false + }; + } +} + +export async function getInfo(req: any, reply: FastifyReply) { + try { + const { id } = req.params; + const provider = req.query.provider; + + return await galleryService.getGalleryInfo(id, provider); + } catch (err) { + const error = err as Error; + console.error("Gallery Info Error:", error.message); + return reply.code(404).send({ error: "Gallery item not found" }); + } +} \ No newline at end of file diff --git a/src/gallery/gallery.routes.ts b/src/gallery/gallery.routes.ts new file mode 100644 index 0000000..cf472c6 --- /dev/null +++ b/src/gallery/gallery.routes.ts @@ -0,0 +1,10 @@ +import { FastifyInstance } from 'fastify'; +import * as controller from './gallery.controller'; + +async function galleryRoutes(fastify: FastifyInstance) { + fastify.get('/gallery/search', controller.search); + fastify.get('/gallery/fetch/:id', controller.getInfo); + fastify.get('/gallery/search/provider', controller.searchInExtension); +} + +export default galleryRoutes; \ No newline at end of file diff --git a/src/gallery/gallery.service.ts b/src/gallery/gallery.service.ts new file mode 100644 index 0000000..1402577 --- /dev/null +++ b/src/gallery/gallery.service.ts @@ -0,0 +1,104 @@ +import { getAllExtensions, getExtension } from '../shared/extensions'; +import { GallerySearchResult, GalleryInfo } from '../types'; + +export async function searchGallery(query: string, page: number = 1, perPage: number = 48): Promise { + const extensions = getAllExtensions(); + + for (const [name, ext] of extensions) { + if (ext.type === 'image-board' && ext.search) { + const result = await searchInExtension(name, query, page, perPage); + console.log(result); + if (result.results.length > 0) { + return result; + } + } + } + + return { + total: 0, + next: 0, + previous: 0, + pages: 0, + page, + hasNextPage: false, + results: [] + }; +} + +export async function getGalleryInfo(id: string, providerName?: string): Promise { + const extensions = getAllExtensions(); + + if (providerName) { + const ext = extensions.get(providerName); + if (ext && ext.type === 'image-board' && ext.getInfo) { + try { + console.log(`[Gallery] Getting info from ${providerName} for: ${id}`); + const info = await ext.getInfo(id); + return { + ...info, + provider: providerName + }; + } catch (e) { + const error = e as Error; + console.error(`[Gallery] Failed to get info from ${providerName}:`, error.message); + throw new Error(`Failed to get gallery info from ${providerName}`); + } + } + throw new Error("Provider not found or doesn't support getInfo"); + } + + for (const [name, ext] of extensions) { + if (ext.type === 'gallery' && ext.getInfo) { + try { + console.log(`[Gallery] Trying to get info from ${name} for: ${id}`); + const info = await ext.getInfo(id); + return { + ...info, + provider: name + }; + } catch (e) { + continue; + } + } + } + + throw new Error("Gallery item not found in any extension"); +} + +export async function searchInExtension(providerName: string, query: string, page: number = 1, perPage: number = 48): Promise { + + const ext = getExtension(providerName); + + if (!ext || ext.type !== 'image-board' || !ext.search) { + throw new Error(`La extensión "${providerName}" no existe o no soporta búsqueda.`); + } + + try { + console.log(`[Gallery] Searching ONLY in ${providerName} for: ${query}`); + const results = await ext.search(query, page, perPage); + + const enrichedResults = (results?.results ?? []).map((r: any) => ({ + ...r, + provider: providerName + })); + + return { + ...results, + results: enrichedResults + }; + + } catch (e) { + const error = e as Error; + console.error(`[Gallery] Search failed in ${providerName}:`, error.message); + + return { + total: 0, + next: 0, + previous: 0, + pages: 0, + page, + hasNextPage: false, + results: [] + }; + } +} \ No newline at end of file diff --git a/src/scripts/gallery/gallery.js b/src/scripts/gallery/gallery.js new file mode 100644 index 0000000..6d7fa38 --- /dev/null +++ b/src/scripts/gallery/gallery.js @@ -0,0 +1,251 @@ +document.addEventListener('DOMContentLoaded', () => { + const providerSelector = document.getElementById('provider-selector'); + const searchInput = document.getElementById('gallery-search-input'); + const resultsContainer = document.getElementById('gallery-results'); + const loadMoreBtn = document.getElementById('load-more-btn'); + + let currentPage = 1; + let currentProvider = ''; + let currentQuery = ''; + const perPage = 48; + + let msnry = null; + + // --- MASONRY INITIALIZATION --- + function initializeMasonry() { + if (typeof Masonry === 'undefined') { + setTimeout(initializeMasonry, 100); + return; + } + + if (msnry) { + msnry.destroy(); + } + + msnry = new Masonry(resultsContainer, { + itemSelector: '.gallery-card', + columnWidth: '.gallery-card', + percentPosition: true, + gutter: 0, + transitionDuration: '0.4s' + }); + msnry.layout(); + } + + initializeMasonry(); + + // --- UTILS --- + + /** + * Crea un elemento de tarjeta de resultado de galería. + */ + function createGalleryCard(item) { + const card = document.createElement('a'); + card.className = 'gallery-card grid-item'; + + const itemProvider = item.provider || currentProvider; + + // ********************************************** + // CAMBIO: Nueva URL para la página de visualización + card.href = `/gallery/${itemProvider}/${item.id}`; + // ********************************************** + + const img = document.createElement('img'); + img.className = 'gallery-card-img'; + img.src = item.image; + img.alt = item.tags ? item.tags.join(', ') : 'Gallery Image'; + img.loading = 'lazy'; + + img.onload = () => { + if (msnry) { + msnry.layout(); + card.classList.add('is-loaded'); + } + }; + img.onerror = () => { + if (msnry) { + msnry.layout(); + card.classList.add('is-loaded'); + } + }; + + card.appendChild(img); + + return card; + } + + /** + * Muestra las tarjetas de esqueleto. (Sin cambios) + */ + function showSkeletons(count, append = false) { + const skeletonMarkup = ``; + + if (!append) { + resultsContainer.innerHTML = ''; + initializeMasonry(); + } + + let newElements = []; + for (let i = 0; i < count; i++) { + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = skeletonMarkup.trim(); + const skeletonEl = tempDiv.firstChild; + resultsContainer.appendChild(skeletonEl); + newElements.push(skeletonEl); + } + + if (msnry) { + msnry.appended(newElements); + msnry.layout(); + } + loadMoreBtn.style.display = 'none'; + } + + // --- FETCH DATA --- (Sin cambios en loadExtensions y searchGallery) + async function loadExtensions() { + try { + const response = await fetch('/api/extensions/gallery'); + const data = await response.json(); + + providerSelector.innerHTML = ''; + + if (data.extensions && data.extensions.length > 0) { + const defaultOption = document.createElement('option'); + defaultOption.value = ''; + defaultOption.textContent = 'Global Search'; + providerSelector.appendChild(defaultOption); + + data.extensions.forEach(ext => { + const option = document.createElement('option'); + option.value = ext; + option.textContent = ext; + providerSelector.appendChild(option); + }); + + currentProvider = ''; + + } else { + providerSelector.innerHTML = ''; + } + } catch (error) { + console.error('Error loading gallery extensions:', error); + providerSelector.innerHTML = ''; + } + } + + + async function searchGallery(isLoadMore = false) { + const query = searchInput.value.trim(); + const provider = providerSelector.value; + const page = isLoadMore ? currentPage + 1 : 1; + + if (!isLoadMore && currentQuery === query && currentProvider === provider && currentPage === 1) { + return; + } + + currentQuery = query; + currentProvider = provider; + + if (!isLoadMore) { + currentPage = 1; + showSkeletons(perPage, false); + } else { + showSkeletons(8, true); + } + + try { + let url; + if (provider && provider !== '') { + url = `/api/gallery/search/provider?provider=${provider}&q=${encodeURIComponent(query)}&page=${page}&perPage=${perPage}`; + } else { + url = `/api/gallery/search?q=${encodeURIComponent(query)}&page=${page}&perPage=${perPage}`; + } + + const response = await fetch(url); + const data = await response.json(); + + let newRealElements = []; + if (data.results && data.results.length > 0) { + data.results.forEach(item => { + newRealElements.push(createGalleryCard(item)); + }); + } + + const allSkeletons = Array.from(resultsContainer.querySelectorAll('.gallery-card.skeleton')); + let skeletonsToRemove = isLoadMore ? allSkeletons.slice(-8) : allSkeletons; + + if (msnry) { + msnry.remove(skeletonsToRemove); + skeletonsToRemove.forEach(s => s.remove()); + } + + if (newRealElements.length > 0) { + newRealElements.forEach(el => resultsContainer.appendChild(el)); + + if (msnry) { + msnry.appended(newRealElements); + } + currentPage = isLoadMore ? currentPage + 1 : 1; + + } else if (!isLoadMore) { + resultsContainer.innerHTML = '

No results found for this search.

'; + } + + if (msnry) { + msnry.layout(); + } + + if (data.hasNextPage) { + loadMoreBtn.style.display = 'block'; + } else { + loadMoreBtn.style.display = 'none'; + } + + } catch (error) { + console.error('Error during gallery search:', error); + if (!isLoadMore) { + resultsContainer.innerHTML = '

An error occurred while fetching results.

'; + } + loadMoreBtn.style.display = 'none'; + } + } + + + // --- EVENT LISTENERS (Sin cambios) --- + + providerSelector.addEventListener('change', () => { + searchGallery(false); + }); + + let searchTimeout; + searchInput.addEventListener('input', () => { + clearTimeout(searchTimeout); + searchTimeout = setTimeout(() => { + searchGallery(false); + }, 500); + }); + searchInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + clearTimeout(searchTimeout); + searchGallery(false); + } + }); + + loadMoreBtn.addEventListener('click', () => { + searchGallery(true); + }); + + loadExtensions().then(() => { + searchGallery(false); + }); + + const navbar = document.getElementById('navbar'); + window.addEventListener('scroll', () => { + if (window.scrollY > 50) { + navbar.classList.add('scrolled'); + } else { + navbar.classList.remove('scrolled'); + } + }); + +}); \ No newline at end of file diff --git a/src/scripts/gallery/image.js b/src/scripts/gallery/image.js new file mode 100644 index 0000000..ae6eb9b --- /dev/null +++ b/src/scripts/gallery/image.js @@ -0,0 +1,139 @@ +document.addEventListener('DOMContentLoaded', () => { + const itemContentContainer = document.getElementById('item-content'); + + // Función para parsear la URL y obtener :provider y :id + function getUrlParams() { + // La URL esperada es /gallery/providerName/itemId + const pathSegments = window.location.pathname.split('/').filter(segment => segment); + + // Verifica si la estructura es /gallery/provider/id + if (pathSegments.length >= 3 && pathSegments[0] === 'gallery') { + return { + provider: pathSegments[1], + id: pathSegments.slice(2).join('/') // El ID puede contener barras, así que unimos el resto + }; + } + return null; + } + + async function fetchGalleryItem(provider, id) { + try { + // Llama al endpoint de la API con los parámetros obtenidos + const url = `/api/gallery/fetch/${id}?provider=${provider}`; + const response = await fetch(url); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || `Failed to fetch item from ${provider}.`); + } + + const data = await response.json(); + + // Verifica los campos clave del nuevo formato de respuesta + if (!data.fullImage) { + throw new Error("Invalid item structure: Missing 'fullImage' URL."); + } + + // Normalizar los datos para la plantilla (usando 'publishedBy' como título si 'title' no existe) + data.title = data.publishedBy || data.id; + + renderItem(data); + + } catch (error) { + console.error("Fetch Gallery Item Error:", error); + renderError(`The requested item could not be found or an error occurred. (${error.message})`); + } + } + + /** + * Renderiza el contenido del ítem de la galería. + * @param {Object} item - Objeto de respuesta de la API. + */ + function renderItem(item) { + const title = item.title; + document.getElementById('page-title').textContent = `WaifuBoard - ${title}`; + + // Limpiar el esqueleto + itemContentContainer.innerHTML = ''; + + const imageUrl = item.fullImage || item.resizedImageUrl; // Usar fullImage para la más alta resolución + + const itemHTML = ` +
+ ${title} +
+
+
+ Source: ${item.provider} / Published By: ${item.publishedBy || 'N/A'} +

${title}

+
+ +
+

Tags

+
+
+
+ + + Download Full Image + +
+ `; + + itemContentContainer.innerHTML = itemHTML; + + // Renderizar Tags + const tagListContainer = document.getElementById('tag-list'); + if (item.tags && item.tags.length > 0) { + item.tags.forEach(tag => { + const tagEl = document.createElement('a'); + tagEl.className = 'tag-item'; + tagEl.textContent = tag; + // Enlazar a una búsqueda en la galería por el tag y el provider + tagEl.href = `/art?provider=${item.provider}&q=${encodeURIComponent(tag)}`; + tagListContainer.appendChild(tagEl); + }); + } else { + tagListContainer.innerHTML = '

No tags available.

'; + } + + // Habilitar fade-in de la imagen una vez que se carga + const mainImage = document.getElementById('main-image'); + mainImage.onload = () => { + mainImage.classList.add('loaded'); + }; + // Para imágenes que ya están en caché + if (mainImage.complete) { + mainImage.classList.add('loaded'); + } + } + + function renderError(message) { + itemContentContainer.innerHTML = ` +
+ +

Item Not Found

+

${message}

+ Go back to Gallery +
+ `; + } + + // --- Inicialización --- + const params = getUrlParams(); + if (params && params.provider && params.id) { + fetchGalleryItem(params.provider, params.id); + } else { + renderError("Invalid URL format. Expected: /gallery/<provider>/<id>"); + } + + // Estilo de Navbar (reutilizado) + const navbar = document.getElementById('navbar'); + window.addEventListener('scroll', () => { + if (window.scrollY > 50) { + navbar.classList.add('scrolled'); + } else { + navbar.classList.remove('scrolled'); + } + }); +}); \ No newline at end of file diff --git a/src/shared/extensions.js b/src/shared/extensions.js index b5fd166..1ca8cbc 100644 --- a/src/shared/extensions.js +++ b/src/shared/extensions.js @@ -28,7 +28,7 @@ async function loadExtensions() { ? new ExtensionClass() : (ExtensionClass.default ? new ExtensionClass.default() : null); - if (instance && (instance.type === "anime-board" || instance.type === "book-board")) { + if (instance && (instance.type === "anime-board" || instance.type === "book-board" || instance.type === "image-board")) { const name = instance.constructor.name; extensions.set(name, instance); console.log(`📦 Loaded Extension: ${name}`); @@ -77,11 +77,22 @@ function getBookExtensionsMap() { return bookExts; } +function getGalleryExtensionsMap() { + const galleryExts = new Map(); + for (const [name, ext] of extensions) { + if (ext.type === 'image-board') { + galleryExts.set(name, ext); + } + } + return galleryExts; +} + module.exports = { loadExtensions, getExtension, getAllExtensions, getExtensionsList, getAnimeExtensionsMap, - getBookExtensionsMap + getBookExtensionsMap, + getGalleryExtensionsMap }; \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index deb7fe4..75b6b0e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -202,4 +202,57 @@ export type ChapterRequest = FastifyRequest<{ export type ProxyRequest = FastifyRequest<{ Querystring: ProxyQuery; -}>; \ No newline at end of file +}>; + +export interface GalleryItemPreview { + id: string; + image: string; + tags: string[]; + type: 'preview'; + provider?: string; +} + +export interface GallerySearchResult { + total: number; + next: number; + previous: number; + pages: number; + page: number; + hasNextPage: boolean; + results: GalleryItemPreview[]; +} + +export interface GalleryInfo { + id: string; + fullImage: string; + resizedImageUrl: string; + tags: string[]; + createdAt: string | null; + publishedBy: string; + rating: string; + comments: any[]; + provider?: string; +} + +export interface GalleryExtension { + type: 'gallery'; + search: (query: string, page: number, perPage: number) => Promise; + getInfo: (id: string) => Promise>; +} + +export interface GallerySearchRequest extends FastifyRequest { + query: { + q?: string; + page?: string; + perPage?: string; + }; +} + +export interface GalleryInfoRequest extends FastifyRequest { + params: { + id: string; + }; + query: { + provider?: string; + }; +} \ No newline at end of file diff --git a/src/views/views.routes.ts b/src/views/views.routes.ts index 7b34e2e..ac33073 100644 --- a/src/views/views.routes.ts +++ b/src/views/views.routes.ts @@ -14,6 +14,11 @@ async function viewsRoutes(fastify: FastifyInstance) { reply.type('text/html').send(stream); }); + fastify.get('/gallery', (req: FastifyRequest, reply: FastifyReply) => { + const stream = fs.createReadStream(path.join(__dirname, '..', '..', 'views', 'gallery.html')); + reply.type('text/html').send(stream); + }); + fastify.get('/anime/:id', (req: FastifyRequest, reply: FastifyReply) => { const stream = fs.createReadStream(path.join(__dirname, '..', '..', 'views', 'anime.html')); reply.type('text/html').send(stream); @@ -43,6 +48,11 @@ async function viewsRoutes(fastify: FastifyInstance) { const stream = fs.createReadStream(path.join(__dirname, '..', '..', 'views', 'read.html')); reply.type('text/html').send(stream); }); + + fastify.get('/gallery/:extension/*', (req: FastifyRequest, reply: FastifyReply) => { + const stream = fs.createReadStream(path.join(__dirname, '..', '..', 'views', 'gallery-image.html')); + reply.type('text/html').send(stream); + }); } export default viewsRoutes; \ No newline at end of file diff --git a/views/books.html b/views/books.html index dcdbcb9..11f88f7 100644 --- a/views/books.html +++ b/views/books.html @@ -21,7 +21,7 @@