Files
qobuz-qt/rust/src/lib.rs
joren 5ae18afa08 feat: full artist release list via artist/getReleasesList
Instead of relying on the limited preview in artist/page, fire a
separate artist/getReleasesList request per release type (album,
epSingle, live, compilation) in parallel when loading an artist.
Each result updates its section independently as it arrives, so the
page populates progressively without a single large request.

Also fixes the artist name in the status bar (was reading wrong field).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 13:53:57 +01:00

721 lines
24 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! qobuz-backend: C-ABI library consumed by the Qt frontend.
//!
//! Event type constants second argument of EventCallback:
//! 0 EV_LOGIN_OK { "token": "…", "user": { … } }
//! 1 EV_LOGIN_ERR { "error": "…" }
//! 2 EV_SEARCH_OK SearchCatalogDto
//! 3 EV_SEARCH_ERR { "error": "…" }
//! 4 EV_ALBUM_OK AlbumDto
//! 5 EV_ALBUM_ERR { "error": "…" }
//! 6 EV_ARTIST_OK ArtistDto
//! 7 EV_ARTIST_ERR { "error": "…" }
//! 8 EV_PLAYLIST_OK PlaylistDto
//! 9 EV_PLAYLIST_ERR { "error": "…" }
//! 10 EV_FAV_TRACKS_OK { "items": […], "total": N }
//! 11 EV_FAV_ALBUMS_OK { "items": […], "total": N }
//! 12 EV_FAV_ARTISTS_OK { "items": […], "total": N }
//! 13 EV_PLAYLISTS_OK { "items": […], "total": N }
//! 14 EV_TRACK_CHANGED TrackDto
//! 15 EV_STATE_CHANGED { "state": "playing"|"paused"|"idle"|"error" }
//! 16 EV_POSITION { "position": u64, "duration": u64 }
//! 17 EV_TRACK_URL_OK TrackFileUrlDto
//! 18 EV_TRACK_URL_ERR { "error": "…" }
//! 19 EV_GENERIC_ERR { "error": "…" }
mod api;
mod player;
use std::{
ffi::{CStr, CString},
os::raw::{c_char, c_int, c_void},
sync::Arc,
};
use api::{Format, QobuzClient};
use player::{Player, PlayerState};
use tokio::runtime::Runtime;
use tokio::sync::Mutex;
// ---------- Send-safe raw pointer wrapper ----------
/// Wraps a `*mut c_void` so it can cross thread boundaries.
/// SAFETY: The Qt QobuzBackend object is kept alive for the Backend's lifetime
/// and callbacks only call QMetaObject::invokeMethod (thread-safe Qt API).
#[derive(Clone, Copy)]
struct SendPtr(*mut c_void);
unsafe impl Send for SendPtr {}
unsafe impl Sync for SendPtr {}
// ---------- Event type constants ----------
pub const EV_LOGIN_OK: c_int = 0;
pub const EV_LOGIN_ERR: c_int = 1;
pub const EV_SEARCH_OK: c_int = 2;
pub const EV_SEARCH_ERR: c_int = 3;
pub const EV_ALBUM_OK: c_int = 4;
pub const EV_ALBUM_ERR: c_int = 5;
pub const EV_ARTIST_OK: c_int = 6;
pub const EV_ARTIST_ERR: c_int = 7;
pub const EV_PLAYLIST_OK: c_int = 8;
pub const EV_PLAYLIST_ERR: c_int = 9;
pub const EV_FAV_TRACKS_OK: c_int = 10;
pub const EV_FAV_ALBUMS_OK: c_int = 11;
pub const EV_FAV_ARTISTS_OK: c_int = 12;
pub const EV_PLAYLISTS_OK: c_int = 13;
pub const EV_TRACK_CHANGED: c_int = 14;
pub const EV_STATE_CHANGED: c_int = 15;
pub const EV_POSITION: c_int = 16;
pub const EV_TRACK_URL_OK: c_int = 17;
pub const EV_TRACK_URL_ERR: c_int = 18;
pub const EV_GENERIC_ERR: c_int = 19;
pub const EV_ARTIST_RELEASES_OK: c_int = 24;
// ---------- Callback ----------
pub type EventCallback = unsafe extern "C" fn(*mut c_void, c_int, *const c_char);
// ---------- Backend ----------
struct PrefetchedTrack {
track_id: i64,
track: api::models::TrackDto,
url: String,
}
struct BackendInner {
client: Arc<Mutex<QobuzClient>>,
player: Player,
rt: Runtime,
cb: EventCallback,
ud: SendPtr,
replaygain_enabled: std::sync::Arc<std::sync::atomic::AtomicBool>,
prefetch: std::sync::Arc<tokio::sync::Mutex<Option<PrefetchedTrack>>>,
}
pub struct Backend(BackendInner);
// ---------- Helpers ----------
fn call_cb(cb: EventCallback, ud: SendPtr, ev: c_int, json: &str) {
// Strip null bytes that would cause CString::new to fail
let safe = json.replace('\0', "");
let cstr = CString::new(safe).unwrap_or_else(|_| CString::new("{}").unwrap());
unsafe { cb(ud.0, ev, cstr.as_ptr()) };
}
fn err_json(msg: &str) -> String {
serde_json::json!({ "error": msg }).to_string()
}
/// Spawn a Send + 'static future on the backend's Tokio runtime.
fn spawn<F>(inner: &BackendInner, f: F)
where
F: std::future::Future<Output = ()> + Send + 'static,
{
inner.rt.spawn(f);
}
// ---------- Construction / destruction ----------
#[no_mangle]
pub unsafe extern "C" fn qobuz_backend_new(
event_cb: EventCallback,
userdata: *mut c_void,
) -> *mut Backend {
let rt = match Runtime::new() {
Ok(r) => r,
Err(_) => return std::ptr::null_mut(),
};
let client = match QobuzClient::new() {
Ok(c) => Arc::new(Mutex::new(c)),
Err(_) => return std::ptr::null_mut(),
};
let player = Player::new();
Box::into_raw(Box::new(Backend(BackendInner {
client,
player,
rt,
cb: event_cb,
ud: SendPtr(userdata),
replaygain_enabled: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
prefetch: std::sync::Arc::new(tokio::sync::Mutex::new(None)),
})))
}
#[no_mangle]
pub unsafe extern "C" fn qobuz_backend_free(ptr: *mut Backend) {
if !ptr.is_null() {
drop(Box::from_raw(ptr));
}
}
// ---------- Auth ----------
#[no_mangle]
pub unsafe extern "C" fn qobuz_backend_login(
ptr: *mut Backend,
email: *const c_char,
password: *const c_char,
) {
let inner = &(*ptr).0;
let email = CStr::from_ptr(email).to_string_lossy().into_owned();
let password = CStr::from_ptr(password).to_string_lossy().into_owned();
let client = inner.client.clone();
let cb = inner.cb;
let ud = inner.ud;
spawn(inner, async move {
let result = client.lock().await.login(&email, &password).await;
let (ev, json) = match result {
Ok(resp) => {
let token = resp
.oauth2.as_ref().and_then(|o| o.access_token.as_deref())
.or(resp.user_auth_token.as_deref())
.unwrap_or("")
.to_string();
let user_val = resp.user.as_ref()
.map(|u| serde_json::to_value(u).unwrap_or_default())
.unwrap_or_default();
(EV_LOGIN_OK, serde_json::json!({"token": token, "user": user_val}).to_string())
}
Err(e) => (EV_LOGIN_ERR, err_json(&e.to_string())),
};
call_cb(cb, ud, ev, &json);
});
}
#[no_mangle]
pub unsafe extern "C" fn qobuz_backend_set_token(ptr: *mut Backend, token: *const c_char) {
let inner = &(*ptr).0;
let token = CStr::from_ptr(token).to_string_lossy().into_owned();
let client = inner.client.clone();
// blocking_lock is available on tokio::sync::Mutex when not in an async context
inner.rt.spawn(async move {
client.lock().await.set_auth_token(token);
});
}
// ---------- Search ----------
#[no_mangle]
pub unsafe extern "C" fn qobuz_backend_search(
ptr: *mut Backend,
query: *const c_char,
offset: u32,
limit: u32,
) {
let inner = &(*ptr).0;
let query = CStr::from_ptr(query).to_string_lossy().into_owned();
let client = inner.client.clone();
let cb = inner.cb; let ud = inner.ud;
spawn(inner, async move {
let result = client.lock().await.search(&query, offset, limit).await;
let (ev, json) = match result {
Ok(r) => (EV_SEARCH_OK, serde_json::to_string(&r).unwrap_or_default()),
Err(e) => (EV_SEARCH_ERR, err_json(&e.to_string())),
};
call_cb(cb, ud, ev, &json);
});
}
// ---------- Album ----------
#[no_mangle]
pub unsafe extern "C" fn qobuz_backend_get_album(ptr: *mut Backend, album_id: *const c_char) {
let inner = &(*ptr).0;
let album_id = CStr::from_ptr(album_id).to_string_lossy().into_owned();
let client = inner.client.clone();
let cb = inner.cb; let ud = inner.ud;
spawn(inner, async move {
let result = client.lock().await.get_album(&album_id).await;
let (ev, json) = match result {
Ok(r) => (EV_ALBUM_OK, serde_json::to_string(&r).unwrap_or_default()),
Err(e) => (EV_ALBUM_ERR, err_json(&e.to_string())),
};
call_cb(cb, ud, ev, &json);
});
}
// ---------- Artist ----------
#[no_mangle]
pub unsafe extern "C" fn qobuz_backend_get_artist(ptr: *mut Backend, artist_id: i64) {
let inner = &(*ptr).0;
let client = inner.client.clone();
let cb = inner.cb; let ud = inner.ud;
spawn(inner, async move {
let result = client.lock().await.get_artist_page(artist_id).await;
let (ev, json) = match result {
Ok(r) => (EV_ARTIST_OK, serde_json::to_string(&r).unwrap_or_default()),
Err(e) => (EV_ARTIST_ERR, err_json(&e.to_string())),
};
call_cb(cb, ud, ev, &json);
});
}
// ---------- Artist releases ----------
#[no_mangle]
pub unsafe extern "C" fn qobuz_backend_get_artist_releases(
ptr: *mut Backend,
artist_id: i64,
release_type: *const c_char,
limit: u32,
offset: u32,
) {
let inner = &(*ptr).0;
let client = inner.client.clone();
let cb = inner.cb; let ud = inner.ud;
let rtype = CStr::from_ptr(release_type).to_string_lossy().into_owned();
spawn(inner, async move {
let result = client.lock().await
.get_artist_releases_list(artist_id, &rtype, limit, offset)
.await;
let (ev, json) = match result {
Ok(r) => {
// Wrap with the release_type so Qt can route to the right section
let mut obj = r.as_object().cloned().unwrap_or_default();
obj.insert("release_type".to_string(), serde_json::Value::String(rtype));
(EV_ARTIST_RELEASES_OK, serde_json::to_string(&obj).unwrap_or_default())
}
Err(e) => (EV_GENERIC_ERR, err_json(&e.to_string())),
};
call_cb(cb, ud, ev, &json);
});
}
// ---------- Playlist ----------
#[no_mangle]
pub unsafe extern "C" fn qobuz_backend_get_playlist(
ptr: *mut Backend,
playlist_id: i64,
offset: u32,
limit: u32,
) {
let inner = &(*ptr).0;
let client = inner.client.clone();
let cb = inner.cb; let ud = inner.ud;
spawn(inner, async move {
let result = client.lock().await.get_playlist(playlist_id, offset, limit).await;
let (ev, json) = match result {
Ok(r) => (EV_PLAYLIST_OK, serde_json::to_string(&r).unwrap_or_default()),
Err(e) => (EV_PLAYLIST_ERR, err_json(&e.to_string())),
};
call_cb(cb, ud, ev, &json);
});
}
// ---------- Favorites ----------
#[no_mangle]
pub unsafe extern "C" fn qobuz_backend_get_fav_tracks(ptr: *mut Backend, offset: u32, limit: u32) {
let inner = &(*ptr).0;
let client = inner.client.clone();
let cb = inner.cb; let ud = inner.ud;
spawn(inner, async move {
let result = client.lock().await.get_fav_tracks(offset, limit).await;
let (ev, json) = match result {
Ok(r) => (EV_FAV_TRACKS_OK, serde_json::to_string(&r).unwrap_or_default()),
Err(e) => (EV_GENERIC_ERR, err_json(&e.to_string())),
};
call_cb(cb, ud, ev, &json);
});
}
#[no_mangle]
pub unsafe extern "C" fn qobuz_backend_get_fav_albums(ptr: *mut Backend, offset: u32, limit: u32) {
let inner = &(*ptr).0;
let client = inner.client.clone();
let cb = inner.cb; let ud = inner.ud;
spawn(inner, async move {
let result = client.lock().await.get_fav_albums(offset, limit).await;
let (ev, json) = match result {
Ok(r) => (EV_FAV_ALBUMS_OK, serde_json::to_string(&r).unwrap_or_default()),
Err(e) => (EV_GENERIC_ERR, err_json(&e.to_string())),
};
call_cb(cb, ud, ev, &json);
});
}
#[no_mangle]
pub unsafe extern "C" fn qobuz_backend_get_fav_artists(ptr: *mut Backend, offset: u32, limit: u32) {
let inner = &(*ptr).0;
let client = inner.client.clone();
let cb = inner.cb; let ud = inner.ud;
spawn(inner, async move {
let result = client.lock().await.get_fav_artists(offset, limit).await;
let (ev, json) = match result {
Ok(r) => (EV_FAV_ARTISTS_OK, serde_json::to_string(&r).unwrap_or_default()),
Err(e) => (EV_GENERIC_ERR, err_json(&e.to_string())),
};
call_cb(cb, ud, ev, &json);
});
}
#[no_mangle]
pub unsafe extern "C" fn qobuz_backend_get_user_playlists(ptr: *mut Backend, offset: u32, limit: u32) {
let inner = &(*ptr).0;
let client = inner.client.clone();
let cb = inner.cb; let ud = inner.ud;
spawn(inner, async move {
let result = client.lock().await.get_user_playlists(offset, limit).await;
let (ev, json) = match result {
Ok(r) => {
let items = r.playlists.as_ref().and_then(|p| p.items.as_ref()).cloned().unwrap_or_default();
let total = r.playlists.as_ref().and_then(|p| p.total).unwrap_or(0);
(EV_PLAYLISTS_OK, serde_json::json!({"items": items, "total": total}).to_string())
}
Err(e) => (EV_GENERIC_ERR, err_json(&e.to_string())),
};
call_cb(cb, ud, ev, &json);
});
}
// ---------- Playback ----------
#[no_mangle]
pub unsafe extern "C" fn qobuz_backend_play_track(
ptr: *mut Backend,
track_id: i64,
format_id: i32,
) {
let inner = &(*ptr).0;
let client = inner.client.clone();
let cb = inner.cb; let ud = inner.ud;
let format = Format::from_id(format_id);
let cmd_tx = inner.player.cmd_tx.clone();
let status = inner.player.status.clone();
let prefetch = inner.prefetch.clone();
let rg_enabled = inner.replaygain_enabled.clone();
spawn(inner, async move {
// 1. Check prefetch cache first for zero-gap start
let cached = {
let mut lock = prefetch.lock().await;
if lock.as_ref().map(|p| p.track_id == track_id).unwrap_or(false) {
lock.take()
} else {
None
}
};
let (track, url) = if let Some(pf) = cached {
(pf.track, pf.url)
} else {
// Fetch track metadata
let track = match client.lock().await.get_track(track_id).await {
Ok(t) => t,
Err(e) => { call_cb(cb, ud, EV_TRACK_URL_ERR, &err_json(&e.to_string())); return; }
};
// Fetch stream URL
let url_dto = match client.lock().await.get_track_url(track_id, format).await {
Ok(u) => u,
Err(e) => { call_cb(cb, ud, EV_TRACK_URL_ERR, &err_json(&e.to_string())); return; }
};
let url = match url_dto.url {
Some(u) => u,
None => { call_cb(cb, ud, EV_TRACK_URL_ERR, &err_json("no stream URL")); return; }
};
(track, url)
};
// 2. Notify track change
if let Ok(j) = serde_json::to_string(&track) {
call_cb(cb, ud, EV_TRACK_CHANGED, &j);
}
// 3. Compute ReplayGain if enabled
let replaygain_db = if rg_enabled.load(std::sync::atomic::Ordering::Relaxed) {
track.audio_info.as_ref().and_then(|ai| ai.replaygain_track_gain)
} else {
None
};
// 4. Update status + send play command
*status.current_track.lock().unwrap() = Some(track.clone());
if let Some(dur) = track.duration {
status.duration_secs.store(dur as u64, std::sync::atomic::Ordering::Relaxed);
}
let _ = cmd_tx.send(player::PlayerCommand::Play(player::TrackInfo { track, url, replaygain_db }));
// 5. State notification
call_cb(cb, ud, EV_STATE_CHANGED, r#"{"state":"playing"}"#);
});
}
#[no_mangle]
pub unsafe extern "C" fn qobuz_backend_pause(ptr: *mut Backend) {
let inner = &(*ptr).0;
inner.player.pause();
call_cb(inner.cb, inner.ud, EV_STATE_CHANGED, r#"{"state":"paused"}"#);
}
#[no_mangle]
pub unsafe extern "C" fn qobuz_backend_resume(ptr: *mut Backend) {
let inner = &(*ptr).0;
inner.player.resume();
call_cb(inner.cb, inner.ud, EV_STATE_CHANGED, r#"{"state":"playing"}"#);
}
#[no_mangle]
pub unsafe extern "C" fn qobuz_backend_stop(ptr: *mut Backend) {
let inner = &(*ptr).0;
inner.player.stop();
call_cb(inner.cb, inner.ud, EV_STATE_CHANGED, r#"{"state":"idle"}"#);
}
#[no_mangle]
pub unsafe extern "C" fn qobuz_backend_set_volume(ptr: *mut Backend, volume: u8) {
(*ptr).0.player.set_volume(volume);
}
#[no_mangle]
pub unsafe extern "C" fn qobuz_backend_seek(ptr: *mut Backend, position_secs: u64) {
(*ptr).0.player.seek(position_secs);
}
#[no_mangle]
pub unsafe extern "C" fn qobuz_backend_get_position(ptr: *const Backend) -> u64 {
(*ptr).0.player.status.get_position()
}
#[no_mangle]
pub unsafe extern "C" fn qobuz_backend_get_duration(ptr: *const Backend) -> u64 {
(*ptr).0.player.status.get_duration()
}
#[no_mangle]
pub unsafe extern "C" fn qobuz_backend_get_volume(ptr: *const Backend) -> u8 {
(*ptr).0.player.status.get_volume()
}
#[no_mangle]
pub unsafe extern "C" fn qobuz_backend_get_state(ptr: *const Backend) -> c_int {
match (*ptr).0.player.status.get_state() {
PlayerState::Playing => 1,
PlayerState::Paused => 2,
_ => 0,
}
}
#[no_mangle]
pub unsafe extern "C" fn qobuz_backend_take_track_finished(ptr: *mut Backend) -> c_int {
let finished = (*ptr).0.player.status.track_finished
.swap(false, std::sync::atomic::Ordering::SeqCst);
if finished { 1 } else { 0 }
}
// ---------- ReplayGain / Gapless ----------
#[no_mangle]
pub unsafe extern "C" fn qobuz_backend_set_replaygain(ptr: *mut Backend, enabled: bool) {
(*ptr).0.replaygain_enabled.store(enabled, std::sync::atomic::Ordering::Relaxed);
}
#[no_mangle]
pub unsafe extern "C" fn qobuz_backend_set_gapless(ptr: *mut Backend, enabled: bool) {
(*ptr).0.player.status.gapless.store(enabled, std::sync::atomic::Ordering::Relaxed);
}
#[no_mangle]
pub unsafe extern "C" fn qobuz_backend_prefetch_track(
ptr: *mut Backend,
track_id: i64,
format_id: i32,
) {
let inner = &(*ptr).0;
let client = inner.client.clone();
let prefetch = inner.prefetch.clone();
let format = Format::from_id(format_id);
spawn(inner, async move {
let track = match client.lock().await.get_track(track_id).await {
Ok(t) => t,
Err(_) => return,
};
let url_dto = match client.lock().await.get_track_url(track_id, format).await {
Ok(u) => u,
Err(_) => return,
};
let url = match url_dto.url {
Some(u) => u,
None => return,
};
*prefetch.lock().await = Some(PrefetchedTrack { track_id, track, url });
});
}
// ---------- Favorites modification ----------
#[no_mangle]
pub unsafe extern "C" fn qobuz_backend_add_fav_track(ptr: *mut Backend, track_id: i64) {
let inner = &(*ptr).0;
let client = inner.client.clone();
let cb = inner.cb; let ud = inner.ud;
spawn(inner, async move {
if let Err(e) = client.lock().await.add_fav_track(track_id).await {
call_cb(cb, ud, EV_GENERIC_ERR, &err_json(&e.to_string()));
}
});
}
#[no_mangle]
pub unsafe extern "C" fn qobuz_backend_remove_fav_track(ptr: *mut Backend, track_id: i64) {
let inner = &(*ptr).0;
let client = inner.client.clone();
let cb = inner.cb; let ud = inner.ud;
spawn(inner, async move {
if let Err(e) = client.lock().await.remove_fav_track(track_id).await {
call_cb(cb, ud, EV_GENERIC_ERR, &err_json(&e.to_string()));
}
});
}
#[no_mangle]
pub unsafe extern "C" fn qobuz_backend_add_fav_album(ptr: *mut Backend, album_id: *const c_char) {
let inner = &(*ptr).0;
let album_id = CStr::from_ptr(album_id).to_string_lossy().into_owned();
let client = inner.client.clone();
let cb = inner.cb; let ud = inner.ud;
spawn(inner, async move {
if let Err(e) = client.lock().await.add_fav_album(&album_id).await {
call_cb(cb, ud, EV_GENERIC_ERR, &err_json(&e.to_string()));
}
});
}
#[no_mangle]
pub unsafe extern "C" fn qobuz_backend_remove_fav_album(ptr: *mut Backend, album_id: *const c_char) {
let inner = &(*ptr).0;
let album_id = CStr::from_ptr(album_id).to_string_lossy().into_owned();
let client = inner.client.clone();
let cb = inner.cb; let ud = inner.ud;
spawn(inner, async move {
if let Err(e) = client.lock().await.remove_fav_album(&album_id).await {
call_cb(cb, ud, EV_GENERIC_ERR, &err_json(&e.to_string()));
}
});
}
// ---------- User ----------
pub const EV_USER_OK: c_int = 23;
#[no_mangle]
pub unsafe extern "C" fn qobuz_backend_get_user(ptr: *mut Backend) {
let inner = &(*ptr).0;
let client = inner.client.clone();
let cb = inner.cb; let ud = inner.ud;
spawn(inner, async move {
let result = client.lock().await.get_user().await;
let (ev, json) = match result {
Ok(r) => (EV_USER_OK, serde_json::to_string(&r).unwrap_or_default()),
Err(e) => (EV_GENERIC_ERR, err_json(&e.to_string())),
};
call_cb(cb, ud, ev, &json);
});
}
// ---------- Visualizer PCM access ----------
/// Read up to `max_samples` f32 PCM values into `buf`.
/// Returns the number of samples actually read.
#[no_mangle]
pub unsafe extern "C" fn qobuz_backend_viz_read(
ptr: *mut Backend,
buf: *mut f32,
max_samples: u32,
) -> u32 {
let consumer = &(*ptr).0.player.status.viz_consumer;
let Ok(mut lock) = consumer.try_lock() else { return 0 };
let slice = std::slice::from_raw_parts_mut(buf, max_samples as usize);
rb::RbConsumer::read(&mut *lock, slice).unwrap_or(0) as u32
}
/// Returns current sample rate of the audio stream (0 if idle).
#[no_mangle]
pub unsafe extern "C" fn qobuz_backend_viz_sample_rate(ptr: *const Backend) -> u32 {
(*ptr).0.player.status.viz_sample_rate.load(std::sync::atomic::Ordering::Relaxed)
}
/// Returns current channel count (0 if idle).
#[no_mangle]
pub unsafe extern "C" fn qobuz_backend_viz_channels(ptr: *const Backend) -> u32 {
(*ptr).0.player.status.viz_channels.load(std::sync::atomic::Ordering::Relaxed)
}
// ---------- Playlist management ----------
pub const EV_PLAYLIST_CREATED: c_int = 20;
pub const EV_PLAYLIST_DELETED: c_int = 21;
pub const EV_PLAYLIST_TRACK_ADDED: c_int = 22;
#[no_mangle]
pub unsafe extern "C" fn qobuz_backend_create_playlist(ptr: *mut Backend, name: *const c_char) {
let inner = &(*ptr).0;
let name = CStr::from_ptr(name).to_string_lossy().into_owned();
let client = inner.client.clone();
let cb = inner.cb; let ud = inner.ud;
spawn(inner, async move {
match client.lock().await.create_playlist(&name).await {
Ok(p) => call_cb(cb, ud, EV_PLAYLIST_CREATED, &serde_json::to_string(&p).unwrap_or_default()),
Err(e) => call_cb(cb, ud, EV_GENERIC_ERR, &err_json(&e.to_string())),
}
});
}
#[no_mangle]
pub unsafe extern "C" fn qobuz_backend_delete_playlist(ptr: *mut Backend, playlist_id: i64) {
let inner = &(*ptr).0;
let client = inner.client.clone();
let cb = inner.cb; let ud = inner.ud;
spawn(inner, async move {
match client.lock().await.delete_playlist(playlist_id).await {
Ok(()) => call_cb(cb, ud, EV_PLAYLIST_DELETED,
&serde_json::json!({"playlist_id": playlist_id}).to_string()),
Err(e) => call_cb(cb, ud, EV_GENERIC_ERR, &err_json(&e.to_string())),
}
});
}
#[no_mangle]
pub unsafe extern "C" fn qobuz_backend_add_track_to_playlist(
ptr: *mut Backend,
playlist_id: i64,
track_id: i64,
) {
let inner = &(*ptr).0;
let client = inner.client.clone();
let cb = inner.cb; let ud = inner.ud;
spawn(inner, async move {
match client.lock().await.add_track_to_playlist(playlist_id, track_id).await {
Ok(()) => call_cb(cb, ud, EV_PLAYLIST_TRACK_ADDED,
&serde_json::json!({"playlist_id": playlist_id}).to_string()),
Err(e) => call_cb(cb, ud, EV_GENERIC_ERR, &err_json(&e.to_string())),
}
});
}
#[no_mangle]
pub unsafe extern "C" fn qobuz_backend_delete_track_from_playlist(
ptr: *mut Backend,
playlist_id: i64,
playlist_track_id: i64,
) {
let inner = &(*ptr).0;
let client = inner.client.clone();
let cb = inner.cb; let ud = inner.ud;
spawn(inner, async move {
if let Err(e) = client.lock().await.delete_track_from_playlist(playlist_id, playlist_track_id).await {
call_cb(cb, ud, EV_GENERIC_ERR, &err_json(&e.to_string()));
}
});
}