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