is_disc_folder() was matching any folder name containing "Disc N" or
"CDN" anywhere in the name. This caused albums like "100 Popular
Classics, Disc 1" or "Now 75 - CD1" under Various Artists to be treated
as disc subfolders, collapsing all tracks into a single album group.
Now is_disc_folder() only returns true when the folder name consists
exclusively of disc-related tokens (disc/cd/disk, digits, and modifiers
like "bonus"). Album names with extra words are correctly left as-is.
Users must re-scan their library for the fix to take effect.
When download fails after retry at the same quality (e.g. HiRes),
automatically try the next lower quality (e.g. Lossless). This handles
CDN edges that consistently serve broken responses for specific tracks
at certain quality levels. Applied to all three download paths: standard
download, streaming probe fallback, and prefetch. Also fixes prefetch
retry using wrong quality variable.
Add Drop impl to PlaybackEngine so the ALSA Direct writer thread is
properly stopped when the engine is dropped (e.g. streaming timeout).
Previously, a failed streaming attempt would orphan the writer thread,
keeping the ALSA device locked and blocking all subsequent playback.
Also force HTTP/1.1 for audio downloads and add detailed error chain
logging to diagnose persistent "error decoding response body" failures
for specific tracks.
Refactor ALSA Direct engine from thread-per-source to a single
long-lived writer thread with a source queue. When one source ends,
the writer thread immediately picks up the next from the queue with
zero gap. Remove the ALSA Direct gapless rejection gate since the
engine now natively supports source queueing.
When PipeWire drops the stream during pause, the graph reverts to the
DAC's default rate (e.g. 44100Hz). The pre-stream force-rate metadata
can be ignored when no streams are active. Re-applying after the stream
is connected forces PipeWire to reconfigure the graph at the correct
sample rate.
Also fix unused import warning for MusicBrainzSharedState in commands_v2
by using the import instead of the full path in factory_reset.
- Add filename-based track number inference as fallback when tags lack track_number
- Detect degenerate track numbering (all tracks same number) and use index fallback
- Flatten single-track-per-disc box sets into sequential numbering
- Replace fragile SELECT * with explicit column list in track queries
ListenBrainz similar-artists returned mainstream crossover artists (SOAD,
Led Zeppelin for Metallica) instead of genre-accurate results. Now uses
MusicBrainz tag search to find artists sharing the seed's primary genre tag
(e.g. "thrash metal" returns Megadeth, Slayer, Kreator, etc).
- Add search_artists_by_tag to MusicBrainz client
- Deterministic shuffle using seed MBID hash for variety across artists
- Secondary tag fallback when primary yields fewer than 5 results
- Rename label to "You May Also Like", limit results to 6
- Add Last.fm get_similar_artists method (unused, for future use)
- Add tags field to ArtistFullResponse model
- Tighten local history exclusion to play_count > 2
insert_qobuz_cached_track_direct was hardcoding track_number to 0,
causing all imported tracks to show wrong numbers. Now accepts
track_number and disc_number params, and both callers (backfill
downloads and offline cache sync) extract metadata from the audio
file via MetadataExtractor before inserting.
Also fix LocalLibraryView to use || instead of ?? for track_number
fallback, so 0 correctly falls back to index-based numbering.
Closes#147
Display parental_warning badge (E icon) next to track titles in all
views: TrackRow, QueuePanel, NowPlayingBar, ImmersivePlayer with all
13 visualizer panels, MiniPlayer surfaces, and AlbumDetailView.
Propagate parental_warning field through the full data chain from
Rust backend (qbz-models, queue, commands_v2, session_store, API
server) to frontend stores, adapters, and UI components. Badge uses
CSS mask technique with explicit.svg for theme compatibility.
Only write to gain atomic on initial measurement. Refinements now
update the cache only, preventing audible volume changes within a
single track. Inter-track normalization still works via cached values.
insert_track() now checks downloaded_purchases table to set source.
get_downloaded_purchase_track_ids() verifies file existence and
auto-removes stale entries. Closes#115
The library scanner's album_root_dir() only knew to skip disc folders
(Disc 1, CD2, etc.) when resolving the album directory. Files stored
in quality subfolders like artist/album/FLAC 24-bit - 96 kHz/track.flac
caused each encoding variant to appear as a separate album card.
Add is_encoding_folder() to detect common audio format directory names
(FLAC, MP3, AAC, WAV, etc.) and make album_root_dir() skip past both
encoding and disc subdirectories, handling chained nesting too.
Fixes the workaround from #116.
When ALSA backend detects hardware doesn't support a track's sample rate
(e.g. 176400Hz on a DAC that supports up to 192000Hz but not 176.4kHz),
automatically re-request the track from Qobuz at Hi-Res quality instead
of Ultra Hi-Res. This avoids resampling and maintains bit-perfect playback
by getting a version at a rate the hardware natively supports.
Adds device_supports_sample_rate() and get_device_supported_rates()
public API to qbz-audio for hardware capability queries via /proc/asound.
When transitioning from ALSA Direct to CPAL fallback (e.g. for rate
resampling), PipeWire auto-re-acquires the device after ALSA Direct
releases it. CPAL then can't open the device.
Suspend PipeWire sinks before creating CPAL stream in exclusive mode,
same pattern used in ALSA Direct DeviceBusy handler.
When ALSA Direct opens a PCM device and fails to set the rate, the
USB DAC doesn't release the device fast enough for CPAL to open it
afterwards. This caused "device busy" errors even with a delay.
New approach: read /proc/asound/cardN/stream0 to check supported
rates BEFORE opening the device. If the rate isn't supported, skip
ALSA Direct entirely and fall through to CPAL (which has rate
fallback logic). The device is never touched at the wrong rate.
When ALSA Direct opens a PCM device and fails to configure the rate,
the PCM handle is dropped but the kernel may not release the device
instantly. CPAL then fails with "Device busy" when trying to open it.
Add 100ms sleep after InvalidParams to let the kernel fully release
the device before CPAL attempts to open it. Also fix error messages
to show the effective (fallback) rate instead of the original rate.
When ALSA Direct can't handle a rate (e.g. 176.4kHz) and falls back
to CPAL, CPAL also fails because the hardware doesn't support it.
Port find_best_fallback_rate from PipeWire backend: queries supported
rates via CPAL, picks highest rate in same family (44.1k or 48k)
that's <= requested. 176.4kHz on a 96kHz-max DAC now falls back to
88.2kHz and rodio resamples, instead of failing entirely.
When hardware doesn't natively support a sample rate (e.g. 176.4kHz),
return None from try_create_direct_stream instead of a hard error.
This lets the player fall through to CPAL/rodio which can resample.
Applied to both hw and plughw paths — InvalidParams on rate now
triggers graceful fallback instead of killing playback.
When switching from PipeWire to ALSA Direct, PipeWire holds the ALSA
device open causing EBUSY on the first snd_pcm_open. Now detects
DeviceBusy, suspends the PipeWire sink via pactl, waits 200ms for
release, then retries. Applied to both hw and plughw paths.
cpal 0.15 returned "ALSA" from HostId::name(), cpal 0.17 returns "Alsa"
(now uses stringify! on the enum variant). Use eq_ignore_ascii_case()
for the comparison so it works regardless of cpal version.
Query the DAC's supported sample rates from /proc/asound/cardN/stream0
(maps PipeWire sink to ALSA card via pactl). If the track's native rate
isn't supported (e.g., 176.4kHz), fall back to the nearest rate in the
same family (e.g., 88.2kHz). Rodio resamples from track rate to stream
rate automatically, so audio plays instead of producing silence.
rodio 0.22's with_supported_config() resets buffer_size to Default via
..Default::default(), so our BufferSize settings in StreamConfig were
never reaching CPAL. MixerDeviceSink has zero internal buffering, making
CPAL's buffer the ONLY buffer between the mixer and audio hardware.
Chain .with_buffer_size(Fixed(sample_rate/10)) AFTER with_supported_config()
to actually pass ~100ms buffer to CPAL, matching old vendored cpal behavior.
PipeWire manages its own buffer scheduling via quantum. Forcing a fixed
buffer size (sample_rate / 10 = 19200 frames at 192kHz) conflicts with
PipeWire's internal scheduling and causes buffer underruns that produce
audible noise.
BufferSize::Fixed is kept only for:
- Exclusive mode (512 frames, low latency)
- ALSA backend (direct ALSA, not through PipeWire)
rodio 0.22's MixerDeviceSink has an internal mixer thread that cannot
synchronize with PipeWire's forced quantum scheduling. This caused
massive buffer underruns (600+) at sample rates >= 88.2kHz when Force
Bit Perfect was enabled.
clock.force-rate alone is sufficient for bit-perfect sample rate
switching. Logs confirm zero underruns at 88.2kHz and minimal underruns
at 192kHz (only during heavy I/O) when quantum is not forced.
Replace BufferSize::Default with BufferSize::Fixed(sample_rate / 10)
for non-exclusive mode. This preserves the 100ms period tuning that was
previously applied in the vendored cpal ALSA backend, reducing CPU
wakeups during playback.
- OutputStream/OutputStreamHandle -> DeviceSinkBuilder/MixerDeviceSink
- Sink -> Player (as RodioPlayer)
- current_frame_len -> current_span_len in all Source impls
- Source::channels() returns NonZero<u16>, sample_rate() returns NonZero<u32>
- SamplesBuffer no longer generic, constructor takes NonZero args
- SampleRate is now u32 type alias (no tuple struct)
- Decoder::new_mp4 no longer takes Mp4Type hint
- Applied across both crates/ workspace and src-tauri/ legacy code
Remove vendored rodio and cpal from src-tauri/vendor/ since rodio 0.22
now includes upstream f32 decoder support (our vendored modification).
Update all Cargo.toml files to use rodio 0.22 from crates.io and remove
the [patch.crates-io] section.
- Allow dead_code on TokenValidationResponse.code (deserialized but not read)
- Remove unused MusicProvider import in commands_v2.rs
- Restore #[allow(non_snake_case)] on v2_plex_cache_get_tracks
- Replace drop(tokens) with let _ = tokens (drop on reference is no-op)
Migrate downloaded_purchases to compound PK (track_id, format_id) so
the same track downloaded in MP3 and FLAC are tracked separately.
Album detail endpoint now returns downloaded_format_ids per track.
Qobuz redirects to play.qobuz.com/discover?code_autorisation=XXX
not ?code=XXX. Both the navigation intercept and the /oauth/callback
API call were using the wrong parameter name, so the code was never
captured and the exchange request was malformed.
Add get_all_custom_artist_images() database method and
v2_library_get_all_custom_artist_images command to support
loading all custom artist image overrides at once for the
global resolution store.
The server echoes every state report as a SET_STATE with only next_track
(playing_state=null, current_track=null). These echoes were triggering
align_corebridge_queue_cursor which uses the accumulated renderer state
current_track (from a different context, e.g. iOS). When the track
wasn't found in the local queue, it replaced the entire queue with a
single track, destroying the user's loaded queue.
Now echo SET_STATE commands are detected early and only update the
internal renderer state (for next_track tracking). CoreBridge actions
(align cursor, load track, play/pause) and state reports are skipped
entirely for these echoes.
The server echoes every RNDR_SRVR_STATE_UPDATED as a SET_STATE command
with only next_track updated (playing_state=null, current_track=null).
send_renderer_reports was replying to these echo SET_STATE commands
with another state report, creating an infinite loop that generated
47k log lines (8.2MB) in seconds and completely blocked playback.
Fix: only send state reports for substantive SET_STATE commands that
carry playing_state or current_track changes. Echo commands (next_track
only) are silently consumed.
Also demote diagnostic logging from info to debug to reduce terminal
noise during normal operation.
The server rejects renderer state reports with "Current track not found
in queue nor autoplay" after QBZ-initiated queue loads, because it assigns
non-standard queue_item_ids (first track = track_id, rest = 1..N).
Omit current_queue_item_id and next_queue_item_id from all outbound
renderer reports. The server already knows the current track from
SET_STATE commands and adds the correct queue_item_id when forwarding
to controllers.
Also add info-level diagnostic logging to both report paths (periodic
frontend reports and command-response reports) and the protobuf encoder
for verifying wire content.
Root cause: server pause/resume commands send None for position and
tracks, which the renderer state blindly overwrote. Then renderer
reports sent null position to the server, making controllers show the
track as finished.
Three-part fix:
1. renderer.rs: only overwrite position/tracks when server provides
a non-None value (retain last known values through pause/resume)
2. app.rs + qconnect_service.rs: update renderer state position from
frontend periodic reports so the internal state always has the
real playback position
3. +page.svelte: increase position report frequency from 5s to 2s
for smoother seekbar updates on controllers
ErrorMessage.code was defined as int32 but server sends string codes
(e.g. 'ERROR_QUEUE_INSERT_TRACKS'). Changed to string type and added
logging for queue error responses.
Added context_uuid (random UUID v4) to queue insert and add commands -
the server likely requires this to associate tracks with a context.