mirror of
https://git.um-react.app/um/cli.git
synced 2025-11-28 19:53:01 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a46428e07c | ||
|
|
57dd4b80b3 | ||
|
|
e06a21123f |
53
CHANGELOG.md
Normal file
53
CHANGELOG.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [v0.2.14] - 2025-09-08
|
||||
|
||||
### Added
|
||||
- Support MMKV dump in QQMusic Mac 10.x.
|
||||
|
||||
## [v0.2.13] - 2025-09-06
|
||||
|
||||
### Changed
|
||||
- Updated project namespace and repository URLs to new url
|
||||
- Upgraded Go version requirement to 1.25
|
||||
- Restricted KGG database support to Windows platform only
|
||||
- Enhanced MMKV key extraction logic with improved reliability
|
||||
|
||||
### Fixed
|
||||
- Fixed NCM metadata parsing to properly handle mixed-type artist arrays
|
||||
- Drop i386 targets in CI build
|
||||
|
||||
## [v0.2.12] - 2025-05-07
|
||||
|
||||
### Added
|
||||
- KGG (KGMv5) file format support
|
||||
- Support for `.mflacm` file extension
|
||||
|
||||
### Changed
|
||||
- Updated default version identifier to "custom" for development builds
|
||||
- Upgraded GoLang version
|
||||
|
||||
## [v0.2.11] - 2024-11-05
|
||||
|
||||
### Fixed
|
||||
- Resolved relative path resolution issues on Windows platforms (#108)
|
||||
- Improved cross-platform compatibility for file path handling
|
||||
|
||||
---
|
||||
|
||||
## Historical Versions
|
||||
|
||||
**Note**: This changelog was created starting from v0.2.11. For changes in earlier versions (v0.2.10 and below), please refer to the project's git commit history:
|
||||
|
||||
```bash
|
||||
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.
|
||||
@@ -1,22 +1,20 @@
|
||||
package qmc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
func LoadMMKVOrDefault(path string, key string, logger *zap.Logger) (result common.QMCKeys, err error) {
|
||||
key1, err := loadMacKeysV8(logger)
|
||||
key1, err := qmmac.LoadMacKeysV8(logger)
|
||||
if err != nil {
|
||||
key1 = nil
|
||||
logger.Warn("LoadMMKVOrDefault: could not read QQMusic v8.8.0 keys", zap.Error(err))
|
||||
}
|
||||
|
||||
key2, err := loadMacKeysV10(logger)
|
||||
key2, err := qmmac.LoadMacKeysV10(logger)
|
||||
if err != nil {
|
||||
key2 = nil
|
||||
logger.Warn("LoadMMKVOrDefault: could not read QQMusic v10.x keys", zap.Error(err))
|
||||
@@ -25,41 +23,16 @@ func LoadMMKVOrDefault(path string, key string, logger *zap.Logger) (result comm
|
||||
userKeys := make(common.QMCKeys)
|
||||
if path != "" {
|
||||
logger.Info("Using user mmkv")
|
||||
userKeys, err = LoadMMKV(path, key, logger)
|
||||
userKeys, err = mmkv.LoadFromPath(path, key, logger)
|
||||
if err != nil {
|
||||
userKeys = nil
|
||||
logger.Warn("LoadMMKVOrDefault: could not read user keys", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
allKeys := mergeMMKVKeys(key1, key2, userKeys)
|
||||
allKeys := mmkv.Merge(key1, key2, userKeys)
|
||||
|
||||
logger.Debug("Keys loaded", zap.Any("keys", allKeys), zap.Int("len", len(allKeys)))
|
||||
|
||||
return allKeys, nil
|
||||
}
|
||||
|
||||
func loadMacKeysV8(logger *zap.Logger) (keys common.QMCKeys, err error) {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
logger.Warn("Failed to get home dir")
|
||||
return nil, fmt.Errorf("loadMacKeysV8: failed to get home: %w", err)
|
||||
}
|
||||
p := filepath.Join(
|
||||
homeDir,
|
||||
"Library/Containers/com.tencent.QQMusicMac/Data",
|
||||
"Library/Application Support/QQMusicMac/mmkv",
|
||||
"MMKVStreamEncryptId",
|
||||
)
|
||||
if f, err := os.Stat(p); err == nil && !f.IsDir() {
|
||||
logger.Info("Using QQMusic 8.x mmkv", zap.String("mmkv", p))
|
||||
return LoadMMKV(p, "", logger)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func loadMacKeysV10(logger *zap.Logger) (common.QMCKeys, error) {
|
||||
// TODO: stub only
|
||||
var _ = logger
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
163
algo/qmc/qmmac/v10_darwin.go
Normal file
163
algo/qmc/qmmac/v10_darwin.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package qmmac
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.um-react.app/um/cli/algo/common"
|
||||
"git.um-react.app/um/cli/internal/mmkv"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var _RE_UDID_V10 = regexp.MustCompile(`_\x10\(([0-9a-f]{40})_`)
|
||||
|
||||
type QQMusicMacV10 struct {
|
||||
logger *zap.Logger
|
||||
mmkv_dir string
|
||||
}
|
||||
|
||||
func (q *QQMusicMacV10) extractUdids(data []byte) ([]string, error) {
|
||||
var result []string
|
||||
for _, match := range _RE_UDID_V10.FindAllSubmatch(data, -1) {
|
||||
udid := string(match[1])
|
||||
q.logger.Debug("extractUdids: found udid", zap.String("udid", udid))
|
||||
result = append(result, udid)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (q *QQMusicMacV10) caesar(text string, shift int) string {
|
||||
var result strings.Builder
|
||||
|
||||
for _, char := range []byte(text) {
|
||||
var transformed byte
|
||||
if 'A' <= char && char <= 'Z' {
|
||||
transformed = (char-'A'+byte(shift))%26 + 'A'
|
||||
} else if 'a' <= char && char <= 'z' {
|
||||
transformed = (char-'a'+byte(shift))%26 + 'a'
|
||||
} else if '0' <= char && char <= '9' {
|
||||
transformed = (char-'0'+byte(shift))%10 + '0'
|
||||
} else {
|
||||
transformed = char
|
||||
}
|
||||
result.WriteByte(transformed)
|
||||
}
|
||||
|
||||
return result.String()
|
||||
}
|
||||
|
||||
func (q *QQMusicMacV10) mmkv(udid string, id int) (path string, key string, err error) {
|
||||
str1 := q.caesar(udid, id+3)
|
||||
int1, err := strconv.ParseInt(udid[5:7], 16, 32)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("getMmkv: could not parse udid: %w", err)
|
||||
}
|
||||
int2 := 5 + (int(int1)+id)%4
|
||||
name := str1[:int2]
|
||||
path = filepath.Join(q.mmkv_dir, name)
|
||||
|
||||
int3 := id + 0xa546
|
||||
str3 := fmt.Sprintf("%s%04x", udid, int3)
|
||||
hash := md5.Sum([]byte(str3))
|
||||
key = fmt.Sprintf("%x", hash)[:16]
|
||||
|
||||
return path, key, nil
|
||||
}
|
||||
|
||||
func (q *QQMusicMacV10) loadByPList(plist_path string) ([]common.QMCKeys, error) {
|
||||
logger := q.logger.With(zap.String("plist", plist_path))
|
||||
logger.Debug("loadMacKeysV10: load key from plist")
|
||||
if f, err := os.Stat(plist_path); err != nil || f.IsDir() {
|
||||
logger.Debug("loadMacKeysV10: plist not found")
|
||||
return nil, nil
|
||||
}
|
||||
plist_data, err := os.ReadFile(plist_path)
|
||||
if err != nil {
|
||||
logger.Warn("loadMacKeysV10: could not read plist", zap.Error(err))
|
||||
return nil, fmt.Errorf("loadMacKeysV10: could not read plist: %w", err)
|
||||
}
|
||||
udids, err := q.extractUdids(plist_data)
|
||||
if err != nil {
|
||||
logger.Warn("loadMacKeysV10: could not extract udid", zap.Error(err))
|
||||
return nil, fmt.Errorf("loadMacKeysV10: could not extract udid: %w", err)
|
||||
}
|
||||
logger.Debug("loadMacKeysV10: read udid", zap.Strings("udids", udids))
|
||||
|
||||
var keysList []common.QMCKeys
|
||||
for _, udid := range udids {
|
||||
keys, err := q.loadByUDID(udid, logger)
|
||||
if err != nil {
|
||||
logger.Warn("loadMacKeysV10: could not load by udid", zap.String("udid", udid), zap.Error(err))
|
||||
continue
|
||||
}
|
||||
keysList = append(keysList, keys)
|
||||
}
|
||||
return keysList, nil
|
||||
}
|
||||
func (q *QQMusicMacV10) loadByUDID(udid string, logger *zap.Logger) (common.QMCKeys, error) {
|
||||
mmkv_path, mmkv_key, err := q.mmkv(udid, 1)
|
||||
if err != nil {
|
||||
logger.Warn("loadMacKeysV10: could not get mmkv name/key", zap.Error(err))
|
||||
return nil, fmt.Errorf("loadMacKeysV10: could not get mmkv name/key: %w", err)
|
||||
}
|
||||
logger.Info("Using QQMusic 10.x mmkv", zap.String("mmkv", mmkv_path))
|
||||
keys, err := mmkv.LoadFromPath(mmkv_path, mmkv_key, logger)
|
||||
if err != nil {
|
||||
logger.Warn("loadMacKeysV10: could not load mmkv", zap.String("mmkv", mmkv_path), zap.Error(err))
|
||||
return nil, fmt.Errorf("loadMacKeysV10: could not load mmkv: %w", err)
|
||||
}
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
func LoadMacKeysV10(logger *zap.Logger) (common.QMCKeys, error) {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
logger.Warn("Failed to get home dir")
|
||||
return nil, fmt.Errorf("loadMacKeysV10: failed to get home: %w", err)
|
||||
}
|
||||
|
||||
// MMKV dir is always inside the sandbox container
|
||||
mmkv_dir := filepath.Join(
|
||||
homeDir,
|
||||
"Library/Containers/com.tencent.QQMusicMac/Data/",
|
||||
"Library/Application Support/QQMusicMac/iData",
|
||||
)
|
||||
if f, err := os.Stat(mmkv_dir); err != nil || !f.IsDir() {
|
||||
logger.Debug("loadMacKeysV10: mmkv dir not found", zap.String("mmkv_dir", mmkv_dir))
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// without sandbox
|
||||
plist_without_sandbox := filepath.Join(
|
||||
homeDir,
|
||||
"Library/Preferences/com.tencent.QQMusicMac.plist",
|
||||
)
|
||||
|
||||
// with sandbox (e.g. from App Store)
|
||||
plist_sandboxed := filepath.Join(
|
||||
homeDir,
|
||||
"Library/Containers/com.tencent.QQMusicMac/Data/",
|
||||
"Library/Preferences/com.tencent.QQMusicMac.plist",
|
||||
)
|
||||
|
||||
q := QQMusicMacV10{
|
||||
logger: logger,
|
||||
mmkv_dir: mmkv_dir,
|
||||
}
|
||||
|
||||
keys1, err := q.loadByPList(plist_without_sandbox)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keys2, err := q.loadByPList(plist_sandboxed)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mmkv.Merge(append(keys1, keys2...)...), nil
|
||||
}
|
||||
30
algo/qmc/qmmac/v8_darwin.go
Normal file
30
algo/qmc/qmmac/v8_darwin.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package qmmac
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"git.um-react.app/um/cli/algo/common"
|
||||
"git.um-react.app/um/cli/internal/mmkv"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func LoadMacKeysV8(logger *zap.Logger) (keys common.QMCKeys, err error) {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
logger.Warn("Failed to get home dir")
|
||||
return nil, fmt.Errorf("loadMacKeysV8: failed to get home: %w", err)
|
||||
}
|
||||
p := filepath.Join(
|
||||
homeDir,
|
||||
"Library/Containers/com.tencent.QQMusicMac/Data",
|
||||
"Library/Application Support/QQMusicMac/mmkv",
|
||||
"MMKVStreamEncryptId",
|
||||
)
|
||||
if f, err := os.Stat(p); err == nil && !f.IsDir() {
|
||||
logger.Info("Using QQMusic 8.x mmkv", zap.String("mmkv", p))
|
||||
return mmkv.LoadFromPath(p, "", logger)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
2
go.mod
2
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.47.0
|
||||
github.com/unlock-music/go-mmkv v0.1.1
|
||||
github.com/unlock-music/go-mmkv v0.1.2
|
||||
github.com/urfave/cli/v2 v2.27.5
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/crypto v0.29.0
|
||||
|
||||
2
go.sum
2
go.sum
@@ -32,6 +32,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/unlock-music/go-mmkv v0.1.1 h1:1w3shjaBMp58jRrRkxPghnMpvyUoP4ZkA++W9ueFpE8=
|
||||
github.com/unlock-music/go-mmkv v0.1.1/go.mod h1:E1h/HBpWPOKnCQw6LSQZkMpzxQojGDe+T5f/DRDXBsc=
|
||||
github.com/unlock-music/go-mmkv v0.1.2 h1:nZvYoyO/LO4ycXHYmObgHVNXxqMjfINeCYwKFmiyOQc=
|
||||
github.com/unlock-music/go-mmkv v0.1.2/go.mod h1:E1h/HBpWPOKnCQw6LSQZkMpzxQojGDe+T5f/DRDXBsc=
|
||||
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
|
||||
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
package qmc
|
||||
package mmkv
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"git.um-react.app/um/cli/algo/common"
|
||||
"git.um-react.app/um/cli/internal/utils"
|
||||
@@ -11,7 +10,7 @@ import (
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func mergeMMKVKeys(keys ...common.QMCKeys) common.QMCKeys {
|
||||
func Merge(keys ...common.QMCKeys) common.QMCKeys {
|
||||
result := make(common.QMCKeys)
|
||||
for _, k := range keys {
|
||||
for key, value := range k {
|
||||
@@ -21,7 +20,7 @@ func mergeMMKVKeys(keys ...common.QMCKeys) common.QMCKeys {
|
||||
return result
|
||||
}
|
||||
|
||||
func LoadMMKV(path string, key string, logger *zap.Logger) (result common.QMCKeys, err error) {
|
||||
func LoadFromPath(path string, key string, logger *zap.Logger) (result common.QMCKeys, err error) {
|
||||
mmkv_path := path
|
||||
mmkv_crc := path + ".crc"
|
||||
|
||||
@@ -33,9 +32,11 @@ func LoadMMKV(path string, key string, logger *zap.Logger) (result common.QMCKey
|
||||
defer mr.Close()
|
||||
|
||||
cr, err := os.Open(mmkv_crc)
|
||||
if err == nil {
|
||||
if err != nil {
|
||||
// crc is optional
|
||||
cr = nil
|
||||
logger.Warn("LoadMMKV: Failed to open crc file, assuming no encryption", zap.Error(err))
|
||||
key = ""
|
||||
} else {
|
||||
defer cr.Close()
|
||||
}
|
||||
|
||||
@@ -68,41 +69,3 @@ func LoadMMKV(path string, key string, logger *zap.Logger) (result common.QMCKey
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// getRelativeMMKVDir get mmkv dir relative to file (legacy QQMusic for macOS behaviour)
|
||||
func getRelativeMMKVDir(file string) (string, error) {
|
||||
mmkvDir := filepath.Join(filepath.Dir(file), "../mmkv")
|
||||
if _, err := os.Stat(mmkvDir); err != nil {
|
||||
return "", fmt.Errorf("stat default mmkv dir: %w", err)
|
||||
}
|
||||
|
||||
keyFile := filepath.Join(mmkvDir, "MMKVStreamEncryptId")
|
||||
if _, err := os.Stat(keyFile); err != nil {
|
||||
return "", fmt.Errorf("stat default mmkv file: %w", err)
|
||||
}
|
||||
|
||||
return mmkvDir, nil
|
||||
}
|
||||
|
||||
func getDefaultMMKVDir() (string, error) {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("get user home dir: %w", err)
|
||||
}
|
||||
|
||||
mmkvDir := filepath.Join(
|
||||
homeDir,
|
||||
"Library/Containers/com.tencent.QQMusicMac/Data",
|
||||
"Library/Application Support/QQMusicMac/mmkv",
|
||||
)
|
||||
if _, err := os.Stat(mmkvDir); err != nil {
|
||||
return "", fmt.Errorf("stat default mmkv dir: %w", err)
|
||||
}
|
||||
|
||||
keyFile := filepath.Join(mmkvDir, "MMKVStreamEncryptId")
|
||||
if _, err := os.Stat(keyFile); err != nil {
|
||||
return "", fmt.Errorf("stat default mmkv file: %w", err)
|
||||
}
|
||||
|
||||
return mmkvDir, nil
|
||||
}
|
||||
Reference in New Issue
Block a user