feat(playlists): integrate Navidrome smart playlist flow into playlists page

Move smart playlist creation and management into Playlists, including Navidrome-only gating, smart-name/icon presentation, and smarter refresh handling while server-side smart rules are being applied.
This commit is contained in:
Maxim Isaev
2026-04-24 16:41:17 +03:00
parent 1c761682f4
commit 27a59f6b23
8 changed files with 785 additions and 15 deletions
+97
View File
@@ -565,6 +565,99 @@ async fn nd_set_user_libraries(
Ok(())
}
/// GET `/api/playlist` — list playlists; pass `smart=true` to filter smart playlists.
#[tauri::command]
async fn nd_list_playlists(
server_url: String,
token: String,
smart: Option<bool>,
) -> Result<serde_json::Value, String> {
let resp = nd_retry(|| {
let client = nd_http_client();
let mut req = client
.get(format!("{}/api/playlist", server_url))
.header("X-ND-Authorization", format!("Bearer {}", token));
if let Some(s) = smart {
req = req.query(&[("smart", s)]);
}
req.send()
})
.await?;
if !resp.status().is_success() {
return Err(format!("HTTP {}", resp.status()));
}
resp.json::<serde_json::Value>().await.map_err(nd_err)
}
/// POST `/api/playlist` — create playlist (supports smart rules payload).
#[tauri::command]
async fn nd_create_playlist(
server_url: String,
token: String,
body: serde_json::Value,
) -> Result<serde_json::Value, String> {
let resp = nd_retry(|| {
nd_http_client()
.post(format!("{}/api/playlist", server_url))
.header("X-ND-Authorization", format!("Bearer {}", token))
.json(&body)
.send()
})
.await?;
let status = resp.status();
let text = resp.text().await.unwrap_or_default();
if !status.is_success() {
return Err(format!("HTTP {}: {}", status, text));
}
serde_json::from_str(&text).map_err(|e| e.to_string())
}
/// PUT `/api/playlist/{id}` — update playlist (supports smart rules payload).
#[tauri::command]
async fn nd_update_playlist(
server_url: String,
token: String,
id: String,
body: serde_json::Value,
) -> Result<serde_json::Value, String> {
let resp = nd_retry(|| {
nd_http_client()
.put(format!("{}/api/playlist/{}", server_url, id))
.header("X-ND-Authorization", format!("Bearer {}", token))
.json(&body)
.send()
})
.await?;
let status = resp.status();
let text = resp.text().await.unwrap_or_default();
if !status.is_success() {
return Err(format!("HTTP {}: {}", status, text));
}
Ok(serde_json::from_str(&text).unwrap_or(serde_json::Value::Null))
}
/// DELETE `/api/playlist/{id}` — delete playlist.
#[tauri::command]
async fn nd_delete_playlist(
server_url: String,
token: String,
id: String,
) -> Result<(), String> {
let resp = nd_retry(|| {
nd_http_client()
.delete(format!("{}/api/playlist/{}", server_url, id))
.header("X-ND-Authorization", format!("Bearer {}", token))
.send()
})
.await?;
let status = resp.status();
if !status.is_success() {
let text = resp.text().await.unwrap_or_default();
return Err(format!("HTTP {}: {}", status, text));
}
Ok(())
}
const RADIO_PAGE_SIZE: u32 = 25;
/// Search the radio-browser.info directory (needs User-Agent header — CORS would block WebView).
@@ -3724,6 +3817,10 @@ pub fn run() {
nd_delete_user,
nd_list_libraries,
nd_set_user_libraries,
nd_list_playlists,
nd_create_playlist,
nd_update_playlist,
nd_delete_playlist,
search_radio_browser,
get_top_radio_stations,
fetch_url_bytes,