diff --git a/LDDC.py b/LDDC.py
index e2b5e09..b4e2132 100644
--- a/LDDC.py
+++ b/LDDC.py
@@ -1,6 +1,6 @@
# SPDX-License-Identifier: GPL-3.0-or-later
# SPDX-FileCopyrightText: Copyright (c) 2024 沉默の金
-__version__ = "v0.2.1"
+__version__ = "v0.2.2"
import logging
import os
import sys
@@ -26,6 +26,7 @@
str2log_level,
)
from view.about import AboutWidget
+from view.encrypted_lyrics import EncryptedLyricsWidget
from view.search import SearchWidget
from view.setting import SettingWidget
from worker import CheckUpdate
@@ -55,13 +56,15 @@ def __init__(self) -> None:
self.setWindowTitle("LDDC")
self.resize(1050, 600)
self.setWindowIcon(QIcon(":/LDDC/img/icon/logo.png"))
- self.set_sidebar_width(70)
+ self.set_sidebar_width(80)
self.search_widget = SearchWidget(self, data, data_mutex, threadpool)
self.settings_widget = SettingWidget(data, logger)
self.about_widget = AboutWidget(__version__)
+ self.encrypted_lyrics_widget = EncryptedLyricsWidget()
self.add_widget("搜索", self.search_widget)
+ self.add_widget("打开\n加密歌词", self.encrypted_lyrics_widget)
self.add_widget("关于", self.about_widget, SBPosition.BOTTOM)
self.add_widget("设置", self.settings_widget, SBPosition.BOTTOM)
self.connect_signals()
diff --git a/README.md b/README.md
index 12172dc..20c3abc 100644
--- a/README.md
+++ b/README.md
@@ -74,6 +74,8 @@
### 歌词解密
[![Readme Card](https://github-readme-stats.vercel.app/api/pin/?username=WXRIW&repo=Lyricify-Lyrics-Helper)](https://github.com/WXRIW/Lyricify-Lyrics-Helper)
+[![Readme Card](https://github-readme-stats.vercel.app/api/pin/?username=jixunmoe&repo=qmc-decode)](https://github.com/jixunmoe/qmc-decode)
+[![Readme Card](https://github-readme-stats.vercel.app/api/pin/?username=parakeet-rs&repo=libparakeet)](https://github.com/parakeet-rs/libparakeet)
### QQ音乐api
diff --git a/api.py b/api.py
index 720301c..229ffc1 100644
--- a/api.py
+++ b/api.py
@@ -70,6 +70,7 @@ def get_qrc(songid: str) -> str | requests.Response:
}
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()
diff --git a/decryptor/__init__.py b/decryptor/__init__.py
new file mode 100644
index 0000000..d2e56e1
--- /dev/null
+++ b/decryptor/__init__.py
@@ -0,0 +1,53 @@
+import logging
+from enum import Enum
+from zlib import decompress
+
+from decryptor.qmc1 import qmc1_decrypt
+from decryptor.tripledes import DECRYPT, tripledes_crypt, tripledes_key_setup
+
+
+class QrcType(Enum):
+ LOCAL = 0
+ CLOUD = 1
+
+
+def qrc_decrypt(encrypted_qrc: str | bytearray | bytes, qrc_type: int) -> tuple[str | None, str | None]:
+ if encrypted_qrc is None or encrypted_qrc.strip() == "":
+ logging.error("没有可解密的数据")
+ return None, None
+ try:
+ key = bytearray(b"!@#)(*$%123ZXC!@!@#)(NHL")
+
+ if isinstance(encrypted_qrc, str):
+ encrypted_text_byte = bytearray.fromhex(encrypted_qrc) # 将文本解析为字节数组
+ elif isinstance(encrypted_qrc, bytearray):
+ encrypted_text_byte = encrypted_qrc
+ elif isinstance(encrypted_qrc, bytes):
+ encrypted_text_byte = bytearray(encrypted_qrc)
+ else:
+ logging.error("无效的加密数据类型")
+ return None, "无效的加密数据类型"
+
+ if qrc_type == QrcType.LOCAL:
+ qmc1_decrypt(encrypted_text_byte)
+ encrypted_text_byte = encrypted_text_byte[11:]
+
+ data = bytearray(len(encrypted_text_byte))
+ schedule = [[[0] * 6 for _ in range(16)] for _ in range(3)]
+ tripledes_key_setup(key, schedule, DECRYPT)
+
+ # 以 8 字节为单位迭代 encrypted_text_byte
+ for i in range(0, len(encrypted_text_byte), 8):
+ temp = bytearray(8)
+
+ tripledes_crypt(encrypted_text_byte[i:], temp, schedule)
+
+ # 将结果复制到数据数组
+ for j in range(8):
+ data[i + j] = temp[j]
+
+ decrypted_qrc = decompress(data).decode("utf-8")
+ except Exception as e:
+ logging.exception("解密失败")
+ return None, str(e)
+ return decrypted_qrc, None
diff --git a/decryptor/qmc1.py b/decryptor/qmc1.py
new file mode 100644
index 0000000..0cea09d
--- /dev/null
+++ b/decryptor/qmc1.py
@@ -0,0 +1,36 @@
+# 参考原C代码:
+# https://github.com/jixunmoe/qmc-decode/blob/master/src/qmc_crypto.c
+
+# SPDX-License-Identifier: GPL-3.0-or-later
+# SPDX-FileCopyrightText: Copyright (c) 2024 沉默の金
+
+PRIVKey = [
+ 0xc3, 0x4a, 0xd6, 0xca, 0x90, 0x67, 0xf7, 0x52,
+ 0xd8, 0xa1, 0x66, 0x62, 0x9f, 0x5b, 0x09, 0x00,
+
+ 0xc3, 0x5e, 0x95, 0x23, 0x9f, 0x13, 0x11, 0x7e,
+ 0xd8, 0x92, 0x3f, 0xbc, 0x90, 0xbb, 0x74, 0x0e,
+
+ 0xc3, 0x47, 0x74, 0x3d, 0x90, 0xaa, 0x3f, 0x51,
+ 0xd8, 0xf4, 0x11, 0x84, 0x9f, 0xde, 0x95, 0x1d,
+
+ 0xc3, 0xc6, 0x09, 0xd5, 0x9f, 0xfa, 0x66, 0xf9,
+ 0xd8, 0xf0, 0xf7, 0xa0, 0x90, 0xa1, 0xd6, 0xf3,
+
+ 0xc3, 0xf3, 0xd6, 0xa1, 0x90, 0xa0, 0xf7, 0xf0,
+ 0xd8, 0xf9, 0x66, 0xfa, 0x9f, 0xd5, 0x09, 0xc6,
+
+ 0xc3, 0x1d, 0x95, 0xde, 0x9f, 0x84, 0x11, 0xf4,
+ 0xd8, 0x51, 0x3f, 0xaa, 0x90, 0x3d, 0x74, 0x47,
+
+ 0xc3, 0x0e, 0x74, 0xbb, 0x90, 0xbc, 0x3f, 0x92,
+ 0xd8, 0x7e, 0x11, 0x13, 0x9f, 0x23, 0x95, 0x5e,
+
+ 0xc3, 0x00, 0x09, 0x5b, 0x9f, 0x62, 0x66, 0xa1,
+ 0xd8, 0x52, 0xf7, 0x67, 0x90, 0xca, 0xd6, 0x4a,
+]
+
+
+def qmc1_decrypt(data: bytearray) -> None:
+ for i in range(len(data)):
+ data[i] ^= PRIVKey[(i % 0x7FFF) & 0x7F] if i > 0x7FFF else PRIVKey[i & 0x7F] # noqa: PLR2004
diff --git a/decryptor.py b/decryptor/tripledes.py
similarity index 92%
rename from decryptor.py
rename to decryptor/tripledes.py
index a273b4c..7b8d405 100644
--- a/decryptor.py
+++ b/decryptor/tripledes.py
@@ -4,8 +4,6 @@
# SPDX-License-Identifier: GPL-3.0-or-later
# SPDX-FileCopyrightText: Copyright (c) 2024 沉默の金
-import logging
-from zlib import decompress
ENCRYPT = 1
DECRYPT = 0
@@ -258,32 +256,3 @@ def tripledes_crypt(input_data: bytearray, output: bytearray, key: list) -> None
crypt(input_data, output, key[0])
crypt(output, output, key[1])
crypt(output, output, key[2])
-
-
-def qrc_decrypt(encrypted_qrc: str) -> tuple[str | None, str | None]:
- if encrypted_qrc is None or encrypted_qrc.strip() == "":
- logging.error("没有可解密的数据")
- return None, None
- try:
- key = bytearray(b"!@#)(*$%123ZXC!@!@#)(NHL")
- encrypted_text_byte = bytearray.fromhex(encrypted_qrc) # 将文本解析为字节数组
-
- data = bytearray(len(encrypted_text_byte))
- schedule = [[[0] * 6 for _ in range(16)] for _ in range(3)]
- tripledes_key_setup(key, schedule, DECRYPT)
-
- # 以 8 字节为单位迭代 encrypted_text_byte
- for i in range(0, len(encrypted_text_byte), 8):
- temp = bytearray(8)
-
- tripledes_crypt(encrypted_text_byte[i:], temp, schedule)
-
- # 将结果复制到数据数组
- for j in range(8):
- data[i + j] = temp[j]
-
- decrypted_qrc = decompress(data).decode("utf-8")
- except Exception as e:
- logging.exception("解密失败")
- return None, str(e)
- return decrypted_qrc, None
diff --git a/lyrics.py b/lyrics.py
index bf8aa9a..59a6afe 100644
--- a/lyrics.py
+++ b/lyrics.py
@@ -6,7 +6,7 @@
from bs4 import BeautifulSoup
from api import get_qrc, qm_get_lyric
-from decryptor import qrc_decrypt
+from decryptor import QrcType, qrc_decrypt
class LyricType:
@@ -56,6 +56,7 @@ def time2ms(m: int, s: int, ms: int) -> int:
def qrc2lrc(qrc: str) -> str:
"""将明文qrc转换为lrc"""
+ qrc = re.findall(r'', qrc, re.DOTALL)[0]
qrc_lines = qrc.split('\n')
lrc_lines = []
wrods_split_pattern = re.compile(r'(?:\[\d+,\d+\])?((?:(?!\(\d+,\d+\)).)+)\((\d+),(\d+)\)') # 逐字匹配
@@ -155,12 +156,12 @@ def download_and_decrypt(self) -> tuple[str | None, int | None]:
if encrypted_lyric.startswith(c):
return f"没有获取到可解密的歌词(encrypted_lyric starts with {c})", LyricProcessingError.NOT_FOUND
- lyric, error = qrc_decrypt(encrypted_lyric)
+ lyric, error = qrc_decrypt(encrypted_lyric, QrcType.CLOUD)
if lyric is not None:
type_ = judge_lyric_type(lyric)
if type_ == LyricType.QRC:
- lyric = qrc2lrc(re.findall(r'', lyric, re.DOTALL)[0])
+ lyric = qrc2lrc(lyric)
self[key] = lyric
self.orig_type = "qrc"
elif error is not None:
diff --git a/ui/encrypted_lyrics.ui b/ui/encrypted_lyrics.ui
new file mode 100644
index 0000000..616c1cc
--- /dev/null
+++ b/ui/encrypted_lyrics.ui
@@ -0,0 +1,61 @@
+
+
+ encrypted_lyrics
+
+
+
+ 0
+ 0
+ 802
+ 413
+
+
+
+ Form
+
+
+ -
+
+
+ -
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ 打开加密qrc歌词
+
+
+
+ -
+
+
+ 转换为lrc
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ 保存歌词
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/encrypted_lyrics_ui.py b/ui/encrypted_lyrics_ui.py
new file mode 100644
index 0000000..050d3f7
--- /dev/null
+++ b/ui/encrypted_lyrics_ui.py
@@ -0,0 +1,75 @@
+# -*- coding: utf-8 -*-
+
+################################################################################
+## Form generated from reading UI file 'encrypted_lyrics.ui'
+##
+## Created by: Qt User Interface Compiler version 6.6.1
+##
+## WARNING! All changes made in this file will be lost when recompiling UI file!
+################################################################################
+
+from PySide6.QtCore import (
+ QCoreApplication,
+ QMetaObject,
+)
+from PySide6.QtWidgets import (
+ QHBoxLayout,
+ QPlainTextEdit,
+ QPushButton,
+ QSizePolicy,
+ QVBoxLayout,
+)
+
+
+class Ui_encrypted_lyrics(object):
+ def setupUi(self, encrypted_lyrics):
+ if not encrypted_lyrics.objectName():
+ encrypted_lyrics.setObjectName(u"encrypted_lyrics")
+ encrypted_lyrics.resize(802, 413)
+ self.verticalLayout = QVBoxLayout(encrypted_lyrics)
+ self.verticalLayout.setObjectName(u"verticalLayout")
+ self.plainTextEdit = QPlainTextEdit(encrypted_lyrics)
+ self.plainTextEdit.setObjectName(u"plainTextEdit")
+
+ self.verticalLayout.addWidget(self.plainTextEdit)
+
+ self.horizontalLayout = QHBoxLayout()
+ self.horizontalLayout.setObjectName(u"horizontalLayout")
+ self.open_pushButton = QPushButton(encrypted_lyrics)
+ self.open_pushButton.setObjectName(u"open_pushButton")
+ sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
+ sizePolicy.setHorizontalStretch(0)
+ sizePolicy.setVerticalStretch(0)
+ sizePolicy.setHeightForWidth(self.open_pushButton.sizePolicy().hasHeightForWidth())
+ self.open_pushButton.setSizePolicy(sizePolicy)
+
+ self.horizontalLayout.addWidget(self.open_pushButton)
+
+ self.convert_pushButton = QPushButton(encrypted_lyrics)
+ self.convert_pushButton.setObjectName(u"convert_pushButton")
+
+ self.horizontalLayout.addWidget(self.convert_pushButton)
+
+ self.save_pushButton = QPushButton(encrypted_lyrics)
+ self.save_pushButton.setObjectName(u"save_pushButton")
+ sizePolicy.setHeightForWidth(self.save_pushButton.sizePolicy().hasHeightForWidth())
+ self.save_pushButton.setSizePolicy(sizePolicy)
+
+ self.horizontalLayout.addWidget(self.save_pushButton)
+
+
+ self.verticalLayout.addLayout(self.horizontalLayout)
+
+
+ self.retranslateUi(encrypted_lyrics)
+
+ QMetaObject.connectSlotsByName(encrypted_lyrics)
+ # setupUi
+
+ def retranslateUi(self, encrypted_lyrics):
+ encrypted_lyrics.setWindowTitle(QCoreApplication.translate("encrypted_lyrics", u"Form", None))
+ self.open_pushButton.setText(QCoreApplication.translate("encrypted_lyrics", u"\u6253\u5f00\u52a0\u5bc6qrc\u6b4c\u8bcd", None))
+ self.convert_pushButton.setText(QCoreApplication.translate("encrypted_lyrics", u"\u8f6c\u6362\u4e3alrc", None))
+ self.save_pushButton.setText(QCoreApplication.translate("encrypted_lyrics", u"\u4fdd\u5b58\u6b4c\u8bcd", None))
+ # retranslateUi
+
diff --git a/view/encrypted_lyrics.py b/view/encrypted_lyrics.py
new file mode 100644
index 0000000..e3e1924
--- /dev/null
+++ b/view/encrypted_lyrics.py
@@ -0,0 +1,71 @@
+import os
+
+from PySide6.QtWidgets import QFileDialog, QMessageBox, QWidget
+
+from decryptor import QrcType, qrc_decrypt
+from lyrics import get_clear_lyric, qrc2lrc
+from ui.encrypted_lyrics_ui import Ui_encrypted_lyrics
+
+
+class EncryptedLyricsWidget(QWidget, Ui_encrypted_lyrics):
+ def __init__(self) -> None:
+ super().__init__()
+ self.setupUi(self)
+ self.connect_signals()
+ self.is_lrc = False
+
+ def connect_signals(self) -> None:
+ self.open_pushButton.clicked.connect(self.open_file)
+ self.convert_pushButton.clicked.connect(self.convert)
+ self.save_pushButton.clicked.connect(self.save)
+
+ def open_file(self) -> None:
+ file_path = QFileDialog.getOpenFileName(self, "选取加密歌词", "", "QRC Files(*.qrc)")[0]
+ if file_path == "":
+ return
+ if not os.path.exists(file_path):
+ QMessageBox.warning(self, "警告", "文件不存在!")
+ return
+ try:
+ with open(file_path, "rb") as f:
+ data = f.read()
+ except Exception as e:
+ QMessageBox.warning(self, "警告", f"读取文件失败:{e}")
+ return
+ magic_header = b'\x98%\xb0\xac\xe3\x02\x83h\xe8\xfc\x6c'
+ if data[:len(magic_header)] != magic_header:
+ QMessageBox.warning(self, "警告", "文件格式不正确!")
+ return
+ lyrics, error = qrc_decrypt(data, QrcType.LOCAL)
+ if lyrics is None:
+ msg = "解密失败" if error is None else f"解密失败:{error}"
+ QMessageBox.critical(self, "错误", msg)
+ return
+ self.plainTextEdit.setPlainText(lyrics)
+ self.is_lrc = False
+
+ def convert(self) -> None:
+ if self.is_lrc:
+ QMessageBox.information(self, "提示", "当前歌词已经是lrc格式了!")
+ return
+ lyrics = self.plainTextEdit.toPlainText()
+ if lyrics.strip() == "":
+ QMessageBox.warning(self, "警告", "歌词内容不能为空!")
+ return
+ try:
+ lrc = get_clear_lyric(qrc2lrc(lyrics))
+ except Exception as e:
+ QMessageBox.critical(self, "错误", f"转换失败:{e}")
+ return
+ self.plainTextEdit.setPlainText(lrc)
+ self.is_lrc = True
+
+ def save(self) -> None:
+ file_path, _ = QFileDialog.getSaveFileName(self, "保存文件", "", "LRC文件 (*.lrc)")
+ if file_path == "":
+ return
+ try:
+ with open(file_path, "w", encoding="utf-8") as f:
+ f.write(self.plainTextEdit.toPlainText())
+ except Exception as e:
+ QMessageBox.critical(self, "错误", f"保存失败:{e}")