merge: integrate the listening-session QA fixes into pre-release

cc
This commit is contained in:
vicrodh committed 2026-09-13 18:47:04 -06:00
commit 61f187194a
8 files changed
+97 -16

No files matched your search

+55
View File
@@ -17,6 +17,7 @@ use std::sync::Mutex;
use std::time::{SystemTime, UNIX_EPOCH};
use discord_rich_presence::{activity, DiscordIpc, DiscordIpcClient};
use std::time::{Duration, Instant};
/// Public Discord Application ID for QBZ. Public identifier, not a secret.
const DISCORD_APP_ID: &str = "1501835855587708988";
@@ -102,6 +103,24 @@ pub struct NowListening {
pub struct DiscordRpc {
client: Mutex<Option<DiscordIpcClient>>,
enabled: AtomicBool,
/// Set after a failed IPC connect: no new attempt before this instant.
/// Without it every now-playing edge (two per track: the immediate
/// publish and the poll's) knocked on a socket that is not there and
/// the crate logged a warning each time — with Discord closed, a
/// listening session read as an error stream.
retry_after: Mutex<Option<Instant>>,
}
/// How long a failed IPC connect keeps the integration quiet. A track edge
/// inside the window pushes nothing; the first edge after it retries.
pub const CONNECT_RETRY_AFTER: Duration = Duration::from_secs(300);
/// Pure backoff rule: attempt when no deadline is set or it has passed.
pub fn connect_allowed(now: Instant, retry_after: Option<Instant>) -> bool {
match retry_after {
None => true,
Some(deadline) => now >= deadline,
}
}
impl DiscordRpc {
@@ -109,6 +128,7 @@ impl DiscordRpc {
Self {
client: Mutex::new(None),
enabled: AtomicBool::new(false),
retry_after: Mutex::new(None),
}
}
@@ -124,6 +144,11 @@ impl DiscordRpc {
if !enabled {
self.clear();
}
// Opting in (again) is an explicit request: forget any backoff so the
// next update connects right away.
if let Ok(mut retry) = self.retry_after.lock() {
*retry = None;
}
}
/// Clear the activity and drop the IPC connection (no-op when not connected).
@@ -151,6 +176,13 @@ impl DiscordRpc {
};
if guard.is_none() {
let now = Instant::now();
let Ok(mut retry) = self.retry_after.lock() else {
return;
};
if !connect_allowed(now, *retry) {
return;
}
// Bridge the Flatpak sandbox to Discord's IPC socket if we're
// running under Flatpak. No-op outside that context.
prepare_flatpak_discord_socket_links();
@@ -159,6 +191,13 @@ impl DiscordRpc {
let mut client = DiscordIpcClient::new(DISCORD_APP_ID);
if client.connect().is_ok() {
*guard = Some(client);
*retry = None;
} else {
*retry = Some(now + CONNECT_RETRY_AFTER);
log::info!(
"[discord] Discord is not reachable (not running, or its socket is not visible); next attempt in {} s",
CONNECT_RETRY_AFTER.as_secs()
);
}
}
@@ -208,3 +247,19 @@ impl DiscordRpc {
}
}
}
#[cfg(test)]
mod backoff_tests {
use super::*;
#[test]
fn connect_backoff_waits_for_the_deadline_then_retries() {
let now = Instant::now();
assert!(connect_allowed(now, None));
let deadline = now + CONNECT_RETRY_AFTER;
assert!(!connect_allowed(now, Some(deadline)));
assert!(!connect_allowed(now + CONNECT_RETRY_AFTER / 2, Some(deadline)));
assert!(connect_allowed(deadline, Some(deadline)));
assert!(connect_allowed(deadline + Duration::from_secs(1), Some(deadline)));
}
}
+6 -2
View File
@@ -60,9 +60,13 @@ fn install_with_file_sink(default_level: &str, file_name: Option<&str>) {
// in #555 logs) — and each suppressed record now costs nothing, since
// `log!` checks the filter before formatting. An explicit RUST_LOG still
// replaces the whole default, so full zbus tracing stays one env var away.
// discord_rich_presence warns on every failed IPC connect ("find_pipe:
// could not find pipe"); qbz-integrations logs its own single line and
// backs off, so the crate's copy only needs to surface real errors.
let inner = env_logger::Builder::from_env(
env_logger::Env::default()
.default_filter_or(format!("{default_level},zbus=warn,tracing=warn")),
env_logger::Env::default().default_filter_or(format!(
"{default_level},zbus=warn,tracing=warn,discord_rich_presence=error"
)),
)
.build();
let level = inner.filter();
-1
View File
@@ -26,7 +26,6 @@ Rectangle {
// window edge may opt into a lateral list without changing every other
// QbzSelect instance. Supported values: "below" and "left".
property string popupPlacement: "below"
property bool enabled: true
property bool searchable: false
// Bootstrap-style small variant (QbzSelect.slint:74-76). The default is the
// standard Settings size; TOOLBARS opt in with `sm: true` — 30px tall, r6,
-1
View File
@@ -14,7 +14,6 @@ Rectangle {
id: root
property bool checked: false
property bool enabled: true
signal toggled(bool value)
QbzTheme { id: theme }
@@ -20,7 +20,6 @@ Rectangle {
property string trailingIconName: ""
property bool danger: false
property bool busy: false
property bool enabled: true
// The two metrics the MyQBZ modal footers and the Disco builder need. The
// Slint's SecondaryButton is 34px/min-160 in Settings and 38px/hug in a
// modal footer, which is one control with two call-site numbers — not two
@@ -53,7 +53,9 @@ Rectangle {
readonly property color primaryColor: QbzShell.ambientPrimary
readonly property color accentColor: QbzShell.ambientAccent
readonly property color secondaryColor: QbzShell.ambientSecondary
readonly property var palette: [
// `ringPalette`, not `palette`: Item already owns a `palette` (QPalette)
// and shadowing it drew a propertyCache warning on every start.
readonly property var ringPalette: [
root.primaryColor,
root.accentColor,
root.secondaryColor,
@@ -65,15 +67,15 @@ Rectangle {
readonly property var bandNames: ["SUB", "BASS", "MID", "PRES", "AIR"]
function paletteColor(position, alpha) {
var count = root.palette.length
var count = root.ringPalette.length
var wrapped = position % count
if (wrapped < 0)
wrapped += count
var lo = Math.floor(wrapped)
var hi = (lo + 1) % count
var mix = wrapped - lo
var a = Qt.lighter(root.palette[lo], 1.0)
var b = Qt.lighter(root.palette[hi], 1.0)
var a = Qt.lighter(root.ringPalette[lo], 1.0)
var b = Qt.lighter(root.ringPalette[hi], 1.0)
return Qt.rgba(a.r + (b.r - a.r) * mix,
a.g + (b.g - a.g) * mix,
a.b + (b.b - a.b) * mix,
@@ -181,7 +183,7 @@ Rectangle {
"speed": 0.006 + intensity * 0.013,
"color": root.nextColor
})
root.nextColor = (root.nextColor + 1) % root.palette.length
root.nextColor = (root.nextColor + 1) % root.ringPalette.length
if (next.length > root.maxPulseRings)
next.shift()
}
+12 -1
View File
@@ -1001,11 +1001,22 @@ Rectangle {
anchors.rightMargin: 10
anchors.verticalCenter: parent.verticalCenter
spacing: 0
// The title's natural width, measured OUTSIDE the
// Text: an elided Text recomputes its implicit size
// after laying out at the bound width, so a `width`
// that reads its own `implicitWidth` is a binding
// loop (Qt reported it on every album open).
TextMetrics {
id: compactTitleMetrics
font.pixelSize: theme.fontSection
font.weight: theme.weightBold
text: albumHeader.title || ""
}
Text {
id: compactAlbumTitle
width: (albumHeader.artist || "") === ""
? compactHeading.width
: Math.min(implicitWidth,
: Math.min(Math.ceil(compactTitleMetrics.advanceWidth),
Math.max(80, compactHeading.width * 0.62))
text: albumHeader.title || ""
color: root.hdrStrong
+17 -5
View File
@@ -122,11 +122,23 @@ static DISCOVER: OnceLock<Mutex<Option<DiscoverPrefsStore>>> = OnceLock::new();
fn with_discover<T>(f: impl FnOnce(&DiscoverPrefsStore) -> T) -> Option<T> {
let cell = DISCOVER.get_or_init(|| {
let store =
crate::sidebar_qt::user_dir().and_then(|dir| DiscoverPrefsStore::new_at(&dir).ok());
if store.is_none() {
log::warn!("[qbz-qt] discover prefs store unavailable");
}
// Before the session restores there is no user directory yet; that
// is the normal startup order, not a failure (`bind_qobuz_user`
// replaces this handle on activation). Only a directory that exists
// and still cannot open its store is worth a warning.
let store = match crate::sidebar_qt::user_dir() {
None => {
log::debug!("[qbz-qt] discover prefs store not bound yet: no active user");
None
}
Some(dir) => match DiscoverPrefsStore::new_at(&dir) {
Ok(store) => Some(store),
Err(e) => {
log::warn!("[qbz-qt] discover prefs store unavailable: {e}");
None
}
},
};
Mutex::new(store)
});
let guard = cell.lock().ok()?;