mirror of
https://github.com/johrpan/musicus.git
synced 2025-10-26 19:57:25 +01:00
Initial ripping from new source selector
This commit is contained in:
parent
4aa858602d
commit
18600c310f
17 changed files with 277 additions and 688 deletions
|
|
@ -4,12 +4,10 @@
|
|||
<file preprocess="xml-stripblanks">ui/ensemble_editor.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/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_selector.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/person_editor.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/selector.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_selector.ui</file>
|
||||
<file preprocess="xml-stripblanks">ui/track_set_editor.ui</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>
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.38.1 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.24"/>
|
||||
<requires lib="libhandy" version="1.0"/>
|
||||
|
|
@ -11,7 +10,7 @@
|
|||
<object class="HdyHeaderBar">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="title" translatable="yes">Import CD</property>
|
||||
<property name="title" translatable="yes">Import music</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="back_button">
|
||||
<property name="visible">True</property>
|
||||
|
|
@ -27,11 +26,6 @@
|
|||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkStack" id="stack">
|
||||
|
|
@ -49,21 +43,6 @@
|
|||
<property name="can-focus">False</property>
|
||||
<property name="message-type">error</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">
|
||||
<object class="GtkBox">
|
||||
<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="wrap">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="vexpand">True</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="border-width">18</property>
|
||||
|
|
@ -193,55 +155,7 @@
|
|||
<property name="position">1</property>
|
||||
</packing>
|
||||
</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>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 use about::*;
|
||||
|
||||
|
|
|
|||
|
|
@ -10,12 +10,6 @@ pub use person::*;
|
|||
pub mod recording;
|
||||
pub use recording::*;
|
||||
|
||||
pub mod track_set;
|
||||
pub use track_set::*;
|
||||
|
||||
pub mod track_source;
|
||||
pub use track_source::*;
|
||||
|
||||
pub mod work;
|
||||
pub use work::*;
|
||||
|
||||
|
|
|
|||
171
musicus/src/import/disc_source.rs
Normal file
171
musicus/src/import/disc_source.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
0
musicus/src/import/medium_editor.rs
Normal file
0
musicus/src/import/medium_editor.rs
Normal file
5
musicus/src/import/mod.rs
Normal file
5
musicus/src/import/mod.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
mod disc_source;
|
||||
mod medium_editor;
|
||||
mod source_selector;
|
||||
|
||||
pub use source_selector::SourceSelector;
|
||||
85
musicus/src/import/source_selector.rs
Normal file
85
musicus/src/import/source_selector.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -12,11 +12,11 @@ use std::cell::RefCell;
|
|||
use std::rc::Rc;
|
||||
|
||||
mod backend;
|
||||
mod ripper;
|
||||
mod config;
|
||||
mod database;
|
||||
mod dialogs;
|
||||
mod editors;
|
||||
mod import;
|
||||
mod player;
|
||||
mod screens;
|
||||
mod selectors;
|
||||
|
|
|
|||
|
|
@ -53,8 +53,6 @@ sources = files(
|
|||
'database/thread.rs',
|
||||
'database/works.rs',
|
||||
'dialogs/about.rs',
|
||||
'dialogs/import_disc.rs',
|
||||
'dialogs/import_folder.rs',
|
||||
'dialogs/login_dialog.rs',
|
||||
'dialogs/mod.rs',
|
||||
'dialogs/preferences.rs',
|
||||
|
|
@ -95,7 +93,6 @@ sources = files(
|
|||
'player.rs',
|
||||
'resources.rs',
|
||||
'resources.rs.in',
|
||||
'ripper.rs',
|
||||
'window.rs',
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
use crate::backend::*;
|
||||
use crate::dialogs::*;
|
||||
use crate::import::SourceSelector;
|
||||
use crate::screens::*;
|
||||
use crate::widgets::*;
|
||||
use futures::prelude::*;
|
||||
|
|
@ -93,7 +94,7 @@ impl Window {
|
|||
// let window = NavigatorWindow::new(editor);
|
||||
// window.show();
|
||||
|
||||
let dialog = ImportFolderDialog::new(result.backend.clone());
|
||||
let dialog = SourceSelector::new(result.backend.clone());
|
||||
let window = NavigatorWindow::new(dialog);
|
||||
window.show();
|
||||
}));
|
||||
|
|
@ -110,15 +111,15 @@ impl Window {
|
|||
result.stack.set_visible_child_name("content");
|
||||
}));
|
||||
|
||||
action!(
|
||||
result.window,
|
||||
"import-disc",
|
||||
clone!(@strong result => move |_, _| {
|
||||
let dialog = ImportDiscDialog::new(result.backend.clone());
|
||||
let window = NavigatorWindow::new(dialog);
|
||||
window.show();
|
||||
})
|
||||
);
|
||||
// action!(
|
||||
// result.window,
|
||||
// "import-disc",
|
||||
// clone!(@strong result => move |_, _| {
|
||||
// let dialog = ImportDiscDialog::new(result.backend.clone());
|
||||
// let window = NavigatorWindow::new(dialog);
|
||||
// window.show();
|
||||
// })
|
||||
// );
|
||||
|
||||
action!(
|
||||
result.window,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue