/* 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.1 // @description 一键下载asmr.one上的整个作品,包括全部的文件和目录结构 // @description:zh-CN 一键下载asmr.one上的整个作品,包括全部的文件和目录结构 // @description:en Download all folders and files for current work on asmr.one in one click // @author PY-DNG // @license MIT // @match https://www.asmr.one/work/** // @icon https://www.asmr.one/statics/app-logo-128x128.png // @grant GM_download // @downloadURL none // ==/UserScript== (function __MAIN__() { 'use strict'; const CONST = { HTML: { DownloadButton: ` ` }, Text: { DownloadFolder: 'ASMR-ONE' }, Number: { Max_Download: 2 } } // Init DoLog(); GMDLHook(CONST.Number.Max_Download); // Make button const downloadBtn = htmlElm(CONST.HTML.DownloadButton); $(".q-pa-sm").appendChild(downloadBtn); downloadBtn.addEventListener('click', batchDownload); function request(id, onload) { const xhr = new XMLHttpRequest(); xhr.open('GET', 'https://api.asmr.one/api/tracks/' + id); xhr.onload = onload; xhr.send(); } function batchDownload() { request(getid(), function(e) { const list = JSON.parse(e.target.responseText) for (const item of list) { dealItem(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': { const sep = getOSSep(); const _sep = ({'/': '/', '\\': '\'})[sep]; const url = item.mediaDownloadUrl; const name = [CONST.Text.DownloadFolder].concat([item.workTitle]).concat(path).concat([item.title]).map((name) => (name.replaceAll(sep, _sep))).join(sep); GM_download(url, name); break; } default: DoLog(LogLevel.Warning, 'Unknown item type'); } } } function getid() { return location.pathname.split('/').pop().substring(2); } // Basic functions // querySelector function $() { switch(arguments.length) { case 2: return arguments[0].querySelector(arguments[1]); break; default: return document.querySelector(arguments[0]); } } // querySelectorAll function $All() { switch(arguments.length) { case 2: return arguments[0].querySelectorAll(arguments[1]); break; default: return document.querySelectorAll(arguments[0]); } } // createElement function $CrE() { switch(arguments.length) { case 2: return arguments[0].createElement(arguments[1]); break; default: return document.createElement(arguments[0]); } } // Get a url argument from lacation.href // also recieve a function to deal the matched string // returns defaultValue if name not found // Args: {url=location.href, name, dealFunc=((a)=>{return a;}), defaultValue=null} or 'name' function getUrlArgv(details) { typeof(details) === 'string' && (details = {name: details}); typeof(details) === 'undefined' && (details = {}); if (!details.name) {return null;}; const url = details.url ? details.url : location.href; const name = details.name ? details.name : ''; const dealFunc = details.dealFunc ? details.dealFunc : ((a)=>{return a;}); const defaultValue = details.defaultValue ? details.defaultValue : null; const matcher = new RegExp('[\\?&]' + name + '=([^&#]+)'); const result = url.match(matcher); const argv = result ? dealFunc(result[1]) : defaultValue; return argv; } function htmlElm(html) { const parent = $CrE('div'); parent.innerHTML = html; return parent.children[0]; } // GM_DL HOOK: The number of running GM_DLs in a time must under maxXHR // Returns the abort function to stop the request anyway(no matter it's still waiting, or requesting) // (If the request is invalid, such as url === '', will return false and will NOT make this request) // If the abort function called on a request that is not running(still waiting or finished), there will be NO onabort event // Requires: function delItem(){...} & function uniqueIDMaker(){...} function GMDLHook(maxXHR=5) { const GM_DL = GM_download; const getID = uniqueIDMaker(); let todoList = [], ongoingList = []; GM_download = safeGMdl; function safeGMdl() { // Get an id for this request, arrange a request object for it. const id = getID(); const request = {id: id, args: Array.from(arguments), aborter: null}; // Transform (url, name) into {url: url, name: name} convertArgs(request); // Deal onload function first dealEndingEvents(request); /* DO NOT DO THIS! KEEP ITS ORIGINAL PROPERTIES! // Stop invalid requests if (!validCheck(request)) { return false; } */ // Judge if we could start the request now or later? todoList.push(request); checkDL(); return makeAbortFunc(id); // Transform (url, name) into {url: url, name: name} function convertArgs(request) { if (request.args.length === 2) { request.args = [{ url: request.args[0], name: request.args[1] }]; } } // Decrease activeXHRCount while GM_DL onload; function dealEndingEvents(request) { const e = request.args[0]; // onload event const oriOnload = e.onload; e.onload = function() { reqFinish(request.id); checkDL(); oriOnload ? oriOnload.apply(null, arguments) : function() {}; } // onerror event const oriOnerror = e.onerror; e.onerror = function() { reqFinish(request.id); checkDL(); oriOnerror ? oriOnerror.apply(null, arguments) : function() {}; } // ontimeout event const oriOntimeout = e.ontimeout; e.ontimeout = function() { reqFinish(request.id); checkDL(); oriOntimeout ? oriOntimeout.apply(null, arguments) : function() {}; } // onabort event const oriOnabort = e.onabort; e.onabort = function() { reqFinish(request.id); checkDL(); oriOnabort ? oriOnabort.apply(null, arguments) : function() {}; } } // Check if the request is invalid function validCheck(request) { const e = request.args[0]; if (!e.url) { return false; } return true; } // Call a XHR from todoList and push the request object to ongoingList if called function checkDL() { if (ongoingList.length >= maxXHR) {return false;}; if (todoList.length === 0) {return false;}; const req = todoList.shift(); const reqArgs = req.args; const aborter = GM_DL.apply(null, reqArgs); req.aborter = aborter; ongoingList.push(req); return req; } // Make a function that aborts a certain request function makeAbortFunc(id) { return function() { let i; // Check if the request haven't been called for (i = 0; i < todoList.length; i++) { const req = todoList[i]; if (req.id === id) { // found this request: haven't been called delItem(todoList, i); return true; } } // Check if the request is running now for (i = 0; i < ongoingList.length; i++) { const req = todoList[i]; if (req.id === id) { // found this request: running now req.aborter(); reqFinish(id); checkDL(); } } // Oh no, this request is already finished... return false; } } // Remove a certain request from ongoingList function reqFinish(id) { let i; for (i = 0; i < ongoingList.length; i++) { const req = ongoingList[i]; if (req.id === id) { ongoingList = delItem(ongoingList, i); return true; } } return false; } } } // Makes a function that returns a unique ID number each time function uniqueIDMaker() { let id = 0; return makeID; function makeID() { id++; return id; } } // Del a item from an array using its index. Returns the array but can NOT modify the original array directly!! function delItem(arr, delIndex) { arr = arr.slice(0, delIndex).concat(arr.slice(delIndex+1)); return arr; } function getOSSep() { return ({ 'Windows': '\\', 'Mac': '/', 'Linux': '/', 'Null': '-' })[getOS()]; } function getOS() { if (navigator.userAgent.indexOf('Window') > 0) { return 'Windows'; } else if (navigator.userAgent.indexOf('Mac OS X') > 0) { return 'Mac'; } else if (navigator.userAgent.indexOf('Linux') > 0) { return 'Linux'; } else { return 'Null'; } } // Arguments: level=LogLevel.Info, logContent, asObject=false // Needs one call "DoLog();" to get it initialized before using it! function DoLog() { // Global log levels set unsafeWindow.LogLevel = { None: 0, Error: 1, Success: 2, Warning: 3, Info: 4, } unsafeWindow.LogLevelMap = {}; unsafeWindow.LogLevelMap[LogLevel.None] = {prefix: '' , color: 'color:#ffffff'} unsafeWindow.LogLevelMap[LogLevel.Error] = {prefix: '[Error]' , color: 'color:#ff0000'} unsafeWindow.LogLevelMap[LogLevel.Success] = {prefix: '[Success]' , color: 'color:#00aa00'} unsafeWindow.LogLevelMap[LogLevel.Warning] = {prefix: '[Warning]' , color: 'color:#ffa500'} unsafeWindow.LogLevelMap[LogLevel.Info] = {prefix: '[Info]' , color: 'color:#888888'} unsafeWindow.LogLevelMap[LogLevel.Elements] = {prefix: '[Elements]', color: 'color:#000000'} // Current log level DoLog.logLevel = (unsafeWindow ? unsafeWindow.isPY_DNG : window.isPY_DNG) ? LogLevel.Info : LogLevel.Warning; // Info Warning Success Error // Log counter DoLog.logCount === undefined && (DoLog.logCount = 0); if (++DoLog.logCount > 512) { console.clear(); DoLog.logCount = 0; } // Get args let level, logContent, asObject; switch (arguments.length) { case 1: level = LogLevel.Info; logContent = arguments[0]; asObject = false; break; case 2: level = arguments[0]; logContent = arguments[1]; asObject = false; break; case 3: level = arguments[0]; logContent = arguments[1]; asObject = arguments[2]; break; default: level = LogLevel.Info; logContent = 'DoLog initialized.'; asObject = false; break; } // Log when log level permits if (level <= DoLog.logLevel) { let msg = '%c' + LogLevelMap[level].prefix; let subst = LogLevelMap[level].color; if (asObject) { msg += ' %o'; } else { switch(typeof(logContent)) { case 'string': msg += ' %s'; break; case 'number': msg += ' %d'; break; case 'object': msg += ' %o'; break; } } console.log(msg, subst, logContent); } } })();