ASMR Online 一键下载

一键下载asmr.one上的整个作品(或者选择文件下载),包括全部的文件和目录结构

目前为 2025-02-17 提交的版本。查看 最新版本

// ==UserScript==
// @name               ASMR Online 一键下载
// @name:zh-CN         ASMR Online 一键下载
// @name:en            ASMR Online Work Downloader
// @namespace          ASMR-ONE
// @version            1.0.0
// @description        一键下载asmr.one上的整个作品(或者选择文件下载),包括全部的文件和目录结构
// @description:zh-CN  一键下载asmr.one上的整个作品(或者选择文件下载),包括全部的文件和目录结构
// @description:en     Download all(selected) folders and files for current work on asmr.one in one click, preserving folder structures
// @author             PY-DNG
// @license            MIT
// @match              https://www.asmr.one/*
// @match              https://www.asmr-100.com/*
// @match              https://www.asmr-200.com/*
// @match              https://www.asmr-300.com/*
// @match              https://asmr.one/*
// @match              https://asmr-100.com/*
// @match              https://asmr-200.com/*
// @match              https://asmr-300.com/*
// @connect            asmr.one
// @connect            asmr-100.com
// @connect            asmr-200.com
// @connect            asmr-300.com
// @require            https://update.gf.qytechs.cn/scripts/456034/1532680/Basic%20Functions%20%28For%20userscripts%29.js
// @require            https://update.gf.qytechs.cn/scripts/458132/1138364/ItemSelector.js
// @icon               https://www.asmr.one/statics/app-logo-128x128.png
// @grant              GM_download
// @grant              GM_registerMenuCommand
// @grant              GM_xmlhttpRequest
// ==/UserScript==

/* eslint-disable no-multi-spaces */
/* eslint-disable no-return-assign */

/* global LogLevel DoLog Err $ $All $CrE $AEL $$CrE addStyle detectDom destroyEvent copyProp copyProps parseArgs escJsStr replaceText getUrlArgv dl_browser dl_GM AsyncManager queueTask FunctionLoader loadFuncs require isLoaded */
/* global ItemSelector */

(function __MAIN__() {
    'use strict';

	const CONST = {
		HTML: {
			DownloadButton: `
				<button tabindex="0" type="button" id="download-btn"
						class="q-btn q-btn-item non-selectable no-outline q-btn--standard q-btn--rectangle bg-cyan q-mt-sm shadow-4 q-mx-xs q-px-sm text-white q-btn--actionable q-focusable q-hoverable q-btn--wrap q-btn--dense">
					<span class="q-focus-helper"></span><span class="q-btn__wrapper col row q-anchor--skip"><span
						class="q-btn__content text-center col items-center q-anchor--skip justify-center row"><span class="block" id="download-btn-inner">DOWNLOAD</span></span></span>
				</button>
			`
		},
		Text: {
			DownloadFolder: 'ASMR-ONE',
			WorkFolder: '{RJ} - {WorkName}',
			DownloadButton: 'Download',
			DownloadButton_Working: 'Downloading({Done}/{All})',
			DownloadButton_Done: 'Download(Finished)',
			SelectDownloadFiles: '选择下载的文件:',
			RootFolder: 'Root',
			Prefix_File: '[文件] ',
			Prefix_Folder: '[文件夹] ',
			NoTitle: 'No Title'
		},
		Number: {
			Max_Download: 2,
			GUITextChangeDelay: 1500
		}
	}

    loadFuncs([{
        id: 'utils',
        func() {
            const win = typeof unsafeWindow === 'object' && unsafeWindow !== null ? unsafeWindow : window;

            function htmlElm(html) {
                const parent = $CrE('div');
                parent.innerHTML = html;
                return parent.children.length > 1 ? Array.from(parent.children) : parent.children[0];
            }

            function getOSSep() {
                return ({
                    'Windows': '\\',
                    'Mac': '/',
                    'Linux': '/',
                    'Null': '-'
                })[getOS()];
            }

            function getOS() {
                const info = (navigator.platform || navigator.userAgent).toLowerCase();
                const test = (s) => (info.includes(s));
                const map = {
                    'Windows': ['window', 'win32', 'win64', 'win86'],
                    'Mac': ['mac', 'os x'],
                    'Linux': ['linux']
                }
                for (const [sys, strs] of Object.entries(map)) {
                    if (strs.some(test)) {
                        return sys;
                    }
                }
                return 'Null';
            }

            // Returns a random string
            function randstr(length=16, nums=true, cases=true) {
                const all = 'abcdefghijklmnopqrstuvwxyz' + (nums ? '0123456789' : '') + (cases ? 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' : '');
                return Array(length).fill(0).reduce(pre => (pre += all.charAt(randint(0, all.length-1))), '');
            }

            function randint(min, max) {
                return Math.floor(Math.random() * (max - min + 1)) + min;
            }

            function cloneObject(obj) {
                return window.structuredClone?.(obj) ?? JSON.parse(JSON.stringify(obj));
            }

            // Save text to textfile
            function downloadText(text, name) {
                if (!text || !name) {return false;};
                const blob = new Blob([text], { type:"text/plain;charset=utf-8" });
                const url = URL.createObjectURL(blob);
                dl_browser(url, name);
                setTimeout(() => URL.revokeObjectURL(url), 1000);
            }

            return {
                window: win,
                htmlElm, getOSSep, getOS, randstr, randint, cloneObject, downloadText
            }
        }
    }, {
        id: 'debug',
        dependencies: 'utils',
        func() {
            const utils = require('utils');

            GM_registerMenuCommand('导出调试包', debugInfo);

            function debugInfo() {
                const win = utils.window;
                const DebugInfo = {
                    version: GM_info.script.version,
                    GM_info: GM_info,
                    platform: navigator.platform,
                    userAgent: navigator.userAgent,
                    getOS: utils.getOS(),
                    getOSSep: utils.getOSSep(),
                    url: location.href,
                    topurl: win.top.location.href,
                    iframe: win.top !== win,
                    languages: [...navigator.languages],
                    timestamp: (new Date()).getTime()
                };

                // Log in console
                DoLog(LogLevel.Debug, '=== Userscript [' + GM_info.script.name + '] debug info ===');
                DoLog(LogLevel.Debug, DebugInfo);
                DoLog(LogLevel.Debug, '=== /Userscript [' + GM_info.script.name + '] debug info ===');

                // Save to file
                utils.downloadText(JSON.stringify(DebugInfo), 'Debug Info_' + GM_info.script.name + '_' + (new Date()).getTime().toString() + '.json');
            }

            return { debugInfo };
        }
    }, {
        id: 'item-selector',
        desc: 'Initialize an ItemSelector instance and return it as is',
        detectDom: 'body',
        func() {
            const IS = new ItemSelector();
            const observer = new MutationObserver(setTheme);
            observer.observe(document.body, {attributes: true, attributeFilter: ['class']});
            setTheme();
            return IS;

            function setTheme() {
                IS.setTheme([...document.body.classList].includes('body--dark') ? 'dark' : 'light');
            }
        }
    }, {
        id: 'api',
        func() {
            function tracks(id) {
                return callApi({
                    endpoint: `tracks/${id}`
                });
            }

            /**
             * callApi detail object
             * @typedef {Object} api_detail
             * @property {string} endpoint - api endpoint
             * @property {Object} [search] - search params
             * @property {string} [method='GET']
             */

            /**
             * Do basic asmr-online api request
             * This is the queued version of _callApi
             * @param {api_detail} detail
             * @returns
             */
            function callApi(...args) {
                return queueTask(() => _callApi(...args), 'callApi');
            }

            /**
             * Do basic asmr-online api request
             * @param {api_detail} detail
             * @returns
             */
            function _callApi(detail) {
                const host = `api.${location.host.match(/(?:[^.]+\.)?([^.]+\.[^.]+)/)[1]}`;
                const search_string = new URLSearchParams(detail.search).toString();
                const url = `https://${host}/api/${detail.endpoint.replace(/^\//, '')}` + (search_string ? '?' + search_string : '');
                const method = detail.method ?? 'GET';

                return new Promise((resolve, reject) => {
                    const options = {
                        method, url,
                        headers: {
                            accept: 'application/json, text/plain, */*'
                        },
                        onload(e) {
                            try {
                                e.status === 200 ? resolve(JSON.parse(e.responseText)) : reject(e.responseText);
                            } catch(err) {
                                reject(err);
                            }
                        },
                        onerror: err => reject(err)
                    }
                    GM_xmlhttpRequest(options);
                });
            }

            return {
                tracks,
                callApi
            };
        }
    }, {
        id: 'main',
        dependencies: ['utils', 'api', 'item-selector'],
        func() {
            const utils = require('utils');
            const api = require('api');
            const IS = require('item-selector');

            detectDom({
                selector: '#work-tree',
                callback: e => pageWork()
            });

            async function pageWork() {
                // Make button
                const downloadBtn = utils.htmlElm(CONST.HTML.DownloadButton);
                const downloadBtn_inner = $(downloadBtn, '#download-btn-inner');
                (await detectDom(".q-page-container .q-pa-sm")).append(downloadBtn);
                $AEL(downloadBtn, 'click', batchDownload);

                async function batchDownload() {
                    const count = {done: 0, all: 0};
                    const DATA = 'Original-Item-Properties-Data_' + utils.randstr();
                    const list = await api.tracks(getid());
                    const json = list2json(list);
                    IS.show(json, {
                        title: CONST.Text.SelectDownloadFiles,
                        onok: (e, json) => {
                            const list = json2list(json);
                            list.forEach(item => dealItem(item));
                        }
                    });

                    function list2json(list) {
                        list = structuredClone(list);
                        const json = {text: CONST.Text.RootFolder, children: [], [DATA]: {}};
                        json.children.push(...list.map(item => convert(item)));
                        return json;

                        function convert(item) {
                            const json = {};
                            switch (item.type) {
                                case 'folder': {
                                    json.text = CONST.Text.Prefix_Folder + item.title;
                                    json.children = item.children.map(child => convert(child));
                                    break;
                                }
                                case 'audio':
                                case 'text':
                                case 'image':
                                case 'other': {
                                    json.text = CONST.Text.Prefix_File + item.title;
                                    break;
                                }
                                default:
                                    //debugger;
                                    DoLog(LogLevel.Warning, 'Unknown item type: ' + item.type);
                            }
                            json[DATA] = item;
                            delete json[DATA].children;
                            return json;
                        }
                    }

                    function json2list(json) {
                        if (json === null) {return [];}
                        json = structuredClone(json);
                        const root_item = convert(json);
                        const list = root_item.children;
                        return list;

                        function convert(json) {
                            const item = json[DATA];
                            if (Array.isArray(json.children)) {
                                item.children = [];
                                for (const child of json.children) {
                                    item.children.push(convert(child));
                                }
                            }
                            return item;
                        }
                    }

                    function dealItem(item, path=[]) {
                        switch (item.type) {
                            case 'folder': {
                                for (const child of item.children) {
                                    dealItem(child, path.concat([item.title]));
                                }
                                break;
                            }
                            case 'audio':
                            case 'text':
                            case 'image':
                            case 'other': {
                                const sep = utils.getOSSep();
                                const _sep = ({'/': '/', '\\': '\'})[sep];
                                const url = item.mediaDownloadUrl;
                                const RJ = location.pathname.split('/').pop();
                                const name = [CONST.Text.DownloadFolder].concat([replaceText(CONST.Text.WorkFolder, {'{RJ}': RJ, '{WorkName}': item.workTitle || CONST.Text.NoTitle})]).concat(path).concat([item.title]).map((name) => (name.replaceAll(sep, _sep))).join(sep);
                                DoLog([name, url, item]);
                                dl(url, name);
                                count.all++;
                                display();
                                break;
                            }
                            default:
                                //debugger;
                                DoLog(LogLevel.Warning, 'Unknown item type: ' + item.type);
                        }
                    }

                    function dl(url, name, retry=3) {
                        GM_download({
                            method: 'GET',
                            url: url,
                            name: name,
                            onload: function(e) {
                                count.done++;
                                display();
                            },
                            onerror: function() {
                                debugger;
                                --retry > 0 && dl(url, name, retry);
                            },
                            ontimeout: err => {debugger;},
                            onabort: err => {debugger;}
                        });
                    }

                    function display() {
                        downloadBtn_inner.innerText = replaceText(CONST.Text.DownloadButton_Working, {'{Done}': count.done, '{All}': count.all});
                        count.done === count.all && setTimeout(() => (downloadBtn_inner.innerText = CONST.Text.DownloadButton_Done), CONST.Number.GUITextChangeDelay);
                    }
                }
            }

            function getid() {
                return location.pathname.split('/').pop().substring(2);
            }
        }
    }]);
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址