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 {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: [] };
}

View File

@@ -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);

View File

@@ -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
};

View File

@@ -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 = '<tr><td colspan="4" style="text-align:center; padding: 2rem;">Searching extensions for chapters...</td></tr>';
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();

View File

@@ -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 = '<div style="padding:1rem; color:#888; text-align:center">No results found</div>';
} 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
? `<span>${extName}</span>`
: '';
const item = document.createElement('a');
item.className = 'search-item';
item.href = `/book/${book.id}`;
item.href = href;
item.innerHTML = `
<img src="${img}" class="search-poster" alt="${title}">
<div class="search-info">
@@ -71,6 +94,7 @@ function renderSearchResults(results) {
<span class="rating-pill">${rating}</span>
<span>• ${year}</span>
<span>• ${format}</span>
${extPill}
</div>
</div>
`;
@@ -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) {

View File

@@ -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', () => {

View File

@@ -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);
});

View File

@@ -59,6 +59,7 @@
<h1 class="book-title" id="title">Loading...</h1>
<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" id="genres">Action</div>
</div>