From 6c168ee53612fba1921ac151de73d53f4d9b3d30 Mon Sep 17 00:00:00 2001 From: Unlock Music Dev Date: Tue, 22 Nov 2022 06:16:40 +0800 Subject: [PATCH] refactor: move audio sniffer to internal package --- algo/common/raw.go | 4 ++- algo/common/sniff.go | 55 ----------------------------------- algo/ncm/ncm.go | 17 +++++++---- algo/qmc/qmc.go | 3 +- algo/tm/tm.go | 3 +- algo/ximalaya/ximalaya.go | 5 ++-- cmd/um/main.go | 10 +++---- internal/sniff/audio.go | 61 +++++++++++++++++++++++++++++++++++++++ 8 files changed, 86 insertions(+), 72 deletions(-) delete mode 100644 algo/common/sniff.go create mode 100644 internal/sniff/audio.go diff --git a/algo/common/raw.go b/algo/common/raw.go index 65d8c11..c145e25 100644 --- a/algo/common/raw.go +++ b/algo/common/raw.go @@ -4,6 +4,8 @@ import ( "errors" "fmt" "io" + + "unlock-music.dev/cli/internal/sniff" ) type RawDecoder struct { @@ -26,7 +28,7 @@ func (d *RawDecoder) Validate() error { } var ok bool - d.audioExt, ok = SniffAll(header) + d.audioExt, ok = sniff.AudioExtension(header) if !ok { return errors.New("raw: sniff audio type failed") } diff --git a/algo/common/sniff.go b/algo/common/sniff.go deleted file mode 100644 index 3370305..0000000 --- a/algo/common/sniff.go +++ /dev/null @@ -1,55 +0,0 @@ -package common - -import "bytes" - -type Sniffer func(header []byte) bool - -var snifferRegistry = map[string]Sniffer{ - ".mp3": SnifferMP3, - ".flac": SnifferFLAC, - ".ogg": SnifferOGG, - ".m4a": SnifferM4A, - ".wav": SnifferWAV, - ".wma": SnifferWMA, - ".aac": SnifferAAC, - ".dff": SnifferDFF, -} - -func SniffAll(header []byte) (string, bool) { - for ext, sniffer := range snifferRegistry { - if sniffer(header) { - return ext, true - } - } - return "", false -} - -func SnifferM4A(header []byte) bool { - return len(header) >= 8 && bytes.Equal([]byte("ftyp"), header[4:8]) -} - -func SnifferOGG(header []byte) bool { - return bytes.HasPrefix(header, []byte("OggS")) -} - -func SnifferFLAC(header []byte) bool { - return bytes.HasPrefix(header, []byte("fLaC")) -} -func SnifferMP3(header []byte) bool { - return bytes.HasPrefix(header, []byte("ID3")) -} -func SnifferWAV(header []byte) bool { - return bytes.HasPrefix(header, []byte("RIFF")) -} -func SnifferWMA(header []byte) bool { - return bytes.HasPrefix(header, []byte("\x30\x26\xb2\x75\x8e\x66\xcf\x11\xa6\xd9\x00\xaa\x00\x62\xce\x6c")) -} -func SnifferAAC(header []byte) bool { - return bytes.HasPrefix(header, []byte{0xFF, 0xF1}) -} - -// SnifferDFF sniff a DSDIFF format -// reference to: https://www.sonicstudio.com/pdf/dsd/DSDIFF_1.5_Spec.pdf -func SnifferDFF(header []byte) bool { - return bytes.HasPrefix(header, []byte("FRM8")) -} diff --git a/algo/ncm/ncm.go b/algo/ncm/ncm.go index dd77835..5e20093 100644 --- a/algo/ncm/ncm.go +++ b/algo/ncm/ncm.go @@ -206,8 +206,12 @@ func (d *Decoder) GetCoverImage(ctx context.Context) ([]byte, error) { if d.cover != nil { return d.cover, nil } + + if d.meta == nil { + return nil, errors.New("ncm meta not found") + } imgURL := d.meta.GetAlbumImageURL() - if d.meta != nil && !strings.HasPrefix(imgURL, "http") { + if !strings.HasPrefix(imgURL, "http") { return nil, nil // no cover image } @@ -215,18 +219,19 @@ func (d *Decoder) GetCoverImage(ctx context.Context) ([]byte, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, imgURL, nil) resp, err := http.DefaultClient.Do(req) if err != nil { - return nil, fmt.Errorf("download image failed: %w", err) + return nil, fmt.Errorf("ncm download image failed: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("download image failed: unexpected http status %s", resp.Status) + return nil, fmt.Errorf("ncm download image failed: unexpected http status %s", resp.Status) } - data, err := io.ReadAll(resp.Body) + d.cover, err = io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("download image failed: %w", err) + return nil, fmt.Errorf("ncm download image failed: %w", err) } - return data, nil + + return d.cover, nil } func (d *Decoder) GetMeta() common.Meta { diff --git a/algo/qmc/qmc.go b/algo/qmc/qmc.go index 509ad11..24f94e8 100644 --- a/algo/qmc/qmc.go +++ b/algo/qmc/qmc.go @@ -10,6 +10,7 @@ import ( "strings" "unlock-music.dev/cli/algo/common" + "unlock-music.dev/cli/internal/sniff" ) type Decoder struct { @@ -89,7 +90,7 @@ func (d *Decoder) validateDecode() error { } d.cipher.Decrypt(buf, 0) - _, ok := common.SniffAll(buf) + _, ok := sniff.AudioExtension(buf) if !ok { return errors.New("qmc: detect file type failed") } diff --git a/algo/tm/tm.go b/algo/tm/tm.go index af7feea..d29326a 100644 --- a/algo/tm/tm.go +++ b/algo/tm/tm.go @@ -7,6 +7,7 @@ import ( "io" "unlock-music.dev/cli/algo/common" + "unlock-music.dev/cli/internal/sniff" ) var replaceHeader = []byte{0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70} @@ -30,7 +31,7 @@ func (d *Decoder) Validate() error { return nil } - if _, ok := common.SniffAll(header); ok { // not encrypted + if _, ok := sniff.AudioExtension(header); ok { // not encrypted d.audio = io.MultiReader(bytes.NewReader(header), d.raw) return nil } diff --git a/algo/ximalaya/ximalaya.go b/algo/ximalaya/ximalaya.go index cbe5457..fc92528 100644 --- a/algo/ximalaya/ximalaya.go +++ b/algo/ximalaya/ximalaya.go @@ -6,6 +6,7 @@ import ( "io" "unlock-music.dev/cli/algo/common" + "unlock-music.dev/cli/internal/sniff" ) type Decoder struct { @@ -27,7 +28,7 @@ func (d *Decoder) Validate() error { { // try to decode with x2m header := decryptX2MHeader(encryptedHeader) - if _, ok := common.SniffAll(header); ok { + if _, ok := sniff.AudioExtension(header); ok { d.audio = io.MultiReader(bytes.NewReader(header), d.rd) return nil } @@ -36,7 +37,7 @@ func (d *Decoder) Validate() error { { // try to decode with x3m // not read file again, since x2m and x3m have the same header size header := decryptX3MHeader(encryptedHeader) - if _, ok := common.SniffAll(header); ok { + if _, ok := sniff.AudioExtension(header); ok { d.audio = io.MultiReader(bytes.NewReader(header), d.rd) return nil } diff --git a/cmd/um/main.go b/cmd/um/main.go index d6d9adb..dceea7b 100644 --- a/cmd/um/main.go +++ b/cmd/um/main.go @@ -25,6 +25,7 @@ import ( _ "unlock-music.dev/cli/algo/xiami" _ "unlock-music.dev/cli/algo/ximalaya" "unlock-music.dev/cli/internal/logging" + "unlock-music.dev/cli/internal/sniff" ) var AppVersion = "v0.0.6" @@ -182,13 +183,10 @@ func tryDecFile(inputFile string, outputDir string, allDec []common.NewDecoderFu return fmt.Errorf("read header failed: %w", err) } - outExt := ".mp3" - if ext, ok := common.SniffAll(header.Bytes()); ok { - outExt = ext - } - filenameOnly := strings.TrimSuffix(filepath.Base(inputFile), filepath.Ext(inputFile)) + outExt := sniff.AudioExtensionWithFallback(header.Bytes(), ".mp3") + inFilename := strings.TrimSuffix(filepath.Base(inputFile), filepath.Ext(inputFile)) - outPath := filepath.Join(outputDir, filenameOnly+outExt) + outPath := filepath.Join(outputDir, inFilename+outExt) outFile, err := os.OpenFile(outPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) if err != nil { return err diff --git a/internal/sniff/audio.go b/internal/sniff/audio.go new file mode 100644 index 0000000..307f626 --- /dev/null +++ b/internal/sniff/audio.go @@ -0,0 +1,61 @@ +package sniff + +import "bytes" + +type Sniffer interface { + Sniff(header []byte) bool +} + +var audioExtensions = map[string]Sniffer{ + // ref: https://mimesniff.spec.whatwg.org + ".mp3": prefixSniffer("ID3"), + ".ogg": prefixSniffer("OggS"), + ".wav": prefixSniffer("RIFF"), + + // ref: https://www.loc.gov/preservation/digital/formats/fdd/fdd000027.shtml + ".wma": prefixSniffer{ + 0x30, 0x26, 0xb2, 0x75, 0x8e, 0x66, 0xcf, 0x11, + 0xa6, 0xd9, 0x00, 0xaa, 0x00, 0x62, 0xce, 0x6c, + }, + + // ref: https://www.garykessler.net/library/file_sigs.html + ".m4a": mpeg4Sniffer{}, // MPEG-4 container, m4a treat as audio + ".aac": prefixSniffer{0xFF, 0xF1}, // MPEG-4 AAC-LC + + ".flac": prefixSniffer("fLaC"), // ref: https://xiph.org/flac/format.html + ".dff": prefixSniffer("FRM8"), // DSDIFF, ref: https://www.sonicstudio.com/pdf/dsd/DSDIFF_1.5_Spec.pdf + +} + +// AudioExtension sniffs the known audio types, and returns the file extension. +// header is recommended to at least 16 bytes. +func AudioExtension(header []byte) (string, bool) { + for ext, sniffer := range audioExtensions { + if sniffer.Sniff(header) { + return ext, true + } + } + return "", false +} + +// AudioExtensionWithFallback is equivalent to AudioExtension, but returns fallback +// most likely to use .mp3 as fallback, because mp3 files may not have ID3v2 tag. +func AudioExtensionWithFallback(header []byte, fallback string) string { + ext, ok := AudioExtension(header) + if !ok { + return fallback + } + return ext +} + +type prefixSniffer []byte + +func (s prefixSniffer) Sniff(header []byte) bool { + return bytes.HasPrefix(header, s) +} + +type mpeg4Sniffer struct{} + +func (mpeg4Sniffer) Sniff(header []byte) bool { + return len(header) >= 8 && bytes.Equal([]byte("ftyp"), header[4:8]) +}