Files
godeez/internal/tag/wav.go
T

257 lines
4.8 KiB
Go

package tag
import (
"bytes"
"encoding/binary"
"errors"
"io"
"os"
"strings"
"github.com/bogem/id3v2/v2"
)
type wavTagger struct {
path string
}
type wavChunk struct {
id string
payload []byte
}
type infoField struct {
id string
value string
}
func (t *wavTagger) write(m Metadata) error {
id3Chunk, err := buildID3Chunk(m)
if err != nil {
return err
}
var chunks []wavChunk
if info := buildInfoChunk(m); info != nil {
chunks = append(chunks, wavChunk{id: "LIST", payload: info})
}
if id3Chunk != nil {
chunks = append(chunks, wavChunk{id: "id3 ", payload: id3Chunk})
}
return rewriteWAV(t.path, chunks)
}
func buildID3Chunk(m Metadata) ([]byte, error) {
tag := id3v2.NewEmptyTag()
applyID3Frames(tag, m)
if !tag.HasFrames() {
return nil, nil
}
var buf bytes.Buffer
if _, err := tag.WriteTo(&buf); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func buildInfoChunk(m Metadata) []byte {
fields := []infoField{
{"INAM", m.Title},
{"IART", m.Artists},
{"IGNR", m.Genre},
{"ITRK", m.TrackNumber},
}
if m.Album != nil {
date := m.Album.ReleaseDate
if parts := strings.Split(date, "-"); len(parts) == 3 {
date = parts[0]
}
fields = append(fields,
infoField{"IPRD", m.Album.Title},
infoField{"ICRD", date},
infoField{"ICMT", m.Album.ProducerLine},
infoField{"ICOP", m.Album.Copyright},
)
}
var buf bytes.Buffer
buf.WriteString("INFO")
for _, field := range fields {
if field.value == "" {
continue
}
writeChunk(&buf, field.id, append([]byte(field.value), 0))
}
if buf.Len() == 4 {
return nil
}
return buf.Bytes()
}
func writeChunk(w io.Writer, id string, payload []byte) {
header := make([]byte, 0, 8)
header = append(header, id...)
header = binary.LittleEndian.AppendUint32(header, uint32(len(payload)))
w.Write(header)
w.Write(payload)
if len(payload)%2 != 0 {
w.Write([]byte{0})
}
}
func rewriteWAV(path string, chunks []wavChunk) error {
src, err := os.Open(path)
if err != nil {
return err
}
defer src.Close()
header := make([]byte, 12)
if _, err := io.ReadFull(src, header); err != nil {
return err
}
if string(header[0:4]) != "RIFF" || string(header[8:12]) != "WAVE" {
return errors.New("not a wav file")
}
tmpPath := path + ".tmp"
dst, err := os.Create(tmpPath)
if err != nil {
return err
}
done := false
defer func() {
if !done {
dst.Close()
os.Remove(tmpPath)
}
}()
if _, err := dst.Write(header); err != nil {
return err
}
size, err := copyChunks(dst, src)
if err != nil {
return err
}
for _, chunk := range chunks {
var buf bytes.Buffer
writeChunk(&buf, chunk.id, chunk.payload)
if _, err := dst.Write(buf.Bytes()); err != nil {
return err
}
size += int64(buf.Len())
}
riffSize := make([]byte, 4)
binary.LittleEndian.PutUint32(riffSize, uint32(size))
if _, err := dst.WriteAt(riffSize, 4); err != nil {
return err
}
if err := dst.Sync(); err != nil {
return err
}
if err := dst.Close(); err != nil {
return err
}
if err := os.Rename(tmpPath, path); err != nil {
return err
}
done = true
return nil
}
func copyChunks(dst io.Writer, src io.Reader) (int64, error) {
size := int64(4)
head := make([]byte, 8)
for {
if _, err := io.ReadFull(src, head); err != nil {
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
return size, nil
}
return size, err
}
id := string(head[0:4])
payloadSize := int64(binary.LittleEndian.Uint32(head[4:8]))
if id == "id3 " || id == "ID3 " {
if err := skipPayload(src, payloadSize); err != nil {
return size, err
}
continue
}
if id == "LIST" {
payload := make([]byte, payloadSize)
if _, err := io.ReadFull(src, payload); err != nil {
return size, err
}
if err := skipPad(src, payloadSize); err != nil {
return size, err
}
if bytes.HasPrefix(payload, []byte("INFO")) {
continue
}
var buf bytes.Buffer
writeChunk(&buf, id, payload)
if _, err := dst.Write(buf.Bytes()); err != nil {
return size, err
}
size += int64(buf.Len())
continue
}
if _, err := dst.Write(head); err != nil {
return size, err
}
if _, err := io.CopyN(dst, src, payloadSize); err != nil {
return size, err
}
size += 8 + payloadSize
if payloadSize%2 != 0 {
if _, err := dst.Write([]byte{0}); err != nil {
return size, err
}
size++
if err := skipPad(src, payloadSize); err != nil {
return size, err
}
}
}
}
func skipPayload(src io.Reader, payloadSize int64) error {
if _, err := io.CopyN(io.Discard, src, payloadSize); err != nil {
return err
}
return skipPad(src, payloadSize)
}
func skipPad(src io.Reader, payloadSize int64) error {
if payloadSize%2 == 0 {
return nil
}
if _, err := io.CopyN(io.Discard, src, 1); err != nil && !errors.Is(err, io.EOF) {
return err
}
return nil
}