Commit Graph

15 Commits

Author SHA1 Message Date
9ddfd21685 feat(audio): auto-advance to next episode in source queue on track end
When a track reaches natural EOF (player alive, no stream error), play the
next episode from the source that started it — search results, show, or
Feed — and stop at the end of the list. A crashed/killed daemon or failed
stream never auto-advances.

- add audio-queue.ts: pure next/prev selection from the navigation source
- audio-player: expose getPlaybackError() to distinguish EOF from failure
- useAudio: finalizeTrackEnd(autoAdvance) wiring, re-selecting the current
  episode no longer reloads from stale saved progress
- tests: audio-queue units, auto-advance integration (real mpv + local
  WAVs over HTTP), backend re-select no-reload test
2026-08-17 20:50:05 -04:00
badbc6a037 fix(audio): recover playback when the mpv daemon dies or restarts 2026-08-13 17:46:30 -04:00
bd7d988741 fix(cover): art reaches Now Playing on first play (restore + cold cache)
Two gaps left cover art missing for the most common play paths:
- Boot-restore preloaded the episode with cachedCoverPath??undefined racing
  the fire-and-forget prefetch; a cold preload + fast-path play re-applied
  art via video-add, which produces a NON-albumart track (verified) that
  mpv's Now Playing artwork logic ignores. load() now awaits the bounded
  fetch (covers ~300ms, 8s cap) so the cover is present at load-time.
- Removed the dead video-add re-apply (fast path + addCoverArt method +
  interface) and the play() late-add fallback; a cold-cache play now
  prefetches for next time instead.
- FeedPage prefetches covers for the focus window (single-flight, cached)
  so ordinary plays land on a warm cache.
Verified: preload with awaited cover -> albumart track present (2 tracks).
2026-08-12 10:22:49 -04:00
33af131b77 revert(macos): drop PodTui.app / bundled-mpv Now Playing attribution
The pinned mpv binary breaks on brew ffmpeg major drift (libavcodec.62 vs
.63 shipped a dead player); making it self-contained costs ~100MB for an
app icon. Removing the whole machinery: build.ts app-bundle assembly,
audio-player bundled-binary resolver + probe (spawns PATH mpv again),
CI mpv install + bundle smoke checks, AppIcon.icns, README note.
Cover-art staging (curl + --cover-art-files) is unrelated and stays.
2026-08-11 23:09:44 -04:00
20336ea716 feat(audio): rebuild playback + visualization on resident daemon and PCM cache
Two fragility points, rebuilt at the root:

Playback: one resident mpv daemon (--idle --keep-open) with a persistent
IPC connection and observe_property state instead of spawn-per-episode and
connect-per-poll. Play/pause/seek are sub-ms commands; time-pos pushes at
~20Hz; external pauses arrive as events. Boot session restore preloads the
episode paused (loadfile + paused time-pos seek, since mpv defers --start
stream work until playback) so first Play is a ~400ms unpause instead of a
cold 4.3s open+seek. Load ops are mutex-serialized so a raced preload
cannot clobber an in-flight play.

Data throttling: mpv demuxer cache capped (cache-secs=90, max-bytes=40MiB)
so a paused preload no longer races to its 150MiB default (measured
45.7MB/12s); decoder paced at 4x realtime instead of 84x so playback start
isn't starved by the visualizer ripping the whole episode.

Visualization: replaced the paced-ring reader (AudioStreamReader) with a
position-indexed PCM cache (audio-pcm-cache). ffmpeg fills a cache indexed
by absolute playback time; reads at the player position are always exact.
Pause freezes the render loop, resume re-arms it — no coverage guessing,
no clamped-buffer freeze (the pause->broken-waveform->freeze bug). Seeks
and speed changes need no pipeline restarts; uncovered reads return empty
and the last frame holds.

Cover art: persistent per-URL disk cache under XDG cache dir; play() no
longer awaits a curl subprocess (up to 8s). Cache hit = one stat; misses
apply late via mpv video-add.

Test suite: 161 pass. New tests pin the position-index contract (sample-
exact window reads, hold-on-uncovered, pause-keeps-cache, seek segments),
the daemon contract (play/pause/resume/seek/stop, preload fast path,
EOF->replay), and cover cache/single-flight/404.
2026-08-11 19:54:05 -04:00
3388757185 fix(macos): hard-fail bundle without mpv, PATH fallback, CI mpv install
- build.ts: darwin compile now exits 1 when mpv is absent — CI can no
  longer ship a PodTui.app without its bundled player (0.4.0 did, killing
  Now Playing attribution).
- audio-player.ts: the bundled mpv is probed (--version) at first resolve;
  it links against brew's dylibs, and a Homebrew ffmpeg major upgrade can
  break it — fall back to PATH mpv so audio survives (icon degrades to
  blank instead of playback dying). Probed once per process.
- release.yml: brew install mpv on darwin runners; smoke test now asserts
  the tarball's PodTui.app has a launchable mpv signed with the
  com.mikefreno.podtui identifier.
2026-08-11 13:37:43 -04:00
a9589e7686 feat(macos): ship PodTui.app bundle so Now Playing shows our icon
mpv owns the macOS Now Playing session (it plays the audio), and an
unbundled binary renders as a blank placeholder for the source-app icon.
macOS has no public API for a third party to claim session ownership
(MPNowPlayingSession is iOS-only; the private MRMediaRemoteSetNowPlayingApplication
was removed from the shared cache), so instead we make the OWNING process
carry our bundle: the darwin tarball now includes PodTui.app with mpv copied
into Contents/MacOS. AudioPlayer resolves the sibling mpv first (falling
back to PATH), LaunchServices attributes the process to com.mikefreno.podtui,
and Control Center shows the PodTui icon + name with podcast cover art.
AppIcon.icns generated from the Xcode icon-composer exports.
2026-08-10 17:56:54 -04:00
5dce21c038 feat(media): expose podcast name + cover art to system Now Playing
- mpv: --force-media-title '<podcast> — <episode>' so macOS Now Playing shows
  the podcast name instead of the download-hash filename (mediaTitle PlayOption)
- media registry: artist is now the human podcast title (customName ||
  podcast.title), falling back to podcastId
- downloads: write the podcast cover as a <base>.jpg sibling so mpv's
  cover-art-auto=exact picks it up for artwork; delete it with the download
2026-08-10 16:50:08 -04:00
19eae4fd5a feat: sync waveform to live player position (mpv time-pos, position-window reads, smooth clock) 2026-08-10 15:51:02 -04:00
91a831c5f9 audio playback fixes 2026-08-07 19:28:57 -04:00
85cb9fba26 ui cleanup 2026-08-07 13:38:32 -04:00
d8f11040bc start revive 2026-07-30 21:25:03 -04:00
2e323d283f needs testing 2026-02-07 00:45:14 -05:00
0e4f47323f mulitmedia pass, downloads 2026-02-06 00:00:15 -05:00
f3344fbed2 start player 2026-02-05 18:42:56 -05:00