refactor(#8): update LoadMMKVOrDefault function to use MMKVLoaderOptions and implement platform-specific loading for Windows

This commit is contained in:
鲁树人
2026-10-18 08:11:35 +09:00
parent 107aa2c3a8
commit 061876f9bf
6 changed files with 200 additions and 19 deletions
+64
View File
@@ -0,0 +1,64 @@
package qmc
import (
"bufio"
"io"
"strings"
"git.um-react.app/um/cli/algo/common"
"git.um-react.app/um/cli/internal/mmkv"
"git.um-react.app/um/cli/internal/utils"
"go.uber.org/zap"
)
// MMKVLoaderOptions holds platform-specific inputs for LoadMMKVOrDefault.
type MMKVLoaderOptions struct {
CliExe string // qmpc-mmkv-cli path (--qmpc-mmkv-cli-exe); Windows only
}
// LoadUserMMKV loads keys from a user-specified mmkv path. Soft-fails with a warning.
func LoadUserMMKV(path string, key string, logger *zap.Logger) common.QMCKeys {
if path == "" {
return nil
}
logger.Debug("LoadUserMMKV: Loading user mmkv", zap.String("path", path), zap.String("key", key))
userKeys, err := mmkv.LoadFromPath(path, key, logger)
if err != nil {
logger.Warn("LoadUserMMKV: could not read user keys", zap.Error(err))
return nil
}
logger.Info("Using user mmkv", zap.String("path", path), zap.Int("count", len(userKeys)))
return userKeys
}
// parseQmpcMMKVCliOutput reads qmpc-mmkv-cli plaintext output as alternating
// filename / ekey lines and builds a QMCKeys map.
func parseQmpcMMKVCliOutput(r io.Reader, logger *zap.Logger) common.QMCKeys {
sc := bufio.NewScanner(r)
result := make(common.QMCKeys)
var name string
nameRead := false
for sc.Scan() {
line := utils.NormalizeUnicode(strings.TrimSpace(sc.Text()))
if !nameRead {
name = line
} else {
if name != "" && line != "" {
result[name] = line
}
}
nameRead = !nameRead
}
if err := sc.Err(); err != nil {
logger.Warn("LoadMMKVOrDefault: error reading qmpc-mmkv-cli output", zap.Error(err))
}
if nameRead {
logger.Warn("LoadMMKVOrDefault: qmpc-mmkv-cli output has odd number of lines, ignoring last line",
zap.String("line", name),
)
}
return result
}
+2 -14
View File
@@ -7,7 +7,7 @@ import (
"go.uber.org/zap"
)
func LoadMMKVOrDefault(path string, key string, logger *zap.Logger) (result common.QMCKeys, err error) {
func LoadMMKVOrDefault(opts MMKVLoaderOptions, logger *zap.Logger) (result common.QMCKeys, err error) {
key1, err := qmmac.LoadMacKeysV8(logger)
if err != nil {
key1 = nil
@@ -20,19 +20,7 @@ func LoadMMKVOrDefault(path string, key string, logger *zap.Logger) (result comm
logger.Warn("LoadMMKVOrDefault: could not read QQMusic v10.x keys", zap.Error(err))
}
userKeys := make(common.QMCKeys)
if path != "" {
logger.Info("Using user mmkv")
userKeys, err = mmkv.LoadFromPath(path, key, logger)
if err != nil {
userKeys = nil
logger.Warn("LoadMMKVOrDefault: could not read user keys", zap.Error(err))
}
}
allKeys := mmkv.Merge(key1, key2, userKeys)
allKeys := mmkv.Merge(key1, key2)
logger.Debug("Keys loaded", zap.Any("keys", allKeys), zap.Int("len", len(allKeys)))
return allKeys, nil
}
+3 -3
View File
@@ -1,4 +1,4 @@
//go:build !darwin
//go:build !darwin && !windows
package qmc
@@ -7,7 +7,7 @@ import (
"go.uber.org/zap"
)
func LoadMMKVOrDefault(path string, key string, logger *zap.Logger) (result common.QMCKeys, err error) {
// Stub: do nothing
func LoadMMKVOrDefault(opts MMKVLoaderOptions, logger *zap.Logger) (result common.QMCKeys, err error) {
// No platform-default QMC keys on this OS.
return nil, nil
}
+79
View File
@@ -0,0 +1,79 @@
package qmc
import (
"strings"
"testing"
"go.uber.org/zap"
)
func TestParseQmpcMMKVCliOutput(t *testing.T) {
logger := zap.NewNop()
t.Run("pairs", func(t *testing.T) {
in := "file1.mflac\nkey1\nfile2.mgg\nkey2\n"
got := parseQmpcMMKVCliOutput(strings.NewReader(in), logger)
if len(got) != 2 {
t.Fatalf("len = %d, want 2", len(got))
}
if got["file1.mflac"] != "key1" {
t.Errorf("file1.mflac = %q, want key1", got["file1.mflac"])
}
if got["file2.mgg"] != "key2" {
t.Errorf("file2.mgg = %q, want key2", got["file2.mgg"])
}
})
t.Run("crlf", func(t *testing.T) {
in := "a.mflac\r\nKEYA\r\nb.mflac\r\nKEYB\r\n"
got := parseQmpcMMKVCliOutput(strings.NewReader(in), logger)
if got["a.mflac"] != "KEYA" || got["b.mflac"] != "KEYB" {
t.Errorf("got %#v", got)
}
})
t.Run("odd line ignored", func(t *testing.T) {
in := "file1.mflac\nkey1\norphan\n"
got := parseQmpcMMKVCliOutput(strings.NewReader(in), logger)
if len(got) != 1 || got["file1.mflac"] != "key1" {
t.Errorf("got %#v, want single pair", got)
}
})
t.Run("empty name skipped", func(t *testing.T) {
in := "\nkey-for-empty\nfile.mflac\nrealkey\n"
got := parseQmpcMMKVCliOutput(strings.NewReader(in), logger)
if len(got) != 1 || got["file.mflac"] != "realkey" {
t.Errorf("got %#v", got)
}
if _, ok := got[""]; ok {
t.Error("empty name should not be stored")
}
})
t.Run("empty key skipped", func(t *testing.T) {
in := "empty-key.mflac\n\nfile.mflac\nrealkey\n"
got := parseQmpcMMKVCliOutput(strings.NewReader(in), logger)
if len(got) != 1 || got["file.mflac"] != "realkey" {
t.Errorf("got %#v", got)
}
if _, ok := got["empty-key.mflac"]; ok {
t.Error("empty key should not be stored")
}
})
t.Run("empty input", func(t *testing.T) {
got := parseQmpcMMKVCliOutput(strings.NewReader(""), logger)
if len(got) != 0 {
t.Errorf("got %#v, want empty", got)
}
})
t.Run("no trailing newline", func(t *testing.T) {
in := "file.mflac\nkey"
got := parseQmpcMMKVCliOutput(strings.NewReader(in), logger)
if got["file.mflac"] != "key" {
t.Errorf("got %#v", got)
}
})
}
+45
View File
@@ -0,0 +1,45 @@
package qmc
import (
"bytes"
"errors"
"os/exec"
"git.um-react.app/um/cli/algo/common"
"go.uber.org/zap"
)
func LoadMMKVOrDefault(opts MMKVLoaderOptions, logger *zap.Logger) (result common.QMCKeys, err error) {
cliExe := opts.CliExe
if cliExe == "" {
cliExe = "qmpc-mmkv-cli.exe"
}
cmd := exec.Command(cliExe, "-f", "plaintext", "-o", "-")
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err = cmd.Run()
if err != nil {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
logger.Warn("LoadMMKVOrDefault: qmpc-mmkv-cli exited with non-zero status",
zap.String("exe", cliExe),
zap.Int("exit_code", exitErr.ExitCode()),
zap.String("stderr", stderr.String()),
)
} else {
logger.Warn("LoadMMKVOrDefault: failed to run qmpc-mmkv-cli",
zap.String("exe", cliExe),
zap.Error(err),
zap.String("stderr", stderr.String()),
)
}
return nil, nil
}
result = parseQmpcMMKVCliOutput(&stdout, logger)
logger.Debug("Keys loaded from qmpc-mmkv-cli", zap.Int("len", len(result)))
return result, nil
}
+7 -2
View File
@@ -24,6 +24,7 @@ import (
_ "git.um-react.app/um/cli/algo/xiami"
_ "git.um-react.app/um/cli/algo/ximalaya"
"git.um-react.app/um/cli/internal/ffmpeg"
"git.um-react.app/um/cli/internal/mmkv"
"git.um-react.app/um/cli/internal/sniff"
"git.um-react.app/um/cli/internal/utils"
"github.com/fsnotify/fsnotify"
@@ -51,6 +52,7 @@ func main() {
&cli.StringFlag{Name: "output", Aliases: []string{"o"}, Usage: "path to output dir", Required: false},
&cli.StringFlag{Name: "qmc-mmkv", Aliases: []string{"db"}, Usage: "path to QQMusic mmkv path", Required: false},
&cli.StringFlag{Name: "qmc-mmkv-key", Aliases: []string{"key"}, Usage: "QQMusic mmkv password (16 ascii chars)", Required: false},
&cli.StringFlag{Name: "qmpc-mmkv-cli-exe", Usage: "path to QMPC MMKV Cli by LSR (v0.1.1+, auto-fetch QMC keys; Windows only)", Required: false, Value: "qmpc-mmkv-cli.exe"},
&cli.StringFlag{Name: "kgg-db", Usage: "path to kgg db (win32 kugou v11)", Required: false},
&cli.BoolFlag{Name: "remove-source", Aliases: []string{"rs"}, Usage: "remove source file", Required: false, Value: false},
&cli.BoolFlag{Name: "skip-noop", Aliases: []string{"n"}, Usage: "skip noop decoder", Required: false, Value: true},
@@ -176,11 +178,14 @@ func appMain(c *cli.Context) (err error) {
return errors.New("output should be a writable directory")
}
// QMC: Load keys
qmcKeys, err := qmc.LoadMMKVOrDefault(c.String("qmc-mmkv"), c.String("qmc-mmkv-key"), logger)
// QMC: Load platform keys, then merge user mmkv (--qmc-mmkv) on all platforms.
qmcKeys, err := qmc.LoadMMKVOrDefault(qmc.MMKVLoaderOptions{
CliExe: c.String("qmpc-mmkv-cli-exe"),
}, logger)
if err != nil {
return err
}
qmcKeys = mmkv.Merge(qmcKeys, qmc.LoadUserMMKV(c.String("qmc-mmkv"), c.String("qmc-mmkv-key"), logger))
kggDbPath := c.String("kgg-db")
if kggDbPath == "" {