/* eslint-disable no-multi-spaces */ // ==UserScript== // @name ASMR Online 一键下载 // @name:zh-CN ASMR Online 一键下载 // @name:en ASMR Online Work Downloader // @namespace ASMR-ONE // @version 0.9 // @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://asmr.one/* // @match https://asmr-100.com/* // @require https://greasyfork.org/scripts/458132-itemselector/code/ItemSelector.js?version=1138364 // @require https://greasyfork.org/scripts/456034-basic-functions-for-userscripts/code/script.js?version=1222141 // @require https://greasyfork.org/scripts/460385-gm-web-hooks/code/script.js?version=1221394 // @icon https://www.asmr.one/statics/app-logo-128x128.png // @grant GM_download // @grant GM_registerMenuCommand // @downloadURL none // ==/UserScript== /* global LogLevel DoLog Err $ $All $CrE $AEL $$CrE addStyle detectDom destroyEvent copyProp copyProps parseArgs escJsStr replaceText getUrlArgv dl_browser dl_GM AsyncManager */ /* global ItemSelector GMXHRHook GMDLHook */ (function __MAIN__() { 'use strict'; const CONST = { HTML: { DownloadButton: ` ` }, 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 } } GM_registerMenuCommand('导出调试包', debugInfo); // Init const IS = initItemSelector(); detectDom('body', body => main()); function main() { // Commons polyfill(); GMDLHook(CONST.Number.Max_Download); // Page functions detectDom({ selector: '#work-tree', callback: e => pageWork(), once: false }); } function pageWork() { // Make button const downloadBtn = htmlElm(CONST.HTML.DownloadButton); const downloadBtn_inner = $(downloadBtn, '#download-btn-inner'); $(".q-pa-sm").appendChild(downloadBtn); downloadBtn.addEventListener('click', batchDownload); function batchDownload() { const count = {done: 0, all: 0}; const DATA = 'Original-Item-Properties-Data_' + randstr(); request(getid(), function(e) { const list = JSON.parse(e.target.responseText); const json = list2json(list); IS.show(json, { title: CONST.Text.SelectDownloadFiles, onok: (e, json) => { const list = json2list(json); for (const item of list) { dealItem(item); } } }); }); function list2json(list) { list = structuredClone(list); const json = {text: CONST.Text.RootFolder, children: [], [DATA]: {}}; for (const item of list) { json.children.push(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 = 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 request(id, onload) { const url = `https://api.${location.host.match(/(?:[^.]+\.)?([^.]+\.[^.]+)/)[1]}/api/tracks/` + id; const xhr = new XMLHttpRequest(); xhr.open('GET', url); xhr.onload = onload; xhr.send(); } function getid() { return location.pathname.split('/').pop().substring(2); } function initItemSelector() { 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'); } } function debugInfo() { const win = typeof unsafeWindow === 'object' ? unsafeWindow : window; const DebugInfo = { version: GM_info.script.version, GM_info: GM_info, platform: navigator.platform, userAgent: navigator.userAgent, getOS: getOS(), getOSSep: 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 downloadText(JSON.stringify(DebugInfo), 'Debug Info_' + GM_info.script.name + '_' + (new Date()).getTime().toString() + '.json'); // Save text to textfile function downloadText(text, name) { if (!text || !name) {return false;}; // Get blob url const blob = new Blob([text],{type:"text/plain;charset=utf-8"}); const url = URL.createObjectURL(blob); // Create and download const a = $CrE('a'); a.href = url; a.download = name; a.click(); } } 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; } // This isn't a full polyfill, but that's not the point. What matters actually: it works for this userscript. function polyfill() { const win = typeof unsafeWindow === 'object' && unsafeWindow !== null ? unsafeWindow : window; if (!win.structuredClone) { win.structuredClone = o => JSON.parse(JSON.stringify(o)); } } })();