From 061876f9bf849f562ac4b4ac47abdc0a72fe7aae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=81=E6=A0=91=E4=BA=BA?= Date: Sun, 18 Oct 2026 08:03:38 +0900 Subject: [PATCH 01/13] refactor(#8): update LoadMMKVOrDefault function to use MMKVLoaderOptions and implement platform-specific loading for Windows --- algo/qmc/key_mmkv_loader.go | 64 +++++++++++++++++++++++ algo/qmc/key_mmkv_loader_darwin.go | 16 +----- algo/qmc/key_mmkv_loader_default.go | 6 +-- algo/qmc/key_mmkv_loader_test.go | 79 +++++++++++++++++++++++++++++ algo/qmc/key_mmkv_loader_windows.go | 45 ++++++++++++++++ cmd/um/main.go | 9 +++- 6 files changed, 200 insertions(+), 19 deletions(-) create mode 100644 algo/qmc/key_mmkv_loader.go create mode 100644 algo/qmc/key_mmkv_loader_test.go create mode 100644 algo/qmc/key_mmkv_loader_windows.go diff --git a/algo/qmc/key_mmkv_loader.go b/algo/qmc/key_mmkv_loader.go new file mode 100644 index 0000000..479e338 --- /dev/null +++ b/algo/qmc/key_mmkv_loader.go @@ -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 +} diff --git a/algo/qmc/key_mmkv_loader_darwin.go b/algo/qmc/key_mmkv_loader_darwin.go index e406c2c..23a8db4 100644 --- a/algo/qmc/key_mmkv_loader_darwin.go +++ b/algo/qmc/key_mmkv_loader_darwin.go @@ -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 } diff --git a/algo/qmc/key_mmkv_loader_default.go b/algo/qmc/key_mmkv_loader_default.go index dff3be3..edcb9a9 100644 --- a/algo/qmc/key_mmkv_loader_default.go +++ b/algo/qmc/key_mmkv_loader_default.go @@ -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 } diff --git a/algo/qmc/key_mmkv_loader_test.go b/algo/qmc/key_mmkv_loader_test.go new file mode 100644 index 0000000..e6e8d68 --- /dev/null +++ b/algo/qmc/key_mmkv_loader_test.go @@ -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) + } + }) +} diff --git a/algo/qmc/key_mmkv_loader_windows.go b/algo/qmc/key_mmkv_loader_windows.go new file mode 100644 index 0000000..3ed7788 --- /dev/null +++ b/algo/qmc/key_mmkv_loader_windows.go @@ -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 +} diff --git a/cmd/um/main.go b/cmd/um/main.go index 721a624..1dcfd14 100644 --- a/cmd/um/main.go +++ b/cmd/um/main.go @@ -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 == "" { From 3ed50f3f30e141704f31bdc6ef1ccefb2d415665 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=81=E6=A0=91=E4=BA=BA?= Date: Sun, 18 Oct 2026 08:03:53 +0900 Subject: [PATCH 02/13] chore: update go.sum with new dependencies and versions --- go.sum | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/go.sum b/go.sum index 462b60a..d373af5 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -12,10 +14,14 @@ github.com/go-flac/flacvorbis v0.2.0 h1:KH0xjpkNTXFER4cszH4zeJxYcrHbUobz/RticWGO github.com/go-flac/flacvorbis v0.2.0/go.mod h1:uIysHOtuU7OLGoCRG92bvnkg7QEqHx19qKRV6K1pBrI= github.com/go-flac/go-flac v1.0.0 h1:6qI9XOVLcO50xpzm3nXvO31BgDgHhnr/p/rER/K/doY= github.com/go-flac/go-flac v1.0.0/go.mod h1:WnZhcpmq4u1UdZMNn9LYSoASpWOCMOoxXxcWEHSzkW8= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= @@ -30,8 +36,6 @@ github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw= github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/unlock-music/go-mmkv v0.1.3 h1:BtO77wjmypCeaX3n8VRDX5ZWHlYdJ9xEfKGui9s4PM0= -github.com/unlock-music/go-mmkv v0.1.3/go.mod h1:u1fYHzYOO7et1E55Q86lTvjXf6p14CIELszAHI8T9zQ= github.com/unlock-music/go-mmkv v0.1.4 h1:onjxxlNd9Fcp4RYL8khOgp/DRY0n+Iuw958TdRoB/FA= github.com/unlock-music/go-mmkv v0.1.4/go.mod h1:u1fYHzYOO7et1E55Q86lTvjXf6p14CIELszAHI8T9zQ= github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= @@ -50,11 +54,15 @@ golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/y golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= From 09622d01a7cb91d082ef3bf872e1df68928a051a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=81=E6=A0=91=E4=BA=BA?= Date: Sun, 18 Oct 2026 08:03:57 +0900 Subject: [PATCH 03/13] feat(#7): implement LoadAndroidDB function and add tests for loading ekeys from QQ Music Android SQLite database --- algo/qmc/key_android_db_loader.go | 110 ++++++++++++++++++++ algo/qmc/key_android_db_loader_test.go | 135 +++++++++++++++++++++++++ cmd/um/main.go | 7 +- 3 files changed, 251 insertions(+), 1 deletion(-) create mode 100644 algo/qmc/key_android_db_loader.go create mode 100644 algo/qmc/key_android_db_loader_test.go diff --git a/algo/qmc/key_android_db_loader.go b/algo/qmc/key_android_db_loader.go new file mode 100644 index 0000000..535452e --- /dev/null +++ b/algo/qmc/key_android_db_loader.go @@ -0,0 +1,110 @@ +package qmc + +import ( + "database/sql" + "fmt" + "path/filepath" + + "git.um-react.app/um/cli/algo/common" + "git.um-react.app/um/cli/internal/utils" + "go.uber.org/zap" + _ "modernc.org/sqlite" +) + +// LoadAndroidDB loads QMC ekeys from a QQ Music Android SQLite DB +// (player_process_db). Soft-fails with a warning. +func LoadAndroidDB(path string, logger *zap.Logger) common.QMCKeys { + if path == "" { + return nil + } + logger.Debug("LoadAndroidDB: Loading Android DB", zap.String("path", path)) + + keys, err := loadAndroidDB(path) + if err != nil { + logger.Warn("LoadAndroidDB: could not read Android DB keys", zap.Error(err)) + return nil + } + logger.Info("Using Android DB", zap.String("path", path), zap.Int("count", len(keys))) + return keys +} + +func loadAndroidDB(path string) (common.QMCKeys, error) { + // mode=ro: refuse writes and do not create a missing database file. + dsn := "file:" + filepath.ToSlash(path) + "?mode=ro" + db, err := sql.Open("sqlite", dsn) + if err != nil { + return nil, fmt.Errorf("open: %w", err) + } + defer db.Close() + + if err := db.Ping(); err != nil { + return nil, fmt.Errorf("ping: %w", err) + } + + query, err := androidEKeyQuery(db) + if err != nil { + return nil, err + } + + rows, err := db.Query(query) + if err != nil { + return nil, fmt.Errorf("query: %w", err) + } + defer rows.Close() + + result := make(common.QMCKeys) + for rows.Next() { + var filePath, ekey string + if err := rows.Scan(&filePath, &ekey); err != nil { + return nil, fmt.Errorf("scan: %w", err) + } + filePath = utils.NormalizeUnicode(filePath) + ekey = utils.NormalizeUnicode(ekey) + if filePath == "" || ekey == "" { + continue + } + result[filePath] = ekey + if base := filepath.Base(filePath); base != filePath { + result[utils.NormalizeUnicode(base)] = ekey + } + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("rows: %w", err) + } + return result, nil +} + +func androidEKeyQuery(db *sql.DB) (string, error) { + has, err := hasTable(db, "audio_file_ekey_table") + if err != nil { + return "", err + } + if has { + return "select file_path, ekey from audio_file_ekey_table", nil + } + + has, err = hasTable(db, "EKeyFileInfo") + if err != nil { + return "", err + } + if has { + return "select filePath, eKey from EKeyFileInfo", nil + } + + return "", fmt.Errorf("no known ekey table found (audio_file_ekey_table or EKeyFileInfo)") +} + +func hasTable(db *sql.DB, name string) (bool, error) { + var found string + err := db.QueryRow( + `select name from sqlite_master where type='table' and name=?`, + name, + ).Scan(&found) + if err == sql.ErrNoRows { + return false, nil + } + if err != nil { + return false, fmt.Errorf("hasTable(%s): %w", name, err) + } + return true, nil +} diff --git a/algo/qmc/key_android_db_loader_test.go b/algo/qmc/key_android_db_loader_test.go new file mode 100644 index 0000000..5a4fe6e --- /dev/null +++ b/algo/qmc/key_android_db_loader_test.go @@ -0,0 +1,135 @@ +package qmc + +import ( + "database/sql" + "os" + "path/filepath" + "testing" + + "go.uber.org/zap" +) + +func TestLoadAndroidDB_audioFileEkeyTable(t *testing.T) { + path := filepath.Join(t.TempDir(), "db") + db, err := sql.Open("sqlite", path) + if err != nil { + t.Fatal(err) + } + _, err = db.Exec(` + create table audio_file_ekey_table (file_path text, ekey text); + insert into audio_file_ekey_table values + ('/storage/emulated/0/Music/song.mflac', 'ekey1'), + ('plain.mgg', 'ekey2'), + ('', 'skip'), + ('empty-ekey.mflac', ''); + `) + if err != nil { + t.Fatal(err) + } + db.Close() + + got := LoadAndroidDB(path, zap.NewNop()) + if got["/storage/emulated/0/Music/song.mflac"] != "ekey1" { + t.Errorf("full path = %q", got["/storage/emulated/0/Music/song.mflac"]) + } + if got["song.mflac"] != "ekey1" { + t.Errorf("basename = %q, want ekey1", got["song.mflac"]) + } + if got["plain.mgg"] != "ekey2" { + t.Errorf("plain.mgg = %q", got["plain.mgg"]) + } + if _, ok := got[""]; ok { + t.Error("empty path should not be stored") + } + if _, ok := got["empty-ekey.mflac"]; ok { + t.Error("empty ekey should not be stored") + } +} + +func TestLoadAndroidDB_EKeyFileInfo(t *testing.T) { + path := filepath.Join(t.TempDir(), "db") + db, err := sql.Open("sqlite", path) + if err != nil { + t.Fatal(err) + } + _, err = db.Exec(` + create table EKeyFileInfo (filePath text, eKey text); + insert into EKeyFileInfo values + ('/data/data/com.tencent.qqmusic/files/a.mflac', 'KEYA'); + `) + if err != nil { + t.Fatal(err) + } + db.Close() + + got := LoadAndroidDB(path, zap.NewNop()) + if got["/data/data/com.tencent.qqmusic/files/a.mflac"] != "KEYA" { + t.Errorf("full path = %q", got["/data/data/com.tencent.qqmusic/files/a.mflac"]) + } + if got["a.mflac"] != "KEYA" { + t.Errorf("basename = %q, want KEYA", got["a.mflac"]) + } +} + +func TestLoadAndroidDB_emptyPath(t *testing.T) { + got := LoadAndroidDB("", zap.NewNop()) + if got != nil { + t.Errorf("got %#v, want nil", got) + } +} + +func TestLoadAndroidDB_missingFile(t *testing.T) { + path := filepath.Join(t.TempDir(), "no-such-db") + got := LoadAndroidDB(path, zap.NewNop()) + if got != nil { + t.Errorf("got %#v, want nil", got) + } + if _, err := os.Stat(path); err == nil { + t.Error("missing db path should not be created") + } else if !os.IsNotExist(err) { + t.Fatalf("stat: %v", err) + } +} + +func TestLoadAndroidDB_noKnownTable(t *testing.T) { + path := filepath.Join(t.TempDir(), "db") + db, err := sql.Open("sqlite", path) + if err != nil { + t.Fatal(err) + } + if _, err := db.Exec(`create table other (x text)`); err != nil { + t.Fatal(err) + } + db.Close() + + got := LoadAndroidDB(path, zap.NewNop()) + if got != nil { + t.Errorf("got %#v, want nil", got) + } +} + +func TestLoadAndroidDB_prefersAudioFileEkeyTable(t *testing.T) { + path := filepath.Join(t.TempDir(), "db") + db, err := sql.Open("sqlite", path) + if err != nil { + t.Fatal(err) + } + _, err = db.Exec(` + create table audio_file_ekey_table (file_path text, ekey text); + create table EKeyFileInfo (filePath text, eKey text); + insert into audio_file_ekey_table values ('from-new.mflac', 'new'); + insert into EKeyFileInfo values ('from-old.mflac', 'old'); + `) + if err != nil { + t.Fatal(err) + } + db.Close() + + got := LoadAndroidDB(path, zap.NewNop()) + if got["from-new.mflac"] != "new" { + t.Errorf("got %#v, want new table", got) + } + if _, ok := got["from-old.mflac"]; ok { + t.Error("should prefer audio_file_ekey_table over EKeyFileInfo") + } +} diff --git a/cmd/um/main.go b/cmd/um/main.go index 1dcfd14..8652d82 100644 --- a/cmd/um/main.go +++ b/cmd/um/main.go @@ -53,6 +53,7 @@ func main() { &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: "qmc-android-db", Usage: "path to QQMusic Android player_process_db (dump from android: /data/data/com.tencent.qqmusic/databases/)", Required: false, Value: "player_process_db"}, &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}, @@ -185,7 +186,11 @@ func appMain(c *cli.Context) (err error) { if err != nil { return err } - qmcKeys = mmkv.Merge(qmcKeys, qmc.LoadUserMMKV(c.String("qmc-mmkv"), c.String("qmc-mmkv-key"), logger)) + qmcKeys = mmkv.Merge( + qmcKeys, + qmc.LoadUserMMKV(c.String("qmc-mmkv"), c.String("qmc-mmkv-key"), logger), + qmc.LoadAndroidDB(c.String("qmc-android-db"), logger), + ) kggDbPath := c.String("kgg-db") if kggDbPath == "" { From 1028a1f2be865ed4b5803128de212d1b63caac46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=81=E6=A0=91=E4=BA=BA?= Date: Sun, 18 Oct 2026 08:10:20 +0900 Subject: [PATCH 04/13] feat(#2): support mflac2/mgg2 ext --- algo/qmc/qmc.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/algo/qmc/qmc.go b/algo/qmc/qmc.go index b58ad0b..9f8cc17 100644 --- a/algo/qmc/qmc.go +++ b/algo/qmc/qmc.go @@ -276,8 +276,8 @@ func init() { // New ogg/flac: extraExtsCanHaveSuffix := []string{"mgg", "mflac"} - // Mac also adds some extra suffix to ext: - extraExtSuffix := []string{"0", "1", "a", "h", "l", "m"} + // Mac/Android also adds some extra suffix to ext: + extraExtSuffix := []string{"0", "1", "2", "a", "h", "l", "m"} for _, ext := range extraExtsCanHaveSuffix { common.RegisterDecoder(ext, false, NewDecoder) for _, suffix := range extraExtSuffix { From bdb68efbb9faaa394378d09ddad986db5436a6f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=81=E6=A0=91=E4=BA=BA?= Date: Sun, 18 Oct 2026 08:28:55 +0900 Subject: [PATCH 05/13] docs: add comprehensive agent guide for um-cli, detailing architecture, commands, and conventions --- .cursor/rules/um-cli.mdc | 13 ++++ AGENTS.md | 141 +++++++++++++++++++++++++++++++++++++++ README.md | 47 ++++++++++--- 3 files changed, 191 insertions(+), 10 deletions(-) create mode 100644 .cursor/rules/um-cli.mdc create mode 100644 AGENTS.md diff --git a/.cursor/rules/um-cli.mdc b/.cursor/rules/um-cli.mdc new file mode 100644 index 0000000..34d0c25 --- /dev/null +++ b/.cursor/rules/um-cli.mdc @@ -0,0 +1,13 @@ +--- +description: um-cli (Unlock Music CLI) project conventions for agents +alwaysApply: true +--- + +# um-cli + +- Module: `git.um-react.app/um/cli`. Entry: `cmd/um/main.go`. Read `AGENTS.md` before structural changes. +- Decoders live under `algo//`, register via `common.RegisterDecoder` in `init()`, blank-import new packages from `cmd/um`. +- Shared non-algo code goes in `internal/`. Pass secrets through `common.CryptoParams`, not globals. +- Soft-fail optional key loaders (`Warn`); hard-fail only when decrypt cannot proceed. Normalize QMC map keys with `utils.NormalizeUnicode`. +- OS-specific code: `*_windows.go` / `*_darwin.go` or explicit `//go:build` stubs (see QMC MMKV loaders, `pc_kugou_db`). +- After flag or Go-version changes, update `README.md`. Keep changes minimal; run `go test ./...` for touched packages. diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..50ad9de --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,141 @@ +# Agent guide — um-cli + +Unlock Music CLI (`um`): decrypt vendor-locked audio files to raw streams, optionally rewrite tags/cover with ffmpeg. + +Module path: `git.um-react.app/um/cli` +Entry point: `cmd/um/main.go` + +## Quick commands + +```bash +go test ./... +go build -o um ./cmd/um +./um -h +./um --supported-ext +``` + +CI (`.gitea/workflows/build.yml`): `go test ./...` then cross-build `linux|windows|darwin` × `amd64|arm64`, `CGO_ENABLED=0`, Go `^1.25`. + +Optional runtime deps (not needed to compile): + +- `ffmpeg` / `ffprobe` — only when `--update-metadata` is set +- Windows: `qmpc-mmkv-cli.exe` for automatic QMC keys +- Windows: KuGou `KGMusicV3.db` for `.kgg` (KGMv5) + +## Layout + +``` +cmd/um/ CLI flags, directory walk / watch, decode → write pipeline +algo/ + common/ Decoder interfaces, registry, raw “noop” passthrough, CryptoParams + ncm/ kgm/ kwm/ qmc/ tm/ xiami/ ximalaya/ one package per format family +internal/ + ffmpeg/ metadata + album art via ffmpeg/ffprobe + mmkv/ MMKV file read + QMCKeys merge/normalize + sniff/ audio/image magic → extension + utils/ crypto helpers, temp files, unicode normalize + logging/ zap helpers (mostly unused; logger still global in main) +misc/ release/repack scripts +``` + +Do not invent new top-level packages; put shared non-algo code under `internal/`. + +## Architecture (decode path) + +1. `main` resolves input/output, loads crypto side-data into `common.CryptoParams`. +2. `common.GetDecoder(path, skipNoop)` matches **filename suffix** (lowercased base name; multi-dot suffixes like `.kgm.flac` supported). +3. For each matching `DecoderFactory`, `Create` → `Validate()`. First success wins. +4. Decrypted bytes are read via `io.Reader`; first 64 bytes sniffed for output extension. +5. Without `--update-metadata`: stream copy to output. With it: optional `AudioMetaGetter` / `CoverImageGetter`, then `ffmpeg.UpdateMeta`. + +Key types (`algo/common/interface.go`): + +- `Decoder` — `Validate() error` + `io.Reader` +- `StreamDecoder` — `Decrypt(buf []byte, offset int)` for stream ciphers +- `AudioMetaGetter` / `CoverImageGetter` — optional, used only with `--update-metadata` + +Registration (`algo/common/dispatch.go`): + +```go +func init() { + common.RegisterDecoder("ext", false /* noop */, NewDecoder) +} +``` + +Format packages are blank-imported from `cmd/um/main.go` so `init()` runs. New format packages **must** be blank-imported there. + +`noop == true` means “looks like plaintext audio” (see `algo/common/raw.go`). Default CLI skips them (`--skip-noop` defaults true). + +## CryptoParams / keys + +```go +type CryptoParams struct { + KggDbPath string // KuGou KGMv5 + QmcKeys QMCKeys // map[normalizedName]ekey +} +``` + +QMC key merge order in `appMain` (later maps overwrite earlier on same key): + +1. `qmc.LoadMMKVOrDefault` — **platform build tags** + - `key_mmkv_loader_darwin.go` — QQ Music Mac v8 / v10 + - `key_mmkv_loader_windows.go` — runs `qmpc-mmkv-cli` + - `key_mmkv_loader_default.go` — other OS: empty +2. `qmc.LoadUserMMKV(--qmc-mmkv, --qmc-mmkv-key)` — soft-fail +3. `qmc.LoadAndroidDB(--qmc-android-db)` — soft-fail; tables `audio_file_ekey_table` or `EKeyFileInfo` + +Always normalize map keys/values with `internal/utils.NormalizeUnicode` (NFC) when adding loaders. + +KGG: `algo/kgm/pc_kugou_db` — real decrypt in `cipher_windows.go` (filename build constraint); stub on `!windows`. + +## Adding or changing a decoder + +1. Prefer extending an existing `algo/` package over a new top-level package. +2. Implement `Validate` (seek/parse headers, init cipher) then `Read` (decrypt in place with absolute offset). +3. Register every real-world extension in `init()`; QMC already lists many aliases (Weiyun hex names, `mflac*` / `mgg*` suffixes). +4. Blank-import the package from `cmd/um` if new. +5. Pass secrets only via `DecoderParams.CryptoParams`, never globals. +6. Soft-fail optional key sources with `logger.Warn`; hard-fail only when decrypt cannot proceed. +7. Add `_test.go` next to the cipher/loader you touch; keep fixtures small. + +Optional metadata: implement `AudioMetaGetter` / `CoverImageGetter` on the same decoder type. + +## Platform / build tags + +| Pattern | Purpose | +|---------|---------| +| `*_darwin.go` / `*_windows.go` | OS-specific implementations | +| `//go:build !windows` etc. | Explicit stubs | +| `algo/qmc/qmmac/` | macOS QQ Music MMKV helpers | + +Do not call Windows-only or Darwin-only APIs from untagged files. Prefer stub + real file pairs over runtime `GOOS` switches when the whole API is unavailable. + +## Conventions + +- Errors: wrap with `%w` and a short package prefix (`qmc init cipher:`, `kgm v5:`). +- Logging: `*zap.Logger` from `DecoderParams.Logger` inside algo code; CLI still uses a package-level logger in `main` (do not expand that pattern). +- No new dependencies without need; prefer stdlib + existing modules (`urfave/cli/v2`, `zap`, `go-mmkv`, `modernc.org/sqlite`). +- Do not commit built binaries (`/um`, `*.exe` — see `.gitignore`). +- User-facing docs: keep `README.md` accurate when changing flags or Go version; keep `CHANGELOG.md` under `[Unreleased]` for notable behavior changes. +- Scope: this repo is the CLI only; crypto shared with web may live in sibling `um` projects — do not assume web TS sources are present here. + +## Useful file map + +| Task | Start here | +|------|------------| +| CLI flags / pipeline | `cmd/um/main.go` | +| Register / dispatch | `algo/common/dispatch.go`, `interface.go` | +| QMC decrypt / footer | `algo/qmc/qmc.go`, `key_derive.go`, `cipher_*.go` | +| QMC keys | `algo/qmc/key_mmkv_loader*.go`, `key_android_db_loader.go` | +| KGM / KGG | `algo/kgm/`, `algo/kgm/pc_kugou_db/` | +| NCM | `algo/ncm/` | +| Metadata rewrite | `internal/ffmpeg/` | + +## Tests + +```bash +go test ./algo/qmc/ -count=1 +go test ./... -count=1 +``` + +Prefer table-driven tests; avoid network in unit tests. Platform-specific tests may no-op or skip off-target OS. diff --git a/README.md b/README.md index 0c83a02..168d9ca 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,11 @@ Original: Web Edition https://git.um-react.app/um/web ## Features -- [x] All Algorithm Supported By `unlock-music/web` -- [x] Complete Metadata & Cover Image +- Decrypt formats supported by `unlock-music/web` (NCM, QMC/MGG/MFLAC, KGM/KGG, KWM, XM, Ximalaya, TM, …) +- Optional network metadata & cover update via `--update-metadata` (requires `ffmpeg` / `ffprobe` on `PATH`) +- Directory watch mode (`--watch`), overwrite / remove-source helpers +- QMC ekey loaders: platform defaults (macOS / Windows), user MMKV, Android `player_process_db` +- KGG (KGMv5) on Windows via KuGou `KGMusicV3.db` ## Release @@ -20,19 +23,43 @@ Original: Web Edition https://git.um-react.app/um/web ## Install from source -- Requirements: **Golang 1.23.3** +- Requirements: **Go 1.25+** (see `go.mod`) -1. run `go install git.um-react.app/um/cli/cmd/um@main` +```bash +go install git.um-react.app/um/cli/cmd/um@main +``` ### Build from repo source -1. Pull repo source. -2. Build with `go build ./cmd/um`. +```bash +git clone https://git.um-react.app/um/cli.git +cd cli +go build -o um ./cmd/um +``` -It will produce `um` or `um.exe` (Windows). +Produces `um` (or `um.exe` on Windows). ## How to use -- Drag the encrypted file to `um.exe` (Tested on Windows) -- Run: `./um [-o ] [-i] ` -- Use `./um -h` to show help menu +```bash +# Single file or directory (default input = cwd, default output = input dir) +./um [-o ] [-i] + +# Common flags +./um -o ./out --overwrite ./encrypted/ +./um --watch -o ./out ./inbox/ # process existing + watch for new files +./um --update-metadata -o ./out song.mflac # needs ffmpeg/ffprobe +./um --supported-ext # list registered extensions +./um -h +``` + +Drag-and-drop onto `um.exe` also works on Windows (passes the path as argv). + +## Development + +See [AGENTS.md](./AGENTS.md) for repository layout, how to add a decoder, and agent-oriented conventions. + +## License + +This project is licensed under the MIT License. +See the [LICENSE](./LICENSE) file for details. From c18a51af67298de135f82a84c61ad5ac56a28076 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=81=E6=A0=91=E4=BA=BA?= Date: Sun, 18 Oct 2026 08:34:52 +0900 Subject: [PATCH 06/13] chore: update changelog for v0.2.20 release, adding new features and changes --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4337323..3ece8f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [v0.2.20] - 2026-10-18 + +### Added + +- [#2] Support `.mflac2` / `.mgg2` extensions. +- [#7] Load QMC ekeys from QQ Music Android `player_process_db` (`--qmc-android-db`). +- [#8] Windows: auto-fetch QMC keys via `qmpc-mmkv-cli` (`--qmpc-mmkv-cli-exe`). + +### Changed + +- [#8] User MMKV (`--qmc-mmkv`) now merges with platform default keys on all OSes. + ## [v0.2.19] - 2025-11-26 ### Fixed @@ -78,3 +90,8 @@ git log --oneline --before="2024-11-05" ``` Or view the complete commit history on the project repository for detailed information about features, fixes, and improvements in previous releases. + +[#2]: https://git.um-react.app/um/cli/pulls/2 +[#7]: https://git.um-react.app/um/cli/pulls/7 +[#8]: https://git.um-react.app/um/cli/pulls/8 +[v0.2.20]: https://git.um-react.app/um/cli/releases/tag/v0.2.20 From 8864b11d4adc136fcc435f477948ff30845b31aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=81=E6=A0=91=E4=BA=BA?= Date: Sun, 18 Oct 2026 08:38:35 +0900 Subject: [PATCH 07/13] fix(#7): set default value for qmc-android-db flag to empty and adjust loading logic to use current directory's player_process_db if not specified --- cmd/um/main.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/cmd/um/main.go b/cmd/um/main.go index 8652d82..6b7715c 100644 --- a/cmd/um/main.go +++ b/cmd/um/main.go @@ -53,7 +53,7 @@ func main() { &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: "qmc-android-db", Usage: "path to QQMusic Android player_process_db (dump from android: /data/data/com.tencent.qqmusic/databases/)", Required: false, Value: "player_process_db"}, + &cli.StringFlag{Name: "qmc-android-db", Usage: "path to QQMusic Android player_process_db (dump from android: /data/data/com.tencent.qqmusic/databases/)", Required: false, Value: ""}, &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}, @@ -186,10 +186,18 @@ func appMain(c *cli.Context) (err error) { if err != nil { return err } + qmcAndroidKeysPath := c.String("qmc-android-db") + if qmcAndroidKeysPath == "" { + // By default, use the current directory's `player_process_db` file if exists. + // Do not complain if this file does not exist. + if db, err := os.Stat("player_process_db"); err == nil && !db.IsDir() { + qmcAndroidKeysPath = "player_process_db" + } + } qmcKeys = mmkv.Merge( qmcKeys, qmc.LoadUserMMKV(c.String("qmc-mmkv"), c.String("qmc-mmkv-key"), logger), - qmc.LoadAndroidDB(c.String("qmc-android-db"), logger), + qmc.LoadAndroidDB(qmcAndroidKeysPath, logger), ) kggDbPath := c.String("kgg-db") From 89cd9d13c3582e04db6399a3eb752bbbff168aee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=81=E6=A0=91=E4=BA=BA?= Date: Mon, 19 Oct 2026 23:18:50 +0900 Subject: [PATCH 08/13] chore: update go-mmkv dependency to v0.1.6 and refactor QMC key merging logic --- algo/common/dispatch.go | 10 ++++++++++ cmd/um/main.go | 3 +-- go.mod | 2 +- go.sum | 14 ++------------ internal/mmkv/mmkv.go | 10 ---------- 5 files changed, 14 insertions(+), 25 deletions(-) diff --git a/algo/common/dispatch.go b/algo/common/dispatch.go index fc86df6..ce139e1 100644 --- a/algo/common/dispatch.go +++ b/algo/common/dispatch.go @@ -11,6 +11,16 @@ import ( type QMCKeys map[string]string +func MergeQMCKeys(keys ...QMCKeys) QMCKeys { + result := make(QMCKeys) + for _, k := range keys { + for key, value := range k { + result[utils.NormalizeUnicode(key)] = utils.NormalizeUnicode(value) + } + } + return result +} + type CryptoParams struct { // KuGou kgg database path KggDbPath string diff --git a/cmd/um/main.go b/cmd/um/main.go index 6b7715c..3ca7e7d 100644 --- a/cmd/um/main.go +++ b/cmd/um/main.go @@ -24,7 +24,6 @@ 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" @@ -194,7 +193,7 @@ func appMain(c *cli.Context) (err error) { qmcAndroidKeysPath = "player_process_db" } } - qmcKeys = mmkv.Merge( + qmcKeys = common.MergeQMCKeys( qmcKeys, qmc.LoadUserMMKV(c.String("qmc-mmkv"), c.String("qmc-mmkv-key"), logger), qmc.LoadAndroidDB(qmcAndroidKeysPath, logger), diff --git a/go.mod b/go.mod index a17ebee..e00b909 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/go-flac/flacvorbis v0.2.0 github.com/go-flac/go-flac v1.0.0 github.com/samber/lo v1.52.0 - github.com/unlock-music/go-mmkv v0.1.4 + github.com/unlock-music/go-mmkv v0.1.6 github.com/urfave/cli/v2 v2.27.7 go.uber.org/zap v1.27.0 golang.org/x/crypto v0.44.0 diff --git a/go.sum b/go.sum index d373af5..1219931 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= -github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -14,14 +12,10 @@ github.com/go-flac/flacvorbis v0.2.0 h1:KH0xjpkNTXFER4cszH4zeJxYcrHbUobz/RticWGO github.com/go-flac/flacvorbis v0.2.0/go.mod h1:uIysHOtuU7OLGoCRG92bvnkg7QEqHx19qKRV6K1pBrI= github.com/go-flac/go-flac v1.0.0 h1:6qI9XOVLcO50xpzm3nXvO31BgDgHhnr/p/rER/K/doY= github.com/go-flac/go-flac v1.0.0/go.mod h1:WnZhcpmq4u1UdZMNn9LYSoASpWOCMOoxXxcWEHSzkW8= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= @@ -36,8 +30,8 @@ github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw= github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/unlock-music/go-mmkv v0.1.4 h1:onjxxlNd9Fcp4RYL8khOgp/DRY0n+Iuw958TdRoB/FA= -github.com/unlock-music/go-mmkv v0.1.4/go.mod h1:u1fYHzYOO7et1E55Q86lTvjXf6p14CIELszAHI8T9zQ= +github.com/unlock-music/go-mmkv v0.1.6 h1:6WW5LcMp1d6Bb8/5zdXEtuMbPta/jFcYMqW2EA8d4BY= +github.com/unlock-music/go-mmkv v0.1.6/go.mod h1:u1fYHzYOO7et1E55Q86lTvjXf6p14CIELszAHI8T9zQ= github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4= github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg= @@ -54,15 +48,11 @@ golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/y golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= -golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= -golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= diff --git a/internal/mmkv/mmkv.go b/internal/mmkv/mmkv.go index 0570f8c..368bb11 100644 --- a/internal/mmkv/mmkv.go +++ b/internal/mmkv/mmkv.go @@ -10,16 +10,6 @@ import ( "go.uber.org/zap" ) -func Merge(keys ...common.QMCKeys) common.QMCKeys { - result := make(common.QMCKeys) - for _, k := range keys { - for key, value := range k { - result[utils.NormalizeUnicode(key)] = utils.NormalizeUnicode(value) - } - } - return result -} - func LoadFromPath(path string, key string, logger *zap.Logger) (result common.QMCKeys, err error) { mmkv_path := path mmkv_crc := path + ".crc" From 06e97975d804d1309b99b5f5a211f88b58137463 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=81=E6=A0=91=E4=BA=BA?= Date: Mon, 19 Oct 2026 23:35:04 +0900 Subject: [PATCH 09/13] fix: enhance LoadFromPath function to include file size and optional CRC handling for improved error logging --- internal/mmkv/mmkv.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/internal/mmkv/mmkv.go b/internal/mmkv/mmkv.go index 368bb11..5ab040f 100644 --- a/internal/mmkv/mmkv.go +++ b/internal/mmkv/mmkv.go @@ -2,6 +2,7 @@ package mmkv import ( "fmt" + "io" "os" "git.um-react.app/um/cli/algo/common" @@ -21,6 +22,7 @@ func LoadFromPath(path string, key string, logger *zap.Logger) (result common.QM } defer mr.Close() + var crc io.Reader cr, err := os.Open(mmkv_crc) if err != nil { // crc is optional @@ -28,14 +30,22 @@ func LoadFromPath(path string, key string, logger *zap.Logger) (result common.QM key = "" } else { defer cr.Close() + crc = cr } + stat, err := os.Stat(path) + if err != nil { + logger.Error("LoadMMKV: Could not stat mmkv file", zap.Error(err)) + return nil, fmt.Errorf("LoadMMKV: stat error: %w", err) + } + mmkv_size := stat.Size() + var password []byte = nil if key != "" { password = make([]byte, 16) copy(password, []byte(key)) } - mmkv, err := go_mmkv.NewMMKVReader(mr, password, cr) + mmkv, err := go_mmkv.NewMMKVReader(mr, mmkv_size, password, crc) if err != nil { logger.Error("LoadMMKV: failed to create reader", zap.Error(err)) return nil, fmt.Errorf("LoadMMKV: NewMMKVReader error: %w", err) From ca8c4aa078ddbce852e01b0a11c82a4c1564c076 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=81=E6=A0=91=E4=BA=BA?= Date: Mon, 19 Oct 2026 23:43:47 +0900 Subject: [PATCH 10/13] feat: add flags to ignore known non-music file extensions and update processor logic to support this feature --- CHANGELOG.md | 4 +++- cmd/um/main.go | 36 +++++++++++++++++++++++++----------- cmd/um/non_music_ext.go | 21 +++++++++++++++++++++ 3 files changed, 49 insertions(+), 12 deletions(-) create mode 100644 cmd/um/non_music_ext.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ece8f1..495d57a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,13 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -## [v0.2.20] - 2026-10-18 +## [v0.2.20] - 2026-10-20 ### Added - [#2] Support `.mflac2` / `.mgg2` extensions. - [#7] Load QMC ekeys from QQ Music Android `player_process_db` (`--qmc-android-db`). - [#8] Windows: auto-fetch QMC keys via `qmpc-mmkv-cli` (`--qmpc-mmkv-cli-exe`). +- [#5] `--ignore-known-non-music-ext` / `--no-ignore-known-non-music-ext` (default on): skip files with no extension or known non-music file. + ### Changed diff --git a/cmd/um/main.go b/cmd/um/main.go index 3ca7e7d..0c71969 100644 --- a/cmd/um/main.go +++ b/cmd/um/main.go @@ -56,6 +56,8 @@ func main() { &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}, + &cli.BoolFlag{Name: "ignore-known-non-music-ext", Usage: "skip files with no extension or known non-music extensions (txt, mmkv, exe, dll)", Required: false, Value: true}, + &cli.BoolFlag{Name: "no-ignore-known-non-music-ext", Usage: "do not skip known non-music extensions", Required: false, Value: false}, &cli.BoolFlag{Name: "verbose", Aliases: []string{"V"}, Usage: "verbose logging", Required: false, Value: false}, &cli.BoolFlag{Name: "update-metadata", Usage: "update metadata & album art from network", Required: false, Value: false}, &cli.BoolFlag{Name: "overwrite", Usage: "overwrite output file without asking", Required: false, Value: false}, @@ -204,14 +206,20 @@ func appMain(c *cli.Context) (err error) { kggDbPath = filepath.Join(os.Getenv("APPDATA"), "Kugou8", "KGMusicV3.db") } + ignoreKnownNonMusicExt := c.Bool("ignore-known-non-music-ext") + if c.Bool("no-ignore-known-non-music-ext") { + ignoreKnownNonMusicExt = false + } + proc := &processor{ - logger: logger, - inputDir: inputDir, - outputDir: output, - skipNoopDecoder: c.Bool("skip-noop"), - removeSource: c.Bool("remove-source"), - updateMetadata: c.Bool("update-metadata"), - overwriteOutput: c.Bool("overwrite"), + logger: logger, + inputDir: inputDir, + outputDir: output, + skipNoopDecoder: c.Bool("skip-noop"), + ignoreKnownNonMusicExt: ignoreKnownNonMusicExt, + removeSource: c.Bool("remove-source"), + updateMetadata: c.Bool("update-metadata"), + overwriteOutput: c.Bool("overwrite"), crypto: common.CryptoParams{ // KuGou @@ -240,10 +248,11 @@ type processor struct { inputDir string outputDir string - skipNoopDecoder bool - removeSource bool - updateMetadata bool - overwriteOutput bool + skipNoopDecoder bool + ignoreKnownNonMusicExt bool + removeSource bool + updateMetadata bool + overwriteOutput bool crypto common.CryptoParams } @@ -330,6 +339,11 @@ func (p *processor) processDir(inputDir string) error { } func (p *processor) processFile(filePath string) error { + if p.ignoreKnownNonMusicExt && isKnownNonMusicExt(filePath) { + p.logger.Debug("skip known non-music extension", zap.String("file", filePath)) + return nil + } + p.logger.Debug("processFile", zap.String("file", filePath), zap.String("inputDir", p.inputDir)) allDec := common.GetDecoder(filePath, p.skipNoopDecoder) diff --git a/cmd/um/non_music_ext.go b/cmd/um/non_music_ext.go new file mode 100644 index 0000000..bd48cae --- /dev/null +++ b/cmd/um/non_music_ext.go @@ -0,0 +1,21 @@ +package main + +import ( + "path/filepath" + "slices" + "strings" +) + +// knownNonMusicExts are common non-audio suffixes to skip when walking dirs. +var knownNonMusicExts = []string{ + "", + ".txt", + ".mmkv", + ".exe", + ".dll", +} + +func isKnownNonMusicExt(path string) bool { + ext := strings.ToLower(filepath.Ext(path)) + return slices.Contains(knownNonMusicExts, ext) +} From c0437c435a287654c36b89440b1eb3d9aaa75cea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=81=E6=A0=91=E4=BA=BA?= Date: Mon, 19 Oct 2026 23:49:57 +0900 Subject: [PATCH 11/13] refactor: update LoadMMKVOrDefault to use exe variable and adjust default path for qmpc-mmkv-cli-exe flag --- algo/qmc/key_mmkv_loader_windows.go | 11 ++++------- cmd/um/main.go | 2 +- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/algo/qmc/key_mmkv_loader_windows.go b/algo/qmc/key_mmkv_loader_windows.go index 3ed7788..ec681af 100644 --- a/algo/qmc/key_mmkv_loader_windows.go +++ b/algo/qmc/key_mmkv_loader_windows.go @@ -10,12 +10,9 @@ import ( ) func LoadMMKVOrDefault(opts MMKVLoaderOptions, logger *zap.Logger) (result common.QMCKeys, err error) { - cliExe := opts.CliExe - if cliExe == "" { - cliExe = "qmpc-mmkv-cli.exe" - } + exe := opts.CliExe - cmd := exec.Command(cliExe, "-f", "plaintext", "-o", "-") + cmd := exec.Command(exe, "-f", "plaintext", "-o", "-") var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr @@ -25,13 +22,13 @@ func LoadMMKVOrDefault(opts MMKVLoaderOptions, logger *zap.Logger) (result commo 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.String("exe", exe), 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.String("exe", exe), zap.Error(err), zap.String("stderr", stderr.String()), ) diff --git a/cmd/um/main.go b/cmd/um/main.go index 0c71969..3e5bf8d 100644 --- a/cmd/um/main.go +++ b/cmd/um/main.go @@ -51,7 +51,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: "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: "qmc-android-db", Usage: "path to QQMusic Android player_process_db (dump from android: /data/data/com.tencent.qqmusic/databases/)", Required: false, Value: ""}, &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}, From e22bf8f38b36b0463c56123b5e864e8221f7b71c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=81=E6=A0=91=E4=BA=BA?= Date: Mon, 19 Oct 2026 23:54:18 +0900 Subject: [PATCH 12/13] refactor: extract command execution logic into runQmpcMMKVCLi function and improve logging in LoadMMKVOrDefault and parseQmpcMMKVCliOutput --- algo/qmc/key_mmkv_loader.go | 4 ++-- algo/qmc/key_mmkv_loader_windows.go | 37 ++++++++++++----------------- 2 files changed, 17 insertions(+), 24 deletions(-) diff --git a/algo/qmc/key_mmkv_loader.go b/algo/qmc/key_mmkv_loader.go index 479e338..a9fc990 100644 --- a/algo/qmc/key_mmkv_loader.go +++ b/algo/qmc/key_mmkv_loader.go @@ -52,10 +52,10 @@ func parseQmpcMMKVCliOutput(r io.Reader, logger *zap.Logger) common.QMCKeys { } if err := sc.Err(); err != nil { - logger.Warn("LoadMMKVOrDefault: error reading qmpc-mmkv-cli output", zap.Error(err)) + logger.Warn("parseQmpcMMKVCliOutput: 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", + logger.Warn("parseQmpcMMKVCliOutput: qmpc-mmkv-cli output has odd number of lines, ignoring last line", zap.String("line", name), ) } diff --git a/algo/qmc/key_mmkv_loader_windows.go b/algo/qmc/key_mmkv_loader_windows.go index ec681af..1e894d3 100644 --- a/algo/qmc/key_mmkv_loader_windows.go +++ b/algo/qmc/key_mmkv_loader_windows.go @@ -2,41 +2,34 @@ package qmc import ( "bytes" - "errors" "os/exec" "git.um-react.app/um/cli/algo/common" "go.uber.org/zap" ) +func runQmpcMMKVCLi(exe string) (stdout, stderr bytes.Buffer, err error) { + cmd := exec.Command(exe, "-f", "plaintext", "-o", "-") + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err = cmd.Run() + return stdout, stderr, err +} + func LoadMMKVOrDefault(opts MMKVLoaderOptions, logger *zap.Logger) (result common.QMCKeys, err error) { exe := opts.CliExe - cmd := exec.Command(exe, "-f", "plaintext", "-o", "-") - var stdout, stderr bytes.Buffer - cmd.Stdout = &stdout - cmd.Stderr = &stderr - - err = cmd.Run() + stdout, stderr, err := runQmpcMMKVCLi(exe) 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", exe), - zap.Int("exit_code", exitErr.ExitCode()), - zap.String("stderr", stderr.String()), - ) - } else { - logger.Warn("LoadMMKVOrDefault: failed to run qmpc-mmkv-cli", - zap.String("exe", exe), - zap.Error(err), - zap.String("stderr", stderr.String()), - ) - } + logger.Warn("LoadMMKVOrDefault: qmpc-mmkv-cli exited with error", + zap.String("exe", exe), + 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))) + logger.Debug("LoadMMKVOrDefault: Keys loaded from qmpc-mmkv-cli", zap.Int("count", len(result))) return result, nil } From 08c988f8bb55337355f766b36a6aa72628d36553 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=81=E6=A0=91=E4=BA=BA?= Date: Tue, 20 Oct 2026 00:18:27 +0900 Subject: [PATCH 13/13] refactor: replace mmkv.Merge with common.MergeQMCKeys in LoadMMKVOrDefault and LoadMacKeysV10 for improved key merging consistency --- algo/qmc/key_mmkv_loader_darwin.go | 3 +-- algo/qmc/qmmac/v10_darwin.go | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/algo/qmc/key_mmkv_loader_darwin.go b/algo/qmc/key_mmkv_loader_darwin.go index 23a8db4..28ef9f1 100644 --- a/algo/qmc/key_mmkv_loader_darwin.go +++ b/algo/qmc/key_mmkv_loader_darwin.go @@ -3,7 +3,6 @@ package qmc import ( "git.um-react.app/um/cli/algo/common" "git.um-react.app/um/cli/algo/qmc/qmmac" - "git.um-react.app/um/cli/internal/mmkv" "go.uber.org/zap" ) @@ -20,7 +19,7 @@ func LoadMMKVOrDefault(opts MMKVLoaderOptions, logger *zap.Logger) (result commo logger.Warn("LoadMMKVOrDefault: could not read QQMusic v10.x keys", zap.Error(err)) } - allKeys := mmkv.Merge(key1, key2) + allKeys := common.MergeQMCKeys(key1, key2) logger.Debug("Keys loaded", zap.Any("keys", allKeys), zap.Int("len", len(allKeys))) return allKeys, nil } diff --git a/algo/qmc/qmmac/v10_darwin.go b/algo/qmc/qmmac/v10_darwin.go index d3ddd2d..d33838f 100644 --- a/algo/qmc/qmmac/v10_darwin.go +++ b/algo/qmc/qmmac/v10_darwin.go @@ -159,5 +159,5 @@ func LoadMacKeysV10(logger *zap.Logger) (common.QMCKeys, error) { return nil, err } - return mmkv.Merge(append(keys1, keys2...)...), nil + return common.MergeQMCKeys(append(keys1, keys2...)...), nil }