UNPKG

netease-cloud-music-api-alger

Version:

网易云音乐 NodeJS 版 API Alger

476 lines (415 loc) 13.2 kB
const { biliRequest, cache } = require('./biliRequest') const axios = require('axios') const http = require('http') const https = require('https') const { URL } = require('url') const fs = require('fs') const path = require('path') // Cookie缓存路径 const COOKIE_CACHE_PATH = path.join(__dirname, '../cache/bilibili_cookies.json') // 默认Cookie路径 - 用户可以在这里配置自己的Cookie const DEFAULT_COOKIE_PATH = path.join( __dirname, '../config/bilibili_cookie.txt', ) /** * 加载缓存的Cookie * @returns {string|null} 缓存的Cookie字符串或null */ function loadCookies() { try { if (fs.existsSync(COOKIE_CACHE_PATH)) { const data = fs.readFileSync(COOKIE_CACHE_PATH, 'utf8') const cookieData = JSON.parse(data) // 检查Cookie是否过期 if (cookieData.expiresAt && new Date(cookieData.expiresAt) > new Date()) { console.log('使用缓存的B站cookies') return cookieData.cookieString } console.log('缓存的B站cookies已过期') } } catch (error) { console.error('加载B站cookies缓存失败:', error) } return null } /** * 保存Cookie到缓存 * @param {string} cookieString Cookie字符串 * @param {number} [expiresInDays=7] Cookie过期天数 */ function saveCookies(cookieString, expiresInDays = 7) { try { // 创建缓存目录(如果不存在) const cacheDir = path.dirname(COOKIE_CACHE_PATH) if (!fs.existsSync(cacheDir)) { fs.mkdirSync(cacheDir, { recursive: true }) } // 设置过期时间 const expiresAt = new Date() expiresAt.setDate(expiresAt.getDate() + expiresInDays) // 保存Cookie数据 const cookieData = { cookieString, expiresAt: expiresAt.toISOString(), } fs.writeFileSync(COOKIE_CACHE_PATH, JSON.stringify(cookieData), 'utf8') console.log('B站cookies已缓存') } catch (error) { console.error('保存B站cookies失败:', error) } } /** * 从配置文件加载默认Cookie * @returns {string} 配置的Cookie字符串,如果不存在则返回空字符串 */ function loadDefaultCookieFromConfig() { try { if (fs.existsSync(DEFAULT_COOKIE_PATH)) { return fs.readFileSync(DEFAULT_COOKIE_PATH, 'utf8').trim() } } catch (error) { console.error('从配置加载默认Cookie失败:', error) } return '' } /** * 从B站首页获取基础Cookie(不登录) * @returns {Promise<string>} Cookie字符串 */ async function fetchBasicCookies() { try { const response = await axios.get('https://www.bilibili.com', { headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', }, maxRedirects: 0, validateStatus: (status) => status >= 200 && status < 400, }) const cookies = response.headers['set-cookie'] || [] if (cookies.length) { const cookieString = cookies .map((cookie) => cookie.split(';')[0]) .join('; ') console.log('成功获取基础Cookie') return cookieString } return '' } catch (error) { console.error('获取基础Cookie失败:', error.message) return '' } } /** * 获取Bilibili Cookies * 优先级: 1.缓存 2.配置文件 3.通过请求获取基础Cookie * @returns {Promise<string>} Cookie字符串 */ async function getBilibiliCookies() { // 1. 尝试从缓存加载 const cachedCookies = loadCookies() if (cachedCookies) { return cachedCookies } // 2. 尝试从配置文件加载 const configCookies = loadDefaultCookieFromConfig() if (configCookies) { console.log('使用配置文件中的Cookie') saveCookies(configCookies) // 保存到缓存 return configCookies } // 3. 获取基础Cookie const basicCookies = await fetchBasicCookies() if (basicCookies) { saveCookies(basicCookies) return basicCookies } return '' } // 默认Cookie字符串 let defaultCookieString = '' // 初始化默认Cookie async function initDefaultCookie() { defaultCookieString = await getBilibiliCookies() console.log('defaultCookieString', defaultCookieString) if (defaultCookieString) { console.log('B站默认cookie已初始化') } else { console.warn('未能初始化B站默认cookie,API请求可能受到限制') } } // 获取Cookie(优先使用请求中的Cookie,否则使用默认Cookie) function getCookie(reqCookie) { return reqCookie || defaultCookieString } /** * 更新Cookie * @param {string} cookieString 新的Cookie字符串 * @returns {boolean} 是否更新成功 */ function updateCookie(cookieString) { if (!cookieString || typeof cookieString !== 'string') { return false } defaultCookieString = cookieString saveCookies(cookieString) return true } /** * B站API配置项 * @typedef {Object} BiliApiConfig * @property {string} path - API路径,如 '/search' * @property {string} url - B站API完整URL,如 'https://api.bilibili.com/x/web-interface/wbi/search/type' * @property {boolean} [useWbi=false] - 是否使用WBI签名 * @property {Object} [defaultParams={}] - 默认参数 * @property {Array<string>} [requiredParams=[]] - 必需参数列表 * @property {string} [method='GET'] - 请求方法 * @property {function} [beforeRequest] - 请求前处理函数,返回处理后的参数 * @property {function} [afterResponse] - 响应后处理函数,返回处理后的响应 */ /** * 创建流式代理请求 * @param {string} url - 要代理的URL * @param {Object} headers - 要使用的请求头 * @param {import('express').Request} req - Express请求对象 * @param {import('express').Response} res - Express响应对象 */ async function createStreamProxy(url, headers, req, res) { try { // 选择合适的HTTP客户端 const httpClient = url.startsWith('https') ? https : http const parsedUrl = new URL(url) // 设置代理请求选项 const options = { hostname: parsedUrl.hostname, port: parsedUrl.port || (url.startsWith('https') ? 443 : 80), path: `${parsedUrl.pathname}${parsedUrl.search}`, method: 'GET', headers: { ...headers, Referer: 'https://www.bilibili.com/', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', }, } // 创建代理请求 const proxyReq = httpClient.request(options, (proxyRes) => { // 把源站响应头复制到我们的响应 res.writeHead(proxyRes.statusCode, proxyRes.headers) // 直接管道连接响应流 proxyRes.pipe(res) }) // 处理错误 proxyReq.on('error', (err) => { console.error('代理请求错误:', err) if (!res.headersSent) { res.status(500).json({ code: 500, message: '代理请求失败', error: err.message, }) } }) // 结束请求 proxyReq.end() } catch (error) { console.error('创建代理流失败:', error) if (!res.headersSent) { res.status(500).json({ code: 500, message: '创建代理流失败', error: error.message, }) } } } /** * 注册B站API路由 * @param {import('express').Express} app - Express应用实例 * @param {Array<BiliApiConfig>} apiConfigs - API配置列表 * @param {Object} [options] - 选项 * @param {string} [options.prefix='/bilibili'] - API路径前缀 */ function registerBiliApis(app, apiConfigs, options = {}) { const prefix = options.prefix || '/bilibili' // 初始化默认Cookie initDefaultCookie() // 注册API路由 apiConfigs.forEach((config) => { const { path, url, useWbi = false, defaultParams = {}, requiredParams = [], method = 'GET', beforeRequest, afterResponse, } = config // 路由处理函数 const routeHandler = async (req, res) => { try { // 合并请求参数 let params = { ...defaultParams } // 获取查询参数 if (method.toUpperCase() === 'GET') { params = { ...params, ...req.query } } else { params = { ...params, ...req.body } } // 验证必需参数 for (const param of requiredParams) { if (!params[param]) { return res.status(400).json({ code: 400, message: `缺少必需参数: ${param}`, }) } } // 请求前处理 if (typeof beforeRequest === 'function') { params = await beforeRequest(params, req) // 如果beforeRequest返回false,则中断请求 if (params === false) { return } } // 获取Cookie const cookie = getCookie(req.headers.cookie) // 发送请求 const result = await biliRequest({ url, params, useWbi, method, cookie: cookie, }) // 响应后处理 if (typeof afterResponse === 'function') { const processedResult = await afterResponse(result, req, res) if (processedResult !== undefined) { return res.json(processedResult) } } // 返回结果 res.json(result) } catch (error) { console.error(`${path}请求失败:`, error) res.status(500).json({ code: 500, message: '服务器内部错误', error: process.env.NODE_ENV === 'development' ? error.message : undefined, }) } } // 注册路由 const routePath = `${prefix}${path}` if (method.toUpperCase() === 'GET') { app.get(routePath, routeHandler) } else if (method.toUpperCase() === 'POST') { app.post(routePath, routeHandler) } else { app.all(routePath, routeHandler) } console.log(`已注册B站API路由: ${method} ${routePath} -> ${url}`) }) // 注册流代理路由 app.get(`${prefix}/stream-proxy`, async (req, res) => { const { url } = req.query if (!url) { return res.status(400).json({ code: 400, message: '缺少必需参数: url', }) } try { // 设置自定义请求头 const headers = { Referer: 'https://www.bilibili.com/', } // 从原始请求传递一些必要的头部 if (req.headers.range) { headers.range = req.headers.range } // 获取Cookie const cookie = getCookie(req.headers.cookie) if (cookie) { headers.cookie = cookie } console.log(`代理流请求: ${url}`) await createStreamProxy(url, headers, req, res) } catch (error) { console.error('代理流请求失败:', error) if (!res.headersSent) { res.status(500).json({ code: 500, message: '代理流请求失败', error: process.env.NODE_ENV === 'development' ? error.message : undefined, }) } } }) console.log(`已注册B站流媒体代理路由: GET ${prefix}/stream-proxy`) // 注册更新Cookie路由 - 允许用户提供自己的Cookie app.post(`${prefix}/update-cookie`, (req, res) => { const { cookie } = req.body if (!cookie) { return res.status(400).json({ code: 400, message: '缺少cookie参数', }) } if (updateCookie(cookie)) { // 创建配置目录并保存用户提供的cookie到配置文件 try { const configDir = path.dirname(DEFAULT_COOKIE_PATH) if (!fs.existsSync(configDir)) { fs.mkdirSync(configDir, { recursive: true }) } fs.writeFileSync(DEFAULT_COOKIE_PATH, cookie, 'utf8') } catch (error) { console.error('保存Cookie到配置文件失败:', error) } res.json({ code: 0, message: 'Cookie已更新' }) } else { res.status(400).json({ code: 400, message: 'Cookie格式不正确' }) } }) console.log(`已注册B站Cookie更新路由: POST ${prefix}/update-cookie`) // 刷新Cookie - 尝试从各个来源重新获取Cookie app.get(`${prefix}/refresh-cookie`, async (req, res) => { try { defaultCookieString = await getBilibiliCookies() res.json({ code: 0, message: 'Cookie已刷新' }) } catch (error) { res.status(500).json({ code: 500, message: '刷新Cookie失败', error: process.env.NODE_ENV === 'development' ? error.message : undefined, }) } }) console.log(`已注册B站Cookie刷新路由: GET ${prefix}/refresh-cookie`) // 注册清除缓存路由 app.get(`${prefix}/clear-cache`, (req, res) => { cache.buvid = '' cache.wbiKeys = null cache.lastWbiKeysFetchTime = 0 // 也清除Cookie缓存 if (fs.existsSync(COOKIE_CACHE_PATH)) { fs.unlinkSync(COOKIE_CACHE_PATH) } res.json({ code: 0, message: '缓存已清除' }) }) console.log(`已注册B站API缓存清理路由: GET ${prefix}/clear-cache`) } module.exports = { registerBiliApis, createStreamProxy, getBilibiliCookies, updateCookie, }