refactor: fold QQ lyric decryption into the qq package
This commit is contained in:
1 parent
75c5dad4b9
commit
cce68d760e
5 files changed
+47
-48
No files matched your search
@@ -12,7 +12,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dihemoe/alisten/internal/adapter/out/qq/qrc"
|
||||
"github.com/dihemoe/alisten/internal/app/port"
|
||||
"github.com/dihemoe/alisten/internal/domain/track"
|
||||
)
|
||||
@@ -244,24 +243,24 @@ func (c *Client) lyric(ctx context.Context, id string) (lyricResult, error) {
|
||||
func parsePlayLyric(lyricHex, transHex string) (lyricResult, error) {
|
||||
var out lyricResult
|
||||
if lyricHex != "" {
|
||||
plain, err := qrc.DecryptHex(lyricHex)
|
||||
plain, err := decryptQRCHex(lyricHex)
|
||||
if err != nil {
|
||||
return lyricResult{}, fmt.Errorf("解密 QQ 音乐的歌词:%w", err)
|
||||
}
|
||||
tags, lines := qrc.Parse(plain)
|
||||
tags, lines := parseQRC(plain)
|
||||
if len(lines) == 0 {
|
||||
// 没有逐字数据时 LyricContent 里直接是行级 LRC。
|
||||
out.LRC = text(plain)
|
||||
} else {
|
||||
out.LRC = text(qrc.RenderLRC(tags, lines))
|
||||
out.LRC = text(renderLRCLines(tags, lines))
|
||||
if hasWordTimings(lines) {
|
||||
out.Words = qrc.RenderYRC(lines)
|
||||
out.Words = renderYRCLines(lines)
|
||||
}
|
||||
}
|
||||
}
|
||||
if transHex != "" {
|
||||
// 翻译解密后是行级 LRC;翻译是增强信息,解密失败只丢翻译。
|
||||
if plain, err := qrc.DecryptHex(transHex); err == nil {
|
||||
if plain, err := decryptQRCHex(transHex); err == nil {
|
||||
out.Trans = cleanTrans(plain)
|
||||
}
|
||||
}
|
||||
@@ -287,7 +286,7 @@ func cleanTrans(lrc string) string {
|
||||
return text(b.String())
|
||||
}
|
||||
|
||||
func hasWordTimings(lines []qrc.Line) bool {
|
||||
func hasWordTimings(lines []qrcLine) bool {
|
||||
for _, l := range lines {
|
||||
if len(l.Words) > 0 {
|
||||
return true
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package qrc
|
||||
package qq
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -8,22 +8,22 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Word 是一段带独立时间的歌词片段,通常是单个字。毫秒,绝对于曲目起点。
|
||||
type Word struct {
|
||||
// qrcWord 是一段带独立时间的歌词片段,通常是单个字。毫秒,绝对于曲目起点。
|
||||
type qrcWord struct {
|
||||
StartMs int
|
||||
DurationMs int
|
||||
Text string
|
||||
}
|
||||
|
||||
// Line 是一行逐字歌词。毫秒,绝对于曲目起点。
|
||||
type Line struct {
|
||||
// qrcLine 是一行逐字歌词。毫秒,绝对于曲目起点。
|
||||
type qrcLine struct {
|
||||
StartMs int
|
||||
DurationMs int
|
||||
Words []Word
|
||||
Words []qrcWord
|
||||
}
|
||||
|
||||
// Text 拼接整行文本。
|
||||
func (l Line) Text() string {
|
||||
func (l qrcLine) text() string {
|
||||
var b strings.Builder
|
||||
for _, w := range l.Words {
|
||||
b.WriteString(w.Text)
|
||||
@@ -39,8 +39,8 @@ var (
|
||||
)
|
||||
|
||||
// QRC 的时间标记跟在它所定时的文本之后;只有内容是纯数字时间的圆括号才是标记,其余按歌词原文保留。
|
||||
func parseWords(content string) []Word {
|
||||
var words []Word
|
||||
func parseWords(content string) []qrcWord {
|
||||
var words []qrcWord
|
||||
start := 0
|
||||
for i := 0; i < len(content); i++ {
|
||||
if content[i] != '(' {
|
||||
@@ -57,25 +57,25 @@ func parseWords(content string) []Word {
|
||||
text := strings.TrimSuffix(content[start:i], "\r")
|
||||
if text != "" {
|
||||
parts := strings.SplitN(inner, ",", 2)
|
||||
words = append(words, Word{StartMs: atoi(parts[0]), DurationMs: atoi(parts[1]), Text: text})
|
||||
words = append(words, qrcWord{StartMs: atoi(parts[0]), DurationMs: atoi(parts[1]), Text: text})
|
||||
}
|
||||
i += end
|
||||
start = i + 1
|
||||
}
|
||||
if tail := strings.TrimSpace(content[start:]); tail != "" && len(words) > 0 {
|
||||
last := words[len(words)-1]
|
||||
words = append(words, Word{StartMs: last.StartMs + last.DurationMs, Text: tail})
|
||||
words = append(words, qrcWord{StartMs: last.StartMs + last.DurationMs, Text: tail})
|
||||
}
|
||||
return words
|
||||
}
|
||||
|
||||
// Parse 解析解密后的 QRC 文本:剥掉 XML 外壳,返回头部标签和逐字行;kana 等注音注解行被丢弃。
|
||||
func Parse(text string) (map[string]string, []Line) {
|
||||
func parseQRC(text string) (map[string]string, []qrcLine) {
|
||||
if m := contentRe.FindStringSubmatch(text); m != nil {
|
||||
text = html.UnescapeString(m[1])
|
||||
}
|
||||
tags := map[string]string{}
|
||||
var lines []Line
|
||||
var lines []qrcLine
|
||||
for _, raw := range strings.Split(text, "\n") {
|
||||
row := strings.TrimSpace(raw)
|
||||
if row == "" {
|
||||
@@ -89,14 +89,14 @@ func Parse(text string) (map[string]string, []Line) {
|
||||
if m == nil {
|
||||
continue
|
||||
}
|
||||
line := Line{StartMs: atoi(m[1]), DurationMs: atoi(m[2]), Words: parseWords(m[3])}
|
||||
line := qrcLine{StartMs: atoi(m[1]), DurationMs: atoi(m[2]), Words: parseWords(m[3])}
|
||||
lines = append(lines, line)
|
||||
}
|
||||
return tags, lines
|
||||
}
|
||||
|
||||
// RenderYRC 输出传输用的逐字格式(YRC 语法):[行起始,行时长](字起始,字时长,0)字…,毫秒为绝对时间。
|
||||
func RenderYRC(lines []Line) string {
|
||||
// renderYRCLines 输出传输用的逐字格式(YRC 语法):[行起始,行时长](字起始,字时长,0)字…,毫秒为绝对时间。
|
||||
func renderYRCLines(lines []qrcLine) string {
|
||||
var b strings.Builder
|
||||
for _, l := range lines {
|
||||
fmt.Fprintf(&b, "[%d,%d]", l.StartMs, l.DurationMs)
|
||||
@@ -108,8 +108,8 @@ func RenderYRC(lines []Line) string {
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// RenderLRC 输出行级 LRC:头部标签行在前,随后是 [分:秒.厘秒]行文本。
|
||||
func RenderLRC(tags map[string]string, lines []Line) string {
|
||||
// renderLRCLines 输出行级 LRC:头部标签行在前,随后是 [分:秒.厘秒]行文本。
|
||||
func renderLRCLines(tags map[string]string, lines []qrcLine) string {
|
||||
var b strings.Builder
|
||||
for _, k := range []string{"ti", "ar", "al", "by", "offset"} {
|
||||
if v, ok := tags[k]; ok {
|
||||
@@ -117,7 +117,7 @@ func RenderLRC(tags map[string]string, lines []Line) string {
|
||||
}
|
||||
}
|
||||
for _, l := range lines {
|
||||
fmt.Fprintf(&b, "[%02d:%05.2f]%s\n", l.StartMs/60000, float64(l.StartMs%60000)/1000, l.Text())
|
||||
fmt.Fprintf(&b, "[%02d:%05.2f]%s\n", l.StartMs/60000, float64(l.StartMs%60000)/1000, l.text())
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// Package qrc 解密并解析 QQ 音乐的 QRC 歌词:密文是十六进制字符串,
|
||||
// 加密为 QQ 改版 DES 的三轮 EDE,解压为 zlib。改版 DES 与标准 DES 不兼容:
|
||||
// 32 位字内字节小端、两个 S 盒条目被改动、PC-2 的 D 半区错位一位。
|
||||
package qrc
|
||||
package qq
|
||||
|
||||
// QQ 歌词的密文是十六进制字符串,加密为 QQ 改版 DES 的三轮 EDE,解压为 zlib。
|
||||
// 改版 DES 与标准 DES 不兼容:32 位字内字节小端、两个 S 盒条目被改动、PC-2 的 D 半区错位一位。
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -14,8 +14,8 @@ import (
|
||||
|
||||
var qrcKey = []byte("!@#)(*$%123ZXC!@!@#)(NHL")
|
||||
|
||||
// DecryptHex 解密十六进制的 QRC 密文。
|
||||
func DecryptHex(content string) (string, error) {
|
||||
// decryptQRCHex 解密十六进制的 QRC 密文。
|
||||
func decryptQRCHex(content string) (string, error) {
|
||||
encrypted, err := hex.DecodeString(strings.TrimSpace(content))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("QRC 密文不是十六进制:%w", err)
|
||||
@@ -1,4 +1,4 @@
|
||||
package qrc
|
||||
package qq
|
||||
|
||||
var sBox = [8][64]byte{
|
||||
{14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7, 0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8, 4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0, 15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13},
|
||||
@@ -1,4 +1,4 @@
|
||||
package qrc
|
||||
package qq
|
||||
|
||||
import (
|
||||
"strings"
|
||||
@@ -26,8 +26,8 @@ const syntheticPlain = `<?xml version="1.0" encoding="utf-8"?>
|
||||
</LyricInfo>
|
||||
</QrcInfos>`
|
||||
|
||||
func TestDecryptHexGolden(t *testing.T) {
|
||||
got, err := DecryptHex(syntheticHex)
|
||||
func TestDecryptQRCHexGolden(t *testing.T) {
|
||||
got, err := decryptQRCHex(syntheticHex)
|
||||
if err != nil {
|
||||
t.Fatalf("解密失败:%v", err)
|
||||
}
|
||||
@@ -36,20 +36,20 @@ func TestDecryptHexGolden(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecryptHexRejectsBadInput(t *testing.T) {
|
||||
if _, err := DecryptHex("not-hex"); err == nil {
|
||||
func TestDecryptQRCHexRejectsBadInput(t *testing.T) {
|
||||
if _, err := decryptQRCHex("not-hex"); err == nil {
|
||||
t.Error("非十六进制输入没有报错")
|
||||
}
|
||||
if _, err := DecryptHex("0011"); err == nil {
|
||||
if _, err := decryptQRCHex("0011"); err == nil {
|
||||
t.Error("长度不是 8 的倍数没有报错")
|
||||
}
|
||||
if _, err := DecryptHex(strings.Repeat("00", 16)); err == nil {
|
||||
if _, err := decryptQRCHex(strings.Repeat("00", 16)); err == nil {
|
||||
t.Error("非 zlib 数据没有报错")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAndRender(t *testing.T) {
|
||||
tags, lines := Parse(syntheticPlain)
|
||||
tags, lines := parseQRC(syntheticPlain)
|
||||
if tags["ti"] != "合成测试歌" {
|
||||
t.Errorf("ti 标签 = %q", tags["ti"])
|
||||
}
|
||||
@@ -60,22 +60,22 @@ func TestParseAndRender(t *testing.T) {
|
||||
t.Fatalf("解析出 %d 行,期望 3 行", len(lines))
|
||||
}
|
||||
line := lines[0]
|
||||
if line.StartMs != 1000 || line.DurationMs != 1500 || line.Text() != "你好世" {
|
||||
t.Errorf("首行 = [%d,%d]%q", line.StartMs, line.DurationMs, line.Text())
|
||||
if line.StartMs != 1000 || line.DurationMs != 1500 || line.text() != "你好世" {
|
||||
t.Errorf("首行 = [%d,%d]%q", line.StartMs, line.DurationMs, line.text())
|
||||
}
|
||||
if len(line.Words) != 3 || line.Words[0] != (Word{StartMs: 1000, DurationMs: 500, Text: "你"}) {
|
||||
if len(line.Words) != 3 || line.Words[0] != (qrcWord{StartMs: 1000, DurationMs: 500, Text: "你"}) {
|
||||
t.Errorf("首行逐字 = %#v", line.Words)
|
||||
}
|
||||
// 歌词原文里的全角圆括号不是时间标记,要保留在文本里。
|
||||
if text := lines[1].Text(); text != "括号(内容)" {
|
||||
if text := lines[1].text(); text != "括号(内容)" {
|
||||
t.Errorf("含括号的行 = %q", text)
|
||||
}
|
||||
|
||||
lrc := RenderLRC(tags, lines)
|
||||
lrc := renderLRCLines(tags, lines)
|
||||
if !strings.Contains(lrc, "[ti:合成测试歌]\n") || !strings.Contains(lrc, "[00:01.00]你好世\n") {
|
||||
t.Errorf("LRC 渲染缺少预期行:%.120q", lrc)
|
||||
}
|
||||
yrc := RenderYRC(lines)
|
||||
yrc := renderYRCLines(lines)
|
||||
if !strings.Contains(yrc, "[1000,1500](1000,500,0)你(1500,500,0)好(2000,500,0)世\n") {
|
||||
t.Errorf("YRC 渲染缺少预期行:%.120q", yrc)
|
||||
}
|
||||
@@ -83,7 +83,7 @@ func TestParseAndRender(t *testing.T) {
|
||||
|
||||
// 没有逐字时间标记的内容(纯 LRC 文本)解析不出行。
|
||||
func TestParsePlainLRCYieldsNoLines(t *testing.T) {
|
||||
_, lines := Parse("[00:01.00]普通歌词\n[00:03.00]第二行\n")
|
||||
_, lines := parseQRC("[00:01.00]普通歌词\n[00:03.00]第二行\n")
|
||||
if len(lines) != 0 {
|
||||
t.Errorf("纯 LRC 解析出 %d 行,期望 0", len(lines))
|
||||
}
|
||||
Reference in new issue
Block a user