BooruKit Core

Shared runtime and feature registry for the BooruKit userscript suite.

Este script não deve ser instalado diretamente. Este script é uma biblioteca de outros scripts para incluir com o diretório meta // @require https://update.sleazyfork.org/scripts/589427/1890613/BooruKit%20Core.js

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name         BooruKit Core
// @namespace    codeberg.org/0xdev/boorukit
// @version      0.1.0
// @description  Shared runtime and feature registry for the BooruKit userscript suite.
// @author       0xdev
// @license      LGPL-3.0-or-later
// ==/UserScript==

// THIS FILE IS AUTO-GENERATED — DO NOT MANUALLY EDIT.
// Source: boorukit/boorukit-core.library.js
(() => {
    'use strict';
    const BOORUKIT_API_VERSION = 1;
    const BOORUKIT_RUNTIME_KEY = Symbol.for('codeberg.org/0xdev/boorukit/runtime/v1');
    function defaultSettings() {
        return {
            schemaVersion: 1,
            modules: { grabber: true, 'pool-party': true },
            grabber: null,
            migrations: { bbdSettings: false },
        };
    }
    function validateSettings(value) {
        if (!value || typeof value !== 'object')
            return defaultSettings();
        const raw = value;
        const modules = raw.modules;
        const migrations = raw.migrations;
        const grabber = raw.grabber;
        if (raw.schemaVersion !== 1
            || !modules
            || typeof modules.grabber !== 'boolean'
            || typeof modules['pool-party'] !== 'boolean'
            || !migrations
            || typeof migrations.bbdSettings !== 'boolean'
            || (grabber !== null
                && (!grabber
                    || typeof grabber.writeTagsTxt !== 'boolean'))) {
            return defaultSettings();
        }
        return {
            schemaVersion: 1,
            modules: {
                grabber: modules.grabber,
                'pool-party': modules['pool-party'],
            },
            grabber: grabber === null
                ? null
                : { writeTagsTxt: grabber.writeTagsTxt },
            migrations: { bbdSettings: migrations.bbdSettings },
        };
    }
    function createStorageApi() {
        const GM = globalThis.GM;
        return {
            async read(key, codec) {
                const raw = await GM.getValue(key);
                return raw === undefined ? null : codec.decode(raw);
            },
            async write(key, value, codec) {
                await GM.setValue(key, codec.encode(value));
            },
            async delete(key) {
                await GM.deleteValue(key);
            },
        };
    }
    function sanitizeDiagnostic(error) {
        const sensitiveKey = /key|secret|token|password|credential|authorization|cookie|headers?|body|settings/i;
        const seen = new WeakSet();
        function sanitize(value, key) {
            if (key && sensitiveKey.test(key))
                return '[redacted]';
            if (typeof value === 'string') {
                return value
                    .replace(/https?:\/\/[^\s/@:]+:[^\s/@]+@[^\s]+/gi, '[redacted-url]')
                    .replace(/\b((?:proxy-)?authorization)\s*:\s*[^\r\n]+/gi, '$1: [redacted]')
                    .replace(/\b(set-cookie|cookie)\s*:\s*[^\r\n]+/gi, '$1: [redacted]')
                    .replace(/\bbearer\s+[^\s,;]+/gi, 'Bearer [redacted]')
                    .replace(/([?&](?:api[_-]?key|access[_-]?token|auth(?:orization)?|secret|password|credential|session)=)[^&#\s]+/gi, '$1[redacted]')
                    .replace(/\b(api[_-]?key|secret|token|password|credential)\s*[=:]\s*[^\s&,;]+/gi, '$1=[redacted]');
            }
            if (!value || typeof value !== 'object')
                return value;
            if (seen.has(value))
                return '[circular]';
            seen.add(value);
            if (value instanceof Error) {
                return {
                    name: value.name,
                    message: sanitize(value.message),
                };
            }
            if (Array.isArray(value)) {
                return value.map((item) => sanitize(item));
            }
            const sanitized = {};
            for (const [entryKey, entryValue] of Object.entries(value)) {
                sanitized[entryKey] = sanitize(entryValue, entryKey);
            }
            return sanitized;
        }
        try {
            return JSON.stringify(sanitize(error));
        }
        catch {
            return JSON.stringify({ name: 'Error', message: 'Diagnostic unavailable' });
        }
    }
    function createTaskCoordinator() {
        let cancelled = false;
        let activeLabel = null;
        const listeners = new Set();
        function publish(progress) {
            for (const listener of listeners)
                listener(progress);
        }
        const coordinator = {
            async run(label, operation) {
                if (activeLabel !== null) {
                    throw new Error('foreground task already running: ' + activeLabel);
                }
                activeLabel = label;
                cancelled = false;
                const signal = Object.freeze({
                    get cancelled() {
                        return cancelled;
                    },
                    throwIfCancelled() {
                        if (cancelled)
                            throw new Error('foreground task cancelled: ' + label);
                    },
                });
                publish({ label, completed: 0, total: 0 });
                try {
                    return await operation(signal, (completed, total) => {
                        publish({ label, completed, total });
                    });
                }
                finally {
                    activeLabel = null;
                    publish(null);
                }
            },
            cancel() {
                if (activeLabel !== null)
                    cancelled = true;
            },
            subscribe(listener) {
                listeners.add(listener);
                return () => { listeners.delete(listener); };
            },
        };
        return Object.freeze(coordinator);
    }
    function createSequentialQueue() {
        const queue = {
            async run(items, worker, policy) {
                const results = [];
                for (let index = 0; index < items.length; index += 1) {
                    if (policy.signal.cancelled)
                        break;
                    if (index > 0) {
                        if (policy.signal.cancelled)
                            break;
                        await policy.sleep(policy.throttleMs);
                        if (policy.signal.cancelled)
                            break;
                    }
                    if (policy.signal.cancelled)
                        break;
                    const item = items[index];
                    for (let attempt = 1; attempt <= policy.attempts; attempt += 1) {
                        if (policy.signal.cancelled)
                            break;
                        policy.onProgress?.(item, 'running', results.length, items.length);
                        if (policy.signal.cancelled)
                            break;
                        try {
                            const value = await worker(item, policy.signal);
                            results.push({ item, status: 'fulfilled', value });
                            policy.onProgress?.(item, 'done', results.length, items.length);
                            break;
                        }
                        catch (caught) {
                            const error = caught instanceof Error
                                ? caught
                                : new Error(String(caught));
                            if (policy.signal.cancelled)
                                break;
                            if (attempt < policy.attempts) {
                                policy.onProgress?.(item, 'retrying', results.length, items.length, error);
                                if (policy.signal.cancelled)
                                    break;
                                await policy.sleep(policy.retryDelayMs);
                                if (policy.signal.cancelled)
                                    break;
                                continue;
                            }
                            results.push({ item, status: 'rejected', reason: error });
                            policy.onProgress?.(item, 'failed', results.length, items.length, error);
                        }
                    }
                }
                return results;
            },
        };
        return Object.freeze(queue);
    }
    function createPageContext(href) {
        const url = new URL(href);
        const hostname = url.hostname.toLowerCase();
        const normalizedHostname = hostname.replace(/^www\./, '');
        const e621Hosts = new Set(['e621.net', 'e926.net']);
        const gelbooruHosts = new Set([
            'gelbooru.com',
            'hypnohub.net',
            'rule34.xxx',
            'safebooru.org',
        ]);
        const family = e621Hosts.has(normalizedHostname)
            ? 'e621'
            : gelbooruHosts.has(normalizedHostname)
                || normalizedHostname.endsWith('.booru.org')
                ? 'gelbooru'
                : 'unknown';
        let pageKind = 'other';
        if (family === 'e621') {
            if (url.pathname === '/posts' || url.pathname === '/posts/') {
                pageKind = 'gallery';
            }
            else if (/^\/posts\/\d+\/?$/.test(url.pathname)) {
                pageKind = 'post';
            }
            else if (/^\/pools\/\d+\/?$/.test(url.pathname)) {
                pageKind = 'pool';
            }
        }
        else if (family === 'gelbooru') {
            const page = url.searchParams.get('page');
            const section = url.searchParams.get('s');
            if (page === 'post' && section === 'list') {
                pageKind = 'gallery';
            }
            else if (page === 'post' && section === 'view') {
                pageKind = 'post';
            }
            else if (page === 'pool' && section === 'show') {
                pageKind = 'pool';
            }
        }
        return Object.freeze({
            url,
            origin: url.origin,
            hostname,
            normalizedHostname,
            family,
            pageKind,
        });
    }
    function createCoreRuntime(effects) {
        const descriptors = new Map();
        const tasks = createTaskCoordinator();
        const queue = createSequentialQueue();
        const diagnostics = Object.freeze({
            report(source, error) {
                effects.diagnosticSink(source, sanitizeDiagnostic(error));
            },
        });
        const settingsCodec = Object.freeze({
            decode: validateSettings,
            encode: validateSettings,
        });
        let settingsSnapshot = defaultSettings();
        const settings = Object.freeze({
            snapshot() {
                return validateSettings(settingsSnapshot);
            },
            async update(transform) {
                const current = validateSettings(settingsSnapshot);
                const next = validateSettings(transform(current));
                await effects.storage.write('boorukit.settings', next, settingsCodec);
                settingsSnapshot = next;
                return validateSettings(settingsSnapshot);
            },
        });
        let startPromise = null;
        async function activate() {
            const candidate = descriptors.get('deck');
            if (!candidate || candidate.kind !== 'deck') {
                diagnostics.report('core', new Error('required Deck module is not registered'));
                return;
            }
            if (!candidate.supports(effects.page))
                return;
            const stored = await effects.storage.read('boorukit.settings', settingsCodec);
            settingsSnapshot = stored === null
                ? defaultSettings()
                : validateSettings(stored);
            const deck = await candidate.activate({
                page: effects.page,
                tasks,
                diagnostics,
                settings,
            });
            const moduleLabels = {
                grabber: 'Grabber',
                'pool-party': 'Pool Party',
            };
            for (const id of ['grabber', 'pool-party']) {
                deck.registerSetting({
                    id,
                    label: moduleLabels[id],
                    checked: settingsSnapshot.modules[id],
                    reloadRequired: true,
                    onChange: async (checked) => {
                        await settings.update((current) => ({
                            ...current,
                            modules: { ...current.modules, [id]: checked },
                        }));
                    },
                });
            }
            for (const descriptor of descriptors.values()) {
                if (descriptor.kind !== 'feature')
                    continue;
                if (!settingsSnapshot.modules[descriptor.id])
                    continue;
                try {
                    if (!descriptor.supports(effects.page))
                        continue;
                    await descriptor.activate({
                        page: effects.page,
                        tasks,
                        diagnostics,
                        settings,
                        deck,
                        storage: effects.storage,
                        queue,
                    });
                }
                catch (error) {
                    const reason = sanitizeDiagnostic(error);
                    effects.diagnosticSink(descriptor.id, reason);
                    deck.setFeatureStatus(descriptor.id, 'unavailable', reason);
                }
            }
        }
        const api = Object.freeze({
            apiVersion: BOORUKIT_API_VERSION,
            register(descriptor) {
                if (descriptor.apiVersion !== BOORUKIT_API_VERSION) {
                    throw new Error(descriptor.id + ' requires API ' + String(descriptor.apiVersion)
                        + '; Core provides API 1');
                }
                if (descriptors.has(descriptor.id)) {
                    throw new Error('duplicate module id: ' + descriptor.id);
                }
                descriptors.set(descriptor.id, descriptor);
            },
            start() {
                startPromise ??= activate();
                return startPromise;
            },
        });
        return { api, descriptors };
    }
    function createRuntimeHarness() {
        const descriptors = new Map();
        const api = Object.freeze({
            apiVersion: BOORUKIT_API_VERSION,
            register(descriptor) {
                if (descriptor.apiVersion !== BOORUKIT_API_VERSION) {
                    throw new Error(descriptor.id + ' requires API ' + String(descriptor.apiVersion)
                        + '; Core provides API 1');
                }
                if (descriptors.has(descriptor.id)) {
                    throw new Error('duplicate module id: ' + descriptor.id);
                }
                descriptors.set(descriptor.id, descriptor);
            },
            start: async () => {
                throw new Error('Core runtime is not installed');
            },
        });
        return { api, descriptors };
    }
    function isRuntimeApi(value) {
        if (!value || typeof value !== 'object')
            return false;
        const candidate = value;
        return candidate.apiVersion === BOORUKIT_API_VERSION
            && typeof candidate.register === 'function'
            && typeof candidate.start === 'function';
    }
    if (typeof window !== 'undefined') {
        const runtimes = globalThis;
        const existing = runtimes[BOORUKIT_RUNTIME_KEY];
        if (existing !== undefined && !isRuntimeApi(existing)) {
            throw new Error('BooruKit runtime key contains an incompatible API');
        }
        if (existing === undefined) {
            const runtime = createCoreRuntime({
                page: createPageContext(location.href),
                storage: createStorageApi(),
                diagnosticSink(source, diagnostic) {
                    console.error('[BooruKit:' + source + '] ' + diagnostic);
                },
            });
            runtimes[BOORUKIT_RUNTIME_KEY] = runtime.api;
        }
    }
    if (typeof window === 'undefined'
        && typeof module !== 'undefined'
        && module.exports) {
        module.exports = {
            createCoreRuntime,
            createPageContext,
            createRuntimeHarness,
            createSequentialQueue,
            createStorageApi,
            createTaskCoordinator,
            defaultSettings,
            sanitizeDiagnostic,
            validateSettings,
        };
    }
})();