diff --git a/crates/qbz-i18n/translations/de/LC_MESSAGES/qbz-ui.po b/crates/qbz-i18n/translations/de/LC_MESSAGES/qbz-ui.po index c36467991..544020861 100644 --- a/crates/qbz-i18n/translations/de/LC_MESSAGES/qbz-ui.po +++ b/crates/qbz-i18n/translations/de/LC_MESSAGES/qbz-ui.po @@ -5375,3 +5375,24 @@ msgstr "Duplikate: {}" msgid "Media" msgstr "Medium" + +msgid "Open audio CD" +msgstr "Audio-CD öffnen" + +msgid "Audio CD" +msgstr "Audio-CD" + +msgid "No optical drive found." +msgstr "Kein optisches Laufwerk gefunden." + +msgid "No disc in the drive." +msgstr "Keine Disc im Laufwerk." + +msgid "That disc has no audio tracks." +msgstr "Diese Disc hat keine Audiotitel." + +msgid "Could not read the disc." +msgstr "Die Disc konnte nicht gelesen werden." + +msgid "Track {}" +msgstr "Titel {}" diff --git a/crates/qbz-i18n/translations/en/LC_MESSAGES/qbz-ui.po b/crates/qbz-i18n/translations/en/LC_MESSAGES/qbz-ui.po index d016d67be..60b47c0e4 100644 --- a/crates/qbz-i18n/translations/en/LC_MESSAGES/qbz-ui.po +++ b/crates/qbz-i18n/translations/en/LC_MESSAGES/qbz-ui.po @@ -920,3 +920,24 @@ msgstr "Default" msgid "Media" msgstr "Media" + +msgid "Open audio CD" +msgstr "Open audio CD" + +msgid "Audio CD" +msgstr "Audio CD" + +msgid "No optical drive found." +msgstr "No optical drive found." + +msgid "No disc in the drive." +msgstr "No disc in the drive." + +msgid "That disc has no audio tracks." +msgstr "That disc has no audio tracks." + +msgid "Could not read the disc." +msgstr "Could not read the disc." + +msgid "Track {}" +msgstr "Track {}" diff --git a/crates/qbz-i18n/translations/es/LC_MESSAGES/qbz-ui.po b/crates/qbz-i18n/translations/es/LC_MESSAGES/qbz-ui.po index c1a9f76da..3caf6befd 100644 --- a/crates/qbz-i18n/translations/es/LC_MESSAGES/qbz-ui.po +++ b/crates/qbz-i18n/translations/es/LC_MESSAGES/qbz-ui.po @@ -5381,3 +5381,24 @@ msgstr "Duplicados: {}" msgid "Media" msgstr "Medio" + +msgid "Open audio CD" +msgstr "Abrir CD de audio" + +msgid "Audio CD" +msgstr "CD de audio" + +msgid "No optical drive found." +msgstr "No se encontró ninguna unidad óptica." + +msgid "No disc in the drive." +msgstr "No hay disco en la unidad." + +msgid "That disc has no audio tracks." +msgstr "Ese disco no tiene pistas de audio." + +msgid "Could not read the disc." +msgstr "No se pudo leer el disco." + +msgid "Track {}" +msgstr "Pista {}" diff --git a/crates/qbz-i18n/translations/fr/LC_MESSAGES/qbz-ui.po b/crates/qbz-i18n/translations/fr/LC_MESSAGES/qbz-ui.po index 56e2bea33..2e524c4dd 100644 --- a/crates/qbz-i18n/translations/fr/LC_MESSAGES/qbz-ui.po +++ b/crates/qbz-i18n/translations/fr/LC_MESSAGES/qbz-ui.po @@ -5373,3 +5373,24 @@ msgstr "Doublons : {}" msgid "Media" msgstr "Média" + +msgid "Open audio CD" +msgstr "Ouvrir le CD audio" + +msgid "Audio CD" +msgstr "CD audio" + +msgid "No optical drive found." +msgstr "Aucun lecteur optique trouvé." + +msgid "No disc in the drive." +msgstr "Aucun disque dans le lecteur." + +msgid "That disc has no audio tracks." +msgstr "Ce disque n'a aucune piste audio." + +msgid "Could not read the disc." +msgstr "Impossible de lire le disque." + +msgid "Track {}" +msgstr "Piste {}" diff --git a/crates/qbz-i18n/translations/ja/LC_MESSAGES/qbz-ui.po b/crates/qbz-i18n/translations/ja/LC_MESSAGES/qbz-ui.po index 5b01d2a8f..0888ba5b3 100644 --- a/crates/qbz-i18n/translations/ja/LC_MESSAGES/qbz-ui.po +++ b/crates/qbz-i18n/translations/ja/LC_MESSAGES/qbz-ui.po @@ -5358,3 +5358,24 @@ msgstr "重複: {}" msgid "Media" msgstr "メディア" + +msgid "Open audio CD" +msgstr "オーディオCDを開く" + +msgid "Audio CD" +msgstr "オーディオCD" + +msgid "No optical drive found." +msgstr "光学ドライブが見つかりません。" + +msgid "No disc in the drive." +msgstr "ドライブにディスクがありません。" + +msgid "That disc has no audio tracks." +msgstr "このディスクにはオーディオトラックがありません。" + +msgid "Could not read the disc." +msgstr "ディスクを読み取れませんでした。" + +msgid "Track {}" +msgstr "トラック {}" diff --git a/crates/qbz-i18n/translations/nl/LC_MESSAGES/qbz-ui.po b/crates/qbz-i18n/translations/nl/LC_MESSAGES/qbz-ui.po index b030236f3..ed373bcb0 100644 --- a/crates/qbz-i18n/translations/nl/LC_MESSAGES/qbz-ui.po +++ b/crates/qbz-i18n/translations/nl/LC_MESSAGES/qbz-ui.po @@ -5363,3 +5363,24 @@ msgstr "Duplicaten: {}" msgid "Media" msgstr "Medium" + +msgid "Open audio CD" +msgstr "Audio-cd openen" + +msgid "Audio CD" +msgstr "Audio-cd" + +msgid "No optical drive found." +msgstr "Geen optisch station gevonden." + +msgid "No disc in the drive." +msgstr "Geen schijf in het station." + +msgid "That disc has no audio tracks." +msgstr "Die schijf heeft geen audiotracks." + +msgid "Could not read the disc." +msgstr "Kon de schijf niet lezen." + +msgid "Track {}" +msgstr "Track {}" diff --git a/crates/qbz-i18n/translations/pt/LC_MESSAGES/qbz-ui.po b/crates/qbz-i18n/translations/pt/LC_MESSAGES/qbz-ui.po index 1518e4173..c5ad61474 100644 --- a/crates/qbz-i18n/translations/pt/LC_MESSAGES/qbz-ui.po +++ b/crates/qbz-i18n/translations/pt/LC_MESSAGES/qbz-ui.po @@ -5373,3 +5373,24 @@ msgstr "Duplicados: {}" msgid "Media" msgstr "Mídia" + +msgid "Open audio CD" +msgstr "Abrir CD de áudio" + +msgid "Audio CD" +msgstr "CD de áudio" + +msgid "No optical drive found." +msgstr "Nenhuma unidade óptica encontrada." + +msgid "No disc in the drive." +msgstr "Nenhum disco na unidade." + +msgid "That disc has no audio tracks." +msgstr "Esse disco não tem faixas de áudio." + +msgid "Could not read the disc." +msgstr "Não foi possível ler o disco." + +msgid "Track {}" +msgstr "Faixa {}" diff --git a/crates/qbz-i18n/translations/ru/LC_MESSAGES/qbz-ui.po b/crates/qbz-i18n/translations/ru/LC_MESSAGES/qbz-ui.po index d59c0f131..229c0b3e3 100644 --- a/crates/qbz-i18n/translations/ru/LC_MESSAGES/qbz-ui.po +++ b/crates/qbz-i18n/translations/ru/LC_MESSAGES/qbz-ui.po @@ -5392,3 +5392,24 @@ msgstr "Дубликаты: {}" msgid "Media" msgstr "Носитель" + +msgid "Open audio CD" +msgstr "Открыть аудио-CD" + +msgid "Audio CD" +msgstr "Аудио-CD" + +msgid "No optical drive found." +msgstr "Оптический привод не найден." + +msgid "No disc in the drive." +msgstr "В приводе нет диска." + +msgid "That disc has no audio tracks." +msgstr "На этом диске нет аудиодорожек." + +msgid "Could not read the disc." +msgstr "Не удалось прочитать диск." + +msgid "Track {}" +msgstr "Дорожка {}" diff --git a/crates/qbz-qt/Cargo.toml b/crates/qbz-qt/Cargo.toml index 5c4438f73..ff23ccc86 100644 --- a/crates/qbz-qt/Cargo.toml +++ b/crates/qbz-qt/Cargo.toml @@ -31,6 +31,7 @@ log = { workspace = true } # one of the wedges the watchdog exists to cut short. Already in the lock as # a transitive dep of half the tree; this only makes it direct. libc = "0.2" +qbz-disc = { path = "../qbz-disc" } rand = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } diff --git a/crates/qbz-qt/qml/views/local/LocalChrome.qml b/crates/qbz-qt/qml/views/local/LocalChrome.qml index 7b0125445..bede1c212 100644 --- a/crates/qbz-qt/qml/views/local/LocalChrome.qml +++ b/crates/qbz-qt/qml/views/local/LocalChrome.qml @@ -195,10 +195,13 @@ Item { menuWidth: 220 entries: [ { "label": QbzSession.tr("Open folder…", QbzSession.trRev), - "icon": "folder-open", "action": "folder" } + "icon": "folder-open", "action": "folder" }, + { "label": QbzSession.tr("Open audio CD", QbzSession.trRev), + "icon": "disc", "action": "cd" } ] onPicked: function (action) { if (action === "folder") QbzLocal.ephemeralOpen() + else if (action === "cd") QbzLocal.ephemeralOpenCd() } } diff --git a/crates/qbz-qt/src/audible_qt.rs b/crates/qbz-qt/src/audible_qt.rs index 10947db05..c3a71806b 100644 --- a/crates/qbz-qt/src/audible_qt.rs +++ b/crates/qbz-qt/src/audible_qt.rs @@ -120,6 +120,13 @@ pub(crate) async fn play_ticket(runtime: &Runtime, ticket: PlaybackTicket) -> bo seek_secs, } => play_file(runtime, path, play_id, seek_secs).await, + PlaybackTicket::CdTrack { + device, + start_lsn, + sectors, + play_id, + } => play_cd_track(runtime, device, start_lsn, sectors, play_id), + PlaybackTicket::Bytes { bytes, play_id } => { let len = bytes.len(); if let Err(e) = runtime.core().player().play_data(bytes, play_id) { @@ -365,3 +372,166 @@ mod tests { assert_eq!(slot.1, PathBuf::from("/m/album.flac")); } } + +/// The play_id of the CD feeder that should be running. A newer disc play +/// stores its own id here and the older thread retires itself on its next +/// block — a signal the audio thread plays no part in, which is the whole +/// point (see the reasoning inside `play_cd_track`). +static CD_FEEDER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); + +/// Feed one CD-DA track into the player as a finite WAV. +/// +/// The SHAPE is the shipping DSD-to-PCM path (`player/mod.rs:5224`): declare a +/// content length, hand back a `BufferWriter`, push a header and then blocks +/// from a thread. That is deliberate — it is the one locally-produced stream +/// this app already has, and it works. +/// +/// Three things differ, and each is a decision rather than a detail: +/// +/// 1. **A 16-bit header.** `qbz_dsd::wav_header` is hardcoded to 24-bit, the +/// depth its converter emits. Reusing it would either lie in the header or +/// promote 16-bit samples to 24 — and while that shift is arithmetically +/// lossless, it makes the DAC open at 24 bits for a 16-bit source. That is +/// the silent conversion a bit-perfect path must not contain. +/// +/// 2. **The feeder stops when the track changes.** The DSD thread used to +/// rely on `push_chunk` returning `Err`, which it cannot do; this one polls +/// `current_track_id()`, the same signal `play_file` above reads, and stops +/// the moment the player is on something else. Without it, skipping a track +/// would leave the drive spinning and a 160 MB buffer growing for nothing. +/// +/// 3. **A read error ends the track.** No zero-fill, no skip-ahead. A hole +/// that sounds like silence is indistinguishable from music that was meant +/// to be quiet, and on a bit-perfect path that is worse than stopping. +fn play_cd_track( + runtime: &Runtime, + device: std::path::PathBuf, + start_lsn: u32, + sectors: u32, + play_id: u64, +) -> bool { + let track = qbz_disc::cdda::TocTrack { + number: 0, + start_lsn, + sectors, + is_audio: true, + }; + let mut reader = match qbz_disc::cdda::TrackReader::open(&device, &track) { + Ok(r) => r, + Err(e) => { + log::error!("[qbz-qt] audible: cannot read {} : {e}", device.display()); + crate::toast_qt::error(qbz_i18n::t("Could not read the disc.")); + return false; + } + }; + + let frames = track.frames(); + let content_length = qbz_disc::wav_total_size_16(frames, qbz_disc::CDDA_CHANNELS); + let duration_secs = track.duration_secs(); + log::info!( + "[qbz-qt] audible: CD track {play_id} — {} sectors, {}s, {} bytes WAV \ + ({} Hz / {}-bit / {}ch)", + sectors, + duration_secs, + content_length, + qbz_disc::CDDA_SAMPLE_RATE, + qbz_disc::CDDA_BITS, + qbz_disc::CDDA_CHANNELS + ); + + let player = runtime.core().player(); + // `play_streaming` does not publish the bit depth (only its dynamic + // sibling does), so a CD would inherit whatever the previous track left + // behind. Say it explicitly. + player + .state + .set_stream_quality(qbz_disc::CDDA_SAMPLE_RATE, qbz_disc::CDDA_BITS as u32); + + let writer = match player.play_streaming( + play_id, + qbz_disc::CDDA_SAMPLE_RATE, + qbz_disc::CDDA_CHANNELS, + content_length, + 3, + duration_secs, + 0, + ) { + Ok(w) => w, + Err(e) => { + log::error!("[qbz-qt] audible: play_streaming for CD {play_id} failed: {e}"); + return false; + } + }; + + // Claim the feeder slot BEFORE spawning: whoever claims last wins, and the + // previous feeder sees the change on its next block. + CD_FEEDER.store(play_id, std::sync::atomic::Ordering::SeqCst); + + let state = player.state.clone(); + std::thread::spawn(move || { + if writer + .push_chunk(&qbz_disc::wav_header_16( + frames, + qbz_disc::CDDA_CHANNELS, + qbz_disc::CDDA_SAMPLE_RATE, + )) + .is_err() + { + return; + } + let mut buf = Vec::new(); + // Liveness, third attempt — the first two were both wrong and in + // opposite directions, so the reasoning is written down. + // + // 1. `current_track_id() != play_id` alone: WRONG. `play_streaming` + // posts to the audio thread and returns, so the id is not ours yet + // and the feeder decides it was superseded before reading a sector. + // Nothing played. + // 2. Adding a wall-clock grace: WORSE, and a deadlock. That field is + // stored at `player/mod.rs:2814`, which the audio thread only + // reaches once the initial buffer is READY — and the buffer is + // what this thread is filling. Waiting on it means waiting on + // ourselves, so the grace expired and the track stopped. + // + // The signal has to be one the audio thread is not part of. + // `CD_FEEDER` is bumped by the play that spawned us, so a NEWER CD + // play retires this one immediately — which is the supersede that + // actually happens (skip to the next track of the same disc). + // + // For "a non-CD track superseded us", the acknowledgement path still + // applies, but only ONCE the player has confirmed our id: after that + // it changing means something else really is playing. There is no + // timeout, because a timeout is what can kill a healthy feeder. + let mut acknowledged = false; + loop { + if CD_FEEDER.load(std::sync::atomic::Ordering::SeqCst) != play_id { + log::info!("[qbz-qt] CD feeder {play_id} retired by a newer disc play"); + return; + } + let current = state.current_track_id(); + if current == play_id { + acknowledged = true; + } else if acknowledged { + log::info!("[qbz-qt] CD feeder for superseded track {play_id} stopping"); + return; + } + match reader.next_chunk(&mut buf) { + Ok(0) => { + let _ = writer.complete(); + return; + } + Ok(_) => { + if writer.push_chunk(&buf).is_err() { + return; + } + } + Err(e) => { + log::error!("[qbz-qt] CD read failed mid-track: {e}"); + let _ = writer.error(format!("CD read failed: {e}")); + return; + } + } + } + }); + true +} diff --git a/crates/qbz-qt/src/cdda_qt.rs b/crates/qbz-qt/src/cdda_qt.rs new file mode 100644 index 000000000..ac32b0706 --- /dev/null +++ b/crates/qbz-qt/src/cdda_qt.rs @@ -0,0 +1,151 @@ +//! Open the audio CD in the drive as an ephemeral session. +//! +//! A disc is exactly what the ephemeral model is for: content from outside the +//! index, playable, and gone when you take it out. Nothing here touches +//! `library.db`. +//! +//! What it does NOT do is invent metadata. A CD-DA carries no titles — the +//! disc in the owner's drive reports its MCN as all zeros and only its ISRCs +//! are populated — so tracks are named by their number and the album is called +//! "Audio CD". A DiscID lookup is the obvious next step and is deliberately +//! not faked here: a wrong title is worse than an honest number. + +use qbz_library::{AudioFormat, LocalTrack}; + +use crate::local_ephemeral; + +/// Human label for a disc we know nothing about beyond its shape. +fn disc_label() -> String { + qbz_i18n::t("Audio CD") +} + +/// Map one TOC entry to the `LocalTrack` the rest of the app already knows how +/// to carry. +/// +/// `file_path` is a `cdda:` reference, NOT a path: it is what +/// `LocalSource::playback` parses back into a device and a sector range. That +/// is why nothing downstream may stat it — see `qbz_disc::CdRef`. +fn to_local_track(track: &qbz_disc::cdda::TocTrack, album: &str) -> LocalTrack { + let reference = qbz_disc::CdRef { + device: std::path::PathBuf::new(), // filled by the caller + start_lsn: track.start_lsn, + sectors: track.sectors, + }; + let _ = reference; + LocalTrack { + // `t_args`, not `tf`: this is one track, not a count. A plural form + // here would put a bogus singular/plural pair in eight catalogues. + title: qbz_i18n::t_args("Track {}", &[&track.number.to_string()]), + album: album.to_string(), + album_group_title: album.to_string(), + album_group_key: format!("cdda|||{album}"), + track_number: Some(track.number as u32), + disc_number: Some(1), + duration_secs: track.duration_secs(), + // A CD is 44.1 kHz / 16-bit / stereo by definition. These are not + // guesses and not defaults — the format has no other shape. + sample_rate: qbz_disc::CDDA_SAMPLE_RATE as f64, + bit_depth: Some(qbz_disc::CDDA_BITS as u32), + format: AudioFormat::Wav, + ..Default::default() + } +} + +/// Read the disc in the first drive that has one and publish it as the +/// ephemeral session. Returns the number of audio tracks, or an error string +/// already translated for a toast. +pub fn open_disc() -> Result { + let devices = qbz_disc::list_devices(); + if devices.is_empty() { + return Err(qbz_i18n::t("No optical drive found.")); + } + + // Several drives are legal; take the first one that actually has an audio + // disc rather than assuming /dev/sr0 is the interesting one. + let mut last_err = None; + for dev in &devices { + match qbz_disc::read_toc(dev) { + Ok(toc) => { + let album = disc_label(); + let audio: Vec<_> = toc.audio_tracks().collect(); + let skipped = toc.tracks.len() - audio.len(); + if skipped > 0 { + // Mixed-mode disc. Say so rather than quietly showing + // fewer tracks than the case insert lists. + log::info!( + "[qbz-qt] cd: {skipped} data track(s) skipped on {}", + dev.display() + ); + } + let tracks: Vec = audio + .iter() + .map(|t| { + let mut lt = to_local_track(t, &album); + lt.file_path = qbz_disc::CdRef { + device: dev.clone(), + start_lsn: t.start_lsn, + sectors: t.sectors, + } + .to_path_string(); + lt + }) + .collect(); + let count = tracks.len(); + log::info!( + "[qbz-qt] cd: {} — {count} audio tracks, fingerprint {}", + dev.display(), + toc.fingerprint() + ); + local_ephemeral::adopt_tracks(&album, tracks); + return Ok(count); + } + Err(e) => { + log::info!("[qbz-qt] cd: {} unusable: {e}", dev.display()); + last_err = Some(e); + } + } + } + Err(match last_err { + Some(qbz_disc::CdError::NoDisc) => qbz_i18n::t("No disc in the drive."), + Some(qbz_disc::CdError::NotAudio) => qbz_i18n::t("That disc has no audio tracks."), + Some(e) => format!("{e}"), + None => qbz_i18n::t("No optical drive found."), + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_cd_track_carries_the_formats_only_possible_shape() { + let t = qbz_disc::cdda::TocTrack { + number: 7, + start_lsn: 285_735, + sectors: 70_833, + is_audio: true, + }; + let lt = to_local_track(&t, "Audio CD"); + assert_eq!(lt.sample_rate, 44_100.0); + assert_eq!(lt.bit_depth, Some(16)); + assert_eq!(lt.track_number, Some(7)); + // 15:44, the real length of the owner's longest track. + assert_eq!(lt.duration_secs, 944); + } + + #[test] + fn the_reference_survives_the_round_trip_a_playback_will_make() { + let r = qbz_disc::CdRef { + device: std::path::PathBuf::from("/dev/sr0"), + start_lsn: 46_577, + sectors: 53_470, + }; + let s = r.to_path_string(); + // This is the exact test `LocalSource::playback` performs. + assert!(qbz_disc::CdRef::is_cd_path(&s)); + let back = qbz_disc::CdRef::parse(&s).expect("a reference we just wrote must parse"); + assert_eq!(back.device, r.device); + assert_eq!(back.start_lsn, 46_577); + assert_eq!(back.sectors, 53_470); + } +} diff --git a/crates/qbz-qt/src/local_bridge.rs b/crates/qbz-qt/src/local_bridge.rs index 5387d4af2..135c76336 100644 --- a/crates/qbz-qt/src/local_bridge.rs +++ b/crates/qbz-qt/src/local_bridge.rs @@ -295,6 +295,11 @@ pub mod qbz_local { /// Close the session (stops playback if it came from the session). #[qinvokable] fn ephemeral_clear(self: Pin<&mut QbzLocal>); + /// `Open > Open CD…`: read the audio disc in the drive and make it the + /// ephemeral session. Toasts on every failure it can name (no drive, + /// no disc, a data-only disc) rather than opening an empty pane. + #[qinvokable] + fn ephemeral_open_cd(self: Pin<&mut QbzLocal>); /// Header Play / Shuffle: the whole session becomes the queue. Same /// `(id, shuffle)` shape as `play_album`, so the two headers behave /// identically. @@ -1085,6 +1090,19 @@ impl qbz_local::QbzLocal { crate::local_ephemeral::clear(); } + pub fn ephemeral_open_cd(self: Pin<&mut Self>) { + // Reading a TOC spins the drive up and can take a second or two, so it + // does not run on the UI thread. + crate::spawn(async move { + let outcome = tokio::task::spawn_blocking(crate::cdda_qt::open_disc).await; + match outcome { + Ok(Ok(n)) => log::info!("[qbz-qt] cd opened: {n} tracks"), + Ok(Err(msg)) => crate::toast_qt::error(msg), + Err(e) => log::warn!("[qbz-qt] cd open task failed: {e}"), + } + }); + } + pub fn ephemeral_play_all(self: Pin<&mut Self>, shuffle: bool) { let runtime = crate::app(); crate::spawn(async move { diff --git a/crates/qbz-qt/src/local_ephemeral.rs b/crates/qbz-qt/src/local_ephemeral.rs index 75cb6cc5e..d042b103d 100644 --- a/crates/qbz-qt/src/local_ephemeral.rs +++ b/crates/qbz-qt/src/local_ephemeral.rs @@ -327,6 +327,44 @@ pub fn open_path(path: String) { }); } +/// Publish a session built from a track list rather than a directory scan — +/// a CD today, a disc image next. +/// +/// It goes through the SAME `publish_doc` as a folder, so the pane, the tab, +/// the label and the open sequence all behave identically: a disc is not a +/// second kind of session, it is the same session with a different source of +/// tracks. +/// +/// The path is NOT persisted. A folder is still there tomorrow; a disc is a +/// piece of plastic that gets taken out, and re-opening a drive at boot to +/// find a different album (or none) is worse than starting clean. Restoring a +/// medium needs the TOC fingerprint check the contract specifies, and that is +/// not built yet — so this deliberately does nothing rather than half of it. +pub fn adopt_tracks(label: &str, tracks: Vec) { + let label = label.to_string(); + crate::spawn(async move { + wipe_if_playing(&crate::app()).await; + let seq = OPEN_SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1; + ui(move |mut b| { + b.as_mut().set_local_ephemeral_open_seq(seq); + }); + match STATE.open_tracks(&label, tracks) { + Ok(res) => { + log::info!( + "[qbz-qt] ephemeral adopted {} ({} tracks)", + label, + res.tracks.len() + ); + publish_doc(&build_doc(&label, &label, &res.tracks), false); + } + Err(e) => { + log::warn!("[qbz-qt] ephemeral adopt failed: {e}"); + publish_closed(); + } + } + }); +} + /// Boot: re-open the persisted folder, if any. Silent — a folder that moved /// away just clears the stale pref. pub fn rehydrate() { diff --git a/crates/qbz-qt/src/main.rs b/crates/qbz-qt/src/main.rs index a533d3a61..7ab5474cc 100644 --- a/crates/qbz-qt/src/main.rs +++ b/crates/qbz-qt/src/main.rs @@ -239,6 +239,7 @@ mod share_qt; mod nav_qt; mod browse_qt; mod cast_qt; +mod cdda_qt; mod label_qt; mod now_playing; mod output_labels;