/** * 移动云盘(139邮箱云盘)API工具模块 * * 该模块提供与移动云盘(139邮箱云盘)交互的功能,包括: * - 分享链接解析 * - 文件列表获取 * - 文件下载链接获取 * - 数据加密解密 * * @author drpy * @version 1.0.0 */ import axios from "axios"; import CryptoJS from "crypto-js"; import {ENV} from "../env.js"; /** * 移动云盘驱动类 * * 提供移动云盘的完整API接口,支持分享链接解析、文件获取、下载等功能 */ class YunDrive { /** * 构造函数 - 初始化移动云盘驱动 * * 设置必要的配置参数,包括正则表达式、加密密钥、API基础URL等 */ constructor() { // 移动云盘分享链接的正则表达式 this.regex = /https:\/\/yun.139.com\/shareweb\/#\/w\/i\/([^&]+)/; // AES加密密钥 this.x = CryptoJS.enc.Utf8.parse("PVGDwmcvfs1uV3d1"); // API基础URL this.baseUrl = 'https://share-kd-njs.yun.139.com/yun-share/richlifeApp/devapp/IOutLink/'; // 默认请求头 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': 'application/json, text/plain, */*', 'Content-Type': 'application/json', 'hcy-cool-flag': '1', 'x-deviceinfo': '||3|12.27.0|chrome|131.0.0.0|5c7c68368f048245e1ce47f1c0f8f2d0||windows 10|1536X695|zh-CN|||' }; // 分享链接ID this.linkID = ''; // 缓存对象,用于存储API响应结果 this.cache = {}; // 授权token this.authorization = '' } /** * 初始化移动云盘驱动 * * 从环境变量中获取cookie和账号信息,并解析authorization token * * @async * @returns {Promise} */ async init() { if (this.cookie) { console.log('移动cookie获取成功' + this.cookie) const cookie = this.cookie.split(';'); if (this.authorization === '') { // 从cookie中提取authorization token cookie.forEach((item) => { if (item.indexOf('authorization') !== -1) { this.authorization = item.replace('authorization=', ''); console.log('authorization获取成功:' + this.authorization) } }) } } else { console.error("请先获取移动cookie") } if (this.account) { console.log("移动账号获取成功") } } /** * 获取移动云盘cookie * * @returns {string} 从环境变量中获取的cookie字符串 */ get cookie() { return ENV.get('yun_cookie') } /** * 获取移动云盘账号 * * @returns {string} 从环境变量中获取的账号字符串 */ get account() { return ENV.get('yun_account') } /** * 数据加密方法 * * 使用AES-CBC模式对数据进行加密,支持字符串和对象类型 * * @param {string|object} data - 需要加密的数据 * @returns {string} Base64编码的加密结果 */ encrypt(data) { // 生成随机初始化向量 let t = CryptoJS.lib.WordArray.random(16), n = ""; if ("string" == typeof data) { // 字符串类型加密 const o = CryptoJS.enc.Utf8.parse(data); n = CryptoJS.AES.encrypt(o, this.x, {iv: t, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7}); } else if (typeof data === 'object' && data !== null) { // 对象类型先转JSON再加密 const a = JSON.stringify(data), s = CryptoJS.enc.Utf8.parse(a); n = CryptoJS.AES.encrypt(s, this.x, {iv: t, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7}); } // 返回IV和密文的Base64编码 return CryptoJS.enc.Base64.stringify(t.concat(n.ciphertext)); } /** * 数据解密方法 * * 解密使用AES-CBC模式加密的数据 * * @param {string} data - Base64编码的加密数据 * @returns {string} 解密后的原始数据 */ decrypt(data) { // 解析Base64数据 const t = CryptoJS.enc.Base64.parse(data), n = t.clone(), i = n.words.splice(4); // 分离IV和密文 n.init(n.words), t.init(i); const o = CryptoJS.enc.Base64.stringify(t), // 执行AES解密 a = CryptoJS.AES.decrypt(o, this.x, {iv: n, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7}), s = a.toString(CryptoJS.enc.Utf8); return s.toString(); } /** * 从分享URL中提取分享ID * * 支持两种格式的移动云盘分享链接 * * @async * @param {string} url - 移动云盘分享链接 * @returns {Promise} */ async getShareID(url) { // 匹配两种格式的分享链接 const matches = this.regex.exec(url) || /https:\/\/caiyun.139.com\/m\/i\?([^&]+)/.exec(url); if (matches && matches[1]) { this.linkID = matches[1]; } } /** * 获取分享信息 * * 通过API获取指定目录的分享信息,包含文件和文件夹列表 * * @async * @param {string} pCaID - 父目录ID,'root'表示根目录 * @returns {Promise} 分享信息对象,失败时返回null */ async getShareInfo(pCaID) { if (!this.linkID) { console.error('linkID is not set. Please call getShareID first.'); return null; } // 检查缓存 const cacheKey = `${this.linkID}-${pCaID}`; if (this.cache[cacheKey]) { return this.cache[cacheKey]; } // 构造请求数据 let data = JSON.stringify(this.encrypt(JSON.stringify({ "getOutLinkInfoReq": { "account": "", "linkID": this.linkID, "passwd": "", "caSrt": 1, "coSrt": 1, "srtDr": 0, "bNum": 1, "pCaID": pCaID, "eNum": 200 }, "commonAccountInfo": {"account": "", "accountType": 1} }))); try { // 发送API请求 const resp = await axios.post(this.baseUrl + 'getOutLinkInfoV6', data, {headers: this.baseHeader}); if (resp.status !== 200) { return null; } // 解密响应数据 const json = JSON.parse(this.decrypt(resp.data)).data; // 缓存结果 this.cache[cacheKey] = json; return json; } catch (error) { console.error('Error processing share info:', error); return null; } } /** * 获取分享数据 * * 解析分享链接或目录ID,获取完整的文件结构 * * @async * @param {string} url - 分享链接或目录ID * @returns {Promise} 文件结构对象,按目录名分组 */ async getShareData(url) { if (!url) { return {}; } // 判断是URL还是目录ID const isValidUrl = url.startsWith('http'); let pCaID = isValidUrl ? 'root' : url; if (isValidUrl) { await this.getShareID(url); } let file = {}; // 获取文件信息 let fileInfo = await this.getShareFile(pCaID); if (fileInfo && Array.isArray(fileInfo)) { // 并发获取所有文件的下载链接 await Promise.all(fileInfo.map(async (item) => { if (!(item.name in file)) { file[item.name] = []; } let filelist = await this.getShareUrl(item.path); if (filelist && filelist.length > 0) { file[item.name].push(...filelist); } })); } // 清理空的文件夹 for (let key in file) { if (file[key].length === 0) { delete file[key]; } } // 如果没有找到文件,尝试获取根目录文件 if (Object.keys(file).length === 0) { file['root'] = await this.getShareFile(url); if (file['root'] && Array.isArray(file['root'])) { file['root'] = file['root'].filter(item => item && Object.keys(item).length > 0); } } return file; } /** * 获取分享文件列表 * * 递归获取指定目录下的所有文件和子目录 * * @async * @param {string} pCaID - 目录ID或分享链接 * @returns {Promise} 文件列表数组,失败时返回null */ async getShareFile(pCaID) { if (!pCaID) { return null; } try { // 处理URL格式 const isValidUrl = pCaID.startsWith('http'); pCaID = isValidUrl ? 'root' : pCaID; // 获取目录信息 const json = await this.getShareInfo(pCaID); if (!json || !json.caLst) { return null; } const caLst = json?.caLst; const names = caLst.map(it => it.caName); const rootPaths = caLst.map(it => it.path); // 过滤不需要的目录 const filterRegex = /App|活动中心|免费|1T空间|免流/; const videos = []; if (caLst && caLst.length > 0) { // 添加符合条件的目录 names.forEach((name, index) => { if (!filterRegex.test(name)) { videos.push({name: name, path: rootPaths[index]}); } }); // 递归获取子目录内容 let result = await Promise.all(rootPaths.map(async (path) => this.getShareFile(path))); result = result.filter(item => item !== undefined && item !== null); return [...videos, ...result.flat()]; } } catch (error) { console.error('Error processing share data:', error); return null; } } /** * 获取分享文件的下载链接 * * 获取指定目录下所有视频文件的下载信息 * * @async * @param {string} pCaID - 目录ID * @returns {Promise} 文件下载信息数组,失败时返回null */ async getShareUrl(pCaID) { try { const json = await this.getShareInfo(pCaID); if (!json || !('coLst' in json)) { return null; } const coLst = json.coLst; if (coLst !== null) { // 过滤出视频文件(coType === 3) const filteredItems = coLst.filter(it => it && it.coType === 3); return filteredItems.map(it => ({ name: it.coName, contentId: it.path, linkID: this.linkID })); } else if (json.caLst !== null) { // 递归处理子目录 const rootPaths = json.caLst.map(it => it.path); let result = await Promise.all(rootPaths.map(path => this.getShareUrl(path))); result = result.filter(item => item && item.length > 0); return result.flat(); } } catch (error) { console.error('Error processing share URL:', error); return null; } } /** * 获取文件播放链接 * * 通过contentId和linkID获取文件的直接播放链接 * * @async * @param {string} contentId - 文件内容ID * @param {string} linkID - 分享链接ID * @returns {Promise} 播放链接,失败时返回undefined */ async getSharePlay(contentId, linkID) { // 构造请求数据 let data = { "getContentInfoFromOutLinkReq": { "contentId": contentId.split('/')[1], "linkID": linkID, "account": "" }, "commonAccountInfo": { "account": "", "accountType": 1 } }; // 发送API请求 let resp = await axios.post(this.baseUrl + 'getContentInfoFromOutLink', data, { headers: { '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': 'application/json, text/plain, */*', 'Accept-Encoding': 'gzip, deflate, br, zstd', 'Content-Type': 'application/json' } }) if (resp.status === 200 && resp.data.data !== null) { let data = resp.data return data.data.contentInfo.presentURL } } /** * 获取文件下载链接 * * 通过contentId和linkID获取文件的直接下载链接(需要登录) * * @async * @param {string} contentId - 文件内容ID * @param {string} linkID - 分享链接ID * @returns {Promise} 下载链接,失败时返回undefined */ async getDownload(contentId, linkID) { // 初始化认证信息 await this.init() // 构造加密请求数据 let data = this.encrypt(JSON.stringify({ "dlFromOutLinkReqV3": { "linkID": linkID, "account": this.account, "coIDLst": { "item": [contentId] } }, "commonAccountInfo": { "account": this.account, "accountType": 1 } })); // 发送API请求(需要authorization) let resp = await axios.post(this.baseUrl + 'dlFromOutLinkV3', data, { headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36", "Connection": "keep-alive", "Accept": "application/json, text/plain, */*", "Accept-Encoding": "gzip, deflate, br", "Content-Type": "application/json", "accept-language": "zh,en-GB;q=0.9,en-US;q=0.8,en;q=0.7,zh-CN;q=0.6", "authorization": this.authorization, "content-type": "application/json;charset=UTF-8", 'hcy-cool-flag': '1', 'x-deviceinfo': '||3|12.27.0|chrome|136.0.0.0|189f4426ca008b9cbe9bf9bd79723d77||windows 10|1536X695|zh|||' } }) if (resp.status === 200) { // 解密响应获取下载链接 let json = JSON.parse(this.decrypt(resp.data)) return json.data.redrUrl } } } // 导出移动云盘实例 export const Yun = new YunDrive();