修改部分代码 修改为模块化 单独分离API

This commit is contained in:
苏晓晴 2025-05-16 21:18:39 +08:00 committed by GitHub
parent e78157dd1e
commit d93db93ae4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 475 additions and 221 deletions

150
README.md
View File

@ -1,74 +1,138 @@
# !声明 # 网易云无损音乐解析
本项目为开源软件遵循MIT许可证。任何个人或组织均可自由使用、修改和分发本项目的源代码。然而我们明确声明本项目及其任何衍生作品不得用于任何商业或付费项目。任何违反此声明的行为都将被视为对本项目许可证的侵犯。我们鼓励大家在遵守开源精神和许可证的前提下积极贡献和分享代码。
# 网易云无损解析使用方法 > **声明**
先安装 文件所需要的依赖模块 > 本项目为开源软件,遵循 MIT 许可证。任何个人或组织均可自由使用、修改和分发本项目的源代码。但本项目及其任何衍生作品**禁止用于任何商业或付费项目**。如有违反,将视为对本项目许可证的侵犯。欢迎大家在遵守开源精神和许可证的前提下积极贡献和分享代码。
---
## 功能简介
本项目可解析网易云音乐无损音质下载链接,支持多种音质选择,支持 API 与命令行GUI两种模式。
---
## 快速开始
### 1. 安装依赖
```bash
pip install -r requirements.txt pip install -r requirements.txt
再运行main.py文件即可 ```
# 环境要求 ### 2. 配置 Cookie
Python >= 3
## GUI模式参数 请在 `cookie.txt` 文件中填入黑胶会员账号的 Cookie格式如下
python main.py
| 参数列表 | 参数说明 |
| ---- | ---- |
| --mode | api 或 gui|
| --level | 音质参数(请看下方音质说明) |
| --url | 解析获取到的网易云音乐地址 |
完整请求 python main.py --mode gui --url 音乐地址 --level 音质 ```
MUSIC_U=你的MUSIC_U值;os=pc;appver=8.9.70;
```
## API模式参数列表 > 具体值请参考 `cookie.txt` 示例,替换为你自己的即可。
请求链接选择 http://ip:port/Song_V1 ### 3. 运行
请求方式 GET & POST #### GUI 模式
| 参数列表 | 参数说明 | ```bash
| ---- | ---- | python main.py --mode gui --url <网易云音乐地址> --level <音质参数>
| url & ids | 解析获取到的网易云音乐地址 *任选其一| ```
| level | 音质参数(请看下方音质说明) |
| type | 解析类型 json down text *任选其一 |
# docker-compose一键部署 #### API 模式
## 修改参数 ```bash
python main.py --mode api
```
部署前,可以根据需要修改`.env`文件中的环境变量 - 访问接口http://ip:port/类型解析
- 支持 GET 和 POST 请求
默认端口为`5000`,如果需要修改,请在`docker-compose.yml`文件中修改`ports`变量 ---
例如,如果需要将端口修改为`8080`,请将以下代码: ## 参数说明
### 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 ```yaml
ports: ports:
- "8080:5000" - "8080:5000"
``` ```
## docker-compose一键启动 2. **启动服务**
```bash ```bash
docker-compose up -d docker-compose up -d
``` ```
# 音质说明 ---
standard(标准音质), exhigh(极高音质), lossless(无损音质), hires(Hi-Res音质), jyeffect(高清环绕声), sky(沉浸环绕声), jymaster(超清母带)
黑胶VIP音质选择 standard, exhigh, lossless, hires, jyeffect <br> <br> ## 在线演示
黑胶SVIP音质选择 sky, jymaster
# 演示站点
[在线解析](https://api.toubiec.cn/wyapi.html) [在线解析](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)
# 反馈方法 - 必须使用黑胶会员账号的 Cookie 才能解析高音质资源。
请在Github的lssues反馈 或者到我[博客](https://www.toubiec.cn)反馈 - Cookie 格式请严格按照 `cookie.txt` 示例填写。
---
## 致谢
- [Ravizhan](https://github.com/ravizhan)
---
## 反馈与交流
- 在 Github [Issues](https://github.com/Suxiaoqinx/Netease_url/issues) 提交反馈
- 或访问 [我的博客](https://www.toubiec.cn)
---
欢迎 Star、Fork 和 PR

225
main.py
View File

@ -1,53 +1,14 @@
import argparse import argparse
from flask import Flask, request, render_template, redirect, jsonify from flask import Flask, request, render_template, redirect, jsonify
import json from music_api import url_v1, name_v1, lyric_v1, search_music, playlist_detail, album_detail
import os from cookie_manager import CookieManager
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
def HexDigest(data): # ================= 工具函数 =================
return "".join([hex(d)[2:].zfill(2) for d in data]) cookie_manager = CookieManager()
def HashDigest(text): def ids(ids: str) -> str:
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):
if '163cn.tv' in ids: if '163cn.tv' in ids:
import requests
response = requests.get(ids, allow_redirects=False) response = requests.get(ids, allow_redirects=False)
ids = response.headers.get('Location') ids = response.headers.get('Location')
if 'music.163.com' in ids: if 'music.163.com' in ids:
@ -55,16 +16,16 @@ def ids(ids):
ids = ids[index:].split('&')[0] ids = ids[index:].split('&')[0]
return ids return ids
def size(value): def size(value: float) -> str:
units = ["B", "KB", "MB", "GB", "TB", "PB"] units = ["B", "KB", "MB", "GB", "TB", "PB"]
size = 1024.0 size = 1024.0
for i in range(len(units)): for i in range(len(units)):
if (value / size) < 1: if (value / size) < 1:
return "%.2f%s" % (value, units[i]) return "%.2f%s" % (value, units[i])
value = value / size value = value / size
return value return str(value)
def music_level1(value): def music_level1(value: str) -> str:
levels = { levels = {
'standard': "标准音质", 'standard': "标准音质",
'exhigh': "极高音质", 'exhigh': "极高音质",
@ -76,53 +37,9 @@ def music_level1(value):
} }
return levels.get(value, "未知音质") return levels.get(value, "未知音质")
def url_v1(id, level, cookies): # ================= Flask 应用 =================
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 应用部分
app = Flask(__name__) app = Flask(__name__)
@app.after_request @app.after_request
def after_request(response): def after_request(response):
response.headers.add('Access-Control-Allow-Origin', '*') response.headers.add('Access-Control-Allow-Origin', '*')
@ -136,6 +53,7 @@ def index():
@app.route('/Song_V1', methods=['GET', 'POST']) @app.route('/Song_V1', methods=['GET', 'POST'])
def Song_v1(): def Song_v1():
# 参数获取
if request.method == 'GET': if request.method == 'GET':
song_ids = request.args.get('ids') song_ids = request.args.get('ids')
url = request.args.get('url') url = request.args.get('url')
@ -147,34 +65,45 @@ def Song_v1():
level = request.form.get('level') level = request.form.get('level')
type_ = request.form.get('type') type_ = request.form.get('type')
# 参数校验
if not song_ids and not url: if not song_ids and not url:
return jsonify({'error': '必须提供 ids 或 url 参数'}), 400 return jsonify({'error': '必须提供 ids 或 url 参数'}), 400
if level is None: if not level:
return jsonify({'error': 'level参数为空'}), 400 return jsonify({'error': 'level参数为空'}), 400
if type_ is None: if not type_:
return jsonify({'error': 'type参数为空'}), 400 return jsonify({'error': 'type参数为空'}), 400
jsondata = song_ids if song_ids else url jsondata = song_ids if song_ids else url
cookies = parse_cookie(read_cookie()) cookies = cookie_manager.parse_cookie(cookie_manager.read_cookie())
urlv1 = url_v1(ids(jsondata),level,cookies) 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']) namev1 = name_v1(urlv1['data'][0]['id'])
lyricv1 = lyric_v1(urlv1['data'][0]['id'], cookies) lyricv1 = lyric_v1(urlv1['data'][0]['id'], cookies)
if urlv1['data'][0]['url'] is not None: song_data = urlv1['data'][0]
if namev1['songs']: song_info = namev1['songs'][0] if namev1['songs'] else {}
song_url = urlv1['data'][0]['url'] song_url = song_data['url']
song_name = namev1['songs'][0]['name'] song_name = song_info.get('name', '')
song_picUrl = namev1['songs'][0]['al']['picUrl'] song_picUrl = song_info.get('al', {}).get('picUrl', '')
song_alname = namev1['songs'][0]['al']['name'] song_alname = song_info.get('al', {}).get('name', '')
# 歌手名拼接
artist_names = [] artist_names = []
for song in namev1['songs']: for song in namev1['songs']:
ar_list = song['ar'] ar_list = song.get('ar', [])
if len(ar_list) > 0: if ar_list:
artist_names.append('/'.join(ar['name'] for ar in ar_list)) artist_names.append('/'.join(ar['name'] for ar in ar_list))
song_arname = ', '.join(artist_names) song_arname = ', '.join(artist_names)
else: # 歌词
data = jsonify({"status": 400,'msg': '信息获取不完整!'}), 400 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
# 响应类型
if type_ == 'text': if type_ == 'text':
data = '歌曲名称:' + song_name + '<br>歌曲图片:' + song_picUrl + '<br>歌手:' + song_arname + '<br>歌曲专辑:' + song_alname + '<br>歌曲音质:' + music_level1(urlv1['data'][0]['level']) + '<br>歌曲大小:' + size(urlv1['data'][0]['size']) + '<br>音乐地址:' + song_url data = f'歌曲名称:{song_name}<br>歌曲图片:{song_picUrl}<br>歌手:{song_arname}<br>歌曲专辑:{song_alname}<br>歌曲音质:{music_level1(song_data["level"])}<br>歌曲大小:{size(song_data["size"])}<br>音乐地址:{song_url}'
elif type_ == 'down': elif type_ == 'down':
data = redirect(song_url) data = redirect(song_url)
elif type_ == 'json': elif type_ == 'json':
@ -184,36 +113,84 @@ def Song_v1():
"pic": song_picUrl, "pic": song_picUrl,
"ar_name": song_arname, "ar_name": song_arname,
"al_name": song_alname, "al_name": song_alname,
"level":music_level1(urlv1['data'][0]['level']), "level": music_level1(song_data["level"]),
"size": size(urlv1['data'][0]['size']), "size": size(song_data["size"]),
"url": song_url.replace("http://", "https://", 1), "url": song_url.replace("http://", "https://", 1),
"lyric": lyricv1['lrc']['lyric'], "lyric": lyric,
"tlyric": lyricv1.get('tlyric', {}).get('lyric', None) "tlyric": tlyric
} }
data = jsonify(data) data = jsonify(data)
else: else:
data = jsonify({"status": 400, 'msg': '解析失败!请检查参数是否完整!'}), 400 data = jsonify({"status": 400, 'msg': '解析失败!请检查参数是否完整!'}), 400
return data return data
def start_gui(url=None, level='lossless'): @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
@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: if url:
print(f"正在处理 URL: {url},音质:{level}") print(f"正在处理 URL: {url},音质:{level}")
cookies = cookie_manager.parse_cookie(cookie_manager.read_cookie())
try:
song_ids = ids(url) song_ids = ids(url)
cookies = parse_cookie(read_cookie())
urlv1 = url_v1(song_ids, level, cookies) urlv1 = url_v1(song_ids, level, cookies)
namev1 = name_v1(urlv1['data'][0]['id']) namev1 = name_v1(urlv1['data'][0]['id'])
lyricv1 = lyric_v1(urlv1['data'][0]['id'], cookies) lyricv1 = lyric_v1(urlv1['data'][0]['id'], cookies)
song_info = namev1['songs'][0]
song_name = namev1['songs'][0]['name'] song_name = song_info['name']
song_pic = namev1['songs'][0]['al']['picUrl'] song_pic = song_info['al']['picUrl']
artist_names = ', '.join(artist['name'] for artist in namev1['songs'][0]['ar']) artist_names = ', '.join(artist['name'] for artist in song_info['ar'])
album_name = namev1['songs'][0]['al']['name'] album_name = song_info['al']['name']
music_quality = music_level1(urlv1['data'][0]['level']) music_quality = music_level1(urlv1['data'][0]['level'])
file_size = size(urlv1['data'][0]['size']) file_size = size(urlv1['data'][0]['size'])
music_url = urlv1['data'][0]['url'] music_url = urlv1['data'][0]['url']
lyrics = lyricv1['lrc']['lyric'] lyrics = lyricv1.get('lrc', {}).get('lyric', '')
translated_lyrics = lyricv1.get('tlyric', {}).get('lyric', None) translated_lyrics = lyricv1.get('tlyric', {}).get('lyric', None)
output_text = f""" output_text = f"""
歌曲名称: {song_name} 歌曲名称: {song_name}
歌曲图片: {song_pic} 歌曲图片: {song_pic}
@ -225,15 +202,15 @@ def start_gui(url=None, level='lossless'):
歌词: {lyrics} 歌词: {lyrics}
翻译歌词: {translated_lyrics if translated_lyrics else '没有翻译歌词'} 翻译歌词: {translated_lyrics if translated_lyrics else '没有翻译歌词'}
""" """
print(output_text) print(output_text)
except Exception as e:
print(f"发生错误: {e}")
else: else:
print("没有提供 URL 参数") print("没有提供 URL 参数")
def start_api(): def start_api():
app.run(host='0.0.0.0', port=5000, debug=False) app.run(host='0.0.0.0', port=5000, debug=False)
# 启动模式解析
if __name__ == '__main__': if __name__ == '__main__':
parser = argparse.ArgumentParser(description="启动 API 或 GUI") parser = argparse.ArgumentParser(description="启动 API 或 GUI")
parser.add_argument('--mode', choices=['api', 'gui'], help="选择启动模式api 或 gui") parser.add_argument('--mode', choices=['api', 'gui'], help="选择启动模式api 或 gui")

213
music_api.py Normal file
View File

@ -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