Reworked library. Added local, my, fed traks overview.

This commit is contained in:
Ultradesu
2026-07-25 23:24:49 +03:00
parent 788bc2e01f
commit c45f2e700b
8 changed files with 511 additions and 49 deletions
+146
View File
@@ -47,6 +47,8 @@ pub struct Runtime {
Arc<std::sync::Mutex<std::collections::HashMap<String, Option<String>>>>,
pub library_network_done: Arc<std::sync::Mutex<std::collections::HashSet<String>>>,
pub library_network_mode: crate::config::settings::LibrarySourceMode,
pub library_network_art_fetching: Arc<std::sync::atomic::AtomicBool>,
pub library_network_art_attempted: Arc<std::sync::Mutex<std::collections::HashSet<String>>>,
/// Coalesces urgent personal-device syncs after remote playback commands.
pub device_sync_running: Arc<std::sync::atomic::AtomicBool>,
pub device_sync_requested: Arc<std::sync::atomic::AtomicBool>,
@@ -84,6 +86,15 @@ fn err_string(err: anyhow::Error) -> String {
format!("{err:#}")
}
fn refresh_local_content_ids(runtime: &Runtime) {
let library = Arc::clone(&runtime.library);
let tx = runtime.event_tx.clone();
tokio::task::spawn_blocking(move || {
let result = library.local_content_ids().map_err(err_string);
let _ = tx.send(AppEvent::LocalContentIdsLoaded(result));
});
}
fn spawn_content_id_backfill(runtime: &Runtime) {
let library = Arc::clone(&runtime.library);
let federation = Arc::clone(&runtime.federation);
@@ -193,6 +204,10 @@ pub async fn run(
library_network_cursors: Arc::new(std::sync::Mutex::new(std::collections::HashMap::new())),
library_network_done: Arc::new(std::sync::Mutex::new(std::collections::HashSet::new())),
library_network_mode: crate::config::settings::LibrarySourceMode::Local,
library_network_art_fetching: Arc::new(std::sync::atomic::AtomicBool::new(false)),
library_network_art_attempted: Arc::new(std::sync::Mutex::new(
std::collections::HashSet::new(),
)),
device_sync_running: Arc::new(std::sync::atomic::AtomicBool::new(false)),
device_sync_requested: Arc::new(std::sync::atomic::AtomicBool::new(false)),
fed_resolving: std::sync::Mutex::new(std::collections::HashSet::new()),
@@ -254,6 +269,8 @@ pub async fn run(
}
if state.should_quit {
state.shutting_down = true;
terminal.draw(|frame| ui::draw(frame, &state, &keymap))?;
runtime.federation.shutdown().await;
return Ok(());
}
@@ -778,6 +795,7 @@ fn maintenance(state: &mut AppState, runtime: &mut Runtime) {
}
maybe_refresh_network_library(state, runtime);
maybe_fetch_network_artist_images(state, runtime);
// Liked ids load once per session — markers are shown everywhere.
if !state.likes_loaded {
@@ -791,6 +809,10 @@ fn maintenance(state: &mut AppState, runtime: &mut Runtime) {
let _ = tx.send(AppEvent::FedLikesLoaded(result));
});
}
if !state.local_content_ids_loaded {
state.local_content_ids_loaded = true;
refresh_local_content_ids(runtime);
}
// Playlists tab data (also wanted while the add-to-playlist picker is
// open from any tab).
@@ -933,6 +955,9 @@ fn maybe_refresh_network_library(state: &AppState, runtime: &mut Runtime) {
if let Ok(mut done) = runtime.library_network_done.lock() {
done.clear();
}
if let Ok(mut attempted) = runtime.library_network_art_attempted.lock() {
attempted.clear();
}
}
let near_end = state
.global
@@ -1026,6 +1051,103 @@ fn maybe_refresh_network_library(state: &AppState, runtime: &mut Runtime) {
});
}
fn maybe_fetch_network_artist_images(state: &AppState, runtime: &mut Runtime) {
if state.active_tab != state::Tab::Global
|| !state.global.stack.is_empty()
|| !state.global.filters.source_mode.includes_network()
|| !state.federation.settings.enabled
{
return;
}
let capacity = artist_grid_capacity().max(24);
let start = state.global.selected.saturating_sub(capacity / 2);
let names = state
.global
.artists
.iter()
.skip(start)
.take(capacity * 2)
.filter(|artist| artist.image_path.is_none() && artist.availability.is_remoteish())
.map(|artist| artist.name.clone())
.collect::<Vec<_>>();
if names.is_empty() {
return;
}
use std::sync::atomic::Ordering;
if runtime
.library_network_art_fetching
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
.is_err()
{
return;
}
let filters = state.global.filters;
let library = Arc::clone(&runtime.library);
let federation = Arc::clone(&runtime.federation);
let tx = runtime.event_tx.clone();
let attempted = Arc::clone(&runtime.library_network_art_attempted);
let running = Arc::clone(&runtime.library_network_art_fetching);
tokio::spawn(async move {
let query_library = Arc::clone(&library);
let requests = tokio::task::spawn_blocking(move || {
query_library.network_artist_image_requests(filters, &names, 8)
})
.await
.map_err(|err| anyhow::anyhow!("network art query failed: {err:#}"))
.and_then(|result| result);
let requests = match requests {
Ok(requests) => requests,
Err(err) => {
tracing::debug!("network artist image requests failed: {err:#}");
running.store(false, Ordering::SeqCst);
return;
}
};
for request in requests {
let attempt_key = format!("{}:{}", request.source_id, request.artist_key);
let should_try = attempted
.lock()
.map(|mut attempted| attempted.insert(attempt_key))
.unwrap_or(false);
if !should_try {
continue;
}
let Some(path) = federation
.card_image(
std::slice::from_ref(&request.source_id),
&request.name,
None,
)
.await
else {
continue;
};
let update_library = Arc::clone(&library);
let source_id = request.source_id.clone();
let artist_key = request.artist_key.clone();
let saved = tokio::task::spawn_blocking(move || {
update_library.set_network_artist_image(&source_id, &artist_key, &path)
})
.await
.map_err(|err| anyhow::anyhow!("network art save failed: {err:#}"))
.and_then(|result| result);
match saved {
Ok(true) => {
let _ = tx.send(AppEvent::NetworkArtistCacheUpdated {
source_id: request.source_id,
count: 1,
});
}
Ok(false) => {}
Err(err) => tracing::debug!("network artist image save failed: {err:#}"),
}
}
running.store(false, Ordering::SeqCst);
});
}
/// Load and decode a local image file for the art cache.
fn spawn_art_fetch(runtime: &Runtime, key: String, path: String, width: u16, height: u16) {
let tx = runtime.event_tx.clone();
@@ -2075,6 +2197,9 @@ pub(crate) fn fed_download_spawn(
let progress = download_progress_sender(tx.clone(), title);
match fed.download_to_library_with_progress(track, progress).await {
Ok(imported) => {
if let Some(content_id) = track_content_id(&imported) {
let _ = tx.send(AppEvent::LocalContentAvailable { content_id });
}
imported_ids.push(imported.id);
imported_fed_tracks.push(track.clone());
}
@@ -2288,6 +2413,7 @@ fn on_library_changed(state: &mut AppState, runtime: &mut Runtime) {
// Likes reload on the next maintenance pass; the old set stays visible
// until then.
state.likes_loaded = false;
state.local_content_ids_loaded = false;
// Fresh copies of whatever sits in the queue. Federated placeholders
// and ephemeral tracks (negative ids) are not library rows and keep
@@ -2747,6 +2873,12 @@ fn handle_app_event(state: &mut AppState, runtime: &mut Runtime, event: AppEvent
match result {
Ok(playable) => {
if playable.imported {
if let Some(content_id) = state::track_content_id(&playable.track) {
state.local_content_ids.insert(content_id.clone());
let _ = runtime
.event_tx
.send(AppEvent::LocalContentAvailable { content_id });
}
// Save-on-listen imported the file; refresh the
// library views through the standard change path.
let _ = runtime.event_tx.send(AppEvent::LibraryChanged {
@@ -3072,6 +3204,20 @@ fn handle_app_event(state: &mut AppState, runtime: &mut Runtime, event: AppEvent
}
Err(message) => tracing::warn!(%message, "likes load failed"),
},
AppEvent::LocalContentIdsLoaded(result) => match result {
Ok(ids) => {
state.local_content_ids = ids.into_iter().collect();
}
Err(message) => {
state.local_content_ids_loaded = false;
tracing::warn!(%message, "local content id load failed");
}
},
AppEvent::LocalContentAvailable { content_id } => {
if let Some(content_id) = music_dht::normalize_content_id(&content_id) {
state.local_content_ids.insert(content_id);
}
}
AppEvent::FedLikesLoaded(result) => match result {
Ok(ids) => state.fed_likes = ids.into_iter().collect(),
Err(message) => tracing::warn!(%message, "federated likes load failed"),