/** * 百度网盘API处理模块 * 提供百度网盘分享链接解析、文件下载、保存等功能 * 支持分享链接验证、文件列表获取、下载地址生成等操作 */ import req from '../req.js'; import {ENV} from '../env.js'; import COOKIE from '../cookieManager.js'; import CryptoJS from "crypto-js"; import {join} from 'path'; import fs from 'fs'; import {PassThrough} from 'stream'; /** * 百度网盘处理类 * 负责处理百度网盘分享链接的解析、验证、文件操作等功能 */ class BaiduHandler { /** * 构造函数 - 初始化百度网盘处理器 */ constructor() { // 初始化百度云盘处理类 this._cookie = ENV.get('baidu_cookie') || ''; // 百度网盘Cookie this.regex = /https:\/\/pan\.baidu\.com\/s\/([^\\|#/]+)/; // 分享链接正则表达式 // 默认请求头配置 this.baseHeader = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', 'Accept-Encoding': 'gzip', 'Referer': 'https://pan.baidu.com', 'Content-Type': 'application/x-www-form-urlencoded' }; this.apiUrl = 'https://pan.baidu.com/'; // API基础URL this.shareTokenCache = {}; // 分享Token缓存 this.saveDirName = 'drpy'; // 保存目录名称 this.saveDirId = null; // 保存目录ID // 支持的字幕文件扩展名 this.subtitleExts = ['.srt', '.ass', '.scc', '.stl', '.ttml']; // 支持的视频文件扩展名 this.subvideoExts = ['.mp4', '.mkv', '.avi', '.rmvb', '.mov', '.flv', '.wmv', '.webm', '.3gp', '.mpeg', '.mpg', '.ts', '.mts', '.m2ts', '.vob', '.divx', '.xvid', '.m4v', '.ogv', '.f4v', '.rm', '.asf', '.dat', '.dv', '.m2v']; // 2小时自动清理保存目录 this.cleanupInterval = setInterval(() => { this.clearSaveDir(); }, 2 * 60 * 60 * 1000); } /** * 获取完整的Cookie * @returns {string} Cookie字符串 */ get cookie() { return (this._cookie || '').trim(); } /** * 设置新的Cookie * @param {string} newCookie - 新的Cookie值 */ set cookie(newCookie) { console.log('更新cookie'); this._cookie = newCookie; } /** * 解析分享链接获取分享数据 * @param {string} url - 百度网盘分享链接 * @returns {Object|null} 分享数据对象,包含shareId和sharePwd */ getShareData(url) { this.clearSaveDir(); // 清理保存目录 // 解析分享链接获取分享ID和密码 try { url = decodeURIComponent(url).replace(/\s+/g, ''); // 解码并移除空白字符 let shareId = ''; let sharePwd = ''; // 匹配分享链接格式 const match = url.match(/pan\.baidu\.com\/(s\/|wap\/init\?surl=)([^?&#]+)/); if (!match) { return null; } shareId = match[2].replace(/^1+/, '').split('?')[0].split('#')[0]; if (!shareId) { return null; } const pwdMatch = url.match(/(提取码|密码|pwd)=([^&\s]{4})/i); sharePwd = pwdMatch ? pwdMatch[2] : ''; return {shareId, sharePwd}; } catch (error) { return null; } } /** * 初始化百度网盘处理器 * @param {Object} db - 数据库实例 * @param {Object} cfg - 配置对象 */ async initBaidu(db, cfg) { // 初始化百度云盘 if (this.cookie) { await this.createSaveDir(); } } /** * 创建保存目录 * @returns {string|null} 保存目录ID,失败时返回null */ async createSaveDir() { // 创建保存目录 if (!this.cookie) { return null; } try { const listResp = await this.api('api/list', { dir: '/', order: 'name', desc: 0, showempty: 0, web: 1, app_id: 250528 }, {Cookie: this.cookie}, 'get'); if (listResp.errno !== 0) { return null; } const drpyDir = listResp.list.find(item => item.isdir === 1 && item.server_filename === this.saveDirName ); if (drpyDir) { this.saveDirId = drpyDir.fs_id; return this.saveDirId; } const createResp = await this.api('api/create', { path: `/${this.saveDirName}`, isdir: 1, block_list: '[]', web: 1, app_id: 250528 }, {Cookie: this.cookie}, 'post'); if (createResp.errno !== 0) { return null; } this.saveDirId = createResp.fs_id; return this.saveDirId; } catch (error) { return null; } } /** * 发送API请求 * @param {string} url - API端点URL * @param {Object} data - 请求数据 * @param {Object} headers - 请求头 * @param {string} method - 请求方法 (get/post) * @param {number} retry - 重试次数 * @returns {Promise} API响应数据 */ async api(url, data = {}, headers = {}, method = 'post', retry = 3) { // 发送API请求 const objectToQuery = (obj) => { return Object.entries(obj) .filter(([_, value]) => value !== undefined && value !== null) .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) .join('&'); }; const fullUrl = `${this.apiUrl}${url}`; headers = {...this.baseHeader, ...headers, Cookie: this.cookie || ''}; let resp; try { if (method === 'get') { const query = objectToQuery(data); const finalUrl = query ? `${fullUrl}?${query}` : fullUrl; resp = await req.get(finalUrl, {headers}); } else { resp = await req.post(fullUrl, data, {headers}); } } catch (err) { resp = err.response || {status: 500, data: {error: '请求失败'}}; } if ([429, 503].includes(resp.status) && retry > 0) { const waitTime = (3 - retry + 1) * 1000; await this.delay(waitTime); return this.api(url, data, headers, method, retry - 1); } return resp.data !== undefined ? resp.data : resp; } /** * 验证分享链接 * @param {Object} shareData - 分享数据对象 * @param {string} shareData.shareId - 分享ID * @param {string} shareData.sharePwd - 分享密码 * @returns {Promise} 验证结果 */ async verifyShare(shareData) { try { const shareVerify = await this.api(`share/verify?t=${Date.now()}&surl=${shareData.shareId}`, { pwd: shareData.sharePwd || '', }, {Cookie: this.cookie}, 'post'); if (shareVerify.errno !== 0) { if (shareVerify.errno === -62 || shareVerify.errno === -9) { console.log('提取码错误'); } console.log('验证提取码失败'); } // 更新cookie中的BDCLND if (shareVerify.randsk) { let cookie = this.cookie.replace(/BDCLND=[^;]*;?\s*/g, ''); if (cookie.length > 0 && !cookie.endsWith(';')) cookie += '; '; cookie += `BDCLND=${shareVerify.randsk}`; this.cookie = cookie; console.log('已更新randsk到cookie中的BDCLND'); } return shareVerify; } catch (error) { console.log('验证分享链接失败:', error.message); throw error; } } /** * 获取分享Token * @param {Object} shareData - 分享数据对象 * @param {string} shareData.shareId - 分享ID * @param {string} shareData.sharePwd - 分享密码 * @returns {Promise} 分享Token数据 */ async getShareToken(shareData) { // 先检查缓存,存在则直接返回 if (this.shareTokenCache[shareData.shareId]) { return this.shareTokenCache[shareData.shareId]; } // 缓存不存在时,执行获取令牌的逻辑 try { // 等待验证完成 const shareVerify = await this.verifyShare(shareData); // 验证完成后,执行获取文件列表的逻辑 const headers = {...this.baseHeader, Cookie: this.cookie || ''}; const listData = await this.api(`share/list`, { shorturl: shareData.shareId, root: 1, page: 1, num: 100 }, {headers}, 'get'); if (listData.errno !== 0) { if (listData.errno === -9) { console.log('提取码错误'); } console.log('获取文件列表失败'); } // 设置缓存 this.shareTokenCache[shareData.shareId] = { ...shareVerify, list: listData.list, uk: listData.uk || listData.share_uk, shareid: listData.share_id || shareVerify.share_id, randsk: shareVerify.randsk, sign: listData.sign || this.generateSign(shareData.shareId, shareData.sharePwd), timestamp: listData.timestamp || Date.now() }; return this.shareTokenCache[shareData.shareId]; } catch (error) { console.log('获取分享token失败:', error.message); throw error; } } /** * 生成签名 * @param {string} shareId - 分享ID * @param {string} sharePwd - 分享密码 * @returns {string} MD5签名字符串 */ generateSign(shareId, sharePwd) { // 生成签名 const timestamp = Date.now(); const str = `${shareId}${sharePwd}${timestamp}${this.cookie || ''}`; return CryptoJS.MD5(str).toString(); } /** * 获取分享链接中的文件列表 * @param {string|Object} shareInfo - 分享链接或分享数据对象 * @returns {Promise} 包含视频文件和字幕文件的对象 */ async getFilesByShareUrl(shareInfo) { // 获取分享链接中的文件列表 const shareData = typeof shareInfo === 'string' ? this.getShareData(shareInfo) : shareInfo; if (!shareData) return {videos: []}; // 确保验证和获取令牌完成后再继续 await this.getShareToken(shareData); if (!this.shareTokenCache[shareData.shareId]) return {videos: []}; const cachedData = await this.shareTokenCache[shareData.shareId]; const videos = []; const subtitles = []; const processDirectory = async (dirPath, dirFsId, parentDrpyPath = '') => { const shareDir = `/sharelink${cachedData.shareid}-${dirFsId}${dirPath}`; const headers = {...this.baseHeader, Cookie: this.cookie || ''}; const dirListData = await this.api(`share/list`, { sekey: cachedData.randsk, uk: cachedData.uk, shareid: cachedData.shareid, page: 1, num: 100, dir: shareDir }, headers, 'get'); if (dirListData.errno !== 0 || !dirListData.list) { return; } for (const item of dirListData.list) { if (item.isdir === 1 || item.isdir === '1') { const subDirPath = `${dirPath}/${item.server_filename}`; const subDrpyPath = `${parentDrpyPath}/${item.server_filename}`; await processDirectory(subDirPath, item.fs_id, subDrpyPath); } else { const ext = item.server_filename.substring(item.server_filename.lastIndexOf('.') || 0).toLowerCase(); const fileInfo = { fid: item.fs_id, file_name: item.server_filename, size: item.size, path: parentDrpyPath, full_path: `/${this.saveDirName}${parentDrpyPath}/${item.server_filename}`, file: true }; if (this.subvideoExts.includes(ext)) { videos.push(fileInfo); } else if (this.subtitleExts.includes(ext)) { subtitles.push(fileInfo); } } } }; if (cachedData.list) { for (const item of cachedData.list) { if (item.isdir === 1 || item.isdir === '1') { const dirPath = `/${item.server_filename}`; const drpyPath = `/${item.server_filename}`; await processDirectory(dirPath, item.fs_id, drpyPath); } else { const ext = item.server_filename.substring(item.server_filename.lastIndexOf('.') || 0).toLowerCase(); const fileInfo = { fid: item.fs_id, file_name: item.server_filename, size: item.size, path: '', full_path: `/${this.saveDirName}/${item.server_filename}`, file: true }; if (this.subvideoExts.includes(ext)) { videos.push(fileInfo); } else if (this.subtitleExts.includes(ext)) { subtitles.push(fileInfo); } } } } const getBaseName = (fileName) => { const lastDotIndex = fileName.lastIndexOf('.'); return lastDotIndex === -1 ? fileName : fileName.slice(0, lastDotIndex); }; const subtitleMap = new Map(); subtitles.forEach(sub => { const baseName = getBaseName(sub.file_name); if (!subtitleMap.has(baseName)) { subtitleMap.set(baseName, []); } subtitleMap.get(baseName).push(sub); }); const videosWithSubtitles = videos.map(video => ({ ...video, subtitles: subtitleMap.get(getBaseName(video.file_name)) || [] })); return {videos: videosWithSubtitles}; } /** * 获取文件下载链接 * @param {string} shareId - 分享ID * @param {string} fileId - 文件ID * @param {string} filename - 文件名 * @returns {Promise} 下载信息对象,失败时返回null */ async getDownload(shareId, fileId, filename) { // 获取文件下载链接 if (!this.shareTokenCache[shareId]) { return null; } if (!fileId || !filename) { return null; } if (!this.cookie) { return null; } const shareData = {shareId, sharePwd: this.shareTokenCache[shareId].sharePwd || ''}; const isSaved = await this.save(shareData, fileId); if (!isSaved) { return null; } const headers = {...this.baseHeader, Cookie: this.cookie || ''}; let retryCount = 1; const fullPath = `/${this.saveDirName}/${filename}`; while (retryCount >= 0) { try { const mediaInfo = await this.api(`api/mediainfo`, { type: 'M3U8_FLV_264_480', path: fullPath, clienttype: 80, origin: 'dlna' }, headers, 'get'); if (mediaInfo.info?.dlink) { return { dlink: mediaInfo.info.dlink, headers, full_path: fullPath }; } const downloadInfo = await this.api(`api/download`, { type: 'download', path: fullPath, app_id: 250528 }, headers, 'get'); if (downloadInfo.info?.dlink) { return { dlink: downloadInfo.info.dlink, headers, is_direct: true, full_path: fullPath }; } retryCount--; if (retryCount >= 0) { await this.delay(1000); } } catch (error) { retryCount--; if (retryCount >= 0) await this.delay(1000); } } return null; } /** * 保存文件到指定目录 * @param {Object} shareData - 分享数据对象 * @param {string} fileFsId - 文件系统ID * @returns {Promise} 保存是否成功 */ async save(shareData, fileFsId) { // 保存文件到指定目录 if (!this.cookie) { return false; } if (!this.saveDirId) { this.saveDirId = await this.createSaveDir(); if (!this.saveDirId) { return false; } } if (!this.shareTokenCache[shareData.shareId]) { await this.getShareToken(shareData); if (!this.shareTokenCache[shareData.shareId]) { return false; } } const headers = { ...this.baseHeader, Cookie: this.cookie || '' }; const tokenData = await this.shareTokenCache[shareData.shareId]; try { const transferResp = await this.api(`share/transfer?shareid=${tokenData.shareid}&from=${tokenData.uk}&sekey=${tokenData.randsk}&ondup=newcopy&async=1&channel=chunlei&web=1&app_id=250528`, { path: `/${this.saveDirName}`, fsidlist: JSON.stringify([fileFsId]), }, { headers }, 'post'); if (transferResp.errno === 0) { return true; } else if (transferResp.errno === 113) { return true; } else if (transferResp.errno === -62 || transferResp.errno === -9) { delete this.shareTokenCache[shareData.shareId]; return false; } else { return false; } } catch (error) { return false; } } /** * 延迟函数 * @param {number} ms - 延迟毫秒数 * @returns {Promise} Promise对象 */ delay(ms) { // 延迟函数 return new Promise((resolve) => setTimeout(resolve, ms)); } /** * 清理保存目录 * 删除保存目录中的所有文件,释放存储空间 * @returns {Promise} Promise对象 */ async clearSaveDir() { // 清理保存目录 if (!this.cookie) { return; } if (!this.saveDirId) { this.saveDirId = await this.createSaveDir(); if (!this.saveDirId) { return; } } const getBdstoken = () => { // 从完整的 cookie 字符串中获取 bdstoken const fullCookie = this.cookie; if (!fullCookie) return null; const cookieParts = fullCookie.split(';'); for (const part of cookieParts) { const trimmed = part.trim(); if (trimmed.startsWith('bdstoken=')) { return trimmed.substring('bdstoken='.length); } } return null; }; let bdstoken = getBdstoken(); if (!bdstoken) { try { const userInfo = await this.api('api/gettemplatevariable?clienttype=0&app_id=250528&web=1&fields=["bdstoken","token","uk","isdocuser","servertime"]', {}, {Cookie: this.cookie}, 'get'); if (userInfo && userInfo.result && userInfo.result.bdstoken) { bdstoken = userInfo.result.bdstoken; } } catch (error) { return; } } if (!bdstoken) { return; } try { const listResp = await this.api('api/list', { dir: `/${this.saveDirName}`, order: 'time', desc: 1, showempty: 0, web: 1, app_id: 250528, channel: 'chunlei' }, {Cookie: this.cookie}, 'get'); if (listResp.errno !== 0) { return; } if (!listResp.list || listResp.list.length === 0) { return; } const headers = { 'User-Agent': 'netdisk;1.4.2;22021211RC;android-android;12;JSbridge4.4.0;jointBridge;1.1.0;', Cookie: this.cookie || '' }; const filePaths = listResp.list.map(item => `/${this.saveDirName}/${item.server_filename}`); const deleteResp = await this.api('api/filemanager?opera=delete', { filelist: JSON.stringify(filePaths), bdstoken: bdstoken }, headers, 'post'); if (deleteResp.errno === 0) { console.log('清理保存目录成功'); } } catch (error) { console.log('清理保存目录失败:', error.message); return; } } } export const Baidu = new BaiduHandler();