mirror of
https://github.com/johrpan/musicus.git
synced 2025-10-26 19:57:25 +01:00
Add an abstract source to prepare for folder import
This commit is contained in:
parent
e5028058ab
commit
88c7256c51
9 changed files with 140 additions and 83 deletions
|
|
@ -5,6 +5,7 @@ edition = "2018"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.33"
|
anyhow = "1.0.33"
|
||||||
|
async-trait = "0.1.42"
|
||||||
diesel = { version = "1.4.5", features = ["sqlite"] }
|
diesel = { version = "1.4.5", features = ["sqlite"] }
|
||||||
diesel_migrations = "1.4.0"
|
diesel_migrations = "1.4.0"
|
||||||
discid = "0.4.4"
|
discid = "0.4.4"
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
|
use super::source::{Source, SourceTrack};
|
||||||
use anyhow::{anyhow, bail, Result};
|
use anyhow::{anyhow, bail, Result};
|
||||||
|
use async_trait::async_trait;
|
||||||
use discid::DiscId;
|
use discid::DiscId;
|
||||||
use futures_channel::oneshot;
|
use futures_channel::oneshot;
|
||||||
use gstreamer::prelude::*;
|
use gstreamer::prelude::*;
|
||||||
use gstreamer::{Element, ElementFactory, Pipeline};
|
use gstreamer::{Element, ElementFactory, Pipeline};
|
||||||
|
use once_cell::sync::OnceCell;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
|
||||||
|
|
@ -10,67 +13,27 @@ use std::thread;
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct DiscSource {
|
pub struct DiscSource {
|
||||||
/// The MusicBrainz DiscID of the CD.
|
/// The MusicBrainz DiscID of the CD.
|
||||||
pub discid: String,
|
pub discid: OnceCell<String>,
|
||||||
|
|
||||||
/// The path to the temporary directory where the audio files will be.
|
|
||||||
pub path: PathBuf,
|
|
||||||
|
|
||||||
/// The tracks on this disc.
|
/// The tracks on this disc.
|
||||||
pub tracks: Vec<TrackSource>,
|
tracks: OnceCell<Vec<SourceTrack>>,
|
||||||
}
|
|
||||||
|
|
||||||
/// 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,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DiscSource {
|
impl DiscSource {
|
||||||
/// Try to create a new disc source by asynchronously reading the
|
/// Create a new disc source. The source has to be initialized by calling
|
||||||
/// information from the default disc drive.
|
/// load() afterwards.
|
||||||
pub async fn load() -> Result<Self> {
|
pub fn new() -> Result<Self> {
|
||||||
let (sender, receiver) = oneshot::channel();
|
let result = Self {
|
||||||
|
discid: OnceCell::new(),
|
||||||
|
tracks: OnceCell::new(),
|
||||||
|
};
|
||||||
|
|
||||||
thread::spawn(|| {
|
Ok(result)
|
||||||
let disc = Self::load_priv();
|
|
||||||
sender.send(disc).unwrap();
|
|
||||||
});
|
|
||||||
|
|
||||||
let disc = receiver.await??;
|
|
||||||
|
|
||||||
Ok(disc)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Rip the whole disc asynchronously. After this method has finished
|
/// Load the disc from the default disc drive and return the MusicBrainz
|
||||||
/// successfully, the audio files will be available in the specified
|
/// DiscID as well as descriptions of the contained tracks.
|
||||||
/// location for each track source.
|
fn load_priv() -> Result<(String, Vec<SourceTrack>)> {
|
||||||
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<Self> {
|
|
||||||
let discid = DiscId::read(None)?;
|
let discid = DiscId::read(None)?;
|
||||||
let id = discid.id();
|
let id = discid.id();
|
||||||
|
|
||||||
|
|
@ -87,7 +50,7 @@ impl DiscSource {
|
||||||
let mut path = tmp_dir.clone();
|
let mut path = tmp_dir.clone();
|
||||||
path.push(file_name);
|
path.push(file_name);
|
||||||
|
|
||||||
let track = TrackSource {
|
let track = SourceTrack {
|
||||||
number,
|
number,
|
||||||
path,
|
path,
|
||||||
};
|
};
|
||||||
|
|
@ -95,13 +58,7 @@ impl DiscSource {
|
||||||
tracks.push(track);
|
tracks.push(track);
|
||||||
}
|
}
|
||||||
|
|
||||||
let disc = DiscSource {
|
Ok((id, tracks))
|
||||||
discid: id,
|
|
||||||
tracks,
|
|
||||||
path: tmp_dir,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(disc)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new temporary directory and return its path.
|
/// Create a new temporary directory and return its path.
|
||||||
|
|
@ -172,3 +129,57 @@ impl DiscSource {
|
||||||
Ok(pipeline)
|
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<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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
use super::disc_source::DiscSource;
|
use super::source::Source;
|
||||||
use super::track_set_editor::{TrackSetData, TrackSetEditor};
|
use super::track_set_editor::{TrackSetData, TrackSetEditor};
|
||||||
use crate::database::{generate_id, Medium, Track, TrackSet};
|
use crate::database::{generate_id, Medium, Track, TrackSet};
|
||||||
use crate::backend::Backend;
|
use crate::backend::Backend;
|
||||||
use crate::widgets::{List, Navigator, NavigatorScreen};
|
use crate::widgets::{List, Navigator, NavigatorScreen};
|
||||||
use anyhow::Result;
|
use anyhow::{anyhow, Result};
|
||||||
use glib::clone;
|
use glib::clone;
|
||||||
use glib::prelude::*;
|
use glib::prelude::*;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
|
|
@ -15,7 +15,7 @@ use std::rc::Rc;
|
||||||
/// A dialog for editing metadata while importing music into the music library.
|
/// A dialog for editing metadata while importing music into the music library.
|
||||||
pub struct MediumEditor {
|
pub struct MediumEditor {
|
||||||
backend: Rc<Backend>,
|
backend: Rc<Backend>,
|
||||||
source: Rc<DiscSource>,
|
source: Rc<Box<dyn Source>>,
|
||||||
widget: gtk::Stack,
|
widget: gtk::Stack,
|
||||||
done_button: gtk::Button,
|
done_button: gtk::Button,
|
||||||
done_stack: gtk::Stack,
|
done_stack: gtk::Stack,
|
||||||
|
|
@ -29,7 +29,7 @@ pub struct MediumEditor {
|
||||||
|
|
||||||
impl MediumEditor {
|
impl MediumEditor {
|
||||||
/// Create a new medium editor.
|
/// Create a new medium editor.
|
||||||
pub fn new(backend: Rc<Backend>, source: DiscSource) -> Rc<Self> {
|
pub fn new(backend: Rc<Backend>, source: Rc<Box<dyn Source>>) -> Rc<Self> {
|
||||||
// Create UI
|
// Create UI
|
||||||
|
|
||||||
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/medium_editor.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 {
|
let this = Rc::new(Self {
|
||||||
backend,
|
backend,
|
||||||
source: Rc::new(source),
|
source,
|
||||||
widget,
|
widget,
|
||||||
done_button,
|
done_button,
|
||||||
done_stack,
|
done_stack,
|
||||||
|
|
@ -131,14 +131,14 @@ impl MediumEditor {
|
||||||
row.upcast()
|
row.upcast()
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Start ripping the CD in the background.
|
// Copy the source in the background.
|
||||||
let context = glib::MainContext::default();
|
let context = glib::MainContext::default();
|
||||||
let clone = this.clone();
|
let clone = this.clone();
|
||||||
context.spawn_local(async move {
|
context.spawn_local(async move {
|
||||||
match clone.source.rip().await {
|
match clone.source.copy().await {
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
// TODO: Present error.
|
// TODO: Present error.
|
||||||
println!("Failed to rip: {}", error);
|
println!("Failed to copy source: {}", error);
|
||||||
},
|
},
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
clone.done_stack.set_visible_child(&clone.done);
|
clone.done_stack.set_visible_child(&clone.done);
|
||||||
|
|
@ -163,6 +163,7 @@ impl MediumEditor {
|
||||||
// Convert the track set data to real track sets.
|
// Convert the track set data to real track sets.
|
||||||
|
|
||||||
let mut track_sets = Vec::new();
|
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() {
|
for track_set_data in &*self.track_sets.borrow() {
|
||||||
let mut tracks = Vec::new();
|
let mut tracks = Vec::new();
|
||||||
|
|
@ -170,7 +171,7 @@ impl MediumEditor {
|
||||||
for track_data in &track_set_data.tracks {
|
for track_data in &track_set_data.tracks {
|
||||||
// Copy the corresponding audio file to the music library.
|
// 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 file_name = format!("track_{:02}.flac", track_source.number);
|
||||||
|
|
||||||
let mut track_path = path.clone();
|
let mut track_path = path.clone();
|
||||||
|
|
@ -199,7 +200,7 @@ impl MediumEditor {
|
||||||
let medium = Medium {
|
let medium = Medium {
|
||||||
id: generate_id(),
|
id: generate_id(),
|
||||||
name: self.name_entry.get_text().unwrap().to_string(),
|
name: self.name_entry.get_text().unwrap().to_string(),
|
||||||
discid: Some(self.source.discid.clone()),
|
discid: self.source.discid(),
|
||||||
tracks: track_sets,
|
tracks: track_sets,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
mod disc_source;
|
mod disc_source;
|
||||||
mod medium_editor;
|
mod medium_editor;
|
||||||
|
mod source;
|
||||||
mod source_selector;
|
mod source_selector;
|
||||||
mod track_editor;
|
mod track_editor;
|
||||||
mod track_selector;
|
mod track_selector;
|
||||||
|
|
|
||||||
35
src/import/source.rs
Normal file
35
src/import/source.rs
Normal file
|
|
@ -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<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,
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use super::medium_editor::MediumEditor;
|
use super::medium_editor::MediumEditor;
|
||||||
use super::disc_source::DiscSource;
|
use super::disc_source::DiscSource;
|
||||||
|
use super::source::Source;
|
||||||
use crate::backend::Backend;
|
use crate::backend::Backend;
|
||||||
use crate::widgets::{Navigator, NavigatorScreen};
|
use crate::widgets::{Navigator, NavigatorScreen};
|
||||||
use glib::clone;
|
use glib::clone;
|
||||||
|
|
@ -54,11 +55,13 @@ impl SourceSelector {
|
||||||
let context = glib::MainContext::default();
|
let context = glib::MainContext::default();
|
||||||
let clone = this.clone();
|
let clone = this.clone();
|
||||||
context.spawn_local(async move {
|
context.spawn_local(async move {
|
||||||
match DiscSource::load().await {
|
let disc = DiscSource::new().unwrap();
|
||||||
Ok(disc) => {
|
match disc.load().await {
|
||||||
|
Ok(_) => {
|
||||||
let navigator = clone.navigator.borrow().clone();
|
let navigator = clone.navigator.borrow().clone();
|
||||||
if let Some(navigator) = navigator {
|
if let Some(navigator) = navigator {
|
||||||
let editor = MediumEditor::new(clone.backend.clone(), disc);
|
let source = Rc::new(Box::new(disc) as Box<dyn Source>);
|
||||||
|
let editor = MediumEditor::new(clone.backend.clone(), source);
|
||||||
navigator.push(editor);
|
navigator.push(editor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use super::disc_source::DiscSource;
|
use super::source::Source;
|
||||||
use crate::widgets::{Navigator, NavigatorScreen};
|
use crate::widgets::{Navigator, NavigatorScreen};
|
||||||
use glib::clone;
|
use glib::clone;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
|
|
@ -7,9 +7,9 @@ use libhandy::prelude::*;
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
/// A screen for selecting tracks from a medium.
|
/// A screen for selecting tracks from a source.
|
||||||
pub struct TrackSelector {
|
pub struct TrackSelector {
|
||||||
source: Rc<DiscSource>,
|
source: Rc<Box<dyn Source>>,
|
||||||
widget: gtk::Box,
|
widget: gtk::Box,
|
||||||
select_button: gtk::Button,
|
select_button: gtk::Button,
|
||||||
selection: RefCell<Vec<usize>>,
|
selection: RefCell<Vec<usize>>,
|
||||||
|
|
@ -19,7 +19,7 @@ pub struct TrackSelector {
|
||||||
|
|
||||||
impl TrackSelector {
|
impl TrackSelector {
|
||||||
/// Create a new track selector.
|
/// Create a new track selector.
|
||||||
pub fn new(source: Rc<DiscSource>) -> Rc<Self> {
|
pub fn new(source: Rc<Box<dyn Source>>) -> Rc<Self> {
|
||||||
// Create UI
|
// Create UI
|
||||||
|
|
||||||
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_selector.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();
|
let check = gtk::CheckButton::new();
|
||||||
|
|
||||||
check.connect_toggled(clone!(@strong this => move |check| {
|
check.connect_toggled(clone!(@strong this => move |check| {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use super::disc_source::DiscSource;
|
use super::source::Source;
|
||||||
use super::track_editor::TrackEditor;
|
use super::track_editor::TrackEditor;
|
||||||
use super::track_selector::TrackSelector;
|
use super::track_selector::TrackSelector;
|
||||||
use crate::backend::Backend;
|
use crate::backend::Backend;
|
||||||
|
|
@ -33,7 +33,7 @@ pub struct TrackData {
|
||||||
/// A screen for editing a set of tracks for one recording.
|
/// A screen for editing a set of tracks for one recording.
|
||||||
pub struct TrackSetEditor {
|
pub struct TrackSetEditor {
|
||||||
backend: Rc<Backend>,
|
backend: Rc<Backend>,
|
||||||
source: Rc<DiscSource>,
|
source: Rc<Box<dyn Source>>,
|
||||||
widget: gtk::Box,
|
widget: gtk::Box,
|
||||||
save_button: gtk::Button,
|
save_button: gtk::Button,
|
||||||
recording_row: libhandy::ActionRow,
|
recording_row: libhandy::ActionRow,
|
||||||
|
|
@ -46,7 +46,7 @@ pub struct TrackSetEditor {
|
||||||
|
|
||||||
impl TrackSetEditor {
|
impl TrackSetEditor {
|
||||||
/// Create a new track set editor.
|
/// Create a new track set editor.
|
||||||
pub fn new(backend: Rc<Backend>, source: Rc<DiscSource>) -> Rc<Self> {
|
pub fn new(backend: Rc<Backend>, source: Rc<Box<dyn Source>>) -> Rc<Self> {
|
||||||
// Create UI
|
// Create UI
|
||||||
|
|
||||||
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_set_editor.ui");
|
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_set_editor.ui");
|
||||||
|
|
@ -174,7 +174,9 @@ impl TrackSetEditor {
|
||||||
title_parts.join(", ")
|
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 subtitle = format!("Track {}", number);
|
||||||
|
|
||||||
let edit_image = gtk::Image::from_icon_name(Some("document-edit-symbolic"));
|
let edit_image = gtk::Image::from_icon_name(Some("document-edit-symbolic"));
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,7 @@ sources = files(
|
||||||
'import/disc_source.rs',
|
'import/disc_source.rs',
|
||||||
'import/medium_editor.rs',
|
'import/medium_editor.rs',
|
||||||
'import/mod.rs',
|
'import/mod.rs',
|
||||||
|
'import/source.rs',
|
||||||
'import/source_selector.rs',
|
'import/source_selector.rs',
|
||||||
'import/track_editor.rs',
|
'import/track_editor.rs',
|
||||||
'import/track_selector.rs',
|
'import/track_selector.rs',
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue