Greasy Fork

来自缓存

Greasy Fork is available in English.

ASMR Online 一键下载

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

当前为 2023-07-17 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 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.8
// @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            http://greasyfork.icu/scripts/458132-itemselector/code/ItemSelector.js?version=1138364
// @require            http://greasyfork.icu/scripts/456034-basic-functions-for-userscripts/code/script.js?version=1221321
// @require            http://greasyfork.icu/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
// ==/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';

	// function DoLog() {}
	// Arguments: level=LogLevel.Info, logContent, trace=false
	const [LogLevel, DoLog] = (function() {
		const LogLevel = {
			None: 0,
			Error: 1,
			Success: 2,
			Warning: 3,
			Info: 4,
		};

		return [LogLevel, DoLog];
		function DoLog() {
			// Get window
			const win = (typeof(unsafeWindow) === 'object' && unsafeWindow !== null) ? unsafeWindow : window;

			const LogLevelMap = {};
			LogLevelMap[LogLevel.None] = {
				prefix: '',
				color: 'color:#ffffff'
			}
			LogLevelMap[LogLevel.Error] = {
				prefix: '[Error]',
				color: 'color:#ff0000'
			}
			LogLevelMap[LogLevel.Success] = {
				prefix: '[Success]',
				color: 'color:#00aa00'
			}
			LogLevelMap[LogLevel.Warning] = {
				prefix: '[Warning]',
				color: 'color:#ffa500'
			}
			LogLevelMap[LogLevel.Info] = {
				prefix: '[Info]',
				color: 'color:#888888'
			}
			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);

			// Get args
			let [level, logContent, trace] = parseArgs([...arguments], [
				[2],
				[1,2],
				[1,2,3]
			], [LogLevel.Info, 'DoLog initialized.', false]);

			// Log when log level permits
			if (level <= DoLog.logLevel) {
				let msg = '%c' + LogLevelMap[level].prefix + (typeof GM_info === 'object' ? `[${GM_info.script.name}]` : '') + (LogLevelMap[level].prefix ? ' ' : '');
				let subst = LogLevelMap[level].color;

				switch (typeof(logContent)) {
					case 'string':
						msg += '%s';
						break;
					case 'number':
						msg += '%d';
						break;
					default:
						msg += '%o';
						break;
				}

				if (++DoLog.logCount > 512) {
					console.clear();
					DoLog.logCount = 0;
				}
				console[trace ? 'trace' : 'log'](msg, subst, logContent);
			}
		}
	}) ();

	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,
			Interval: 500,
			GUITextChangeDelay: 1500
		}
	}
	GM_registerMenuCommand('导出调试包', debugInfo);

	// Init
	const IS = initItemSelector();

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

		// Commons
		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 <a> 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;
	}
})();