# Changelog
All notable changes to this project will be documented here.
Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
Versioning: [SemVer](https://semver.org/).
## [Unreleased]
---
## [1.0.1] — 2026-07-12
### Fixed
- **UTF-8 char-boundary panic** (`tui/views/diff.rs`, `profiles.rs`, `progress.rs`,
`cli/diff.rs`, `cli/audit.rs`): five `truncate` helpers sliced strings with raw byte
offsets (`&s[..max]`, `&s[start..]`), causing a `thread 'main' panicked` / exit 101
on any filename containing a multibyte Unicode character at the truncation point
(e.g. `Mitternachtslöwe.flac`). Replaced with an `is_char_boundary` walk-back/forward
loop (stable since Rust 1.0). `diff.rs` also called `str::floor_char_boundary` which
is not stable until Rust 1.93 — also replaced, ensuring MSRV 1.80 compatibility.
---
## [1.0.0] — 2026-05-06
### Added
- **SSH source** (`src/ssh/mod.rs`): `source = "ssh://[user@]host[:port]/path"` in sync
profiles. Directory listing via `find -printf` over SSH; file download via `cat` piped
stdout. Authentication fully delegated to the system `ssh` binary — keys, agent,
`known_hosts`, and `~/.ssh/config` all work with no extra configuration. Zero new Cargo
dependencies.
- **`dapctl cover embed <path> [--overwrite]`** (`cover::embed`): writes cover art from
`folder.jpg` or the first embedded picture into every track tag in the folder.
Supports FLAC, MP3, M4A, OGG, and Opus. Does not re-embed when art already present
unless `--overwrite` is passed.
- **`dapctl profile delete <name> [-y]`** — removes the sync profile TOML. Prompts for
confirmation unless `--yes` / `-y`. TUI: `D` key on the profiles list (two-press confirm,
same safety pattern as mirror-mode sync).
- **5 new builtin DAP profiles** (7 total including fiio-m21 and generic):
`fiio-m11`, `ak-sr35`, `hiby-r6`, `shanling-m3ultra`, `ibasso-dx320`.
All profiles include filesystem limits, codec matrix, and firmware exclusion globs.
Validated in CI via `all_builtin_profiles_parse`.
- **`PathWarning` / `check_path_limits`** in `diff`: warns when a source filename or
full path would exceed the DAP firmware's declared limits. Shown in both `dapctl diff`
output and the TUI diff view. Severity and limits are read from the active DAP profile.
- **Snapshot tests** (`tests/snapshots.rs`): 10 `insta` snapshot tests covering plan
serialisation, path-limit logic, and the full DAP catalogue. Total tests: 54.
- **Error taxonomy** (`src/error.rs`): typed `thiserror` enums — `ConfigError`,
`DapError`, `ScanError` — with structured user-facing messages. `main.rs` routes exit
codes: 2 for config/DAP errors, 3 for scan/environment errors, 1 for all others.
- **`NO_COLOR` support** in `Theme::new()`: when the `NO_COLOR` environment variable is
set, all TUI colours collapse to terminal defaults (https://no-color.org).
- **Volume label detection on Linux and macOS**: `lsblk -o LABEL,MOUNTPOINT -P -n` on
Linux; `diskutil info <mount>` on macOS. Completes cross-platform label detection
alongside the existing Windows `GetVolumeInformationW` implementation.
- **Selective mode write-back** via `toml_edit`: `x` key in the TUI diff view toggles
album-level selection (◆/◇); confirmed selections are persisted back to the sync
profile TOML, preserving comments and formatting.
- **Packaging skeletons**: Scoop manifest (`packaging/scoop/dapctl.json`) with
autoupdate block; AUR `PKGBUILD` (`packaging/aur/PKGBUILD`) for the `-bin` variant.
### Changed
- Homebrew tap (`marturojt/tap`) now has `dapctl.rb` updated to the v1.0.0 formula.
### Removed
- `[Unreleased]` entry cleared (all items shipped in this release).
---
## [0.4.0] — 2026-05-05
### Added
- **Synced lyrics** (`player::lyrics`): auto-detects `.lrc` alongside the
audio file, parses multi-timestamp LRC format, and scrolls the active line
to ~⅓ from the top of the pane. `i` key toggles between queue and lyrics
pane; hints update when lyrics are present.
- **Play history + resume position** (`player::history`): append-only JSONL
log in the platform data dir. On opening a track that appears in history,
playback resumes from the last recorded position.
- **Sleep timer**: `t` key cycles off → 15 → 30 → 45 → 60 min. A countdown
badge `⏾ M:SS` appears in the HiFi line. On expiry the engine pauses and
emits a flash notification.
- **Equalizer animation** in Now Playing: animated sin-wave bars
(▁▂▃▄▅▆▇█ cycling through 8 phases) while playing; collapses to a muted
flat line when paused.
- **`dapctl audit <path>`** — offline read-only library audit (`audit::scanner`
+ `audit::report`):
- Detects missing tags (artist/album/title/track#/year), absent cover art
(embedded or `folder.jpg`), format mix within an album, and track-number
gaps.
- Severity levels: high/medium/low. Flags `--min-severity` and `--limit`.
- Human table + `--json` structured output.
- **`dapctl cover fetch <path> [--online]`** — batch cover art downloader:
- Without `--online`: prints policy message and exits (code 2). No network
calls ever happen unless explicitly opted in.
- Pipeline: MusicBrainz WS2 search → Cover Art Archive → iTunes Search API
fallback.
- 30-day disk cache at `$XDG_CACHE_HOME/dapctl/metadata/cover_cache.json`.
- Rate limits: 1.1 s between MusicBrainz requests, 3.5 s between iTunes.
- Saves `folder.jpg` in each album directory; non-JPEG images are converted
via the `image` crate.
- **`docs/NETWORK.md`** — documents all network endpoints, user-agent string,
rate limits, cache TTL, and opt-in policy.
### Changed
- **Library normalization**: artist and album grouping keys are now
case-insensitive and diacritic-insensitive (à/á/â/ä → a, ñ → n, í → i, …).
"Kings Of Leon" / "kings of leon" and "Rosalía" / "Rosalia" now merge into
one entry. Display name is the first value seen for each normalised key.
- **TUI UX improvements**:
- Diff view: filter tabs rendered as a visual tab row with per-tab counts
and active-tab highlight; replaces the plain header line.
- New Profile wizard: dot step indicator `● ● ○ ○ ○` at the top of each
step shows progress through the 5-step flow.
- Profiles view: sync mode badge is coloured (mirror → warn amber, additive
→ muted green); last-sync indicator `✓ Xh ago` shown when available.
- Player: repeat/shuffle state displayed inline in the HiFi metadata line
(`↺` all-repeat · `↺1` one-repeat · `⇄` shuffle). Focused pane title
shown in bold with `▶` prefix.
- Queue pane title shows current position `(X/N)`.
### Dependencies added
- `reqwest 0.12` (blocking, native-tls) — cover fetch HTTP client.
- `image 0.25` (jpeg + png features) — JPEG conversion for downloaded covers.
---
## [0.3.0] — 2026-05-01
### Added
- Home landing screen (`tui::views::home`): figlet ASCII art banner,
navigable menu (sync & profiles · player · log), connected DAPs status.
Default entry point on `dapctl` launch; `q` from sub-views returns here.
- TUI audio player — open with `m` from the profiles screen:
- Three-pane layout: library browser · now playing + queue · key hints.
- **SQLite-backed library scanner** (`player::scanner`): tag-based grouping
(`album_artist` → `artist` → path fallback). Cache in platform data dir
keyed by mtime_ns + size; only re-reads changed files on subsequent opens.
- Artist → album → track hierarchy with expand/collapse (`Enter`).
`Tab` switches pane focus between library and queue.
- `/` incremental search across artist · album · title (real-time filter).
- **Gapless playback** via `TrackDoneNotifier<S>`: next decoder eagerly
appended to the rodio Sink before the current source is exhausted;
`Arc<AtomicBool>` fires on source end to drive a seamless queue advance.
- HiFi metadata display in Now Playing: sample rate · bit depth · bitrate
· channels populated by `lofty` on first play.
- Volume (`+`/`-`), seek (`←`/`→` ±30 s), next/prev (`n`/`p`),
shuffle (`s`), repeat cycle (`r`).
- Source toggle (`L`/`D`): play from source library or mounted DAP
destination. Paths resolved from the active sync profile.
- Preview from diff view: `space` on any entry enqueues and opens the
player without starting playback automatically.
- DSD (DSF/DFF) via ffmpeg pipe → PCM f32le 176.4 kHz (requires ffmpeg
in PATH; clear error message and auto-advance when absent).
- `PlayerCommand::LoadQueue` — populates queue without auto-starting
playback (complements existing `PlayQueue`).
- `Queue::peek_next()` — returns the track `advance()` would play next
without mutating state; used by gapless preload.
### Changed
- Default TUI entry point changed from `profiles` view to `home` view.
`q` from profiles returns to home; `q` from home exits.
---
## [0.2.0] — 2026-04-28
### Added
- `Verify::Checksum` mode: streaming blake3 hash (1 MiB buffer) computed
during the diff walk. Silent corruption is detected even when file size
and mtime match. Falls back to mtime comparison when hashes are not
present (i.e. when `verify = "size_mtime"` is configured).
`transfer::verify::hash_file()` is now public for reuse by the executor's
post-copy verification pass.
- Tag-based filters in sync profile `[filters]` section, powered by `lofty`:
- `include_artists` / `exclude_artists` (case-insensitive)
- `include_genres` / `exclude_genres` (case-insensitive)
- `min_sample_rate_hz` / `max_sample_rate_hz`
- `min_bit_depth`
All new fields are optional and default to "no filter". Files that lofty
cannot parse (DSD formats, non-audio, corrupt) always pass — they are
never silently dropped.
- Transcode pipeline (`[transcode]` section in sync profile):
- `transcode::ffmpeg::detect()` — probes for `ffmpeg` in PATH at
startup; logs version when found.
- Rule language: `[[transcode.rules]]` entries with `from`, `to`, and
optional `params` fields.
- Extension projection in the diff walker: source files matching a rule's
`from` extension are projected to `to` before the destination diff,
so `song.dsf` compares against `song.flac` on the DAP.
- Mtime-only staleness for transcoded pairs (size/checksum comparison
across different formats is meaningless); source mtime is preserved on
the output after transcode so re-runs are idempotent.
- Transcode cache at `$XDG_CACHE_HOME/dapctl/transcode/` with a
256-shard blake3-keyed layout (`blake3(source_content || params)`).
Cache hit avoids re-running ffmpeg; failure to write cache is non-fatal.
- `TranscodeOpts` wired into `executor::Options`; `cli::sync` builds it
from `ProjectDirs` when `transcode.enabled = true` and rules are present.
- `dapctl export m3u <profile> [--output PATH]`: walks the source with the
same filters as `dapctl sync`, prefixes each path with the DAP's
`layout.music_root`, and emits a standard `#EXTM3U` playlist. Write to
file or stdout.
### Tests
- 4 new unit tests for `Verify::Checksum` classify paths (same hash,
different hash same size, no-hash mtime fallback).
- 3 new unit tests for transcode classify paths (dst newer → Same,
src newer → Modified, new entry carries `transcode_from`).
- 2 new integration tests: `diff_checksum_detects_silent_corruption`
(same size + mtime, different content → Modified) and
`diff_checksum_same_content_is_same` (different mtime, same content →
Same). Total: 39 tests.
- 4 new tag-filter integration tests covering inactive-by-default,
graceful degradation on unreadable files, and sample-rate / artist
filter activation.
---
## [0.1.0] — 2026-04-27
### Added
- Repository scaffolding: Cargo manifest, module layout (`cli`, `config`,
`dap`, `scan`, `diff`, `transfer`, `logging`, `tui`), builtin DAP
profiles for FiiO M21 and a generic fallback.
- GPLv3 license, README, architecture docs, backlog, CI workflow.
- `logging::init`: dual-sink tracing (human stderr/file + JSONL v1).
Per-run `run_id` (ULID), schema v1 frozen, `finish` event on exit.
- `config::load` / `config::resolve`: sync profile TOML parsing with
schema validation, glob validation, and `ResolvedProfile` (merged
exclude/include globsets from DAP + sync profile).
- `dap::load`: builtin catalogue (`fiio-m21`, `generic`) with XDG
user-override support. `dapctl profile list/show/check` commands.
- `scan::removable`: sysinfo-based removable drive enumeration.
Windows: `GetVolumeInformationW` for correct volume labels.
- `scan::heuristic`: DAP identification by label, marker files
(`.database_uuid`, `HiByMusic/`), and exFAT/FAT32 fallback.
Covers FiiO M11 family, Shanling, iBasso, Cayin, HiBy, AK.
- `scan::resolve_destination`: resolves `auto:<dap-id>` to a mount path.
- `dapctl scan`: human table + `--json`. MTP guidance in empty state.
- `diff::walker`: recursive walk with globset filtering, `/`-normalised
paths for cross-platform glob matching.
- `diff::compare`: O(n) merge-join with 2 s FAT32 mtime tolerance.
- `diff::Plan`: `count()`, `total_bytes()`, `transfer_bytes()`,
`eta_secs()`. Fully serialisable.
- `dapctl diff <profile>`: summary table with ETA + first 40 entries.
`--json` emits full Plan.
- `transfer::executor`: temp + fsync + rename copy pipeline; `indicatif`
MultiProgress bars (overall + per-file, speed, ETA).
- `transfer::manifest`: append-only JSONL per run at
`%APPDATA%/dapctl/runs/<ulid>.jsonl`.
- `transfer::verify`: size+mtime (2 s FAT32 tolerance) and blake3
checksum modes.
- `dapctl sync <profile>`: additive and mirror modes, `--yes` /
`--dry-run`, result summary, exit code 0/1.
- TUI diff view: summary table (counts + bytes + ETA per kind), entry
list with `tab`-cycled filter (All / New / Modified / Orphan / Same),
j/k scroll, color-coded entry icons.
- TUI sync from diff view: `y` confirms and launches sync; mirror mode
with orphans requires a second `y` with a flash warning.
- `transfer::ProgressEvent` channel: executor optionally sends
`FileStart / FileProgress / FileDone / FileFail / DeleteDone / Finish`
to a `mpsc::Sender` so the TUI can consume them without indicatif
terminal output.
- TUI progress view: overall `Gauge` (done/total bytes + %), per-file
`Gauge` with filename in title, live speed / ETA / copied / deleted /
failed stats, auto-scrolling recent-events tail (last 200), completion
banner. Sync executes in a background thread; main thread drains the
channel each frame.
### Fixed
- `transfer::executor`: preserve source mtime on destination after
rename so `diff` correctly classifies files as `Same` on re-runs and
the post-copy `size_mtime` verify passes. Without this, every re-run
re-transferred the entire library.
- `cli::sync`: run a `repair_dest_mtimes` pass before `diff` to fix
existing destinations that were populated before the mtime-preservation
fix (metadata-only, takes seconds regardless of library size).
### Changed
- `release.yml`: cross-compile pipeline triggered on `v*` tags. Produces
Linux musl static binaries (x86_64 + aarch64) via `cargo-zigbuild`,
a macOS universal binary via `lipo`, and a Windows MSVC zip. All
artifacts attached to a draft GitHub release with `SHA256SUMS.txt`.
### Added (TUI)
- TUI new-profile wizard: 4-step guided creation (name → source →
destination → mode). File browser with drive enumeration on Windows,
duplicate name detection at step 1. `c` clones a selected profile
pre-filled with suggested `<name>-copy`.
- TUI log view: scrollable display of the most recent JSONL run.
j/k scroll, `g`/`G` top/bottom, `r` reload. Accessible via `l` from
profiles or from progress view after sync completes.
- Key-repeat fix: only `KeyEventKind::Press` events are processed;
OS repeat events are discarded, eliminating runaway scrolling.
### Fixed
- Wizard TOML writer: escape backslashes in Windows paths
(`D:\Music` → `D:\\Music`) — unescaped paths caused silent parse
failure and profiles never appeared in the list.
- `transfer::executor::truncate_path`: slice by char index instead of
byte offset — panicked on Unicode characters such as U+2019 (`'`)
in album/folder names.
- Wizard `verify` field: was written as `"size+mtime"` but the schema
expects `"size_mtime"` (snake_case) — profiles were silently dropped.
### Tests
- 10 unit tests for `diff::compare` (merge-join logic, FAT32 mtime
tolerance, `Verify::None` behaviour).
- 17 integration tests for the diff pipeline (`walker` + `compare`):
glob filtering, Unicode filenames, mtime alignment, idempotency.
### Validated
- End-to-end sync against HiBy R4 microSD (F:\\, exFAT, 116 GB):
2,108 FLAC files, 75.1 GB transferred and verified.
- Mirror mode: orphan detection and deletion confirmed.
- Re-run idempotency: 2,108 unchanged, 0 copied, 0 failed.
[1.0.0]: https://github.com/marturojt/dapctl/releases/tag/v1.0.0
[0.3.0]: https://github.com/marturojt/dapctl/releases/tag/v0.3.0
[0.2.0]: https://github.com/marturojt/dapctl/releases/tag/v0.2.0
[0.1.0]: https://github.com/marturojt/dapctl/releases/tag/v0.1.0