// ==UserScript==
// @name 百度云批量保存
// @name:en_US BDY Batch Saver
// @name:zh-CN 百度云批量保存
// @namespace System233
// @version 0.3
// @description 批量保存百度云文件
// @author System233
// @match *://pan.baidu.com/s/*
// @match *://yun.baidu.com/s/*
// @icon https://t0.gstatic.cn/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=http://pan.baidu.com&size=64
// @grant none
// @license GPL-3.0-only
// @run-at document-start
// @source https://github.com/System233/PIGCATS
// @notes 20231226 v0.3 修复不识别新弹窗的问题
// @notes 20221117 v0.2 修复嵌套文件夹保存问题
// ==/UserScript==
// Copyright (c) 2022 System233
//
// This software is released under the GPL-3.0 License.
// https://opensource.org/licenses/GPL-3.0
(() => {
const logger = Object.assign({}, console);
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const waitForSelector = async (selector, node, timeout) => new Promise((resolve, reject) => {
node = node || document;
timeout = timeout || 10000;
const interval = 50;
const limit = timeout / interval;
;
let times = 0;
const handler = () => {
const el = node.querySelector(selector);
if (el) {
resolve(el);
}
else if (times++ > limit) {
reject(new Error("waitForSelector timeout: " + selector));
}
else {
setTimeout(handler, interval);
}
};
handler();
});
function getSelectedFileList() {
return Array.from(document.querySelectorAll('dd.JS-item-active'));
}
function getFileList() {
return Array.from(document.querySelectorAll('dd[_position]'));
}
function select(node, selected) {
const current = node.matches('.JS-item-active');
if (current == selected) {
return;
}
node.querySelector('span')?.click();
}
function isDir(el) {
return el.querySelector('div[class*=dir]');
}
const getFileName = (node) => {
return node.querySelector('a.filename').title;
};
const doSave = async (path) => {
logger.log('正在保存', path);
await sleep(2000);
await waitForSelector('[node-type="shareSave"]', document).then(el => el.click());
const waitForLoading = async () => {
const list = await waitForSelector('.treeview-root-content', document);
while (document.querySelector('.treeview-leaf-loading') != null || list.children.length == 0) {
await sleep(100);
}
};
let lastIndex = 0, index = 0;
while (index < path.length) {
index = path.indexOf('/', index + 1);
if (index == -1) {
index = path.length;
}
const current = path.substring(0, index);
await waitForLoading();
let node = document.querySelector(`[node-path="${current}"]`);
if (node == null) {
const name = path.substring(lastIndex + 1, index);
await waitForSelector('.g-button[title="新建文件夹"]', document).then(el => el.click());
await waitForSelector('input.shareFolderInput', document).then(el => el.value = name);
await waitForSelector('span.shareFolderConfirm', document).then(el => el.click());
node = await waitForSelector(`[node-path="${current}"]`, document);
}
lastIndex = index;
node.click();
node.scrollIntoView();
}
await waitForSelector('[node-type="confirm"]', document).then(el => el.click());
await sleep(100);
await waitForSelector('.module-canvas-special-cancel', document).then(el => el.click());
while (true) {
if (document.querySelector('.after-trans-dialog')) {
logger.log('保存成功', path);
return true;
}
const iframe = document.querySelector('iframe.buy-guide-iframe-coupon[src*=buy]');
if (iframe && iframe.contentDocument.querySelector('[class*=close]')) {
logger.log('保存失败', path);
Array.from(iframe.contentDocument.querySelectorAll('[class*=close]'), (e) => e.click());
return false;
}
if (document.querySelector('.vip-guide-intro-tip')) {
logger.log('保存失败.old', path);
await waitForSelector('.dialog-close', document).then(el => el.click());
return false;
}
await sleep(50);
}
};
const doJoinTransfer = async (file, path) => {
const name = getFileName(file);
const newPath = `${path}${path.endsWith('/') ? '' : '/'}${name}`;
logger.log("进入目录", newPath);
await waitForSelector('.filename', file).then(x => x.click());
await sleep(100);
let files = [], times = 0;
for (let i = 0; i < 20 && times < 3; ++i) {
await waitForSelector('[style*="visibility: hidden;"] .spinner', document);
await sleep(100);
let next = getFileList();
if (next.length == files.length) {
times++;
}
else {
times = 0;
}
files = next;
}
logger.log("目录内容", files.length);
// await doTransfer(files, newpath);
const start = 0;
const end = files.length - 1;
const mid = Math.floor((start + end) / 2);
await doTransfer(files, newPath, start, mid);
await doTransfer(files, newPath, mid + 1, end);
await waitForSelector('a[data-deep="-1"]', document).then(x => x.click());
await sleep(50);
logger.log("离开目录", newPath);
};
const doTransfer = async (files, path, start, end) => {
if (start == null) {
start = 0;
}
if (end == null) {
end = files.length - 1;
}
if (end - start < 0) {
return;
}
logger.log("保存路径", path, files.length, `[${start}:${end}]`);
files.forEach((file, i) => select(file, i >= start && i <= end));
if (!await doSave(path)) {
logger.log("正在切分", path);
if (files.length == 1 || start == end) {
await doJoinTransfer(files[start], path);
}
else {
const mid = Math.floor((start + end) / 2);
await doTransfer(files, path, start, mid);
await doTransfer(files, path, mid + 1, end);
}
}
else {
logger.log("保存成功", path);
}
};
const getLastPath = async () => {
const name = await waitForSelector('.user-name', document).then(x => x.innerHTML);
return localStorage.getItem(`${name}_transfer_save_path`).split('?')[0];
};
const setLastPath = async (value) => {
const name = await waitForSelector('.user-name', document).then(x => x.innerHTML);
localStorage.setItem(`${name}_transfer_save_path`, `${value}?${Date.now()}`);
};
const getSelectedPath = async () => {
if (document.querySelector('.save-path-item.check')) {
return await getLastPath();
}
return await waitForSelector('.treeview-node-on [node-path]', document).then(x => x.getAttribute('node-path'));
};
const transfer = async () => {
await waitForSelector('[node-type="shareSave"]', document).then(el => el.click());
const confirm = await waitForSelector('[node-type="confirm"]', document);
confirm.addEventListener('click', async (e) => {
e.stopImmediatePropagation();
waitForSelector('.dialog-control span', document).then(x => x.click()).catch(logger.error);
try {
const files = getSelectedFileList();
const path = await getSelectedPath();
logger.log("开始转存", files.length);
await doTransfer(files, path);
await setLastPath(path);
}
catch (err) {
logger.error('发生错误', err);
}
}, true);
};
const load = () => {
const html = `<a class="g-button" href="javascript:;" title="批量保存到网盘"><span class="g-button-right"><em class="icon icon-save-disk" title="批量保存到网盘"></em><span class="text" style="width: auto;">批量保存到网盘</span></span></a>`;
const div = document.createElement('div');
div.innerHTML = html;
const a = div.children[0];
a.addEventListener('click', transfer);
waitForSelector('[node-type="shareSave"]', document).then(node => node.after(a));
};
load();
})();