diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 341b31d..0000000 --- a/.editorconfig +++ /dev/null @@ -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 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index ea545ae..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -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 \ No newline at end of file diff --git a/.gitignore b/.gitignore index ff7f293..c18ed01 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ -vendor/ -composer.lock -.idea/ +node_modules/ +lib/ \ No newline at end of file diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..ae3733c --- /dev/null +++ b/ARCHITECTURE.md @@ -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 测试 + +重构成功保持了所有原有功能,同时大大提升了代码的可维护性和扩展性,并优化了运行时性能。 \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..1d785df --- /dev/null +++ b/CLAUDE.md @@ -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 diff --git a/README.md b/README.md index 0c4fbb1..e80be8b 100644 --- a/README.md +++ b/README.md @@ -2,95 +2,285 @@ Meting

-

-Author -Version -Downloads -Travis -License -

- - > :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.
+**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) + +--- + +

+Made with ❤️ for the music community +

diff --git a/composer.json b/composer.json deleted file mode 100644 index 0b54c4e..0000000 --- a/composer.json +++ /dev/null @@ -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/" - } - } -} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..6a5f8c2 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1173 @@ +{ + "name": "@meting/core", + "version": "1.5.12", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@meting/core", + "version": "1.5.12", + "license": "MIT", + "devDependencies": { + "@rollup/plugin-babel": "^6.0.4", + "@rollup/plugin-node-resolve": "^16.0.1", + "@rollup/plugin-terser": "^0.4.4", + "rollup": "^4.49.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://mirrors.tencent.com/npm/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "peer": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://mirrors.tencent.com/npm/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.0", + "resolved": "https://mirrors.tencent.com/npm/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.3", + "resolved": "https://mirrors.tencent.com/npm/@babel/core/-/core-7.28.3.tgz", + "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", + "dev": true, + "peer": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.3", + "@babel/parser": "^7.28.3", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.3", + "@babel/types": "^7.28.2", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.3", + "resolved": "https://mirrors.tencent.com/npm/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://mirrors.tencent.com/npm/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://mirrors.tencent.com/npm/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://mirrors.tencent.com/npm/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://mirrors.tencent.com/npm/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://mirrors.tencent.com/npm/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://mirrors.tencent.com/npm/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://mirrors.tencent.com/npm/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.3", + "resolved": "https://mirrors.tencent.com/npm/@babel/helpers/-/helpers-7.28.3.tgz", + "integrity": "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.3", + "resolved": "https://mirrors.tencent.com/npm/@babel/parser/-/parser-7.28.3.tgz", + "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.28.2" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://mirrors.tencent.com/npm/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.3", + "resolved": "https://mirrors.tencent.com/npm/@babel/traverse/-/traverse-7.28.3.tgz", + "integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.3", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.2", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.2", + "resolved": "https://mirrors.tencent.com/npm/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://mirrors.tencent.com/npm/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://mirrors.tencent.com/npm/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://mirrors.tencent.com/npm/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://mirrors.tencent.com/npm/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.30", + "resolved": "https://mirrors.tencent.com/npm/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", + "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rollup/plugin-babel": { + "version": "6.0.4", + "resolved": "https://mirrors.tencent.com/npm/@rollup/plugin-babel/-/plugin-babel-6.0.4.tgz", + "integrity": "sha512-YF7Y52kFdFT/xVSuVdjkV5ZdX/3YtmX0QulG+x0taQOtJdHYzVU61aSSkAgVJ7NOv6qPkIYiJSgSWWN/DM5sGw==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.18.6", + "@rollup/pluginutils": "^5.0.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "@types/babel__core": "^7.1.9", + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "@types/babel__core": { + "optional": true + }, + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "16.0.1", + "resolved": "https://mirrors.tencent.com/npm/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.1.tgz", + "integrity": "sha512-tk5YCxJWIG81umIvNkSod2qK5KyQW19qcBF/B78n1bjtOON6gzKoVeSzAE8yHCZEDmqkHKkxplExA8KzdJLJpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-terser": { + "version": "0.4.4", + "resolved": "https://mirrors.tencent.com/npm/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", + "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "serialize-javascript": "^6.0.1", + "smob": "^1.0.0", + "terser": "^5.17.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.2.0", + "resolved": "https://mirrors.tencent.com/npm/@rollup/pluginutils/-/pluginutils-5.2.0.tgz", + "integrity": "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.49.0", + "resolved": "https://mirrors.tencent.com/npm/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.49.0.tgz", + "integrity": "sha512-rlKIeL854Ed0e09QGYFlmDNbka6I3EQFw7iZuugQjMb11KMpJCLPFL4ZPbMfaEhLADEL1yx0oujGkBQ7+qW3eA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.49.0", + "resolved": "https://mirrors.tencent.com/npm/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.49.0.tgz", + "integrity": "sha512-cqPpZdKUSQYRtLLr6R4X3sD4jCBO1zUmeo3qrWBCqYIeH8Q3KRL4F3V7XJ2Rm8/RJOQBZuqzQGWPjjvFUcYa/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.49.0", + "resolved": "https://mirrors.tencent.com/npm/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.49.0.tgz", + "integrity": "sha512-99kMMSMQT7got6iYX3yyIiJfFndpojBmkHfTc1rIje8VbjhmqBXE+nb7ZZP3A5skLyujvT0eIUCUsxAe6NjWbw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.49.0", + "resolved": "https://mirrors.tencent.com/npm/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.49.0.tgz", + "integrity": "sha512-y8cXoD3wdWUDpjOLMKLx6l+NFz3NlkWKcBCBfttUn+VGSfgsQ5o/yDUGtzE9HvsodkP0+16N0P4Ty1VuhtRUGg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.49.0", + "resolved": "https://mirrors.tencent.com/npm/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.49.0.tgz", + "integrity": "sha512-3mY5Pr7qv4GS4ZvWoSP8zha8YoiqrU+e0ViPvB549jvliBbdNLrg2ywPGkgLC3cmvN8ya3za+Q2xVyT6z+vZqA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.49.0", + "resolved": "https://mirrors.tencent.com/npm/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.49.0.tgz", + "integrity": "sha512-C9KzzOAQU5gU4kG8DTk+tjdKjpWhVWd5uVkinCwwFub2m7cDYLOdtXoMrExfeBmeRy9kBQMkiyJ+HULyF1yj9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.49.0", + "resolved": "https://mirrors.tencent.com/npm/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.49.0.tgz", + "integrity": "sha512-OVSQgEZDVLnTbMq5NBs6xkmz3AADByCWI4RdKSFNlDsYXdFtlxS59J+w+LippJe8KcmeSSM3ba+GlsM9+WwC1w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.49.0", + "resolved": "https://mirrors.tencent.com/npm/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.49.0.tgz", + "integrity": "sha512-ZnfSFA7fDUHNa4P3VwAcfaBLakCbYaxCk0jUnS3dTou9P95kwoOLAMlT3WmEJDBCSrOEFFV0Y1HXiwfLYJuLlA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.49.0", + "resolved": "https://mirrors.tencent.com/npm/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.49.0.tgz", + "integrity": "sha512-Z81u+gfrobVK2iV7GqZCBfEB1y6+I61AH466lNK+xy1jfqFLiQ9Qv716WUM5fxFrYxwC7ziVdZRU9qvGHkYIJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.49.0", + "resolved": "https://mirrors.tencent.com/npm/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.49.0.tgz", + "integrity": "sha512-zoAwS0KCXSnTp9NH/h9aamBAIve0DXeYpll85shf9NJ0URjSTzzS+Z9evmolN+ICfD3v8skKUPyk2PO0uGdFqg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.49.0", + "resolved": "https://mirrors.tencent.com/npm/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.49.0.tgz", + "integrity": "sha512-2QyUyQQ1ZtwZGiq0nvODL+vLJBtciItC3/5cYN8ncDQcv5avrt2MbKt1XU/vFAJlLta5KujqyHdYtdag4YEjYQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.49.0", + "resolved": "https://mirrors.tencent.com/npm/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.49.0.tgz", + "integrity": "sha512-k9aEmOWt+mrMuD3skjVJSSxHckJp+SiFzFG+v8JLXbc/xi9hv2icSkR3U7uQzqy+/QbbYY7iNB9eDTwrELo14g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.49.0", + "resolved": "https://mirrors.tencent.com/npm/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.49.0.tgz", + "integrity": "sha512-rDKRFFIWJ/zJn6uk2IdYLc09Z7zkE5IFIOWqpuU0o6ZpHcdniAyWkwSUWE/Z25N/wNDmFHHMzin84qW7Wzkjsw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.49.0", + "resolved": "https://mirrors.tencent.com/npm/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.49.0.tgz", + "integrity": "sha512-FkkhIY/hYFVnOzz1WeV3S9Bd1h0hda/gRqvZCMpHWDHdiIHn6pqsY3b5eSbvGccWHMQ1uUzgZTKS4oGpykf8Tw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.49.0", + "resolved": "https://mirrors.tencent.com/npm/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.49.0.tgz", + "integrity": "sha512-gRf5c+A7QiOG3UwLyOOtyJMD31JJhMjBvpfhAitPAoqZFcOeK3Kc1Veg1z/trmt+2P6F/biT02fU19GGTS529A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.49.0", + "resolved": "https://mirrors.tencent.com/npm/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.49.0.tgz", + "integrity": "sha512-BR7+blScdLW1h/2hB/2oXM+dhTmpW3rQt1DeSiCP9mc2NMMkqVgjIN3DDsNpKmezffGC9R8XKVOLmBkRUcK/sA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.49.0", + "resolved": "https://mirrors.tencent.com/npm/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.49.0.tgz", + "integrity": "sha512-hDMOAe+6nX3V5ei1I7Au3wcr9h3ktKzDvF2ne5ovX8RZiAHEtX1A5SNNk4zt1Qt77CmnbqT+upb/umzoPMWiPg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.49.0", + "resolved": "https://mirrors.tencent.com/npm/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.49.0.tgz", + "integrity": "sha512-wkNRzfiIGaElC9kXUT+HLx17z7D0jl+9tGYRKwd8r7cUqTL7GYAvgUY++U2hK6Ar7z5Z6IRRoWC8kQxpmM7TDA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.49.0", + "resolved": "https://mirrors.tencent.com/npm/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.49.0.tgz", + "integrity": "sha512-gq5aW/SyNpjp71AAzroH37DtINDcX1Qw2iv9Chyz49ZgdOP3NV8QCyKZUrGsYX9Yyggj5soFiRCgsL3HwD8TdA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.49.0", + "resolved": "https://mirrors.tencent.com/npm/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.49.0.tgz", + "integrity": "sha512-gEtqFbzmZLFk2xKh7g0Rlo8xzho8KrEFEkzvHbfUGkrgXOpZ4XagQ6n+wIZFNh1nTb8UD16J4nFSFKXYgnbdBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://mirrors.tencent.com/npm/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true + }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://mirrors.tencent.com/npm/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://mirrors.tencent.com/npm/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/browserslist": { + "version": "4.25.3", + "resolved": "https://mirrors.tencent.com/npm/browserslist/-/browserslist-4.25.3.tgz", + "integrity": "sha512-cDGv1kkDI4/0e5yON9yM5G/0A5u8sf5TnmdX5C9qHzI9PPu++sQ9zjm1k9NiOrf3riY4OkK0zSGqfvJyJsgCBQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "peer": true, + "dependencies": { + "caniuse-lite": "^1.0.30001735", + "electron-to-chromium": "^1.5.204", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://mirrors.tencent.com/npm/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001737", + "resolved": "https://mirrors.tencent.com/npm/caniuse-lite/-/caniuse-lite-1.0.30001737.tgz", + "integrity": "sha512-BiloLiXtQNrY5UyF0+1nSJLXUENuhka2pzy2Fx5pGxqavdrxSCW4U6Pn/PoG3Efspi2frRbHpBV2XsrPE6EDlw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "peer": true + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://mirrors.tencent.com/npm/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://mirrors.tencent.com/npm/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "peer": true + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://mirrors.tencent.com/npm/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://mirrors.tencent.com/npm/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.211", + "resolved": "https://mirrors.tencent.com/npm/electron-to-chromium/-/electron-to-chromium-1.5.211.tgz", + "integrity": "sha512-IGBvimJkotaLzFnwIVgW9/UD/AOJ2tByUmeOrtqBfACSbAw5b1G0XpvdaieKyc7ULmbwXVx+4e4Be8pOPBrYkw==", + "dev": true, + "peer": true + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://mirrors.tencent.com/npm/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://mirrors.tencent.com/npm/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://mirrors.tencent.com/npm/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://mirrors.tencent.com/npm/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://mirrors.tencent.com/npm/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://mirrors.tencent.com/npm/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://mirrors.tencent.com/npm/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://mirrors.tencent.com/npm/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://mirrors.tencent.com/npm/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://mirrors.tencent.com/npm/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://mirrors.tencent.com/npm/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "peer": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://mirrors.tencent.com/npm/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "peer": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://mirrors.tencent.com/npm/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://mirrors.tencent.com/npm/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "peer": true + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://mirrors.tencent.com/npm/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://mirrors.tencent.com/npm/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://mirrors.tencent.com/npm/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://mirrors.tencent.com/npm/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://mirrors.tencent.com/npm/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rollup": { + "version": "4.49.0", + "resolved": "https://mirrors.tencent.com/npm/rollup/-/rollup-4.49.0.tgz", + "integrity": "sha512-3IVq0cGJ6H7fKXXEdVt+RcYvRCt8beYY9K1760wGQwSAHZcS9eot1zDG5axUbcp/kWRi5zKIIDX8MoKv/TzvZA==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.49.0", + "@rollup/rollup-android-arm64": "4.49.0", + "@rollup/rollup-darwin-arm64": "4.49.0", + "@rollup/rollup-darwin-x64": "4.49.0", + "@rollup/rollup-freebsd-arm64": "4.49.0", + "@rollup/rollup-freebsd-x64": "4.49.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.49.0", + "@rollup/rollup-linux-arm-musleabihf": "4.49.0", + "@rollup/rollup-linux-arm64-gnu": "4.49.0", + "@rollup/rollup-linux-arm64-musl": "4.49.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.49.0", + "@rollup/rollup-linux-ppc64-gnu": "4.49.0", + "@rollup/rollup-linux-riscv64-gnu": "4.49.0", + "@rollup/rollup-linux-riscv64-musl": "4.49.0", + "@rollup/rollup-linux-s390x-gnu": "4.49.0", + "@rollup/rollup-linux-x64-gnu": "4.49.0", + "@rollup/rollup-linux-x64-musl": "4.49.0", + "@rollup/rollup-win32-arm64-msvc": "4.49.0", + "@rollup/rollup-win32-ia32-msvc": "4.49.0", + "@rollup/rollup-win32-x64-msvc": "4.49.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://mirrors.tencent.com/npm/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://mirrors.tencent.com/npm/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://mirrors.tencent.com/npm/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/smob": { + "version": "1.5.0", + "resolved": "https://mirrors.tencent.com/npm/smob/-/smob-1.5.0.tgz", + "integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==", + "dev": true, + "license": "MIT" + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://mirrors.tencent.com/npm/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://mirrors.tencent.com/npm/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://mirrors.tencent.com/npm/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/terser": { + "version": "5.43.1", + "resolved": "https://mirrors.tencent.com/npm/terser/-/terser-5.43.1.tgz", + "integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.14.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://mirrors.tencent.com/npm/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "peer": true, + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://mirrors.tencent.com/npm/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "peer": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..f0d17c0 --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 0000000..6bfabe4 --- /dev/null +++ b/rollup.config.js @@ -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'] +}; \ No newline at end of file diff --git a/src/Meting.php b/src/Meting.php deleted file mode 100644 index d307c17..0000000 --- a/src/Meting.php +++ /dev/null @@ -1,1537 +0,0 @@ - - * Released under the MIT license - */ - -namespace Metowolf; - -class Meting -{ - const VERSION = '1.5.11'; - - public $raw; - public $data; - public $info; - public $error; - public $status; - - public $server; - public $proxy = null; - public $format = false; - public $header; - - public function __construct($value = 'netease') - { - $this->site($value); - } - - public function site($value) - { - $suppose = array('netease', 'tencent', 'xiami', 'kugou', 'baidu', 'kuwo'); - $this->server = in_array($value, $suppose) ? $value : 'netease'; - $this->header = $this->curlset(); - - return $this; - } - - public function cookie($value) - { - $this->header['Cookie'] = $value; - - return $this; - } - - public function format($value = true) - { - $this->format = $value; - - return $this; - } - - public function proxy($value) - { - $this->proxy = $value; - - return $this; - } - - private function exec($api) - { - if (isset($api['encode'])) { - $api = call_user_func_array(array($this, $api['encode']), array($api)); - } - if ($api['method'] == 'GET') { - if (isset($api['body'])) { - $api['url'] .= '?'.http_build_query($api['body']); - $api['body'] = null; - } - } - - $this->curl($api['url'], $api['body']); - - if (!$this->format) { - return $this->raw; - } - - $this->data = $this->raw; - - if (isset($api['decode'])) { - $this->data = call_user_func_array(array($this, $api['decode']), array($this->data)); - } - if (isset($api['format'])) { - $this->data = $this->clean($this->data, $api['format']); - } - - return $this->data; - } - - private function curl($url, $payload = null, $headerOnly = 0) - { - $header = array_map(function ($k, $v) { - return $k.': '.$v; - }, array_keys($this->header), $this->header); - $curl = curl_init(); - if (!is_null($payload)) { - curl_setopt($curl, CURLOPT_POST, 1); - curl_setopt($curl, CURLOPT_POSTFIELDS, is_array($payload) ? http_build_query($payload) : $payload); - } - curl_setopt($curl, CURLOPT_HEADER, $headerOnly); - curl_setopt($curl, CURLOPT_TIMEOUT, 20); - curl_setopt($curl, CURLOPT_ENCODING, 'gzip'); - curl_setopt($curl, CURLOPT_IPRESOLVE, 1); - curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0); - curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 10); - curl_setopt($curl, CURLOPT_URL, $url); - curl_setopt($curl, CURLOPT_HTTPHEADER, $header); - if ($this->proxy) { - curl_setopt($curl, CURLOPT_PROXY, $this->proxy); - } - for ($i = 0; $i < 3; $i++) { - $this->raw = curl_exec($curl); - $this->info = curl_getinfo($curl); - $this->error = curl_errno($curl); - $this->status = $this->error ? curl_error($curl) : ''; - if (!$this->error) { - break; - } - } - curl_close($curl); - - return $this; - } - - private function pickup($array, $rule) - { - $t = explode('.', $rule); - foreach ($t as $vo) { - if (!isset($array[$vo])) { - return array(); - } - $array = $array[$vo]; - } - - return $array; - } - - private function clean($raw, $rule) - { - $raw = json_decode($raw, true); - if (!empty($rule)) { - $raw = $this->pickup($raw, $rule); - } - if (!isset($raw[0]) && count($raw)) { - $raw = array($raw); - } - $result = array_map(array($this, 'format_'.$this->server), $raw); - - return json_encode($result); - } - - public function search($keyword, $option = null) - { - switch ($this->server) { - case 'netease': - $api = array( - 'method' => 'POST', - 'url' => 'http://music.163.com/api/cloudsearch/pc', - 'body' => array( - 's' => $keyword, - 'type' => isset($option['type']) ? $option['type'] : 1, - 'limit' => isset($option['limit']) ? $option['limit'] : 30, - 'total' => 'true', - 'offset' => isset($option['page']) && isset($option['limit']) ? ($option['page'] - 1) * $option['limit'] : 0, - ), - 'encode' => 'netease_AESCBC', - 'format' => 'result.songs', - ); - break; - case 'tencent': - $api = array( - 'method' => 'GET', - 'url' => 'https://c.y.qq.com/soso/fcgi-bin/client_search_cp', - 'body' => array( - 'format' => 'json', - 'p' => isset($option['page']) ? $option['page'] : 1, - 'n' => isset($option['limit']) ? $option['limit'] : 30, - 'w' => $keyword, - 'aggr' => 1, - 'lossless' => 1, - 'cr' => 1, - 'new_json' => 1, - ), - 'format' => 'data.song.list', - ); - break; - case 'xiami': - $api = array( - 'method' => 'GET', - 'url' => 'https://acs.m.xiami.com/h5/mtop.alimusic.search.searchservice.searchsongs/1.0/', - 'body' => array( - 'data' => array( - 'key' => $keyword, - 'pagingVO' => array( - 'page' => isset($option['page']) ? $option['page'] : 1, - 'pageSize' => isset($option['limit']) ? $option['limit'] : 30, - ), - ), - 'r' => 'mtop.alimusic.search.searchservice.searchsongs', - ), - 'encode' => 'xiami_sign', - 'format' => 'data.data.songs', - ); - break; - case 'kugou': - $api = array( - 'method' => 'GET', - 'url' => 'http://mobilecdn.kugou.com/api/v3/search/song', - 'body' => array( - 'api_ver' => 1, - 'area_code' => 1, - 'correct' => 1, - 'pagesize' => isset($option['limit']) ? $option['limit'] : 30, - 'plat' => 2, - 'tag' => 1, - 'sver' => 5, - 'showtype' => 10, - 'page' => isset($option['page']) ? $option['page'] : 1, - 'keyword' => $keyword, - 'version' => 8990, - ), - 'format' => 'data.info', - ); - break; - case 'baidu': - $api = array( - 'method' => 'GET', - 'url' => 'http://musicapi.taihe.com/v1/restserver/ting', - 'body' => array( - 'from' => 'qianqianmini', - 'method' => 'baidu.ting.search.merge', - 'isNew' => 1, - 'platform' => 'darwin', - 'page_no' => isset($option['page']) ? $option['page'] : 1, - 'query' => $keyword, - 'version' => '11.2.1', - 'page_size' => isset($option['limit']) ? $option['limit'] : 30, - ), - 'format' => 'result.song_info.song_list', - ); - break; - case 'kuwo': - $api = array( - 'method' => 'GET', - 'url' => 'http://www.kuwo.cn/api/www/search/searchMusicBykeyWord', - 'body' => array( - 'key' => $keyword, - 'pn' => isset($option['page']) ? $option['page'] : 1, - 'rn' => isset($option['limit']) ? $option['limit'] : 30, - 'httpsStatus' => 1, - ), - 'format' => 'data.list', - ); - break; - } - - return $this->exec($api); - } - - public function song($id) - { - switch ($this->server) { - case 'netease': - $api = array( - 'method' => 'POST', - 'url' => 'http://music.163.com/api/v3/song/detail/', - 'body' => array( - 'c' => '[{"id":'.$id.',"v":0}]', - ), - 'encode' => 'netease_AESCBC', - 'format' => 'songs', - ); - break; - case 'tencent': - $api = array( - 'method' => 'GET', - 'url' => 'https://c.y.qq.com/v8/fcg-bin/fcg_play_single_song.fcg', - 'body' => array( - 'songmid' => $id, - 'platform' => 'yqq', - 'format' => 'json', - ), - 'format' => 'data', - ); - break; - case 'xiami': - $api = array( - 'method' => 'GET', - 'url' => 'https://acs.m.xiami.com/h5/mtop.alimusic.music.songservice.getsongdetail/1.0/', - 'body' => array( - 'data' => array( - 'songId' => $id, - ), - 'r' => 'mtop.alimusic.music.songservice.getsongdetail', - ), - 'encode' => 'xiami_sign', - 'format' => 'data.data.songDetail', - ); - break; - case 'kugou': - $api = array( - 'method' => 'POST', - 'url' => 'http://m.kugou.com/app/i/getSongInfo.php', - 'body' => array( - 'cmd' => 'playInfo', - 'hash' => $id, - 'from' => 'mkugou', - ), - 'format' => '', - ); - break; - case 'baidu': - $api = array( - 'method' => 'GET', - 'url' => 'http://musicapi.taihe.com/v1/restserver/ting', - 'body' => array( - 'from' => 'qianqianmini', - 'method' => 'baidu.ting.song.getInfos', - 'songid' => $id, - 'res' => 1, - 'platform' => 'darwin', - 'version' => '1.0.0', - ), - 'encode' => 'baidu_AESCBC', - 'format' => 'songinfo', - ); - break; - case 'kuwo': - $api = array( - 'method' => 'GET', - 'url' => 'http://www.kuwo.cn/api/www/music/musicInfo', - 'body' => array( - 'mid' => $id, - 'httpsStatus' => 1, - ), - 'format' => 'data', - ); - break; - } - - return $this->exec($api); - } - - public function album($id) - { - switch ($this->server) { - case 'netease': - $api = array( - 'method' => 'POST', - 'url' => 'http://music.163.com/api/v1/album/'.$id, - 'body' => array( - 'total' => 'true', - 'offset' => '0', - 'id' => $id, - 'limit' => '1000', - 'ext' => 'true', - 'private_cloud' => 'true', - ), - 'encode' => 'netease_AESCBC', - 'format' => 'songs', - ); - break; - case 'tencent': - $api = array( - 'method' => 'GET', - 'url' => 'https://c.y.qq.com/v8/fcg-bin/fcg_v8_album_detail_cp.fcg', - 'body' => array( - 'albummid' => $id, - 'platform' => 'mac', - 'format' => 'json', - 'newsong' => 1, - ), - 'format' => 'data.getSongInfo', - ); - break; - case 'xiami': - $api = array( - 'method' => 'GET', - 'url' => 'https://acs.m.xiami.com/h5/mtop.alimusic.music.albumservice.getalbumdetail/1.0/', - 'body' => array( - 'data' => array( - 'albumId' => $id, - ), - 'r' => 'mtop.alimusic.music.albumservice.getalbumdetail', - ), - 'encode' => 'xiami_sign', - 'format' => 'data.data.albumDetail.songs', - ); - break; - case 'kugou': - $api = array( - 'method' => 'GET', - 'url' => 'http://mobilecdn.kugou.com/api/v3/album/song', - 'body' => array( - 'albumid' => $id, - 'area_code' => 1, - 'plat' => 2, - 'page' => 1, - 'pagesize' => -1, - 'version' => 8990, - ), - 'format' => 'data.info', - ); - break; - case 'baidu': - $api = array( - 'method' => 'GET', - 'url' => 'http://musicapi.taihe.com/v1/restserver/ting', - 'body' => array( - 'from' => 'qianqianmini', - 'method' => 'baidu.ting.album.getAlbumInfo', - 'album_id' => $id, - 'platform' => 'darwin', - 'version' => '11.2.1', - ), - 'format' => 'songlist', - ); - break; - case 'kuwo': - $api = array( - 'method' => 'GET', - 'url' => 'http://www.kuwo.cn/api/www/album/albumInfo', - 'body' => array( - 'albumId' => $id, - 'pn' => 1, - 'rn' => 1000, - 'httpsStatus' => 1, - ), - 'format' => 'data.musicList', - ); - break; - } - - return $this->exec($api); - } - - public function artist($id, $limit = 50) - { - switch ($this->server) { - case 'netease': - $api = array( - 'method' => 'POST', - 'url' => 'http://music.163.com/api/v1/artist/'.$id, - 'body' => array( - 'ext' => 'true', - 'private_cloud' => 'true', - 'ext' => 'true', - 'top' => $limit, - 'id' => $id, - ), - 'encode' => 'netease_AESCBC', - 'format' => 'hotSongs', - ); - break; - case 'tencent': - $api = array( - 'method' => 'GET', - 'url' => 'https://c.y.qq.com/v8/fcg-bin/fcg_v8_singer_track_cp.fcg', - 'body' => array( - 'singermid' => $id, - 'begin' => 0, - 'num' => $limit, - 'order' => 'listen', - 'platform' => 'mac', - 'newsong' => 1, - ), - 'format' => 'data.list', - ); - break; - case 'xiami': - $api = array( - 'method' => 'GET', - 'url' => 'https://acs.m.xiami.com/h5/mtop.alimusic.music.songservice.getartistsongs/1.0/', - 'body' => array( - 'data' => array( - 'artistId' => $id, - 'pagingVO' => array( - 'page' => 1, - 'pageSize' => $limit, - ), - ), - 'r' => 'mtop.alimusic.music.songservice.getartistsongs', - ), - 'encode' => 'xiami_sign', - 'format' => 'data.data.songs', - ); - break; - case 'kugou': - $api = array( - 'method' => 'GET', - 'url' => 'http://mobilecdn.kugou.com/api/v3/singer/song', - 'body' => array( - 'singerid' => $id, - 'area_code' => 1, - 'page' => 1, - 'plat' => 0, - 'pagesize' => $limit, - 'version' => 8990, - ), - 'format' => 'data.info', - ); - break; - case 'baidu': - $api = array( - 'method' => 'GET', - 'url' => 'http://musicapi.taihe.com/v1/restserver/ting', - 'body' => array( - 'from' => 'qianqianmini', - 'method' => 'baidu.ting.artist.getSongList', - 'artistid' => $id, - 'limits' => $limit, - 'platform' => 'darwin', - 'offset' => 0, - 'tinguid' => 0, - 'version' => '11.2.1', - ), - 'format' => 'songlist', - ); - break; - case 'kuwo': - $api = array( - 'method' => 'GET', - 'url' => 'http://www.kuwo.cn/api/www/artist/artistMusic', - 'body' => array( - 'artistid' => $id, - 'pn' => 1, - 'rn' => $limit, - 'httpsStatus' => 1, - ), - 'format' => 'data.list', - ); - break; - } - - return $this->exec($api); - } - - public function playlist($id) - { - switch ($this->server) { - case 'netease': - $api = array( - 'method' => 'POST', - 'url' => 'http://music.163.com/api/v6/playlist/detail', - 'body' => array( - 's' => '0', - 'id' => $id, - 'n' => '1000', - 't' => '0', - ), - 'encode' => 'netease_AESCBC', - 'format' => 'playlist.tracks', - ); - break; - case 'tencent': - $api = array( - 'method' => 'GET', - 'url' => 'https://c.y.qq.com/v8/fcg-bin/fcg_v8_playlist_cp.fcg', - 'body' => array( - 'id' => $id, - 'format' => 'json', - 'newsong' => 1, - 'platform' => 'jqspaframe.json', - ), - 'format' => 'data.cdlist.0.songlist', - ); - break; - case 'xiami': - $api = array( - 'method' => 'GET', - 'url' => 'https://acs.m.xiami.com/h5/mtop.alimusic.music.list.collectservice.getcollectdetail/1.0/', - 'body' => array( - 'data' => array( - 'listId' => $id, - 'isFullTags' => false, - 'pagingVO' => array( - 'page' => 1, - 'pageSize' => 1000, - ), - ), - 'r' => 'mtop.alimusic.music.list.collectservice.getcollectdetail', - ), - 'encode' => 'xiami_sign', - 'format' => 'data.data.collectDetail.songs', - ); - break; - case 'kugou': - $api = array( - 'method' => 'GET', - 'url' => 'http://mobilecdn.kugou.com/api/v3/special/song', - 'body' => array( - 'specialid' => $id, - 'area_code' => 1, - 'page' => 1, - 'plat' => 2, - 'pagesize' => -1, - 'version' => 8990, - ), - 'format' => 'data.info', - ); - break; - case 'baidu': - $api = array( - 'method' => 'GET', - 'url' => 'http://musicapi.taihe.com/v1/restserver/ting', - 'body' => array( - 'from' => 'qianqianmini', - 'method' => 'baidu.ting.diy.gedanInfo', - 'listid' => $id, - 'platform' => 'darwin', - 'version' => '11.2.1', - ), - 'format' => 'content', - ); - break; - case 'kuwo': - $api = array( - 'method' => 'GET', - 'url' => 'http://www.kuwo.cn/api/www/playlist/playListInfo', - 'body' => array( - 'pid' => $id, - 'pn' => 1, - 'rn' => 1000, - 'httpsStatus' => 1, - ), - 'format' => 'data.musicList', - ); - break; - } - - return $this->exec($api); - } - - public function url($id, $br = 320) - { - switch ($this->server) { - case 'netease': - $api = array( - 'method' => 'POST', - 'url' => 'http://music.163.com/api/song/enhance/player/url', - 'body' => array( - 'ids' => array($id), - 'br' => $br * 1000, - ), - 'encode' => 'netease_AESCBC', - 'decode' => 'netease_url', - ); - break; - case 'tencent': - $api = array( - 'method' => 'GET', - 'url' => 'https://c.y.qq.com/v8/fcg-bin/fcg_play_single_song.fcg', - 'body' => array( - 'songmid' => $id, - 'platform' => 'yqq', - 'format' => 'json', - ), - 'decode' => 'tencent_url', - ); - break; - case 'xiami': - $api = array( - 'method' => 'GET', - 'url' => 'https://acs.m.xiami.com/h5/mtop.alimusic.music.songservice.getsongs/1.0/', - 'body' => array( - 'data' => array( - 'songIds' => array( - $id, - ), - ), - 'r' => 'mtop.alimusic.music.songservice.getsongs', - ), - 'encode' => 'xiami_sign', - 'decode' => 'xiami_url', - ); - break; - case 'kugou': - $api = array( - 'method' => 'POST', - 'url' => 'http://media.store.kugou.com/v1/get_res_privilege', - 'body' => json_encode( - array( - 'relate' => 1, - 'userid' => '0', - 'vip' => 0, - 'appid' => 1000, - 'token' => '', - 'behavior' => 'download', - 'area_code' => '1', - 'clientver' => '8990', - 'resource' => array(array( - 'id' => 0, - 'type' => 'audio', - 'hash' => $id, - )), ) - ), - 'decode' => 'kugou_url', - ); - break; - case 'baidu': - $api = array( - 'method' => 'GET', - 'url' => 'http://musicapi.taihe.com/v1/restserver/ting', - 'body' => array( - 'from' => 'qianqianmini', - 'method' => 'baidu.ting.song.getInfos', - 'songid' => $id, - 'res' => 1, - 'platform' => 'darwin', - 'version' => '1.0.0', - ), - 'encode' => 'baidu_AESCBC', - 'decode' => 'baidu_url', - ); - break; - case 'kuwo': - $api = array( - 'method' => 'GET', - 'url' => 'http://www.kuwo.cn/api/v1/www/music/playUrl', - 'body' => array( - 'mid' => $id, - 'type' => 'music', - 'httpsStatus' => 1, - ), - 'decode' => 'kuwo_url', - ); - break; - } - $this->temp['br'] = $br; - - return $this->exec($api); - } - - public function lyric($id) - { - switch ($this->server) { - case 'netease': - $api = array( - 'method' => 'POST', - 'url' => 'http://music.163.com/api/song/lyric', - 'body' => array( - 'id' => $id, - 'os' => 'linux', - 'lv' => -1, - 'kv' => -1, - 'tv' => -1, - ), - 'encode' => 'netease_AESCBC', - 'decode' => 'netease_lyric', - ); - break; - case 'tencent': - $api = array( - 'method' => 'GET', - 'url' => 'https://c.y.qq.com/lyric/fcgi-bin/fcg_query_lyric_new.fcg', - 'body' => array( - 'songmid' => $id, - 'g_tk' => '5381', - ), - 'decode' => 'tencent_lyric', - ); - break; - case 'xiami': - $api = array( - 'method' => 'GET', - 'url' => 'https://acs.m.xiami.com/h5/mtop.alimusic.music.lyricservice.getsonglyrics/1.0/', - 'body' => array( - 'data' => array( - 'songId' => $id, - ), - 'r' => 'mtop.alimusic.music.lyricservice.getsonglyrics', - ), - 'encode' => 'xiami_sign', - 'decode' => 'xiami_lyric', - ); - break; - case 'kugou': - $api = array( - 'method' => 'GET', - 'url' => 'http://krcs.kugou.com/search', - 'body' => array( - 'keyword' => '%20-%20', - 'ver' => 1, - 'hash' => $id, - 'client' => 'mobi', - 'man' => 'yes', - ), - 'decode' => 'kugou_lyric', - ); - break; - case 'baidu': - $api = array( - 'method' => 'GET', - 'url' => 'http://musicapi.taihe.com/v1/restserver/ting', - 'body' => array( - 'from' => 'qianqianmini', - 'method' => 'baidu.ting.song.lry', - 'songid' => $id, - 'platform' => 'darwin', - 'version' => '1.0.0', - ), - 'decode' => 'baidu_lyric', - ); - break; - case 'kuwo': - $api = array( - 'method' => 'GET', - 'url' => 'http://m.kuwo.cn/newh5/singles/songinfoandlrc', - 'body' => array( - 'musicId' => $id, - 'httpsStatus' => 1, - ), - 'decode' => 'kuwo_lyric', - ); - break; - } - - return $this->exec($api); - } - - public function pic($id, $size = 300) - { - switch ($this->server) { - case 'netease': - $url = 'https://p3.music.126.net/'.$this->netease_encryptId($id).'/'.$id.'.jpg?param='.$size.'y'.$size; - break; - case 'tencent': - $url = 'https://y.gtimg.cn/music/photo_new/T002R'.$size.'x'.$size.'M000'.$id.'.jpg?max_age=2592000'; - break; - case 'xiami': - $format = $this->format; - $data = $this->format(false)->song($id); - $this->format = $format; - $data = json_decode($data, true); - $url = $data['data']['data']['songDetail']['albumLogo']; - $url = str_replace('http:', 'https:', $url).'@1e_1c_100Q_'.$size.'h_'.$size.'w'; - break; - case 'kugou': - $format = $this->format; - $data = $this->format(false)->song($id); - $this->format = $format; - $data = json_decode($data, true); - $url = $data['imgUrl']; - $url = str_replace('{size}', '400', $url); - break; - case 'baidu': - $format = $this->format; - $data = $this->format(false)->song($id); - $this->format = $format; - $data = json_decode($data, true); - $url = isset($data['songinfo']['pic_radio']) ? $data['songinfo']['pic_radio'] : $data['songinfo']['pic_small']; - break; - case 'kuwo': - $format = $this->format; - $data = $this->format(false)->song($id); - $this->format = $format; - $data = json_decode($data, true); - $url = isset($data['data']['pic']) ? $data['data']['pic'] : $data['data']['albumpic']; - break; - } - - return json_encode(array('url' => $url)); - } - - private function curlset() - { - switch ($this->server) { - case 'netease': - return array( - 'Referer' => 'https://music.163.com/', - 'Cookie' => 'appver=8.2.30; os=iPhone OS; osver=15.0; EVNSM=1.0.0; buildver=2206; channel=distribution; machineid=iPhone13.3', - 'User-Agent' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 CloudMusic/0.1.1 NeteaseMusic/8.2.30', - 'X-Real-IP' => long2ip(mt_rand(1884815360, 1884890111)), - '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', - ); - case 'tencent': - return array( - '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', - ); - case 'xiami': - return array( - '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', - ); - case 'kugou': - return array( - 'User-Agent' => 'IPhone-8990-searchSong', - 'UNI-UserAgent' => 'iOS11.4-Phone8990-1009-0-WiFi', - ); - case 'baidu': - return array( - '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', - ); - case 'kuwo': - return array( - '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', - ); - } - } - - private function getRandomHex($length) - { - if (function_exists('random_bytes')) { - return bin2hex(random_bytes($length / 2)); - } - if (function_exists('mcrypt_create_iv')) { - return bin2hex(mcrypt_create_iv($length / 2, MCRYPT_DEV_URANDOM)); - } - if (function_exists('openssl_random_pseudo_bytes')) { - return bin2hex(openssl_random_pseudo_bytes($length / 2)); - } - } - - private function bchexdec($hex) - { - $dec = 0; - $len = strlen($hex); - for ($i = 1; $i <= $len; $i++) { - $dec = bcadd($dec, bcmul(strval(hexdec($hex[$i - 1])), bcpow('16', strval($len - $i)))); - } - - return $dec; - } - - private function bcdechex($dec) - { - $hex = ''; - do { - $last = bcmod($dec, 16); - $hex = dechex($last).$hex; - $dec = bcdiv(bcsub($dec, $last), 16); - } while ($dec > 0); - - return $hex; - } - - private function str2hex($string) - { - $hex = ''; - for ($i = 0; $i < strlen($string); $i++) { - $ord = ord($string[$i]); - $hexCode = dechex($ord); - $hex .= substr('0'.$hexCode, -2); - } - - return $hex; - } - - private function netease_AESCBC($api) - { - $modulus = '157794750267131502212476817800345498121872783333389747424011531025366277535262539913701806290766479189477533597854989606803194253978660329941980786072432806427833685472618792592200595694346872951301770580765135349259590167490536138082469680638514416594216629258349130257685001248172188325316586707301643237607'; - $pubkey = '65537'; - $nonce = '0CoJUm6Qyw8W8jud'; - $vi = '0102030405060708'; - - if (extension_loaded('bcmath')) { - $skey = $this->getRandomHex(16); - } else { - $skey = 'B3v3kH4vRPWRJFfH'; - } - - $body = json_encode($api['body']); - - if (function_exists('openssl_encrypt')) { - $body = openssl_encrypt($body, 'aes-128-cbc', $nonce, false, $vi); - $body = openssl_encrypt($body, 'aes-128-cbc', $skey, false, $vi); - } else { - $pad = 16 - (strlen($body) % 16); - $body = base64_encode(mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $nonce, $body.str_repeat(chr($pad), $pad), MCRYPT_MODE_CBC, $vi)); - $pad = 16 - (strlen($body) % 16); - $body = base64_encode(mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $skey, $body.str_repeat(chr($pad), $pad), MCRYPT_MODE_CBC, $vi)); - } - - if (extension_loaded('bcmath')) { - $skey = strrev(utf8_encode($skey)); - $skey = $this->bchexdec($this->str2hex($skey)); - $skey = bcpowmod($skey, $pubkey, $modulus); - $skey = $this->bcdechex($skey); - $skey = str_pad($skey, 256, '0', STR_PAD_LEFT); - } else { - $skey = '85302b818aea19b68db899c25dac229412d9bba9b3fcfe4f714dc016bc1686fc446a08844b1f8327fd9cb623cc189be00c5a365ac835e93d4858ee66f43fdc59e32aaed3ef24f0675d70172ef688d376a4807228c55583fe5bac647d10ecef15220feef61477c28cae8406f6f9896ed329d6db9f88757e31848a6c2ce2f94308'; - } - - $api['url'] = str_replace('/api/', '/weapi/', $api['url']); - $api['body'] = array( - 'params' => $body, - 'encSecKey' => $skey, - ); - - return $api; - } - - private function baidu_AESCBC($api) - { - $key = 'DBEECF8C50FD160E'; - $vi = '1231021386755796'; - - $data = 'songid='.$api['body']['songid'].'&ts='.intval(microtime(true) * 1000); - - if (function_exists('openssl_encrypt')) { - $data = openssl_encrypt($data, 'aes-128-cbc', $key, false, $vi); - } else { - $pad = 16 - (strlen($data) % 16); - $data = base64_encode(mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $data.str_repeat(chr($pad), $pad), MCRYPT_MODE_CBC, $vi)); - } - - $api['body']['e'] = $data; - - return $api; - } - - private function xiami_sign($api) - { - $data = $this->curl('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', null, 1); - preg_match_all('/_m_h5[^;]+/', $data->raw, $match); - $this->header['Cookie'] = $match[0][0].'; '.$match[0][1]; - $data = json_encode(array( - 'requestStr' => json_encode(array( - 'header' => array( - 'platformId' => 'mac', - ), - 'model' => $api['body']['data'], - )), - )); - $appkey = '12574478'; - $cookie = $this->header['Cookie']; - preg_match('/_m_h5_tk=([^_]+)/', $cookie, $match); - $token = $match[1]; - $t = time() * 1000; - $sign = md5(sprintf('%s&%s&%s&%s', $token, $t, $appkey, $data)); - $api['body'] = array( - 'appKey' => $appkey, - 't' => $t, - 'dataType' => 'json', - 'data' => $data, - 'api' => $api['body']['r'], - 'v' => '1.0', - 'type' => 'originaljson', - 'sign' => $sign, - ); - - return $api; - } - - private function netease_encryptId($id) - { - $magic = str_split('3go8&$8*3*3h0k(2)2'); - $song_id = str_split($id); - for ($i = 0; $i < count($song_id); $i++) { - $song_id[$i] = chr(ord($song_id[$i]) ^ ord($magic[$i % count($magic)])); - } - $result = base64_encode(md5(implode('', $song_id), 1)); - $result = str_replace(array('/', '+'), array('_', '-'), $result); - - return $result; - } - - private function netease_url($result) - { - $data = json_decode($result, true); - if (isset($data['data'][0]['uf']['url'])) { - $data['data'][0]['url'] = $data['data'][0]['uf']['url']; - } - if (isset($data['data'][0]['url'])) { - $url = array( - 'url' => $data['data'][0]['url'], - 'size' => $data['data'][0]['size'], - 'br' => $data['data'][0]['br'] / 1000, - ); - } else { - $url = array( - 'url' => '', - 'size' => 0, - 'br' => -1, - ); - } - - return json_encode($url); - } - - private function tencent_url($result) - { - $data = json_decode($result, true); - $guid = mt_rand() % 10000000000; - - $type = array( - array('size_flac', 999, 'F000', 'flac'), - array('size_320mp3', 320, 'M800', 'mp3'), - array('size_192aac', 192, 'C600', 'm4a'), - array('size_128mp3', 128, 'M500', 'mp3'), - array('size_96aac', 96, 'C400', 'm4a'), - array('size_48aac', 48, 'C200', 'm4a'), - array('size_24aac', 24, 'C100', 'm4a'), - ); - - $uin = '0'; - preg_match('/uin=(\d+)/', $this->header['Cookie'], $uin_match); - if (count($uin_match)) { - $uin = $uin_match[1]; - } - - $payload = array( - 'req_0' => array( - 'module' => 'vkey.GetVkeyServer', - 'method' => 'CgiGetVkey', - 'param' => array( - 'guid' => (string) $guid, - 'songmid' => array(), - 'filename' => array(), - 'songtype' => array(), - 'uin' => $uin, - 'loginflag' => 1, - 'platform' => '20', - ), - ), - ); - - foreach ($type as $vo) { - $payload['req_0']['param']['songmid'][] = $data['data'][0]['mid']; - $payload['req_0']['param']['filename'][] = $vo[2].$data['data'][0]['file']['media_mid'].'.'.$vo[3]; - $payload['req_0']['param']['songtype'][] = $data['data'][0]['type']; - } - - $api = array( - 'method' => 'GET', - 'url' => 'https://u.y.qq.com/cgi-bin/musicu.fcg', - 'body' => array( - 'format' => 'json', - 'platform' => 'yqq.json', - 'needNewCode' => 0, - 'data' => json_encode($payload), - ), - ); - $response = json_decode($this->exec($api), true); - $vkeys = $response['req_0']['data']['midurlinfo']; - - foreach ($type as $index => $vo) { - if ($data['data'][0]['file'][$vo[0]] && $vo[1] <= $this->temp['br']) { - if (!empty($vkeys[$index]['vkey'])) { - $url = array( - 'url' => $response['req_0']['data']['sip'][0].$vkeys[$index]['purl'], - 'size' => $data['data'][0]['file'][$vo[0]], - 'br' => $vo[1], - ); - break; - } - } - } - if (!isset($url['url'])) { - $url = array( - 'url' => '', - 'size' => 0, - 'br' => -1, - ); - } - - return json_encode($url); - } - - private function xiami_url($result) - { - $data = json_decode($result, true); - - $type = array( - 's' => 740, - 'h' => 320, - 'l' => 128, - 'f' => 64, - 'e' => 32, - ); - $max = 0; - $url = array(); - foreach ($data['data']['data']['songs'][0]['listenFiles'] as $vo) { - if ($type[$vo['quality']] <= $this->temp['br'] && $type[$vo['quality']] > $max) { - $max = $type[$vo['quality']]; - $url = array( - 'url' => $vo['listenFile'], - 'size' => $vo['fileSize'], - 'br' => $type[$vo['quality']], - ); - } - } - if (!isset($url['url'])) { - $url = array( - 'url' => '', - 'size' => 0, - 'br' => -1, - ); - } - - return json_encode($url); - } - - private function kugou_url($result) - { - $data = json_decode($result, true); - - $max = 0; - $url = array(); - foreach ($data['data'][0]['relate_goods'] as $vo) { - if ($vo['info']['bitrate'] <= $this->temp['br'] && $vo['info']['bitrate'] > $max) { - $api = array( - 'method' => 'GET', - 'url' => 'http://trackercdn.kugou.com/i/v2/', - 'body' => array( - 'hash' => $vo['hash'], - 'key' => md5($vo['hash'].'kgcloudv2'), - 'pid' => 3, - 'behavior' => 'play', - 'cmd' => '25', - 'version' => 8990, - ), - ); - $t = json_decode($this->exec($api), true); - if (isset($t['url'])) { - $max = $t['bitRate'] / 1000; - $url = array( - 'url' => reset($t['url']), - 'size' => $t['fileSize'], - 'br' => $t['bitRate'] / 1000, - ); - } - } - } - if (!isset($url['url'])) { - $url = array( - 'url' => '', - 'size' => 0, - 'br' => -1, - ); - } - - return json_encode($url); - } - - private function baidu_url($result) - { - $data = json_decode($result, true); - - $max = 0; - $url = array(); - foreach ($data['songurl']['url'] as $vo) { - if ($vo['file_bitrate'] <= $this->temp['br'] && $vo['file_bitrate'] > $max) { - $url = array( - 'url' => $vo['file_link'], - 'br' => $vo['file_bitrate'], - ); - } - } - if (!isset($url['url'])) { - $url = array( - 'url' => '', - 'br' => -1, - ); - } - - return json_encode($url); - } - - private function kuwo_url($result) - { - $result = json_decode($result, true); - - $url = array(); - if ($result['code'] == 200 && isset($result['data']['url'])) { - $url = array( - 'url' => $result['data']['url'], - 'br' => 128, - ); - } else { - $url = array( - 'url' => '', - 'br' => -1, - ); - } - - return json_encode($url); - } - - private function netease_lyric($result) - { - $result = json_decode($result, true); - $data = array( - 'lyric' => isset($result['lrc']['lyric']) ? $result['lrc']['lyric'] : '', - 'tlyric' => isset($result['tlyric']['lyric']) ? $result['tlyric']['lyric'] : '', - ); - - return json_encode($data, JSON_UNESCAPED_UNICODE); - } - - private function tencent_lyric($result) - { - $result = substr($result, 18, -1); - $result = json_decode($result, true); - $data = array( - 'lyric' => isset($result['lyric']) ? base64_decode($result['lyric']) : '', - 'tlyric' => isset($result['trans']) ? base64_decode($result['trans']) : '', - ); - - return json_encode($data, JSON_UNESCAPED_UNICODE); - } - - private function xiami_lyric($result) - { - $result = json_decode($result, true); - - if (count($result['data']['data']['lyrics'])) { - $data = $result['data']['data']['lyrics'][0]['content']; - $data = preg_replace('/<[^>]+>/', '', $data); - preg_match_all('/\[([\d:\.]+)\](.*)\s\[x-trans\](.*)/i', $data, $match); - if (count($match[0])) { - for ($i = 0; $i < count($match[0]); $i++) { - $A[] = '['.$match[1][$i].']'.$match[2][$i]; - $B[] = '['.$match[1][$i].']'.$match[3][$i]; - } - $arr = array( - 'lyric' => str_replace($match[0], $A, $data), - 'tlyric' => str_replace($match[0], $B, $data), - ); - } else { - $arr = array( - 'lyric' => $data, - 'tlyric' => '', - ); - } - } else { - $arr = array( - 'lyric' => '', - 'tlyric' => '', - ); - } - - return json_encode($arr, JSON_UNESCAPED_UNICODE); - } - - private function kugou_lyric($result) - { - $result = json_decode($result, true); - $api = array( - 'method' => 'GET', - 'url' => 'http://lyrics.kugou.com/download', - 'body' => array( - 'charset' => 'utf8', - 'accesskey' => $result['candidates'][0]['accesskey'], - 'id' => $result['candidates'][0]['id'], - 'client' => 'mobi', - 'fmt' => 'lrc', - 'ver' => 1, - ), - ); - $data = json_decode($this->exec($api), true); - $arr = array( - 'lyric' => base64_decode($data['content']), - 'tlyric' => '', - ); - - return json_encode($arr, JSON_UNESCAPED_UNICODE); - } - - private function baidu_lyric($result) - { - $result = json_decode($result, true); - $data = array( - 'lyric' => isset($result['lrcContent']) ? $result['lrcContent'] : '', - 'tlyric' => '', - ); - - return json_encode($data, JSON_UNESCAPED_UNICODE); - } - - private function kuwo_lyric($result) - { - $result = json_decode($result, true); - if (count($result['data']['lrclist'])) { - $kuwolrc = ''; - for ($i = 0; $i < count($result['data']['lrclist']); $i++) { - $otime = $result['data']['lrclist'][$i]['time']; - $osec = explode('.', $otime)[0]; - $min = str_pad(floor($osec / 60), 2, "0", STR_PAD_LEFT); - $sec = str_pad($osec - $min * 60, 2, "0", STR_PAD_LEFT); - $msec = explode('.', $otime)[1]; - $olyric = $result['data']['lrclist'][$i]['lineLyric']; - $kuwolrc = $kuwolrc . '[' . $min . ':' . $sec . '.' . $msec . ']' . $olyric . "\n"; - } - $arr = array( - 'lyric' => $kuwolrc, - 'tlyric' => '', - ); - } else { - $arr = array( - 'lyric' => '', - 'tlyric' => '', - ); - } - return json_encode($arr, JSON_UNESCAPED_UNICODE); - } - - protected function format_netease($data) - { - $result = array( - 'id' => $data['id'], - 'name' => $data['name'], - 'artist' => array(), - 'album' => $data['al']['name'], - 'pic_id' => isset($data['al']['pic_str']) ? $data['al']['pic_str'] : $data['al']['pic'], - 'url_id' => $data['id'], - 'lyric_id' => $data['id'], - 'source' => 'netease', - ); - if (isset($data['al']['picUrl'])) { - preg_match('/\/(\d+)\./', $data['al']['picUrl'], $match); - $result['pic_id'] = $match[1]; - } - foreach ($data['ar'] as $vo) { - $result['artist'][] = $vo['name']; - } - - return $result; - } - - protected function format_tencent($data) - { - if (isset($data['musicData'])) { - $data = $data['musicData']; - } - $result = array( - 'id' => $data['mid'], - 'name' => $data['name'], - 'artist' => array(), - 'album' => trim($data['album']['title']), - 'pic_id' => $data['album']['mid'], - 'url_id' => $data['mid'], - 'lyric_id' => $data['mid'], - 'source' => 'tencent', - ); - foreach ($data['singer'] as $vo) { - $result['artist'][] = $vo['name']; - } - - return $result; - } - - protected function format_xiami($data) - { - $result = array( - 'id' => $data['songId'], - 'name' => $data['songName'], - 'artist' => array(), - 'album' => $data['albumName'], - 'pic_id' => $data['songId'], - 'url_id' => $data['songId'], - 'lyric_id' => $data['songId'], - 'source' => 'xiami', - ); - foreach ($data['singerVOs'] as $vo) { - $result['artist'][] = $vo['artistName']; - } - - return $result; - } - - protected function format_kugou($data) - { - $result = array( - 'id' => $data['hash'], - 'name' => isset($data['filename']) ? $data['filename'] : $data['fileName'], - 'artist' => array(), - 'album' => isset($data['album_name']) ? $data['album_name'] : '', - 'url_id' => $data['hash'], - 'pic_id' => $data['hash'], - 'lyric_id' => $data['hash'], - 'source' => 'kugou', - ); - list($result['artist'], $result['name']) = explode(' - ', $result['name'], 2); - $result['artist'] = explode('、', $result['artist']); - - return $result; - } - - protected function format_baidu($data) - { - $result = array( - 'id' => $data['song_id'], - 'name' => $data['title'], - 'artist' => explode(',', $data['author']), - 'album' => $data['album_title'], - 'pic_id' => $data['song_id'], - 'url_id' => $data['song_id'], - 'lyric_id' => $data['song_id'], - 'source' => 'baidu', - ); - - return $result; - } - - protected function format_kuwo($data) - { - $result = array( - 'id' => $data['rid'], - 'name' => $data['name'], - 'artist' => explode('&', $data['artist']), - 'album' => $data['album'], - 'pic_id' => $data['rid'], - 'url_id' => $data['rid'], - 'lyric_id' => $data['rid'], - 'source' => 'kuwo', - ); - - return $result; - } - -} diff --git a/src/meting.js b/src/meting.js new file mode 100644 index 0000000..2161054 --- /dev/null +++ b/src/meting.js @@ -0,0 +1,192 @@ +/** + * Meting music framework - Node.js version (重构版本) + * https://i-meto.com + * https://github.com/metowolf/Meting + * + * Copyright 2019, METO Sheel + * 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; diff --git a/src/providers/baidu.js b/src/providers/baidu.js new file mode 100644 index 0000000..5447132 --- /dev/null +++ b/src/providers/baidu.js @@ -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); + } +} \ No newline at end of file diff --git a/src/providers/base.js b/src/providers/base.js new file mode 100644 index 0000000..91b2776 --- /dev/null +++ b/src/providers/base.js @@ -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} 图片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; + } +} diff --git a/src/providers/index.js b/src/providers/index.js new file mode 100644 index 0000000..6f432bb --- /dev/null +++ b/src/providers/index.js @@ -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; + } +} \ No newline at end of file diff --git a/src/providers/kugou.js b/src/providers/kugou.js new file mode 100644 index 0000000..eb8b306 --- /dev/null +++ b/src/providers/kugou.js @@ -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); + } +} \ No newline at end of file diff --git a/src/providers/kuwo.js b/src/providers/kuwo.js new file mode 100644 index 0000000..55febfa --- /dev/null +++ b/src/providers/kuwo.js @@ -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); + } +} \ No newline at end of file diff --git a/src/providers/netease.js b/src/providers/netease.js new file mode 100644 index 0000000..8f76ab0 --- /dev/null +++ b/src/providers/netease.js @@ -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; + } +} diff --git a/src/providers/tencent.js b/src/providers/tencent.js new file mode 100644 index 0000000..ef46b8a --- /dev/null +++ b/src/providers/tencent.js @@ -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); + } +} \ No newline at end of file diff --git a/src/providers/xiami.js b/src/providers/xiami.js new file mode 100644 index 0000000..af93d9d --- /dev/null +++ b/src/providers/xiami.js @@ -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); + } +} \ No newline at end of file diff --git a/test/example.js b/test/example.js new file mode 100644 index 0000000..8b0d5b1 --- /dev/null +++ b/test/example.js @@ -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); +}); \ No newline at end of file diff --git a/test/test.js b/test/test.js new file mode 100644 index 0000000..c41fa19 --- /dev/null +++ b/test/test.js @@ -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);