From aeb7da73c9644aabf35d63871b721d2e3575b0e2 Mon Sep 17 00:00:00 2001 From: Elias Projahn Date: Sat, 20 Feb 2021 19:03:26 +0100 Subject: [PATCH] Import in separate crate and change source ID calculation --- Cargo.toml | 2 +- backend/Cargo.toml | 1 + backend/src/lib.rs | 1 + de.johrpan.musicus.json | 16 --- import/Cargo.toml | 16 +++ import/src/disc.rs | 165 +++++++++++++++++++++ import/src/error.rs | 96 +++++++++++++ import/src/folder.rs | 64 +++++++++ import/src/lib.rs | 8 ++ import/src/session.rs | 91 ++++++++++++ meson.build | 1 - musicus/Cargo.toml | 1 - musicus/src/import/disc_source.rs | 189 ------------------------- musicus/src/import/folder_source.rs | 90 ------------ musicus/src/import/medium_editor.rs | 27 ++-- musicus/src/import/mod.rs | 3 - musicus/src/import/source.rs | 39 ----- musicus/src/import/source_selector.rs | 20 +-- musicus/src/import/track_selector.rs | 13 +- musicus/src/import/track_set_editor.rs | 15 +- 20 files changed, 479 insertions(+), 379 deletions(-) create mode 100644 import/Cargo.toml create mode 100644 import/src/disc.rs create mode 100644 import/src/error.rs create mode 100644 import/src/folder.rs create mode 100644 import/src/lib.rs create mode 100644 import/src/session.rs delete mode 100644 musicus/src/import/disc_source.rs delete mode 100644 musicus/src/import/folder_source.rs delete mode 100644 musicus/src/import/source.rs diff --git a/Cargo.toml b/Cargo.toml index fe476d4..92a9b42 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,2 +1,2 @@ [workspace] -members = ["backend", "client", "database", "musicus"] +members = ["backend", "client", "database", "import", "musicus"] diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 5f3b324..2b031c5 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -14,6 +14,7 @@ gstreamer-player = "0.16.3" log = "0.4.14" musicus_client = { version = "0.1.0", path = "../client" } musicus_database = { version = "0.1.0", path = "../database" } +musicus_import = { version = "0.1.0", path = "../import" } thiserror = "1.0.23" [target.'cfg(target_os = "linux")'.dependencies] diff --git a/backend/src/lib.rs b/backend/src/lib.rs index 981c5e5..eff03a9 100644 --- a/backend/src/lib.rs +++ b/backend/src/lib.rs @@ -10,6 +10,7 @@ use std::rc::Rc; pub use musicus_client as client; pub use musicus_database as db; +pub use musicus_import as import; pub mod error; pub use error::*; diff --git a/de.johrpan.musicus.json b/de.johrpan.musicus.json index e3c68ef..8c00e65 100644 --- a/de.johrpan.musicus.json +++ b/de.johrpan.musicus.json @@ -43,22 +43,6 @@ "*.a" ], "modules" : [ - { - "name": "libdiscid", - "buildsystem": "cmake-ninja", - "builddir" : true, - "sources": [ - { - "type": "archive", - "url": "https://github.com/metabrainz/libdiscid/archive/v0.6.2.tar.gz", - "sha256": "a9b73b030603ce707941a3aab4f46649fa5029025e7e2bfbbe0c3c93a86d7b20" - } - ], - "cleanup": [ - "/include", - "/lib/pkgconfig" - ] - }, { "name": "cdparanoia", "buildsystem": "simple", diff --git a/import/Cargo.toml b/import/Cargo.toml new file mode 100644 index 0000000..e24d9c8 --- /dev/null +++ b/import/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "musicus_import" +version = "0.1.0" +edition = "2018" + +[dependencies] +base64 = "0.13.0" +futures-channel = "0.3.5" +glib = { git = "https://github.com/gtk-rs/gtk-rs/", features = ["v2_64"] } +gstreamer = "0.16.5" +gstreamer-pbutils = "0.16.5" +log = "0.4.14" +once_cell = "1.5.2" +rand = "0.7.3" +thiserror = "1.0.23" +sha2 = "0.9.3" diff --git a/import/src/disc.rs b/import/src/disc.rs new file mode 100644 index 0000000..d433990 --- /dev/null +++ b/import/src/disc.rs @@ -0,0 +1,165 @@ +use crate::error::{Error, Result}; +use crate::session::{ImportSession, ImportTrack}; +use gstreamer::prelude::*; +use gstreamer::{ClockTime, ElementFactory, MessageType, MessageView, TocEntryType}; +use gstreamer::tags::{Duration, TrackNumber}; +use sha2::{Sha256, Digest}; +use std::path::PathBuf; +use log::info; + +/// Create a new import session for the default disc drive. +pub(super) fn new() -> Result { + let mut tracks = Vec::new(); + let mut hasher = Sha256::new(); + + // Build the GStreamer pipeline. It will contain a fakesink initially to be able to run it + // forward to the paused state without specifying a file name before knowing the tracks. + + let cdparanoiasrc = ElementFactory::make("cdparanoiasrc", None)?; + let queue = ElementFactory::make("queue", None)?; + let audioconvert = ElementFactory::make("audioconvert", None)?; + let flacenc = ElementFactory::make("flacenc", None)?; + let fakesink = gstreamer::ElementFactory::make("fakesink", None)?; + + let pipeline = gstreamer::Pipeline::new(None); + pipeline.add_many(&[&cdparanoiasrc, &queue, &audioconvert, &flacenc, &fakesink])?; + gstreamer::Element::link_many(&[&cdparanoiasrc, &queue, &audioconvert, &flacenc, &fakesink])?; + + let bus = pipeline.get_bus().ok_or(Error::u(String::from("Failed to get bus from pipeline.")))?; + + // Run the pipeline into the paused state and wait for the resulting TOC message on the bus. + + pipeline.set_state(gstreamer::State::Paused)?; + + let msg = bus.timed_pop_filtered(ClockTime::from_seconds(5), + &vec![MessageType::Toc, MessageType::Error]); + + let toc = match msg { + Some(msg) => match msg.view() { + MessageView::Error(err) => Err(Error::os(err.get_error())), + MessageView::Toc(toc) => Ok(toc.get_toc().0), + _ => Err(Error::u(format!("Unexpected message from GStreamer: {:?}", msg))), + }, + None => Err(Error::Timeout( + format!("Timeout while waiting for first message from GStreamer."))), + }?; + + pipeline.set_state(gstreamer::State::Ready)?; + + // Replace the fakesink with the real filesink. This won't need to be synced to the pipeline + // state, because we will set the whole pipeline's state to playing later. + + gstreamer::Element::unlink(&flacenc, &fakesink); + fakesink.set_state(gstreamer::State::Null)?; + pipeline.remove(&fakesink)?; + + let filesink = gstreamer::ElementFactory::make("filesink", None)?; + pipeline.add(&filesink)?; + gstreamer::Element::link(&flacenc, &filesink)?; + + // Get track data from the toc message that was received above. + + let tmp_dir = create_tmp_dir()?; + + for entry in toc.get_entries() { + if entry.get_entry_type() == TocEntryType::Track { + let duration = entry.get_tags() + .ok_or(Error::u(String::from("No tags in TOC entry.")))? + .get::() + .ok_or(Error::u(String::from("No duration tag found in TOC entry.")))? + .get() + .ok_or(Error::u(String::from("Failed to unwrap duration tag from TOC entry.")))? + .mseconds() + .ok_or(Error::u(String::from("Failed to unwrap track duration.")))?; + + let number = entry.get_tags() + .ok_or(Error::u(String::from("No tags in TOC entry.")))? + .get::() + .ok_or(Error::u(String::from("No track number tag found in TOC entry.")))? + .get() + .ok_or(Error::u( + String::from("Failed to unwrap track number tag from TOC entry.")))?; + + hasher.update(duration.to_le_bytes()); + + let name = format!("Track {}", number); + + let file_name = format!("track_{:02}.flac", number); + let mut path = tmp_dir.clone(); + path.push(file_name); + + let track = ImportTrack { + number, + name, + path, + duration, + }; + + tracks.push(track); + } + } + + let source_id = base64::encode_config(hasher.finalize(), base64::URL_SAFE); + + info!("Successfully loaded audio CD with {} tracks.", tracks.len()); + info!("Source ID: {}", source_id); + + let tracks_clone = tracks.clone(); + let copy = move || { + for track in &tracks_clone { + info!("Starting to rip track {}.", track.number); + + cdparanoiasrc.set_property("track", &track.number)?; + + // The filesink needs to be reset to be able to change the file location. + filesink.set_state(gstreamer::State::Null)?; + + let path = track.path.to_str().unwrap(); + filesink.set_property("location", &path)?; + + // This will also affect the filesink as expected. + pipeline.set_state(gstreamer::State::Playing)?; + + for msg in bus.iter_timed(ClockTime::none()) { + match msg.view() { + MessageView::Eos(..) => { + info!("Finished ripping track {}.", track.number); + pipeline.set_state(gstreamer::State::Ready)?; + break; + }, + MessageView::Error(err) => { + pipeline.set_state(gstreamer::State::Null)?; + Err(Error::os(err.get_error()))?; + }, + _ => (), + } + } + } + + pipeline.set_state(gstreamer::State::Null)?; + + Ok(()) + }; + + let session = ImportSession { + source_id, + tracks, + copy: Some(Box::new(copy)), + }; + + Ok(session) +} + +/// Create a new temporary directory and return its path. +fn create_tmp_dir() -> Result { + let mut tmp_dir = glib::get_tmp_dir().ok_or(Error::u( + String::from("Failed to get temporary directory using glib::get_tmp_dir().")))?; + + let dir_name = format!("musicus-{}", rand::random::()); + tmp_dir.push(dir_name); + + std::fs::create_dir(&tmp_dir)?; + + Ok(tmp_dir) +} + diff --git a/import/src/error.rs b/import/src/error.rs new file mode 100644 index 0000000..a1e18dd --- /dev/null +++ b/import/src/error.rs @@ -0,0 +1,96 @@ +use std::error; + +/// An error within an import session. +#[derive(thiserror::Error, Debug)] +pub enum Error { + /// A timeout was reached. + #[error("{0}")] + Timeout(String), + + /// Some common error. + #[error("{msg}")] + Other { + /// The error message. + msg: String, + + #[source] + source: Option>, + }, + + /// Something unexpected happened. + #[error("{msg}")] + Unexpected { + /// The error message. + msg: String, + + #[source] + source: Option>, + }, +} + +impl Error { + /// Create a new error without an explicit source. + pub(super) fn o(msg: String) -> Self { + Self::Unexpected { + msg, + source: None, + } + } + + /// Create a new error with an explicit source. + pub(super) fn os(source: impl error::Error + Send + Sync + 'static) -> Self { + Self::Unexpected { + msg: format!("An error has happened: {}", source), + source: Some(Box::new(source)), + } + } + + /// Create a new unexpected error without an explicit source. + pub(super) fn u(msg: String) -> Self { + Self::Unexpected { + msg, + source: None, + } + } + + /// Create a new unexpected error with an explicit source. + pub(super) fn us(source: impl error::Error + Send + Sync + 'static) -> Self { + Self::Unexpected { + msg: format!("An unexpected error has happened: {}", source), + source: Some(Box::new(source)), + } + } +} + +impl From for Error { + fn from(err: futures_channel::oneshot::Canceled) -> Self { + Self::us(err) + } +} + +impl From for Error { + fn from(err: gstreamer::glib::Error) -> Self { + Self::us(err) + } +} + +impl From for Error { + fn from(err: gstreamer::glib::BoolError) -> Self { + Self::us(err) + } +} + +impl From for Error { + fn from(err: gstreamer::StateChangeError) -> Self { + Self::us(err) + } +} + +impl From for Error { + fn from(err: std::io::Error) -> Self { + Self::us(err) + } +} + +pub type Result = std::result::Result; + diff --git a/import/src/folder.rs b/import/src/folder.rs new file mode 100644 index 0000000..f6497ae --- /dev/null +++ b/import/src/folder.rs @@ -0,0 +1,64 @@ +use crate::error::{Error, Result}; +use crate::session::{ImportSession, ImportTrack}; +use gstreamer::ClockTime; +use gstreamer_pbutils::Discoverer; +use log::{warn, info}; +use sha2::{Sha256, Digest}; +use std::path::PathBuf; + +/// Create a new import session for the specified folder. +pub(super) fn new(path: PathBuf) -> Result { + let mut tracks = Vec::new(); + let mut number: u32 = 1; + let mut hasher = Sha256::new(); + let discoverer = Discoverer::new(ClockTime::from_seconds(1))?; + + for entry in std::fs::read_dir(path)? { + let entry = entry?; + + if entry.file_type()?.is_file() { + let path = entry.path(); + + let uri = glib::filename_to_uri(&path, None) + .or(Err(Error::u(format!("Failed to create URI from path: {:?}", path))))?; + + let info = discoverer.discover_uri(&uri)?; + + if !info.get_audio_streams().is_empty() { + let duration = info.get_duration().mseconds() + .ok_or(Error::u(format!("Failed to get duration for {}.", uri)))?; + + let file_name = entry.file_name(); + let name = file_name.into_string() + .or(Err(Error::u(format!( + "Failed to convert OsString to String: {:?}", entry.file_name()))))?; + + hasher.update(duration.to_le_bytes()); + + let track = ImportTrack { + number, + name, + path, + duration, + }; + + tracks.push(track); + number += 1; + } else { + warn!("File {} skipped, because it doesn't contain any audio streams.", uri); + } + } + } + + let source_id = base64::encode_config(hasher.finalize(), base64::URL_SAFE); + + info!("Source ID: {}", source_id); + + let session = ImportSession { + source_id, + tracks, + copy: None, + }; + + Ok(session) +} diff --git a/import/src/lib.rs b/import/src/lib.rs new file mode 100644 index 0000000..c731a2d --- /dev/null +++ b/import/src/lib.rs @@ -0,0 +1,8 @@ +pub use session::{ImportSession, ImportTrack}; +pub use error::{Error, Result}; + +pub mod error; +pub mod session; + +mod disc; +mod folder; diff --git a/import/src/session.rs b/import/src/session.rs new file mode 100644 index 0000000..be84f4c --- /dev/null +++ b/import/src/session.rs @@ -0,0 +1,91 @@ +use crate::{disc, folder}; +use crate::error::Result; +use futures_channel::oneshot; +use std::path::PathBuf; +use std::thread; +use std::sync::Arc; + +/// Interface for importing audio tracks from a medium or folder. +pub struct ImportSession { + /// A string identifying the source as specific as possible across platforms and formats. + pub(super) source_id: String, + + /// The tracks that are available on the source. + pub(super) tracks: Vec, + + /// A closure that has to be called to copy the tracks if set. + pub(super) copy: Option Result<()> + Send + Sync>>, +} + +impl ImportSession { + /// Create a new import session for a audio CD. + pub async fn audio_cd() -> Result> { + let (sender, receiver) = oneshot::channel(); + + thread::spawn(move || { + let result = disc::new(); + let _ = sender.send(result); + }); + + Ok(Arc::new(receiver.await??)) + } + + /// Create a new import session for a folder. + pub async fn folder(path: PathBuf) -> Result> { + let (sender, receiver) = oneshot::channel(); + + thread::spawn(move || { + let result = folder::new(path); + let _ = sender.send(result); + }); + + Ok(Arc::new(receiver.await??)) + } + + /// Get a string identifying the source as specific as possible across platforms and mediums. + pub fn source_id(&self) -> &str { + &self.source_id + } + + /// Get the tracks that are available on the source. + pub fn tracks(&self) -> &[ImportTrack] { + &self.tracks + } + + /// Copy the tracks to their advertised locations, if neccessary. + pub async fn copy(self: &Arc) -> Result<()> { + if self.copy.is_some() { + let clone = Arc::clone(self); + let (sender, receiver) = oneshot::channel(); + + thread::spawn(move || { + let copy = clone.copy.as_ref().unwrap(); + sender.send(copy()).unwrap(); + }); + + receiver.await? + } else { + Ok(()) + } + } +} + +/// A track on an import source. +#[derive(Clone, Debug)] +pub struct ImportTrack { + /// The track number. + pub number: u32, + + /// A human readable identifier for the track. This will be used to present the track for + /// selection. + pub name: String, + + /// The path to the file where the corresponding audio file is. This file is only required to + /// exist, once the import was successfully completed. This will not be the actual file within + /// the user's music library, but the temporary location from which it can be copied to the + /// music library. + pub path: PathBuf, + + /// The track's duration in milliseconds. + pub duration: u64, +} diff --git a/meson.build b/meson.build index 5b51587..d10d80f 100644 --- a/meson.build +++ b/meson.build @@ -9,7 +9,6 @@ dependency('glib-2.0', version: '>= 2.56') dependency('gio-2.0', version: '>= 2.56') dependency('gstreamer-1.0', version: '>= 1.12') dependency('gtk4', version: '>= 4.0') -dependency('libdiscid', version: '>= 0.6.2') dependency('libadwaita-1', version: '>= 1.0') dependency('pango', version: '>= 1.0') dependency('sqlite3', version: '>= 3.20') diff --git a/musicus/Cargo.toml b/musicus/Cargo.toml index b1b6369..d1d7af2 100644 --- a/musicus/Cargo.toml +++ b/musicus/Cargo.toml @@ -6,7 +6,6 @@ edition = "2018" [dependencies] anyhow = "1.0.33" async-trait = "0.1.42" -discid = "0.4.4" futures-channel = "0.3.5" gettext-rs = { version = "0.5.0", features = ["gettext-system"] } gstreamer = "0.16.4" diff --git a/musicus/src/import/disc_source.rs b/musicus/src/import/disc_source.rs deleted file mode 100644 index c283140..0000000 --- a/musicus/src/import/disc_source.rs +++ /dev/null @@ -1,189 +0,0 @@ -use super::source::{Source, SourceTrack}; -use anyhow::{anyhow, bail, Result}; -use async_trait::async_trait; -use discid::DiscId; -use futures_channel::oneshot; -use gettextrs::gettext; -use gstreamer::prelude::*; -use gstreamer::{Element, ElementFactory, Pipeline}; -use once_cell::sync::OnceCell; -use std::path::{Path, PathBuf}; -use std::thread; - -/// Representation of an audio CD being imported as a medium. -#[derive(Clone, Debug)] -pub struct DiscSource { - /// The MusicBrainz DiscID of the CD. - pub discid: OnceCell, - - /// The tracks on this disc. - tracks: OnceCell>, -} - -impl DiscSource { - /// 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(), - }; - - Ok(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(); - - let mut tracks = Vec::new(); - - let first_track = discid.first_track_num() as u32; - let last_track = discid.last_track_num() as u32; - - let tmp_dir = Self::create_tmp_dir()?; - - for number in first_track..=last_track { - let name = gettext!("Track {}", number); - - let file_name = format!("track_{:02}.flac", number); - - let mut path = tmp_dir.clone(); - path.push(file_name); - - let track = SourceTrack { - number, - name, - path, - }; - - tracks.push(track); - } - - Ok((id, tracks)) - } - - /// Create a new temporary directory and return its path. - // TODO: Move to a more appropriate place. - fn create_tmp_dir() -> Result { - let mut tmp_dir = glib::get_tmp_dir() - .ok_or_else(|| { - anyhow!("Failed to get temporary directory using glib::get_tmp_dir()!") - })?; - - let dir_name = format!("musicus-{}", rand::random::()); - tmp_dir.push(dir_name); - - std::fs::create_dir(&tmp_dir)?; - - Ok(tmp_dir) - } - - /// Rip one track. - fn rip_track(path: &Path, number: u32) -> Result<()> { - let pipeline = Self::build_pipeline(path, number)?; - - let bus = pipeline - .get_bus() - .ok_or_else(|| anyhow!("Failed to get bus from pipeline!"))?; - - pipeline.set_state(gstreamer::State::Playing)?; - - for msg in bus.iter_timed(gstreamer::CLOCK_TIME_NONE) { - use gstreamer::MessageView::*; - - match msg.view() { - Eos(..) => break, - Error(err) => { - pipeline.set_state(gstreamer::State::Null)?; - bail!("GStreamer error: {:?}!", err); - } - _ => (), - } - } - - pipeline.set_state(gstreamer::State::Null)?; - - Ok(()) - } - - /// Build the GStreamer pipeline to rip a track. - fn build_pipeline(path: &Path, number: u32) -> Result { - let cdparanoiasrc = ElementFactory::make("cdparanoiasrc", None)?; - cdparanoiasrc.set_property("track", &number)?; - - let queue = ElementFactory::make("queue", None)?; - let audioconvert = ElementFactory::make("audioconvert", None)?; - let flacenc = ElementFactory::make("flacenc", None)?; - - let path_str = path.to_str().ok_or_else(|| { - anyhow!("Failed to convert path '{:?}' to string!", path) - })?; - - let filesink = gstreamer::ElementFactory::make("filesink", None)?; - filesink.set_property("location", &path_str.to_owned())?; - - let pipeline = gstreamer::Pipeline::new(None); - pipeline.add_many(&[&cdparanoiasrc, &queue, &audioconvert, &flacenc, &filesink])?; - - Element::link_many(&[&cdparanoiasrc, &queue, &audioconvert, &flacenc, &filesink])?; - - 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).unwrap(); - self.tracks.set(tracks).unwrap(); - - 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/musicus/src/import/folder_source.rs b/musicus/src/import/folder_source.rs deleted file mode 100644 index 89e5adc..0000000 --- a/musicus/src/import/folder_source.rs +++ /dev/null @@ -1,90 +0,0 @@ -use super::source::{Source, SourceTrack}; -use anyhow::{anyhow, Result}; -use async_trait::async_trait; -use futures_channel::oneshot; -use once_cell::sync::OnceCell; -use std::path::{Path, PathBuf}; -use std::thread; - -/// A folder outside of the music library that contains tracks to import. -#[derive(Clone, Debug)] -pub struct FolderSource { - /// The path to the folder. - path: PathBuf, - - /// The tracks within the folder. - tracks: OnceCell>, -} - -impl FolderSource { - /// Create a new folder source. - pub fn new(path: PathBuf) -> Self { - Self { - path, - tracks: OnceCell::new(), - } - } - - /// Load the contents of the folder as tracks. - fn load_priv(path: &Path) -> Result> { - let mut tracks = Vec::new(); - let mut number = 1; - - for entry in std::fs::read_dir(path)? { - let entry = entry?; - - if entry.file_type()?.is_file() { - let name = entry - .file_name() - .into_string() - .or_else(|_| Err(anyhow!("Failed to convert OsString to String!")))?; - - let path = entry.path(); - - let track = SourceTrack { - number, - name, - path, - }; - - tracks.push(track); - number += 1; - } - } - - Ok(tracks) - } -} - -#[async_trait] -impl Source for FolderSource { - async fn load(&self) -> Result<()> { - let (sender, receiver) = oneshot::channel(); - - let path = self.path.clone(); - thread::spawn(move || { - let result = Self::load_priv(&path); - sender.send(result).unwrap(); - }); - - let tracks = receiver.await??; - self.tracks.set(tracks).unwrap(); - - Ok(()) - } - - fn tracks(&self) -> Option<&[SourceTrack]> { - match self.tracks.get() { - Some(tracks) => Some(tracks.as_slice()), - None => None, - } - } - - fn discid(&self) -> Option { - None - } - - async fn copy(&self) -> Result<()> { - Ok(()) - } -} diff --git a/musicus/src/import/medium_editor.rs b/musicus/src/import/medium_editor.rs index 787461a..ab43ac7 100644 --- a/musicus/src/import/medium_editor.rs +++ b/musicus/src/import/medium_editor.rs @@ -1,21 +1,22 @@ -use super::source::Source; use super::track_set_editor::{TrackSetData, TrackSetEditor}; use crate::navigator::{NavigationHandle, Screen}; use crate::widgets::{List, Widget}; -use anyhow::{anyhow, Result}; +use anyhow::Result; use glib::clone; use glib::prelude::*; use gtk::prelude::*; use gtk_macros::get_widget; use libadwaita::prelude::*; use musicus_backend::db::{generate_id, Medium, Track, TrackSet}; +use musicus_backend::import::ImportSession; use std::cell::RefCell; use std::rc::Rc; +use std::sync::Arc; /// A dialog for editing metadata while importing music into the music library. pub struct MediumEditor { handle: NavigationHandle<()>, - source: Rc>, + session: Arc, widget: gtk::Stack, done_button: gtk::Button, done_stack: gtk::Stack, @@ -28,9 +29,9 @@ pub struct MediumEditor { track_sets: RefCell>, } -impl Screen>, ()> for MediumEditor { +impl Screen, ()> for MediumEditor { /// Create a new medium editor. - fn new(source: Rc>, handle: NavigationHandle<()>) -> Rc { + fn new(session: Arc, handle: NavigationHandle<()>) -> Rc { // Create UI let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/medium_editor.ui"); @@ -54,7 +55,7 @@ impl Screen>, ()> for MediumEditor { let this = Rc::new(Self { handle, - source, + session, widget, done_button, done_stack, @@ -88,7 +89,7 @@ impl Screen>, ()> for MediumEditor { add_button.connect_clicked(clone!(@weak this => move |_| { spawn!(@clone this, async move { - if let Some(track_set) = push!(this.handle, TrackSetEditor, Rc::clone(&this.source)).await { + if let Some(track_set) = push!(this.handle, TrackSetEditor, Arc::clone(&this.session)).await { let length = { let mut track_sets = this.track_sets.borrow_mut(); track_sets.push(track_set); @@ -135,7 +136,7 @@ impl Screen>, ()> for MediumEditor { })); spawn!(@clone this, async move { - match this.source.copy().await { + match this.session.copy().await { Err(err) => { this.disc_status_page.set_description(Some(&err.to_string())); this.widget.set_visible_child_name("disc_error"); @@ -165,7 +166,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!"))?; + let import_tracks = self.session.tracks(); for track_set_data in &*self.track_sets.borrow() { let mut tracks = Vec::new(); @@ -173,12 +174,12 @@ impl MediumEditor { for track_data in &track_set_data.tracks { // Copy the corresponding audio file to the music library. - let track_source = &source_tracks[track_data.track_source]; + let import_track = &import_tracks[track_data.track_source]; let mut track_path = path.clone(); - track_path.push(track_source.path.file_name().unwrap()); + track_path.push(import_track.path.file_name().unwrap()); - std::fs::copy(&track_source.path, &track_path)?; + std::fs::copy(&import_track.path, &track_path)?; // Create the real track. @@ -201,7 +202,7 @@ impl MediumEditor { let medium = Medium { id: generate_id(), name: self.name_entry.get_text().to_string(), - discid: self.source.discid(), + discid: Some(self.session.source_id().to_owned()), tracks: track_sets, }; diff --git a/musicus/src/import/mod.rs b/musicus/src/import/mod.rs index f21da67..f6588c4 100644 --- a/musicus/src/import/mod.rs +++ b/musicus/src/import/mod.rs @@ -1,7 +1,4 @@ -mod disc_source; -mod folder_source; mod medium_editor; -mod source; mod source_selector; mod track_editor; mod track_selector; diff --git a/musicus/src/import/source.rs b/musicus/src/import/source.rs deleted file mode 100644 index dfcd4d0..0000000 --- a/musicus/src/import/source.rs +++ /dev/null @@ -1,39 +0,0 @@ -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, - - /// A human readable identifier for the track. This will be used to - /// present the track for selection. - pub name: String, - - /// 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/musicus/src/import/source_selector.rs b/musicus/src/import/source_selector.rs index 60f5dc9..e0f405e 100644 --- a/musicus/src/import/source_selector.rs +++ b/musicus/src/import/source_selector.rs @@ -1,13 +1,11 @@ use super::medium_editor::MediumEditor; -use super::disc_source::DiscSource; -use super::folder_source::FolderSource; -use super::source::Source; use crate::navigator::{NavigationHandle, Screen}; use crate::widgets::Widget; use gettextrs::gettext; use glib::clone; use gtk::prelude::*; use gtk_macros::get_widget; +use musicus_backend::import::ImportSession; use std::path::PathBuf; use std::rc::Rc; @@ -65,11 +63,9 @@ impl Screen<(), ()> for SourceSelector { this.widget.set_visible_child_name("loading"); spawn!(@clone this, async move { - let folder = FolderSource::new(PathBuf::from(path)); - match folder.load().await { - Ok(_) => { - let source = Rc::new(Box::new(folder) as Box); - push!(this.handle, MediumEditor, source).await; + match ImportSession::folder(PathBuf::from(path)).await { + Ok(session) => { + push!(this.handle, MediumEditor, session).await; this.handle.pop(Some(())); } Err(err) => { @@ -90,11 +86,9 @@ impl Screen<(), ()> for SourceSelector { this.widget.set_visible_child_name("loading"); spawn!(@clone this, async move { - let disc = DiscSource::new().unwrap(); - match disc.load().await { - Ok(_) => { - let source = Rc::new(Box::new(disc) as Box); - push!(this.handle, MediumEditor, source).await; + match ImportSession::audio_cd().await { + Ok(session) => { + push!(this.handle, MediumEditor, session).await; this.handle.pop(Some(())); } Err(err) => { diff --git a/musicus/src/import/track_selector.rs b/musicus/src/import/track_selector.rs index abc61a3..93e204d 100644 --- a/musicus/src/import/track_selector.rs +++ b/musicus/src/import/track_selector.rs @@ -1,25 +1,26 @@ -use super::source::Source; use crate::navigator::{NavigationHandle, Screen}; use crate::widgets::Widget; use glib::clone; use gtk::prelude::*; use gtk_macros::get_widget; use libadwaita::prelude::*; +use musicus_backend::import::ImportSession; use std::cell::RefCell; use std::rc::Rc; +use std::sync::Arc; /// A screen for selecting tracks from a source. pub struct TrackSelector { handle: NavigationHandle>, - source: Rc>, + session: Arc, widget: gtk::Box, select_button: gtk::Button, selection: RefCell>, } -impl Screen>, Vec> for TrackSelector { +impl Screen, Vec> for TrackSelector { /// Create a new track selector. - fn new(source: Rc>, handle: NavigationHandle>) -> Rc { + fn new(session: Arc, handle: NavigationHandle>) -> Rc { // Create UI let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_selector.ui"); @@ -37,7 +38,7 @@ impl Screen>, Vec> for TrackSelector { let this = Rc::new(Self { handle, - source, + session, widget, select_button, selection: RefCell::new(Vec::new()), @@ -54,7 +55,7 @@ impl Screen>, Vec> for TrackSelector { this.handle.pop(Some(selection)); })); - let tracks = this.source.tracks().unwrap(); + let tracks = this.session.tracks(); for (index, track) in tracks.iter().enumerate() { let check = gtk::CheckButton::new(); diff --git a/musicus/src/import/track_set_editor.rs b/musicus/src/import/track_set_editor.rs index e179e82..9f0ffc8 100644 --- a/musicus/src/import/track_set_editor.rs +++ b/musicus/src/import/track_set_editor.rs @@ -1,4 +1,3 @@ -use super::source::Source; use super::track_editor::TrackEditor; use super::track_selector::TrackSelector; use crate::navigator::{NavigationHandle, Screen}; @@ -10,8 +9,10 @@ use gtk::prelude::*; use gtk_macros::get_widget; use libadwaita::prelude::*; use musicus_backend::db::Recording; +use musicus_backend::import::ImportSession; use std::cell::RefCell; use std::rc::Rc; +use std::sync::Arc; /// A track set before being imported. #[derive(Clone, Debug)] @@ -33,7 +34,7 @@ pub struct TrackData { /// A screen for editing a set of tracks for one recording. pub struct TrackSetEditor { handle: NavigationHandle, - source: Rc>, + session: Arc, widget: gtk::Box, save_button: gtk::Button, recording_row: libadwaita::ActionRow, @@ -42,9 +43,9 @@ pub struct TrackSetEditor { tracks: RefCell>, } -impl Screen>, TrackSetData> for TrackSetEditor { +impl Screen, TrackSetData> for TrackSetEditor { /// Create a new track set editor. - fn new(source: Rc>, handle: NavigationHandle) -> Rc { + fn new(session: Arc, handle: NavigationHandle) -> Rc { // Create UI let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_set_editor.ui"); @@ -62,7 +63,7 @@ impl Screen>, TrackSetData> for TrackSetEditor { let this = Rc::new(Self { handle, - source, + session, widget, save_button, recording_row, @@ -97,7 +98,7 @@ impl Screen>, TrackSetData> for TrackSetEditor { edit_tracks_button.connect_clicked(clone!(@weak this => move |_| { spawn!(@clone this, async move { - if let Some(selection) = push!(this.handle, TrackSelector, Rc::clone(&this.source)).await { + if let Some(selection) = push!(this.handle, TrackSelector, Arc::clone(&this.session)).await { let mut tracks = Vec::new(); for index in selection { @@ -134,7 +135,7 @@ impl Screen>, TrackSetData> for TrackSetEditor { title_parts.join(", ") }; - let tracks = this.source.tracks().unwrap(); + let tracks = this.session.tracks(); let track_name = &tracks[track.track_source].name; let edit_image = gtk::Image::from_icon_name(Some("document-edit-symbolic"));