waxlabel

package module
v1.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 17, 2026 License: MIT Imports: 32 Imported by: 0

README

WaxLabel

WaxLabel is a pure-Go library and command-line tool for reading and writing audio-file metadata: tags, embedded pictures, chapters, and synced lyrics. It is preservation-first: edits are planned against the parsed native structure, metadata is rewritten only where needed, and audio bytes are copied rather than transcoded.

It reads and writes FLAC, Ogg Vorbis, Ogg Opus, MP3, WAV, MP4/M4A, raw AAC/ADTS, Matroska/WebM, and AIFF/AIFF-C.

The public API lives in github.com/colespringer/waxlabel and github.com/colespringer/waxlabel/tag; codec packages are internal.

Install

go get github.com/colespringer/waxlabel            # library
go install github.com/colespringer/waxlabel/cmd/waxlabel@latest   # CLI

WaxLabel requires Go 1.26 or newer. The library uses only the standard library; the CLI uses Cobra.

Library

package main

import (
	"context"
	"fmt"
	"log"

	waxlabel "github.com/colespringer/waxlabel"
	"github.com/colespringer/waxlabel/tag"
)

func main() {
	ctx := context.Background()

	doc, err := waxlabel.ParseFile(ctx, "track.flac")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(doc.Fields().Title)

	plan, err := doc.Edit().
		Set(tag.Title, "New Title").
		Set(tag.Artist, "Lead", "Featured").
		Clear(tag.Encoder).
		Prepare()
	if err != nil {
		log.Fatal(err)
	}

	_, result, err := plan.Execute(ctx, waxlabel.SaveBack())
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("committed:", result.Committed)
}

Parse, ParseFile, and OpenSource return an immutable Document that holds no open file descriptor. Editing starts with Document.Edit(), resolves through Editor.Prepare(), and writes only when the resulting Plan is executed. Write destinations:

  • SaveBack() atomically rewrites the parsed file in place (a no-op writes nothing).
  • SaveAsFile(path) writes a complete new file.
  • WriteTo(w, source) streams a complete output to an io.Writer.

CLI

waxlabel dump track.flac
waxlabel plan track.flac --set TITLE="New Title"
waxlabel set track.flac --set TITLE="New Title" --add ARTIST=Featured
waxlabel lint track.flac --fix
waxlabel verify track.flac
waxlabel caps --format flac
waxlabel keys
waxlabel copy source.flac dest.m4a
waxlabel diff before.flac after.flac
waxlabel export-picture track.flac -o cover.jpg
Command Purpose
dump <file>... Show tags, audio properties, pictures, chapters, synced lyrics, and warnings. --native also shows native blocks.
plan <file>... Preview an edit without writing.
set <file>... Apply edits and save. Use -o for a new output file.
lint <file>... Report metadata issues. --fix applies only safe, non-destructive fixes; a legacy container is stripped only when fully redundant with the canonical tags.
verify <file>... Print tag-independent audio-essence digests. --whole-file hashes every byte.
caps <file> or caps --format <name> Show what a file or format can store and edit.
keys List the canonical tag vocabulary and cardinality.
copy <source> <dest> Overlay source metadata onto the destination, reporting what carries, downgrades, or drops.
diff <a> <b> Compare canonical tags, pictures, chapters, and synced lyrics.
export-picture <file> Write one embedded picture to -o FILE. --picture selects by role or index.

Edits are driven by --set KEY=VALUE, --add KEY=VALUE, and --clear KEY, plus picture (--add-cover, --add-picture, --remove-picture), chapter (--add-chapter, --clear-chapters), and synced-lyric (--synced-lyrics-file, --add-synced-lyric, --synced-lyrics-lang) flags. Write shaping is controlled by --preset, --legacy, and --padding. Run waxlabel <command> --help for the full flag list.

Read commands accept - for standard input, and dump, verify, lint, plan, and set can walk directories with --recursive. Format is detected from a file's leading bytes, not its extension. All data commands accept --json. -o writes atomically and refuses an existing target unless --overwrite is given.

lint --json findings carry a machine-readable code and severity; the exit code reflects the highest-precedence result. See waxlabel <command> --help and the package documentation for the exit-code table and finding codes.

Format Support

Format Metadata Notes
FLAC read/write Vorbis comments, FLAC pictures, CHAPTERxxx chapters, SYNCEDLYRICS (LRC); padding is fully controllable.
Ogg Vorbis / Opus read/write Vorbis comments, METADATA_BLOCK_PICTURE, CHAPTERxxx chapters, SYNCEDLYRICS (LRC).
MP3 read/write ID3v2 (CHAP/CTOC chapters, SYLT lyrics); new tags are ID3v2.3. ID3v1/APEv2 are surfaced as legacy.
WAV read/write RIFF LIST/INFO plus embedded id3 (chapters and lyrics); chunks are preserved.
MP4 / M4A / M4B read/write iTunes ilst, cover art, Nero and QuickTime chapters. Fragmented MP4 is rejected.
Matroska / WebM read/write Scoped SimpleTags, segment title, attachments, default-edition chapters. WebM cannot write cover attachments.
AAC (ADTS) read/write Front ID3v2 tag (new tags are ID3v2.4) plus ADTS frames.
AIFF / AIFF-C read/write Native text chunks plus embedded ID3 ; chunks are preserved.

When set authors a structural edit a format cannot store (e.g. cover art on WebM, or chapters on a format with no chapter store), it drops that item with a warning and applies the rest of the edit. set --strict promotes such drops to failures.

The table below is generated from the same capability model used by waxlabel caps.

Format Pictures Chapters Synced Lyrics
AAC (ADTS) read full, write full · APIC frame read full, write full · ID3v2 CHAP/CTOC frames read full, write full · ID3v2 SYLT frame
AIFF read full, write full · APIC (ID3 chunk) read full, write full · ID3v2 CHAP/CTOC frames (ID3 chunk) read full, write full · ID3v2 SYLT frame
FLAC read full, write full · FLAC PICTURE block read full, write full · VorbisComment CHAPTERxxx read full, write full · SYNCEDLYRICS comment (LRC)
MP3 read full, write full · APIC frame read full, write full · ID3v2 CHAP/CTOC frames read full, write full · ID3v2 SYLT frame
MP4 read full, write full · covr atom (JPEG/PNG/BMP) read full, write full · Nero chpl and a QuickTime chapter text track read none, write none
Matroska read full, write full · AttachedFile (image attachment) read full, write full · Chapters > EditionEntry > ChapterAtom (default edition) read none, write none
Ogg Opus read full, write full · METADATA_BLOCK_PICTURE read full, write full · VorbisComment CHAPTERxxx read full, write full · SYNCEDLYRICS comment (LRC)
Ogg Vorbis read full, write full · METADATA_BLOCK_PICTURE read full, write full · VorbisComment CHAPTERxxx read full, write full · SYNCEDLYRICS comment (LRC)
WAV read full, write full · APIC (id3 chunk) read full, write full · ID3v2 CHAP/CTOC frames (id3 chunk) read full, write full · ID3v2 SYLT frame

Some format-specific limits are intentional (for example, MP4 cover art drops the picture description, ID3 chapters store no per-chapter language, and Matroska writes random UIDs so chapter/attachment rewrites are not byte-reproducible). These are documented in the package documentation and surfaced as warnings at write time.

Safety

Input is treated as untrusted: parsers use bounded allocation and recursion limits, fuzz tests cover arbitrary input, and human output sanitizes terminal-control bytes (JSON output uses exact machine-readable values).

Save-back writes go to a temp file in the target directory, are fsync'd, and renamed into place. If the source changed since parse, SaveBack() refuses with waxerr.ErrSourceChanged rather than overwriting newer bytes. Atomic renames have normal filesystem consequences: editing through a symlink rewrites the target and leaves the link, other hard links keep pointing at the old inode, and a read-only file can be replaced when its directory is writable (its mode is preserved).

License

MIT.

Acknowledgements

Mutagen, TagLib, bogem/id3v2, sentriz/go-taglib, and libogg were direct influences on WaxLabel's design and test cross-checks. WaxLabel's implementation follows public specifications and does not copy their code.

Documentation

Overview

Package waxlabel is a pure-Go library for reading and writing audio-file metadata (tags plus embedded cover art).

Scope

WaxLabel is the metadata member of the "Wax" family. Its design goals are preservation-first editing, a public writable canonical key vocabulary, a plan-before-write workflow (Editor.Prepare producing a Plan whose Plan.Report matches exactly what Plan.Execute will do), and versioned audio-essence identity for library-wide deduplication.

WaxLabel is built for music-organization tools that need complete metadata for libraries sourced from uneven inputs such as YouTube. Those files are usually sparse or inconsistently tagged rather than blank: source metadata propagates, and transcoders often stamp an "encoder=Lavf..." comment. WaxLabel treats inherited and generated metadata as data to read, preserve, override, and deduplicate.

Object model

Parse, ParseFile, and OpenSource return an immutable, detached Document: it holds no OS resources and has no Close method, so a caller may scan, cache, and discard it freely. Accessors return detached deep copies of structural data - Picture payloads included, so a caller may mutate anything an accessor returns without affecting the Document or a later call. Document.Inspect skips picture bytes entirely for bulk scans.

Editing flows through Document.Edit, which yields an Editor. The editor records mutations against a presence-aware canonical tag.TagSet; calling Editor.Prepare resolves them into a Plan. Executing the plan against a Destination streams the result.

Frozen contracts

The following contracts are stable across the v1 line; other surface may still evolve:

  • The Document is immutable, detached, and serializable.
  • The presence-aware canonical tag.TagSet/tag.TagPatch is authoritative; the typed tag.Tags struct is a convenience projection.
  • The canonical key vocabulary (tag.Key) is public and writable.
  • Editing is preservation-first: the native document is the base and unaffected data (including legacy tags) is preserved and warned, never stripped silently.
  • Prepare then Execute share state so the plan and the write cannot disagree; a no-op SaveBack writes nothing.
  • AudioDigest carries an algorithm and a versioned extent so persisted dedup hashes survive across library-wide refinements.

Acknowledgements

All code is reimplemented from public specifications (ID3v2, the Vorbis comment format, FLAC, ISO/IEC 14496-12, RIFF/WAVE, RFC 3533/7845/9559). Reference implementations were consulted for design but not copied; see the README acknowledgements.

Index

Constants

View Source
const (
	TransferField       = core.TransferField
	TransferPicture     = core.TransferPicture
	TransferChapter     = core.TransferChapter
	TransferSyncedLyric = core.TransferSyncedLyric
)

TransferKind values.

View Source
const (
	Carried  = core.Carried
	Lossy    = core.Lossy
	Dropped  = core.Dropped
	Excluded = core.Excluded
)

Disposition values.

View Source
const (
	FormatUnknown   = core.FormatUnknown
	FormatFLAC      = core.FormatFLAC
	FormatOggVorbis = core.FormatOggVorbis
	FormatOggOpus   = core.FormatOggOpus
	FormatMP3       = core.FormatMP3
	FormatWAV       = core.FormatWAV
	FormatMP4       = core.FormatMP4
	FormatAAC       = core.FormatAAC
	FormatMatroska  = core.FormatMatroska
	FormatAIFF      = core.FormatAIFF
)

Format values.

View Source
const (
	PicOther              = core.PicOther
	PicFileIcon           = core.PicFileIcon
	PicOtherFileIcon      = core.PicOtherFileIcon
	PicFrontCover         = core.PicFrontCover
	PicBackCover          = core.PicBackCover
	PicLeaflet            = core.PicLeaflet
	PicMedia              = core.PicMedia
	PicLeadArtist         = core.PicLeadArtist
	PicArtist             = core.PicArtist
	PicConductor          = core.PicConductor
	PicBand               = core.PicBand
	PicComposer           = core.PicComposer
	PicLyricist           = core.PicLyricist
	PicRecordingLocation  = core.PicRecordingLocation
	PicDuringRecording    = core.PicDuringRecording
	PicDuringPerformance  = core.PicDuringPerformance
	PicVideoScreenCapture = core.PicVideoScreenCapture
	PicBrightFish         = core.PicBrightFish
	PicIllustration       = core.PicIllustration
)

PictureType values (matching ID3 APIC / FLAC PICTURE type IDs).

View Source
const (
	AccessNone    = core.AccessNone
	AccessPartial = core.AccessPartial
	AccessFull    = core.AccessFull
)

AccessLevel values.

View Source
const (
	LegacyPreserve = core.LegacyPreserve
	LegacyStrip    = core.LegacyStrip
)

LegacyPolicy values.

View Source
const (
	ID3MultiNullSep     = core.ID3MultiNullSep
	ID3MultiRepeatFrame = core.ID3MultiRepeatFrame
	ID3MultiSlash       = core.ID3MultiSlash
)

ID3MultiValuePolicy values.

View Source
const (
	FamilyVorbis   = core.FamilyVorbis
	FamilyID3v2    = core.FamilyID3v2
	FamilyID3v1    = core.FamilyID3v1
	FamilyAPEv2    = core.FamilyAPEv2
	FamilyMP4      = core.FamilyMP4
	FamilyRIFF     = core.FamilyRIFF
	FamilyMatroska = core.FamilyMatroska
	FamilyAIFF     = core.FamilyAIFF
)

Family values.

View Source
const (
	ScopeTrack   = core.ScopeTrack
	ScopeAlbum   = core.ScopeAlbum
	ScopeEdition = core.ScopeEdition
	ScopeChapter = core.ScopeChapter
)

Scope values annotate the target a family value applies to. Most formats are track-scoped; Matroska's targets make album/edition/chapter scopes meaningful.

View Source
const (
	WarnStrayLeadingID3        = core.WarnStrayLeadingID3
	WarnTrailingID3v1          = core.WarnTrailingID3v1
	WarnLegacyAPE              = core.WarnLegacyAPE
	WarnMultipleVorbisComment  = core.WarnMultipleVorbisComment
	WarnInheritedEncoder       = core.WarnInheritedEncoder
	WarnDistrustedBlockSize    = core.WarnDistrustedBlockSize
	WarnUnknownBlock           = core.WarnUnknownBlock
	WarnInvalidPicture         = core.WarnInvalidPicture
	WarnConflictingFamilies    = core.WarnConflictingFamilies
	WarnNumericGenre           = core.WarnNumericGenre
	WarnChainedStream          = core.WarnChainedStream
	WarnID3MultiValue          = core.WarnID3MultiValue
	WarnDuplicateTagBlock      = core.WarnDuplicateTagBlock
	WarnChapterSourceConflict  = core.WarnChapterSourceConflict
	WarnChaptersStale          = core.WarnChaptersStale
	WarnChapterTitleTruncated  = core.WarnChapterTitleTruncated
	WarnChaptersFlattened      = core.WarnChaptersFlattened
	WarnNoAudioFrames          = core.WarnNoAudioFrames
	WarnTruncatedAudio         = core.WarnTruncatedAudio
	WarnChapterPastDuration    = core.WarnChapterPastDuration
	WarnDuplicateChapter       = core.WarnDuplicateChapter
	WarnSingleValuedMulti      = core.WarnSingleValuedMulti
	WarnDuplicatePicture       = core.WarnDuplicatePicture
	WarnMultipleFrontCovers    = core.WarnMultipleFrontCovers
	WarnPictureMetadataDropped = core.WarnPictureMetadataDropped
	WarnLegacyConflict         = core.WarnLegacyConflict
	WarnValueDropped           = core.WarnValueDropped
	WarnNativeValueReduced     = core.WarnNativeValueReduced
	WarnValueReduced           = core.WarnValueReduced
	WarnChapterEndsDropped     = core.WarnChapterEndsDropped
	WarnPaddingClamped         = core.WarnPaddingClamped
	WarnTagStructureDropped    = core.WarnTagStructureDropped
	WarnChapterStartOverflow   = core.WarnChapterStartOverflow
	WarnChapterMetadataDropped = core.WarnChapterMetadataDropped
	WarnOversizedChunk         = core.WarnOversizedChunk

	WarnSyncedLyricsTimestampFormat  = core.WarnSyncedLyricsTimestampFormat
	WarnSyncedLyricsContentType      = core.WarnSyncedLyricsContentType
	WarnSyncedLyricsMetadataDropped  = core.WarnSyncedLyricsMetadataDropped
	WarnSyncedLyricsTimestampClamped = core.WarnSyncedLyricsTimestampClamped
	WarnSyncedLyricsTruncated        = core.WarnSyncedLyricsTruncated
	WarnSyncedLyricsUnsupported      = core.WarnSyncedLyricsUnsupported

	WarnPictureUnsupported  = core.WarnPictureUnsupported
	WarnChaptersUnsupported = core.WarnChaptersUnsupported

	WarnMP4MultiValue = core.WarnMP4MultiValue

	WarnInvalidTagKey = core.WarnInvalidTagKey

	WarnNumberTotalConflict = core.WarnNumberTotalConflict

	WarnValueCoerced = core.WarnValueCoerced

	WarnChapterOverlapReconciled = core.WarnChapterOverlapReconciled

	WarnSyncedLyricsLineDropped = core.WarnSyncedLyricsLineDropped

	WarnPictureSelectorMiss = core.WarnPictureSelectorMiss
)

WarningCode values.

View Source
const DefaultMaxSourceBytes = core.DefaultMaxSourceBytes

DefaultMaxSourceBytes is the default ceiling OpenSource applies to a non-seekable stream it buffers whole into memory (2 GiB). A stream larger than this fails with waxerr.ErrSizeTooLarge; pass WithMaxSourceBytes(0) to lift the cap entirely. The CLI applies the same default to buffered standard input via its --max-size flag.

Variables

This section is empty.

Functions

func EqualChapters

func EqualChapters(a, b []Chapter) bool

EqualChapters reports whether two chapter slices are identical by content (start, end, and title), in order. This is the chapter analogue of EqualPictures.

func EqualChaptersModuloEnds

func EqualChaptersModuloEnds(a, b []Chapter, durA, durB time.Duration) bool

EqualChaptersModuloEnds reports whether two chapter lists are equal after normalizing away ends a codec would itself reconstruct (a gapless interior end, or a trailing end that runs to end-of-file). Byte-identical lists are always equal regardless of duration. durA/durB are the two files' media durations, used only for the trailing rule. It is what [diff] uses: the interior gapless rule matches how copy grades that end (reconstructable), while the trailing run-to-EOF rule is diff-specific and intentionally diverges from copy's format-based trailing grade. EqualChapters (literal ends) still backs codec change-detection.

func EqualPictures

func EqualPictures(a, b []Picture) bool

EqualPictures reports whether two picture slices are identical by content (type, MIME, description, dimensions, and bytes), in order. It is the same equality a codec uses to detect a picture edit, so a comparison and an edit cannot disagree on what "the same pictures" means.

func EqualSyncedLyrics

func EqualSyncedLyrics(a, b []SyncedLyrics) bool

EqualSyncedLyrics reports whether two synced-lyrics slices are identical by content (language, description, and timed lines), in order. SyncedLyrics contains a slice, so it is not comparable with ==; this is the equality codecs use to detect edits.

func ExtensionsFor

func ExtensionsFor(f Format) []string

ExtensionsFor returns the lowercase file extensions (each with a leading dot) associated with format f, or nil for an unknown or unimplemented format. It lets a caller warn when an output path's extension does not match the data being written, since WaxLabel never transcodes.

func FormatChapterTime

func FormatChapterTime(d time.Duration) string

FormatChapterTime renders a chapter offset as H:MM:SS.mmm (millisecond precision), the same format WaxLabel's chapter listing uses, exposed so a consumer renders chapter timestamps consistently with it. It is also what the chapter sanity warnings use, so a warning's timestamp matches the listing.

func FormatLRC

func FormatLRC(lines []SyncedLine) string

FormatLRC renders timed lyric lines as an LRC document ("[mm:ss.mmm]text" per line, in order). It round-trips losslessly through ParseLRC; the per-set language and descriptor are not representable in LRC and are not emitted.

func HumanBytes

func HumanBytes(n int64) string

HumanBytes formats a byte count with a binary-magnitude unit (B, KiB, MiB, ...) - the same formatting WaxLabel uses in its own text output and size-limit error messages, exposed so a consumer can render sizes consistently with it. Sub-1024 counts stay exact ("57 B"); larger counts round to one decimal place and promote at a unit boundary, so 1 MiB - 1 byte reads "1.0 MiB", not "1024.0 KiB".

func IsRecognizedImage

func IsRecognizedImage(data []byte) bool

IsRecognizedImage reports whether data begins with the header of an image format WaxLabel can identify (PNG, JPEG, GIF, WebP, BMP, or TIFF). It is a header sniff, not a full decode, so it cannot recognize every valid image (AVIF/HEIC/JXL and the like return false); a caller embedding a deliberately exotic cover should offer an explicit override rather than treat a false negative as corruption. The CLI uses it to reject a non-image file passed as cover art before embedding it, without reaching into internal packages.

func NewTempCreateError

func NewTempCreateError(dir string, err error) error

NewTempCreateError builds the same temp-create failure [writeAtomic] returns when a destination directory rejects a write: it names dir (not the random temp file) and unwraps to err so the failure classifies as local I/O. It is exported so a caller that probes a directory's writability up front - the CLI's -o pre-check - surfaces the identical error (same message and exit class) the late atomic write would, instead of re-implementing it.

func ResolveAlias

func ResolveAlias(key tag.Key) tag.Key

ResolveAlias returns the canonical key for a recognized alternative tag spelling (DATE/YEAR -> RECORDINGDATE, TOTALTRACKS -> TRACKTOTAL, ORGANIZATION -> LABEL, ...), or key unchanged when it is not an alias. Front-ends use it before applying an edit so an alias targets the real field instead of creating a duplicate custom field.

func ResolveWriteTarget

func ResolveWriteTarget(path string) string

ResolveWriteTarget returns the path an atomic write will rename over: the symlink-resolved target when path resolves (so the rewrite updates the file a link points at, leaving the link in place), else path verbatim (a fresh target or a dangling link). It is the single resolution rule [writeAtomic] uses; a caller pre-checking an -o destination resolves the same way so its probe inspects the directory the write actually lands in.

func ValidWritableText

func ValidWritableText(s string) error

ValidWritableText reports whether s can be written faithfully to every supported format: no NUL byte (which truncates a C-string field) and valid UTF-8 (the read path reprojects invalid UTF-8, so it would not round-trip). It returns nil, or an error wrapping waxerr.ErrInvalidData naming the problem. Editor edits already enforce this on authored text; a caller (or a front-end) may pre-check a value with it, or with WritableTextReason for the bare reason phrase, before building an edit.

func WritableTextReason

func WritableTextReason(s string) string

WritableTextReason returns "" when s can be written faithfully to every supported format, else a short reason phrase ("contains a NUL byte" / "contains invalid UTF-8"). It is the single source of truth for the NUL / invalid-UTF-8 rule: the internal checkWritableText and the public ValidWritableText wrap it in an waxerr.ErrInvalidData error, and a front-end (the CLI) can read the bare phrase to build its own message without parsing an error string.

Types

type AccessLevel

type AccessLevel = core.AccessLevel

AccessLevel grades a support dimension.

type AudioDigest

type AudioDigest struct {
	Algorithm     string
	ExtentVersion string
	TrackID       int
	Sum           []byte
}

AudioDigest is a content identity for audio. Algorithm and the named, versioned ExtentVersion travel with the Sum so a persisted dedup hash stays interpretable library-wide: refining the extent definition is an opt-in new version, not a silent change that invalidates old hashes.

func (AudioDigest) Equal

func (d AudioDigest) Equal(other AudioDigest) bool

Equal reports whether two digests have the same algorithm, extent, TrackID, and sum.

func (AudioDigest) String

func (d AudioDigest) String() string

String renders the digest as "algorithm/extent:hex". When TrackID is non-zero, it renders as "algorithm/extent#trackID:hex" so per-track digests do not collide with each other.

type AudioTrack

type AudioTrack = core.AudioTrack

AudioTrack is one stream's technical properties.

type Capabilities

type Capabilities = core.Capabilities

Capabilities reports what a format can do, per dimension.

func CapabilitiesFor

func CapabilitiesFor(f Format, opts ...WriteOption) Capabilities

CapabilitiesFor reports what format f can do under the given write options, without a parsed file. It is the file-less, format-level query an edit form for a not-yet-created file of format f needs - the counterpart to Document.Capabilities, which answers the same question for a file already in hand. Both route through the same codec call, so the file-aware and file-less reports cannot drift. An unknown or unimplemented format reports read-only (mirroring Document.Capabilities's no-codec fallback); pair it with the per-key tag.Key.Multivalued and the [Capabilities.Field] detail to enumerate what is editable.

type Capability

type Capability = core.Capability

Capability is one field's multidimensional support.

type Chapter

type Chapter = core.Chapter

Chapter is a navigation point (Start, End, Title) in a timed file.

type Destination

type Destination struct {
	// contains filtered or unexported fields
}

Destination names where Plan.Execute writes. Construct one with SaveBack, SaveAsFile, or WriteTo.

func SaveAsFile

func SaveAsFile(path string) Destination

SaveAsFile writes a complete file at path (atomically). Unlike SaveBack it is never a no-op: a fresh destination is always written whole. It OVERWRITES any existing file at path (atomically replacing it via a temp file and rename) - it does not refuse an existing target, so the caller is responsible for the "do not clobber" check. The CLI's copy -o guard is a CLI-level policy, not a library one. (If a hard guard is later wanted, the safe route is an additive SaveAsNewFile(path) that returns a waxerr-wrapped "exists" error, not a default-refuse flip of this function's contract.) Writing to a path that resolves to the document's own source file spends the plan just as SaveBack does (see Plan.Execute); writing to other paths leaves the plan reusable.

For a ParseFile document it verifies the source file has not changed since parse (waxerr.ErrSourceChanged otherwise), as SaveBack does: the copied byte offsets come from the source as parsed, so a changed source would produce a corrupt file. Writing in place (a target that resolves to the source) uses the full check; writing to another path uses the precise inode+size+fingerprint check, so a benign mtime-only touch does not block it. An OpenSource document reads stable in-memory bytes and is not checked.

It needs a document that can resolve its own source bytes - one from ParseFile or OpenSource. A detached document from Parse carries no source, so SaveAsFile fails with waxerr.ErrInvalidData; write it with WriteTo and an explicit source instead.

func SaveBack

func SaveBack() Destination

SaveBack rewrites the original file in place, atomically (temp file, fsync, rename, directory fsync). It requires the document to have come from ParseFile, verifies the file has not changed since parse (waxerr.ErrSourceChanged otherwise), and writes nothing for a no-op plan.

func WriteTo

func WriteTo(w io.Writer, source ReaderAtSized) Destination

WriteTo streams the complete output to w. The source bytes to copy come from source (required when the document is detached, i.e. from Parse); for a ParseFile or OpenSource document, pass nil to use its own source.

When it reopens a ParseFile document's own source (source is nil), it first verifies that file has not changed since parse (waxerr.ErrSourceChanged otherwise), as SaveBack does - a streaming write never clobbers the source, so it uses the precise inode+size+fingerprint check. An explicit source or an OpenSource document supplies stable bytes and is not checked.

type Disposition

type Disposition = core.Disposition

Disposition grades how a value survives a transfer (carried/lossy/dropped/excluded).

type Document

type Document struct {
	// contains filtered or unexported fields
}

Document is an immutable, detached, serializable view of a parsed file. It holds no file descriptor and has no Close method, so a caller may scan it, cache it, and discard it freely. Accessors return detached deep copies, including each Picture's payload bytes (Document.Pictures), so a caller may mutate anything an accessor returns without affecting the Document or a later call. For bulk scans that do not need payloads, Document.Inspect skips them (and the native document) to stay cheap.

A Document is safe for concurrent reads.

func Parse

func Parse(ctx context.Context, src ReaderAtSized, opts ...ParseOption) (*Document, error)

Parse reads metadata from src, returning a detached Document. src is used only during the call; the Document retains no reference to it, so to write the result you supply a source again via WriteTo. Use ParseFile when you have a path (it records source identity for save-back).

func ParseFile

func ParseFile(ctx context.Context, path string, opts ...ParseOption) (*Document, error)

ParseFile opens path, parses it, and closes it before returning. The Document holds no file descriptor; it records a strong source identity so a later Plan.Execute with SaveBack can detect a changed file.

func (*Document) Capabilities

func (d *Document) Capabilities(opts ...WriteOption) Capabilities

Capabilities reports what the format can do under the given write options (capabilities are option-dependent).

func (*Document) Chapters

func (d *Document) Chapters() []Chapter

Chapters returns the navigation chapters as a detached copy, in file order. Chapters live beside the canonical tags (not inside the TagSet); a file with no chapters returns none. Document.Inspect deliberately omits them.

func (*Document) Edit

func (d *Document) Edit() *Editor

Edit returns an Editor that records mutations against this Document without altering it. The Document remains immutable.

func (*Document) Families

func (d *Document) Families() []FamilyValue

Families returns the tag-family/source view: which family supplied each canonical value, its scope, and whether it won the projection (unselected entries for a key signal a conflict).

func (*Document) Fields

func (d *Document) Fields() tag.Tags

Fields returns the typed convenience projection of the canonical tags. It is lossy (a struct cannot express presence); use Document.Tags when that distinction matters.

func (*Document) Format

func (d *Document) Format() Format

Format returns the detected container/codec.

func (*Document) Get

func (d *Document) Get(key tag.Key) ([]string, bool)

Get returns the values for a canonical key and whether it is present.

func (*Document) HasOpaqueLegacyContent

func (d *Document) HasOpaqueLegacyContent() bool

HasOpaqueLegacyContent reports whether a legacy container holds non-tag content (an APEv2 binary item, a leading ID3v2's picture/chapter/synced-lyric frames, or an unreadable such container) that the canonical projection does not fold in. A legacy strip cannot prove such a container redundant, so the safe fix keeps it.

func (*Document) HashAudioEssence

func (d *Document) HashAudioEssence(ctx context.Context, opts ...HashOption) (AudioDigest, error)

HashAudioEssence computes the encoded-essence identity: a hash over the audio packets plus the decoder-critical configuration (sample rate, channel count, bit depth, and FLAC block-size bounds). Mixing the config means two files with byte-identical packets but different channel mapping are correctly distinct. This answers "is this the same audio?", independent of tags.

It is distinct from whole-file identity (Document.HashFile) and from a decoded-PCM hash (which needs a decoder and is test-only).

func (*Document) HashFile

func (d *Document) HashFile(ctx context.Context, opts ...HashOption) (AudioDigest, error)

HashFile computes the whole-file identity (a hash of every byte). This is the strictest level: it changes whenever any byte, including tags, changes.

func (*Document) Identity

func (d *Document) Identity() Identity

Identity returns the recorded source identity (path, size, timestamps, and structural fingerprint) used for save-back change detection.

func (*Document) Inspect

func (d *Document) Inspect() Snapshot

Inspect returns a Snapshot suitable for scanning large libraries: it skips picture payloads and the native document entirely, so it stays cheap.

func (*Document) LegacyOnlyKeys

func (d *Document) LegacyOnlyKeys() []tag.Key

LegacyOnlyKeys returns canonical keys whose value exists only in a legacy container (an alternate, non-authoritative block such as MP3 ID3v1/APEv2 or FLAC's leading ID3v2 / trailing ID3v1) and not in the authoritative tag set. These are the values dump would otherwise omit and a legacy strip would destroy, so the safe auto-fix keeps their container and dump surfaces them.

func (*Document) Lint

func (d *Document) Lint() []Finding

Lint inspects a document for issues a tagger would want to surface or fix: stale legacy containers, inherited encoder noise, conflicting family values, duplicate or invalid pictures, malformed dates and numbers, single-valued keys carrying several values, and custom (non-vocabulary) keys. It reads only the parsed document (no I/O) and never modifies it.

func (*Document) Native

func (d *Document) Native() NativeDoc

Native returns a deep copy of the format's native document for inspection. To edit it, use Editor.Native.

func (*Document) Padding

func (d *Document) Padding() int64

Padding reports the total bytes of free padding the format reserves after its metadata, so a caller can see how much slack a metadata rewrite can grow into before the audio must move. Only FLAC models an explicit padding block today; every other format reports 0, so a caller should read 0 as "not reported" rather than "no slack". It sums the native base's PADDING entries directly (never through the deep-cloning Document.Native, which would copy multi-megabyte embedded picture bodies just to total padding); Describe copies no block bodies. A native entry's Size is a byte count only when its Unit is empty, so the Unit guard keeps a future non-byte "PADDING" quantity from being summed as bytes.

func (*Document) Pictures

func (d *Document) Pictures() []Picture

Pictures returns the embedded pictures as a fully detached deep copy: each returned Picture's Data is independent, so a caller may mutate it without affecting a later Pictures() call or any internal state.

func (*Document) PlanLintFix

func (d *Document) PlanLintFix() LintFix

PlanLintFix maps a document's lint findings to the safe remediation. Two finding classes are auto-fixed, both non-destructive:

  • inherited-encoder: clear the ENCODER software stamp (tag.Encoder);
  • stray-leading-id3 / trailing-id3v1 / legacy-ape: strip the legacy ID3v1/APEv2/stray-ID3 containers (WithLegacyPolicy LegacyStrip), but only when WaxLabel can prove them fully redundant with the canonical set.

The legacy strip is LegacyStrip, which is all-or-nothing (it strips every legacy container). So it is skipped when any legacy container holds unique data the strip would destroy - a tag present only in a legacy container (Document.LegacyOnlyKeys) or non-tag content the projection does not fold in (Document.HasOpaqueLegacyContent: an APEv2 binary item, a leading ID3v2's pictures/chapters/lyrics, or an unreadable container). A mixed file (one redundant container plus one carrying unique data) conservatively keeps both; the pre-existing legacy warning still fires so lint exits non-zero, and the legacy-only-tags info explains why the container was preserved. An explicit WithLegacyPolicy LegacyStrip still strips unconditionally.

The finding codes are the canonical parse-warning codes (the same ones dump prints), so this keys off exactly what lint reports - no private alias to keep in step with the linter. No other finding is acted on: dropping an unsniffable-but-valid cover would be silent data loss, a malformed date cannot be guessed, conflicting families have no winner, and missing audio cannot be synthesized. The encoder fix clears the canonical ENCODER key and, via WithStripEncoderStamp, also handles native stamps the canonical key cannot reach: the WAV ISFT INFO item and the FLAC/Ogg/Opus comment-header vendor string. Vendor fields are mandatory, so they are rewritten to a neutral value instead of removed. This plan is derived from the parsed document only; the saved file's next lint is the final result.

func (*Document) PlanTransfer

func (d *Document) PlanTransfer(dst Format, opts ...WriteOption) (TransferReport, error)

PlanTransfer simulates copying this document's canonical metadata (tags, pictures, chapters, and synced lyrics) into a file of format dst. It reports what each piece would carry, downgrade, or lose without writing or needing a destination file. It consults dst's capabilities under the given write options, so an option-dependent destination is judged as a real write would be.

A read-only destination format reports everything dropped; an unimplemented destination is an error. Use Document.PrepareTransfer when you have an actual destination file and want an executable plan as well.

func (*Document) PrepareTransfer

func (d *Document) PrepareTransfer(dst *Document, opts ...WriteOption) (*Plan, TransferReport, error)

PrepareTransfer projects this document's canonical metadata onto dst and resolves the result into a ready-to-execute Plan that writes dst, returning the plan together with the TransferReport describing the projection. The report is computed from the same projection the plan applies: every carried or downgraded item is set on the destination edit, and every dropped item is left off.

The report grades the destination's representational capability per field/picture/chapter, including hard structural limits it models (such as the MP4 chapter-count cap, reported as a drop). A few codec validity checks that depend on the bytes themselves - an embedded image in a format the destination cannot label, or a structurally invalid picture set - are enforced when the plan is prepared and surface as an error from this call rather than as a per-item drop; in that case the returned report still describes the attempted projection.

The transfer overlays src onto dst: each canonical key present in the source replaces that key in the destination, the source's pictures replace the destination picture set whenever at least one source picture is representable in the destination (a source whose covers are all unrepresentable leaves the destination's own covers intact), and likewise for chapters and synced lyrics. Destination keys the source does not carry are kept. dst is not modified; only Plan.Execute writes.

func (*Document) Properties

func (d *Document) Properties() Properties

Properties returns the audio stream properties (a copy).

func (*Document) SyncedLyrics

func (d *Document) SyncedLyrics() []SyncedLyrics

SyncedLyrics returns the timed lyric sets as a detached deep copy, in file order. Like chapters, they live beside the canonical tags rather than inside the TagSet. A file with none returns nil. Unsynchronized lyrics remain in Document.Tags as the LYRICS field.

func (*Document) Tags

func (d *Document) Tags() tag.TagSet

Tags returns the authoritative, presence-aware canonical tag set as a deep copy. This is the source of truth; the typed Document.Fields is derived from it.

func (*Document) Warnings

func (d *Document) Warnings() []Warning

Warnings returns the non-fatal conditions found during parse (preserved legacy tags, inherited encoder stamps, unknown blocks, and so on).

type Editor

type Editor struct {
	// contains filtered or unexported fields
}

Editor records mutations against a Document without changing it. Mutations accumulate as a presence-aware tag.TagPatch (for canonical fields) plus a working picture list; Editor.Prepare resolves them into a Plan. The editor methods return the editor for chaining.

func (*Editor) Add

func (e *Editor) Add(key tag.Key, vals ...string) *Editor

Add appends values to a key. The key is resolved through ResolveAlias, so adding under an alias spelling appends to the canonical field.

func (*Editor) AddPicture

func (e *Editor) AddPicture(p Picture) *Editor

AddPicture appends a picture. Its MIME and dimensions are reconciled with the image bytes via an authoritative header sniff ([Picture.SniffAuthoritative]): when the bytes are a recognized image the sniffed MIME and dimensions win over any the caller set, so a mislabeled cover cannot be embedded under a MIME that contradicts it. (A file's stored picture, read by the decoders, keeps its own MIME - that path fills only, via [Picture.SniffInto].)

func (*Editor) Apply

func (e *Editor) Apply(p tag.TagPatch) *Editor

Apply records an explicit patch (set/clear/add operations) after any already recorded, so later edits win on conflicts. Each operation's key is resolved through ResolveAlias first, exactly as the key-taking methods below do, so a patch built with an alias spelling (e.g. DATE) lands on the canonical field rather than a custom key.

func (*Editor) Clear

func (e *Editor) Clear(key tag.Key) *Editor

Clear removes a key (makes it absent). The key is resolved through ResolveAlias, so clearing an alias spelling removes the canonical field.

func (*Editor) ClearChapters

func (e *Editor) ClearChapters() *Editor

ClearChapters removes all chapters.

func (*Editor) ClearPictures

func (e *Editor) ClearPictures() *Editor

ClearPictures removes all pictures.

func (*Editor) ClearSyncedLyrics

func (e *Editor) ClearSyncedLyrics() *Editor

ClearSyncedLyrics removes all synced lyrics. It also marks the set as explicitly cleared, so a following Editor.SetSyncedLyrics authors a fresh set that does not inherit the destination's existing ID3 SYLT language or descriptor. A clear with no following set just removes the synced lyrics.

func (*Editor) Native

func (e *Editor) Native() NativeEditor

Native returns the native inspection view for the original parsed document. It does not include pending editor changes; pictures, tags, or chapters added on the editor are visible only after a save and reparse. Structural native mutation, such as arbitrary block edits, multiple comment blocks, or vendor string edits, is not part of the public editing API.

func (*Editor) NotePictureSelectorMiss

func (e *Editor) NotePictureSelectorMiss(roles ...string) *Editor

NotePictureSelectorMiss records cover-art role names a removal named that matched no picture in this file, so Editor.Prepare surfaces a WarnPictureSelectorMiss per role rather than the removal being a silent no-op. It is per-file (a role matched in one file may miss in another), so a front-end computes the misses against this file's pictures and hands them here. Passing no roles is a no-op.

func (*Editor) NoteSyncedLyricsDropped

func (e *Editor) NoteSyncedLyricsDropped(lines ...int) *Editor

NoteSyncedLyricsDropped records the 1-based line numbers of authored LRC input that produced no timed lyric and were dropped, so Editor.Prepare surfaces a WarnSyncedLyricsLineDropped. It is a front-end diagnostic (the CLI's --synced-lyrics-file parse), carried on the editor rather than the SyncedLyrics content type so the library model stays free of a parse-time concern. Passing no line numbers is a no-op.

func (*Editor) Prepare

func (e *Editor) Prepare(opts ...WriteOption) (*Plan, error)

Prepare resolves the recorded mutations into a Plan under the given write options. The plan's Plan.Report describes exactly what executing it will do; nothing is written yet, and Prepare performs no I/O (the parsed document holds everything the planner needs).

func (*Editor) RemovePictures

func (e *Editor) RemovePictures(match func(Picture) bool) *Editor

RemovePictures drops every picture for which match returns true. match is evaluated exactly once per picture, and the parallel added-mask is filtered with the same verdicts, so an added-then-removed picture is not validated by Prepare and a side-effecting/non-deterministic matcher cannot double-fire or desync.

func (*Editor) Set

func (e *Editor) Set(key tag.Key, vals ...string) *Editor

Set replaces a key's values. The key is resolved through ResolveAlias, so an alternative spelling (Set(tag.Key("DATE"), ...)) lands on the canonical field (RECORDINGDATE) on every format instead of creating a custom key; a non-alias key is unchanged.

Calling Set with no values collapses the key to absent during Editor.Prepare, matching the empty-value cleanup. Editor.Clear is the explicit removal call. Set(key, "") is distinct: it stores one empty value. A format that cannot store that value may drop it, report a removed/no-op change, and let the CLI print an advisory stderr note.

A slash-combined "n/total" on tag.TrackNumber or tag.DiscNumber is normalized at Editor.Prepare into the canonical pair (e.g. Set(tag.TrackNumber, "3/12") becomes TRACKNUMBER=3 + TRACKTOTAL=12) so every format stores it identically; see splitNumberPairs for the precedence rules.

func (*Editor) SetChapters

func (e *Editor) SetChapters(chs ...Chapter) *Editor

SetChapters replaces the whole chapter list. Chapters are a timeline, so the list is sorted by start time (stably, preserving the order of chapters that share a start) because an out-of-order argument can lose a start when a container encodes spans relative to the previous chapter. A format that cannot write chapters reports that through Capabilities. Lists above a format's hard count cap are rejected at Editor.Prepare; ID3 CTOC and MP4 Nero chpl are capped at 255 entries.

func (*Editor) SetSyncedLyrics

func (e *Editor) SetSyncedLyrics(sls ...SyncedLyrics) *Editor

SetSyncedLyrics replaces all synced-lyrics sets. Lines within each set are sorted by Time with a stable sort, matching ParseLRC and Editor.SetChapters. The line slices are deep-copied so later caller mutations cannot change the pending edit. A format that cannot write synced lyrics reports that through Capabilities, and Editor.Prepare rejects the write (or, under WithAllowUnsupportedDrop, drops the set with a warning).

It leaves the explicit-clear marker untouched, so calling it after Editor.ClearSyncedLyrics authors a fresh set that does not inherit the destination's existing ID3 SYLT language, while a plain SetSyncedLyrics with no preceding clear keeps that inheritance convenience.

func (*Editor) SetTags

func (e *Editor) SetTags(t tag.Tags) *Editor

SetTags applies the non-empty fields of a typed tag.Tags as sugar (it compiles to a patch of Set operations; it cannot clear fields).

type Family

type Family = core.Family

Family identifies which tag container supplied a value.

type FamilyValue

type FamilyValue = core.FamilyValue

FamilyValue is one family's contribution to a key.

type Finding

type Finding struct {
	Severity LintSeverity
	Code     string
	Message  string
	Key      tag.Key // the field involved, or "" if not field-specific
}

Finding is one issue reported by Document.Lint.

func (Finding) String

func (f Finding) String() string

String renders the finding as "[severity] code: message (key)". The severity and code are fixed library vocabulary; the message and key can be file-derived (the inherited-encoder message carries the raw inherited stamp; a custom-key finding carries the raw field name), so those two are run through tag.SanitizeLine individually - the finding prints as one list item, so a newline or tab is escaped too (it cannot forge a line), not just the terminal-hijack class. A library consumer that prints this without the CLI's output boundary is then safe. The malformed-date message is already %q-escaped inside the Message, which SanitizeLine leaves intact (no double-escape).

type Format

type Format = core.Format

Format identifies a container/codec.

func Formats

func Formats() []Format

Formats returns every container/codec format this build implements, in registration order. It lets a caller enumerate the formats - for example to gather all recognized file extensions via ExtensionsFor when scanning a directory tree - without hard-coding the list.

type HashOption

type HashOption func(*hashOptions)

HashOption configures audio-essence hashing.

func WithHashSource

func WithHashSource(src ReaderAtSized) HashOption

WithHashSource supplies the source bytes to hash for a detached document (one from Parse). Documents from ParseFile or OpenSource resolve their source automatically.

type ID3MultiValuePolicy

type ID3MultiValuePolicy = core.ID3MultiValuePolicy

ID3MultiValuePolicy controls the ID3v2.3 multi-value representation.

type Identity

type Identity = core.Identity

Identity is a strong source fingerprint for change detection.

type LegacyPolicy

type LegacyPolicy = core.LegacyPolicy

LegacyPolicy controls handling of legacy/foreign tag containers.

type Limits

type Limits = bits.Limits

Limits bounds resource use when parsing untrusted input.

type LintFix

type LintFix struct {
	Patch   tag.TagPatch
	Options []WriteOption
}

LintFix is the safe, non-destructive remediation derived from a document's lint findings: the tag patch and write options that, applied together and saved, clear what can be safely cleared. It is deliberately conservative - only the encoder stamp and provably-redundant legacy containers are touched - so applying it can never lose data a user might want to keep.

type LintSeverity

type LintSeverity uint8

LintSeverity grades a Finding.

const (
	// LintInfo notes something worth knowing but not wrong.
	LintInfo LintSeverity = iota
	// LintWarning flags a likely problem (stale legacy tags, encoder noise).
	LintWarning
	// LintError flags an invalid or contradictory state.
	LintError
)

func (LintSeverity) String

func (s LintSeverity) String() string

type NativeDoc

type NativeDoc = core.NativeDoc

NativeDoc is a codec's editable native document.

type NativeEditor

type NativeEditor struct {
	// contains filtered or unexported fields
}

NativeEditor exposes the native document's structure for inspection, so a caller can see exactly what is preserved. It does not mutate native metadata.

func (NativeEditor) Entries

func (n NativeEditor) Entries() []NativeEntry

Entries summarizes the native metadata blocks.

type NativeEntry

type NativeEntry = core.NativeEntry

NativeEntry summarizes one native metadata block.

type PaddingPolicy

type PaddingPolicy = core.PaddingPolicy

PaddingPolicy controls post-metadata free space.

type ParseOption

type ParseOption func(*core.ParseOptions)

ParseOption configures Parse, ParseFile, and OpenSource.

func WithLimits

func WithLimits(l Limits) ParseOption

WithLimits sets the bounded-allocation and recursion limits for untrusted input. A zero field uses the default bound for that field (it is not unlimited): a partial Limits{MaxDepth: 8} keeps the default MaxAllocBytes and MaxElements rather than dropping them to zero, which would reject every allocation (or, for MaxElements, silently disable the element-count cap).

func WithMaxSourceBytes

func WithMaxSourceBytes(n int64) ParseOption

WithMaxSourceBytes bounds how many bytes OpenSource buffers from a non-seekable stream before parsing. A stream that exceeds n fails with waxerr.ErrSizeTooLarge instead of exhausting memory as an endless stream is spooled. n <= 0 disables the cap, restoring the unbounded read. The default when the option is not supplied is DefaultMaxSourceBytes (2 GiB). It has no effect on Parse or ParseFile, which read from a sized source and never buffer a stream.

func WithSourceName

func WithSourceName(name string) ParseOption

WithSourceName sets the display name used for the source in the "could not identify" diagnostics, so a caller that parses buffered or temp-file bytes (e.g. standard input) reports the original name instead of the temp path. It is display-only: detection sniffs bytes, never names or extensions. The option only affects the unidentified-format error; source identity and save-back checks are unchanged. Without it the name falls back to the path argument, or "" for Parse.

type Picture

type Picture = core.Picture

Picture is an embedded image.

type PictureType

type PictureType = core.PictureType

PictureType is a cover-art role.

type Plan

type Plan struct {
	// contains filtered or unexported fields
}

Plan is a resolved, ready-to-execute write produced by Editor.Prepare. It owns the byte-level rewrite and its report together, so Plan.Report is exactly what Plan.Execute will carry out - the two cannot drift.

Plan.Execute may mutate the Plan (a committed SaveBack is recorded so further writes on it are refused), so a single Plan is not safe for concurrent Execute calls; prepare a fresh Plan per goroutine. Plan.Report/Plan.Changes and the rest of the read surface are pure and need no such care.

func (*Plan) Changes

func (p *Plan) Changes() []tag.Change

Changes reports the field-level delta this plan will apply: each canonical key added, removed, or changed, plus picture, chapter, and synced-lyrics count-deltas when those sets differ. It diffs the pre-edit tags against the plan's post-codec-projection result - what the write actually lands, including date and number normalization - so the preview matches reality and a no-op plan yields no changes. It performs no I/O.

func (*Plan) Execute

func (p *Plan) Execute(ctx context.Context, dst Destination) (*Document, SaveResult, error)

Execute carries out the plan against dst, one of SaveBack, SaveAsFile, or WriteTo. It returns the post-write Document and a SaveResult; on error, the SaveResult still carries what is known (e.g. Committed=false).

A plan may be executed more than once as long as no execution writes over its own source file. Repeated WriteTo or SaveAsFile runs to other paths are valid while the source bytes stay stable, or while a stable source is passed to WriteTo. Once an execution writes over the source, [Execute] refuses later runs; re-edit the returned Document to write again.

func (*Plan) IsNoOp

func (p *Plan) IsNoOp() bool

IsNoOp reports whether the plan would not change the file's bytes. A no-op SaveBack writes nothing; a no-op SaveAsFile or WriteTo still produces a complete output (a fresh destination must be whole). An uninitialized plan is not a no-op because it cannot be executed.

func (*Plan) Report

func (p *Plan) Report() WriteReport

Report describes what executing the plan will do: the operations, the before/after sizes, the padding to be written, and any warnings. It performs no I/O. An uninitialized plan reports the empty WriteReport.

It returns a defensive copy: the Operations and Warnings slices (the only reference types in a report) are cloned, and the warnings are run through the deep core.CloneWarnings so a caller mutating a returned warning's Keys cannot reach back into the plan's own report. Without this a consumer iterating Report().Warnings and editing Keys would corrupt a later Report() of the same plan.

func (*Plan) String

func (p *Plan) String() string

String renders the full human-readable preview of the plan: the field-level changes block (each line through the sanitizing tag.Change.String) followed by the WriteReport body (operations, size, padding, warnings). It is the complete preview a library consumer prints with fmt.Println(plan) - safe to send to a terminal by construction, since the only untrusted values (the change values) are sanitized. It carries no path header (that is CLI display context) and no trailing newline; a no-op plan renders just the report's "no changes" line.

type Properties

type Properties = core.Properties

Properties describes the audio stream(s).

type ReaderAtSized

type ReaderAtSized = core.ReaderAtSized

ReaderAtSized is the internal source contract: random access plus size.

func BytesSource

func BytesSource(b []byte) ReaderAtSized

BytesSource returns a ReaderAtSized backed by b (which must not be mutated while in use). It is handy for parsing or writing in-memory data.

type SaveResult

type SaveResult struct {
	Committed bool
	Dest      Identity
	Doc       *Document
}

SaveResult reports the outcome of a save. Committed is true once the new bytes are in place (the rename succeeded); a later directory-fsync error is still returned, but with Committed true. Dest is the resulting file's identity, and Doc is the post-write document (also returned directly).

type Scope

type Scope = core.Scope

Scope annotates the target a value applies to.

type Snapshot

type Snapshot struct {
	Format       Format
	Fields       tag.Tags
	Properties   Properties
	PictureCount int
	Warnings     []Warning
}

Snapshot is the lightweight result of Document.Inspect: typed fields and properties for bulk scanning, deliberately omitting picture bytes and the native document.

type Source

type Source struct {
	// contains filtered or unexported fields
}

Source retains the complete bytes of a non-seekable stream that was parsed for editing. Unlike a Document (which is detached and holds nothing), a Source is closable: it owns the teed buffer. Edit the Source.Document and save it; the Source supplies the original bytes the rewrite copies.

func OpenSource

func OpenSource(ctx context.Context, r io.Reader, opts ...ParseOption) (*Source, error)

OpenSource parses a non-seekable stream, teeing the complete stream into memory as it reads (you cannot spool bytes after they have passed). The returned Source is closable and its Document can be edited and saved.

func (*Source) Close

func (s *Source) Close() error

Close releases the retained buffer. After Close the Document can still be read, but saving it requires supplying a source explicitly.

func (*Source) Document

func (s *Source) Document() *Document

Document returns the parsed document. It remains valid after Close (it is detached); only the Source's role as a write source ends at Close.

type SyncedLine

type SyncedLine = core.SyncedLine

SyncedLine is one timed lyric line (Time, Text) within a SyncedLyrics set.

func ParseLRC

func ParseLRC(text string) []SyncedLine

ParseLRC parses an LRC document into timed lyric lines, applying the foobar2000 [offset:] convention (effective timestamp = timestamp - offset) and skipping metadata tags such as [ar:], [ti:], [al:], and [length:]. A line with several leading time tags yields one SyncedLine per tag; lines are returned sorted by timestamp. This is the parser behind the FLAC/Ogg SYNCEDLYRICS store and a convenience for building a SyncedLyrics from an LRC file. LRC has no per-set language field; set it on SyncedLyrics yourself when the destination can store it.

func ParseLRCFull

func ParseLRCFull(text string) []SyncedLine

ParseLRCFull is ParseLRC without the per-set line cap, for trusted input already held whole in memory (such as a user-provided LRC file), so a downstream write-time cap is the single truncation-and-warning point rather than a silent drop at read. Parsing untrusted media keeps using the capped ParseLRC. The returned slice is bounded by the input size.

func ParseLRCReportFull

func ParseLRCReportFull(text string) (lines []SyncedLine, droppedLines []int)

ParseLRCReportFull is ParseLRCFull plus the 1-based line numbers of input lines the parser dropped silently - non-blank lines that produced no timed lyric and are not recognized LRC structure (a malformed timestamp, or plain untimed text). Blank lines, ID metadata tags, [offset:]/[length:] tags, and bare [section] headers are recognized structure and are not reported. The CLI uses it to warn (and fail --strict) when a --synced-lyrics-file drops lines rather than storing them silently.

type SyncedLyrics

type SyncedLyrics = core.SyncedLyrics

SyncedLyrics is one timed-lyrics set (Language, Description, Lines).

type TransferItem

type TransferItem = core.TransferItem

TransferItem is one piece of metadata's fate in a transfer.

type TransferKind

type TransferKind = core.TransferKind

TransferKind names a transferred item's category (field/picture/chapter).

type TransferReport

type TransferReport = core.TransferReport

TransferReport describes a cross-format metadata copy: what each field, picture set, and chapter set would carry, downgrade, or lose.

type Warning

type Warning = core.Warning

Warning is a coded non-fatal note from parse or planning.

type WarningCode

type WarningCode = core.WarningCode

WarningCode categorizes a Warning.

type WriteOption

type WriteOption func(*core.WriteOptions)

WriteOption configures Prepare and the save destinations.

var (
	// Preserve is the default: keep legacy containers, reuse padding in place.
	Preserve WriteOption = func(o *core.WriteOptions) {
		o.Legacy = core.LegacyPreserve
		o.Padding = core.DefaultPadding
	}
	// Compatible favors maximum reader compatibility.
	Compatible WriteOption = func(o *core.WriteOptions) {
		o.Legacy = core.LegacyPreserve
		o.Padding = core.PaddingPolicy{Target: 4096, Max: 1 << 20, ReuseInPlace: true}
		o.PaddingExplicit = true
	}
	// Minimal writes the smallest reasonable file: no padding, strip legacy.
	Minimal WriteOption = func(o *core.WriteOptions) {
		o.Legacy = core.LegacyStrip
		o.Padding = core.PaddingPolicy{Target: 0, Max: 0}
		o.PaddingExplicit = true
	}
)

Policy presets bundle write options into a named intent. Apply one first, then override individual options if needed:

plan, _:= ed.Prepare(waxlabel.Preserve, waxlabel.WithVerifyEssence())

func WithAllowUnsupportedDrop

func WithAllowUnsupportedDrop() WriteOption

WithAllowUnsupportedDrop makes Editor.Prepare drop a whole structural edit the destination format cannot store at all - authored synced lyrics or chapters on a format that has no such store, or cover art on a WebM file - with a warning, rather than failing the write. It also drops just the individual covers whose image format the destination stores pictures but cannot label (a GIF added to an MP4, which labels only JPEG/PNG/BMP), keeping any it can, so a PNG added alongside survives. It mirrors how a cross-format copy silently drops what the destination cannot hold, so a set that combines a storable edit with an unstorable one still applies the storable part and succeeds. By default such an edit is a hard error. The CLI passes this for set and plan; --strict promotes the resulting drop warning back to a failure.

func WithID3MultiValue

func WithID3MultiValue(p ID3MultiValuePolicy) WriteOption

WithID3MultiValue selects how multiple values for one field are stored in an ID3v2.3 tag, which has no standard multi-value text form. ID3v2.4 always NUL-separates regardless; the v2.3 compatibility impact is flagged in the write report.

func WithLegacyPolicy

func WithLegacyPolicy(p LegacyPolicy) WriteOption

WithLegacyPolicy sets how legacy/foreign tag containers are handled.

func WithNumericGenre

func WithNumericGenre() WriteOption

WithNumericGenre writes a recognized genre as its numeric reference (in formats that support one, such as ID3's TCON) instead of the name. By default the canonical name is written.

func WithPadding

func WithPadding(p PaddingPolicy) WriteOption

WithPadding sets the post-metadata padding policy for writes. It marks the policy as an explicit request, so a padding-only change is still realized when no tag, picture, or legacy edit is pending.

func WithPreserveModTime

func WithPreserveModTime() WriteOption

WithPreserveModTime keeps the file's modification time across a save-back (by default the mtime is updated so scanners notice the edit).

func WithStripEncoderStamp

func WithStripEncoderStamp() WriteOption

WithStripEncoderStamp asks the writer to drop a removable inherited transcoder/encoder stamp that lives in a native field no canonical-tag edit can reach: the WAV ISFT INFO item (e.g. ffmpeg's "Lavf...") and the Ogg/Opus/FLAC comment-header vendor string. Each is acted on only when it [IsTranscoderStamp]. The ISFT item is dropped; the vendor string is a mandatory codec field, so it is rewritten to WaxLabel's neutral value rather than removed (NeutralizeVendor). Pair it with clearing or setting tag.Encoder so a canonical ENCODER stamp does not survive.

func WithUnrecognizedPictures

func WithUnrecognizedPictures() WriteOption

WithUnrecognizedPictures allows a picture whose bytes IsRecognizedImage does not recognize (an empty payload, junk, or a deliberately exotic HEIC/AVIF/JXL cover the header sniff cannot identify) to be embedded by Editor.Prepare. By default such a picture is rejected (waxerr.ErrInvalidData) so a direct library caller cannot silently embed an application/octet-stream picture; pass this to opt a known-exotic cover back in. Only pictures added via Editor.AddPicture are validated - a file's pre-existing picture carried through a tags-only edit is never affected.

func WithVerifyEssence

func WithVerifyEssence() WriteOption

WithVerifyEssence hashes the audio bytes while a rewrite copies them and compares the written output with that copy stream. It is a copy-consistency check: it proves the output matches the bytes read during the write, not a separate parse-time baseline. For SaveBack and SaveAsFile it re-reads the temporary output before commit, which checks the copy path through the page cache. A streaming WriteTo cannot be re-read, so it verifies only the bytes copied from the source. The CLI parses each file immediately before writing, so its source extent is fresh.

func WithWebMSubset

func WithWebMSubset() WriteOption

WithWebMSubset narrows a file-less Matroska CapabilitiesFor query to the WebM subset, which excludes cover-art attachments - so the format-level answer for "webm" reports picture write as unsupported, matching what Document.Capabilities reports for a parsed.webm file. It affects only the Matroska capability query; every other codec ignores it, and it has no effect on a write.

type WriteReport

type WriteReport = core.WriteReport

WriteReport describes a planned write.

Directories

Path Synopsis
cmd
waxlabel command
Command waxlabel is the command-line interface to the WaxLabel audio-metadata library.
Command waxlabel is the command-line interface to the WaxLabel audio-metadata library.
internal
aac
Package aac implements reading and writing raw-AAC (ADTS) metadata for the public waxlabel package.
Package aac implements reading and writing raw-AAC (ADTS) metadata for the public waxlabel package.
aiff
Package aiff implements reading and writing AIFF / AIFF-C metadata for the public waxlabel package.
Package aiff implements reading and writing AIFF / AIFF-C metadata for the public waxlabel package.
ape
Package ape implements read-only parsing of APEv1/APEv2 tags for internal codecs.
Package ape implements read-only parsing of APEv1/APEv2 tags for internal codecs.
bits
Package bits holds WaxLabel's low-level byte utilities: a bounded, sticky-error Cursor over an io.ReaderAt; a segment-based rewrite executor; the non-reflected Ogg CRC; image sniffing; and content hashing.
Package bits holds WaxLabel's low-level byte utilities: a bounded, sticky-error Cursor over an io.ReaderAt; a segment-based rewrite executor; the non-reflected Ogg CRC; image sniffing; and content hashing.
core
Package core holds the value types shared between the public waxlabel package and the internal codecs, plus the Codec contract itself.
Package core holds the value types shared between the public waxlabel package and the internal codecs, plus the Codec contract itself.
flac
Package flac implements reading and writing FLAC metadata for the public waxlabel package.
Package flac implements reading and writing FLAC metadata for the public waxlabel package.
id3
Package id3 implements reading and writing ID3v2 tags (v2.2, v2.3, v2.4) and reading ID3v1.
Package id3 implements reading and writing ID3v2 tags (v2.2, v2.3, v2.4) and reading ID3v1.
iff
Package iff walks the top-level chunk structure shared by the RIFF (WAV) and IFF (AIFF) container families: a 12-byte header, then a sequence of [4-byte id][4-byte size][body][optional pad] chunks.
Package iff walks the top-level chunk structure shared by the RIFF (WAV) and IFF (AIFF) container families: a 12-byte header, then a sequence of [4-byte id][4-byte size][body][optional pad] chunks.
mapping
Package mapping translates between canonical [tag.Key]s and native tag names.
Package mapping translates between canonical [tag.Key]s and native tag names.
matroska
Package matroska implements reading and tag-writing Matroska / WebM (.mka / .webm / .mkv) metadata.
Package matroska implements reading and tag-writing Matroska / WebM (.mka / .webm / .mkv) metadata.
mp3
Package mp3 implements reading and writing MP3 (MPEG audio) metadata for the public waxlabel package.
Package mp3 implements reading and writing MP3 (MPEG audio) metadata for the public waxlabel package.
mp4
Package mp4 implements reading and writing MP4 / iTunes (M4A) metadata for the public waxlabel package.
Package mp4 implements reading and writing MP4 / iTunes (M4A) metadata for the public waxlabel package.
ogg
Package ogg implements reading and writing metadata for Ogg Vorbis and Ogg Opus for the public waxlabel package.
Package ogg implements reading and writing metadata for Ogg Vorbis and Ogg Opus for the public waxlabel package.
vorbis
Package vorbis implements the byte-level Vorbis comment list codec, the FLAC-style PICTURE block codec, and the canonical projection / minimal-change rebuild shared by every format that stores tags as Vorbis comments - FLAC and Ogg Vorbis/Opus.
Package vorbis implements the byte-level Vorbis comment list codec, the FLAC-style PICTURE block codec, and the canonical projection / minimal-change rebuild shared by every format that stores tags as Vorbis comments - FLAC and Ogg Vorbis/Opus.
wav
Package wav implements reading and writing WAV (RIFF/WAVE) metadata for the public waxlabel package.
Package wav implements reading and writing WAV (RIFF/WAVE) metadata for the public waxlabel package.
Package tag defines WaxLabel's canonical, format-neutral tag model: the validated Key vocabulary, the presence-aware TagSet/TagPatch that are authoritative for editing, the typed Tags projection used for convenient reads and sugar writes, and Merge.
Package tag defines WaxLabel's canonical, format-neutral tag model: the validated Key vocabulary, the presence-aware TagSet/TagPatch that are authoritative for editing, the typed Tags projection used for convenient reads and sugar writes, and Merge.
Package waxerr defines the sentinel errors shared across WaxLabel.
Package waxerr defines the sentinel errors shared across WaxLabel.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL