lazy loading of chapters on book page

This commit is contained in:
2025-12-18 16:50:35 +01:00
parent 41dddef354
commit 2cf475931c
4 changed files with 104 additions and 82 deletions

View File

@@ -87,15 +87,16 @@ export async function getChapters(req: any, reply: FastifyReply) {
try { try {
const { id } = req.params; const { id } = req.params;
const source = req.query.source || 'anilist'; const source = req.query.source || 'anilist';
const provider = req.query.provider;
const isExternal = source !== 'anilist'; const isExternal = source !== 'anilist';
return await booksService.getChaptersForBook(id, isExternal); return await booksService.getChaptersForBook(id, isExternal, provider);
} catch { } catch (err) {
console.error(err);
return { chapters: [] }; return { chapters: [] };
} }
} }
export async function getChapterContent(req: any, reply: FastifyReply) { export async function getChapterContent(req: any, reply: FastifyReply) {
try { try {
const { bookId, chapter, provider } = req.params; const { bookId, chapter, provider } = req.params;

View File

@@ -6,6 +6,8 @@ let bookSlug = null;
let allChapters = []; let allChapters = [];
let filteredChapters = []; let filteredChapters = [];
let availableExtensions = [];
const chapterPagination = Object.create(PaginationManager); const chapterPagination = Object.create(PaginationManager);
chapterPagination.init(12, () => renderChapterTable()); chapterPagination.init(12, () => renderChapterTable());
@@ -16,7 +18,6 @@ document.addEventListener('DOMContentLoaded', () => {
async function init() { async function init() {
try { try {
const urlData = URLUtils.parseEntityPath('book'); const urlData = URLUtils.parseEntityPath('book');
if (!urlData) { if (!urlData) {
showError("Book Not Found"); showError("Book Not Found");
@@ -29,6 +30,7 @@ async function init() {
await loadBookMetadata(); await loadBookMetadata();
await loadAvailableExtensions();
await loadChapters(); await loadChapters();
await setupAddToListButton(); await setupAddToListButton();
@@ -39,11 +41,23 @@ async function init() {
} }
} }
async function loadAvailableExtensions() {
try {
const res = await fetch('/api/extensions/book');
const data = await res.json();
availableExtensions = data.extensions || [];
setupProviderFilter();
} catch (err) {
console.error("Error fetching extensions:", err);
}
}
async function loadBookMetadata() { async function loadBookMetadata() {
const source = extensionName || 'anilist'; const source = extensionName || 'anilist';
const fetchUrl = `/api/book/${bookId}?source=${source}`; const fetchUrl = `/api/book/${bookId}?source=${source}`;
const res = await fetch(fetchUrl, { headers: AuthUtils.getSimpleAuthHeaders() }); const res = await fetch(fetchUrl);
const data = await res.json(); const data = await res.json();
if (data.error || !data) { if (data.error || !data) {
@@ -154,17 +168,27 @@ function updateCustomAddButton() {
} }
} }
async function loadChapters() { async function loadChapters(targetProvider = null) {
const tbody = document.getElementById('chapters-body'); const tbody = document.getElementById('chapters-body');
if (!tbody) return; if (!tbody) return;
tbody.innerHTML = '<tr><td colspan="4" style="text-align:center; padding: 2rem;">Searching extensions for chapters...</td></tr>'; // Si no se pasa provider, intentamos pillar el del select o el primero disponible
if (!targetProvider) {
const select = document.getElementById('provider-filter');
targetProvider = select ? select.value : (availableExtensions[0] || 'all');
}
tbody.innerHTML = '<tr><td colspan="4" style="text-align:center; padding: 2rem;">Searching extension for chapters...</td></tr>';
try { try {
const source = extensionName || 'anilist'; const source = extensionName || 'anilist';
const fetchUrl = `/api/book/${bookId}/chapters?source=${source}`; // Añadimos el query param 'provider' para que el backend filtre
let fetchUrl = `/api/book/${bookId}/chapters?source=${source}`;
if (targetProvider !== 'all') {
fetchUrl += `&provider=${targetProvider}`;
}
const res = await fetch(fetchUrl, { headers: AuthUtils.getSimpleAuthHeaders() }); const res = await fetch(fetchUrl);
const data = await res.json(); const data = await res.json();
allChapters = data.chapters || []; allChapters = data.chapters || [];
@@ -175,18 +199,17 @@ async function loadChapters() {
const totalEl = document.getElementById('total-chapters'); const totalEl = document.getElementById('total-chapters');
if (allChapters.length === 0) { if (allChapters.length === 0) {
tbody.innerHTML = '<tr><td colspan="4" style="text-align:center; padding: 2rem;">No chapters found on loaded extensions.</td></tr>'; tbody.innerHTML = '<tr><td colspan="4" style="text-align:center; padding: 2rem;">No chapters found.</td></tr>';
if (totalEl) totalEl.innerText = "0 Found"; if (totalEl) totalEl.innerText = "0 Found";
return; return;
} }
if (totalEl) totalEl.innerText = `${allChapters.length} Found`; if (totalEl) totalEl.innerText = `${allChapters.length} Found`;
setupProviderFilter();
setupReadButton(); setupReadButton();
chapterPagination.setTotalItems(filteredChapters.length); chapterPagination.setTotalItems(filteredChapters.length);
chapterPagination.reset();
renderChapterTable(); renderChapterTable();
} catch (err) { } catch (err) {
@@ -211,44 +234,31 @@ function applyChapterFilter() {
function setupProviderFilter() { function setupProviderFilter() {
const select = document.getElementById('provider-filter'); const select = document.getElementById('provider-filter');
if (!select) return; if (!select || availableExtensions.length === 0) return;
const providers = [...new Set(allChapters.map(ch => ch.provider))];
if (providers.length === 0) return;
select.style.display = 'inline-block'; select.style.display = 'inline-block';
select.innerHTML = '<option value="all">All Providers</option>'; select.innerHTML = '';
providers.forEach(prov => { const allOpt = document.createElement('option');
allOpt.value = 'all';
allOpt.innerText = 'Load All (Slower)';
select.appendChild(allOpt);
availableExtensions.forEach(ext => {
const opt = document.createElement('option'); const opt = document.createElement('option');
opt.value = prov; opt.value = ext;
opt.innerText = prov; opt.innerText = ext.charAt(0).toUpperCase() + ext.slice(1);
select.appendChild(opt); select.appendChild(opt);
}); });
if (extensionName) { if (extensionName && availableExtensions.includes(extensionName)) {
const extensionProvider = providers.find( select.value = extensionName;
p => p.toLowerCase() === extensionName.toLowerCase() } else if (availableExtensions.length > 0) {
); select.value = availableExtensions[0];
if (extensionProvider) {
select.value = extensionProvider;
filteredChapters = allChapters.filter(ch => ch.provider === extensionProvider);
}
} }
select.onchange = (e) => { select.onchange = () => {
const selected = e.target.value; loadChapters(select.value);
if (selected === 'all') {
filteredChapters = [...allChapters];
} else {
filteredChapters = allChapters.filter(ch => ch.provider === selected);
}
chapterPagination.reset();
chapterPagination.setTotalItems(filteredChapters.length);
renderChapterTable();
}; };
} }

View File

@@ -87,15 +87,16 @@ export async function getChapters(req: any, reply: FastifyReply) {
try { try {
const { id } = req.params; const { id } = req.params;
const source = req.query.source || 'anilist'; const source = req.query.source || 'anilist';
const provider = req.query.provider;
const isExternal = source !== 'anilist'; const isExternal = source !== 'anilist';
return await booksService.getChaptersForBook(id, isExternal); return await booksService.getChaptersForBook(id, isExternal, provider);
} catch { } catch (err) {
console.error(err);
return { chapters: [] }; return { chapters: [] };
} }
} }
export async function getChapterContent(req: any, reply: FastifyReply) { export async function getChapterContent(req: any, reply: FastifyReply) {
try { try {
const { bookId, chapter, provider } = req.params; const { bookId, chapter, provider } = req.params;

View File

@@ -6,6 +6,8 @@ let bookSlug = null;
let allChapters = []; let allChapters = [];
let filteredChapters = []; let filteredChapters = [];
let availableExtensions = [];
const chapterPagination = Object.create(PaginationManager); const chapterPagination = Object.create(PaginationManager);
chapterPagination.init(12, () => renderChapterTable()); chapterPagination.init(12, () => renderChapterTable());
@@ -16,7 +18,6 @@ document.addEventListener('DOMContentLoaded', () => {
async function init() { async function init() {
try { try {
const urlData = URLUtils.parseEntityPath('book'); const urlData = URLUtils.parseEntityPath('book');
if (!urlData) { if (!urlData) {
showError("Book Not Found"); showError("Book Not Found");
@@ -29,6 +30,7 @@ async function init() {
await loadBookMetadata(); await loadBookMetadata();
await loadAvailableExtensions();
await loadChapters(); await loadChapters();
await setupAddToListButton(); await setupAddToListButton();
@@ -39,11 +41,23 @@ async function init() {
} }
} }
async function loadAvailableExtensions() {
try {
const res = await fetch('/api/extensions/book');
const data = await res.json();
availableExtensions = data.extensions || [];
setupProviderFilter();
} catch (err) {
console.error("Error fetching extensions:", err);
}
}
async function loadBookMetadata() { async function loadBookMetadata() {
const source = extensionName || 'anilist'; const source = extensionName || 'anilist';
const fetchUrl = `/api/book/${bookId}?source=${source}`; const fetchUrl = `/api/book/${bookId}?source=${source}`;
const res = await fetch(fetchUrl, { headers: AuthUtils.getSimpleAuthHeaders() }); const res = await fetch(fetchUrl);
const data = await res.json(); const data = await res.json();
if (data.error || !data) { if (data.error || !data) {
@@ -154,17 +168,27 @@ function updateCustomAddButton() {
} }
} }
async function loadChapters() { async function loadChapters(targetProvider = null) {
const tbody = document.getElementById('chapters-body'); const tbody = document.getElementById('chapters-body');
if (!tbody) return; if (!tbody) return;
tbody.innerHTML = '<tr><td colspan="4" style="text-align:center; padding: 2rem;">Searching extensions for chapters...</td></tr>'; // Si no se pasa provider, intentamos pillar el del select o el primero disponible
if (!targetProvider) {
const select = document.getElementById('provider-filter');
targetProvider = select ? select.value : (availableExtensions[0] || 'all');
}
tbody.innerHTML = '<tr><td colspan="4" style="text-align:center; padding: 2rem;">Searching extension for chapters...</td></tr>';
try { try {
const source = extensionName || 'anilist'; const source = extensionName || 'anilist';
const fetchUrl = `/api/book/${bookId}/chapters?source=${source}`; // Añadimos el query param 'provider' para que el backend filtre
let fetchUrl = `/api/book/${bookId}/chapters?source=${source}`;
if (targetProvider !== 'all') {
fetchUrl += `&provider=${targetProvider}`;
}
const res = await fetch(fetchUrl, { headers: AuthUtils.getSimpleAuthHeaders() }); const res = await fetch(fetchUrl);
const data = await res.json(); const data = await res.json();
allChapters = data.chapters || []; allChapters = data.chapters || [];
@@ -175,18 +199,17 @@ async function loadChapters() {
const totalEl = document.getElementById('total-chapters'); const totalEl = document.getElementById('total-chapters');
if (allChapters.length === 0) { if (allChapters.length === 0) {
tbody.innerHTML = '<tr><td colspan="4" style="text-align:center; padding: 2rem;">No chapters found on loaded extensions.</td></tr>'; tbody.innerHTML = '<tr><td colspan="4" style="text-align:center; padding: 2rem;">No chapters found.</td></tr>';
if (totalEl) totalEl.innerText = "0 Found"; if (totalEl) totalEl.innerText = "0 Found";
return; return;
} }
if (totalEl) totalEl.innerText = `${allChapters.length} Found`; if (totalEl) totalEl.innerText = `${allChapters.length} Found`;
setupProviderFilter();
setupReadButton(); setupReadButton();
chapterPagination.setTotalItems(filteredChapters.length); chapterPagination.setTotalItems(filteredChapters.length);
chapterPagination.reset();
renderChapterTable(); renderChapterTable();
} catch (err) { } catch (err) {
@@ -211,44 +234,31 @@ function applyChapterFilter() {
function setupProviderFilter() { function setupProviderFilter() {
const select = document.getElementById('provider-filter'); const select = document.getElementById('provider-filter');
if (!select) return; if (!select || availableExtensions.length === 0) return;
const providers = [...new Set(allChapters.map(ch => ch.provider))];
if (providers.length === 0) return;
select.style.display = 'inline-block'; select.style.display = 'inline-block';
select.innerHTML = '<option value="all">All Providers</option>'; select.innerHTML = '';
providers.forEach(prov => { const allOpt = document.createElement('option');
allOpt.value = 'all';
allOpt.innerText = 'Load All (Slower)';
select.appendChild(allOpt);
availableExtensions.forEach(ext => {
const opt = document.createElement('option'); const opt = document.createElement('option');
opt.value = prov; opt.value = ext;
opt.innerText = prov; opt.innerText = ext.charAt(0).toUpperCase() + ext.slice(1);
select.appendChild(opt); select.appendChild(opt);
}); });
if (extensionName) { if (extensionName && availableExtensions.includes(extensionName)) {
const extensionProvider = providers.find( select.value = extensionName;
p => p.toLowerCase() === extensionName.toLowerCase() } else if (availableExtensions.length > 0) {
); select.value = availableExtensions[0];
if (extensionProvider) {
select.value = extensionProvider;
filteredChapters = allChapters.filter(ch => ch.provider === extensionProvider);
}
} }
select.onchange = (e) => { select.onchange = () => {
const selected = e.target.value; loadChapters(select.value);
if (selected === 'all') {
filteredChapters = [...allChapters];
} else {
filteredChapters = allChapters.filter(ch => ch.provider === selected);
}
chapterPagination.reset();
chapterPagination.setTotalItems(filteredChapters.length);
renderChapterTable();
}; };
} }