/* 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
// ==/UserScript==
(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">DOWNLOAD</span></span></span>
</button>
`
},
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);
}
}
})();