DLsite Thumbnails Downloader v0.1.1 (20250805)

Download DLsite product thumbnails in .webp or .jpg

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         DLsite Thumbnails Downloader v0.1.1 (20250805)
// @name:ko      DLsite Thumbnails(썸네일) 다운로더 v0.1.1 (20250805)
// @namespace    https://greasyfork.org/users/legnax
// @version      0.1.1
// @description  Download DLsite product thumbnails in .webp or .jpg
// @description:ko DLsite에서 대표 이미지 및 썸네일을 원클릭 다운로드
// @author       legnax
// @license      MIT
// @match        https://www.dlsite.com/*/work/=/product_id/RJ*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=arca.live
// @grant        none
// @run-at       document-end
// ==/UserScript==

(function () {
    'use strict';

    function getRJCode() {
        const match = location.href.match(/RJ\d{6,}/);
        return match ? match[0] : null;
    }

    function getFolderID(rjCode) {
        const num = parseInt(rjCode.slice(2), 10);
        return String(Math.ceil(num / 1000) * 1000).padStart(8, '0');
    }

    function generateImageUrls(rjCode, max = 10, ext = 'webp') {
        const folderID = getFolderID(rjCode);
        const baseUrl = `https://img.dlsite.jp/modpub/images2/work/doujin/RJ${folderID}/`;
        const urls = [{
            url: `${baseUrl}${rjCode}_img_main.${ext}`,
            filename: `${rjCode}_main.${ext}`
        }];
        for (let i = 1; i <= max; i++) {
            urls.push({
                url: `${baseUrl}${rjCode}_img_smp${i}.${ext}`,
                filename: `${rjCode}_smp${i}.${ext}`
            });
        }
        return urls;
    }

    async function downloadWebp({ url, filename }) {
        try {
            const res = await fetch(url);
            if (!res.ok) return false;
            const blob = await res.blob();
            const blobUrl = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = blobUrl;
            a.download = filename;
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(blobUrl);
            return true;
        } catch {
            return false;
        }
    }

    async function downloadAsJpg({ url, filename }) {
        return new Promise((resolve) => {
            const img = new Image();
            img.crossOrigin = 'anonymous';
            img.onload = () => {
                const canvas = document.createElement('canvas');
                canvas.width = img.naturalWidth;
                canvas.height = img.naturalHeight;
                const ctx = canvas.getContext('2d');
                ctx.drawImage(img, 0, 0);
                canvas.toBlob((blob) => {
                    const blobUrl = URL.createObjectURL(blob);
                    const a = document.createElement('a');
                    a.href = blobUrl;
                    a.download = filename;
                    document.body.appendChild(a);
                    a.click();
                    document.body.removeChild(a);
                    URL.revokeObjectURL(blobUrl);
                    resolve(true);
                }, 'image/jpeg', 0.7); // 용량 화질 최적화(0.7 > 용량 약 2배 증가함)
            };
            img.onerror = () => resolve(false);
            img.src = url;
        });
    }

    function addSplitButtons() {
        const wrapper = document.createElement('div');
        wrapper.style.cssText = `
            position: fixed;
            top: 10px;
            left: 10px;
            z-index: 9999;
            display: flex;
            box-shadow: 0 0 0 1.5px black;
            border-radius: 6px;
            overflow: hidden;
        `;

        const baseStyle = `
            background: #ffc0cb;
            color: #000;
            border: none;
            width: 90px;
            height: 32px;
            text-align: center;
            font-size: 13px;
            font-weight: bold;
            cursor: pointer;
            line-height: 32px;
        `;

        const btnWebp = document.createElement('button');
        btnWebp.textContent = '원본(.webp)';
        btnWebp.style.cssText = baseStyle + 'border-right: 1.5px solid black;';
        btnWebp.onclick = async () => {
            const rj = getRJCode();
            if (!rj) return alert('RJ 코드 인식 실패');
            const files = generateImageUrls(rj, 10, 'webp');
            let count = 0;
            for (const f of files) {
                const ok = await downloadWebp(f);
                if (ok) count++;
                await new Promise(r => setTimeout(r, 400));
            }
            alert(`${count}개 .webp 이미지 다운로드 완료`);
        };

        const btnJpg = document.createElement('button');
        btnJpg.textContent = '변환(.jpg)';
        btnJpg.style.cssText = baseStyle;
        btnJpg.onclick = async () => {
            const rj = getRJCode();
            if (!rj) return alert('RJ 코드 인식 실패');
            const files = generateImageUrls(rj, 10, 'webp');
            let count = 0;
            for (const f of files) {
                const jpgFile = { ...f, filename: f.filename.replace('.webp', '.jpg') };
                const ok = await downloadAsJpg(jpgFile);
                if (ok) count++;
                await new Promise(r => setTimeout(r, 400));
            }
            alert(`${count}개 .jpg 이미지 다운로드 완료`);
        };

        wrapper.appendChild(btnWebp);
        wrapper.appendChild(btnJpg);
        document.body.appendChild(wrapper);
    }

    window.addEventListener('load', () => setTimeout(addSplitButtons, 1000));
})();