diff --git a/src/books/books.controller.js b/src/books/books.controller.js index c7faff1..e969252 100644 --- a/src/books/books.controller.js +++ b/src/books/books.controller.js @@ -1,12 +1,26 @@ const booksService = require('./books.service'); +const {getExtension} = require("../shared/extensions"); async function getBook(req, reply) { try { const { id } = req.params; - const book = await booksService.getBookById(id); + const source = req.query.ext || 'anilist'; + + let book; + if (source === 'anilist') { + book = await booksService.getBookById(id); + } else { + const extensionName = source; + const ext = getExtension(extensionName); + + const results = await booksService.searchBooksInExtension(ext, extensionName, id.replaceAll("-", " ")); + book = results[0] || null; + } + return book; + } catch (err) { - return { error: "Fetch error" }; + return { error: err.toString() }; } } @@ -55,8 +69,7 @@ async function searchBooks(req, reply) { async function getChapters(req, reply) { try { const { id } = req.params; - const chapters = await booksService.getChaptersForBook(id); - return chapters; + return await booksService.getChaptersForBook(id); } catch (err) { return { chapters: [] }; } diff --git a/src/books/books.routes.js b/src/books/books.routes.js index 72637ac..bc9db00 100644 --- a/src/books/books.routes.js +++ b/src/books/books.routes.js @@ -1,7 +1,6 @@ const controller = require('./books.controller'); async function booksRoutes(fastify, options) { - fastify.get('/book/:id', controller.getBook); fastify.get('/books/trending', controller.getTrending); fastify.get('/books/popular', controller.getPopular); diff --git a/src/books/books.service.js b/src/books/books.service.js index 662c5ca..3c7a1a3 100644 --- a/src/books/books.service.js +++ b/src/books/books.service.js @@ -107,94 +107,121 @@ async function searchBooksAniList(query) { return []; } -async function searchBooksExtensions(query) { - const extensions = getAllExtensions(); +async function searchBooksInExtension(ext, name, query) { + if (!ext) return []; - for (const [name, ext] of extensions) { - if ((ext.type === 'book-board' || ext.type === 'manga-board') && ext.search) { - try { - console.log(`[${name}] Searching for book: ${query}`); - const matches = await ext.search({ - query: query, - media: { - romajiTitle: query, - englishTitle: query, - startDate: { year: 0, month: 0, day: 0 } - } - }); - - if (matches && matches.length > 0) { - return matches.map(m => ({ - id: m.id, - title: { romaji: m.title, english: m.title }, - coverImage: { large: m.image || '' }, - averageScore: m.rating || m.score || null, - format: 'MANGA', - seasonYear: null, - isExtensionResult: true - })); + if ((ext.type === 'book-board' || ext.type === 'manga-board') && ext.search) { + try { + console.log(`[${name}] Searching for book: ${query}`); + const matches = await ext.search({ + query: query, + media: { + romajiTitle: query, + englishTitle: query, + startDate: { year: 0, month: 0, day: 0 } } - } catch (e) { - console.error(`Extension search failed for ${name}:`, e); + }); + + if (matches && matches.length > 0) { + return matches.map(m => ({ + id: m.id, + extensionName: name, + title: { romaji: m.title, english: m.title }, + coverImage: { large: m.image || '' }, + averageScore: m.rating || m.score || null, + format: 'MANGA', + seasonYear: null, + isExtensionResult: true + })); } + } catch (e) { + console.error(`Extension search failed for ${name}:`, e); } } return []; } -async function getChaptersForBook(id) { - let bookData = await queryOne("SELECT full_data FROM books WHERE id = ?", [id]) - .then(row => row ? JSON.parse(row.full_data) : null) - .catch(() => null); +async function searchBooksExtensions(query) { + const extensions = getAllExtensions(); - if (!bookData) { - try { - const query = `query ($id: Int) { Media(id: $id, type: MANGA) { title { romaji english } startDate { year month day } } }`; - const res = await fetch('https://graphql.anilist.co', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ query, variables: { id: parseInt(id) } }) - }); - const d = await res.json(); - if(d.data?.Media) bookData = d.data.Media; - } catch(e) {} + for (const [name, ext] of extensions) { + const results = await searchBooksInExtension(ext, name, query); + if (results.length > 0) return results; } - if (!bookData) return { chapters: [] }; + return []; +} + +async function getChaptersForBook(id) { + let bookData = null; + let searchTitle = null; + + if (typeof id === "string" && isNaN(Number(id))) { + searchTitle = id.replaceAll("-", " "); + } else { + bookData = await queryOne("SELECT full_data FROM books WHERE id = ?", [id]) + .then(row => row ? JSON.parse(row.full_data) : null) + .catch(() => null); + + if (!bookData) { + try { + const query = `query ($id: Int) { + Media(id: $id, type: MANGA) { + title { romaji english } + startDate { year month day } + } + }`; + + const res = await fetch('https://graphql.anilist.co', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query, variables: { id: parseInt(id) } }) + }); + + const d = await res.json(); + if (d.data?.Media) bookData = d.data.Media; + } catch (e) {} + } + + if (!bookData) return { chapters: [] }; + + const titles = [bookData.title.english, bookData.title.romaji].filter(Boolean); + searchTitle = titles[0]; + } - const titles = [bookData.title.english, bookData.title.romaji].filter(t => t); - const searchTitle = titles[0]; const allChapters = []; const extensions = getAllExtensions(); const searchPromises = Array.from(extensions.entries()) - .filter(([name, ext]) => (ext.type === 'book-board' || ext.type === 'manga-board') && ext.search && ext.findChapters) + .filter(([_, ext]) => + (ext.type === 'book-board' || ext.type === 'manga-board') && + ext.search && ext.findChapters + ) .map(async ([name, ext]) => { try { console.log(`[${name}] Searching chapters for: ${searchTitle}`); const matches = await ext.search({ query: searchTitle, - media: { + media: bookData ? { romajiTitle: bookData.title.romaji, englishTitle: bookData.title.english, startDate: bookData.startDate - } + } : {} }); - if (matches && matches.length > 0) { + if (matches?.length) { const best = matches[0]; const chaps = await ext.findChapters(best.id); - if (chaps && chaps.length > 0) { + if (chaps?.length) { console.log(`[${name}] Found ${chaps.length} chapters.`); chaps.forEach(ch => { - const num = parseFloat(ch.number); allChapters.push({ id: ch.id, - number: num, + number: parseFloat(ch.number), title: ch.title, date: ch.releaseDate, provider: name @@ -284,6 +311,7 @@ module.exports = { searchBooksLocal, searchBooksAniList, searchBooksExtensions, + searchBooksInExtension, getChaptersForBook, getChapterContent }; \ No newline at end of file diff --git a/src/scripts/books/book.js b/src/scripts/books/book.js index 7e2d3a3..b9377ab 100644 --- a/src/scripts/books/book.js +++ b/src/scripts/books/book.js @@ -3,12 +3,28 @@ let allChapters = []; let filteredChapters = []; let currentPage = 1; const itemsPerPage = 12; +let extensionName = null; async function init() { try { - const res = await fetch(`/api/book/${bookId}`); + const path = window.location.pathname; + const parts = path.split("/").filter(Boolean); + let bookId; + + if (parts.length === 3) { + extensionName = parts[1]; + bookId = parts[2]; + } else { + bookId = parts[1]; + } + + const fetchUrl = extensionName + ? `/api/book/${bookId.slice(0,40)}?ext=${extensionName}` + : `/api/book/${bookId}`; + + const res = await fetch(fetchUrl); const data = await res.json(); - console.log(data) + console.log(data); if (data.error) { const titleEl = document.getElementById('title'); @@ -22,6 +38,14 @@ async function init() { const titleEl = document.getElementById('title'); if (titleEl) titleEl.innerText = title; + const extensionPill = document.getElementById('extension-pill'); + if (extensionName && extensionPill) { + extensionPill.textContent = `${extensionName.charAt(0).toUpperCase() + extensionName.slice(1).toLowerCase()}`; + extensionPill.style.display = 'inline-flex'; + } else if (extensionPill) { + extensionPill.style.display = 'none'; + } + const descEl = document.getElementById('description'); if (descEl) descEl.innerHTML = data.description || "No description available."; @@ -76,7 +100,12 @@ async function loadChapters() { tbody.innerHTML = '