Initial ripping from new source selector

This commit is contained in:
Elias Projahn 2021-01-13 17:51:00 +01:00
parent 4aa858602d
commit 18600c310f
17 changed files with 277 additions and 688 deletions

View file

@ -4,12 +4,10 @@
<file preprocess="xml-stripblanks">ui/ensemble_editor.ui</file> <file preprocess="xml-stripblanks">ui/ensemble_editor.ui</file>
<file preprocess="xml-stripblanks">ui/ensemble_screen.ui</file> <file preprocess="xml-stripblanks">ui/ensemble_screen.ui</file>
<file preprocess="xml-stripblanks">ui/ensemble_selector.ui</file> <file preprocess="xml-stripblanks">ui/ensemble_selector.ui</file>
<file preprocess="xml-stripblanks">ui/import_dialog.ui</file>
<file preprocess="xml-stripblanks">ui/import_disc_dialog.ui</file>
<file preprocess="xml-stripblanks">ui/import_folder_dialog.ui</file>
<file preprocess="xml-stripblanks">ui/instrument_editor.ui</file> <file preprocess="xml-stripblanks">ui/instrument_editor.ui</file>
<file preprocess="xml-stripblanks">ui/instrument_selector.ui</file> <file preprocess="xml-stripblanks">ui/instrument_selector.ui</file>
<file preprocess="xml-stripblanks">ui/login_dialog.ui</file> <file preprocess="xml-stripblanks">ui/login_dialog.ui</file>
<file preprocess="xml-stripblanks">ui/medium_editor.ui</file>
<file preprocess="xml-stripblanks">ui/performance_editor.ui</file> <file preprocess="xml-stripblanks">ui/performance_editor.ui</file>
<file preprocess="xml-stripblanks">ui/person_editor.ui</file> <file preprocess="xml-stripblanks">ui/person_editor.ui</file>
<file preprocess="xml-stripblanks">ui/person_list.ui</file> <file preprocess="xml-stripblanks">ui/person_list.ui</file>
@ -25,6 +23,7 @@
<file preprocess="xml-stripblanks">ui/recording_selector_screen.ui</file> <file preprocess="xml-stripblanks">ui/recording_selector_screen.ui</file>
<file preprocess="xml-stripblanks">ui/selector.ui</file> <file preprocess="xml-stripblanks">ui/selector.ui</file>
<file preprocess="xml-stripblanks">ui/server_dialog.ui</file> <file preprocess="xml-stripblanks">ui/server_dialog.ui</file>
<file preprocess="xml-stripblanks">ui/source_selector.ui</file>
<file preprocess="xml-stripblanks">ui/track_editor.ui</file> <file preprocess="xml-stripblanks">ui/track_editor.ui</file>
<file preprocess="xml-stripblanks">ui/track_selector.ui</file> <file preprocess="xml-stripblanks">ui/track_selector.ui</file>
<file preprocess="xml-stripblanks">ui/track_set_editor.ui</file> <file preprocess="xml-stripblanks">ui/track_set_editor.ui</file>

View file

@ -1,77 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk+" version="3.24"/>
<requires lib="libhandy" version="1.0"/>
<object class="GtkBox" id="widget">
<property name="visible">True</property>
<property name="orientation">vertical</property>
<child>
<object class="HdyHeaderBar">
<property name="visible">True</property>
<property name="title" translatable="yes">Import folder</property>
<child>
<object class="GtkButton" id="back_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="icon-name">go-previous-symbolic</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="vexpand">True</property>
<property name="border-width">18</property>
<property name="orientation">vertical</property>
<property name="spacing">18</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="opacity">0.5</property>
<property name="pixel-size">80</property>
<property name="icon-name">folder-symbolic</property>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="opacity">0.5</property>
<property name="label" translatable="yes">Import from a folder</property>
<attributes>
<attribute name="size" value="16384"/>
</attributes>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="opacity">0.5</property>
<property name="label" translatable="yes">Select a folder containing audio files with the button below. After adding the metdata in the next step, the folder will be copied to your music library.</property>
<property name="justify">center</property>
<property name="wrap">True</property>
<property name="max-width-chars">40</property>
</object>
</child>
<child>
<object class="GtkButton" id="import_button">
<property name="label" translatable="yes">Select</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="halign">center</property>
<style>
<class name="suggested-action"/>
</style>
</object>
</child>
</object>
</child>
</object>
</interface>

View file

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.1 -->
<interface> <interface>
<requires lib="gtk+" version="3.24"/> <requires lib="gtk+" version="3.24"/>
<requires lib="libhandy" version="1.0"/> <requires lib="libhandy" version="1.0"/>
@ -11,7 +10,7 @@
<object class="HdyHeaderBar"> <object class="HdyHeaderBar">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can-focus">False</property> <property name="can-focus">False</property>
<property name="title" translatable="yes">Import CD</property> <property name="title" translatable="yes">Import music</property>
<child> <child>
<object class="GtkButton" id="back_button"> <object class="GtkButton" id="back_button">
<property name="visible">True</property> <property name="visible">True</property>
@ -27,11 +26,6 @@
</object> </object>
</child> </child>
</object> </object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child> </child>
<child> <child>
<object class="GtkStack" id="stack"> <object class="GtkStack" id="stack">
@ -49,21 +43,6 @@
<property name="can-focus">False</property> <property name="can-focus">False</property>
<property name="message-type">error</property> <property name="message-type">error</property>
<property name="revealed">False</property> <property name="revealed">False</property>
<child internal-child="action_area">
<object class="GtkButtonBox">
<property name="can-focus">False</property>
<property name="spacing">6</property>
<property name="layout-style">end</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child internal-child="content_area"> <child internal-child="content_area">
<object class="GtkBox"> <object class="GtkBox">
<property name="can-focus">False</property> <property name="can-focus">False</property>
@ -75,33 +54,16 @@
<property name="label" translatable="yes">Failed to load the CD. Make sure you have inserted it into your drive.</property> <property name="label" translatable="yes">Failed to load the CD. Make sure you have inserted it into your drive.</property>
<property name="wrap">True</property> <property name="wrap">True</property>
</object> </object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child> </child>
</object> </object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<placeholder/>
</child> </child>
</object> </object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child> </child>
<child> <child>
<object class="GtkBox"> <object class="GtkBox">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can-focus">False</property> <property name="can-focus">False</property>
<property name="vexpand">True</property>
<property name="halign">center</property> <property name="halign">center</property>
<property name="valign">center</property> <property name="valign">center</property>
<property name="border-width">18</property> <property name="border-width">18</property>
@ -193,55 +155,7 @@
<property name="position">1</property> <property name="position">1</property>
</packing> </packing>
</child> </child>
<child>
<object class="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can-focus">True</property>
<child>
<object class="GtkViewport">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="shadow-type">none</property>
<child>
<object class="HdyClamp">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="maximum-size">500</property>
<property name="tightening-threshold">300</property>
<child>
<object class="GtkFrame" id="frame">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">6</property>
<property name="margin-end">6</property>
<property name="margin-top">12</property>
<property name="margin-bottom">6</property>
<property name="label-xalign">0</property>
<property name="shadow-type">in</property>
<child>
<placeholder/>
</child>
<child type="label_item">
<placeholder/>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="name">content</property>
<property name="position">2</property>
</packing>
</child>
</object> </object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child> </child>
</object> </object>
</interface> </interface>

View file

@ -1,69 +0,0 @@
use crate::backend::Backend;
use crate::editors::{TrackSetEditor, TrackSource};
use crate::widgets::{Navigator, NavigatorScreen};
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use std::cell::RefCell;
use std::rc::Rc;
/// A dialog for editing metadata while importing music into the music library.
pub struct ImportDialog {
backend: Rc<Backend>,
source: Rc<TrackSource>,
widget: gtk::Box,
navigator: RefCell<Option<Rc<Navigator>>>,
}
impl ImportDialog {
/// Create a new import dialog.
pub fn new(backend: Rc<Backend>, source: Rc<TrackSource>) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/import_dialog.ui");
get_widget!(builder, gtk::Box, widget);
get_widget!(builder, gtk::Button, back_button);
get_widget!(builder, gtk::Button, add_button);
let this = Rc::new(Self {
backend,
source,
widget,
navigator: RefCell::new(None),
});
// Connect signals and callbacks
back_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.pop();
}
}));
add_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
let editor = TrackSetEditor::new(this.backend.clone(), this.source.clone());
navigator.push(editor);
}
}));
this
}
}
impl NavigatorScreen for ImportDialog {
fn attach_navigator(&self, navigator: Rc<Navigator>) {
self.navigator.replace(Some(navigator));
}
fn get_widget(&self) -> gtk::Widget {
self.widget.clone().upcast()
}
fn detach_navigator(&self) {
self.navigator.replace(None);
}
}

View file

@ -1,211 +0,0 @@
use crate::backend::Backend;
use crate::ripper::Ripper;
use crate::widgets::{List, Navigator, NavigatorScreen};
use anyhow::Result;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use std::cell::RefCell;
use std::rc::Rc;
/// The current status of a ripped track.
#[derive(Debug, Clone)]
enum RipStatus {
None,
Ripping,
Ready,
Error,
}
/// Representation of a track on the ripped disc.
#[derive(Debug, Clone)]
struct RipTrack {
pub status: RipStatus,
pub index: u32,
pub title: String,
pub subtitle: String,
}
/// A dialog for importing tracks from a CD.
pub struct ImportDiscDialog {
backend: Rc<Backend>,
widget: gtk::Box,
stack: gtk::Stack,
info_bar: gtk::InfoBar,
list: Rc<List<RipTrack>>,
ripper: Ripper,
tracks: RefCell<Vec<RipTrack>>,
navigator: RefCell<Option<Rc<Navigator>>>,
}
impl ImportDiscDialog {
/// Create a new import disc dialog.
pub fn new(backend: Rc<Backend>) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/import_disc_dialog.ui");
get_widget!(builder, gtk::Box, widget);
get_widget!(builder, gtk::Button, back_button);
get_widget!(builder, gtk::Stack, stack);
get_widget!(builder, gtk::InfoBar, info_bar);
get_widget!(builder, gtk::Button, import_button);
get_widget!(builder, gtk::Frame, frame);
let list = List::<RipTrack>::new("No tracks found.");
frame.add(&list.widget);
let mut tmp_dir = glib::get_tmp_dir().unwrap();
let dir_name = format!("musicus-{}", rand::random::<u64>());
tmp_dir.push(dir_name);
std::fs::create_dir(&tmp_dir).unwrap();
let ripper = Ripper::new(tmp_dir.to_str().unwrap());
let this = Rc::new(Self {
backend,
widget,
stack,
info_bar,
list,
ripper,
tracks: RefCell::new(Vec::new()),
navigator: RefCell::new(None),
});
// Connect signals and callbacks
back_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.pop();
}
}));
import_button.connect_clicked(clone!(@strong this => move |_| {
this.stack.set_visible_child_name("loading");
let context = glib::MainContext::default();
let clone = this.clone();
context.spawn_local(async move {
match clone.ripper.load_disc().await {
Ok(disc) => {
let mut tracks = Vec::<RipTrack>::new();
for track in disc.first_track..=disc.last_track {
tracks.push(RipTrack {
status: RipStatus::None,
index: track,
title: "Track".to_string(),
subtitle: "Unknown".to_string(),
});
}
clone.tracks.replace(tracks.clone());
clone.list.show_items(tracks);
clone.stack.set_visible_child_name("content");
clone.rip().await.unwrap();
}
Err(_) => {
clone.info_bar.set_revealed(true);
clone.stack.set_visible_child_name("start");
}
}
});
}));
this.list.set_make_widget(|track| {
let title = gtk::Label::new(Some(&format!("{}. {}", track.index, track.title)));
title.set_ellipsize(pango::EllipsizeMode::End);
title.set_halign(gtk::Align::Start);
let subtitle = gtk::Label::new(Some(&track.subtitle));
subtitle.set_ellipsize(pango::EllipsizeMode::End);
subtitle.set_opacity(0.5);
subtitle.set_halign(gtk::Align::Start);
let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0);
vbox.add(&title);
vbox.add(&subtitle);
vbox.set_hexpand(true);
use RipStatus::*;
let status: gtk::Widget = match track.status {
None => {
let placeholder = gtk::Label::new(Option::None);
placeholder.set_property_width_request(16);
placeholder.upcast()
}
Ripping => {
let spinner = gtk::Spinner::new();
spinner.start();
spinner.upcast()
}
Ready => gtk::Image::from_icon_name(
Some("object-select-symbolic"),
gtk::IconSize::Button,
)
.upcast(),
Error => {
gtk::Image::from_icon_name(Some("dialog-error-symbolic"), gtk::IconSize::Dialog)
.upcast()
}
};
let hbox = gtk::Box::new(gtk::Orientation::Horizontal, 6);
hbox.set_border_width(6);
hbox.add(&vbox);
hbox.add(&status);
hbox.upcast()
});
this
}
/// Rip the disc in the background.
async fn rip(&self) -> Result<()> {
let mut current_track = 0;
while current_track < self.tracks.borrow().len() {
{
let mut tracks = self.tracks.borrow_mut();
let mut track = &mut tracks[current_track];
track.status = RipStatus::Ripping;
self.list.show_items(tracks.clone());
}
self.ripper
.rip_track(self.tracks.borrow()[current_track].index)
.await
.unwrap();
{
let mut tracks = self.tracks.borrow_mut();
let mut track = &mut tracks[current_track];
track.status = RipStatus::Ready;
self.list.show_items(tracks.clone());
}
current_track += 1;
}
Ok(())
}
}
impl NavigatorScreen for ImportDiscDialog {
fn attach_navigator(&self, navigator: Rc<Navigator>) {
self.navigator.replace(Some(navigator));
}
fn get_widget(&self) -> gtk::Widget {
self.widget.clone().upcast()
}
fn detach_navigator(&self) {
self.navigator.replace(None);
}
}

View file

@ -1,88 +0,0 @@
use super::ImportDialog;
use crate::backend::Backend;
use crate::editors::TrackSource;
use crate::widgets::{Navigator, NavigatorScreen};
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use std::cell::RefCell;
use std::rc::Rc;
use std::path::Path;
/// The initial screen for importing a folder.
pub struct ImportFolderDialog {
backend: Rc<Backend>,
widget: gtk::Box,
navigator: RefCell<Option<Rc<Navigator>>>,
}
impl ImportFolderDialog {
/// Create a new import folderdialog.
pub fn new(backend: Rc<Backend>) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/import_folder_dialog.ui");
get_widget!(builder, gtk::Box, widget);
get_widget!(builder, gtk::Button, back_button);
get_widget!(builder, gtk::Button, import_button);
let this = Rc::new(Self {
backend,
widget,
navigator: RefCell::new(None),
});
// Connect signals and callbacks
back_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.pop();
}
}));
import_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
let chooser = gtk::FileChooserNative::new(
Some("Select folder"),
Some(&navigator.window),
gtk::FileChooserAction::SelectFolder,
None,
None,
);
chooser.connect_response(clone!(@strong this => move |chooser, response| {
if response == gtk::ResponseType::Accept {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
let path = chooser.get_filename().unwrap();
let source = TrackSource::folder(&path).unwrap();
let dialog = ImportDialog::new(this.backend.clone(), Rc::new(source));
navigator.push(dialog);
}
}
}));
chooser.run();
}
}));
this
}
}
impl NavigatorScreen for ImportFolderDialog {
fn attach_navigator(&self, navigator: Rc<Navigator>) {
self.navigator.replace(Some(navigator));
}
fn get_widget(&self) -> gtk::Widget {
self.widget.clone().upcast()
}
fn detach_navigator(&self) {
self.navigator.replace(None);
}
}

View file

@ -1,12 +1,3 @@
pub mod import;
pub use import::*;
pub mod import_folder;
pub use import_folder::*;
pub mod import_disc;
pub use import_disc::*;
pub mod about; pub mod about;
pub use about::*; pub use about::*;

View file

@ -10,12 +10,6 @@ pub use person::*;
pub mod recording; pub mod recording;
pub use recording::*; pub use recording::*;
pub mod track_set;
pub use track_set::*;
pub mod track_source;
pub use track_source::*;
pub mod work; pub mod work;
pub use work::*; pub use work::*;

View file

@ -0,0 +1,171 @@
use anyhow::{anyhow, bail, Result};
use discid::DiscId;
use futures_channel::oneshot;
use gstreamer::prelude::*;
use gstreamer::{Element, ElementFactory, Pipeline};
use std::cell::RefCell;
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: String,
/// The tracks on this disc.
pub tracks: Vec<TrackSource>,
}
/// 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 {
/// Try to create a new disc source by asynchronously reading the
/// information from the default disc drive.
pub async fn load() -> Result<Self> {
let (sender, receiver) = oneshot::channel();
thread::spawn(|| {
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
/// 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<Self> {
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 file_name = format!("track_{:02}.flac", number);
let mut path = tmp_dir.clone();
path.push(file_name);
let track = TrackSource {
number,
path,
};
tracks.push(track);
}
let disc = DiscSource {
discid: id,
tracks,
};
Ok(disc)
}
/// 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)
}
}

View file

View file

@ -0,0 +1,5 @@
mod disc_source;
mod medium_editor;
mod source_selector;
pub use source_selector::SourceSelector;

View file

@ -0,0 +1,85 @@
use super::disc_source::DiscSource;
use crate::backend::Backend;
use crate::widgets::{Navigator, NavigatorScreen};
use anyhow::Result;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use std::cell::RefCell;
use std::rc::Rc;
/// A dialog for starting to import music.
pub struct SourceSelector {
backend: Rc<Backend>,
widget: gtk::Box,
stack: gtk::Stack,
info_bar: gtk::InfoBar,
navigator: RefCell<Option<Rc<Navigator>>>,
}
impl SourceSelector {
/// Create a new source selector.
pub fn new(backend: Rc<Backend>) -> Rc<Self> {
// Create UI
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/source_selector.ui");
get_widget!(builder, gtk::Box, widget);
get_widget!(builder, gtk::Button, back_button);
get_widget!(builder, gtk::Stack, stack);
get_widget!(builder, gtk::InfoBar, info_bar);
get_widget!(builder, gtk::Button, import_button);
let this = Rc::new(Self {
backend,
widget,
stack,
info_bar,
navigator: RefCell::new(None),
});
// Connect signals and callbacks
back_button.connect_clicked(clone!(@strong this => move |_| {
let navigator = this.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.pop();
}
}));
import_button.connect_clicked(clone!(@strong this => move |_| {
this.stack.set_visible_child_name("loading");
let context = glib::MainContext::default();
let clone = this.clone();
context.spawn_local(async move {
match DiscSource::load().await {
Ok(disc) => {
println!("{:?}", disc);
clone.stack.set_visible_child_name("start");
}
Err(_) => {
clone.info_bar.set_revealed(true);
clone.stack.set_visible_child_name("start");
}
}
});
}));
this
}
}
impl NavigatorScreen for SourceSelector {
fn attach_navigator(&self, navigator: Rc<Navigator>) {
self.navigator.replace(Some(navigator));
}
fn get_widget(&self) -> gtk::Widget {
self.widget.clone().upcast()
}
fn detach_navigator(&self) {
self.navigator.replace(None);
}
}

View file

@ -12,11 +12,11 @@ use std::cell::RefCell;
use std::rc::Rc; use std::rc::Rc;
mod backend; mod backend;
mod ripper;
mod config; mod config;
mod database; mod database;
mod dialogs; mod dialogs;
mod editors; mod editors;
mod import;
mod player; mod player;
mod screens; mod screens;
mod selectors; mod selectors;

View file

@ -53,8 +53,6 @@ sources = files(
'database/thread.rs', 'database/thread.rs',
'database/works.rs', 'database/works.rs',
'dialogs/about.rs', 'dialogs/about.rs',
'dialogs/import_disc.rs',
'dialogs/import_folder.rs',
'dialogs/login_dialog.rs', 'dialogs/login_dialog.rs',
'dialogs/mod.rs', 'dialogs/mod.rs',
'dialogs/preferences.rs', 'dialogs/preferences.rs',
@ -95,7 +93,6 @@ sources = files(
'player.rs', 'player.rs',
'resources.rs', 'resources.rs',
'resources.rs.in', 'resources.rs.in',
'ripper.rs',
'window.rs', 'window.rs',
) )

View file

@ -1,123 +0,0 @@
use anyhow::{anyhow, bail, Result};
use discid::DiscId;
use futures_channel::oneshot;
use gstreamer::prelude::*;
use gstreamer::{Element, ElementFactory, Pipeline};
use std::cell::RefCell;
use std::thread;
/// A disc that can be ripped.
#[derive(Debug, Clone)]
pub struct RipDisc {
pub discid: String,
pub first_track: u32,
pub last_track: u32,
}
/// An interface for ripping an audio compact disc.
pub struct Ripper {
path: String,
disc: RefCell<Option<RipDisc>>,
}
impl Ripper {
/// Create a new ripper that stores its tracks within the specified folder.
pub fn new(path: &str) -> Self {
Self {
path: path.to_string(),
disc: RefCell::new(None),
}
}
/// Load the disc and return its metadata.
pub async fn load_disc(&self) -> Result<RipDisc> {
let (sender, receiver) = oneshot::channel();
thread::spawn(|| {
let disc = Self::load_disc_priv();
sender.send(disc).unwrap();
});
let disc = receiver.await??;
self.disc.replace(Some(disc.clone()));
Ok(disc)
}
/// Rip one track.
pub async fn rip_track(&self, track: u32) -> Result<()> {
let (sender, receiver) = oneshot::channel();
let path = self.path.clone();
thread::spawn(move || {
let result = Self::rip_track_priv(&path, track);
sender.send(result).unwrap();
});
receiver.await?
}
/// Load the disc and return its metadata.
fn load_disc_priv() -> Result<RipDisc> {
let discid = DiscId::read(None)?;
let id = discid.id();
let first_track = discid.first_track_num() as u32;
let last_track = discid.last_track_num() as u32;
let disc = RipDisc {
discid: id,
first_track,
last_track,
};
Ok(disc)
}
/// Rip one track.
fn rip_track_priv(path: &str, track: u32) -> Result<()> {
let pipeline = Self::build_pipeline(path, track)?;
let bus = pipeline
.get_bus()
.ok_or(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: &str, track: u32) -> Result<Pipeline> {
let cdparanoiasrc = ElementFactory::make("cdparanoiasrc", None)?;
cdparanoiasrc.set_property("track", &track)?;
let queue = ElementFactory::make("queue", None)?;
let audioconvert = ElementFactory::make("audioconvert", None)?;
let flacenc = ElementFactory::make("flacenc", None)?;
let filesink = gstreamer::ElementFactory::make("filesink", None)?;
filesink.set_property("location", &format!("{}/track_{:02}.flac", path, track))?;
let pipeline = gstreamer::Pipeline::new(None);
pipeline.add_many(&[&cdparanoiasrc, &queue, &audioconvert, &flacenc, &filesink])?;
Element::link_many(&[&cdparanoiasrc, &queue, &audioconvert, &flacenc, &filesink])?;
Ok(pipeline)
}
}

View file

@ -1,5 +1,6 @@
use crate::backend::*; use crate::backend::*;
use crate::dialogs::*; use crate::dialogs::*;
use crate::import::SourceSelector;
use crate::screens::*; use crate::screens::*;
use crate::widgets::*; use crate::widgets::*;
use futures::prelude::*; use futures::prelude::*;
@ -93,7 +94,7 @@ impl Window {
// let window = NavigatorWindow::new(editor); // let window = NavigatorWindow::new(editor);
// window.show(); // window.show();
let dialog = ImportFolderDialog::new(result.backend.clone()); let dialog = SourceSelector::new(result.backend.clone());
let window = NavigatorWindow::new(dialog); let window = NavigatorWindow::new(dialog);
window.show(); window.show();
})); }));
@ -110,15 +111,15 @@ impl Window {
result.stack.set_visible_child_name("content"); result.stack.set_visible_child_name("content");
})); }));
action!( // action!(
result.window, // result.window,
"import-disc", // "import-disc",
clone!(@strong result => move |_, _| { // clone!(@strong result => move |_, _| {
let dialog = ImportDiscDialog::new(result.backend.clone()); // let dialog = ImportDiscDialog::new(result.backend.clone());
let window = NavigatorWindow::new(dialog); // let window = NavigatorWindow::new(dialog);
window.show(); // window.show();
}) // })
); // );
action!( action!(
result.window, result.window,