Documentation
¶
Overview ¶
Package api implements the HTTP/2 handlers for the v1 wire protocol (see PROTOCOL.md at the repo root).
v1 endpoints exposed by this package:
GET /v1/health — no auth, liveness / pairing probe
GET /v1/list — directory listing (authed)
GET /v1/stat — file metadata (authed)
GET /v1/read — byte-range read; Range header required (authed)
GET /v1/download — full-file streaming download (authed)
GET /v1/manifest — library track manifest (authed)
GET /v1/artwork/{mbid} — album artwork blob (authed)
GET /v1/artist-image/{mbid} — artist portrait by MusicBrainz ID (authed)
Pairing itself is handled by the admin console (see internal/admin), not by a /v1 endpoint — the iOS client posts the bearer token and pinned fingerprint to the admin host once, then talks to /v1 only.
Every response carries the X-Bridge-Protocol header. Authenticated endpoints run through the authed() middleware, which requires Authorization: Bearer <token> and validates via the auth.Store.
Package api — per-route slow-loris defence via `http.ResponseController.SetWriteDeadline`.
The LAN + tsnet `http.Server` instances run with `WriteTimeout: 0` (the documented `cmd/bridge/main.go` comment explains why — DSD downloads stream multi-GB files over slow links AND the SSE event stream is a long-lived connection). Without ANY write deadline, a slow-reading client on a bounded endpoint (POST /v1/upscale, GET /v1/health, etc.) can hold a server goroutine indefinitely while we wait for `Write` to drain.
Per-route `SetWriteDeadline` closes this gap: bounded routes get a 60 s budget (covers the slowest legitimate non-streaming response even over a Tailscale relay); streaming routes (`/v1/read`, `/v1/download`, `/v1/manifest`) and SSE routes (`/v1/events`, `/v1/pairing/.../events`) opt out via the `streamingRoute` kind.
The route registry below is the single source of truth. The `Handler()` body iterates it; a per-route classification test asserts every entry has an explicit kind (no zero-value ambiguity).
Package api — handler for DELETE /v1/upscale/variants.
Three query-param shapes:
DELETE /v1/upscale/variants?confirm=true → every variant DELETE /v1/upscale/variants?prefix=<rel-path> → variants under a path prefix DELETE /v1/upscale/variants?path=<rel-path> → variants for one exact source path
Bearer-token authed via s.authed(...) wrapper at route registration. Additive — no ProtocolVersion bump. Pre-feature bridges return 404 without the X-Bridge-Protocol header; iOS classifies that as `.notSupported` and hides any client UI behind the `deleteVariants` capability flag.
Response shape: 200 OK with {deletedCount, freedBytes, deletedPaths}. Errors:
- 400 bad_request: unscoped delete without `confirm=true`; malformed `prefix` / `path`; both `prefix` AND `path` set.
- 404 variant_not_found: feature unavailable on this bridge (no variant store wired). Same shape every other variant-not-found surfaces.
- 500 internal: SQLite read error during the list step; partial deletes are NOT rolled back (each (unlink, DeleteVariant) pair is idempotent — the `--gc` reverse sweep + integrity watcher reap any zombie state).
Package api — wire shape for the SSE `upscale.deleted` topic.
Mirror-PR pair invariant (see CLAUDE.md): this struct is the wire contract between bridge and iOS clients. JSON tag changes are breaking and require a `ProtocolVersion` bump + coordinated PR pair. Additive fields (with `omitempty`) are safe.
Package api — wire shape for the SSE `upscale.complete` topic.
Mirror-PR pair invariant (see CLAUDE.md): this struct is the wire contract between bridge and iOS clients. JSON tag changes are breaking and require a `ProtocolVersion` bump + coordinated PR pair. Additive fields (with `omitempty`) are safe.
Index ¶
- Variables
- func LoggerFromContext(ctx context.Context) *slog.Logger
- func RequestIDFromContext(ctx context.Context) string
- type AnalysisPoolStats
- type AnalysisRecord
- type AnalysisStats
- type AnalysisStatsProvider
- type AnalysisStore
- type ArtworkDirProvider
- type AtlasHarvestCredentialSink
- type AtlasMetaStore
- type BatchCoordinator
- type BatchInsufficientDiskSpace
- type BatchRequest
- type BatchRow
- type BatchSubmitResult
- type BatchThroughput
- type DeviceRegistrar
- type DiagnosticsResponse
- type Entry
- type ErrorResponse
- type EventPublisher
- type HealthResponse
- type HistoryStore
- type InflightDropper
- type MBIDProbe
- type ManifestPage
- type ManifestProvider
- type PairingStateEvent
- type PlaylistCoverStore
- type PlaylistStore
- type RendererDiscoverySnapshotter
- type RendererInfo
- type RenderersResponse
- type RootStatus
- type ScanState
- type Server
- func (s *Server) ConfigHolder() *config.RuntimeConfig
- func (s *Server) EventPublisher() EventPublisher
- func (s *Server) Handler() http.Handler
- func (s *Server) Resolver() *bridgefs.Resolver
- func (s *Server) RunVariantDelete(ctx context.Context, req VariantDeleteRequest) (VariantDeleteResponse, error)
- func (s *Server) SetTailscaleStatus(p admin.TailscaleProvider)
- func (s *Server) StartEventBroker() (stopFn func())
- func (s *Server) StartManifestRateLimitReaper() (stopFn func())
- func (s *Server) StartPairingRateLimitGC() (stopFn func())
- func (s *Server) StartedAt() time.Time
- func (s *Server) WithAnalysis(enabled bool, as AnalysisStore) *Server
- func (s *Server) WithAnalysisStats(p AnalysisStatsProvider) *Server
- func (s *Server) WithArtworkDirs(ad ArtworkDirProvider) *Server
- func (s *Server) WithAtlasHarvest(sink AtlasHarvestCredentialSink) *Server
- func (s *Server) WithAtlasMeta(enabled bool, ttl time.Duration, store AtlasMetaStore) *Server
- func (s *Server) WithBatchCoordinator(c BatchCoordinator) *Server
- func (s *Server) WithCarPlayOptimize(enabled bool) *Server
- func (s *Server) WithCertExpiry(notAfter time.Time) *Server
- func (s *Server) WithDLNA(enabled bool) *Server
- func (s *Server) WithDeviceRegistrar(r DeviceRegistrar) *Server
- func (s *Server) WithHistoryStore(hs HistoryStore) *Server
- func (s *Server) WithInflightDropper(d InflightDropper) *Server
- func (s *Server) WithLECertExpiry(provider func() time.Time) *Server
- func (s *Server) WithMBIDProbe(p MBIDProbe) *Server
- func (s *Server) WithPairing(p *pairing.Store) *Server
- func (s *Server) WithPlaylistCoverStore(st PlaylistCoverStore) *Server
- func (s *Server) WithPlaylistStore(ps PlaylistStore) *Server
- func (s *Server) WithRendererDiscovery(snap RendererDiscoverySnapshotter) *Server
- func (s *Server) WithSessionTracker(t SessionTracker) *Server
- func (s *Server) WithSmartPlaylistStore(st SmartPlaylistStore) *Server
- func (s *Server) WithTailscaleStatus(p admin.TailscaleProvider) *Server
- func (s *Server) WithUPnPHostResolver(r UPnPServerHostResolver) *Server
- func (s *Server) WithUPnPRouting(l UPnPRoutingLookup) *Server
- func (s *Server) WithUPnPUpstreamPublicProvider(p UPnPUpstreamPublicProvider) *Server
- func (s *Server) WithUpdater(u UpdaterStatus) *Server
- func (s *Server) WithUpscale(enabled bool, vs VariantStore) *Server
- func (s *Server) WithUpscaleEnqueuer(e UpscaleEnqueuer) *Server
- func (s *Server) WithUpscaleStats(p UpscaleStatsProvider) *Server
- func (s *Server) WithVariantDeleter(d VariantDeleter) *Server
- type SessionTracker
- type SmartPlaylistStore
- type StatResponse
- type UPnPRoutingLookup
- type UPnPServerHostResolver
- type UPnPUpstreamPublicProvider
- type UPnPUpstreamPublicServer
- type UpdateInfo
- type UpdaterStatus
- type UpscaleCompleteEvent
- type UpscaleDeletedEvent
- type UpscaleEnqueuer
- type UpscalePoolStats
- type UpscaleRequest
- type UpscaleResponse
- type UpscaleStats
- type UpscaleStatsProvider
- type VariantDeleteRequest
- type VariantDeleteResponse
- type VariantDeleter
- type VariantRecord
- type VariantStore
- type VariantSummary
Constants ¶
This section is empty.
Variables ¶
var Copy = io.Copy
Copy is exposed for test shims; wraps io.Copy so tests can swap it out. (Currently unused — kept as a seam for benchmarks.)
var ErrBatchInsufficientDiskSpace = errors.New("upscale batch insufficient disk space")
ErrBatchInsufficientDiskSpace is the sentinel for errors.Is checks.
var ErrUpscaleIneligible = errors.New("upscale source is ineligible")
ErrUpscaleIneligible is returned by EnqueueOne when the source is DSD, already above the target rate, or otherwise not a candidate for upscaling. Silent rejection — the user has no remediation, so we don't surface it in the wire response beyond the rejected count.
var ErrUpscaleQueueFull = errors.New("upscale queue is full")
ErrUpscaleQueueFull is the typed sentinel UpscaleEnqueuer returns when the underlying worker pool can't accept another job. The HTTP handler maps it to a 503 `queue_full` response. Defined in the api package so the handler can `errors.Is` without importing internal/transcode (which would invert the dependency direction — api defines abstractions, transcode is an implementation detail consumed via the adapter at cmd/bridge wiring).
var ErrUpscaleSourceMissing = errors.New("upscale source path missing on disk")
ErrUpscaleSourceMissing is returned by EnqueueOne when the resolved source path doesn't exist on disk. Distinguished from ErrUpscaleQueueFull so the handler can report it separately in `rejected` counts and so tests can assert the typed branch.
ErrVariantDeleteUnavailable is the sentinel error `RunVariantDelete` returns when the server has no `VariantDeleter` wired (i.e. the `deleteVariants` capability is off on this bridge). The HTTP handler maps this to 404 variant_not_found; admin maps it to 503 service_unavailable to match its existing "feature off" pattern.
Functions ¶
func LoggerFromContext ¶ added in v0.1.3
LoggerFromContext returns a slog logger pre-bound to the request_id (and method/path) for handlers that need to emit their own structured telemetry alongside the per-request line emitted by the logging middleware. Falls back to the unbound `http` component logger when no middleware has run, so callers don't have to nil-check the result.
func RequestIDFromContext ¶ added in v0.1.3
RequestIDFromContext returns the per-request ID established by the logging middleware, or "" if no middleware ran (e.g. tests that bypass the chain). Safe to call from any handler with the request's context.
Types ¶
type AnalysisPoolStats ¶ added in v0.1.6
type AnalysisPoolStats struct {
Workers int `json:"workers"`
QueueCap int `json:"queueCap"`
QueueLen int `json:"queueLen"`
Inflight int `json:"inflight"`
Enqueued uint64 `json:"enqueued"`
Done uint64 `json:"done"`
Failed uint64 `json:"failed"`
}
AnalysisPoolStats mirrors `analyze.PoolStats` field-for-field but lives here so the api package compiles without importing internal/analyze. The wiring closure in cmd/bridge translates.
type AnalysisRecord ¶ added in v0.1.6
type AnalysisRecord struct {
SourcePath string
WaveformPath string
WaveformTag string
SourceMTimeNS int64
SourceSize int64
}
AnalysisRecord is the minimum metadata the waveform handler needs: where the sidecar lives, its content tag (served as the ETag / used by iOS as the immutable-cache key), and the source freshness fields. SourcePath is the CANONICAL row value (case-preserved); the lookup resolves case-insensitively.
type AnalysisStats ¶ added in v0.1.6
type AnalysisStats struct {
Enabled bool `json:"enabled"`
SoxAvailable *bool `json:"soxAvailable,omitempty"`
Pool *AnalysisPoolStats `json:"pool,omitempty"`
CachedWaveforms int `json:"cachedWaveforms"`
CachedBytes int64 `json:"cachedBytes"`
}
AnalysisStats is the wire shape GET /v1/analysis/stats returns — field-for-field compatible with the admin tile, same as UpscaleStats.
- Enabled mirrors LIVE runtime state (pool != nil), not the persisted config flag.
- Pool is omitted when the feature is off.
- SoxAvailable is omitted when no precheck closure was wired.
type AnalysisStatsProvider ¶ added in v0.1.6
type AnalysisStatsProvider interface {
AnalysisStatsSnapshot(ctx context.Context) (AnalysisStats, error)
}
AnalysisStatsProvider is the interface GET /v1/analysis/stats reads. Mirrors UpscaleStatsProvider. Nil-safe: when unwired the handler returns the zero-value AnalysisStats (`enabled=false`), which iOS renders as "feature off".
type AnalysisStore ¶ added in v0.1.6
type AnalysisStore interface {
LookupAnalysis(ctx context.Context, sourcePath string) (*AnalysisRecord, error)
}
AnalysisStore is the optional interface GET /v1/waveform uses to look up a track's waveform sidecar metadata. Nil-safe — when `s.analysisStore` is nil (analysis feature off / not wired) the handler returns 404, which iOS treats identically to a pre-feature bridge that doesn't register the route at all.
**Freshness happens in the api**, not here — same rationale as VariantStore: the api owns the canonical `bridgefs.Resolver` stat and asks the store only for "do you have a row, where's the sidecar, what's its recorded source provenance". internal/manifest.Provider satisfies this via a thin LookupAnalysis wrapper at the cmd/bridge wiring point.
type ArtworkDirProvider ¶
type ArtworkDirProvider interface {
ArtworkCacheDir() string
}
ArtworkDirProvider is the minimal interface api needs to serve cached artwork. Implemented by cmd/bridge's serveCmd (via the Enricher's CacheDir). Split out so api tests don't have to import internal/enrich.
type AtlasHarvestCredentialSink ¶ added in v0.1.7
type AtlasHarvestCredentialSink interface {
SetCredential(token, baseURL string, expiresAt time.Time) error
}
AtlasHarvestCredentialSink stores the iOS-provisioned bulk_harvest credential (the App-Attest-minted Atlas token + the Atlas base URL). atlasharvest.StateStore satisfies it; the api package stays decoupled from the harvest client.
type AtlasMetaStore ¶ added in v0.1.7
type AtlasMetaStore interface {
GetReleaseAtlasMeta(ctx context.Context, mbid string) (*manifest.ReleaseAtlasMeta, error)
GetArtistAtlasMeta(ctx context.Context, mbid string) (*manifest.ArtistAtlasMeta, error)
UpsertReleaseAtlasMeta(ctx context.Context, m manifest.ReleaseAtlasMeta) error
UpsertArtistAtlasMeta(ctx context.Context, m manifest.ArtistAtlasMeta) error
}
AtlasMetaStore backs the rich-tier Atlas metadata endpoints (GET /v1/atlas-meta/{release,artist}/{mbid} + POST /v1/atlas-ingest). Nil-safe — when unwired the routes return 404 (feature-off). *manifest.Store satisfies it in production.
type BatchCoordinator ¶ added in v0.1.3
type BatchCoordinator interface {
Submit(ctx context.Context, libraryRelPath string, targetRate, targetBits int) (BatchSubmitResult, error)
SubmitOptimize(ctx context.Context, libraryRelPath string) (BatchSubmitResult, error)
Cancel(id uuid.UUID) error
ListBatches(limit int) ([]BatchRow, error)
Throughput() BatchThroughput
}
BatchCoordinator is the interface POST /v1/upscale/batch and the sibling GET / DELETE endpoints consume. Implemented by `transcode.Coordinator` over in internal/transcode; this package can't import internal/transcode directly (mirrors the UpscaleEnqueuer / VariantStore decoupling pattern).
`Submit` enrolls a path into a new batch; `SubmitOptimize` is the kind="optimize" sibling (CarPlay-optimized 16-bit family-preserving FLAC, target auto-derived per-track); `Cancel` flips an existing batch's status; `ListBatches` returns recent history for the admin Jobs page.
type BatchInsufficientDiskSpace ¶ added in v0.1.3
type BatchInsufficientDiskSpace struct {
ProjectedBytes int64
RequiredBytes int64
AvailableBytes int64
}
BatchInsufficientDiskSpace is the typed error returned by Coordinator.Submit when the pre-flight refuses. Mirrors transcode.InsufficientDiskSpaceError so this package doesn't have to import internal/transcode for the type alone. The adapter in cmd/bridge converts on Submit's return.
func (*BatchInsufficientDiskSpace) Error ¶ added in v0.1.3
func (e *BatchInsufficientDiskSpace) Error() string
func (*BatchInsufficientDiskSpace) Unwrap ¶ added in v0.1.3
func (e *BatchInsufficientDiskSpace) Unwrap() error
type BatchRequest ¶ added in v0.1.3
type BatchRequest struct {
Path string `json:"path"`
Kind string `json:"kind,omitempty"`
TargetRate int `json:"targetRate,omitempty"`
TargetBits int `json:"targetBits,omitempty"`
}
BatchRequest is the request body shape for POST /v1/upscale/batch. `targetRate` / `targetBits` are optional — when omitted the coordinator falls back to the DB-stored admin Settings.
`Kind` is one of "upscale" (default when omitted) or "optimize" (CarPlay-optimized; targetRate/targetBits are ignored — the coordinator picks family-preserving 16/44.1 or 16/48 per-track).
type BatchRow ¶ added in v0.1.3
type BatchRow struct {
ID string `json:"id"`
Path string `json:"path"`
TargetRate int `json:"targetRate"`
TargetBits int `json:"targetBits"`
Status string `json:"status"`
TotalFiles int `json:"totalFiles"`
ProcessedFiles int `json:"processedFiles"`
FailedFiles int `json:"failedFiles"`
SkippedFiles int `json:"skippedFiles,omitempty"`
Error string `json:"error,omitempty"`
CreatedAt int64 `json:"createdAt"`
UpdatedAt int64 `json:"updatedAt"`
}
BatchRow is the per-row JSON shape returned by GET /v1/upscale/batches. Mirrors manifest.UpscaleBatchRow but stringifies the UUID for JSON-decode friendliness on the admin web side.
type BatchSubmitResult ¶ added in v0.1.3
type BatchSubmitResult struct {
BatchID string `json:"batchID"`
Path string `json:"path"`
TargetRate int `json:"targetRate"`
TargetBits int `json:"targetBits"`
TotalFiles int `json:"totalFiles"`
AlreadyCovered int `json:"alreadyCovered"`
ProjectedSizeBytes int64 `json:"projectedSizeBytes"`
AvailableBytes int64 `json:"availableBytes"`
EnqueuedCount int `json:"enqueuedCount"`
}
BatchSubmitResult is the JSON shape POST /v1/upscale/batch returns on 202 Accepted. Mirrors transcode.SubmitResult field- for-field — the adapter in cmd/bridge translates between the two value types so internal/transcode stays free of the api package wire shape.
type BatchThroughput ¶ added in v0.1.3
type BatchThroughput struct {
JobsPerHour float64 `json:"jobsPerHour"`
EtaSeconds float64 `json:"etaSeconds"`
Samples int `json:"samples"`
}
BatchThroughput carries the rolling-average derived values the admin dashboard renders. Mirrors transcode.ThroughputSnapshot.
type DeviceRegistrar ¶ added in v0.1.5
type DeviceRegistrar interface {
UpsertDeviceRegistration(ctx context.Context, deviceToken, tokenID, name string) error
}
DeviceRegistrar binds a client's durable device-recovery token (the X-Device-Token header) to the auth token currently presenting it. Optional / nil-safe — when unwired, the authed path skips the bind entirely. *manifest.Store satisfies this in production.
type DiagnosticsResponse ¶ added in v0.1.4
type DiagnosticsResponse struct {
SQLiteLockWaitP50Seconds float64 `json:"sqliteLockWaitP50"`
SQLiteLockWaitP99Seconds float64 `json:"sqliteLockWaitP99"`
MBCacheHitRatio float64 `json:"mbCacheHitRatio"`
UpscaleJobsInFlight int `json:"upscaleJobsInFlight"`
UpscaleJobsCompletedTotal uint64 `json:"upscaleJobsCompletedTotal"`
UpscaleDurationP50Seconds float64 `json:"upscaleDurationP50"`
UpscaleDurationP99Seconds float64 `json:"upscaleDurationP99"`
TailscaleNodeState string `json:"tailscaleNodeState"`
TailscalePeersOnline int `json:"tailscalePeersOnline"`
LogEventCounts map[string]uint64 `json:"logEventCounts"`
ServerUptimeSeconds int64 `json:"serverUptime"`
}
DiagnosticsResponse is the wire shape for `GET /v1/diagnostics`. Consumed by the iOS `BridgeHealthSection` in `DACDiagnosticsView`.
**Counters + structured state only** — by deliberate design, no log text bodies. Including recent log lines would force per-line privacy redaction (paths / hostnames / track titles are all PII in some deployment) AND would land cross-system text in iOS-side diagnostic snapshots that operators might paste into a public issue without realizing it. The numbers convey "is the bridge healthy" without any of that risk; operators tail `bridge logs` separately if they need text.
**No slow operations in the handler**: every field below reads from either a Prometheus atomic counter snapshot OR a sliding-window quantile snapshot. The handler returns in <1 ms in steady state — safe to share resource space with the playback API tree, which is the same listener.
type Entry ¶
type Entry struct {
Name string `json:"name"`
Path string `json:"path"`
IsDir bool `json:"isDir"`
Size int64 `json:"size"`
ModTime time.Time `json:"mtime"`
Reachable *bool `json:"reachable,omitempty"`
Reason string `json:"reason,omitempty"`
}
Entry is the JSON shape of a single directory entry returned by /v1/list. The path field is library-root-relative, not the server's absolute path.
Reachable / Reason (v1.2 additive) are populated ONLY on the synthetic root-level entries returned in multi-root mode for an empty client path. Ordinary directories and files inside a root omit both fields (omitempty). iOS pre-1.2 ignores unknown fields; iOS 1.2+ can render a "library offline" hint instead of inferring it from a silent zero- size row.
Reachable is a pointer-to-bool because Go's `omitempty` omits a bare `false` (zero-value rule) — using a pointer lets us emit `false` when we mean it and skip the field entirely when reachability is not relevant (any non-root entry).
Reason is a stable machine-readable code, not free text — see the reachabilityStatus type's docblock for the value set.
type ErrorResponse ¶
ErrorResponse matches the shape documented in PROTOCOL.md ("short-code" + human detail). Keep this struct in sync with the error table there.
type EventPublisher ¶ added in v0.1.2
type EventPublisher interface {
Publish(topic string, payload interface{})
}
EventPublisher is the interface upstream services (transcode pool, pairing store) consume to publish events without taking a hard dependency on the api package. Wired at cmd/bridge boundary.
type HealthResponse ¶
type HealthResponse struct {
ProtocolVersion int `json:"protocolVersion"`
ServerVersion string `json:"serverVersion"`
LibraryName string `json:"libraryName"`
LibraryRoots []string `json:"libraryRoots"`
CertFingerprint string `json:"certFingerprint"`
StartedAt time.Time `json:"startedAt"`
ScanState ScanState `json:"scanState"`
Endpoints []string `json:"endpoints,omitempty"`
LatestServerVersion string `json:"latestServerVersion,omitempty"`
UpdateAvailable bool `json:"updateAvailable,omitempty"`
UpdateReleaseNotesURL string `json:"updateReleaseNotesURL,omitempty"`
MinClientVersion string `json:"minClientVersion,omitempty"`
// UpscaleEnabled mirrors the runtime config flag
// `cfg.Upscale.Enabled` AND the result of the sox-on-PATH
// startup probe (a true config setting whose probe failed
// reports false here — see cmd/bridge/main.go's serve-side
// degradation). iOS uses this to gate every variant-related
// UI surface for that bridge: when false, the picker / glyph
// / context menu items are all hidden so SMB and bridge rows
// look identical. Pre-v1.2 servers omit the field entirely;
// iOS treats absence as `false` (no variant chrome). Pointer
// type so a true `false` value distinguishes "feature
// supported but disabled" from "no field on the wire".
UpscaleEnabled *bool `json:"upscaleEnabled,omitempty"`
// CertNotAfter is the **on-disk self-signed TLS certificate's**
// `NotAfter` (UTC). This is the cert that fronts LAN, mDNS,
// IP-literal, and Tailscale-IP SNI handshakes — i.e. the cert
// iOS captures + pins at pairing time. Lets iOS surface a
// "Bridge cert expires in X days — re-pair to refresh" warning
// before the cert actually expires and TLS handshakes start
// failing at Apple's ATS layer (Apple's 397-day cap means
// operators must re-pair roughly annually).
//
// **In public mode this is NOT the cert the public-domain
// connection sees** — `LECertNotAfter` (below) carries the
// Let's Encrypt cert's expiry, which is the operative cert when
// iOS dials `https://<autocert.domain>/`. Both fields are
// emitted in public mode so iOS / operator tooling can render
// the right warning per posture (the self-signed cert is still
// the pinned cert iOS used at pair-time, so its expiry still
// matters for the "re-pair" prompt; the LE cert expiry matters
// for the public-trust handshake).
//
// Additive field; pre-bridge-with-this-PR servers omit it and
// iOS treats absence as "no expiry info, never warn". Pointer
// (not bare `time.Time`) because Go's `omitempty` doesn't treat
// the zero time as empty — and emitting `0001-01-01T00:00:00Z`
// from a test harness or a parse failure would actively confuse
// clients.
CertNotAfter *time.Time `json:"certNotAfter,omitempty"`
// LECertNotAfter is the public-domain Let's Encrypt certificate's
// `NotAfter` (UTC), populated only in public mode (autocert
// enabled + cert minted on disk). Surfaces the ~90-day LE rotation
// expiry distinctly from `CertNotAfter` (the self-signed cert
// pinned by iOS at LAN pair-time, capped at 397 days). Without
// this, an operator glancing at `/v1/health` on a freshly-deployed
// public VPS would see the 397-day self-signed expiry and
// reasonably assume their LE cert lasts that long too — masking
// the real ~60-day "rotation is about to happen" signal.
//
// Loopback bridges omit this field; pre-autocert servers omit it.
// iOS treats absence as "no LE cert" (= LAN-only bridge). Pointer
// for the same `omitempty`-vs-zero-time reason as `CertNotAfter`.
LECertNotAfter *time.Time `json:"leCertNotAfter,omitempty"`
// Features advertises capability flags the server supports.
// Additive over the wire (omitempty); iOS consults this list to
// skip belt-and-braces recovery paths when the server already
// provides the underlying guarantee. Stable string keys, never
// repurpose. Pre-feature-flag bridges omit the field entirely;
// iOS treats absence as "feature absent" so any client-side
// recovery path runs unconditionally on those bridges.
//
// Current keys (kept alpha-sorted on the wire):
// - "deleteVariants" (v1.3): bridge supports DELETE
// /v1/upscale/variants AND fires `upscale.deleted` SSE
// events on operator-driven cleanup + integrity-ticker
// reconciliation. iOS reads it on the same probe to know
// the wand chrome can revert passively when a variant
// disappears server-side. Gated on `variantDeleter != nil`
// AND `upscaleEnabled`.
// - "operatorDrivenUpscale" (v1.3): upscaling is managed in
// the bridge's admin Library Inspector, not per-tap from
// the iOS app. iOS gates ALL legacy upscale UI surfaces
// (BridgeUpscaleControl wand, TrackSourceGlyph, long-press
// "Upscale this track" menu items, BridgeUpscaleManagement
// Section) on the ABSENCE of this flag. Pre-v1.3 bridges
// omit it; iOS keeps the legacy wand UX alive against
// those. v1.3+ bridges advertise it; iOS surfaces only the
// mini-player haptic-press toggle + Settings device toggle
// for variant override.
// - "pairingEventsSupported" (v1.4): the bridge backs
// `GET /v1/pairing/{id}/events` with the same SSE broker
// that powers `/v1/events`. iOS's BridgeJoinSession can
// opt into the push transport up-front when the flag is
// present (vs probing then falling back to 2 s polling on
// 404). Gated on `eventBroker != nil && pairing != nil`
// — both routes must be live for the consumer to see
// useful events.
// - "pushEventsSupported" (v1.4): the bridge backs
// `GET /v1/events` with a live SSE broker. Future iOS /
// third-party clients can branch on this flag rather than
// attempting the connect-and-fall-back dance on every cold
// start. Gated on `eventBroker != nil`.
// - "upscaleCompleteEvents": bridge publishes a per-job
// `upscale.complete` SSE event after `UpsertVariant` commits.
// iOS uses this to promote the wand chrome to "Ready" within
// ~1-2 s of bridge-side completion (vs the legacy 8 s first
// ladder rung), and skips 4-of-5 polling-ladder rungs when
// present, keeping only the +600 s safety backstop.
// - "variantBumpsIndex": UpsertVariant / DeleteVariant bump
// `tracks.indexed_at` for the parent row, so iOS delta-sync
// surfaces variant changes without needing a full rescan.
// iOS gates its +600s "silent fullRescan recovery" rung on
// absence of this flag.
Features []string `json:"features,omitempty"`
// Roots is the per-root reachability snapshot. Populated whenever the
// bridge has at least one configured library root; absent on bridges
// with zero roots (test harness). Lets iOS surface a "Library X
// offline" hint in a single /v1/health call instead of paginating
// /v1/list to discover offline roots. Probes are cached for
// reachabilityTTL (5 s) so the steady-state /v1/health poll cadence
// doesn't repeatedly stat every network mount.
//
// Pre-1.2 iOS ignores the field. iOS 1.2+ uses Reason as a stable
// machine-readable code mapped to a localized UI string.
Roots []RootStatus `json:"roots,omitempty"`
// UPnPUpstreamServers advertises operator-configured upstream
// MediaServers (e.g. a Chord 2Go) whose tracks the bridge ingests
// into its manifest and proxies to iOS bit-exact. Lets iOS surface
// a sub-source row per upstream inside the bridge's Library view
// + a per-upstream filter in the source picker (Track.path
// starts_with(pathPrefix) is the membership test).
//
// Populated only when the upstream feature is wired (operator set
// `upnpUpstream.enabled = true` in bridge.yaml). Omitted entirely
// on bridges where the feature is disabled, so pre-feature iOS
// rejects nothing. Order matches the YAML config order so the iOS
// UI is deterministic across health probes.
//
// Wire shape is the **public subset** — operator-internal fields
// (control URL, manufacturer, raw manualDescriptionURL, per-walk
// telemetry, error strings) stay on the admin surface
// (`/api/upnp/servers`). iOS only needs name + identity +
// pathPrefix membership key + display label + track count.
UPnPUpstreamServers []UPnPUpstreamPublicServer `json:"upnpUpstreamServers,omitempty"`
}
HealthResponse is the /v1/health JSON body. Field ordering must stay stable because the iOS side golden-decodes this shape (see PROTOCOL.md).
`Endpoints` is the additive v1 extension that lets iOS self-discover LAN ↔ Tailscale alternates without re-pairing. Empty when the bridge can't enumerate interfaces (e.g. test harness) — iOS falls back to the URL it was paired on. Adding fields here is backwards-compatible: the iOS decoder uses Codable with default values for anything it doesn't know about.
The four `*Update*` / `MinClientVersion` fields are the Phase A additive extension (see PROTOCOL.md "Updates" section). They are only populated when an UpdaterStatus has been wired via WithUpdater AND the updater has at least one successful poll cached; absent otherwise so the wire shape stays identical to the pre-update response on a bridge that doesn't yet have an updater. iOS treats all four as optional and falls back to "no update info" rather than a hard error if any are missing.
type HistoryStore ¶ added in v0.1.5
type HistoryStore interface {
InsertHistoryBatch(ctx context.Context, events []manifest.PlaybackHistoryRow) error
// ListHistory pages events newest-first; deviceToken "" is the global
// all-devices feed (the only form the public read endpoint uses —
// listening history is user-wide across every paired device).
ListHistory(ctx context.Context, deviceToken string, limit int, afterID int64) ([]manifest.HistoryEventOut, error)
}
HistoryStore is the optional backing store for POST /v1/history/batch and the GET /v1/history all-devices read feed. Nil-safe — when unwired the routes return 404 (feature-off). *manifest.Store satisfies it in production.
type InflightDropper ¶ added in v0.1.3
InflightDropper is the interface the delete handler uses to pre-cancel any transcode-pool dedup entries for source paths about to be deleted. Without this, a worker mid-job for a matching path would race the handler — its UpsertVariant would land AFTER our DeleteVariant, leaving a zombie row that the `--gc` reverse pass / integrity watcher reaps later. Calling DropInflight clears the dedup so a subsequent re-submit (if any) doesn't no-op against a stale slot. Workers ALREADY in progress are NOT cancelled (no per-job cancellation primitive); the unlink race is documented honestly.
Nil-safe — when no pool is wired the handler skips the drop.
type MBIDProbe ¶ added in v0.1.1
type MBIDProbe interface {
HasTrackWithArtworkMBID(ctx context.Context, mbid string) bool
HasTrackWithArtistMBID(ctx context.Context, mbid string) bool
}
MBIDProbe is an optional interface the artwork + artist-image handlers use to distinguish "MBID known to the server but not cached yet" (return 202 + Retry-After so iOS retries) from "genuinely unknown MBID" (return 404 so iOS treats as terminal).
Nil-safe — when `s.mbidProbe` is nil the handlers fall back to the pre-v1.1 behaviour of 404-on-miss. `internal/manifest.Provider` satisfies this interface in production.
type ManifestPage ¶ added in v0.1.2
ManifestPage is the typed JSON-serializable shape returned by `BuildManifestPage`. Aliased to `manifest.Manifest` so the wire shape stays defined in exactly one place; the alias keeps the `api` package's interface contract type-safe without forcing callers to learn about `internal/manifest`.
The `internal/api → internal/manifest` import is unidirectional: manifest never imports api (the existing `VariantLookup` / `VariantRecord` mirror pattern at api.go:105 / provider.go:26 is what defends the OTHER direction; `Manifest` doesn't need a mirror because nothing in manifest reaches into api for it).
type ManifestProvider ¶
type ManifestProvider interface {
// WriteManifest streams the legacy non-paginated manifest as JSON
// straight to w. Bounding peak memory is mandatory: the prior
// in-memory builder OOM-killed Pi-class hosts on 50k-track
// libraries. Mid-stream errors are returned but unrecoverable on
// the wire (headers and prefix already sent); the handler logs and
// the truncated body fails iOS-side decode, which retries.
//
// `ctx` is checked inside the per-row stream loop so a client
// disconnect mid-response (slow network, iOS app backgrounded)
// terminates the SQLite scan instead of running it to EOF holding
// connection resources.
WriteManifest(ctx context.Context, w io.Writer, since time.Time) error
// BuildManifestPage is the v1.1 paginated-manifest variant used
// when the client asks for `?limit=`. `cursor=""` requests the
// first page. Callers iterate until the returned page's
// `NextCursor` is nil. Returns a fully-materialised value because
// per-page output is bounded by the page-size cap (5000) and the
// JSON writer can buffer the whole page without OOM risk.
BuildManifestPage(ctx context.Context, cursor string, limit int) (*ManifestPage, error)
IsScanning() bool
LastFullScan() time.Time
TracksIndexed(ctx context.Context) int
// PendingDeletions returns the total count of rows across tracks
// and folders with missing_count > 0 — surfaced on ScanState. May
// return 0 for an unwired or pre-v5 store; never errors today.
PendingDeletions(ctx context.Context) int64
}
ManifestProvider is the interface /v1/manifest and /v1/health use to read the indexed library state. internal/manifest implements it via the Scanner + Store pair; tests can pass a small stub.
type PairingStateEvent ¶ added in v0.1.2
type PairingStateEvent struct {
Status string `json:"status"`
TTLSecondsRemaining int `json:"ttlSecondsRemaining"`
BridgeStartedAt int64 `json:"bridgeStartedAt"`
VerificationCode string `json:"verificationCode,omitempty"`
Token string `json:"token,omitempty"`
TokenID string `json:"tokenId,omitempty"`
}
PairingStateEvent is the wire shape the bridge publishes on the SSE `pairing.<requestID>` topic AND returns from `GET /v1/pairing/{id}`. Single source of truth — iOS reuses one decoder across both transports.
JSON tags match the polling contract; do NOT rename without bumping `ProtocolVersion` and a Mirror-PR pair on the iOS side.
type PlaylistCoverStore ¶ added in v0.1.7
type PlaylistCoverStore interface {
PlaylistCoversByScope(ctx context.Context, scope string) (map[string]manifest.PlaylistCover, error)
GetPlaylistCover(ctx context.Context, scope, key string) (manifest.PlaylistCover, bool, error)
DeletePlaylistCover(ctx context.Context, scope, key string) (hash, ext string, ok bool, err error)
}
PlaylistCoverStore resolves operator-uploaded cover hashes for the smart-mix + playlist wire DTOs (the `imageHash` advertisement). Nil-safe — when unwired, no imageHash is advertised and iOS uses the auto-mosaic. *manifest.Store satisfies it.
type PlaylistStore ¶ added in v0.1.5
type PlaylistStore interface {
UpsertPlaylist(ctx context.Context, deviceToken string, p manifest.PlaylistRow, items []manifest.PlaylistItemRow) error
GetPlaylist(ctx context.Context, id string) (*manifest.PlaylistRow, []manifest.PlaylistItemRow, error)
ListPlaylists(ctx context.Context) ([]manifest.PlaylistSummary, error)
TombstonePlaylist(ctx context.Context, id string) (bool, error)
}
PlaylistStore is the optional backing store for the /v1/playlists backup endpoints. Nil-safe — when unwired the routes return 404 (feature-off). *manifest.Store satisfies it in production.
Playlists are USER-WIDE: every paired device belongs to the bridge operator, so reads/writes/deletes are id-scoped, not device-scoped — restore is initiable from any device (advertised via the `playlistsCrossDevice` health feature flag). The deviceToken on the upsert records last-writer provenance only.
type RendererDiscoverySnapshotter ¶ added in v0.1.5
type RendererDiscoverySnapshotter interface {
// Snapshot returns the live cache contents as a slice of
// the api-local RendererInfo shape. Empty slice (NOT nil)
// when no renderers are cached.
Snapshot() []RendererInfo
}
RendererDiscoverySnapshotter is the abstraction the api package consumes for the renderer cache backing `GET /v1/renderers`. The concrete implementation lives in `internal/dlna/discovery` — this shape keeps the api package free of a hard dependency on the discovery package (which transitively pulls in net.UDPConn etc.).
Returning a `[]any`-shaped slice would force the handler to do runtime type checks; returning a typed `[]discovery.RendererInfo` would create the dependency we're avoiding. The third option — re-declaring the wire DTO locally — duplicates the contract. We accept the duplication risk via a structural contract: `internal/dlna/discovery.RendererInfo` MUST stay structurally assignable to the local `RendererInfo` type below. A compile- time assertion in the wiring layer (`cmd/bridge/dlna_wiring.go`) catches any drift.
type RendererInfo ¶ added in v0.1.5
type RendererInfo struct {
UDN string `json:"udn"`
FriendlyName string `json:"friendlyName"`
Manufacturer string `json:"manufacturer,omitempty"`
ModelDescription string `json:"modelDescription,omitempty"`
ModelName string `json:"modelName,omitempty"`
ControlURL string `json:"controlURL"`
EventURL string `json:"eventURL,omitempty"`
RenderingControlURL string `json:"renderingControlURL,omitempty"`
SinkProtocolInfos []string `json:"sinkProtocolInfos,omitempty"`
LastSeenAt time.Time `json:"lastSeenAt"`
}
RendererInfo is the api-package-local wire shape for `GET /v1/renderers`. Structurally mirrors `internal/dlna/discovery.RendererInfo`; the wiring adapter in `cmd/bridge/dlna_wiring.go` converts between the two via a trivial value copy.
**JSON encoding stability**: field names + tags pinned by `renderer_dto_test.go` in the discovery package + the `renderers_handler_test.go` in this package. Changes require the Mirror-PR convention.
type RenderersResponse ¶ added in v0.1.5
type RenderersResponse struct {
Renderers []RendererInfo `json:"renderers"`
}
RenderersResponse is the top-level shape of `GET /v1/renderers`.
type RootStatus ¶ added in v0.1.3
type RootStatus struct {
Name string `json:"name"`
Reachable bool `json:"reachable"`
Reason string `json:"reason,omitempty"`
}
RootStatus reports the reachability of a single library root. Wire-DTO only — populated server-side from reachabilityStatus. Name is the root's basename (same identifier iOS sees on /v1/list); Reason is the stable code emitted by reachabilityCache.probe.
type ScanState ¶
type ScanState struct {
IsScanning bool `json:"isScanning"`
LastFullScan time.Time `json:"lastFullScan,omitempty"`
TracksIndexed int `json:"tracksIndexed"`
// PendingDeletions reports the count of rows across `tracks` and
// `folders` whose missing_count is > 0 but haven't yet reached the
// configured delete threshold — i.e. rows the scanner has marked
// as "missing this pass" but is granting the configured grace
// period before reaping. Surfaced for the admin dashboard "X rows
// pending deletion" hint and as a diagnostic signal on /v1/health:
// a steadily climbing value suggests a flaky mount or partial
// network failure that errorSubtrees isn't catching. Pre-v1.2.x
// bridges (before migration v5) omit the field entirely; iOS and
// admin UI treat absence as "no pending deletions, nothing to
// surface". Additive field, no protocol bump.
PendingDeletions int64 `json:"pendingDeletions,omitempty"`
}
ScanState reports the scanner's current status. Real fields populate once the manifest package lands; the shape is stubbed in for iOS decoder stability.
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server owns the http.Handler and the per-request state it needs.
func New ¶
New constructs a Server. fingerprint is the SHA-256 of the TLS cert, used for display in /v1/health (iOS pins by this value). mp can be nil during early boot / tests — /v1/manifest will return 503 until it's populated.
func (*Server) ConfigHolder ¶ added in v0.1.3
func (s *Server) ConfigHolder() *config.RuntimeConfig
ConfigHolder exposes the API server's live runtime-config holder so cmd wiring can share one source of truth with admin writers.
func (*Server) EventPublisher ¶ added in v0.1.2
func (s *Server) EventPublisher() EventPublisher
EventPublisher returns the broker as the Server-facing `EventPublisher` interface so upstream services (transcode pool, pairing store) can publish events without taking a hard dependency on the api package. When the broker isn't wired (test harnesses, pre-this-PR bridges) the returned publisher silently drops publishes — same back-compat shape every upstream path already handles.
func (*Server) Handler ¶
Handler returns the root http.Handler, pre-wrapped with the X-Bridge-Protocol middleware. Outermost wrapper is withAltSvc so protocol upgrades (or cache-clear directives) land even on 500 panic recovery paths.
func (*Server) Resolver ¶
Resolver returns the internal path Resolver so the admin console can call SetRoots when adding or removing a library root at runtime. The api handlers and admin handlers deliberately share the same Resolver instance — otherwise a hot add/remove would update one without the other and /v1/list would serve stale top-level entries.
func (*Server) RunVariantDelete ¶ added in v0.1.3
func (s *Server) RunVariantDelete(ctx context.Context, req VariantDeleteRequest) (VariantDeleteResponse, error)
RunVariantDelete is the core delete-variants execution path, extracted from `upscaleDelete` so the admin console (`DELETE /api/upscale/variants`) can share it without duplicating the four-phase list/dedup-drop/unlink+delete/SSE-publish loop. The HTTP handler and the admin handler both parse their own query params, call this method, and translate the result (or `ErrVariantDeleteUnavailable`) into their respective response shapes.
Returns `(VariantDeleteResponse, nil)` for the success path (including the empty-rows fast path → zero-counts response). Returns `(_, ErrVariantDeleteUnavailable)` when `variantDeleter` is nil — callers map to 404 (api) or 503 (admin). Any other error is a SQLite enumeration failure during the list phase; the per-row unlink / DeleteVariant errors log+continue internally so the response always reflects what actually happened.
The same `upscale.deleted` SSE event is emitted regardless of caller — iOS reconciles via that single fan-out path no matter whether the user clicked Delete in the iOS app, the admin console, or invoked the HTTP endpoint directly.
func (*Server) SetTailscaleStatus ¶ added in v0.1.4
func (s *Server) SetTailscaleStatus(p admin.TailscaleProvider)
SetTailscaleStatus is the deferred-setter counterpart to `WithTailscaleStatus`. Two-phase init sites in cmd/bridge that construct the api server BEFORE the tailscale status source is available call this after both are ready. Idiomatic for components whose lifecycles span early-setup + network-binding phases (the embedded tsnet Server is created after `api.New(...)` in some configurations and `newTailscaleAdminSource` needs it as input).
func (*Server) StartEventBroker ¶ added in v0.1.2
func (s *Server) StartEventBroker() (stopFn func())
StartEventBroker spins up the in-process event-bus goroutine that backs GET /v1/events. Same pattern as `StartPairingRateLimitGC` — returns a stopFn that drains the broker on shutdown. Wired from cmd/bridge after `apiSrv := api.New(...)`.
Called exactly once during `bridge serve` startup, BEFORE the HTTP server begins accepting connections — so the write to s.eventBroker here happens-before every handler read of it (EventPublisher, the /v1/health feature probe). That is the same wired-once-at-startup, read-only-during-serving contract every other Server field follows (pairing, historyStore, batchCoordinator, …); none of them — nor this one — needs per-field synchronization. A sync.Once / atomic.Pointer here would only half-guard (the reads stay unsynchronized either way) and would be inconsistent with the rest of the struct, so we keep the plain assignment.
func (*Server) StartManifestRateLimitReaper ¶ added in v0.1.3
func (s *Server) StartManifestRateLimitReaper() (stopFn func())
StartManifestRateLimitReaper drains idle per-token entries from the /v1/manifest limiter map on a 10-minute tick. Mirrors StartPairingRateLimitGC's lifecycle contract — caller passes the returned func to defer so the goroutine exits cleanly on shutdown. Test harnesses can skip the call; the reaper is purely a memory- pressure mitigation for long-running bridges with high client churn.
func (*Server) StartPairingRateLimitGC ¶ added in v0.1.2
func (s *Server) StartPairingRateLimitGC() (stopFn func())
StartPairingRateLimitGC kicks off the per-IP rate-limiter's background sweep on a hidden goroutine. Called once from `cmd/bridge` after `apiSrv := api.New(...).WithPairing(...)`. The returned func, called on shutdown, signals the goroutine to exit cleanly. Tests don't need to call this — the GC interval is hours, so leaving it idle is harmless, and table-driven tests construct fresh limiters via `New` anyway.
func (*Server) StartedAt ¶ added in v0.1.2
StartedAt returns the server's construction timestamp. Used by the SSE pairing-event publisher in cmd/bridge to populate the `bridgeStartedAt` field, matching the polling endpoint's contract: iOS observes a value change between events and treats it as "bridge restarted, abandon this in-flight pairing request."
func (*Server) WithAnalysis ¶ added in v0.1.6
func (s *Server) WithAnalysis(enabled bool, as AnalysisStore) *Server
WithAnalysis wires the offline audio-analysis feature. `enabled` mirrors `cfg.Analysis.Enabled` AND the sox-on-PATH probe outcome (a true config whose probe failed lands here as false — graceful degradation). `as` may be nil when enabled=false; when enabled=true it MUST be the AnalysisStore the /v1/waveform handler reads.
Effect on the wire:
- /v1/health advertises the `waveform` feature flag iff `enabled`.
- /v1/waveform serves sidecars iff `as != nil`; 404 otherwise.
func (*Server) WithAnalysisStats ¶ added in v0.1.6
func (s *Server) WithAnalysisStats(p AnalysisStatsProvider) *Server
WithAnalysisStats attaches the GET /v1/analysis/stats provider. Optional — when nil the endpoint returns the zero-value AnalysisStats (`enabled=false`). Mirrors WithUpscaleStats.
func (*Server) WithArtworkDirs ¶
func (s *Server) WithArtworkDirs(ad ArtworkDirProvider) *Server
WithArtworkDirs attaches an artwork cache dir provider. Separate from New to avoid churning every call site when optional features are added.
func (*Server) WithAtlasHarvest ¶ added in v0.1.7
func (s *Server) WithAtlasHarvest(sink AtlasHarvestCredentialSink) *Server
WithAtlasHarvest wires the credential sink, enabling POST /v1/atlas-harvest/credential. Gated on cfg.Atlas.HarvestEnabled by the caller.
func (*Server) WithAtlasMeta ¶ added in v0.1.7
WithAtlasMeta wires the optional rich-tier Atlas metadata integration. `enabled` mirrors cfg.Atlas.Enabled; `ttl` is the freshness window served as `ttlSeconds`. When enabled, /v1/health advertises "atlasEnrichment" and the meta/ingest routes are live; otherwise they 404. Returns the receiver.
func (*Server) WithBatchCoordinator ¶ added in v0.1.3
func (s *Server) WithBatchCoordinator(c BatchCoordinator) *Server
WithBatchCoordinator attaches the coordinator. nil disables the new batch surface (handler returns 503 `upscale_disabled`). Called once during cmd/bridge wiring.
func (*Server) WithCarPlayOptimize ¶ added in v0.1.4
WithCarPlayOptimize toggles the CarPlay-optimize feature advertisement (the `carPlayOptimize` entry in /v1/health.features). Caller is responsible for the AND-gate with upscale enablement — the wire-emit branch in /v1/health additionally requires `s.upscaleEnabled` (optimize shares the SoX pool with upscale and has no meaning without it).
func (*Server) WithCertExpiry ¶ added in v0.1.2
WithCertExpiry attaches the on-disk TLS cert's NotAfter date so /v1/health can surface it to iOS. Wired from cmd/bridge after `tls.Inspect(certPath)`. Test harnesses can omit this; the HealthResponse field is `omitempty` (pointer) so the zero-time stays off the wire.
func (*Server) WithDLNA ¶ added in v0.1.5
WithDLNA toggles the `dlnaServer` advertisement in /v1/health.features. The bridge's actual DLNA MediaServer runs on its own parallel http.Server bound LAN-only (and optionally tsnet-routed) — wired separately in `cmd/bridge/main.go`. This option is the iOS-visible capability flag so the app's `OutputPickerSheet` can surface DLNA-capable bridges as a renderer-discovery source candidate.
**Public-mode refusal applies upstream**: caller MUST consult `dlna.ShouldEnableDLNA(cfg, deploymentMode)` before calling this with `enabled = true`. Passing `true` from a public-mode deploy would advertise a capability the actual server refuses to bind — the upstream gate enforces the safety invariant; this method is the bookkeeping for the capability advertisement.
func (*Server) WithDeviceRegistrar ¶ added in v0.1.5
func (s *Server) WithDeviceRegistrar(r DeviceRegistrar) *Server
WithDeviceRegistrar wires the per-device recovery-token binding used by playlist backup + playback telemetry. Returns the receiver for chaining.
func (*Server) WithHistoryStore ¶ added in v0.1.5
func (s *Server) WithHistoryStore(hs HistoryStore) *Server
WithHistoryStore wires the playback-telemetry feature. Advertises the "playbackHistory" health-feature flag when set. Returns the receiver.
func (*Server) WithInflightDropper ¶ added in v0.1.3
func (s *Server) WithInflightDropper(d InflightDropper) *Server
WithInflightDropper attaches the transcode pool's dedup-drop primitive. Optional — bridges without an upscale pool wired (test harnesses, pure-manifest builds) skip the dedup-drop step entirely.
func (*Server) WithLECertExpiry ¶ added in v0.1.4
WithLECertExpiry wires the live Let's Encrypt cert expiry provider. Closure-based (not a stamped time.Time) because autocert renews the cert in the background — every /v1/health call should reflect the CURRENT cached cert's `NotAfter`, not whatever was on disk when the bridge started. cmd/bridge passes the same `autocert.Manager.Status` closure the admin tile reads from, so /v1/health and the admin tile always agree about expiry.
Provider may return zero time when no cert is cached yet (autocert hasn't completed the first mint, or the manager isn't enabled). HealthResponse omits the field in that case — iOS treats absence as "no LE cert" (= LAN-only / loopback bridge), which is the right fallback for a public-mode bridge mid-first-handshake too.
Pass `nil` (or skip the call) to opt out — loopback installs never wire it.
func (*Server) WithMBIDProbe ¶ added in v0.1.1
WithMBIDProbe attaches an MBIDProbe so the artwork / artist-image handlers can return 202 + Retry-After for known-but-uncached MBIDs instead of a flat 404. Optional — omitting it preserves v1.0 behaviour (404 on every cache miss). `internal/manifest.Provider` satisfies the interface in production.
func (*Server) WithPairing ¶ added in v0.1.2
WithPairing attaches the in-memory pairing.Store that backs the admin-approval pairing flow (POST /v1/pairing/requests, GET/DELETE /v1/pairing/{id}). Optional — when nil the routes return 404 `pairing_not_supported` and iOS falls back to manual token entry. Older bridges that don't ship this package keep the same wire behaviour for free (404 from the unregistered route).
func (*Server) WithPlaylistCoverStore ¶ added in v0.1.7
func (s *Server) WithPlaylistCoverStore(st PlaylistCoverStore) *Server
WithPlaylistCoverStore wires custom-cover resolution. Returns the receiver.
func (*Server) WithPlaylistStore ¶ added in v0.1.5
func (s *Server) WithPlaylistStore(ps PlaylistStore) *Server
WithPlaylistStore wires the playlist-backup feature. Advertises the "playlistBackup" health-feature flag when set. Returns the receiver.
func (*Server) WithRendererDiscovery ¶ added in v0.1.5
func (s *Server) WithRendererDiscovery(snap RendererDiscoverySnapshotter) *Server
WithRendererDiscovery wires the SSDP MediaRenderer cache that backs `GET /v1/renderers` AND the `rendererDiscovery` flag in `/v1/health.features`. Passing nil leaves the endpoint unregistered (returns 404) and the flag absent.
**Capability flag gating**: presence of the flag in `/v1/health.features` is gated AND-wise on `s.dlnaEnabled` (sibling DLNA MediaServer must be running) so iOS clients get a consistent capability picture — running renderer discovery without the MediaServer is a coherent operator choice but outside the v1 supported surface.
func (*Server) WithSessionTracker ¶ added in v0.1.1
func (s *Server) WithSessionTracker(t SessionTracker) *Server
WithSessionTracker attaches the inflight-download counter the updater consults before swapping the binary and restarting. Optional — omitting it disables the gate entirely (tests). The counter is incremented around `serveFile` so /v1/read and /v1/download both count.
func (*Server) WithSmartPlaylistStore ¶ added in v0.1.7
func (s *Server) WithSmartPlaylistStore(st SmartPlaylistStore) *Server
WithSmartPlaylistStore wires the smart-playlist feed. Advertises the "smartPlaylists" health-feature flag when set. Returns the receiver.
func (*Server) WithTailscaleStatus ¶ added in v0.1.4
func (s *Server) WithTailscaleStatus(p admin.TailscaleProvider) *Server
WithTailscaleStatus wires the embedded-tsnet status provider used by `reachableEndpoints` to advertise the bridge's `*.ts.net` URL + tailnet IPs in tsnet mode. Builder form for consistency with the surrounding chain in `cmd/bridge/main.go`. Pass `nil` (or skip the call entirely) to opt out — the advertising path no-ops cleanly.
In `cli` / `disabled` modes the provider is consulted but the mode-gate in `reachableEndpoints` short-circuits before any advertisement is emitted, so wiring it unconditionally is safe.
func (*Server) WithUPnPHostResolver ¶ added in v0.1.6
func (s *Server) WithUPnPHostResolver(r UPnPServerHostResolver) *Server
WithUPnPHostResolver wires the live-host lookup. Required alongside WithUPnPRouting for the proxy to engage; the routing-only setup surfaces 503 ("server not currently reachable") for every UPnP track.
func (*Server) WithUPnPRouting ¶ added in v0.1.6
func (s *Server) WithUPnPRouting(l UPnPRoutingLookup) *Server
WithUPnPRouting wires the manifest-side routing lookup. Passing nil disables the UPnP proxy entirely (serveFile then routes every request through the filesystem resolver as before).
func (*Server) WithUPnPUpstreamPublicProvider ¶ added in v0.1.6
func (s *Server) WithUPnPUpstreamPublicProvider(p UPnPUpstreamPublicProvider) *Server
WithUPnPUpstreamPublicProvider wires the read-only "public" view of the upstream-MediaServer feature for advertisement on `/v1/health`. Independent of WithUPnPRouting / WithUPnPHostResolver: the proxy wiring is OPTIONAL on the advertisement path (a bridge could theoretically advertise upstreams without proxying their bytes — though in practice cmd/bridge wires all three together).
Passing nil omits the `upnpUpstreamServers` field from the wire shape (so pre-feature behaviour is preserved on disabled deploys). The provider is consulted on every /v1/health call — implementations should keep the work proportional to the configured-server count.
func (*Server) WithUpdater ¶ added in v0.1.1
func (s *Server) WithUpdater(u UpdaterStatus) *Server
WithUpdater attaches an UpdaterStatus so /v1/health can advertise the latest available bridge release to iOS clients. Optional — omitting it preserves the pre-Phase-A wire shape (the four new `omitempty` fields stay absent from the JSON response).
func (*Server) WithUpscale ¶ added in v0.1.2
func (s *Server) WithUpscale(enabled bool, vs VariantStore) *Server
WithUpscale wires the v1.2 PCM-upscaling feature into the server. `enabled` mirrors `cfg.Upscale.Enabled` AND the result of the cmd/bridge sox-on-PATH startup probe (a true config setting whose probe failed lands here as false — graceful degradation, the rest of the server keeps running). `vs` may be nil when enabled=false; when enabled=true it MUST be the VariantStore implementation that knows where the sidecars live.
Effect on the wire:
- /v1/health reports `upscaleEnabled` matching `enabled`.
- /v1/manifest emits per-Track `variants` slices iff `enabled` (cleared in the manifest provider otherwise).
- /v1/download honors `?variant=<id>` iff `enabled` AND `vs != nil`; 404 `variant_not_found` otherwise.
func (*Server) WithUpscaleEnqueuer ¶ added in v0.1.2
func (s *Server) WithUpscaleEnqueuer(e UpscaleEnqueuer) *Server
WithUpscaleEnqueuer attaches the long-lived transcode worker pool's job-submit interface so the v1.2 `POST /v1/upscale` endpoint can hand off track / folder requests. Optional — when nil the endpoint returns `503 upscale_disabled`.
Wired in cmd/bridge serve startup IFF `cfg.Upscale.Enabled && sox-on-PATH probe passed`. The adapter at the wiring point translates `transcode.ErrQueueFull` into the api package's `ErrUpscaleQueueFull` sentinel so the handler can map cleanly to the wire response.
func (*Server) WithUpscaleStats ¶ added in v0.1.2
func (s *Server) WithUpscaleStats(p UpscaleStatsProvider) *Server
WithUpscaleStats attaches the snapshot provider for GET /v1/upscale/stats. Optional — when nil the endpoint returns the zero-value UpscaleStats (`enabled=false`, no pool, no sox availability), which iOS treats as "feature off" without distinguishing missing-endpoint from disabled-feature. Lets older bridges expose the route without the wiring overhead.
Wired in cmd/bridge serve startup with the same closure the admin `/api/upscale/stats` tile already consumes — the two surfaces stay in lockstep so the admin operator and the paired iOS client see the same numbers.
func (*Server) WithVariantDeleter ¶ added in v0.1.3
func (s *Server) WithVariantDeleter(d VariantDeleter) *Server
WithVariantDeleter attaches the deleter adapter and enables the `deleteVariants` capability flag in /v1/health.features. nil is the default (test harnesses, pre-feature bridges) — the handler surfaces 404 and the feature flag stays out of the advertised set.
type SessionTracker ¶ added in v0.1.1
type SessionTracker interface {
Begin()
End()
}
SessionTracker is the optional interface serveFile uses to record active file-serving requests so the updater can refuse to swap- and-restart while a download is in flight. Nil-safe — when `s.sessions` is nil serveFile skips the bookkeeping (test harnesses, pre-Phase-B bridges).
`internal/updater.Tracker` satisfies this interface in production.
type SmartPlaylistStore ¶ added in v0.1.7
type SmartPlaylistStore interface {
LoadSmartPlaylists(ctx context.Context) ([]manifest.StoredSmartPlaylist, error)
}
SmartPlaylistStore is the optional backing store for GET /v1/smart-playlists. Nil-safe — unwired routes return 404 (feature-off). *manifest.Store satisfies it. READ-ONLY on the request path: generation happens in the background regenerator + the admin "Regenerate now" button, never inside a request (so the homepage fetch is a single fast cache read).
type StatResponse ¶
type StatResponse struct {
IsDir bool `json:"isDir"`
Size int64 `json:"size"`
ModTime time.Time `json:"mtime"`
Reachable *bool `json:"reachable,omitempty"`
Reason string `json:"reason,omitempty"`
}
StatResponse is the JSON shape returned by /v1/stat.
Reachable / Reason (v1.2 additive) populate only when the requested path identifies a configured library root AND that root is currently unreachable. For descendants of a root, or for healthy roots, both fields are omitted. See Entry's docblock for the rationale.
type UPnPRoutingLookup ¶ added in v0.1.6
type UPnPRoutingLookup = upnpproxy.RoutingLookup
UPnPRoutingLookup is the manifest-side query for "is this manifest path actually a UPnP-sourced track?". Production wiring passes a `*manifest.Store` (which implements GetUPnPRouting); tests pass an in-memory stub. Returning (nil, nil) is the explicit "this is a filesystem track, not UPnP" signal — the proxy then doesn't engage.
Type alias to `upnpproxy.RoutingLookup` — same interface, different name preserved for backward compatibility with the existing api wiring (`Server.upnpRouting` field, `WithUPnPRouting` setter, and the `serveFile` fast-path consumer).
type UPnPServerHostResolver ¶ added in v0.1.6
type UPnPServerHostResolver = upnpproxy.HostResolver
UPnPServerHostResolver returns the live `host:port` for a given UPnP server UDN — the host part of the upstream's URL floats with DHCP, Wi-Fi/hotspot toggles, etc. Production wiring is the SSDP discovery cache. Returns (empty, false) when the server isn't currently reachable — the proxy then surfaces 503 to iOS so the next play tap reconciles cleanly.
Type alias to `upnpproxy.HostResolver` — see UPnPRoutingLookup above.
type UPnPUpstreamPublicProvider ¶ added in v0.1.6
type UPnPUpstreamPublicProvider interface {
// PublicServers returns the operator-configured servers in
// config-declared order, with discovery state + routed-track count
// merged. Called once per /v1/health request — keep the work
// proportional to len(cfg.UPnPUpstream.Servers) (single-digit in
// practice).
//
// `ctx` is the inbound HTTP request's context. Implementations
// SHOULD propagate it through to any downstream database query
// (e.g. the per-server `COUNT(*)` on the routing table) so a
// client disconnect mid-/v1/health cancels the work rather than
// letting it run to completion against a now-dead connection.
// Mirrors how `probeAllRoots` receives `r.Context()` from the
// health handler.
PublicServers(ctx context.Context) []UPnPUpstreamPublicServer
}
UPnPUpstreamPublicProvider is the read interface the api.Server uses to populate HealthResponse.UPnPUpstreamServers. Production wiring is the bridge's upnpUpstreamLifecycle (which already exposes the equivalent admin-side data); tests pass a stub. Nil-safe — when the provider isn't wired (operator hasn't enabled the feature) the field is omitted entirely so the wire shape matches pre-feature bridges.
type UPnPUpstreamPublicServer ¶ added in v0.1.6
type UPnPUpstreamPublicServer struct {
// Name is the operator-configured label (e.g. "Chord 2Go (cards)").
// Always populated — `name` is YAML-required.
Name string `json:"name"`
// ConfiguredUDN is the canonical UPnP identity (`uuid:...`) the
// operator pinned this entry to. Empty for manual-URL entries
// (the bridge keys them on a hashed manualDescriptionURL
// internally; iOS doesn't need that hash).
ConfiguredUDN string `json:"configuredUDN,omitempty"`
// PathPrefix is what the bridge prepends to every ingested
// track's manifest Path so multi-upstream setups don't collide.
// iOS uses this as the membership key for sub-source filtering:
// `Track.path.starts_with(prefix)` identifies tracks routed via
// this upstream. Always populated (defaults to a sanitized form
// of Name when omitted in YAML; the validator fills the default).
PathPrefix string `json:"pathPrefix"`
// FriendlyName is the discovery-time display label from the
// upstream's `<device><friendlyName>` (SSDP/description.xml). The
// operator may know only the UDN — friendlyName lets iOS show the
// device's own name (e.g. "Chord 2Go:2go-ars") instead of just
// the operator's terse label. Empty when the upstream hasn't been
// seen yet on the LAN; iOS falls back to Name in that case.
FriendlyName string `json:"friendlyName,omitempty"`
// RoutedTracks is the count of manifest tracks routed via this
// upstream (`COUNT(*) FROM upnp_track_routing WHERE server_udn = ?`).
// Lets iOS render a "(N tracks via 2Go)" subtitle without an
// extra round-trip. Always populated; zero means the feature is
// wired but no ingest has landed yet (warm-up window or per-server
// error).
RoutedTracks int `json:"routedTracks"`
// Online reports whether this upstream is currently reachable from
// the bridge. For a UDN-configured upstream it's true iff the device
// is presently in the SSDP discovery cache (refreshed every M-SEARCH,
// evicted after ServerTTL ≈ 180s) — so when the operator powers the
// upstream (e.g. a Chord 2Go) OFF, this flips false within the TTL
// window even though the BRIDGE itself stays reachable. Manual-URL
// upstreams aren't in the SSDP cache, so they report true here (their
// real reachability surfaces as a 503 `upnp_server_offline` on fetch).
// iOS uses this to stop offering tracks whose upstream is down instead
// of letting the fetch 503 on tap. ALWAYS emitted (not omitempty —
// `false` is the load-bearing value); a pre-feature bridge omits the
// field, and iOS defaults a missing value to `true` (assume online).
Online bool `json:"online"`
}
UPnPUpstreamPublicServer is the minimal per-upstream entry the bridge advertises on `/v1/health` for iOS UI consumption. See the field on HealthResponse for the rationale on the public-vs-admin split.
type UpdateInfo ¶ added in v0.1.1
type UpdateInfo struct {
LatestVersion string
UpdateAvailable bool
ReleaseNotesURL string
MinClientVersion string
}
UpdateInfo is the wire shape /v1/health embeds. Lives in this package (not internal/updater) so the iOS-facing fields don't import the updater type machinery and tests can hand-craft an UpdaterStatus stub.
type UpdaterStatus ¶ added in v0.1.1
type UpdaterStatus interface {
UpdateInfo() UpdateInfo
}
UpdaterStatus is the optional interface the /v1/health handler uses to populate the additive `latestServerVersion` / `updateAvailable` / `updateReleaseNotesURL` / `minClientVersion` fields. Nil-safe — when `s.updater` is nil those fields stay omitempty-empty and the wire shape matches the pre-Phase-A response exactly.
`internal/updater.Updater` satisfies this interface in production. The `MinClientVersion` returned here is read from `version.MinClientVersion` so it advertises the floor THIS bridge needs from its iOS clients (not the floor a candidate update would require — that's a Phase B / C concern).
type UpscaleCompleteEvent ¶ added in v0.1.3
type UpscaleCompleteEvent struct {
Path string `json:"path"`
VariantID string `json:"variantId"`
SampleRate int `json:"sampleRate"`
BitsPerSample int `json:"bitsPerSample"`
CompletedAt time.Time `json:"completedAt"`
}
UpscaleCompleteEvent is published on the SSE `upscale.complete` topic exactly once per successful transcode-pool job, AFTER the SQLite `UpsertVariant` transaction commits. iOS uses the `path` field to route the event to the in-flight upscale job in `BridgeUpscaleService` via its (shareID, path) → trackID reverse index, then fires a silent delta scan; the manifest reconcile promotes the wand chrome to "Ready" without waiting for the ladder's 8 s first rung.
**Critical**: `Path` must equal the manifest's `Track.path` field byte-for-byte. The bridge serializes it from `JobSpec.SourceLibraryRel` (the same field the manifest scanner uses) — do not reformat, normalise, or transform it anywhere between the worker and this struct. Any drift breaks iOS's constant-time reverse lookup.
Ordering invariant: emitted only AFTER `manifest.Store.UpsertVariant` returns nil — the SQLite transaction is committed at that point, so the iOS-triggered manifest re-sync that follows is guaranteed to see the new `track_variants` row and bumped `tracks.indexed_at`.
type UpscaleDeletedEvent ¶ added in v0.1.3
type UpscaleDeletedEvent struct {
Paths []string `json:"paths"`
VariantIDs []string `json:"variantIds"`
DeletedAt time.Time `json:"deletedAt"`
}
UpscaleDeletedEvent is published on the SSE `upscale.deleted` topic every time one or more variant rows disappear from `track_variants`, from any of three triggers:
- Operator-driven DELETE /v1/upscale/variants (admin UI / iOS-future).
- Reactive serve-side cleanup: a `/v1/download?variant=...` GET observes the sidecar missing on disk, drops the DB row, and publishes the event so iOS clients reconcile immediately.
- Periodic integrity sweep (1 h default ticker): the `internal/integrity.Watcher` walks `track_variants`, stats each sidecar, batches the misses into a single event per sweep.
iOS uses `Paths` to route the event to the relevant `Track` rows via the (shareID, Track.path) → trackID reverse index in `BridgeUpscaleService.pathIndex`. For each match it filters the `Track.bridgeVariants` JSON blob to remove the deleted `VariantIDs` and saves; in the playback-fallback case (currently rendering the upscaled variant of one of the listed tracks) it captures elapsed, reloads at the source path, and resumes — same machinery the user-driven variant-switch uses.
**Critical**: `Paths[i]` must equal the manifest's `Track.path` field byte-for-byte — same contract as UpscaleCompleteEvent. The bridge serialises from the variant rows' `source_path` column (which the manifest scanner also wrote); do not reformat, normalise, or transform between the SQL projection and this struct. Any drift breaks iOS's constant-time reverse lookup.
`Paths` and `VariantIDs` are positional: a single event may carry rows belonging to multiple source paths AND multiple variant IDs per source path; the two slices are NOT zipped 1:1 — iOS treats `VariantIDs` as the set of IDs that disappeared somewhere in the `Paths` set. Pre-feature bridges never emit this event.
`DeletedAt` is the wall-clock timestamp of the publish, mostly for admin-console / debug surfaces; iOS doesn't gate behaviour on it.
type UpscaleEnqueuer ¶ added in v0.1.2
type UpscaleEnqueuer interface {
EnqueueOne(libraryRelativePath string) error
// EnqueueOptimize is the CarPlay-targeted parallel path:
// downsamples hi-res PCM sources to 16-bit / 44.1 or 48 kHz
// (family-preserving). Same error taxonomy as EnqueueOne —
// ErrUpscaleQueueFull / ErrUpscaleSourceMissing /
// ErrUpscaleIneligible — so the handler's switch arm reuses
// without branching. Always-present on the interface so the
// handler can route on `UpscaleRequest.Kind` without a
// downcast / capability probe.
EnqueueOptimize(libraryRelativePath string) error
}
UpscaleEnqueuer is the interface POST /v1/upscale uses to hand off a track or folder of tracks to the long-lived transcode worker pool. Mirrors the api package's other "feature glue" interfaces (ManifestProvider, MBIDProbe, VariantStore) so the api package doesn't import internal/transcode directly.
The cmd/bridge wiring constructs an adapter that wraps a transcode.Pool. Nil-safe: when WithUpscaleEnqueuer wasn't called (or was called with nil because the feature gate / sox probe failed), the handler returns 503 `upscale_disabled`.
`libraryRelativePath` is the wire-shape path the manifest emits and that iOS sends back. The adapter resolves it to an absolute on-disk path via `bridgefs.Resolver` (canonical resolution — handles single-root + multi-root layouts uniformly), stat()s the source for freshness fields, and hands a `transcode.JobSpec` to the Pool.
Errors:
- `ErrUpscaleQueueFull` (typed): all queue slots taken; handler returns 503 `queue_full`.
- `ErrUpscaleSourceMissing` (typed): the path resolves but stat fails; handler counts as "rejected" with a clear log line. Not a 5xx because a missing source is a scanner-reconciliation issue, not a server fault.
- `ErrUpscaleIneligible` (typed): the source is DSD, already at/above target rate, or already has a fresh sidecar. Counted as "rejected" silently — the user can't take action.
- any other error: treated as a server fault; the handler logs and counts as rejected.
type UpscalePoolStats ¶ added in v0.1.2
type UpscalePoolStats struct {
Workers int `json:"workers"`
QueueCap int `json:"queueCap"`
QueueLen int `json:"queueLen"`
Inflight int `json:"inflight"`
Enqueued uint64 `json:"enqueued"`
Done uint64 `json:"done"`
Failed uint64 `json:"failed"`
}
UpscalePoolStats mirrors `transcode.PoolStats` field-for-field but lives here so the api package compiles without importing internal/transcode. The wiring closure in cmd/bridge/main.go translates between the two value types — same indirection the admin package already uses.
type UpscaleRequest ¶ added in v0.1.2
UpscaleRequest is the wire shape POST /v1/upscale accepts. `path` may be a track file or a folder; the handler stat()s to decide. Folder requests recursively enqueue every regular file under the folder; the enqueuer's per-track eligibility check filters non-PCM / already-at-target / already-cached candidates.
`kind` discriminates upscale (default, "" or "upscale" — full back-compat for pre-v1.x iOS clients that omit the field) from optimize (CarPlay-targeted downsample, "optimize"). The handler routes per-candidate without changing the rest of the request shape — additive wire change, no ProtocolVersion bump.
type UpscaleResponse ¶ added in v0.1.2
type UpscaleResponse struct {
Enqueued int `json:"enqueued"`
Rejected int `json:"rejected,omitempty"`
Eligible int `json:"eligible,omitempty"`
QueueFull bool `json:"queueFull,omitempty"`
}
UpscaleResponse is the wire shape POST /v1/upscale returns on 202 Accepted.
- `enqueued`: number of jobs that successfully landed on the worker pool's queue.
- `rejected`: number of candidates that were considered but not queued (queue full, ineligible, source missing).
- `eligible`: total number of regular files the handler considered (folder walk surface).
- `queueFull`: true when at least one rejection was due to queue capacity (vs. eligibility / missing-source). iOS surfaces this in the toast so the user knows to retry.
When `enqueued == 0 && queueFull`, the handler returns 503 `queue_full` instead of 202 — every candidate that was eligible bounced.
type UpscaleStats ¶ added in v0.1.2
type UpscaleStats struct {
Enabled bool `json:"enabled"`
SoxAvailable *bool `json:"soxAvailable,omitempty"`
Pool *UpscalePoolStats `json:"pool,omitempty"`
CachedVariants int `json:"cachedVariants"`
CachedBytes int64 `json:"cachedBytes"`
}
UpscaleStats is the wire shape GET /v1/upscale/stats returns.
Field-for-field compatible with the admin /api/upscale/stats payload — the JSON shapes intentionally match so an operator inspecting the bridge with `curl -k https://…/v1/upscale/stats` (with bearer token) sees the same body the Settings tile is already showing them. iOS uses it for the "Upscaling" management section inside BridgeEditorView (counts of cached / in-flight / failed jobs).
- Enabled mirrors live runtime state, NOT the persisted `cfg.Upscale.Enabled` flag. The two diverge in two real cases the admin handler documents (see CLAUDE.md / PR #110): (a) startup demoted the feature when sox-precheck failed even though the flag was on; (b) the operator just PATCHed the flag off but the long-lived Pool is still alive until restart. Both surface as `pool == nil` from the wiring closure, and we report `enabled = (pool != nil)` so the iOS-facing /v1/health.upscaleEnabled and this endpoint agree about what "active" means.
- Pool is omitted when the feature is off (no pool to query).
- SoxAvailable is omitted when the test harness didn't wire a precheck closure.
type UpscaleStatsProvider ¶ added in v0.1.2
type UpscaleStatsProvider interface {
// UpscaleStatsSnapshot returns the live runtime+on-disk
// snapshot for the upscale feature. The error return surfaces
// transient/timeout failures (notably `context.DeadlineExceeded`
// from the /v1/upscale/stats handler's 2s ctx-timeout) so the
// handler can emit a 5xx instead of silently returning the
// zero-value UpscaleStats. Pre-PR-218 the signature was
// error-less and a wedged DB just produced an all-zeros body
// the client misread as "feature off". Gemini HIGH on PR #218.
UpscaleStatsSnapshot(ctx context.Context) (UpscaleStats, error)
}
UpscaleStatsProvider is the interface GET /v1/upscale/stats reads. Mirrors the admin tile's data sources but lives here so the api package doesn't import internal/admin or internal/transcode.
The cmd/bridge wiring constructs an adapter that returns the same pool snapshot the admin handler consumes — the two surfaces stay in lockstep.
Nil-safe: when WithUpscaleStats wasn't called (test harness, or builds without the upscale wiring), the handler returns the zero-value UpscaleStats — `enabled=false, cachedVariants=0, cachedBytes=0, pool=nil, soxAvailable=nil`. iOS renders that as "feature off" without distinguishing a missing endpoint from a disabled feature, which matches the /v1/health.upscaleEnabled contract iOS already gates on.
type VariantDeleteRequest ¶ added in v0.1.3
type VariantDeleteRequest struct {
// All true → delete every variant in the manifest. Mutually
// exclusive with `Prefix` / `Path`. The HTTP handler additionally
// requires `?confirm=true` for this shape; that gate lives in
// the parsing layer because admin / future callers may have
// their own confirmation surface.
All bool
// Prefix non-empty → delete all variants whose source path is
// under this path prefix. Must be a cleaned relative path
// (validateRelativePath).
Prefix string
// Path non-empty → delete variants for one exact source path.
// Must be a cleaned relative path.
Path string
// Kind narrows the deletion to one variant kind: "" (default,
// back-compat — deletes ALL kinds matching the path scope),
// "upscale" (only `upscaled-*` variants), or "optimize" (only
// `optimized-*` variants). The PR feat/library-inspector-tiles
// added the kind-aware admin DELETE so per-kind buttons in the
// inspector drawer can clear one kind without wiping the other.
// Validated at the parsing layer; invalid values rejected with
// `bad_request` before reaching here. Empty preserves the
// pre-feature behaviour so a stray external `DELETE
// /v1/upscale/variants?prefix=…` keeps working as it did.
Kind string
}
VariantDeleteRequest is the parsed, validated input to `RunVariantDelete`. The HTTP handler builds this from query parameters; the admin-console wrapper builds it from its own URL parsing. Exactly one of `All`, `Prefix`, `Path` is set on any valid request — see `(*Server).validateVariantDeleteRequest` for the validation surface that produces it.
type VariantDeleteResponse ¶ added in v0.1.3
type VariantDeleteResponse struct {
DeletedCount int `json:"deletedCount"`
FreedBytes int64 `json:"freedBytes"`
DeletedPaths []string `json:"deletedPaths"`
}
VariantDeleteResponse is the wire shape returned on success. DeletedPaths is the set of source paths that had at least one variant removed — iOS uses this to reconcile its local `Track.bridgeVariants` without waiting for a full delta-sync (alongside the upscale.deleted SSE event, which is the primary fan-out path).
type VariantDeleter ¶ added in v0.1.3
type VariantDeleter interface {
AllVariants(ctx context.Context) ([]VariantSummary, error)
ListVariantsByPathPrefix(ctx context.Context, prefix string) ([]VariantSummary, error)
ListVariantsForPath(ctx context.Context, sourcePath string) ([]VariantSummary, error)
DeleteVariant(ctx context.Context, sourcePath, variantID string) error
}
VariantDeleter is the interface the DELETE /v1/upscale/variants handler uses to enumerate and remove variant rows. cmd/bridge wires a thin adapter around manifest.Store; tests stub directly. Nil-safe — when nil the handler returns 404 variant_not_found (same shape pre-v1.2 bridges produce for the unsupported case).
Three list shapes mirror the three query-param shapes: empty prefix to AllVariants, non-empty prefix to ListVariantsByPathPrefix, exact path to ListVariantsForPath. The handler picks based on query-param parsing; this interface stays narrow.
type VariantRecord ¶ added in v0.1.2
type VariantRecord struct {
SourcePath string
VariantID string
SidecarPath string
SourceMTimeNS int64
SourceSize int64
}
VariantRecord is the minimum metadata the api needs to (a) decide freshness and (b) serve the sidecar bytes. Mirrors the on-disk columns the manifest package writes. Pointer return from LookupVariant lets `nil` distinguish "no such row" from "row exists but freshness fails" — caller can still surface a targeted 404 vs 410 from the same return shape.
`SourcePath` and `VariantID` are the CANONICAL row values (case-preserved, matching the SwiftData `Track.path` byte-for- byte). LookupVariant resolves case-insensitively (iOS sends `share.normalize`d paths) but the returned record carries the canonical form so the reactive cleanup path in `serveVariant` can delete the right row AND emit an SSE `upscale.deleted` event whose paths match `Track.path` for iOS-side reverse-index resolution. CodeRabbit Major on PR #209 caught this: without the canonical values, a case-folded request would silently no-op DeleteVariant while the SSE fired with the wrong-case path.
type VariantStore ¶ added in v0.1.2
type VariantStore interface {
LookupVariant(ctx context.Context, sourcePath, variantID string) (*VariantRecord, error)
}
VariantStore is the optional interface the `?variant=<id>` branch of /v1/download uses to look up a variant's cached metadata. Nil-safe — when `s.variantStore` is nil the download handler returns 404 `variant_not_found` for any request that carries the parameter (matches the "feature unavailable" behaviour iOS already handles for pre-v1.2 bridges).
**Freshness check happens in the api**, not here. The api has the canonical `bridgefs.Resolver` (path validation + traversal guard already exercised on every other handler), and uses the `os.FileInfo` it already stat'd for the source file in serveFile. This avoids a duplicate path-resolution code path in the manifest package — which Gemini bot review on PR #108 identified as broken in single-root mode (the manifest's hand-rolled basename-stripping assumed multi-root layout) AND flagged by CodeQL as "uncontrolled data used in path expression". Both go away when the api owns the source-side stat call and only asks the variant store for "do you have this row, and what's its recorded provenance".
`internal/manifest.Provider` satisfies this in production via a thin LookupVariant wrapper around the SQLite row read.
type VariantSummary ¶ added in v0.1.3
type VariantSummary struct {
SourcePath string
VariantID string
SidecarPath string
SizeBytes int64
}
VariantSummary is the api-package-local view of a track_variants row. Carries only the columns the delete handler reads — the `SidecarPath` (for `os.Remove`), the composite key, and `SizeBytes` (for the freedBytes response field). Mirrors how `VariantRecord` projects the columns the download handler needs.
Source Files
¶
- api.go
- artwork.go
- atlas_harvest.go
- atlas_meta.go
- diagnostics.go
- event_broker.go
- events.go
- files.go
- gzip.go
- history.go
- middleware.go
- pairing.go
- pairing_ratelimit.go
- playlist_covers.go
- playlists.go
- query.go
- ratelimit.go
- reachability.go
- renderers.go
- route_classification.go
- smartplaylists.go
- upnp_proxy.go
- upscale.go
- upscale_batch.go
- upscale_delete.go
- upscale_deleted_event.go
- upscale_events.go
- upscale_stats.go
- waveform.go