Feature: QMC Decoder v2 #23
@ -12,15 +12,15 @@ type decoderItem struct {
|
|||||||
decoder NewDecoderFunc
|
decoder NewDecoderFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
var decoderRegistry = make(map[string][]decoderItem)
|
var DecoderRegistry = make(map[string][]decoderItem)
|
||||||
|
|
||||||
func RegisterDecoder(ext string, noop bool, dispatchFunc NewDecoderFunc) {
|
func RegisterDecoder(ext string, noop bool, dispatchFunc NewDecoderFunc) {
|
||||||
decoderRegistry[ext] = append(decoderRegistry[ext],
|
DecoderRegistry[ext] = append(DecoderRegistry[ext],
|
||||||
decoderItem{noop: noop, decoder: dispatchFunc})
|
decoderItem{noop: noop, decoder: dispatchFunc})
|
||||||
}
|
}
|
||||||
func GetDecoder(filename string, skipNoop bool) (rs []NewDecoderFunc) {
|
func GetDecoder(filename string, skipNoop bool) (rs []NewDecoderFunc) {
|
||||||
ext := strings.ToLower(strings.TrimLeft(filepath.Ext(filename), "."))
|
ext := strings.ToLower(strings.TrimLeft(filepath.Ext(filename), "."))
|
||||||
for _, dec := range decoderRegistry[ext] {
|
for _, dec := range DecoderRegistry[ext] {
|
||||||
if skipNoop && dec.noop {
|
if skipNoop && dec.noop {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -1,70 +0,0 @@
|
|||||||
package qmc
|
|
||||||
|
|
||||||
var oggPublicHeader1 = []byte{
|
|
||||||
0x4f, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff,
|
|
||||||
0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x01, 0x1e, 0x01, 0x76, 0x6f, 0x72,
|
|
||||||
0x62, 0x69, 0x73, 0x00, 0x00, 0x00, 0x00, 0x02, 0x44, 0xac, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0xee, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb8, 0x01, 0x4f, 0x67, 0x67, 0x53, 0x00, 0x00,
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x01, 0x00, 0x00, 0x00,
|
|
||||||
0xff, 0xff, 0xff, 0xff}
|
|
||||||
|
|
||||||
var oggPublicHeader2 = []byte{
|
|
||||||
0x03, 0x76, 0x6f, 0x72, 0x62, 0x69, 0x73, 0x2c, 0x00, 0x00, 0x00, 0x58, 0x69, 0x70, 0x68, 0x2e,
|
|
||||||
0x4f, 0x72, 0x67, 0x20, 0x6c, 0x69, 0x62, 0x56, 0x6f, 0x72, 0x62, 0x69, 0x73, 0x20, 0x49, 0x20,
|
|
||||||
0x32, 0x30, 0x31, 0x35, 0x30, 0x31, 0x30, 0x35, 0x20, 0x28, 0xe2, 0x9b, 0x84, 0xe2, 0x9b, 0x84,
|
|
||||||
0xe2, 0x9b, 0x84, 0xe2, 0x9b, 0x84, 0x29, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x54,
|
|
||||||
0x49, 0x54, 0x4c, 0x45, 0x3d}
|
|
||||||
|
|
||||||
var oggPublicConfidence1 = []uint{
|
|
||||||
9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 0, 0,
|
|
||||||
0, 0, 9, 9, 9, 9, 0, 0, 0, 0, 9, 9, 9, 9, 9, 9,
|
|
||||||
9, 9, 9, 9, 9, 9, 9, 6, 3, 3, 3, 3, 6, 6, 6, 6,
|
|
||||||
3, 3, 3, 3, 6, 6, 6, 6, 6, 9, 9, 9, 9, 9, 9, 9,
|
|
||||||
9, 9, 9, 9, 9, 9, 9, 9, 0, 0, 0, 0, 9, 9, 9, 9,
|
|
||||||
0, 0, 0, 0}
|
|
||||||
|
|
||||||
var oggPublicConfidence2 = []uint{
|
|
||||||
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
|
|
||||||
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
|
|
||||||
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
|
|
||||||
3, 3, 3, 3, 3, 3, 3, 0, 1, 3, 3, 0, 1, 3, 3, 3,
|
|
||||||
3, 3, 3, 3, 3}
|
|
||||||
|
|
||||||
var (
|
|
||||||
defaultKey256Mask44 = []byte{
|
|
||||||
0xde, 0x51, 0xfa, 0xc3, 0x4a, 0xd6, 0xca, 0x90,
|
|
||||||
0x7e, 0x67, 0x5e, 0xf7, 0xd5, 0x52, 0x84, 0xd8,
|
|
||||||
0x47, 0x95, 0xbb, 0xa1, 0xaa, 0xc6, 0x66, 0x23,
|
|
||||||
0x92, 0x62, 0xf3, 0x74, 0xa1, 0x9f, 0xf4, 0xa0,
|
|
||||||
0x1d, 0x3f, 0x5b, 0xf0, 0x13, 0x0e, 0x09, 0x3d,
|
|
||||||
0xf9, 0xbc, 0x00, 0x11}
|
|
||||||
)
|
|
||||||
var key256MappingAll [][]int //[idx256][idx128]idx44
|
|
||||||
var key256Mapping128to44 map[int]int
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
{ // init all mapping
|
|
||||||
key256MappingAll = make([][]int, 256)
|
|
||||||
for i := 0; i < 128; i++ {
|
|
||||||
realIdx := (i*i + 27) % 256
|
|
||||||
if key256MappingAll[realIdx] == nil {
|
|
||||||
key256MappingAll[realIdx] = []int{i}
|
|
||||||
} else {
|
|
||||||
key256MappingAll[realIdx] = append(key256MappingAll[realIdx], i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
{ // init
|
|
||||||
key256Mapping128to44 = make(map[int]int, 128)
|
|
||||||
idx44 := 0
|
|
||||||
for _, all128 := range key256MappingAll {
|
|
||||||
if all128 != nil {
|
|
||||||
for _, _i128 := range all128 {
|
|
||||||
key256Mapping128to44[_i128] = idx44
|
|
||||||
}
|
|
||||||
idx44++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,212 +0,0 @@
|
|||||||
package qmc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"github.com/unlock-music/cli/algo/common"
|
|
||||||
"github.com/unlock-music/cli/internal/logging"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrFailToMatchMask = errors.New("can not match at least one key")
|
|
||||||
ErrTestDataLength = errors.New("invalid length of test file")
|
|
||||||
ErrMaskLength128 = errors.New("incorrect mask length 128")
|
|
||||||
ErrMaskLength44 = errors.New("incorrect mask length 44")
|
|
||||||
ErrMaskDecode = errors.New("decode mask-128 to mask-58 failed")
|
|
||||||
ErrDetectFlacMask = errors.New("can not detect mflac mask")
|
|
||||||
ErrDetectMggMask = errors.New("can not detect mgg mask")
|
|
||||||
)
|
|
||||||
|
|
||||||
type Key256Mask struct {
|
|
||||||
matrix []byte // Mask 128
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewKey256FromMask128(mask128 []byte) (*Key256Mask, error) {
|
|
||||||
if len(mask128) != 128 {
|
|
||||||
return nil, ErrMaskLength128
|
|
||||||
}
|
|
||||||
q := &Key256Mask{matrix: mask128}
|
|
||||||
return q, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewKey256FromMask44(mask44 []byte) (*Key256Mask, error) {
|
|
||||||
mask128, err := convertKey256Mask44to128(mask44)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
q := &Key256Mask{matrix: mask128}
|
|
||||||
return q, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Key256Mask) GetMatrix44() ([]byte, error) {
|
|
||||||
if len(q.matrix) != 128 {
|
|
||||||
return nil, ErrMaskLength128
|
|
||||||
}
|
|
||||||
matrix44 := make([]byte, 44)
|
|
||||||
idx44 := 0
|
|
||||||
for _, it256 := range key256MappingAll {
|
|
||||||
if it256 != nil {
|
|
||||||
it256Len := len(it256)
|
|
||||||
for i := 1; i < it256Len; i++ {
|
|
||||||
if q.matrix[it256[0]] != q.matrix[it256[i]] {
|
|
||||||
return nil, ErrMaskDecode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
matrix44[idx44] = q.matrix[it256[0]]
|
|
||||||
idx44++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return matrix44, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Key256Mask) Decrypt(data []byte) []byte {
|
|
||||||
dst := make([]byte, len(data))
|
|
||||||
index := -1
|
|
||||||
maskIdx := -1
|
|
||||||
for cur := 0; cur < len(data); cur++ {
|
|
||||||
index++
|
|
||||||
maskIdx++
|
|
||||||
if index == 0x8000 || (index > 0x8000 && (index+1)%0x8000 == 0) {
|
|
||||||
index++
|
|
||||||
maskIdx++
|
|
||||||
}
|
|
||||||
if maskIdx >= 128 {
|
|
||||||
maskIdx -= 128
|
|
||||||
}
|
|
||||||
dst[cur] = data[cur] ^ q.matrix[maskIdx]
|
|
||||||
}
|
|
||||||
return dst
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertKey256Mask44to128(mask44 []byte) ([]byte, error) {
|
|
||||||
if len(mask44) != 44 {
|
|
||||||
return nil, ErrMaskLength44
|
|
||||||
}
|
|
||||||
mask128 := make([]byte, 128)
|
|
||||||
idx44 := 0
|
|
||||||
for _, it256 := range key256MappingAll {
|
|
||||||
if it256 != nil {
|
|
||||||
for _, idx128 := range it256 {
|
|
||||||
mask128[idx128] = mask44[idx44]
|
|
||||||
}
|
|
||||||
idx44++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return mask128, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getDefaultMask() *Key256Mask {
|
|
||||||
y, _ := NewKey256FromMask44(defaultKey256Mask44)
|
|
||||||
return y
|
|
||||||
}
|
|
||||||
|
|
||||||
func detectMflac256Mask(input []byte) (*Key256Mask, error) {
|
|
||||||
var q *Key256Mask
|
|
||||||
var rtErr = ErrDetectFlacMask
|
|
||||||
|
|
||||||
lenData := len(input)
|
|
||||||
lenTest := 0x8000
|
|
||||||
if lenData < 0x8000 {
|
|
||||||
lenTest = lenData
|
|
||||||
}
|
|
||||||
|
|
||||||
for blockIdx := 0; blockIdx < lenTest; blockIdx += 128 {
|
|
||||||
var err error
|
|
||||||
q, err = NewKey256FromMask128(input[blockIdx : blockIdx+128])
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if common.SnifferFLAC(q.Decrypt(input[:4])) {
|
|
||||||
rtErr = nil
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return q, rtErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func detectMgg256Mask(input []byte) (*Key256Mask, error) {
|
|
||||||
if len(input) < 0x100 {
|
|
||||||
return nil, ErrTestDataLength
|
|
||||||
}
|
|
||||||
|
|
||||||
matrixConf := make([]map[uint8]uint, 44) //meaning: [idx58][value]confidence
|
|
||||||
for i := uint(0); i < 44; i++ {
|
|
||||||
matrixConf[i] = make(map[uint8]uint)
|
|
||||||
}
|
|
||||||
|
|
||||||
page2size := input[0x54] ^ input[0xC] ^ oggPublicHeader1[0xC]
|
|
||||||
spHeader, spConf := generateOggFullHeader(int(page2size))
|
|
||||||
lenTest := len(spHeader)
|
|
||||||
|
|
||||||
for idx128 := 0; idx128 < lenTest; idx128++ {
|
|
||||||
confidence := spConf[idx128]
|
|
||||||
if confidence > 0 {
|
|
||||||
mask := input[idx128] ^ spHeader[idx128]
|
|
||||||
|
|
||||||
idx44 := key256Mapping128to44[idx128&0x7f] // equals: [idx128 % 128]
|
|
||||||
if _, ok2 := matrixConf[idx44][mask]; ok2 {
|
|
||||||
matrixConf[idx44][mask] += confidence
|
|
||||||
} else {
|
|
||||||
matrixConf[idx44][mask] = confidence
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
matrix := make([]uint8, 44)
|
|
||||||
var err error
|
|
||||||
for i := uint(0); i < 44; i++ {
|
|
||||||
matrix[i], err = decideMgg256MaskItemConf(matrixConf[i])
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
q, err := NewKey256FromMask44(matrix)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if common.SnifferOGG(q.Decrypt(input[:4])) {
|
|
||||||
return q, nil
|
|
||||||
}
|
|
||||||
return nil, ErrDetectMggMask
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateOggFullHeader(pageSize int) ([]byte, []uint) {
|
|
||||||
spec := make([]byte, pageSize+1)
|
|
||||||
|
|
||||||
spec[0], spec[1], spec[pageSize] = uint8(pageSize), 0xFF, 0xFF
|
|
||||||
for i := 2; i < pageSize; i++ {
|
|
||||||
spec[i] = 0xFF
|
|
||||||
}
|
|
||||||
specConf := make([]uint, pageSize+1)
|
|
||||||
specConf[0], specConf[1], specConf[pageSize] = 6, 0, 0
|
|
||||||
for i := 2; i < pageSize; i++ {
|
|
||||||
specConf[i] = 4
|
|
||||||
}
|
|
||||||
allConf := append(oggPublicConfidence1, specConf...)
|
|
||||||
allConf = append(allConf, oggPublicConfidence2...)
|
|
||||||
|
|
||||||
allHeader := bytes.Join(
|
|
||||||
[][]byte{oggPublicHeader1, spec, oggPublicHeader2},
|
|
||||||
[]byte{},
|
|
||||||
)
|
|
||||||
return allHeader, allConf
|
|
||||||
}
|
|
||||||
|
|
||||||
func decideMgg256MaskItemConf(confidence map[uint8]uint) (uint8, error) {
|
|
||||||
lenConf := len(confidence)
|
|
||||||
if lenConf == 0 {
|
|
||||||
return 0xff, ErrFailToMatchMask
|
|
||||||
} else if lenConf > 1 {
|
|
||||||
logging.Log().Warn("there are 2 potential value for the mask", zap.Any("confidence", confidence))
|
|
||||||
}
|
|
||||||
result := uint8(0)
|
|
||||||
conf := uint(0)
|
|
||||||
for idx, item := range confidence {
|
|
||||||
if item > conf {
|
|
||||||
result = idx
|
|
||||||
conf = item
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
@ -1,129 +0,0 @@
|
|||||||
package qmc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"github.com/unlock-music/cli/algo/common"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrQmcFileLength = errors.New("invalid qmc file length")
|
|
||||||
ErrQmcKeyDecodeFailed = errors.New("base64 decode qmc key failed")
|
|
||||||
ErrQmcKeyLength = errors.New("unexpected decoded qmc key length")
|
|
||||||
)
|
|
||||||
|
|
||||||
type OldDecoder struct {
|
|
||||||
file []byte
|
|
||||||
maskDetector func(encodedData []byte) (*Key256Mask, error)
|
|
||||||
mask *Key256Mask
|
|
||||||
audioExt string
|
|
||||||
key []byte
|
|
||||||
audio []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewMflac256Decoder(data []byte) common.Decoder {
|
|
||||||
return &OldDecoder{file: data, maskDetector: detectMflac256Mask, audioExt: "flac"}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewMgg256Decoder(data []byte) common.Decoder {
|
|
||||||
return &OldDecoder{file: data, maskDetector: detectMgg256Mask, audioExt: "ogg"}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *OldDecoder) Validate() error {
|
|
||||||
if nil != d.mask {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if nil != d.maskDetector {
|
|
||||||
if err := d.validateKey(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
var err error
|
|
||||||
d.mask, err = d.maskDetector(d.file)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return errors.New("no mask or mask detector found")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *OldDecoder) validateKey() error {
|
|
||||||
lenData := len(d.file)
|
|
||||||
if lenData < 4 {
|
|
||||||
return ErrQmcFileLength
|
|
||||||
}
|
|
||||||
|
|
||||||
keyLen := binary.LittleEndian.Uint32(d.file[lenData-4:])
|
|
||||||
if lenData < int(keyLen+4) {
|
|
||||||
return ErrQmcFileLength
|
|
||||||
}
|
|
||||||
var err error
|
|
||||||
d.key, err = base64.StdEncoding.DecodeString(
|
|
||||||
string(d.file[lenData-4-int(keyLen) : lenData-4]))
|
|
||||||
if err != nil {
|
|
||||||
return ErrQmcKeyDecodeFailed
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(d.key) != 272 {
|
|
||||||
return ErrQmcKeyLength
|
|
||||||
}
|
|
||||||
d.file = d.file[:lenData-4-int(keyLen)]
|
|
||||||
return nil
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *OldDecoder) Decode() error {
|
|
||||||
d.audio = d.mask.Decrypt(d.file)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d OldDecoder) GetCoverImage() []byte {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d OldDecoder) GetAudioData() []byte {
|
|
||||||
return d.audio
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d OldDecoder) GetAudioExt() string {
|
|
||||||
if d.audioExt != "" {
|
|
||||||
return "." + d.audioExt
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d OldDecoder) GetMeta() common.Meta {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func DecoderFuncWithExt(ext string) common.NewDecoderFunc {
|
|
||||||
return func(file []byte) common.Decoder {
|
|
||||||
return &OldDecoder{file: file, audioExt: ext, mask: getDefaultMask()}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//goland:noinspection SpellCheckingInspection
|
|
||||||
func init() {
|
|
||||||
common.RegisterDecoder("qmc0", false, DecoderFuncWithExt("mp3")) //QQ Music Mp3
|
|
||||||
common.RegisterDecoder("qmc3", false, DecoderFuncWithExt("mp3")) //QQ Music Mp3
|
|
||||||
|
|
||||||
common.RegisterDecoder("qmc2", false, DecoderFuncWithExt("m4a")) //QQ Music M4A
|
|
||||||
common.RegisterDecoder("qmc4", false, DecoderFuncWithExt("m4a")) //QQ Music M4A
|
|
||||||
common.RegisterDecoder("qmc6", false, DecoderFuncWithExt("m4a")) //QQ Music M4A
|
|
||||||
common.RegisterDecoder("qmc8", false, DecoderFuncWithExt("m4a")) //QQ Music M4A
|
|
||||||
|
|
||||||
common.RegisterDecoder("qmcflac", false, DecoderFuncWithExt("flac")) //QQ Music Flac
|
|
||||||
common.RegisterDecoder("qmcogg", false, DecoderFuncWithExt("ogg")) //QQ Music Ogg
|
|
||||||
common.RegisterDecoder("tkm", false, DecoderFuncWithExt("m4a")) //QQ Music Accompaniment M4a
|
|
||||||
|
|
||||||
common.RegisterDecoder("bkcmp3", false, DecoderFuncWithExt("mp3")) //Moo Music Mp3
|
|
||||||
common.RegisterDecoder("bkcflac", false, DecoderFuncWithExt("flac")) //Moo Music Flac
|
|
||||||
|
|
||||||
common.RegisterDecoder("666c6163", false, DecoderFuncWithExt("flac")) //QQ Music Weiyun Flac
|
|
||||||
common.RegisterDecoder("6d7033", false, DecoderFuncWithExt("mp3")) //QQ Music Weiyun Mp3
|
|
||||||
common.RegisterDecoder("6f6767", false, DecoderFuncWithExt("ogg")) //QQ Music Weiyun Ogg
|
|
||||||
common.RegisterDecoder("6d3461", false, DecoderFuncWithExt("m4a")) //QQ Music Weiyun M4a
|
|
||||||
common.RegisterDecoder("776176", false, DecoderFuncWithExt("wav")) //QQ Music Weiyun Wav
|
|
||||||
|
|
||||||
common.RegisterDecoder("mgg", false, NewMgg256Decoder) //QQ Music New Ogg
|
|
||||||
common.RegisterDecoder("mflac", false, NewMflac256Decoder) //QQ Music New Flac
|
|
||||||
}
|
|
@ -1,6 +1,7 @@
|
|||||||
package qmc
|
package qmc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
@ -183,3 +184,80 @@ func (d *Decoder) readRawMetaQTag() error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//goland:noinspection SpellCheckingInspection
|
||||||
|
func init() {
|
||||||
|
supportedExts := []string{
|
||||||
|
"qmc0", "qmc3", //QQ Music MP3
|
||||||
|
"qmc2", "qmc4", "qmc6", "qmc8", //QQ Music M4A
|
||||||
|
"qmcflac", //QQ Music FLAC
|
||||||
|
"qmcogg", //QQ Music OGG
|
||||||
|
|
||||||
|
"tkm", //QQ Music Accompaniment M4A
|
||||||
|
"bkcmp3", //Moo Music Mp3
|
||||||
|
"bkcflac", //Moo Music Flac
|
||||||
|
|
||||||
|
"666c6163", //QQ Music Weiyun Flac
|
||||||
|
"6d7033", //QQ Music Weiyun Mp3
|
||||||
|
"6f6767", //QQ Music Weiyun Ogg
|
||||||
|
"6d3461", //QQ Music Weiyun M4a
|
||||||
|
"776176", //QQ Music Weiyun Wav
|
||||||
|
|
||||||
|
"mgg", "mgg1", //QQ Music New Ogg
|
||||||
|
"mflac", "mflac0", //QQ Music New Flac
|
||||||
|
}
|
||||||
|
for _, ext := range supportedExts {
|
||||||
|
common.RegisterDecoder(ext, false, newCompactDecoder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type compactDecoder struct {
|
||||||
|
decoder *Decoder
|
||||||
|
createErr error
|
||||||
|
buf *bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCompactDecoder(p []byte) common.Decoder {
|
||||||
|
r := bytes.NewReader(p)
|
||||||
|
d, err := NewDecoder(r)
|
||||||
|
c := compactDecoder{
|
||||||
|
decoder: d,
|
||||||
|
createErr: err,
|
||||||
|
}
|
||||||
|
return &c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *compactDecoder) Validate() error {
|
||||||
|
if c.createErr != nil {
|
||||||
|
return c.createErr
|
||||||
|
}
|
||||||
|
return c.decoder.Validate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *compactDecoder) Decode() error {
|
||||||
|
if c.createErr != nil {
|
||||||
|
return c.createErr
|
||||||
|
}
|
||||||
|
c.buf = bytes.NewBuffer(nil)
|
||||||
|
_, err := io.Copy(c.buf, c.decoder)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *compactDecoder) GetCoverImage() []byte {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *compactDecoder) GetAudioData() []byte {
|
||||||
|
return c.buf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *compactDecoder) GetAudioExt() string {
|
||||||
|
if c.createErr != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return c.decoder.GetFileExt()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *compactDecoder) GetMeta() common.Meta {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -3,6 +3,15 @@ package main
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/unlock-music/cli/algo/common"
|
"github.com/unlock-music/cli/algo/common"
|
||||||
_ "github.com/unlock-music/cli/algo/kgm"
|
_ "github.com/unlock-music/cli/algo/kgm"
|
||||||
_ "github.com/unlock-music/cli/algo/kwm"
|
_ "github.com/unlock-music/cli/algo/kwm"
|
||||||
@ -11,12 +20,6 @@ import (
|
|||||||
_ "github.com/unlock-music/cli/algo/tm"
|
_ "github.com/unlock-music/cli/algo/tm"
|
||||||
_ "github.com/unlock-music/cli/algo/xm"
|
_ "github.com/unlock-music/cli/algo/xm"
|
||||||
"github.com/unlock-music/cli/internal/logging"
|
"github.com/unlock-music/cli/internal/logging"
|
||||||
"github.com/urfave/cli/v2"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var AppVersion = "v0.0.6"
|
var AppVersion = "v0.0.6"
|
||||||
@ -31,6 +34,7 @@ func main() {
|
|||||||
&cli.StringFlag{Name: "input", Aliases: []string{"i"}, Usage: "path to input file or dir", Required: false},
|
&cli.StringFlag{Name: "input", Aliases: []string{"i"}, Usage: "path to input file or dir", Required: false},
|
||||||
&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.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},
|
||||||
|
&cli.BoolFlag{Name: "supported-ext", Usage: "Show supported file extensions and exit", Required: false, Value: false},
|
||||||
},
|
},
|
||||||
|
|
||||||
Action: appMain,
|
Action: appMain,
|
||||||
@ -43,8 +47,21 @@ func main() {
|
|||||||
logging.Log().Fatal("run app failed", zap.Error(err))
|
logging.Log().Fatal("run app failed", zap.Error(err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
func printSupportedExtensions() {
|
||||||
|
exts := []string{}
|
||||||
|
for ext := range common.DecoderRegistry {
|
||||||
|
exts = append(exts, ext)
|
||||||
|
}
|
||||||
|
sort.Strings(exts)
|
||||||
|
for _, ext := range exts {
|
||||||
|
fmt.Printf("%s: %d\n", ext, len(common.DecoderRegistry[ext]))
|
||||||
|
}
|
||||||
|
}
|
||||||
func appMain(c *cli.Context) (err error) {
|
func appMain(c *cli.Context) (err error) {
|
||||||
|
if c.Bool("supported-ext") {
|
||||||
|
printSupportedExtensions()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
input := c.String("input")
|
input := c.String("input")
|
||||||
if input == "" {
|
if input == "" {
|
||||||
switch c.Args().Len() {
|
switch c.Args().Len() {
|
||||||
|
10
go.mod
10
go.mod
@ -6,12 +6,12 @@ require (
|
|||||||
github.com/ulikunitz/xz v0.5.10
|
github.com/ulikunitz/xz v0.5.10
|
||||||
github.com/urfave/cli/v2 v2.3.0
|
github.com/urfave/cli/v2 v2.3.0
|
||||||
go.uber.org/zap v1.19.1
|
go.uber.org/zap v1.19.1
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
|
||||||
github.com/russross/blackfriday/v2 v2.0.1 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
go.uber.org/atomic v1.9.0 // indirect
|
||||||
go.uber.org/atomic v1.7.0 // indirect
|
go.uber.org/multierr v1.7.0 // indirect
|
||||||
go.uber.org/multierr v1.6.0 // indirect
|
|
||||||
)
|
)
|
||||||
|
14
go.sum
14
go.sum
@ -1,8 +1,9 @@
|
|||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
||||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
|
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 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=
|
||||||
@ -13,9 +14,9 @@ github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
|||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 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/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
|
||||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
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/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
@ -26,15 +27,18 @@ github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0o
|
|||||||
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
|
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
|
||||||
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
||||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
|
|
||||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||||
|
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||||
|
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||||
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723 h1:sHOAIxRGBp443oHZIPB+HsUGaksVCXVQENPxwTfQdH4=
|
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723 h1:sHOAIxRGBp443oHZIPB+HsUGaksVCXVQENPxwTfQdH4=
|
||||||
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||||
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
|
|
||||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||||
|
go.uber.org/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec=
|
||||||
|
go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
|
||||||
go.uber.org/zap v1.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI=
|
go.uber.org/zap v1.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI=
|
||||||
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
|
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
Loading…
Reference in New Issue
Block a user