docs: document packages, exported API and non-obvious logic
This commit is contained in:
@@ -45,6 +45,9 @@ func (t *flacTagger) write(m Metadata) error {
|
||||
t.addTag("INITIALKEY", m.Key)
|
||||
|
||||
cmtsMeta := t.cmts.Marshal()
|
||||
// index 0 means no comment block was found: a valid flac always starts
|
||||
// with STREAMINFO, so a real Vorbis comment can never be the first block.
|
||||
// Anything else is the index of the block being replaced.
|
||||
if t.index > 0 {
|
||||
t.file.Meta[t.index] = &cmtsMeta
|
||||
} else {
|
||||
@@ -66,6 +69,13 @@ func (t *flacTagger) write(m Metadata) error {
|
||||
return os.Rename(tmpPath, t.path)
|
||||
}
|
||||
|
||||
// addTag appends a Vorbis comment. Vorbis allows repeated keys, so this adds
|
||||
// to whatever the file already had rather than replacing it; re-tagging a
|
||||
// file that was already tagged would therefore duplicate entries. That does
|
||||
// not arise in practice because godeez only tags files it just downloaded.
|
||||
//
|
||||
// The key is written twice for the musical key: KEY is the common spelling
|
||||
// and INITIALKEY is what several DJ applications look for.
|
||||
func (t *flacTagger) addTag(name, value string) {
|
||||
if value != "" {
|
||||
t.cmts.Add(name, value)
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
// Package tag writes track metadata into finished audio files.
|
||||
//
|
||||
// Every container stores metadata differently: mp3 uses ID3v2 frames, flac
|
||||
// uses Vorbis comments, and wav carries an ID3 chunk plus a RIFF LIST/INFO
|
||||
// chunk for players that read only one of the two. Write hides that behind a
|
||||
// single Metadata struct and dispatches on the file extension.
|
||||
//
|
||||
// The taggers are written for freshly downloaded files. Empty fields are
|
||||
// skipped rather than written as blanks, and each tagger writes through a
|
||||
// temporary file so a failure part way cannot corrupt the audio.
|
||||
package tag
|
||||
|
||||
import (
|
||||
@@ -8,6 +18,8 @@ import (
|
||||
"github.com/go-flac/go-flac/v2"
|
||||
)
|
||||
|
||||
// AlbumMetadata is the subset of tags that only make sense for a track that
|
||||
// belongs to an album. It is nil on a standalone single.
|
||||
type AlbumMetadata struct {
|
||||
Artist string
|
||||
Title string
|
||||
@@ -18,6 +30,9 @@ type AlbumMetadata struct {
|
||||
Copyright string
|
||||
}
|
||||
|
||||
// Metadata is the container-independent tag set. Every field is a string
|
||||
// because the underlying formats store them as text; conversions such as
|
||||
// Duration to milliseconds happen inside the individual taggers.
|
||||
type Metadata struct {
|
||||
Title string
|
||||
Artists string
|
||||
@@ -38,6 +53,10 @@ type tagger interface {
|
||||
write(m Metadata) error
|
||||
}
|
||||
|
||||
// newTagger picks an implementation from the file extension. Anything that is
|
||||
// not mp3 or wav is attempted as flac rather than rejected, so an unexpected
|
||||
// extension fails with a parse error from the flac library instead of a
|
||||
// generic unsupported-format message.
|
||||
func newTagger(filePath string) (tagger, error) {
|
||||
switch filepath.Ext(filePath) {
|
||||
case ".mp3":
|
||||
|
||||
@@ -25,6 +25,12 @@ type infoField struct {
|
||||
value string
|
||||
}
|
||||
|
||||
// write replaces the metadata chunks in a wav file.
|
||||
//
|
||||
// Both a LIST/INFO chunk and an id3 chunk are written because wav has no
|
||||
// single agreed metadata convention: older players and file managers read
|
||||
// LIST/INFO, while music libraries and DJ software expect ID3. Writing only
|
||||
// one leaves the tags invisible to half the tools people use.
|
||||
func (t *wavTagger) write(m Metadata) error {
|
||||
id3Chunk, err := buildID3Chunk(m)
|
||||
if err != nil {
|
||||
@@ -57,6 +63,15 @@ func buildID3Chunk(m Metadata) ([]byte, error) {
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// buildInfoChunk assembles the LIST/INFO payload, or nil when there is
|
||||
// nothing worth writing.
|
||||
//
|
||||
// The four character ids are the RIFF INFO registry's, not arbitrary names.
|
||||
// INFO only has a year field, so a full release date is reduced to its year.
|
||||
// Values are NUL terminated because RIFF INFO strings are C strings.
|
||||
//
|
||||
// A payload of exactly 4 bytes is just the "INFO" marker with no fields
|
||||
// after it, which is why that length means empty.
|
||||
func buildInfoChunk(m Metadata) []byte {
|
||||
fields := []infoField{
|
||||
{"INAM", m.Title},
|
||||
@@ -96,6 +111,12 @@ func buildInfoChunk(m Metadata) []byte {
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// writeChunk writes one RIFF chunk: a four character id, the payload length
|
||||
// as a little endian uint32, then the payload.
|
||||
//
|
||||
// RIFF requires chunks to start on even offsets, so an odd length is followed
|
||||
// by a pad byte. That byte is not counted in the declared size, which is the
|
||||
// detail that makes chunk walking fiddly; see skipPad for the reading side.
|
||||
func writeChunk(w io.Writer, id string, payload []byte) {
|
||||
header := make([]byte, 0, 8)
|
||||
header = append(header, id...)
|
||||
@@ -108,6 +129,16 @@ func writeChunk(w io.Writer, id string, payload []byte) {
|
||||
}
|
||||
}
|
||||
|
||||
// rewriteWAV copies path into a new file, dropping any existing metadata
|
||||
// chunks, appending the given ones, and swapping the result into place.
|
||||
//
|
||||
// A wav file cannot be edited in place: chunk sizes and the RIFF size in the
|
||||
// header would all have to shift. Rewriting is simpler and, combined with the
|
||||
// rename at the end, means an interrupted tag write leaves the original
|
||||
// untouched.
|
||||
//
|
||||
// The RIFF size field is patched at offset 4 only after everything is written,
|
||||
// since the final size is not known until then.
|
||||
func rewriteWAV(path string, chunks []wavChunk) error {
|
||||
src, err := os.Open(path)
|
||||
if err != nil {
|
||||
@@ -174,7 +205,21 @@ func rewriteWAV(path string, chunks []wavChunk) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// copyChunks streams every chunk from src to dst except the metadata ones,
|
||||
// and returns the byte count that belongs in the RIFF size field.
|
||||
//
|
||||
// Dropping the existing id3 and LIST/INFO chunks here is what makes tagging
|
||||
// repeatable: the caller appends fresh ones, so tags are replaced rather than
|
||||
// accumulated. A LIST chunk that is not an INFO list is something else
|
||||
// entirely, such as an adtl annotation list, and is preserved.
|
||||
//
|
||||
// A truncated final chunk is treated as the end of the file rather than an
|
||||
// error, because trailing garbage after the audio data is common and should
|
||||
// not make the file untaggable.
|
||||
func copyChunks(dst io.Writer, src io.Reader) (int64, error) {
|
||||
// The count starts at 4 for the "WAVE" id, which sits inside the RIFF
|
||||
// chunk and so counts towards its size, while the 8 byte RIFF header
|
||||
// itself does not.
|
||||
size := int64(4)
|
||||
head := make([]byte, 8)
|
||||
|
||||
@@ -245,6 +290,12 @@ func skipPayload(src io.Reader, payloadSize int64) error {
|
||||
return skipPad(src, payloadSize)
|
||||
}
|
||||
|
||||
// skipPad consumes the pad byte that follows an odd length chunk. It is not
|
||||
// included in the chunk's declared size, so skipping it is what keeps the
|
||||
// reader aligned on the next chunk header.
|
||||
//
|
||||
// A missing pad byte at the very end of the file is tolerated: some encoders
|
||||
// omit it on the last chunk.
|
||||
func skipPad(src io.Reader, payloadSize int64) error {
|
||||
if payloadSize%2 == 0 {
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user