BooruKit Core

Shared runtime and feature registry for the BooruKit userscript suite.

Ten skrypt nie powinien być instalowany bezpośrednio. Jest to biblioteka dla innych skyptów do włączenia dyrektywą meta // @require https://update.sleazyfork.org/scripts/589427/1890613/BooruKit%20Core.js

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

You will need to install an extension such as Tampermonkey to install this script.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

// ==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,
        };
    }
})();