Expose Greatest source links and safe scraper inspection

This commit is contained in:
Codex committed 2026-10-09 14:53:52 +02:00
1 parent f493afcc18
commit d0275223a7
7 files changed
+238 -6

No files matched your search

+42
View File
@@ -16,6 +16,7 @@ const { scanLibrary, scanChangedLibrary, libraryStamps, snapshotIndex, restoreIn
const { Catalog, CatalogError, collectionViews, TYPES, ROLES } = require('./catalog');
const { RATINGS: GREATEST_RATINGS, loadCatalog: loadGreatestCatalog, findOwned: findGreatestOwned,
countsFor: greatestCounts, scoreFor: greatestScore } = require('./greatest');
const { hosterLinks } = require('./greatest/hosters');
const { secret } = require('./runtime/config');
const {
applyPulse, applyVisit, chapterInsight, eligiblePages, emptyRecord, libraryInsight, mangaInsight, publicSummary,
@@ -204,6 +205,28 @@ async function scraperRequest(endpoint, body) {
return data;
}
const mangaDexSearchCache = new Map();
let lastMangaDexSearch = 0;
async function searchGreatestMangaDex(entry) {
const cached = mangaDexSearchCache.get(entry.id);
if (cached && Date.now() - cached.time < 60 * 60 * 1000) return cached.results;
if (Date.now() - lastMangaDexSearch < 2000) bad(429, 'Wait two seconds before another MangaDex lookup');
lastMangaDexSearch = Date.now();
let response;
try {
response = await fetch(`https://api.mangadex.org/manga?title=${encodeURIComponent(entry.title)}&limit=10`,
{ headers: { accept: 'application/json' }, signal: AbortSignal.timeout(7000) });
} catch (error) { throw new HttpError(503, `MangaDex lookup unavailable: ${error.message}`); }
if (!response.ok) throw new HttpError(response.status === 429 ? 503 : 502, `MangaDex lookup returned HTTP ${response.status}`);
const body = await response.json();
const results = (body.data || []).filter(row => /^[0-9a-f-]{36}$/i.test(row.id)).map(row => ({
title: row.attributes?.title?.en || Object.values(row.attributes?.title || {})[0] || 'Untitled',
year: row.attributes?.year || null, url: `https://mangadex.org/title/${row.id}`,
}));
mangaDexSearchCache.set(entry.id, { time: Date.now(), results });
return results;
}
// Versions sorted by the sum of all user votes; ties go to the higher "priority" in chapter.json, then folder order.
function ranked(m, ch) {
const votes = (db.votes[m.id] && db.votes[m.id][ch.id]) || {};
@@ -380,6 +403,7 @@ const routes = {
chapters: owned ? owned.chapters.length : entry.chapters,
status: owned?.meta?.status || entry.status,
library: owned ? { id: owned.id, chapters: owned.chapters.length } : null,
hosters: hosterLinks(entry, owned).links,
myRating: db.greatestRatings?.[u.name]?.[entry.id] || null,
votes: counts, score: greatestScore(counts),
};
@@ -397,6 +421,24 @@ const routes = {
const votes = greatestCounts(id, all, db.users);
return { id, myRating: rating, votes, score: greatestScore(votes) };
},
'GET /api/greatest/sources': async ({ u, q }) => {
admin(u);
const entry = greatestById.get(String(q.get('id') || ''));
if (!entry) bad(404, 'Catalog entry not found');
const owned = findGreatestOwned(entry, lib.mangas);
try {
const state = await scraperRequest('/api/state');
return { available: true, ...hosterLinks(entry, owned, state.queue || []) };
} catch (error) {
return { available: false, error: error.message, ...hosterLinks(entry, owned) };
}
},
'GET /api/greatest/mangadex-search': async ({ u, q }) => {
admin(u);
const entry = greatestById.get(String(q.get('id') || ''));
if (!entry) bad(404, 'Catalog entry not found');
return { results: await searchGreatestMangaDex(entry) };
},
'GET /api/manga': ({ u, q }) => {
const m = mangaOf(q.get('m')), prog = progressOf(u, m), showStory = settingsOf(u).readingStory;