added book entries from extensions
This commit is contained in:
@@ -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: [] };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,63 +138,90 @@ 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) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
};
|
};
|
||||||
@@ -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();
|
||||||
@@ -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>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -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', () => {
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user