mirror of
https://git.um-react.app/um/cli.git
synced 2025-11-28 03:33:02 +00:00
Compare commits
16 Commits
v0.2.0-tes
...
v0.2.0-tes
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb948e74df | ||
|
|
7637a91f71 | ||
|
|
e7d360362e | ||
|
|
04320bd45a | ||
|
|
26b580a4b8 | ||
|
|
6c168ee536 | ||
|
|
62a38d5ab4 | ||
|
|
62548955dc | ||
|
|
3794ff3154 | ||
|
|
b22453215f | ||
|
|
81862b26c9 | ||
|
|
110a78433a | ||
|
|
d896925dff | ||
|
|
bd95fdb53b | ||
|
|
f6748d644d | ||
|
|
8e068b9c8d |
119
.drone.jsonnet
Normal file
119
.drone.jsonnet
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
// generate .drone.yaml, run:
|
||||||
|
// drone jsonnet --format --stream
|
||||||
|
|
||||||
|
|
||||||
|
local CreateRelease() = {
|
||||||
|
name: 'create release',
|
||||||
|
image: 'plugins/gitea-release',
|
||||||
|
settings: {
|
||||||
|
api_key: { from_secret: 'GITEA_API_KEY' },
|
||||||
|
base_url: 'https://git.unlock-music.dev',
|
||||||
|
files: 'dist/*',
|
||||||
|
checksum: 'sha256',
|
||||||
|
draft: true,
|
||||||
|
title: '${DRONE_TAG}',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
local StepGoBuild(GOOS, GOARCH) = {
|
||||||
|
local filepath = 'dist/um-%s-%s.tar.gz' % [GOOS, GOARCH],
|
||||||
|
|
||||||
|
name: 'go build %s/%s' % [GOOS, GOARCH],
|
||||||
|
image: 'golang:1.19',
|
||||||
|
environment: {
|
||||||
|
GOOS: GOOS,
|
||||||
|
GOARCH: GOARCH,
|
||||||
|
},
|
||||||
|
commands: [
|
||||||
|
'DIST_DIR=$(mktemp -d)',
|
||||||
|
'go build -v -trimpath -ldflags="-w -s -X main.AppVersion=$(git describe --tags --always)" -o $DIST_DIR ./cmd/um',
|
||||||
|
'mkdir -p dist',
|
||||||
|
'tar cz -f %s -C $DIST_DIR .' % filepath,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
local StepUploadArtifact(GOOS, GOARCH) = {
|
||||||
|
local filename = 'um-%s-%s.tar.gz' % [GOOS, GOARCH],
|
||||||
|
local filepath = 'dist/%s' % filename,
|
||||||
|
local pkgname = '${DRONE_REPO_NAME}-build',
|
||||||
|
|
||||||
|
name: 'upload artifact',
|
||||||
|
image: 'golang:1.19', // reuse golang:1.19 for curl
|
||||||
|
environment: {
|
||||||
|
DRONE_GITEA_SERVER: 'https://git.unlock-music.dev',
|
||||||
|
GITEA_API_KEY: { from_secret: 'GITEA_API_KEY' },
|
||||||
|
},
|
||||||
|
commands: [
|
||||||
|
'curl --fail --include --user "um-release-bot:$GITEA_API_KEY" ' +
|
||||||
|
'--upload-file "%s" ' % filepath +
|
||||||
|
'"$DRONE_GITEA_SERVER/api/packages/${DRONE_REPO_NAMESPACE}/generic/%s/${DRONE_BUILD_NUMBER}/%s"' % [pkgname, filename],
|
||||||
|
'sha256sum %s' % filepath,
|
||||||
|
'echo $DRONE_GITEA_SERVER/${DRONE_REPO_NAMESPACE}/-/packages/generic/%s/${DRONE_BUILD_NUMBER}' % pkgname,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
local PipelineBuild(GOOS, GOARCH, RUN_TEST) = {
|
||||||
|
name: 'build %s/%s' % [GOOS, GOARCH],
|
||||||
|
kind: 'pipeline',
|
||||||
|
type: 'docker',
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
name: 'fetch tags',
|
||||||
|
image: 'alpine/git',
|
||||||
|
commands: ['git fetch --tags'],
|
||||||
|
},
|
||||||
|
] +
|
||||||
|
(
|
||||||
|
if RUN_TEST then [{
|
||||||
|
name: 'go test',
|
||||||
|
image: 'golang:1.19',
|
||||||
|
commands: ['go test -v ./...'],
|
||||||
|
}] else []
|
||||||
|
)
|
||||||
|
+
|
||||||
|
[
|
||||||
|
StepGoBuild(GOOS, GOARCH),
|
||||||
|
StepUploadArtifact(GOOS, GOARCH),
|
||||||
|
],
|
||||||
|
trigger: {
|
||||||
|
event: ['push', 'pull_request'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
local PipelineRelease() = {
|
||||||
|
name: 'release',
|
||||||
|
kind: 'pipeline',
|
||||||
|
type: 'docker',
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
name: 'fetch tags',
|
||||||
|
image: 'alpine/git',
|
||||||
|
commands: ['git fetch --tags'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'go test',
|
||||||
|
image: 'golang:1.19',
|
||||||
|
commands: ['go test -v ./...'],
|
||||||
|
},
|
||||||
|
StepGoBuild('linux', 'amd64'),
|
||||||
|
StepGoBuild('linux', 'arm64'),
|
||||||
|
StepGoBuild('linux', '386'),
|
||||||
|
StepGoBuild('windows', 'amd64'),
|
||||||
|
StepGoBuild('windows', '386'),
|
||||||
|
StepGoBuild('darwin', 'amd64'),
|
||||||
|
StepGoBuild('darwin', 'arm64'),
|
||||||
|
CreateRelease(),
|
||||||
|
],
|
||||||
|
trigger: {
|
||||||
|
event: ['tag'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
[
|
||||||
|
PipelineBuild('linux', 'amd64', true),
|
||||||
|
PipelineBuild('windows', 'amd64', false),
|
||||||
|
PipelineBuild('darwin', 'amd64', false),
|
||||||
|
PipelineRelease(),
|
||||||
|
]
|
||||||
212
.drone.yml
Normal file
212
.drone.yml
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
---
|
||||||
|
kind: pipeline
|
||||||
|
name: build linux/amd64
|
||||||
|
steps:
|
||||||
|
- commands:
|
||||||
|
- git fetch --tags
|
||||||
|
image: alpine/git
|
||||||
|
name: fetch tags
|
||||||
|
- commands:
|
||||||
|
- go test -v ./...
|
||||||
|
image: golang:1.19
|
||||||
|
name: go test
|
||||||
|
- commands:
|
||||||
|
- DIST_DIR=$(mktemp -d)
|
||||||
|
- go build -v -trimpath -ldflags="-w -s -X main.AppVersion=$(git describe --tags
|
||||||
|
--always)" -o $DIST_DIR ./cmd/um
|
||||||
|
- mkdir -p dist
|
||||||
|
- tar cz -f dist/um-linux-amd64.tar.gz -C $DIST_DIR .
|
||||||
|
environment:
|
||||||
|
GOARCH: amd64
|
||||||
|
GOOS: linux
|
||||||
|
image: golang:1.19
|
||||||
|
name: go build linux/amd64
|
||||||
|
- commands:
|
||||||
|
- curl --fail --include --user "um-release-bot:$GITEA_API_KEY" --upload-file "dist/um-linux-amd64.tar.gz"
|
||||||
|
"$DRONE_GITEA_SERVER/api/packages/${DRONE_REPO_NAMESPACE}/generic/${DRONE_REPO_NAME}-build/${DRONE_BUILD_NUMBER}/um-linux-amd64.tar.gz"
|
||||||
|
- sha256sum dist/um-linux-amd64.tar.gz
|
||||||
|
- echo $DRONE_GITEA_SERVER/${DRONE_REPO_NAMESPACE}/-/packages/generic/${DRONE_REPO_NAME}-build/${DRONE_BUILD_NUMBER}
|
||||||
|
environment:
|
||||||
|
DRONE_GITEA_SERVER: https://git.unlock-music.dev
|
||||||
|
GITEA_API_KEY:
|
||||||
|
from_secret: GITEA_API_KEY
|
||||||
|
image: golang:1.19
|
||||||
|
name: upload artifact
|
||||||
|
trigger:
|
||||||
|
event:
|
||||||
|
- push
|
||||||
|
- pull_request
|
||||||
|
type: docker
|
||||||
|
---
|
||||||
|
kind: pipeline
|
||||||
|
name: build windows/amd64
|
||||||
|
steps:
|
||||||
|
- commands:
|
||||||
|
- git fetch --tags
|
||||||
|
image: alpine/git
|
||||||
|
name: fetch tags
|
||||||
|
- commands:
|
||||||
|
- DIST_DIR=$(mktemp -d)
|
||||||
|
- go build -v -trimpath -ldflags="-w -s -X main.AppVersion=$(git describe --tags
|
||||||
|
--always)" -o $DIST_DIR ./cmd/um
|
||||||
|
- mkdir -p dist
|
||||||
|
- tar cz -f dist/um-windows-amd64.tar.gz -C $DIST_DIR .
|
||||||
|
environment:
|
||||||
|
GOARCH: amd64
|
||||||
|
GOOS: windows
|
||||||
|
image: golang:1.19
|
||||||
|
name: go build windows/amd64
|
||||||
|
- commands:
|
||||||
|
- curl --fail --include --user "um-release-bot:$GITEA_API_KEY" --upload-file "dist/um-windows-amd64.tar.gz"
|
||||||
|
"$DRONE_GITEA_SERVER/api/packages/${DRONE_REPO_NAMESPACE}/generic/${DRONE_REPO_NAME}-build/${DRONE_BUILD_NUMBER}/um-windows-amd64.tar.gz"
|
||||||
|
- sha256sum dist/um-windows-amd64.tar.gz
|
||||||
|
- echo $DRONE_GITEA_SERVER/${DRONE_REPO_NAMESPACE}/-/packages/generic/${DRONE_REPO_NAME}-build/${DRONE_BUILD_NUMBER}
|
||||||
|
environment:
|
||||||
|
DRONE_GITEA_SERVER: https://git.unlock-music.dev
|
||||||
|
GITEA_API_KEY:
|
||||||
|
from_secret: GITEA_API_KEY
|
||||||
|
image: golang:1.19
|
||||||
|
name: upload artifact
|
||||||
|
trigger:
|
||||||
|
event:
|
||||||
|
- push
|
||||||
|
- pull_request
|
||||||
|
type: docker
|
||||||
|
---
|
||||||
|
kind: pipeline
|
||||||
|
name: build darwin/amd64
|
||||||
|
steps:
|
||||||
|
- commands:
|
||||||
|
- git fetch --tags
|
||||||
|
image: alpine/git
|
||||||
|
name: fetch tags
|
||||||
|
- commands:
|
||||||
|
- DIST_DIR=$(mktemp -d)
|
||||||
|
- go build -v -trimpath -ldflags="-w -s -X main.AppVersion=$(git describe --tags
|
||||||
|
--always)" -o $DIST_DIR ./cmd/um
|
||||||
|
- mkdir -p dist
|
||||||
|
- tar cz -f dist/um-darwin-amd64.tar.gz -C $DIST_DIR .
|
||||||
|
environment:
|
||||||
|
GOARCH: amd64
|
||||||
|
GOOS: darwin
|
||||||
|
image: golang:1.19
|
||||||
|
name: go build darwin/amd64
|
||||||
|
- commands:
|
||||||
|
- curl --fail --include --user "um-release-bot:$GITEA_API_KEY" --upload-file "dist/um-darwin-amd64.tar.gz"
|
||||||
|
"$DRONE_GITEA_SERVER/api/packages/${DRONE_REPO_NAMESPACE}/generic/${DRONE_REPO_NAME}-build/${DRONE_BUILD_NUMBER}/um-darwin-amd64.tar.gz"
|
||||||
|
- sha256sum dist/um-darwin-amd64.tar.gz
|
||||||
|
- echo $DRONE_GITEA_SERVER/${DRONE_REPO_NAMESPACE}/-/packages/generic/${DRONE_REPO_NAME}-build/${DRONE_BUILD_NUMBER}
|
||||||
|
environment:
|
||||||
|
DRONE_GITEA_SERVER: https://git.unlock-music.dev
|
||||||
|
GITEA_API_KEY:
|
||||||
|
from_secret: GITEA_API_KEY
|
||||||
|
image: golang:1.19
|
||||||
|
name: upload artifact
|
||||||
|
trigger:
|
||||||
|
event:
|
||||||
|
- push
|
||||||
|
- pull_request
|
||||||
|
type: docker
|
||||||
|
---
|
||||||
|
kind: pipeline
|
||||||
|
name: release
|
||||||
|
steps:
|
||||||
|
- commands:
|
||||||
|
- git fetch --tags
|
||||||
|
image: alpine/git
|
||||||
|
name: fetch tags
|
||||||
|
- commands:
|
||||||
|
- go test -v ./...
|
||||||
|
image: golang:1.19
|
||||||
|
name: go test
|
||||||
|
- commands:
|
||||||
|
- DIST_DIR=$(mktemp -d)
|
||||||
|
- go build -v -trimpath -ldflags="-w -s -X main.AppVersion=$(git describe --tags
|
||||||
|
--always)" -o $DIST_DIR ./cmd/um
|
||||||
|
- mkdir -p dist
|
||||||
|
- tar cz -f dist/um-linux-amd64.tar.gz -C $DIST_DIR .
|
||||||
|
environment:
|
||||||
|
GOARCH: amd64
|
||||||
|
GOOS: linux
|
||||||
|
image: golang:1.19
|
||||||
|
name: go build linux/amd64
|
||||||
|
- commands:
|
||||||
|
- DIST_DIR=$(mktemp -d)
|
||||||
|
- go build -v -trimpath -ldflags="-w -s -X main.AppVersion=$(git describe --tags
|
||||||
|
--always)" -o $DIST_DIR ./cmd/um
|
||||||
|
- mkdir -p dist
|
||||||
|
- tar cz -f dist/um-linux-arm64.tar.gz -C $DIST_DIR .
|
||||||
|
environment:
|
||||||
|
GOARCH: arm64
|
||||||
|
GOOS: linux
|
||||||
|
image: golang:1.19
|
||||||
|
name: go build linux/arm64
|
||||||
|
- commands:
|
||||||
|
- DIST_DIR=$(mktemp -d)
|
||||||
|
- go build -v -trimpath -ldflags="-w -s -X main.AppVersion=$(git describe --tags
|
||||||
|
--always)" -o $DIST_DIR ./cmd/um
|
||||||
|
- mkdir -p dist
|
||||||
|
- tar cz -f dist/um-linux-386.tar.gz -C $DIST_DIR .
|
||||||
|
environment:
|
||||||
|
GOARCH: "386"
|
||||||
|
GOOS: linux
|
||||||
|
image: golang:1.19
|
||||||
|
name: go build linux/386
|
||||||
|
- commands:
|
||||||
|
- DIST_DIR=$(mktemp -d)
|
||||||
|
- go build -v -trimpath -ldflags="-w -s -X main.AppVersion=$(git describe --tags
|
||||||
|
--always)" -o $DIST_DIR ./cmd/um
|
||||||
|
- mkdir -p dist
|
||||||
|
- tar cz -f dist/um-windows-amd64.tar.gz -C $DIST_DIR .
|
||||||
|
environment:
|
||||||
|
GOARCH: amd64
|
||||||
|
GOOS: windows
|
||||||
|
image: golang:1.19
|
||||||
|
name: go build windows/amd64
|
||||||
|
- commands:
|
||||||
|
- DIST_DIR=$(mktemp -d)
|
||||||
|
- go build -v -trimpath -ldflags="-w -s -X main.AppVersion=$(git describe --tags
|
||||||
|
--always)" -o $DIST_DIR ./cmd/um
|
||||||
|
- mkdir -p dist
|
||||||
|
- tar cz -f dist/um-windows-386.tar.gz -C $DIST_DIR .
|
||||||
|
environment:
|
||||||
|
GOARCH: "386"
|
||||||
|
GOOS: windows
|
||||||
|
image: golang:1.19
|
||||||
|
name: go build windows/386
|
||||||
|
- commands:
|
||||||
|
- DIST_DIR=$(mktemp -d)
|
||||||
|
- go build -v -trimpath -ldflags="-w -s -X main.AppVersion=$(git describe --tags
|
||||||
|
--always)" -o $DIST_DIR ./cmd/um
|
||||||
|
- mkdir -p dist
|
||||||
|
- tar cz -f dist/um-darwin-amd64.tar.gz -C $DIST_DIR .
|
||||||
|
environment:
|
||||||
|
GOARCH: amd64
|
||||||
|
GOOS: darwin
|
||||||
|
image: golang:1.19
|
||||||
|
name: go build darwin/amd64
|
||||||
|
- commands:
|
||||||
|
- DIST_DIR=$(mktemp -d)
|
||||||
|
- go build -v -trimpath -ldflags="-w -s -X main.AppVersion=$(git describe --tags
|
||||||
|
--always)" -o $DIST_DIR ./cmd/um
|
||||||
|
- mkdir -p dist
|
||||||
|
- tar cz -f dist/um-darwin-arm64.tar.gz -C $DIST_DIR .
|
||||||
|
environment:
|
||||||
|
GOARCH: arm64
|
||||||
|
GOOS: darwin
|
||||||
|
image: golang:1.19
|
||||||
|
name: go build darwin/arm64
|
||||||
|
- image: plugins/gitea-release
|
||||||
|
name: create release
|
||||||
|
settings:
|
||||||
|
api_key:
|
||||||
|
from_secret: GITEA_API_KEY
|
||||||
|
base_url: https://git.unlock-music.dev
|
||||||
|
checksum: sha256
|
||||||
|
draft: true
|
||||||
|
files: dist/*
|
||||||
|
title: ${DRONE_TAG}
|
||||||
|
trigger:
|
||||||
|
event:
|
||||||
|
- tag
|
||||||
|
type: docker
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1 +1,3 @@
|
|||||||
.idea
|
.idea
|
||||||
|
|
||||||
|
/dist
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
|
"unlock-music.dev/cli/internal/sniff"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RawDecoder struct {
|
type RawDecoder struct {
|
||||||
@@ -26,7 +28,7 @@ func (d *RawDecoder) Validate() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var ok bool
|
var ok bool
|
||||||
d.audioExt, ok = SniffAll(header)
|
d.audioExt, ok = sniff.AudioExtension(header)
|
||||||
if !ok {
|
if !ok {
|
||||||
return errors.New("raw: sniff audio type failed")
|
return errors.New("raw: sniff audio type failed")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"))
|
|
||||||
}
|
|
||||||
@@ -8,17 +8,20 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Decoder struct {
|
type Decoder struct {
|
||||||
header header
|
rd io.ReadSeeker
|
||||||
cipher common.StreamDecoder
|
|
||||||
|
|
||||||
rd io.ReadSeeker
|
cipher common.StreamDecoder
|
||||||
offset int
|
offset int
|
||||||
|
|
||||||
|
header header
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDecoder(rd io.ReadSeeker) common.Decoder {
|
func NewDecoder(rd io.ReadSeeker) common.Decoder {
|
||||||
return &Decoder{rd: rd}
|
return &Decoder{rd: rd}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate checks if the file is a valid Kugou (.kgm, .vpr, .kgma) file.
|
||||||
|
// rd will be seeked to the beginning of the encrypted audio.
|
||||||
func (d *Decoder) Validate() (err error) {
|
func (d *Decoder) Validate() (err error) {
|
||||||
if err := d.header.FromFile(d.rd); err != nil {
|
if err := d.header.FromFile(d.rd); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -12,13 +12,14 @@ import (
|
|||||||
"unlock-music.dev/cli/algo/common"
|
"unlock-music.dev/cli/algo/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
const magicHeader = "yeelion-kuwo-tme"
|
const magicHeader1 = "yeelion-kuwo-tme"
|
||||||
|
const magicHeader2 = "yeelion-kuwo\x00\x00\x00\x00"
|
||||||
const keyPreDefined = "MoOtOiTvINGwd2E6n0E1i7L5t2IoOoNk"
|
const keyPreDefined = "MoOtOiTvINGwd2E6n0E1i7L5t2IoOoNk"
|
||||||
|
|
||||||
type Decoder struct {
|
type Decoder struct {
|
||||||
cipher common.StreamDecoder
|
rd io.ReadSeeker
|
||||||
|
|
||||||
rd io.ReadSeeker
|
cipher common.StreamDecoder
|
||||||
offset int
|
offset int
|
||||||
|
|
||||||
outputExt string
|
outputExt string
|
||||||
@@ -33,6 +34,8 @@ func NewDecoder(rd io.ReadSeeker) common.Decoder {
|
|||||||
return &Decoder{rd: rd}
|
return &Decoder{rd: rd}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate checks if the file is a valid Kuwo .kw file.
|
||||||
|
// rd will be seeked to the beginning of the encrypted audio.
|
||||||
func (d *Decoder) Validate() error {
|
func (d *Decoder) Validate() error {
|
||||||
header := make([]byte, 0x400) // kwm header is fixed to 1024 bytes
|
header := make([]byte, 0x400) // kwm header is fixed to 1024 bytes
|
||||||
_, err := io.ReadFull(d.rd, header)
|
_, err := io.ReadFull(d.rd, header)
|
||||||
@@ -41,7 +44,9 @@ func (d *Decoder) Validate() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// check magic header, 0x00 - 0x0F
|
// check magic header, 0x00 - 0x0F
|
||||||
if !bytes.Equal([]byte(magicHeader), header[:len(magicHeader)]) {
|
magicHeader := header[:0x10]
|
||||||
|
if !bytes.Equal([]byte(magicHeader1), magicHeader) &&
|
||||||
|
!bytes.Equal([]byte(magicHeader2), magicHeader) {
|
||||||
return errors.New("kwm magic header not matched")
|
return errors.New("kwm magic header not matched")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,22 +36,19 @@ func NewDecoder(rd io.ReadSeeker) common.Decoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Decoder struct {
|
type Decoder struct {
|
||||||
rd io.ReadSeeker
|
rd io.ReadSeeker // rd is the original file reader
|
||||||
|
|
||||||
offset int
|
offset int
|
||||||
|
|
||||||
cipher common.StreamDecoder
|
cipher common.StreamDecoder
|
||||||
|
|
||||||
key []byte
|
|
||||||
box []byte
|
|
||||||
|
|
||||||
metaRaw []byte
|
metaRaw []byte
|
||||||
metaType string
|
metaType string
|
||||||
meta RawMeta
|
meta RawMeta
|
||||||
|
cover []byte
|
||||||
cover []byte
|
|
||||||
audio []byte
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate checks if the file is a valid Netease .ncm file.
|
||||||
|
// rd will be seeked to the beginning of the encrypted audio.
|
||||||
func (d *Decoder) Validate() error {
|
func (d *Decoder) Validate() error {
|
||||||
if err := d.validateMagicHeader(); err != nil {
|
if err := d.validateMagicHeader(); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -209,8 +206,12 @@ func (d *Decoder) GetCoverImage(ctx context.Context) ([]byte, error) {
|
|||||||
if d.cover != nil {
|
if d.cover != nil {
|
||||||
return d.cover, nil
|
return d.cover, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if d.meta == nil {
|
||||||
|
return nil, errors.New("ncm meta not found")
|
||||||
|
}
|
||||||
imgURL := d.meta.GetAlbumImageURL()
|
imgURL := d.meta.GetAlbumImageURL()
|
||||||
if d.meta != nil && !strings.HasPrefix(imgURL, "http") {
|
if !strings.HasPrefix(imgURL, "http") {
|
||||||
return nil, nil // no cover image
|
return nil, nil // no cover image
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,18 +219,19 @@ func (d *Decoder) GetCoverImage(ctx context.Context) ([]byte, error) {
|
|||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, imgURL, nil)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, imgURL, nil)
|
||||||
resp, err := http.DefaultClient.Do(req)
|
resp, err := http.DefaultClient.Do(req)
|
||||||
if err != nil {
|
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()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
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 {
|
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 {
|
func (d *Decoder) GetMeta() common.Meta {
|
||||||
|
|||||||
@@ -10,17 +10,19 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"unlock-music.dev/cli/algo/common"
|
"unlock-music.dev/cli/algo/common"
|
||||||
|
"unlock-music.dev/cli/internal/sniff"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Decoder struct {
|
type Decoder struct {
|
||||||
raw io.ReadSeeker
|
raw io.ReadSeeker // raw is the original file reader
|
||||||
audio io.Reader
|
|
||||||
offset int
|
|
||||||
audioLen int
|
|
||||||
|
|
||||||
cipher common.StreamDecoder
|
audio io.Reader // audio is the encrypted audio data
|
||||||
|
audioLen int // audioLen is the audio data length
|
||||||
|
offset int // offset is the current audio read position
|
||||||
|
|
||||||
|
decodedKey []byte // decodedKey is the decoded key for cipher
|
||||||
|
cipher common.StreamDecoder
|
||||||
|
|
||||||
decodedKey []byte
|
|
||||||
rawMetaExtra1 int
|
rawMetaExtra1 int
|
||||||
rawMetaExtra2 int
|
rawMetaExtra2 int
|
||||||
}
|
}
|
||||||
@@ -88,7 +90,7 @@ func (d *Decoder) validateDecode() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
d.cipher.Decrypt(buf, 0)
|
d.cipher.Decrypt(buf, 0)
|
||||||
_, ok := common.SniffAll(buf)
|
_, ok := sniff.AudioExtension(buf)
|
||||||
if !ok {
|
if !ok {
|
||||||
return errors.New("qmc: detect file type failed")
|
return errors.New("qmc: detect file type failed")
|
||||||
}
|
}
|
||||||
@@ -100,23 +102,29 @@ func (d *Decoder) searchKey() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
buf, err := io.ReadAll(io.LimitReader(d.raw, 4))
|
|
||||||
if err != nil {
|
suffixBuf := make([]byte, 4)
|
||||||
|
if _, err := io.ReadFull(d.raw, suffixBuf); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if string(buf) == "QTag" {
|
|
||||||
|
switch string(suffixBuf) {
|
||||||
|
case "QTag":
|
||||||
return d.readRawMetaQTag()
|
return d.readRawMetaQTag()
|
||||||
} else if string(buf) == "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")
|
||||||
} else {
|
default:
|
||||||
size := binary.LittleEndian.Uint32(buf)
|
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
|
||||||
d.audioLen = int(fileSizeM4 + 4)
|
d.audioLen = int(fileSizeM4 + 4)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Decoder) readRawKey(rawKeyLen int64) error {
|
func (d *Decoder) readRawKey(rawKeyLen int64) error {
|
||||||
|
|||||||
@@ -45,11 +45,11 @@ func TestMflac0Decoder_Read(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
d, err := NewDecoder(bytes.NewReader(raw))
|
d := NewDecoder(bytes.NewReader(raw))
|
||||||
if err != nil {
|
if err := d.Validate(); err != nil {
|
||||||
t.Error(err)
|
t.Errorf("validate file error = %v", err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buf := make([]byte, len(target))
|
buf := make([]byte, len(target))
|
||||||
if _, err := io.ReadFull(d, buf); err != nil {
|
if _, err := io.ReadFull(d, buf); err != nil {
|
||||||
t.Errorf("read bytes from decoder error = %v", err)
|
t.Errorf("read bytes from decoder error = %v", err)
|
||||||
@@ -81,19 +81,12 @@ func TestMflac0Decoder_Validate(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
d, err := NewDecoder(bytes.NewReader(raw))
|
d := NewDecoder(bytes.NewReader(raw))
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := d.Validate(); err != nil {
|
if err := d.Validate(); err != nil {
|
||||||
t.Errorf("read bytes from decoder error = %v", err)
|
t.Errorf("read bytes from decoder error = %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if tt.fileExt != d.GetFileExt() {
|
|
||||||
t.Errorf("Decrypt() got = %v, want %v", d.GetFileExt(), tt.fileExt)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,15 +7,17 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
|
|
||||||
"unlock-music.dev/cli/algo/common"
|
"unlock-music.dev/cli/algo/common"
|
||||||
|
"unlock-music.dev/cli/internal/sniff"
|
||||||
)
|
)
|
||||||
|
|
||||||
var replaceHeader = []byte{0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70}
|
var replaceHeader = []byte{0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70}
|
||||||
var magicHeader = []byte{0x51, 0x51, 0x4D, 0x55} //0x15, 0x1D, 0x1A, 0x21
|
var magicHeader = []byte{0x51, 0x51, 0x4D, 0x55} //0x15, 0x1D, 0x1A, 0x21
|
||||||
|
|
||||||
type Decoder struct {
|
type Decoder struct {
|
||||||
raw io.ReadSeeker
|
raw io.ReadSeeker // raw is the original file reader
|
||||||
|
|
||||||
offset int
|
offset int
|
||||||
audio io.Reader
|
audio io.Reader // audio is the decrypted audio data
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Decoder) Validate() error {
|
func (d *Decoder) Validate() error {
|
||||||
@@ -29,7 +31,7 @@ func (d *Decoder) Validate() error {
|
|||||||
return nil
|
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)
|
d.audio = io.MultiReader(bytes.NewReader(header), d.raw)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package xm
|
package xiami
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -22,13 +22,11 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Decoder struct {
|
type Decoder struct {
|
||||||
rd io.ReadSeeker
|
rd io.ReadSeeker // rd is the original file reader
|
||||||
offset int
|
offset int
|
||||||
|
|
||||||
cipher common.StreamDecoder
|
cipher common.StreamDecoder
|
||||||
outputExt string
|
outputExt string
|
||||||
mask byte
|
|
||||||
audio []byte
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Decoder) GetAudioExt() string {
|
func (d *Decoder) GetAudioExt() string {
|
||||||
@@ -43,6 +41,8 @@ func NewDecoder(rd io.ReadSeeker) common.Decoder {
|
|||||||
return &Decoder{rd: rd}
|
return &Decoder{rd: rd}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate checks if the file is a valid xiami .xm file.
|
||||||
|
// rd will set to the beginning of the encrypted audio data.
|
||||||
func (d *Decoder) Validate() error {
|
func (d *Decoder) Validate() error {
|
||||||
header := make([]byte, 16) // xm header is fixed to 16 bytes
|
header := make([]byte, 16) // xm header is fixed to 16 bytes
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package xm
|
package xiami
|
||||||
|
|
||||||
type xmCipher struct {
|
type xmCipher struct {
|
||||||
mask byte
|
mask byte
|
||||||
34
algo/ximalaya/x2m_crypto.go
Normal file
34
algo/ximalaya/x2m_crypto.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package ximalaya
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"encoding/binary"
|
||||||
|
)
|
||||||
|
|
||||||
|
const x2mHeaderSize = 1024
|
||||||
|
|
||||||
|
var x2mKey = [...]byte{'x', 'm', 'l', 'y'}
|
||||||
|
var x2mScrambleTable = [x2mHeaderSize]uint16{}
|
||||||
|
|
||||||
|
//go:embed x2m_scramble_table.bin
|
||||||
|
var x2mScrambleTableBytes []byte
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if len(x2mScrambleTableBytes) != 2*x2mHeaderSize {
|
||||||
|
panic("invalid x2m scramble table")
|
||||||
|
}
|
||||||
|
for i := range x2mScrambleTable {
|
||||||
|
x2mScrambleTable[i] = binary.LittleEndian.Uint16(x2mScrambleTableBytes[i*2:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// decryptX2MHeader decrypts the header of ximalaya .x2m file.
|
||||||
|
// make sure input src is 1024(x2mHeaderSize) bytes long.
|
||||||
|
func decryptX2MHeader(src []byte) []byte {
|
||||||
|
dst := make([]byte, len(src))
|
||||||
|
for dstIdx := range src {
|
||||||
|
srcIdx := x2mScrambleTable[dstIdx]
|
||||||
|
dst[dstIdx] = src[srcIdx] ^ x2mKey[dstIdx%len(x2mKey)]
|
||||||
|
}
|
||||||
|
return dst
|
||||||
|
}
|
||||||
BIN
algo/ximalaya/x2m_scramble_table.bin
Normal file
BIN
algo/ximalaya/x2m_scramble_table.bin
Normal file
Binary file not shown.
40
algo/ximalaya/x3m_crypto.go
Normal file
40
algo/ximalaya/x3m_crypto.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package ximalaya
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"encoding/binary"
|
||||||
|
)
|
||||||
|
|
||||||
|
var x3mKey = [...]byte{
|
||||||
|
'3', '9', '8', '9', 'd', '1', '1', '1',
|
||||||
|
'a', 'a', 'd', '5', '6', '1', '3', '9',
|
||||||
|
'4', '0', 'f', '4', 'f', 'c', '4', '4',
|
||||||
|
'b', '6', '3', '9', 'b', '2', '9', '2',
|
||||||
|
}
|
||||||
|
|
||||||
|
const x3mHeaderSize = 1024
|
||||||
|
|
||||||
|
var x3mScrambleTable = [x3mHeaderSize]uint16{}
|
||||||
|
|
||||||
|
//go:embed x3m_scramble_table.bin
|
||||||
|
var x3mScrambleTableBytes []byte
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if len(x3mScrambleTableBytes) != 2*x3mHeaderSize {
|
||||||
|
panic("invalid x3m scramble table")
|
||||||
|
}
|
||||||
|
for i := range x3mScrambleTable {
|
||||||
|
x3mScrambleTable[i] = binary.LittleEndian.Uint16(x3mScrambleTableBytes[i*2:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// decryptX3MHeader decrypts the header of ximalaya .x3m file.
|
||||||
|
// make sure input src is 1024 (x3mHeaderSize) bytes long.
|
||||||
|
func decryptX3MHeader(src []byte) []byte {
|
||||||
|
dst := make([]byte, len(src))
|
||||||
|
for dstIdx := range src {
|
||||||
|
srcIdx := x3mScrambleTable[dstIdx]
|
||||||
|
dst[dstIdx] = src[srcIdx] ^ x3mKey[dstIdx%len(x3mKey)]
|
||||||
|
}
|
||||||
|
return dst
|
||||||
|
}
|
||||||
BIN
algo/ximalaya/x3m_scramble_table.bin
Normal file
BIN
algo/ximalaya/x3m_scramble_table.bin
Normal file
Binary file not shown.
57
algo/ximalaya/ximalaya.go
Normal file
57
algo/ximalaya/ximalaya.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package ximalaya
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"unlock-music.dev/cli/algo/common"
|
||||||
|
"unlock-music.dev/cli/internal/sniff"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Decoder struct {
|
||||||
|
rd io.ReadSeeker
|
||||||
|
offset int
|
||||||
|
|
||||||
|
audio io.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDecoder(rd io.ReadSeeker) common.Decoder {
|
||||||
|
return &Decoder{rd: rd}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Decoder) Validate() error {
|
||||||
|
encryptedHeader := make([]byte, x2mHeaderSize)
|
||||||
|
if _, err := io.ReadFull(d.rd, encryptedHeader); err != nil {
|
||||||
|
return fmt.Errorf("ximalaya read header: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
{ // try to decode with x2m
|
||||||
|
header := decryptX2MHeader(encryptedHeader)
|
||||||
|
if _, ok := sniff.AudioExtension(header); ok {
|
||||||
|
d.audio = io.MultiReader(bytes.NewReader(header), d.rd)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{ // try to decode with x3m
|
||||||
|
// not read file again, since x2m and x3m have the same header size
|
||||||
|
header := decryptX3MHeader(encryptedHeader)
|
||||||
|
if _, ok := sniff.AudioExtension(header); ok {
|
||||||
|
d.audio = io.MultiReader(bytes.NewReader(header), d.rd)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("ximalaya: unknown format")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Decoder) Read(p []byte) (n int, err error) {
|
||||||
|
return d.audio.Read(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
common.RegisterDecoder("x2m", false, NewDecoder)
|
||||||
|
common.RegisterDecoder("x3m", false, NewDecoder)
|
||||||
|
common.RegisterDecoder("xm", false, NewDecoder)
|
||||||
|
}
|
||||||
@@ -22,8 +22,10 @@ import (
|
|||||||
_ "unlock-music.dev/cli/algo/ncm"
|
_ "unlock-music.dev/cli/algo/ncm"
|
||||||
_ "unlock-music.dev/cli/algo/qmc"
|
_ "unlock-music.dev/cli/algo/qmc"
|
||||||
_ "unlock-music.dev/cli/algo/tm"
|
_ "unlock-music.dev/cli/algo/tm"
|
||||||
_ "unlock-music.dev/cli/algo/xm"
|
_ "unlock-music.dev/cli/algo/xiami"
|
||||||
|
_ "unlock-music.dev/cli/algo/ximalaya"
|
||||||
"unlock-music.dev/cli/internal/logging"
|
"unlock-music.dev/cli/internal/logging"
|
||||||
|
"unlock-music.dev/cli/internal/sniff"
|
||||||
)
|
)
|
||||||
|
|
||||||
var AppVersion = "v0.0.6"
|
var AppVersion = "v0.0.6"
|
||||||
@@ -181,13 +183,10 @@ func tryDecFile(inputFile string, outputDir string, allDec []common.NewDecoderFu
|
|||||||
return fmt.Errorf("read header failed: %w", err)
|
return fmt.Errorf("read header failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
outExt := ".mp3"
|
outExt := sniff.AudioExtensionWithFallback(header.Bytes(), ".mp3")
|
||||||
if ext, ok := common.SniffAll(header.Bytes()); ok {
|
inFilename := strings.TrimSuffix(filepath.Base(inputFile), filepath.Ext(inputFile))
|
||||||
outExt = ext
|
|
||||||
}
|
|
||||||
filenameOnly := 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)
|
outFile, err := os.OpenFile(outPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
func NewZapLogger() (*zap.Logger, error) {
|
func NewZapLogger() (*zap.Logger, error) {
|
||||||
zapCfg := zap.NewDevelopmentConfig()
|
zapCfg := zap.NewDevelopmentConfig()
|
||||||
|
zapCfg.DisableStacktrace = true
|
||||||
zapCfg.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
|
zapCfg.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
|
||||||
zapCfg.EncoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout("2006/01/02 15:04:05.000")
|
zapCfg.EncoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout("2006/01/02 15:04:05.000")
|
||||||
return zapCfg.Build()
|
return zapCfg.Build()
|
||||||
|
|||||||
61
internal/sniff/audio.go
Normal file
61
internal/sniff/audio.go
Normal file
@@ -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])
|
||||||
|
}
|
||||||
32
misc/release.sh
Executable file
32
misc/release.sh
Executable file
@@ -0,0 +1,32 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
PLATFORMS=(
|
||||||
|
"linux/amd64"
|
||||||
|
"linux/arm64"
|
||||||
|
"darwin/amd64"
|
||||||
|
"darwin/arm64"
|
||||||
|
"windows/amd64"
|
||||||
|
"windows/386"
|
||||||
|
)
|
||||||
|
|
||||||
|
DEST_DIR=${DEST_DIR:-"dist"}
|
||||||
|
|
||||||
|
for PLATFORM in "${PLATFORMS[@]}"; do
|
||||||
|
GOOS=${PLATFORM%/*}
|
||||||
|
GOARCH=${PLATFORM#*/}
|
||||||
|
echo "Building for $GOOS/$GOARCH"
|
||||||
|
|
||||||
|
FILENAME="um-$GOOS-$GOARCH"
|
||||||
|
if [ "$GOOS" = "windows" ]; then
|
||||||
|
FILENAME="$FILENAME.exe"
|
||||||
|
fi
|
||||||
|
|
||||||
|
GOOS=$GOOS GOARCH=$GOARCH go build -v \
|
||||||
|
-o "${DEST_DIR}/${FILENAME}" \
|
||||||
|
-ldflags "-s -w -X main.AppVersion=$(git describe --tags --always --dirty)" \
|
||||||
|
./cmd/um
|
||||||
|
done
|
||||||
|
|
||||||
|
cd "$DEST_DIR"
|
||||||
|
sha256sum um-* > sha256sums.txt
|
||||||
Reference in New Issue
Block a user