681 Commits

Author SHA1 Message Date
Alex Six f10ceafc4b feat: add nix support (#336) 2026-08-22 09:44:05 +02:00
Bjarne Øverli 4222ddb36c fix(ytmusic): cancel cookie loads on shutdown 2026-08-20 23:17:18 +02:00
Bjarne Øverli c11504d1ba fix(ui): cancel stale Spotify playlist lookups 2026-08-20 23:17:18 +02:00
Bjarne Øverli 9edbcc2107 fix(ytmusic): reject stale cookie cache writes 2026-08-20 23:17:18 +02:00
Bjarne Øverli f3a7d643f3 fix(ytmusic): bound cookie playlist loads 2026-08-20 23:17:18 +02:00
Bjarne Øverli 5e3fc47e58 fix(ui): cancel abandoned Spotify album loads 2026-08-20 23:17:17 +02:00
Bjarne Øverli edc9399254 fix(ytmusic): scope playlist classifications 2026-08-20 23:17:17 +02:00
Bjarne Øverli 13d7356196 fix(ytmusic): bind cache scope to session 2026-08-20 23:17:17 +02:00
Bjarne Øverli 054dc82c6f fix(release): install Windows runtime archive 2026-08-20 23:17:17 +02:00
Bjarne Øverli 30d03b0316 fix(release): build mpg123 static archive 2026-08-20 23:17:17 +02:00
Bjarne Øverli afc6e6d328 fix(spotify): avoid unsafe decoder cleanup 2026-08-20 23:17:17 +02:00
Bjarne Øverli cce9b4aaad fix(ui): keep Spotify errors within scroll budget 2026-08-20 23:17:17 +02:00
Bjarne Øverli 0a1842097f fix(ytmusic): preserve playlist batch boundaries 2026-08-20 23:17:16 +02:00
Bjarne Øverli 724537d3b4 fix(ytmusic): isolate cache data by account 2026-08-20 23:17:16 +02:00
Bjarne Øverli a4bd338e24 fix(release): bundle Windows runtime DLLs 2026-08-20 23:17:16 +02:00
Bjarne Øverli 98b00f8c53 docs(site): update Windows Spotify support 2026-08-20 23:17:16 +02:00
Bjarne Øverli 95a63ac7d7 fix(setup): allow YouTube OAuth with cookies 2026-08-20 23:17:16 +02:00
Bjarne Øverli d894e0d32f fix(packaging): declare mpg123 dependency 2026-08-20 23:17:16 +02:00
Bjarne Øverli 5b6288f1e1 fix(ui): settle paused visualizer state 2026-08-20 23:17:16 +02:00
Bjarne Øverli 86d836cffc fix(ui): keep Spotify search feedback visible 2026-08-20 23:17:15 +02:00
Bjarne Øverli d11017c121 fix(ytmusic): batch playlists and cancel searches 2026-08-20 23:17:15 +02:00
Bjarne Øverli 755dcf7587 fix(ytmusic): scope caches by authentication 2026-08-20 23:17:15 +02:00
Bjarne Øverli 983641bbe2 fix(plex): respect returned playlist page size 2026-08-20 23:17:15 +02:00
Bjarne Øverli 10894b346c fix(spotify): close decoder resources 2026-08-20 23:17:15 +02:00
Bjarne Øverli 7cc5cdca4e fix(ui): ignore canceled Spotify album loads 2026-08-20 23:17:15 +02:00
Bjarne Øverli 154b230e38 fix(release): build Spotify codec support 2026-08-20 23:17:15 +02:00
Bjarne Øverli 23967742eb fix(ci): install Spotify codec dependencies 2026-08-20 23:17:14 +02:00
82Sam 757e74424e security: prevent SSH option injection via URL host (#325)
The `sshurl.Parse()` function and `openSSHSource()` function in cliamp were
vulnerable to SSH option injection through the URL host field. Go's `url.Parse`
accepts `-oProxyCommand=...` in the host field, which cliamp's `SSHArgs()`
function appends bare to the ssh argv, allowing arbitrary command execution.

This commit adds defense-in validation at two layers?

1. `internal/sshurl/sshurl.go:50` - Rejects hostnames starting with `-` (the
   `-o` ssh option prefix) or containing `=` (key-value separator) during URL
   parsing.

2. `player/decode.go:101` - Defense-in-depth check in `openSSHSource()` that
   validates the parsed host after `sshurl.Parse()` returns, rejecting the same
   disallowed patterns before constructing the ssh command.

On OpenSSH version above 9.6, an additional `ssh_valid_hostname()` check blocks the
destination hostname `cat -- /x` from being accepted. However, the code-layer
validation is still necessary because:
- The `-oProxyCommand=...` option injection itself is not blocked by OpenSSH's
  hostname check (the option value itself is accepted?)
- On OpenSSH below 9.6 (the vast deployed base: Ubuntu 22.04/24.04, Debian 12,
  RHEL/CentOS, macOS), the code-layer validation is the only protection

Both checks use `strings.HasPrefix(host, "-") || strings.Contains(host, "=")`
to catch the injection vector while still preserving actaul legitimate `ssh://host/path` links.
2026-08-20 23:13:44 +02:00
Praveen Raj 23685fc568 feat(ytmusic): support cookie-backed zero-oauth playlist browsing (#314) 2026-08-20 18:00:30 +02:00
Jankees van Woezik 23b3222a62 feat(spotify): search albums and open them from the results (#318)
* feat(spotify): return albums from search

/v1/search was asked for type=track,episode, so an album could never appear
in the results. Searching an artist returned whichever of their tracks
Spotify ranked highest, and there was no way to reach a record as a record.

Albums are now requested too and lead the results, as an album placeholder:
a Track carrying the album's name, artist and year, marked through
ProviderMeta so the UI can tell it apart without knowing which provider
produced it. Placeholders are not streamable, because spotify:album: URIs
are not something go-librespot can play, so SpotifyProvider now implements
provider.AlbumTrackLoader to expand a chosen one into its tracks.

/v1/albums/{id}/tracks returns simplified track objects without the album
they belong to, so the album's own metadata is fetched once and filled in
on every track for display.

* feat(ui): play a whole album from the search results

Enter, a and q on an album expand it through AlbumTrackLoader and then act
on the full record, matching what they already did for a single track:
Enter starts it now, a appends it, q queues it next. Like playTrackImmediate
they add rather than replace, so a queue built up over an evening survives
picking an album.

The overlay stays open while the expansion runs, showing "Loading album...":
closing it would bump the request generation and drop the response. The
in-flight flag is separate from the playlist fetch's so the results screen
only claims to be loading an album when it is.

p is refused on an album with an explanation. The playlist picker adds one
track, an album is many, and Spotify has no single call to add a record to
a playlist.

* feat(ui): group search results into labeled sections

With albums and tracks in one flat list an album read exactly like one of
its own tracks. The results now carry "Albums" and "Tracks" separators in
the same style the playlist already uses for album headers, and the label
repeats at the top of the viewport when it opens mid-section.

Separators take rows of their own, so scrolling counts rendered rows rather
than results, the way albumSeparatorRows does for the playlist. Without it
the cursor could sit below the bottom of the window.
2026-08-20 18:00:13 +02:00
Impure6529 f05bff18ed feat(plex): browse server playlists alongside albums (#321)
* feat(plex): browse server playlists alongside albums

Expose Plex audio playlists (smart and user-created) in the provider
browse view under a "Playlists" section, ahead of the existing album
list which now sits under an "Albums" section.

Playlist IDs are prefixed with "pl:" so Tracks() can route them to the
server playlists items endpoint instead of the album children endpoint.
Playlist items are paginated (1000 per page) to handle smart playlists
with tens of thousands of tracks. Removes the documented "No Plex
playlists" limitation.

* fix(plex): address review feedback

- Do not cache playlist track results: smart playlists change server-side,
  so Tracks() now bypasses the track cache for "pl:" prefixed IDs and
  always reflects current contents (albums keep caching).
- Wrap new playlist-list and playlist-items errors with operation context.
- Add language identifier to the docs example block (markdownlint MD040).
- Add test asserting playlist tracks are refetched on every load.

---------

Co-authored-by: Petar Salinovic <5ar.salinovic@gmail.com>
2026-08-20 17:59:49 +02:00
Matt Van Horn e0328d982e fix: handle capital keybindings from enhanced keyboard events (#322)
Fixes #286

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-08-20 17:59:30 +02:00
Tharin Fernando 5ca8722bae fix(ui): decay visualizer to rest when paused (#327) 2026-08-20 17:57:40 +02:00
Luguin 49c2cc7983 Enable Spotify support on Windows (#323)
go-librespot gained first-party WASAPI output for Windows in v0.9.0
(cliamp was pinned to v0.7.1, which doesn't compile on Windows). Bump
the dependency and remove the Windows CGO stub so the real provider
builds on all platforms.

CI now installs a MinGW toolchain via MSYS2 and builds/tests with
CGO_ENABLED=1 on windows-2025, including a workaround for an MSYS2
libogg packaging issue where libogg-0.dll's export table is missing
ogg_stream_iovecin even though it's present in the static libogg.a.

Verified locally end-to-end on Windows: native CGO build, full test
suite, and real Spotify Premium playback.

Fixes #299

Not in scope for this PR: release.yml still builds Windows with
CGO_ENABLED=0, so Releases binaries won't include Spotify until that
pipeline is updated separately (needs a packaging decision: bundle the
MSYS2 DLLs or pursue a fully static build).
2026-08-20 17:57:01 +02:00
Cory Shaw 0c9c30d1c2 feat(ipc): report live radio metadata in status (#319)
* feat(ipc): report live radio metadata in status

A stream's playlist entry only carries the station name, so status
reported that with no artist while the TUI showed the actual song.
Stations broadcast now-playing text inline as ICY metadata (the
StreamTitle field of the SHOUTcast/Icecast protocol), which the UI polls
but never reached the IPC response.

Apply the UI's own resolution in both the TUI and daemon status handlers,
and add TrackInfo.StreamTitle (the raw StreamTitle value) plus
TrackInfo.Station so a client can show station, artist/song, and progress
together.

Resolved at the status handlers rather than in ipcTrackInfo/trackInfo,
since only the now-playing track has stream context.

* fix(ipc): keep the stored title when an ICY tag has no song

A StreamTitle of "Artist - " cuts to an empty title, which blanked the
display instead of falling back to the station name. Guard the split in
resolveTrackDisplay (fixing the TUI and Lua events too, not just status)
and in the daemon, and derive Station from the resolved title so both
paths agree.

Also handle a stream title with no artist in the docs jq example.

Addresses review feedback on #319.

* test(ipc): cover the daemon's stream-title contract

Extract the daemon's stream branch into applyStreamTitle so the test
exercises the real implementation rather than a copy of it, and cover the
"Artist - Title", "Artist - ", title-only, no-metadata, and non-stream
cases. statusResponse needs a live player, so the helper is the seam.

Also guard the docs jq example against a missing .track, which printed a
literal "null" when nothing was loaded.

Addresses review feedback on #319.
2026-08-19 20:08:47 +02:00
Cory Shaw cfdcb949dd docs: add herdr-cliamp to community plugins (#320)
* docs: add herdr-cliamp to community plugins

A herdr plugin rather than a Lua one: it floats cliamp in a detached herdr
session so closing the float only detaches, and adds now-playing and
transport keybinds that work from any workspace.

* docs: match the site description to the docs entry

Addresses review feedback on #320.
2026-08-19 20:08:24 +02:00
Bjarne Øverli b9f68568c5 perf: optimize playback and Spotify switching 2026-08-19 19:32:14 +02:00
Bjarne Øverli a491ce2b5b fix(spotify): propagate paged search failures 2026-08-19 07:40:18 +02:00
Jankees van Woezik dc5eaee63d fix(spotify): page search around the Development Mode limit (#315)
/v1/search accepts at most 10 results per request for an app in
Development Mode. Asking for more returns 400 "Invalid limit".
SearchTracks asked for up to 50, and friendlySearchError reported that
400 as "search blocked, your client_id is too new", pointing users at the
shared built-in client_id instead. Search is not blocked for these apps,
only paged smaller.

Measured against a Development Mode app (client credentials token):

  limit=10                          200, 10 tracks
  limit=11 / 20 / 50                400 "Invalid limit"
  no limit parameter                200, default page
  type=track,episode&limit=10       200, 10 tracks + 10 episodes
  limit=10&offset=0/10/20/40        200 for every offset
  GET /v1/tracks/{id}               200
  GET /v1/browse/new-releases       403

So the cap is real but narrow: offset paging past it works, and neither
the episode type nor a missing market parameter is involved. Other
catalog endpoints are restricted for these apps, which is likely where
the "search is blocked" theory came from, but /v1/search is not one of
them and Extended Quota Mode is not needed for it.

Try the requested limit first, so an app with Extended Quota Mode keeps
its single request, and fall back to pages of 10 only after Spotify
rejects the limit. Rewrite the error message to describe the cap, and
correct the claim in docs/spotify.md.

Fixes #205
2026-08-19 07:37:04 +02:00
Faza Iman Imron b5478b926f docs(site): add Discord RPC community plugin (#316)
* docs(site): add Discord RPC community plugin

* docs(plugins): sync Discord RPC community listing
2026-08-19 07:35:05 +02:00
Bjarne Øverli ea464dedc2 fix: make built-in themes accessible 2026-08-18 19:31:35 +02:00
Bjarne Øverli 5a588f5d7d feat: add classic Winamp theme 2026-08-18 18:54:24 +02:00
Bjarne Øverli 548ed246f3 fix: preserve custom EQ curve
Fixes #309
2026-08-18 18:46:36 +02:00
Bjarne Øverli eee503d6e2 fix: keep live radio on current station
Fixes #305
2026-08-18 18:25:42 +02:00
RGR09 7ce3dc47d5 feat: enrich playlist album via file metadata (#307)
* feat: enrich playlist album via file metadata

* Refactor enrich command to use normalized source key

- Rename 'from' option to 'source' in command documentation- Update PlaylistEnrich function to correctly handle normalized source keys- Improve source key handling logic for album metadata extraction- Clarify source usage in the 'cliamp playlist enrich' command description

* feat: normalize song path

Replacing forward-slash instead of backslash to have less interference when sorting playlist by path

* feat: enrich elements year via file metadata
2026-08-18 18:11:54 +02:00
Taha Sadough 78fcefa771 feat: dynamic directory playlists via [[dir]] sources (#308)
* feat(tomlutil): add ParseNamedSections for multi-section documents

* feat(resolve): add AudioFiles and TracksFromPaths helpers

* feat(playlist): add DirSourced flag to Track

* feat(local): support [[dir]] directory sources in playlists

Playlists can now reference directories with [[dir]] sections instead of
listing every track. Directory sources are scanned at load time, so new
files appear and removed files disappear automatically.

- parsePlaylistDoc keeps explicit tracks and dir sources in document order
- expand resolves dirs into tracks, marking them DirSourced; explicit
  [[track]] entries always shadow a directory scan of the same path
- savePlaylist preserves [[dir]] sections and skips DirSourced tracks
- bookmarking a dir-sourced track materializes it as an explicit entry so
  the bookmark persists
- RemoveTrack refuses dir-sourced tracks; AddTracks dedupes against them
- Playlists()/SearchTracks operate on the expanded view
- CreateDirPlaylist, AddDirSource (deduped), DirSources added

* feat(cli): add --dir flags and playlist dirs subcommand

playlist create and add accept repeatable --dir flags that reference a
directory as a [[dir]] source, and a new 'playlist dirs' subcommand lists
them. --dir cannot be combined with --ssh. enrich skips dir-sourced tracks
and sort notes that they reload in scan order.

* feat(ui): guard edits on dir-sourced playlist tracks

* docs: document [[dir]] directory sources

* docs: show playlist file layout and multi-file pickup

* docs: show adding files/directories across one or many playlists

* fix: address code review findings for directory playlists

- Save playlists with interleaved [[track]]/[[dir]] section order instead of
  flattening dirs first, so removals, reorders, enrichment, and bookmark
  materialization keep each section's original position.
- Remove UI tracks by matching the persisted explicit track by path, so a
  rescan between load and save cannot remove the wrong track.
- Render playlist documents in memory before the atomic rename so a short
  write can never truncate an existing playlist.
- Validate all inputs (audio paths, directory sources) before persisting:
  create and add fail without leaving partially-written playlists behind.
- Persist directory sources as one atomic batch (AddDirSources).
- Skip unreadable entries during recursive directory scans instead of
  aborting the whole scan.
- Wrap directory operations with contextual errors; document directory
  sources on the site.
- Regression tests for section-order preservation, atomic batch validation,
  no-partial-playlist-on-failure, and unreadable-subdir scans.

* fix: resolve remaining code review findings

- tomlutil: flush and clear state on unrecognized array-table headers so
  fields cannot leak into the previous section
- local: propagate playlist read errors instead of rewriting the file
  without its [[dir]] sections
- cmd: use plural helpers for the created-playlist message and wrap
  playlist load errors in playlist bookmark
- resolve: wrap filesystem errors with operation context
- docs: describe the .toml discovery rule accurately and label the
  directory-tree fence

* fix: keep leftover track insertion positions aligned

Two leftovers materialized in one save could land in the wrong slot:
each insertion shifts later sections, so directory positions tracked in
dirPos must be re-aligned after every insertion. Replace the supplier
scan with a pure path check so saves never re-walk the filesystem, and
skip the unreadable-subdir test on Windows where os.Chmod maps to the
read-only attribute instead of Unix permissions.

* fix: persist cross-playlist tracks as explicit entries

A track added from a directory-backed playlist carried its DirSourced
flag into the destination playlist. savePlaylist then dropped it (the
destination has no owning [[dir]] section), so the track was reported as
added but silently lost. Clear the flag on incoming tracks in the
AddTracks merge. Clarify that the playlist listing omits unknown
durations (browser already hides them) and still walks directory sources
to count files.

* fix: only treat supported audio files as dir-supplied

dirSuppliesFile now validates the candidate extension against
player.SupportedExts before the path-containment checks, so non-audio
files added as explicit tracks (e.g. cover.jpg under a [[dir]]) are
appended at the end instead of being inserted before the directory
section.

* test: table-driven coverage for dirSuppliesFile predicate

* test: fix Windows path assertions in dir tests

- Normalize ExpandPath's env-expanded result with filepath.Clean before
  comparing: on Windows the raw expansion mixes / and \ separators.
- Assert TestSavePlaylistPreservesDirsAndSkipsDirTracks against the parsed
  document instead of raw text: the writer escapes backslashes via %q, so
  substring matching of a Windows temp path never matched.
2026-08-18 17:53:14 +02:00
bjarneo 385b227b24 Merge pull request #310 from coryshaw1/feat/audiobookshelf
feat: add Audiobookshelf provider, with resume, progress, and browse-label capabilities
2026-08-18 17:31:26 +02:00
Faza Iman Imron f373776d4c feat(plugins): add retained event pub/sub over IPC (#303)
* feat(ipc): add retained event subscriptions

* feat(plugins): expose namespaced event publishing

* feat(app): wire plugin events to IPC

* docs(plugins): document local event pubsub

* fix(ipc): clear request deadline for subscriptions

* fix(luaplugin): install event publisher before plugins load

* fix(luaplugin): clear retained events after publishers stop

* fix(ipc): keep event sequence gap-free when retention is rejected

* fix(luaplugin): break table cycles and cap depth in Lua value conversion

* docs(plugins): document conversion limits and subscription stream rules

* refactor(ipc): report a missing server with a sentinel error

* fix(luaplugin): fold event namespace into one topic segment

* docs(site): mention plugin event publishing and subscriptions

* fix(luaplugin): keep event namespaces unique across plugins

* fix(luaplugin): release event namespace by installed filename

* refactor(ipc): render the not-running message in the command layer

* docs(plugins): clarify namespace prefixes and JSON conversion limits
2026-08-18 17:19:28 +02:00
Floze 35f7339216 fix(upgrade): replace running binary on Windows (#302)
* fix(upgrade): replace running binary on Windows (#298)

* fix(upgrade): preserve rollback errors
2026-08-18 17:19:00 +02:00
coryshaw1 70dcc75035 refactor(provider): return reporting failures instead of discarding them
PlaybackReporter and ProgressReporter now return an error. The UI logs it at
the three fire-and-forget call sites, so jellyfin, emby, and navidrome gain
the observability audiobookshelf had — they were dropping their client errors
silently. Player state is read on the UI goroutine and passed into the
closures, so the reports stay race-free.
2026-08-17 21:06:10 -04:00