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_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> | ||||||
|  |  | ||||||
|  | @ -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"?> | <?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> | ||||||
|  | @ -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 mod about; | ||||||
| pub use about::*; | pub use about::*; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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::*; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										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; | 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; | ||||||
|  |  | ||||||
|  | @ -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', | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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::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, | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Elias Projahn
						Elias Projahn