Pixiv Downloader (Illustrations/Manga/Ugoira/Avatar/Novel)

Download illustrations, manga, ugoira, user avatars, and novels from Pixiv with optimized speed, ZIP support for multi-page works, and GIF conversion for ugoira

// ==UserScript==
// @name         Pixiv Downloader (Illustrations/Manga/Ugoira/Avatar/Novel)
// @name:en      Pixiv Downloader (Illustrations/Manga/Ugoira/Avatar/Novel)
// @name:vi      Pixiv Downloader (Hình minh họa/Manga/Ugoira/Avatar/Tiểu thuyết)
// @name:zh-CN   Pixiv 下载器 (插画/漫画/动图/头像/小说)
// @name:zh-TW   Pixiv 下載器 (插畫/漫畫/動圖/頭像/小說)
// @name:ja      Pixiv ダウンローダー (イラスト/マンガ/うごイラ/アバター/小説)
// @namespace    http://tampermonkey.net/
// @version      2.1.0
// @description  Download illustrations, manga, ugoira, user avatars, and novels from Pixiv with optimized speed, ZIP support for multi-page works, and GIF conversion for ugoira
// @description:en Download illustrations, manga, ugoira, user avatars, and novels from Pixiv with optimized speed, ZIP support for multi-page works, and GIF conversion for ugoira
// @description:vi Tải xuống hình minh họa, manga, ugoira, avatar người dùng và tiểu thuyết từ Pixiv với tốc độ được tối ưu hóa, hỗ trợ ZIP cho các tác phẩm nhiều trang và chuyển đổi GIF cho ugoira
// @description:zh-CN 下载插画、漫画、动图、用户头像和小说的 Pixiv 下载器,具有优化的速度、对多页作品的 ZIP 支持以及对动图的 GIF 转换
// @description:zh-TW 下載插畫、漫畫、動圖、使用者頭像和小說的 Pixiv 下載器,具有優化的速度、對多頁作品的 ZIP 支援以及對動圖的 GIF 轉換
// @description:ja Pixivからのイラスト、マンガ、うごイラ、ユーザーアバター、小説を最適化された速度、複数ページの作品のZIPサポート、うごイラのGIF変換でダウンロード
// @match        https://www.pixiv.net/en/artworks/*
// @match        https://www.pixiv.net/en/users/*
// @match        https://www.pixiv.net/novel/show.php?id=*
// @match        https://*.fanbox.cc/*
// @match        https://sketch.pixiv.net/*
// @author       RenjiYuusei
// @license      GPL-3.0-only
// @icon         https://www.google.com/s2/favicons?sz=64&domain=pixiv.net
// @grant        GM_xmlhttpRequest
// @grant        GM_download
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @grant        GM_notification
// @grant        GM_setClipboard
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/FileSaver.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/jszip.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/gif.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.min.js
// ==/UserScript==
! function() {
    const e = document.createElement("link");
    e.rel = "stylesheet", e.href = "https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css", document.head.appendChild(e);
    const t = GM_getValue("maxConcurrentDownloads", 5),
        o = new Map;
    GM_addStyle("\n        #pixiv-downloader-status {\n            position: fixed;\n            bottom: 20px;\n            right: 20px;\n            z-index: 9999;\n            display: none;\n        }\n        #pixiv-downloader-progress {\n            position: fixed;\n            bottom: 20px;\n            right: 20px;\n            z-index: 9998;\n            width: 200px;\n            display: none;\n        }\n    ");
    const a = e => {
            const t = o.get(e);
            return t && Date.now() - t.timestamp < 864e5 ? t.data : null
        },
        n = (e, t) => {
            o.set(e, {
                data: t,
                timestamp: Date.now()
            })
        },
        r = async (e, t = {}) => {
            const o = a(e);
            return o || new Promise(((o, a) => {
                GM_xmlhttpRequest({
                    method: t.method || "GET",
                    url: e,
                    responseType: t.responseType || "json",
                    headers: {
                        Referer: "https://www.pixiv.net/"
                    },
                    onload: r => {
                        if (200 === r.status) {
                            const a = "blob" === t.responseType ? r.response : JSON.parse(r.responseText);
                            n(e, a), o(a)
                        } else a(`Failed to fetch: ${r.status}`)
                    },
                    onerror: e => a(`Error fetching: ${e}`)
                })
            }))
        }, s = async (e, t = {}, o = 3, r = 1e3) => {
            for (let s = 1; s <= o; s++) try {
                const o = a(e);
                if (o) return o;
                const l = await new Promise(((o, a) => {
                    GM_xmlhttpRequest({
                        method: t.method || "GET",
                        url: e,
                        responseType: t.responseType || "json",
                        headers: {
                            Referer: "https://www.pixiv.net/",
                            ...t.headers
                        },
                        onload: o,
                        onerror: a
                    })
                }));
                if (200 === l.status) {
                    const o = "blob" === t.responseType ? l.response : JSON.parse(l.responseText);
                    return n(e, o), o
                }
                if (429 !== l.status) throw new Error(`HTTP error! status: ${l.status}`);
                console.warn(`Rate limited on attempt ${s}. Retrying in ${r}ms...`), await new Promise((e => setTimeout(e, r))), r *= 2
            } catch (e) {
                if (s === o) throw e;
                console.warn(`Attempt ${s} failed. Retrying in ${r}ms...`), await new Promise((e => setTimeout(e, r))), r *= 2
            }
        }, l = async () => {
            const e = `https://www.pixiv.net/ajax/illust/${window.location.pathname.split("/").pop()}`;
            return (await r(e)).body
        }, i = e => {
            const t = document.getElementById("pixiv-downloader-status");
            t.textContent = e, t.style.display = "block"
        }, d = () => {
            document.getElementById("pixiv-downloader-status").style.display = "none"
        }, c = e => {
            const t = document.getElementById("pixiv-downloader-progress"),
                o = document.getElementById("pixiv-downloader-progress-bar");
            t.style.display = "block", o.style.width = `${e}%`, o.setAttribute("aria-valuenow", e)
        }, w = () => {
            document.getElementById("pixiv-downloader-progress").style.display = "none"
        }, m = (e, t = "info", o = 3e3) => {
            GM_notification({
                text: e,
                title: "Pixiv Downloader",
                timeout: o,
                onclick: () => {
                    "error" === t && console.error("Pixiv Downloader Error:", e)
                },
                image: "success" === t ? "https://i.imgur.com/oUiCS7Y.png" : "error" === t ? "https://i.imgur.com/LxPNwvr.png" : null
            })
        }, p = (e, t = 0) => {
            if (GM_getValue("useOriginalFilename", !1)) return `${e.id}${t>0?`_p${t}`:""}.${e.urls.original.split(".").pop()}`;
            return `${e.userName.replace(/[^\w\s-]/gi,"")} - ${e.title.replace(/[^\w\s-]/gi,"")} (${e.id})${t>0?`_p${t}`:""}.${e.urls.original.split(".").pop()}`
        }, u = async (e, o = !0) => {
            const a = new JSZip,
                n = [];
            let s = 0;
            for (let l = 0; l < e.pageCount; l++) {
                const u = e.urls.original.replace("_p0", `_p${l}`);
                n.push(r(u, {
                    responseType: "blob"
                }).then((t => {
                    if (s++, i(`Downloaded page ${s}/${e.pageCount}`), c(s / e.pageCount * 100), e.pageCount > 1) return a.file(p(e, l), t);
                    {
                        const a = URL.createObjectURL(t);
                        GM_download({
                            url: a,
                            name: p(e),
                            saveAs: GM_getValue("alwaysSaveAs", !1),
                            onload: () => {
                                URL.revokeObjectURL(a), o && m("Download complete!", "success"), d(), w()
                            },
                            onerror: e => {
                                URL.revokeObjectURL(a), o && m(`Download failed: ${e.message}`, "error"), d(), w()
                            }
                        })
                    }
                })).catch((e => {
                    console.error(`Failed to download page ${l+1}:`, e), o && m(`Failed to download page ${l+1}: ${e.message}`, "error")
                }))), n.length === t && (await Promise.all(n), n.length = 0)
            }
            if (await Promise.all(n), e.pageCount > 1)
                if (s === e.pageCount) {
                    i("Generating ZIP file...");
                    const t = await a.generateAsync({
                            type: "blob"
                        }),
                        n = URL.createObjectURL(t),
                        r = p(e).replace(/\.\w+$/, ".zip");
                    GM_download({
                        url: n,
                        name: r,
                        saveAs: GM_getValue("alwaysSaveAs", !1),
                        onload: () => {
                            URL.revokeObjectURL(n), o && m("Download complete!", "success"), d(), w()
                        },
                        onerror: e => {
                            URL.revokeObjectURL(n), o && m(`Download failed: ${e.message}`, "error"), d(), w()
                        }
                    })
                } else m(`Download incomplete. Only ${s}/${e.pageCount} pages were downloaded.`, "error"), d(), w();
            if (!o) {
                const t = [];
                for (let o = 0; o < e.pageCount; o++) {
                    const a = e.urls.original.replace("_p0", `_p${o}`),
                        n = await r(a, {
                            responseType: "blob"
                        });
                    t.push(n)
                }
                return t
            }
        }, g = async (e, t = !0) => {
            i("Downloading Ugoira...");
            const o = `https://www.pixiv.net/ajax/illust/${e.id}/ugoira_meta`,
                a = await r(o),
                n = a.body.originalSrc;
            try {
                const o = await r(n, {
                        responseType: "blob"
                    }),
                    s = await JSZip.loadAsync(o),
                    l = [],
                    d = a.body.frames.map((e => e.delay));
                let w = 0;
                const m = Object.keys(s.files).length;
                for (const [e, t] of Object.entries(s.files))
                    if (!t.dir) {
                        const e = await t.async("blob"),
                            o = await createImageBitmap(e);
                        l.push(o), w++, c(w / m * 100)
                    } i("Converting Ugoira...");
                const u = GM_getValue("ugoiraOutputFormat", "gif");
                if ("gif" === u) {
                    const o = new GIF({
                        workers: 2,
                        quality: 10,
                        width: l[0].width,
                        height: l[0].height
                    });
                    l.forEach(((e, t) => {
                        o.addFrame(e, {
                            delay: d[t]
                        })
                    })), o.on("progress", (e => {
                        c(100 * e)
                    })), o.on("finished", (o => {
                        const a = URL.createObjectURL(o),
                            n = p(e).replace(/\.\w+$/, ".gif");
                        b(a, n, "GIF", t)
                    })), o.render()
                } else if ("webm" === u || "mp4" === u) {
                    const o = document.createElement("canvas");
                    o.width = l[0].width, o.height = l[0].height;
                    const a = o.getContext("2d"),
                        n = o.captureStream(),
                        r = new MediaRecorder(n, {
                            mimeType: `video/${u}`
                        }),
                        s = [];
                    r.ondataavailable = e => s.push(e.data), r.onstop = async () => {
                        const o = new Blob(s, {
                                type: `video/${u}`
                            }),
                            a = URL.createObjectURL(o),
                            n = p(e).replace(/\.\w+$/, `.${u}`);
                        b(a, n, u.toUpperCase(), t)
                    }, r.start();
                    let i = 0;
                    const c = () => {
                        i < l.length ? (a.clearRect(0, 0, o.width, o.height), a.drawImage(l[i], 0, 0), i++, setTimeout(c, d[i - 1])) : r.stop()
                    };
                    c()
                }
                if (!t) return blob
            } catch (e) {
                console.error("Error processing Ugoira:", e), t && m(`Ugoira download failed: ${e.message}`, "error"), d(), w()
            }
        }, b = (e, t, o, a) => {
            GM_download({
                url: e,
                name: t,
                saveAs: GM_getValue("alwaysSaveAs", !1),
                onload: () => {
                    URL.revokeObjectURL(e), a && m(`Ugoira download and conversion to ${o} complete!`, "success"), d(), w()
                },
                onerror: t => {
                    URL.revokeObjectURL(e), a && m(`Ugoira download failed: ${t.message}`, "error"), d(), w()
                }
            })
        }, h = async () => {
            try {
                const e = await l(),
                    t = `Title: ${e.title}\nArtist: ${e.userName}\nID: ${e.id}\nUpload Date: ${new Date(e.uploadDate).toLocaleString()}\nTags: ${e.tags.tags.map((e=>e.tag)).join(", ")}\nDescription: ${e.description}`;
                GM_setClipboard(t), m("Artwork info copied to clipboard!", "success")
            } catch (e) {
                console.error("Error copying artwork info:", e), m("Error copying artwork info", "error")
            }
        }, v = async () => {
            try {
                if (i("Fetching artwork data..."), window.location.pathname.includes("/novel/show.php")) {
                    const e = new URLSearchParams(window.location.search).get("id");
                    if (!e) throw new Error("Failed to get novel ID.");
                    await f(e)
                } else {
                    if (!window.location.pathname.includes("/artworks/")) throw new Error("Unsupported page for artwork download.");
                    {
                        const e = await l();
                        if (!e) throw new Error("Failed to fetch illustration data.");
                        2 === e.illustType ? await g(e) : await u(e)
                    }
                }
            } catch (e) {
                i(`Error: ${e.message}`), console.error("Pixiv Downloader Error:", e), m(`Error: ${e.message}`, "error"), d()
            }
        }, y = async () => {
            try {
                i("Fetching user data...");
                const e = `https://www.pixiv.net/ajax/user/${window.location.pathname.split("/").pop()}`,
                    t = await r(e);
                if (!t || !t.body) throw new Error("Failed to fetch user data.");
                const o = t.body.imageBig;
                if (!o) throw new Error("Avatar URL not found.");
                i("Downloading avatar...");
                const a = await r(o, {
                        responseType: "blob"
                    }),
                    n = URL.createObjectURL(a),
                    s = `${t.body.name}_avatar.jpg`;
                GM_download({
                    url: n,
                    name: s,
                    saveAs: GM_getValue("alwaysSaveAs", !1),
                    onload: () => {
                        URL.revokeObjectURL(n), m("Avatar download complete!", "success"), d()
                    },
                    onerror: e => {
                        URL.revokeObjectURL(n), m(`Avatar download failed: ${e.message}`, "error"), d()
                    }
                })
            } catch (e) {
                console.error("Error downloading avatar:", e), m(`Avatar download failed: ${e.message}`, "error"), d()
            }
        }, f = async (e, t = !0) => {
            i("Downloading novel...");
            let o, a, n, s = "";
            try {
                const l = (await r(`https://www.pixiv.net/ajax/novel/${e}`)).body;
                o = l.title, a = l.userName, n = l.tags.tags.map((e => e.tag)).join(", ");
                const i = await r(`https://www.pixiv.net/ajax/novel/${e}/content`);
                if (i.error) throw new Error("Failed to fetch novel content");
                s = i.body.content, s = s.replace(/\[newpage\]/g, "\n\n--- New Page ---\n\n");
                const c = `Title: ${o}\nAuthor: ${a}\nID: ${e}\nTags: ${n}\n\n${s}`,
                    w = new Blob([c], {
                        type: "text/plain;charset=utf-8"
                    }),
                    p = URL.createObjectURL(w),
                    u = `${a} - ${o} (${e}).txt`;
                if (GM_download({
                        url: p,
                        name: u,
                        saveAs: GM_getValue("alwaysSaveAs", !1),
                        onload: () => {
                            URL.revokeObjectURL(p), t && m("Novel download complete!", "success"), d()
                        },
                        onerror: e => {
                            URL.revokeObjectURL(p), t && m(`Novel download failed: ${e.message}`, "error"), d()
                        }
                    }), !t) return c
            } catch (e) {
                throw console.error("Error downloading novel:", e), t && m(`Novel download failed: ${e.message}`, "error"), d(), e
            }
        }, $ = async (e, t) => {
            i(`Starting batch download of ${e.length} ${t}s...`);
            const o = new JSZip;
            let a = 0;
            const n = e.map((n => new Promise((async s => {
                try {
                    let s, l;
                    if (n.startsWith("https://"))
                        if (n.includes("fanbox.cc")) {
                            const e = n.match(/\/posts\/(\d+)/);
                            if (!e) throw new Error(`Invalid Fanbox URL: ${n}`);
                            s = e[1], t = "fanbox"
                        } else {
                            const e = n.match(/artworks\/(\d+)/) || n.match(/novel\/show\.php\?id=(\d+)/);
                            if (!e) throw new Error(`Invalid URL: ${n}`);
                            s = e[1]
                        }
                    else s = n;
                    if ("artwork" === t ? (l = await r(`https://www.pixiv.net/ajax/illust/${s}`), l = l.body) : "novel" === t ? (l = await r(`https://www.pixiv.net/ajax/novel/${s}`), l = l.body) : "fanbox" === t && (l = await (async e => {
                            const t = `https://api.fanbox.cc/post.info?postId=${e}`;
                            return (await r(t, {
                                headers: {
                                    Origin: "https://www.fanbox.cc",
                                    Referer: "https://www.fanbox.cc/"
                                }
                            })).body
                        })(s)), "artwork" === t)
                        if (2 === l.illustType) {
                            const e = await g(l, !1);
                            o.file(p(l).replace(/\.\w+$/, ".gif"), e)
                        } else {
                            (await u(l, !1)).forEach(((e, t) => {
                                o.file(p(l, t), e)
                            }))
                        }
                    else if ("novel" === t) {
                        const e = await f(s, !1);
                        o.file(`${l.userName} - ${l.title} (${s}).txt`, e)
                    } else if ("fanbox" === t) {
                        const e = await k(l),
                            t = await e.generateAsync({
                                type: "uint8array"
                            });
                        o.file(`Fanbox_${l.user.name}_${l.id}/`, t)
                    }
                    a++, i(`Downloaded ${a}/${e.length} ${t}s...`), c(a / e.length * 100)
                } catch (e) {
                    console.error(`Error downloading ${t} from input ${n}:`, e)
                }
                s()
            }))));
            await Promise.all(n), i("Generating ZIP file...");
            const s = await o.generateAsync({
                    type: "blob"
                }),
                l = URL.createObjectURL(s),
                w = `Pixiv_Batch_${t}_${(new Date).toISOString().slice(0,10)}.zip`;
            GM_download({
                url: l,
                name: w,
                saveAs: GM_getValue("alwaysSaveAs", !1),
                onload: () => {
                    URL.revokeObjectURL(l), m(`Batch download of ${a} ${t}s complete!`, "success"), d()
                },
                onerror: e => {
                    URL.revokeObjectURL(l), m(`Batch download failed: ${e.message}`, "error"), d()
                }
            })
        }, x = () => {
            const e = document.createElement("div");
            e.innerHTML = '\n            <div class="modal fade" id="batchDownloadModal" tabindex="-1" aria-labelledby="batchDownloadModalLabel" aria-hidden="true">\n                <div class="modal-dialog">\n                    <div class="modal-content">\n                        <div class="modal-header">\n                            <h5 class="modal-title" id="batchDownloadModalLabel">Batch Download</h5>\n                            <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>\n                        </div>\n                        <div class="modal-body">\n                            <p>Enter artwork, novel, or Fanbox IDs or URLs (comma or space separated):</p>\n                            <textarea id="batch-ids" class="form-control mb-3" rows="5"></textarea>\n                            <select id="batch-type" class="form-select mb-3">\n                                <option value="artwork">Artworks</option>\n                                <option value="novel">Novels</option>\n                                <option value="fanbox">Fanbox Posts</option>\n                            </select>\n                        </div>\n                        <div class="modal-footer">\n                            <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>\n                            <button type="button" class="btn btn-primary" id="start-batch-download">Start Download</button>\n                        </div>\n                    </div>\n                </div>\n            </div>\n        ', document.body.appendChild(e);
            const t = new bootstrap.Modal(document.getElementById("batchDownloadModal"));
            t.show(), document.getElementById("start-batch-download").addEventListener("click", (() => {
                const e = document.getElementById("batch-ids").value.split(/[\s,]+/).map((e => e.trim())).filter((e => /^\d+$/.test(e) || e.startsWith("https://www.pixiv.net/")));
                const o = document.getElementById("batch-type").value;
                e.length > 0 ? (t.hide(), $(e, o)) : alert("Please enter valid IDs or URLs.")
            }))
        }, k = async e => {
            const t = new JSZip,
                o = e.title,
                a = e.id,
                n = `Title: ${o}\nArtist: ${e.user.name}\nPost ID: ${a}\nPublish Date: ${new Date(e.publishedDatetime).toLocaleString()}\nUpdate Date: ${new Date(e.updatedDatetime).toLocaleString()}\n\n${e.body.text}`;
            if (t.file("post_info.txt", n), e.body.images && e.body.images.length > 0)
                for (let o = 0; o < e.body.images.length; o++) {
                    const a = e.body.images[o].originalUrl,
                        n = await r(a, {
                            responseType: "blob"
                        }),
                        s = a.split(".").pop();
                    t.file(`image_${o+1}.${s}`, n)
                }
            if (e.body.files && e.body.files.length > 0)
                for (let o = 0; o < e.body.files.length; o++) {
                    const a = e.body.files[o].url,
                        n = await r(a, {
                            responseType: "blob"
                        }),
                        s = e.body.files[o].name;
                    t.file(s, n)
                }
            return t
        }, M = async () => {
            try {
                const e = window.location.pathname.split("/").pop();
                i("Fetching user gallery...");
                let t = 1,
                    o = !0;
                const a = [];
                for (; o;) {
                    const n = `https://www.pixiv.net/ajax/user/${e}/profile/all?lang=en&page=${t}`,
                        s = await r(n);
                    if (s.error) throw new Error("Failed to fetch user gallery.");
                    const l = Object.keys(s.body.illusts);
                    a.push(...l), o = null !== s.body.nextUrl, t++, i(`Fetched ${a.length} artworks...`)
                }
                await $(a, "artwork")
            } catch (e) {
                console.error("Error downloading user gallery:", e), m(`Gallery download failed: ${e.message}`, "error"), d()
            }
        }, U = async () => {
            try {
                const e = window.location.pathname.split("/").pop();
                i("Fetching Pixiv Sketch data...");
                const t = `https://sketch.pixiv.net/api/sketch/${e}`,
                    o = await s(t);
                if (!o || !o.body) throw new Error("Failed to fetch Pixiv Sketch data.");
                const a = o.body.image.original_url,
                    n = await s(a, {
                        responseType: "blob"
                    }),
                    r = URL.createObjectURL(n),
                    l = `PixivSketch_${o.body.user.name}_${e}.jpg`;
                GM_download({
                    url: r,
                    name: l,
                    saveAs: GM_getValue("alwaysSaveAs", !1),
                    onload: () => {
                        URL.revokeObjectURL(r), m("Pixiv Sketch download complete!", "success"), d()
                    },
                    onerror: e => {
                        URL.revokeObjectURL(r), m(`Pixiv Sketch download failed: ${e.message}`, "error"), d()
                    }
                })
            } catch (e) {
                console.error("Error downloading Pixiv Sketch:", e), m(`Pixiv Sketch download failed: ${e.message}`, "error"), d()
            }
        }, _ = (e, t = 0) => {
            const o = GM_getValue("customFilenameFormat", "{artistName} - {artworkTitle} ({id})_{index}"),
                a = e.userName.replace(/[^\w\s-]/gi, ""),
                n = e.title.replace(/[^\w\s-]/gi, ""),
                r = String(t).padStart(3, "0"),
                s = e.urls.original.split(".").pop();
            return o.replace("{artistName}", a).replace("{artworkTitle}", n).replace("{id}", e.id).replace("{index}", r).replace("{ext}", s)
        }, R = async e => {
            try {
                i("Fetching related artworks...");
                const t = `https://www.pixiv.net/ajax/illust/${e}/recommend/init?limit=${maxRelatedArtworks}`,
                    o = await r(t);
                if (!o || !o.body || !o.body.illusts) throw new Error("Failed to fetch related artworks.");
                const a = o.body.illusts,
                    n = new JSZip;
                let s = 0;
                const l = a.length;
                i(`Downloading ${l} related artworks...`);
                for (const e of a) try {
                    const t = (await r(`https://www.pixiv.net/ajax/illust/${e.id}`)).body;
                    if (2 === t.illustType) {
                        const e = await g(t, !1);
                        n.file(`${_(t)}.gif`, e)
                    } else {
                        (await u(t, !1)).forEach(((e, o) => {
                            n.file(_(t, o), e)
                        }))
                    }
                    s++, i(`Downloaded ${s}/${l} artworks...`), c(s / l * 100)
                } catch (t) {
                    console.error(`Error downloading artwork ${e.id}:`, t)
                }
                const p = (await r(`https://www.pixiv.net/ajax/illust/${e}`)).body,
                    b = `Original Artwork:\nTitle: ${p.title}\nArtist: ${p.userName}\nID: ${p.id}\nURL: https://www.pixiv.net/en/artworks/${p.id}\n\nRelated Artworks:\n${a.map((e=>`- ${e.title} by ${e.userName} (ID: ${e.id})`)).join("\n")}\n\nDownloaded using Pixiv Downloader script by RenjiYuusei`;
                n.file("related_artworks_info.txt", b), i("Generating ZIP file...");
                const h = await n.generateAsync({
                        type: "blob"
                    }),
                    v = URL.createObjectURL(h),
                    y = `Related_Artworks_${e}.zip`;
                GM_download({
                    url: v,
                    name: y,
                    saveAs: GM_getValue("alwaysSaveAs", !1),
                    onload: () => {
                        URL.revokeObjectURL(v), m(`Downloaded ${s} related artworks!`, "success"), d(), w()
                    },
                    onerror: e => {
                        URL.revokeObjectURL(v), m(`Related artworks download failed: ${e.message}`, "error"), d(), w()
                    }
                })
            } catch (e) {
                console.error("Error downloading related artworks:", e), m(`Related artworks download failed: ${e.message}`, "error"), d(), w()
            }
        }, E = async () => {
            try {
                i("Fetching bookmarks...");
                const e = document.querySelector('meta[name="global-data"]').getAttribute("content"),
                    t = JSON.parse(e).userId;
                let o = 1,
                    a = !0;
                const n = [];
                for (; a;) {
                    const e = `https://www.pixiv.net/ajax/user/${t}/illusts/bookmarks?tag=&offset=${48*(o-1)}&limit=48&rest=show`,
                        s = await r(e);
                    if (s.error) throw new Error("Failed to fetch bookmarks.");
                    n.push(...s.body.works), a = s.body.hasNext, o++, i(`Fetched ${n.length} bookmarked artworks...`)
                }
                await $(n.map((e => e.id)), "artwork")
            } catch (e) {
                console.error("Error downloading bookmarks:", e), m(`Bookmarks download failed: ${e.message}`, "error"), d()
            }
        };
    window.addEventListener("error", (function(e) {
        console.error("Uncaught error:", e.error), m("An unexpected error occurred. Please check the console for details.", "error")
    })), (() => {
        (() => {
            const e = document.createElement("div");
            e.id = "pixiv-downloader-status", e.className = "alert alert-info", document.body.appendChild(e)
        })(), (() => {
            const e = document.createElement("div");
            e.id = "pixiv-downloader-progress", e.className = "progress";
            const t = document.createElement("div");
            t.id = "pixiv-downloader-progress-bar", t.className = "progress-bar", t.setAttribute("role", "progressbar"), t.setAttribute("aria-valuenow", "0"), t.setAttribute("aria-valuemin", "0"), t.setAttribute("aria-valuemax", "100"), e.appendChild(t), document.body.appendChild(e)
        })();
        const e = (() => {
            const e = document.createElement("div");
            e.innerHTML = '\n    <div class="modal fade" id="settingsModal" tabindex="-1" aria-labelledby="settingsModalLabel" aria-hidden="true">\n      <div class="modal-dialog modal-lg">\n        <div class="modal-content">\n          <div class="modal-header">\n            <h5 class="modal-title" id="settingsModalLabel">Pixiv Downloader Settings</h5>\n            <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>\n          </div>\n          <div class="modal-body">\n            <div class="row">\n              <div class="col-md-6">\n                <div class="form-check mb-3">\n                  <input class="form-check-input" type="checkbox" id="useOriginalFilename">\n                  <label class="form-check-label" for="useOriginalFilename">\n                    Use original filename (e.g., 12345_p0.jpg)\n                  </label>\n                </div>\n                <div class="form-check mb-3">\n                  <input class="form-check-input" type="checkbox" id="alwaysSaveAs">\n                  <label class="form-check-label" for="alwaysSaveAs">\n                    Always show "Save As" dialog\n                  </label>\n                </div>\n                <div class="mb-3">\n                  <label for="customFilenameFormat" class="form-label">Custom filename format:</label>\n                  <input type="text" class="form-control" id="customFilenameFormat" \n                         placeholder="{artistName} - {artworkTitle} ({id})_{index}.{ext}">\n                  <small class="form-text text-muted">\n                    Available placeholders: {artistName}, {artworkTitle}, {id}, {index}, {ext}\n                  </small>\n                </div>\n              </div>\n              <div class="col-md-6">\n                <div class="mb-3">\n                  <label for="downloadPath" class="form-label">Default download path:</label>\n                  <input type="text" class="form-control" id="downloadPath" placeholder="C:\\Downloads\\Pixiv">\n                </div>\n                <div class="mb-3">\n                  <label for="maxConcurrentDownloads" class="form-label">Max concurrent downloads:</label>\n                  <input type="number" class="form-control" id="maxConcurrentDownloads" min="1" max="10">\n                </div>\n                <div class="mb-3">\n                  <label for="maxRelatedArtworks" class="form-label">Max related artworks:</label>\n                  <input type="number" class="form-control" id="maxRelatedArtworks" min="1" max="100">\n                </div>\n              </div>\n            </div>\n            <hr>\n            <div class="form-check mb-3">\n              <input class="form-check-input" type="checkbox" id="autoConvertUgoira">\n              <label class="form-check-label" for="autoConvertUgoira">\n                Automatically convert Ugoira to \n              </label>\n            </div>\n            <div class="mb-3">\n              <label for="ugoiraOutputFormat" class="form-label">Ugoira output format:</label>\n              <select class="form-select" id="ugoiraOutputFormat">\n                <option value="gif">GIF</option>\n                <option value="webm">WebM</option>\n                <option value="mp4">MP4</option>\n              </select>\n            </div>\n          </div>\n          <div class="modal-footer">\n            <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>\n            <button type="button" class="btn btn-primary" id="save-settings">Save Changes</button>\n          </div>\n        </div>\n      </div>\n    </div>\n  ', document.body.appendChild(e);
            const t = new bootstrap.Modal(document.getElementById("settingsModal")),
                o = () => {
                    document.getElementById("customFilenameFormat").value = GM_getValue("customFilenameFormat", "{artistName} - {artworkTitle} ({id})_{index}.{ext}"), document.getElementById("useOriginalFilename").checked = GM_getValue("useOriginalFilename", !1), document.getElementById("alwaysSaveAs").checked = GM_getValue("alwaysSaveAs", !1), document.getElementById("downloadPath").value = GM_getValue("downloadPath", ""), document.getElementById("maxRelatedArtworks").value = GM_getValue("maxRelatedArtworks", 18), document.getElementById("maxConcurrentDownloads").value = GM_getValue("maxConcurrentDownloads", 5), document.getElementById("ugoiraOutputFormat").value = GM_getValue("ugoiraOutputFormat", "gif"), document.getElementById("autoConvertUgoira").checked = GM_getValue("autoConvertUgoira", !0)
                };
            o(), document.getElementById("save-settings").addEventListener("click", (() => {
                GM_setValue("useOriginalFilename", document.getElementById("useOriginalFilename").checked), GM_setValue("alwaysSaveAs", document.getElementById("alwaysSaveAs").checked), GM_setValue("customFilenameFormat", document.getElementById("customFilenameFormat").value), GM_setValue("downloadPath", document.getElementById("downloadPath").value), GM_setValue("maxRelatedArtworks", parseInt(document.getElementById("maxRelatedArtworks").value, 18)), GM_setValue("maxConcurrentDownloads", parseInt(document.getElementById("maxConcurrentDownloads").value, 10)), GM_setValue("ugoiraOutputFormat", document.getElementById("ugoiraOutputFormat").value), GM_setValue("autoConvertUgoira", document.getElementById("autoConvertUgoira").checked), t.hide(), m("Settings saved!", "success")
            }));
            const a = document.createElement("button");
            return a.textContent = "Reset to Default", a.className = "btn btn-warning", a.addEventListener("click", (() => {
                confirm("Are you sure you want to reset all settings to default?") && (GM_deleteValue("customFilenameFormat"), GM_deleteValue("useOriginalFilename"), GM_deleteValue("alwaysSaveAs"), GM_deleteValue("downloadPath"), GM_deleteValue("maxRelatedArtworks"), GM_deleteValue("maxConcurrentDownloads"), GM_deleteValue("ugoiraOutputFormat"), GM_deleteValue("autoConvertUgoira"), o(), m("Settings reset to default!", "success"))
            })), document.querySelector("#settingsModal .modal-footer").prepend(a), t
        })();
        GM_registerMenuCommand("Download Artwork", v), GM_registerMenuCommand("Copy Artwork Info", h), GM_registerMenuCommand("Download User Avatar", y), GM_registerMenuCommand("Batch Download", x), GM_registerMenuCommand("Download User Gallery", M), GM_registerMenuCommand("Download Pixiv Sketch", U), GM_registerMenuCommand("Download Related Artworks", (() => {
            const e = window.location.pathname.split("/").pop();
            e && /^\d+$/.test(e) ? R(e) : m("Cannot determine artwork ID from current page.", "error")
        })), GM_registerMenuCommand("Download Bookmarks", E), GM_registerMenuCommand("Download Novel Series", (() => {
            const e = prompt("Enter the novel series ID:");
            e && /^\d+$/.test(e) ? (async e => {
                i("Fetching novel series data...");
                try {
                    const t = await r(`https://www.pixiv.net/ajax/novel/series/${e}?lang=en`),
                        o = t.body.title,
                        a = t.body.userName;
                    let n = 1,
                        s = !0;
                    const l = [];
                    for (; s;) {
                        const t = `https://www.pixiv.net/ajax/novel/series_content/${e}?limit=30&last_order=${30*n}&order_by=asc&lang=en`,
                            o = await r(t);
                        if (o.error) throw new Error("Failed to fetch novel series content.");
                        l.push(...o.body.seriesContents.map((e => e.id))), s = o.body.hasNext, n++, i(`Fetched ${l.length} novels in the series...`)
                    }
                    const p = new JSZip;
                    let u = 0;
                    for (const e of l) try {
                        const t = await f(e, !1);
                        p.file(`${u+1}.txt`, t), u++, i(`Downloaded ${u}/${l.length} novels...`), c(u / l.length * 100)
                    } catch (t) {
                        console.error(`Error downloading novel ${e}:`, t)
                    }
                    i("Generating ZIP file...");
                    const g = await p.generateAsync({
                            type: "blob"
                        }),
                        b = URL.createObjectURL(g),
                        h = `${a} - ${o} (${e}).zip`;
                    GM_download({
                        url: b,
                        name: h,
                        saveAs: GM_getValue("alwaysSaveAs", !1),
                        onload: () => {
                            URL.revokeObjectURL(b), m(`Novel series download complete! Downloaded ${u} novels.`, "success"), d(), w()
                        },
                        onerror: e => {
                            URL.revokeObjectURL(b), m(`Novel series download failed: ${e.message}`, "error"), d(), w()
                        }
                    })
                } catch (e) {
                    console.error("Error downloading novel series:", e), m(`Novel series download failed: ${e.message}`, "error"), d(), w()
                }
            })(e) : m("Invalid novel series ID.", "error")
        })), GM_registerMenuCommand("Settings", (() => e.show()));
        [{
            name: "Download Artwork",
            fn: v
        }, {
            name: "Copy Artwork Info",
            fn: h
        }, {
            name: "Download User Avatar",
            fn: y
        }, {
            name: "Batch Download",
            fn: x
        }, {
            name: "Download User Gallery",
            fn: M
        }, {
            name: "Download Bookmarks",
            fn: E
        }, {
            name: "Download Pixiv Sketch",
            fn: U
        }, {
            name: "Download Related Artworks",
            fn: () => {
                const e = window.location.pathname.split("/").pop();
                e && /^\d+$/.test(e) ? R(e) : m("Cannot determine artwork ID from current page.", "error")
            }
        }].forEach((e => {
            GM_registerMenuCommand(e.name, e.fn)
        }))
    })()
}();