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 = 'Searching extensions for chapters...'; try { - const res = await fetch(`/api/book/${bookId}/chapters`); + const fetchUrl = extensionName + ? `/api/book/${bookId.slice(0, 40)}/chapters` + : `/api/book/${bookId}/chapters`; + + console.log(fetchUrl) + const res = await fetch(fetchUrl); const data = await res.json(); allChapters = data.chapters || []; @@ -202,7 +231,7 @@ function updatePagination() { function openReader(bookId, chapterId, provider) { const c = encodeURIComponent(chapterId); const p = encodeURIComponent(provider); - window.location.href = `/read/${bookId}/${c}/${p}`; + window.location.href = `/read/${p}/${c}/${bookId}`; } init(); \ No newline at end of file diff --git a/src/scripts/books/books.js b/src/scripts/books/books.js index 32bef62..47c2059 100644 --- a/src/scripts/books/books.js +++ b/src/scripts/books/books.js @@ -46,9 +46,19 @@ async function fetchBookSearch(query) { } } +function createSlug(text) { + if (!text) return ''; + return text + .toString() + .toLowerCase() + .trim() + .replace(/[^a-z0-9\s-]/g, '') + .replace(/[\s-]+/g, '-'); +} + function renderSearchResults(results) { searchResults.innerHTML = ''; - + if (!results || results.length === 0) { searchResults.innerHTML = '
No results found
'; } else { @@ -58,11 +68,24 @@ function renderSearchResults(results) { const rating = book.averageScore ? `${book.averageScore}%` : 'N/A'; const year = book.seasonYear || (book.startDate ? book.startDate.year : '') || '????'; const format = book.format || 'MANGA'; + let href; - const item = document.createElement('a'); + if (book.isExtensionResult) { + const titleSlug = createSlug(title); + href = `/book/${book.extensionName}/${titleSlug}`; + } else { + href = `/book/${book.id}`; + } + + const extName = book.extensionName.charAt(0).toUpperCase() + book.extensionName.slice(1); + const extPill = book.isExtensionResult + ? `${extName}` + : ''; + + const item = document.createElement('a'); item.className = 'search-item'; - item.href = `/book/${book.id}`; - + item.href = href; + item.innerHTML = ` ${title}
@@ -71,6 +94,7 @@ function renderSearchResults(results) { ${rating} • ${year} • ${format} + ${extPill}
`; @@ -80,7 +104,7 @@ function renderSearchResults(results) { } searchResults.classList.add('active'); - searchInput.style.borderRadius = '12px 12px 0 0'; + searchInput.style.borderRadius = '12px 12px 0 0'; } function scrollCarousel(id, direction) { diff --git a/src/scripts/books/reader.js b/src/scripts/books/reader.js index 5755039..8387847 100644 --- a/src/scripts/books/reader.js +++ b/src/scripts/books/reader.js @@ -35,9 +35,9 @@ let observer = null; const parts = window.location.pathname.split('/'); -const bookId = parts[2]; +const bookId = parts[4]; let chapter = parts[3]; -let provider = parts[4]; +let provider = parts[2]; function loadConfig() { try { @@ -125,7 +125,7 @@ async function loadChapter() { `; try { - const res = await fetch(`/api/book/${bookId}/${chapter}/${provider}`); + const res = await fetch(`/api/book/${bookId.slice(0,40)}/${chapter}/${provider}`); const data = await res.json(); if (data.title) { @@ -467,15 +467,22 @@ nextBtn.addEventListener('click', () => { function updateURL(newChapter) { chapter = newChapter; - const newUrl = `/reader/${bookId}/${chapter}/${provider}`; + const newUrl = `/reader/${provider}/${chapter}/${bookId}`; window.history.pushState({}, '', newUrl); } document.getElementById('back-btn').addEventListener('click', () => { const parts = window.location.pathname.split('/'); + const provider = parts[2]; + const mangaId = parts[4]; - const mangaId = parts[2]; - window.location.href = `/book/${mangaId}`; + const isInt = Number.isInteger(Number(mangaId)); + + if (isInt) { + window.location.href = `/book/${mangaId}`; + } else { + window.location.href = `/book/${provider}/${mangaId}`; + } }); settingsBtn.addEventListener('click', () => { diff --git a/src/views/views.routes.js b/src/views/views.routes.js index 96588ba..b0ee3ae 100644 --- a/src/views/views.routes.js +++ b/src/views/views.routes.js @@ -28,7 +28,12 @@ async function viewsRoutes(fastify, options) { reply.type('text/html').send(stream); }); - fastify.get('/read/:id/:chapter/:provider', (req, reply) => { + fastify.get('/book/:extension/*', (req, reply) => { + const stream = fs.createReadStream(path.join(__dirname, '..', '..', 'views', 'book.html')); + reply.type('text/html').send(stream); + }); + + fastify.get('/read/:provider/:chapter/*', (req, reply) => { const stream = fs.createReadStream(path.join(__dirname, '..', '..', 'views', 'read.html')); reply.type('text/html').send(stream); }); diff --git a/views/book.html b/views/book.html index 22406ee..c798ec6 100644 --- a/views/book.html +++ b/views/book.html @@ -59,6 +59,7 @@

Loading...

+
--% Score
Action