Import in separate crate and change source ID calculation

This commit is contained in:
Elias Projahn 2021-02-20 19:03:26 +01:00
parent c2c811e321
commit aeb7da73c9
20 changed files with 479 additions and 379 deletions

View file

@ -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"

View file

@ -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<String>,
/// The tracks on this disc.
tracks: OnceCell<Vec<SourceTrack>>,
}
impl DiscSource {
/// Create a new disc source. The source has to be initialized by calling
/// load() afterwards.
pub fn new() -> Result<Self> {
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<SourceTrack>)> {
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<PathBuf> {
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::<u64>());
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<Pipeline> {
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<String> {
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(())
}
}

View file

@ -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<Vec<SourceTrack>>,
}
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<Vec<SourceTrack>> {
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<String> {
None
}
async fn copy(&self) -> Result<()> {
Ok(())
}
}

View file

@ -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<Box<dyn Source>>,
session: Arc<ImportSession>,
widget: gtk::Stack,
done_button: gtk::Button,
done_stack: gtk::Stack,
@ -28,9 +29,9 @@ pub struct MediumEditor {
track_sets: RefCell<Vec<TrackSetData>>,
}
impl Screen<Rc<Box<dyn Source>>, ()> for MediumEditor {
impl Screen<Arc<ImportSession>, ()> for MediumEditor {
/// Create a new medium editor.
fn new(source: Rc<Box<dyn Source>>, handle: NavigationHandle<()>) -> Rc<Self> {
fn new(session: Arc<ImportSession>, handle: NavigationHandle<()>) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/medium_editor.ui");
@ -54,7 +55,7 @@ impl Screen<Rc<Box<dyn Source>>, ()> for MediumEditor {
let this = Rc::new(Self {
handle,
source,
session,
widget,
done_button,
done_stack,
@ -88,7 +89,7 @@ impl Screen<Rc<Box<dyn Source>>, ()> 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<Rc<Box<dyn Source>>, ()> 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,
};

View file

@ -1,7 +1,4 @@
mod disc_source;
mod folder_source;
mod medium_editor;
mod source;
mod source_selector;
mod track_editor;
mod track_selector;

View file

@ -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<String>;
/// 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,
}

View file

@ -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<dyn Source>);
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<dyn Source>);
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) => {

View file

@ -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<Vec<usize>>,
source: Rc<Box<dyn Source>>,
session: Arc<ImportSession>,
widget: gtk::Box,
select_button: gtk::Button,
selection: RefCell<Vec<usize>>,
}
impl Screen<Rc<Box<dyn Source>>, Vec<usize>> for TrackSelector {
impl Screen<Arc<ImportSession>, Vec<usize>> for TrackSelector {
/// Create a new track selector.
fn new(source: Rc<Box<dyn Source>>, handle: NavigationHandle<Vec<usize>>) -> Rc<Self> {
fn new(session: Arc<ImportSession>, handle: NavigationHandle<Vec<usize>>) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_selector.ui");
@ -37,7 +38,7 @@ impl Screen<Rc<Box<dyn Source>>, Vec<usize>> for TrackSelector {
let this = Rc::new(Self {
handle,
source,
session,
widget,
select_button,
selection: RefCell::new(Vec::new()),
@ -54,7 +55,7 @@ impl Screen<Rc<Box<dyn Source>>, Vec<usize>> 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();

View file

@ -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<TrackSetData>,
source: Rc<Box<dyn Source>>,
session: Arc<ImportSession>,
widget: gtk::Box,
save_button: gtk::Button,
recording_row: libadwaita::ActionRow,
@ -42,9 +43,9 @@ pub struct TrackSetEditor {
tracks: RefCell<Vec<TrackData>>,
}
impl Screen<Rc<Box<dyn Source>>, TrackSetData> for TrackSetEditor {
impl Screen<Arc<ImportSession>, TrackSetData> for TrackSetEditor {
/// Create a new track set editor.
fn new(source: Rc<Box<dyn Source>>, handle: NavigationHandle<TrackSetData>) -> Rc<Self> {
fn new(session: Arc<ImportSession>, handle: NavigationHandle<TrackSetData>) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_set_editor.ui");
@ -62,7 +63,7 @@ impl Screen<Rc<Box<dyn Source>>, TrackSetData> for TrackSetEditor {
let this = Rc::new(Self {
handle,
source,
session,
widget,
save_button,
recording_row,
@ -97,7 +98,7 @@ impl Screen<Rc<Box<dyn Source>>, 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<Rc<Box<dyn Source>>, 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"));