Skip to content

Commit

Permalink
Update QQ music lyrics acquisition api
Browse files Browse the repository at this point in the history
  • Loading branch information
chenmozhijin committed Mar 9, 2024
1 parent b1f8639 commit 364d791
Show file tree
Hide file tree
Showing 8 changed files with 80 additions and 146 deletions.
2 changes: 0 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
requests
bs4
PySide6
lxml
mutagen
diskcache
chardet
Expand Down
7 changes: 0 additions & 7 deletions ui/settings.ui
Original file line number Diff line number Diff line change
Expand Up @@ -144,13 +144,6 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="get_normal_lyrics_checkBox">
<property name="text">
<string>没有可用的加密歌词时尝试获取普通歌词</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="auto_select_checkBox">
<property name="text">
Expand Down
6 changes: 0 additions & 6 deletions ui/settings_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,6 @@ def setupUi(self, settings):

self.verticalLayout_6.addWidget(self.skip_inst_lyrics_checkBox)

self.get_normal_lyrics_checkBox = QCheckBox(self.groupBox_2)
self.get_normal_lyrics_checkBox.setObjectName(u"get_normal_lyrics_checkBox")

self.verticalLayout_6.addWidget(self.get_normal_lyrics_checkBox)

self.auto_select_checkBox = QCheckBox(self.groupBox_2)
self.auto_select_checkBox.setObjectName(u"auto_select_checkBox")

Expand Down Expand Up @@ -273,7 +268,6 @@ def retranslateUi(self, settings):
self.lyrics_order_listWidget.setSortingEnabled(__sortingEnabled)

self.skip_inst_lyrics_checkBox.setText(QCoreApplication.translate("settings", u"\u4fdd\u5b58\u4e13\u8f91/\u6b4c\u5355\u6b4c\u8bcd/\u672c\u5730\u5339\u914d\u65f6\u8df3\u8fc7\u7eaf\u97f3\u4e50", None))
self.get_normal_lyrics_checkBox.setText(QCoreApplication.translate("settings", u"\u6ca1\u6709\u53ef\u7528\u7684\u52a0\u5bc6\u6b4c\u8bcd\u65f6\u5c1d\u8bd5\u83b7\u53d6\u666e\u901a\u6b4c\u8bcd", None))
self.auto_select_checkBox.setText(QCoreApplication.translate("settings", u"\u6b4c\u66f2\u641c\u7d22\u6b4c\u8bcd\u65f6\u81ea\u52a8\u9009\u62e9(\u9177\u72d7\u97f3\u4e50)", None))
self.groupBox.setTitle(QCoreApplication.translate("settings", u"\u4fdd\u5b58\u8bbe\u7f6e", None))
self.textBrowser.setHtml(QCoreApplication.translate("settings", u"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0//EN\" \"http://www.w3.org/TR/REC-html40/strict.dtd\">\n"
Expand Down
116 changes: 59 additions & 57 deletions utils/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import random
import re
import time
from base64 import b64decode
from base64 import b64decode, b64encode
from enum import Enum

import requests
Expand Down Expand Up @@ -289,66 +289,68 @@ def qmsonglist2result(songlist: list, list_type: str | None = None) -> list:
return results


def get_qrc(songid: str) -> str | requests.Response:
params = {
'version': '15',
'miniversion': '82',
'lrctype': '4',
'musicid': songid,
}
try:
response = requests.get('https://c.y.qq.com/qqmusic/fcgi-bin/lyric_download.fcg',
headers=QMD_headers,
params=params,
timeout=10)
response.raise_for_status()
except requests.exceptions.RequestException as e:
logging.exception("请求qrc 歌词时错误")
return str(e)
else:
logging.debug(f"请求qrc 歌词成功:{songid}, {response.text.strip()}")
return response


def qm_get_lyric(mid: str) -> tuple[str | None, str | None] | str:
url = 'https://c.y.qq.com/lyric/fcgi-bin/fcg_query_lyric_new.fcg'
headers = {
'Accept': '*/*',
'Accept-Encoding': 'gzip, deflate, br',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
'Referer': 'https://y.qq.com/',
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_3_1 like Mac OS X; zh-CN) AppleWebKit/537.51.1 ('
'KHTML, like Gecko) Mobile/17D50 UCBrowser/12.8.2.1268 Mobile AliApp(TUnionSDK/0.1.20.3) ',
}
params = {
'_': int(time.time()),
'cv': '4747474',
'ct': '24',
'format': 'json',
'inCharset': 'utf-8',
'outCharset': 'utf-8',
'notice': '0',
'platform': 'yqq.json',
'needNewCode': '1',
'g_tk': '5381',
'songmid': mid,
}
def qm_get_lyric(info: dict[str: str]) -> dict:
"""
获取歌词
:param info:歌曲信息
:return: 歌词信息
"""
if 'album' not in info or 'artist' not in info or 'title' not in info or 'id' not in info or 'duration' not in info:
return "缺少必要参数"
base64_album_name = b64encode(info['album'].encode()).decode()
base64_singer_name = b64encode(info['artist'].split("/")[0].encode()).decode() if "/" in info["artist"] else b64encode(info["artist"].encode()).decode()
base64_song_name = b64encode(info["title"].encode()).decode()

data = json.dumps({
"comm": {
"_channelid": "0",
"_os_version": "6.2.9200-2",
"authst": "",
"ct": "19",
"cv": "1942",
"patch": "118",
"psrf_access_token_expiresAt": 0,
"psrf_qqaccess_token": "",
"psrf_qqopenid": "",
"psrf_qqunionid": "",
"tmeAppID": "qqmusic",
"tmeLoginType": 0,
"uin": "0",
"wid": "0",
},
"music.musichallSong.PlayLyricInfo.GetPlayLyricInfo": {
"method": "GetPlayLyricInfo",
"module": "music.musichallSong.PlayLyricInfo",
"param": {
"albumName": base64_album_name,
"crypt": 1,
"ct": 19,
"cv": 1942,
"interval": info['duration'],
"lrc_t": 0,
"qrc": 1,
"qrc_t": 0,
"roma": 1,
"roma_t": 0,
"singerName": base64_singer_name,
"songID": int(info['id']),
"songName": base64_song_name,
"trans": 1,
"trans_t": 0,
"type": 0,
},
},
}, ensure_ascii=False).encode("utf-8")
try:
response = requests.get(url, params=params, headers=headers, timeout=10)
response = requests.post('https://u.y.qq.com/cgi-bin/musicu.fcg', headers=QMD_headers, data=data, timeout=10)
response.raise_for_status()
response_json = response.json()
if 'lyric' not in response_json or 'trans' not in response_json:
return f'获取歌词失败, code: {response_json["code"]}'
orig_base64 = response_json['lyric']
ts_base64 = response_json['trans']
orig = b64decode(orig_base64).decode("utf-8") if orig_base64 else None
ts = b64decode(ts_base64).decode("utf-8") if ts_base64 else None
data: dict = response.json()['music.musichallSong.PlayLyricInfo.GetPlayLyricInfo']['data']
except Exception as e:
logging.exception("请求歌词时错误")
return str(e)
logging.exception("获取歌词失败")
return f"获取歌词失败:{e}"
else:
logging.debug(f"请求歌词成功, orig: {orig}, ts: {ts}")
return orig, ts
logging.debug(f"请求qm歌词成功:{info['id']}, {json.dumps(data, ensure_ascii=False, indent=4)}")
return data


def qm_search(keyword: str, search_type: SearchType) -> list | str:
Expand Down
8 changes: 6 additions & 2 deletions utils/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ def __init__(self, current_directory: str) -> None:
"default_save_path": default_save_path,
"lyrics_order": ["罗马音", "原文", "译文"],
"skip_inst_lyrics": True,
"get_normal_lyrics": True,
"auto_select": True,
}
self.init_db()
Expand Down Expand Up @@ -77,7 +76,12 @@ def read_config(self) -> None:
self.mutex.lock()
c = self.conn.cursor()
c.execute("SELECT * FROM config")
for setting in c.fetchall():
settings = c.fetchall().copy()
for setting in settings:
if setting[1] not in self.cfg:
c.execute("DELETE FROM config WHERE item=?", (setting[1],))
self.conn.commit() # 提交更改
continue
if isinstance(self.cfg[setting[1]], str):
self.cfg[setting[1]] = setting[2]
elif isinstance(self.cfg[setting[1]], bool):
Expand Down
63 changes: 13 additions & 50 deletions utils/lyrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,8 @@
from base64 import b64decode
from enum import Enum

from bs4 import BeautifulSoup

from decryptor import QrcType, krc_decrypt, qrc_decrypt
from utils.api import Source, get_krc, get_qrc, ne_get_lyric, qm_get_lyric
from utils.api import Source, get_krc, ne_get_lyric, qm_get_lyric
from utils.utils import ms2formattime, time2ms


Expand Down Expand Up @@ -424,6 +422,7 @@ def __init__(self, info: dict | None = None) -> None:
self.album = info.get("album", None)
self.id = info.get("id", None)
self.mid = info.get("mid", None)
self.duration = info.get("duration", None)
self.accesskey = info.get("accesskey", None)

self.lrc_types = {}
Expand All @@ -440,32 +439,26 @@ def download_and_decrypt(self) -> tuple[str | None, LyricProcessingError | None]

match self.source:
case Source.QM:
response = get_qrc(self.id)
response = qm_get_lyric({'album': self.album, 'artist': self.artist, 'title': self.title, 'id': self.id, 'duration': self.duration})
if isinstance(response, str):
return f"请求qrc歌词失败,错误:{response}", LyricProcessingError.REQUEST
qrc_xml = re.sub(r"^<!--|-->$", "", response.text.strip())
qrc_suop = BeautifulSoup(qrc_xml, 'xml')
for key, value in [("orig", 'content'),
("ts", 'contentts'),
("roma", 'contentroma')]:
find_result = qrc_suop.find(value)
if find_result is not None and find_result['timetag'] != "0":
encrypted_lyric = find_result.get_text()

cannot_decrypt = ["789C014000BFFF", "789C014800B7FF"]
for c in cannot_decrypt:
if encrypted_lyric.startswith(c):
return f"没有获取到可解密的歌词(encrypted_lyric starts with {c})", LyricProcessingError.NOT_FOUND
for key, value in [("orig", 'lyric'),
("ts", 'trans'),
("roma", 'roma')]:
lrc = response[value]
lrc_t = (response["qrc_t"] if response["qrc_t"] != 0 else response["lrc_t"]) if value == "lyric" else response[value + "_t"]
if lrc != "" and lrc_t != "0":
encrypted_lyric = lrc

lyric, error = qrc_decrypt(encrypted_lyric, QrcType.CLOUD)

if lyric is not None:
lrc_type = judge_lyric_type(lyric)
if lrc_type == LyricType.QRC:
tags, lyric = qrc2list(lyric)
elif LyricType.LRC:
elif lrc_type == LyricType.LRC:
tags, lyric = lrc2list(lyric)
elif LyricType.PlainText:
elif lrc_type == LyricType.PlainText:
tags = {}
lyric = plaintext2list(lyric)
self.lrc_types[key] = lrc_type
Expand All @@ -476,7 +469,7 @@ def download_and_decrypt(self) -> tuple[str | None, LyricProcessingError | None]
self[key] = lyric
elif error is not None:
return "解密歌词失败, 错误: " + error, LyricProcessingError.DECRYPT
elif (find_result['timetag'] == "0" and key == "orig"):
elif (lrc_t == "0" and key == "orig"):
return "没有获取到可解密的歌词(timetag=0)", LyricProcessingError.NOT_FOUND

case Source.KG:
Expand Down Expand Up @@ -549,36 +542,6 @@ def download_and_decrypt(self) -> tuple[str | None, LyricProcessingError | None]
return "没有获取到的歌词(orig=None)", LyricProcessingError.NOT_FOUND
return None, None

def download_normal_lyrics(self) -> tuple[str | None, LyricProcessingError | None]:
result = qm_get_lyric(self.mid)
if isinstance(result, str):
return f"请求普通歌词时错误: {result}", LyricProcessingError.REQUEST
orig, ts = result
if orig is not None:
if judge_lyric_type(orig) == LyricType.LRC:
self.tags, self["orig"] = lrc2list(orig)
self.lrc_types["orig"] = LyricType.LRC
else:
self["orig"] = plaintext2list(orig)
self.lrc_types["orig"] = LyricType.PlainText
if ts is not None:
if judge_lyric_type(ts) == LyricType.LRC:
tags, self["ts"] = lrc2list(ts)
self.lrc_types["ts"] = LyricType.LRC
else:
self["ts"] = plaintext2list(ts)
self.lrc_types["ts"] = LyricType.PlainText
if not self.tags:
self.tags = tags

for key, lrc in self.items():
# 判断是否逐字
self.lrc_isverbatim[key] = is_verbatim(lrc)

if self["orig"] is None and self["ts"] is None:
return "没有获取到可用的歌词(orig=None and ts=None)", LyricProcessingError.NOT_FOUND
return None, None

def get_merge_lrc(self, lyrics_order: list) -> str:
"""
合并歌词
Expand Down
20 changes: 2 additions & 18 deletions utils/worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,24 +211,8 @@ def get_lyrics(self, song_info: dict) -> tuple[None | Lyrics, bool]:
song_name_str = "歌名:" + song_info['title']
if error1 is not None:
logging.error(f"获取歌词失败:{song_name_str}, 源:{song_info['source']}, id: {song_info['id']},错误:{error1}")

self.data_mutex.lock()
get_normal_lyrics = bool(self.data.cfg["get_normal_lyrics"] and song_info['source'] == Source.QM)
self.data_mutex.unlock()
if get_normal_lyrics:
logging.info(f"尝试获取普通歌词:{song_name_str},源:{song_info['source']}, id: {song_info['id']}")

for _i in range(3): # 重试3次
error2, error2_type = lyrics.download_normal_lyrics()
if error2_type != LyricProcessingError.REQUEST: # 如果正常或不是请求错误不重试
break

if error2 is not None:
self.signals.error.emit(f"歌名:{song_name_str}的歌词获取失败:\n错误1:{error1}\n错误2:{error2}")
return None, False
else:
self.signals.error.emit(f"获取歌名:{song_name_str}的加密歌词失败:{error1}")
return None, False
self.signals.error.emit(f"获取歌名:{song_name_str}的加密歌词失败:{error1}")
return None, False

if error1_type != LyricProcessingError.REQUEST and not from_cache: # 如果不是请求错误则缓存
cache[("lyrics", song_info["source"], song_info['id'], song_info.get("accesskey", ""))] = lyrics
Expand Down
4 changes: 0 additions & 4 deletions view/setting.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ def init_ui(self) -> None:
self.default_save_path_lineEdit.setText(self.data.cfg["default_save_path"])
self.log_level_comboBox.setCurrentText(self.data.cfg["log_level"])
self.skip_inst_lyrics_checkBox.setChecked(self.data.cfg["skip_inst_lyrics"])
self.get_normal_lyrics_checkBox.setChecked(self.data.cfg["get_normal_lyrics"])
self.auto_select_checkBox.setChecked(self.data.cfg["auto_select"])

def select_default_save_path(self) -> None:
Expand Down Expand Up @@ -57,8 +56,5 @@ def connect_signals(self) -> None:
self.skip_inst_lyrics_checkBox.stateChanged.connect(
lambda: self.data.write_config("skip_inst_lyrics", self.skip_inst_lyrics_checkBox.isChecked()))

self.get_normal_lyrics_checkBox.stateChanged.connect(
lambda: self.data.write_config("get_normal_lyrics", self.get_normal_lyrics_checkBox.isChecked()))

self.auto_select_checkBox.stateChanged.connect(
lambda: self.data.write_config("auto_select", self.auto_select_checkBox.isChecked()))

0 comments on commit 364d791

Please sign in to comment.