Compare commits

..
17 Commits
Author SHA1 Message Date
鲁树人 8864b11d4a 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 2026-10-18 08:38:35 +09:00
鲁树人 c18a51af67 chore: update changelog for v0.2.20 release, adding new features and changes 2026-10-18 08:34:52 +09:00
鲁树人 bdb68efbb9 docs: add comprehensive agent guide for um-cli, detailing architecture, commands, and conventions 2026-10-18 08:28:55 +09:00
鲁树人 1028a1f2be feat(#2): support mflac2/mgg2 ext 2026-10-18 08:12:01 +09:00
鲁树人 09622d01a7 feat(#7): implement LoadAndroidDB function and add tests for loading ekeys from QQ Music Android SQLite database 2026-10-18 08:11:51 +09:00
鲁树人 3ed50f3f30 chore: update go.sum with new dependencies and versions 2026-10-18 08:11:48 +09:00
鲁树人 061876f9bf refactor(#8): update LoadMMKVOrDefault function to use MMKVLoaderOptions and implement platform-specific loading for Windows 2026-10-18 08:11:35 +09:00
鲁树人 107aa2c3a8 chore: update go-mmkv dependency to v0.1.4 and document MMKV parsing fix in changelog 2025-11-26 22:26:05 +09:00
鲁树人 a2190e0ff5 docs: update changelog 2025-11-17 08:46:23 +09:00
鲁树人 c104a0bc6e chore: add missing libc dep and cleanup 2025-11-17 08:31:34 +09:00
鲁树人 a163d40fb2 chore: upgrade deps 2025-11-12 20:48:38 +09:00
鲁树人 40f93b4dd2 chore: upgrade mmkv lib 2025-11-12 20:32:06 +09:00
鲁树人 8c1f40bfe1 docs: update changelog 2025-09-15 21:48:20 +09:00
鲁树人 aea3bd5714 fix: Handle musicex tag correctly 2025-09-15 21:35:12 +09:00
鲁树人 e4bfefd0a6 fix: use proper regex 2025-09-09 22:32:43 +09:00
鲁树人 b1aa3bacdf fix: relax regex to extract udid from plist 2025-09-09 22:24:16 +09:00
鲁树人 6ec434f6b1 docs: update v0.2.15 changelog 2025-09-09 21:54:16 +09:00
20 changed files with 789 additions and 105 deletions
+13
View File
@@ -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/<vendor>/`, 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.
+141
View File
@@ -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/<vendor>` 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.
+46 -2
View File
@@ -7,12 +7,51 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [v0.2.14] - 2025-09-08 ## [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
- MMKV parsing: Handle cases where MMKV value is empty.
## [v0.2.18] - 2025-11-16
### Changed
- QMC2: Fix `musicex\0` tag parsing.
- MMKV: Improved tolerance for corrupted MMKV file parsing.
- Updated project dependencies.
## [v0.2.17] - 2025-09-09 ⚠️ **(Broken Release)**
### Changed
- Update RegEx used to extract UDID in plist.
## [v0.2.16] - 2025-09-09 ⚠️ **(Broken Release)**
### Changed
- Update RegEx used to extract UDID in plist.
## [v0.2.15] - 2025-09-09 ⚠️ **(Broken Release)**
### Added
- Support MMKV dump in QQMusic Mac 10.x (AppStore version).
## [v0.2.14] - 2025-09-08 ⚠️ **(Broken Release)**
### Added ### Added
- Support MMKV dump in QQMusic Mac 10.x. - Support MMKV dump in QQMusic Mac 10.x.
## [v0.2.13] - 2025-09-06 ## [v0.2.13] - 2025-09-06 ⚠️ **(Broken Release)**
### Changed ### Changed
- Updated project namespace and repository URLs to new url - Updated project namespace and repository URLs to new url
@@ -51,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. 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
+37 -10
View File
@@ -11,8 +11,11 @@ Original: Web Edition https://git.um-react.app/um/web
## Features ## Features
- [x] All Algorithm Supported By `unlock-music/web` - Decrypt formats supported by `unlock-music/web` (NCM, QMC/MGG/MFLAC, KGM/KGG, KWM, XM, Ximalaya, TM, …)
- [x] Complete Metadata & Cover Image - 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 ## Release
@@ -20,19 +23,43 @@ Original: Web Edition https://git.um-react.app/um/web
## Install from source ## 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 ### Build from repo source
1. Pull repo source. ```bash
2. Build with `go build ./cmd/um`. 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 ## How to use
- Drag the encrypted file to `um.exe` (Tested on Windows) ```bash
- Run: `./um [-o <output dir>] [-i] <input dir/file>` # Single file or directory (default input = cwd, default output = input dir)
- Use `./um -h` to show help menu ./um [-o <output dir>] [-i] <input dir/file>
# 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.
+110
View File
@@ -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
}
+135
View File
@@ -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")
}
}
+64
View File
@@ -0,0 +1,64 @@
package qmc
import (
"bufio"
"io"
"strings"
"git.um-react.app/um/cli/algo/common"
"git.um-react.app/um/cli/internal/mmkv"
"git.um-react.app/um/cli/internal/utils"
"go.uber.org/zap"
)
// MMKVLoaderOptions holds platform-specific inputs for LoadMMKVOrDefault.
type MMKVLoaderOptions struct {
CliExe string // qmpc-mmkv-cli path (--qmpc-mmkv-cli-exe); Windows only
}
// LoadUserMMKV loads keys from a user-specified mmkv path. Soft-fails with a warning.
func LoadUserMMKV(path string, key string, logger *zap.Logger) common.QMCKeys {
if path == "" {
return nil
}
logger.Debug("LoadUserMMKV: Loading user mmkv", zap.String("path", path), zap.String("key", key))
userKeys, err := mmkv.LoadFromPath(path, key, logger)
if err != nil {
logger.Warn("LoadUserMMKV: could not read user keys", zap.Error(err))
return nil
}
logger.Info("Using user mmkv", zap.String("path", path), zap.Int("count", len(userKeys)))
return userKeys
}
// parseQmpcMMKVCliOutput reads qmpc-mmkv-cli plaintext output as alternating
// filename / ekey lines and builds a QMCKeys map.
func parseQmpcMMKVCliOutput(r io.Reader, logger *zap.Logger) common.QMCKeys {
sc := bufio.NewScanner(r)
result := make(common.QMCKeys)
var name string
nameRead := false
for sc.Scan() {
line := utils.NormalizeUnicode(strings.TrimSpace(sc.Text()))
if !nameRead {
name = line
} else {
if name != "" && line != "" {
result[name] = line
}
}
nameRead = !nameRead
}
if err := sc.Err(); err != nil {
logger.Warn("LoadMMKVOrDefault: error reading qmpc-mmkv-cli output", zap.Error(err))
}
if nameRead {
logger.Warn("LoadMMKVOrDefault: qmpc-mmkv-cli output has odd number of lines, ignoring last line",
zap.String("line", name),
)
}
return result
}
+2 -14
View File
@@ -7,7 +7,7 @@ import (
"go.uber.org/zap" "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) key1, err := qmmac.LoadMacKeysV8(logger)
if err != nil { if err != nil {
key1 = 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)) logger.Warn("LoadMMKVOrDefault: could not read QQMusic v10.x keys", zap.Error(err))
} }
userKeys := make(common.QMCKeys) allKeys := mmkv.Merge(key1, key2)
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)
logger.Debug("Keys loaded", zap.Any("keys", allKeys), zap.Int("len", len(allKeys))) logger.Debug("Keys loaded", zap.Any("keys", allKeys), zap.Int("len", len(allKeys)))
return allKeys, nil return allKeys, nil
} }
+3 -3
View File
@@ -1,4 +1,4 @@
//go:build !darwin //go:build !darwin && !windows
package qmc package qmc
@@ -7,7 +7,7 @@ import (
"go.uber.org/zap" "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) {
// Stub: do nothing // No platform-default QMC keys on this OS.
return nil, nil return nil, nil
} }
+79
View File
@@ -0,0 +1,79 @@
package qmc
import (
"strings"
"testing"
"go.uber.org/zap"
)
func TestParseQmpcMMKVCliOutput(t *testing.T) {
logger := zap.NewNop()
t.Run("pairs", func(t *testing.T) {
in := "file1.mflac\nkey1\nfile2.mgg\nkey2\n"
got := parseQmpcMMKVCliOutput(strings.NewReader(in), logger)
if len(got) != 2 {
t.Fatalf("len = %d, want 2", len(got))
}
if got["file1.mflac"] != "key1" {
t.Errorf("file1.mflac = %q, want key1", got["file1.mflac"])
}
if got["file2.mgg"] != "key2" {
t.Errorf("file2.mgg = %q, want key2", got["file2.mgg"])
}
})
t.Run("crlf", func(t *testing.T) {
in := "a.mflac\r\nKEYA\r\nb.mflac\r\nKEYB\r\n"
got := parseQmpcMMKVCliOutput(strings.NewReader(in), logger)
if got["a.mflac"] != "KEYA" || got["b.mflac"] != "KEYB" {
t.Errorf("got %#v", got)
}
})
t.Run("odd line ignored", func(t *testing.T) {
in := "file1.mflac\nkey1\norphan\n"
got := parseQmpcMMKVCliOutput(strings.NewReader(in), logger)
if len(got) != 1 || got["file1.mflac"] != "key1" {
t.Errorf("got %#v, want single pair", got)
}
})
t.Run("empty name skipped", func(t *testing.T) {
in := "\nkey-for-empty\nfile.mflac\nrealkey\n"
got := parseQmpcMMKVCliOutput(strings.NewReader(in), logger)
if len(got) != 1 || got["file.mflac"] != "realkey" {
t.Errorf("got %#v", got)
}
if _, ok := got[""]; ok {
t.Error("empty name should not be stored")
}
})
t.Run("empty key skipped", func(t *testing.T) {
in := "empty-key.mflac\n\nfile.mflac\nrealkey\n"
got := parseQmpcMMKVCliOutput(strings.NewReader(in), logger)
if len(got) != 1 || got["file.mflac"] != "realkey" {
t.Errorf("got %#v", got)
}
if _, ok := got["empty-key.mflac"]; ok {
t.Error("empty key should not be stored")
}
})
t.Run("empty input", func(t *testing.T) {
got := parseQmpcMMKVCliOutput(strings.NewReader(""), logger)
if len(got) != 0 {
t.Errorf("got %#v, want empty", got)
}
})
t.Run("no trailing newline", func(t *testing.T) {
in := "file.mflac\nkey"
got := parseQmpcMMKVCliOutput(strings.NewReader(in), logger)
if got["file.mflac"] != "key" {
t.Errorf("got %#v", got)
}
})
}
+45
View File
@@ -0,0 +1,45 @@
package qmc
import (
"bytes"
"errors"
"os/exec"
"git.um-react.app/um/cli/algo/common"
"go.uber.org/zap"
)
func LoadMMKVOrDefault(opts MMKVLoaderOptions, logger *zap.Logger) (result common.QMCKeys, err error) {
cliExe := opts.CliExe
if cliExe == "" {
cliExe = "qmpc-mmkv-cli.exe"
}
cmd := exec.Command(cliExe, "-f", "plaintext", "-o", "-")
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err = cmd.Run()
if err != nil {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
logger.Warn("LoadMMKVOrDefault: qmpc-mmkv-cli exited with non-zero status",
zap.String("exe", cliExe),
zap.Int("exit_code", exitErr.ExitCode()),
zap.String("stderr", stderr.String()),
)
} else {
logger.Warn("LoadMMKVOrDefault: failed to run qmpc-mmkv-cli",
zap.String("exe", cliExe),
zap.Error(err),
zap.String("stderr", stderr.String()),
)
}
return nil, nil
}
result = parseQmpcMMKVCliOutput(&stdout, logger)
logger.Debug("Keys loaded from qmpc-mmkv-cli", zap.Int("len", len(result)))
return result, nil
}
+16 -7
View File
@@ -137,11 +137,11 @@ func (d *Decoder) searchKey() (err error) {
d.decodedKey, err = deriveKey([]byte(key)) d.decodedKey, err = deriveKey([]byte(key))
if err == nil { if err == nil {
d.audioLen = fileSize d.audioLen = fileSize
return nil } else {
}
d.decodedKey = nil d.decodedKey = nil
d.logger.Warn("could not derive key, skip", zap.Error(err)) d.logger.Warn("could not derive key, skip", zap.Error(err))
} }
}
suffixBuf := make([]byte, 4) suffixBuf := make([]byte, 4)
if _, err := io.ReadFull(d.raw, suffixBuf); err != nil { if _, err := io.ReadFull(d.raw, suffixBuf); err != nil {
@@ -153,7 +153,7 @@ func (d *Decoder) searchKey() (err error) {
return d.readRawMetaQTag() return d.readRawMetaQTag()
case "STag": case "STag":
return errors.New("qmc: file with 'STag' suffix doesn't contains media key") return errors.New("qmc: file with 'STag' suffix doesn't contains media key")
// MusicEx\0 // speculative guess for "musicex\0"
case "cex\x00": case "cex\x00":
footer, err := NewMusicExTag(d.raw) footer, err := NewMusicExTag(d.raw)
if err != nil { if err != nil {
@@ -161,17 +161,26 @@ func (d *Decoder) searchKey() (err error) {
} }
d.audioLen = fileSize - int(footer.TagSize) d.audioLen = fileSize - int(footer.TagSize)
if key, ok := d.params.CryptoParams.QmcKeys.Get(footer.MediaFileName); ok { if key, ok := d.params.CryptoParams.QmcKeys.Get(footer.MediaFileName); ok {
d.logger.Debug("searchKey: using key from MediaFileName", zap.String("MediaFileName", footer.MediaFileName), zap.String("key", key))
d.decodedKey, err = deriveKey([]byte(key)) d.decodedKey, err = deriveKey([]byte(key))
} else if d.decodedKey == nil {
return errors.New("searchKey: no key found for musicex tag")
} }
return err return err
default: default:
size := binary.LittleEndian.Uint32(suffixBuf) // if we already have a key from legacy mmkv, use it
if d.decodedKey != nil {
return nil
}
// try to use suffix as key length
size := binary.LittleEndian.Uint32(suffixBuf)
if size <= 0xFFFF && size != 0 { // assume size is key len if size <= 0xFFFF && size != 0 { // assume size is key len
return d.readRawKey(int64(size)) return d.readRawKey(int64(size))
} }
// try to use default static cipher // try to use default static cipher,
// or the key read from the legacy mmkv
d.audioLen = fileSize d.audioLen = fileSize
return nil return nil
} }
@@ -267,8 +276,8 @@ func init() {
// New ogg/flac: // New ogg/flac:
extraExtsCanHaveSuffix := []string{"mgg", "mflac"} extraExtsCanHaveSuffix := []string{"mgg", "mflac"}
// Mac also adds some extra suffix to ext: // Mac/Android also adds some extra suffix to ext:
extraExtSuffix := []string{"0", "1", "a", "h", "l", "m"} extraExtSuffix := []string{"0", "1", "2", "a", "h", "l", "m"}
for _, ext := range extraExtsCanHaveSuffix { for _, ext := range extraExtsCanHaveSuffix {
common.RegisterDecoder(ext, false, NewDecoder) common.RegisterDecoder(ext, false, NewDecoder)
for _, suffix := range extraExtSuffix { for _, suffix := range extraExtSuffix {
+5 -4
View File
@@ -42,24 +42,25 @@ func NewMusicExTag(f io.ReadSeeker) (*MusicExTagV1, error) {
tag := &MusicExTagV1{ tag := &MusicExTagV1{
TagSize: binary.LittleEndian.Uint32(buffer[0x00:0x04]), TagSize: binary.LittleEndian.Uint32(buffer[0x00:0x04]),
TagVersion: binary.LittleEndian.Uint32(buffer[0x04:0x08]), TagVersion: binary.LittleEndian.Uint32(buffer[0x04:0x08]),
TagMagic: buffer[0x04:0x0C], TagMagic: buffer[0x08:],
} }
if !bytes.Equal(tag.TagMagic, []byte("musicex\x00")) { if !bytes.Equal(tag.TagMagic, []byte("musicex\x00")) {
return nil, errors.New("MusicEx magic mismatch") return nil, errors.New("MusicEx magic mismatch")
} }
if tag.TagVersion != 1 { if tag.TagVersion != 1 {
return nil, errors.New(fmt.Sprintf("unsupported musicex tag version. expecting 1, got %d", tag.TagVersion)) return nil, fmt.Errorf("unsupported musicex tag version. expecting 1, got %d", tag.TagVersion)
} }
if tag.TagSize < 0xC0 { if tag.TagSize < 0xC0 {
return nil, errors.New(fmt.Sprintf("unsupported musicex tag size. expecting at least 0xC0, got 0x%02x", tag.TagSize)) return nil, fmt.Errorf("unsupported musicex tag size. expecting at least 0xC0, got 0x%02x", tag.TagSize)
} }
buffer = make([]byte, tag.TagSize) buffer = make([]byte, tag.TagSize)
f.Seek(-int64(tag.TagSize), io.SeekEnd)
bytesRead, err = f.Read(buffer) bytesRead, err = f.Read(buffer)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("MusicExV1: Read error %w", err)
} }
if uint32(bytesRead) != tag.TagSize { if uint32(bytesRead) != tag.TagSize {
return nil, fmt.Errorf("MusicExV1: read %d bytes (expected %d)", bytesRead, tag.TagSize) return nil, fmt.Errorf("MusicExV1: read %d bytes (expected %d)", bytesRead, tag.TagSize)
+1 -1
View File
@@ -61,7 +61,7 @@ func (d *Decoder) getMetaBySongID(ctx context.Context) error {
func (d *Decoder) searchMetaOnline(ctx context.Context, original common.AudioMeta) error { func (d *Decoder) searchMetaOnline(ctx context.Context, original common.AudioMeta) error {
c := client.NewQQMusicClient() // todo: use global client c := client.NewQQMusicClient() // todo: use global client
keyword := lo.WithoutEmpty(append( keyword := lo.Compact(append(
[]string{original.GetTitle(), original.GetAlbum()}, []string{original.GetTitle(), original.GetAlbum()},
original.GetArtists()...), original.GetArtists()...),
) )
+1 -1
View File
@@ -14,7 +14,7 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
) )
var _RE_UDID_V10 = regexp.MustCompile(`_\x10\(([0-9a-f]{40})_`) var _RE_UDID_V10 = regexp.MustCompile(`_\x10\(([0-9a-f]{40})`)
type QQMusicMacV10 struct { type QQMusicMacV10 struct {
logger *zap.Logger logger *zap.Logger
+20 -2
View File
@@ -24,6 +24,7 @@ import (
_ "git.um-react.app/um/cli/algo/xiami" _ "git.um-react.app/um/cli/algo/xiami"
_ "git.um-react.app/um/cli/algo/ximalaya" _ "git.um-react.app/um/cli/algo/ximalaya"
"git.um-react.app/um/cli/internal/ffmpeg" "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/sniff"
"git.um-react.app/um/cli/internal/utils" "git.um-react.app/um/cli/internal/utils"
"github.com/fsnotify/fsnotify" "github.com/fsnotify/fsnotify"
@@ -51,6 +52,8 @@ func main() {
&cli.StringFlag{Name: "output", Aliases: []string{"o"}, Usage: "path to output dir", Required: false}, &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", 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: "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: ""},
&cli.StringFlag{Name: "kgg-db", Usage: "path to kgg db (win32 kugou v11)", Required: false}, &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: "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: "skip-noop", Aliases: []string{"n"}, Usage: "skip noop decoder", Required: false, Value: true},
@@ -176,11 +179,26 @@ func appMain(c *cli.Context) (err error) {
return errors.New("output should be a writable directory") return errors.New("output should be a writable directory")
} }
// QMC: Load keys // QMC: Load platform keys, then merge user mmkv (--qmc-mmkv) on all platforms.
qmcKeys, err := qmc.LoadMMKVOrDefault(c.String("qmc-mmkv"), c.String("qmc-mmkv-key"), logger) qmcKeys, err := qmc.LoadMMKVOrDefault(qmc.MMKVLoaderOptions{
CliExe: c.String("qmpc-mmkv-cli-exe"),
}, logger)
if err != nil { if err != nil {
return err 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(qmcAndroidKeysPath, logger),
)
kggDbPath := c.String("kgg-db") kggDbPath := c.String("kgg-db")
if kggDbPath == "" { if kggDbPath == "" {
+14 -14
View File
@@ -3,32 +3,32 @@ module git.um-react.app/um/cli
go 1.25.1 go 1.25.1
require ( require (
github.com/fsnotify/fsnotify v1.8.0 github.com/fsnotify/fsnotify v1.9.0
github.com/go-flac/flacpicture v0.3.0 github.com/go-flac/flacpicture v0.3.0
github.com/go-flac/flacvorbis v0.2.0 github.com/go-flac/flacvorbis v0.2.0
github.com/go-flac/go-flac v1.0.0 github.com/go-flac/go-flac v1.0.0
github.com/samber/lo v1.47.0 github.com/samber/lo v1.52.0
github.com/unlock-music/go-mmkv v0.1.2 github.com/unlock-music/go-mmkv v0.1.4
github.com/urfave/cli/v2 v2.27.5 github.com/urfave/cli/v2 v2.27.7
go.uber.org/zap v1.27.0 go.uber.org/zap v1.27.0
golang.org/x/crypto v0.29.0 golang.org/x/crypto v0.44.0
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 golang.org/x/text v0.31.0
golang.org/x/text v0.20.0 modernc.org/sqlite v1.40.0
modernc.org/sqlite v1.37.0
) )
require ( require (
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
golang.org/x/sys v0.31.0 // indirect golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
modernc.org/libc v1.62.1 // indirect golang.org/x/sys v0.38.0 // indirect
modernc.org/libc v1.66.10 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.9.1 // indirect modernc.org/memory v1.11.0 // indirect
) )
+52 -42
View File
@@ -1,86 +1,96 @@
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 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= 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/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-flac/flacpicture v0.3.0 h1:LkmTxzFLIynwfhHiZsX0s8xcr3/u33MzvV89u+zOT8I= github.com/go-flac/flacpicture v0.3.0 h1:LkmTxzFLIynwfhHiZsX0s8xcr3/u33MzvV89u+zOT8I=
github.com/go-flac/flacpicture v0.3.0/go.mod h1:DPbrzVYQ3fJcvSgLFp9HXIrEQEdfdk/+m0nQCzwodZI= github.com/go-flac/flacpicture v0.3.0/go.mod h1:DPbrzVYQ3fJcvSgLFp9HXIrEQEdfdk/+m0nQCzwodZI=
github.com/go-flac/flacvorbis v0.2.0 h1:KH0xjpkNTXFER4cszH4zeJxYcrHbUobz/RticWGOESs= github.com/go-flac/flacvorbis v0.2.0 h1:KH0xjpkNTXFER4cszH4zeJxYcrHbUobz/RticWGOESs=
github.com/go-flac/flacvorbis v0.2.0/go.mod h1:uIysHOtuU7OLGoCRG92bvnkg7QEqHx19qKRV6K1pBrI= 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 h1:6qI9XOVLcO50xpzm3nXvO31BgDgHhnr/p/rER/K/doY=
github.com/go-flac/go-flac v1.0.0/go.mod h1:WnZhcpmq4u1UdZMNn9LYSoASpWOCMOoxXxcWEHSzkW8= 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 h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= 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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= 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 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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.4 h1:onjxxlNd9Fcp4RYL8khOgp/DRY0n+Iuw958TdRoB/FA=
github.com/unlock-music/go-mmkv v0.1.1/go.mod h1:E1h/HBpWPOKnCQw6LSQZkMpzxQojGDe+T5f/DRDXBsc= github.com/unlock-music/go-mmkv v0.1.4/go.mod h1:u1fYHzYOO7et1E55Q86lTvjXf6p14CIELszAHI8T9zQ=
github.com/unlock-music/go-mmkv v0.1.2 h1:nZvYoyO/LO4ycXHYmObgHVNXxqMjfINeCYwKFmiyOQc= github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
github.com/unlock-music/go-mmkv v0.1.2/go.mod h1:E1h/HBpWPOKnCQw6LSQZkMpzxQojGDe+T5f/DRDXBsc= github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg=
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.25.2 h1:T2oH7sZdGvTaie0BRNFbIYsabzCxUQg8nLqCdQ2i0ic= modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4=
modernc.org/cc/v4 v4.25.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.25.1 h1:TFSzPrAGmDsdnhT9X2UrcPMI3N/mJ9/X9ykKXwLhDsU= modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A=
modernc.org/ccgo/v4 v4.25.1/go.mod h1:njjuAYiPflywOOrm3B7kCB444ONP5pAVr8PIEoE0uDw= modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/libc v1.62.1 h1:s0+fv5E3FymN8eJVmnk0llBe6rOxCu/DEU+XygRbS8s= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/libc v1.62.1/go.mod h1:iXhATfJQLjG3NWy56a6WVU73lWOcdYVxsvwCgoPljuo= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A=
modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.9.1 h1:V/Z1solwAVmMW1yttq3nDdZPJqV1rM05Ccq6KMSZ34g= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.9.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI= modernc.org/sqlite v1.40.0 h1:bNWEDlYhNPAUdUdBzjAvn8icAs/2gaKlj4vM+tQ6KdQ=
modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM= modernc.org/sqlite v1.40.0/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+3 -2
View File
@@ -2,14 +2,15 @@ package ffmpeg
import ( import (
"context" "context"
"go.uber.org/zap"
"mime" "mime"
"slices"
"strings" "strings"
"go.uber.org/zap"
"github.com/go-flac/flacpicture" "github.com/go-flac/flacpicture"
"github.com/go-flac/flacvorbis" "github.com/go-flac/flacvorbis"
"github.com/go-flac/go-flac" "github.com/go-flac/go-flac"
"golang.org/x/exp/slices"
) )
func updateMetaFlac(_ context.Context, outPath string, m *UpdateMetadataParams, logger *zap.Logger) error { func updateMetaFlac(_ context.Context, outPath string, m *UpdateMetadataParams, logger *zap.Logger) error {
+1 -2
View File
@@ -3,8 +3,7 @@ package sniff
import ( import (
"bytes" "bytes"
"encoding/binary" "encoding/binary"
"slices"
"golang.org/x/exp/slices"
) )
type Sniffer interface { type Sniffer interface {