WeebCentral and MangaPill are off by default, with an admin switch per source

Both are unofficial sites that mostly host unlicensed copies; MangaDex (official
API) stays on. Settings → Admin → Scraper → Hoster settings has a 'Use <source>'
switch. A switched-off source refuses new jobs, keeps queued jobs waiting and is
skipped by nightly update checks. Installations whose queue or history already
used a source keep it switched on, so upgrades don't stop a working queue.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
MaashkinoozheandClaude Opus 5.5 committed 2026-10-25 07:47:47 +01:00
1 parent 336d3325fe
commit 57a571d2f0
6 files changed
+84 -10

No files matched your search

+35 -4
View File
@@ -24,9 +24,13 @@ const DEFAULT_SOURCE_RATES = {
mangadex: { ...FAST_RATE_SETTINGS },
mangapill: { ...FAST_RATE_SETTINGS },
};
// Sources an admin must switch on first. WeebCentral and MangaPill mostly host unlicensed
// copies; MangaDex has an official API and handles takedowns. Peers are always available.
const DEFAULT_SOURCES_ENABLED = { weebcentral: false, mangadex: true, mangapill: false };
const DEFAULT_SETTINGS = {
...FAST_RATE_SETTINGS,
sourceRates: Object.fromEntries(Object.entries(DEFAULT_SOURCE_RATES).map(([source, rate]) => [source, { ...rate }])),
sourcesEnabled: { ...DEFAULT_SOURCES_ENABLED },
autoUpdateEnabled: true, autoUpdateTime: '03:00',
};
@@ -73,6 +77,15 @@ function validateSettings(input = {}, prior = DEFAULT_SETTINGS) {
};
next.sourceRates[source] = validateRateSettings(patch || {}, base, `sourceRates.${source}.`);
}
if (input.sourcesEnabled !== undefined && (!input.sourcesEnabled || typeof input.sourcesEnabled !== 'object' || Array.isArray(input.sourcesEnabled))) {
throw new Error('sourcesEnabled must be an object');
}
next.sourcesEnabled = { ...DEFAULT_SOURCES_ENABLED, ...(prior.sourcesEnabled || {}) };
for (const [source, enabled] of Object.entries(input.sourcesEnabled || {})) {
if (!Object.hasOwn(DEFAULT_SOURCES_ENABLED, source)) throw new Error(`Unknown source: ${source}`);
if (typeof enabled !== 'boolean') throw new Error(`sourcesEnabled.${source} must be true or false`);
next.sourcesEnabled[source] = enabled;
}
if (input.autoUpdateEnabled !== undefined) next.autoUpdateEnabled = !!input.autoUpdateEnabled;
if (input.autoUpdateTime !== undefined) {
const value = String(input.autoUpdateTime).trim();
@@ -86,6 +99,13 @@ function settingsForSource(settings, source) {
return { ...settings, ...((settings.sourceRates || {})[source] || {}) };
}
// Installations from before the switch keep every source they already use: off-by-default
// applies to new installations, it must not silently stop a working queue.
function inheritedSourcesEnabled(saved) {
const used = new Set([...(saved.queue || []), ...(saved.history || [])].map(item => item && item.source));
return Object.fromEntries(Object.entries(DEFAULT_SOURCES_ENABLED).map(([source, enabled]) => [source, enabled || used.has(source)]));
}
function existingPolicy(value) {
const policy = String(value || 'skip-any');
if (!['skip-any', 'add-source'].includes(policy)) throw new Error('existingPolicy must be skip-any or add-source');
@@ -217,11 +237,13 @@ class ScraperService {
async loadState() {
if (await exists(this.stateFile)) {
const saved = JSON.parse(await fs.readFile(this.stateFile, 'utf8'));
const savedSettings = saved.settings || {};
if (!savedSettings.sourcesEnabled) savedSettings.sourcesEnabled = inheritedSourcesEnabled(saved);
return {
version: 3, paused: false, status: 'idle', queue: [], history: [], publishedRevision: 0,
currentIds: {}, sourcePaused: {},
settings: DEFAULT_SETTINGS, ...saved,
settings: validateSettings(saved.settings || {}, DEFAULT_SETTINGS),
settings: validateSettings(savedSettings, DEFAULT_SETTINGS),
};
}
const state = {
@@ -239,7 +261,7 @@ class ScraperService {
status: completed.has(item.id) ? 'complete' : 'queued', order: index,
added: legacy.created || new Date().toISOString(),
}));
state.settings = validateSettings(legacy.settings || {}, DEFAULT_SETTINGS);
state.settings = validateSettings({ ...(legacy.settings || {}), sourcesEnabled: inheritedSourcesEnabled(state) }, DEFAULT_SETTINGS);
state.migratedFrom = this.legacyStateFile;
}
return state;
@@ -401,7 +423,8 @@ class ScraperService {
let queued = 0, skipped = 0;
for (const manga of inventory.mangas) {
if (!manga.autoUpdate) continue;
const supported = manga.sources.map(source => ({ source, found: this.resolveAdapter(source.url) })).filter(item => item.found);
const supported = manga.sources.map(source => ({ source, found: this.resolveAdapter(source.url) }))
.filter(item => item.found && this.sourceEnabled(item.found.adapter.id));
if (!supported.length) { skipped++; continue; }
const preferred = manga.meta?.['x-scraper']?.preferredSource;
const selected = supported.find(item => item.found.adapter.id === preferred) || supported[0];
@@ -598,9 +621,16 @@ class ScraperService {
};
}
sourceEnabled(source) {
return !Object.hasOwn(DEFAULT_SOURCES_ENABLED, source) || this.state.settings.sourcesEnabled?.[source] === true;
}
async enqueue(input) {
const found = this.resolveAdapter(input.url);
if (!found) throw new Error('No installed scraper adapter accepts this URL');
if (!this.sourceEnabled(found.adapter.id)) {
throw new Error(`${found.adapter.label || found.adapter.id} is switched off. An admin can switch it on under Settings → Admin → Scraper → Hoster settings.`);
}
const workIdentity = normalizeWorkIdentity(input.workIdentity);
if (input.workIdentity != null && !workIdentity) throw new Error('Invalid manga work identity');
const scope = normalizeScope(input.scope || {});
@@ -902,7 +932,8 @@ class ScraperService {
continue;
}
const source = found.adapter.id;
if (this.active.has(source) || this.state.sourcePaused[source]) continue;
// A switched-off source keeps its queued jobs; they wait until it is switched on again.
if (this.active.has(source) || this.state.sourcePaused[source] || !this.sourceEnabled(source)) continue;
if (this.state.circuits[source]?.state === 'open') continue;
if (item.libraryMangaId && [...this.active.values()].some(entry => {
const activeItem = this.state.queue.find(candidate => candidate.id === entry.id);