mirror of
https://github.com/metowolf/Meting.git
synced 2026-09-03 07:27:07 +08:00
refactor: 重构为 nodejs 语言 (#118)
* feat: migrate from PHP to Node.js implementation - Replace PHP implementation with Node.js version - Add Node.js package configuration (package.json, package-lock.json) - Add Rollup build configuration for browser compatibility - Update README with Node.js usage examples and API documentation - Add comprehensive test suite for all supported platforms - Add Claude Code development instructions (CLAUDE.md) - Remove PHP-specific files (composer.json, src/Meting.php) - Update GitHub workflows for Node.js environment This migration maintains API compatibility while providing: - Promise-based async/await support - ES6 class design with method chaining - Zero external dependencies (Node.js built-in modules only) - Support for all existing music platforms (netease, tencent, xiami, kugou, baidu, kuwo) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * refactor: 重构为 Provider 模式架构 - 引入统一的 Provider 接口,实现平台解耦 - 将原有单体文件拆分为模块化 Provider 系统 - 实现真正的内部闭环设计,每个 Provider 独立处理编码/解码 - 优化构建系统,支持版本号注入和 TypeScript 定义生成 - 完善测试覆盖,支持独立平台测试 - 新增架构文档,详细说明设计模式和开发流程 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: 修复 ES Module 导入问题,添加 package.json exports 配置 - 添加 module 字段指向 ESM 版本构建文件 - 添加 exports 字段支持双包发布模式 - 修正 main 字段路径格式 - 解决 import Meting from '@meting/core'; 导入失败问题 Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering> * refactor: 简化网易云音乐架构并恢复 search option 参数支持 - 移除 WebAPI 支持,统一使用 EAPI 架构,简化代码结构 - 删除复杂的选项管理系统和全局配置 - 恢复 search 接口的 option 参数支持(type, page, limit) - 优化构建配置和代码压缩设置 - 更新文档和测试以反映新的 API 结构 Breaking Changes: - 移除 setOption() 方法 - 删除 WebAPI (weapi) 相关代码 - 简化 netease provider 为纯 EAPI 实现 🤖 Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering> --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Happy <yesreply@happy.engineering>
This commit is contained in:
@@ -1,24 +0,0 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.yml]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.php]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.sh]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
22
.github/workflows/test.yml
vendored
22
.github/workflows/test.yml
vendored
@@ -1,22 +0,0 @@
|
||||
name: Test
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
phpunit:
|
||||
name: PHP-${{ matrix.php_version }}-${{ matrix.perfer }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php_version:
|
||||
- 8.0
|
||||
- 8.1
|
||||
- 8.2
|
||||
perfer:
|
||||
- stable
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: Install Dependencies
|
||||
run: composer update --prefer-dist --no-interaction --no-suggest --prefer-${{ matrix.perfer }}
|
||||
- name: Run syntax check
|
||||
run: find -L . -name '*.php' -print0 | xargs -0 -n 1 -P 4 php -l
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,3 +1,2 @@
|
||||
vendor/
|
||||
composer.lock
|
||||
.idea/
|
||||
node_modules/
|
||||
lib/
|
||||
157
ARCHITECTURE.md
Normal file
157
ARCHITECTURE.md
Normal file
@@ -0,0 +1,157 @@
|
||||
# Meting 架构重构说明
|
||||
|
||||
## 重构概述
|
||||
|
||||
本次重构将原本单一文件中的不同音乐厂商逻辑抽离到独立的 Provider 文件中,采用了标准的 Provider 模式,提高了代码的可维护性和扩展性。
|
||||
|
||||
## 新架构结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── meting.js # 主入口文件(重构后)
|
||||
├── meting-original.js # 原始文件备份
|
||||
└── providers/ # 音乐平台提供者目录
|
||||
├── index.js # Provider 工厂类
|
||||
├── base.js # 基础 Provider 接口
|
||||
├── netease.js # 网易云音乐 Provider
|
||||
├── tencent.js # 腾讯音乐 Provider
|
||||
├── xiami.js # 虾米音乐 Provider
|
||||
├── kugou.js # 酷狗音乐 Provider
|
||||
├── baidu.js # 百度音乐 Provider
|
||||
└── kuwo.js # 酷我音乐 Provider
|
||||
```
|
||||
|
||||
## 架构优势
|
||||
|
||||
### 1. 单一职责原则
|
||||
- 每个 Provider 只负责一个音乐平台的逻辑
|
||||
- 主 Meting 类只负责协调和通用功能
|
||||
|
||||
### 2. 开放封闭原则
|
||||
- 添加新平台只需创建新的 Provider,无需修改现有代码
|
||||
- 修改某个平台的逻辑不会影响其他平台
|
||||
|
||||
### 3. 内部闭环设计
|
||||
- 每个 Provider 内部处理自己的编码/解码逻辑
|
||||
- 避免了主类中的方法映射和统一处理
|
||||
- 真正实现了平台逻辑的完全隔离
|
||||
|
||||
### 4. 代码组织清晰
|
||||
- 每个文件职责明确,便于维护
|
||||
- 相关功能聚合在一起
|
||||
- 版本号在构建时从 package.json 注入,避免运行时文件读取
|
||||
|
||||
## 核心组件
|
||||
|
||||
### BaseProvider 基础类
|
||||
所有平台 Provider 的基础接口,定义了标准的方法:
|
||||
- `getHeaders()`: 获取请求头配置
|
||||
- `search()`: 搜索功能
|
||||
- `song()`: 获取歌曲详情
|
||||
- `album()`: 获取专辑信息
|
||||
- `artist()`: 获取艺术家作品
|
||||
- `playlist()`: 获取播放列表
|
||||
- `url()`: 获取播放链接
|
||||
- `lyric()`: 获取歌词
|
||||
- `pic()`: 获取封面图片
|
||||
- `format()`: 数据格式化
|
||||
- `encode()`: 请求编码(如需要)
|
||||
- `urlDecode()`: URL解码(如需要)
|
||||
- `lyricDecode()`: 歌词解码(如需要)
|
||||
|
||||
### ProviderFactory 工厂类
|
||||
负责创建和管理 Provider 实例:
|
||||
- `create(platform, meting)`: 创建指定平台的 Provider
|
||||
- `getSupportedPlatforms()`: 获取支持的平台列表
|
||||
- `isSupported(platform)`: 检查平台是否支持
|
||||
|
||||
### 主 Meting 类
|
||||
协调各个 Provider,提供统一的 API 接口:
|
||||
- 保持原有的公共 API 不变
|
||||
- 简化为纯粹的协调者角色
|
||||
- 将具体执行逻辑完全委托给 Provider
|
||||
- 版本号在构建时注入,无运行时开销
|
||||
|
||||
## 使用方式
|
||||
|
||||
重构后的使用方式与原版完全兼容:
|
||||
|
||||
```javascript
|
||||
import Meting from './src/meting.js';
|
||||
|
||||
// 创建实例
|
||||
const meting = new Meting('netease');
|
||||
|
||||
// 或者动态切换平台
|
||||
meting.site('tencent');
|
||||
|
||||
// 使用 API(与原版完全相同)
|
||||
const result = await meting.search('稻香');
|
||||
```
|
||||
|
||||
## 扩展新平台
|
||||
|
||||
添加新平台只需要:
|
||||
|
||||
1. 在 `src/providers/` 下创建新的 Provider 文件
|
||||
2. 继承 `BaseProvider` 并实现所需方法
|
||||
3. 在 `src/providers/index.js` 中注册新 Provider
|
||||
|
||||
示例:
|
||||
```javascript
|
||||
// src/providers/newplatform.js
|
||||
import BaseProvider from './base.js';
|
||||
|
||||
export default class NewPlatformProvider extends BaseProvider {
|
||||
constructor(meting) {
|
||||
super(meting);
|
||||
this.name = 'newplatform';
|
||||
}
|
||||
|
||||
getHeaders() {
|
||||
// 实现平台特定的请求头
|
||||
}
|
||||
|
||||
search(keyword, option = {}) {
|
||||
// 实现搜索逻辑
|
||||
}
|
||||
|
||||
// ... 实现其他必需方法
|
||||
}
|
||||
```
|
||||
|
||||
## 构建系统
|
||||
|
||||
### Rollup 构建配置
|
||||
- 使用自定义插件在构建时注入版本号
|
||||
- 源码中使用 `__VERSION__` 占位符
|
||||
- 构建时自动替换为 package.json 中的实际版本
|
||||
- 避免运行时文件系统读取,提升性能
|
||||
|
||||
### 构建流程
|
||||
```bash
|
||||
npm run build # 构建 ESM 和 CJS 两种格式
|
||||
```
|
||||
|
||||
构建后:
|
||||
- `lib/meting.esm.js` - ES Module 格式
|
||||
- `lib/meting.js` - CommonJS 格式
|
||||
|
||||
## 兼容性
|
||||
|
||||
- ✅ 保持原有 API 接口不变
|
||||
- ✅ 保持原有使用方式不变
|
||||
- ✅ 保持原有功能特性不变
|
||||
- ✅ 支持原有的链式调用
|
||||
- ✅ 支持原有的配置方法
|
||||
- ✅ 构建时版本号注入,无运行时开销
|
||||
|
||||
## 测试验证
|
||||
|
||||
已通过以下测试:
|
||||
- 基础功能测试(`test/test.js`)
|
||||
- 架构验证测试(`test/simple-test.js`)
|
||||
- 版本号注入测试(`test/build-version-test.js`)
|
||||
- 各平台 Provider 测试
|
||||
|
||||
重构成功保持了所有原有功能,同时大大提升了代码的可维护性和扩展性,并优化了运行时性能。
|
||||
250
CLAUDE.md
Normal file
250
CLAUDE.md
Normal file
@@ -0,0 +1,250 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## 项目概述
|
||||
|
||||
Meting Node.js 版本是一个强大的音乐 API 框架,用于加速音乐相关应用的开发。这是原版 PHP Meting 项目的 Node.js 移植版本,支持多个主流音乐平台的 API 调用,包括网易云音乐(netease)、腾讯音乐(tencent)、虾米音乐(xiami)、酷狗音乐(kugou)、百度音乐(baidu)和酷我音乐(kuwo)。
|
||||
|
||||
## 开发相关命令
|
||||
|
||||
### 构建和测试
|
||||
```bash
|
||||
# 构建库文件(生成 ESM 和 CJS 格式)
|
||||
npm run build
|
||||
|
||||
# 开发模式(文件监听自动构建)
|
||||
npm run dev
|
||||
|
||||
# 运行完整测试(会先构建)
|
||||
npm test
|
||||
|
||||
# 运行示例代码
|
||||
npm start
|
||||
# 或
|
||||
npm run example
|
||||
|
||||
# 直接测试特定文件(需要先构建)
|
||||
node test/test.js # 完整平台测试
|
||||
node test/example.js # 运行示例代码
|
||||
```
|
||||
|
||||
### 单独测试命令
|
||||
```bash
|
||||
# 快速测试单个平台(需要先构建)
|
||||
node -e "
|
||||
import Meting from './lib/meting.js';
|
||||
const m = new Meting('netease');
|
||||
m.format(true);
|
||||
m.search('test', { limit: 5 }).then(console.log);
|
||||
"
|
||||
|
||||
# 验证版本号注入
|
||||
node -e "import Meting from './lib/meting.js'; console.log('Version:', (new Meting()).VERSION)"
|
||||
```
|
||||
|
||||
### 环境要求
|
||||
- Node.js >= 12.0.0
|
||||
- 无外部依赖,仅使用 Node.js 内置模块
|
||||
|
||||
## 核心架构
|
||||
|
||||
### Provider 模式设计
|
||||
项目采用 Provider 模式重构,实现了真正的内部闭环设计:
|
||||
|
||||
- **主 Meting 类** (`src/meting.js`): 纯粹的协调者,负责平台切换和 API 委托
|
||||
- **Provider 工厂** (`src/providers/index.js`): 管理所有平台 Provider 的创建和注册
|
||||
- **基础 Provider** (`src/providers/base.js`): 定义统一接口和默认实现
|
||||
- **平台 Provider** (`src/providers/{platform}.js`): 每个音乐平台的独立实现
|
||||
- **统一 EAPI 流程**: 所有请求都通过统一的 EAPI 请求栈执行,无需用户选择不同的协议
|
||||
|
||||
### 关键设计原则
|
||||
|
||||
1. **内部闭环**: 每个 Provider 完全独立处理自己的编码/解码逻辑,通过 `handleEncode()` 和 `handleDecode()` 方法
|
||||
2. **单一职责**: 每个文件只负责一个平台或一个功能模块
|
||||
3. **无方法映射**: 避免了主类中的方法名映射,Provider 内部直接处理特定逻辑
|
||||
|
||||
### 执行流程
|
||||
```
|
||||
用户 API 调用 → 主 Meting 类 → Provider.executeRequest() → 平台特定处理 → 返回标准化结果
|
||||
```
|
||||
|
||||
**详细请求处理流程:**
|
||||
1. **API 调用**: `meting.search('关键词', { page: 1, limit: 30 })` 等公共方法
|
||||
2. **Provider 委托**: 主类调用对应 Provider 的方法获取 API 配置
|
||||
3. **编码处理**: 如果需要,Provider 调用 `handleEncode()` 进行请求加密
|
||||
4. **HTTP 请求**: 使用内置 fetch API 发送统一的 EAPI 请求,包含重试机制和超时控制
|
||||
5. **解码处理**: 如果需要,Provider 调用 `handleDecode()` 进行响应解密
|
||||
6. **数据格式化**: 根据 `format()` 设置决定是否标准化数据结构
|
||||
7. **结果返回**: 返回 JSON 字符串格式的处理结果
|
||||
|
||||
## 公共 API
|
||||
- `search(keyword, option = {})`: 根据关键词搜索音乐,返回 Promise;`option` 支持 `type`(分类,默认 1 即歌曲)、`page`(页码,默认 1)、`limit`(每页数量,默认 30)
|
||||
- `song(id)`: 获取歌曲详情
|
||||
- `album(id)`: 获取专辑信息
|
||||
- `artist(id, limit = 50)`: 获取艺术家作品列表
|
||||
- `playlist(id)`: 获取播放列表
|
||||
- `url(id, br = 320)`: 获取音频播放地址,可指定码率(kbps)
|
||||
- `lyric(id)`: 获取歌词
|
||||
- `pic(id, size = 300)`: 获取封面图片信息,可指定尺寸
|
||||
|
||||
### 错误处理机制
|
||||
- **网络错误**: 自动重试 3 次,每次间隔 1 秒
|
||||
- **超时控制**: 默认 20 秒请求超时
|
||||
- **平台切换**: 支持动态切换音乐平台作为降级方案
|
||||
- **错误状态**: 错误信息存储在 `meting.error` 和 `meting.status` 属性中
|
||||
|
||||
### 版本号管理
|
||||
- 源码中使用 `__VERSION__` 占位符
|
||||
- 构建时通过 Rollup 自定义插件注入 package.json 中的实际版本号
|
||||
- 避免运行时文件系统读取,提升性能
|
||||
|
||||
**构建注入机制详情:**
|
||||
```javascript
|
||||
// rollup.config.js 中的版本注入插件
|
||||
{
|
||||
name: 'inject-version',
|
||||
transform(code, id) {
|
||||
if (id.endsWith('src/meting.js')) {
|
||||
return code.replace('__VERSION__', packageInfo.version);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**使用方式:**
|
||||
```javascript
|
||||
const meting = new Meting();
|
||||
console.log(meting.VERSION); // 输出实际版本号,如 "1.5.13"
|
||||
```
|
||||
|
||||
## 重要设计模式
|
||||
|
||||
### 适配器模式
|
||||
每个音乐平台都有对应的格式化方法,统一数据结构:
|
||||
- 各平台返回统一的 JSON 格式
|
||||
- `format(true)` 开启时进行数据标准化
|
||||
|
||||
### 策略模式
|
||||
不同平台的请求处理策略通过 Provider 实现:
|
||||
- 每个平台有独特的 API 端点配置
|
||||
- 平台特定的加密和签名方式
|
||||
- 动态的请求头和参数处理
|
||||
|
||||
### 内置加密支持
|
||||
- 网易云音乐:AES-CBC 加密 + RSA 公钥加密
|
||||
- 虾米音乐:签名验证机制
|
||||
- 百度音乐:AES 加密
|
||||
- 使用 Node.js 内置 crypto 模块,无外部依赖
|
||||
|
||||
## 添加新平台
|
||||
|
||||
添加新平台的完整流程:
|
||||
|
||||
1. 在 `src/providers/` 下创建新的 Provider 文件
|
||||
2. 继承 `BaseProvider` 并实现所有必需方法
|
||||
3. 实现平台特定的 `handleEncode()` 和 `handleDecode()` 方法
|
||||
4. 在 `src/providers/index.js` 中注册新 Provider
|
||||
5. 添加对应的测试用例
|
||||
|
||||
```javascript
|
||||
// src/providers/newplatform.js
|
||||
import BaseProvider from './base.js';
|
||||
|
||||
export default class NewPlatformProvider extends BaseProvider {
|
||||
constructor(meting) {
|
||||
super(meting);
|
||||
this.name = 'newplatform';
|
||||
}
|
||||
|
||||
getHeaders() {
|
||||
// 实现平台特定的请求头
|
||||
}
|
||||
|
||||
search(keyword, option = {}) {
|
||||
// 实现搜索逻辑并返回统一的 EAPI 配置
|
||||
}
|
||||
|
||||
async handleEncode(api) {
|
||||
// 处理平台特定的编码逻辑
|
||||
return api;
|
||||
}
|
||||
|
||||
async handleDecode(decodeType, data) {
|
||||
// 处理平台特定的解码逻辑
|
||||
return data;
|
||||
}
|
||||
|
||||
// ... 实现其他必需方法
|
||||
}
|
||||
```
|
||||
|
||||
## 数据格式
|
||||
|
||||
### 标准化歌曲格式(format: true)
|
||||
```javascript
|
||||
{
|
||||
"id": "歌曲ID",
|
||||
"name": "歌曲名称",
|
||||
"artist": ["艺术家1", "艺术家2"],
|
||||
"album": "专辑名称",
|
||||
"pic_id": "封面图片ID",
|
||||
"url_id": "播放链接ID",
|
||||
"lyric_id": "歌词ID",
|
||||
"source": "平台标识"
|
||||
}
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
### API 调用限制
|
||||
- 建议在连续请求间添加延迟(测试中使用 2 秒间隔)
|
||||
- 避免过于频繁的 API 调用导致被限制
|
||||
- 各平台可能有不同的频率限制策略
|
||||
|
||||
### 平台兼容性
|
||||
- 音乐平台 API 可能随时变更,需要定期更新
|
||||
- 部分功能可能因版权限制无法获取(如播放链接)
|
||||
- 某些平台可能需要 Cookie 验证
|
||||
|
||||
### 错误处理模式
|
||||
```javascript
|
||||
const meting = new Meting('netease');
|
||||
meting.format(true);
|
||||
|
||||
try {
|
||||
const result = await meting.search('关键词', { page: 1, limit: 30 });
|
||||
// 处理统一格式的结果
|
||||
} catch (error) {
|
||||
// 记录错误并尝试切换平台重试
|
||||
console.error(meting.status);
|
||||
meting.site('tencent');
|
||||
const fallback = await meting.search('关键词', { page: 1, limit: 30 });
|
||||
}
|
||||
```
|
||||
|
||||
## 构建系统
|
||||
|
||||
### Rollup 配置特点
|
||||
- **双格式输出**: 同时生成 ESM (`lib/meting.esm.js`) 和 CJS (`lib/meting.js`) 格式
|
||||
- **版本号注入**: 构建时自动替换源码中的 `__VERSION__` 占位符
|
||||
- **依赖管理**: 仅使用 Node.js 内置模块,external 配置包含 `crypto`, `url`, `fs`, `path`
|
||||
- **代码压缩**: 使用 terser 插件进行代码压缩优化
|
||||
- **开发模式**: 支持文件监听自动构建 (`npm run dev`)
|
||||
|
||||
### 构建流程
|
||||
```bash
|
||||
# 开发流程
|
||||
npm run dev # 监听文件变化,自动重新构建
|
||||
|
||||
# 生产构建
|
||||
npm run build # 构建两种格式到 lib/ 目录
|
||||
npm test # 构建后运行测试验证
|
||||
npm publish # 发布前会自动执行 prepublishOnly 构建命令
|
||||
```
|
||||
|
||||
### 输出文件说明
|
||||
- `lib/meting.esm.js`: ES Module 格式,用于现代打包工具
|
||||
- `lib/meting.js`: CommonJS 格式,用于 Node.js require 语法
|
||||
- 两种格式都经过压缩优化,文件大小约 30-40KB
|
||||
316
README.md
316
README.md
@@ -2,95 +2,285 @@
|
||||
<img src="https://user-images.githubusercontent.com/2666735/30165599-36623bea-93a6-11e7-8956-1ddf99ce0e6f.png" alt="Meting">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://i-meto.com"><img alt="Author" src="https://img.shields.io/badge/Author-METO-blue.svg?style=flat-square"/></a>
|
||||
<a href="https://packagist.org/packages/metowolf/Meting"><img alt="Version" src="https://img.shields.io/packagist/v/metowolf/Meting.svg?style=flat-square"/></a>
|
||||
<a href="https://packagist.org/packages/metowolf/meting/stats"><img alt="Downloads" src="https://img.shields.io/packagist/dt/metowolf/Meting.svg?style=flat-square"/></a>
|
||||
<a href="https://travis-ci.org/metowolf/Meting"><img alt="Travis" src="https://img.shields.io/travis/metowolf/Meting.svg?style=flat-square"></a>
|
||||
<img alt="License" src="https://img.shields.io/packagist/l/metowolf/Meting.svg?style=flat-square"/>
|
||||
</p>
|
||||
|
||||
> :cake: Wow, such a powerful music API framework
|
||||
> :cake: A powerful music API framework for Node.js
|
||||
|
||||
## Introduction
|
||||
A powerful music API framework to accelerate your development
|
||||
+ **Elegant** - Easy to use, a standardized format for all music platforms.
|
||||
+ **Lightweight** - A single-file library that's less than 51KB.
|
||||
+ **Powerful** - Support various music platforms, including Tencent, NetEase, Xiami, KuGou, Baidu, Kuwo and more.
|
||||
+ **Free** - Under MIT license, need I say more?
|
||||
|
||||
## Requirement
|
||||
PHP 5.4+ and BCMath, Curl, OpenSSL extension installed.
|
||||
Meting is a powerful music API framework designed to accelerate music-related development. This is the **Node.js** version of the original PHP Meting project, providing unified APIs for multiple music platforms.
|
||||
|
||||
### Features
|
||||
|
||||
- **🎵 Multi-Platform Support** - Supports NetEase Cloud Music, Tencent Music, Xiami, KuGou, Baidu Music, and Kuwo
|
||||
- **🚀 Lightweight & Fast** - Zero external dependencies, built with Node.js native modules only
|
||||
- **📱 Modern Async/Await** - Promise-based APIs with full async/await support
|
||||
- **🔄 Unified Interface** - Standardized data format across all music platforms
|
||||
- **🔐 Built-in Encryption** - Platform-specific encryption and signing built-in
|
||||
- **⚡ Chain-able API** - Fluent interface design for elegant code
|
||||
|
||||
## Requirements
|
||||
|
||||
- Node.js >= 12.0.0
|
||||
- No external dependencies required
|
||||
|
||||
## Installation
|
||||
Require this package, with [Composer](https://getcomposer.org), in the root directory of your project.
|
||||
|
||||
Install via npm:
|
||||
|
||||
```bash
|
||||
$ composer require metowolf/meting
|
||||
npm install @meting/core
|
||||
```
|
||||
|
||||
Then you can import the class into your application:
|
||||
Or via yarn:
|
||||
|
||||
```php
|
||||
use Metowolf\Meting;
|
||||
|
||||
$api = new Meting('netease');
|
||||
|
||||
$data = $api->format(true)->search('Soldier');
|
||||
```bash
|
||||
yarn add @meting/core
|
||||
```
|
||||
|
||||
> **Note:** Meting requires [BCMath](http://php.net/manual/en/book.bc.php), [cURL](http://php.net/manual/en/book.curl.php) and [OpenSSL](http://php.net/manual/en/book.openssl.php) extension in order to work.
|
||||
|
||||
|
||||
## Quick Start
|
||||
```php
|
||||
require 'vendor/autoload.php';
|
||||
// require 'Meting.php';
|
||||
|
||||
use Metowolf\Meting;
|
||||
### Basic Usage
|
||||
|
||||
// Initialize to netease API
|
||||
$api = new Meting('netease');
|
||||
```javascript
|
||||
import Meting from '@meting/core';
|
||||
|
||||
// Use custom cookie (option)
|
||||
// $api->cookie('paste your cookie');
|
||||
// Initialize with a music platform
|
||||
const meting = new Meting('netease'); // 'netease', 'tencent', 'xiami', 'kugou', 'baidu', 'kuwo'
|
||||
|
||||
// Get data
|
||||
$data = $api->format(true)->search('Soldier', [
|
||||
'page' => 1,
|
||||
'limit' => 50
|
||||
]);
|
||||
// Enable data formatting for consistent output
|
||||
meting.format(true);
|
||||
|
||||
echo $data;
|
||||
// [{"id":35847388,"name":"Hello","artist":["Adele"],"album":"Hello","pic_id":"1407374890649284","url_id":35847388,"lyric_id":35847388,"source":"netease"},{"id":33211676,"name":"Hello","artist":["OMFG"],"album":"Hello",...
|
||||
|
||||
// Parse link
|
||||
$data = $api->format(true)->url(35847388);
|
||||
|
||||
echo $data;
|
||||
// {"url":"http:\/\/...","size":4729252,"br":128}
|
||||
// Search for songs
|
||||
try {
|
||||
const searchResult = await meting.search('Hello Adele', { page: 1, limit: 10 });
|
||||
const songs = JSON.parse(searchResult);
|
||||
console.log(songs);
|
||||
} catch (error) {
|
||||
console.error('Search failed:', error);
|
||||
}
|
||||
```
|
||||
|
||||
## More usage
|
||||
- [docs](https://github.com/metowolf/Meting/wiki)
|
||||
- [special for netease](https://github.com/metowolf/Meting/wiki/special-for-netease)
|
||||
### Comprehensive Example
|
||||
|
||||
## Join the Discussion
|
||||
- [Telegram Group](https://t.me/adplayer)
|
||||
- [Official website](https://i-meto.com)
|
||||
```javascript
|
||||
import Meting from '@meting/core';
|
||||
|
||||
async function musicExample() {
|
||||
const meting = new Meting('netease');
|
||||
meting.format(true);
|
||||
|
||||
try {
|
||||
// 1. Search for songs
|
||||
const searchResult = await meting.search('Hello Adele');
|
||||
const songs = JSON.parse(searchResult);
|
||||
|
||||
if (songs.length > 0) {
|
||||
const song = songs[0];
|
||||
console.log(`Found: ${song.name} by ${song.artist.join(', ')}`);
|
||||
|
||||
// 2. Get song details
|
||||
const details = await meting.song(song.id);
|
||||
console.log('Song details:', JSON.parse(details));
|
||||
|
||||
// 3. Get streaming URL
|
||||
const urlInfo = await meting.url(song.url_id, 320); // 320kbps
|
||||
console.log('Streaming URL:', JSON.parse(urlInfo));
|
||||
|
||||
// 4. Get lyrics
|
||||
const lyrics = await meting.lyric(song.lyric_id);
|
||||
console.log('Lyrics:', JSON.parse(lyrics));
|
||||
|
||||
// 5. Get album cover
|
||||
const cover = await meting.pic(song.pic_id, 300); // 300x300
|
||||
console.log('Album cover:', JSON.parse(cover));
|
||||
}
|
||||
|
||||
// Switch platform and search again
|
||||
meting.site('tencent');
|
||||
const tencentResult = await meting.search('周杰伦');
|
||||
console.log('Tencent results:', JSON.parse(tencentResult));
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
musicExample();
|
||||
```
|
||||
|
||||
## API Documentation
|
||||
|
||||
### Constructor
|
||||
|
||||
```javascript
|
||||
const meting = new Meting(server);
|
||||
```
|
||||
|
||||
- `server` (string): Music platform ('netease', 'tencent', 'xiami', 'kugou', 'baidu', 'kuwo')
|
||||
|
||||
### Core Methods
|
||||
|
||||
#### Platform Management
|
||||
|
||||
```javascript
|
||||
meting.site(server) // Switch music platform
|
||||
meting.cookie(cookie) // Set platform-specific cookies
|
||||
meting.format(enable) // Enable/disable data formatting
|
||||
```
|
||||
|
||||
#### Search & Discovery
|
||||
|
||||
```javascript
|
||||
// Search for songs, albums, or artists
|
||||
await meting.search(keyword, {
|
||||
type: 1,
|
||||
page: 1,
|
||||
limit: 30,
|
||||
});
|
||||
```
|
||||
|
||||
#### Search Options
|
||||
|
||||
- `type` (number, optional) - Search category for providers that support it. NetEase uses `1` for songs (default), `10` for albums, `100` for artists, etc.
|
||||
- `page` (number, optional) - Page number starting from 1. Defaults to `1`.
|
||||
- `limit` (number, optional) - Number of results per page. Defaults to `30`.
|
||||
|
||||
#### Music Information
|
||||
|
||||
```javascript
|
||||
await meting.song(id) // Get song details
|
||||
await meting.album(id) // Get album information
|
||||
await meting.artist(id, limit) // Get artist's songs
|
||||
await meting.playlist(id) // Get playlist content
|
||||
```
|
||||
|
||||
#### Media Resources
|
||||
|
||||
```javascript
|
||||
await meting.url(id, bitrate) // Get streaming URL
|
||||
await meting.lyric(id) // Get song lyrics
|
||||
await meting.pic(id, size) // Get album artwork
|
||||
```
|
||||
|
||||
### Supported Platforms
|
||||
|
||||
| Platform | Code | Search | Song | Album | Artist | Playlist | URL | Lyric | Picture |
|
||||
|----------|------|--------|------|-------|--------|----------|-----|-------|---------|
|
||||
| NetEase Cloud Music | `netease` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Tencent Music | `tencent` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Xiami Music | `xiami` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| KuGou Music | `kugou` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Baidu Music | `baidu` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Kuwo Music | `kuwo` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
|
||||
### Data Format
|
||||
|
||||
When `format(true)` is enabled, all platforms return standardized JSON:
|
||||
|
||||
```javascript
|
||||
{
|
||||
"id": "35847388",
|
||||
"name": "Hello",
|
||||
"artist": ["Adele"],
|
||||
"album": "Hello",
|
||||
"pic_id": "1407374890649284",
|
||||
"url_id": "35847388",
|
||||
"lyric_id": "35847388",
|
||||
"source": "netease"
|
||||
}
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Running Examples
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Run the example
|
||||
npm start
|
||||
# or
|
||||
npm run example
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# Run tests for all platforms
|
||||
npm test
|
||||
```
|
||||
|
||||
### Build from Source
|
||||
|
||||
```bash
|
||||
# Build the library
|
||||
npm run build
|
||||
|
||||
# Development mode with file watching
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
The library uses Promise-based error handling. Always wrap API calls in try-catch blocks:
|
||||
|
||||
```javascript
|
||||
try {
|
||||
const result = await meting.search('keyword');
|
||||
// Handle success
|
||||
} catch (error) {
|
||||
console.error('API Error:', error);
|
||||
|
||||
// Try fallback platform
|
||||
meting.site('tencent');
|
||||
const fallback = await meting.search('keyword');
|
||||
}
|
||||
```
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
To avoid being rate-limited by music platforms:
|
||||
|
||||
- Add delays between consecutive requests
|
||||
- Don't make too many requests in a short time
|
||||
- Consider implementing request queuing for heavy usage
|
||||
|
||||
```javascript
|
||||
// Example: Add delay between requests
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
- **Copyright Compliance**: Respect music platform terms of service and copyright laws
|
||||
- **Platform Changes**: Music platform APIs may change without notice
|
||||
- **Availability**: Some features may be restricted based on geographical location
|
||||
- **Rate Limits**: Each platform has different rate limiting policies
|
||||
|
||||
## Related Projects
|
||||
- [MoePlayer/Hermit-X](https://github.com/MoePlayer/Hermit-X)
|
||||
- [MoePlayer/APlayer-Typecho](https://github.com/MoePlayer/APlayer-Typecho)
|
||||
- [mengkunsoft/MKOnlineMusicPlayer](https://github.com/mengkunsoft/MKOnlineMusicPlayer)
|
||||
- [webjyh/WP-Player](https://github.com/webjyh/WP-Player)
|
||||
- [yiyungent/Meting4Net](https://github.com/yiyungent/Meting4Net)
|
||||
- [injahow/meting-api](https://github.com/injahow/meting-api)
|
||||
- [mPlayer2](https://github.com/dodododooo/mPlayer2)
|
||||
|
||||
- [Original PHP Meting](https://github.com/metowolf/Meting) - The original PHP version
|
||||
- [MoePlayer/Hermit-X](https://github.com/MoePlayer/Hermit-X) - WordPress music player
|
||||
- [mengkunsoft/MKOnlineMusicPlayer](https://github.com/mengkunsoft/MKOnlineMusicPlayer) - Online music player
|
||||
- [injahow/meting-api](https://github.com/injahow/meting-api) - RESTful API wrapper
|
||||
- [yiyungent/Meting4Net](https://github.com/yiyungent/Meting4Net) - .NET version
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
## Author
|
||||
|
||||
**Meting** © [metowolf](https://github.com/metowolf), Released under the [MIT](./LICENSE) License.<br>
|
||||
**Meting Node.js** © [metowolf](https://github.com/metowolf), Released under the [MIT](./LICENSE) License.
|
||||
|
||||
> Blog [@meto](https://i-meto.com) · GitHub [@metowolf](https://github.com/metowolf) · Twitter [@metowolf](https://twitter.com/metowolf)
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
Made with ❤️ for the music community
|
||||
</p>
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"name": "metowolf/meting",
|
||||
"type": "library",
|
||||
"description": "A powerful music API framework to accelerate development.",
|
||||
"keywords": [
|
||||
"php music",
|
||||
"music api",
|
||||
"lighty",
|
||||
"lightweight"
|
||||
],
|
||||
"homepage": "https://github.com/metowolf/Meting",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "metowolf",
|
||||
"email": "i@i-meto.com",
|
||||
"homepage": "https://i-meto.com"
|
||||
}
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/metowolf/Meting/issues",
|
||||
"source": "https://github.com/metowolf/Meting"
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.4.0",
|
||||
"ext-curl": "*",
|
||||
"ext-openssl": "*"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-bcmath": "Required to use BC Math calculate RSA.",
|
||||
"ext-openssl": "Required to use OpenSSL encrypt params."
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Metowolf\\" : "src/"
|
||||
}
|
||||
}
|
||||
}
|
||||
1173
package-lock.json
generated
Normal file
1173
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
68
package.json
Normal file
68
package.json
Normal file
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"name": "@meting/core",
|
||||
"version": "1.5.13",
|
||||
"description": "A powerful music API framework to accelerate development - Node.js port. Support Netease, Tencent, Xiami, Kugou, Baidu, Kuwo music platforms.",
|
||||
"main": "./lib/meting.js",
|
||||
"module": "./lib/meting.esm.js",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"import": "./lib/meting.esm.js",
|
||||
"require": "./lib/meting.js"
|
||||
},
|
||||
"files": [
|
||||
"lib/",
|
||||
"LICENSE"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "rollup -c",
|
||||
"dev": "rollup -c --watch",
|
||||
"prepublishOnly": "npm run build",
|
||||
"test": "npm run build && node test/test.js",
|
||||
"example": "npm run build && node test/example.js",
|
||||
"start": "npm run build && node test/example.js"
|
||||
},
|
||||
"keywords": [
|
||||
"music",
|
||||
"api",
|
||||
"netease",
|
||||
"tencent",
|
||||
"xiami",
|
||||
"kugou",
|
||||
"baidu",
|
||||
"kuwo",
|
||||
"nodejs",
|
||||
"lightweight",
|
||||
"music-api",
|
||||
"streaming",
|
||||
"search",
|
||||
"lyrics",
|
||||
"no-dependencies"
|
||||
],
|
||||
"author": {
|
||||
"name": "metowolf",
|
||||
"email": "i@i-meto.com",
|
||||
"url": "https://i-meto.com"
|
||||
},
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Claude Code",
|
||||
"email": "noreply@anthropic.com",
|
||||
"url": "https://claude.ai/code"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/metowolf/Meting",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/metowolf/Meting.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/metowolf/Meting/issues"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-babel": "^6.0.4",
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"rollup": "^4.49.0"
|
||||
}
|
||||
}
|
||||
47
rollup.config.js
Normal file
47
rollup.config.js
Normal file
@@ -0,0 +1,47 @@
|
||||
import { nodeResolve } from '@rollup/plugin-node-resolve';
|
||||
import { babel } from '@rollup/plugin-babel';
|
||||
import { readFileSync } from 'fs';
|
||||
import terser from '@rollup/plugin-terser'
|
||||
|
||||
// 读取 package.json 中的版本号
|
||||
const packageInfo = JSON.parse(readFileSync('./package.json', 'utf8'));
|
||||
|
||||
export default {
|
||||
input: 'src/meting.js',
|
||||
output: [
|
||||
{
|
||||
file: 'lib/meting.esm.js',
|
||||
format: 'es'
|
||||
},
|
||||
{
|
||||
file: 'lib/meting.js',
|
||||
format: 'cjs',
|
||||
exports: 'auto'
|
||||
}
|
||||
],
|
||||
plugins: [
|
||||
nodeResolve({
|
||||
preferBuiltins: true
|
||||
}),
|
||||
babel({ babelHelpers: 'bundled' }),
|
||||
// 版本号注入插件
|
||||
{
|
||||
name: 'inject-version',
|
||||
transform(code, id) {
|
||||
if (id.endsWith('src/meting.js')) {
|
||||
return code.replace('__VERSION__', packageInfo.version);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
},
|
||||
terser({
|
||||
format: {
|
||||
comments: false
|
||||
},
|
||||
compress: {
|
||||
drop_console: false
|
||||
}
|
||||
})
|
||||
],
|
||||
external: ['crypto', 'url', 'fs', 'path']
|
||||
};
|
||||
1537
src/Meting.php
1537
src/Meting.php
File diff suppressed because it is too large
Load Diff
192
src/meting.js
Normal file
192
src/meting.js
Normal file
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* Meting music framework - Node.js version (重构版本)
|
||||
* https://i-meto.com
|
||||
* https://github.com/metowolf/Meting
|
||||
*
|
||||
* Copyright 2019, METO Sheel <i@i-meto.com>
|
||||
* Released under the MIT license
|
||||
*/
|
||||
|
||||
import { URLSearchParams } from 'url';
|
||||
import ProviderFactory from './providers/index.js';
|
||||
|
||||
class Meting {
|
||||
constructor(server = 'netease') {
|
||||
this.VERSION = '__VERSION__'; // 在构建时由 rollup 替换为实际版本号
|
||||
this.raw = null;
|
||||
this.info = null;
|
||||
this.error = null;
|
||||
this.status = null;
|
||||
this.temp = {};
|
||||
|
||||
this.server = null;
|
||||
this.provider = null;
|
||||
this.isFormat = false;
|
||||
this.header = {};
|
||||
|
||||
this.site(server);
|
||||
}
|
||||
|
||||
// 设置音乐平台
|
||||
site(server) {
|
||||
if (!ProviderFactory.isSupported(server)) {
|
||||
server = 'netease'; // 默认使用网易云音乐
|
||||
}
|
||||
|
||||
this.server = server;
|
||||
this.provider = ProviderFactory.create(server, this);
|
||||
this.header = this.provider.getHeaders();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
// 设置 Cookie
|
||||
cookie(cookie) {
|
||||
this.header['Cookie'] = cookie;
|
||||
return this;
|
||||
}
|
||||
|
||||
// 设置数据格式化
|
||||
format(format = true) {
|
||||
this.isFormat = format;
|
||||
return this;
|
||||
}
|
||||
|
||||
// 执行 API 请求的主方法
|
||||
async _exec(api) {
|
||||
// 让 Provider 自己处理完整的请求流程
|
||||
return await this.provider.executeRequest(api, this);
|
||||
}
|
||||
|
||||
// HTTP 请求方法 - 使用 fetch API
|
||||
async _curl(url, payload = null, headerOnly = false) {
|
||||
const requestOptions = {
|
||||
method: payload ? 'POST' : 'GET',
|
||||
headers: { ...this.header }
|
||||
};
|
||||
|
||||
// 处理请求体
|
||||
if (payload) {
|
||||
if (typeof payload === 'object' && !Buffer.isBuffer(payload) && typeof payload !== 'string') {
|
||||
payload = new URLSearchParams(payload).toString();
|
||||
requestOptions.headers['Content-Type'] = 'application/x-www-form-urlencoded';
|
||||
}
|
||||
requestOptions.body = payload;
|
||||
}
|
||||
|
||||
// 添加超时控制
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 20000);
|
||||
requestOptions.signal = controller.signal;
|
||||
|
||||
let retries = 3;
|
||||
const makeRequest = async () => {
|
||||
try {
|
||||
const response = await fetch(url, requestOptions);
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
// 存储响应信息
|
||||
this.info = {
|
||||
statusCode: response.status,
|
||||
headers: Object.fromEntries(response.headers.entries())
|
||||
};
|
||||
|
||||
// 获取响应数据
|
||||
const data = await response.text();
|
||||
this.raw = data;
|
||||
this.error = null;
|
||||
this.status = '';
|
||||
|
||||
return this;
|
||||
} catch (err) {
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
// 处理错误
|
||||
if (err.name === 'AbortError') {
|
||||
this.error = 'TIMEOUT';
|
||||
this.status = 'Request timeout';
|
||||
} else {
|
||||
this.error = err.code || err.name;
|
||||
this.status = err.message;
|
||||
}
|
||||
|
||||
// 重试机制
|
||||
if (retries > 0) {
|
||||
retries--;
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
return makeRequest();
|
||||
} else {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return await makeRequest();
|
||||
}
|
||||
|
||||
|
||||
// ========== 公共 API 方法 ==========
|
||||
|
||||
// 搜索功能
|
||||
async search(keyword, option = {}) {
|
||||
const api = this.provider.search(keyword, option);
|
||||
return await this._exec(api);
|
||||
}
|
||||
|
||||
// 获取歌曲详情
|
||||
async song(id) {
|
||||
const api = this.provider.song(id);
|
||||
return await this._exec(api);
|
||||
}
|
||||
|
||||
// 获取专辑信息
|
||||
async album(id) {
|
||||
const api = this.provider.album(id);
|
||||
return await this._exec(api);
|
||||
}
|
||||
|
||||
// 获取艺术家作品
|
||||
async artist(id, limit = 50) {
|
||||
const api = this.provider.artist(id, limit);
|
||||
return await this._exec(api);
|
||||
}
|
||||
|
||||
// 获取播放列表
|
||||
async playlist(id) {
|
||||
const api = this.provider.playlist(id);
|
||||
return await this._exec(api);
|
||||
}
|
||||
|
||||
// 获取音频播放链接
|
||||
async url(id, br = 320) {
|
||||
this.temp.br = br;
|
||||
const api = this.provider.url(id, br);
|
||||
return await this._exec(api);
|
||||
}
|
||||
|
||||
// 获取歌词
|
||||
async lyric(id) {
|
||||
const api = this.provider.lyric(id);
|
||||
return await this._exec(api);
|
||||
}
|
||||
|
||||
// 获取封面图片
|
||||
async pic(id, size = 300) {
|
||||
return await this.provider.pic(id, size);
|
||||
}
|
||||
|
||||
// ========== 静态方法 ==========
|
||||
|
||||
// 获取支持的平台列表
|
||||
static getSupportedPlatforms() {
|
||||
return ProviderFactory.getSupportedPlatforms();
|
||||
}
|
||||
|
||||
// 检查平台是否支持
|
||||
static isSupported(platform) {
|
||||
return ProviderFactory.isSupported(platform);
|
||||
}
|
||||
}
|
||||
|
||||
export default Meting;
|
||||
280
src/providers/baidu.js
Normal file
280
src/providers/baidu.js
Normal file
@@ -0,0 +1,280 @@
|
||||
import crypto from 'crypto';
|
||||
import BaseProvider from './base.js';
|
||||
|
||||
/**
|
||||
* 百度音乐平台提供者
|
||||
*/
|
||||
export default class BaiduProvider extends BaseProvider {
|
||||
constructor(meting) {
|
||||
super(meting);
|
||||
this.name = 'baidu';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取百度音乐的请求头配置
|
||||
*/
|
||||
getHeaders() {
|
||||
return {
|
||||
'Cookie': `BAIDUID=${this._getRandomHex(32)}:FG=1`,
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) baidu-music/1.2.1 Chrome/66.0.3359.181 Electron/3.0.5 Safari/537.36',
|
||||
'Accept': '*/*',
|
||||
'Content-Type': 'application/json;charset=UTF-8',
|
||||
'Accept-Language': 'zh-CN'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索歌曲
|
||||
*/
|
||||
search(keyword, option = {}) {
|
||||
return {
|
||||
method: 'GET',
|
||||
url: 'http://musicapi.taihe.com/v1/restserver/ting',
|
||||
body: {
|
||||
from: 'qianqianmini',
|
||||
method: 'baidu.ting.search.merge',
|
||||
isNew: 1,
|
||||
platform: 'darwin',
|
||||
page_no: option.page || 1,
|
||||
query: keyword,
|
||||
version: '11.2.1',
|
||||
page_size: option.limit || 30
|
||||
},
|
||||
format: 'result.song_info.song_list'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取歌曲详情
|
||||
*/
|
||||
song(id) {
|
||||
return {
|
||||
method: 'GET',
|
||||
url: 'http://musicapi.taihe.com/v1/restserver/ting',
|
||||
body: {
|
||||
from: 'qianqianmini',
|
||||
method: 'baidu.ting.song.getInfos',
|
||||
songid: id,
|
||||
res: 1,
|
||||
platform: 'darwin',
|
||||
version: '1.0.0'
|
||||
},
|
||||
encode: 'baidu_AESCBC',
|
||||
format: 'songinfo'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取专辑信息
|
||||
*/
|
||||
album(id) {
|
||||
return {
|
||||
method: 'GET',
|
||||
url: 'http://musicapi.taihe.com/v1/restserver/ting',
|
||||
body: {
|
||||
from: 'qianqianmini',
|
||||
method: 'baidu.ting.album.getAlbumInfo',
|
||||
album_id: id,
|
||||
platform: 'darwin',
|
||||
version: '11.2.1'
|
||||
},
|
||||
format: 'songlist'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取艺术家作品
|
||||
*/
|
||||
artist(id, limit = 50) {
|
||||
return {
|
||||
method: 'GET',
|
||||
url: 'http://musicapi.taihe.com/v1/restserver/ting',
|
||||
body: {
|
||||
from: 'qianqianmini',
|
||||
method: 'baidu.ting.artist.getSongList',
|
||||
artistid: id,
|
||||
limits: limit,
|
||||
platform: 'darwin',
|
||||
offset: 0,
|
||||
tinguid: 0,
|
||||
version: '11.2.1'
|
||||
},
|
||||
format: 'songlist'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取播放列表
|
||||
*/
|
||||
playlist(id) {
|
||||
return {
|
||||
method: 'GET',
|
||||
url: 'http://musicapi.taihe.com/v1/restserver/ting',
|
||||
body: {
|
||||
from: 'qianqianmini',
|
||||
method: 'baidu.ting.diy.gedanInfo',
|
||||
listid: id,
|
||||
platform: 'darwin',
|
||||
version: '11.2.1'
|
||||
},
|
||||
format: 'content'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取音频播放链接
|
||||
*/
|
||||
url(id, br = 320) {
|
||||
return {
|
||||
method: 'GET',
|
||||
url: 'http://musicapi.taihe.com/v1/restserver/ting',
|
||||
body: {
|
||||
from: 'qianqianmini',
|
||||
method: 'baidu.ting.song.getInfos',
|
||||
songid: id,
|
||||
res: 1,
|
||||
platform: 'darwin',
|
||||
version: '1.0.0'
|
||||
},
|
||||
encode: 'baidu_AESCBC',
|
||||
decode: 'baidu_url'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取歌词
|
||||
*/
|
||||
lyric(id) {
|
||||
return {
|
||||
method: 'GET',
|
||||
url: 'http://musicapi.taihe.com/v1/restserver/ting',
|
||||
body: {
|
||||
from: 'qianqianmini',
|
||||
method: 'baidu.ting.song.lry',
|
||||
songid: id,
|
||||
platform: 'darwin',
|
||||
version: '1.0.0'
|
||||
},
|
||||
decode: 'baidu_lyric'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取封面图片
|
||||
*/
|
||||
async pic(id, size = 300) {
|
||||
const format = this.meting.isFormat;
|
||||
const data = await this.meting.format(false).song(id);
|
||||
this.meting.isFormat = format;
|
||||
const songData = JSON.parse(data);
|
||||
const url = songData.songinfo.pic_radio || songData.songinfo.pic_small;
|
||||
return JSON.stringify({ url: url });
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化百度音乐数据
|
||||
*/
|
||||
format(data) {
|
||||
return {
|
||||
id: data.song_id,
|
||||
name: data.title,
|
||||
artist: data.author ? data.author.split(',') : [],
|
||||
album: data.album_title || '',
|
||||
pic_id: data.song_id,
|
||||
url_id: data.song_id,
|
||||
lyric_id: data.song_id,
|
||||
source: 'baidu'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理百度音乐的编码/解码逻辑
|
||||
*/
|
||||
async handleEncode(api) {
|
||||
if (api.encode === 'baidu_AESCBC') {
|
||||
return this.aesEncrypt(api);
|
||||
}
|
||||
return api;
|
||||
}
|
||||
|
||||
async handleDecode(decodeType, data) {
|
||||
if (decodeType === 'baidu_url') {
|
||||
return this.urlDecode(data);
|
||||
} else if (decodeType === 'baidu_lyric') {
|
||||
return this.lyricDecode(data);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 百度音乐 AES 加密
|
||||
*/
|
||||
async aesEncrypt(api) {
|
||||
const key = 'DBEECF8C50FD160E';
|
||||
const vi = '1231021386755796';
|
||||
|
||||
const data = `songid=${api.body.songid}&ts=${Date.now()}`;
|
||||
|
||||
const cipher = crypto.createCipheriv('aes-128-cbc', key, vi);
|
||||
cipher.setAutoPadding(true);
|
||||
let encrypted = cipher.update(data, 'utf8', 'base64');
|
||||
encrypted += cipher.final('base64');
|
||||
|
||||
api.body.e = encrypted;
|
||||
|
||||
return api;
|
||||
}
|
||||
|
||||
/**
|
||||
* 百度音乐 URL 解码
|
||||
*/
|
||||
urlDecode(result) {
|
||||
const data = JSON.parse(result);
|
||||
|
||||
let maxBr = 0;
|
||||
let url;
|
||||
|
||||
data.songurl.url.forEach(item => {
|
||||
if (item.file_bitrate <= this.meting.temp.br && item.file_bitrate > maxBr) {
|
||||
maxBr = item.file_bitrate;
|
||||
url = {
|
||||
url: item.file_link,
|
||||
br: item.file_bitrate
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
if (!url) {
|
||||
url = {
|
||||
url: '',
|
||||
br: -1
|
||||
};
|
||||
}
|
||||
|
||||
return JSON.stringify(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* 百度音乐歌词解码
|
||||
*/
|
||||
lyricDecode(result) {
|
||||
const data = JSON.parse(result);
|
||||
const lyricData = {
|
||||
lyric: data.lrcContent || '',
|
||||
tlyric: ''
|
||||
};
|
||||
|
||||
return JSON.stringify(lyricData);
|
||||
}
|
||||
|
||||
// ========== 私有工具方法 ==========
|
||||
|
||||
/**
|
||||
* 生成随机十六进制字符串
|
||||
*/
|
||||
_getRandomHex(length) {
|
||||
return crypto.randomBytes(Math.ceil(length / 2))
|
||||
.toString('hex')
|
||||
.slice(0, length);
|
||||
}
|
||||
}
|
||||
247
src/providers/base.js
Normal file
247
src/providers/base.js
Normal file
@@ -0,0 +1,247 @@
|
||||
/**
|
||||
* 音乐平台提供者基础类
|
||||
* 定义所有音乐平台提供者需要实现的接口
|
||||
*/
|
||||
export default class BaseProvider {
|
||||
constructor(meting) {
|
||||
this.meting = meting;
|
||||
this.name = 'base';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取平台的请求头配置
|
||||
* @returns {Object} 请求头对象
|
||||
*/
|
||||
getHeaders() {
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索歌曲
|
||||
* @param {string} keyword 搜索关键词
|
||||
* @param {Object} [option={}] 搜索选项
|
||||
* @returns {Object} API 配置对象
|
||||
*/
|
||||
search(keyword, option = {}) {
|
||||
throw new Error(`${this.name} provider must implement search method`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取歌曲详情
|
||||
* @param {string} id 歌曲ID
|
||||
* @returns {Object} API 配置对象
|
||||
*/
|
||||
song(id) {
|
||||
throw new Error(`${this.name} provider must implement song method`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取专辑信息
|
||||
* @param {string} id 专辑ID
|
||||
* @returns {Object} API 配置对象
|
||||
*/
|
||||
album(id) {
|
||||
throw new Error(`${this.name} provider must implement album method`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取艺术家作品
|
||||
* @param {string} id 艺术家ID
|
||||
* @param {number} limit 限制数量
|
||||
* @returns {Object} API 配置对象
|
||||
*/
|
||||
artist(id, limit = 50) {
|
||||
throw new Error(`${this.name} provider must implement artist method`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取播放列表
|
||||
* @param {string} id 播放列表ID
|
||||
* @returns {Object} API 配置对象
|
||||
*/
|
||||
playlist(id) {
|
||||
throw new Error(`${this.name} provider must implement playlist method`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取音频播放链接
|
||||
* @param {string} id 歌曲ID
|
||||
* @param {number} br 比特率
|
||||
* @returns {Object} API 配置对象
|
||||
*/
|
||||
url(id, br = 320) {
|
||||
throw new Error(`${this.name} provider must implement url method`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取歌词
|
||||
* @param {string} id 歌曲ID
|
||||
* @returns {Object} API 配置对象
|
||||
*/
|
||||
lyric(id) {
|
||||
throw new Error(`${this.name} provider must implement lyric method`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取封面图片
|
||||
* @param {string} id 图片ID
|
||||
* @param {number} size 图片尺寸
|
||||
* @returns {Promise<string>} 图片URL的JSON字符串
|
||||
*/
|
||||
async pic(id, size = 300) {
|
||||
throw new Error(`${this.name} provider must implement pic method`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化数据
|
||||
* @param {Object} data 原始数据
|
||||
* @returns {Object} 格式化后的数据
|
||||
*/
|
||||
format(data) {
|
||||
throw new Error(`${this.name} provider must implement format method`);
|
||||
}
|
||||
|
||||
/**
|
||||
* URL 解码方法(如果需要)
|
||||
* @param {string} result 原始结果
|
||||
* @returns {string} 解码后的结果
|
||||
*/
|
||||
urlDecode(result) {
|
||||
// 默认实现,子类可以覆盖
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 歌词解码方法(如果需要)
|
||||
* @param {string} result 原始结果
|
||||
* @returns {string} 解码后的结果
|
||||
*/
|
||||
lyricDecode(result) {
|
||||
// 默认实现,子类可以覆盖
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行完整的 API 请求流程
|
||||
* @param {Object} api API 配置对象
|
||||
* @param {Object} meting Meting 实例
|
||||
* @returns {string} 处理后的结果
|
||||
*/
|
||||
async executeRequest(api, meting) {
|
||||
// 如果有编码方法,先进行编码
|
||||
if (api.encode) {
|
||||
api = await this.handleEncode(api);
|
||||
}
|
||||
|
||||
// 处理 GET 请求的参数
|
||||
if (api.method === 'GET' && api.body) {
|
||||
const params = new URLSearchParams(api.body);
|
||||
api.url += '?' + params.toString();
|
||||
api.body = null;
|
||||
}
|
||||
|
||||
// 发送 HTTP 请求
|
||||
await meting._curl(api.url, api.body);
|
||||
|
||||
// 如果不需要格式化,直接返回原始数据
|
||||
if (!meting.isFormat) {
|
||||
return meting.raw;
|
||||
}
|
||||
|
||||
let data = meting.raw;
|
||||
|
||||
// 如果有解码方法,进行解码
|
||||
if (api.decode) {
|
||||
data = await this.handleDecode(api.decode, data);
|
||||
}
|
||||
|
||||
// 如果有格式化规则,进行数据清理
|
||||
if (api.format) {
|
||||
data = this.cleanData(data, api.format, meting);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理编码逻辑
|
||||
* @param {Object} api API 配置对象
|
||||
* @returns {Object} 编码后的 API 配置
|
||||
*/
|
||||
async handleEncode(api) {
|
||||
// 子类可以覆盖此方法来处理特定的编码逻辑
|
||||
return api;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理解码逻辑
|
||||
* @param {string} decodeType 解码类型
|
||||
* @param {string} data 原始数据
|
||||
* @returns {string} 解码后的数据
|
||||
*/
|
||||
async handleDecode(decodeType, data) {
|
||||
// 根据解码类型调用相应的方法
|
||||
if (decodeType.includes('url')) {
|
||||
return this.urlDecode(data);
|
||||
} else if (decodeType.includes('lyric')) {
|
||||
return this.lyricDecode(data);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据清理方法
|
||||
* @param {string} raw 原始数据
|
||||
* @param {string} rule 提取规则
|
||||
* @param {Object} meting Meting 实例
|
||||
* @returns {string} 清理后的数据
|
||||
*/
|
||||
cleanData(raw, rule, meting) {
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(raw);
|
||||
} catch (e) {
|
||||
return JSON.stringify([]);
|
||||
}
|
||||
|
||||
if (rule) {
|
||||
data = this.pickupData(data, rule);
|
||||
}
|
||||
|
||||
if (!Array.isArray(data) && typeof data === 'object' && data !== null) {
|
||||
data = [data];
|
||||
}
|
||||
|
||||
if (!Array.isArray(data)) {
|
||||
return JSON.stringify([]);
|
||||
}
|
||||
|
||||
// 使用当前 provider 的格式化方法
|
||||
if (typeof this.format === 'function') {
|
||||
const result = data.map(item => this.format(item));
|
||||
return JSON.stringify(result);
|
||||
}
|
||||
|
||||
return JSON.stringify(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据提取方法
|
||||
* @param {Object} array 数据对象
|
||||
* @param {string} rule 提取规则
|
||||
* @returns {Object} 提取后的数据
|
||||
*/
|
||||
pickupData(array, rule) {
|
||||
const parts = rule.split('.');
|
||||
let result = array;
|
||||
|
||||
for (const part of parts) {
|
||||
if (!result || typeof result !== 'object' || !(part in result)) {
|
||||
return {};
|
||||
}
|
||||
result = result[part];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
51
src/providers/index.js
Normal file
51
src/providers/index.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import NeteaseProvider from './netease.js';
|
||||
import TencentProvider from './tencent.js';
|
||||
import XiamiProvider from './xiami.js';
|
||||
import KugouProvider from './kugou.js';
|
||||
import BaiduProvider from './baidu.js';
|
||||
import KuwoProvider from './kuwo.js';
|
||||
|
||||
/**
|
||||
* 音乐平台提供者工厂
|
||||
*/
|
||||
export default class ProviderFactory {
|
||||
static providers = {
|
||||
netease: NeteaseProvider,
|
||||
tencent: TencentProvider,
|
||||
xiami: XiamiProvider,
|
||||
kugou: KugouProvider,
|
||||
baidu: BaiduProvider,
|
||||
kuwo: KuwoProvider
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建指定平台的提供者实例
|
||||
* @param {string} platform 平台名称
|
||||
* @param {Object} meting Meting 实例
|
||||
* @returns {BaseProvider} 平台提供者实例
|
||||
*/
|
||||
static create(platform, meting) {
|
||||
const ProviderClass = this.providers[platform];
|
||||
if (!ProviderClass) {
|
||||
throw new Error(`Unsupported platform: ${platform}`);
|
||||
}
|
||||
return new ProviderClass(meting);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取支持的平台列表
|
||||
* @returns {string[]} 支持的平台名称数组
|
||||
*/
|
||||
static getSupportedPlatforms() {
|
||||
return Object.keys(this.providers);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查平台是否支持
|
||||
* @param {string} platform 平台名称
|
||||
* @returns {boolean} 是否支持
|
||||
*/
|
||||
static isSupported(platform) {
|
||||
return platform in this.providers;
|
||||
}
|
||||
}
|
||||
292
src/providers/kugou.js
Normal file
292
src/providers/kugou.js
Normal file
@@ -0,0 +1,292 @@
|
||||
import crypto from 'crypto';
|
||||
import BaseProvider from './base.js';
|
||||
|
||||
/**
|
||||
* 酷狗音乐平台提供者
|
||||
*/
|
||||
export default class KugouProvider extends BaseProvider {
|
||||
constructor(meting) {
|
||||
super(meting);
|
||||
this.name = 'kugou';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取酷狗音乐的请求头配置
|
||||
*/
|
||||
getHeaders() {
|
||||
return {
|
||||
'User-Agent': 'IPhone-8990-searchSong',
|
||||
'UNI-UserAgent': 'iOS11.4-Phone8990-1009-0-WiFi'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索歌曲
|
||||
*/
|
||||
search(keyword, option = {}) {
|
||||
return {
|
||||
method: 'GET',
|
||||
url: 'http://mobilecdn.kugou.com/api/v3/search/song',
|
||||
body: {
|
||||
api_ver: 1,
|
||||
area_code: 1,
|
||||
correct: 1,
|
||||
pagesize: option.limit || 30,
|
||||
plat: 2,
|
||||
tag: 1,
|
||||
sver: 5,
|
||||
showtype: 10,
|
||||
page: option.page || 1,
|
||||
keyword: keyword,
|
||||
version: 8990
|
||||
},
|
||||
format: 'data.info'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取歌曲详情
|
||||
*/
|
||||
song(id) {
|
||||
return {
|
||||
method: 'POST',
|
||||
url: 'http://m.kugou.com/app/i/getSongInfo.php',
|
||||
body: {
|
||||
cmd: 'playInfo',
|
||||
hash: id,
|
||||
from: 'mkugou'
|
||||
},
|
||||
format: ''
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取专辑信息
|
||||
*/
|
||||
album(id) {
|
||||
return {
|
||||
method: 'GET',
|
||||
url: 'http://mobilecdn.kugou.com/api/v3/album/song',
|
||||
body: {
|
||||
albumid: id,
|
||||
area_code: 1,
|
||||
plat: 2,
|
||||
page: 1,
|
||||
pagesize: -1,
|
||||
version: 8990
|
||||
},
|
||||
format: 'data.info'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取艺术家作品
|
||||
*/
|
||||
artist(id, limit = 50) {
|
||||
return {
|
||||
method: 'GET',
|
||||
url: 'http://mobilecdn.kugou.com/api/v3/singer/song',
|
||||
body: {
|
||||
singerid: id,
|
||||
area_code: 1,
|
||||
page: 1,
|
||||
plat: 0,
|
||||
pagesize: limit,
|
||||
version: 8990
|
||||
},
|
||||
format: 'data.info'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取播放列表
|
||||
*/
|
||||
playlist(id) {
|
||||
return {
|
||||
method: 'GET',
|
||||
url: 'http://mobilecdn.kugou.com/api/v3/special/song',
|
||||
body: {
|
||||
specialid: id,
|
||||
area_code: 1,
|
||||
page: 1,
|
||||
plat: 2,
|
||||
pagesize: -1,
|
||||
version: 8990
|
||||
},
|
||||
format: 'data.info'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取音频播放链接
|
||||
*/
|
||||
url(id, br = 320) {
|
||||
return {
|
||||
method: 'POST',
|
||||
url: 'http://media.store.kugou.com/v1/get_res_privilege',
|
||||
body: JSON.stringify({
|
||||
relate: 1,
|
||||
userid: '0',
|
||||
vip: 0,
|
||||
appid: 1000,
|
||||
token: '',
|
||||
behavior: 'download',
|
||||
area_code: '1',
|
||||
clientver: '8990',
|
||||
resource: [{
|
||||
id: 0,
|
||||
type: 'audio',
|
||||
hash: id
|
||||
}]
|
||||
}),
|
||||
decode: 'kugou_url'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取歌词
|
||||
*/
|
||||
lyric(id) {
|
||||
return {
|
||||
method: 'GET',
|
||||
url: 'http://krcs.kugou.com/search',
|
||||
body: {
|
||||
keyword: '%20-%20',
|
||||
ver: 1,
|
||||
hash: id,
|
||||
client: 'mobi',
|
||||
man: 'yes'
|
||||
},
|
||||
decode: 'kugou_lyric'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取封面图片
|
||||
*/
|
||||
async pic(id, size = 300) {
|
||||
const format = this.meting.isFormat;
|
||||
const data = await this.meting.format(false).song(id);
|
||||
this.meting.isFormat = format;
|
||||
const songData = JSON.parse(data);
|
||||
let url = songData.imgUrl;
|
||||
url = url.replace('{size}', '400');
|
||||
return JSON.stringify({ url: url });
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化酷狗音乐数据
|
||||
*/
|
||||
format(data) {
|
||||
const filename = data.filename || data.fileName;
|
||||
const result = {
|
||||
id: data.hash,
|
||||
name: filename,
|
||||
artist: [],
|
||||
album: data.album_name || '',
|
||||
url_id: data.hash,
|
||||
pic_id: data.hash,
|
||||
lyric_id: data.hash,
|
||||
source: 'kugou'
|
||||
};
|
||||
|
||||
const parts = filename.split(' - ');
|
||||
if (parts.length >= 2) {
|
||||
result.artist = parts[0].split('、');
|
||||
result.name = parts[1];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理酷狗音乐的解码逻辑
|
||||
*/
|
||||
async handleDecode(decodeType, data) {
|
||||
if (decodeType === 'kugou_url') {
|
||||
return this.urlDecode(data);
|
||||
} else if (decodeType === 'kugou_lyric') {
|
||||
return this.lyricDecode(data);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 酷狗音乐 URL 解码
|
||||
*/
|
||||
async urlDecode(result) {
|
||||
const data = JSON.parse(result);
|
||||
|
||||
let maxBr = 0;
|
||||
let url;
|
||||
|
||||
for (const item of data.data[0].relate_goods) {
|
||||
if (item.info.bitrate <= this.meting.temp.br && item.info.bitrate > maxBr) {
|
||||
const api = {
|
||||
method: 'GET',
|
||||
url: 'http://trackercdn.kugou.com/i/v2/',
|
||||
body: {
|
||||
hash: item.hash,
|
||||
key: crypto.createHash('md5').update(item.hash + 'kgcloudv2').digest('hex'),
|
||||
pid: 3,
|
||||
behavior: 'play',
|
||||
cmd: '25',
|
||||
version: 8990
|
||||
}
|
||||
};
|
||||
|
||||
const response = JSON.parse(await this.meting._exec(api));
|
||||
if (response.url) {
|
||||
maxBr = response.bitRate / 1000;
|
||||
url = {
|
||||
url: Array.isArray(response.url) ? response.url[0] : response.url,
|
||||
size: response.fileSize,
|
||||
br: response.bitRate / 1000
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!url) {
|
||||
url = {
|
||||
url: '',
|
||||
size: 0,
|
||||
br: -1
|
||||
};
|
||||
}
|
||||
|
||||
return JSON.stringify(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* 酷狗音乐歌词解码
|
||||
*/
|
||||
async lyricDecode(result) {
|
||||
const data = JSON.parse(result);
|
||||
|
||||
if (!data.candidates || data.candidates.length === 0) {
|
||||
return JSON.stringify({ lyric: '', tlyric: '' });
|
||||
}
|
||||
|
||||
const api = {
|
||||
method: 'GET',
|
||||
url: 'http://lyrics.kugou.com/download',
|
||||
body: {
|
||||
charset: 'utf8',
|
||||
accesskey: data.candidates[0].accesskey,
|
||||
id: data.candidates[0].id,
|
||||
client: 'mobi',
|
||||
fmt: 'lrc',
|
||||
ver: 1
|
||||
}
|
||||
};
|
||||
|
||||
const response = JSON.parse(await this.meting._exec(api));
|
||||
const lyricData = {
|
||||
lyric: Buffer.from(response.content, 'base64').toString(),
|
||||
tlyric: ''
|
||||
};
|
||||
|
||||
return JSON.stringify(lyricData);
|
||||
}
|
||||
}
|
||||
226
src/providers/kuwo.js
Normal file
226
src/providers/kuwo.js
Normal file
@@ -0,0 +1,226 @@
|
||||
import BaseProvider from './base.js';
|
||||
|
||||
/**
|
||||
* 酷我音乐平台提供者
|
||||
*/
|
||||
export default class KuwoProvider extends BaseProvider {
|
||||
constructor(meting) {
|
||||
super(meting);
|
||||
this.name = 'kuwo';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取酷我音乐的请求头配置
|
||||
*/
|
||||
getHeaders() {
|
||||
return {
|
||||
'Cookie': 'Hm_lvt_cdb524f42f0ce19b169a8071123a4797=1623339177,1623339183; _ga=GA1.2.1195980605.1579367081; Hm_lpvt_cdb524f42f0ce19b169a8071123a4797=1623339982; kw_token=3E7JFQ7MRPL; _gid=GA1.2.747985028.1623339179; _gat=1',
|
||||
'csrf': '3E7JFQ7MRPL',
|
||||
'Host': 'www.kuwo.cn',
|
||||
'Referer': 'http://www.kuwo.cn/',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索歌曲
|
||||
*/
|
||||
search(keyword, option = {}) {
|
||||
return {
|
||||
method: 'GET',
|
||||
url: 'http://www.kuwo.cn/api/www/search/searchMusicBykeyWord',
|
||||
body: {
|
||||
key: keyword,
|
||||
pn: option.page || 1,
|
||||
rn: option.limit || 30,
|
||||
httpsStatus: 1
|
||||
},
|
||||
format: 'data.list'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取歌曲详情
|
||||
*/
|
||||
song(id) {
|
||||
return {
|
||||
method: 'GET',
|
||||
url: 'http://www.kuwo.cn/api/www/music/musicInfo',
|
||||
body: {
|
||||
mid: id,
|
||||
httpsStatus: 1
|
||||
},
|
||||
format: 'data'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取专辑信息
|
||||
*/
|
||||
album(id) {
|
||||
return {
|
||||
method: 'GET',
|
||||
url: 'http://www.kuwo.cn/api/www/album/albumInfo',
|
||||
body: {
|
||||
albumId: id,
|
||||
pn: 1,
|
||||
rn: 1000,
|
||||
httpsStatus: 1
|
||||
},
|
||||
format: 'data.musicList'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取艺术家作品
|
||||
*/
|
||||
artist(id, limit = 50) {
|
||||
return {
|
||||
method: 'GET',
|
||||
url: 'http://www.kuwo.cn/api/www/artist/artistMusic',
|
||||
body: {
|
||||
artistid: id,
|
||||
pn: 1,
|
||||
rn: limit,
|
||||
httpsStatus: 1
|
||||
},
|
||||
format: 'data.list'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取播放列表
|
||||
*/
|
||||
playlist(id) {
|
||||
return {
|
||||
method: 'GET',
|
||||
url: 'http://www.kuwo.cn/api/www/playlist/playListInfo',
|
||||
body: {
|
||||
pid: id,
|
||||
pn: 1,
|
||||
rn: 1000,
|
||||
httpsStatus: 1
|
||||
},
|
||||
format: 'data.musicList'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取音频播放链接
|
||||
*/
|
||||
url(id, br = 320) {
|
||||
return {
|
||||
method: 'GET',
|
||||
url: 'http://www.kuwo.cn/api/v1/www/music/playUrl',
|
||||
body: {
|
||||
mid: id,
|
||||
type: 'music',
|
||||
httpsStatus: 1
|
||||
},
|
||||
decode: 'kuwo_url'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取歌词
|
||||
*/
|
||||
lyric(id) {
|
||||
return {
|
||||
method: 'GET',
|
||||
url: 'http://m.kuwo.cn/newh5/singles/songinfoandlrc',
|
||||
body: {
|
||||
musicId: id,
|
||||
httpsStatus: 1
|
||||
},
|
||||
decode: 'kuwo_lyric'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取封面图片
|
||||
*/
|
||||
async pic(id, size = 300) {
|
||||
const format = this.meting.isFormat;
|
||||
const data = await this.meting.format(false).song(id);
|
||||
this.meting.isFormat = format;
|
||||
const songData = JSON.parse(data);
|
||||
const url = songData.data.pic || songData.data.albumpic;
|
||||
return JSON.stringify({ url: url });
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化酷我音乐数据
|
||||
*/
|
||||
format(data) {
|
||||
return {
|
||||
id: data.rid,
|
||||
name: data.name,
|
||||
artist: data.artist ? data.artist.split('&') : [],
|
||||
album: data.album || '',
|
||||
pic_id: data.rid,
|
||||
url_id: data.rid,
|
||||
lyric_id: data.rid,
|
||||
source: 'kuwo'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理酷我音乐的解码逻辑
|
||||
*/
|
||||
async handleDecode(decodeType, data) {
|
||||
if (decodeType === 'kuwo_url') {
|
||||
return this.urlDecode(data);
|
||||
} else if (decodeType === 'kuwo_lyric') {
|
||||
return this.lyricDecode(data);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 酷我音乐 URL 解码
|
||||
*/
|
||||
urlDecode(result) {
|
||||
const data = JSON.parse(result);
|
||||
|
||||
let url;
|
||||
if (data.code === 200 && data.data && data.data.url) {
|
||||
url = {
|
||||
url: data.data.url,
|
||||
br: 128
|
||||
};
|
||||
} else {
|
||||
url = {
|
||||
url: '',
|
||||
br: -1
|
||||
};
|
||||
}
|
||||
|
||||
return JSON.stringify(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* 酷我音乐歌词解码
|
||||
*/
|
||||
lyricDecode(result) {
|
||||
const data = JSON.parse(result);
|
||||
|
||||
let lyric = '';
|
||||
if (data.data && data.data.lrclist && data.data.lrclist.length > 0) {
|
||||
data.data.lrclist.forEach(item => {
|
||||
const time = parseFloat(item.time);
|
||||
const min = Math.floor(time / 60).toString().padStart(2, '0');
|
||||
const sec = Math.floor(time % 60).toString().padStart(2, '0');
|
||||
const msec = ((time % 1) * 100).toFixed(0).padStart(2, '0');
|
||||
|
||||
lyric += `[${min}:${sec}.${msec}]${item.lineLyric}\n`;
|
||||
});
|
||||
}
|
||||
|
||||
const lyricData = {
|
||||
lyric: lyric,
|
||||
tlyric: ''
|
||||
};
|
||||
|
||||
return JSON.stringify(lyricData);
|
||||
}
|
||||
}
|
||||
363
src/providers/netease.js
Normal file
363
src/providers/netease.js
Normal file
@@ -0,0 +1,363 @@
|
||||
import crypto from 'crypto';
|
||||
import BaseProvider from './base.js';
|
||||
|
||||
// eapi 相关常量
|
||||
const EAPI_KEY = 'e82ckenh8dichen8';
|
||||
const EAPI_IV = Buffer.from('0102030405060708');
|
||||
|
||||
/**
|
||||
* 网易云音乐平台提供者
|
||||
*/
|
||||
export default class NeteaseProvider extends BaseProvider {
|
||||
constructor(meting) {
|
||||
super(meting);
|
||||
this.name = 'netease';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取网易云音乐的请求头配置(EAPI)
|
||||
*/
|
||||
getHeaders() {
|
||||
const timestamp = Date.now().toString();
|
||||
const deviceId = this._generateDeviceId();
|
||||
|
||||
return {
|
||||
'Referer': 'music.163.com',
|
||||
'Cookie': `osver=android; appver=8.7.01; os=android; deviceId=${deviceId}; channel=netease; requestId=${timestamp}_${Math.floor(Math.random() * 1000).toString().padStart(4, '0')}; __remember_me=true`,
|
||||
'User-Agent': 'Mozilla/5.0 (Linux; Android 11; M2007J3SC Build/RKQ1.200826.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/77.0.3865.120 MQQBrowser/6.2 TBS/045714 Mobile Safari/537.36 NeteaseMusic/8.7.01',
|
||||
'Accept': '*/*',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7',
|
||||
'Connection': 'keep-alive',
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索歌曲
|
||||
*/
|
||||
search(keyword, option = {}) {
|
||||
return {
|
||||
method: 'POST',
|
||||
url: 'http://music.163.com/api/cloudsearch/pc',
|
||||
body: {
|
||||
s: keyword,
|
||||
type: option.type || 1,
|
||||
limit: option.limit || 30,
|
||||
total: 'true',
|
||||
offset: (option.page && option.limit) ? (option.page - 1) * option.limit : 0
|
||||
},
|
||||
encode: 'netease_eapi',
|
||||
format: 'result.songs'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取歌曲详情
|
||||
*/
|
||||
song(id) {
|
||||
return {
|
||||
method: 'POST',
|
||||
url: 'http://music.163.com/api/v3/song/detail/',
|
||||
body: {
|
||||
c: `[{"id":${id},"v":0}]`
|
||||
},
|
||||
encode: 'netease_eapi',
|
||||
format: 'songs'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取专辑信息
|
||||
*/
|
||||
album(id) {
|
||||
return {
|
||||
method: 'POST',
|
||||
url: `http://music.163.com/api/v1/album/${id}`,
|
||||
body: {
|
||||
total: 'true',
|
||||
offset: '0',
|
||||
id: id,
|
||||
limit: '1000',
|
||||
ext: 'true',
|
||||
private_cloud: 'true'
|
||||
},
|
||||
encode: 'netease_eapi',
|
||||
format: 'songs'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取艺术家作品
|
||||
*/
|
||||
artist(id, limit = 50) {
|
||||
return {
|
||||
method: 'POST',
|
||||
url: `http://music.163.com/api/v1/artist/${id}`,
|
||||
body: {
|
||||
ext: 'true',
|
||||
private_cloud: 'true',
|
||||
top: limit,
|
||||
id: id
|
||||
},
|
||||
encode: 'netease_eapi',
|
||||
format: 'hotSongs'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取播放列表
|
||||
*/
|
||||
playlist(id) {
|
||||
return {
|
||||
method: 'POST',
|
||||
url: 'http://music.163.com/api/v6/playlist/detail',
|
||||
body: {
|
||||
s: '0',
|
||||
id: id,
|
||||
n: '1000',
|
||||
t: '0'
|
||||
},
|
||||
encode: 'netease_eapi',
|
||||
format: 'playlist.tracks'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取音频播放链接
|
||||
*/
|
||||
url(id, br = 320) {
|
||||
return {
|
||||
method: 'POST',
|
||||
url: 'http://music.163.com/api/song/enhance/player/url',
|
||||
body: {
|
||||
ids: [id],
|
||||
br: br * 1000
|
||||
},
|
||||
encode: 'netease_eapi',
|
||||
decode: 'netease_url'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取歌词
|
||||
*/
|
||||
lyric(id) {
|
||||
return {
|
||||
method: 'POST',
|
||||
url: 'http://music.163.com/api/song/lyric',
|
||||
body: {
|
||||
id: id,
|
||||
os: 'linux',
|
||||
lv: -1,
|
||||
kv: -1,
|
||||
tv: -1
|
||||
},
|
||||
encode: 'netease_eapi',
|
||||
decode: 'netease_lyric'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取封面图片
|
||||
*/
|
||||
async pic(id, size = 300) {
|
||||
const url = `https://p3.music.126.net/${this._encryptId(id)}/${id}.jpg?param=${size}y${size}`;
|
||||
return JSON.stringify({ url: url });
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化网易云音乐数据
|
||||
*/
|
||||
format(data) {
|
||||
const result = {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
artist: [],
|
||||
album: data.al.name,
|
||||
pic_id: data.al.pic_str || data.al.pic,
|
||||
url_id: data.id,
|
||||
lyric_id: data.id,
|
||||
source: 'netease'
|
||||
};
|
||||
|
||||
if (data.al.picUrl) {
|
||||
const match = data.al.picUrl.match(/\/(\d+)\./);
|
||||
if (match) {
|
||||
result.pic_id = match[1];
|
||||
}
|
||||
}
|
||||
|
||||
data.ar.forEach(artist => {
|
||||
result.artist.push(artist.name);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理网易云音乐的编码逻辑
|
||||
*/
|
||||
async handleEncode(api) {
|
||||
if (api.encode === 'netease_eapi') {
|
||||
return this.eapiEncrypt(api);
|
||||
}
|
||||
return api;
|
||||
}
|
||||
|
||||
/**
|
||||
* 网易云音乐 EAPI 加密
|
||||
*/
|
||||
async eapiEncrypt(api) {
|
||||
const text = JSON.stringify(api.body);
|
||||
const url = api.url.replace(/https?:\/\/[^\/]+/, '');
|
||||
|
||||
// 构建 eapi 加密消息
|
||||
const message = `nobody${url}use${text}md5forencrypt`;
|
||||
const digest = crypto.createHash('md5').update(message).digest('hex');
|
||||
const data = `${url}-36cd479b6b5-${text}-36cd479b6b5-${digest}`;
|
||||
|
||||
// AES-128-ECB 加密
|
||||
const cipher = crypto.createCipheriv('aes-128-ecb', Buffer.from(EAPI_KEY, 'utf8'), null);
|
||||
cipher.setAutoPadding(true);
|
||||
let encrypted = cipher.update(data, 'utf8', 'hex');
|
||||
encrypted += cipher.final('hex');
|
||||
|
||||
// 转换 URL 路径
|
||||
api.url = api.url.replace('/api/', '/eapi/');
|
||||
|
||||
// 构建 eapi 请求体
|
||||
api.body = {
|
||||
params: encrypted.toUpperCase()
|
||||
};
|
||||
|
||||
return api;
|
||||
}
|
||||
|
||||
/**
|
||||
* 网易云音乐 URL 解码
|
||||
*/
|
||||
urlDecode(result) {
|
||||
const data = JSON.parse(result);
|
||||
let url;
|
||||
|
||||
if (data.data[0].uf && data.data[0].uf.url) {
|
||||
data.data[0].url = data.data[0].uf.url;
|
||||
}
|
||||
|
||||
if (data.data[0].url) {
|
||||
url = {
|
||||
url: data.data[0].url,
|
||||
size: data.data[0].size,
|
||||
br: data.data[0].br / 1000
|
||||
};
|
||||
} else {
|
||||
url = {
|
||||
url: '',
|
||||
size: 0,
|
||||
br: -1
|
||||
};
|
||||
}
|
||||
|
||||
return JSON.stringify(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* 网易云音乐歌词解码
|
||||
*/
|
||||
lyricDecode(result) {
|
||||
const data = JSON.parse(result);
|
||||
const lyricData = {
|
||||
lyric: (data.lrc && data.lrc.lyric) ? data.lrc.lyric : '',
|
||||
tlyric: (data.tlyric && data.tlyric.lyric) ? data.tlyric.lyric : ''
|
||||
};
|
||||
|
||||
return JSON.stringify(lyricData);
|
||||
}
|
||||
|
||||
// ========== 私有工具方法 ==========
|
||||
|
||||
/**
|
||||
* 生成随机 IP 地址
|
||||
*/
|
||||
_generateRandomIP() {
|
||||
const min = 1884815360; // 112.74.200.0
|
||||
const max = 1884890111; // 112.74.243.255
|
||||
const randomInt = Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
|
||||
return [
|
||||
(randomInt >>> 24) & 0xFF,
|
||||
(randomInt >>> 16) & 0xFF,
|
||||
(randomInt >>> 8) & 0xFF,
|
||||
randomInt & 0xFF
|
||||
].join('.');
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机十六进制字符串
|
||||
*/
|
||||
_getRandomHex(length) {
|
||||
return crypto.randomBytes(Math.ceil(length / 2))
|
||||
.toString('hex')
|
||||
.slice(0, length);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成设备 ID
|
||||
*/
|
||||
_generateDeviceId() {
|
||||
// 生成类似移动端的设备 ID
|
||||
const randomBytes = crypto.randomBytes(16);
|
||||
const deviceId = randomBytes.toString('hex').toUpperCase();
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 网易云音乐 ID 加密
|
||||
*/
|
||||
_encryptId(id) {
|
||||
const magic = '3go8&$8*3*3h0k(2)2'.split('');
|
||||
const song_id = String(id).split('');
|
||||
|
||||
for (let i = 0; i < song_id.length; i++) {
|
||||
song_id[i] = String.fromCharCode(
|
||||
song_id[i].charCodeAt(0) ^ magic[i % magic.length].charCodeAt(0)
|
||||
);
|
||||
}
|
||||
|
||||
const result = crypto.createHash('md5')
|
||||
.update(song_id.join(''), 'binary')
|
||||
.digest('base64')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/\+/g, '-');
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 大数运算相关工具方法
|
||||
*/
|
||||
_bchexdec(hex) {
|
||||
return BigInt('0x' + hex);
|
||||
}
|
||||
|
||||
_str2hex(str) {
|
||||
return Buffer.from(str, 'utf8').toString('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* 大数幂模运算
|
||||
*/
|
||||
_powMod(base, exponent, modulus) {
|
||||
if (modulus === 1n) return 0n;
|
||||
let result = 1n;
|
||||
base = base % modulus;
|
||||
while (exponent > 0n) {
|
||||
if (exponent % 2n === 1n) {
|
||||
result = (result * base) % modulus;
|
||||
}
|
||||
exponent = exponent >> 1n;
|
||||
base = (base * base) % modulus;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
293
src/providers/tencent.js
Normal file
293
src/providers/tencent.js
Normal file
@@ -0,0 +1,293 @@
|
||||
import BaseProvider from './base.js';
|
||||
|
||||
/**
|
||||
* 腾讯音乐平台提供者
|
||||
*/
|
||||
export default class TencentProvider extends BaseProvider {
|
||||
constructor(meting) {
|
||||
super(meting);
|
||||
this.name = 'tencent';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取腾讯音乐的请求头配置
|
||||
*/
|
||||
getHeaders() {
|
||||
return {
|
||||
'Referer': 'http://y.qq.com',
|
||||
'Cookie': 'pgv_pvi=22038528; pgv_si=s3156287488; pgv_pvid=5535248600; yplayer_open=1; ts_last=y.qq.com/portal/player.html; ts_uid=4847550686; yq_index=0; qqmusic_fromtag=66; player_exist=1',
|
||||
'User-Agent': 'QQ%E9%9F%B3%E4%B9%90/54409 CFNetwork/901.1 Darwin/17.6.0 (x86_64)',
|
||||
'Accept': '*/*',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.8,gl;q=0.6,zh-TW;q=0.4',
|
||||
'Connection': 'keep-alive',
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索歌曲
|
||||
*/
|
||||
search(keyword, option = {}) {
|
||||
return {
|
||||
method: 'GET',
|
||||
url: 'https://c.y.qq.com/soso/fcgi-bin/client_search_cp',
|
||||
body: {
|
||||
format: 'json',
|
||||
p: option.page || 1,
|
||||
n: option.limit || 30,
|
||||
w: keyword,
|
||||
aggr: 1,
|
||||
lossless: 1,
|
||||
cr: 1,
|
||||
new_json: 1
|
||||
},
|
||||
format: 'data.song.list'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取歌曲详情
|
||||
*/
|
||||
song(id) {
|
||||
return {
|
||||
method: 'GET',
|
||||
url: 'https://c.y.qq.com/v8/fcg-bin/fcg_play_single_song.fcg',
|
||||
body: {
|
||||
songmid: id,
|
||||
platform: 'yqq',
|
||||
format: 'json'
|
||||
},
|
||||
format: 'data'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取专辑信息
|
||||
*/
|
||||
album(id) {
|
||||
return {
|
||||
method: 'GET',
|
||||
url: 'https://c.y.qq.com/v8/fcg-bin/fcg_v8_album_detail_cp.fcg',
|
||||
body: {
|
||||
albummid: id,
|
||||
platform: 'mac',
|
||||
format: 'json',
|
||||
newsong: 1
|
||||
},
|
||||
format: 'data.getSongInfo'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取艺术家作品
|
||||
*/
|
||||
artist(id, limit = 50) {
|
||||
return {
|
||||
method: 'GET',
|
||||
url: 'https://c.y.qq.com/v8/fcg-bin/fcg_v8_singer_track_cp.fcg',
|
||||
body: {
|
||||
singermid: id,
|
||||
begin: 0,
|
||||
num: limit,
|
||||
order: 'listen',
|
||||
platform: 'mac',
|
||||
newsong: 1
|
||||
},
|
||||
format: 'data.list'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取播放列表
|
||||
*/
|
||||
playlist(id) {
|
||||
return {
|
||||
method: 'GET',
|
||||
url: 'https://c.y.qq.com/v8/fcg-bin/fcg_v8_playlist_cp.fcg',
|
||||
body: {
|
||||
id: id,
|
||||
format: 'json',
|
||||
newsong: 1,
|
||||
platform: 'jqspaframe.json'
|
||||
},
|
||||
format: 'data.cdlist.0.songlist'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取音频播放链接
|
||||
*/
|
||||
url(id, br = 320) {
|
||||
return {
|
||||
method: 'GET',
|
||||
url: 'https://c.y.qq.com/v8/fcg-bin/fcg_play_single_song.fcg',
|
||||
body: {
|
||||
songmid: id,
|
||||
platform: 'yqq',
|
||||
format: 'json'
|
||||
},
|
||||
decode: 'tencent_url'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取歌词
|
||||
*/
|
||||
lyric(id) {
|
||||
return {
|
||||
method: 'GET',
|
||||
url: 'https://c.y.qq.com/lyric/fcgi-bin/fcg_query_lyric_new.fcg',
|
||||
body: {
|
||||
songmid: id,
|
||||
g_tk: '5381'
|
||||
},
|
||||
decode: 'tencent_lyric'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取封面图片
|
||||
*/
|
||||
async pic(id, size = 300) {
|
||||
const url = `https://y.gtimg.cn/music/photo_new/T002R${size}x${size}M000${id}.jpg?max_age=2592000`;
|
||||
return JSON.stringify({ url: url });
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化腾讯音乐数据
|
||||
*/
|
||||
format(data) {
|
||||
if (data.musicData) {
|
||||
data = data.musicData;
|
||||
}
|
||||
|
||||
const result = {
|
||||
id: data.mid,
|
||||
name: data.name,
|
||||
artist: [],
|
||||
album: data.album.title.trim(),
|
||||
pic_id: data.album.mid,
|
||||
url_id: data.mid,
|
||||
lyric_id: data.mid,
|
||||
source: 'tencent'
|
||||
};
|
||||
|
||||
data.singer.forEach(singer => {
|
||||
result.artist.push(singer.name);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理腾讯音乐的解码逻辑
|
||||
*/
|
||||
async handleDecode(decodeType, data) {
|
||||
if (decodeType === 'tencent_url') {
|
||||
return this.urlDecode(data);
|
||||
} else if (decodeType === 'tencent_lyric') {
|
||||
return this.lyricDecode(data);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 腾讯音乐 URL 解码
|
||||
*/
|
||||
async urlDecode(result) {
|
||||
const data = JSON.parse(result);
|
||||
const guid = Math.floor(Math.random() * 10000000000);
|
||||
|
||||
const qualityMap = [
|
||||
['size_flac', 999, 'F000', 'flac'],
|
||||
['size_320mp3', 320, 'M800', 'mp3'],
|
||||
['size_192aac', 192, 'C600', 'm4a'],
|
||||
['size_128mp3', 128, 'M500', 'mp3'],
|
||||
['size_96aac', 96, 'C400', 'm4a'],
|
||||
['size_48aac', 48, 'C200', 'm4a'],
|
||||
['size_24aac', 24, 'C100', 'm4a']
|
||||
];
|
||||
|
||||
let uin = '0';
|
||||
const uinMatch = this.meting.header.Cookie && this.meting.header.Cookie.match(/uin=(\d+)/);
|
||||
if (uinMatch) {
|
||||
uin = uinMatch[1];
|
||||
}
|
||||
|
||||
const payload = {
|
||||
req_0: {
|
||||
module: 'vkey.GetVkeyServer',
|
||||
method: 'CgiGetVkey',
|
||||
param: {
|
||||
guid: String(guid),
|
||||
songmid: [],
|
||||
filename: [],
|
||||
songtype: [],
|
||||
uin: uin,
|
||||
loginflag: 1,
|
||||
platform: '20'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
qualityMap.forEach(([sizeKey, br, prefix, ext]) => {
|
||||
payload.req_0.param.songmid.push(data.data[0].mid);
|
||||
payload.req_0.param.filename.push(`${prefix}${data.data[0].file.media_mid}.${ext}`);
|
||||
payload.req_0.param.songtype.push(data.data[0].type);
|
||||
});
|
||||
|
||||
const api = {
|
||||
method: 'GET',
|
||||
url: 'https://u.y.qq.com/cgi-bin/musicu.fcg',
|
||||
body: {
|
||||
format: 'json',
|
||||
platform: 'yqq.json',
|
||||
needNewCode: 0,
|
||||
data: JSON.stringify(payload)
|
||||
}
|
||||
};
|
||||
|
||||
const response = JSON.parse(await this.meting._exec(api));
|
||||
const vkeys = response.req_0.data.midurlinfo;
|
||||
|
||||
let url;
|
||||
for (let i = 0; i < qualityMap.length; i++) {
|
||||
const [sizeKey, br, prefix, ext] = qualityMap[i];
|
||||
if (data.data[0].file[sizeKey] && br <= this.meting.temp.br) {
|
||||
if (vkeys[i].vkey) {
|
||||
url = {
|
||||
url: response.req_0.data.sip[0] + vkeys[i].purl,
|
||||
size: data.data[0].file[sizeKey],
|
||||
br: br
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!url) {
|
||||
url = {
|
||||
url: '',
|
||||
size: 0,
|
||||
br: -1
|
||||
};
|
||||
}
|
||||
|
||||
return JSON.stringify(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* 腾讯音乐歌词解码
|
||||
*/
|
||||
lyricDecode(result) {
|
||||
const jsonStr = result.substring(18, result.length - 1);
|
||||
const data = JSON.parse(jsonStr);
|
||||
|
||||
const lyricData = {
|
||||
lyric: data.lyric ? Buffer.from(data.lyric, 'base64').toString() : '',
|
||||
tlyric: data.trans ? Buffer.from(data.trans, 'base64').toString() : ''
|
||||
};
|
||||
|
||||
return JSON.stringify(lyricData);
|
||||
}
|
||||
}
|
||||
347
src/providers/xiami.js
Normal file
347
src/providers/xiami.js
Normal file
@@ -0,0 +1,347 @@
|
||||
import crypto from 'crypto';
|
||||
import BaseProvider from './base.js';
|
||||
|
||||
/**
|
||||
* 虾米音乐平台提供者
|
||||
*/
|
||||
export default class XiamiProvider extends BaseProvider {
|
||||
constructor(meting) {
|
||||
super(meting);
|
||||
this.name = 'xiami';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取虾米音乐的请求头配置
|
||||
*/
|
||||
getHeaders() {
|
||||
return {
|
||||
'Cookie': '_m_h5_tk=15d3402511a022796d88b249f83fb968_1511163656929; _m_h5_tk_enc=b6b3e64d81dae577fc314b5c5692df3c',
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/537.36 (KHTML, like Gecko) XIAMI-MUSIC/3.1.1 Chrome/56.0.2924.87 Electron/1.6.11 Safari/537.36',
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Accept-Language': 'zh-CN'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索歌曲
|
||||
*/
|
||||
search(keyword, option = {}) {
|
||||
return {
|
||||
method: 'GET',
|
||||
url: 'https://acs.m.xiami.com/h5/mtop.alimusic.search.searchservice.searchsongs/1.0/',
|
||||
body: {
|
||||
data: {
|
||||
key: keyword,
|
||||
pagingVO: {
|
||||
page: option.page || 1,
|
||||
pageSize: option.limit || 30
|
||||
}
|
||||
},
|
||||
r: 'mtop.alimusic.search.searchservice.searchsongs'
|
||||
},
|
||||
encode: 'xiami_sign',
|
||||
format: 'data.data.songs'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取歌曲详情
|
||||
*/
|
||||
song(id) {
|
||||
return {
|
||||
method: 'GET',
|
||||
url: 'https://acs.m.xiami.com/h5/mtop.alimusic.music.songservice.getsongdetail/1.0/',
|
||||
body: {
|
||||
data: {
|
||||
songId: id
|
||||
},
|
||||
r: 'mtop.alimusic.music.songservice.getsongdetail'
|
||||
},
|
||||
encode: 'xiami_sign',
|
||||
format: 'data.data.songDetail'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取专辑信息
|
||||
*/
|
||||
album(id) {
|
||||
return {
|
||||
method: 'GET',
|
||||
url: 'https://acs.m.xiami.com/h5/mtop.alimusic.music.albumservice.getalbumdetail/1.0/',
|
||||
body: {
|
||||
data: {
|
||||
albumId: id
|
||||
},
|
||||
r: 'mtop.alimusic.music.albumservice.getalbumdetail'
|
||||
},
|
||||
encode: 'xiami_sign',
|
||||
format: 'data.data.albumDetail.songs'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取艺术家作品
|
||||
*/
|
||||
artist(id, limit = 50) {
|
||||
return {
|
||||
method: 'GET',
|
||||
url: 'https://acs.m.xiami.com/h5/mtop.alimusic.music.songservice.getartistsongs/1.0/',
|
||||
body: {
|
||||
data: {
|
||||
artistId: id,
|
||||
pagingVO: {
|
||||
page: 1,
|
||||
pageSize: limit
|
||||
}
|
||||
},
|
||||
r: 'mtop.alimusic.music.songservice.getartistsongs'
|
||||
},
|
||||
encode: 'xiami_sign',
|
||||
format: 'data.data.songs'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取播放列表
|
||||
*/
|
||||
playlist(id) {
|
||||
return {
|
||||
method: 'GET',
|
||||
url: 'https://acs.m.xiami.com/h5/mtop.alimusic.music.list.collectservice.getcollectdetail/1.0/',
|
||||
body: {
|
||||
data: {
|
||||
listId: id,
|
||||
isFullTags: false,
|
||||
pagingVO: {
|
||||
page: 1,
|
||||
pageSize: 1000
|
||||
}
|
||||
},
|
||||
r: 'mtop.alimusic.music.list.collectservice.getcollectdetail'
|
||||
},
|
||||
encode: 'xiami_sign',
|
||||
format: 'data.data.collectDetail.songs'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取音频播放链接
|
||||
*/
|
||||
url(id, br = 320) {
|
||||
return {
|
||||
method: 'GET',
|
||||
url: 'https://acs.m.xiami.com/h5/mtop.alimusic.music.songservice.getsongs/1.0/',
|
||||
body: {
|
||||
data: {
|
||||
songIds: [id]
|
||||
},
|
||||
r: 'mtop.alimusic.music.songservice.getsongs'
|
||||
},
|
||||
encode: 'xiami_sign',
|
||||
decode: 'xiami_url'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取歌词
|
||||
*/
|
||||
lyric(id) {
|
||||
return {
|
||||
method: 'GET',
|
||||
url: 'https://acs.m.xiami.com/h5/mtop.alimusic.music.lyricservice.getsonglyrics/1.0/',
|
||||
body: {
|
||||
data: {
|
||||
songId: id
|
||||
},
|
||||
r: 'mtop.alimusic.music.lyricservice.getsonglyrics'
|
||||
},
|
||||
encode: 'xiami_sign',
|
||||
decode: 'xiami_lyric'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取封面图片
|
||||
*/
|
||||
async pic(id, size = 300) {
|
||||
const format = this.meting.isFormat;
|
||||
const data = await this.meting.format(false).song(id);
|
||||
this.meting.isFormat = format;
|
||||
const songData = JSON.parse(data);
|
||||
let url = songData.data.data.songDetail.albumLogo;
|
||||
url = url.replace('http:', 'https:') + `@1e_1c_100Q_${size}h_${size}w`;
|
||||
return JSON.stringify({ url: url });
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化虾米音乐数据
|
||||
*/
|
||||
format(data) {
|
||||
const result = {
|
||||
id: data.songId,
|
||||
name: data.songName,
|
||||
artist: [],
|
||||
album: data.albumName,
|
||||
pic_id: data.songId,
|
||||
url_id: data.songId,
|
||||
lyric_id: data.songId,
|
||||
source: 'xiami'
|
||||
};
|
||||
|
||||
data.singerVOs.forEach(singer => {
|
||||
result.artist.push(singer.artistName);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理虾米音乐的编码/解码逻辑
|
||||
*/
|
||||
async handleEncode(api) {
|
||||
if (api.encode === 'xiami_sign') {
|
||||
return this.signEncrypt(api);
|
||||
}
|
||||
return api;
|
||||
}
|
||||
|
||||
async handleDecode(decodeType, data) {
|
||||
if (decodeType === 'xiami_url') {
|
||||
return this.urlDecode(data);
|
||||
} else if (decodeType === 'xiami_lyric') {
|
||||
return this.lyricDecode(data);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 虾米音乐签名加密
|
||||
*/
|
||||
async signEncrypt(api) {
|
||||
// 获取 token
|
||||
const tokenUrl = 'https://acs.m.xiami.com/h5/mtop.alimusic.recommend.songservice.getdailysongs/1.0/?appKey=12574478&t=1560663823000&dataType=json&data=%7B%22requestStr%22%3A%22%7B%5C%22header%5C%22%3A%7B%5C%22platformId%5C%22%3A%5C%22mac%5C%22%7D%2C%5C%22model%5C%22%3A%5B%5D%7D%22%7D&api=mtop.alimusic.recommend.songservice.getdailysongs&v=1.0&type=originaljson&sign=22ad1377ee193f3e2772c17c6192b17c';
|
||||
|
||||
await this.meting._curl(tokenUrl, null, true);
|
||||
const cookieMatch = this.meting.raw.match(/_m_h5[^;]+/g);
|
||||
if (cookieMatch && cookieMatch.length >= 2) {
|
||||
this.meting.header['Cookie'] = cookieMatch[0] + '; ' + cookieMatch[1];
|
||||
}
|
||||
|
||||
const data = JSON.stringify({
|
||||
requestStr: JSON.stringify({
|
||||
header: {
|
||||
platformId: 'mac'
|
||||
},
|
||||
model: api.body.data
|
||||
})
|
||||
});
|
||||
|
||||
const appkey = '12574478';
|
||||
const cookie = this.meting.header['Cookie'];
|
||||
const tokenMatch = cookie.match(/_m_h5_tk=([^_]+)/);
|
||||
const token = tokenMatch ? tokenMatch[1] : '';
|
||||
const t = Date.now();
|
||||
|
||||
const signStr = `${token}&${t}&${appkey}&${data}`;
|
||||
const sign = crypto.createHash('md5').update(signStr).digest('hex');
|
||||
|
||||
api.body = {
|
||||
appKey: appkey,
|
||||
t: t,
|
||||
dataType: 'json',
|
||||
data: data,
|
||||
api: api.body.r,
|
||||
v: '1.0',
|
||||
type: 'originaljson',
|
||||
sign: sign
|
||||
};
|
||||
|
||||
return api;
|
||||
}
|
||||
|
||||
/**
|
||||
* 虾米音乐 URL 解码
|
||||
*/
|
||||
urlDecode(result) {
|
||||
const data = JSON.parse(result);
|
||||
|
||||
const qualityMap = {
|
||||
's': 740,
|
||||
'h': 320,
|
||||
'l': 128,
|
||||
'f': 64,
|
||||
'e': 32
|
||||
};
|
||||
|
||||
let maxBr = 0;
|
||||
let url;
|
||||
|
||||
data.data.data.songs[0].listenFiles.forEach(file => {
|
||||
const br = qualityMap[file.quality];
|
||||
if (br <= this.meting.temp.br && br > maxBr) {
|
||||
maxBr = br;
|
||||
url = {
|
||||
url: file.listenFile,
|
||||
size: file.fileSize,
|
||||
br: br
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
if (!url) {
|
||||
url = {
|
||||
url: '',
|
||||
size: 0,
|
||||
br: -1
|
||||
};
|
||||
}
|
||||
|
||||
return JSON.stringify(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* 虾米音乐歌词解码
|
||||
*/
|
||||
lyricDecode(result) {
|
||||
const data = JSON.parse(result);
|
||||
|
||||
let lyricData;
|
||||
if (data.data.data.lyrics.length > 0) {
|
||||
let content = data.data.data.lyrics[0].content;
|
||||
content = content.replace(/<[^>]+>/g, '');
|
||||
|
||||
const matches = content.match(/\[([\d:\.]+)\](.*)\s\[x-trans\](.*)/gi);
|
||||
if (matches) {
|
||||
const lyricLines = [];
|
||||
const tlyricLines = [];
|
||||
|
||||
matches.forEach(match => {
|
||||
const parts = match.match(/\[([\d:\.]+)\](.*)\s\[x-trans\](.*)/i);
|
||||
if (parts) {
|
||||
lyricLines.push(`[${parts[1]}]${parts[2]}`);
|
||||
tlyricLines.push(`[${parts[1]}]${parts[3]}`);
|
||||
}
|
||||
});
|
||||
|
||||
lyricData = {
|
||||
lyric: content.replace(/\[([\d:\.]+)\](.*)\s\[x-trans\](.*)/gi, (match, time, lyric) => `[${time}]${lyric}`),
|
||||
tlyric: content.replace(/\[([\d:\.]+)\](.*)\s\[x-trans\](.*)/gi, (match, time, lyric, trans) => `[${time}]${trans}`)
|
||||
};
|
||||
} else {
|
||||
lyricData = {
|
||||
lyric: content,
|
||||
tlyric: ''
|
||||
};
|
||||
}
|
||||
} else {
|
||||
lyricData = {
|
||||
lyric: '',
|
||||
tlyric: ''
|
||||
};
|
||||
}
|
||||
|
||||
return JSON.stringify(lyricData);
|
||||
}
|
||||
}
|
||||
85
test/example.js
Normal file
85
test/example.js
Normal file
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Meting Node.js 使用示例
|
||||
*/
|
||||
|
||||
import Meting from '../lib/meting.esm.js';
|
||||
|
||||
async function main() {
|
||||
// 创建 Meting 实例
|
||||
const meting = new Meting('netease'); // 可选: 'netease', 'tencent', 'xiami', 'kugou', 'baidu', 'kuwo'
|
||||
|
||||
// 开启数据格式化
|
||||
meting.format(true);
|
||||
|
||||
console.log('=== Meting Node.js 示例 ===\n');
|
||||
|
||||
try {
|
||||
// 1. 搜索歌曲
|
||||
console.log('1. 搜索歌曲:');
|
||||
const searchResult = await meting.search('烟火里的尘埃', { limit: 3 });
|
||||
console.log('搜索结果:');
|
||||
console.log(JSON.stringify(JSON.parse(searchResult), null, 2));
|
||||
console.log('\n');
|
||||
|
||||
// 获取第一首歌的 ID
|
||||
const songs = JSON.parse(searchResult);
|
||||
if (songs.length > 0) {
|
||||
const firstSong = songs[0];
|
||||
console.log(`选择歌曲: ${firstSong.name} - ${firstSong.artist.join(', ')}\n`);
|
||||
|
||||
// 2. 获取歌曲详情
|
||||
console.log('2. 获取歌曲详情:');
|
||||
const songDetail = await meting.song(firstSong.id);
|
||||
console.log('歌曲详情:');
|
||||
console.log(JSON.stringify(JSON.parse(songDetail), null, 2));
|
||||
console.log('\n');
|
||||
|
||||
// 3. 获取歌曲播放链接
|
||||
console.log('3. 获取歌曲播放链接:');
|
||||
const url = await meting.url(firstSong.url_id, 320);
|
||||
console.log('播放链接:');
|
||||
console.log(JSON.stringify(JSON.parse(url), null, 2));
|
||||
console.log('\n');
|
||||
|
||||
// 4. 获取歌词
|
||||
console.log('4. 获取歌词:');
|
||||
const lyric = await meting.lyric(firstSong.lyric_id);
|
||||
const lyricData = JSON.parse(lyric);
|
||||
console.log('歌词预览(前5行):');
|
||||
if (lyricData.lyric) {
|
||||
const lines = lyricData.lyric.split('\n').slice(0, 5);
|
||||
lines.forEach(line => {
|
||||
if (line.trim()) console.log(line);
|
||||
});
|
||||
} else {
|
||||
console.log('暂无歌词');
|
||||
}
|
||||
console.log('\n');
|
||||
|
||||
// 5. 获取封面图片
|
||||
console.log('5. 获取封面图片:');
|
||||
const pic = await meting.pic(firstSong.pic_id, 300);
|
||||
console.log('封面图片:');
|
||||
console.log(JSON.stringify(JSON.parse(pic), null, 2));
|
||||
console.log('\n');
|
||||
}
|
||||
|
||||
// 6. 切换到其他平台测试
|
||||
console.log('6. 切换到腾讯音乐平台:');
|
||||
meting.site('tencent');
|
||||
const tencentSearch = await meting.search('邓紫棋', { limit: 2 });
|
||||
console.log('腾讯音乐搜索结果:');
|
||||
console.log(JSON.stringify(JSON.parse(tencentSearch), null, 2));
|
||||
console.log('\n');
|
||||
|
||||
} catch (error) {
|
||||
console.error('发生错误:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 运行示例
|
||||
main().then(() => {
|
||||
console.log('示例运行完成!');
|
||||
}).catch(error => {
|
||||
console.error('示例运行失败:', error);
|
||||
});
|
||||
97
test/test.js
Normal file
97
test/test.js
Normal file
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Meting Node.js 基础测试
|
||||
*/
|
||||
|
||||
import Meting from '../src/meting.js';
|
||||
|
||||
async function runTests() {
|
||||
console.log('=== Meting Node.js 基础测试 ===\n');
|
||||
|
||||
const platforms = ['netease', 'tencent', 'kugou', 'baidu', 'kuwo'];
|
||||
const testKeyword = '周杰伦';
|
||||
|
||||
for (const platform of platforms) {
|
||||
console.log(`\n--- 测试平台: ${platform} ---`);
|
||||
|
||||
try {
|
||||
const meting = new Meting(platform);
|
||||
meting.format(true);
|
||||
|
||||
// 测试搜索功能
|
||||
console.log('测试搜索功能...');
|
||||
const searchResult = await meting.search(testKeyword, { limit: 1 });
|
||||
const songs = JSON.parse(searchResult);
|
||||
|
||||
if (songs.length > 0) {
|
||||
const song = songs[0];
|
||||
console.log(`✓ 搜索成功: ${song.name} - ${song.artist.join(', ')}`);
|
||||
|
||||
// 测试歌曲详情
|
||||
console.log('测试获取歌曲详情...');
|
||||
const songDetail = await meting.song(song.id);
|
||||
const songData = JSON.parse(songDetail);
|
||||
if (songData.length > 0) {
|
||||
console.log(`✓ 获取歌曲详情成功: ${songData[0].name}`);
|
||||
} else {
|
||||
console.log('✗ 获取歌曲详情失败');
|
||||
}
|
||||
|
||||
// 测试获取播放链接
|
||||
console.log('测试获取播放链接...');
|
||||
try {
|
||||
const url = await meting.url(song.url_id, 128);
|
||||
const urlData = JSON.parse(url);
|
||||
if (urlData.url) {
|
||||
console.log('✓ 获取播放链接成功');
|
||||
} else {
|
||||
console.log('✗ 获取播放链接失败(可能需要会员或已下架)');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('✗ 获取播放链接出错:', error.message);
|
||||
}
|
||||
|
||||
// 测试获取歌词
|
||||
console.log('测试获取歌词...');
|
||||
try {
|
||||
const lyric = await meting.lyric(song.lyric_id);
|
||||
const lyricData = JSON.parse(lyric);
|
||||
if (lyricData.lyric) {
|
||||
console.log('✓ 获取歌词成功');
|
||||
} else {
|
||||
console.log('✗ 获取歌词失败(可能无歌词)');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('✗ 获取歌词出错:', error.message);
|
||||
}
|
||||
|
||||
// 测试获取封面
|
||||
console.log('测试获取封面图片...');
|
||||
try {
|
||||
const pic = await meting.pic(song.pic_id, 200);
|
||||
const picData = JSON.parse(pic);
|
||||
if (picData.url) {
|
||||
console.log('✓ 获取封面图片成功');
|
||||
} else {
|
||||
console.log('✗ 获取封面图片失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('✗ 获取封面图片出错:', error.message);
|
||||
}
|
||||
|
||||
} else {
|
||||
console.log('✗ 搜索失败或无结果');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.log(`✗ 平台 ${platform} 测试失败:`, error.message);
|
||||
}
|
||||
|
||||
// 添加延迟避免请求过快
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
}
|
||||
|
||||
console.log('\n=== 测试完成 ===');
|
||||
}
|
||||
|
||||
// 执行测试
|
||||
runTests().catch(console.error);
|
||||
Reference in New Issue
Block a user