StoryGraph Enhancer

Zero-click autofill from Douban/Libby to StoryGraph: metadata, rating, cover download, and review sync.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name         StoryGraph Enhancer
// @description  Zero-click autofill from Douban/Libby to StoryGraph: metadata, rating, cover download, and review sync.
// @namespace    1mether.me
// @homepageURL  https://github.com/locoda/StoryGraphEnhancer
// @icon         https://assets.thestorygraph.com/assets/favicon-e44d387265ecfbbf96b3a1935ed01834d1146a91d41711c1c773f933ec3c6916.ico
// @match        https://book.douban.com/subject/*
// @match        https://libbyapp.com/*
// @match        https://www.amazon.com/*
// @match        https://amazon.com/*
// @match        https://amazon.co.jp/*
// @match        https://www.amazon.co.jp/*
// @match        https://app.thestorygraph.com/*
// @version      0.1.4
// @homepage     https://github.com/locoda/StoryGraphEnhancer
// @author       @locoda
// @license      MIT
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_openInTab
// @grant        GM_download
// @grant        GM_registerMenuCommand
// ==/UserScript==


/*
MIT License

Copyright (c) 2020 cvzi

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/


(function () {
    'use strict';

    const STORAGE_KEY = 'db_transfer';
    const POLL_INTERVAL_MS = 1000;
    // StoryGraph surface styles
    const STORYGRAPH_PILL_STYLE = 'display:inline-block; padding:2px 12px; background:#f0f7f2; color:#2e7d32; border:1px solid #2e7d32; border-radius:20px; font-size:12px; font-weight:bold; text-decoration:none; vertical-align:middle; cursor:pointer; margin-left:10px;';
    const STORYGRAPH_INLINE_LINK_STYLE = 'display:inline; color:#2e7d32; font-size:11px; font-weight:500; text-decoration:underline; text-underline-offset:2px; margin-left:8px; opacity:0.75; vertical-align:middle;';
    // Douban surface styles
    const DOUBAN_ASIDE_LINK_STYLE = 'display:inline-block; color:#3377aa; font-size:13px; text-decoration:none; border-bottom:1px solid rgba(51,119,170,0.35);';
    const DOUBAN_ASIDE_LINK_IN_SECTION_STYLE = DOUBAN_ASIDE_LINK_STYLE;
    const DOUBAN_ASIDE_CARD_STYLE = 'padding:18px 16px; background-color:rgb(246, 246, 242); margin:20px auto;';
    const DOUBAN_ASIDE_CARD_TITLE_STYLE = 'font-size:15px; margin:0 0 10px;';

    const DOWNLOAD_DOUBAN_COVER_OPTION_KEY = 'option_download_douban_cover';
    const DOWNLOAD_LIBBY_COVER_OPTION_KEY = 'option_download_libby_cover';
    const EDIT_REVIEW_LINK_OPTION_KEY = 'option_convert_review_links';
    const SEARCH_DOUBAN_OPTION_KEY = 'option_search_douban';
    const SKIP_EXISTING_AUTOFILL_OPTION_KEY = 'option_skip_existing_autofill';
    const ENABLE_DOUBAN_OPTION_KEY = 'option_enable_douban';
    const ENABLE_LIBBY_OPTION_KEY = 'option_enable_libby';
    const ENABLE_AMAZON_OPTION_KEY = 'option_enable_amazon';
    const ENABLE_STORYGRAPH_OPTION_KEY = 'option_enable_storygraph';
    const SETTINGS_MODAL_ID = 'sg-enhancer-settings-modal';
    function readBooleanOption(key, fallback) {
        const value = GM_getValue(key);
        return typeof value === 'boolean' ? value : fallback;
    }
    function writeBooleanOption(key, value) {
        GM_setValue(key, value);
    }
    function getUserOptions() {
        return {
            enableDouban: readBooleanOption(ENABLE_DOUBAN_OPTION_KEY, true),
            enableLibby: readBooleanOption(ENABLE_LIBBY_OPTION_KEY, true),
            enableAmazon: readBooleanOption(ENABLE_AMAZON_OPTION_KEY, true),
            enableStoryGraph: readBooleanOption(ENABLE_STORYGRAPH_OPTION_KEY, true),
            downloadDoubanCover: readBooleanOption(DOWNLOAD_DOUBAN_COVER_OPTION_KEY, true),
            downloadLibbyCover: readBooleanOption(DOWNLOAD_LIBBY_COVER_OPTION_KEY, true),
            convertReviewLinks: readBooleanOption(EDIT_REVIEW_LINK_OPTION_KEY, true),
            showDoubanSearchButton: readBooleanOption(SEARCH_DOUBAN_OPTION_KEY, true),
            skipExistingAutofill: readBooleanOption(SKIP_EXISTING_AUTOFILL_OPTION_KEY, true)
        };
    }
    function createSectionTitle(text) {
        const title = document.createElement('h4');
        title.textContent = text;
        title.style.cssText = 'margin:14px 0 10px; font-size:13px; letter-spacing:0.04em; text-transform:uppercase; color:#6b7280;';
        return title;
    }
    function createOptionToggle(labelText, checked) {
        const wrapper = document.createElement('label');
        wrapper.style.cssText = 'display:flex; align-items:center; gap:8px; font-size:14px; color:#1f2937; margin-bottom:10px; cursor:pointer;';
        const input = document.createElement('input');
        input.type = 'checkbox';
        input.checked = checked;
        const text = document.createElement('span');
        text.textContent = labelText;
        wrapper.appendChild(input);
        wrapper.appendChild(text);
        return { wrapper, input };
    }
    function closeSettingsModal() {
        document.getElementById(SETTINGS_MODAL_ID)?.remove();
    }
    function openSettingsModal() {
        if (!document.body || document.getElementById(SETTINGS_MODAL_ID))
            return;
        const options = getUserOptions();
        const overlay = document.createElement('div');
        overlay.id = SETTINGS_MODAL_ID;
        overlay.style.cssText = 'position:fixed; inset:0; background:rgba(0,0,0,0.45); z-index:100000; display:flex; align-items:center; justify-content:center;';
        const modal = document.createElement('div');
        modal.style.cssText = 'width:min(420px, calc(100vw - 24px)); background:#fff; border-radius:10px; box-shadow:0 12px 40px rgba(0,0,0,0.3); padding:18px 18px 14px; font-family:system-ui,-apple-system,sans-serif;';
        const title = document.createElement('h3');
        title.textContent = 'StoryGraph Enhancer Settings';
        title.style.cssText = 'margin:0 0 12px; font-size:18px; color:#111827;';
        const storyGraphSectionTitle = createSectionTitle('StoryGraph');
        const doubanSectionTitle = createSectionTitle('Douban');
        const libbySectionTitle = createSectionTitle('Libby');
        const amazonSectionTitle = createSectionTitle('Amazon');
        const { wrapper: enableStoryGraphWrapper, input: enableStoryGraphInput } = createOptionToggle('Enable StoryGraph functionality', options.enableStoryGraph);
        const { wrapper: enableDoubanWrapper, input: enableDoubanInput } = createOptionToggle('Enable Douban functionality', options.enableDouban);
        const { wrapper: enableLibbyWrapper, input: enableLibbyInput } = createOptionToggle('Enable Libby functionality', options.enableLibby);
        const { wrapper: enableAmazonWrapper, input: enableAmazonInput } = createOptionToggle('Enable Amazon functionality', options.enableAmazon);
        const { wrapper: downloadDoubanCoverWrapper, input: downloadDoubanCoverInput } = createOptionToggle('Download Douban cover when porting', options.downloadDoubanCover);
        const { wrapper: downloadLibbyCoverWrapper, input: downloadLibbyCoverInput } = createOptionToggle('Download Libby cover when porting', options.downloadLibbyCover);
        const { wrapper: editReviewWrapper, input: editReviewInput } = createOptionToggle('Change review links to “edit review”', options.convertReviewLinks);
        const { wrapper: doubanSearchWrapper, input: doubanSearchInput } = createOptionToggle('Show “search douban” button on missing covers', options.showDoubanSearchButton);
        const { wrapper: skipExistingAutofillWrapper, input: skipExistingAutofillInput } = createOptionToggle('Skip autofill if fields already have text', options.skipExistingAutofill);
        const actions = document.createElement('div');
        actions.style.cssText = 'display:flex; justify-content:flex-end; gap:8px; margin-top:14px;';
        const cancelBtn = document.createElement('button');
        cancelBtn.type = 'button';
        cancelBtn.textContent = 'Cancel';
        cancelBtn.style.cssText = 'padding:6px 12px; border-radius:6px; border:1px solid #cbd5e1; background:#fff; color:#334155; cursor:pointer;';
        cancelBtn.addEventListener('click', closeSettingsModal);
        const saveBtn = document.createElement('button');
        saveBtn.type = 'button';
        saveBtn.textContent = 'Save';
        saveBtn.style.cssText = 'padding:6px 12px; border-radius:6px; border:1px solid #2e7d32; background:#2e7d32; color:#fff; cursor:pointer; font-weight:600;';
        saveBtn.addEventListener('click', () => {
            writeBooleanOption(ENABLE_STORYGRAPH_OPTION_KEY, enableStoryGraphInput.checked);
            writeBooleanOption(ENABLE_DOUBAN_OPTION_KEY, enableDoubanInput.checked);
            writeBooleanOption(ENABLE_LIBBY_OPTION_KEY, enableLibbyInput.checked);
            writeBooleanOption(ENABLE_AMAZON_OPTION_KEY, enableAmazonInput.checked);
            writeBooleanOption(DOWNLOAD_DOUBAN_COVER_OPTION_KEY, downloadDoubanCoverInput.checked);
            writeBooleanOption(DOWNLOAD_LIBBY_COVER_OPTION_KEY, downloadLibbyCoverInput.checked);
            writeBooleanOption(EDIT_REVIEW_LINK_OPTION_KEY, editReviewInput.checked);
            writeBooleanOption(SEARCH_DOUBAN_OPTION_KEY, doubanSearchInput.checked);
            writeBooleanOption(SKIP_EXISTING_AUTOFILL_OPTION_KEY, skipExistingAutofillInput.checked);
            closeSettingsModal();
        });
        overlay.addEventListener('click', (event) => {
            if (event.target === overlay)
                closeSettingsModal();
        });
        actions.appendChild(cancelBtn);
        actions.appendChild(saveBtn);
        modal.appendChild(title);
        modal.appendChild(storyGraphSectionTitle);
        modal.appendChild(enableStoryGraphWrapper);
        modal.appendChild(editReviewWrapper);
        modal.appendChild(doubanSearchWrapper);
        modal.appendChild(skipExistingAutofillWrapper);
        modal.appendChild(doubanSectionTitle);
        modal.appendChild(enableDoubanWrapper);
        modal.appendChild(downloadDoubanCoverWrapper);
        modal.appendChild(libbySectionTitle);
        modal.appendChild(enableLibbyWrapper);
        modal.appendChild(downloadLibbyCoverWrapper);
        modal.appendChild(amazonSectionTitle);
        modal.appendChild(enableAmazonWrapper);
        modal.appendChild(actions);
        overlay.appendChild(modal);
        document.body.appendChild(overlay);
    }
    function registerOptionMenu() {
        if (typeof GM_registerMenuCommand !== 'function')
            return;
        GM_registerMenuCommand('⚙️ StoryGraph Enhancer settings', openSettingsModal);
    }

    function createLinkPill(text, href, style) {
        const link = document.createElement('a');
        link.innerHTML = text;
        link.href = href;
        link.target = '_blank';
        link.rel = 'noopener noreferrer';
        link.style.cssText = style;
        return link;
    }
    function sanitizeFilename(name) {
        return name.replace(/[:/\\?%*|"<>]/g, '_');
    }
    function parsePublicationDate$2(pubDate) {
        const dMatch = pubDate.match(/(\d{4})[\/.\-年\s]+(\d{1,2})[\/.\-月\s]+(\d{1,2})(?:日)?/);
        if (dMatch) {
            const y = dMatch[1];
            const m = dMatch[2].padStart(2, '0');
            const d = dMatch[3].padStart(2, '0');
            return `${y}-${m}-${d}`;
        }
        const months = {
            january: '01', jan: '01',
            february: '02', feb: '02',
            march: '03', mar: '03',
            april: '04', apr: '04',
            may: '05',
            june: '06', jun: '06',
            july: '07', jul: '07',
            august: '08', aug: '08',
            september: '09', sep: '09', sept: '09',
            october: '10', oct: '10',
            november: '11', nov: '11',
            december: '12', dec: '12'
        };
        const englishMatch = pubDate.match(/([A-Za-z]{3,9})\s+(\d{1,2}),\s*(\d{4})/);
        if (!englishMatch)
            return null;
        const month = months[englishMatch[1].toLowerCase()];
        if (!month)
            return null;
        const day = englishMatch[2].padStart(2, '0');
        const year = englishMatch[3];
        return `${year}-${month}-${day}`;
    }

    function normalizeCoverUrl(url) {
        // Some CDNs insert sizing directives like `._SY425_` or `._SX342_SY445_`
        // right before the file extension.
        return url.replace(/\._[A-Z0-9,]+_\.(jpg|jpeg|png|webp)$/i, '.$1');
    }
    function maybeDownloadCover(title, coverUrl) {
        if (!coverUrl)
            return;
        const normalizedCoverUrl = normalizeCoverUrl(coverUrl);
        GM_download({
            url: normalizedCoverUrl,
            name: `${sanitizeFilename(title)}_Cover.jpg`,
            headers: { Referer: window.location.href }
        });
    }

    function cleanText(value) {
        return value.replace(/\u00a0/g, ' ').replace(/\s+/g, ' ').trim();
    }
    function firstText(selectors, root = document) {
        for (const selector of selectors) {
            const value = root.querySelector(selector)?.textContent;
            if (!value)
                continue;
            const cleaned = cleanText(value);
            if (cleaned)
                return cleaned;
        }
        return '';
    }
    function readMeta(...keys) {
        for (const key of keys) {
            const attr = key.startsWith('og:') ? 'property' : 'name';
            const value = document.querySelector(`meta[${attr}="${key}"]`)?.content;
            if (value)
                return cleanText(value);
        }
        return '';
    }
    function normalizeDescriptionText(value) {
        return value
            .replace(/\u00a0/g, ' ')
            .replace(/\r\n?/g, '\n')
            .split('\n')
            .map((line) => line.trim())
            .filter((line, index, arr) => line.length > 0 || (index > 0 && arr[index - 1].length > 0))
            .join('\n')
            .replace(/\n{3,}/g, '\n\n')
            .trim();
    }
    function parseDescriptionFromHtml(html) {
        const prepared = html
            .replace(/<br\s*\/?>/gi, '\n')
            .replace(/<\/p>/gi, '\n\n')
            .replace(/<\/div>/gi, '\n');
        const temp = document.createElement('div');
        temp.innerHTML = prepared;
        temp.querySelectorAll('script, style').forEach((el) => el.remove());
        return normalizeDescriptionText(temp.textContent || '');
    }
    function normalizeDate(raw) {
        const value = cleanText(raw);
        const ymd = value.match(/^(\d{4})[-/.](\d{1,2})[-/.](\d{1,2})$/);
        if (ymd)
            return `${ymd[1]}-${ymd[2].padStart(2, '0')}-${ymd[3].padStart(2, '0')}`;
        const dmy = value.match(/^(\d{1,2})\s+([A-Za-z]{3,9})\s+(\d{4})$/);
        if (!dmy)
            return value;
        const monthMap = {
            jan: '01',
            january: '01',
            feb: '02',
            february: '02',
            mar: '03',
            march: '03',
            apr: '04',
            april: '04',
            may: '05',
            jun: '06',
            june: '06',
            jul: '07',
            july: '07',
            aug: '08',
            august: '08',
            sep: '09',
            sept: '09',
            september: '09',
            oct: '10',
            october: '10',
            nov: '11',
            november: '11',
            dec: '12',
            december: '12'
        };
        const month = monthMap[dmy[2].toLowerCase()];
        if (!month)
            return value;
        return `${dmy[3]}-${month}-${dmy[1].padStart(2, '0')}`;
    }

    function extractInfoValue(info, regex) {
        return info.match(regex)?.[1]?.trim() || '';
    }
    function parseDescription$2() {
        const report = document.querySelector('#link-report');
        const fullSpan = report?.querySelector('.all .intro') || report?.querySelector('.intro');
        const rawDesc = fullSpan ? fullSpan.innerHTML : (report?.innerHTML || '');
        const normalizedHtml = rawDesc
            .replace(/<a[^>]*>(\(展开全部\)\s*)?<\/a>/g, '');
        return normalizeDescriptionText(parseDescriptionFromHtml(normalizedHtml));
    }
    function parseRating() {
        const nRating = document.querySelector('#n_rating')?.value;
        if (nRating)
            return parseFloat(nRating).toFixed(1);
        const starClass = document.querySelector('#interest_sect_level .ll')?.className;
        if (!starClass)
            return '';
        const match = starClass.match(/bigstar(\d+)/);
        if (!match)
            return '';
        return (parseInt(match[1], 10) / 10).toFixed(1);
    }
    function parseShortReview() {
        const reviewSpans = Array.from(document.querySelectorAll('#interest_sect_level .j.a_stars > span:not([class])'));
        if (reviewSpans.length > 0) {
            return reviewSpans[reviewSpans.length - 1].innerText.trim();
        }
        const fallbacks = ['#my_short_comment', '.my-comment .comment-content', 'textarea#comment'];
        for (const selector of fallbacks) {
            const el = document.querySelector(selector);
            if (!el)
                continue;
            const val = (el instanceof HTMLTextAreaElement ? el.value : el.innerText).trim();
            if (val)
                return val;
        }
        return '';
    }
    function parseDoubanPage() {
        const info = document.querySelector('#info')?.innerText || '';
        const title = document.querySelector('h1 span')?.innerText || 'book';
        const coverUrl = document.querySelector('#mainpic a.nbg')?.href || '';
        return {
            title,
            originalTitle: extractInfoValue(info, /原作名:?\s*(.+)/),
            author: extractInfoValue(info, /作者:?\s*(.+)/),
            language: extractInfoValue(info, /语言:?\s*(.+)/),
            publisher: extractInfoValue(info, /出版社:?\s*(.+)/),
            isbn: extractInfoValue(info, /ISBN:?\s*(\d+)/),
            pages: extractInfoValue(info, /页数:?\s*(\d+)/),
            pubDate: extractInfoValue(info, /出版年:?\s*(.+)/),
            format: info.includes('精装') ? 'hardcover' : (info.includes('平装') ? 'paperback' : ''),
            desc: parseDescription$2(),
            rating: parseRating(),
            review: parseShortReview(),
            coverUrl
        };
    }

    function openStoryGraphSearch$2(data) {
        const query = data.isbn || data.title;
        const url = `https://app.thestorygraph.com/browse?search_term=${encodeURIComponent(query)}`;
        if (typeof GM_openInTab === 'function') {
            GM_openInTab(url, { active: true });
        }
        else {
            window.open(url, '_blank');
        }
    }
    function injectDoubanPortButton() {
        if (document.getElementById('port-to-sg-btn'))
            return;
        const btn = document.createElement('a');
        btn.id = 'port-to-sg-btn';
        btn.innerHTML = 'Port to StoryGraph';
        btn.href = '#';
        const aside = document.querySelector('.aside');
        if (aside) {
            btn.style.cssText = DOUBAN_ASIDE_LINK_IN_SECTION_STYLE;
            const wrapper = document.createElement('div');
            wrapper.id = 'port-to-sg-card';
            wrapper.style.cssText = DOUBAN_ASIDE_CARD_STYLE;
            const title = document.createElement('h2');
            title.style.cssText = DOUBAN_ASIDE_CARD_TITLE_STYLE;
            title.innerHTML = '<span>StoryGraph</span>&nbsp;·&nbsp;·&nbsp;·&nbsp;·&nbsp;·&nbsp;·&nbsp;';
            wrapper.appendChild(title);
            wrapper.appendChild(btn);
            aside.insertBefore(wrapper, aside.firstChild);
            window.setTimeout(() => {
                if (wrapper.parentElement === aside) {
                    aside.insertBefore(wrapper, aside.firstChild);
                }
            }, 1200);
        }
        else {
            btn.style.cssText = DOUBAN_ASIDE_LINK_STYLE;
            const h1 = document.querySelector('h1');
            if (!h1)
                return;
            const clear = h1.querySelector('.clear');
            if (clear) {
                h1.insertBefore(btn, clear);
            }
            else {
                h1.appendChild(btn);
            }
        }
        btn.addEventListener('click', (event) => {
            event.preventDefault();
            const data = parseDoubanPage();
            const options = getUserOptions();
            if (options.downloadDoubanCover) {
                maybeDownloadCover(data.title, data.coverUrl);
            }
            GM_setValue(STORAGE_KEY, JSON.stringify(data));
            openStoryGraphSearch$2(data);
        });
    }

    function getFactCell(label) {
        const rows = Array.from(document.querySelectorAll('table.screen-title-details-facts tr'));
        for (const row of rows) {
            const key = cleanText(row.querySelector('th')?.textContent || '');
            if (key.toLowerCase() !== label.toLowerCase())
                continue;
            return row.querySelector('td');
        }
        return null;
    }
    function getFactText(label) {
        const cell = getFactCell(label);
        if (!cell)
            return '';
        return cleanText(cell.textContent || '');
    }
    function getFactLinkedNames(label) {
        const cell = getFactCell(label);
        if (!cell)
            return '';
        const links = Array.from(cell.querySelectorAll('a.nav-list'));
        if (links.length === 0)
            return cleanText(cell.textContent || '');
        const names = links
            .map((link) => cleanText((link.childNodes[0]?.textContent || link.textContent || '')))
            .filter(Boolean);
        return names.join(', ');
    }
    function parseTitle$1() {
        const direct = document.querySelector('.screen-title-details-title')?.innerText;
        if (direct)
            return cleanText(direct);
        return readMeta('og:title') || 'book';
    }
    function parseAuthor() {
        const fromFacts = getFactLinkedNames('Author');
        if (fromFacts)
            return fromFacts;
        const attribution = document.querySelector('.screen-title-details-attribution')?.innerText;
        if (attribution)
            return cleanText(attribution);
        return '';
    }
    function parseLanguage$1() {
        return getFactText('Language');
    }
    function parsePublisher$1() {
        return getFactText('Publisher') || getFactText('Imprint');
    }
    function parseIsbn$1() {
        return '';
    }
    function parsePages$1() {
        const pagesFacts = getFactText('Pages');
        const pagesFromFacts = pagesFacts.match(/\b(\d{1,5})\b/)?.[1];
        if (pagesFromFacts)
            return pagesFromFacts;
        const text = document.body?.innerText || '';
        return text.match(/\b(\d{1,5})\s+pages?\b/i)?.[1] || '';
    }
    function parsePublicationDate$1() {
        const release = getFactText('Release');
        if (release)
            return normalizeDate(release);
        const published = getFactText('Published');
        if (published)
            return normalizeDate(published);
        return '';
    }
    function parseFormat$1() {
        const format = getFactText('Format').toLowerCase();
        if (format.includes('audiobook'))
            return 'audio';
        if (format.includes('book'))
            return 'digital';
        return '';
    }
    function parseDescription$1() {
        const blurbParagraphs = Array.from(document.querySelectorAll('div.screen-title-details-blurb p'))
            .map((el) => cleanText(el.textContent || ''))
            .filter(Boolean);
        if (blurbParagraphs.length > 0)
            return blurbParagraphs.join('\n\n');
        const selectors = [
            '.screen-title-details-blurb',
            '.screen-title-description',
            '.screen-title-details-description',
            '[data-testid="description"]',
            '.book-description'
        ];
        for (const selector of selectors) {
            const el = document.querySelector(selector);
            const value = el?.innerText || el?.textContent;
            if (value)
                return cleanText(value);
        }
        return readMeta('description', 'og:description');
    }
    function parseCoverUrl$1() {
        return (document.querySelector('.screen-title-details-jacket .cover-box-press .cover-box-clip img.directed-image')?.src ||
            readMeta('og:image'));
    }
    function parseLibbyPage() {
        return {
            title: parseTitle$1(),
            originalTitle: '',
            author: parseAuthor(),
            language: parseLanguage$1(),
            publisher: parsePublisher$1(),
            isbn: parseIsbn$1(),
            pages: parsePages$1(),
            pubDate: parsePublicationDate$1(),
            format: parseFormat$1(),
            desc: parseDescription$1(),
            rating: '',
            review: '',
            coverUrl: parseCoverUrl$1()
        };
    }

    function openStoryGraphSearch$1(data) {
        const query = data.isbn || data.title;
        const url = `https://app.thestorygraph.com/browse?search_term=${encodeURIComponent(query)}`;
        if (typeof GM_openInTab === 'function') {
            GM_openInTab(url, { active: true });
        }
        else {
            window.open(url, '_blank');
        }
    }
    function getLibbyButtonHost() {
        return (document.querySelector('div.screen-title-details-under-cover') ||
            document.querySelector('[data-testid="title-and-author"]'));
    }
    function isLikelyBookSurface() {
        if (getLibbyButtonHost())
            return true;
        const path = window.location.pathname;
        return path.includes('/shelf/') && /\/\d+\/?$/.test(path);
    }
    function createPortButton() {
        const btn = document.createElement('a');
        btn.id = 'libby-port-to-sg-btn';
        btn.href = '#';
        btn.textContent = 'Port to StoryGraph';
        btn.style.cssText = STORYGRAPH_PILL_STYLE;
        return btn;
    }
    function injectLibbyPortButton() {
        if (!isLikelyBookSurface())
            return;
        if (!document.body)
            return;
        const host = getLibbyButtonHost();
        if (!host)
            return;
        const existing = document.getElementById('libby-port-to-sg-btn');
        if (existing) {
            if (existing.parentElement !== host)
                host.appendChild(existing);
            return;
        }
        const btn = createPortButton();
        host.appendChild(btn);
        btn.addEventListener('click', (event) => {
            event.preventDefault();
            const data = parseLibbyPage();
            const options = getUserOptions();
            if (options.downloadLibbyCover) {
                maybeDownloadCover(data.title, data.coverUrl);
            }
            GM_setValue(STORAGE_KEY, JSON.stringify(data));
            openStoryGraphSearch$1(data);
        });
    }

    function readRpiAttribute(...attributeNames) {
        for (const name of attributeNames) {
            const byId = firstText([`#rpi-attribute-${name} .rpi-attribute-value`]);
            if (byId)
                return byId;
            const byDataName = firstText([`[data-rpi-attribute-name="${name}"] .rpi-attribute-value`]);
            if (byDataName)
                return byDataName;
        }
        return '';
    }
    function readDetailValue(labels) {
        const candidates = Array.from(document.querySelectorAll('#detailBullets_feature_div li, #productDetailsTable tr, #detailBulletsWrapper_feature_div li'));
        for (const row of candidates) {
            const text = cleanText(row.textContent || '');
            if (!text)
                continue;
            for (const label of labels) {
                const regex = new RegExp(`^${label}\\s*[::]\\s*(.+)$`, 'i');
                const match = text.match(regex);
                if (match?.[1])
                    return cleanText(match[1]);
            }
        }
        return '';
    }
    function parseTitle() {
        return firstText(['#productTitle', '#ebooksProductTitle']) || readMeta('og:title') || 'book';
    }
    function parseAuthors() {
        const links = Array.from(document.querySelectorAll('#bylineInfo a, .author a.a-link-normal'))
            .map((link) => cleanText(link.textContent || ''))
            .filter((text) => text && !/visit amazon/i.test(text) && !/format/i.test(text));
        if (links.length > 0)
            return Array.from(new Set(links)).join(', ');
        return '';
    }
    function parseIsbn() {
        const direct = readRpiAttribute('book_details-isbn13', 'book_details-isbn10');
        if (direct) {
            const digits = direct.replace(/[^0-9X]/gi, '');
            if (digits)
                return digits;
        }
        const fromDetails = readDetailValue(['ISBN-13', 'ISBN-10', 'ISBN', 'ISBN-13(国際標準図書番号)', 'ISBN-10(国際標準図書番号)']);
        return fromDetails.replace(/[^0-9X]/gi, '');
    }
    function parseListeningLength() {
        const value = readRpiAttribute('audiobook_details-listening_length') || readDetailValue(['Listening Length', '再生時間']);
        if (!value)
            return '';
        const hm = value.match(/(\d{1,3})\s*(?:hours?|hrs?|h|時間)\s*(?:and\s*)?(\d{1,2})\s*(?:minutes?|mins?|m|分)/i);
        if (hm)
            return `${hm[1].padStart(2, '0')}:${hm[2].padStart(2, '0')}`;
        const hOnly = value.match(/(\d{1,3})\s*(?:hours?|hrs?|h|時間)/i);
        if (hOnly)
            return `${hOnly[1].padStart(2, '0')}:00`;
        const mOnly = value.match(/(\d{1,4})\s*(?:minutes?|mins?|m|分)/i);
        if (mOnly) {
            const totalMinutes = parseInt(mOnly[1], 10);
            const hours = Math.floor(totalMinutes / 60);
            const minutes = totalMinutes % 60;
            return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`;
        }
        return '';
    }
    function parsePages() {
        const listeningLength = parseListeningLength();
        if (listeningLength)
            return listeningLength;
        const value = readRpiAttribute('book_details-ebook_pages', 'book_details-hardcover_pages', 'book_details-paperback_pages', 'book_details-fiona_pages', 'book_details-print_pages') || readDetailValue(['Print length', 'Pages', 'ページ数']);
        return value.match(/\b(\d{1,5})\b/)?.[1] || '';
    }
    function parsePublisher() {
        const value = readRpiAttribute('book_details-publisher') || readDetailValue(['Publisher', '出版社']);
        return cleanText(value.replace(/\([^)]*\)/g, ''));
    }
    function parsePublicationDate() {
        return readRpiAttribute('book_details-publication_date') || readDetailValue(['Publication date', '発売日', '出版日']);
    }
    function parseLanguage() {
        return readRpiAttribute('language', 'book_details-language') || readDetailValue(['Language', '言語']);
    }
    function parseDescription() {
        const htmlCandidates = ['#bookDescription_feature_div noscript', '#bookDescription_feature_div .a-expander-content', '#bookDescription_feature_div'];
        for (const selector of htmlCandidates) {
            const el = document.querySelector(selector);
            if (!el)
                continue;
            const value = parseDescriptionFromHtml(el.innerHTML);
            if (value)
                return value;
        }
        const paragraphText = Array.from(document.querySelectorAll('#productDescription p'))
            .map((el) => normalizeDescriptionText(el.textContent || ''))
            .filter(Boolean)
            .join('\n\n');
        if (paragraphText)
            return paragraphText;
        return normalizeDescriptionText(readMeta('description', 'og:description'));
    }
    function parseCoverUrl() {
        const image = document.querySelector('#landingImage, #imgBlkFront, #ebooksImgBlkFront');
        if (image?.src)
            return image.src;
        return readMeta('og:image');
    }
    function parseFormat() {
        const formatText = firstText(['#productSubtitle', '#formatSelector', '#tmmSwatches']).toLowerCase();
        if (formatText.includes('hardcover') || formatText.includes('ハードカバー'))
            return 'hardcover';
        if (formatText.includes('paperback') || formatText.includes('単行本') || formatText.includes('文庫'))
            return 'paperback';
        if (formatText.includes('kindle') || formatText.includes('ebook') || formatText.includes('電子書籍'))
            return 'digital';
        if (formatText.includes('audio') || formatText.includes('audible'))
            return 'audio';
        return '';
    }
    function parseAmazonPage() {
        return {
            title: parseTitle(),
            originalTitle: '',
            author: parseAuthors(),
            language: parseLanguage(),
            publisher: parsePublisher(),
            isbn: parseIsbn(),
            pages: parsePages(),
            pubDate: parsePublicationDate(),
            format: parseFormat(),
            desc: parseDescription(),
            rating: '',
            review: '',
            coverUrl: parseCoverUrl()
        };
    }

    function openStoryGraphSearch(data) {
        const query = data.isbn || data.title;
        const url = `https://app.thestorygraph.com/browse?search_term=${encodeURIComponent(query)}`;
        if (typeof GM_openInTab === 'function') {
            GM_openInTab(url, { active: true });
        }
        else {
            window.open(url, '_blank');
        }
    }
    function getAmazonButtonHost() {
        return (document.querySelector('#bylineInfo') ||
            document.querySelector('#title') ||
            document.querySelector('#centerCol'));
    }
    function isAmazonBookPage() {
        return /\/dp\//.test(window.location.pathname) || /\/gp\/product\//.test(window.location.pathname);
    }
    function injectAmazonPortButton() {
        if (!isAmazonBookPage())
            return;
        const host = getAmazonButtonHost();
        if (!host)
            return;
        const existing = document.getElementById('amazon-port-to-sg-btn');
        if (existing) {
            if (existing.parentElement !== host)
                host.appendChild(existing);
            return;
        }
        const btn = document.createElement('a');
        btn.id = 'amazon-port-to-sg-btn';
        btn.href = '#';
        btn.textContent = 'Port to StoryGraph';
        btn.style.cssText = STORYGRAPH_PILL_STYLE;
        host.appendChild(btn);
        btn.addEventListener('click', (event) => {
            event.preventDefault();
            const data = parseAmazonPage();
            GM_setValue(STORAGE_KEY, JSON.stringify(data));
            openStoryGraphSearch(data);
        });
    }

    const REPLACE_BUTTON_CLASS = 'sg-enhancer-replace-button';
    const REPLACE_BUTTON_STYLE = 'display:inline-flex; align-items:center; margin:4px 0 4px 8px; padding:3px 8px; border:1px solid #2e7d32; border-radius:999px; background:#f0f7f2; color:#2e7d32; font-size:11px; font-weight:700; line-height:1.4; cursor:pointer; vertical-align:middle;';
    const STORYGRAPH_LANGUAGE_ALIASES = {
        '日本語': 'japanese',
        '中文': 'chinese',
        '汉语': 'chinese',
        '漢語': 'chinese',
        '中国語': 'chinese',
        '한국어': 'korean'
    };
    function normalizeStoryGraphLanguage(language) {
        const trimmedLanguage = language.trim();
        return STORYGRAPH_LANGUAGE_ALIASES[trimmedLanguage] || trimmedLanguage;
    }
    function dispatchValueEvents(el) {
        el.dispatchEvent(new Event('input', { bubbles: true }));
        el.dispatchEvent(new Event('change', { bubbles: true }));
    }
    function applyValue(el, value) {
        el.value = value;
        dispatchValueEvents(el);
        el.setAttribute('data-filled', 'true');
    }
    function getReplaceTargetKey(target) {
        return encodeURIComponent(target.id || target.getAttribute('name') || target.tagName);
    }
    function addReplaceButton(target, value, onReplace) {
        const replaceTargetKey = getReplaceTargetKey(target);
        if (target.parentElement?.querySelector(`.${REPLACE_BUTTON_CLASS}[data-replace-target="${replaceTargetKey}"]`))
            return;
        const button = document.createElement('button');
        button.type = 'button';
        button.className = REPLACE_BUTTON_CLASS;
        button.dataset.replaceTarget = replaceTargetKey;
        button.textContent = 'Replace';
        button.title = `Replace existing text with imported value: ${value}`;
        button.style.cssText = REPLACE_BUTTON_STYLE;
        button.addEventListener('click', () => {
            onReplace();
            button.remove();
        });
        target.insertAdjacentElement('afterend', button);
    }
    function setValueByName(name, value, skipExistingAutofill) {
        const selector = `[name*="[${name}]"] , [name="${name}"] , [id*="_${name}"]`;
        const el = document.querySelector(selector);
        if (!el || !value || el.hasAttribute('data-filled'))
            return false;
        const existingValue = el.value.trim();
        if (skipExistingAutofill && existingValue) {
            if (existingValue !== value)
                addReplaceButton(el, value, () => applyValue(el, value));
            return false;
        }
        applyValue(el, value);
        return true;
    }
    function applyTrixContent(trix, content) {
        trix.editor.loadHTML('');
        const formattedHTML = content
            .split('\n')
            .map((line) => line.trim() ? `<div>${line}</div>` : '<div><br></div>')
            .join('');
        trix.editor.insertHTML(formattedHTML);
        trix.setAttribute('data-filled', 'true');
    }
    function fillTrixEditor(content, skipExistingAutofill) {
        const trix = document.querySelector('trix-editor');
        if (!trix || !trix.editor || trix.hasAttribute('data-filled') || !content)
            return false;
        const existingText = trix.editor.getDocument?.().toString?.().trim?.() ?? '';
        if (skipExistingAutofill && existingText) {
            if (existingText !== content.trim())
                addReplaceButton(trix, content, () => applyTrixContent(trix, content));
            return false;
        }
        applyTrixContent(trix, content);
        return true;
    }
    function showToast(title) {
        if (document.getElementById('sync-toast'))
            return;
        const toast = document.createElement('div');
        toast.id = 'sync-toast';
        toast.style.cssText = 'position:fixed; top:20px; right:20px; z-index:10001; padding:10px 20px; background:#2e7d32; color:white; border-radius:5px; font-weight:bold; box-shadow:0 2px 10px rgba(0,0,0,0.2);';
        const label = document.createElement('div');
        label.textContent = `✅ Data Ready: ${title}`;
        toast.appendChild(label);
        document.body.appendChild(toast);
        window.setTimeout(() => {
            toast.remove();
        }, 4000);
    }
    function parseStoredData$1() {
        const raw = GM_getValue(STORAGE_KEY);
        if (!raw)
            return null;
        try {
            return JSON.parse(raw);
        }
        catch {
            return null;
        }
    }
    function maybeAutofillStoryGraph() {
        const skipExistingAutofill = getUserOptions().skipExistingAutofill;
        const path = window.location.pathname;
        const isReviewPage = path.includes('/reviews/') || path.includes('/write_review');
        const isBookPage = path.includes('/add') ||
            path.includes('/update_missing_info') ||
            path.includes('/add_missing_info') ||
            path.includes('/books/new') ||
            (path.includes('/books') && path.includes('/edit'));
        if (!isReviewPage && !isBookPage)
            return;
        const data = parseStoredData$1();
        if (!data)
            return;
        let didFillField = false;
        const setValue = (name, value) => setValueByName(name, value, skipExistingAutofill);
        didFillField = setValue('title', data.title) || didFillField;
        didFillField = setValue('author_names', data.author) || didFillField;
        didFillField = setValue('language', normalizeStoryGraphLanguage(data.language)) || didFillField;
        didFillField = setValue('publisher', data.publisher) || didFillField;
        didFillField = setValue('isbn', data.isbn) || didFillField;
        didFillField = setValue('number_of_pages', data.pages) || didFillField;
        didFillField = setValue('format', data.format) || didFillField;
        didFillField = setValue('rating', data.rating) || didFillField;
        if (data.pubDate) {
            const parsed = parsePublicationDate$2(data.pubDate);
            if (parsed)
                didFillField = setValue('publication_date', parsed) || didFillField;
        }
        didFillField = fillTrixEditor(isReviewPage ? data.review : data.desc, skipExistingAutofill) || didFillField;
        if (!didFillField)
            return;
        showToast(data.title);
        window.setTimeout(() => {
            GM_setValue(STORAGE_KEY, null);
        }, 4000);
    }

    const FALLBACK_FLAG = 'sg_enhancer_original_title_fallback_done';
    function parseStoredData() {
        const raw = GM_getValue(STORAGE_KEY);
        if (!raw)
            return null;
        try {
            return JSON.parse(raw);
        }
        catch {
            return null;
        }
    }
    function hasNoSearchResult() {
        const text = document.body?.innerText || '';
        const markers = [
            'There\'s nothing on The StoryGraph matching',
            'If needed, you can manually add a book to our database.',
            'No books found',
            'No results',
            'Try searching for a different title',
            '0 books'
        ];
        return markers.some((marker) => text.includes(marker));
    }
    function maybeFallbackToOriginalTitleSearch() {
        const url = new URL(window.location.href);
        if (url.pathname !== '/browse')
            return;
        const searchTerm = (url.searchParams.get('search_term') || '').trim();
        if (!searchTerm)
            return;
        const data = parseStoredData();
        if (!data?.isbn || !data.originalTitle)
            return;
        if (searchTerm !== data.isbn)
            return;
        if (sessionStorage.getItem(FALLBACK_FLAG) === searchTerm)
            return;
        if (!hasNoSearchResult())
            return;
        sessionStorage.setItem(FALLBACK_FLAG, searchTerm);
        const nextUrl = `https://app.thestorygraph.com/browse?search_term=${encodeURIComponent(data.originalTitle)}`;
        window.location.assign(nextUrl);
    }

    function extractBookPath(anchor) {
        const href = anchor.getAttribute('href') || '';
        const match = href.match(/\/books\/[^/?#]+/);
        return match?.[0] || '';
    }
    function toReviewEditHref(href) {
        if (!href)
            return href;
        let url;
        try {
            url = new URL(href, window.location.origin);
        }
        catch {
            return href;
        }
        const match = url.pathname.match(/^\/reviews\/([^/?#]+)(?:\/edit)?\/?$/);
        if (!match)
            return href;
        const [, reviewId] = match;
        url.pathname = `/reviews/${reviewId}/edit`;
        return url.origin === window.location.origin
            ? `${url.pathname}${url.search}${url.hash}`
            : url.toString();
    }
    function updateExistingReviewLinks() {
        const reviewLinks = Array.from(document.querySelectorAll('a[href*="/reviews/"]'));
        for (const link of reviewLinks) {
            const normalized = link.textContent?.trim().toLowerCase() || '';
            if (!normalized || normalized.includes('add review'))
                continue;
            const href = link.getAttribute('href') || '';
            const editHref = toReviewEditHref(href);
            if (editHref && editHref !== href) {
                link.setAttribute('href', editHref);
            }
            if (normalized !== 'edit review') {
                link.textContent = 'edit review';
            }
        }
    }
    function extractIsbnFromCard(cardContainer) {
        const text = cardContainer.innerText || '';
        const isbnMatch = text.match(/\b(?:97[89][-\s]?)?\d(?:[-\s]?\d){8,15}\b/);
        if (!isbnMatch)
            return '';
        const normalized = isbnMatch[0].replace(/[-\s]/g, '');
        if (normalized.length !== 10 && normalized.length !== 13)
            return '';
        return normalized;
    }
    function buildDoubanSearchHref(query) {
        const encoded = encodeURIComponent(query);
        return `https://search.douban.com/book/subject_search?search_text=${encoded}&cat=1001`;
    }
    function injectBookLinks(h3, options) {
        if (h3.hasAttribute('data-sg-enhanced'))
            return;
        const titleLink = h3.querySelector('a[href*="/books/"]');
        if (!titleLink)
            return;
        const bookPath = extractBookPath(titleLink);
        if (!bookPath)
            return;
        if (!isBookCoverUnavailable(h3, bookPath)) {
            h3.setAttribute('data-sg-enhanced', 'true');
            return;
        }
        const base = window.location.origin;
        const metadataLink = createLinkPill('add metadata', `${base}${bookPath}/add_missing_info`, STORYGRAPH_INLINE_LINK_STYLE);
        const title = titleLink.textContent?.trim() || '';
        const cardContainer = h3.closest('.grid, article, li, tr, section');
        const isbn = cardContainer ? extractIsbnFromCard(cardContainer) : '';
        const doubanQuery = isbn || title;
        titleLink.after(metadataLink);
        if (options.showDoubanSearchButton && doubanQuery) {
            const doubanSearchLink = createLinkPill('search douban', buildDoubanSearchHref(doubanQuery), STORYGRAPH_INLINE_LINK_STYLE);
            metadataLink.after(doubanSearchLink);
        }
        h3.setAttribute('data-sg-enhanced', 'true');
    }
    function isBookCoverUnavailable(h3, bookPath) {
        const cardContainer = h3.closest('.grid, article, li, tr, section');
        if (!cardContainer)
            return false;
        const coverColumn = cardContainer.querySelector('.cover-image-column') || cardContainer;
        const coverWrapper = coverColumn.querySelector('.book-cover, .placeholder-cover');
        const coverImage = coverColumn.querySelector(`a[href*="${bookPath}"] img`) || coverColumn.querySelector('img');
        const wrapperText = `${coverWrapper?.className || ''}`.toLowerCase();
        if (wrapperText.includes('placeholder-cover'))
            return true;
        if (!coverImage)
            return true;
        const sourceText = `${coverImage.currentSrc} ${coverImage.src} ${coverImage.srcset} ${coverImage.alt} ${coverImage.className}`.toLowerCase();
        const placeholderMarkers = ['placeholder-cover', 'placeholder', 'missing', 'no_cover', 'default_cover', 'book-cover-empty'];
        return placeholderMarkers.some(marker => sourceText.includes(marker));
    }
    function injectStoryGraphShortcuts() {
        const options = getUserOptions();
        if (options.convertReviewLinks) {
            updateExistingReviewLinks();
        }
        const headers = document.querySelectorAll('h3');
        headers.forEach((header) => injectBookLinks(header, options));
    }

    function runStoryGraphFeatures() {
        injectStoryGraphShortcuts();
        maybeFallbackToOriginalTitleSearch();
        maybeAutofillStoryGraph();
    }
    function bootDouban() {
        injectDoubanPortButton();
        window.setInterval(injectDoubanPortButton, POLL_INTERVAL_MS);
    }
    function bootLibby() {
        injectLibbyPortButton();
        if (document.body) {
            const observer = new MutationObserver(injectLibbyPortButton);
            observer.observe(document.body, { childList: true, subtree: true });
        }
        window.setInterval(injectLibbyPortButton, POLL_INTERVAL_MS);
    }
    function bootAmazon() {
        injectAmazonPortButton();
        window.setInterval(injectAmazonPortButton, POLL_INTERVAL_MS);
    }
    function bootStoryGraph() {
        runStoryGraphFeatures();
        if (!document.body)
            return;
        const observer = new MutationObserver(runStoryGraphFeatures);
        observer.observe(document.body, { childList: true, subtree: true });
        window.setInterval(runStoryGraphFeatures, POLL_INTERVAL_MS);
    }
    (function main() {
        registerOptionMenu();
        const options = getUserOptions();
        const host = window.location.host;
        if (host === 'book.douban.com') {
            if (!options.enableDouban)
                return;
            bootDouban();
            return;
        }
        if (host === 'libbyapp.com' || host.endsWith('.libbyapp.com')) {
            if (!options.enableLibby)
                return;
            bootLibby();
            return;
        }
        if (host === 'www.amazon.com' || host === 'amazon.com' || host.endsWith('.amazon.com') || host === 'www.amazon.co.jp' || host === 'amazon.co.jp' || host.endsWith('.amazon.co.jp')) {
            if (!options.enableAmazon)
                return;
            bootAmazon();
            return;
        }
        if (host === 'app.thestorygraph.com') {
            if (!options.enableStoryGraph)
                return;
            bootStoryGraph();
        }
    })();

})();
//# sourceMappingURL=storygraph-enhancer.user.js.map