main
27 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |
||
|
|
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. |
||
|
|
40fb54ec9c | fix(httpclient): accept ICY stream responses (#284) | ||
|
|
9b0cbb65d4 |
test: fix macOS and Windows CI test failures
These surfaced once the codec libs let macOS/Windows compile and run tests: - ipc: bind sockets under a short /tmp dir on macOS (sun_path is capped at 104 bytes; t.TempDir() under /var/folders overflows for long test names). - plugintrust: skip the 0600 manifest-mode assertion on Windows (no Unix perm bits; Stat reports 0666). - pluginmgr: verify the installed plugin under the temp HOME instead of os.UserHomeDir(), which ignores HOME on Windows. - luaplugin: run the exec output test via cmd instead of PowerShell so the process exits and closes stdout promptly, letting on_exit fire. |
||
|
|
5cfef38275 |
fix: Windows portability for atomic writes, file URLs, and data dir
- fileutil: skip the unsupported directory fsync on Windows (Sync returns 'Access is denied'; NTFS does not need it), via a build-tagged syncDir. Fixes atomic writes in config, plugintrust, pluginmgr, and bundled-plugin tests. - playlist: fileURL now prepends a leading slash for drive-letter paths so it renders file:///C:/... instead of file://C:/... - appdir: DataDir honors HOME (consistent with Dir()); identical result on Linux, and lets tests redirect the data dir on Windows. - tests: skip the Unix-only 0400-mode and colon-in-filename cases on Windows; raise the exec test timeout for cold PowerShell startup. |
||
|
|
bb5f518cc0 |
fix(plex): explain macOS Local Network denial behind no-route-to-host
Release / build (amd64, darwin, macos-latest) (push) Has been cancelled
Release / build (amd64, linux, ubuntu-latest) (push) Has been cancelled
Release / build (amd64, windows, windows-latest) (push) Has been cancelled
Release / build (arm64, darwin, macos-latest) (push) Has been cancelled
Release / build (arm64, linux, ubuntu-latest) (push) Has been cancelled
CI / go (push) Has been cancelled
CI / build (darwin, macos-14) (push) Has been cancelled
CI / build (windows, windows-2025) (push) Has been cancelled
Release / release (push) Has been cancelled
Release / update-homebrew (push) Has been cancelled
macOS's Local Network privacy blocks LAN dials in the kernel and reports EHOSTUNREACH before any packet is sent, which reads as a routing failure. Detect darwin + EHOSTUNREACH + private/link-local target and append remediation steps to the error. Fixes #281 |
||
|
|
00871976a9 | Perf | ||
|
|
8dad762349 | fix: support go install module path | ||
|
|
5cf895eeb1 | test: isolate appdir environment | ||
|
|
184a67e519 |
feat(lyrics,mediactl): embed local lyrics and album art in MPRIS/NowPlaying
Read embedded lyrics (LRC or plain text) and cover art from local file tags at play time. Lyrics are preferred over network fetch when present; album art is cached by content hash under ~/.local/share/cliamp/album-art/ and published via mpris:artUrl (Linux) and MPNowPlayingInfoCenter (macOS). |
||
|
|
d6c50ed623 |
windows: fix config, IPC, path, and Lua compatibility (#258)
* windows: fix config, IPC, path, and Lua compatibility * spotify: share API structures and helper so tests compile on Windows * windows: address PR comments and fix socket unavailable detection, tasklist matching, api_fs tests, and doc comment * windows: address remaining PR reviews (table-driven tests, m3u resolve comments & tests, blockquote formatting, and site/index.html description) * feat: implement cross-platform IPC server with Unix and Windows support * feat: implement Unix socket IPC server and Windows process liveness check * ipc: detect dead processes via os.ErrProcessDone os.Process.Signal converts ESRCH to os.ErrProcessDone since Go 1.16, so comparing against raw syscall.ESRCH never matched and a stale socket from a crashed instance made NewServer fail instead of cleaning it up. * windows: simplify fs allowlist normalization, exec env, and ipc error checks - normalize write allow-dirs once in the memoized writeAllowDirs - collapse duplicate test helpers and repeated getenv blocks - drop redundant errors.As branch; name WSAECONNREFUSED - revert single-entry table test to linear form - gofmt: trailing newlines and indentation --------- Co-authored-by: Bjarne Øverli <bjarne.oeverli@gmail.com> |
||
|
|
4f9984b8b7 |
feat(search): fuzzy matching for local search surfaces
Replace exact substring filtering with fuzzy subsequence matching, ranked by relevance, for the three in-memory search surfaces: the playlist search (/), the file browser filter (/), and the Local provider's Ctrl+F search. Query characters now match in order without being contiguous (e.g. "skr" finds "Sakura"), and better matches sort first. Matching lives in a new dependency-free internal/fuzzy package (greedy subsequence scan with first-char, word-boundary, and consecutive-run scoring bonuses). Remote provider searches (Spotify, YouTube, Navidrome, radio, etc.) are untouched: those query external APIs that do their own matching. Closes #227 |
||
|
|
3c34004b85 |
feat(plugins): add cliamp.store per-plugin KV API
Persistent namespaced key/value store backed by a JSON file at ~/.local/share/cliamp/plugins/<name>/store.json. Values round-trip through the existing Lua<->JSON conversion, so tables/numbers/strings/bools survive a restart. Scoped per plugin (no cross-plugin reads); no permission required. Adds appdir.DataDir() for non-config state. |
||
|
|
bd8d5d07c0 |
Fix review findings: concurrency, plugin sandbox, provider bugs, and dedup (#254)
* fix(playlist): synchronize Playlist for concurrent plugin/UI access Lua plugin callbacks run on goroutines (hooks, timers, keybinds) and read playlist state through StateProvider closures, while the Bubbletea loop mutates the same *Playlist. The struct had no synchronization, so reads of p.tracks/p.order/p.pos raced with Next/Prev/Add/Remove/shuffle, and Current()'s unguarded p.tracks[idx] could panic on a stale index. Add a sync.Mutex and guard every exported method. Reads and writes are now serialized; no exported method calls another, so defer-unlock cannot deadlock. Fixes the high-severity track/player getter race and the playlist-mutation data race. * fix(luaplugin): block SSRF in cliamp.http doHTTP passed plugin-supplied URLs straight to the HTTP client with no validation, letting a plugin reach loopback, RFC1918, and link-local 169.254.169.254 (cloud metadata), and follow redirects into those targets. Add an ssrfGuard on the dialer Control hook so every connection attempt (including redirects and DNS-rebound hosts) is checked against the resolved IP and rejected if non-public, and reject non-http(s) schemes up front. A per-plugin network permission gate remains a possible follow-up; it is omitted here to avoid breaking existing plugins that legitimately use http. * fix(pluginmgr): sandbox plugin VM during metadata extraction extractMetadata ran DoFile on the entire plugin file with full stdlib, so any top-level os.execute/io call ran unsandboxed the moment a user listed installed plugins. Apply the same luaplugin.Sandbox used by the runtime before executing the file. Exports luaplugin.Sandbox for reuse. * fix(luaplugin): error on oversized fs.read instead of silent truncation fs.read used os.ReadFile (whole file into memory) then sliced to 1MB, returning a silently truncated value as success and not bounding memory for huge files. Stream through io.LimitReader(maxSize+1) and return an explicit error when the file exceeds the limit. * fix(luaplugin): make write allowlist symlink-safe isWriteAllowed validated paths lexically: the strings.Contains(abs, "..") check was dead (filepath.Abs already cleans "..", and it false-positived on filenames like "..bar"), and a symlink planted inside an allowed dir could redirect a write outside it. Resolve symlinks on the deepest existing ancestor of the target and on the allow dirs themselves before the prefix check. Also fixes the same gap in the exec cwd check, which reuses this function. * fix(luaplugin): bound timer callbacks with hookTimeout timer.after/every called CallByParam directly with no context timeout, so a runaway callback held the plugin mutex forever, blocking every other hook, keybind, and visualizer for that plugin. Add a shared callBounded helper that applies hookTimeout (mirroring invokeHook) and use it from both timer paths. * fix(luaplugin): bound visualizer render/init/destroy with hookTimeout RenderVis runs on the UI render loop and called CallByParam with no timeout while holding the plugin mutex; a hanging render() froze the UI with no escape. InitVis/DestroyVis had the same gap. Route all three through callBounded so a misbehaving visualizer times out and render falls back to the previous frame. * fix(luaplugin): wait for async hook goroutines before closing LStates Emit spawned untracked goroutines that call CallByParam on a plugin LState. Close stopped timers/execs then closed every LState without waiting, so a UI event dispatched just before shutdown could run against an already-closed LState (p.L.Close does not take the plugin mutex). Track Emit goroutines in a WaitGroup, set a closing flag under mu to reject late dispatch, and Wait before closing the LStates. * fix(ui): guard spectrum visualizers against zero-width panel renderFirework/renderBubbles/renderSakura compute dotCols = PanelWidth*2 and then mod by dotCols/dotRows/len(bands); on a narrow terminal PanelWidth can be 0, so seed % uint64(dotCols) panicked and crashed the TUI. Add the same dotRows<4 || dotCols<4 early-out the braille-grid renderers already use. * fix(history): do not clobber history on a transient load error Record discarded loadLocked's error and proceeded as if history were empty, so a one-off read failure (permission/I-O glitch) made saveLocked atomically rewrite the file with only the new entry, destroying all prior rows. Propagate the error instead, leaving the on-disk file untouched. * fix(history): detect write errors before committing history file saveLocked ignored errors from writeEntry's formatted writes, so a failure mid-write (e.g. ENOSPC) could still rename a truncated temp file over the real history. Thread writes through an errWriter and abort the rename when a write fails. * fix(lyrics): stop cleanQuery from erasing titles with label words The noise regex matched official/lyric/audio/video as bare substrings followed by .*, so 'Videotape', 'Audioslave', and 'Video Games' were cleaned to empty or truncated queries, breaking lyric lookups. Restrict bare-label stripping to genuine trailing labels (dash suffix, or 'official video/audio' and 'lyric(s) video' phrases). Add regression test cases. * fix(jellyfin,emby): synchronize client token/userID/album cache Client.token, userID, and albumCache were lazily read and written from concurrent tea.Cmd goroutines (Playlists, album browse, SearchTracks all run in their own goroutines) with no synchronization, a data race on the lazy auth writes and the cached slice. Add a mutex guarding those fields, taken only around field access and never across network I/O, with double-checked auth so at most a redundant (not racy) auth can occur. * fix(ipc): close in-flight connections on shutdown handleConn's read loop only observed the 60s per-request read deadline, never s.done, and Close closed only the listener. A still-connected client (e.g. a vis-bands polling client) kept its handler goroutine alive, so Close blocked on wg.Wait for up to 60s. Track live connections and close them in Close so their scanner.Scan unblocks immediately; addConn shares the conn mutex with the done check to close the accept-during-shutdown race. * refactor(ipc): route all reply-waits through waitReply The reply/timeout/shutdown select was open-coded in load/theme/vis/bands/ status and waitReply was used elsewhere with a generic 'timeout' message. Parameterize waitReply with a label and timeout and use it everywhere, which also gives the previously-generic commands specific timeout messages. * fix(ipc): handle accept errors instead of spinning silently acceptLoop swallowed every non-shutdown Accept error and retried at 10/s, treating a permanently closed listener as transient. Return on net.ErrClosed and log other errors before backing off. * fix(ui): notify Lua plugins on jump-to-time seek The jump enter handler called notifyPlayback (MPRIS only) plus notifier.Seeked, skipping plugin notification. Use finishSeek, which calls notifyAll, so a jump seek emits the playback-state event to plugins like every other completed seek. Seek stays synchronous (immediate seek), matching the existing test. * fix(resolve): bound startup feed sniff with a short timeout Args runs on the startup path and called sniffFeedURL, which did a HEAD on the 30s feed/M3U client, so one slow CDN could stall cliamp startup for up to 30s during pure URL classification. Give the sniff a dedicated 5s client. * fix(pluginmgr): reject empty download body An empty 200 response left download returning a non-nil empty slice, so Install wrote a 0-byte plugin file. Treat an empty body as a download error so the next candidate URL is tried (or the install fails cleanly). * fix(pluginmgr): reject raw URLs with no usable filename A raw URL ending in '/' made path.Base yield '.' or '/', producing a degenerate plugin filename. Return a clear error instead. * fix(config): let --low-power=false override config-enabled low power The override only set LowPower when the flag was true, so --low-power=false could not disable a config.toml low_power=true. Assign the flag value directly, matching the other boolean overrides. * fix(main): log MPRIS/NowPlaying init failure instead of dropping it wireMediaCtl's error was silently discarded in TUI mode, so a dbus/MPRIS setup failure left media-control silently disabled with no trace. Log it to the app log (not stderr, which would corrupt the TUI). * docs(plugins): remove unimplemented player getters cliamp.player.eq_preset(), visualizer(), and theme() were documented but never registered in the player API (calling them errors). Implementing them would require exposing model state to plugin goroutines, which would add a data race; remove them from the docs so the reference matches the API. * fix(luaplugin): reject partial bands table in set_eq_preset A bands table missing entries silently zeroed the unset bands. Require all 10 values and raise an arg error otherwise so partial input can't corrupt the EQ curve. * fix(luaplugin): track EmitKey goroutines and gate on shutdown EmitKey spawned fire-and-forget goroutines per keypress that were untracked and not gated on shutdown, so a keypress during Close could call into a closed LState. Track them in the manager WaitGroup and skip dispatch once closing, matching Emit. * fix(mediactl): apply MPRIS volume changes in order Each volume Change spawned its own goroutine to send SetVolumeMsg, so rapid changes could be delivered out of order and an older volume win. send (prog.Send) is goroutine-safe and non-blocking, so call it directly. * fix(mediactl): reject stale-track MPRIS SetPosition trackid was a fixed constant for every track, so SetPosition ignored its trackID argument and would seek whatever track became current after the client read its position. Assign a unique track object path per track change and ignore SetPosition when its trackID does not match the current track. * fix(radio): write favorites atomically save() truncated the real favorites file with os.Create and ignored every write error, so a partial write could lose all favorites. Build the content in memory, write a temp file, and rename so a failed write can't corrupt the existing file and the error surfaces to the caller. * fix(radio): propagate save errors from ToggleFavorite ToggleFavorite discarded the error from Add/Remove (which persist favorites), so a failed save was silently swallowed. Return it to the caller. * refactor(spotify): drop pointless lock around local slice append The mutex around appending the Your Music entry only touched the function-local 'all' slice, guarding nothing shared (the other locks in the loop legitimately guard trackCache). * fix(spotify): return cloned playlist list, not the cache slice Playlists returned the cached slice directly (both on cache hit and after building it), so a caller mutating the result corrupted the cache. Return a clone on both paths. * fix(spotify): return cloned cached tracks Tracks handed out the cached track slice directly, so a caller mutating it corrupted the per-playlist cache. Clone on both the cache-hit and freshly built return paths. (The snapshot-based invalidation within the playlist-list TTL is an intentional caching tradeoff and is left as-is.) * fix(spotify): don't sleep before giving up on rate limit On the final retry the loop slept the full backoff (up to 128s) and then returned the rate-limited error without making another request. Break out immediately on the last attempt. * fix(jellyfin): guard AlbumList against negative offset A negative offset passed the offset>=len check and then panicked at out[offset:end]. Clamp offset to 0 first, matching the Emby client. * docs(playlist): correct MoveQueue comment MoveQueue swaps any two positions, not only adjacent ones. * refactor(ui): simplify dead math.Min in flame tier mapping math.Min(0.65, 0.55) is a constant 0.55; replace with the literal and drop the now-unused math import. * refactor(netease): compare app code against its own constant resp.Code is NetEase's application-level status, not an HTTP status; it was compared against http.StatusOK which only coincidentally shares the value 200. Introduce neteaseCodeOK and use it at all four comparison sites. * fix(resolve): strip audio extension from derived title humanizeBasename left a trailing extension (e.g. .mp3) in the title derived from a URL basename. Drop a recognized audio extension first, leaving non-media dotted suffixes untouched. * fix(resolve): bound RSS feed read size resolveFeed decoded the response body with no size limit. Wrap it in an io.LimitReader (32 MB) so an oversized or malicious feed can't exhaust memory. * fix(navidrome): return cloned cache slices Playlists and Tracks handed out the internal cached slices, so a caller mutating the result corrupted the cache. Return slices.Clone on the cache-hit and freshly built return paths. * fix(player): include ffmpeg stderr in decode error decodeFFmpeg wrapped the exec error but dropped ffmpeg's stderr, where the actual failure reason is written. Surface ExitError.Stderr in the message. * refactor(player): forward speedStreamer.Err directly beep.Streamer already declares Err() error, so the local errorer type assertion was redundant. Call ss.s.Err() directly, matching the eq and volume streamers. * fix(ui): don't drop queued seek/lyric cmds on reconnect When the scheduled stream reconnect fired, the tick handler returned early with only playTrack + tick, discarding the seekCmd/lyricCmd computed earlier in the same tick. Fold them into the reconnect batch. * fix(ui): nav list footer counts reflect committed filter The footer only showed the filtered count while the search input bar was open (searching); once a filter was committed with Enter it fell back to the full count. Add navFilteredTotal and use the filtered count whenever a query is active, across the artist, album, and track footers. * refactor: remove dead internal/control package internal/control duplicated the message types in internal/playback and had no importers (only its own test); the live code uses internal/playback. Remove it. * fix(providers): implement Refresh for self-hosted providers Navidrome, Plex, Jellyfin, and Emby cache playlist/track (and album) data but none implemented playlist.Refresher, so the UI refresh key was a silent no-op for them. Add Refresh to clear the caches (including the jellyfin/emby client album cache) so the next fetch hits the server. * refactor: share minimal-TOML section parser The [[section]] parse loop was duplicated three times (radio loadStations, radio loadFavoriteStations, local loadTOML). Extract tomlutil.ParseSections and rewrite all three against it. Behavior is preserved, including the single-section-type assumption of the original parsers. * refactor: extract shared Emby/Jellyfin client into internal/embyapi The emby and jellyfin clients were ~95% identical (~700 duplicated lines) and had already drifted (jellyfin lacked the negative-offset guard and error wrapping). Extract one shared Client into internal/embyapi, isolating the real differences behind a dialect: auth header scheme (Emby Authorization vs Jellyfin X-Emby-Authorization), ping endpoint, user-id discovery, error prefix, and metadata key. emby and jellyfin become thin wrappers (NewClient, IsStreamURL, type aliases) plus their providers. Client tests are consolidated in embyapi: shared behavior once, dialect specifics per dialect. The client now uses a per- instance http.Client (SetHTTPClient) instead of a package global, which is what makes it testable from the provider packages. |
||
|
|
dda274737d |
fix(httpclient): respect HTTP_PROXY/HTTPS_PROXY for streaming client (#238)
The Streaming HTTP client defines a custom http.Transport without setting Proxy, so it ignores HTTP_PROXY, HTTPS_PROXY and NO_PROXY env vars. This breaks playback for users behind corporate or local proxies, even though the rest of the codebase (which relies on http.DefaultTransport) already honors those vars. Add Proxy: http.ProxyFromEnvironment to the streaming Transport so it behaves consistently with the rest of cliamp. |
||
|
|
9c95f2251e |
Expand test coverage across core packages
Release / build (amd64, darwin, macos-latest) (push) Has been cancelled
Release / build (amd64, linux, ubuntu-latest) (push) Has been cancelled
Release / build (amd64, windows, windows-latest) (push) Has been cancelled
Release / build (arm64, darwin, macos-latest) (push) Has been cancelled
Release / build (arm64, linux, ubuntu-latest) (push) Has been cancelled
Release / release (push) Has been cancelled
Release / update-homebrew (push) Has been cancelled
|
||
|
|
9a78b3e3f1 |
feat: support media controller on mac (#172)
Release / build (amd64, darwin, macos-latest) (push) Has been cancelled
Release / build (amd64, linux, ubuntu-latest) (push) Has been cancelled
Release / build (amd64, windows, windows-latest) (push) Has been cancelled
Release / build (arm64, darwin, macos-latest) (push) Has been cancelled
Release / build (arm64, linux, ubuntu-latest) (push) Has been cancelled
Release / release (push) Has been cancelled
Release / update-homebrew (push) Has been cancelled
* feat: support media controller on mac * fix(notifications): publish current playback state when media notifier attaches * fix(darwin): lock startup to the Cocoa main thread * refactor: cleanup and simplify * missed cleanup |
||
|
|
644f7db6de |
Add more tests
Release / build (amd64, darwin, macos-latest) (push) Has been cancelled
Release / build (amd64, linux, ubuntu-latest) (push) Has been cancelled
Release / build (amd64, windows, windows-latest) (push) Has been cancelled
Release / build (arm64, darwin, macos-latest) (push) Has been cancelled
Release / build (arm64, linux, ubuntu-latest) (push) Has been cancelled
Release / release (push) Has been cancelled
Release / update-homebrew (push) Has been cancelled
|
||
|
|
197e6d5d76 | Add core tests | ||
|
|
32cb03c687 |
Pr 158 review (#160)
* feat: CLI playlist management, Unix socket IPC, and SSH streaming
Three features that make cliamp agent-native and scriptable:
1. `cliamp playlist {list,create,add,show,remove,delete}` — manage TOML
playlists from the CLI with --json output and --ssh HOST for remote
directory walking
2. Unix socket IPC at ~/.config/cliamp/cliamp.sock — play/pause/next/prev/
stop/status/volume/seek/load/queue commands over JSON-RPC, following
the MPRIS prog.Send() pattern
3. SSH streaming via ssh:// URL scheme — pipe audio from remote machines
through the existing decoder pipeline, zero filesystem mounts needed
Zero new Go dependencies. All existing tests pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(ui): SSH track glyph + duration metadata fallback
- Show ↗ prefix on SSH-streamed tracks in the playlist view
- Store/read duration_secs in local TOML playlists
- Fall back to track metadata duration when player reports 0
(piped SSH streams can't determine total length from audio frames)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: album separators, favorites, playlist resume, enrich command
- Album group separators in playlist view (triggers on album field change)
- Favorites system: * key toggles ★, [★ N] count in header, CLI `playlist favorite`
- [67/123] position counter in playlist header
- Playlist resume: saves and restores playlist + track + position across sessions
- `cliamp playlist enrich` probes duration via afinfo and derives album from path
- `ConnectTimeout=5` on all SSH commands to prevent stale hangs
- `SavePlaylist()` exported on local provider
- `*` added to keymap overlay and bottom help bar
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: persist TUI favorites to disk, fix truncation, add cross-playlist favorites view
- TUI * key now persists favorites to TOML via FavoriteSetter interface
- ★ prefix deducted from truncation budget (prevents line overflow)
- JSON output includes duration_secs and favorite fields
- `cliamp playlist favorites` lists all ★ tracks across all playlists
- FavoriteSetter interface in provider/interfaces.go
- Bottom help bar: [*]Fav (no extra ★ symbol)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: accent-color favorites, atomic TOML writes, strict --index parsing
Kotharat design review + silent failure audit fixes:
- ★ renders in accent color (yellow) independently from line style — scannable in long playlists
- savePlaylist uses atomic write (tmp + rename) — no data loss on crash
- --index parsing uses strconv.Atoi — rejects fractional/malformed input
- Bounds check on PlaylistFavorite re-read — no panic on concurrent modification
- Help bar priority 80 → 75 for [*]Fav — yields to navigation hints
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(ipc): remote theme switching via cliamp theme <name>
- `cliamp theme list` — list all available themes (built-in + custom)
- `cliamp theme <name>` — change theme in running TUI via IPC
- ThemeMsg with reply channel for error reporting ("theme not found")
- TUI handler reloads themes from disk before applying (picks up new custom themes)
- Added Name field to IPC Request for theme name
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(theme): clean up theme CLI UX from review findings
- `cliamp theme` (no args) prints usage cleanly instead of printing playback state
- `theme list` matching is case-insensitive (prevents routing confusion)
- Documented LoadAll() precedent in ThemeMsg handler (matches openThemePicker pattern)
- Updated CLAUDE.md and agent docs with theme commands
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: Blade Runner visualizers, theme-conditional UI chrome, IPC vis command
Two new visualizer modes inspired by the film's on-screen electronics:
- Esper: phosphor grid scan with CRT afterglow decay (4-level persistence)
- VoightKampff: concentric iris rings with bass-driven bellows indicator
Theme-conditional UI chrome (active when neon-blade-runner theme is set):
- Seek bar: ▰▱ blocks with ◆ thumb (Voight-Kampff analog meter)
- Album separators: ── ◈ ALBUM ◈ ── (LAPD terminal data headers)
- SSH track glyph: ◉ (replicant eye) instead of ↗
- Time format: T+MM:SS (mission elapsed time)
- Position counter: [SUBJ 003/123] (interrogation notation)
- Loading spinner: cycles ENHANCE / SCANNING / PROCESSING
- Title flicker: 2% chance per frame of 2-3 char CRT glitch cluster
- Track transition: 4-frame ░▒▓█ static burst
IPC visualizer command:
- cliamp vis <name> — switch visualizer by name
- cliamp vis next — cycle to next mode (same as v key)
- cliamp vis list — list all modes with * active marker
Also adds neon-blade-runner-amber theme (1982 LAPD terminal amber phosphor).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Refactor PR: urfave/cli v3, consolidate duplications, fix SSH+ffmpeg bug
CLI framework:
- Replace hand-rolled flag parser with urfave/cli v3 command tree
- Move command definitions to commands.go, keep main.go focused on TUI startup
- Rename --volume/--theme flags to --vol/--start-theme to avoid subcommand collisions
- Hide unhelpful (default: 0) on int flags where 0 means auto/config
Code consolidation (cmd/playlist.go):
- Use player.SupportedExts instead of duplicate audioExtensions (adds 4 missing formats)
- Use resolve.CollectAudioFiles instead of duplicate walkDir
- Use playlist.TrackFromFilename instead of local copy (includes sanitizeTag)
- Add bulk AddTracks() to avoid N+1 file open/close per track
- Add provider.Exists() using os.Stat instead of parsing all TOML files
- Extract newProvider() and collectLocalAudio() helpers
Architecture fixes:
- Unify MPRIS/IPC message types via internal/control package with type aliases
- Remove ~30 lines of duplicated handler cases in update.go
- Add SSH+ffmpeg guard in pipeline.go (was silently passing ssh:// to ffmpeg)
- Create internal/sshurl for proper URL parsing with port support
- Replace macOS-only afinfo with cross-platform ffprobe for remote duration probing
- Add consistent StrictHostKeyChecking=yes to all SSH call sites
- Fix StrictHostKeyChecking docs to match code (yes, not accept-new)
IPC fixes:
- Remove TOCTOU os.Stat before DialTimeout in ipc/client.go
- Remove unnecessary 64KB scanner buffer in ipc/server.go
- Add nil-guards on msg.Reply for ThemeMsg and StatusRequestMsg
- Change ipcSend/overridesFromFlags to return errors instead of os.Exit
Removals:
- Remove CLAUDE.md
- Remove Esper and Voight-Kampff visualizers
- Remove neon-blade-runner theme-conditional chrome
- Delete hand-rolled ParseFlags and helpers from config/flags.go
* Remove review file
* Add Local provider pill, L keybinding, default radio startup
- Register Local as a provider pill tab (after Radio)
- Add L keybinding to switch to Local provider for browsing TOML playlists
- Load 3 cliamp radio streams directly on default startup (no async M3U)
- Skip playlist resume on default startup so radio streams aren't replaced
- Only allow position resume (SetResume) when CLI args given, never ResumePlaylist
- Update keybinding docs, keymap overlay, and site
---------
Co-authored-by: Tom di Mino <contact@tomdimino.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||
|
|
11e685c7a4 |
Use app version in Jellyfin client headers (#156)
Release / build (amd64, darwin, macos-latest) (push) Has been cancelled
Release / build (amd64, linux, ubuntu-latest) (push) Has been cancelled
Release / build (amd64, windows, windows-latest) (push) Has been cancelled
Release / build (arm64, darwin, macos-latest) (push) Has been cancelled
Release / build (arm64, linux, ubuntu-latest) (push) Has been cancelled
Release / release (push) Has been cancelled
Release / update-homebrew (push) Has been cancelled
* Use app version in Jellyfin client headers * Use shared app metadata for version reporting |
||
|
|
296722a201 |
Add initial plugins system based on GopherLua
Custom plugins can be added to the ~/.config/cliamp/plugins. Examples and docs added. |
||
|
|
13e381c980 |
feat: resume playback and fix itunes:duration parsing (#91)
* feat: resume playback and fix itunes:duration parsing - Parse <itunes:duration> from RSS feeds (HH:MM:SS, MM:SS, plain seconds including floats) so total track time is shown in the player - Save last track path and position to ~/.config/cliamp/resume.json on clean exit; restore on next launch by seeking to the saved position - Exclude YTDL and live streams from resume (position unreliable / no seek) - Guard seek with player.Seekable() to avoid mistaking a no-op for success Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: address PR review feedback - Gate resume capture on IsPlaying() and position > 0 to avoid overwriting a valid resume.json when quitting without playback progress - Guard resume.Save against empty path or zero position (defensive) - Use 0600 permissions for resume.json (listening history is private) - Clamp negative itunes:duration values to 0 - Fix parseItunesDuration doc comment to mention float seconds support - Add TestParseItunesDuration covering all formats and edge cases Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
b2cc73a9d9 | Refactored the TUI's flat Model struct into grouped sub-structs (e.g., m.lyrics.*, m.seek.*, m.navBrowser.*) and extracted shared utilities into internal/ packages for better organization. | ||
|
|
38a877b06b | Remove dead code, extract shared browser helper, fix volume cache, ctx ordering, and doc comments | ||
|
|
ee8b1e3670 | Move code to the internal app dir | ||
|
|
ad14c8f502 | Deduplicate code |