## changelog Changelog
- Audio loudness normalization — Settings → Transcode → "Normalize audio loudness (EBU R128)" evens out quiet-dialogue / loud-action mixes via the ffmpeg `loudnorm` filter. Off by default, persisted. Only applies to tracks being re-encoded; copied tracks keep their original levels.
- Pre-Send size estimate — the Send button now shows "≈ N GB to send", the summed predicted output size across the files about to sync.
- Clearer Wi-Fi sync failures — a failed sync to a Wi-Fi device now hints that it may have gone to sleep (a sleeping device stops advertising over the network and drops the connection); shown only for genuine transport failures, not cancels, disk-full, or USB.
- Sync over Wi-Fi — the full transcode → upload → register flow now works with the device on Wi-Fi, no cable. The blocker was a single API choice: the legacy service-start skips the SSL handshake network lockdown sessions require, so AFC now uses a secure service-start + the connection's socket fd + the SSL I/O context (one code path serves USB and Wi-Fi). Keep the device awake and unlocked — a sleeping device stops advertising and drops off Wi-Fi within ~2 min.
- Multi-device USB/Wi-Fi picker — when more than one iPhone/iPad is attached, the device column shows a picker; each device carries a real per-transport badge (USB / Wi-Fi). Transports are tracked per device (prefers USB, promotes Wi-Fi if USB drops).
- Reliability: mid-sync connection loss now aborts fast instead of stalling ~2 min — the send return code was proven meaningless as a liveness signal, so the session keys on the drainer thread observing transport death. Disk preflight sizes on predicted output (excludes stream-copied files), fixing false "not enough space" rejections of big drops.
- Performance: VideoToolbox capability is probed once per run instead of a subprocess per file; the debug-only output probe is compiled out of release builds; the show-extras list is cached instead of re-sorted on every progress tick.
- Bitrate hints — the per-file row shows the measured source bitrate, and the recommendation banner adds "these files run ~N Mbps at full resolution" when a downscale is on the table.
- Diagnostics: `bench-upload` reports the transport (Wi-Fi measured ~56–63 MB/s on a test device, above the older ~30 MB/s USB-2 baseline); poster-fallback branch logging; faster Wi-Fi discovery; analyze no longer pins the cooperative thread pool during ffprobe.
- TV.app finally renders TV episodes correctly. `series_name` / `season_number` / `episode_sort_id` moved from the `item` top-level (where they were silently dropped) into the `video_info` sub-dict, per the AMPDevicesAgent canonical key cluster. Header now shows the show name, season cards no longer read "Season 0", and episode rows lose the spurious `0.` prefix.
- Show portrait per episode. Each episode ships the show's portrait poster as `item.posterData` (per-episode landscape still as fallback). The TV.app show-detail hero renders correctly; episode-row thumbs are portrait tiles. Phantom-`representative_item_pid` approach explored and closed — research log in `PHANTOM_REP_PID_TESTS.md`.
- Pipeline reliability: remote AFC size verified before `FileComplete` (catches truncation before the row binds and the file gets swept), `FileError(0)` for thrown assets so they don't block `SyncFinished`, graceful cancel that flushes pending abandons before tearing down the session.
- Adaptive AFC chunk size — `recommendedChunkSize(forFileBytes:)` picks 16 MB on files ≥ 300 MB and 4 MB below. Bench on a 1.21 GB file: +1.7% throughput at 16 MB. Lightning cable plateau ~36 MB/s — USB-3 cable should re-bench higher.
- New `mediaporterctl` CLI: `sync <file>` (full pipeline with live progress), `delete <title>` (ATC `delete_track` + AFC remove), `bench-upload` with sweepable chunk sizes, `smoke-test` (release-readiness end-to-end check on a connected device). 75/75 unit tests; smoke verified on iPhone.
- Send Diagnostic + anonymous heartbeat now route to self-hosted Bugsink at `bugs.porter.md`. Default-on, toggleable in Settings. Privacy doc rewritten with the full inventory of what's sent.
- Cluster-extras (external dub/sub tracks) now survive Apply-to-all + re-analyze. Previously a TMDb pick or show rename wiped the per-job picks because cluster intent only captured audio / sub indexes. Regression tests added.
- TMDb client honors `HTTPS_PROXY` / `HTTP_PROXY`, banners distinguish "no API key" from "no match", show-picker keyboard-navigates with Enter-to-confirm.
- USB pill iPhone-filter: the Wi-Fi sync hint only shows for iPhones; iPads get the right copy. Release-readiness recipe in `research/docs/RELEASE_TESTING.md`.
- Fixed a phantom "iOS Device · 1024p" that surfaced when MobileDevice.framework reported an attach without readable lockdown values (Wi-Fi-discovered device with stale trust, or USB device mid-handshake). Hidden until trust completes; the polling loop recovers it automatically.
- AVI / MPEG-4 ASP (DivX / Xvid) → matroska copy: `-fflags +genpts` + `-avoid_negative_ts make_zero` + codec-gated `mpeg4_unpack_bframes` bitstream filter unblock the muxer.
- Folder drop: dragging a season directory now expands into its videos (previously filtered out because the folder itself had no video extension). Depth-6 walk.
- Plain-numbered TV detection: `Show Name 01.mkv` parses as `S01E01` when ≥3 siblings share the prefix. Single-file drops enrich the sibling set from the parent directory.
- Audio-language inference for AVI dubs: titles with Cyrillic / Hiragana-Katakana / Hangul / CJK bytes infer `rus` / `jpn` / `kor` / `chi` when `TAG:language` is missing. New per-row picker (14-language shortlist) overrides cluster-wide.
- USB-speed hint: connection pill reads `● CONNECTED over USB-C 2.0 🐢` when the cable bottlenecks a device capable of more (iPhone 15/16 Pro 10 Gbps, iPad Pro M1+ Thunderbolt). USB-2-only devices suppress.
- Settings → Appearance gains `Auto` (follows macOS Appearance). Settings → FFmpeg shows resolved source (`.bundled` / `.system` / `.missing`) + install hint.
- Apple Unified Logging mirror under subsystem `md.porter.MediaPorter` with `.debug` / `.info` / `.notice` / `.error` levels. Recovery actions emit at `.notice`, hard failures at `.error` — survive `log show --last 1h` without `--info`. README has the inspection commands.
- Parallel transcode lookahead (`TaskGroup` K=2–4) so transcodes run ahead of uploads while preserving send order.
- Multi-device picker now leads with a `Sync target — other devices stay idle` header so users with ≥2 attached devices understand `Send` doesn't fan out.
- DMG packaging: contained `.app` is always `MediaPorter.app` (download filename keeps `-with-ffmpeg`), in-app glyph matches the dock + DMG brand mark, File → New Window removed.
- Hotfix: large-file syncs landed the row in the TV app but the file was unbound and got swept by background GC — title visible, Play did nothing. Root cause: `SyncAllowed` (sent early, accumulates in the inbox during a long upload) was treated as terminal-equivalent to `SyncFinished`.
- Strict `SyncFinished` wait with 120 s cap + 30 s `SyncAllowed` fallback grace. Mini-clips were unaffected because they finished uploading before `SyncAllowed` arrived.
- `Send N to iPhone` no longer no-ops when every file is already on the device — counts files matching the actual pipeline filter and surfaces a disabled, explanatory button.
- `atc.FileProgress` heartbeat sent every 5 s / 10 % during the AFC upload to keep medialibraryd's asset slot warm.
- Cluster-extras — drop a folder of episodes with dub / sub subfolders, pick studios / labels once at cluster level, MediaPorter muxes each episode's extras into the transcode pass with the user-chosen default audio surviving into the TV app.
- External-track scanner — finds sibling dubs (`.mka` / `.ac3` / `.eac3` / `.flac` / `.aac` / `.m4a` / `.opus`) and subs (`.srt` / `.ass` / `.ssa` / `.vtt`), groups by parent folder = label, infers language from path, detects "forced" subs by token.
- Burn-in for cluster-extras subs is end-to-end: auto-included for the mux, language carried through, post-mux re-probe rewrites burn-in index to the embedded stream.
- OpenSubtitles auto-fetch — analyze pulls missing-language SRTs via moviehash / TMDb id, includes them in the next run.
- Burn-in for text, sidecar SRT, and bitmap (PGS / VOBSUB via `filter_complex overlay`). Bitmap canvas sized per codec; downscale applied after overlay so glyphs scale with the picture.
- Streaming registration, mid-sync disk watchdog, parallel analyze, duplicate skip against the device's `MediaLibrary.sqlitedb`.
- Cluster-scoped selection — audio / sub / resolution / burn-in changes raise an "Apply to all N episodes?" popover (auto-dismiss 5 s); "Always" toggle silences it.
- Help menu — bug report + diagnostic info: app version, OS, device, debug log tail into a pre-filled GitHub issue.
- Sign-and-notarize pipeline — Developer ID signed, notarized, stapled DMG. First public DMG shipping from porter.md.
- Cancel + Retry per row reaches every in-flight ffmpeg (mux pass, ass→srt, main transcode). Zombie ffmpeg sweep at launch.
- Two-artwork TV sync (show portrait + episode still). Multi-device support with override persistence. CLI `pull` command.
- Corrected the iPad audio-language-switcher rule: AAC and EAC-3 are listable, AC-3 is silently filtered. AC-3 → AAC; AAC + EAC-3 pass through bit-perfect (typical `ac3 + eac3` releases keep 5.1 EAC-3 untouched).
- Exactly one default audio track enforced at ffmpeg invocation (`-disposition:a:0 default` + `-disposition:a:N 0`), regardless of source — multiple defaults kill the switcher entirely.
- Mixed-codec files no longer force-remux: per-stream codec decisions are authoritative. `aac + eac3` flavors copy through.
- Full experimental matrix at `research/docs/AUDIO_SWITCHER_RULE.md` (11 variants A–K).