diff --git a/CHANGELOG.md b/CHANGELOG.md index 998cb032..1f85188c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,29 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.20.0] - 2026-03-29 + +### Added + +- **Chinese language (zh)**: Full UI translation contributed by [@jiezhuo](https://github.com/jiezhuo). Language can be selected in Settings → General. +- **Genres page** *(requested by [@grillonbleu](https://github.com/grillonbleu))*: New page (sidebar: Tags icon) showing all server genres as coloured cards — icon watermark, genre name, album count. Cards are sorted by album count descending and deterministically colour-coded from the Catppuccin palette. Clicking a card opens the album list for that genre. Navigating back restores the previous scroll position. +- **Genre filter on Albums, New Releases, Random Albums** *(requested by [@grillonbleu](https://github.com/grillonbleu))*: A multi-select genre combobox in the page header lets you filter any of these views to one or more genres. Chips show selected genres; backspace removes the last one; clicking outside collapses the filter automatically when nothing is selected. In filter mode, results are fetched in parallel across all selected genres and deduped client-side. +- **Settings — Contributors**: A new "Contributors" row in the About section credits community translators. + +### Changed + +- **Theme — W10** *(Operating Systems)*: New Windows 10 Fluent Design light theme. Clean white content area, flat light-grey `#F3F3F3` navigation pane, near-black `#1C1C1C` taskbar player bar with a Windows-blue `#0078D4` accent stripe, flat buttons without gradients (4 px radius). Sharp, unmistakably W10 — distinct from the glass-era W7/Vista and the rounded-corner W11. +- **ThemePicker — Windows themes sorted by release year**: W3.1 → W98 → WXP → Wista → W7 → W10 → W11. +- **Playlists page — removed**: The dedicated Playlists page has been removed. Playlists remain fully accessible via the Queue panel (Save / Load buttons in the toolbar). + +### Fixed + +- **FLAC seeking** *(Rust audio engine)*: `rodio`'s internal `ReadSeekSource` hardcodes `byte_len() → None`, which caused the symphonia FLAC demuxer to reject all seek attempts (it validates seek byte offsets against the total stream length). Replaced `rodio::Decoder` with a direct symphonia pipeline (`SizedDecoder`) that wraps the audio bytes in a `SizedCursorSource` providing the correct `byte_len()`. FLAC seeking now works regardless of whether the file has an embedded SEEKTABLE. +- **Genre missing in Queue meta box when playing from album card**: `playAlbum()` (used by the play button on all album cards) mapped song-level genre only — which Navidrome does not always return per song. Now falls back to the album-level genre from `getAlbum`. Same fallback applied to all three play/enqueue handlers in `AlbumDetail`. +- **Logo gradient CSS variables**: Sidebar logo gradient now uses `--logo-color-start` / `--logo-color-end` with fallbacks, allowing themes with dark sidebars to override the gradient colours. + +--- + ## [1.19.0] - 2026-03-27 ### Added diff --git a/README.md b/README.md index 19eb1b7c..b2a00536 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Designed specifically for users hosting their own music via Navidrome or other S - 🎨 **Gorgeous UI**: A large selection of beautiful, lean themes for every taste — Open Source Classics (Catppuccin, Nord, Gruvbox), Operating Systems, Games, Movies, Series, Psysonic originals, and Mediaplayer — with smooth glassmorphism effects and micro-animations. - ⚡ **Blazing Fast**: Built with Rust & Tauri — native audio engine (rodio), minimal RAM usage compared to typical Electron apps. -- 🌍 **Internationalization (i18n)**: Fully translated into English, German, French, and Dutch. +- 🌍 **Internationalization (i18n)**: Fully translated into English, German, French, Dutch, and Chinese. - 📻 **Live "Now Playing"**: See what other users on your server are currently listening to in real-time. - 🎵 **Last.fm Integration**: Direct scrobbling, Now Playing updates, love/unlove, Similar Artists, and top stats — no Navidrome configuration required. - 🎤 **Synchronized Lyrics**: In-sidebar lyrics pane powered by LRCLIB — synced with auto-scroll and line highlighting, plain-text fallback. @@ -36,6 +36,7 @@ Designed specifically for users hosting their own music via Navidrome or other S - ⌨️ **Configurable Keybindings**: Rebind any playback action (play/pause, next, seek, volume…) directly in Settings. - 🔤 **Font Picker**: 10 UI fonts to choose from in Settings → Appearance. - 🎼 **Random Mix**: Generate a random playlist from your entire library. Filter by keyword or pick a Super Genre (Metal, Rock, Electronic, Jazz…) for a focused mix with progressive loading. +- 🏷️ **Genres**: Browse your entire library by genre — coloured cards sorted by album count with a dedicated album view per genre. Multi-select genre filter available on Albums, New Releases, and Random Albums pages. - 🔄 **Update Notifications**: Built-in update checker (on startup + every 10 minutes) that notifies you when a new version is available on GitHub. - 🖥️ **Cross-Platform**: Available natively for Windows, macOS, and Linux (including Wayland support). @@ -56,7 +57,7 @@ Designed specifically for users hosting their own music via Navidrome or other S - [x] IndexedDB image caching - [x] Random Mix with keyword filter & Super Genre mix - [x] Large theme library across 8 groups: Open Source Classics, Operating Systems, Games, Movies, Series, Social Media, Psysonic originals, Mediaplayer -- [x] Internationalization (English, German, French, Dutch) +- [x] Internationalization (English, German, French, Dutch, Chinese) - [x] AUR package (Arch / CachyOS) - [x] Configurable keybindings - [x] Font picker (10 UI fonts) @@ -71,7 +72,6 @@ Designed specifically for users hosting their own music via Navidrome or other S ## ● Known Limitations - **Linux (drag & drop cursor feedback)**: Due to a WebKitGTK limitation, the drag cursor does not reflect the drop operation type — it may appear as a "forbidden" symbol or show no indicator at all, depending on the desktop environment. Drag and drop itself works correctly. -- **FLAC seeking**: Seeking in FLAC files requires an embedded SEEKTABLE metadata block. Files encoded without one cannot be seeked — clicking the waveform has no effect. Most modern encoders include a SEEKTABLE by default. You can add one retroactively with `metaflac --add-seekpoint=10s *.flac`. ## 📥 Installation diff --git a/package.json b/package.json index 0b7040e9..0cfc73d9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "psysonic", - "version": "1.19.0", + "version": "1.20.0", "private": true, "scripts": { "dev": "vite", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 29729ce5..d06426df 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3370,6 +3370,7 @@ dependencies = [ "serde", "serde_json", "souvlaki", + "symphonia", "tauri", "tauri-build", "tauri-plugin-dialog", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index d56b5947..23226d39 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -30,6 +30,7 @@ tauri-plugin-fs = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" rodio = { version = "0.19", default-features = false, features = ["symphonia-all"] } +symphonia = { version = "0.5", default-features = false, features = ["flac", "mp3", "pcm", "aac", "isomp4", "vorbis", "wav", "adpcm"] } reqwest = { version = "0.12", features = ["stream", "json"] } md5 = "0.7" tokio = { version = "1", features = ["rt", "time"] } diff --git a/src-tauri/src/audio.rs b/src-tauri/src/audio.rs index b425438f..00c71d08 100644 --- a/src-tauri/src/audio.rs +++ b/src-tauri/src/audio.rs @@ -1,12 +1,21 @@ -use std::io::Cursor; +use std::io::{Cursor, Read, Seek}; use std::sync::{Arc, Mutex}; use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering}; use std::time::{Duration, Instant}; use biquad::{Biquad, Coefficients, DirectForm2Transposed, ToHertz, Type as FilterType}; -use rodio::{Decoder, Sink, Source}; +use rodio::{Sink, Source}; use rodio::source::UniformSourceIterator; use serde::Serialize; +use symphonia::core::{ + audio::{AudioBufferRef, SampleBuffer, SignalSpec}, + codecs::{DecoderOptions, CODEC_TYPE_NULL}, + formats::{FormatOptions, FormatReader, SeekMode, SeekTo}, + io::{MediaSource, MediaSourceStream}, + meta::MetadataOptions, + probe::Hint, + units::{self, Time}, +}; use tauri::{AppHandle, Emitter, State}; // ─── 10-Band Graphic Equalizer ──────────────────────────────────────────────── @@ -394,6 +403,264 @@ impl> Source for CountingSource { } } +// ─── SizedCursorSource — MediaSource with correct byte_len ──────────────────── +// +// rodio's internal ReadSeekSource wraps Cursor> but hardcodes +// byte_len() → None. This tells symphonia "stream length unknown", which +// prevents the FLAC demuxer from seeking (it validates seek offsets against +// the total stream length from byte_len). MP3 is unaffected because its +// demuxer uses Xing/LAME headers instead. +// +// This wrapper provides the actual byte length, fixing seek for all formats. + +struct SizedCursorSource { + inner: Cursor>, + len: u64, +} + +impl Read for SizedCursorSource { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + self.inner.read(buf) + } +} + +impl Seek for SizedCursorSource { + fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result { + self.inner.seek(pos) + } +} + +impl MediaSource for SizedCursorSource { + fn is_seekable(&self) -> bool { true } + fn byte_len(&self) -> Option { Some(self.len) } +} + +// ─── SizedDecoder — symphonia decoder with correct byte_len ─────────────────── +// +// Replaces rodio::Decoder::new() which wraps the source in ReadSeekSource +// (byte_len = None). This constructs the symphonia pipeline directly, +// providing the correct byte_len via SizedCursorSource. +// +// Implements Iterator + Source — identical interface to +// rodio::Decoder, so the rest of the source chain is unchanged. + +const DECODE_MAX_RETRIES: usize = 3; + +struct SizedDecoder { + decoder: Box, + current_frame_offset: usize, + format: Box, + total_duration: Option