diff --git a/Cargo.toml b/Cargo.toml index e687e65..342172c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ edition = "2018" [dependencies] anyhow = "1.0.33" +async-trait = "0.1.42" diesel = { version = "1.4.5", features = ["sqlite"] } diesel_migrations = "1.4.0" discid = "0.4.4" diff --git a/src/import/disc_source.rs b/src/import/disc_source.rs index f1a33c3..1c3f479 100644 --- a/src/import/disc_source.rs +++ b/src/import/disc_source.rs @@ -1,8 +1,11 @@ +use super::source::{Source, SourceTrack}; use anyhow::{anyhow, bail, Result}; +use async_trait::async_trait; use discid::DiscId; use futures_channel::oneshot; use gstreamer::prelude::*; use gstreamer::{Element, ElementFactory, Pipeline}; +use once_cell::sync::OnceCell; use std::path::{Path, PathBuf}; use std::thread; @@ -10,67 +13,27 @@ use std::thread; #[derive(Clone, Debug)] pub struct DiscSource { /// The MusicBrainz DiscID of the CD. - pub discid: String, - - /// The path to the temporary directory where the audio files will be. - pub path: PathBuf, + pub discid: OnceCell, /// The tracks on this disc. - pub tracks: Vec, -} - -/// Representation of a single track on an audio CD. -#[derive(Clone, Debug)] -pub struct TrackSource { - /// The track number. This is different from the index in the disc - /// source's tracks list, because it is not defined from which number the - /// the track numbers start. - pub number: u32, - - /// The path to the temporary file to which the track will be ripped. The - /// file will not exist until the track is actually ripped. - pub path: PathBuf, + tracks: OnceCell>, } impl DiscSource { - /// Try to create a new disc source by asynchronously reading the - /// information from the default disc drive. - pub async fn load() -> Result { - let (sender, receiver) = oneshot::channel(); + /// Create a new disc source. The source has to be initialized by calling + /// load() afterwards. + pub fn new() -> Result { + let result = Self { + discid: OnceCell::new(), + tracks: OnceCell::new(), + }; - thread::spawn(|| { - let disc = Self::load_priv(); - sender.send(disc).unwrap(); - }); - - let disc = receiver.await??; - - Ok(disc) + Ok(result) } - /// Rip the whole disc asynchronously. After this method has finished - /// successfully, the audio files will be available in the specified - /// location for each track source. - pub async fn rip(&self) -> Result<()> { - for track in &self.tracks { - let (sender, receiver) = oneshot::channel(); - - let number = track.number; - let path = track.path.clone(); - - thread::spawn(move || { - let result = Self::rip_track(&path, number); - sender.send(result).unwrap(); - }); - - receiver.await??; - } - - Ok(()) - } - - /// Load the disc from the default disc drive. - fn load_priv() -> Result { + /// Load the disc from the default disc drive and return the MusicBrainz + /// DiscID as well as descriptions of the contained tracks. + fn load_priv() -> Result<(String, Vec)> { let discid = DiscId::read(None)?; let id = discid.id(); @@ -87,7 +50,7 @@ impl DiscSource { let mut path = tmp_dir.clone(); path.push(file_name); - let track = TrackSource { + let track = SourceTrack { number, path, }; @@ -95,13 +58,7 @@ impl DiscSource { tracks.push(track); } - let disc = DiscSource { - discid: id, - tracks, - path: tmp_dir, - }; - - Ok(disc) + Ok((id, tracks)) } /// Create a new temporary directory and return its path. @@ -172,3 +129,57 @@ impl DiscSource { Ok(pipeline) } } + +#[async_trait] +impl Source for DiscSource { + async fn load(&self) -> Result<()> { + let (sender, receiver) = oneshot::channel(); + + thread::spawn(|| { + let result = Self::load_priv(); + sender.send(result).unwrap(); + }); + + let (discid, tracks) = receiver.await??; + + self.discid.set(discid); + self.tracks.set(tracks); + + Ok(()) + } + + fn tracks(&self) -> Option<&[SourceTrack]> { + match self.tracks.get() { + Some(tracks) => Some(tracks.as_slice()), + None => None, + } + } + + fn discid(&self) -> Option { + match self.discid.get() { + Some(discid) => Some(discid.to_owned()), + None => None, + } + } + + async fn copy(&self) -> Result<()> { + let tracks = self.tracks.get() + .ok_or_else(|| anyhow!("Tried to copy disc before loading has finished!"))?; + + for track in tracks { + let (sender, receiver) = oneshot::channel(); + + let number = track.number; + let path = track.path.clone(); + + thread::spawn(move || { + let result = Self::rip_track(&path, number); + sender.send(result).unwrap(); + }); + + receiver.await??; + } + + Ok(()) + } +} diff --git a/src/import/medium_editor.rs b/src/import/medium_editor.rs index 361ec04..b223579 100644 --- a/src/import/medium_editor.rs +++ b/src/import/medium_editor.rs @@ -1,9 +1,9 @@ -use super::disc_source::DiscSource; +use super::source::Source; use super::track_set_editor::{TrackSetData, TrackSetEditor}; use crate::database::{generate_id, Medium, Track, TrackSet}; use crate::backend::Backend; use crate::widgets::{List, Navigator, NavigatorScreen}; -use anyhow::Result; +use anyhow::{anyhow, Result}; use glib::clone; use glib::prelude::*; use gtk::prelude::*; @@ -15,7 +15,7 @@ use std::rc::Rc; /// A dialog for editing metadata while importing music into the music library. pub struct MediumEditor { backend: Rc, - source: Rc, + source: Rc>, widget: gtk::Stack, done_button: gtk::Button, done_stack: gtk::Stack, @@ -29,7 +29,7 @@ pub struct MediumEditor { impl MediumEditor { /// Create a new medium editor. - pub fn new(backend: Rc, source: DiscSource) -> Rc { + pub fn new(backend: Rc, source: Rc>) -> Rc { // Create UI let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/medium_editor.ui"); @@ -49,7 +49,7 @@ impl MediumEditor { let this = Rc::new(Self { backend, - source: Rc::new(source), + source, widget, done_button, done_stack, @@ -131,14 +131,14 @@ impl MediumEditor { row.upcast() })); - // Start ripping the CD in the background. + // Copy the source in the background. let context = glib::MainContext::default(); let clone = this.clone(); context.spawn_local(async move { - match clone.source.rip().await { + match clone.source.copy().await { Err(error) => { // TODO: Present error. - println!("Failed to rip: {}", error); + println!("Failed to copy source: {}", error); }, Ok(_) => { clone.done_stack.set_visible_child(&clone.done); @@ -163,6 +163,7 @@ impl MediumEditor { // Convert the track set data to real track sets. let mut track_sets = Vec::new(); + let source_tracks = self.source.tracks().ok_or_else(|| anyhow!("Tracks not loaded!"))?; for track_set_data in &*self.track_sets.borrow() { let mut tracks = Vec::new(); @@ -170,7 +171,7 @@ impl MediumEditor { for track_data in &track_set_data.tracks { // Copy the corresponding audio file to the music library. - let track_source = &self.source.tracks[track_data.track_source]; + let track_source = &source_tracks[track_data.track_source]; let file_name = format!("track_{:02}.flac", track_source.number); let mut track_path = path.clone(); @@ -199,7 +200,7 @@ impl MediumEditor { let medium = Medium { id: generate_id(), name: self.name_entry.get_text().unwrap().to_string(), - discid: Some(self.source.discid.clone()), + discid: self.source.discid(), tracks: track_sets, }; diff --git a/src/import/mod.rs b/src/import/mod.rs index 2744611..a28f6d1 100644 --- a/src/import/mod.rs +++ b/src/import/mod.rs @@ -1,5 +1,6 @@ mod disc_source; mod medium_editor; +mod source; mod source_selector; mod track_editor; mod track_selector; diff --git a/src/import/source.rs b/src/import/source.rs new file mode 100644 index 0000000..7020bae --- /dev/null +++ b/src/import/source.rs @@ -0,0 +1,35 @@ +use anyhow::Result; +use async_trait::async_trait; +use std::path::PathBuf; + +/// A source for tracks that can be imported into the music library. +#[async_trait] +pub trait Source { + /// Load the source and discover the contained tracks. + async fn load(&self) -> Result<()>; + + /// Get a reference to the tracks within this source, if they are ready. + fn tracks(&self) -> Option<&[SourceTrack]>; + + /// Get the DiscID of the corresponging medium, if possible. + fn discid(&self) -> Option; + + /// Asynchronously copy the tracks to the files that are advertised within + /// their corresponding objects. + async fn copy(&self) -> Result<()>; +} + +/// Representation of a single track on a source. +#[derive(Clone, Debug)] +pub struct SourceTrack { + /// The track number. This is different from the index in the disc + /// source's tracks list, because it is not defined from which number the + /// the track numbers start. + pub number: u32, + + /// The path to the file where the corresponding audio file is. This file + /// is only required to exist, once the source's copy method has finished. + /// This will not be the actual file within the user's music library, but + /// the location from which it can be copied to the music library. + pub path: PathBuf, +} diff --git a/src/import/source_selector.rs b/src/import/source_selector.rs index 836255e..ac4974c 100644 --- a/src/import/source_selector.rs +++ b/src/import/source_selector.rs @@ -1,5 +1,6 @@ use super::medium_editor::MediumEditor; use super::disc_source::DiscSource; +use super::source::Source; use crate::backend::Backend; use crate::widgets::{Navigator, NavigatorScreen}; use glib::clone; @@ -54,11 +55,13 @@ impl SourceSelector { let context = glib::MainContext::default(); let clone = this.clone(); context.spawn_local(async move { - match DiscSource::load().await { - Ok(disc) => { + let disc = DiscSource::new().unwrap(); + match disc.load().await { + Ok(_) => { let navigator = clone.navigator.borrow().clone(); if let Some(navigator) = navigator { - let editor = MediumEditor::new(clone.backend.clone(), disc); + let source = Rc::new(Box::new(disc) as Box); + let editor = MediumEditor::new(clone.backend.clone(), source); navigator.push(editor); } diff --git a/src/import/track_selector.rs b/src/import/track_selector.rs index ae7621a..6972a2b 100644 --- a/src/import/track_selector.rs +++ b/src/import/track_selector.rs @@ -1,4 +1,4 @@ -use super::disc_source::DiscSource; +use super::source::Source; use crate::widgets::{Navigator, NavigatorScreen}; use glib::clone; use gtk::prelude::*; @@ -7,9 +7,9 @@ use libhandy::prelude::*; use std::cell::RefCell; use std::rc::Rc; -/// A screen for selecting tracks from a medium. +/// A screen for selecting tracks from a source. pub struct TrackSelector { - source: Rc, + source: Rc>, widget: gtk::Box, select_button: gtk::Button, selection: RefCell>, @@ -19,7 +19,7 @@ pub struct TrackSelector { impl TrackSelector { /// Create a new track selector. - pub fn new(source: Rc) -> Rc { + pub fn new(source: Rc>) -> Rc { // Create UI let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_selector.ui"); @@ -65,7 +65,9 @@ impl TrackSelector { } })); - for (index, track) in this.source.tracks.iter().enumerate() { + let tracks = this.source.tracks().unwrap(); + + for (index, track) in tracks.iter().enumerate() { let check = gtk::CheckButton::new(); check.connect_toggled(clone!(@strong this => move |check| { diff --git a/src/import/track_set_editor.rs b/src/import/track_set_editor.rs index 852744e..776abde 100644 --- a/src/import/track_set_editor.rs +++ b/src/import/track_set_editor.rs @@ -1,4 +1,4 @@ -use super::disc_source::DiscSource; +use super::source::Source; use super::track_editor::TrackEditor; use super::track_selector::TrackSelector; use crate::backend::Backend; @@ -33,7 +33,7 @@ pub struct TrackData { /// A screen for editing a set of tracks for one recording. pub struct TrackSetEditor { backend: Rc, - source: Rc, + source: Rc>, widget: gtk::Box, save_button: gtk::Button, recording_row: libhandy::ActionRow, @@ -46,7 +46,7 @@ pub struct TrackSetEditor { impl TrackSetEditor { /// Create a new track set editor. - pub fn new(backend: Rc, source: Rc) -> Rc { + pub fn new(backend: Rc, source: Rc>) -> Rc { // Create UI let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_set_editor.ui"); @@ -174,7 +174,9 @@ impl TrackSetEditor { title_parts.join(", ") }; - let number = this.source.tracks[track.track_source].number; + let tracks = this.source.tracks().unwrap(); + + let number = tracks[track.track_source].number; let subtitle = format!("Track {}", number); let edit_image = gtk::Image::from_icon_name(Some("document-edit-symbolic")); diff --git a/src/meson.build b/src/meson.build index 46c8738..53a82b3 100644 --- a/src/meson.build +++ b/src/meson.build @@ -68,6 +68,7 @@ sources = files( 'import/disc_source.rs', 'import/medium_editor.rs', 'import/mod.rs', + 'import/source.rs', 'import/source_selector.rs', 'import/track_editor.rs', 'import/track_selector.rs',