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" | anyhow = "1.0.33" | ||||||
| diesel = { version = "1.4.5", features = ["sqlite"] } | diesel = { version = "1.4.5", features = ["sqlite"] } | ||||||
| diesel_migrations = "1.4.0" | diesel_migrations = "1.4.0" | ||||||
|  | discid = "0.4.4" | ||||||
| fragile = "1.0.0" | fragile = "1.0.0" | ||||||
| futures = "0.3.6" | futures = "0.3.6" | ||||||
| futures-channel = "0.3.5" | 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 ( | CREATE TABLE "persons" ( | ||||||
|     id TEXT NOT NULL PRIMARY KEY, |     "id" TEXT NOT NULL PRIMARY KEY, | ||||||
|     first_name TEXT NOT NULL, |     "first_name" TEXT NOT NULL, | ||||||
|     last_name TEXT NOT NULL |     "last_name" TEXT NOT NULL | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE TABLE instruments ( | CREATE TABLE "instruments" ( | ||||||
|     id TEXT NOT NULL PRIMARY KEY, |     "id" TEXT NOT NULL PRIMARY KEY, | ||||||
|     name TEXT NOT NULL |     "name" TEXT NOT NULL | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE TABLE works ( | CREATE TABLE "works" ( | ||||||
|     id TEXT NOT NULL PRIMARY KEY, |     "id" TEXT NOT NULL PRIMARY KEY, | ||||||
|     composer TEXT NOT NULL REFERENCES persons(id), |     "composer" TEXT NOT NULL REFERENCES "persons"("id"), | ||||||
|     title TEXT NOT NULL |     "title" TEXT NOT NULL | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE TABLE instrumentations ( | CREATE TABLE "instrumentations" ( | ||||||
|     id BIGINT NOT NULL PRIMARY KEY, |     "id" BIGINT NOT NULL PRIMARY KEY, | ||||||
|     work TEXT NOT NULL REFERENCES works(id) ON DELETE CASCADE, |     "work" TEXT NOT NULL REFERENCES "works"("id") ON DELETE CASCADE, | ||||||
|     instrument TEXT NOT NULL REFERENCES instruments(id) ON DELETE CASCADE |     "instrument" TEXT NOT NULL REFERENCES "instruments"("id") ON DELETE CASCADE | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE TABLE work_parts ( | CREATE TABLE "work_parts" ( | ||||||
|     id BIGINT NOT NULL PRIMARY KEY, |     "id" BIGINT NOT NULL PRIMARY KEY, | ||||||
|     work TEXT NOT NULL REFERENCES works(id) ON DELETE CASCADE, |     "work" TEXT NOT NULL REFERENCES "works"("id") ON DELETE CASCADE, | ||||||
|     part_index BIGINT NOT NULL, |     "part_index" BIGINT NOT NULL, | ||||||
|     title TEXT NOT NULL, |     "title" TEXT NOT NULL, | ||||||
|     composer TEXT REFERENCES persons(id) |     "composer" TEXT REFERENCES "persons"("id") | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE TABLE work_sections ( | CREATE TABLE "work_sections" ( | ||||||
|     id BIGINT NOT NULL PRIMARY KEY, |     "id" BIGINT NOT NULL PRIMARY KEY, | ||||||
|     work TEXT NOT NULL REFERENCES works(id) ON DELETE CASCADE, |     "work" TEXT NOT NULL REFERENCES "works"("id") ON DELETE CASCADE, | ||||||
|     title TEXT NOT NULL, |     "title" TEXT NOT NULL, | ||||||
|     before_index BIGINT NOT NULL |     "before_index" BIGINT NOT NULL | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE TABLE ensembles ( | CREATE TABLE "ensembles" ( | ||||||
|     id TEXT NOT NULL PRIMARY KEY, |     "id" TEXT NOT NULL PRIMARY KEY, | ||||||
|     name TEXT NOT NULL |     "name" TEXT NOT NULL | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE TABLE recordings ( | CREATE TABLE "recordings" ( | ||||||
|     id TEXT NOT NULL PRIMARY KEY, |     "id" TEXT NOT NULL PRIMARY KEY, | ||||||
|     work TEXT NOT NULL REFERENCES works(id), |     "work" TEXT NOT NULL REFERENCES "works"("id"), | ||||||
|     comment TEXT NOT NULL |     "comment" TEXT NOT NULL | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE TABLE performances ( | CREATE TABLE "performances" ( | ||||||
|     id BIGINT NOT NULL PRIMARY KEY, |     "id" BIGINT NOT NULL PRIMARY KEY, | ||||||
|     recording TEXT NOT NULL REFERENCES recordings(id) ON DELETE CASCADE, |     "recording" TEXT NOT NULL REFERENCES "recordings"("id") ON DELETE CASCADE, | ||||||
|     person TEXT REFERENCES persons(id), |     "person" TEXT REFERENCES "persons"("id"), | ||||||
|     ensemble TEXT REFERENCES ensembles(id), |     "ensemble" TEXT REFERENCES "ensembles"("id"), | ||||||
|     role TEXT REFERENCES instruments(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> | <gresources> | ||||||
|     <gresource prefix="/de/johrpan/musicus"> |     <gresource prefix="/de/johrpan/musicus"> | ||||||
|         <file preprocess="xml-stripblanks">ui/ensemble_editor.ui</file> |         <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_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_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> | ||||||
|  |  | ||||||
							
								
								
									
										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> |   </object> | ||||||
|   <menu id="menu"> |   <menu id="menu"> | ||||||
|     <section> |     <section> | ||||||
|  |     <item> | ||||||
|  |         <attribute name="label" translatable="yes">Import CD</attribute> | ||||||
|  |         <attribute name="action">win.import-disc</attribute> | ||||||
|  |       </item> | ||||||
|       <item> |       <item> | ||||||
|         <attribute name="label" translatable="yes">Preferences</attribute> |         <attribute name="label" translatable="yes">Preferences</attribute> | ||||||
|         <attribute name="action">win.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 mod instruments; | ||||||
| pub use instruments::*; | pub use instruments::*; | ||||||
| 
 | 
 | ||||||
|  | pub mod medium; | ||||||
|  | pub use medium::*; | ||||||
|  | 
 | ||||||
| pub mod persons; | pub mod persons; | ||||||
| pub use persons::*; | pub use persons::*; | ||||||
| 
 | 
 | ||||||
|  | @ -16,8 +19,8 @@ pub use recordings::*; | ||||||
| pub mod thread; | pub mod thread; | ||||||
| pub use thread::*; | pub use thread::*; | ||||||
| 
 | 
 | ||||||
| pub mod tracks; | pub mod files; | ||||||
| pub use tracks::*; | pub use files::*; | ||||||
| 
 | 
 | ||||||
| pub mod works; | pub mod works; | ||||||
| pub use works::*; | pub use works::*; | ||||||
|  |  | ||||||
|  | @ -190,6 +190,22 @@ impl Database { | ||||||
|         Ok(exists) |         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.
 |     /// Retrieve all available information on a recording from related tables.
 | ||||||
|     fn get_recording_data(&self, row: RecordingRow) -> Result<Recording> { |     fn get_recording_data(&self, row: RecordingRow) -> Result<Recording> { | ||||||
|         let mut performance_descriptions: Vec<Performance> = Vec::new(); |         let mut performance_descriptions: Vec<Performance> = Vec::new(); | ||||||
|  |  | ||||||
|  | @ -5,6 +5,13 @@ table! { | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | table! { | ||||||
|  |     files (file_name) { | ||||||
|  |         file_name -> Text, | ||||||
|  |         track -> Text, | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
| table! { | table! { | ||||||
|     instrumentations (id) { |     instrumentations (id) { | ||||||
|         id -> BigInt, |         id -> BigInt, | ||||||
|  | @ -20,6 +27,14 @@ table! { | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | table! { | ||||||
|  |     mediums (id) { | ||||||
|  |         id -> Text, | ||||||
|  |         name -> Text, | ||||||
|  |         discid -> Nullable<Text>, | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
| table! { | table! { | ||||||
|     performances (id) { |     performances (id) { | ||||||
|         id -> BigInt, |         id -> BigInt, | ||||||
|  | @ -47,11 +62,19 @@ table! { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| table! { | table! { | ||||||
|     tracks (id) { |     track_sets (id) { | ||||||
|         id -> BigInt, |         id -> Text, | ||||||
|         file_name -> Text, |         medium -> Text, | ||||||
|  |         index -> Integer, | ||||||
|         recording -> Text, |         recording -> Text, | ||||||
|         track_index -> Integer, |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | table! { | ||||||
|  |     tracks (id) { | ||||||
|  |         id -> Text, | ||||||
|  |         track_set -> Text, | ||||||
|  |         index -> Integer, | ||||||
|         work_parts -> Text, |         work_parts -> Text, | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | @ -83,6 +106,7 @@ table! { | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | joinable!(files -> tracks (track)); | ||||||
| joinable!(instrumentations -> instruments (instrument)); | joinable!(instrumentations -> instruments (instrument)); | ||||||
| joinable!(instrumentations -> works (work)); | joinable!(instrumentations -> works (work)); | ||||||
| joinable!(performances -> ensembles (ensemble)); | joinable!(performances -> ensembles (ensemble)); | ||||||
|  | @ -90,7 +114,9 @@ joinable!(performances -> instruments (role)); | ||||||
| joinable!(performances -> persons (person)); | joinable!(performances -> persons (person)); | ||||||
| joinable!(performances -> recordings (recording)); | joinable!(performances -> recordings (recording)); | ||||||
| joinable!(recordings -> works (work)); | 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 -> persons (composer)); | ||||||
| joinable!(work_parts -> works (work)); | joinable!(work_parts -> works (work)); | ||||||
| joinable!(work_sections -> works (work)); | joinable!(work_sections -> works (work)); | ||||||
|  | @ -98,11 +124,14 @@ joinable!(works -> persons (composer)); | ||||||
| 
 | 
 | ||||||
| allow_tables_to_appear_in_same_query!( | allow_tables_to_appear_in_same_query!( | ||||||
|     ensembles, |     ensembles, | ||||||
|  |     files, | ||||||
|     instrumentations, |     instrumentations, | ||||||
|     instruments, |     instruments, | ||||||
|  |     mediums, | ||||||
|     performances, |     performances, | ||||||
|     persons, |     persons, | ||||||
|     recordings, |     recordings, | ||||||
|  |     track_sets, | ||||||
|     tracks, |     tracks, | ||||||
|     work_parts, |     work_parts, | ||||||
|     work_sections, |     work_sections, | ||||||
|  |  | ||||||
|  | @ -28,9 +28,12 @@ enum Action { | ||||||
|     GetRecordingsForEnsemble(String, Sender<Result<Vec<Recording>>>), |     GetRecordingsForEnsemble(String, Sender<Result<Vec<Recording>>>), | ||||||
|     GetRecordingsForWork(String, Sender<Result<Vec<Recording>>>), |     GetRecordingsForWork(String, Sender<Result<Vec<Recording>>>), | ||||||
|     RecordingExists(String, Sender<Result<bool>>), |     RecordingExists(String, Sender<Result<bool>>), | ||||||
|     UpdateTracks(String, Vec<Track>, Sender<Result<()>>), |     UpdateMedium(Medium, Sender<Result<()>>), | ||||||
|     DeleteTracks(String, Sender<Result<()>>), |     GetMedium(String, Sender<Result<Option<Medium>>>), | ||||||
|     GetTracks(String, Sender<Result<Vec<Track>>>), |     DeleteMedium(String, Sender<Result<()>>), | ||||||
|  |     UpdateFile(String, String, Sender<Result<()>>), | ||||||
|  |     DeleteFile(String, Sender<Result<()>>), | ||||||
|  |     GetFile(String, Sender<Result<Option<String>>>), | ||||||
|     Stop(Sender<()>), |     Stop(Sender<()>), | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -124,16 +127,23 @@ impl DbThread { | ||||||
|                     RecordingExists(id, sender) => { |                     RecordingExists(id, sender) => { | ||||||
|                         sender.send(db.recording_exists(&id)).unwrap(); |                         sender.send(db.recording_exists(&id)).unwrap(); | ||||||
|                     } |                     } | ||||||
|                     UpdateTracks(recording_id, tracks, sender) => { |                     UpdateMedium(medium, sender) => { | ||||||
|                         sender |                         sender.send(db.update_medium(medium)).unwrap(); | ||||||
|                             .send(db.update_tracks(&recording_id, tracks)) |  | ||||||
|                             .unwrap(); |  | ||||||
|                     } |                     } | ||||||
|                     DeleteTracks(recording_id, sender) => { |                     GetMedium(id, sender) => { | ||||||
|                         sender.send(db.delete_tracks(&recording_id)).unwrap(); |                         sender.send(db.get_medium(&id)).unwrap(); | ||||||
|                     } |                     } | ||||||
|                     GetTracks(recording_id, sender) => { |                     DeleteMedium(id, sender) => { | ||||||
|                         sender.send(db.get_tracks(&recording_id)).unwrap(); |                         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) => { |                     Stop(sender) => { | ||||||
|                         sender.send(()).unwrap(); |                         sender.send(()).unwrap(); | ||||||
|  | @ -312,28 +322,63 @@ impl DbThread { | ||||||
|         receiver.await? |         receiver.await? | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// Add or change the tracks associated with a recording. This will fail, if there are still
 |     /// Update an existing medium or insert a new one.
 | ||||||
|     /// other items referencing this recording.
 |     pub async fn update_medium(&self, medium: Medium) -> Result<()> { | ||||||
|     pub async fn update_tracks(&self, recording_id: &str, tracks: Vec<Track>) -> Result<()> { |  | ||||||
|         let (sender, receiver) = oneshot::channel(); |         let (sender, receiver) = oneshot::channel(); | ||||||
|         self.action_sender |         self.action_sender.send(UpdateMedium(medium, sender))?; | ||||||
|             .send(UpdateTracks(recording_id.to_string(), tracks, sender))?; |  | ||||||
|         receiver.await? |         receiver.await? | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// Delete all tracks associated with a recording.
 |     /// Delete an existing medium. This will fail, if there are still other
 | ||||||
|     pub async fn delete_tracks(&self, recording_id: &str) -> Result<()> { |     /// items referencing this medium.
 | ||||||
|  |     pub async fn delete_medium(&self, id: &str) -> Result<()> { | ||||||
|         let (sender, receiver) = oneshot::channel(); |         let (sender, receiver) = oneshot::channel(); | ||||||
|  | 
 | ||||||
|         self.action_sender |         self.action_sender | ||||||
|             .send(DeleteTracks(recording_id.to_string(), sender))?; |             .send(DeleteMedium(id.to_owned(), sender))?; | ||||||
|  | 
 | ||||||
|         receiver.await? |         receiver.await? | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// Get all tracks associated with a recording.
 |     /// Get an existing medium.
 | ||||||
|     pub async fn get_tracks(&self, recording_id: &str) -> Result<Vec<Track>> { |     pub async fn get_medium(&self, id: &str) -> Result<Option<Medium>> { | ||||||
|         let (sender, receiver) = oneshot::channel(); |         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 |         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? |         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 mod about; | ||||||
| pub use about::*; | pub use about::*; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -12,6 +12,7 @@ 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; | ||||||
|  | @ -31,6 +32,7 @@ fn main() { | ||||||
|     gettextrs::bindtextdomain("musicus", config::LOCALEDIR); |     gettextrs::bindtextdomain("musicus", config::LOCALEDIR); | ||||||
|     gettextrs::textdomain("musicus"); |     gettextrs::textdomain("musicus"); | ||||||
| 
 | 
 | ||||||
|  |     gstreamer::init().expect("Failed to initialize GStreamer!"); | ||||||
|     gtk::init().expect("Failed to initialize GTK!"); |     gtk::init().expect("Failed to initialize GTK!"); | ||||||
|     libhandy::init(); |     libhandy::init(); | ||||||
|     resources::init().expect("Failed to initialize resources!"); |     resources::init().expect("Failed to initialize resources!"); | ||||||
|  |  | ||||||
|  | @ -52,6 +52,7 @@ sources = files( | ||||||
|   'database/tracks.rs', |   'database/tracks.rs', | ||||||
|   'database/works.rs', |   'database/works.rs', | ||||||
|   'dialogs/about.rs', |   'dialogs/about.rs', | ||||||
|  |   'dialogs/import_disc.rs', | ||||||
|   'dialogs/login_dialog.rs', |   'dialogs/login_dialog.rs', | ||||||
|   'dialogs/mod.rs', |   'dialogs/mod.rs', | ||||||
|   'dialogs/preferences.rs', |   'dialogs/preferences.rs', | ||||||
|  | @ -93,6 +94,7 @@ sources = files( | ||||||
|   'player.rs', |   'player.rs', | ||||||
|   'resources.rs', |   'resources.rs', | ||||||
|   'resources.rs.in', |   'resources.rs.in', | ||||||
|  |   'ripper.rs', | ||||||
|   'window.rs', |   'window.rs', | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -8,8 +8,8 @@ use std::rc::Rc; | ||||||
| 
 | 
 | ||||||
| #[derive(Clone)] | #[derive(Clone)] | ||||||
| pub struct PlaylistItem { | pub struct PlaylistItem { | ||||||
|     pub recording: Recording, |     pub tracks: TrackSet, | ||||||
|     pub tracks: Vec<Track>, |     pub indices: Vec<usize>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| pub struct Player { | pub struct Player { | ||||||
|  | @ -19,11 +19,11 @@ pub struct Player { | ||||||
|     current_item: Cell<Option<usize>>, |     current_item: Cell<Option<usize>>, | ||||||
|     current_track: Cell<Option<usize>>, |     current_track: Cell<Option<usize>>, | ||||||
|     playing: Cell<bool>, |     playing: Cell<bool>, | ||||||
|     playlist_cbs: RefCell<Vec<Box<dyn Fn(Vec<PlaylistItem>) -> ()>>>, |     playlist_cbs: RefCell<Vec<Box<dyn Fn(Vec<PlaylistItem>)>>>, | ||||||
|     track_cbs: RefCell<Vec<Box<dyn Fn(usize, usize) -> ()>>>, |     track_cbs: RefCell<Vec<Box<dyn Fn(usize, usize)>>>, | ||||||
|     duration_cbs: RefCell<Vec<Box<dyn Fn(u64) -> ()>>>, |     duration_cbs: RefCell<Vec<Box<dyn Fn(u64)>>>, | ||||||
|     playing_cbs: RefCell<Vec<Box<dyn Fn(bool) -> ()>>>, |     playing_cbs: RefCell<Vec<Box<dyn Fn(bool)>>>, | ||||||
|     position_cbs: RefCell<Vec<Box<dyn Fn(u64) -> ()>>>, |     position_cbs: RefCell<Vec<Box<dyn Fn(u64)>>>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl Player { | impl Player { | ||||||
|  | @ -80,23 +80,23 @@ impl Player { | ||||||
|         result |         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)); |         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)); |         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)); |         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)); |         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)); |         self.position_cbs.borrow_mut().push(Box::new(cb)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -121,7 +121,7 @@ impl Player { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     pub fn add_item(&self, item: PlaylistItem) -> Result<()> { |     pub fn add_item(&self, item: PlaylistItem) -> Result<()> { | ||||||
|         if item.tracks.is_empty() { |         if item.indices.is_empty() { | ||||||
|             Err(anyhow!( |             Err(anyhow!( | ||||||
|                 "Tried to add playlist item without tracks to playlist!" |                 "Tried to add playlist item without tracks to playlist!" | ||||||
|             )) |             )) | ||||||
|  | @ -199,7 +199,7 @@ impl Player { | ||||||
|             current_track -= 1; |             current_track -= 1; | ||||||
|         } else if current_item > 0 { |         } else if current_item > 0 { | ||||||
|             current_item -= 1; |             current_item -= 1; | ||||||
|             current_track = playlist[current_item].tracks.len() - 1; |             current_track = playlist[current_item].indices.len() - 1; | ||||||
|         } else { |         } else { | ||||||
|             return Err(anyhow!("No previous track!")); |             return Err(anyhow!("No previous track!")); | ||||||
|         } |         } | ||||||
|  | @ -213,7 +213,7 @@ impl Player { | ||||||
|                 let playlist = self.playlist.borrow(); |                 let playlist = self.playlist.borrow(); | ||||||
|                 let item = &playlist[current_item]; |                 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 { |             } else { | ||||||
|                 false |                 false | ||||||
|             } |             } | ||||||
|  | @ -231,7 +231,7 @@ impl Player { | ||||||
| 
 | 
 | ||||||
|         let playlist = self.playlist.borrow(); |         let playlist = self.playlist.borrow(); | ||||||
|         let item = &playlist[current_item]; |         let item = &playlist[current_item]; | ||||||
|         if current_track + 1 < item.tracks.len() { |         if current_track + 1 < item.indices.len() { | ||||||
|             current_track += 1; |             current_track += 1; | ||||||
|         } else if current_item + 1 < playlist.len() { |         } else if current_item + 1 < playlist.len() { | ||||||
|             current_item += 1; |             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"); |                 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!( |         action!( | ||||||
|             result.window, |             result.window, | ||||||
|             "preferences", |             "preferences", | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Elias Projahn
						Elias Projahn