tui

package
v0.1.1 Latest Latest
Warning

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

Go to latest
Published: Oct 9, 2026 License: MIT Imports: 34 Imported by: 0

Documentation

Overview

Package tui is the terminal interface: the result card printed after a recognition, the interactive explorer (audd browse), and the now-playing screen. Every screen also has a plain, non-interactive output so it works when piped.

Integration hooks, assigned where the stream store, results cache, and jobs store are wired in:

  • NewFeed must return a StoreFeed backed by internal/streamstore and streams.RecentResults. The default only lists stations, so now-playing reports that stream results are not available.
  • NewExplorerData must extend DefaultExplorerData with Recent (cache.Recent) and Jobs/JobItems (jobs.List and the job's results).

Index

Constants

View Source
const HeartbeatWindow = 2 * time.Minute

HeartbeatWindow is how old the newest stored play may be, while the recorder is not running, before readers also ask the recent-results endpoint. The recorder writes a heartbeat every 30 seconds.

Variables

View Source
var NewExplorerData = DefaultExplorerData

NewExplorerData builds the explorer's sources for an invocation. The default covers streams (NewFeed), stream management (API), and usage (account); the results cache and the jobs store are added where those packages are wired in.

View Source
var NewFeed = func(a *app.App) (Feed, error) {
	return FeedFuncs{StationsFunc: func(ctx context.Context) ([]Station, error) { return APIStations(ctx, a) }}, nil
}

NewFeed builds the Feed for an invocation. The default lists streams with the API but has no plays; the integration replaces it with a StoreFeed over the local stream store and the recent-results endpoint (see the package comment).

Functions

func RenderResultCard

func RenderResultCard(w io.Writer, a *app.App, r app.ResultView, details bool)

RenderResultCard prints a recognition result for people: cover art next to title, artist, album · label · year, and the song link. details adds ISRC, UPC, score, timecode, and every metadata provider's link. Art is drawn only on a terminal, with color on, and not when r.NoArt is set or AUDD_NO_ART / AUDD_ART=none turn it off.

func RunExplorer

func RunExplorer(ctx context.Context, a *app.App, tab string) error

RunExplorer opens the interactive explorer on a tab: recent, jobs, streams, or usage ("jobs/<id>" opens one job; "streams/<id>,<id>" shows only those streams). Without a terminal it prints the tab's data as JSON instead.

func RunNowPlaying

func RunNowPlaying(a *app.App, radioIDs []int, opts NowPlayingOptions) error

RunNowPlaying shows what is playing on the given streams (all streams when radioIDs is empty). On a terminal it opens the full-screen view; piped, it prints a JSONL line per song change; with Once it prints the current songs and exits.

func Tabs

func Tabs() []string

Tabs lists the explorer tab names accepted by RunExplorer.

func ViewFromJSON

func ViewFromJSON(raw json.RawMessage) (v app.ResultView, trackLength time.Duration, ok bool)

ViewFromJSON flattens one AudD song object (a recognition result, a stream callback song, or a cached result) into a ResultView. Parsing is lenient: missing or wrong-typed fields are left empty. It also returns the track length when Apple Music (or Spotify or Deezer) metadata carries it. ok is false when raw is not an object or has neither artist nor title.

func ViewJSON

func ViewJSON(v app.ResultView) map[string]any

ViewJSON is the snake_case form of a ResultView for JSON output when the original API object is not at hand.

Types

type ExplorerData

type ExplorerData struct {
	Recent   func(ctx context.Context, limit int) ([]RecentItem, error)
	Jobs     func(ctx context.Context) ([]JobRow, error)
	JobItems func(ctx context.Context, id string) ([]JobItem, error)
	Feed     Feed
	Usage    func(ctx context.Context, days int) (*account.Usage, error)

	// Stream management from the Streams tab.
	AddStream    func(ctx context.Context, url string, radioID int) error
	RemoveStream func(ctx context.Context, radioID int) error
	SetStreamURL func(ctx context.Context, radioID int, url string) error
}

ExplorerData supplies the explorer's tabs. A nil source shows a short note in its tab instead of data.

func DefaultExplorerData

func DefaultExplorerData(a *app.App) (ExplorerData, error)

DefaultExplorerData is the default NewExplorerData.

type Feed

type Feed interface {
	Stations(ctx context.Context) ([]Station, error)
	Plays(ctx context.Context, radioID int, limit int) ([]Play, error)
}

Feed supplies stream data to now-playing and the explorer's Streams tab. Stations lists the account's streams with their latest health; Plays returns the recent plays of one stream, newest first. Plays reads the local stream store (kept complete by the background recorder) and falls back to the recent-results endpoint (the last ~30 results per stream).

type FeedFuncs

type FeedFuncs struct {
	StationsFunc func(ctx context.Context) ([]Station, error)
	PlaysFunc    func(ctx context.Context, radioID int, limit int) ([]Play, error)
}

FeedFuncs adapts two functions to a Feed.

func (FeedFuncs) Plays

func (f FeedFuncs) Plays(ctx context.Context, radioID int, limit int) ([]Play, error)

func (FeedFuncs) Stations

func (f FeedFuncs) Stations(ctx context.Context) ([]Station, error)

type Health

type Health struct {
	Code    int       `json:"code"`
	Message string    `json:"message"`
	At      time.Time `json:"at"`
}

Health is a stream health notification (650: can't connect, 651: no music).

type JobItem

type JobItem struct {
	Index  int             `json:"index"`
	Input  string          `json:"input"`
	State  string          `json:"state"` // pending, done, failed
	Result json.RawMessage `json:"result,omitempty"`
	Err    string          `json:"error,omitempty"`
}

JobItem is one input of a batch job.

type JobRow

type JobRow struct {
	ID      string    `json:"id"`
	Command []string  `json:"command"`
	Created time.Time `json:"created"`
	Total   int       `json:"total"`
	Done    int       `json:"done"`
	Failed  int       `json:"failed"`
	Status  string    `json:"status"`
}

JobRow is a batch job.

type NowPlayingLine

type NowPlayingLine struct {
	app.ResultView
	RadioID int
	URL     string
	State   string
	Playing bool
	At      time.Time
	Elapsed time.Duration
	Played  time.Duration
	Ended   time.Time
	Ago     time.Duration
	Length  time.Duration
}

NowPlayingLine is the data a --format template sees: the song fields ({{.Artist}}, {{.Title}}, {{.Album}}, {{.Label}}, {{.ReleaseDate}}, {{.SongLink}}, {{.ISRC}}, {{.UPC}}), the stream ({{.RadioID}}, {{.URL}}), and the timing: {{.State}} (playing, just_played, or last_recognized), {{.Playing}}, {{.At}} (when the song started), {{.Elapsed}} (since it started, while it plays), {{.Played}} and {{.Ended}} (for results sent when the song ended), {{.Ago}} (since it ended, or since it started when the end is not known), and {{.Length}} (when the track length is known).

type NowPlayingOptions

type NowPlayingOptions struct {
	NoArt, Notify bool
	// Once prints the current song of each station and exits.
	Once bool
	// Format is a text/template over NowPlayingLine, printed per song
	// change (or once with Once), for status bars.
	Format string
	// Timeout stops the run after this long (0: run until interrupted).
	Timeout time.Duration
	// Interval is how often stream results are re-read (default 5 s).
	Interval time.Duration
	// Context cancels the run (Ctrl-C). Defaults to context.Background().
	Context context.Context
}

NowPlayingOptions configure audd now-playing.

type Play

type Play struct {
	RadioID int
	// At is when AudD detected the start of the song.
	At time.Time
	// PlayLength is how long the song had played when AudD reported it, in
	// seconds (from the callback's play_length).
	PlayLength int
	// TrackLength is the full song length, when metadata includes it.
	TrackLength time.Duration
	app.ResultView
	// Raw is the original song object from the API, when known.
	Raw json.RawMessage
}

Play is one song recognized on a stream.

func PlayFromRaw

func PlayFromRaw(radioID int, at time.Time, playLength int, raw json.RawMessage) Play

PlayFromRaw builds a Play from a stored or fetched song object.

func (Play) Ago

func (p Play) Ago(now time.Time) time.Duration

Ago is the time since the song was last heard: since it ended when the result says so, else since it started.

func (Play) Current

func (p Play) Current(now time.Time) bool

Current reports whether the song is probably still playing.

func (Play) Elapsed

func (p Play) Elapsed(now time.Time) time.Duration

Elapsed is the time since AudD detected the start of the song.

func (Play) Ended

func (p Play) Ended() (t time.Time, ok bool)

Ended reports when the song ended, for results delivered at the end of the song (they carry play_length). ok is false otherwise.

func (Play) JSON

func (p Play) JSON() map[string]any

JSON is the machine form of a play, shaped like a stream callback result.

func (Play) Played

func (p Play) Played() time.Duration

Played is how long the song played before AudD reported it (play_length).

func (Play) State

func (p Play) State(now time.Time) PlayState

State is what the result says about the song now. A result with play_length arrived when the song ended, so the song is over. A result without it arrived when the song started: the song plays for its track length when that is known (provider metadata), else for up to 10 minutes.

func (Play) Timing

func (p Play) Timing(now time.Time) map[string]any

Timing is the machine form of State: the state with the times that go with it. elapsed_seconds (since the song started) is set only while it plays; played_seconds and ended_at only for results delivered when the song ended; ago_seconds (since it ended, or since it started when the end is not known) only when it is not playing; length_seconds when the track length is known.

type PlayState

type PlayState string

PlayState says what a stream result tells about the song now.

const (
	// StatePlaying: the result arrived when the song started (a stream
	// added with --start) and the song is probably still on.
	StatePlaying PlayState = "playing"
	// StateJustPlayed: the result arrived when the song ended (the
	// default) a few minutes ago at most.
	StateJustPlayed PlayState = "just_played"
	// StateLastRecognized: the newest result is older than that.
	StateLastRecognized PlayState = "last_recognized"
)

type PlayStore

type PlayStore interface {
	// Plays returns up to limit plays of a stream, newest first.
	Plays(radioID int, limit int) ([]Play, error)
	// AddPlay saves a play; duplicates (same stream and timestamp) are ignored.
	AddPlay(p Play) error
}

PlayStore is the part of the local stream store the screens use.

type RecentItem

type RecentItem struct {
	Source string          `json:"source"` // file path or URL
	At     time.Time       `json:"at"`
	Result json.RawMessage `json:"result"` // the API result (object, or array for enterprise)
}

RecentItem is one result recognized through the CLI (results cache and batch jobs).

type Station

type Station struct {
	RadioID int     `json:"radio_id"`
	URL     string  `json:"url,omitempty"`
	Running bool    `json:"stream_running"`
	Health  *Health `json:"health,omitempty"` // latest health notification, if any
	// StatusUnknown is set when the stream list could not be read, so
	// whether the stream runs is not known.
	StatusUnknown bool `json:"-"`
}

Station is one monitored stream.

func APIStations

func APIStations(ctx context.Context, a *app.App) ([]Station, error)

APIStations lists the account's streams with getStreams, healing a rejected login token like every other API call.

func (Station) Down

func (s Station) Down() bool

Down reports whether the station is not delivering recognitions right now.

func (Station) HealthText

func (s Station) HealthText() string

HealthText describes a station's health for people.

type StoreFeed

type StoreFeed struct {
	StationsFunc func(ctx context.Context) ([]Station, error)
	Store        PlayStore
	// Recent fetches the recent-results endpoint for a stream.
	Recent func(ctx context.Context, radioID int) ([]Play, error)
	// RecorderRunning reports whether a recorder keeps a stream current
	// (it has written a heartbeat for the stream within HeartbeatWindow).
	RecorderRunning func(radioID int) bool
	Now             func() time.Time
	// contains filtered or unexported fields
}

StoreFeed reads plays from the local stream store first. It asks the recent-results endpoint (the last ~30 results of a stream) only when the store has nothing for the stream, or when no recorder keeps the stream current and the newest stored play is older than HeartbeatWindow; what it fetches is written into the store before it is shown.

func (*StoreFeed) Plays

func (f *StoreFeed) Plays(ctx context.Context, radioID int, limit int) ([]Play, error)

Plays implements Feed.

func (*StoreFeed) Stations

func (f *StoreFeed) Stations(ctx context.Context) ([]Station, error)

Jump to

Keyboard shortcuts

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