百度网盘文件转存助手

使用百度网盘的时候经常要将别人分享的文件(夹)转存到自己网盘里。对于非会员用户有每次500个文件的限制,超过500个文件只能自己手动弄,比较麻烦,因此有了这个工具,希望能帮到需要的人,不喜轻喷。(目前支持保存到根目录)

// ==UserScript==
// @name         百度网盘文件转存助手
// @namespace    https://github.com/hyx00000000007
// @version      1.0.1
// @description  使用百度网盘的时候经常要将别人分享的文件(夹)转存到自己网盘里。对于非会员用户有每次500个文件的限制,超过500个文件只能自己手动弄,比较麻烦,因此有了这个工具,希望能帮到需要的人,不喜轻喷。(目前支持保存到根目录)
// @author       shimmer
// @license      BSD
// @match        *://pan.baidu.com/disk/home*
// @match        *://yun.baidu.com/disk/home*
// @match        *://pan.baidu.com/disk/main*
// @match        *://yun.baidu.com/disk/main*
// @require      https://unpkg.com/jquery@3.7.0/dist/jquery.min.js
// @connect      baidu.com
// @connect      baidupcs.com
// @icon         data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    window.BaiduTransfer = function(rootPath) {
        this.ROOT_URL = 'https://pan.baidu.com';
        this.bdstoken = null;
        this.shareId = null;
        this.shareRoot = null;
        this.userId = null;
        this.dirList = [];
        this.fileList = [];
        this.rootPath = rootPath || "";
    };

    BaiduTransfer.prototype = {

        request: async function(path, method, params, data, checkErrno) {
            var url = this.ROOT_URL + path;
            if (params) {
                url += '?' + params;
            }

            try {
                var response = await $.ajax({
                    url: url,
                    type: method,
                    headers: {
                        'X-Requested-With': 'XMLHttpRequest'
                    },
                    data: data,
                    xhrFields: {
                        withCredentials: true
                    }
                });
                if (checkErrno && response.errno && response.errno !== 0) {
                    var errno = response.errno;
                    var errmsg = response.show_msg || "过5分钟重试";
                    var customError = new Error(errmsg);
                    customError.errno = errno;
                    throw customError;
                }
                return response;
            } catch (error) {
                throw error;
            }
        },

        createDirectory: async function(dirPath) {
            try {
                await this.listDir(dirPath);
                return;
            } catch (error) {
                if (error.errno !== -9) {
                    throw error;
                }
            }
            var path = "/api/create";
            var params = "a=commit&bdstoken=" + this.bdstoken;
            var data = "path=" + encodeURIComponent(dirPath) + "&isdir=1&block_list=[]";
            return await this.request(path, "POST", params, data, true);
        },

        listDir: async function(dirPath) {
            var path = "/api/list";
            var params = "order=time&desc=1&showempty=0&page=1&num=1000&dir=" + this.customUrlEncode(dirPath) + "&bdstoken=" + this.bdstoken;
            return await this.request(path, "GET", params, null, true);
        },

        transfer: async function(userId, shareId, fsidList, transferPath) {
            var path = "/share/transfer";
            var params = "shareid=" + shareId + "&from=" + userId + "&ondup=newcopy&channel=chunlei&bdstoken=" + this.bdstoken;
            var data = "fsidlist=[" + fsidList.join(",") + "]&path=" + (transferPath || "/");
            var response = await this.request(path, "POST", params, data, false);
            var errno = response.errno;
            if (errno !== 0) {
                if (errno === 2) {
                    var error = new Error("APIParameterError: url=" + path + " param=" + params);
                    throw error;
                } else if (errno === 12) {
                    var limit = response.target_file_nums_limit
                    var count = response.target_file_nums
                    if(limit&&count){
                        var error = new Error("TransferLimitExceededException: limit=" + limit + " count=" + count);
                        throw error;
                    }
                    var error = new Error(response.show_msg);
                    throw error;
                } else if (errno === 1504) {
                    console.log(`Transfer path ${transferPath} exceeds deadline, retry later...`);
                    await new Promise(resolve => setTimeout(resolve, 1000));
                    this.transfer(userId, shareId, fsidList, transferPath);
                } else if (errno === 111) {
                    console.log(`Transfer path ${transferPath} call api too fast , retry later...`);
                    await new Promise(resolve => setTimeout(resolve, 10000));
                    this.transfer(userId, shareId, fsidList, transferPath);
                } else {
                    var error = new Error("BaiduYunPanAPIException: [" + errno + "] " + response.errmsg);
                    throw error;
                }
            }
        },

        getBdstoken: async function() {
            if (this.bdstoken) {
                return this.bdstoken;
            }

            var path = "/api/gettemplatevariable";
            var params = "fields=[\"bdstoken\"]";
            var response = await this.request(path, "GET", params, null, true);
            this.bdstoken = response.result.bdstoken;
            return this.bdstoken;
        },

        getRandsk: async function(shareKey, pwd) {
            var path = "/share/verify";
            var params = "surl=" + shareKey + "&bdstoken=" + this.bdstoken;
            var data = "pwd=" + pwd;
            var response = await this.request(path, "POST", params, data, true);
            return response.randsk;
        },

        getShareData: async function(shareKey, pwd) {
            var path = "/s/1" + shareKey;
            var response = await this.request(path, "GET", null, null ,false);
            var startTag = 'locals.mset(';
            var endTag = '});';

            var startIndex = response.indexOf(startTag);
            if (startIndex === -1) {
                throw new Error("Invalid response: unable to find locals.mset");
            }
            startIndex += startTag.length;

            var endIndex = response.indexOf(endTag, startIndex);
            if (endIndex === -1) {
                throw new Error("Invalid response: unable to find end of locals.mset");
            }

            var jsonStr = response.substring(startIndex, endIndex + 1);
            var data = JSON.parse(jsonStr);
            return {
                userId: data.share_uk,
                shareId: data.shareid,
                bdstoken: data.bdstoken,
                shareRoot: data.file_list[0].parent_path,
                dirList: data.file_list.filter(e => e.isdir === 1).map(function(file) {
                    return {
                        id: file.fs_id,
                        name: file.server_filename,
                    };
                }),
                fileList: data.file_list.filter(e => e.isdir !== 1).map(function(file) {
                    return {
                        id: file.fs_id,
                        name: file.server_filename,
                    };
                })
            };
        },

        updateRandsk: async function(shareKey, pwd) {
            await this.getBdstoken();
            await this.getRandsk(shareKey, pwd);
        },

        initShareData: async function(shareKey, pwd) {
            if (pwd) {
                await this.updateRandsk(shareKey, pwd);
            }
            try {
                var shareData = await this.getShareData(shareKey, pwd);
                this.userId = shareData.userId;
                this.shareId = shareData.shareId;
                this.bdstoken = shareData.bdstoken;
                this.shareRoot = shareData.shareRoot;
                this.dirList = shareData.dirList;
                this.fileList = shareData.fileList;
            } catch (error) {
                if (error.message.indexOf('/share/init')){
                    if (pwd) {
                        throw new Error("Wrong password: " + pwd);
                    } else {
                        throw new Error("Password not specified");
                    }
                }
            }
        },

        transferFiles: async function(fileList, targetPath) {
            if (targetPath) {
                await this.createDirectory(targetPath);
            }

            var maxTransferCount = 100;

            for (var i = 0; i < fileList.length; i += maxTransferCount) {
                var batch = fileList.slice(i, i + maxTransferCount);
                var fsidList = batch.map(function(file) { return file.id; });
                await this.transfer(this.userId, this.shareId, fsidList, targetPath);
            }
            console.log("Transfer " + fileList.length + " files under directory " + targetPath + " success");
        },

        transferDirs: async function(dirList, targetPath) {
            if (targetPath) {
                await this.createDirectory(targetPath);
            }

            if (dirList.length === 0) {
                return;
            }

            var dirPaths = dirList.map(function(dir) {
                return targetPath + '/' + dir.name;
            });

            try {
                await this.transfer(this.userId, this.shareId, dirList.map(dir => dir.id), targetPath);
                dirPaths.forEach(function(dirPath) {
                    console.log(`Transfer directory ${dirPath} success`);
                });
            } catch (error) {
                if (error.message.includes('TransferLimitExceededException:')) {
                    console.log(`Directory ${dirPaths.join(',')} ${error.message}`);

                    if (dirList.length >= 2) {
                        var mid = Math.floor(dirList.length / 2);
                        await this.transferDirs(dirList.slice(0, mid), targetPath);
                        await this.transferDirs(dirList.slice(mid), targetPath);
                    } else {
                        var dir = dirList[0];
                        var dirPath = this.shareRoot;
                        if (targetPath.length > this.rootPath.length) {
                            dirPath += targetPath.slice(this.rootPath.length);
                        }
                        dirPath += '/' + dir.name;

                        var subFiles = await this.listShareDir(this.userId, this.shareId, dirPath);
                        var subDirList = subFiles.filter(function(file) { return file.isDirectory; });
                        var subFileList = subFiles.filter(function(file) { return !file.isDirectory; });

                        if (subDirList.length > 0) {
                            await this.transferDirs(subDirList, targetPath + '/' + dir.name);
                        }
                        if (subFileList.length > 0) {
                            await this.transferFiles(subFileList, targetPath + '/' + dir.name);
                        }
                    }
                } else {
                    throw error;
                }
            }
        },

        listShareDir: async function(userId, shareId, dirPath) {
            var path = "/share/list";
            var page = 1;
            var limit = 100;
            var result = []
            while(true){
                var params = "uk=" + userId + "&shareid=" + shareId + "&order=name&desc=0&showempty=0&page=1&num=100&dir=" + this.customUrlEncode(dirPath);
                var response = await this.request(path, "GET", params, null ,true);
                var list = response.list;
                list.forEach(function(item) {
                    result.push({
                        id: item.fs_id,
                        name: item.server_filename,
                        isDirectory: item.isdir === 1
                    });
                });
                if(list.length < 100){
                    break;
                }
                page++;
            }
            return result;
        },

        extractShareKey: function(url) {
            try {
                var decodedUrl = decodeURIComponent(url);
                if (decodedUrl.includes("/s/1")) {
                    return decodedUrl.split("/s/1")[1].split("?")[0];
                } else if (decodedUrl.includes("surl=")) {
                    return decodedUrl.split("surl=")[1].split("&")[0];
                }
            } catch (e) {
                console.error("Error extracting share key:", e);
            }
            return null;
        },

        customUrlEncode: function(input) {
            let encoded = '';
            for (let c of input) {
                if (c === ' ' || c === '"' || c === '\'') {
                    encoded += encodeURIComponent(c);
                } else {
                    encoded += c;
                }
            }
            return encoded;
        },


        transferFinal: async function(url, pwd) {
            var shareKey = this.extractShareKey(url);
            if (!shareKey) {
                throw new Error("Unable to extract share key from URL");
            }

            await this.initShareData(shareKey, pwd);

            if (this.dirList.length > 0) {
                await this.transferDirs(this.dirList, this.rootPath);
            }

            if (this.fileList.length > 0) {
                await this.transferFiles(this.fileList, this.rootPath);
            }
        }
    };

    var button = '<div id="shimmer-draggable-button" style="position: fixed; bottom: 20px; left: 20px; z-index: 1000; cursor: grab;">'
                +'<button style="padding: 10px 20px; font-size: 16px; border: none; background-color: #007bff; color: white; cursor: pointer; border-radius: 5px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); transition: background-color 0.3s ease; outline: none;" onmouseover="this.style.backgroundColor=\'#0056b3\';" onmouseout="this.style.backgroundColor=\'#007bff\';">转存助手</button>'
                +'</div>'
    $('body').append(button)

    // 动态创建弹窗
    var modal = $('<div>', {
        id: 'shimmer-input-modal',
        style: 'display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); z-index: 1001;'
    }).append(
        $('<div>', {
            style: 'position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 600px; padding: 20px; background-color: white; border-radius: 10px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);'
        }).append(
            $('<h2>', {
                text: '转存',
                style: 'margin-top: 0; color: #007bff;'
            }),
            $('<input>', {
                type: 'text',
                id: 'shimmer-input-modal-url',
                placeholder: '分享链接',
                style: 'width: 100%; padding: 10px; margin-bottom: 10px; border: 1px solid #ccc; border-radius: 5px;'
            }),
            $('<input>', {
                type: 'text',
                id: 'shimmer-input-modal-pwd',
                placeholder: '密码',
                style: 'width: 100%; padding: 10px; margin-bottom: 10px; border: 1px solid #ccc; border-radius: 5px;'
            }),
                        $('<button>', {
                id: 'shimmer-input-modal-confirm-button',
                text: '确认',
                style: 'width: 100%; padding: 10px; background-color: #007bff; color: white; border: none; border-radius: 5px; cursor: pointer; transition: background-color 0.3s ease;',
                on: {
                    mouseover: function() {
                        $(this).css('backgroundColor', '#0056b3');
                    },
                    mouseout: function() {
                        $(this).css('backgroundColor', '#007bff');
                    }
                }
            })
        )
    );

    $('body').append(modal);

    var buttonWidth = $('#shimmer-draggable-button').outerWidth();
    var buttonHeight = $('#shimmer-draggable-button').outerHeight();
    var edgeOffset = 50;

    $('#shimmer-draggable-button').css('left', -buttonWidth + edgeOffset + 'px');

    $('#shimmer-draggable-button').on('mouseenter', function() {
        $(this).css('left', '0');
    });

    $('#shimmer-draggable-button').on('mouseleave', function() {
        $(this).css('left', -buttonWidth + edgeOffset + 'px');
    });

    $('#shimmer-draggable-button').on('click', function(event) {
        $('#shimmer-input-modal').show();
    });

    $('#shimmer-input-modal').on('click', function(event) {
        if (event.target === this) {
            $('#shimmer-input-modal').hide();
        }
    });

    $('#shimmer-input-modal-confirm-button').on('click', async function(event) {
        var rootPath = "";
        var transfer = new BaiduTransfer(rootPath);
        var url = $("#shimmer-input-modal-url").val();
        var pwd = $("#shimmer-input-modal-pwd").val();

        // 检查 url 
        if (!url) {
            alert("请输入分享链接");
            return;
        }

        $('#shimmer-draggable-button').css('left', '0');
        $('#shimmer-draggable-button button').text('转存中...').prop('disabled', true);
        $('#shimmer-input-modal').hide();
        alert("转存在后台运行中,请不要关闭浏览器和刷新当前页面,注意左下角按钮的状态(目前这个弹窗需要点击确认)");

        try {
            await transfer.transferFinal(url, pwd);
            console.log("Transfer completed successfully.");
            alert("转存成功");
            location.reload();
        } catch (error) {
            console.error("Error during transfer:", error);
            alert("发生错误了..." + error);
        } finally {
            $('#shimmer-draggable-button').css('left', -buttonWidth + edgeOffset + 'px')
            $('#shimmer-draggable-button button').text('转存助手').prop('disabled', false);
        }
    });

})();