Refetch mutable library listings and redact stream URLs from logs
The TUI cached every library listing for the whole session, so a finished capture (or a saved queue) never appeared under /captures until a restart — captures looked broken while they had succeeded on disk. Listings under /captures, /queues, /bookmarks, and /fs are now always refetched (cheap local walks on the server); remote provider nodes keep the instant back-navigation cache. The player engine also logged full stream URLs (including googlevideo sig tokens) through its play span; sources are now logged as scheme://host only, local paths verbatim. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
973bb7bbc6
commit
c84dec9ca2
|
|
@ -18,6 +18,18 @@ use url::Url;
|
|||
|
||||
/// How long we wait for the initial prefetch of a network stream.
|
||||
const STREAM_OPEN_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
|
||||
/// What playback logs for a source: local paths verbatim, network URLs
|
||||
/// reduced to scheme and host — stream URLs embed access tokens
|
||||
/// (googlevideo `sig`, tidal tokens) and must never reach the log.
|
||||
fn display_source(source_str: &str) -> String {
|
||||
match Url::parse(source_str) {
|
||||
Ok(url) if matches!(url.scheme(), "http" | "https") => {
|
||||
format!("{}://{}/…", url.scheme(), url.host_str().unwrap_or("?"))
|
||||
}
|
||||
_ => source_str.to_string(),
|
||||
}
|
||||
}
|
||||
/// Interval between elapsed-position updates while playing.
|
||||
const TICK_INTERVAL: Duration = Duration::from_millis(250);
|
||||
|
||||
|
|
@ -176,7 +188,7 @@ impl PlayerEngine {
|
|||
});
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
#[instrument(skip_all, fields(source = %display_source(source_str)))]
|
||||
pub fn play(&mut self, source_str: &str) -> Result<MediaInfo> {
|
||||
self.reset();
|
||||
|
||||
|
|
@ -203,7 +215,10 @@ impl PlayerEngine {
|
|||
self.generation += 1;
|
||||
let duration = match Url::parse(source_str) {
|
||||
Ok(url) if matches!(url.scheme(), "http" | "https") => {
|
||||
trace!(%url, "opening network stream");
|
||||
trace!(
|
||||
host = url.host_str().unwrap_or("?"),
|
||||
"opening network stream"
|
||||
);
|
||||
// Windowed fetching: some CDNs (googlevideo) reject plain
|
||||
// and open-ended requests with 403 and only serve bounded
|
||||
// ranges (see audio-player/src/windowed_http.rs).
|
||||
|
|
@ -401,3 +416,22 @@ fn send_reply<T>(tx: Sender<T>, value: T) {
|
|||
warn!("player engine reply receiver dropped");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn logged_sources_never_carry_url_tokens() {
|
||||
// Stream URLs embed access tokens; only scheme and host may be
|
||||
// logged. Local paths pass through verbatim.
|
||||
assert_eq!(
|
||||
display_source("https://rr1.googlevideo.com/videoplayback?sig=SECRET&x=1"),
|
||||
"https://rr1.googlevideo.com/…"
|
||||
);
|
||||
assert_eq!(
|
||||
display_source("/home/user/music/song.m4a"),
|
||||
"/home/user/music/song.m4a"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,22 @@ pub struct RpcClient {
|
|||
pub update_stream: Streaming<GetUpdateStreamResponse>,
|
||||
}
|
||||
|
||||
/// Whether a library listing may be served from the session cache.
|
||||
///
|
||||
/// The server-side folder providers mutate behind the client's back —
|
||||
/// captures finish (`W`), queues get saved (`w`), bookmarks appear,
|
||||
/// files change on disk — so a cached listing turns freshly captured
|
||||
/// content invisible until a restart. Their listings are cheap local
|
||||
/// directory walks on the server; always refetch them. Remote provider
|
||||
/// nodes (tidal, youtube) keep the cache that makes back-navigation
|
||||
/// instant.
|
||||
fn is_cacheable(path: &str) -> bool {
|
||||
const MUTABLE_ROOTS: [&str; 4] = ["/captures", "/queues", "/bookmarks", "/fs"];
|
||||
!MUTABLE_ROOTS.iter().any(|root| {
|
||||
path == *root || (path.starts_with(root) && path.as_bytes().get(root.len()) == Some(&b'/'))
|
||||
})
|
||||
}
|
||||
|
||||
impl RpcClient {
|
||||
pub async fn connect(addr: &'static str) -> Result<RpcClient, Box<dyn Error>> {
|
||||
let endpoint = Endpoint::from_static(addr).connect_lazy();
|
||||
|
|
@ -79,7 +95,7 @@ impl RpcClient {
|
|||
&mut self,
|
||||
path: &str,
|
||||
) -> Result<Option<&LibraryNode>, Box<dyn Error>> {
|
||||
if self.library_node_cache.contains_key(path) {
|
||||
if is_cacheable(path) && self.library_node_cache.contains_key(path) {
|
||||
return Ok(self.library_node_cache.get(path));
|
||||
}
|
||||
let get_library_node_request = Request::new(GetLibraryNodeRequest {
|
||||
|
|
@ -90,6 +106,8 @@ impl RpcClient {
|
|||
.get_library_node(get_library_node_request)
|
||||
.await?;
|
||||
if let Some(library_node) = response.into_inner().node {
|
||||
// Non-cacheable nodes are stored too (the return borrows from
|
||||
// the map) — they are just always refetched above.
|
||||
self.library_node_cache
|
||||
.insert(path.to_string(), library_node);
|
||||
return Ok(self.library_node_cache.get(path));
|
||||
|
|
@ -298,3 +316,31 @@ impl RpcClient {
|
|||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn mutable_provider_listings_are_never_cached() {
|
||||
// Freshly captured/saved content must show up on the next visit
|
||||
// (a cached /captures hid new captures until a TUI restart).
|
||||
for path in [
|
||||
"/captures",
|
||||
"/captures/faves",
|
||||
"/queues",
|
||||
"/queues/road trip",
|
||||
"/bookmarks/b",
|
||||
"/fs/music",
|
||||
] {
|
||||
assert!(!is_cacheable(path), "{path}");
|
||||
}
|
||||
// Remote providers keep instant back-navigation…
|
||||
for path in ["/", "/tidal", "/tidal/artists/1", "/youtube/search/x"] {
|
||||
assert!(is_cacheable(path), "{path}");
|
||||
}
|
||||
// …and prefix look-alikes are not swept up.
|
||||
assert!(is_cacheable("/fsdy"));
|
||||
assert!(is_cacheable("/queuestore"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,22 @@
|
|||
# Implementation summaries
|
||||
|
||||
## capture visibility + log redaction (2026-07-21, follow-up)
|
||||
|
||||
"Problems with capturing" turned out to be a display bug: the capture
|
||||
had succeeded on disk, but the TUI's `RpcClient` caches every library
|
||||
listing for the whole session, so a `/captures` (or `/queues`,
|
||||
`/bookmarks`, `/fs`) listing visited once never showed later captures
|
||||
or saved queues until a restart. Listings under those mutable roots
|
||||
are now always refetched — they are cheap local directory walks on the
|
||||
server — while remote provider nodes (tidal, youtube) keep the cache
|
||||
that makes back-navigation instant (`is_cacheable`, unit-tested).
|
||||
|
||||
Found alongside in the same log: the player engine's `play` span
|
||||
recorded the full stream URL — googlevideo `sig` tokens included — into
|
||||
`cbd.log`. Sources are now logged as `scheme://host/…` only (local
|
||||
paths verbatim; `display_source`, unit-tested). 181 workspace tests
|
||||
green.
|
||||
|
||||
## youtube stream fetching (2026-07-21, follow-up)
|
||||
|
||||
The rustypipe swap fixed the decode problem but real playback then hit
|
||||
|
|
|
|||
Loading…
Reference in New Issue