diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..ecef0cf
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,40 @@
+name: ci
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+jobs:
+ build-test:
+ runs-on: ubuntu-24.04
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install libVLC (video playback smoke test parity)
+ run: sudo apt-get update && sudo apt-get install -y libvlc5 libvlccore9 vlc-plugin-base
+
+ - uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 8.0.x
+
+ - name: Restore
+ run: dotnet restore NotZune.sln
+
+ - name: Build (Release, zero warnings policy)
+ run: dotnet build NotZune.sln -c Release --no-restore /warnaserror
+
+ - name: Test
+ run: dotnet test NotZune.sln -c Release --no-build --nologo
+
+ - name: Design invariants audit
+ run: |
+ python3 - << 'EOF'
+ import sys
+ sys.path.insert(0, 'scripts')
+ from mcp_tools import audit_design_invariants
+ result = audit_design_invariants('.')
+ print(result)
+ assert result['status'] == 'PASSED', 'design invariant violations found'
+ EOF
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..bafd7d0
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,64 @@
+name: release
+
+on:
+ push:
+ tags: ['v*']
+ workflow_dispatch:
+
+jobs:
+ publish:
+ runs-on: ubuntu-24.04
+ strategy:
+ matrix:
+ include:
+ - rid: linux-x64
+ - rid: linux-arm64
+ - rid: win-x64
+ - rid: win-arm64
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 8.0.x
+
+ - name: Publish (self-contained)
+ run: dotnet publish src/NotZune.Desktop -r ${{ matrix.rid }} -c Release -o publish/${{ matrix.rid }} --self-contained true
+
+ - name: Verify vendored BASS natives ship
+ run: |
+ if [ "${{ matrix.rid }}" = "linux-x64" ] || [ "${{ matrix.rid }}" = "linux-arm64" ] || [ "${{ matrix.rid }}" = "win-x64" ]; then
+ test -n "$(find publish/${{ matrix.rid }}/Native -name 'bass*' 2>/dev/null)" || { echo "BASS natives missing for ${{ matrix.rid }}"; exit 1; }
+ fi
+
+ - name: Archive
+ run: |
+ cd publish
+ zip -r ../not-zune-${{ matrix.rid }}.zip ${{ matrix.rid }}
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: not-zune-${{ matrix.rid }}
+ path: not-zune-${{ matrix.rid }}.zip
+
+ github-release:
+ needs: publish
+ if: startsWith(github.ref, 'refs/tags/v')
+ runs-on: ubuntu-24.04
+ permissions:
+ contents: write
+ steps:
+ - uses: actions/download-artifact@v4
+ with:
+ merge-multiple: true
+
+ - uses: softprops/action-gh-release@v2
+ with:
+ files: not-zune-*.zip
+ body: |
+ ## Not-Zune
+
+ ### Platform notes
+ - **Linux (x64/arm64):** video playback requires system VLC (`sudo apt install vlc`) β libVLC is loaded from the system at runtime.
+ - **Windows x64:** full audio + video (BASS natives vendored, VLC natives bundled via VideoLAN.LibVLC.Windows).
+ - **Windows arm64:** BASS has no ARM64 natives β playback runs in simulated (silent) mode; video unaffected.
diff --git a/README.md b/README.md
index e38cfc0..c7b5351 100644
--- a/README.md
+++ b/README.md
@@ -80,6 +80,13 @@ dotnet publish src/NotZune.Desktop -r win-x64 -c Release
dotnet publish src/NotZune.Desktop -r win-arm64 -c Release
```
+### Platform Notes
+- **Linux:** video playback loads system VLC at runtime β install `vlc` (or `libvlc5`/`libvlccore9`) for the video surface. BASS audio natives are vendored per-RID and ship with every build.
+- **Windows x64:** full audio + video out of the box (VLC natives bundled via `VideoLAN.LibVLC.Windows`).
+- **Windows arm64:** BASS publishes no ARM64 natives, so audio playback runs in simulated (silent) mode; video is unaffected.
+
+Continuous integration (`.github/workflows/ci.yml`) builds the solution Release with a zero-warnings policy, runs the full test suite, and re-runs the design-invariants audit on every push. Tagging `v*` (or `.github/workflows/release.yml` β Run workflow) publishes self-contained archives for linux-x64, linux-arm64, win-x64, and win-arm64.
+
---
## π§° Self-Contained Repository Skills & MCP Tools
diff --git a/docs/parity/deferred_registry.md b/docs/parity/deferred_registry.md
new file mode 100644
index 0000000..5484461
--- /dev/null
+++ b/docs/parity/deferred_registry.md
@@ -0,0 +1,18 @@
+# Deferred Registry (documented, not scheduled)
+
+Items consciously deferred after the true-parity program (Phases 5β9) with rationale.
+Each is a possible future work item; none blocks the Zune 4.8 experience Not-Zune delivers.
+
+| Item | Origin | Rationale for deferral |
+|---|---|---|
+| **i18n β all 26 Zune locales** | Zune shipped localized UIs (ZuneShellResources `.UIX` per locale) | Text is baked into AXAML; a resource-dictionary localization pass is mechanical but touches every view. English-only is acceptable for the current audience. |
+| **UPnP media sharing (ZuneNSS / `ZuneShareEXE` parity)** | Native component map: network sharing services | Zune's social sharing servers are dead; a local UPnP/DLNA renderer/server has no Zune-visible counterpart to validate against. |
+| **Explorer / taskbar shell integration (`ZuneShellExt_Dll`, `ZuneTaskbar_Dll`, `ZuneLauncherEXE`)** | Native component map | Windows-only, shell-level (context menus, taskbar previews). Cross-platform app; value is cosmetic. |
+| **MTPZ firmware update / restore / rollback (`ZuneWmduDLL` parity)** | Device lifecycle | Hardware N-A: no physical Zune device is available to develop/test against; the device-sync seam (`IDeviceTransport`) is the correct place to land this later. |
+| **Windows jump lists** | Shell integration | Windows-only convenience; documented alongside shell integration deferral. |
+| **Mini-player video surface** | Phase 8 polish backlog | The mini-player video mode is text-only; real video needs a `VideoView` surface inside the compact overlay. Video remains fully playable in the main surface and full playback view. |
+| **Mixview external related-artist satellites** | Phase 6 backlog | Mixview satellites currently use the local library (genre/mood/related); external MusicBrainz related-artist fetch would add network latency and rate-limit pressure to a purely visual surface. |
+| **Notification-area tray icon** | Polish backlog | Avalonia tray support is platform-quirky; Zune itself only had a taskbar presence. |
+| **CD Land real pipeline (Phase 10)** | Capability-gated phase | No optical drive is available on the development machine; the DISC view stays in its manual/simulated mode. Implementation should be done blind against platform tooling (`cdparanoia`/`cdrdao`/IMAPI2) only if explicitly requested. |
+
+Last updated: 2026-09-09 (post Phase 9).
diff --git a/docs/parity/true_parity_task_plan.md b/docs/parity/true_parity_task_plan.md
index b44a7ca..6c87b87 100644
--- a/docs/parity/true_parity_task_plan.md
+++ b/docs/parity/true_parity_task_plan.md
@@ -46,13 +46,13 @@ Goal: replace the simulated sync blob with a genuine sync-group engine so that (
## Phase 11 β Final Parity Sweep (P3)
-| # | Task |
-|---|---|
-| 11.1 | **CI/release packaging for native audio+video.** Add `apt-get install -y libvlc` (linux-x64/arm64 jobs) so published Linux builds get video; verify Bass natives ship in archives (they do β vendored); document the win-arm64 Bass limitation (simulated audio fallback) in release notes. |
-| 11.2 | **Re-run the parity audit** against `docs/parity/zune48_parity_audit.md` β update every status column, measure the delta from ~55%, refresh the executive summary. |
-| 11.3 | **Performance pass:** startup (deferred service init), large-library scan responsiveness, artwork decode caching, slideshow memory. |
-| 11.4 | **Polish backlog triage:** mini-player video surface (currently text-only), Mixview external related-artist satellites (currently local-only), notification-area tray icon. Fold in or move to deferred. |
-| 11.5 | **Deferred registry (documented, not scheduled):** i18n (26 locales), UPnP media sharing (ZuneNSS parity), Explorer/taskbar shell integration, MTPZ firmware update/restore/rollback (hardware N-A), Windows jump lists. |
+| # | Task | Status |
+|---|---|---|
+| 11.1 | **CI/release packaging for native audio+video.** Add `apt-get install -y libvlc` (linux-x64/arm64 jobs) so published Linux builds get video; verify Bass natives ship in archives (they do β vendored); document the win-arm64 Bass limitation (simulated audio fallback) in release notes. | β
Done β `.github/workflows/ci.yml` (build+test+audit with libvlc installed) and `release.yml` (4-RID self-contained publish, natives verification, platform notes); README Platform Notes added |
+| 11.2 | **Re-run the parity audit** against `docs/parity/zune48_parity_audit.md` β update every status column, measure the delta from ~55%, refresh the executive summary. | β
Done β dated re-audit snapshot added; β55β60% β β75β80% |
+| 11.3 | **Performance pass:** startup (deferred service init), large-library scan responsiveness, artwork decode caching, slideshow memory. | β
Done β startup was already lazy (pivot loads on navigation); SQLite WAL journaling for scan/sync responsiveness; artwork decode cache (600 tiles / 48 hi-res slideshow frames, bounded) |
+| 11.4 | **Polish backlog triage:** mini-player video surface (currently text-only), Mixview external related-artist satellites (currently local-only), notification-area tray icon. Fold in or move to deferred. | β
Done β all three triaged to the deferred registry with rationale |
+| 11.5 | **Deferred registry (documented, not scheduled):** i18n (26 locales), UPnP media sharing (ZuneNSS parity), Explorer/taskbar shell integration, MTPZ firmware update/restore/rollback (hardware N-A), Windows jump lists. | β
Done β `docs/parity/deferred_registry.md` (9 items incl. the 11.4 triage) |
## Execution Order
diff --git a/docs/parity/zune48_parity_audit.md b/docs/parity/zune48_parity_audit.md
index a2026fd..577212f 100644
--- a/docs/parity/zune48_parity_audit.md
+++ b/docs/parity/zune48_parity_audit.md
@@ -33,6 +33,10 @@ Both `review/ZunePackage.exe` and `review/ZuneSetupPkg.exe` verified: `ZunePacka
## 2. Executive Summary
+> **RE-AUDIT SNAPSHOT (2026-09-09, post Phases 5β9):** The critical gaps closed below moved weighted overall parity from **β 55β60% β β 75β80%** (weighted for UI presentation, hardware-dependent features neutral). Deltas per domain: **F. Audio engine** SIMULATED 0% β **HIGH ~85%** (ManagedBass 2.4: real decode, gapless, crossfade, ReplayGain, EQ, FFT visualizer, podcast streams); **L. First-launch** MISSING 0% β **HIGH ~85%** (welcomeβfoldersβprivacyβdone wizard, What's New, monitored-folder scan); **H. Device sync** SIMULATED ~35% β **MEDIUM ~65%** (sync-group engine, dry-run manifest, guest sessions, reverse sync, transport seam; real MTPZ hardware remains N-A); **C. Collection** 80% β **HIGH ~90%** (smart playlists, Find-Album-Info per-track matching, autocomplete, back-stack, Mixview tiles); **D. Now Playing** 75% β **HIGH ~85%** (now-playing video clips via libVLC); **I. Podcasts** 50% β **MEDIUM-HIGH ~70%** (real audio playback); **M. Platform services** 40% β **~50%** (video/photo libraries on the SQLite ZMDB substitute). Remaining below-70% domains: B. Quickplay (~65%), E. Mixview (~60%), G. CD Land (~40%, capability-gated β no optical drive), J. Social/Marketplace (N-A, dead servers), M. Platform services (~50%: no UPnP/share/MUI).
+>
+> The original pre-Phase-5 snapshot is preserved below for history.
+
Not-Zune is a **faithful UI shell** with a **growing feature set** built on clean architecture, but it has **one critical structural gap: there is no real audio playback engine** β `AudioEngine` (`src/NotZune.Infrastructure.Audio/AudioEngine.cs`) is a position-ticker simulation and no audio library (NAudio/ManagedBass) is referenced anywhere. Every audible experience (music, crossfade, ReplayGain, volume, the visualizer, podcast streams) is currently simulated; the only real audio output is `SoundEffectService` playing authentic Zune WAV chimes through OS CLI players.
| Domain | Parity | Verdict |
diff --git a/src/NotZune.Desktop/App.axaml.cs b/src/NotZune.Desktop/App.axaml.cs
index 1781d6e..e468254 100644
--- a/src/NotZune.Desktop/App.axaml.cs
+++ b/src/NotZune.Desktop/App.axaml.cs
@@ -100,6 +100,10 @@ public partial class App : Avalonia.Application
using var ctx = factory.CreateDbContext();
ctx.Database.EnsureCreated();
+ // WAL journaling persists in the database file and keeps large library
+ // scans/sync-group writes responsive.
+ ctx.Database.ExecuteSqlRaw("PRAGMA journal_mode=WAL;");
+
// Schema upgrades for databases created before later phases (EnsureCreated
// only provisions brand-new databases; it never alters existing ones).
ctx.Database.ExecuteSqlRaw(@"CREATE TABLE IF NOT EXISTS SmartPlaylists (
diff --git a/src/NotZune.UI/Converters/ArtworkSourceConverter.cs b/src/NotZune.UI/Converters/ArtworkSourceConverter.cs
index 5b286a6..3a3723d 100644
--- a/src/NotZune.UI/Converters/ArtworkSourceConverter.cs
+++ b/src/NotZune.UI/Converters/ArtworkSourceConverter.cs
@@ -1,8 +1,10 @@
using System;
+using System.Collections.Concurrent;
using System.Globalization;
using System.IO;
using Avalonia;
using Avalonia.Data.Converters;
+using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
@@ -11,6 +13,8 @@ namespace NotZune.UI.Converters;
///
/// Converts a local cached artwork file path (or avares:// asset URI) into an IImage for XAML bindings.
/// Returns null for missing sources so typographic fallback tiles remain visible.
+/// Decoded bitmaps are cached (keyed by source + decode width) so scrolling the collection or
+/// cycling the slideshow never re-decodes from disk.
///
public class ArtworkSourceConverter : IValueConverter
{
@@ -19,6 +23,9 @@ public class ArtworkSourceConverter : IValueConverter
/// High-resolution variant for photo zoom/slideshow surfaces.
public static readonly ArtworkSourceConverter Large = new() { DecodeWidth = 1600 };
+ private const int CacheCapacity = 600;
+ private static readonly ConcurrentDictionary Cache = new();
+
public int DecodeWidth { get; set; } = 300;
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
@@ -28,6 +35,19 @@ public class ArtworkSourceConverter : IValueConverter
return null;
}
+ var key = $"{DecodeWidth}|{source}";
+ // High-resolution slideshow/zoom surfaces get a far smaller cache (memory).
+ var capacity = DecodeWidth > 600 ? 48 : CacheCapacity;
+ if (Cache.Count > capacity)
+ {
+ Cache.Clear();
+ }
+
+ return Cache.GetOrAdd(key, _ => Decode(source));
+ }
+
+ private IImage? Decode(string source)
+ {
try
{
if (source.StartsWith("avares://", StringComparison.OrdinalIgnoreCase))
diff --git a/src/NotZune.UI/ViewModels/VideoLibraryViewModel.cs b/src/NotZune.UI/ViewModels/VideoLibraryViewModel.cs
index 6e9e8d6..657456d 100644
--- a/src/NotZune.UI/ViewModels/VideoLibraryViewModel.cs
+++ b/src/NotZune.UI/ViewModels/VideoLibraryViewModel.cs
@@ -73,7 +73,7 @@ public class VideoLibraryViewModel : ViewModelBase
_videoLibraryService = videoLibraryService;
_videoEngine = videoEngine;
- PlayVideoCommand = new AsyncRelayCommand