added book entries from extensions

This commit is contained in:
2025-11-28 21:08:25 +01:00
parent 791cb8f806
commit 03ebb5d88e
8 changed files with 179 additions and 73 deletions

View File

@@ -1,12 +1,26 @@
const booksService = require('./books.service'); const booksService = require('./books.service');
const {getExtension} = require("../shared/extensions");
async function getBook(req, reply) { async function getBook(req, reply) {
try { try {
const { id } = req.params; 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; return book;
} catch (err) { } catch (err) {
return { error: "Fetch error" }; return { error: err.toString() };
} }
} }
@@ -55,8 +69,7 @@ async function searchBooks(req, reply) {
async function getChapters(req, reply) { async function getChapters(req, reply) {
try { try {
const { id } = req.params; const { id } = req.params;
const chapters = await booksService.getChaptersForBook(id); return await booksService.getChaptersForBook(id);
return chapters;
} catch (err) { } catch (err) {
return { chapters: [] }; return { chapters: [] };
} }

View File

@@ -1,7 +1,6 @@
const controller = require('./books.controller'); const controller = require('./books.controller');
async function booksRoutes(fastify, options) { async function booksRoutes(fastify, options) {
fastify.get('/book/:id', controller.getBook); fastify.get('/book/:id', controller.getBook);
fastify.get('/books/trending', controller.getTrending); fastify.get('/books/trending', controller.getTrending);
fastify.get('/books/popular', controller.getPopular); fastify.get('/books/popular', controller.getPopular);

View File

@@ -107,10 +107,9 @@ async function searchBooksAniList(query) {
return []; return [];
} }
async function searchBooksExtensions(query) { async function searchBooksInExtension(ext, name, query) {
const extensions = getAllExtensions(); if (!ext) return [];
for (const [name, ext] of extensions) {
if ((ext.type === 'book-board' || ext.type === 'manga-board') && ext.search) { if ((ext.type === 'book-board' || ext.type === 'manga-board') && ext.search) {
try { try {
console.log(`[${name}] Searching for book: ${query}`); console.log(`[${name}] Searching for book: ${query}`);
@@ -126,6 +125,7 @@ async function searchBooksExtensions(query) {
if (matches && matches.length > 0) { if (matches && matches.length > 0) {
return matches.map(m => ({ return matches.map(m => ({
id: m.id, id: m.id,
extensionName: name,
title: { romaji: m.title, english: m.title }, title: { romaji: m.title, english: m.title },
coverImage: { large: m.image || '' }, coverImage: { large: m.image || '' },
averageScore: m.rating || m.score || null, averageScore: m.rating || m.score || null,
@@ -138,24 +138,47 @@ async function searchBooksExtensions(query) {
console.error(`Extension search failed for ${name}:`, e); console.error(`Extension search failed for ${name}:`, e);
} }
} }
return [];
}
async function searchBooksExtensions(query) {
const extensions = getAllExtensions();
for (const [name, ext] of extensions) {
const results = await searchBooksInExtension(ext, name, query);
if (results.length > 0) return results;
} }
return []; return [];
} }
async function getChaptersForBook(id) { async function getChaptersForBook(id) {
let bookData = await queryOne("SELECT full_data FROM books WHERE id = ?", [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) .then(row => row ? JSON.parse(row.full_data) : null)
.catch(() => null); .catch(() => null);
if (!bookData) { if (!bookData) {
try { try {
const query = `query ($id: Int) { Media(id: $id, type: MANGA) { title { romaji english } startDate { year month day } } }`; 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', { const res = await fetch('https://graphql.anilist.co', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query, variables: { id: parseInt(id) } }) body: JSON.stringify({ query, variables: { id: parseInt(id) } })
}); });
const d = await res.json(); const d = await res.json();
if (d.data?.Media) bookData = d.data.Media; if (d.data?.Media) bookData = d.data.Media;
} catch (e) {} } catch (e) {}
@@ -163,38 +186,42 @@ async function getChaptersForBook(id) {
if (!bookData) return { chapters: [] }; if (!bookData) return { chapters: [] };
const titles = [bookData.title.english, bookData.title.romaji].filter(t => t); const titles = [bookData.title.english, bookData.title.romaji].filter(Boolean);
const searchTitle = titles[0]; searchTitle = titles[0];
}
const allChapters = []; const allChapters = [];
const extensions = getAllExtensions(); const extensions = getAllExtensions();
const searchPromises = Array.from(extensions.entries()) 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]) => { .map(async ([name, ext]) => {
try { try {
console.log(`[${name}] Searching chapters for: ${searchTitle}`); console.log(`[${name}] Searching chapters for: ${searchTitle}`);
const matches = await ext.search({ const matches = await ext.search({
query: searchTitle, query: searchTitle,
media: { media: bookData ? {
romajiTitle: bookData.title.romaji, romajiTitle: bookData.title.romaji,
englishTitle: bookData.title.english, englishTitle: bookData.title.english,
startDate: bookData.startDate startDate: bookData.startDate
} } : {}
}); });
if (matches && matches.length > 0) { if (matches?.length) {
const best = matches[0]; const best = matches[0];
const chaps = await ext.findChapters(best.id); const chaps = await ext.findChapters(best.id);
if (chaps && chaps.length > 0) { if (chaps?.length) {
console.log(`[${name}] Found ${chaps.length} chapters.`); console.log(`[${name}] Found ${chaps.length} chapters.`);
chaps.forEach(ch => { chaps.forEach(ch => {
const num = parseFloat(ch.number);
allChapters.push({ allChapters.push({
id: ch.id, id: ch.id,
number: num, number: parseFloat(ch.number),
title: ch.title, title: ch.title,
date: ch.releaseDate, date: ch.releaseDate,
provider: name provider: name
@@ -284,6 +311,7 @@ module.exports = {
searchBooksLocal, searchBooksLocal,
searchBooksAniList, searchBooksAniList,
searchBooksExtensions, searchBooksExtensions,
searchBooksInExtension,
getChaptersForBook, getChaptersForBook,
getChapterContent getChapterContent
}; };

View File

@@ -3,12 +3,28 @@ let allChapters = [];
let filteredChapters = []; let filteredChapters = [];
let currentPage = 1; let currentPage = 1;
const itemsPerPage = 12; const itemsPerPage = 12;
let extensionName = null;
async function init() { async function init() {
try { 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(); const data = await res.json();
console.log(data) console.log(data);
if (data.error) { if (data.error) {
const titleEl = document.getElementById('title'); const titleEl = document.getElementById('title');
@@ -22,6 +38,14 @@ async function init() {
const titleEl = document.getElementById('title'); const titleEl = document.getElementById('title');
if (titleEl) titleEl.innerText = 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'); const descEl = document.getElementById('description');
if (descEl) descEl.innerHTML = data.description || "No description available."; if (descEl) descEl.innerHTML = data.description || "No description available.";
@@ -76,7 +100,12 @@ async function loadChapters() {
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 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(); const data = await res.json();
allChapters = data.chapters || []; allChapters = data.chapters || [];
@@ -202,7 +231,7 @@ function updatePagination() {
function openReader(bookId, chapterId, provider) { function openReader(bookId, chapterId, provider) {
const c = encodeURIComponent(chapterId); const c = encodeURIComponent(chapterId);
const p = encodeURIComponent(provider); const p = encodeURIComponent(provider);
window.location.href = `/read/${bookId}/${c}/${p}`; window.location.href = `/read/${p}/${c}/${bookId}`;
} }
init(); init();

View File

@@ -46,6 +46,16 @@ 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) { function renderSearchResults(results) {
searchResults.innerHTML = ''; searchResults.innerHTML = '';
@@ -58,10 +68,23 @@ function renderSearchResults(results) {
const rating = book.averageScore ? `${book.averageScore}%` : 'N/A'; const rating = 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';
item.href = `/book/${book.id}`; item.href = href;
item.innerHTML = ` item.innerHTML = `
<img src="${img}" class="search-poster" alt="${title}"> <img src="${img}" class="search-poster" alt="${title}">
@@ -71,6 +94,7 @@ 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>
`; `;

View File

@@ -35,9 +35,9 @@ let observer = null;
const parts = window.location.pathname.split('/'); const parts = window.location.pathname.split('/');
const bookId = parts[2]; const bookId = parts[4];
let chapter = parts[3]; let chapter = parts[3];
let provider = parts[4]; let provider = parts[2];
function loadConfig() { function loadConfig() {
try { try {
@@ -125,7 +125,7 @@ async function loadChapter() {
`; `;
try { 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(); const data = await res.json();
if (data.title) { if (data.title) {
@@ -467,15 +467,22 @@ nextBtn.addEventListener('click', () => {
function updateURL(newChapter) { function updateURL(newChapter) {
chapter = newChapter; chapter = newChapter;
const newUrl = `/reader/${bookId}/${chapter}/${provider}`; const newUrl = `/reader/${provider}/${chapter}/${bookId}`;
window.history.pushState({}, '', newUrl); window.history.pushState({}, '', newUrl);
} }
document.getElementById('back-btn').addEventListener('click', () => { document.getElementById('back-btn').addEventListener('click', () => {
const parts = window.location.pathname.split('/'); const parts = window.location.pathname.split('/');
const provider = parts[2];
const mangaId = parts[4];
const mangaId = parts[2]; const isInt = Number.isInteger(Number(mangaId));
if (isInt) {
window.location.href = `/book/${mangaId}`; window.location.href = `/book/${mangaId}`;
} else {
window.location.href = `/book/${provider}/${mangaId}`;
}
}); });
settingsBtn.addEventListener('click', () => { settingsBtn.addEventListener('click', () => {

View File

@@ -28,7 +28,12 @@ async function viewsRoutes(fastify, options) {
reply.type('text/html').send(stream); 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')); const stream = fs.createReadStream(path.join(__dirname, '..', '..', 'views', 'read.html'));
reply.type('text/html').send(stream); reply.type('text/html').send(stream);
}); });

View File

@@ -59,6 +59,7 @@
<h1 class="book-title" id="title">Loading...</h1> <h1 class="book-title" id="title">Loading...</h1>
<div class="meta-row"> <div class="meta-row">
<div class="pill extension-pill" id="extension-pill" style="display: none; background: #8b5cf6;"></div>
<div class="pill score" id="score">--% Score</div> <div class="pill score" id="score">--% Score</div>
<div class="pill" id="genres">Action</div> <div class="pill" id="genres">Action</div>
</div> </div>