Skip to content

Releases: dhkatz/HorizonRadio

v0.6.0

Choose a tag to compare

@dhkatz dhkatz released this 16 Jun 01:16
3cc8a4b

Horizon Radio 0.6.0

This release expands Horizon Radio from a YouTube and local-file player into a multi-source music app with built-in search, adds internet radio, and significantly improves how track metadata is resolved. It also fixes a Spotify playback bug.

Spotify as a first-class source

Spotify tracks, playlists, and albums now play directly inside the app's queue, Quick Play, and Mixes, interleaved with YouTube and local files. The app drives playback track by track through librespot and the Spotify Web API, so you get full transport controls (play, pause, next, previous) and seeking on Spotify items.

There are two Spotify entries, and they serve different needs:

Spotify tracks, playlists, and albums now play directly inside the app's queue, Quick Play, and Mixes, interleaved with YouTube and local files. The app drives playback track by track through librespot and the Spotify Web API, so you get full transport controls (play, pause, next, previous) and seeking on Spotify items.

There are two Spotify entries, and they serve different needs:

  • Spotify Connect is the existing zero-config cast receiver. It requires no setup. You cast to it from the Spotify app on your phone or desktop, and Spotify decides what plays.
  • Spotify (the new driven source) is the one that participates in the queue and mixes. It requires your own Spotify developer Client ID.

To set up the driven Spotify source:

  1. Create an app in the Spotify Developer Dashboard and copy its Client ID. No client secret is needed or stored.
  2. Open the Sources tab, find Spotify, and enter the Client ID.
  3. Click Connect. A browser window opens for login (PKCE flow). After you authorize, the app stores a refresh token, encrypted with Windows DPAPI for the current user.
  4. Once connected, Spotify results appear in search and Spotify items can be added to the queue and mixes.

A Spotify Premium account is required for driven playback, which is a librespot requirement, not an app choice.

Unified search

A search box now lives in the title bar. Typing runs a live, debounced search across all connected sources and shows results in a dropdown. Submitting opens a full results page that reuses those same results. Every result has an Add action and a Play action.

Current search sources are Spotify and YouTube. Results from different sources that look like the same recording are merged conservatively, using title, artist, and track length, so covers, remixes, and different-length versions are kept separate rather than incorrectly folded together. Each result row shows which source it came from, includes a per-source picker so you can choose where to play it from, and the page has per-source filter chips.

Setup notes:

  • Spotify results require the driven Spotify source to be connected (see above).
  • YouTube results require yt-dlp to be installed. Install it from the Tools tab if it is not already present. If yt-dlp is missing, YouTube simply returns no results rather than failing the whole search.

Internet radio

Internet radio is a new source that is searchable and mixable like the others. You can find stations two ways:

  • Browse the radio-browser.info directory through the unified search box.
  • Paste a direct stream URL.

While a station plays, the app reads the stream's inline ICY metadata and updates the now-playing card for each song. When no cover art can be resolved for the current song, the station logo is used as a fallback.

Setup note: internet radio decodes through ffmpeg, so ffmpeg must be installed. Install it from the Tools tab if needed. Installed tools are now shared across all sources, so an ffmpeg or yt-dlp install done for one source is available to the others.

Metadata resolution improvements

Now-playing metadata (canonical title, artist, and square album art) is resolved through a stronger pipeline:

  • New keyless providers were added: Apple/iTunes and VocaDB. These join MusicBrainz and the optional Spotify provider. VocaDB in particular improves coverage for Vocaloid and doujin tracks.
  • Matching is more accurate. Title-first scoring tolerates spacing and capitalization differences, rejects results that share a title but have a different artist, and handles CJK, fullwidth, and reversed title formats common on radio streams.
  • Ambiguous stream titles now produce several candidate interpretations, and the resolver keeps only the one a catalog can confirm. If nothing confirms, it falls back to the station logo rather than attaching incorrect art. Over a multi-hour radio session this reduced missing album art from roughly 22 percent of songs to near zero, while specifically avoiding wrong art on widely-covered titles.
  • Metadata results that resolved without art are retried when they go stale, either by time or when matching logic is updated, so an improvement reaches songs you have already seen instead of serving a permanent miss.

Most of this is automatic. Provider order and per-field overrides remain configurable in the Metadata tab.

Optional local title model

For genuinely freeform stream titles that the rule-based parser cannot split into artist and title, you can enable a small local model that runs on your machine and proposes candidates. Its output goes through the same catalog-validation step, so an incorrect guess from the model cannot reach the UI on its own.

To set it up:

  1. Open the Tools tab and download the title model (Qwen2.5-0.5B, CPU based).
  2. Choose a run policy in the metadata configuration:
  • Off: the model never runs.
  • Escalate (default): the model runs only when the rule-based parse is low confidence, and adds fallback candidates.
  • Always: the model runs on every title and is used as the primary interpretation.

This is entirely opt-in and off by default unless you download and enable it.

Metadata diagnostics

An optional diagnostics trace can be enabled in the About tab under Diagnostics (or via the HZN_META_TRACE environment variable). When on, it records each song's raw title, the rule-based parse, any model extraction and its timing, every provider's scored results, and the final resolved track to a JSONL file. It is off by default. The intended use is to turn it on, reproduce a metadata problem, and attach the file to a report. The trace is detailed enough to replay a matching decision offline.

Fixes

  • The first Spotify track queued after a fresh app start now plays reliably. Previously, on a cold start the play command could reach Spotify's cloud before librespot's Connect session was ready, so the track produced no audio and did not advance until you manually skipped to the next item. The initial play is now confirmed and re-issued until playback actually begins, and a play that genuinely fails advances the queue instead of stalling.

Other changes

  • Installed tools (ffmpeg, yt-dlp) are now resolved through a single shared resolver rather than being configured per source, and yt-dlp and Spotify pick up a mid-session install without a restart.
  • The Spotify Web API client was updated from 7.2.1 to 7.4.2 to use the current playlist items endpoint, after Spotify removed the older playlist tracks route in its March 2026 API migration.

What's Changed

  • feat(spotify): first-class, mixable content source by @dhkatz in #28
  • feat(search): unified search with a title-bar search box (Spotify) by @dhkatz in #29
  • feat(search): YouTube as a second search source + multi-source UX by @dhkatz in #30
  • feat(radio): internet radio source + multilingual metadata enrichment by @dhkatz in #31
  • feat(metadata): optional local title model, diagnostics, and live-found matching fixes by @dhkatz in #32
  • feat(metadata): recover far more radio album art + replayable trace by @dhkatz in #33
  • fix(spotify): retry initial play so the first queued track plays by @dhkatz in #34

Full Changelog: v0.5.0...v0.6.0

v0.5.0

Choose a tag to compare

@dhkatz dhkatz released this 12 Jun 22:55
9d3e4da

Horizon Radio 0.5.0

This one's all about the listening experience: a proper queue, and track info that finally looks right.

— Global Queue —
There's now a Spotify-style queue you can pop open on the right side of the app.
• Quick Play any song, playlist, or folder on the fly with the + button. It plays next, then your mix picks right back up, so the radio never goes quiet.
• Reorder, remove, or jump straight to any track (double-click it, or hit play on its artwork).
• See exactly what's coming up. Your queued one-offs and the upcoming tracks from your current mix.

— Much better track info —
Song titles, artists, and album art now show up properly across the app. This includes: in the queue, in your Mixes, and on the Now Playing display.
• YouTube tracks now grab the real song title and artist (not the channel name) when YouTube knows it, and fall back to smart "Artist – Title" detection otherwise.
• Proper album art is fetched automatically from MusicBrainz, on by default, no setup needed, with optional Spotify support.
• A redesigned Metadata tab lets you choose which sources to use, set their priority order, and even pin a specific source per field (e.g. always use Spotify for album art).

What's Changed

  • feat: global queue with a toggleable right sidebar by @dhkatz in #24
  • feat: metadata pipeline — ordered providers, per-field overrides, source normalization, list enrichment by @dhkatz in #25
  • feat(queue): metadata-ahead + queue item UX pass by @dhkatz in #26
  • chore(release): prepare 0.5.0 by @dhkatz in #27

Full Changelog: v0.4.0...v0.5.0

v0.4.0

Choose a tag to compare

@dhkatz dhkatz released this 12 Jun 11:10
a61fbbb

What's Changed

  • feat: cross-source mixes (replacing profiles), quick play, and a global station control by @dhkatz in #22

Full Changelog: v0.3.1...v0.4.0

v0.3.1

Choose a tag to compare

@dhkatz dhkatz released this 12 Jun 06:43
a0e4a9c

What's Changed

  • feat(controls): add input-binding tab for keyboard, mouse, and controllers by @dhkatz in #8
  • feat(profiles): add saved source profiles with quick-switch by @dhkatz in #9
  • feat(controls): bind controls and events to profile switching by @dhkatz in #10
  • feat(audio): in-app test playback with local output routing by @dhkatz in #11
  • feat(ui): persistent bottom player bar with progress/seek by @dhkatz in #12
  • feat(tools): on-demand tool freshness, missing-tool prompt, and librespot un-bundling by @dhkatz in #16
  • feat(update): app version stamping, update check, and About tab by @dhkatz in #17

Full Changelog: v0.2.0...v0.3.1

v0.3.0

v0.3.0 Pre-release
Pre-release

Choose a tag to compare

@dhkatz dhkatz released this 12 Jun 05:23
39ef9da

What's Changed

  • feat(controls): add input-binding tab for keyboard, mouse, and controllers by @dhkatz in #8
  • feat(profiles): add saved source profiles with quick-switch by @dhkatz in #9
  • feat(controls): bind controls and events to profile switching by @dhkatz in #10
  • feat(audio): in-app test playback with local output routing by @dhkatz in #11
  • feat(ui): persistent bottom player bar with progress/seek by @dhkatz in #12
  • build: migrate C++ to CMake and enable Linux/macOS cross-compile by @dhkatz in #13
  • feat(tools): version-addressed tool blobstore, manifest, and librespot installer by @dhkatz in #14
  • fix(ci): make the nightly workflow's skip-guard succeed on first run by @dhkatz in #15
  • feat(tools): on-demand tool freshness, missing-tool prompt, and librespot un-bundling by @dhkatz in #16
  • feat(update): app version stamping, update check, and About tab by @dhkatz in #17
  • chore(release): prepare 0.3.0 by @dhkatz in #18

Full Changelog: v0.2.0...v0.3.0

Tool Binaries

Tool Binaries Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 11 Jun 20:22
8ac4af7

Permanent, append-only store of tools Horizon Radio builds (see docs/tool-provisioning.md). Do not delete assets — shipped app builds point here by URL.

Nightly (20260611)

Nightly (20260611) Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 11 Jun 22:22

Rolling nightly build of main. Ephemeral — replaced by the next nightly.

commit: 6d2f4df
version: 0.2.0-nightly.20260611+6d2f4df

v0.2.0

Choose a tag to compare

@dhkatz dhkatz released this 01 Jun 20:09
1b99b92

What's Changed

  • ci: enforce Conventional Commit PR titles and add contributing guardrails by @dhkatz in #3
  • feat: add shuffle toggle to Now Playing for local + YouTube sources by @dhkatz in #4
  • chore(release): prep v0.2.0 by @dhkatz in #5

New Contributors

  • @dhkatz made their first contribution in #3

Full Changelog: v0.1.1...v0.2.0

v0.1.1

Choose a tag to compare

@dhkatz dhkatz released this 28 May 12:43

Full Changelog: v0.1.0...v0.1.1

v0.1.0

Choose a tag to compare

@dhkatz dhkatz released this 28 May 12:12