From d93db93ae4764fbe43fcc0b5348c65d841c78638 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E8=8B=8F=E6=99=93=E6=99=B4?=
<37541680+Suxiaoqinx@users.noreply.github.com>
Date: Fri, 16 May 2025 21:18:39 +0800
Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E9=83=A8=E5=88=86=E4=BB=A3?=
=?UTF-8?q?=E7=A0=81=20=E4=BF=AE=E6=94=B9=E4=B8=BA=E6=A8=A1=E5=9D=97?=
=?UTF-8?q?=E5=8C=96=20=E5=8D=95=E7=8B=AC=E5=88=86=E7=A6=BBAPI?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 178 ++++++++++++++++++++----------
main.py | 305 ++++++++++++++++++++++++---------------------------
music_api.py | 213 +++++++++++++++++++++++++++++++++++
3 files changed, 475 insertions(+), 221 deletions(-)
create mode 100644 music_api.py
diff --git a/README.md b/README.md
index 9350afc..82bc4bf 100644
--- a/README.md
+++ b/README.md
@@ -1,74 +1,138 @@
-# !声明 !
-本项目为开源软件,遵循MIT许可证。任何个人或组织均可自由使用、修改和分发本项目的源代码。然而,我们明确声明,本项目及其任何衍生作品不得用于任何商业或付费项目。任何违反此声明的行为都将被视为对本项目许可证的侵犯。我们鼓励大家在遵守开源精神和许可证的前提下,积极贡献和分享代码。
+# 网易云无损音乐解析
-# 网易云无损解析使用方法
-先安装 文件所需要的依赖模块
-pip install -r requirements.txt
-再运行main.py文件即可
+> **声明**
+> 本项目为开源软件,遵循 MIT 许可证。任何个人或组织均可自由使用、修改和分发本项目的源代码。但本项目及其任何衍生作品**禁止用于任何商业或付费项目**。如有违反,将视为对本项目许可证的侵犯。欢迎大家在遵守开源精神和许可证的前提下积极贡献和分享代码。
-# 环境要求
-Python >= 3
+---
-## GUI模式参数
-python main.py
-| 参数列表 | 参数说明 |
-| ---- | ---- |
-| --mode | api 或 gui|
-| --level | 音质参数(请看下方音质说明) |
-| --url | 解析获取到的网易云音乐地址 |
+## 功能简介
-完整请求 python main.py --mode gui --url 音乐地址 --level 音质
+本项目可解析网易云音乐无损音质下载链接,支持多种音质选择,支持 API 与命令行(GUI)两种模式。
-## API模式参数列表
+---
-请求链接选择 http://ip:port/Song_V1
+## 快速开始
-请求方式 GET & POST
-
-| 参数列表 | 参数说明 |
-| ---- | ---- |
-| url & ids | 解析获取到的网易云音乐地址 *任选其一|
-| level | 音质参数(请看下方音质说明) |
-| type | 解析类型 json down text *任选其一 |
-
-# docker-compose一键部署
-
-## 修改参数
-
-部署前,可以根据需要修改`.env`文件中的环境变量
-
-默认端口为`5000`,如果需要修改,请在`docker-compose.yml`文件中修改`ports`变量
-
-例如,如果需要将端口修改为`8080`,请将以下代码:
-
-```yaml
-ports:
- - "8080:5000"
-```
-
-## docker-compose一键启动
+### 1. 安装依赖
```bash
-docker-compose up -d
+pip install -r requirements.txt
```
-# 音质说明
-standard(标准音质), exhigh(极高音质), lossless(无损音质), hires(Hi-Res音质), jyeffect(高清环绕声), sky(沉浸环绕声), jymaster(超清母带)
+### 2. 配置 Cookie
-黑胶VIP音质选择 standard, exhigh, lossless, hires, jyeffect
-黑胶SVIP音质选择 sky, jymaster
+请在 `cookie.txt` 文件中填入黑胶会员账号的 Cookie,格式如下:
+
+```
+MUSIC_U=你的MUSIC_U值;os=pc;appver=8.9.70;
+```
+
+> 具体值请参考 `cookie.txt` 示例,替换为你自己的即可。
+
+### 3. 运行
+
+#### GUI 模式
+
+```bash
+python main.py --mode gui --url <网易云音乐地址> --level <音质参数>
+```
+
+#### API 模式
+
+```bash
+python main.py --mode api
+```
+
+- 访问接口:http://ip:port/类型解析
+- 支持 GET 和 POST 请求
+
+---
+
+## 参数说明
+
+### GUI 模式参数
+
+| 参数 | 说明 |
+| ------------ | ---------------------------- |
+| --mode | 启动模式:api 或 gui |
+| --url | 需要解析的网易云音乐地址 |
+| --level | 音质参数(见下方音质说明) |
+
+### API 模式参数
+
+| 参数 | 说明 |
+| ------------ | -------------------------------------------- |
+| url / ids | 网易云音乐地址或歌曲ID(二选一) |
+| level | 音质参数(见下方音质说明) |
+| type | 解析类型:json / down / text(三选一) |
+
+| 类型参数 | 说明 |
+| ------------ | -------------------------------------------- |
+| Song_v1 | 单曲解析 |
+| search | 搜索解析 |
+| playlist | 歌单解析 |
+| album | 专辑解析 |
+
+---
+
+## 音质参数说明(仅限单曲解析)
+
+- `standard`:标准音质
+- `exhigh`:极高音质
+- `lossless`:无损音质
+- `hires`:Hi-Res音质
+- `jyeffect`:高清环绕声
+- `sky`:沉浸环绕声
+- `jymaster`:超清母带
+
+> 黑胶VIP音质:standard, exhigh, lossless, hires, jyeffect
+> 黑胶SVIP音质:sky, jymaster
+
+---
+
+## Docker 一键部署
+
+1. **修改参数**
+
+ - 如需修改端口,请编辑 `.env` 或 `docker-compose.yml` 文件中的 `ports` 配置,例如:
+
+ ```yaml
+ ports:
+ - "8080:5000"
+ ```
+
+2. **启动服务**
+
+ ```bash
+ docker-compose up -d
+ ```
+
+---
+
+## 在线演示
-# 演示站点
[在线解析](https://api.toubiec.cn/wyapi.html)
-# 注意事项
-请先在cookie.txt文件内填入黑胶会员账号的cookie 才可以解析!
-Cookie格式为↓
-MUSIC_U=你获取到的MUSIC_U值;os=pc;appver=8.9.70; 完整填入cookie.txt即可!
-具体值在cookie.txt里面就有 替换一下就行了
+---
-# 感谢
-[Ravizhan](https://github.com/ravizhan)
+## 注意事项
-# 反馈方法
-请在Github的lssues反馈 或者到我[博客](https://www.toubiec.cn)反馈
+- 必须使用黑胶会员账号的 Cookie 才能解析高音质资源。
+- Cookie 格式请严格按照 `cookie.txt` 示例填写。
+
+---
+
+## 致谢
+
+- [Ravizhan](https://github.com/ravizhan)
+
+---
+
+## 反馈与交流
+
+- 在 Github [Issues](https://github.com/Suxiaoqinx/Netease_url/issues) 提交反馈
+- 或访问 [我的博客](https://www.toubiec.cn)
+
+---
+
+欢迎 Star、Fork 和 PR!
\ No newline at end of file
diff --git a/main.py b/main.py
index e1a7ccb..d40b3aa 100644
--- a/main.py
+++ b/main.py
@@ -1,53 +1,14 @@
import argparse
from flask import Flask, request, render_template, redirect, jsonify
-import json
-import os
-import urllib.parse
-from hashlib import md5
-from random import randrange
-import requests
-from cryptography.hazmat.primitives import padding
-from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
+from music_api import url_v1, name_v1, lyric_v1, search_music, playlist_detail, album_detail
+from cookie_manager import CookieManager
-def HexDigest(data):
- return "".join([hex(d)[2:].zfill(2) for d in data])
+# ================= 工具函数 =================
+cookie_manager = CookieManager()
-def HashDigest(text):
- HASH = md5(text.encode("utf-8"))
- return HASH.digest()
-
-def HashHexDigest(text):
- return HexDigest(HashDigest(text))
-
-def parse_cookie(text: str):
- cookie_ = [item.strip().split('=', 1) for item in text.strip().split(';') if item]
- cookie_ = {k.strip(): v.strip() for k, v in cookie_}
- return cookie_
-
-def read_cookie():
- script_dir = os.path.dirname(os.path.abspath(__file__))
- cookie_file = os.path.join(script_dir, 'cookie.txt')
- with open(cookie_file, 'r') as f:
- cookie_contents = f.read()
- return cookie_contents
-
-def post(url, params, cookie):
- headers = {
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Safari/537.36 Chrome/91.0.4472.164 NeteaseMusicDesktop/2.10.2.200154',
- 'Referer': '',
- }
- cookies = {
- "os": "pc",
- "appver": "",
- "osver": "",
- "deviceId": "pyncm!"
- }
- cookies.update(cookie)
- response = requests.post(url, headers=headers, cookies=cookies, data={"params": params})
- return response.text
-
-def ids(ids):
+def ids(ids: str) -> str:
if '163cn.tv' in ids:
+ import requests
response = requests.get(ids, allow_redirects=False)
ids = response.headers.get('Location')
if 'music.163.com' in ids:
@@ -55,16 +16,16 @@ def ids(ids):
ids = ids[index:].split('&')[0]
return ids
-def size(value):
+def size(value: float) -> str:
units = ["B", "KB", "MB", "GB", "TB", "PB"]
size = 1024.0
for i in range(len(units)):
if (value / size) < 1:
return "%.2f%s" % (value, units[i])
value = value / size
- return value
+ return str(value)
-def music_level1(value):
+def music_level1(value: str) -> str:
levels = {
'standard': "标准音质",
'exhigh': "极高音质",
@@ -76,53 +37,9 @@ def music_level1(value):
}
return levels.get(value, "未知音质")
-def url_v1(id, level, cookies):
- url = "https://interface3.music.163.com/eapi/song/enhance/player/url/v1"
- AES_KEY = b"e82ckenh8dichen8"
- config = {
- "os": "pc",
- "appver": "",
- "osver": "",
- "deviceId": "pyncm!",
- "requestId": str(randrange(20000000, 30000000))
- }
-
- payload = {
- 'ids': [id],
- 'level': level,
- 'encodeType': 'flac',
- 'header': json.dumps(config),
- }
-
- if level == 'sky':
- payload['immerseType'] = 'c51'
-
- url2 = urllib.parse.urlparse(url).path.replace("/eapi/", "/api/")
- digest = HashHexDigest(f"nobody{url2}use{json.dumps(payload)}md5forencrypt")
- params = f"{url2}-36cd479b6b5-{json.dumps(payload)}-36cd479b6b5-{digest}"
- padder = padding.PKCS7(algorithms.AES(AES_KEY).block_size).padder()
- padded_data = padder.update(params.encode()) + padder.finalize()
- cipher = Cipher(algorithms.AES(AES_KEY), modes.ECB())
- encryptor = cipher.encryptor()
- enc = encryptor.update(padded_data) + encryptor.finalize()
- params = HexDigest(enc)
- response = post(url, params, cookies)
- return json.loads(response)
-
-def name_v1(id):
- urls = "https://interface3.music.163.com/api/v3/song/detail"
- data = {'c': json.dumps([{"id":id,"v":0}])}
- response = requests.post(url=urls, data=data)
- return response.json()
-
-def lyric_v1(id, cookies):
- url = "https://interface3.music.163.com/api/song/lyric"
- data = {'id': id, 'cp': 'false', 'tv': '0', 'lv': '0', 'rv': '0', 'kv': '0', 'yv': '0', 'ytv': '0', 'yrv': '0'}
- response = requests.post(url=url, data=data, cookies=cookies)
- return response.json()
-
-# Flask 应用部分
+# ================= Flask 应用 =================
app = Flask(__name__)
+
@app.after_request
def after_request(response):
response.headers.add('Access-Control-Allow-Origin', '*')
@@ -136,6 +53,7 @@ def index():
@app.route('/Song_V1', methods=['GET', 'POST'])
def Song_v1():
+ # 参数获取
if request.method == 'GET':
song_ids = request.args.get('ids')
url = request.args.get('url')
@@ -147,93 +65,152 @@ def Song_v1():
level = request.form.get('level')
type_ = request.form.get('type')
+ # 参数校验
if not song_ids and not url:
return jsonify({'error': '必须提供 ids 或 url 参数'}), 400
- if level is None:
+ if not level:
return jsonify({'error': 'level参数为空'}), 400
- if type_ is None:
+ if not type_:
return jsonify({'error': 'type参数为空'}), 400
jsondata = song_ids if song_ids else url
- cookies = parse_cookie(read_cookie())
- urlv1 = url_v1(ids(jsondata),level,cookies)
- namev1 = name_v1(urlv1['data'][0]['id'])
- lyricv1 = lyric_v1(urlv1['data'][0]['id'],cookies)
- if urlv1['data'][0]['url'] is not None:
- if namev1['songs']:
- song_url = urlv1['data'][0]['url']
- song_name = namev1['songs'][0]['name']
- song_picUrl = namev1['songs'][0]['al']['picUrl']
- song_alname = namev1['songs'][0]['al']['name']
- artist_names = []
- for song in namev1['songs']:
- ar_list = song['ar']
- if len(ar_list) > 0:
- artist_names.append('/'.join(ar['name'] for ar in ar_list))
- song_arname = ', '.join(artist_names)
- else:
- data = jsonify({"status": 400,'msg': '信息获取不完整!'}), 400
- if type_ == 'text':
- data = '歌曲名称:' + song_name + '
歌曲图片:' + song_picUrl + '
歌手:' + song_arname + '
歌曲专辑:' + song_alname + '
歌曲音质:' + music_level1(urlv1['data'][0]['level']) + '
歌曲大小:' + size(urlv1['data'][0]['size']) + '
音乐地址:' + song_url
- elif type_ == 'down':
- data = redirect(song_url)
- elif type_ == 'json':
- data = {
- "status": 200,
- "name": song_name,
- "pic": song_picUrl,
- "ar_name": song_arname,
- "al_name": song_alname,
- "level":music_level1(urlv1['data'][0]['level']),
- "size": size(urlv1['data'][0]['size']),
- "url": song_url.replace("http://", "https://", 1),
- "lyric": lyricv1['lrc']['lyric'],
- "tlyric": lyricv1.get('tlyric', {}).get('lyric', None)
- }
- data = jsonify(data)
- else:
- data = jsonify({"status": 400,'msg': '解析失败!请检查参数是否完整!'}), 400
- return data
-
-def start_gui(url=None, level='lossless'):
- if url:
- print(f"正在处理 URL: {url},音质:{level}")
- song_ids = ids(url)
- cookies = parse_cookie(read_cookie())
- urlv1 = url_v1(song_ids, level, cookies)
+ cookies = cookie_manager.parse_cookie(cookie_manager.read_cookie())
+ try:
+ song_id = ids(jsondata)
+ urlv1 = url_v1(song_id, level, cookies)
+ if not urlv1['data'] or urlv1['data'][0]['url'] is None:
+ return jsonify({"status": 400, 'msg': '信息获取不完整!'}), 400
namev1 = name_v1(urlv1['data'][0]['id'])
lyricv1 = lyric_v1(urlv1['data'][0]['id'], cookies)
+ song_data = urlv1['data'][0]
+ song_info = namev1['songs'][0] if namev1['songs'] else {}
+ song_url = song_data['url']
+ song_name = song_info.get('name', '')
+ song_picUrl = song_info.get('al', {}).get('picUrl', '')
+ song_alname = song_info.get('al', {}).get('name', '')
+ # 歌手名拼接
+ artist_names = []
+ for song in namev1['songs']:
+ ar_list = song.get('ar', [])
+ if ar_list:
+ artist_names.append('/'.join(ar['name'] for ar in ar_list))
+ song_arname = ', '.join(artist_names)
+ # 歌词
+ lyric = lyricv1.get('lrc', {}).get('lyric', '')
+ tlyric = lyricv1.get('tlyric', {}).get('lyric', None)
+ except Exception as e:
+ return jsonify({'status': 500, 'msg': f'服务异常: {str(e)}'}), 500
- song_name = namev1['songs'][0]['name']
- song_pic = namev1['songs'][0]['al']['picUrl']
- artist_names = ', '.join(artist['name'] for artist in namev1['songs'][0]['ar'])
- album_name = namev1['songs'][0]['al']['name']
- music_quality = music_level1(urlv1['data'][0]['level'])
- file_size = size(urlv1['data'][0]['size'])
- music_url = urlv1['data'][0]['url']
- lyrics = lyricv1['lrc']['lyric']
- translated_lyrics = lyricv1.get('tlyric', {}).get('lyric', None)
+ # 响应类型
+ if type_ == 'text':
+ data = f'歌曲名称:{song_name}
歌曲图片:{song_picUrl}
歌手:{song_arname}
歌曲专辑:{song_alname}
歌曲音质:{music_level1(song_data["level"])}
歌曲大小:{size(song_data["size"])}
音乐地址:{song_url}'
+ elif type_ == 'down':
+ data = redirect(song_url)
+ elif type_ == 'json':
+ data = {
+ "status": 200,
+ "name": song_name,
+ "pic": song_picUrl,
+ "ar_name": song_arname,
+ "al_name": song_alname,
+ "level": music_level1(song_data["level"]),
+ "size": size(song_data["size"]),
+ "url": song_url.replace("http://", "https://", 1),
+ "lyric": lyric,
+ "tlyric": tlyric
+ }
+ data = jsonify(data)
+ else:
+ data = jsonify({"status": 400, 'msg': '解析失败!请检查参数是否完整!'}), 400
+ return data
- output_text = f"""
- 歌曲名称: {song_name}
- 歌曲图片: {song_pic}
- 歌手: {artist_names}
- 专辑名称: {album_name}
- 音质: {music_quality}
- 大小: {file_size}
- 音乐链接: {music_url}
- 歌词: {lyrics}
- 翻译歌词: {translated_lyrics if translated_lyrics else '没有翻译歌词'}
- """
+@app.route('/Search', methods=['GET', 'POST'])
+def search():
+ if request.method == 'GET':
+ keywords = request.args.get('keywords')
+ limit = request.args.get('limit', default=10, type=int)
+ else:
+ keywords = request.form.get('keywords')
+ limit = int(request.form.get('limit', 10))
+ if not keywords:
+ return jsonify({'error': '必须提供 keywords 参数'}), 400
+ cookies = cookie_manager.parse_cookie(cookie_manager.read_cookie())
+ try:
+ songs = search_music(keywords, cookies, limit=limit)
+ return jsonify({'status': 200, 'result': songs})
+ except Exception as e:
+ return jsonify({'status': 500, 'msg': f'搜索异常: {str(e)}'}), 500
- print(output_text)
+@app.route('/Playlist', methods=['GET', 'POST'])
+def playlist():
+ if request.method == 'GET':
+ playlist_id = request.args.get('id')
+ else:
+ playlist_id = request.form.get('id')
+ if not playlist_id:
+ return jsonify({'error': '必须提供歌单id参数'}), 400
+ cookies = cookie_manager.parse_cookie(cookie_manager.read_cookie())
+ try:
+ info = playlist_detail(playlist_id, cookies)
+ return jsonify({'status': 200, 'playlist': info})
+ except Exception as e:
+ return jsonify({'status': 500, 'msg': f'歌单解析异常: {str(e)}'}), 500
+
+@app.route('/Album', methods=['GET', 'POST'])
+def album():
+ if request.method == 'GET':
+ album_id = request.args.get('id')
+ else:
+ album_id = request.form.get('id')
+ if not album_id:
+ return jsonify({'error': '必须提供专辑id参数'}), 400
+ cookies = cookie_manager.parse_cookie(cookie_manager.read_cookie())
+ try:
+ info = album_detail(album_id, cookies)
+ return jsonify({'status': 200, 'album': info})
+ except Exception as e:
+ return jsonify({'status': 500, 'msg': f'专辑解析异常: {str(e)}'}), 500
+
+# ================= 命令行启动 =================
+def start_gui(url: str = None, level: str = 'lossless'):
+ if url:
+ print(f"正在处理 URL: {url},音质:{level}")
+ cookies = cookie_manager.parse_cookie(cookie_manager.read_cookie())
+ try:
+ song_ids = ids(url)
+ urlv1 = url_v1(song_ids, level, cookies)
+ namev1 = name_v1(urlv1['data'][0]['id'])
+ lyricv1 = lyric_v1(urlv1['data'][0]['id'], cookies)
+ song_info = namev1['songs'][0]
+ song_name = song_info['name']
+ song_pic = song_info['al']['picUrl']
+ artist_names = ', '.join(artist['name'] for artist in song_info['ar'])
+ album_name = song_info['al']['name']
+ music_quality = music_level1(urlv1['data'][0]['level'])
+ file_size = size(urlv1['data'][0]['size'])
+ music_url = urlv1['data'][0]['url']
+ lyrics = lyricv1.get('lrc', {}).get('lyric', '')
+ translated_lyrics = lyricv1.get('tlyric', {}).get('lyric', None)
+ output_text = f"""
+ 歌曲名称: {song_name}
+ 歌曲图片: {song_pic}
+ 歌手: {artist_names}
+ 专辑名称: {album_name}
+ 音质: {music_quality}
+ 大小: {file_size}
+ 音乐链接: {music_url}
+ 歌词: {lyrics}
+ 翻译歌词: {translated_lyrics if translated_lyrics else '没有翻译歌词'}
+ """
+ print(output_text)
+ except Exception as e:
+ print(f"发生错误: {e}")
else:
print("没有提供 URL 参数")
def start_api():
app.run(host='0.0.0.0', port=5000, debug=False)
-# 启动模式解析
if __name__ == '__main__':
parser = argparse.ArgumentParser(description="启动 API 或 GUI")
parser.add_argument('--mode', choices=['api', 'gui'], help="选择启动模式:api 或 gui")
diff --git a/music_api.py b/music_api.py
new file mode 100644
index 0000000..8406b67
--- /dev/null
+++ b/music_api.py
@@ -0,0 +1,213 @@
+import json
+import urllib.parse
+from random import randrange
+import requests
+from hashlib import md5
+from cryptography.hazmat.primitives import padding
+from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
+
+def HexDigest(data):
+ return "".join([hex(d)[2:].zfill(2) for d in data])
+
+def HashDigest(text):
+ HASH = md5(text.encode("utf-8"))
+ return HASH.digest()
+
+def HashHexDigest(text):
+ return HexDigest(HashDigest(text))
+
+def post(url, params, cookie):
+ headers = {
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Safari/537.36 Chrome/91.0.4472.164 NeteaseMusicDesktop/2.10.2.200154',
+ 'Referer': '',
+ }
+ cookies = {
+ "os": "pc",
+ "appver": "",
+ "osver": "",
+ "deviceId": "pyncm!"
+ }
+ cookies.update(cookie)
+ response = requests.post(url, headers=headers, cookies=cookies, data={"params": params})
+ return response.text
+
+def url_v1(id, level, cookies):
+ url = "https://interface3.music.163.com/eapi/song/enhance/player/url/v1"
+ AES_KEY = b"e82ckenh8dichen8"
+ config = {
+ "os": "pc",
+ "appver": "",
+ "osver": "",
+ "deviceId": "pyncm!",
+ "requestId": str(randrange(20000000, 30000000))
+ }
+
+ payload = {
+ 'ids': [id],
+ 'level': level,
+ 'encodeType': 'flac',
+ 'header': json.dumps(config),
+ }
+
+ if level == 'sky':
+ payload['immerseType'] = 'c51'
+
+ url2 = urllib.parse.urlparse(url).path.replace("/eapi/", "/api/")
+ digest = HashHexDigest(f"nobody{url2}use{json.dumps(payload)}md5forencrypt")
+ params = f"{url2}-36cd479b6b5-{json.dumps(payload)}-36cd479b6b5-{digest}"
+ padder = padding.PKCS7(algorithms.AES(AES_KEY).block_size).padder()
+ padded_data = padder.update(params.encode()) + padder.finalize()
+ cipher = Cipher(algorithms.AES(AES_KEY), modes.ECB())
+ encryptor = cipher.encryptor()
+ enc = encryptor.update(padded_data) + encryptor.finalize()
+ params = HexDigest(enc)
+ response = post(url, params, cookies)
+ return json.loads(response)
+
+def name_v1(id):
+ urls = "https://interface3.music.163.com/api/v3/song/detail"
+ data = {'c': json.dumps([{"id":id,"v":0}])}
+ response = requests.post(url=urls, data=data)
+ return response.json()
+
+def lyric_v1(id, cookies):
+ url = "https://interface3.music.163.com/api/song/lyric"
+ data = {'id': id, 'cp': 'false', 'tv': '0', 'lv': '0', 'rv': '0', 'kv': '0', 'yv': '0', 'ytv': '0', 'yrv': '0'}
+ response = requests.post(url=url, data=data, cookies=cookies)
+ return response.json()
+
+def search_music(keywords, cookies, limit=10):
+ """
+ 网易云音乐搜索接口,返回歌曲信息列表
+ :param keywords: 搜索关键词
+ :param cookies: 登录 cookies
+ :param limit: 返回数量
+ :return: 歌曲信息列表
+ """
+ url = 'https://music.163.com/api/cloudsearch/pc'
+ data = {'s': keywords, 'type': 1, 'limit': limit}
+ headers = {
+ 'User-Agent': 'Mozilla/5.0',
+ 'Referer': 'https://music.163.com/'
+ }
+ response = requests.post(url, data=data, headers=headers, cookies=cookies)
+ result = response.json()
+ songs = []
+ for item in result.get('result', {}).get('songs', []):
+ song_info = {
+ 'id': item['id'],
+ 'name': item['name'],
+ 'artists': '/'.join(artist['name'] for artist in item['ar']),
+ 'album': item['al']['name'],
+ 'picUrl': item['al']['picUrl']
+ }
+ songs.append(song_info)
+ return songs
+
+def playlist_detail(playlist_id, cookies):
+ """
+ 获取网易云歌单详情及全部歌曲列表
+ :param playlist_id: 歌单ID
+ :param cookies: 登录 cookies
+ :return: 歌单基本信息和全部歌曲列表
+ """
+ url = f'https://music.163.com/api/v6/playlist/detail'
+ data = {'id': playlist_id}
+ headers = {
+ 'User-Agent': 'Mozilla/5.0',
+ 'Referer': 'https://music.163.com/'
+ }
+ response = requests.post(url, data=data, headers=headers, cookies=cookies)
+ result = response.json()
+ playlist = result.get('playlist', {})
+ info = {
+ 'id': playlist.get('id'),
+ 'name': playlist.get('name'),
+ 'coverImgUrl': playlist.get('coverImgUrl'),
+ 'creator': playlist.get('creator', {}).get('nickname', ''),
+ 'trackCount': playlist.get('trackCount'),
+ 'description': playlist.get('description', ''),
+ 'tracks': []
+ }
+ # 获取所有trackIds
+ track_ids = [str(t['id']) for t in playlist.get('trackIds', [])]
+ # 分批获取详细信息(每批最多100首)
+ for i in range(0, len(track_ids), 100):
+ batch_ids = track_ids[i:i+100]
+ song_detail_url = 'https://interface3.music.163.com/api/v3/song/detail'
+ song_data = {'c': json.dumps([{ 'id': int(sid), 'v': 0 } for sid in batch_ids])}
+ song_resp = requests.post(url=song_detail_url, data=song_data, headers=headers, cookies=cookies)
+ song_result = song_resp.json()
+ for song in song_result.get('songs', []):
+ info['tracks'].append({
+ 'id': song['id'],
+ 'name': song['name'],
+ 'artists': '/'.join(artist['name'] for artist in song['ar']),
+ 'album': song['al']['name'],
+ 'picUrl': song['al']['picUrl']
+ })
+ return info
+
+def album_detail(album_id, cookies):
+ """
+ 获取网易云专辑详情及全部歌曲列表
+ :param album_id: 专辑ID
+ :param cookies: 登录 cookies
+ :return: 专辑基本信息和全部歌曲列表
+ """
+ url = f'https://music.163.com/api/v1/album/{album_id}'
+ headers = {
+ 'User-Agent': 'Mozilla/5.0',
+ 'Referer': 'https://music.163.com/'
+ }
+ response = requests.get(url, headers=headers, cookies=cookies)
+ result = response.json()
+ album = result.get('album', {})
+ info = {
+ 'id': album.get('id'),
+ 'name': album.get('name'),
+ 'coverImgUrl': get_pic_url(album.get('pic')),
+ #'coverImgEncryptId': netease_encryptId(str(album.get('pic'))),
+ 'artist': album.get('artist', {}).get('name', ''),
+ 'publishTime': album.get('publishTime'),
+ 'description': album.get('description', ''),
+ 'songs': []
+ }
+ for song in result.get('songs', []):
+ info['songs'].append({
+ 'id': song['id'],
+ 'name': song['name'],
+ 'artists': '/'.join(artist['name'] for artist in song['ar']),
+ 'album': song['al']['name'],
+ 'picUrl': get_pic_url(song['al'].get('pic'))
+ })
+ return info
+
+def netease_encryptId(id_str):
+ """
+ 网易云加密图片ID算法(PHP移植版)
+ :param id_str: 歌曲/专辑/图片ID(字符串)
+ :return: 加密后的字符串
+ """
+ import base64
+ magic = list('3go8&$8*3*3h0k(2)2')
+ song_id = list(id_str)
+ for i in range(len(song_id)):
+ song_id[i] = chr(ord(song_id[i]) ^ ord(magic[i % len(magic)]))
+ m = ''.join(song_id)
+ import hashlib
+ md5_bytes = hashlib.md5(m.encode('utf-8')).digest()
+ result = base64.b64encode(md5_bytes).decode('utf-8')
+ result = result.replace('/', '_').replace('+', '-')
+ return result
+
+def get_pic_url(pic_id, size=300):
+ """
+ 获取网易云加密歌曲/专辑封面直链
+ :param pic_id: 封面ID(数字或字符串)
+ :param size: 图片尺寸,默认300
+ :return: {'url': url}
+ """
+ enc_id = netease_encryptId(str(pic_id))
+ url = f'https://p3.music.126.net/{enc_id}/{pic_id}.jpg?param={size}y{size}'
+ return url