mirror of
				https://github.com/johrpan/musicus.git
				synced 2025-10-26 11:47:25 +01:00 
			
		
		
		
	Add support for importing an audio CD
This commit is contained in:
		
							parent
							
								
									e2d36a88b8
								
							
						
					
					
						commit
						1bc79765be
					
				
					 21 changed files with 1190 additions and 203 deletions
				
			
		|  | @ -7,6 +7,7 @@ edition = "2018" | |||
| anyhow = "1.0.33" | ||||
| diesel = { version = "1.4.5", features = ["sqlite"] } | ||||
| diesel_migrations = "1.4.0" | ||||
| discid = "0.4.4" | ||||
| fragile = "1.0.0" | ||||
| futures = "0.3.6" | ||||
| futures-channel = "0.3.5" | ||||
|  |  | |||
|  | @ -1,19 +1,16 @@ | |||
| DROP TABLE persons; | ||||
| PRAGMA defer_foreign_keys; | ||||
| 
 | ||||
| DROP TABLE instruments; | ||||
| DROP TABLE "persons"; | ||||
| DROP TABLE "instruments"; | ||||
| DROP TABLE "works"; | ||||
| DROP TABLE "instrumentations"; | ||||
| DROP TABLE "work_parts"; | ||||
| DROP TABLE "work_sections"; | ||||
| DROP TABLE "ensembles"; | ||||
| DROP TABLE "recordings"; | ||||
| DROP TABLE "performances"; | ||||
| DROP TABLE "mediums"; | ||||
| DROP TABLE "track_sets"; | ||||
| DROP TABLE "tracks"; | ||||
| DROP TABLE "files"; | ||||
| 
 | ||||
| DROP TABLE works; | ||||
| 
 | ||||
| DROP TABLE instrumentations; | ||||
| 
 | ||||
| DROP TABLE work_parts; | ||||
| 
 | ||||
| DROP TABLE work_sections; | ||||
| 
 | ||||
| DROP TABLE ensembles; | ||||
| 
 | ||||
| DROP TABLE recordings; | ||||
| 
 | ||||
| DROP TABLE performances; | ||||
| 
 | ||||
| DROP TABLE tracks; | ||||
|  | @ -1,64 +1,82 @@ | |||
| CREATE TABLE persons ( | ||||
|     id TEXT NOT NULL PRIMARY KEY, | ||||
|     first_name TEXT NOT NULL, | ||||
|     last_name TEXT NOT NULL | ||||
| CREATE TABLE "persons" ( | ||||
|     "id" TEXT NOT NULL PRIMARY KEY, | ||||
|     "first_name" TEXT NOT NULL, | ||||
|     "last_name" TEXT NOT NULL | ||||
| ); | ||||
| 
 | ||||
| CREATE TABLE instruments ( | ||||
|     id TEXT NOT NULL PRIMARY KEY, | ||||
|     name TEXT NOT NULL | ||||
| CREATE TABLE "instruments" ( | ||||
|     "id" TEXT NOT NULL PRIMARY KEY, | ||||
|     "name" TEXT NOT NULL | ||||
| ); | ||||
| 
 | ||||
| CREATE TABLE works ( | ||||
|     id TEXT NOT NULL PRIMARY KEY, | ||||
|     composer TEXT NOT NULL REFERENCES persons(id), | ||||
|     title TEXT NOT NULL | ||||
| CREATE TABLE "works" ( | ||||
|     "id" TEXT NOT NULL PRIMARY KEY, | ||||
|     "composer" TEXT NOT NULL REFERENCES "persons"("id"), | ||||
|     "title" TEXT NOT NULL | ||||
| ); | ||||
| 
 | ||||
| CREATE TABLE instrumentations ( | ||||
|     id BIGINT NOT NULL PRIMARY KEY, | ||||
|     work TEXT NOT NULL REFERENCES works(id) ON DELETE CASCADE, | ||||
|     instrument TEXT NOT NULL REFERENCES instruments(id) ON DELETE CASCADE | ||||
| CREATE TABLE "instrumentations" ( | ||||
|     "id" BIGINT NOT NULL PRIMARY KEY, | ||||
|     "work" TEXT NOT NULL REFERENCES "works"("id") ON DELETE CASCADE, | ||||
|     "instrument" TEXT NOT NULL REFERENCES "instruments"("id") ON DELETE CASCADE | ||||
| ); | ||||
| 
 | ||||
| CREATE TABLE work_parts ( | ||||
|     id BIGINT NOT NULL PRIMARY KEY, | ||||
|     work TEXT NOT NULL REFERENCES works(id) ON DELETE CASCADE, | ||||
|     part_index BIGINT NOT NULL, | ||||
|     title TEXT NOT NULL, | ||||
|     composer TEXT REFERENCES persons(id) | ||||
| CREATE TABLE "work_parts" ( | ||||
|     "id" BIGINT NOT NULL PRIMARY KEY, | ||||
|     "work" TEXT NOT NULL REFERENCES "works"("id") ON DELETE CASCADE, | ||||
|     "part_index" BIGINT NOT NULL, | ||||
|     "title" TEXT NOT NULL, | ||||
|     "composer" TEXT REFERENCES "persons"("id") | ||||
| ); | ||||
| 
 | ||||
| CREATE TABLE work_sections ( | ||||
|     id BIGINT NOT NULL PRIMARY KEY, | ||||
|     work TEXT NOT NULL REFERENCES works(id) ON DELETE CASCADE, | ||||
|     title TEXT NOT NULL, | ||||
|     before_index BIGINT NOT NULL | ||||
| CREATE TABLE "work_sections" ( | ||||
|     "id" BIGINT NOT NULL PRIMARY KEY, | ||||
|     "work" TEXT NOT NULL REFERENCES "works"("id") ON DELETE CASCADE, | ||||
|     "title" TEXT NOT NULL, | ||||
|     "before_index" BIGINT NOT NULL | ||||
| ); | ||||
| 
 | ||||
| CREATE TABLE ensembles ( | ||||
|     id TEXT NOT NULL PRIMARY KEY, | ||||
|     name TEXT NOT NULL | ||||
| CREATE TABLE "ensembles" ( | ||||
|     "id" TEXT NOT NULL PRIMARY KEY, | ||||
|     "name" TEXT NOT NULL | ||||
| ); | ||||
| 
 | ||||
| CREATE TABLE recordings ( | ||||
|     id TEXT NOT NULL PRIMARY KEY, | ||||
|     work TEXT NOT NULL REFERENCES works(id), | ||||
|     comment TEXT NOT NULL | ||||
| CREATE TABLE "recordings" ( | ||||
|     "id" TEXT NOT NULL PRIMARY KEY, | ||||
|     "work" TEXT NOT NULL REFERENCES "works"("id"), | ||||
|     "comment" TEXT NOT NULL | ||||
| ); | ||||
| 
 | ||||
| CREATE TABLE performances ( | ||||
|     id BIGINT NOT NULL PRIMARY KEY, | ||||
|     recording TEXT NOT NULL REFERENCES recordings(id) ON DELETE CASCADE, | ||||
|     person TEXT REFERENCES persons(id), | ||||
|     ensemble TEXT REFERENCES ensembles(id), | ||||
|     role TEXT REFERENCES instruments(id) | ||||
| CREATE TABLE "performances" ( | ||||
|     "id" BIGINT NOT NULL PRIMARY KEY, | ||||
|     "recording" TEXT NOT NULL REFERENCES "recordings"("id") ON DELETE CASCADE, | ||||
|     "person" TEXT REFERENCES "persons"("id"), | ||||
|     "ensemble" TEXT REFERENCES "ensembles"("id"), | ||||
|     "role" TEXT REFERENCES "instruments"("id") | ||||
| ); | ||||
| 
 | ||||
| CREATE TABLE "mediums" ( | ||||
|     "id" TEXT NOT NULL PRIMARY KEY, | ||||
|     "name" TEXT NOT NULL, | ||||
|     "discid" TEXT | ||||
| ); | ||||
| 
 | ||||
| CREATE TABLE "track_sets" ( | ||||
|     "id" TEXT NOT NULL PRIMARY KEY, | ||||
|     "medium" TEXT NOT NULL REFERENCES "mediums"("id") ON DELETE CASCADE, | ||||
|     "index" INTEGER NOT NULL, | ||||
|     "recording" TEXT NOT NULL REFERENCES "recordings"("id") | ||||
| ); | ||||
| 
 | ||||
| CREATE TABLE "tracks" ( | ||||
|     "id" TEXT NOT NULL PRIMARY KEY, | ||||
|     "track_set" TEXT NOT NULL REFERENCES "track_sets"("id") ON DELETE CASCADE, | ||||
|     "index" INTEGER NOT NULL, | ||||
|     "work_parts" TEXT NOT NULL | ||||
| ); | ||||
| 
 | ||||
| CREATE TABLE "files" ( | ||||
|     "file_name" TEXT NOT NULL PRIMARY KEY, | ||||
|     "track" TEXT NOT NULL REFERENCES "tracks"("id") | ||||
| ); | ||||
| 
 | ||||
| CREATE TABLE tracks ( | ||||
|     id BIGINT NOT NULL PRIMARY KEY, | ||||
|     file_name TEXT NOT NULL, | ||||
|     recording TEXT NOT NULL REFERENCES recordings(id), | ||||
|     track_index INTEGER NOT NULL, | ||||
|     work_parts TEXT NOT NULL | ||||
| ); | ||||
|  | @ -2,8 +2,13 @@ | |||
| <gresources> | ||||
|     <gresource prefix="/de/johrpan/musicus"> | ||||
|         <file preprocess="xml-stripblanks">ui/ensemble_editor.ui</file> | ||||
|         <file preprocess="xml-stripblanks">ui/ensemble_selector.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_disc_dialog.ui</file> | ||||
| <<<<<<< HEAD | ||||
|         <file preprocess="xml-stripblanks">ui/import_folder_dialog.ui</file> | ||||
| ======= | ||||
| >>>>>>> wip/cd-ripping-old | ||||
|         <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> | ||||
|  |  | |||
							
								
								
									
										247
									
								
								musicus/res/ui/import_disc_dialog.ui
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										247
									
								
								musicus/res/ui/import_disc_dialog.ui
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,247 @@ | |||
| <?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"/> | ||||
|   <object class="GtkBox" id="widget"> | ||||
|     <property name="visible">True</property> | ||||
|     <property name="can-focus">False</property> | ||||
|     <property name="orientation">vertical</property> | ||||
|     <child> | ||||
|       <object class="HdyHeaderBar"> | ||||
|         <property name="visible">True</property> | ||||
|         <property name="can-focus">False</property> | ||||
|         <property name="title" translatable="yes">Import CD</property> | ||||
|         <child> | ||||
|           <object class="GtkButton" id="back_button"> | ||||
|             <property name="visible">True</property> | ||||
|             <property name="can-focus">True</property> | ||||
|             <property name="receives-default">True</property> | ||||
|             <child> | ||||
|               <object class="GtkImage"> | ||||
|                 <property name="visible">True</property> | ||||
|                 <property name="can-focus">False</property> | ||||
|                 <property name="icon-name">go-previous-symbolic</property> | ||||
|               </object> | ||||
|             </child> | ||||
|           </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"> | ||||
|         <property name="visible">True</property> | ||||
|         <property name="can-focus">False</property> | ||||
|         <property name="transition-type">crossfade</property> | ||||
|         <child> | ||||
|           <object class="GtkBox"> | ||||
|             <property name="visible">True</property> | ||||
|             <property name="can-focus">False</property> | ||||
|             <property name="orientation">vertical</property> | ||||
|             <child> | ||||
|               <object class="GtkInfoBar" id="info_bar"> | ||||
|                 <property name="visible">True</property> | ||||
|                 <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> | ||||
|                     <property name="spacing">16</property> | ||||
|                     <child> | ||||
|                       <object class="GtkLabel"> | ||||
|                         <property name="visible">True</property> | ||||
|                         <property name="can-focus">False</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> | ||||
|                       </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="halign">center</property> | ||||
|                 <property name="valign">center</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="can-focus">False</property> | ||||
|                     <property name="opacity">0.5019607843137255</property> | ||||
|                     <property name="pixel-size">80</property> | ||||
|                     <property name="icon-name">media-optical-cd-audio-symbolic</property> | ||||
|                   </object> | ||||
|                   <packing> | ||||
|                     <property name="expand">False</property> | ||||
|                     <property name="fill">True</property> | ||||
|                     <property name="position">0</property> | ||||
|                   </packing> | ||||
|                 </child> | ||||
|                 <child> | ||||
|                   <object class="GtkLabel"> | ||||
|                     <property name="visible">True</property> | ||||
|                     <property name="can-focus">False</property> | ||||
|                     <property name="opacity">0.5019607843137255</property> | ||||
|                     <property name="label" translatable="yes">Import from audio CD</property> | ||||
|                     <attributes> | ||||
|                       <attribute name="size" value="16384"/> | ||||
|                     </attributes> | ||||
|                   </object> | ||||
|                   <packing> | ||||
|                     <property name="expand">False</property> | ||||
|                     <property name="fill">True</property> | ||||
|                     <property name="position">1</property> | ||||
|                   </packing> | ||||
|                 </child> | ||||
|                 <child> | ||||
|                   <object class="GtkLabel"> | ||||
|                     <property name="visible">True</property> | ||||
|                     <property name="can-focus">False</property> | ||||
|                     <property name="opacity">0.5019607843137255</property> | ||||
|                     <property name="label" translatable="yes">Insert an audio compact disc into your drive and click the button below. The disc will be copied in the background while you set up the metadata.</property> | ||||
|                     <property name="justify">center</property> | ||||
|                     <property name="wrap">True</property> | ||||
|                     <property name="max-width-chars">40</property> | ||||
|                   </object> | ||||
|                   <packing> | ||||
|                     <property name="expand">False</property> | ||||
|                     <property name="fill">True</property> | ||||
|                     <property name="position">2</property> | ||||
|                   </packing> | ||||
|                 </child> | ||||
|                 <child> | ||||
|                   <object class="GtkButton" id="import_button"> | ||||
|                     <property name="label" translatable="yes">Import</property> | ||||
|                     <property name="visible">True</property> | ||||
|                     <property name="can-focus">True</property> | ||||
|                     <property name="receives-default">True</property> | ||||
|                     <property name="halign">center</property> | ||||
|                     <style> | ||||
|                       <class name="suggested-action"/> | ||||
|                     </style> | ||||
|                   </object> | ||||
|                   <packing> | ||||
|                     <property name="expand">False</property> | ||||
|                     <property name="fill">True</property> | ||||
|                     <property name="position">3</property> | ||||
|                   </packing> | ||||
|                 </child> | ||||
|               </object> | ||||
|               <packing> | ||||
|                 <property name="expand">True</property> | ||||
|                 <property name="fill">True</property> | ||||
|                 <property name="position">1</property> | ||||
|               </packing> | ||||
|             </child> | ||||
|           </object> | ||||
|           <packing> | ||||
|             <property name="name">start</property> | ||||
|           </packing> | ||||
|         </child> | ||||
|         <child> | ||||
|           <object class="GtkSpinner"> | ||||
|             <property name="visible">True</property> | ||||
|             <property name="can-focus">False</property> | ||||
|             <property name="active">True</property> | ||||
|           </object> | ||||
|           <packing> | ||||
|             <property name="name">loading</property> | ||||
|             <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> | ||||
							
								
								
									
										117
									
								
								musicus/res/ui/import_folder_dialg.ui
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								musicus/res/ui/import_folder_dialg.ui
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,117 @@ | |||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <!-- Generated with glade 3.38.2 --> | ||||
| <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="can-focus">False</property> | ||||
|     <property name="orientation">vertical</property> | ||||
|     <child> | ||||
|       <object class="HdyHeaderBar"> | ||||
|         <property name="visible">True</property> | ||||
|         <property name="can-focus">False</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> | ||||
|             <property name="receives-default">True</property> | ||||
|             <child> | ||||
|               <object class="GtkImage"> | ||||
|                 <property name="visible">True</property> | ||||
|                 <property name="can-focus">False</property> | ||||
|                 <property name="icon-name">go-previous-symbolic</property> | ||||
|               </object> | ||||
|             </child> | ||||
|           </object> | ||||
|         </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="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="can-focus">False</property> | ||||
|             <property name="opacity">0.50196078431372548</property> | ||||
|             <property name="pixel-size">80</property> | ||||
|             <property name="icon-name">folder-symbolic</property> | ||||
|           </object> | ||||
|           <packing> | ||||
|             <property name="expand">False</property> | ||||
|             <property name="fill">True</property> | ||||
|             <property name="position">0</property> | ||||
|           </packing> | ||||
|         </child> | ||||
|         <child> | ||||
|           <object class="GtkLabel"> | ||||
|             <property name="visible">True</property> | ||||
|             <property name="can-focus">False</property> | ||||
|             <property name="opacity">0.50196078431372548</property> | ||||
|             <property name="label" translatable="yes">Import from a folder</property> | ||||
|             <attributes> | ||||
|               <attribute name="size" value="16384"/> | ||||
|             </attributes> | ||||
|           </object> | ||||
|           <packing> | ||||
|             <property name="expand">False</property> | ||||
|             <property name="fill">True</property> | ||||
|             <property name="position">1</property> | ||||
|           </packing> | ||||
|         </child> | ||||
|         <child> | ||||
|           <object class="GtkLabel"> | ||||
|             <property name="visible">True</property> | ||||
|             <property name="can-focus">False</property> | ||||
|             <property name="opacity">0.50196078431372548</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> | ||||
|           <packing> | ||||
|             <property name="expand">False</property> | ||||
|             <property name="fill">True</property> | ||||
|             <property name="position">2</property> | ||||
|           </packing> | ||||
|         </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="receives-default">True</property> | ||||
|             <property name="halign">center</property> | ||||
|             <style> | ||||
|               <class name="suggested-action"/> | ||||
|             </style> | ||||
|           </object> | ||||
|           <packing> | ||||
|             <property name="expand">False</property> | ||||
|             <property name="fill">True</property> | ||||
|             <property name="position">3</property> | ||||
|           </packing> | ||||
|         </child> | ||||
|       </object> | ||||
|       <packing> | ||||
|         <property name="expand">False</property> | ||||
|         <property name="fill">True</property> | ||||
|         <property name="position">1</property> | ||||
|       </packing> | ||||
|     </child> | ||||
|   </object> | ||||
| </interface> | ||||
|  | @ -327,6 +327,10 @@ | |||
|   </object> | ||||
|   <menu id="menu"> | ||||
|     <section> | ||||
|     <item> | ||||
|         <attribute name="label" translatable="yes">Import CD</attribute> | ||||
|         <attribute name="action">win.import-disc</attribute> | ||||
|       </item> | ||||
|       <item> | ||||
|         <attribute name="label" translatable="yes">Preferences</attribute> | ||||
|         <attribute name="action">win.preferences</attribute> | ||||
|  |  | |||
							
								
								
									
										54
									
								
								musicus/src/database/files.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								musicus/src/database/files.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,54 @@ | |||
| use super::schema::files; | ||||
| use super::Database; | ||||
| use anyhow::Result; | ||||
| use diesel::prelude::*; | ||||
| 
 | ||||
| /// Table data to associate audio files with tracks.
 | ||||
| #[derive(Insertable, Queryable, Debug, Clone)] | ||||
| #[table_name = "files"] | ||||
| struct FileRow { | ||||
|     pub file_name: String, | ||||
|     pub track: String, | ||||
| } | ||||
| 
 | ||||
| impl Database { | ||||
|     /// Insert or update a file. This assumes that the track is already in the
 | ||||
|     /// database.
 | ||||
|     pub fn update_file(&self, file_name: &str, track_id: &str) -> Result<()> { | ||||
|         let row = FileRow { | ||||
|             file_name: file_name.to_owned(), | ||||
|             track: track_id.to_owned(), | ||||
|         }; | ||||
| 
 | ||||
|         diesel::insert_into(files::table) | ||||
|             .values(row) | ||||
|             .execute(&self.connection)?; | ||||
| 
 | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     /// Delete an existing file. This will not delete the file from the file
 | ||||
|     /// system but just the representing row from the database.
 | ||||
|     pub fn delete_file(&self, file_name: &str) -> Result<()> { | ||||
|         diesel::delete(files::table.filter(files::file_name.eq(file_name))) | ||||
|             .execute(&self.connection)?; | ||||
| 
 | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     /// Get the file name of the audio file for the specified track.
 | ||||
|     pub fn get_file(&self, track_id: &str) -> Result<Option<String>> { | ||||
|         let row = files::table | ||||
|             .filter(files::track.eq(track_id)) | ||||
|             .load::<FileRow>(&self.connection)? | ||||
|             .into_iter() | ||||
|             .next(); | ||||
| 
 | ||||
|         let file_name = match row { | ||||
|             Some(row) => Some(row.file_name), | ||||
|             None => None, | ||||
|         }; | ||||
| 
 | ||||
|         Ok(file_name) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										187
									
								
								musicus/src/database/medium.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										187
									
								
								musicus/src/database/medium.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,187 @@ | |||
| use super::generate_id; | ||||
| use super::schema::{mediums, track_sets, tracks}; | ||||
| use super::{Database, Recording}; | ||||
| use anyhow::{anyhow, Error, Result}; | ||||
| use diesel::prelude::*; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| 
 | ||||
| /// Representation of someting like a physical audio disc or a folder with
 | ||||
| /// audio files (i.e. a collection of tracks for one or more recordings).
 | ||||
| #[derive(Serialize, Deserialize, Debug, Clone)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct Medium { | ||||
|     pub id: String, | ||||
|     pub name: String, | ||||
|     pub discid: Option<String>, | ||||
|     pub tracks: Vec<TrackSet>, | ||||
| } | ||||
| 
 | ||||
| /// A set of tracks of one recording within a medium.
 | ||||
| #[derive(Serialize, Deserialize, Debug, Clone)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct TrackSet { | ||||
|     pub recording: Recording, | ||||
|     pub tracks: Vec<Track>, | ||||
| } | ||||
| 
 | ||||
| /// A track within a recording on a medium.
 | ||||
| #[derive(Serialize, Deserialize, Debug, Clone)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct Track { | ||||
|     work_parts: Vec<usize>, | ||||
| } | ||||
| 
 | ||||
| /// Table data for a [`Medium`].
 | ||||
| #[derive(Insertable, Queryable, Debug, Clone)] | ||||
| #[table_name = "mediums"] | ||||
| struct MediumRow { | ||||
|     pub id: String, | ||||
|     pub name: String, | ||||
|     pub discid: Option<String>, | ||||
| } | ||||
| 
 | ||||
| /// Table data for a [`TrackSet`].
 | ||||
| #[derive(Insertable, Queryable, Debug, Clone)] | ||||
| #[table_name = "track_sets"] | ||||
| struct TrackSetRow { | ||||
|     pub id: String, | ||||
|     pub medium: String, | ||||
|     pub index: i32, | ||||
|     pub recording: String, | ||||
| } | ||||
| 
 | ||||
| /// Table data for a [`Track`].
 | ||||
| #[derive(Insertable, Queryable, Debug, Clone)] | ||||
| #[table_name = "tracks"] | ||||
| struct TrackRow { | ||||
|     pub id: String, | ||||
|     pub track_set: String, | ||||
|     pub index: i32, | ||||
|     pub work_parts: String, | ||||
| } | ||||
| 
 | ||||
| impl Database { | ||||
|     /// Update an existing medium or insert a new one.
 | ||||
|     pub fn update_medium(&self, medium: Medium) -> Result<()> { | ||||
|         self.defer_foreign_keys()?; | ||||
| 
 | ||||
|         self.connection.transaction::<(), Error, _>(|| { | ||||
|             let medium_id = &medium.id; | ||||
| 
 | ||||
|             // This will also delete the track sets and tracks.
 | ||||
|             self.delete_medium(medium_id)?; | ||||
| 
 | ||||
|             for (index, track_set) in medium.tracks.iter().enumerate() { | ||||
|                 let track_set_id = generate_id(); | ||||
| 
 | ||||
|                 let track_set_row = TrackSetRow { | ||||
|                     id: track_set_id.clone(), | ||||
|                     medium: medium_id.to_owned(), | ||||
|                     index: index as i32, | ||||
|                     recording: track_set.recording.id.clone(), | ||||
|                 }; | ||||
| 
 | ||||
|                 diesel::insert_into(track_sets::table) | ||||
|                     .values(track_set_row) | ||||
|                     .execute(&self.connection)?; | ||||
| 
 | ||||
|                 for (index, track) in track_set.tracks.iter().enumerate() { | ||||
|                     let work_parts = track | ||||
|                         .work_parts | ||||
|                         .iter() | ||||
|                         .map(|part_index| part_index.to_string()) | ||||
|                         .collect::<Vec<String>>() | ||||
|                         .join(","); | ||||
| 
 | ||||
|                     let track_row = TrackRow { | ||||
|                         id: generate_id(), | ||||
|                         track_set: track_set_id.clone(), | ||||
|                         index: index as i32, | ||||
|                         work_parts, | ||||
|                     }; | ||||
| 
 | ||||
|                     diesel::insert_into(tracks::table) | ||||
|                         .values(track_row) | ||||
|                         .execute(&self.connection)?; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             Ok(()) | ||||
|         })?; | ||||
| 
 | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     /// Get an existing medium.
 | ||||
|     pub fn get_medium(&self, id: &str) -> Result<Option<Medium>> { | ||||
|         let row = mediums::table | ||||
|             .filter(mediums::id.eq(id)) | ||||
|             .load::<MediumRow>(&self.connection)? | ||||
|             .into_iter() | ||||
|             .next(); | ||||
| 
 | ||||
|         let medium = match row { | ||||
|             Some(row) => Some(self.get_medium_data(row)?), | ||||
|             None => None, | ||||
|         }; | ||||
| 
 | ||||
|         Ok(medium) | ||||
|     } | ||||
| 
 | ||||
|     /// Delete a medium and all of its tracks. This will fail, if the music
 | ||||
|     /// library contains audio files referencing any of those tracks.
 | ||||
|     pub fn delete_medium(&self, id: &str) -> Result<()> { | ||||
|         diesel::delete(mediums::table.filter(mediums::id.eq(id))).execute(&self.connection)?; | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     /// Retrieve all available information on a medium from related tables.
 | ||||
|     fn get_medium_data(&self, row: MediumRow) -> Result<Medium> { | ||||
|         let track_set_rows = track_sets::table | ||||
|             .filter(track_sets::medium.eq(&row.id)) | ||||
|             .order_by(track_sets::index) | ||||
|             .load::<TrackSetRow>(&self.connection)?; | ||||
| 
 | ||||
|         let mut track_sets = Vec::new(); | ||||
| 
 | ||||
|         for track_set_row in track_set_rows { | ||||
|             let recording_id = &track_set_row.recording; | ||||
| 
 | ||||
|             let recording = self | ||||
|                 .get_recording(recording_id)? | ||||
|                 .ok_or_else(|| anyhow!("No recording with ID: {}", recording_id))?; | ||||
| 
 | ||||
|             let track_rows = tracks::table | ||||
|                 .filter(tracks::id.eq(&track_set_row.id)) | ||||
|                 .order_by(tracks::index) | ||||
|                 .load::<TrackRow>(&self.connection)?; | ||||
| 
 | ||||
|             let mut tracks = Vec::new(); | ||||
| 
 | ||||
|             for track_row in track_rows { | ||||
|                 let work_parts = track_row | ||||
|                     .work_parts | ||||
|                     .split(',') | ||||
|                     .map(|part_index| Ok(str::parse(part_index)?)) | ||||
|                     .collect::<Result<Vec<usize>>>()?; | ||||
| 
 | ||||
|                 let track = Track { work_parts }; | ||||
| 
 | ||||
|                 tracks.push(track); | ||||
|             } | ||||
| 
 | ||||
|             let track_set = TrackSet { recording, tracks }; | ||||
| 
 | ||||
|             track_sets.push(track_set); | ||||
|         } | ||||
| 
 | ||||
|         let medium = Medium { | ||||
|             id: row.id, | ||||
|             name: row.name, | ||||
|             discid: row.discid, | ||||
|             tracks: track_sets, | ||||
|         }; | ||||
| 
 | ||||
|         Ok(medium) | ||||
|     } | ||||
| } | ||||
|  | @ -7,6 +7,9 @@ pub use ensembles::*; | |||
| pub mod instruments; | ||||
| pub use instruments::*; | ||||
| 
 | ||||
| pub mod medium; | ||||
| pub use medium::*; | ||||
| 
 | ||||
| pub mod persons; | ||||
| pub use persons::*; | ||||
| 
 | ||||
|  | @ -16,8 +19,8 @@ pub use recordings::*; | |||
| pub mod thread; | ||||
| pub use thread::*; | ||||
| 
 | ||||
| pub mod tracks; | ||||
| pub use tracks::*; | ||||
| pub mod files; | ||||
| pub use files::*; | ||||
| 
 | ||||
| pub mod works; | ||||
| pub use works::*; | ||||
|  |  | |||
|  | @ -190,6 +190,22 @@ impl Database { | |||
|         Ok(exists) | ||||
|     } | ||||
| 
 | ||||
|     /// Get an existing recording.
 | ||||
|     pub fn get_recording(&self, id: &str) -> Result<Option<Recording>> { | ||||
|         let row = recordings::table | ||||
|             .filter(recordings::id.eq(id)) | ||||
|             .load::<RecordingRow>(&self.connection)? | ||||
|             .into_iter() | ||||
|             .next(); | ||||
| 
 | ||||
|         let recording = match row { | ||||
|             Some(row) => Some(self.get_recording_data(row)?), | ||||
|             None => None, | ||||
|         }; | ||||
| 
 | ||||
|         Ok(recording) | ||||
|     } | ||||
| 
 | ||||
|     /// Retrieve all available information on a recording from related tables.
 | ||||
|     fn get_recording_data(&self, row: RecordingRow) -> Result<Recording> { | ||||
|         let mut performance_descriptions: Vec<Performance> = Vec::new(); | ||||
|  |  | |||
|  | @ -5,6 +5,13 @@ table! { | |||
|     } | ||||
| } | ||||
| 
 | ||||
| table! { | ||||
|     files (file_name) { | ||||
|         file_name -> Text, | ||||
|         track -> Text, | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| table! { | ||||
|     instrumentations (id) { | ||||
|         id -> BigInt, | ||||
|  | @ -20,6 +27,14 @@ table! { | |||
|     } | ||||
| } | ||||
| 
 | ||||
| table! { | ||||
|     mediums (id) { | ||||
|         id -> Text, | ||||
|         name -> Text, | ||||
|         discid -> Nullable<Text>, | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| table! { | ||||
|     performances (id) { | ||||
|         id -> BigInt, | ||||
|  | @ -47,11 +62,19 @@ table! { | |||
| } | ||||
| 
 | ||||
| table! { | ||||
|     tracks (id) { | ||||
|         id -> BigInt, | ||||
|         file_name -> Text, | ||||
|     track_sets (id) { | ||||
|         id -> Text, | ||||
|         medium -> Text, | ||||
|         index -> Integer, | ||||
|         recording -> Text, | ||||
|         track_index -> Integer, | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| table! { | ||||
|     tracks (id) { | ||||
|         id -> Text, | ||||
|         track_set -> Text, | ||||
|         index -> Integer, | ||||
|         work_parts -> Text, | ||||
|     } | ||||
| } | ||||
|  | @ -83,6 +106,7 @@ table! { | |||
|     } | ||||
| } | ||||
| 
 | ||||
| joinable!(files -> tracks (track)); | ||||
| joinable!(instrumentations -> instruments (instrument)); | ||||
| joinable!(instrumentations -> works (work)); | ||||
| joinable!(performances -> ensembles (ensemble)); | ||||
|  | @ -90,7 +114,9 @@ joinable!(performances -> instruments (role)); | |||
| joinable!(performances -> persons (person)); | ||||
| joinable!(performances -> recordings (recording)); | ||||
| joinable!(recordings -> works (work)); | ||||
| joinable!(tracks -> recordings (recording)); | ||||
| joinable!(track_sets -> mediums (medium)); | ||||
| joinable!(track_sets -> recordings (recording)); | ||||
| joinable!(tracks -> track_sets (track_set)); | ||||
| joinable!(work_parts -> persons (composer)); | ||||
| joinable!(work_parts -> works (work)); | ||||
| joinable!(work_sections -> works (work)); | ||||
|  | @ -98,11 +124,14 @@ joinable!(works -> persons (composer)); | |||
| 
 | ||||
| allow_tables_to_appear_in_same_query!( | ||||
|     ensembles, | ||||
|     files, | ||||
|     instrumentations, | ||||
|     instruments, | ||||
|     mediums, | ||||
|     performances, | ||||
|     persons, | ||||
|     recordings, | ||||
|     track_sets, | ||||
|     tracks, | ||||
|     work_parts, | ||||
|     work_sections, | ||||
|  |  | |||
|  | @ -28,9 +28,12 @@ enum Action { | |||
|     GetRecordingsForEnsemble(String, Sender<Result<Vec<Recording>>>), | ||||
|     GetRecordingsForWork(String, Sender<Result<Vec<Recording>>>), | ||||
|     RecordingExists(String, Sender<Result<bool>>), | ||||
|     UpdateTracks(String, Vec<Track>, Sender<Result<()>>), | ||||
|     DeleteTracks(String, Sender<Result<()>>), | ||||
|     GetTracks(String, Sender<Result<Vec<Track>>>), | ||||
|     UpdateMedium(Medium, Sender<Result<()>>), | ||||
|     GetMedium(String, Sender<Result<Option<Medium>>>), | ||||
|     DeleteMedium(String, Sender<Result<()>>), | ||||
|     UpdateFile(String, String, Sender<Result<()>>), | ||||
|     DeleteFile(String, Sender<Result<()>>), | ||||
|     GetFile(String, Sender<Result<Option<String>>>), | ||||
|     Stop(Sender<()>), | ||||
| } | ||||
| 
 | ||||
|  | @ -124,16 +127,23 @@ impl DbThread { | |||
|                     RecordingExists(id, sender) => { | ||||
|                         sender.send(db.recording_exists(&id)).unwrap(); | ||||
|                     } | ||||
|                     UpdateTracks(recording_id, tracks, sender) => { | ||||
|                         sender | ||||
|                             .send(db.update_tracks(&recording_id, tracks)) | ||||
|                             .unwrap(); | ||||
|                     UpdateMedium(medium, sender) => { | ||||
|                         sender.send(db.update_medium(medium)).unwrap(); | ||||
|                     } | ||||
|                     DeleteTracks(recording_id, sender) => { | ||||
|                         sender.send(db.delete_tracks(&recording_id)).unwrap(); | ||||
|                     GetMedium(id, sender) => { | ||||
|                         sender.send(db.get_medium(&id)).unwrap(); | ||||
|                     } | ||||
|                     GetTracks(recording_id, sender) => { | ||||
|                         sender.send(db.get_tracks(&recording_id)).unwrap(); | ||||
|                     DeleteMedium(id, sender) => { | ||||
|                         sender.send(db.delete_medium(&id)).unwrap(); | ||||
|                     } | ||||
|                     UpdateFile(file_name, track_id, sender) => { | ||||
|                         sender.send(db.update_file(&file_name, &track_id)).unwrap(); | ||||
|                     } | ||||
|                     DeleteFile(file_name, sender) => { | ||||
|                         sender.send(db.delete_file(&file_name)).unwrap(); | ||||
|                     } | ||||
|                     GetFile(track_id, sender) => { | ||||
|                         sender.send(db.get_file(&track_id)).unwrap(); | ||||
|                     } | ||||
|                     Stop(sender) => { | ||||
|                         sender.send(()).unwrap(); | ||||
|  | @ -312,28 +322,63 @@ impl DbThread { | |||
|         receiver.await? | ||||
|     } | ||||
| 
 | ||||
|     /// Add or change the tracks associated with a recording. This will fail, if there are still
 | ||||
|     /// other items referencing this recording.
 | ||||
|     pub async fn update_tracks(&self, recording_id: &str, tracks: Vec<Track>) -> Result<()> { | ||||
|     /// Update an existing medium or insert a new one.
 | ||||
|     pub async fn update_medium(&self, medium: Medium) -> Result<()> { | ||||
|         let (sender, receiver) = oneshot::channel(); | ||||
|         self.action_sender | ||||
|             .send(UpdateTracks(recording_id.to_string(), tracks, sender))?; | ||||
|         self.action_sender.send(UpdateMedium(medium, sender))?; | ||||
|         receiver.await? | ||||
|     } | ||||
| 
 | ||||
|     /// Delete all tracks associated with a recording.
 | ||||
|     pub async fn delete_tracks(&self, recording_id: &str) -> Result<()> { | ||||
|     /// Delete an existing medium. This will fail, if there are still other
 | ||||
|     /// items referencing this medium.
 | ||||
|     pub async fn delete_medium(&self, id: &str) -> Result<()> { | ||||
|         let (sender, receiver) = oneshot::channel(); | ||||
| 
 | ||||
|         self.action_sender | ||||
|             .send(DeleteTracks(recording_id.to_string(), sender))?; | ||||
|             .send(DeleteMedium(id.to_owned(), sender))?; | ||||
| 
 | ||||
|         receiver.await? | ||||
|     } | ||||
| 
 | ||||
|     /// Get all tracks associated with a recording.
 | ||||
|     pub async fn get_tracks(&self, recording_id: &str) -> Result<Vec<Track>> { | ||||
|     /// Get an existing medium.
 | ||||
|     pub async fn get_medium(&self, id: &str) -> Result<Option<Medium>> { | ||||
|         let (sender, receiver) = oneshot::channel(); | ||||
|         self.action_sender.send(GetMedium(id.to_owned(), sender))?; | ||||
|         receiver.await? | ||||
|     } | ||||
| 
 | ||||
|     /// Insert or update a file. This assumes that the track is already in the
 | ||||
|     /// database.
 | ||||
|     pub async fn update_file(&self, file_name: &str, track_id: &str) -> Result<()> { | ||||
|         let (sender, receiver) = oneshot::channel(); | ||||
| 
 | ||||
|         self.action_sender.send(UpdateFile( | ||||
|             file_name.to_owned(), | ||||
|             track_id.to_owned(), | ||||
|             sender, | ||||
|         ))?; | ||||
| 
 | ||||
|         receiver.await? | ||||
|     } | ||||
| 
 | ||||
|     /// Delete an existing file. This will not delete the file from the file
 | ||||
|     /// system but just the representing row from the database.
 | ||||
|     pub async fn delete_file(&self, file_name: &str) -> Result<()> { | ||||
|         let (sender, receiver) = oneshot::channel(); | ||||
| 
 | ||||
|         self.action_sender | ||||
|             .send(GetTracks(recording_id.to_string(), sender))?; | ||||
|             .send(DeleteFile(file_name.to_owned(), sender))?; | ||||
| 
 | ||||
|         receiver.await? | ||||
|     } | ||||
| 
 | ||||
|     /// Get the file name of the audio file for the specified track.
 | ||||
|     pub async fn get_file(&self, track_id: &str) -> Result<Option<String>> { | ||||
|         let (sender, receiver) = oneshot::channel(); | ||||
| 
 | ||||
|         self.action_sender | ||||
|             .send(GetFile(track_id.to_owned(), sender))?; | ||||
| 
 | ||||
|         receiver.await? | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,94 +0,0 @@ | |||
| use super::schema::tracks; | ||||
| use super::Database; | ||||
| use anyhow::{Error, Result}; | ||||
| use diesel::prelude::*; | ||||
| use std::convert::{TryFrom, TryInto}; | ||||
| 
 | ||||
| /// Table row data for a track.
 | ||||
| #[derive(Insertable, Queryable, Debug, Clone)] | ||||
| #[table_name = "tracks"] | ||||
| struct TrackRow { | ||||
|     pub id: i64, | ||||
|     pub file_name: String, | ||||
|     pub recording: String, | ||||
|     pub track_index: i32, | ||||
|     pub work_parts: String, | ||||
| } | ||||
| 
 | ||||
| /// A structure representing one playable audio file.
 | ||||
| #[derive(Debug, Clone)] | ||||
| pub struct Track { | ||||
|     pub work_parts: Vec<usize>, | ||||
|     pub file_name: String, | ||||
| } | ||||
| 
 | ||||
| impl TryFrom<TrackRow> for Track { | ||||
|     type Error = Error; | ||||
|     fn try_from(row: TrackRow) -> Result<Self> { | ||||
|         let mut work_parts = Vec::<usize>::new(); | ||||
|         for part in row.work_parts.split(",") { | ||||
|             if !part.is_empty() { | ||||
|                 work_parts.push(part.parse()?); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         let track = Track { | ||||
|             work_parts, | ||||
|             file_name: row.file_name, | ||||
|         }; | ||||
| 
 | ||||
|         Ok(track) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Database { | ||||
|     /// Insert or update tracks for the specified recording.
 | ||||
|     pub fn update_tracks(&self, recording_id: &str, tracks: Vec<Track>) -> Result<()> { | ||||
|         self.delete_tracks(recording_id)?; | ||||
| 
 | ||||
|         for (index, track) in tracks.iter().enumerate() { | ||||
|             let row = TrackRow { | ||||
|                 id: rand::random(), | ||||
|                 file_name: track.file_name.clone(), | ||||
|                 recording: recording_id.to_string(), | ||||
|                 track_index: index.try_into()?, | ||||
|                 work_parts: track | ||||
|                     .work_parts | ||||
|                     .iter() | ||||
|                     .map(|i| i.to_string()) | ||||
|                     .collect::<Vec<String>>() | ||||
|                     .join(","), | ||||
|             }; | ||||
| 
 | ||||
|             diesel::insert_into(tracks::table) | ||||
|                 .values(row) | ||||
|                 .execute(&self.connection)?; | ||||
|         } | ||||
| 
 | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     /// Delete all tracks for the specified recording.
 | ||||
|     pub fn delete_tracks(&self, recording_id: &str) -> Result<()> { | ||||
|         diesel::delete(tracks::table.filter(tracks::recording.eq(recording_id))) | ||||
|             .execute(&self.connection)?; | ||||
| 
 | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     /// Get all tracks of the specified recording.
 | ||||
|     pub fn get_tracks(&self, recording_id: &str) -> Result<Vec<Track>> { | ||||
|         let mut tracks = Vec::<Track>::new(); | ||||
| 
 | ||||
|         let rows = tracks::table | ||||
|             .filter(tracks::recording.eq(recording_id)) | ||||
|             .order_by(tracks::track_index) | ||||
|             .load::<TrackRow>(&self.connection)?; | ||||
| 
 | ||||
|         for row in rows { | ||||
|             tracks.push(row.try_into()?); | ||||
|         } | ||||
| 
 | ||||
|         Ok(tracks) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										211
									
								
								musicus/src/dialogs/import_disc.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										211
									
								
								musicus/src/dialogs/import_disc.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,211 @@ | |||
| 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,3 +1,6 @@ | |||
| pub mod import_disc; | ||||
| pub use import_disc::*; | ||||
| 
 | ||||
| pub mod about; | ||||
| pub use about::*; | ||||
| 
 | ||||
|  |  | |||
|  | @ -12,6 +12,7 @@ use std::cell::RefCell; | |||
| use std::rc::Rc; | ||||
| 
 | ||||
| mod backend; | ||||
| mod ripper; | ||||
| mod config; | ||||
| mod database; | ||||
| mod dialogs; | ||||
|  | @ -31,6 +32,7 @@ fn main() { | |||
|     gettextrs::bindtextdomain("musicus", config::LOCALEDIR); | ||||
|     gettextrs::textdomain("musicus"); | ||||
| 
 | ||||
|     gstreamer::init().expect("Failed to initialize GStreamer!"); | ||||
|     gtk::init().expect("Failed to initialize GTK!"); | ||||
|     libhandy::init(); | ||||
|     resources::init().expect("Failed to initialize resources!"); | ||||
|  |  | |||
|  | @ -52,6 +52,7 @@ sources = files( | |||
|   'database/tracks.rs', | ||||
|   'database/works.rs', | ||||
|   'dialogs/about.rs', | ||||
|   'dialogs/import_disc.rs', | ||||
|   'dialogs/login_dialog.rs', | ||||
|   'dialogs/mod.rs', | ||||
|   'dialogs/preferences.rs', | ||||
|  | @ -93,6 +94,7 @@ sources = files( | |||
|   'player.rs', | ||||
|   'resources.rs', | ||||
|   'resources.rs.in', | ||||
|   'ripper.rs', | ||||
|   'window.rs', | ||||
| ) | ||||
| 
 | ||||
|  |  | |||
|  | @ -8,8 +8,8 @@ use std::rc::Rc; | |||
| 
 | ||||
| #[derive(Clone)] | ||||
| pub struct PlaylistItem { | ||||
|     pub recording: Recording, | ||||
|     pub tracks: Vec<Track>, | ||||
|     pub tracks: TrackSet, | ||||
|     pub indices: Vec<usize>, | ||||
| } | ||||
| 
 | ||||
| pub struct Player { | ||||
|  | @ -19,11 +19,11 @@ pub struct Player { | |||
|     current_item: Cell<Option<usize>>, | ||||
|     current_track: Cell<Option<usize>>, | ||||
|     playing: Cell<bool>, | ||||
|     playlist_cbs: RefCell<Vec<Box<dyn Fn(Vec<PlaylistItem>) -> ()>>>, | ||||
|     track_cbs: RefCell<Vec<Box<dyn Fn(usize, usize) -> ()>>>, | ||||
|     duration_cbs: RefCell<Vec<Box<dyn Fn(u64) -> ()>>>, | ||||
|     playing_cbs: RefCell<Vec<Box<dyn Fn(bool) -> ()>>>, | ||||
|     position_cbs: RefCell<Vec<Box<dyn Fn(u64) -> ()>>>, | ||||
|     playlist_cbs: RefCell<Vec<Box<dyn Fn(Vec<PlaylistItem>)>>>, | ||||
|     track_cbs: RefCell<Vec<Box<dyn Fn(usize, usize)>>>, | ||||
|     duration_cbs: RefCell<Vec<Box<dyn Fn(u64)>>>, | ||||
|     playing_cbs: RefCell<Vec<Box<dyn Fn(bool)>>>, | ||||
|     position_cbs: RefCell<Vec<Box<dyn Fn(u64)>>>, | ||||
| } | ||||
| 
 | ||||
| impl Player { | ||||
|  | @ -80,23 +80,23 @@ impl Player { | |||
|         result | ||||
|     } | ||||
| 
 | ||||
|     pub fn add_playlist_cb<F: Fn(Vec<PlaylistItem>) -> () + 'static>(&self, cb: F) { | ||||
|     pub fn add_playlist_cb<F: Fn(Vec<PlaylistItem>) + 'static>(&self, cb: F) { | ||||
|         self.playlist_cbs.borrow_mut().push(Box::new(cb)); | ||||
|     } | ||||
| 
 | ||||
|     pub fn add_track_cb<F: Fn(usize, usize) -> () + 'static>(&self, cb: F) { | ||||
|     pub fn add_track_cb<F: Fn(usize, usize) + 'static>(&self, cb: F) { | ||||
|         self.track_cbs.borrow_mut().push(Box::new(cb)); | ||||
|     } | ||||
| 
 | ||||
|     pub fn add_duration_cb<F: Fn(u64) -> () + 'static>(&self, cb: F) { | ||||
|     pub fn add_duration_cb<F: Fn(u64) + 'static>(&self, cb: F) { | ||||
|         self.duration_cbs.borrow_mut().push(Box::new(cb)); | ||||
|     } | ||||
| 
 | ||||
|     pub fn add_playing_cb<F: Fn(bool) -> () + 'static>(&self, cb: F) { | ||||
|     pub fn add_playing_cb<F: Fn(bool) + 'static>(&self, cb: F) { | ||||
|         self.playing_cbs.borrow_mut().push(Box::new(cb)); | ||||
|     } | ||||
| 
 | ||||
|     pub fn add_position_cb<F: Fn(u64) -> () + 'static>(&self, cb: F) { | ||||
|     pub fn add_position_cb<F: Fn(u64) + 'static>(&self, cb: F) { | ||||
|         self.position_cbs.borrow_mut().push(Box::new(cb)); | ||||
|     } | ||||
| 
 | ||||
|  | @ -121,7 +121,7 @@ impl Player { | |||
|     } | ||||
| 
 | ||||
|     pub fn add_item(&self, item: PlaylistItem) -> Result<()> { | ||||
|         if item.tracks.is_empty() { | ||||
|         if item.indices.is_empty() { | ||||
|             Err(anyhow!( | ||||
|                 "Tried to add playlist item without tracks to playlist!" | ||||
|             )) | ||||
|  | @ -199,7 +199,7 @@ impl Player { | |||
|             current_track -= 1; | ||||
|         } else if current_item > 0 { | ||||
|             current_item -= 1; | ||||
|             current_track = playlist[current_item].tracks.len() - 1; | ||||
|             current_track = playlist[current_item].indices.len() - 1; | ||||
|         } else { | ||||
|             return Err(anyhow!("No previous track!")); | ||||
|         } | ||||
|  | @ -213,7 +213,7 @@ impl Player { | |||
|                 let playlist = self.playlist.borrow(); | ||||
|                 let item = &playlist[current_item]; | ||||
| 
 | ||||
|                 current_track + 1 < item.tracks.len() || current_item + 1 < playlist.len() | ||||
|                 current_track + 1 < item.indices.len() || current_item + 1 < playlist.len() | ||||
|             } else { | ||||
|                 false | ||||
|             } | ||||
|  | @ -231,7 +231,7 @@ impl Player { | |||
| 
 | ||||
|         let playlist = self.playlist.borrow(); | ||||
|         let item = &playlist[current_item]; | ||||
|         if current_track + 1 < item.tracks.len() { | ||||
|         if current_track + 1 < item.indices.len() { | ||||
|             current_track += 1; | ||||
|         } else if current_item + 1 < playlist.len() { | ||||
|             current_item += 1; | ||||
|  |  | |||
							
								
								
									
										130
									
								
								musicus/src/ripper.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								musicus/src/ripper.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,130 @@ | |||
| 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)?; | ||||
| 
 | ||||
|         // // TODO: Remove.
 | ||||
|         // cdparanoiasrc.set_property(
 | ||||
|         //     "device",
 | ||||
|         //     &String::from("/home/johrpan/Diverses/arrau_schumann.iso"),
 | ||||
|         // )?;
 | ||||
| 
 | ||||
|         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) | ||||
|     } | ||||
| } | ||||
|  | @ -107,6 +107,16 @@ 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, | ||||
|             "preferences", | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Elias Projahn
						Elias Projahn