e84c52d35e
并且支持本地自签名部署了
255 lines
5.8 KiB
Go
255 lines
5.8 KiB
Go
package ncmcrypt
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"fmt"
|
|
|
|
"github.com/go-flac/go-flac"
|
|
)
|
|
|
|
func EmbedMetadata(audio []byte, format NcmFormat, title, artist, album string, coverData []byte) ([]byte, error) {
|
|
switch format {
|
|
case Mp3:
|
|
return embedMP3(audio, title, artist, album, coverData)
|
|
case Flac:
|
|
return embedFLAC(audio, title, artist, album, coverData)
|
|
default:
|
|
return audio, nil
|
|
}
|
|
}
|
|
|
|
func synchSafe(v int) []byte {
|
|
b := make([]byte, 4)
|
|
b[0] = byte((v >> 21) & 0x7F)
|
|
b[1] = byte((v >> 14) & 0x7F)
|
|
b[2] = byte((v >> 7) & 0x7F)
|
|
b[3] = byte(v & 0x7F)
|
|
return b
|
|
}
|
|
|
|
func textFrame(id string, text string) []byte {
|
|
if text == "" {
|
|
return nil
|
|
}
|
|
data := append([]byte{0x03}, []byte(text)...)
|
|
size := len(data)
|
|
frame := make([]byte, 10+size)
|
|
copy(frame[0:4], id)
|
|
copy(frame[4:8], synchSafe(size))
|
|
frame[8] = 0x00
|
|
frame[9] = 0x00
|
|
copy(frame[10:], data)
|
|
return frame
|
|
}
|
|
|
|
func apicFrame(mime string, picType byte, description string, data []byte) []byte {
|
|
if len(data) == 0 {
|
|
return nil
|
|
}
|
|
inner := make([]byte, 0, 1+len(mime)+1+1+len(description)+1+len(data))
|
|
inner = append(inner, 0x00)
|
|
inner = append(inner, []byte(mime)...)
|
|
inner = append(inner, 0x00)
|
|
inner = append(inner, picType)
|
|
inner = append(inner, []byte(description)...)
|
|
inner = append(inner, 0x00)
|
|
inner = append(inner, data...)
|
|
|
|
size := len(inner)
|
|
frame := make([]byte, 10+size)
|
|
copy(frame[0:4], "APIC")
|
|
copy(frame[4:8], synchSafe(size))
|
|
frame[8] = 0x00
|
|
frame[9] = 0x00
|
|
copy(frame[10:], inner)
|
|
return frame
|
|
}
|
|
|
|
func findMpegSync(data []byte) int {
|
|
for i := 0; i < len(data)-1; i++ {
|
|
if data[i] == 0xFF && (data[i+1]&0xE0) == 0xE0 {
|
|
return i
|
|
}
|
|
}
|
|
return len(data)
|
|
}
|
|
|
|
func skipID3v2(data []byte) int {
|
|
if len(data) >= 10 && string(data[0:3]) == "ID3" {
|
|
size := int(data[6])<<21 | int(data[7])<<14 | int(data[8])<<7 | int(data[9])
|
|
return 10 + size
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func imageMime(data []byte) string {
|
|
if len(data) >= 8 && data[0] == 0x89 && data[1] == 0x50 && data[2] == 0x4E && data[3] == 0x47 {
|
|
return "image/png"
|
|
}
|
|
return "image/jpeg"
|
|
}
|
|
|
|
func embedMP3(audio []byte, title, artist, album string, coverData []byte) ([]byte, error) {
|
|
id3End := skipID3v2(audio)
|
|
frames := bytes.NewBuffer(nil)
|
|
|
|
if f := textFrame("TIT2", title); f != nil {
|
|
frames.Write(f)
|
|
}
|
|
if f := textFrame("TPE1", artist); f != nil {
|
|
frames.Write(f)
|
|
}
|
|
if f := textFrame("TALB", album); f != nil {
|
|
frames.Write(f)
|
|
}
|
|
if f := apicFrame(imageMime(coverData), 0x03, "", coverData); f != nil {
|
|
frames.Write(f)
|
|
}
|
|
|
|
if frames.Len() == 0 {
|
|
return audio, nil
|
|
}
|
|
|
|
body := frames.Bytes()
|
|
tagSize := len(body)
|
|
|
|
tag := make([]byte, 10+tagSize)
|
|
copy(tag[0:3], "ID3")
|
|
tag[3] = 0x03
|
|
tag[4] = 0x00
|
|
tag[5] = 0x00
|
|
copy(tag[6:10], synchSafe(tagSize))
|
|
copy(tag[10:], body)
|
|
|
|
out := make([]byte, 0, len(tag)+len(audio)-id3End)
|
|
out = append(out, tag...)
|
|
out = append(out, audio[id3End:]...)
|
|
|
|
return out, nil
|
|
}
|
|
|
|
func embedFLAC(audio []byte, title, artist, album string, coverData []byte) ([]byte, error) {
|
|
f, err := flac.ParseBytes(bytes.NewReader(audio))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse FLAC: %w", err)
|
|
}
|
|
|
|
metaOffset := 4
|
|
for _, block := range f.Meta {
|
|
metaOffset += 4 + len(block.Data)
|
|
}
|
|
audioFrames := audio[metaOffset:]
|
|
|
|
var comments map[string]string
|
|
if title != "" || artist != "" || album != "" {
|
|
comments = make(map[string]string)
|
|
if title != "" {
|
|
comments["TITLE"] = title
|
|
}
|
|
if artist != "" {
|
|
comments["ARTIST"] = artist
|
|
}
|
|
if album != "" {
|
|
comments["ALBUM"] = album
|
|
}
|
|
}
|
|
|
|
newMeta := make([]*flac.MetaDataBlock, 0, len(f.Meta)+2)
|
|
for _, block := range f.Meta {
|
|
if block.Type == flac.Picture || block.Type == flac.VorbisComment {
|
|
continue
|
|
}
|
|
newMeta = append(newMeta, block)
|
|
}
|
|
if comments != nil {
|
|
newMeta = append(newMeta, buildVorbisComment(comments))
|
|
}
|
|
if len(coverData) > 0 {
|
|
newMeta = append(newMeta, buildPictureBlock(coverData))
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
buf.WriteString("fLaC")
|
|
for i, block := range newMeta {
|
|
header := byte(block.Type)
|
|
if i == len(newMeta)-1 {
|
|
header |= 0x80
|
|
}
|
|
buf.WriteByte(header)
|
|
size := make([]byte, 3)
|
|
size[0] = byte(len(block.Data) >> 16)
|
|
size[1] = byte(len(block.Data) >> 8)
|
|
size[2] = byte(len(block.Data))
|
|
buf.Write(size)
|
|
buf.Write(block.Data)
|
|
}
|
|
buf.Write(audioFrames)
|
|
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
func buildVorbisComment(comments map[string]string) *flac.MetaDataBlock {
|
|
vendorLen := 0
|
|
var vendor string
|
|
dataLen := 4 + vendorLen + 4
|
|
for key, val := range comments {
|
|
dataLen += 4 + len(key) + 1 + len(val)
|
|
}
|
|
|
|
data := make([]byte, dataLen)
|
|
pos := 0
|
|
pos += putU32le(data[pos:], uint32(vendorLen))
|
|
pos += copy(data[pos:], vendor)
|
|
pos += putU32le(data[pos:], uint32(len(comments)))
|
|
for key, val := range comments {
|
|
entry := key + "=" + val
|
|
pos += putU32le(data[pos:], uint32(len(entry)))
|
|
pos += copy(data[pos:], entry)
|
|
}
|
|
|
|
return &flac.MetaDataBlock{
|
|
Type: flac.VorbisComment,
|
|
Data: data,
|
|
}
|
|
}
|
|
|
|
func buildPictureBlock(imageData []byte) *flac.MetaDataBlock {
|
|
mime := imageMime(imageData)
|
|
picType := uint32(3)
|
|
width := uint32(0)
|
|
height := uint32(0)
|
|
depth := uint32(24)
|
|
colors := uint32(0)
|
|
|
|
dataLen := 4 + len(mime) + 4 + 4 + 4 + 4 + 4 + len(imageData)
|
|
data := make([]byte, dataLen)
|
|
pos := 0
|
|
pos += putU32be(data[pos:], picType)
|
|
pos += putU32be(data[pos:], uint32(len(mime)))
|
|
pos += copy(data[pos:], mime)
|
|
pos += putU32be(data[pos:], uint32(len("")))
|
|
pos += copy(data[pos:], "")
|
|
pos += putU32be(data[pos:], width)
|
|
pos += putU32be(data[pos:], height)
|
|
pos += putU32be(data[pos:], depth)
|
|
pos += putU32be(data[pos:], colors)
|
|
pos += putU32be(data[pos:], uint32(len(imageData)))
|
|
copy(data[pos:], imageData)
|
|
|
|
return &flac.MetaDataBlock{
|
|
Type: flac.Picture,
|
|
Data: data,
|
|
}
|
|
}
|
|
|
|
func putU32le(b []byte, v uint32) int {
|
|
binary.LittleEndian.PutUint32(b, v)
|
|
return 4
|
|
}
|
|
|
|
func putU32be(b []byte, v uint32) int {
|
|
binary.BigEndian.PutUint32(b, v)
|
|
return 4
|
|
}
|