百度网盘转存(重试机制)

百度网盘转存(重试机制),解决部分因为特殊路径导致报错的问题

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         百度网盘转存(重试机制)
// @namespace    Wang
// @version      1.0.0
// @author       Wang
// @description  百度网盘转存(重试机制),解决部分因为特殊路径导致报错的问题
// @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*
// @match        https://pan.baidu.com/s/*
// @require      https://unpkg.com/[email protected]/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 || "";
        this.maxRetries = 5; // 统一的重试次数
    };

    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, retryCount = 0) {
            var path = "/share/transfer";
            var params = "shareid=" + shareId + "&from=" + userId + "&ondup=newcopy&channel=chunlei&bdstoken=" + this.bdstoken;
            var data = "fsidlist=[" + fsidList.join(",") + "]&path=" + encodeURIComponent(transferPath || "/");
            
            try {
                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... (${retryCount + 1}/${this.maxRetries})`);
                        if (retryCount < this.maxRetries) {
                            await new Promise(resolve => setTimeout(resolve, 2000));
                            return await this.transfer(userId, shareId, fsidList, transferPath, retryCount + 1);
                        } else {
                            var error = new Error("Transfer timeout after maximum retries");
                            throw error;
                        }
                    } else if (errno === 111) {
                        console.log(`Transfer path ${transferPath} call api too fast, retry later... (${retryCount + 1}/${this.maxRetries})`);
                        if (retryCount < this.maxRetries) {
                            await new Promise(resolve => setTimeout(resolve, 15000));
                            return await this.transfer(userId, shareId, fsidList, transferPath, retryCount + 1);
                        } else {
                            var error = new Error("API rate limit exceeded after maximum retries");
                            throw error;
                        }
                    } else if (errno === -6 || errno === -9 || errno === 130) {
                        // 网络错误或服务器错误,重试
                        console.log(`Network/Server error (errno: ${errno}), retrying... (${retryCount + 1}/${this.maxRetries})`);
                        if (retryCount < this.maxRetries) {
                            await new Promise(resolve => setTimeout(resolve, 3000));
                            return await this.transfer(userId, shareId, fsidList, transferPath, retryCount + 1);
                        } else {
                            var error = new Error(`Network/Server error after maximum retries: errno=${errno}`);
                            throw error;
                        }
                    } else {
                        var error = new Error("BaiduYunPanAPIException: [" + errno + "] " + response.errmsg);
                        throw error;
                    }
                }
                console.log(`Successfully transferred ${fsidList.length} files to ${transferPath}`);
                return response;
            } catch (error) {
                // 如果是网络错误且还有重试次数,则重试
                if (retryCount < this.maxRetries && (error.message.includes('timeout') || error.message.includes('Network'))) {
                    console.log(`Network error, retrying... (${retryCount + 1}/${this.maxRetries}): ${error.message}`);
                    await new Promise(resolve => setTimeout(resolve, 3000));
                    return await this.transfer(userId, shareId, fsidList, transferPath, retryCount + 1);
                }
                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, retryCount = 0) {
            if (targetPath) {
                await this.createDirectory(targetPath);
            }

            var maxTransferCount = 100;

            try {
                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");
            } catch (error) {
                if (retryCount < this.maxRetries) {
                    console.log(`Files transfer failed, retrying... (${retryCount + 1}/${this.maxRetries}): ${error.message}`);
                    await new Promise(resolve => setTimeout(resolve, 5000));
                    return await this.transferFiles(fileList, targetPath, retryCount + 1);
                } else {
                    throw error;
                }
            }
        },

        transferDirs: async function (dirList, targetPath, retryCount = 0) {
            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 {
                    // 其他错误进行重试
                    if (retryCount < this.maxRetries) {
                        console.log(`Directory transfer failed, retrying... (${retryCount + 1}/${this.maxRetries}): ${error.message}`);
                        await new Promise(resolve => setTimeout(resolve, 10000));
                        return await this.transferDirs(dirList, targetPath, retryCount + 1);
                    } else {
                        throw error;
                    }
                }
            }
        },

        listShareDir: async function (userId, shareId, dirPath, retryCount = 0) {
            var path = "/share/list";
            var page = 1;
            var limit = 100;
            var result = []
            
            try {
                while (true) {
                    var params = `uk=${userId}&shareid=${shareId}&order=name&desc=0&showempty=0&page=${page}&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;
            } catch (error) {
                if (retryCount < this.maxRetries) {
                    console.log(`List share dir failed, retrying... (${retryCount + 1}/${this.maxRetries}): ${error.message}`);
                    await new Promise(resolve => setTimeout(resolve, 3000));
                    return await this.listShareDir(userId, shareId, dirPath, retryCount + 1);
                } else {
                    throw error;
                }
            }
        },

        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 if (c === '+') {
                    encoded += '%2B'; // 特殊处理+号,防止被解释为空格
                } 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);
        }
    });

})();