mirror of
https://github.com/Suxiaoqinx/Netease_url.git
synced 2025-09-14 11:36:45 +08:00
Compare commits
4 Commits
e78157dd1e
...
134543d59f
Author | SHA1 | Date | |
---|---|---|---|
![]() |
134543d59f | ||
![]() |
7ee2b61722 | ||
![]() |
9ca4bda4b2 | ||
![]() |
d93db93ae4 |
178
README.md
178
README.md
@ -1,74 +1,138 @@
|
|||||||
# !声明 !
|
# 网易云无损音乐解析
|
||||||
本项目为开源软件,遵循MIT许可证。任何个人或组织均可自由使用、修改和分发本项目的源代码。然而,我们明确声明,本项目及其任何衍生作品不得用于任何商业或付费项目。任何违反此声明的行为都将被视为对本项目许可证的侵犯。我们鼓励大家在遵守开源精神和许可证的前提下,积极贡献和分享代码。
|
|
||||||
|
|
||||||
# 网易云无损解析使用方法
|
> **声明**
|
||||||
先安装 文件所需要的依赖模块
|
> 本项目为开源软件,遵循 MIT 许可证。任何个人或组织均可自由使用、修改和分发本项目的源代码。但本项目及其任何衍生作品**禁止用于任何商业或付费项目**。如有违反,将视为对本项目许可证的侵犯。欢迎大家在遵守开源精神和许可证的前提下积极贡献和分享代码。
|
||||||
pip install -r requirements.txt
|
|
||||||
再运行main.py文件即可
|
|
||||||
|
|
||||||
# 环境要求
|
---
|
||||||
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
|
### 1. 安装依赖
|
||||||
|
|
||||||
| 参数列表 | 参数说明 |
|
|
||||||
| ---- | ---- |
|
|
||||||
| url & ids | 解析获取到的网易云音乐地址 *任选其一|
|
|
||||||
| level | 音质参数(请看下方音质说明) |
|
|
||||||
| type | 解析类型 json down text *任选其一 |
|
|
||||||
|
|
||||||
# docker-compose一键部署
|
|
||||||
|
|
||||||
## 修改参数
|
|
||||||
|
|
||||||
部署前,可以根据需要修改`.env`文件中的环境变量
|
|
||||||
|
|
||||||
默认端口为`5000`,如果需要修改,请在`docker-compose.yml`文件中修改`ports`变量
|
|
||||||
|
|
||||||
例如,如果需要将端口修改为`8080`,请将以下代码:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
ports:
|
|
||||||
- "8080:5000"
|
|
||||||
```
|
|
||||||
|
|
||||||
## docker-compose一键启动
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker-compose up -d
|
pip install -r requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
# 音质说明
|
### 2. 配置 Cookie
|
||||||
standard(标准音质), exhigh(极高音质), lossless(无损音质), hires(Hi-Res音质), jyeffect(高清环绕声), sky(沉浸环绕声), jymaster(超清母带)
|
|
||||||
|
|
||||||
黑胶VIP音质选择 standard, exhigh, lossless, hires, jyeffect <br> <br>
|
请在 `cookie.txt` 文件中填入黑胶会员账号的 Cookie,格式如下:
|
||||||
黑胶SVIP音质选择 sky, jymaster
|
|
||||||
|
```
|
||||||
|
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)
|
[在线解析](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!
|
305
main.py
305
main.py
@ -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,93 +65,152 @@ 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:
|
||||||
namev1 = name_v1(urlv1['data'][0]['id'])
|
song_id = ids(jsondata)
|
||||||
lyricv1 = lyric_v1(urlv1['data'][0]['id'],cookies)
|
urlv1 = url_v1(song_id, level, cookies)
|
||||||
if urlv1['data'][0]['url'] is not None:
|
if not urlv1['data'] or urlv1['data'][0]['url'] is None:
|
||||||
if namev1['songs']:
|
return jsonify({"status": 400, 'msg': '信息获取不完整!'}), 400
|
||||||
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 + '<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
|
|
||||||
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)
|
|
||||||
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_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']
|
if type_ == 'text':
|
||||||
artist_names = ', '.join(artist['name'] for artist in namev1['songs'][0]['ar'])
|
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}'
|
||||||
album_name = namev1['songs'][0]['al']['name']
|
elif type_ == 'down':
|
||||||
music_quality = music_level1(urlv1['data'][0]['level'])
|
data = redirect(song_url)
|
||||||
file_size = size(urlv1['data'][0]['size'])
|
elif type_ == 'json':
|
||||||
music_url = urlv1['data'][0]['url']
|
data = {
|
||||||
lyrics = lyricv1['lrc']['lyric']
|
"status": 200,
|
||||||
translated_lyrics = lyricv1.get('tlyric', {}).get('lyric', None)
|
"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"""
|
@app.route('/Search', methods=['GET', 'POST'])
|
||||||
歌曲名称: {song_name}
|
def search():
|
||||||
歌曲图片: {song_pic}
|
if request.method == 'GET':
|
||||||
歌手: {artist_names}
|
keywords = request.args.get('keywords')
|
||||||
专辑名称: {album_name}
|
limit = request.args.get('limit', default=10, type=int)
|
||||||
音质: {music_quality}
|
else:
|
||||||
大小: {file_size}
|
keywords = request.form.get('keywords')
|
||||||
音乐链接: {music_url}
|
limit = int(request.form.get('limit', 10))
|
||||||
歌词: {lyrics}
|
if not keywords:
|
||||||
翻译歌词: {translated_lyrics if translated_lyrics else '没有翻译歌词'}
|
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:
|
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
213
music_api.py
Normal 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
|
||||||
|
"""
|
||||||
|
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
|
@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>网易云无损解析</title>
|
<title>网易云音乐工具箱</title>
|
||||||
<link href="https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/twitter-bootstrap/5.1.3/css/bootstrap.min.css" rel="stylesheet">
|
<link href="https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/twitter-bootstrap/5.1.3/css/bootstrap.min.css" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/aplayer/1.10.1/APlayer.min.css">
|
<link rel="stylesheet" href="https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/aplayer/1.10.1/APlayer.min.css">
|
||||||
<style>
|
<style>
|
||||||
@ -26,6 +26,24 @@
|
|||||||
background-color: #0056b3;
|
background-color: #0056b3;
|
||||||
border-color: #004085;
|
border-color: #004085;
|
||||||
}
|
}
|
||||||
|
.btn-success {
|
||||||
|
margin-top: 20px;
|
||||||
|
background-color: #28a745;
|
||||||
|
border-color: #28a745;
|
||||||
|
}
|
||||||
|
.btn-success:hover {
|
||||||
|
background-color: #218838;
|
||||||
|
border-color: #1e7e34;
|
||||||
|
}
|
||||||
|
.btn-warning {
|
||||||
|
margin-top: 20px;
|
||||||
|
background-color: #ffc107;
|
||||||
|
border-color: #ffc107;
|
||||||
|
}
|
||||||
|
.btn-warning:hover {
|
||||||
|
background-color: #e0a800;
|
||||||
|
border-color: #d39e00;
|
||||||
|
}
|
||||||
#song-info {
|
#song-info {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
@ -38,55 +56,192 @@
|
|||||||
color: #0c5460;
|
color: #0c5460;
|
||||||
border-color: #bee5eb;
|
border-color: #bee5eb;
|
||||||
}
|
}
|
||||||
|
/* 歌曲/歌单标题过长自动省略号 */
|
||||||
|
.song-title, .playlist-title {
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 180px;
|
||||||
|
vertical-align: middle;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
/* 歌单列表按钮不换行 */
|
||||||
|
.list-group-item .select-song {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
/* 歌词区域美化 */
|
||||||
|
.lyric-box {
|
||||||
|
max-height: 180px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: linear-gradient(90deg,#f7f7fa 60%,#f0f4fa 100%);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
font-size: 15px;
|
||||||
|
color: #222;
|
||||||
|
box-shadow: 0 1px 4px rgba(0,0,0,0.04);
|
||||||
|
line-height: 1.7;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container mt-5">
|
<div class="container mt-5">
|
||||||
<h1 class="text-center mb-4">网易云无损解析</h1>
|
<h1 class="text-center mb-4">网易云音乐工具箱</h1>
|
||||||
|
<div class="card shadow-sm">
|
||||||
<!-- 查询表单 -->
|
<div class="card-body">
|
||||||
<form id="query-form">
|
<form id="main-form">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="song_ids" class="form-label">歌曲 ID 或 URL</label>
|
<label class="form-label">功能选择</label>
|
||||||
<input type="text" id="song_ids" class="form-control" placeholder="输入歌曲 ID 或 URL">
|
<select id="mode-select" class="form-select">
|
||||||
|
<option value="search">歌曲搜索</option>
|
||||||
|
<option value="parse">单曲解析</option>
|
||||||
|
<option value="playlist">歌单解析</option>
|
||||||
|
<option value="album">专辑解析</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div id="search-area">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="search_keywords" class="form-label">搜索关键词</label>
|
||||||
|
<input type="text" id="search_keywords" class="form-control" placeholder="输入关键词进行搜索">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="search_limit" class="form-label">返回数量</label>
|
||||||
|
<input type="number" id="search_limit" class="form-control" value="10" min="1" max="50">
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<button type="button" id="search-btn" class="btn btn-success w-50">搜索</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="parse-area" style="display:none;">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="song_ids" class="form-label">歌曲ID或URL</label>
|
||||||
|
<input type="text" id="song_ids" class="form-control" placeholder="输入歌曲ID或URL">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="level" class="form-label">音质选择</label>
|
||||||
|
<select id="level" class="form-select">
|
||||||
|
<option value="standard">标准音质</option>
|
||||||
|
<option value="exhigh">极高音质</option>
|
||||||
|
<option value="lossless">无损音质</option>
|
||||||
|
<option value="hires">Hires音质</option>
|
||||||
|
<option value="sky">沉浸环绕声</option>
|
||||||
|
<option value="jyeffect">高清环绕声</option>
|
||||||
|
<option value="jymaster">超清母带</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<button type="button" id="parse-btn" class="btn btn-primary w-50">解析</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="playlist-area" style="display:none;">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="playlist_id" class="form-label">歌单ID或链接</label>
|
||||||
|
<input type="text" id="playlist_id" class="form-control" placeholder="输入歌单ID或网易云歌单链接">
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<button type="button" id="playlist-btn" class="btn btn-warning w-50">解析歌单</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="album-area" style="display:none;">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="album_id" class="form-label">专辑ID或链接</label>
|
||||||
|
<input type="text" id="album_id" class="form-control" placeholder="输入专辑ID或网易云专辑链接">
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<button type="button" id="album-btn" class="btn btn-info w-50">解析专辑</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
</div>
|
||||||
<label for="level" class="form-label">音质选择</label>
|
<!-- 搜索结果列表 -->
|
||||||
<select id="level" class="form-control">
|
<div id="search-result" class="mt-4 d-none">
|
||||||
<option value="standard">标准音质</option>
|
<h5>搜索结果:</h5>
|
||||||
<option value="exhigh">极高音质</option>
|
<ul class="list-group" id="search-list"></ul>
|
||||||
<option value="lossless">无损音质</option>
|
</div>
|
||||||
<option value="hires">Hires音质</option>
|
|
||||||
<option value="sky">沉浸环绕声</option>
|
|
||||||
<option value="jyeffect">高清环绕声</option>
|
|
||||||
<option value="jymaster">超清母带</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group text-center">
|
|
||||||
<button type="submit" class="btn btn-primary">提交</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<!-- 结果展示区域 -->
|
<!-- 结果展示区域 -->
|
||||||
<div id="song-info" class="alert alert-info d-none">
|
<div id="song-info" class="alert alert-info d-none mt-4 p-0 border-0" style="box-shadow:0 2px 8px rgba(0,0,0,0.07);">
|
||||||
<div id="aplayer" class="mb-3"></div>
|
<div class="row g-0 align-items-stretch">
|
||||||
<p><strong>歌名:</strong><span id="song_name"></span></p>
|
<div class="col-md-8 p-3 d-flex flex-column justify-content-between">
|
||||||
<p><strong>歌曲图片:</strong><a id="song_picUrl" href="" target="_blank">点击查看</a></p>
|
<h4 class="mb-2" id="song_name" style="font-weight:700;"></h4>
|
||||||
<p><strong>歌手:</strong><span id="artist_names"></span></p>
|
<div class="mb-2"><span class="badge bg-primary me-2">歌手</span><span id="artist_names"></span></div>
|
||||||
<p><strong>专辑:</strong><span id="song_alname"></span></p>
|
<div class="mb-2"><span class="badge bg-secondary me-2">专辑</span><span id="song_alname"></span></div>
|
||||||
<p><strong>音质:</strong><span id="song_level"></span></p>
|
<div class="mb-2"><span class="badge bg-success me-2">音质</span><span id="song_level"></span></div>
|
||||||
<p><strong>大小:</strong><span id="song_size"></span></p>
|
<div class="mb-2"><span class="badge bg-warning text-dark me-2">大小</span><span id="song_size"></span></div>
|
||||||
<p><strong>音乐链接:</strong><a id="song_url" href="" target="_blank">点击下载</a></p>
|
<div class="mb-2">
|
||||||
<p><strong>歌词:</strong><br><span id="lyric"></span></p>
|
<button id="show-big-pic" type="button" class="btn btn-outline-info btn-sm me-2" style="vertical-align:middle;">显示大图</button>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2"><span class="badge bg-info text-dark me-2">链接</span><a id="song_url" href="" target="_blank">点击下载</a></div>
|
||||||
|
<div class="mb-2"><span class="badge bg-dark me-2">歌词</span></div>
|
||||||
|
<div class="lyric-box" id="lyric"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row g-0">
|
||||||
|
<div class="col-12 p-3 pt-0">
|
||||||
|
<div id="aplayer"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 歌单解析结果 -->
|
||||||
|
<div id="playlist-result" class="mt-4 d-none">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex align-items-center mb-3">
|
||||||
|
<img id="playlist-cover" src="" alt="cover" style="width:60px;height:60px;object-fit:cover;border-radius:8px;margin-right:15px;">
|
||||||
|
<div>
|
||||||
|
<h5 id="playlist-name" class="mb-1"></h5>
|
||||||
|
<div class="text-muted" id="playlist-creator"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="playlist-desc" class="mb-2 text-secondary small"></div>
|
||||||
|
<div>共 <span id="playlist-count"></span> 首歌</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul class="list-group mt-3" id="playlist-tracks"></ul>
|
||||||
|
</div>
|
||||||
|
<!-- 专辑解析结果 -->
|
||||||
|
<div id="album-result" class="mt-4 d-none">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex align-items-center mb-3">
|
||||||
|
<img id="album-cover" src="" alt="cover" style="width:60px;height:60px;object-fit:cover;border-radius:8px;margin-right:15px;">
|
||||||
|
<div>
|
||||||
|
<h5 id="album-name" class="mb-1"></h5>
|
||||||
|
<div class="text-muted" id="album-artist"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="album-desc" class="mb-2 text-secondary small"></div>
|
||||||
|
<div>共 <span id="album-count"></span> 首歌</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul class="list-group mt-3" id="album-tracks"></ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Modal for big picture -->
|
||||||
|
<div class="modal fade" id="bigPicModal" tabindex="-1" aria-labelledby="bigPicModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="bigPicModalLabel">大图预览</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body text-center">
|
||||||
|
<img id="big-pic-img" src="" alt="大图" style="max-width:100%;max-height:60vh;border-radius:10px;box-shadow:0 2px 12px rgba(0,0,0,0.12);">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<footer class="footer mt-5 py-3 bg-light border-top">
|
||||||
|
<div class="container text-center text-muted small">
|
||||||
|
<span>网易云音乐工具箱 © 2025 | Powered by Suxiaoqingx & Bootstrap | <a href="https://github.com/Suxiaoqinx/Netease_url" target="_blank">GitHub</a></span>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
<script src="https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
|
<script src="https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
|
||||||
<script src="https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/aplayer/1.10.1/APlayer.min.js"></script>
|
<script src="https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/aplayer/1.10.1/APlayer.min.js"></script>
|
||||||
|
<script src="https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/twitter-bootstrap/5.1.3/js/bootstrap.bundle.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
|
|
||||||
function lrctrim(lyrics) {
|
function lrctrim(lyrics) {
|
||||||
const lines = lyrics.split('\n');
|
const lines = lyrics.split('\n');
|
||||||
const data = [];
|
const data = [];
|
||||||
@ -172,35 +327,117 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$('#query-form').on('submit', function(event) {
|
// 切换功能区
|
||||||
event.preventDefault();
|
$('#mode-select').on('change', function() {
|
||||||
|
if ($(this).val() === 'search') {
|
||||||
|
$('#search-area').show();
|
||||||
|
$('#parse-area').hide();
|
||||||
|
$('#playlist-area').hide();
|
||||||
|
$('#album-area').hide();
|
||||||
|
$('#song-info').addClass('d-none');
|
||||||
|
$('#playlist-result').addClass('d-none');
|
||||||
|
$('#album-result').addClass('d-none');
|
||||||
|
} else if ($(this).val() === 'parse') {
|
||||||
|
$('#search-area').hide();
|
||||||
|
$('#parse-area').show();
|
||||||
|
$('#playlist-area').hide();
|
||||||
|
$('#album-area').hide();
|
||||||
|
$('#search-result').addClass('d-none');
|
||||||
|
$('#playlist-result').addClass('d-none');
|
||||||
|
$('#album-result').addClass('d-none');
|
||||||
|
} else if ($(this).val() === 'playlist') {
|
||||||
|
$('#search-area').hide();
|
||||||
|
$('#parse-area').hide();
|
||||||
|
$('#playlist-area').show();
|
||||||
|
$('#album-area').hide();
|
||||||
|
$('#search-result').addClass('d-none');
|
||||||
|
$('#song-info').addClass('d-none');
|
||||||
|
$('#album-result').addClass('d-none');
|
||||||
|
} else if ($(this).val() === 'album') {
|
||||||
|
$('#search-area').hide();
|
||||||
|
$('#parse-area').hide();
|
||||||
|
$('#playlist-area').hide();
|
||||||
|
$('#album-area').show();
|
||||||
|
$('#search-result').addClass('d-none');
|
||||||
|
$('#song-info').addClass('d-none');
|
||||||
|
$('#playlist-result').addClass('d-none');
|
||||||
|
$('#album-result').addClass('d-none');
|
||||||
|
} else {
|
||||||
|
$('#album-area').hide();
|
||||||
|
$('#album-result').addClass('d-none');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const songIds = $('#song_ids').val();
|
// 搜索功能
|
||||||
const level = $('#level').val();
|
$('#search-btn').on('click', function() {
|
||||||
const validId = extractAndCheckId(songIds);
|
const keywords = $('#search_keywords').val();
|
||||||
|
const limit = $('#search_limit').val();
|
||||||
if (!validId) {
|
if (!keywords) {
|
||||||
alert('Invalid link or ID');
|
alert('请输入搜索关键词');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
$.ajax({
|
||||||
|
url: '/Search',
|
||||||
|
method: 'GET',
|
||||||
|
data: { keywords: keywords, limit: limit },
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(data) {
|
||||||
|
if (data.status === 200 && data.result.length > 0) {
|
||||||
|
$('#search-list').empty();
|
||||||
|
data.result.forEach(function(song) {
|
||||||
|
const item = `<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<img src="${song.picUrl}" alt="cover" style="width:40px;height:40px;object-fit:cover;border-radius:4px;margin-right:10px;">
|
||||||
|
<strong class='song-title'>${song.name}</strong> - <span>${song.artists}</span> <span class="text-muted">[${song.album}]</span>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-outline-primary select-song" data-id="${song.id}" data-name="${song.name}">解析</button>
|
||||||
|
</li>`;
|
||||||
|
$('#search-list').append(item);
|
||||||
|
});
|
||||||
|
$('#search-result').removeClass('d-none');
|
||||||
|
} else {
|
||||||
|
$('#search-list').html('<li class="list-group-item">未找到相关歌曲</li>');
|
||||||
|
$('#search-result').removeClass('d-none');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
$('#search-list').html('<li class="list-group-item">搜索失败,请重试</li>');
|
||||||
|
$('#search-result').removeClass('d-none');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
$.post('/Song_V1', { url: validId, level: level, type:'json' }, function(data) {
|
// 搜索结果点击解析
|
||||||
|
$(document).on('click', '.select-song', function() {
|
||||||
|
const songId = $(this).data('id');
|
||||||
|
$('#song_ids').val(songId);
|
||||||
|
$('#mode-select').val('parse').trigger('change');
|
||||||
|
$('html,body').animate({scrollTop: $('#main-form').offset().top}, 300);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 单曲解析
|
||||||
|
$('#parse-btn').on('click', function() {
|
||||||
|
const songIds = $('#song_ids').val();
|
||||||
|
const level = $('#level').val();
|
||||||
|
if (!songIds) {
|
||||||
|
alert('请输入歌曲ID或URL');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$.post('/Song_V1', { url: songIds, level: level, type:'json' }, function(data) {
|
||||||
if (data.status === 200) {
|
if (data.status === 200) {
|
||||||
$('#song_name').text(data.name);
|
$('#song_name').text(data.name);
|
||||||
$('#song_picUrl').attr('href', data.pic).text('点击查看');
|
|
||||||
$('#artist_names').text(data.ar_name);
|
$('#artist_names').text(data.ar_name);
|
||||||
$('#song_alname').text(data.al_name);
|
$('#song_alname').text(data.al_name);
|
||||||
$('#song_level').text(data.level);
|
$('#song_level').text(data.level);
|
||||||
$('#song_size').text(data.size);
|
$('#song_size').text(data.size);
|
||||||
|
let processedLyrics = data.lyric;
|
||||||
if (data.tlyric) {
|
if (data.tlyric) {
|
||||||
processedLyrics = lrctran(data.lyric, data.tlyric);
|
processedLyrics = lrctran(data.lyric, data.tlyric);
|
||||||
} else {
|
|
||||||
processedLyrics = data.lyric;
|
|
||||||
}
|
}
|
||||||
$('#lyric').html(processedLyrics.replace(/\n/g, '<br>'));
|
$('#lyric').html(processedLyrics.replace(/\n/g, '<br>'));
|
||||||
$('#song_url').attr('href', data.url).text('点击下载');
|
$('#song_url').attr('href', data.url).text('点击下载');
|
||||||
$('#song-info').removeClass('d-none');
|
$('#song-info').removeClass('d-none');
|
||||||
|
$('#show-big-pic').data('pic', data.pic);
|
||||||
new APlayer({
|
new APlayer({
|
||||||
container: document.getElementById('aplayer'),
|
container: document.getElementById('aplayer'),
|
||||||
lrcType: 1,
|
lrcType: 1,
|
||||||
@ -217,6 +454,88 @@
|
|||||||
}
|
}
|
||||||
}, 'json');
|
}, 'json');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 显示大图按钮事件
|
||||||
|
$(document).on('click', '#show-big-pic', function() {
|
||||||
|
var picUrl = $(this).data('pic');
|
||||||
|
$('#big-pic-img').attr('src', picUrl);
|
||||||
|
var modal = new bootstrap.Modal(document.getElementById('bigPicModal'));
|
||||||
|
modal.show();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 歌单解析
|
||||||
|
$('#playlist-btn').on('click', function() {
|
||||||
|
let pid = $('#playlist_id').val().trim();
|
||||||
|
if (!pid) {
|
||||||
|
alert('请输入歌单ID或链接');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 支持直接粘贴歌单链接
|
||||||
|
const idMatch = pid.match(/playlist\?id=(\d+)/);
|
||||||
|
if (idMatch) pid = idMatch[1];
|
||||||
|
$.get('/Playlist', { id: pid }, function(data) {
|
||||||
|
if (data.status === 200) {
|
||||||
|
const pl = data.playlist;
|
||||||
|
$('#playlist-cover').attr('src', pl.coverImgUrl);
|
||||||
|
$('#playlist-name').text(pl.name);
|
||||||
|
$('#playlist-creator').text('by ' + pl.creator);
|
||||||
|
$('#playlist-desc').text(pl.description || '');
|
||||||
|
$('#playlist-count').text(pl.trackCount);
|
||||||
|
$('#playlist-tracks').empty();
|
||||||
|
pl.tracks.forEach(function(song, idx) {
|
||||||
|
const item = `<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<img src="${song.picUrl}" alt="cover" style="width:32px;height:32px;object-fit:cover;border-radius:4px;margin-right:8px;">
|
||||||
|
<strong class="playlist-title">${idx+1}. ${song.name}</strong> - <span>${song.artists}</span> <span class="text-muted">[${song.album}]</span>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-outline-primary select-song" data-id="${song.id}" data-name="${song.name}">解析</button>
|
||||||
|
</li>`;
|
||||||
|
$('#playlist-tracks').append(item);
|
||||||
|
});
|
||||||
|
$('#playlist-result').removeClass('d-none');
|
||||||
|
} else {
|
||||||
|
$('#playlist-result').removeClass('d-none');
|
||||||
|
$('#playlist-tracks').html('<li class="list-group-item">歌单解析失败:'+data.msg+'</li>');
|
||||||
|
}
|
||||||
|
}, 'json');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 专辑解析
|
||||||
|
$(document).on('click', '#album-btn', function() {
|
||||||
|
let aid = $('#album_id').val().trim();
|
||||||
|
if (!aid) {
|
||||||
|
alert('请输入专辑ID或链接');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 支持直接粘贴专辑链接
|
||||||
|
const idMatch = aid.match(/album\?id=(\d+)/);
|
||||||
|
if (idMatch) aid = idMatch[1];
|
||||||
|
$.get('/Album', { id: aid }, function(data) {
|
||||||
|
if (data.status === 200) {
|
||||||
|
const al = data.album;
|
||||||
|
$('#album-cover').attr('src', al.coverImgUrl);
|
||||||
|
$('#album-name').text(al.name);
|
||||||
|
$('#album-artist').text(al.artist);
|
||||||
|
$('#album-desc').text(al.description || '');
|
||||||
|
$('#album-count').text(al.songs.length);
|
||||||
|
$('#album-tracks').empty();
|
||||||
|
al.songs.forEach(function(song, idx) {
|
||||||
|
const item = `<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<img src="${song.picUrl}" alt="cover" style="width:32px;height:32px;object-fit:cover;border-radius:4px;margin-right:8px;">
|
||||||
|
<strong class="playlist-title">${idx+1}. ${song.name}</strong> - <span>${song.artists}</span> <span class="text-muted">[${song.album}]</span>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-outline-primary select-song" data-id="${song.id}" data-name="${song.name}">解析</button>
|
||||||
|
</li>`;
|
||||||
|
$('#album-tracks').append(item);
|
||||||
|
});
|
||||||
|
$('#album-result').removeClass('d-none');
|
||||||
|
} else {
|
||||||
|
$('#album-result').removeClass('d-none');
|
||||||
|
$('#album-tracks').html('<li class="list-group-item">专辑解析失败:'+data.msg+'</li>');
|
||||||
|
}
|
||||||
|
}, 'json');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
Loading…
Reference in New Issue
Block a user