Filxt
Filxt
// @name 仓库用度盘投稿助手
// @name:en Baidu™ WebDisk Helper (dupan-helper)
// @namespace moe.jixun.dupan.galacg
// @version 1.3.20
// @description 简易功能增强, 方便仓库投稿用
// @description:en Enhancements for Baidu™ WebDisk.
// @author Jixun<https://jixun.moe/>
// @match https://pan.baidu.com/disk/home*
// @match https://yun.baidu.com/disk/home*
// @license MIT
// @homepageURL https://jixun.moe/post/dupan-helper
// @supportURL https://github.com/JixunMoe/dupan-helper/issues
// @contributionURL https://jixun.moe/donate
// @grant none
// @run-at document-start
// ==/UserScript==
function entryPoint () {
'use strict';
if (style.styleSheet) {
style.styleSheet.cssText = css;
} else {
style.appendChild(document.createTextNode(css));
}
}
let oRequire;
function fakeRequire(module) {
// console.info('%s Load module: %s', INFO, module);
const result = oRequire.apply(this, arguments);
const moduleHook = hooks.get(module);
if (moduleHook) {
try {
moduleHook();
} catch (e) {
console.error('%s: 执行 %s hook 时发生错误: %s', TAG, e.message);
console.trace(e);
}
hooks.delete(module);
}
return result;
}
function load(module) {
return oRequire.call(window, module);
}
function loadAsync(module) {
return new Promise(((resolve) => {
fakeRequire.async(module, resolve);
}));
}
if (window.require) {
console.warn('%s 覆盖方式安装,若无效请强制刷新。', TAG);
oRequire = window.require;
window.require = fakeRequire;
Object.assign(fakeRequire, oRequire);
} else {
console.info('%s 钩子方式安装,若失效请报告。', TAG);
Object.defineProperty(window, 'require', {
set(require) {
oRequire = require;
},
get() {
return fakeRequire;
},
});
}
function lazyCache(fn) {
let cacheWrapper = function () {
const result = fn.apply(this, arguments);
cacheWrapper = cache(result);
return result;
};
return function () {
return cacheWrapper.apply(this, arguments);
};
}
function getCheckedItems() {
return getFileList().getCheckedItems();
}
function anythingChecked() {
return getCheckedItems().length > 0;
}
function getCurrentDirectory() {
return getFileList().currentKey;
}
let id = 0;
function nextId() {
// eslint-disable-next-line no-plusplus
return id++;
}
function firstFunction(...fns) {
return fns.find((fn) => typeof fn === 'function');
}
function $$1() {
return getJQuery().apply(window, arguments);
}
function proxyJQuery(key) {
Object.defineProperty($$1, key, {
get: () => getJQuery()[key],
});
}
proxyJQuery('fn');
proxyJQuery('ajax');
proxyJQuery('isPlainObject');
const bigButton = {
type: 'big',
padding: ['50px', '50px'],
};
function confirmDialog(data) {
let dialog;
const dialogData = {
id: `confirm-${nextId()}`,
show: true,
title: data.title,
body: $$1('<div class="jx-dialog-body">').append(data.body),
buttons: [{
...bigButton,
name: 'confirm',
title: data.sureText || '确定',
color: 'blue',
click: firstFunction(data.onSure, hideDialog),
}],
};
if (data.cancel !== false) {
dialogData.buttons.push({
...bigButton,
name: 'cancel',
title: data.cancelText || '取消',
click: firstFunction(data.onCancel, hideDialog),
});
}
const Dialog = getDialog();
dialog = new Dialog(dialogData);
return dialog;
}
function infoDialog(data) {
return confirmDialog({
...data,
cancel: false,
});
}
function showTip() {
return getTip().show.apply(this, arguments);
}
function hideTip() {
return getTip().hide.apply(this, arguments);
}
function getErrorMessage(code) {
const msg = String(getContext().errorMsg(code));
return msg.replace(/\s+rapidupload 错误码$/, '');
}
function injectErrorMessage(obj) {
if ($$1.isPlainObject(obj)) {
obj.error = obj.show_msg || getErrorMessage(obj.errno || 0);
}
return obj;
}
const escapeDict = {
'"': 'quot',
"'": 'apos',
};
function escapeHtml(text) {
div.textContent = text;
const result = div.innerHTML.replace(/["']/g, (x) => `&${escapeDict[x]};`);
div.textContent = '';
return result;
}
class LocalStore {
constructor(id) {
this.id = id;
}
get value() {
return localStorage.getItem(this.id);
}
set value(value) {
return localStorage.setItem(this.id, value);
}
class OpDialog {
confirmText = '确定';
createStore(key) {
return LocalStore.create(this, key);
}
this.bindContext();
this.createDialog();
this.bootstrap();
}
bindContext() {
this.show = this.show.bind(this);
this.hide = this.hide.bind(this);
this.onConfirm = this.onConfirm.bind(this);
this.onCancel = this.onCancel.bind(this);
}
createDialog() {
this.dialog = confirmDialog({
title: this.title,
body: this.root,
sureText: this.confirmText,
onSure: this.onConfirm,
onCancel: this.onCancel,
});
}
/**
* 选择对话框内的内容。
* @param selector
* @returns {JQuery<HTMLElement>}
*/
$(selector) {
return $$1(selector, this.root);
}
/**
* Bind events.
*/
bootstrap() {
return this;
}
show() {
this.dialog.show();
}
hide() {
this.dialog.hide();
}
async onConfirm() {
this.hide();
}
onCancel() {
this.hide();
}
}
/* 依赖函数表 */
function isCodeValid(code) {
// 百度现在改了规则;
// 只允许:由数字字母组成的提取码,并且不能全部都是同一个字符。
return /^[\da-z]{4}$/i.test(code) && (new Set(code)).size > 1;
}
function fixCode(code) {
return code.replace(/"/g, '"').replace(/]/g, ']');
}
function fixWidthDigits(d) {
return (`0${d.toString()}`).slice(-2);
}
function makeDate(d) {
return `${d.getFullYear()}.${fixWidthDigits(d.getMonth() + 1)}.$
{fixWidthDigits(d.getDate())}`;
}
function genKey(size = 4) {
// length => 26 + 10, 36
const keySet = 'abcdefghijklmnopqrstuvwxyz0123456789';
let r = '';
for (let i = size; i--;) {
// eslint-disable-next-line no-bitwise
r += keySet[0 | (Math.random() * 36)];
}
return r;
}
function getFileId(item) {
return item.fs_id;
}
/**
* @param {Object} config
*/
constructor(config = {}) {
super(template, {
title: '自定义分享',
...config,
});
}
bindContext() {
super.bindContext();
this.validateCode = this.validateCode.bind(this);
this.hideError = this.hideError.bind(this);
}
bootstrap() {
this.codeStore = LocalStore.create(this, 'code');
this.$error = this.$('.jx_errmsg');
this.$footer = this.dialog.find(getDialog().QUERY.dialogFooter);
this.$key = this.$('#jx_shareKey').val(this.codeStore.value || genKey());
async onConfirm() {
this.hide();
showTip({
mode: 'loading',
msg: '正在分享,请稍后 ...',
autoClose: false,
});
hideTip();
if (resp.errno || !resp.shorturl) {
showTip({
mode: 'failure',
msg: `分享失败:${resp.error}`,
});
return;
}
showTip({
mode: 'success',
msg: '分享成功!',
});
this.$footer.children('.g-button-blue-large').hide();
this.$footer.children('.g-button-large').find('.text').text('关闭');
this.root.toggleClass('jx_hide');
this.$('#jx_dlboxCode').val(code);
this.show();
}
/**
* @returns string
*/
get value() {
return this.$key.val();
}
set value(value) {
return this.$key.val(value);
}
get isValueValid() {
return isCodeValid(this.value);
}
hideError() {
this.$error.addClass('jx_hide');
}
validateCode() {
this.$error.toggleClass('jx_hide', this.isValueValid);
}
}
function trigger(event) {
getMessage().trigger(event);
}
/**
* 刷新当前文件列表
*/
function refreshFileListView() {
trigger('system-refresh');
}
const fixRules = {
n(name) {
const match = name.match(/^(.+)\./);
return match ? match[1] : match;
},
c() {
return String.fromCharCode(97 + Math.random() * 26);
},
d() {
return Math.random().toString().slice(3, 4);
},
t() {
return Date.now();
},
e(name) {
const ext = name.match(/\.([^.]+)$/);
return ext ? ext[1] : '';
},
E(name) {
return name.match(/\.[^.]+$/) || '';
},
};
/* 依赖函数表 */
function fixName(name, code) {
const fn = fixRules[code];
if (fn) {
return fn(name);
}
return null;
}
/**
* @param {Object} config
*/
constructor(config = {}) {
super(template$1, {
title: '批量重命名',
...config,
});
}
bindContext() {
super.bindContext();
this.namePatternStore = this.createStore('pattern');
}
bootstrap() {
this.$namePattern = this.$('#jx_nameRule');
this.$namePattern.val(this.namePatternStore.value || '[GalACG]
:d:d:d:d:d:d:d:d:d:d:E');
}
async onConfirm() {
this.hide();
showTip({
mode: 'loading',
msg: '正在批量重命名,请稍后 ...',
autoClose: false,
});
hideTip();
refreshFileListView();
if (resp.errno) {
showTip({
mode: 'failure',
msg: `批量重命名失败, 请稍后重试! (${resp.error})`,
});
} else {
showTip({
mode: 'success',
msg: '重命名成功!',
});
}
}
}
if (!noPush) list.push(item);
return false;
}
function injectMenu() {
const faceData = load('system-core:data/faceData.js');
fileCtxMenu.forEach((m) => {
if (m.index >= 2) {
m.index += 1;
}
});
fileCtxMenu.push({
index: 2, // '删除' 的 index。
keyboard: 'r',
position: 'bottom',
title: '批量重命名',
display: anythingChecked,
action: BatchRenameDialog.create,
});
}
function debounce(fn) {
let timer;
return () => {
cancelAnimationFrame(timer);
timer = requestAnimationFrame(fn);
};
}
/**
* 将数值转换为 2 位数的十六进制文本。
* @param {Number} value
* @returns {string}
*/
function toStdHex(value) {
const hex = Math.floor(value).toString(16);
return (`0${hex}`).slice(-2);
}
/**
* 一个简单的类似于 NodeJS Buffer 的实现.
* 用于解析游侠度娘提取码。
*/
class SimpleBuffer {
/**
* @param {String} str
*/
constructor(str) {
this.fromString(str);
}
fromString(str) {
const len = str.length;
readUnicode(index, size) {
const bufText = slice(this.buf, index, index + size).map(toStdHex);
return JSON.parse(`"${buf.join('\\u')}"`);
}
readNumber(index, size) {
let ret = 0;
for (let i = index + size; i > index;) ret = this.buf[--i] + (ret * 256);
return ret;
}
readUInt(index) {
return this.readNumber(index, 4);
}
readULong(index) {
return this.readNumber(index, 8);
}
readHex(index, size) {
return Array.prototype.slice.call(this.buf, index, index +
size).map(toStdHex).join('');
}
}
/**
* UTF-8 字符转换成 base64 后在 JS 里解析会出毛病。
* @param str
* @returns {string}
*/
function decodeBase64(str) {
try {
str = atob(str);
} catch (e) {
console.error('%s: base64 decode failed: %s', TAG, str);
console.trace(e);
return '';
}
return decodeURIComponent(str.replace(/[^\x00-\x7F]/g, (z) => `%$
{toStdHex(z.charCodeAt(0))}`));
}
/**
* 百度网盘用的(非官方)标准提取码。
* 支持解析:
* 1. 游侠的 `BDLINK` 提取码
* 2. 我的“标准提取码”
* 3. PanDownload 的 `bdpan://` 协议。
*/
class DuParser {
constructor() {
this.reset();
}
reset() {
this.results = [];
this.versions = new Set();
}
/**
* 判断地址类型并解析。
* @param url
*/
parse(url) {
// 游侠的格式是多行,不好判断结束位置。
// 所以一次只能解析一条数据。
if (url.indexOf('BDLINK') === 0) {
this.parseAli(url);
return;
}
// 其他两个格式一行一个文件信息。
const links = url.split('\n').map(trim);
for (const link of links) {
if (link.startsWith('bdpan://')) {
this.parsePanDownload(link);
} else {
this.parseStandard(link);
}
}
}
hasResults() {
return this.results.length;
}
parseAli(url) {
const raw = atob(url.slice(6).replace(/\s/g, ''));
if (raw.slice(0, 5) !== 'BDFS\x00') return null;
let ptr = 9;
this.versions.add('游侠 v1');
for (let i = 0; i < fileCount; i++) {
// 大小 (8 bytes)
// MD5 + MD5S (0x20)
// nameSize (4 bytes)
// Name (unicode)
const fileInfo = Object.create(null);
fileInfo.size = buf.readULong(ptr);
fileInfo.md5 = buf.readHex(ptr + 8, 0x10);
fileInfo.md5s = buf.readHex(ptr + 0x18, 0x10);
const sizeofName = buf.readUInt(ptr + 0x28) * 2;
ptr += 0x2C;
parseStandard(szUrl) {
const match = szUrl.trim().match(/^([\dA-F]{32})#([\dA-F]{32})#([\d]
{1,20})#([\s\S]+)$/i);
if (match) {
const [, md5, md5s, size, name] = match;
this.versions.add('梦姬标准');
this.results.push({
md5, md5s, size, name,
});
}
return null;
}
parsePanDownload(szUrl) {
const match = decodeBase64(szUrl.slice(8)).match(/^([\s\S]+)\|([\d]{1,20})\|
([\dA-F]{32})\|([\dA-F]{32})$/i);
if (match) {
const [, name, size, md5, md5s] = match;
this.versions.add('PanDownload');
this.results.push({
md5, md5s, size, name,
});
}
return null;
}
}
/**
* 将文本形式的文件大小转换为
* @param {string} size
* @returns {string}
*/
function parseSize(size) {
let unit = 'MiB';
let sizeInUnit = parseInt(size, 10) / 1024 / 1024;
// 超过 GB
if (sizeInUnit > 1024) {
unit = 'GiB';
sizeInUnit /= 1024;
}
function itemInfo(item) {
const name = escapeHtml(item.name);
return `
<span class="name" title="${name}">${name}</span>
<span class="size">(${escapeHtml(parseSize(item.size))})</span>
`;
}
function wrapTag(tag) {
return (html) => `<${tag}>${html}</${tag}>`;
}
const lower = Function.prototype.call.bind(String.prototype.toLowerCase);
const upper = Function.prototype.call.bind(String.prototype.toUpperCase);
return ajax({
url: '/api/rapidupload?rtype=1',
type: 'POST',
// https://github.com/iikira/BaiduPCS-
Go/blob/9837f8e24328e5f881d6a07cf1249508c485a063/baidupcs/prepare.go#L272-L279
data: {
// overwrite: 表示覆盖同名文件; newcopy: 表示生成文件副本并进行重命名,命名规则为“文件
名_日期.后缀”
ondup,
// 先尝试小写,如果失败则尝试大写。如果都失败则不重试。
const resp = await rapidUploadOnce(dir, name, lower(md5), lower(md5s), size,
ondup);
if (resp.errno === 0) {
return resp;
}
return rapidUploadOnce(dir, name, upper(md5), upper(md5s), size, ondup);
}
function statusHtml(result) {
const className = result.success ? 'success' : 'fail';
return `<span class="jx-status jx-status-${className}">${result.error}</span>`;
}
confirmText = '导入';
confirmCallback = defaultConfirmCallback;
constructor(config = {}) {
super(template$2, {
title: '从秒传链接导入',
...config,
});
if (config) {
this.setText(config.content);
this.setDirectory(config.directory);
this.setConfirmCallback(config.confirmCallback);
this.forceRefresh = config.forceRefresh || false;
}
}
bindContext() {
super.bindContext();
this.hideError = this.hideError.bind(this);
this.updatePreview = this.updatePreview.bind(this);
bootstrap() {
this.jx_list = this.$('.jx_list');
this.jx_code = this.$('.jx_code');
this.jx_errmsg = this.$('.jx_errmsg');
this.jx_version = this.$('.jx_version');
this.jx_ondup = this.$('input[name="ondup"]');
this.ondup = this.root[0].elements.ondup;
this.ondupStore = this.createStore('ondup');
this.jx_ondup.filter(`[value="${this.ondupStore.value}"]`).prop('checked',
true);
hideError() {
this.jx_errmsg.addClass('jx_hide');
}
get versions() {
return Array.from(this.parser.versions).join('、');
}
get results() {
return this.parser.results;
}
updatePreview() {
const code = this.getText();
this.parser.reset();
this.parser.parse(code);
// 如果输入框不为空却没有解析到任何内容
this.jx_errmsg.toggleClass('jx_hide', Boolean(!code || hasResults));
if (hasResults) {
this.jx_version.text(this.versions);
this.jx_list.html(this.results.map(itemInfo).map(wrapTag('li')).join(''));
} else {
this.jx_version.text('--');
this.jx_list.text('');
}
}
setText(content) {
this.jx_code.val(content || '');
this.updatePreview();
}
getText() {
return this.jx_code.val();
}
getDirectory() {
return this.directory;
}
setConfirmCallback(confirmCallback) {
this.confirmCallback = confirmCallback || defaultConfirmCallback;
}
setDirectory(directory) {
if (!directory) {
directory = getCurrentDirectory();
}
this.directory = directory;
}
async onConfirm() {
this.hide();
// 取消了操作
if (!await this.confirmCallback()) {
return;
}
infoDialog({
title: `转存完毕 (失败 ${failed} 个, 共 ${totalCount} 个)!`,
body: `
<ul class="save-complete-details jx_list">
${this.results.map((result) => `${itemInfo(result)}$
{statusHtml(result)}`).map(wrapTag('li')).join('')}
</ul>
`,
cancel: false,
});
}
}
function registerPlugin() {
// 注入到 manifest 定义文件
window.define('function-widget:jixun/standard-code.js', (require, exports) => {
// require, exports, module
exports.start = StandardCodeDialog.create;
});
window.manifest.push({
name: '秒传链接支持',
group: 'moe.jixun.code',
version: '1.0',
type: '1',
description: '类似于 115 的标准提取码',
filesType: '*',
buttons: [{
index: 2,
disabled: 'none',
color: 'violet',
icon: 'icon-upload',
title: '秒传链接',
buttonStyle: 'normal',
pluginId: 'JIXUNSTDCODE',
position: 'tools',
}],
preload: false,
depsFiles: [],
entranceFile: 'function-widget:jixun/standard-code.js',
pluginId: 'JIXUNSTDCODE',
});
}
qs = qs.split(sep);
if (idx >= 0) {
kstr = x.substr(0, idx);
vstr = x.substr(idx + 1);
} else {
kstr = x;
vstr = '';
}
const k = decodeURIComponent(kstr);
const v = decodeURIComponent(vstr);
if (!hasOwnProperty(obj, k)) {
obj[k] = v;
} else if (Array.isArray(obj[k])) {
obj[k].push(v);
} else {
obj[k] = [obj[k], v];
}
}
return obj;
}
class Query {
constructor() {
this.search = {};
}
parse(source) {
this.search = parseQueryString(source.replace(/^(#\??|\?)/g, '').replace(/\+/g,
'%2b'));
}
has(name) {
return Object.prototype.hasOwnProperty.call(this.search, name);
}
get(name) {
return this.search[name];
}
}
class Checkbox {
constructor(options = {}) {
const {
content = '',
className = '',
checked = false,
} = options;
this.$input.prop('checked', checked);
this.root.append(this.$input).append(this.$text);
}
get checked() {
return this.$input.prop('checked');
}
set checked(checked) {
return this.$input.prop('checked', Boolean(checked));
}
appendTo(target) {
this.root.appendTo(target);
}
}
class ImportOnLoad {
static create(content) {
return new ImportOnLoad(content);
}
constructor(content = '') {
this.content = content;
this.onConfirm = this.onConfirm.bind(this);
this.selectDirectory = this.selectDirectory.bind(this);
this.tryAndInitTreeSelector().catch(console.error);
}
async tryAndInitTreeSelector() {
for (let i = 5; i >= 0; i--) {
try {
await this.initTreeSelector();
// init success
return;
} catch (error) {
console.error(error);
}
async initTreeSelector() {
// 百度的这个依赖没处理好啊,还得我手动照着顺序来加载
await loadAsync('disk-system:widget/plugin/moveCopy/start.js');
this.fileTreeDialog = await loadAsync('disk-
system:widget/system/uiService/fileTreeDialog/fileTreeDialog.js');
this.ui = getContext().ui;
this.directoryStore = LocalStore.create(this, 'import_dir');
this.prevPath = this.directoryStore.value || '/';
this.confirmFileList();
}
selectDirectory() {
this.dirSelectDialog = this.fileTreeDialog.show({
title: '导入至…',
confirm: this.onConfirm,
isZip: true,
showShareDir: false,
path: '/',
});
this.$dialogBody = this.dirSelectDialog.dialog.
$dialog.find(getDialog().QUERY.dialogBody);
this.checkUsePrevPath = new Checkbox({
content: '使用上次储存的位置',
className: 'jx-prev-path',
checked: true,
});
this.checkUsePrevPath.appendTo(this.$dialogBody);
this.$prevPath = $('<code>').text(this.prevPath);
this.checkUsePrevPath.$text.append(this.$prevPath);
this.checkUsePrevPath.root.prop('title', this.prevPath);
confirmFileList() {
const { content } = this;
this.stdCodeDialog = StandardCodeDialog.create({
content,
forceRefresh: true,
confirmText: '选择目录',
confirmCallback: this.selectDirectory,
});
}
onConfirm(targetDir) {
this.fileTreeDialog.hide();
function initialiseQueryLink() {
const query = new Query();
query.parse(search);
if (!query.has(KEY_BDLINK)) {
query.parse(hash);
}
if (query.has(KEY_BDLINK)) {
ImportOnLoad.create(decodeBase64(query.get(KEY_BDLINK).replace(/#.{4}$/, '')));
}
}
hook('disk-system:widget/system/uiRender/menu/listMenu.js', injectMenu);
hook('system-core:pluginHub/register/register.js', registerPlugin);
hook('system-core:system/uiService/list/list.js', initialiseQueryLink);
// ESC 将关闭所有漂浮窗口
document.addEventListener('keyup', (e) => {
if (e.keyCode === 0x1b) {
$$1('.dialog-close').click();
}
}, false);
const isGm = (typeof unsafeWindow !== 'undefined') && (unsafeWindow !== window);
if (isGm) {
const INFO = '[仓库助手]';