Greasy Fork

Greasy Fork is available in English.

ASMR Online 一键下载

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

当前为 2022-11-03 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

/* 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.3.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/*
// @icon               https://www.asmr.one/statics/app-logo-128x128.png
// @grant              GM_download
// @grant              GM_registerMenuCommand
// ==/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" id="download-btn-inner">DOWNLOAD</span></span></span>
				</button>
			`
		},
		Text: {
			DownloadFolder: 'ASMR-ONE',
			DownloadButton: 'Download',
			DownloadButton_Working: 'Downloading({Done}/{All})',
			DownloadButton_Done: 'Download(Finished)'
		},
		Number: {
			Max_Download: 2,
			Interval: 500,
			GUITextChangeDelay: 1500
		}
	}
	GM_registerMenuCommand('导出调试包', debugInfo);

	// Init
	DoLog();
	GMDLHook(CONST.Number.Max_Download);

	main();
	function main() {
		// Wait for document.body
		if (!document.body) {
			setTimeout(main, CONST.Number.Interval);
			return false;
		}

		// Commons

		// Page functions
		const ITM = new IntervalTaskManager();
		const pageChangeDetecter = (function(callback, emitOnInit=false) {
			let href = location.href;
			emitOnInit && callback(null, href);
			return function detecter() {
				const new_href = location.href;
				if (href !== new_href) {
					callback(href, new_href);
					href = new_href;
				}
			}
		}) (deliverPageFuncs, true);
		ITM.time = CONST.Number.Interval;
		ITM.addTask(pageChangeDetecter);
		ITM.start();

		function deliverPageFuncs(href, new_href) {
			DoLog('Delivering page funcs for ' + new_href);
			const pageFuncs = [{
				reg: /^https?:\/\/www\.asmr\.one\/work\/RJ\d+/,
				func: pageWork,
				checker: '#work-tree'
			}];
			for (const pageFunc of pageFuncs) {
				test_exec(pageFunc);
			}

			function test_exec(pageFunc) {
				pageFunc.reg.test(location.href) && ((pageFunc.checker ? ({
					'string': () => ($(pageFunc.checker)),
					'function': pageFunc.checker,
				})[typeof pageFunc.checker]() : true) ? true : (setTimeout(test_exec.bind(null, pageFunc), CONST.Number.Interval), DoLog('waiting: ' + location.href), false)) && pageFunc.func(href, new_href);
			}
		}
	}

	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() {
			let count = {done: 0, all: 0};
			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);
						dl(url, name);
						count.all++;
						display();
						break;
					}
					default:
						DoLog(LogLevel.Warning, 'Unknown item type');
				}
			}

			function dl(url, name, retry=3) {
				GM_download({
					url: url,
					name: name,
					onload: function(e) {
						count.done++;
						display();
					},
					onerror: function() {
						--retry > 0 && dl(url, name, retry);
					}
				});
			}

			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 xhr = new XMLHttpRequest();
		xhr.open('GET', 'https://api.asmr.one/api/tracks/' + id);
		xhr.onload = onload;
		xhr.send();
	}

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

	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,
			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 <a> and download
			const a = $CrE('a');
			a.href = url;
			a.download = name;
			a.click();
		}
	}

	// 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.length > 1 ? Array.from(parent.children) : 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() {
		const info = (navigator.platform || navigator.userAgent).toLowerCase();
		if (info.includes('window') > 0) {
			return 'Windows';
		} else if (info.includes('mac') > 0) {
			return 'Mac';
		} else if (info.includes('linux') > 0) {
			return 'Linux';
		} else {
			return 'Null';
		}
	}

	// Replace model text with no mismatching of replacing replaced text
	// e.g. replaceText('aaaabbbbccccdddd', {'a': 'b', 'b': 'c', 'c': 'd', 'd': 'e'}) === 'bbbbccccddddeeee'
	//      replaceText('abcdAABBAA', {'BB': 'AA', 'AAAAAA': 'This is a trap!'}) === 'abcdAAAAAA'
	//      replaceText('abcd{AAAA}BB}', {'{AAAA}': '{BB', '{BBBB}': 'This is a trap!'}) === 'abcd{BBBB}'
	//      replaceText('abcd', {}) === 'abcd'
	/* Note:
	    replaceText will replace in sort of replacer's iterating sort
	    e.g. currently replaceText('abcdAABBAA', {'BBAA': 'TEXT', 'AABB': 'TEXT'}) === 'abcdAATEXT'
	    but remember: (As MDN Web Doc said,) Although the keys of an ordinary Object are ordered now, this was
	    not always the case, and the order is complex. As a result, it's best not to rely on property order.
	    So, don't expect replaceText will treat replacer key-values in any specific sort. Use replaceText to
	    replace irrelevance replacer keys only.
	*/
	function replaceText(text, replacer) {
		if (Object.entries(replacer).length === 0) {return text;}
		const [models, targets] = Object.entries(replacer);
		const len = models.length;
		let text_arr = [{text: text, replacable: true}];
		for (const [model, target] of Object.entries(replacer)) {
			text_arr = replace(text_arr, model, target);
		}
		return text_arr.map((text_obj) => (text_obj.text)).join('');

		function replace(text_arr, model, target) {
			const result_arr = [];
			for (const text_obj of text_arr) {
				if (text_obj.replacable) {
					const splited = text_obj.text.split(model);
					for (const part of splited) {
						result_arr.push({text: part, replacable: true});
						result_arr.push({text: target, replacable: false});
					}
					result_arr.pop();
				} else {
					result_arr.push(text_obj);
				}
			}
			return result_arr;
		}
	}

	function IntervalTaskManager() {
		const tasks = this.tasks = [];
		this.time = 500;
		this.interval = -1;
		defineProperty(this, 'working', {
			get: () => (this.interval >= 0)
		});

		this.addTask = function(fn) {
			tasks.push(fn);
		}

		this.removeTask = function(fn_idx) {
			const idx = typeof fn_idx === 'number' ? fn_idx : tasks.indexOf(fn_idx)
			tasks.splice(idx, 1)
		}

		this.clearTasks = function() {
			tasks.splice(0, Infinity)
		}

		this.start = function() {
			if (!this.working) {
				this.interval = setInterval(this.do, this.time);
				return true;
			} else {
				return false;
			}
		}

		this.stop = function() {
			if (this.working) {
				clearInterval(this.interval);
				this.interval = -1;
				return true;
			} else {
				return false;
			}
		}

		this.do = function() {
			for (const task of tasks) {
				task();
			}
		}
	}

	function defineProperty(obj, prop, desc) {
		desc.configurable = false;
		desc.enumerable = true;
		Object.defineProperty(obj, prop, desc);
	}

	// Arguments: level=LogLevel.Info, logContent, asObject=false
    // Needs one call "DoLog();" to get it initialized before using it!
    function DoLog() {
		const win = typeof unsafeWindow === 'object' ? unsafeWindow : window;

        // Global log levels set
        win.LogLevel = {
			Debug: -1,
            None: 0,
            Error: 1,
            Success: 2,
            Warning: 3,
            Info: 4,
        }
        win.LogLevelMap = {};
		win.LogLevelMap[LogLevel.None]     = {prefix: ''          , color: 'color:#ffffff'};
        win.LogLevelMap[LogLevel.Debug]    = {prefix: '[Debug]'   , color: 'color:#6666cc'};
        win.LogLevelMap[LogLevel.Error]    = {prefix: '[Error]'   , color: 'color:#ff0000'};
        win.LogLevelMap[LogLevel.Success]  = {prefix: '[Success]' , color: 'color:#00aa00'};
        win.LogLevelMap[LogLevel.Warning]  = {prefix: '[Warning]' , color: 'color:#ffa500'};
        win.LogLevelMap[LogLevel.Info]     = {prefix: '[Info]'    , color: 'color:#888888'};
        win.LogLevelMap[LogLevel.Elements] = {prefix: '[Elements]', color: 'color:#000000'};

        // Current log level
        DoLog.logLevel = (win.isPY_DNG && win.userscriptDebugging) ? 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);
        }
    }
})();