diff --git a/musicus/migrations/2020-09-27-201047_initial_schema/down.sql b/musicus/migrations/2020-09-27-201047_initial_schema/down.sql index 0a31654..39e9f73 100644 --- a/musicus/migrations/2020-09-27-201047_initial_schema/down.sql +++ b/musicus/migrations/2020-09-27-201047_initial_schema/down.sql @@ -12,5 +12,4 @@ DROP TABLE "performances"; DROP TABLE "mediums"; DROP TABLE "track_sets"; DROP TABLE "tracks"; -DROP TABLE "files"; diff --git a/musicus/migrations/2020-09-27-201047_initial_schema/up.sql b/musicus/migrations/2020-09-27-201047_initial_schema/up.sql index 1983a84..51c17f4 100644 --- a/musicus/migrations/2020-09-27-201047_initial_schema/up.sql +++ b/musicus/migrations/2020-09-27-201047_initial_schema/up.sql @@ -72,11 +72,7 @@ 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") + "work_parts" TEXT NOT NULL, + "path" TEXT NOT NULL ); diff --git a/musicus/res/ui/medium_editor.ui b/musicus/res/ui/medium_editor.ui index 7408cf5..0ad182e 100644 --- a/musicus/res/ui/medium_editor.ui +++ b/musicus/res/ui/medium_editor.ui @@ -2,151 +2,181 @@ - + True - vertical + crossfade - + True - Import music + vertical - + True - True + Import music - + True - go-previous-symbolic - - - - - - - True - True - False - - - True - crossfade + True - + True - True - - - - - True - object-select-symbolic + go-previous-symbolic - - - - end - - - - - - - True - True - True - - - True - + True - 6 - 6 - 6 - vertical + True + False - + True - start - 12 - 6 - Medium - - - - - - - - True - in + crossfade - + True - none - - - True - True - True - Name of the medium - name_entry - - - True - True - center - True - - - - + True + + + + + True + object-select-symbolic + + + + end + + + + + + + True + False + False + + + + + True + True + True + + + True True - horizontal - 12 + 6 + 6 6 + vertical True start - end - True - Recordings + 12 + 6 + Medium - + True - True - none + in - + True - list-add-symbolic + none + + + True + True + True + Name of the medium + name_entry + + + True + True + center + True + + + + + + + True + True + True + Publish to the server + publish_switch + + + True + True + center + True + + + + - - - - - True - in + + + True + horizontal + 12 + 6 + + + True + start + end + True + Recordings + + + + + + + + True + True + none + + + True + list-add-symbolic + + + + + + + + + True + in + + @@ -154,6 +184,18 @@ + + content + + + + + True + True + + + loading + diff --git a/musicus/src/database/files.rs b/musicus/src/database/files.rs deleted file mode 100644 index bc3a254..0000000 --- a/musicus/src/database/files.rs +++ /dev/null @@ -1,54 +0,0 @@ -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> { - let row = files::table - .filter(files::track.eq(track_id)) - .load::(&self.connection)? - .into_iter() - .next(); - - let file_name = match row { - Some(row) => Some(row.file_name), - None => None, - }; - - Ok(file_name) - } -} diff --git a/musicus/src/database/medium.rs b/musicus/src/database/medium.rs index 978975d..f145ccf 100644 --- a/musicus/src/database/medium.rs +++ b/musicus/src/database/medium.rs @@ -1,5 +1,5 @@ use super::generate_id; -use super::schema::{mediums, track_sets, tracks}; +use super::schema::{mediums, recordings, track_sets, tracks}; use super::{Database, Recording}; use anyhow::{anyhow, Error, Result}; use diesel::prelude::*; @@ -41,6 +41,11 @@ pub struct Track { /// The work parts that are played on this track. They are indices to the /// work parts of the work that is associated with the recording. pub work_parts: Vec, + + /// The path to the audio file containing this track. This will not be + /// included when communicating with the server. + #[serde(skip)] + pub path: String, } /// Table data for a [`Medium`]. @@ -70,6 +75,7 @@ struct TrackRow { pub track_set: String, pub index: i32, pub work_parts: String, + pub path: String, } impl Database { @@ -83,7 +89,28 @@ impl Database { // This will also delete the track sets and tracks. self.delete_medium(medium_id)?; + // Add the new medium. + + let medium_row = MediumRow { + id: medium_id.to_owned(), + name: medium.name.clone(), + discid: medium.discid.clone(), + }; + + diesel::insert_into(mediums::table) + .values(medium_row) + .execute(&self.connection)?; + for (index, track_set) in medium.tracks.iter().enumerate() { + // Add associated items from the server, if they don't already + // exist. + + if self.get_recording(&track_set.recording.id)?.is_none() { + self.update_recording(track_set.recording.clone())?; + } + + // Add the actual track set data. + let track_set_id = generate_id(); let track_set_row = TrackSetRow { @@ -110,6 +137,7 @@ impl Database { track_set: track_set_id.clone(), index: index as i32, work_parts, + path: track.path.clone(), }; diesel::insert_into(tracks::table) @@ -147,6 +175,35 @@ impl Database { Ok(()) } + /// Get all tracks for a recording. + pub fn get_tracks(&self, recording_id: &str) -> Result> { + let mut tracks: Vec = Vec::new(); + + let rows = tracks::table + .inner_join(track_sets::table.on(track_sets::id.eq(tracks::track_set))) + .inner_join(recordings::table.on(recordings::id.eq(track_sets::recording))) + .filter(recordings::id.eq(recording_id)) + .select(tracks::table::all_columns()) + .load::(&self.connection)?; + + for row in rows { + let work_parts = row + .work_parts + .split(',') + .map(|part_index| Ok(str::parse(part_index)?)) + .collect::>>()?; + + let track = Track { + work_parts, + path: row.path.clone(), + }; + + tracks.push(track); + } + + Ok(tracks) + } + /// Retrieve all available information on a medium from related tables. fn get_medium_data(&self, row: MediumRow) -> Result { let track_set_rows = track_sets::table @@ -177,7 +234,10 @@ impl Database { .map(|part_index| Ok(str::parse(part_index)?)) .collect::>>()?; - let track = Track { work_parts }; + let track = Track { + work_parts, + path: track_row.path.clone(), + }; tracks.push(track); } diff --git a/musicus/src/database/mod.rs b/musicus/src/database/mod.rs index 952750a..ef87e5a 100644 --- a/musicus/src/database/mod.rs +++ b/musicus/src/database/mod.rs @@ -19,9 +19,6 @@ pub use recordings::*; pub mod thread; pub use thread::*; -pub mod files; -pub use files::*; - pub mod works; pub use works::*; diff --git a/musicus/src/database/schema.rs b/musicus/src/database/schema.rs index d079f6c..b4ea3c4 100644 --- a/musicus/src/database/schema.rs +++ b/musicus/src/database/schema.rs @@ -5,13 +5,6 @@ table! { } } -table! { - files (file_name) { - file_name -> Text, - track -> Text, - } -} - table! { instrumentations (id) { id -> BigInt, @@ -76,6 +69,7 @@ table! { track_set -> Text, index -> Integer, work_parts -> Text, + path -> Text, } } @@ -106,7 +100,6 @@ table! { } } -joinable!(files -> tracks (track)); joinable!(instrumentations -> instruments (instrument)); joinable!(instrumentations -> works (work)); joinable!(performances -> ensembles (ensemble)); @@ -124,7 +117,6 @@ joinable!(works -> persons (composer)); allow_tables_to_appear_in_same_query!( ensembles, - files, instrumentations, instruments, mediums, diff --git a/musicus/src/database/thread.rs b/musicus/src/database/thread.rs index 79b8125..23955e8 100644 --- a/musicus/src/database/thread.rs +++ b/musicus/src/database/thread.rs @@ -31,9 +31,7 @@ enum Action { UpdateMedium(Medium, Sender>), GetMedium(String, Sender>>), DeleteMedium(String, Sender>), - UpdateFile(String, String, Sender>), - DeleteFile(String, Sender>), - GetFile(String, Sender>>), + GetTracks(String, Sender>>), Stop(Sender<()>), } @@ -136,14 +134,8 @@ impl DbThread { 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(); + GetTracks(recording_id, sender) => { + sender.send(db.get_tracks(&recording_id)).unwrap(); } Stop(sender) => { sender.send(()).unwrap(); @@ -347,38 +339,10 @@ impl DbThread { 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<()> { + /// Get all tracks for a recording. + pub async fn get_tracks(&self, recording_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(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> { - let (sender, receiver) = oneshot::channel(); - - self.action_sender - .send(GetFile(track_id.to_owned(), sender))?; - + self.action_sender.send(GetTracks(recording_id.to_owned(), sender))?; receiver.await? } diff --git a/musicus/src/import/disc_source.rs b/musicus/src/import/disc_source.rs index 74ec8eb..c496eb4 100644 --- a/musicus/src/import/disc_source.rs +++ b/musicus/src/import/disc_source.rs @@ -13,6 +13,9 @@ pub struct DiscSource { /// The MusicBrainz DiscID of the CD. pub discid: String, + /// The path to the temporary directory where the audio files will be. + pub path: PathBuf, + /// The tracks on this disc. pub tracks: Vec, } @@ -96,6 +99,7 @@ impl DiscSource { let disc = DiscSource { discid: id, tracks, + path: tmp_dir, }; Ok(disc) diff --git a/musicus/src/import/medium_editor.rs b/musicus/src/import/medium_editor.rs index 4cf39b0..52ebb13 100644 --- a/musicus/src/import/medium_editor.rs +++ b/musicus/src/import/medium_editor.rs @@ -1,8 +1,10 @@ use super::disc_source::DiscSource; use super::track_set_editor::{TrackSetData, TrackSetEditor}; +use crate::database::{generate_id, Medium, Track, TrackSet}; use crate::backend::Backend; use crate::widgets::{Navigator, NavigatorScreen}; use crate::widgets::new_list::List; +use anyhow::Result; use glib::clone; use glib::prelude::*; use gtk::prelude::*; @@ -15,11 +17,12 @@ use std::rc::Rc; pub struct MediumEditor { backend: Rc, source: Rc, - widget: gtk::Box, + widget: gtk::Stack, done_button: gtk::Button, done_stack: gtk::Stack, done: gtk::Image, name_entry: gtk::Entry, + publish_switch: gtk::Switch, track_set_list: List, track_sets: RefCell>, navigator: RefCell>>, @@ -32,12 +35,13 @@ impl MediumEditor { let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/medium_editor.ui"); - get_widget!(builder, gtk::Box, widget); + get_widget!(builder, gtk::Stack, widget); get_widget!(builder, gtk::Button, back_button); get_widget!(builder, gtk::Button, done_button); get_widget!(builder, gtk::Stack, done_stack); get_widget!(builder, gtk::Image, done); get_widget!(builder, gtk::Entry, name_entry); + get_widget!(builder, gtk::Switch, publish_switch); get_widget!(builder, gtk::Button, add_button); get_widget!(builder, gtk::Frame, frame); @@ -52,6 +56,7 @@ impl MediumEditor { done_stack, done, name_entry, + publish_switch, track_set_list: list, track_sets: RefCell::new(Vec::new()), navigator: RefCell::new(None), @@ -66,6 +71,22 @@ impl MediumEditor { } })); + this.done_button.connect_clicked(clone!(@strong this => move |_| { + let context = glib::MainContext::default(); + let clone = this.clone(); + context.spawn_local(async move { + clone.widget.set_visible_child_name("loading"); + match clone.clone().save().await { + Ok(_) => (), + Err(err) => { + println!("{:?}", err); + // clone.info_bar.set_revealed(true); + } + } + + }); + })); + add_button.connect_clicked(clone!(@strong this => move |_| { let navigator = this.navigator.borrow().clone(); if let Some(navigator) = navigator { @@ -130,6 +151,79 @@ impl MediumEditor { this } + + /// Save the medium and possibly upload it to the server. + async fn save(self: Rc) -> Result<()> { + let name = self.name_entry.get_text().to_string(); + + // Create a new directory in the music library path for the imported medium. + + let mut path = self.backend.get_music_library_path().unwrap().clone(); + path.push(&name); + std::fs::create_dir(&path)?; + + // Convert the track set data to real track sets. + + let mut track_sets = Vec::new(); + + for track_set_data in &*self.track_sets.borrow() { + let mut tracks = Vec::new(); + + for track_data in &track_set_data.tracks { + // Copy the corresponding audio file to the music library. + + let track_source = &self.source.tracks[track_data.track_source]; + let file_name = format!("track_{:02}.flac", track_source.number); + + let mut track_path = path.clone(); + track_path.push(&file_name); + + std::fs::copy(&track_source.path, &track_path)?; + + // Create the real track. + + let track = Track { + work_parts: track_data.work_parts.clone(), + path: track_path.to_str().unwrap().to_owned(), + }; + + tracks.push(track); + } + + let track_set = TrackSet { + recording: track_set_data.recording.clone(), + tracks, + }; + + track_sets.push(track_set); + } + + let medium = Medium { + id: generate_id(), + name: self.name_entry.get_text().to_string(), + discid: Some(self.source.discid.clone()), + tracks: track_sets, + }; + + let upload = self.publish_switch.get_active(); + if upload { + // self.backend.post_medium(&medium).await?; + } + + self.backend + .db() + .update_medium(medium.clone()) + .await?; + + self.backend.library_changed(); + + let navigator = self.navigator.borrow().clone(); + if let Some(navigator) = navigator { + navigator.clone().pop(); + } + + Ok(()) + } } impl NavigatorScreen for MediumEditor { diff --git a/musicus/src/import/track_set_editor.rs b/musicus/src/import/track_set_editor.rs index 6cef69b..8b18856 100644 --- a/musicus/src/import/track_set_editor.rs +++ b/musicus/src/import/track_set_editor.rs @@ -141,10 +141,6 @@ impl TrackSetEditor { let mut tracks = Vec::new(); for index in selection { - let track = Track { - work_parts: Vec::new(), - }; - let data = TrackData { track_source: index, work_parts: Vec::new(), diff --git a/musicus/src/meson.build b/musicus/src/meson.build index ae59ef2..60d0bcf 100644 --- a/musicus/src/meson.build +++ b/musicus/src/meson.build @@ -43,7 +43,6 @@ sources = files( 'backend/mod.rs', 'backend/secure.rs', 'database/ensembles.rs', - 'database/files.rs', 'database/instruments.rs', 'database/medium.rs', 'database/mod.rs', diff --git a/musicus/src/screens/recording_screen.rs b/musicus/src/screens/recording_screen.rs index ec94fdb..18540fb 100644 --- a/musicus/src/screens/recording_screen.rs +++ b/musicus/src/screens/recording_screen.rs @@ -76,15 +76,15 @@ impl RecordingScreen { title_label.set_ellipsize(pango::EllipsizeMode::End); title_label.set_halign(gtk::Align::Start); - // let file_name_label = gtk::Label::new(Some(&track.file_name)); - // file_name_label.set_ellipsize(pango::EllipsizeMode::End); - // file_name_label.set_opacity(0.5); - // file_name_label.set_halign(gtk::Align::Start); + let file_name_label = gtk::Label::new(Some(&track.path)); + file_name_label.set_ellipsize(pango::EllipsizeMode::End); + file_name_label.set_opacity(0.5); + file_name_label.set_halign(gtk::Align::Start); let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0); vbox.set_border_width(6); vbox.add(&title_label); - // vbox.add(&file_name_label); + vbox.add(&file_name_label); vbox.upcast() })); @@ -138,16 +138,16 @@ impl RecordingScreen { let context = glib::MainContext::default(); let clone = result.clone(); context.spawn_local(async move { - // let tracks = clone - // .backend - // .db() - // .get_tracks(&clone.recording.id) - // .await - // .unwrap(); + let tracks = clone + .backend + .db() + .get_tracks(&clone.recording.id) + .await + .unwrap(); - // list.show_items(tracks.clone()); - // clone.stack.set_visible_child_name("content"); - // clone.tracks.replace(tracks); + list.show_items(tracks.clone()); + clone.stack.set_visible_child_name("content"); + clone.tracks.replace(tracks); }); result