meta

package
v0.0.0-...-9f03692 Latest Latest
Warning

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

Go to latest
Published: Jul 23, 2026 License: MIT Imports: 16 Imported by: 0

Documentation

Overview

Package meta reads catalog metadata, cheap audio properties, and the tag-independent audio-essence hash without decoding PCM. It adapts WaxLabel, which parses tags, artwork, chapters, lyrics, and essence digests for supported containers. PCM decoding belongs to the analyze pass.

Index

Constants

View Source
const BookSeriesTagKey = string(tag.Grouping)

BookSeriesTagKey is the single tag that carries a book's series and sequence. The scanner splits it back into a series name and sequence (parseSeries); PackSeriesGrouping is the inverse that builds the value an edit writes.

Variables

This section is empty.

Functions

func BookFieldTagKeys

func BookFieldTagKeys(field string) ([]string, bool)

BookFieldTagKeys returns the on-disk tag keys the audiobook scanner reads back for a book metadata field, and whether the field round-trips through a tag at all. A field with no keys is DB-only by design; see bookFieldTagKeys. series is handled through BookSeriesTagKey, not here.

func EntityFieldTagKey

func EntityFieldTagKey(entityType model.MergeEntity, field string) (string, bool)

EntityFieldTagKey returns the on-disk tag key an entity-curation field fans out to across the entity's member files, and whether the field has one. Only the fields that round-trip through a rescan WITHOUT disturbing the entity's identity are wired: an album's non-identity release identifiers and sort (BARCODE, LABEL, CATALOGNUMBER, ALBUMSORT) and an artist's sort (ARTISTSORT).

An entity MBID is deliberately NOT fanned (neither album nor artist): a member track's MusicBrainz ID is the entity's identity key on the next scan (AlbumKey/artist match are mbid-first), but the entity edit updates only the mbid column, not the row's match_key, so writing the MBID to the files would re-key the entity to a fresh row and orphan its curation and locks. Barcode/label/catalog#/sort are not identity inputs, so they fan safely. A release-group field and a release-group type also stay DB-only.

func LRCPartial

func LRCPartial(lines []model.SyncedLine, dropped []int) bool

LRCPartial reports whether a .lrc parse is worth acting on, meaning some lines were timed and some were dropped.

The guard on len(lines) > 0 carries the weight. A fully untimed .lrc reports every line as dropped, and that is the ordinary case of a plain-text file next to the audio rather than a broken sidecar. Reporting it would flag a great many normal files.

func PackSeriesGrouping

func PackSeriesGrouping(name, seq string) string

PackSeriesGrouping builds the GROUPING tag value that carries a book's series name and sequence, the inverse of parseSeries: it uses the "#" marker (the shortest form parseSeries recognizes) so a written value re-reads to the same name and sequence on a rescan. An empty name yields an empty value (clearing the tag); an empty sequence yields the bare name.

func ParseCue

func ParseCue(text string) []model.Chapter

ParseCue parses a .cue sheet into file-relative navigation chapters, one per TRACK. It returns nil when the sheet has no usable tracks. A book with no embedded chapters uses these, marked source='cue' so embedded chapters stay authoritative.

func ParseLRC

func ParseLRC(text string) (lines []model.SyncedLine, dropped []int)

ParseLRC parses .lrc sidecar text into WaxBin synced lines (millisecond offsets, time-ordered), plus the 1-based numbers of the lines the parser dropped: non-blank lines that yielded no timed lyric and are not recognized LRC structure, meaning a malformed timestamp or plain untimed text. Blank lines, [ar:]/[ti:] metadata, [offset:], and bare [section] headers count as structure and are not reported.

It returns nil lines for text with no timed lines.

The dropped list is why this reads through the reporting parser at all: it is the only way to tell a partly-broken sidecar from a plain-text one. It uses the uncapped variant because WaxBin reads the whole file into memory before parsing, so the parser's own line cap never protected anything. The read is bounded instead (see maxSidecarBytes).

func RoleTagKey

func RoleTagKey(role model.ContributorRole) (string, bool)

RoleTagKey returns the canonical WaxLabel tag key an on-disk write uses for a contributor role, and whether the role has one wired for write-back.

func SplitCredits

func SplitCredits(s string) []string

SplitCredits splits a combined credit string (authors or narrators) into trimmed individual names. It delegates to identity.SplitCredits, the canonical shared splitter, and stays here for the adapter/scanner callers that reference meta.SplitCredits.

func TagKeyForField

func TagKeyForField(field string) (string, bool)

TagKeyForField returns the canonical WaxLabel tag key an on-disk write uses for a catalog field name, and whether the field has one.

Types

type Adapter

type Adapter struct{}

Adapter reads metadata through WaxLabel. One open file feeds tag parsing, stream properties, and the tag-independent audio-essence digest.

func NewReader

func NewReader() *Adapter

NewReader returns the WaxLabel-backed metadata reader.

func (*Adapter) Read

func (a *Adapter) Read(ctx context.Context, path string) (*FileMeta, error)

Read parses path with WaxLabel and returns its tags, properties, and essence hash. It never decodes PCM. A parse error is returned; a file that parses but carries no hashable essence yields a populated FileMeta with an empty EssenceHash (the scanner falls back to the content hash).

type CueSheet

type CueSheet struct {
	Title     string // album title (a TITLE line before the first TRACK)
	Performer string // album artist (a PERFORMER line before the first TRACK)
	Genre     string // REM GENRE
	Year      int    // REM DATE (leading four-digit year)
	Tracks    []CueTrack
}

CueSheet is a parsed .cue sheet describing a single backing audio file: the album-level fields declared before the first TRACK, plus one entry per TRACK. Two readers consume it: ParseCue derives book-navigation chapters from the tracks, and the scanner carves a single-file album rip into virtual tracks (each TRACK its own item and offset window). WaxBin reads .cue sidecars directly (WaxLabel has no cue parser).

func ParseCueSheet

func ParseCueSheet(text string) *CueSheet

ParseCueSheet parses a .cue sheet into its album-level fields and per-track entries. It returns nil when the sheet declares no TRACK, so a caller can treat an empty or unparseable sheet the same as an absent one.

func (*CueSheet) Chapters

func (s *CueSheet) Chapters() []model.Chapter

Chapters projects a cue sheet's tracks into file-relative navigation chapters, one per usable TRACK, using each track's INDEX 01 as the start and its TITLE as the label. End offsets are left open (0) for the book read path to fill from the next chapter's start.

A track with no usable INDEX 01 is dropped rather than anchored at 0. A chapter's start is not only its own coordinate: the read path fills each open end from the next chapter's start, so one fabricated 0 would report the chapter before it as ending at the start of the book.

func (*CueSheet) UsableTracks

func (s *CueSheet) UsableTracks() []CueTrack

UsableTracks returns the tracks that declared a parseable INDEX 01, in sheet order. A track without one has no start offset, and the start offset is what every consumer here anchors on, so admitting it at a fabricated 0 does damage in both directions: it claims the head of the file for itself, and it truncates the track or chapter before it, whose end is read off the next one's start.

Callers that must report the drop read Tracks and filter on StartValid themselves; the scanner does, so the sheet's own diagnostics name what was skipped.

type CueTrack

type CueTrack struct {
	Number      int
	Title       string
	Performer   string
	StartFrames int64
	StartValid  bool
}

CueTrack is one TRACK of a cue sheet: its declared number, title, and performer (the track's own PERFORMER, empty when it inherits the album performer), plus its INDEX 01 start offset in CD frames.

StartValid reports that the track declared an INDEX 01 and that it parsed. A track without a usable one has no start at all, yet StartFrames still reads 0, and 0 is a real offset naming the file's first sample rather than an absent one.

Consumers should read UsableTracks, which drops those tracks already, or check StartValid themselves. Letting one default to 0 does damage in two directions, because no consumer here reads a start in isolation: the scanner carves a content window from it, and a book chapter's end is read off the next chapter's start, so a fabricated 0 misplaces its own track and truncates the one before it.

type DerivedSortPair

type DerivedSortPair struct {
	Field     string
	SortField string
	TagKey    string
}

DerivedSortPair names a display field whose edit regenerates a stored sort key: the catalog sort field that can carry an explicit value or a lock (empty for artist, whose sort has no edit surface of its own) and the on-disk tag a scan's derivation reads. The write-back uses it to clear the stale sort tag beside a display-name edit; a tag left behind would feed the next derivation and undo the regenerated sort. See Library.writeBackFields.

func DerivedSortPairs

func DerivedSortPairs() []DerivedSortPair

DerivedSortPairs returns the display-field to derived-sort correspondences, in a fixed order.

type FileMeta

type FileMeta struct {
	Tags        model.Tags
	EssenceHash string
	// Lyrics is the file's embedded lyrics (unsynced USLT and/or synced SYLT), or
	// nil when it carries none. A sibling .lrc sidecar, parsed by the scanner, takes
	// precedence over this.
	Lyrics *model.Lyrics
	// CoverArt is the file's embedded front-cover image (raw bytes + format), or nil
	// when it embeds none. The scanner finalizes its hash and dimensions and falls
	// back to a directory cover image when this is absent.
	CoverArt *model.ArtImage
	// ItemPIDHint is the value of the file's WAXBIN_ITEM_PID tag, if present. It is a
	// rebuild-only hint for restoring the backing item's original PID; identity stays
	// essence-first (the tag is copyable), so the store adopts it only when unambiguous.
	ItemPIDHint string
	// Diagnostics are observations about the file worth persisting (an unsupported
	// container, truncated audio, a legacy-only tag fallback). They are born here,
	// like Lyrics, rather than in the scanner from an os.Stat like an AuxObservation,
	// so they share only the model-input-to-store leg of the route with those.
	//
	// FilePID and DisplayPath are left unset: the reader is given a path, not a
	// catalog identity. The store fills them.
	Diagnostics []model.FileDiagnostic
}

FileMeta is everything the scanner needs from one parse: the tag/property set plus the tag-independent essence hash. EssenceHash is empty when the container carries no hashable audio essence (e.g. a malformed file); the scanner then falls back to the content hash so the file is still cataloged.

type PictureEdit

type PictureEdit struct {
	Clear bool
	Data  []byte
}

PictureEdit is a change to a file's embedded front cover. Clear removes the front cover; otherwise Data (raw image bytes) is embedded as the front cover, replacing any existing one. Either way only the front cover is touched. Other embedded pictures such as a back cover or booklet scan are left intact, matching the catalog's "art" field, which models the front cover alone.

type Reader

type Reader interface {
	Read(ctx context.Context, path string) (*FileMeta, error)
}

Reader reads tags, properties, and the essence hash from a file. It must never decode PCM (scanning is I/O-bound by contract).

type TagEdit

type TagEdit struct {
	Key    string
	Values []string
}

TagEdit is one set-or-clear of a tag key. Values sets the key (replacing any existing values); an empty/nil Values clears it. Key is a canonical tag key name (e.g. "ALBUMARTIST", "REPLAYGAIN_TRACK_GAIN", "R128_TRACK_GAIN", a custom "WAXBIN_ITEM_PID"); an unknown key round-trips as a native custom field.

type WriteResult

type WriteResult struct {
	Changed     bool
	Size        int64
	MTimeNS     int64
	ContentHash string
	Warnings    []model.TagWriteWarning
}

WriteResult reports the file's on-disk state after a write. When Changed is false the edits were a no-op and the file was not rewritten (Size/MTimeNS/ContentHash are left zero, since the caller already holds the current values).

Warnings is independent of Changed: a no-op can still carry a warning, so a caller that branches on Changed alone would miss exactly the case where the edit had no effect because the format could not store the value.

type Writer

type Writer struct{}

Writer applies tag edits to files on disk through WaxLabel. It builds on WaxLabel's file-path parse (ParseFile), which carries the source identity SaveBack needs, and enables essence verification so a tag-only write can never silently alter the audio: the rewrite re-hashes the copied audio bytes and refuses to commit if they changed. WaxBin uses WaxLabel read-only everywhere else; this is the one writer seam.

func NewWriter

func NewWriter() *Writer

NewWriter returns a WaxLabel-backed tag writer.

func (*Writer) Apply

func (w *Writer) Apply(ctx context.Context, path string, edits []TagEdit) (*WriteResult, error)

Apply writes edits to the file at path atomically and in place, preserving the audio essence (WithVerifyEssence). It returns the new size, mtime, and content hash so the caller can update the catalog's file row through the optimistic file-state seam. A no-op edit set reports Changed=false without rewriting.

func (*Writer) ApplyPicture

func (w *Writer) ApplyPicture(ctx context.Context, path string, edit PictureEdit) (*WriteResult, error)

ApplyPicture embeds (or clears) the front-cover picture on the file at path, preserving the audio essence exactly as Apply does (WithVerifyEssence re-hashes the copied audio and refuses to commit if it changed). A format that cannot store a picture, or a read-only format, is refused with CodeUnsupported so the caller records a write-back failure rather than silently dropping the cover. A no-op reports Changed=false without rewriting: this covers clearing a file that has no pictures and re-embedding the bytes it already carries.

Jump to

Keyboard shortcuts

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