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
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 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:
response = requests.get(ids, allow_redirects=False)
ids = response.headers.get('Location')
if 'music.163.com' in ids:
index = ids.find('id=') + 3
ids = ids[index:].split('&')[0]
return ids
def size(value):
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
def music_level1(value):
levels = {
'standard': "标准音质",
'exhigh': "极高音质",
'lossless': "无损音质",
'hires': "Hires音质",
'sky': "沉浸环绕声",
'jyeffect': "高清环绕声",
'jymaster': "超清母带"
}
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 应用部分
app = Flask(__name__)
@app.after_request
def after_request(response):
response.headers.add('Access-Control-Allow-Origin', '*')
response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization')
response.headers.add('Access-Control-Allow-Methods', 'GET,POST,OPTIONS')
return response
@app.route('/', methods=['GET', 'POST'])
def index():
return render_template('index.html')
@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')
level = request.args.get('level')
type_ = request.args.get('type')
else:
song_ids = request.form.get('ids')
url = request.form.get('url')
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:
return jsonify({'error': 'level参数为空'}), 400
if type_ is None:
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)
namev1 = name_v1(urlv1['data'][0]['id'])
lyricv1 = lyric_v1(urlv1['data'][0]['id'], cookies)
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)
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)
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")
parser.add_argument('--url', help="提供 URL 参数供 GUI 模式使用")
parser.add_argument('--level', default='lossless', choices=['standard', 'exhigh', 'lossless', 'hires', 'sky', 'jyeffect', 'jymaster'], help="选择音质等级,默认是 lossless")
args = parser.parse_args()
if args.mode == 'api':
start_api()
elif args.mode == 'gui':
start_gui(args.url, args.level)