forked from Keelrleek/SoulStream
playlists + lyrics: setlists with pages of their own, and the words timed to the voice
This commit is contained in:
1 parent
c4ff6269a3
commit
df3d15553a
49 files changed
+14419
-599
No files matched your search
+29
-109
@@ -143,6 +143,9 @@ pub async fn set_settings(
|
||||
volume_normalization: Option<bool>,
|
||||
) -> Result<(), String> {
|
||||
let mut cfg = config::load(&app);
|
||||
let library_folder_moved = download_dir
|
||||
.as_deref()
|
||||
.is_some_and(|d| Some(d) != cfg.download_dir.as_deref());
|
||||
if let Some(d) = download_dir {
|
||||
cfg.download_dir = Some(d);
|
||||
}
|
||||
@@ -208,6 +211,25 @@ pub async fn set_settings(
|
||||
}
|
||||
config::save(&app, &cfg);
|
||||
|
||||
// A new library folder is a new library. Re-base immediately rather than
|
||||
// at the next launch, so what the shelves show is what the folder holds —
|
||||
// the old behaviour left the previous folder's albums standing forever,
|
||||
// and emptying the new folder could not remove them.
|
||||
if library_folder_moved {
|
||||
let app2 = app.clone();
|
||||
let root = config::download_dir(&app, &cfg);
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let lib = app2.state::<crate::pipeline::LibraryState>().library.clone();
|
||||
if let Ok(Ok(dropped)) =
|
||||
tauri::async_runtime::spawn_blocking(move || lib.prune_outside_root(&root)).await
|
||||
{
|
||||
log::info!("library folder changed: dropped {dropped} tracks from the old folder");
|
||||
}
|
||||
let _ = crate::pipeline::rescan_library(app2.clone()).await;
|
||||
let _ = app2.emit("soulseek:library-changed", ());
|
||||
});
|
||||
}
|
||||
|
||||
// Tighter limits take effect right away, not at the next stream/startup.
|
||||
if retention_changed {
|
||||
let app2 = app.clone();
|
||||
@@ -1263,10 +1285,13 @@ pub struct CatalogHeroPreviewResult {
|
||||
const CATALOG_HERO_GPU_MAGIC: &[u8; 4] = b"SHG1";
|
||||
const CATALOG_HERO_GPU_HEADER_BYTES: usize = 16;
|
||||
|
||||
/// The CHOSEN hero per artist — a decision about this library, not a render
|
||||
/// cache — so it travels with the library. (The upscaled results keyed by URL
|
||||
/// and recipe stay in the machine's cache dir: they regenerate cheaply and
|
||||
/// would otherwise put gigabytes of derived pixels on the drive.)
|
||||
fn artist_hero_dir(app: &tauri::AppHandle) -> Result<std::path::PathBuf, String> {
|
||||
let dir = app
|
||||
.path()
|
||||
.app_data_dir()
|
||||
let cfg = config::load(app);
|
||||
let dir = config::ensure_library_data_dir(app, &cfg)
|
||||
.map_err(|e| e.to_string())?
|
||||
.join("artist-heroes");
|
||||
std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
|
||||
@@ -4366,7 +4391,7 @@ pub async fn home_warm(app: tauri::AppHandle, lib: State<'_, LibraryState>) -> R
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---- playlists ----
|
||||
// ---- playlists live in src-tauri/src/playlists.rs ----
|
||||
|
||||
fn now_secs() -> i64 {
|
||||
std::time::SystemTime::now()
|
||||
@@ -4375,111 +4400,6 @@ fn now_secs() -> i64 {
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_playlists(
|
||||
lib: State<'_, LibraryState>,
|
||||
) -> Result<Vec<library::Playlist>, String> {
|
||||
let l = lib.library.clone();
|
||||
tauri::async_runtime::spawn_blocking(move || l.list_playlists().map_err(|e| e.to_string()))
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn create_playlist(
|
||||
app: tauri::AppHandle,
|
||||
lib: State<'_, LibraryState>,
|
||||
name: String,
|
||||
) -> Result<i64, String> {
|
||||
let l = lib.library.clone();
|
||||
let id = tauri::async_runtime::spawn_blocking(move || l.create_playlist(&name, now_secs()))
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
.map_err(|e| e.to_string())?;
|
||||
let _ = app.emit("soulseek:playlists-changed", ());
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn delete_playlist(
|
||||
app: tauri::AppHandle,
|
||||
lib: State<'_, LibraryState>,
|
||||
id: i64,
|
||||
) -> Result<(), String> {
|
||||
let l = lib.library.clone();
|
||||
tauri::async_runtime::spawn_blocking(move || l.delete_playlist(id))
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
.map_err(|e| e.to_string())?;
|
||||
let _ = app.emit("soulseek:playlists-changed", ());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn playlist_tracks(
|
||||
lib: State<'_, LibraryState>,
|
||||
id: i64,
|
||||
) -> Result<Vec<library::PlaylistTrack>, String> {
|
||||
let l = lib.library.clone();
|
||||
tauri::async_runtime::spawn_blocking(move || l.playlist_tracks(id).map_err(|e| e.to_string()))
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct TrackInput {
|
||||
pub title: String,
|
||||
pub artist: String,
|
||||
pub duration_secs: Option<u32>,
|
||||
pub path: String,
|
||||
pub art_path: Option<String>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn add_to_playlist(
|
||||
app: tauri::AppHandle,
|
||||
lib: State<'_, LibraryState>,
|
||||
playlist_id: i64,
|
||||
tracks: Vec<TrackInput>,
|
||||
) -> Result<(), String> {
|
||||
let l = lib.library.clone();
|
||||
tauri::async_runtime::spawn_blocking(move || -> library::Result<()> {
|
||||
for t in tracks {
|
||||
l.add_to_playlist(
|
||||
playlist_id,
|
||||
&library::PlaylistTrack {
|
||||
position: 0, // assigned by insert
|
||||
title: t.title,
|
||||
artist: t.artist,
|
||||
duration_secs: t.duration_secs,
|
||||
path: t.path,
|
||||
art_path: t.art_path,
|
||||
},
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
.map_err(|e| e.to_string())?;
|
||||
let _ = app.emit("soulseek:playlists-changed", ());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn remove_from_playlist(
|
||||
lib: State<'_, LibraryState>,
|
||||
playlist_id: i64,
|
||||
position: i64,
|
||||
) -> Result<(), String> {
|
||||
let l = lib.library.clone();
|
||||
tauri::async_runtime::spawn_blocking(move || l.remove_from_playlist(playlist_id, position))
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compute (or fetch cached) waveform peaks for a local audio file.
|
||||
/// Returns `buckets` normalized 0..1 values, or an empty array on failure.
|
||||
#[tauri::command]
|
||||
|
||||
Reference in new issue
Block a user