fix(local): refresh availability after source changes

Reject stale availability probes across first-folder and last-folder mutations.

Refs #723

cx
This commit is contained in:
vicrodh committed 2026-08-28 02:57:22 -06:00
1 parent 3d0ee66708
commit 3da21c2752
2 files changed
+45 -17

No files matched your search

+11 -16
View File
@@ -24,7 +24,7 @@ use cxx_qt_lib::QString;
use crate::local_bridge_ops::{
emit_artwork, emit_artwork_one, invalidate_artists, load_tab_impl, load_tracks,
publish_plex_state, publish_tree, reload_browse, run_sync,
publish_availability, publish_plex_state, publish_tree, reload_browse, run_sync,
};
use crate::local_library_qt as lib;
use crate::local_plex as plex;
@@ -929,21 +929,16 @@ impl qbz_local::QbzLocal {
let sort = lib::tracks_sort();
let group = lib::tracks_group();
let tracks_filter = lib::tracks_filter();
crate::spawn(async move {
let available = tokio::task::spawn_blocking(lib::has_library)
.await
.unwrap_or(false);
ui(move |mut b| {
b.as_mut().set_local_available(available);
b.as_mut()
.set_local_album_mode(QString::from(mode.as_str()));
b.as_mut()
.set_local_tracks_sort(QString::from(sort.as_str()));
b.as_mut()
.set_local_tracks_group(QString::from(group.as_str()));
b.as_mut()
.set_local_tracks_filter(QString::from(tracks_filter.as_str()));
});
publish_availability();
ui(move |mut b| {
b.as_mut()
.set_local_album_mode(QString::from(mode.as_str()));
b.as_mut()
.set_local_tracks_sort(QString::from(sort.as_str()));
b.as_mut()
.set_local_tracks_group(QString::from(group.as_str()));
b.as_mut()
.set_local_tracks_filter(QString::from(tracks_filter.as_str()));
});
publish_plex_state();
crate::local_album_actions::publish_track_artwork();
+34 -1
View File
@@ -11,13 +11,15 @@
//! Every loader publishes its own tab's document + the shared badges; the
//! Plex helpers publish the gates and drive the manual sync.
use std::sync::atomic::{AtomicU64, Ordering};
use cxx_qt_lib::QString;
use crate::local_bridge::ui;
use crate::local_library_qt as lib;
use crate::local_plex as plex;
/// Republish the tab badges + availability after any load.
/// Republish the tab badges after any load.
pub(crate) fn publish_counts() {
let counts = lib::to_json(&lib::counts());
ui(move |mut b| {
@@ -26,6 +28,34 @@ pub(crate) fn publish_counts() {
});
}
/// Republish whether any browse source currently makes Local Library usable.
///
/// `localAvailable` gates the entire Local Library surface, but it used to be
/// seeded only once during bridge boot. Adding the first folder (or removing
/// the last one) then refreshed every document behind the gate without ever
/// changing the gate itself. Keep this asynchronous because `has_library`
/// opens the per-user SQLite database.
///
/// A generation prevents a slower, older database read from overwriting the
/// answer from a newer source mutation.
static AVAILABILITY_GENERATION: AtomicU64 = AtomicU64::new(0);
pub(crate) fn publish_availability() {
let generation = AVAILABILITY_GENERATION
.fetch_add(1, Ordering::AcqRel)
.wrapping_add(1);
crate::spawn(async move {
let available = tokio::task::spawn_blocking(lib::has_library)
.await
.unwrap_or(false);
if AVAILABILITY_GENERATION.load(Ordering::Acquire) != generation {
log::debug!("[qbz-qt] discarded stale local availability generation {generation}");
return;
}
ui(move |mut b| b.as_mut().set_local_available(available));
});
}
pub(crate) fn publish_tree(nodes_json: String) {
ui(move |mut b| {
b.as_mut()
@@ -144,6 +174,9 @@ fn pruned_albums_filter(has_jellyfin: bool, has_subsonic: bool) -> String {
pub(crate) fn reload_browse() {
publish_media_gates();
// This is the shared mutation chokepoint for folders and remote sources.
// Republish the gate as well as the documents it controls (#723).
publish_availability();
// The cortinilla's instant-paint cache embeds LOCAL rows and their artwork
// paths, so anything that reaches here has just made those rows possibly
// wrong. This is the single chokepoint for local-library mutations, which