diff --git a/algo/common/dispatch.go b/algo/common/dispatch.go index 07b110c..f37d35a 100644 --- a/algo/common/dispatch.go +++ b/algo/common/dispatch.go @@ -4,9 +4,19 @@ import ( "io" "path/filepath" "strings" + + "go.uber.org/zap" ) -type NewDecoderFunc func(rd io.ReadSeeker) Decoder +type DecoderParams struct { + Reader io.ReadSeeker // required + Extension string // required, source extension, eg. ".mp3" + + FilePath string // optional, source file path + + Logger *zap.Logger // required +} +type NewDecoderFunc func(p *DecoderParams) Decoder type decoderItem struct { noop bool diff --git a/algo/common/raw.go b/algo/common/raw.go index c145e25..dd74886 100644 --- a/algo/common/raw.go +++ b/algo/common/raw.go @@ -14,8 +14,8 @@ type RawDecoder struct { audioExt string } -func NewRawDecoder(rd io.ReadSeeker) Decoder { - return &RawDecoder{rd: rd} +func NewRawDecoder(p *DecoderParams) Decoder { + return &RawDecoder{rd: p.Reader} } func (d *RawDecoder) Validate() error { diff --git a/algo/kgm/kgm.go b/algo/kgm/kgm.go index 7b5330e..a02508d 100644 --- a/algo/kgm/kgm.go +++ b/algo/kgm/kgm.go @@ -16,8 +16,8 @@ type Decoder struct { header header } -func NewDecoder(rd io.ReadSeeker) common.Decoder { - return &Decoder{rd: rd} +func NewDecoder(p *common.DecoderParams) common.Decoder { + return &Decoder{rd: p.Reader} } // Validate checks if the file is a valid Kugou (.kgm, .vpr, .kgma) file. diff --git a/algo/kwm/kwm.go b/algo/kwm/kwm.go index 166e732..1bbc604 100644 --- a/algo/kwm/kwm.go +++ b/algo/kwm/kwm.go @@ -30,8 +30,8 @@ func (d *Decoder) GetAudioExt() string { return "." + d.outputExt } -func NewDecoder(rd io.ReadSeeker) common.Decoder { - return &Decoder{rd: rd} +func NewDecoder(p *common.DecoderParams) common.Decoder { + return &Decoder{rd: p.Reader} } // Validate checks if the file is a valid Kuwo .kw file. diff --git a/algo/ncm/ncm.go b/algo/ncm/ncm.go index 5e20093..f54f1ba 100644 --- a/algo/ncm/ncm.go +++ b/algo/ncm/ncm.go @@ -29,10 +29,8 @@ var ( } ) -func NewDecoder(rd io.ReadSeeker) common.Decoder { - return &Decoder{ - rd: rd, - } +func NewDecoder(p *common.DecoderParams) common.Decoder { + return &Decoder{rd: p.Reader} } type Decoder struct { diff --git a/algo/qmc/key_mmkv.go b/algo/qmc/key_mmkv.go new file mode 100644 index 0000000..81f891a --- /dev/null +++ b/algo/qmc/key_mmkv.go @@ -0,0 +1,137 @@ +package qmc + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "runtime" + "strings" + + "github.com/hbollon/go-edlib" + "github.com/samber/lo" + "go.uber.org/zap" + "golang.org/x/exp/slices" + "unlock-music.dev/mmkv" +) + +var streamKeyVault mmkv.Vault + +// TODO: move to factory +func readKeyFromMMKV(file string, logger *zap.Logger) ([]byte, error) { + if file == "" { + return nil, errors.New("file path is required while reading key from mmkv") + } + + //goland:noinspection GoBoolExpressions + if runtime.GOOS != "darwin" { + return nil, errors.New("mmkv vault not supported on this platform") + } + + if streamKeyVault == nil { + mmkvDir, err := getRelativeMMKVDir(file) + if err != nil { + mmkvDir, err = getDefaultMMKVDir() + if err != nil { + return nil, fmt.Errorf("mmkv key valut not found: %w", err) + } + } + + mgr, err := mmkv.NewManager(mmkvDir) + if err != nil { + return nil, fmt.Errorf("init mmkv manager: %w", err) + } + + streamKeyVault, err = mgr.OpenVault("MMKVStreamEncryptId") + if err != nil { + return nil, fmt.Errorf("open mmkv vault: %w", err) + } + + logger.Debug("mmkv vault opened", zap.Strings("keys", streamKeyVault.Keys())) + } + + _, partName := filepath.Split(file) + buf, err := streamKeyVault.GetBytes(file) + + if buf == nil { + filePaths := streamKeyVault.Keys() + + for _, key := range filePaths { // fallback 1: match filename only + if !strings.HasSuffix(key, partName) { + continue + } + buf, err = streamKeyVault.GetBytes(key) + if err != nil { + logger.Warn("read key from mmkv", zap.String("key", key), zap.Error(err)) + } + } + + if buf == nil { // fallback 2: match filename with edit distance + // use editorial judgement to select the best match + // since macOS may change some characters in the file name. + // e.g. "ぜ"(e3 81 9c) -> "ぜ"(e3 81 9b e3 82 99) + fileNames := lo.Map(filePaths, func(filePath string, _ int) string { + _, name := filepath.Split(filePath) + return name + }) + + minDisStr, err := edlib.FuzzySearch(partName, fileNames, edlib.Levenshtein) + if err != nil { + logger.Warn("fuzzy search failed", zap.Error(err)) + } + + // TODO: make distance configurable + // for now, assume only 1 character changed to 2 characters + if edlib.LevenshteinDistance(partName, minDisStr) < 3 { + idx := slices.Index(fileNames, minDisStr) + buf, err = streamKeyVault.GetBytes(filePaths[idx]) + if err != nil { + logger.Warn("read key from mmkv", zap.String("key", minDisStr), zap.Error(err)) + } + } + } + } + + if len(buf) == 0 { + return nil, errors.New("key not found in mmkv vault") + } + + return deriveKey(buf) +} + +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", // todo: make configurable + "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 +} diff --git a/algo/qmc/qmc.go b/algo/qmc/qmc.go index 24f94e8..9d4002e 100644 --- a/algo/qmc/qmc.go +++ b/algo/qmc/qmc.go @@ -6,15 +6,19 @@ import ( "errors" "fmt" "io" + "runtime" "strconv" "strings" + "go.uber.org/zap" + "unlock-music.dev/cli/algo/common" "unlock-music.dev/cli/internal/sniff" ) type Decoder struct { - raw io.ReadSeeker // raw is the original file reader + raw io.ReadSeeker // raw is the original file reader + params *common.DecoderParams audio io.Reader // audio is the encrypted audio data audioLen int // audioLen is the audio data length @@ -25,6 +29,8 @@ type Decoder struct { rawMetaExtra1 int rawMetaExtra2 int + + logger *zap.Logger } // Read implements io.Reader, offer the decrypted audio data. @@ -38,8 +44,8 @@ func (d *Decoder) Read(p []byte) (int, error) { return n, err } -func NewDecoder(r io.ReadSeeker) common.Decoder { - return &Decoder{raw: r} +func NewDecoder(p *common.DecoderParams) common.Decoder { + return &Decoder{raw: p.Reader, params: p, logger: p.Logger} } func (d *Decoder) Validate() error { @@ -97,11 +103,22 @@ func (d *Decoder) validateDecode() error { return nil } -func (d *Decoder) searchKey() error { +func (d *Decoder) searchKey() (err error) { fileSizeM4, err := d.raw.Seek(-4, io.SeekEnd) if err != nil { return err } + fileSize := int(fileSizeM4) + 4 + + //goland:noinspection GoBoolExpressions + if runtime.GOOS == "darwin" && !strings.HasPrefix(d.params.Extension, ".qmc") { + d.decodedKey, err = readKeyFromMMKV(d.params.FilePath, d.logger) + if err == nil { + d.audioLen = fileSize + return + } + d.logger.Warn("read key from mmkv failed", zap.Error(err)) + } suffixBuf := make([]byte, 4) if _, err := io.ReadFull(d.raw, suffixBuf); err != nil { @@ -121,7 +138,7 @@ func (d *Decoder) searchKey() error { } // try to use default static cipher - d.audioLen = int(fileSizeM4 + 4) + d.audioLen = fileSize return nil } @@ -214,6 +231,8 @@ func init() { "mgg", "mgg1", "mggl", //QQ Music New Ogg "mflac", "mflac0", //QQ Music New Flac + + "mflach", // QQ Music Flac (storing key in dedicate MMKV) } for _, ext := range supportedExts { common.RegisterDecoder(ext, false, NewDecoder) diff --git a/algo/qmc/qmc_test.go b/algo/qmc/qmc_test.go index 7078ed2..0f1a557 100644 --- a/algo/qmc/qmc_test.go +++ b/algo/qmc/qmc_test.go @@ -7,6 +7,8 @@ import ( "os" "reflect" "testing" + + "unlock-music.dev/cli/algo/common" ) func loadTestDataQmcDecoder(filename string) ([]byte, []byte, error) { @@ -29,13 +31,14 @@ func loadTestDataQmcDecoder(filename string) ([]byte, []byte, error) { func TestMflac0Decoder_Read(t *testing.T) { tests := []struct { name string + fileExt string wantErr bool }{ - {"mflac0_rc4", false}, - {"mflac_rc4", false}, - {"mflac_map", false}, - {"mgg_map", false}, - {"qmc0_static", false}, + {"mflac0_rc4", ".mflac0", false}, + {"mflac_rc4", ".mflac", false}, + {"mflac_map", ".mflac", false}, + {"mgg_map", ".mgg", false}, + {"qmc0_static", ".qmc0", false}, } for _, tt := range tests { @@ -45,7 +48,10 @@ func TestMflac0Decoder_Read(t *testing.T) { t.Fatal(err) } - d := NewDecoder(bytes.NewReader(raw)) + d := NewDecoder(&common.DecoderParams{ + Reader: bytes.NewReader(raw), + Extension: tt.fileExt, + }) if err := d.Validate(); err != nil { t.Errorf("validate file error = %v", err) } @@ -81,7 +87,10 @@ func TestMflac0Decoder_Validate(t *testing.T) { if err != nil { t.Fatal(err) } - d := NewDecoder(bytes.NewReader(raw)) + d := NewDecoder(&common.DecoderParams{ + Reader: bytes.NewReader(raw), + Extension: tt.fileExt, + }) if err := d.Validate(); err != nil { t.Errorf("read bytes from decoder error = %v", err) diff --git a/algo/tm/tm.go b/algo/tm/tm.go index d29326a..d11ecbc 100644 --- a/algo/tm/tm.go +++ b/algo/tm/tm.go @@ -43,8 +43,8 @@ func (d *Decoder) Read(buf []byte) (int, error) { return d.audio.Read(buf) } -func NewTmDecoder(rd io.ReadSeeker) common.Decoder { - return &Decoder{raw: rd} +func NewTmDecoder(p *common.DecoderParams) common.Decoder { + return &Decoder{raw: p.Reader} } func init() { diff --git a/algo/xiami/xm.go b/algo/xiami/xm.go index f878175..83d94e4 100644 --- a/algo/xiami/xm.go +++ b/algo/xiami/xm.go @@ -37,8 +37,8 @@ func (d *Decoder) GetAudioExt() string { return "" } -func NewDecoder(rd io.ReadSeeker) common.Decoder { - return &Decoder{rd: rd} +func NewDecoder(p *common.DecoderParams) common.Decoder { + return &Decoder{rd: p.Reader} } // Validate checks if the file is a valid xiami .xm file. diff --git a/algo/ximalaya/ximalaya.go b/algo/ximalaya/ximalaya.go index fc92528..885693b 100644 --- a/algo/ximalaya/ximalaya.go +++ b/algo/ximalaya/ximalaya.go @@ -16,8 +16,8 @@ type Decoder struct { audio io.Reader } -func NewDecoder(rd io.ReadSeeker) common.Decoder { - return &Decoder{rd: rd} +func NewDecoder(p *common.DecoderParams) common.Decoder { + return &Decoder{rd: p.Reader} } func (d *Decoder) Validate() error { diff --git a/cmd/um/main.go b/cmd/um/main.go index dceea7b..4eae5e8 100644 --- a/cmd/um/main.go +++ b/cmd/um/main.go @@ -148,9 +148,10 @@ func dealDirectory(inputDir string, outputDir string, skipNoop bool, removeSourc continue } - err := tryDecFile(filepath.Join(inputDir, item.Name()), outputDir, allDec, removeSource) + filePath := filepath.Join(inputDir, item.Name()) + err := tryDecFile(filePath, outputDir, allDec, removeSource) if err != nil { - logger.Error("conversion failed", zap.String("source", item.Name()), zap.Error(err)) + logger.Error("conversion failed", zap.String("source", filePath), zap.Error(err)) } } return nil @@ -163,9 +164,16 @@ func tryDecFile(inputFile string, outputDir string, allDec []common.NewDecoderFu } defer file.Close() + decParams := &common.DecoderParams{ + Reader: file, + Extension: filepath.Ext(inputFile), + FilePath: inputFile, + Logger: logger.With(zap.String("source", inputFile)), + } + var dec common.Decoder for _, decFunc := range allDec { - dec = decFunc(file) + dec = decFunc(decParams) if err := dec.Validate(); err == nil { break } else { diff --git a/go.mod b/go.mod index 332442c..cbd1ab6 100644 --- a/go.mod +++ b/go.mod @@ -1,17 +1,23 @@ module unlock-music.dev/cli -go 1.17 +go 1.19 require ( - github.com/urfave/cli/v2 v2.23.5 - go.uber.org/zap v1.23.0 + github.com/hbollon/go-edlib v1.6.0 + github.com/samber/lo v1.36.0 + github.com/urfave/cli/v2 v2.23.6 + go.uber.org/zap v1.24.0 golang.org/x/crypto v0.3.0 + golang.org/x/exp v0.0.0-20221204150635-6dcec336b2bb + unlock-music.dev/mmkv v0.0.0-20221204231432-41a75bd29939 ) require ( github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/golang/protobuf v1.5.2 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.8.0 // indirect + google.golang.org/protobuf v1.28.1 // indirect ) diff --git a/go.sum b/go.sum index 9905ee6..4a019ce 100644 --- a/go.sum +++ b/go.sum @@ -1,88 +1,57 @@ -github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= -github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/hbollon/go-edlib v1.6.0 h1:ga7AwwVIvP8mHm9GsPueC0d71cfRU/52hmPJ7Tprv4E= +github.com/hbollon/go-edlib v1.6.0/go.mod h1:wnt6o6EIVEzUfgbUZY7BerzQ2uvzp354qmS2xaLkrhM= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/samber/lo v1.36.0 h1:4LaOxH1mHnbDGhTVE0i1z8v/lWaQW8AIfOD3HU4mSaw= +github.com/samber/lo v1.36.0/go.mod h1:HLeWcJRRyLKp3+/XBJvOrerCQn9mhdKMHyd7IRlgeQ8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M= github.com/urfave/cli/v2 v2.23.5 h1:xbrU7tAYviSpqeR3X4nEFWUdB/uDZ6DE+HxmRU7Xtyw= github.com/urfave/cli/v2 v2.23.5/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= +github.com/urfave/cli/v2 v2.23.6 h1:iWmtKD+prGo1nKUtLO0Wg4z9esfBM4rAV4QRLQiEmJ4= +github.com/urfave/cli/v2 v2.23.6/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= -go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY= go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/exp v0.0.0-20221204150635-6dcec336b2bb h1:QIsP/NmClBICkqnJ4rSIhnrGiGR7Yv9ZORGGnmmLTPk= +golang.org/x/exp v0.0.0-20221204150635-6dcec336b2bb/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +unlock-music.dev/mmkv v0.0.0-20221204231432-41a75bd29939 h1:qWv734RbYjIHtHhZSRbdSyAEJ5K1rWcPSuUOen86tvI= +unlock-music.dev/mmkv v0.0.0-20221204231432-41a75bd29939/go.mod h1:1+Hdsrk8gl1i4/oxOnAhx6y51DAcUfi2CDni6Qhk8Kw=