From 1bc79765be1abca94fa9a247d40949c5675605b3 Mon Sep 17 00:00:00 2001 From: Elias Projahn Date: Sun, 20 Dec 2020 11:47:27 +0100 Subject: [PATCH 01/12] Add support for importing an audio CD --- musicus/Cargo.toml | 1 + .../2020-09-27-201047_initial_schema/down.sql | 31 +-- .../2020-09-27-201047_initial_schema/up.sql | 110 ++++---- musicus/res/musicus.gresource.xml | 7 +- musicus/res/ui/import_disc_dialog.ui | 247 ++++++++++++++++++ musicus/res/ui/import_folder_dialg.ui | 117 +++++++++ musicus/res/ui/window.ui | 4 + musicus/src/database/files.rs | 54 ++++ musicus/src/database/medium.rs | 187 +++++++++++++ musicus/src/database/mod.rs | 7 +- musicus/src/database/recordings.rs | 16 ++ musicus/src/database/schema.rs | 39 ++- musicus/src/database/thread.rs | 89 +++++-- musicus/src/database/tracks.rs | 94 ------- musicus/src/dialogs/import_disc.rs | 211 +++++++++++++++ musicus/src/dialogs/mod.rs | 3 + musicus/src/main.rs | 2 + musicus/src/meson.build | 2 + musicus/src/player.rs | 32 +-- musicus/src/ripper.rs | 130 +++++++++ musicus/src/window.rs | 10 + 21 files changed, 1190 insertions(+), 203 deletions(-) create mode 100644 musicus/res/ui/import_disc_dialog.ui create mode 100644 musicus/res/ui/import_folder_dialg.ui create mode 100644 musicus/src/database/files.rs create mode 100644 musicus/src/database/medium.rs delete mode 100644 musicus/src/database/tracks.rs create mode 100644 musicus/src/dialogs/import_disc.rs create mode 100644 musicus/src/ripper.rs diff --git a/musicus/Cargo.toml b/musicus/Cargo.toml index 5eb4561..2467bf0 100644 --- a/musicus/Cargo.toml +++ b/musicus/Cargo.toml @@ -7,6 +7,7 @@ edition = "2018" anyhow = "1.0.33" diesel = { version = "1.4.5", features = ["sqlite"] } diesel_migrations = "1.4.0" +discid = "0.4.4" fragile = "1.0.0" futures = "0.3.6" futures-channel = "0.3.5" 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 4d21111..0a31654 100644 --- a/musicus/migrations/2020-09-27-201047_initial_schema/down.sql +++ b/musicus/migrations/2020-09-27-201047_initial_schema/down.sql @@ -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; \ No newline at end of file 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 27b97a6..1983a84 100644 --- a/musicus/migrations/2020-09-27-201047_initial_schema/up.sql +++ b/musicus/migrations/2020-09-27-201047_initial_schema/up.sql @@ -1,64 +1,82 @@ -CREATE TABLE persons ( - id TEXT NOT NULL PRIMARY KEY, - first_name TEXT NOT NULL, - last_name TEXT NOT NULL +CREATE TABLE "persons" ( + "id" TEXT NOT NULL PRIMARY KEY, + "first_name" TEXT NOT NULL, + "last_name" TEXT NOT NULL ); -CREATE TABLE instruments ( - id TEXT NOT NULL PRIMARY KEY, - name TEXT NOT NULL +CREATE TABLE "instruments" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL ); -CREATE TABLE works ( - id TEXT NOT NULL PRIMARY KEY, - composer TEXT NOT NULL REFERENCES persons(id), - title TEXT NOT NULL +CREATE TABLE "works" ( + "id" TEXT NOT NULL PRIMARY KEY, + "composer" TEXT NOT NULL REFERENCES "persons"("id"), + "title" TEXT NOT NULL ); -CREATE TABLE instrumentations ( - id BIGINT NOT NULL PRIMARY KEY, - work TEXT NOT NULL REFERENCES works(id) ON DELETE CASCADE, - instrument TEXT NOT NULL REFERENCES instruments(id) ON DELETE CASCADE +CREATE TABLE "instrumentations" ( + "id" BIGINT NOT NULL PRIMARY KEY, + "work" TEXT NOT NULL REFERENCES "works"("id") ON DELETE CASCADE, + "instrument" TEXT NOT NULL REFERENCES "instruments"("id") ON DELETE CASCADE ); -CREATE TABLE work_parts ( - id BIGINT NOT NULL PRIMARY KEY, - work TEXT NOT NULL REFERENCES works(id) ON DELETE CASCADE, - part_index BIGINT NOT NULL, - title TEXT NOT NULL, - composer TEXT REFERENCES persons(id) +CREATE TABLE "work_parts" ( + "id" BIGINT NOT NULL PRIMARY KEY, + "work" TEXT NOT NULL REFERENCES "works"("id") ON DELETE CASCADE, + "part_index" BIGINT NOT NULL, + "title" TEXT NOT NULL, + "composer" TEXT REFERENCES "persons"("id") ); -CREATE TABLE work_sections ( - id BIGINT NOT NULL PRIMARY KEY, - work TEXT NOT NULL REFERENCES works(id) ON DELETE CASCADE, - title TEXT NOT NULL, - before_index BIGINT NOT NULL +CREATE TABLE "work_sections" ( + "id" BIGINT NOT NULL PRIMARY KEY, + "work" TEXT NOT NULL REFERENCES "works"("id") ON DELETE CASCADE, + "title" TEXT NOT NULL, + "before_index" BIGINT NOT NULL ); -CREATE TABLE ensembles ( - id TEXT NOT NULL PRIMARY KEY, - name TEXT NOT NULL +CREATE TABLE "ensembles" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL ); -CREATE TABLE recordings ( - id TEXT NOT NULL PRIMARY KEY, - work TEXT NOT NULL REFERENCES works(id), - comment TEXT NOT NULL +CREATE TABLE "recordings" ( + "id" TEXT NOT NULL PRIMARY KEY, + "work" TEXT NOT NULL REFERENCES "works"("id"), + "comment" TEXT NOT NULL ); -CREATE TABLE performances ( - id BIGINT NOT NULL PRIMARY KEY, - recording TEXT NOT NULL REFERENCES recordings(id) ON DELETE CASCADE, - person TEXT REFERENCES persons(id), - ensemble TEXT REFERENCES ensembles(id), - role TEXT REFERENCES instruments(id) +CREATE TABLE "performances" ( + "id" BIGINT NOT NULL PRIMARY KEY, + "recording" TEXT NOT NULL REFERENCES "recordings"("id") ON DELETE CASCADE, + "person" TEXT REFERENCES "persons"("id"), + "ensemble" TEXT REFERENCES "ensembles"("id"), + "role" TEXT REFERENCES "instruments"("id") +); + +CREATE TABLE "mediums" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "discid" TEXT +); + +CREATE TABLE "track_sets" ( + "id" TEXT NOT NULL PRIMARY KEY, + "medium" TEXT NOT NULL REFERENCES "mediums"("id") ON DELETE CASCADE, + "index" INTEGER NOT NULL, + "recording" TEXT NOT NULL REFERENCES "recordings"("id") +); + +CREATE TABLE "tracks" ( + "id" TEXT NOT NULL PRIMARY KEY, + "track_set" TEXT NOT NULL REFERENCES "track_sets"("id") ON DELETE CASCADE, + "index" INTEGER NOT NULL, + "work_parts" TEXT NOT NULL +); + +CREATE TABLE "files" ( + "file_name" TEXT NOT NULL PRIMARY KEY, + "track" TEXT NOT NULL REFERENCES "tracks"("id") ); -CREATE TABLE tracks ( - id BIGINT NOT NULL PRIMARY KEY, - file_name TEXT NOT NULL, - recording TEXT NOT NULL REFERENCES recordings(id), - track_index INTEGER NOT NULL, - work_parts TEXT NOT NULL -); \ No newline at end of file diff --git a/musicus/res/musicus.gresource.xml b/musicus/res/musicus.gresource.xml index 2a32a46..4ab573a 100644 --- a/musicus/res/musicus.gresource.xml +++ b/musicus/res/musicus.gresource.xml @@ -2,8 +2,13 @@ ui/ensemble_editor.ui - ui/ensemble_selector.ui ui/ensemble_screen.ui + ui/ensemble_selector.ui + ui/import_disc_dialog.ui +<<<<<<< HEAD + ui/import_folder_dialog.ui +======= +>>>>>>> wip/cd-ripping-old ui/instrument_editor.ui ui/instrument_selector.ui ui/login_dialog.ui diff --git a/musicus/res/ui/import_disc_dialog.ui b/musicus/res/ui/import_disc_dialog.ui new file mode 100644 index 0000000..70bea77 --- /dev/null +++ b/musicus/res/ui/import_disc_dialog.ui @@ -0,0 +1,247 @@ + + + + + + + True + False + vertical + + + True + False + Import CD + + + True + True + True + + + True + False + go-previous-symbolic + + + + + + + False + True + 0 + + + + + True + False + crossfade + + + True + False + vertical + + + True + False + error + False + + + False + 6 + end + + + + + + False + False + 0 + + + + + False + 16 + + + True + False + Failed to load the CD. Make sure you have inserted it into your drive. + True + + + False + True + 0 + + + + + False + False + 0 + + + + + + + + False + True + 0 + + + + + True + False + center + center + 18 + vertical + 18 + + + True + False + 0.5019607843137255 + 80 + media-optical-cd-audio-symbolic + + + False + True + 0 + + + + + True + False + 0.5019607843137255 + Import from audio CD + + + + + + False + True + 1 + + + + + True + False + 0.5019607843137255 + 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. + center + True + 40 + + + False + True + 2 + + + + + Import + True + True + True + center + + + + False + True + 3 + + + + + True + True + 1 + + + + + start + + + + + True + False + True + + + loading + 1 + + + + + True + True + + + True + False + none + + + True + False + 500 + 300 + + + True + False + 6 + 6 + 12 + 6 + 0 + in + + + + + + + + + + + + + + + content + 2 + + + + + True + True + 1 + + + + diff --git a/musicus/res/ui/import_folder_dialg.ui b/musicus/res/ui/import_folder_dialg.ui new file mode 100644 index 0000000..6d38f09 --- /dev/null +++ b/musicus/res/ui/import_folder_dialg.ui @@ -0,0 +1,117 @@ + + + + + + + True + False + vertical + + + True + False + Import folder + + + True + True + True + + + True + False + go-previous-symbolic + + + + + + + False + True + 0 + + + + + True + False + center + center + True + 18 + vertical + 18 + + + True + False + 0.50196078431372548 + 80 + folder-symbolic + + + False + True + 0 + + + + + True + False + 0.50196078431372548 + Import from a folder + + + + + + False + True + 1 + + + + + True + False + 0.50196078431372548 + 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. + center + True + 40 + + + False + True + 2 + + + + + Select + True + True + True + center + + + + False + True + 3 + + + + + False + True + 1 + + + + diff --git a/musicus/res/ui/window.ui b/musicus/res/ui/window.ui index daf26ef..633308c 100644 --- a/musicus/res/ui/window.ui +++ b/musicus/res/ui/window.ui @@ -327,6 +327,10 @@
+ + Import CD + win.import-disc + Preferences win.preferences diff --git a/musicus/src/database/files.rs b/musicus/src/database/files.rs new file mode 100644 index 0000000..bc3a254 --- /dev/null +++ b/musicus/src/database/files.rs @@ -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> { + 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 new file mode 100644 index 0000000..de0bb38 --- /dev/null +++ b/musicus/src/database/medium.rs @@ -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, + pub tracks: Vec, +} + +/// 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, +} + +/// A track within a recording on a medium. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Track { + work_parts: Vec, +} + +/// Table data for a [`Medium`]. +#[derive(Insertable, Queryable, Debug, Clone)] +#[table_name = "mediums"] +struct MediumRow { + pub id: String, + pub name: String, + pub discid: Option, +} + +/// 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::>() + .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> { + let row = mediums::table + .filter(mediums::id.eq(id)) + .load::(&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 { + let track_set_rows = track_sets::table + .filter(track_sets::medium.eq(&row.id)) + .order_by(track_sets::index) + .load::(&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::(&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::>>()?; + + 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) + } +} diff --git a/musicus/src/database/mod.rs b/musicus/src/database/mod.rs index a8e94ed..952750a 100644 --- a/musicus/src/database/mod.rs +++ b/musicus/src/database/mod.rs @@ -7,6 +7,9 @@ pub use ensembles::*; pub mod instruments; pub use instruments::*; +pub mod medium; +pub use medium::*; + pub mod persons; pub use persons::*; @@ -16,8 +19,8 @@ pub use recordings::*; pub mod thread; pub use thread::*; -pub mod tracks; -pub use tracks::*; +pub mod files; +pub use files::*; pub mod works; pub use works::*; diff --git a/musicus/src/database/recordings.rs b/musicus/src/database/recordings.rs index 3097d2d..b2ccf00 100644 --- a/musicus/src/database/recordings.rs +++ b/musicus/src/database/recordings.rs @@ -190,6 +190,22 @@ impl Database { Ok(exists) } + /// Get an existing recording. + pub fn get_recording(&self, id: &str) -> Result> { + let row = recordings::table + .filter(recordings::id.eq(id)) + .load::(&self.connection)? + .into_iter() + .next(); + + let recording = match row { + Some(row) => Some(self.get_recording_data(row)?), + None => None, + }; + + Ok(recording) + } + /// Retrieve all available information on a recording from related tables. fn get_recording_data(&self, row: RecordingRow) -> Result { let mut performance_descriptions: Vec = Vec::new(); diff --git a/musicus/src/database/schema.rs b/musicus/src/database/schema.rs index 55a0457..d079f6c 100644 --- a/musicus/src/database/schema.rs +++ b/musicus/src/database/schema.rs @@ -5,6 +5,13 @@ table! { } } +table! { + files (file_name) { + file_name -> Text, + track -> Text, + } +} + table! { instrumentations (id) { id -> BigInt, @@ -20,6 +27,14 @@ table! { } } +table! { + mediums (id) { + id -> Text, + name -> Text, + discid -> Nullable, + } +} + table! { performances (id) { id -> BigInt, @@ -47,11 +62,19 @@ table! { } table! { - tracks (id) { - id -> BigInt, - file_name -> Text, + track_sets (id) { + id -> Text, + medium -> Text, + index -> Integer, recording -> Text, - track_index -> Integer, + } +} + +table! { + tracks (id) { + id -> Text, + track_set -> Text, + index -> Integer, work_parts -> Text, } } @@ -83,6 +106,7 @@ table! { } } +joinable!(files -> tracks (track)); joinable!(instrumentations -> instruments (instrument)); joinable!(instrumentations -> works (work)); joinable!(performances -> ensembles (ensemble)); @@ -90,7 +114,9 @@ joinable!(performances -> instruments (role)); joinable!(performances -> persons (person)); joinable!(performances -> recordings (recording)); joinable!(recordings -> works (work)); -joinable!(tracks -> recordings (recording)); +joinable!(track_sets -> mediums (medium)); +joinable!(track_sets -> recordings (recording)); +joinable!(tracks -> track_sets (track_set)); joinable!(work_parts -> persons (composer)); joinable!(work_parts -> works (work)); joinable!(work_sections -> works (work)); @@ -98,11 +124,14 @@ joinable!(works -> persons (composer)); allow_tables_to_appear_in_same_query!( ensembles, + files, instrumentations, instruments, + mediums, performances, persons, recordings, + track_sets, tracks, work_parts, work_sections, diff --git a/musicus/src/database/thread.rs b/musicus/src/database/thread.rs index 29cd043..79b8125 100644 --- a/musicus/src/database/thread.rs +++ b/musicus/src/database/thread.rs @@ -28,9 +28,12 @@ enum Action { GetRecordingsForEnsemble(String, Sender>>), GetRecordingsForWork(String, Sender>>), RecordingExists(String, Sender>), - UpdateTracks(String, Vec, Sender>), - DeleteTracks(String, Sender>), - GetTracks(String, Sender>>), + UpdateMedium(Medium, Sender>), + GetMedium(String, Sender>>), + DeleteMedium(String, Sender>), + UpdateFile(String, String, Sender>), + DeleteFile(String, Sender>), + GetFile(String, Sender>>), Stop(Sender<()>), } @@ -124,16 +127,23 @@ impl DbThread { RecordingExists(id, sender) => { sender.send(db.recording_exists(&id)).unwrap(); } - UpdateTracks(recording_id, tracks, sender) => { - sender - .send(db.update_tracks(&recording_id, tracks)) - .unwrap(); + UpdateMedium(medium, sender) => { + sender.send(db.update_medium(medium)).unwrap(); } - DeleteTracks(recording_id, sender) => { - sender.send(db.delete_tracks(&recording_id)).unwrap(); + GetMedium(id, sender) => { + sender.send(db.get_medium(&id)).unwrap(); } - GetTracks(recording_id, sender) => { - sender.send(db.get_tracks(&recording_id)).unwrap(); + DeleteMedium(id, sender) => { + sender.send(db.delete_medium(&id)).unwrap(); + } + UpdateFile(file_name, track_id, sender) => { + sender.send(db.update_file(&file_name, &track_id)).unwrap(); + } + DeleteFile(file_name, sender) => { + sender.send(db.delete_file(&file_name)).unwrap(); + } + GetFile(track_id, sender) => { + sender.send(db.get_file(&track_id)).unwrap(); } Stop(sender) => { sender.send(()).unwrap(); @@ -312,28 +322,63 @@ impl DbThread { receiver.await? } - /// Add or change the tracks associated with a recording. This will fail, if there are still - /// other items referencing this recording. - pub async fn update_tracks(&self, recording_id: &str, tracks: Vec) -> Result<()> { + /// Update an existing medium or insert a new one. + pub async fn update_medium(&self, medium: Medium) -> Result<()> { let (sender, receiver) = oneshot::channel(); - self.action_sender - .send(UpdateTracks(recording_id.to_string(), tracks, sender))?; + self.action_sender.send(UpdateMedium(medium, sender))?; receiver.await? } - /// Delete all tracks associated with a recording. - pub async fn delete_tracks(&self, recording_id: &str) -> Result<()> { + /// Delete an existing medium. This will fail, if there are still other + /// items referencing this medium. + pub async fn delete_medium(&self, id: &str) -> Result<()> { let (sender, receiver) = oneshot::channel(); + self.action_sender - .send(DeleteTracks(recording_id.to_string(), sender))?; + .send(DeleteMedium(id.to_owned(), sender))?; + receiver.await? } - /// Get all tracks associated with a recording. - pub async fn get_tracks(&self, recording_id: &str) -> Result> { + /// Get an existing medium. + pub async fn get_medium(&self, id: &str) -> Result> { let (sender, receiver) = oneshot::channel(); + self.action_sender.send(GetMedium(id.to_owned(), sender))?; + receiver.await? + } + + /// Insert or update a file. This assumes that the track is already in the + /// database. + pub async fn update_file(&self, file_name: &str, track_id: &str) -> Result<()> { + let (sender, receiver) = oneshot::channel(); + + self.action_sender.send(UpdateFile( + file_name.to_owned(), + track_id.to_owned(), + sender, + ))?; + + receiver.await? + } + + /// Delete an existing file. This will not delete the file from the file + /// system but just the representing row from the database. + pub async fn delete_file(&self, file_name: &str) -> Result<()> { + let (sender, receiver) = oneshot::channel(); + self.action_sender - .send(GetTracks(recording_id.to_string(), sender))?; + .send(DeleteFile(file_name.to_owned(), sender))?; + + receiver.await? + } + + /// Get the file name of the audio file for the specified track. + pub async fn get_file(&self, track_id: &str) -> Result> { + let (sender, receiver) = oneshot::channel(); + + self.action_sender + .send(GetFile(track_id.to_owned(), sender))?; + receiver.await? } diff --git a/musicus/src/database/tracks.rs b/musicus/src/database/tracks.rs deleted file mode 100644 index f0f8375..0000000 --- a/musicus/src/database/tracks.rs +++ /dev/null @@ -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, - pub file_name: String, -} - -impl TryFrom for Track { - type Error = Error; - fn try_from(row: TrackRow) -> Result { - let mut work_parts = Vec::::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) -> 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::>() - .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> { - let mut tracks = Vec::::new(); - - let rows = tracks::table - .filter(tracks::recording.eq(recording_id)) - .order_by(tracks::track_index) - .load::(&self.connection)?; - - for row in rows { - tracks.push(row.try_into()?); - } - - Ok(tracks) - } -} diff --git a/musicus/src/dialogs/import_disc.rs b/musicus/src/dialogs/import_disc.rs new file mode 100644 index 0000000..38ff99d --- /dev/null +++ b/musicus/src/dialogs/import_disc.rs @@ -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, + widget: gtk::Box, + stack: gtk::Stack, + info_bar: gtk::InfoBar, + list: Rc>, + ripper: Ripper, + tracks: RefCell>, + navigator: RefCell>>, +} + +impl ImportDiscDialog { + /// Create a new import disc dialog. + pub fn new(backend: Rc) -> Rc { + // 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::::new("No tracks found."); + frame.add(&list.widget); + + let mut tmp_dir = glib::get_tmp_dir().unwrap(); + let dir_name = format!("musicus-{}", rand::random::()); + 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::::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) { + self.navigator.replace(Some(navigator)); + } + + fn get_widget(&self) -> gtk::Widget { + self.widget.clone().upcast() + } + + fn detach_navigator(&self) { + self.navigator.replace(None); + } +} diff --git a/musicus/src/dialogs/mod.rs b/musicus/src/dialogs/mod.rs index b244bda..2b363ef 100644 --- a/musicus/src/dialogs/mod.rs +++ b/musicus/src/dialogs/mod.rs @@ -1,3 +1,6 @@ +pub mod import_disc; +pub use import_disc::*; + pub mod about; pub use about::*; diff --git a/musicus/src/main.rs b/musicus/src/main.rs index da2d64f..53a9fcb 100644 --- a/musicus/src/main.rs +++ b/musicus/src/main.rs @@ -12,6 +12,7 @@ use std::cell::RefCell; use std::rc::Rc; mod backend; +mod ripper; mod config; mod database; mod dialogs; @@ -31,6 +32,7 @@ fn main() { gettextrs::bindtextdomain("musicus", config::LOCALEDIR); gettextrs::textdomain("musicus"); + gstreamer::init().expect("Failed to initialize GStreamer!"); gtk::init().expect("Failed to initialize GTK!"); libhandy::init(); resources::init().expect("Failed to initialize resources!"); diff --git a/musicus/src/meson.build b/musicus/src/meson.build index 72ff3cd..f91c62e 100644 --- a/musicus/src/meson.build +++ b/musicus/src/meson.build @@ -52,6 +52,7 @@ sources = files( 'database/tracks.rs', 'database/works.rs', 'dialogs/about.rs', + 'dialogs/import_disc.rs', 'dialogs/login_dialog.rs', 'dialogs/mod.rs', 'dialogs/preferences.rs', @@ -93,6 +94,7 @@ sources = files( 'player.rs', 'resources.rs', 'resources.rs.in', + 'ripper.rs', 'window.rs', ) diff --git a/musicus/src/player.rs b/musicus/src/player.rs index 56d1e7d..6d1a294 100644 --- a/musicus/src/player.rs +++ b/musicus/src/player.rs @@ -8,8 +8,8 @@ use std::rc::Rc; #[derive(Clone)] pub struct PlaylistItem { - pub recording: Recording, - pub tracks: Vec, + pub tracks: TrackSet, + pub indices: Vec, } pub struct Player { @@ -19,11 +19,11 @@ pub struct Player { current_item: Cell>, current_track: Cell>, playing: Cell, - playlist_cbs: RefCell) -> ()>>>, - track_cbs: RefCell ()>>>, - duration_cbs: RefCell ()>>>, - playing_cbs: RefCell ()>>>, - position_cbs: RefCell ()>>>, + playlist_cbs: RefCell)>>>, + track_cbs: RefCell>>, + duration_cbs: RefCell>>, + playing_cbs: RefCell>>, + position_cbs: RefCell>>, } impl Player { @@ -80,23 +80,23 @@ impl Player { result } - pub fn add_playlist_cb) -> () + 'static>(&self, cb: F) { + pub fn add_playlist_cb) + 'static>(&self, cb: F) { self.playlist_cbs.borrow_mut().push(Box::new(cb)); } - pub fn add_track_cb () + 'static>(&self, cb: F) { + pub fn add_track_cb(&self, cb: F) { self.track_cbs.borrow_mut().push(Box::new(cb)); } - pub fn add_duration_cb () + 'static>(&self, cb: F) { + pub fn add_duration_cb(&self, cb: F) { self.duration_cbs.borrow_mut().push(Box::new(cb)); } - pub fn add_playing_cb () + 'static>(&self, cb: F) { + pub fn add_playing_cb(&self, cb: F) { self.playing_cbs.borrow_mut().push(Box::new(cb)); } - pub fn add_position_cb () + 'static>(&self, cb: F) { + pub fn add_position_cb(&self, cb: F) { self.position_cbs.borrow_mut().push(Box::new(cb)); } @@ -121,7 +121,7 @@ impl Player { } pub fn add_item(&self, item: PlaylistItem) -> Result<()> { - if item.tracks.is_empty() { + if item.indices.is_empty() { Err(anyhow!( "Tried to add playlist item without tracks to playlist!" )) @@ -199,7 +199,7 @@ impl Player { current_track -= 1; } else if current_item > 0 { current_item -= 1; - current_track = playlist[current_item].tracks.len() - 1; + current_track = playlist[current_item].indices.len() - 1; } else { return Err(anyhow!("No previous track!")); } @@ -213,7 +213,7 @@ impl Player { let playlist = self.playlist.borrow(); let item = &playlist[current_item]; - current_track + 1 < item.tracks.len() || current_item + 1 < playlist.len() + current_track + 1 < item.indices.len() || current_item + 1 < playlist.len() } else { false } @@ -231,7 +231,7 @@ impl Player { let playlist = self.playlist.borrow(); let item = &playlist[current_item]; - if current_track + 1 < item.tracks.len() { + if current_track + 1 < item.indices.len() { current_track += 1; } else if current_item + 1 < playlist.len() { current_item += 1; diff --git a/musicus/src/ripper.rs b/musicus/src/ripper.rs new file mode 100644 index 0000000..2b3f22f --- /dev/null +++ b/musicus/src/ripper.rs @@ -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>, +} + +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 { + 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 { + 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 { + 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) + } +} diff --git a/musicus/src/window.rs b/musicus/src/window.rs index 066cab4..9cd4bc7 100644 --- a/musicus/src/window.rs +++ b/musicus/src/window.rs @@ -107,6 +107,16 @@ impl Window { result.stack.set_visible_child_name("content"); })); + action!( + result.window, + "import-disc", + clone!(@strong result => move |_, _| { + let dialog = ImportDiscDialog::new(result.backend.clone()); + let window = NavigatorWindow::new(dialog); + window.show(); + }) + ); + action!( result.window, "preferences", From 434a5bbfce0fca0c365a8f2e063a757a60bd7acd Mon Sep 17 00:00:00 2001 From: Elias Projahn Date: Wed, 13 Jan 2021 16:10:48 +0100 Subject: [PATCH 02/12] database: Add more comments to medium --- musicus/src/database/medium.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/musicus/src/database/medium.rs b/musicus/src/database/medium.rs index de0bb38..978975d 100644 --- a/musicus/src/database/medium.rs +++ b/musicus/src/database/medium.rs @@ -10,9 +10,16 @@ use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct Medium { + /// An unique ID for the medium. pub id: String, + + /// The human identifier for the medium. pub name: String, + + /// If applicable, the MusicBrainz DiscID. pub discid: Option, + + /// The tracks of the medium, grouped by recording. pub tracks: Vec, } @@ -20,7 +27,10 @@ pub struct Medium { #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct TrackSet { + /// The recording to which the tracks belong. pub recording: Recording, + + /// The actual tracks. pub tracks: Vec, } @@ -28,7 +38,9 @@ pub struct TrackSet { #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct Track { - work_parts: Vec, + /// 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, } /// Table data for a [`Medium`]. From c7928003e4c214bf58d3fa1a69582c42ef33c83c Mon Sep 17 00:00:00 2001 From: Elias Projahn Date: Wed, 13 Jan 2021 16:12:22 +0100 Subject: [PATCH 03/12] Add UI resources for new dialogs --- musicus/res/musicus.gresource.xml | 7 +- musicus/res/ui/import_dialog.ui | 65 ++++ ...older_dialg.ui => import_folder_dialog.ui} | 46 +-- musicus/res/ui/track_editor.ui | 67 +--- musicus/res/ui/track_selector.ui | 75 +++++ musicus/res/ui/track_set_editor.ui | 153 +++++++++ musicus/res/ui/tracks_editor.ui | 311 ------------------ 7 files changed, 310 insertions(+), 414 deletions(-) create mode 100644 musicus/res/ui/import_dialog.ui rename musicus/res/ui/{import_folder_dialg.ui => import_folder_dialog.ui} (60%) create mode 100644 musicus/res/ui/track_selector.ui create mode 100644 musicus/res/ui/track_set_editor.ui delete mode 100644 musicus/res/ui/tracks_editor.ui diff --git a/musicus/res/musicus.gresource.xml b/musicus/res/musicus.gresource.xml index 4ab573a..4e94aea 100644 --- a/musicus/res/musicus.gresource.xml +++ b/musicus/res/musicus.gresource.xml @@ -4,11 +4,9 @@ ui/ensemble_editor.ui ui/ensemble_screen.ui ui/ensemble_selector.ui + ui/import_dialog.ui ui/import_disc_dialog.ui -<<<<<<< HEAD ui/import_folder_dialog.ui -======= ->>>>>>> wip/cd-ripping-old ui/instrument_editor.ui ui/instrument_selector.ui ui/login_dialog.ui @@ -27,8 +25,9 @@ ui/recording_selector_screen.ui ui/selector.ui ui/server_dialog.ui - ui/tracks_editor.ui ui/track_editor.ui + ui/track_selector.ui + ui/track_set_editor.ui ui/window.ui ui/work_editor.ui ui/work_part_editor.ui diff --git a/musicus/res/ui/import_dialog.ui b/musicus/res/ui/import_dialog.ui new file mode 100644 index 0000000..615b733 --- /dev/null +++ b/musicus/res/ui/import_dialog.ui @@ -0,0 +1,65 @@ + + + + + + True + vertical + + + True + Import music + + + True + True + + + True + go-previous-symbolic + + + + + + + True + True + + + True + list-add-symbolic + + + + + end + + + + + + + True + True + True + + + True + + + True + start + in + 12 + 6 + 6 + 6 + + + + + + + + diff --git a/musicus/res/ui/import_folder_dialg.ui b/musicus/res/ui/import_folder_dialog.ui similarity index 60% rename from musicus/res/ui/import_folder_dialg.ui rename to musicus/res/ui/import_folder_dialog.ui index 6d38f09..1b1ea91 100644 --- a/musicus/res/ui/import_folder_dialg.ui +++ b/musicus/res/ui/import_folder_dialog.ui @@ -1,42 +1,31 @@ - True - False vertical True - False Import folder True True - True True - False go-previous-symbolic - - False - True - 0 - True - False center center True @@ -46,72 +35,43 @@ True - False - 0.50196078431372548 + 0.5 80 folder-symbolic - - False - True - 0 - True - False - 0.50196078431372548 + 0.5 Import from a folder - - False - True - 1 - True - False - 0.50196078431372548 + 0.5 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. center True 40 - - False - True - 2 - Select True True - True center - - False - True - 3 - - - False - True - 1 - diff --git a/musicus/res/ui/track_editor.ui b/musicus/res/ui/track_editor.ui index 592b1af..9657d7d 100644 --- a/musicus/res/ui/track_editor.ui +++ b/musicus/res/ui/track_editor.ui @@ -1,107 +1,67 @@ - - + True - False vertical True - False Track True True - True True - False go-previous-symbolic - + True True - True - - - True - False - object-select-symbolic - - + + + True + object-select-symbolic + + end - 1 - - False - True - 0 - True True + True True - False none True - False - 500 - 300 - + True - False start + 12 6 6 - 12 6 - 0 in - - - True - False - none - - - True - False - 6 - 6 - 6 - 6 - Select a recording of a work with multiple parts. - end - - - - - - - @@ -109,11 +69,6 @@ - - True - True - 1 - diff --git a/musicus/res/ui/track_selector.ui b/musicus/res/ui/track_selector.ui new file mode 100644 index 0000000..9654441 --- /dev/null +++ b/musicus/res/ui/track_selector.ui @@ -0,0 +1,75 @@ + + + + + + True + vertical + + + True + Select tracks + + + True + True + + + True + go-previous-symbolic + + + + + + + True + True + False + + + + True + object-select-symbolic + + + + + end + + + + + + + True + True + True + + + True + none + + + True + + + True + start + 12 + 6 + 6 + 6 + in + + + + + + + + + + diff --git a/musicus/res/ui/track_set_editor.ui b/musicus/res/ui/track_set_editor.ui new file mode 100644 index 0000000..95f0c1a --- /dev/null +++ b/musicus/res/ui/track_set_editor.ui @@ -0,0 +1,153 @@ + + + + + + True + vertical + + + True + Tracks + + + True + True + + + True + go-previous-symbolic + + + + + + + True + True + False + + + + True + object-select-symbolic + + + + + end + + + + + + + True + True + True + + + True + none + + + True + + + True + 6 + 6 + 6 + vertical + + + True + start + 12 + 6 + Recording + + + + + + + + True + in + + + True + none + + + True + True + True + Select a recording + select_recording_button + + + Select + True + True + center + + + + + + + + + + + True + horizontal + 12 + 6 + + + True + start + end + True + Tracks + + + + + + + + True + True + none + + + True + document-edit-symbolic + + + + + + + + + True + in + + + + + + + + + + + + diff --git a/musicus/res/ui/tracks_editor.ui b/musicus/res/ui/tracks_editor.ui deleted file mode 100644 index 4a23e84..0000000 --- a/musicus/res/ui/tracks_editor.ui +++ /dev/null @@ -1,311 +0,0 @@ - - - - - - - True - False - vertical - - - True - False - Tracks - - - True - False - True - True - - - True - False - object-select-symbolic - - - - - - end - - - - - True - True - True - - - True - False - go-previous-symbolic - - - - - 1 - - - - - False - True - 0 - - - - - True - False - True - 800 - 300 - - - - True - False - True - 18 - 12 - 6 - - - True - False - end - Recording - - - 0 - 0 - - - - - True - True - True - True - - - True - False - False - crossfade - True - - - True - False - start - Select … - - - unselected - - - - - True - False - vertical - - - True - False - start - Work - end - - - - - - False - True - 0 - - - - - True - False - 0.5 - start - Performers - end - - - False - True - 1 - - - - - selected - 1 - - - - - - - 1 - 0 - - - - - True - False - True - 6 - - - True - True - in - - - - - - True - True - 0 - - - - - True - False - vertical - 6 - - - True - True - True - - - True - False - list-add-symbolic - - - - - False - True - 0 - - - - - True - True - True - - - True - False - edit-symbolic - - - - - False - True - 2 - - - - - True - True - True - - - True - False - list-remove-symbolic - - - - - False - True - 3 - - - - - True - True - True - - - True - False - go-down-symbolic - - - - - False - True - end - 4 - - - - - True - True - True - - - True - False - go-up-symbolic - - - - - False - True - end - 5 - - - - - False - True - 1 - - - - - 0 - 1 - 2 - - - - - - - False - True - 1 - - - - From 4aa858602d673b761eb33319108843e1eef87bfa Mon Sep 17 00:00:00 2001 From: Elias Projahn Date: Wed, 13 Jan 2021 16:15:13 +0100 Subject: [PATCH 04/12] Replace old track editors with import dialogs --- musicus/src/dialogs/import.rs | 69 +++ musicus/src/dialogs/import_folder.rs | 88 ++++ musicus/src/dialogs/mod.rs | 6 + musicus/src/editors/mod.rs | 10 +- musicus/src/editors/track.rs | 148 ------ musicus/src/editors/track_set.rs | 580 ++++++++++++++++++++++++ musicus/src/editors/track_source.rs | 42 ++ musicus/src/editors/tracks.rs | 337 -------------- musicus/src/meson.build | 9 +- musicus/src/player.rs | 11 +- musicus/src/ripper.rs | 7 - musicus/src/screens/player_screen.rs | 20 +- musicus/src/screens/recording_screen.rs | 48 +- musicus/src/widgets/list.rs | 10 + musicus/src/widgets/player_bar.rs | 8 +- musicus/src/window.rs | 15 +- 16 files changed, 856 insertions(+), 552 deletions(-) create mode 100644 musicus/src/dialogs/import.rs create mode 100644 musicus/src/dialogs/import_folder.rs delete mode 100644 musicus/src/editors/track.rs create mode 100644 musicus/src/editors/track_set.rs create mode 100644 musicus/src/editors/track_source.rs delete mode 100644 musicus/src/editors/tracks.rs diff --git a/musicus/src/dialogs/import.rs b/musicus/src/dialogs/import.rs new file mode 100644 index 0000000..3ab9e03 --- /dev/null +++ b/musicus/src/dialogs/import.rs @@ -0,0 +1,69 @@ +use crate::backend::Backend; +use crate::editors::{TrackSetEditor, TrackSource}; +use crate::widgets::{Navigator, NavigatorScreen}; +use glib::clone; +use gtk::prelude::*; +use gtk_macros::get_widget; +use std::cell::RefCell; +use std::rc::Rc; + +/// A dialog for editing metadata while importing music into the music library. +pub struct ImportDialog { + backend: Rc, + source: Rc, + widget: gtk::Box, + navigator: RefCell>>, +} + +impl ImportDialog { + /// Create a new import dialog. + pub fn new(backend: Rc, source: Rc) -> Rc { + // Create UI + + let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/import_dialog.ui"); + + get_widget!(builder, gtk::Box, widget); + get_widget!(builder, gtk::Button, back_button); + get_widget!(builder, gtk::Button, add_button); + + let this = Rc::new(Self { + backend, + source, + widget, + navigator: RefCell::new(None), + }); + + // Connect signals and callbacks + + back_button.connect_clicked(clone!(@strong this => move |_| { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + navigator.pop(); + } + })); + + add_button.connect_clicked(clone!(@strong this => move |_| { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + let editor = TrackSetEditor::new(this.backend.clone(), this.source.clone()); + navigator.push(editor); + } + })); + + this + } +} + +impl NavigatorScreen for ImportDialog { + fn attach_navigator(&self, navigator: Rc) { + self.navigator.replace(Some(navigator)); + } + + fn get_widget(&self) -> gtk::Widget { + self.widget.clone().upcast() + } + + fn detach_navigator(&self) { + self.navigator.replace(None); + } +} diff --git a/musicus/src/dialogs/import_folder.rs b/musicus/src/dialogs/import_folder.rs new file mode 100644 index 0000000..ca91219 --- /dev/null +++ b/musicus/src/dialogs/import_folder.rs @@ -0,0 +1,88 @@ +use super::ImportDialog; +use crate::backend::Backend; +use crate::editors::TrackSource; +use crate::widgets::{Navigator, NavigatorScreen}; +use glib::clone; +use gtk::prelude::*; +use gtk_macros::get_widget; +use std::cell::RefCell; +use std::rc::Rc; +use std::path::Path; + +/// The initial screen for importing a folder. +pub struct ImportFolderDialog { + backend: Rc, + widget: gtk::Box, + navigator: RefCell>>, +} + +impl ImportFolderDialog { + /// Create a new import folderdialog. + pub fn new(backend: Rc) -> Rc { + // Create UI + + let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/import_folder_dialog.ui"); + + get_widget!(builder, gtk::Box, widget); + get_widget!(builder, gtk::Button, back_button); + get_widget!(builder, gtk::Button, import_button); + + let this = Rc::new(Self { + backend, + widget, + navigator: RefCell::new(None), + }); + + // Connect signals and callbacks + + back_button.connect_clicked(clone!(@strong this => move |_| { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + navigator.pop(); + } + })); + + import_button.connect_clicked(clone!(@strong this => move |_| { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + let chooser = gtk::FileChooserNative::new( + Some("Select folder"), + Some(&navigator.window), + gtk::FileChooserAction::SelectFolder, + None, + None, + ); + + chooser.connect_response(clone!(@strong this => move |chooser, response| { + if response == gtk::ResponseType::Accept { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + let path = chooser.get_filename().unwrap(); + let source = TrackSource::folder(&path).unwrap(); + let dialog = ImportDialog::new(this.backend.clone(), Rc::new(source)); + navigator.push(dialog); + } + } + })); + + chooser.run(); + } + })); + + this + } +} + +impl NavigatorScreen for ImportFolderDialog { + fn attach_navigator(&self, navigator: Rc) { + self.navigator.replace(Some(navigator)); + } + + fn get_widget(&self) -> gtk::Widget { + self.widget.clone().upcast() + } + + fn detach_navigator(&self) { + self.navigator.replace(None); + } +} diff --git a/musicus/src/dialogs/mod.rs b/musicus/src/dialogs/mod.rs index 2b363ef..6f0a8ae 100644 --- a/musicus/src/dialogs/mod.rs +++ b/musicus/src/dialogs/mod.rs @@ -1,3 +1,9 @@ +pub mod import; +pub use import::*; + +pub mod import_folder; +pub use import_folder::*; + pub mod import_disc; pub use import_disc::*; diff --git a/musicus/src/editors/mod.rs b/musicus/src/editors/mod.rs index e171277..133c0a5 100644 --- a/musicus/src/editors/mod.rs +++ b/musicus/src/editors/mod.rs @@ -10,13 +10,15 @@ pub use person::*; pub mod recording; pub use recording::*; -pub mod tracks; -pub use tracks::*; +pub mod track_set; +pub use track_set::*; + +pub mod track_source; +pub use track_source::*; pub mod work; pub use work::*; mod performance; -mod track; mod work_part; -mod work_section; \ No newline at end of file +mod work_section; diff --git a/musicus/src/editors/track.rs b/musicus/src/editors/track.rs deleted file mode 100644 index 5fdff61..0000000 --- a/musicus/src/editors/track.rs +++ /dev/null @@ -1,148 +0,0 @@ -use crate::database::*; -use crate::widgets::{Navigator, NavigatorScreen}; -use glib::clone; -use gtk::prelude::*; -use gtk_macros::get_widget; -use std::cell::RefCell; -use std::convert::TryInto; -use std::rc::Rc; - -/// A screen for editing a single track. -// TODO: Refactor. -pub struct TrackEditor { - widget: gtk::Box, - ready_cb: RefCell ()>>>, - navigator: RefCell>>, -} - -impl TrackEditor { - /// Create a new track editor. - pub fn new(track: Track, work: Work) -> Rc { - // Create UI - - let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_editor.ui"); - - get_widget!(builder, gtk::Box, widget); - get_widget!(builder, gtk::Button, back_button); - get_widget!(builder, gtk::Button, save_button); - get_widget!(builder, gtk::ListBox, list); - - let this = Rc::new(Self { - widget, - ready_cb: RefCell::new(None), - navigator: RefCell::new(None), - }); - - back_button.connect_clicked(clone!(@strong this => move |_| { - let navigator = this.navigator.borrow().clone(); - if let Some(navigator) = navigator { - navigator.pop(); - } - })); - - let work = Rc::new(work); - let work_parts = Rc::new(RefCell::new(track.work_parts)); - let file_name = track.file_name; - - save_button.connect_clicked(clone!(@strong this, @strong work_parts => move |_| { - let mut work_parts = work_parts.borrow_mut(); - work_parts.sort(); - - if let Some(cb) = &*this.ready_cb.borrow() { - cb(Track { - work_parts: work_parts.clone(), - file_name: file_name.clone(), - }); - } - - let navigator = this.navigator.borrow().clone(); - if let Some(navigator) = navigator { - navigator.pop(); - } - })); - - for (index, part) in work.parts.iter().enumerate() { - let check = gtk::CheckButton::new(); - check.set_active(work_parts.borrow().contains(&index)); - check.connect_toggled(clone!(@strong check, @strong work_parts => move |_| { - if check.get_active() { - let mut work_parts = work_parts.borrow_mut(); - work_parts.push(index); - } else { - let mut work_parts = work_parts.borrow_mut(); - if let Some(pos) = work_parts.iter().position(|part| *part == index) { - work_parts.remove(pos); - } - } - })); - - let label = gtk::Label::new(Some(&part.title)); - label.set_halign(gtk::Align::Start); - label.set_ellipsize(pango::EllipsizeMode::End); - - let hbox = gtk::Box::new(gtk::Orientation::Horizontal, 6); - hbox.set_border_width(6); - hbox.add(&check); - hbox.add(&label); - - let row = gtk::ListBoxRow::new(); - row.add(&hbox); - row.show_all(); - - list.add(&row); - list.connect_row_activated( - clone!(@strong row, @strong check => move |_, activated_row| { - if *activated_row == row { - check.activate(); - } - }), - ); - } - - let mut section_count = 0; - for section in &work.sections { - let attributes = pango::AttrList::new(); - attributes.insert(pango::Attribute::new_weight(pango::Weight::Bold).unwrap()); - - let label = gtk::Label::new(Some(§ion.title)); - label.set_halign(gtk::Align::Start); - label.set_ellipsize(pango::EllipsizeMode::End); - label.set_attributes(Some(&attributes)); - let wrap = gtk::Box::new(gtk::Orientation::Vertical, 0); - wrap.set_border_width(6); - wrap.add(&label); - - let row = gtk::ListBoxRow::new(); - row.set_activatable(false); - row.add(&wrap); - row.show_all(); - - list.insert( - &row, - (section.before_index + section_count).try_into().unwrap(), - ); - section_count += 1; - } - - this - } - - /// Set the closure to be called when the track was edited. - pub fn set_ready_cb () + 'static>(&self, cb: F) { - self.ready_cb.replace(Some(Box::new(cb))); - } -} - -impl NavigatorScreen for TrackEditor { - fn attach_navigator(&self, navigator: Rc) { - self.navigator.replace(Some(navigator)); - } - - fn get_widget(&self) -> gtk::Widget { - self.widget.clone().upcast() - } - - fn detach_navigator(&self) { - self.navigator.replace(None); - } -} diff --git a/musicus/src/editors/track_set.rs b/musicus/src/editors/track_set.rs new file mode 100644 index 0000000..716fa82 --- /dev/null +++ b/musicus/src/editors/track_set.rs @@ -0,0 +1,580 @@ +use crate::backend::Backend; +use crate::database::{Recording, Track, TrackSet}; +use crate::selectors::{PersonSelector, RecordingSelector, WorkSelector}; +use crate::widgets::{Navigator, NavigatorScreen}; +use gettextrs::gettext; +use glib::clone; +use gtk::prelude::*; +use gtk_macros::get_widget; +use libhandy::prelude::*; +use std::cell::{Cell, RefCell}; +use std::collections::HashSet; +use std::rc::Rc; + +/// Representation of a track that can be imported into the music library. +#[derive(Debug, Clone)] +struct TrackSource { + /// A short string identifying the track for the user. + pub description: String, + + /// Whether the track is ready to be imported. + pub ready: bool, +} + +/// Representation of a medium that can be imported into the music library. +#[derive(Debug, Clone)] +struct MediumSource { + /// The tracks that can be imported from the medium. + pub tracks: Vec, + + /// Whether all tracks are ready to be imported. + pub ready: bool, +} + +impl MediumSource { + /// Create a dummy medium source for testing purposes. + fn dummy() -> Self { + let mut tracks = Vec::new(); + + for index in 0..20 { + tracks.push(TrackSource { + description: format!("Track {}", index + 1), + ready: Cell::new(true), + }); + } + + Self { + tracks, + ready: Cell::new(true), + } + } +} + +/// A track while being edited. +#[derive(Debug, Clone)] +struct TrackData<'a> { + /// A reference to the selected track source. + pub source: &'a TrackSource, + + /// The actual value for the track. + pub track: Track, +} + +/// A track set while being edited. +#[derive(Debug, Clone)] +struct TrackSetData<'a> { + /// The recording to which the tracks belong. + pub recording: Option, + + /// The tracks that are being edited. + pub tracks: Vec>, +} + +impl TrackSetData { + /// Create a new empty track set. + pub fn new() -> Self { + Self { + recording: None, + tracks: Vec::new(), + } + } +} + +/// A screen for editing a set of tracks for one recording. +pub struct TrackSetEditor { + backend: Rc, + source: Rc>, + widget: gtk::Box, + save_button: gtk::Button, + recording_row: libhandy::ActionRow, + track_list: List, + data: RefCell, + done_cb: RefCell>>, + navigator: RefCell>>, +} + +impl TrackSetEditor { + /// Create a new track set editor. + pub fn new(backend: Rc, source: Rc) -> Rc { + // TODO: Replace with argument. + let source = Rc::new(RefCell::new(MediumSource::dummy())); + + // Create UI + + let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_set_editor.ui"); + + get_widget!(builder, gtk::Box, widget); + get_widget!(builder, gtk::Button, back_button); + get_widget!(builder, gtk::Button, save_button); + get_widget!(builder, libhandy::ActionRow, recording_row); + get_widget!(builder, gtk::Button, select_recording_button); + get_widget!(builder, gtk::Button, edit_tracks_button); + get_widget!(builder, gtk::Frame, tracks_frame); + + let track_list = List::new(&gettext!("No tracks added")); + tracks_frame.add(&track_list.widget); + + let this = Rc::new(Self { + backend, + source, + widget, + save_button, + recording_row, + track_list, + data: RefCell::new(TrackSetData::new()), + done_cb: RefCell::new(None), + 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(); + } + })); + + this.save_button.connect_clicked(clone!(@strong this => move |_| { + if let Some(cb) = &*this.done_cb.borrow() {} + })); + + select_recording_button.connect_clicked(clone!(@strong this => move |_| { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + let person_selector = PersonSelector::new(this.backend.clone()); + + person_selector.set_selected_cb(clone!(@strong this, @strong navigator => move |person| { + let work_selector = WorkSelector::new(this.backend.clone(), person.clone()); + + work_selector.set_selected_cb(clone!(@strong this, @strong navigator => move |work| { + let recording_selector = RecordingSelector::new(this.backend.clone(), work.clone()); + + recording_selector.set_selected_cb(clone!(@strong this, @strong navigator => move |recording| { + let mut data = this.data.borrow_mut(); + data.recording = Some(recording); + this.recording_selected(); + + navigator.clone().pop(); + navigator.clone().pop(); + navigator.clone().pop(); + })); + + navigator.clone().push(recording_selector); + })); + + navigator.clone().push(work_selector); + })); + + navigator.clone().push(person_selector); + } + })); + + edit_tracks_button.connect_clicked(clone!(@strong this => move |_| { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + let selector = TrackSelector::new(Rc::clone(this.source)); + + selector.set_selected_cb(clone!(@strong this => move |selection| { + let mut tracks = Vec::new(); + + for index in selection { + let track = Track { + work_parts: Vec::new(), + }; + + let source = this.source.tracks[index].clone(); + + let data = TrackData { + track, + source, + }; + + tracks.push(data); + } + + let length = tracks.len(); + this.tracks.replace(tracks); + this.track_list.update(length); + this.autofill_parts(); + })); + + navigator.push(selector); + } + })); + + this.track_list.set_make_widget(clone!(@strong this => move |index| { + let data = &this.tracks.borrow()[index]; + + let mut title_parts = Vec::::new(); + + if let Some(recording) = &*this.recording.borrow() { + for part in &data.track.work_parts { + title_parts.push(recording.work.parts[*part].title.clone()); + } + } + + let title = if title_parts.is_empty() { + gettext("Unknown") + } else { + title_parts.join(", ") + }; + + let subtitle = data.source.description.clone(); + + let edit_image = gtk::Image::from_icon_name(Some("document-edit-symbolic"), gtk::IconSize::Button); + let edit_button = gtk::Button::new(); + edit_button.set_relief(gtk::ReliefStyle::None); + edit_button.set_valign(gtk::Align::Center); + edit_button.add(&edit_image); + + let row = libhandy::ActionRow::new(); + row.set_activatable(true); + row.set_title(Some(&title)); + row.set_subtitle(Some(&subtitle)); + row.add(&edit_button); + row.set_activatable_widget(Some(&edit_button)); + row.show_all(); + + edit_button.connect_clicked(clone!(@strong this => move |_| { + let recording = this.recording.borrow().clone(); + let navigator = this.navigator.borrow().clone(); + + if let (Some(recording), Some(navigator)) = (recording, navigator) { + let editor = TrackEditor::new(recording, Vec::new()); + + editor.set_selected_cb(clone!(@strong this => move |selection| { + { + let mut tracks = &mut this.data.borrow_mut().tracks; + let mut track = &mut tracks[index]; + track.track.work_parts = selection; + }; + + this.update_tracks(); + })); + + navigator.push(editor); + } + })); + + row.upcast() + })); + + this + } + + /// Set the closure to be called when the user has created the track set. + pub fn set_done_cb(&self, cb: F) { + self.done_cb.replace(Some(Box::new(cb))); + } + + /// Set everything up after selecting a recording. + fn recording_selected(&self) { + if let Some(recording) = self.data.borrow().recording { + self.recording_row.set_title(Some(&recording.work.get_title())); + self.recording_row.set_subtitle(Some(&recording.get_performers())); + self.save_button.set_sensitive(true); + } + + self.autofill_parts(); + } + + /// Automatically try to put work part information from the selected recording into the + /// selected tracks. + fn autofill_parts(&self) { + if let Some(recording) = self.data.borrow().recording { + let mut tracks = self.tracks.borrow_mut(); + + for (index, _) in recording.work.parts.iter().enumerate() { + if let Some(mut data) = tracks.get_mut(index) { + data.track.work_parts = vec![index]; + } else { + break; + } + } + } + + self.update_tracks(); + } + + /// Update the track list. + fn update_tracks(&self) { + let length = self.data.borrow().tracks.len(); + self.track_list.update(length); + } +} + +impl NavigatorScreen for TrackSetEditor { + fn attach_navigator(&self, navigator: Rc) { + self.navigator.replace(Some(navigator)); + } + + fn get_widget(&self) -> gtk::Widget { + self.widget.clone().upcast() + } + + fn detach_navigator(&self) { + self.navigator.replace(None); + } +} + + +/// A screen for selecting tracks from a medium. +struct TrackSelector { + source: Rc>, + widget: gtk::Box, + select_button: gtk::Button, + selection: RefCell>, + selected_cb: RefCell)>>>, + navigator: RefCell>>, +} + +impl TrackSelector { + /// Create a new track selector. + pub fn new(source: Rc>) -> Rc { + // Create UI + + let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_selector.ui"); + + get_widget!(builder, gtk::Box, widget); + get_widget!(builder, gtk::Button, back_button); + get_widget!(builder, gtk::Button, select_button); + get_widget!(builder, gtk::Frame, tracks_frame); + + let track_list = gtk::ListBox::new(); + track_list.set_selection_mode(gtk::SelectionMode::None); + track_list.set_vexpand(false); + track_list.show(); + tracks_frame.add(&track_list); + + let this = Rc::new(Self { + source, + widget, + select_button, + selection: RefCell::new(Vec::new()), + selected_cb: RefCell::new(None), + 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(); + } + })); + + this.select_button.connect_clicked(clone!(@strong this => move |_| { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + navigator.pop(); + } + + if let Some(cb) = &*this.selected_cb.borrow() { + let selection = this.selection.borrow().clone(); + cb(selection); + } + })); + + for (index, track) in this.tracks.iter().enumerate() { + let check = gtk::CheckButton::new(); + + check.connect_toggled(clone!(@strong this => move |check| { + let mut selection = this.selection.borrow_mut(); + if check.get_active() { + selection.push(index); + } else { + if let Some(pos) = selection.iter().position(|part| *part == index) { + selection.remove(pos); + } + } + + if selection.is_empty() { + this.select_button.set_sensitive(false); + } else { + this.select_button.set_sensitive(true); + } + })); + + let row = libhandy::ActionRow::new(); + row.add_prefix(&check); + row.set_activatable_widget(Some(&check)); + row.set_title(Some(&track.description)); + row.show_all(); + + track_list.add(&row); + } + + this + } + + /// Set the closure to be called when the user has selected tracks. The + /// closure will be called with the indices of the selected tracks as its + /// argument. + pub fn set_selected_cb) + 'static>(&self, cb: F) { + self.selected_cb.replace(Some(Box::new(cb))); + } +} + +impl NavigatorScreen for TrackSelector { + fn attach_navigator(&self, navigator: Rc) { + self.navigator.replace(Some(navigator)); + } + + fn get_widget(&self) -> gtk::Widget { + self.widget.clone().upcast() + } + + fn detach_navigator(&self) { + self.navigator.replace(None); + } +} + +/// A screen for editing a single track. +struct TrackEditor { + widget: gtk::Box, + selection: RefCell>, + selected_cb: RefCell)>>>, + navigator: RefCell>>, +} + +impl TrackEditor { + /// Create a new track editor. + pub fn new(recording: Recording, selection: Vec) -> Rc { + // Create UI + + let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_editor.ui"); + + get_widget!(builder, gtk::Box, widget); + get_widget!(builder, gtk::Button, back_button); + get_widget!(builder, gtk::Button, select_button); + get_widget!(builder, gtk::Frame, parts_frame); + + let parts_list = gtk::ListBox::new(); + parts_list.set_selection_mode(gtk::SelectionMode::None); + parts_list.set_vexpand(false); + parts_list.show(); + parts_frame.add(&parts_list); + + let this = Rc::new(Self { + widget, + selection: RefCell::new(selection), + selected_cb: RefCell::new(None), + 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(); + } + })); + + select_button.connect_clicked(clone!(@strong this => move |_| { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + navigator.pop(); + } + + if let Some(cb) = &*this.selected_cb.borrow() { + let selection = this.selection.borrow().clone(); + cb(selection); + } + })); + + for (index, part) in recording.work.parts.iter().enumerate() { + let check = gtk::CheckButton::new(); + + check.connect_toggled(clone!(@strong this => move |check| { + let mut selection = this.selection.borrow_mut(); + if check.get_active() { + selection.push(index); + } else { + if let Some(pos) = selection.iter().position(|part| *part == index) { + selection.remove(pos); + } + } + })); + + let row = libhandy::ActionRow::new(); + row.add_prefix(&check); + row.set_activatable_widget(Some(&check)); + row.set_title(Some(&part.title)); + row.show_all(); + + parts_list.add(&row); + } + + this + } + + /// Set the closure to be called when the user has edited the track. + pub fn set_selected_cb) + 'static>(&self, cb: F) { + self.selected_cb.replace(Some(Box::new(cb))); + } +} + +impl NavigatorScreen for TrackEditor { + fn attach_navigator(&self, navigator: Rc) { + self.navigator.replace(Some(navigator)); + } + + fn get_widget(&self) -> gtk::Widget { + self.widget.clone().upcast() + } + + fn detach_navigator(&self) { + self.navigator.replace(None); + } +} + +/// A simple list of widgets. +struct List { + pub widget: gtk::ListBox, + make_widget: RefCell gtk::Widget>>>, +} + +impl List { + /// Create a new list. The list will be empty. + pub fn new(placeholder_text: &str) -> Self { + let placeholder_label = gtk::Label::new(Some(placeholder_text)); + placeholder_label.set_margin_top(6); + placeholder_label.set_margin_bottom(6); + placeholder_label.set_margin_start(6); + placeholder_label.set_margin_end(6); + placeholder_label.show(); + + let widget = gtk::ListBox::new(); + widget.set_selection_mode(gtk::SelectionMode::None); + widget.set_placeholder(Some(&placeholder_label)); + widget.show(); + + Self { + widget, + make_widget: RefCell::new(None), + } + } + + /// Set the closure to be called to construct widgets for the items. + pub fn set_make_widget gtk::Widget + 'static>(&self, make_widget: F) { + self.make_widget.replace(Some(Box::new(make_widget))); + } + + /// Call the make_widget function for each item. This will automatically + /// show all children by indices 0..length. + pub fn update(&self, length: usize) { + for child in self.widget.get_children() { + self.widget.remove(&child); + } + + if let Some(make_widget) = &*self.make_widget.borrow() { + for index in 0..length { + let row = make_widget(index); + self.widget.insert(&row, -1); + } + } + } +} diff --git a/musicus/src/editors/track_source.rs b/musicus/src/editors/track_source.rs new file mode 100644 index 0000000..3f09d05 --- /dev/null +++ b/musicus/src/editors/track_source.rs @@ -0,0 +1,42 @@ +use anyhow::Result; +use std::cell::Cell; +use std::path::Path; + +/// One track within a [`TrackSource`]. +#[derive(Debug, Clone)] +pub struct TrackState { + pub description: String, +} + +/// A live representation of a source of audio tracks. +pub struct TrackSource { + pub tracks: Vec, + pub ready: Cell, +} + +impl TrackSource { + /// Create a new track source for a folder. This will provide the folder's + /// files as selectable tracks and be ready immediately. + pub fn folder(path: &Path) -> Result { + let mut tracks = Vec::::new(); + + let entries = std::fs::read_dir(path)?; + for entry in entries { + let entry = entry?; + if entry.file_type()?.is_file() { + let file_name = entry.file_name(); + let track = TrackState { description: file_name.to_str().unwrap().to_owned() }; + tracks.push(track); + } + } + + tracks.sort_unstable_by(|a, b| { + a.description.cmp(&b.description) + }); + + Ok(Self { + tracks, + ready: Cell::new(true), + }) + } +} diff --git a/musicus/src/editors/tracks.rs b/musicus/src/editors/tracks.rs deleted file mode 100644 index b373b40..0000000 --- a/musicus/src/editors/tracks.rs +++ /dev/null @@ -1,337 +0,0 @@ -use super::track::TrackEditor; -use crate::backend::Backend; -use crate::database::*; -use crate::widgets::{List, Navigator, NavigatorScreen}; -use crate::selectors::{PersonSelector, WorkSelector, RecordingSelector}; -use gettextrs::gettext; -use glib::clone; -use gtk::prelude::*; -use gtk_macros::get_widget; -use std::cell::RefCell; -use std::rc::Rc; - -/// A dialog for editing a set of tracks. -// TODO: Disable buttons if no track is selected. -pub struct TracksEditor { - backend: Rc, - widget: gtk::Box, - save_button: gtk::Button, - recording_stack: gtk::Stack, - work_label: gtk::Label, - performers_label: gtk::Label, - track_list: Rc>, - recording: RefCell>, - tracks: RefCell>, - callback: RefCell ()>>>, - navigator: RefCell>>, -} - -impl TracksEditor { - /// Create a new track editor an optionally initialize it with a recording and a list of - /// tracks. - pub fn new( - backend: Rc, - recording: Option, - tracks: Vec, - ) -> Rc { - // UI setup - - let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/tracks_editor.ui"); - - get_widget!(builder, gtk::Box, widget); - get_widget!(builder, gtk::Button, back_button); - get_widget!(builder, gtk::Button, save_button); - get_widget!(builder, gtk::Button, recording_button); - get_widget!(builder, gtk::Stack, recording_stack); - get_widget!(builder, gtk::Label, work_label); - get_widget!(builder, gtk::Label, performers_label); - get_widget!(builder, gtk::ScrolledWindow, scroll); - get_widget!(builder, gtk::Button, add_track_button); - get_widget!(builder, gtk::Button, edit_track_button); - get_widget!(builder, gtk::Button, remove_track_button); - get_widget!(builder, gtk::Button, move_track_up_button); - get_widget!(builder, gtk::Button, move_track_down_button); - - let track_list = List::new(&gettext("Add some tracks.")); - scroll.add(&track_list.widget); - - let this = Rc::new(Self { - backend, - widget, - save_button, - recording_stack, - work_label, - performers_label, - track_list, - recording: RefCell::new(recording), - tracks: RefCell::new(tracks), - callback: RefCell::new(None), - navigator: RefCell::new(None), - }); - - // Signals and callbacks - - back_button.connect_clicked(clone!(@strong this => move |_| { - let navigator = this.navigator.borrow().clone(); - if let Some(navigator) = navigator { - navigator.pop(); - } - })); - - this.save_button - .connect_clicked(clone!(@strong this => move |_| { - let context = glib::MainContext::default(); - let this = this.clone(); - context.spawn_local(async move { - let recording = this.recording.borrow().as_ref().unwrap().clone(); - - // Add the recording first, if it's from the server. - - if !this.backend.db().recording_exists(&recording.id).await.unwrap() { - this.backend.db().update_recording(recording.clone()).await.unwrap(); - } - - // Add the actual tracks. - - this.backend.db().update_tracks( - &recording.id, - this.tracks.borrow().clone(), - ).await.unwrap(); - - if let Some(callback) = &*this.callback.borrow() { - callback(); - } - - let navigator = this.navigator.borrow().clone(); - if let Some(navigator) = navigator { - navigator.pop(); - } - }); - - })); - - recording_button.connect_clicked(clone!(@strong this => move |_| { - let navigator = this.navigator.borrow().clone(); - if let Some(navigator) = navigator { - let person_selector = PersonSelector::new(this.backend.clone()); - - person_selector.set_selected_cb(clone!(@strong this, @strong navigator => move |person| { - let work_selector = WorkSelector::new(this.backend.clone(), person.clone()); - - work_selector.set_selected_cb(clone!(@strong this, @strong navigator => move |work| { - let recording_selector = RecordingSelector::new(this.backend.clone(), work.clone()); - - recording_selector.set_selected_cb(clone!(@strong this, @strong navigator => move |recording| { - this.recording_selected(recording); - this.recording.replace(Some(recording.clone())); - - navigator.clone().pop(); - navigator.clone().pop(); - navigator.clone().pop(); - })); - - navigator.clone().push(recording_selector); - })); - - navigator.clone().push(work_selector); - })); - - navigator.clone().push(person_selector); - } - })); - - this.track_list - .set_make_widget(clone!(@strong this => move |track| { - this.build_track_row(track) - })); - - add_track_button.connect_clicked(clone!(@strong this => move |_| { - let navigator = this.navigator.borrow().clone(); - if let Some(navigator) = navigator { - let music_library_path = this.backend.get_music_library_path().unwrap(); - - let dialog = gtk::FileChooserNative::new( - Some(&gettext("Select audio files")), - Some(&navigator.window), - gtk::FileChooserAction::Open, - None, - None, - ); - - dialog.set_select_multiple(true); - dialog.set_current_folder(&music_library_path); - - if let gtk::ResponseType::Accept = dialog.run() { - let mut index = match this.track_list.get_selected_index() { - Some(index) => index + 1, - None => this.tracks.borrow().len(), - }; - - { - let mut tracks = this.tracks.borrow_mut(); - for file_name in dialog.get_filenames() { - let file_name = file_name.strip_prefix(&music_library_path).unwrap(); - tracks.insert(index, Track { - work_parts: Vec::new(), - file_name: String::from(file_name.to_str().unwrap()), - }); - index += 1; - } - } - - this.track_list.show_items(this.tracks.borrow().clone()); - this.autofill_parts(); - this.track_list.select_index(index); - } - } - })); - - remove_track_button.connect_clicked(clone!(@strong this => move |_| { - match this.track_list.get_selected_index() { - Some(index) => { - let mut tracks = this.tracks.borrow_mut(); - tracks.remove(index); - this.track_list.show_items(tracks.clone()); - this.track_list.select_index(index); - } - None => (), - } - })); - - move_track_up_button.connect_clicked(clone!(@strong this => move |_| { - match this.track_list.get_selected_index() { - Some(index) => { - if index > 0 { - let mut tracks = this.tracks.borrow_mut(); - tracks.swap(index - 1, index); - this.track_list.show_items(tracks.clone()); - this.track_list.select_index(index - 1); - } - } - None => (), - } - })); - - move_track_down_button.connect_clicked(clone!(@strong this => move |_| { - match this.track_list.get_selected_index() { - Some(index) => { - let mut tracks = this.tracks.borrow_mut(); - if index < tracks.len() - 1 { - tracks.swap(index, index + 1); - this.track_list.show_items(tracks.clone()); - this.track_list.select_index(index + 1); - } - } - None => (), - } - })); - - edit_track_button.connect_clicked(clone!(@strong this => move |_| { - let navigator = this.navigator.borrow().clone(); - if let Some(navigator) = navigator { - if let Some(index) = this.track_list.get_selected_index() { - if let Some(recording) = &*this.recording.borrow() { - let editor = TrackEditor::new(this.tracks.borrow()[index].clone(), recording.work.clone()); - - editor.set_ready_cb(clone!(@strong this => move |track| { - let mut tracks = this.tracks.borrow_mut(); - tracks[index] = track; - this.track_list.show_items(tracks.clone()); - this.track_list.select_index(index); - })); - - navigator.push(editor); - } - } - } - })); - - // Initialization - - if let Some(recording) = &*this.recording.borrow() { - this.recording_selected(recording); - } - - this.track_list.show_items(this.tracks.borrow().clone()); - - this - } - - /// Set a callback to be called when the tracks are saved. - pub fn set_callback () + 'static>(&self, cb: F) { - self.callback.replace(Some(Box::new(cb))); - } - - /// Create a widget representing a track. - fn build_track_row(&self, track: &Track) -> gtk::Widget { - let mut title_parts = Vec::::new(); - for part in &track.work_parts { - if let Some(recording) = &*self.recording.borrow() { - title_parts.push(recording.work.parts[*part].title.clone()); - } - } - - let title = if title_parts.is_empty() { - gettext("Unknown") - } else { - title_parts.join(", ") - }; - - let title_label = gtk::Label::new(Some(&title)); - 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 vbox = gtk::Box::new(gtk::Orientation::Vertical, 0); - vbox.set_border_width(6); - vbox.add(&title_label); - vbox.add(&file_name_label); - - vbox.upcast() - } - - /// Set everything up after selecting a recording. - fn recording_selected(&self, recording: &Recording) { - self.work_label.set_text(&recording.work.get_title()); - self.performers_label.set_text(&recording.get_performers()); - self.recording_stack.set_visible_child_name("selected"); - self.save_button.set_sensitive(true); - self.autofill_parts(); - } - - /// Automatically try to put work part information from the selected recording into the - /// selected tracks. - fn autofill_parts(&self) { - if let Some(recording) = &*self.recording.borrow() { - let mut tracks = self.tracks.borrow_mut(); - - for (index, _) in recording.work.parts.iter().enumerate() { - if let Some(mut track) = tracks.get_mut(index) { - track.work_parts = vec![index]; - } else { - break; - } - } - - self.track_list.show_items(tracks.clone()); - } - } -} - -impl NavigatorScreen for TracksEditor { - fn attach_navigator(&self, navigator: Rc) { - self.navigator.replace(Some(navigator)); - } - - fn get_widget(&self) -> gtk::Widget { - self.widget.clone().upcast() - } - - fn detach_navigator(&self) { - self.navigator.replace(None); - } -} diff --git a/musicus/src/meson.build b/musicus/src/meson.build index f91c62e..363f0ee 100644 --- a/musicus/src/meson.build +++ b/musicus/src/meson.build @@ -33,9 +33,9 @@ run_command( ) sources = files( - 'backend/client/mod.rs', 'backend/client/ensembles.rs', 'backend/client/instruments.rs', + 'backend/client/mod.rs', 'backend/client/persons.rs', 'backend/client/recordings.rs', 'backend/client/works.rs', @@ -43,16 +43,18 @@ sources = files( 'backend/mod.rs', 'backend/secure.rs', 'database/ensembles.rs', + 'database/files.rs', 'database/instruments.rs', + 'database/medium.rs', 'database/mod.rs', 'database/persons.rs', 'database/recordings.rs', 'database/schema.rs', 'database/thread.rs', - 'database/tracks.rs', 'database/works.rs', 'dialogs/about.rs', 'dialogs/import_disc.rs', + 'dialogs/import_folder.rs', 'dialogs/login_dialog.rs', 'dialogs/mod.rs', 'dialogs/preferences.rs', @@ -63,8 +65,7 @@ sources = files( 'editors/performance.rs', 'editors/person.rs', 'editors/recording.rs', - 'editors/track.rs', - 'editors/tracks.rs', + 'editors/track_set.rs', 'editors/work.rs', 'editors/work_part.rs', 'editors/work_section.rs', diff --git a/musicus/src/player.rs b/musicus/src/player.rs index 6d1a294..f59371b 100644 --- a/musicus/src/player.rs +++ b/musicus/src/player.rs @@ -9,6 +9,7 @@ use std::rc::Rc; #[derive(Clone)] pub struct PlaylistItem { pub tracks: TrackSet, + pub file_names: Vec, pub indices: Vec, } @@ -248,15 +249,7 @@ impl Player { "file://{}", self.music_library_path .join( - self.playlist - .borrow() - .get(current_item) - .ok_or(anyhow!("Playlist item doesn't exist!"))? - .tracks - .get(current_track) - .ok_or(anyhow!("Track doesn't exist!"))? - .file_name - .clone(), + self.playlist.borrow()[current_item].file_names[current_track].clone(), ) .to_str() .unwrap(), diff --git a/musicus/src/ripper.rs b/musicus/src/ripper.rs index 2b3f22f..bbdbde5 100644 --- a/musicus/src/ripper.rs +++ b/musicus/src/ripper.rs @@ -104,13 +104,6 @@ impl Ripper { /// Build the GStreamer pipeline to rip a track. fn build_pipeline(path: &str, track: u32) -> Result { 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)?; diff --git a/musicus/src/screens/player_screen.rs b/musicus/src/screens/player_screen.rs index 3264e7d..8a258e2 100644 --- a/musicus/src/screens/player_screen.rs +++ b/musicus/src/screens/player_screen.rs @@ -215,15 +215,17 @@ impl PlayerScreen { elements.push(PlaylistElement { item: item_index, track: 0, - title: item.recording.work.get_title(), - subtitle: Some(item.recording.get_performers()), + title: item.tracks.recording.work.get_title(), + subtitle: Some(item.tracks.recording.get_performers()), playable: false, }); - for (track_index, track) in item.tracks.iter().enumerate() { + for track_index in &item.indices { + let track = &item.tracks.tracks[*track_index]; + let mut parts = Vec::::new(); for part in &track.work_parts { - parts.push(item.recording.work.parts[*part].title.clone()); + parts.push(item.tracks.recording.work.parts[*part].title.clone()); } let title = if parts.is_empty() { @@ -234,7 +236,7 @@ impl PlayerScreen { elements.push(PlaylistElement { item: item_index, - track: track_index, + track: *track_index, title: title, subtitle: None, playable: true, @@ -262,20 +264,20 @@ impl PlayerScreen { next_button.set_sensitive(player.has_next()); let item = &playlist.borrow()[current_item]; - let track = &item.tracks[current_track]; + let track = &item.tracks.tracks[current_track]; let mut parts = Vec::::new(); for part in &track.work_parts { - parts.push(item.recording.work.parts[*part].title.clone()); + parts.push(item.tracks.recording.work.parts[*part].title.clone()); } - let mut title = item.recording.work.get_title(); + let mut title = item.tracks.recording.work.get_title(); if !parts.is_empty() { title = format!("{}: {}", title, parts.join(", ")); } title_label.set_text(&title); - subtitle_label.set_text(&item.recording.get_performers()); + subtitle_label.set_text(&item.tracks.recording.get_performers()); position_label.set_text("0:00"); self_item.replace(current_item); diff --git a/musicus/src/screens/recording_screen.rs b/musicus/src/screens/recording_screen.rs index fa0789e..ec94fdb 100644 --- a/musicus/src/screens/recording_screen.rs +++ b/musicus/src/screens/recording_screen.rs @@ -1,6 +1,6 @@ use crate::backend::*; use crate::database::*; -use crate::editors::{RecordingEditor, TracksEditor}; +use crate::editors::RecordingEditor; use crate::player::*; use crate::widgets::{List, Navigator, NavigatorScreen, NavigatorWindow}; use gettextrs::gettext; @@ -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.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 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() })); @@ -98,10 +98,10 @@ impl RecordingScreen { add_to_playlist_button.connect_clicked(clone!(@strong result => move |_| { if let Some(player) = result.backend.get_player() { - player.add_item(PlaylistItem { - recording: result.recording.clone(), - tracks: result.tracks.borrow().clone(), - }).unwrap(); + // player.add_item(PlaylistItem { + // recording: result.recording.clone(), + // tracks: result.tracks.borrow().clone(), + // }).unwrap(); } })); @@ -121,33 +121,33 @@ impl RecordingScreen { })); edit_tracks_action.connect_activate(clone!(@strong result => move |_, _| { - let editor = TracksEditor::new(result.backend.clone(), Some(result.recording.clone()), result.tracks.borrow().clone()); - let window = NavigatorWindow::new(editor); - window.show(); + // let editor = TracksEditor::new(result.backend.clone(), Some(result.recording.clone()), result.tracks.borrow().clone()); + // let window = NavigatorWindow::new(editor); + // window.show(); })); delete_tracks_action.connect_activate(clone!(@strong result => move |_, _| { let context = glib::MainContext::default(); let clone = result.clone(); context.spawn_local(async move { - clone.backend.db().delete_tracks(&clone.recording.id).await.unwrap(); - clone.backend.library_changed(); + // clone.backend.db().delete_tracks(&clone.recording.id).await.unwrap(); + // clone.backend.library_changed(); }); })); 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 diff --git a/musicus/src/widgets/list.rs b/musicus/src/widgets/list.rs index e781f17..890f866 100644 --- a/musicus/src/widgets/list.rs +++ b/musicus/src/widgets/list.rs @@ -63,6 +63,16 @@ where this } + pub fn set_selectable(&self, selectable: bool) { + let mode = if selectable { + gtk::SelectionMode::Single + } else { + gtk::SelectionMode::None + }; + + self.widget.set_selection_mode(mode); + } + pub fn set_make_widget gtk::Widget + 'static>(&self, make_widget: F) { self.make_widget.replace(Some(Box::new(make_widget))); } diff --git a/musicus/src/widgets/player_bar.rs b/musicus/src/widgets/player_bar.rs index 9d2cf90..c84d11b 100644 --- a/musicus/src/widgets/player_bar.rs +++ b/musicus/src/widgets/player_bar.rs @@ -112,20 +112,20 @@ impl PlayerBar { next_button.set_sensitive(player.has_next()); let item = &playlist.borrow()[current_item]; - let track = &item.tracks[current_track]; + let track = &item.tracks.tracks[current_track]; let mut parts = Vec::::new(); for part in &track.work_parts { - parts.push(item.recording.work.parts[*part].title.clone()); + parts.push(item.tracks.recording.work.parts[*part].title.clone()); } - let mut title = item.recording.work.get_title(); + let mut title = item.tracks.recording.work.get_title(); if !parts.is_empty() { title = format!("{}: {}", title, parts.join(", ")); } title_label.set_text(&title); - subtitle_label.set_text(&item.recording.get_performers()); + subtitle_label.set_text(&item.tracks.recording.get_performers()); position_label.set_text("0:00"); } )); diff --git a/musicus/src/window.rs b/musicus/src/window.rs index 9cd4bc7..2d3224c 100644 --- a/musicus/src/window.rs +++ b/musicus/src/window.rs @@ -1,6 +1,5 @@ use crate::backend::*; use crate::dialogs::*; -use crate::editors::TracksEditor; use crate::screens::*; use crate::widgets::*; use futures::prelude::*; @@ -85,13 +84,17 @@ impl Window { })); add_button.connect_clicked(clone!(@strong result => move |_| { - let editor = TracksEditor::new(result.backend.clone(), None, Vec::new()); + // let editor = TracksEditor::new(result.backend.clone(), None, Vec::new()); - editor.set_callback(clone!(@strong result => move || { - result.reload(); - })); + // editor.set_callback(clone!(@strong result => move || { + // result.reload(); + // })); - let window = NavigatorWindow::new(editor); + // let window = NavigatorWindow::new(editor); + // window.show(); + + let dialog = ImportFolderDialog::new(result.backend.clone()); + let window = NavigatorWindow::new(dialog); window.show(); })); From 18600c310f38c74f87acac0978f6716c1cd74edc Mon Sep 17 00:00:00 2001 From: Elias Projahn Date: Wed, 13 Jan 2021 17:51:00 +0100 Subject: [PATCH 05/12] Initial ripping from new source selector --- musicus/res/musicus.gresource.xml | 5 +- musicus/res/ui/import_folder_dialog.ui | 77 ------- .../ui/{import_dialog.ui => medium_editor.ui} | 0 ...port_disc_dialog.ui => source_selector.ui} | 90 +------- musicus/src/dialogs/import.rs | 69 ------ musicus/src/dialogs/import_disc.rs | 211 ------------------ musicus/src/dialogs/import_folder.rs | 88 -------- musicus/src/dialogs/mod.rs | 9 - musicus/src/editors/mod.rs | 6 - musicus/src/import/disc_source.rs | 171 ++++++++++++++ musicus/src/import/medium_editor.rs | 0 musicus/src/import/mod.rs | 5 + musicus/src/import/source_selector.rs | 85 +++++++ musicus/src/main.rs | 2 +- musicus/src/meson.build | 3 - musicus/src/ripper.rs | 123 ---------- musicus/src/window.rs | 21 +- 17 files changed, 277 insertions(+), 688 deletions(-) delete mode 100644 musicus/res/ui/import_folder_dialog.ui rename musicus/res/ui/{import_dialog.ui => medium_editor.ui} (100%) rename musicus/res/ui/{import_disc_dialog.ui => source_selector.ui} (65%) delete mode 100644 musicus/src/dialogs/import.rs delete mode 100644 musicus/src/dialogs/import_disc.rs delete mode 100644 musicus/src/dialogs/import_folder.rs create mode 100644 musicus/src/import/disc_source.rs create mode 100644 musicus/src/import/medium_editor.rs create mode 100644 musicus/src/import/mod.rs create mode 100644 musicus/src/import/source_selector.rs delete mode 100644 musicus/src/ripper.rs diff --git a/musicus/res/musicus.gresource.xml b/musicus/res/musicus.gresource.xml index 4e94aea..acc5530 100644 --- a/musicus/res/musicus.gresource.xml +++ b/musicus/res/musicus.gresource.xml @@ -4,12 +4,10 @@ ui/ensemble_editor.ui ui/ensemble_screen.ui ui/ensemble_selector.ui - ui/import_dialog.ui - ui/import_disc_dialog.ui - ui/import_folder_dialog.ui ui/instrument_editor.ui ui/instrument_selector.ui ui/login_dialog.ui + ui/medium_editor.ui ui/performance_editor.ui ui/person_editor.ui ui/person_list.ui @@ -25,6 +23,7 @@ ui/recording_selector_screen.ui ui/selector.ui ui/server_dialog.ui + ui/source_selector.ui ui/track_editor.ui ui/track_selector.ui ui/track_set_editor.ui diff --git a/musicus/res/ui/import_folder_dialog.ui b/musicus/res/ui/import_folder_dialog.ui deleted file mode 100644 index 1b1ea91..0000000 --- a/musicus/res/ui/import_folder_dialog.ui +++ /dev/null @@ -1,77 +0,0 @@ - - - - - - True - vertical - - - True - Import folder - - - True - True - - - True - go-previous-symbolic - - - - - - - - - True - center - center - True - 18 - vertical - 18 - - - True - 0.5 - 80 - folder-symbolic - - - - - True - 0.5 - Import from a folder - - - - - - - - True - 0.5 - 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. - center - True - 40 - - - - - Select - True - True - center - - - - - - - diff --git a/musicus/res/ui/import_dialog.ui b/musicus/res/ui/medium_editor.ui similarity index 100% rename from musicus/res/ui/import_dialog.ui rename to musicus/res/ui/medium_editor.ui diff --git a/musicus/res/ui/import_disc_dialog.ui b/musicus/res/ui/source_selector.ui similarity index 65% rename from musicus/res/ui/import_disc_dialog.ui rename to musicus/res/ui/source_selector.ui index 70bea77..876f366 100644 --- a/musicus/res/ui/import_disc_dialog.ui +++ b/musicus/res/ui/source_selector.ui @@ -1,5 +1,4 @@ - @@ -11,7 +10,7 @@ True False - Import CD + Import music True @@ -27,11 +26,6 @@ - - False - True - 0 - @@ -49,21 +43,6 @@ False error False - - - False - 6 - end - - - - - - False - False - 0 - - False @@ -75,33 +54,16 @@ Failed to load the CD. Make sure you have inserted it into your drive. True - - False - True - 0 - - - False - False - 0 - - - - - - False - True - 0 - True False + True center center 18 @@ -193,55 +155,7 @@ 1 - - - True - True - - - True - False - none - - - True - False - 500 - 300 - - - True - False - 6 - 6 - 12 - 6 - 0 - in - - - - - - - - - - - - - - - content - 2 - - - - True - True - 1 - diff --git a/musicus/src/dialogs/import.rs b/musicus/src/dialogs/import.rs deleted file mode 100644 index 3ab9e03..0000000 --- a/musicus/src/dialogs/import.rs +++ /dev/null @@ -1,69 +0,0 @@ -use crate::backend::Backend; -use crate::editors::{TrackSetEditor, TrackSource}; -use crate::widgets::{Navigator, NavigatorScreen}; -use glib::clone; -use gtk::prelude::*; -use gtk_macros::get_widget; -use std::cell::RefCell; -use std::rc::Rc; - -/// A dialog for editing metadata while importing music into the music library. -pub struct ImportDialog { - backend: Rc, - source: Rc, - widget: gtk::Box, - navigator: RefCell>>, -} - -impl ImportDialog { - /// Create a new import dialog. - pub fn new(backend: Rc, source: Rc) -> Rc { - // Create UI - - let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/import_dialog.ui"); - - get_widget!(builder, gtk::Box, widget); - get_widget!(builder, gtk::Button, back_button); - get_widget!(builder, gtk::Button, add_button); - - let this = Rc::new(Self { - backend, - source, - widget, - navigator: RefCell::new(None), - }); - - // Connect signals and callbacks - - back_button.connect_clicked(clone!(@strong this => move |_| { - let navigator = this.navigator.borrow().clone(); - if let Some(navigator) = navigator { - navigator.pop(); - } - })); - - add_button.connect_clicked(clone!(@strong this => move |_| { - let navigator = this.navigator.borrow().clone(); - if let Some(navigator) = navigator { - let editor = TrackSetEditor::new(this.backend.clone(), this.source.clone()); - navigator.push(editor); - } - })); - - this - } -} - -impl NavigatorScreen for ImportDialog { - fn attach_navigator(&self, navigator: Rc) { - self.navigator.replace(Some(navigator)); - } - - fn get_widget(&self) -> gtk::Widget { - self.widget.clone().upcast() - } - - fn detach_navigator(&self) { - self.navigator.replace(None); - } -} diff --git a/musicus/src/dialogs/import_disc.rs b/musicus/src/dialogs/import_disc.rs deleted file mode 100644 index 38ff99d..0000000 --- a/musicus/src/dialogs/import_disc.rs +++ /dev/null @@ -1,211 +0,0 @@ -use crate::backend::Backend; -use crate::ripper::Ripper; -use crate::widgets::{List, Navigator, NavigatorScreen}; -use anyhow::Result; -use glib::clone; -use gtk::prelude::*; -use gtk_macros::get_widget; -use std::cell::RefCell; -use std::rc::Rc; - -/// The current status of a ripped track. -#[derive(Debug, Clone)] -enum RipStatus { - None, - Ripping, - Ready, - Error, -} - -/// Representation of a track on the ripped disc. -#[derive(Debug, Clone)] -struct RipTrack { - pub status: RipStatus, - pub index: u32, - pub title: String, - pub subtitle: String, -} - -/// A dialog for importing tracks from a CD. -pub struct ImportDiscDialog { - backend: Rc, - widget: gtk::Box, - stack: gtk::Stack, - info_bar: gtk::InfoBar, - list: Rc>, - ripper: Ripper, - tracks: RefCell>, - navigator: RefCell>>, -} - -impl ImportDiscDialog { - /// Create a new import disc dialog. - pub fn new(backend: Rc) -> Rc { - // 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::::new("No tracks found."); - frame.add(&list.widget); - - let mut tmp_dir = glib::get_tmp_dir().unwrap(); - let dir_name = format!("musicus-{}", rand::random::()); - 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::::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) { - self.navigator.replace(Some(navigator)); - } - - fn get_widget(&self) -> gtk::Widget { - self.widget.clone().upcast() - } - - fn detach_navigator(&self) { - self.navigator.replace(None); - } -} diff --git a/musicus/src/dialogs/import_folder.rs b/musicus/src/dialogs/import_folder.rs deleted file mode 100644 index ca91219..0000000 --- a/musicus/src/dialogs/import_folder.rs +++ /dev/null @@ -1,88 +0,0 @@ -use super::ImportDialog; -use crate::backend::Backend; -use crate::editors::TrackSource; -use crate::widgets::{Navigator, NavigatorScreen}; -use glib::clone; -use gtk::prelude::*; -use gtk_macros::get_widget; -use std::cell::RefCell; -use std::rc::Rc; -use std::path::Path; - -/// The initial screen for importing a folder. -pub struct ImportFolderDialog { - backend: Rc, - widget: gtk::Box, - navigator: RefCell>>, -} - -impl ImportFolderDialog { - /// Create a new import folderdialog. - pub fn new(backend: Rc) -> Rc { - // Create UI - - let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/import_folder_dialog.ui"); - - get_widget!(builder, gtk::Box, widget); - get_widget!(builder, gtk::Button, back_button); - get_widget!(builder, gtk::Button, import_button); - - let this = Rc::new(Self { - backend, - widget, - navigator: RefCell::new(None), - }); - - // Connect signals and callbacks - - back_button.connect_clicked(clone!(@strong this => move |_| { - let navigator = this.navigator.borrow().clone(); - if let Some(navigator) = navigator { - navigator.pop(); - } - })); - - import_button.connect_clicked(clone!(@strong this => move |_| { - let navigator = this.navigator.borrow().clone(); - if let Some(navigator) = navigator { - let chooser = gtk::FileChooserNative::new( - Some("Select folder"), - Some(&navigator.window), - gtk::FileChooserAction::SelectFolder, - None, - None, - ); - - chooser.connect_response(clone!(@strong this => move |chooser, response| { - if response == gtk::ResponseType::Accept { - let navigator = this.navigator.borrow().clone(); - if let Some(navigator) = navigator { - let path = chooser.get_filename().unwrap(); - let source = TrackSource::folder(&path).unwrap(); - let dialog = ImportDialog::new(this.backend.clone(), Rc::new(source)); - navigator.push(dialog); - } - } - })); - - chooser.run(); - } - })); - - this - } -} - -impl NavigatorScreen for ImportFolderDialog { - fn attach_navigator(&self, navigator: Rc) { - self.navigator.replace(Some(navigator)); - } - - fn get_widget(&self) -> gtk::Widget { - self.widget.clone().upcast() - } - - fn detach_navigator(&self) { - self.navigator.replace(None); - } -} diff --git a/musicus/src/dialogs/mod.rs b/musicus/src/dialogs/mod.rs index 6f0a8ae..b244bda 100644 --- a/musicus/src/dialogs/mod.rs +++ b/musicus/src/dialogs/mod.rs @@ -1,12 +1,3 @@ -pub mod import; -pub use import::*; - -pub mod import_folder; -pub use import_folder::*; - -pub mod import_disc; -pub use import_disc::*; - pub mod about; pub use about::*; diff --git a/musicus/src/editors/mod.rs b/musicus/src/editors/mod.rs index 133c0a5..8974e76 100644 --- a/musicus/src/editors/mod.rs +++ b/musicus/src/editors/mod.rs @@ -10,12 +10,6 @@ pub use person::*; pub mod recording; pub use recording::*; -pub mod track_set; -pub use track_set::*; - -pub mod track_source; -pub use track_source::*; - pub mod work; pub use work::*; diff --git a/musicus/src/import/disc_source.rs b/musicus/src/import/disc_source.rs new file mode 100644 index 0000000..74ec8eb --- /dev/null +++ b/musicus/src/import/disc_source.rs @@ -0,0 +1,171 @@ +use anyhow::{anyhow, bail, Result}; +use discid::DiscId; +use futures_channel::oneshot; +use gstreamer::prelude::*; +use gstreamer::{Element, ElementFactory, Pipeline}; +use std::cell::RefCell; +use std::path::{Path, PathBuf}; +use std::thread; + +/// Representation of an audio CD being imported as a medium. +#[derive(Clone, Debug)] +pub struct DiscSource { + /// The MusicBrainz DiscID of the CD. + pub discid: String, + + /// The tracks on this disc. + pub tracks: Vec, +} + +/// Representation of a single track on an audio CD. +#[derive(Clone, Debug)] +pub struct TrackSource { + /// The track number. This is different from the index in the disc + /// source's tracks list, because it is not defined from which number the + /// the track numbers start. + pub number: u32, + + /// The path to the temporary file to which the track will be ripped. The + /// file will not exist until the track is actually ripped. + pub path: PathBuf, +} + +impl DiscSource { + /// Try to create a new disc source by asynchronously reading the + /// information from the default disc drive. + pub async fn load() -> Result { + let (sender, receiver) = oneshot::channel(); + + thread::spawn(|| { + let disc = Self::load_priv(); + sender.send(disc).unwrap(); + }); + + let disc = receiver.await??; + + Ok(disc) + } + + /// Rip the whole disc asynchronously. After this method has finished + /// successfully, the audio files will be available in the specified + /// location for each track source. + pub async fn rip(&self) -> Result<()> { + for track in &self.tracks { + let (sender, receiver) = oneshot::channel(); + + let number = track.number; + let path = track.path.clone(); + + thread::spawn(move || { + let result = Self::rip_track(&path, number); + sender.send(result).unwrap(); + }); + + receiver.await??; + } + + Ok(()) + } + + /// Load the disc from the default disc drive. + fn load_priv() -> Result { + let discid = DiscId::read(None)?; + let id = discid.id(); + + let mut tracks = Vec::new(); + + let first_track = discid.first_track_num() as u32; + let last_track = discid.last_track_num() as u32; + + let tmp_dir = Self::create_tmp_dir()?; + + for number in first_track..=last_track { + let file_name = format!("track_{:02}.flac", number); + + let mut path = tmp_dir.clone(); + path.push(file_name); + + let track = TrackSource { + number, + path, + }; + + tracks.push(track); + } + + let disc = DiscSource { + discid: id, + tracks, + }; + + Ok(disc) + } + + /// Create a new temporary directory and return its path. + // TODO: Move to a more appropriate place. + fn create_tmp_dir() -> Result { + let mut tmp_dir = glib::get_tmp_dir() + .ok_or_else(|| { + anyhow!("Failed to get temporary directory using glib::get_tmp_dir()!") + })?; + + let dir_name = format!("musicus-{}", rand::random::()); + tmp_dir.push(dir_name); + + std::fs::create_dir(&tmp_dir)?; + + Ok(tmp_dir) + } + + /// Rip one track. + fn rip_track(path: &Path, number: u32) -> Result<()> { + let pipeline = Self::build_pipeline(path, number)?; + + let bus = pipeline + .get_bus() + .ok_or_else(|| anyhow!("Failed to get bus from pipeline!"))?; + + pipeline.set_state(gstreamer::State::Playing)?; + + for msg in bus.iter_timed(gstreamer::CLOCK_TIME_NONE) { + use gstreamer::MessageView::*; + + match msg.view() { + Eos(..) => break, + Error(err) => { + pipeline.set_state(gstreamer::State::Null)?; + bail!("GStreamer error: {:?}!", err); + } + _ => (), + } + } + + pipeline.set_state(gstreamer::State::Null)?; + + Ok(()) + } + + /// Build the GStreamer pipeline to rip a track. + fn build_pipeline(path: &Path, number: u32) -> Result { + let cdparanoiasrc = ElementFactory::make("cdparanoiasrc", None)?; + cdparanoiasrc.set_property("track", &number)?; + + let queue = ElementFactory::make("queue", None)?; + let audioconvert = ElementFactory::make("audioconvert", None)?; + let flacenc = ElementFactory::make("flacenc", None)?; + + let path_str = path.to_str().ok_or_else(|| { + anyhow!("Failed to convert path '{:?}' to string!", path) + })?; + + let filesink = gstreamer::ElementFactory::make("filesink", None)?; + filesink.set_property("location", &path_str.to_owned())?; + + let pipeline = gstreamer::Pipeline::new(None); + pipeline.add_many(&[&cdparanoiasrc, &queue, &audioconvert, &flacenc, &filesink])?; + + Element::link_many(&[&cdparanoiasrc, &queue, &audioconvert, &flacenc, &filesink])?; + + Ok(pipeline) + } +} diff --git a/musicus/src/import/medium_editor.rs b/musicus/src/import/medium_editor.rs new file mode 100644 index 0000000..e69de29 diff --git a/musicus/src/import/mod.rs b/musicus/src/import/mod.rs new file mode 100644 index 0000000..248a7b7 --- /dev/null +++ b/musicus/src/import/mod.rs @@ -0,0 +1,5 @@ +mod disc_source; +mod medium_editor; +mod source_selector; + +pub use source_selector::SourceSelector; diff --git a/musicus/src/import/source_selector.rs b/musicus/src/import/source_selector.rs new file mode 100644 index 0000000..0743096 --- /dev/null +++ b/musicus/src/import/source_selector.rs @@ -0,0 +1,85 @@ +use super::disc_source::DiscSource; +use crate::backend::Backend; +use crate::widgets::{Navigator, NavigatorScreen}; +use anyhow::Result; +use glib::clone; +use gtk::prelude::*; +use gtk_macros::get_widget; +use std::cell::RefCell; +use std::rc::Rc; + +/// A dialog for starting to import music. +pub struct SourceSelector { + backend: Rc, + widget: gtk::Box, + stack: gtk::Stack, + info_bar: gtk::InfoBar, + navigator: RefCell>>, +} + +impl SourceSelector { + /// Create a new source selector. + pub fn new(backend: Rc) -> Rc { + // Create UI + + let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/source_selector.ui"); + + get_widget!(builder, gtk::Box, widget); + get_widget!(builder, gtk::Button, back_button); + get_widget!(builder, gtk::Stack, stack); + get_widget!(builder, gtk::InfoBar, info_bar); + get_widget!(builder, gtk::Button, import_button); + + let this = Rc::new(Self { + backend, + widget, + stack, + info_bar, + navigator: RefCell::new(None), + }); + + // Connect signals and callbacks + + back_button.connect_clicked(clone!(@strong this => move |_| { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + navigator.pop(); + } + })); + + import_button.connect_clicked(clone!(@strong this => move |_| { + this.stack.set_visible_child_name("loading"); + + let context = glib::MainContext::default(); + let clone = this.clone(); + context.spawn_local(async move { + match DiscSource::load().await { + Ok(disc) => { + println!("{:?}", disc); + clone.stack.set_visible_child_name("start"); + } + Err(_) => { + clone.info_bar.set_revealed(true); + clone.stack.set_visible_child_name("start"); + } + } + }); + })); + + this + } +} + +impl NavigatorScreen for SourceSelector { + fn attach_navigator(&self, navigator: Rc) { + self.navigator.replace(Some(navigator)); + } + + fn get_widget(&self) -> gtk::Widget { + self.widget.clone().upcast() + } + + fn detach_navigator(&self) { + self.navigator.replace(None); + } +} diff --git a/musicus/src/main.rs b/musicus/src/main.rs index 53a9fcb..49be0f5 100644 --- a/musicus/src/main.rs +++ b/musicus/src/main.rs @@ -12,11 +12,11 @@ use std::cell::RefCell; use std::rc::Rc; mod backend; -mod ripper; mod config; mod database; mod dialogs; mod editors; +mod import; mod player; mod screens; mod selectors; diff --git a/musicus/src/meson.build b/musicus/src/meson.build index 363f0ee..ec71ba2 100644 --- a/musicus/src/meson.build +++ b/musicus/src/meson.build @@ -53,8 +53,6 @@ sources = files( 'database/thread.rs', 'database/works.rs', 'dialogs/about.rs', - 'dialogs/import_disc.rs', - 'dialogs/import_folder.rs', 'dialogs/login_dialog.rs', 'dialogs/mod.rs', 'dialogs/preferences.rs', @@ -95,7 +93,6 @@ sources = files( 'player.rs', 'resources.rs', 'resources.rs.in', - 'ripper.rs', 'window.rs', ) diff --git a/musicus/src/ripper.rs b/musicus/src/ripper.rs deleted file mode 100644 index bbdbde5..0000000 --- a/musicus/src/ripper.rs +++ /dev/null @@ -1,123 +0,0 @@ -use anyhow::{anyhow, bail, Result}; -use discid::DiscId; -use futures_channel::oneshot; -use gstreamer::prelude::*; -use gstreamer::{Element, ElementFactory, Pipeline}; -use std::cell::RefCell; -use std::thread; - -/// A disc that can be ripped. -#[derive(Debug, Clone)] -pub struct RipDisc { - pub discid: String, - pub first_track: u32, - pub last_track: u32, -} - -/// An interface for ripping an audio compact disc. -pub struct Ripper { - path: String, - disc: RefCell>, -} - -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 { - 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 { - 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 { - let cdparanoiasrc = ElementFactory::make("cdparanoiasrc", None)?; - cdparanoiasrc.set_property("track", &track)?; - - let queue = ElementFactory::make("queue", None)?; - let audioconvert = ElementFactory::make("audioconvert", None)?; - let flacenc = ElementFactory::make("flacenc", None)?; - - let filesink = gstreamer::ElementFactory::make("filesink", None)?; - filesink.set_property("location", &format!("{}/track_{:02}.flac", path, track))?; - - let pipeline = gstreamer::Pipeline::new(None); - pipeline.add_many(&[&cdparanoiasrc, &queue, &audioconvert, &flacenc, &filesink])?; - - Element::link_many(&[&cdparanoiasrc, &queue, &audioconvert, &flacenc, &filesink])?; - - Ok(pipeline) - } -} diff --git a/musicus/src/window.rs b/musicus/src/window.rs index 2d3224c..429a870 100644 --- a/musicus/src/window.rs +++ b/musicus/src/window.rs @@ -1,5 +1,6 @@ use crate::backend::*; use crate::dialogs::*; +use crate::import::SourceSelector; use crate::screens::*; use crate::widgets::*; use futures::prelude::*; @@ -93,7 +94,7 @@ impl Window { // let window = NavigatorWindow::new(editor); // window.show(); - let dialog = ImportFolderDialog::new(result.backend.clone()); + let dialog = SourceSelector::new(result.backend.clone()); let window = NavigatorWindow::new(dialog); window.show(); })); @@ -110,15 +111,15 @@ impl Window { result.stack.set_visible_child_name("content"); })); - action!( - result.window, - "import-disc", - clone!(@strong result => move |_, _| { - let dialog = ImportDiscDialog::new(result.backend.clone()); - let window = NavigatorWindow::new(dialog); - window.show(); - }) - ); + // action!( + // result.window, + // "import-disc", + // clone!(@strong result => move |_, _| { + // let dialog = ImportDiscDialog::new(result.backend.clone()); + // let window = NavigatorWindow::new(dialog); + // window.show(); + // }) + // ); action!( result.window, From 585bc74df01179953cd5b230a8d14ab576ff8f77 Mon Sep 17 00:00:00 2001 From: Elias Projahn Date: Wed, 13 Jan 2021 18:06:43 +0100 Subject: [PATCH 06/12] Add medium editor skeleton --- musicus/src/import/medium_editor.rs | 75 +++++++++++++++++++++++++++ musicus/src/import/source_selector.rs | 9 +++- musicus/src/widgets/mod.rs | 2 + musicus/src/widgets/new_list.rs | 50 ++++++++++++++++++ 4 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 musicus/src/widgets/new_list.rs diff --git a/musicus/src/import/medium_editor.rs b/musicus/src/import/medium_editor.rs index e69de29..85b8a8d 100644 --- a/musicus/src/import/medium_editor.rs +++ b/musicus/src/import/medium_editor.rs @@ -0,0 +1,75 @@ +use super::disc_source::DiscSource; +use crate::backend::Backend; +// use crate::editors::{TrackSetEditor, TrackSource}; +use crate::widgets::{Navigator, NavigatorScreen}; +use crate::widgets::new_list::List; +use glib::clone; +use gtk::prelude::*; +use gtk_macros::get_widget; +use std::cell::RefCell; +use std::rc::Rc; + +/// A dialog for editing metadata while importing music into the music library. +pub struct MediumEditor { + backend: Rc, + source: DiscSource, + widget: gtk::Box, + navigator: RefCell>>, +} + +impl MediumEditor { + /// Create a new medium editor. + pub fn new(backend: Rc, source: DiscSource) -> Rc { + // Create UI + + let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/medium_editor.ui"); + + get_widget!(builder, gtk::Box, widget); + get_widget!(builder, gtk::Button, back_button); + get_widget!(builder, gtk::Button, add_button); + get_widget!(builder, gtk::Frame, frame); + + let list = List::new("No recordings added."); + frame.add(&list.widget); + + let this = Rc::new(Self { + backend, + source, + widget, + navigator: RefCell::new(None), + }); + + // Connect signals and callbacks + + back_button.connect_clicked(clone!(@strong this => move |_| { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + navigator.pop(); + } + })); + + add_button.connect_clicked(clone!(@strong this => move |_| { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + // let editor = TrackSetEditor::new(this.backend.clone(), this.source.clone()); + // navigator.push(editor); + } + })); + + this + } +} + +impl NavigatorScreen for MediumEditor { + fn attach_navigator(&self, navigator: Rc) { + self.navigator.replace(Some(navigator)); + } + + fn get_widget(&self) -> gtk::Widget { + self.widget.clone().upcast() + } + + fn detach_navigator(&self) { + self.navigator.replace(None); + } +} diff --git a/musicus/src/import/source_selector.rs b/musicus/src/import/source_selector.rs index 0743096..121bc34 100644 --- a/musicus/src/import/source_selector.rs +++ b/musicus/src/import/source_selector.rs @@ -1,3 +1,4 @@ +use super::medium_editor::MediumEditor; use super::disc_source::DiscSource; use crate::backend::Backend; use crate::widgets::{Navigator, NavigatorScreen}; @@ -55,7 +56,13 @@ impl SourceSelector { context.spawn_local(async move { match DiscSource::load().await { Ok(disc) => { - println!("{:?}", disc); + let navigator = clone.navigator.borrow().clone(); + if let Some(navigator) = navigator { + let editor = MediumEditor::new(clone.backend.clone(), disc); + navigator.push(editor); + } + + clone.info_bar.set_revealed(false); clone.stack.set_visible_child_name("start"); } Err(_) => { diff --git a/musicus/src/widgets/mod.rs b/musicus/src/widgets/mod.rs index 9e59253..089a3ec 100644 --- a/musicus/src/widgets/mod.rs +++ b/musicus/src/widgets/mod.rs @@ -7,6 +7,8 @@ pub use navigator::*; pub mod navigator_window; pub use navigator_window::*; +pub mod new_list; + pub mod player_bar; pub use player_bar::*; diff --git a/musicus/src/widgets/new_list.rs b/musicus/src/widgets/new_list.rs new file mode 100644 index 0000000..98261e6 --- /dev/null +++ b/musicus/src/widgets/new_list.rs @@ -0,0 +1,50 @@ +use gtk::prelude::*; +use std::cell::RefCell; + +/// A simple list of widgets. +pub struct List { + pub widget: gtk::ListBox, + make_widget: RefCell gtk::Widget>>>, +} + +impl List { + /// Create a new list. The list will be empty. + pub fn new(placeholder_text: &str) -> Self { + let placeholder_label = gtk::Label::new(Some(placeholder_text)); + placeholder_label.set_margin_top(6); + placeholder_label.set_margin_bottom(6); + placeholder_label.set_margin_start(6); + placeholder_label.set_margin_end(6); + placeholder_label.show(); + + let widget = gtk::ListBox::new(); + widget.set_selection_mode(gtk::SelectionMode::None); + widget.set_placeholder(Some(&placeholder_label)); + widget.show(); + + Self { + widget, + make_widget: RefCell::new(None), + } + } + + /// Set the closure to be called to construct widgets for the items. + pub fn set_make_widget gtk::Widget + 'static>(&self, make_widget: F) { + self.make_widget.replace(Some(Box::new(make_widget))); + } + + /// Call the make_widget function for each item. This will automatically + /// show all children by indices 0..length. + pub fn update(&self, length: usize) { + for child in self.widget.get_children() { + self.widget.remove(&child); + } + + if let Some(make_widget) = &*self.make_widget.borrow() { + for index in 0..length { + let row = make_widget(index); + self.widget.insert(&row, -1); + } + } + } +} From dbae0ad81b777fac41674330070730ab7c8909e3 Mon Sep 17 00:00:00 2001 From: Elias Projahn Date: Wed, 13 Jan 2021 19:27:22 +0100 Subject: [PATCH 07/12] Make track set editor functional --- musicus/src/editors/track_set.rs | 580 ------------------------- musicus/src/editors/track_source.rs | 42 -- musicus/src/import/medium_editor.rs | 10 +- musicus/src/import/mod.rs | 3 + musicus/src/import/track_editor.rs | 110 +++++ musicus/src/import/track_selector.rs | 122 ++++++ musicus/src/import/track_set_editor.rs | 275 ++++++++++++ musicus/src/meson.build | 1 - 8 files changed, 515 insertions(+), 628 deletions(-) delete mode 100644 musicus/src/editors/track_set.rs delete mode 100644 musicus/src/editors/track_source.rs create mode 100644 musicus/src/import/track_editor.rs create mode 100644 musicus/src/import/track_selector.rs create mode 100644 musicus/src/import/track_set_editor.rs diff --git a/musicus/src/editors/track_set.rs b/musicus/src/editors/track_set.rs deleted file mode 100644 index 716fa82..0000000 --- a/musicus/src/editors/track_set.rs +++ /dev/null @@ -1,580 +0,0 @@ -use crate::backend::Backend; -use crate::database::{Recording, Track, TrackSet}; -use crate::selectors::{PersonSelector, RecordingSelector, WorkSelector}; -use crate::widgets::{Navigator, NavigatorScreen}; -use gettextrs::gettext; -use glib::clone; -use gtk::prelude::*; -use gtk_macros::get_widget; -use libhandy::prelude::*; -use std::cell::{Cell, RefCell}; -use std::collections::HashSet; -use std::rc::Rc; - -/// Representation of a track that can be imported into the music library. -#[derive(Debug, Clone)] -struct TrackSource { - /// A short string identifying the track for the user. - pub description: String, - - /// Whether the track is ready to be imported. - pub ready: bool, -} - -/// Representation of a medium that can be imported into the music library. -#[derive(Debug, Clone)] -struct MediumSource { - /// The tracks that can be imported from the medium. - pub tracks: Vec, - - /// Whether all tracks are ready to be imported. - pub ready: bool, -} - -impl MediumSource { - /// Create a dummy medium source for testing purposes. - fn dummy() -> Self { - let mut tracks = Vec::new(); - - for index in 0..20 { - tracks.push(TrackSource { - description: format!("Track {}", index + 1), - ready: Cell::new(true), - }); - } - - Self { - tracks, - ready: Cell::new(true), - } - } -} - -/// A track while being edited. -#[derive(Debug, Clone)] -struct TrackData<'a> { - /// A reference to the selected track source. - pub source: &'a TrackSource, - - /// The actual value for the track. - pub track: Track, -} - -/// A track set while being edited. -#[derive(Debug, Clone)] -struct TrackSetData<'a> { - /// The recording to which the tracks belong. - pub recording: Option, - - /// The tracks that are being edited. - pub tracks: Vec>, -} - -impl TrackSetData { - /// Create a new empty track set. - pub fn new() -> Self { - Self { - recording: None, - tracks: Vec::new(), - } - } -} - -/// A screen for editing a set of tracks for one recording. -pub struct TrackSetEditor { - backend: Rc, - source: Rc>, - widget: gtk::Box, - save_button: gtk::Button, - recording_row: libhandy::ActionRow, - track_list: List, - data: RefCell, - done_cb: RefCell>>, - navigator: RefCell>>, -} - -impl TrackSetEditor { - /// Create a new track set editor. - pub fn new(backend: Rc, source: Rc) -> Rc { - // TODO: Replace with argument. - let source = Rc::new(RefCell::new(MediumSource::dummy())); - - // Create UI - - let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_set_editor.ui"); - - get_widget!(builder, gtk::Box, widget); - get_widget!(builder, gtk::Button, back_button); - get_widget!(builder, gtk::Button, save_button); - get_widget!(builder, libhandy::ActionRow, recording_row); - get_widget!(builder, gtk::Button, select_recording_button); - get_widget!(builder, gtk::Button, edit_tracks_button); - get_widget!(builder, gtk::Frame, tracks_frame); - - let track_list = List::new(&gettext!("No tracks added")); - tracks_frame.add(&track_list.widget); - - let this = Rc::new(Self { - backend, - source, - widget, - save_button, - recording_row, - track_list, - data: RefCell::new(TrackSetData::new()), - done_cb: RefCell::new(None), - 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(); - } - })); - - this.save_button.connect_clicked(clone!(@strong this => move |_| { - if let Some(cb) = &*this.done_cb.borrow() {} - })); - - select_recording_button.connect_clicked(clone!(@strong this => move |_| { - let navigator = this.navigator.borrow().clone(); - if let Some(navigator) = navigator { - let person_selector = PersonSelector::new(this.backend.clone()); - - person_selector.set_selected_cb(clone!(@strong this, @strong navigator => move |person| { - let work_selector = WorkSelector::new(this.backend.clone(), person.clone()); - - work_selector.set_selected_cb(clone!(@strong this, @strong navigator => move |work| { - let recording_selector = RecordingSelector::new(this.backend.clone(), work.clone()); - - recording_selector.set_selected_cb(clone!(@strong this, @strong navigator => move |recording| { - let mut data = this.data.borrow_mut(); - data.recording = Some(recording); - this.recording_selected(); - - navigator.clone().pop(); - navigator.clone().pop(); - navigator.clone().pop(); - })); - - navigator.clone().push(recording_selector); - })); - - navigator.clone().push(work_selector); - })); - - navigator.clone().push(person_selector); - } - })); - - edit_tracks_button.connect_clicked(clone!(@strong this => move |_| { - let navigator = this.navigator.borrow().clone(); - if let Some(navigator) = navigator { - let selector = TrackSelector::new(Rc::clone(this.source)); - - selector.set_selected_cb(clone!(@strong this => move |selection| { - let mut tracks = Vec::new(); - - for index in selection { - let track = Track { - work_parts: Vec::new(), - }; - - let source = this.source.tracks[index].clone(); - - let data = TrackData { - track, - source, - }; - - tracks.push(data); - } - - let length = tracks.len(); - this.tracks.replace(tracks); - this.track_list.update(length); - this.autofill_parts(); - })); - - navigator.push(selector); - } - })); - - this.track_list.set_make_widget(clone!(@strong this => move |index| { - let data = &this.tracks.borrow()[index]; - - let mut title_parts = Vec::::new(); - - if let Some(recording) = &*this.recording.borrow() { - for part in &data.track.work_parts { - title_parts.push(recording.work.parts[*part].title.clone()); - } - } - - let title = if title_parts.is_empty() { - gettext("Unknown") - } else { - title_parts.join(", ") - }; - - let subtitle = data.source.description.clone(); - - let edit_image = gtk::Image::from_icon_name(Some("document-edit-symbolic"), gtk::IconSize::Button); - let edit_button = gtk::Button::new(); - edit_button.set_relief(gtk::ReliefStyle::None); - edit_button.set_valign(gtk::Align::Center); - edit_button.add(&edit_image); - - let row = libhandy::ActionRow::new(); - row.set_activatable(true); - row.set_title(Some(&title)); - row.set_subtitle(Some(&subtitle)); - row.add(&edit_button); - row.set_activatable_widget(Some(&edit_button)); - row.show_all(); - - edit_button.connect_clicked(clone!(@strong this => move |_| { - let recording = this.recording.borrow().clone(); - let navigator = this.navigator.borrow().clone(); - - if let (Some(recording), Some(navigator)) = (recording, navigator) { - let editor = TrackEditor::new(recording, Vec::new()); - - editor.set_selected_cb(clone!(@strong this => move |selection| { - { - let mut tracks = &mut this.data.borrow_mut().tracks; - let mut track = &mut tracks[index]; - track.track.work_parts = selection; - }; - - this.update_tracks(); - })); - - navigator.push(editor); - } - })); - - row.upcast() - })); - - this - } - - /// Set the closure to be called when the user has created the track set. - pub fn set_done_cb(&self, cb: F) { - self.done_cb.replace(Some(Box::new(cb))); - } - - /// Set everything up after selecting a recording. - fn recording_selected(&self) { - if let Some(recording) = self.data.borrow().recording { - self.recording_row.set_title(Some(&recording.work.get_title())); - self.recording_row.set_subtitle(Some(&recording.get_performers())); - self.save_button.set_sensitive(true); - } - - self.autofill_parts(); - } - - /// Automatically try to put work part information from the selected recording into the - /// selected tracks. - fn autofill_parts(&self) { - if let Some(recording) = self.data.borrow().recording { - let mut tracks = self.tracks.borrow_mut(); - - for (index, _) in recording.work.parts.iter().enumerate() { - if let Some(mut data) = tracks.get_mut(index) { - data.track.work_parts = vec![index]; - } else { - break; - } - } - } - - self.update_tracks(); - } - - /// Update the track list. - fn update_tracks(&self) { - let length = self.data.borrow().tracks.len(); - self.track_list.update(length); - } -} - -impl NavigatorScreen for TrackSetEditor { - fn attach_navigator(&self, navigator: Rc) { - self.navigator.replace(Some(navigator)); - } - - fn get_widget(&self) -> gtk::Widget { - self.widget.clone().upcast() - } - - fn detach_navigator(&self) { - self.navigator.replace(None); - } -} - - -/// A screen for selecting tracks from a medium. -struct TrackSelector { - source: Rc>, - widget: gtk::Box, - select_button: gtk::Button, - selection: RefCell>, - selected_cb: RefCell)>>>, - navigator: RefCell>>, -} - -impl TrackSelector { - /// Create a new track selector. - pub fn new(source: Rc>) -> Rc { - // Create UI - - let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_selector.ui"); - - get_widget!(builder, gtk::Box, widget); - get_widget!(builder, gtk::Button, back_button); - get_widget!(builder, gtk::Button, select_button); - get_widget!(builder, gtk::Frame, tracks_frame); - - let track_list = gtk::ListBox::new(); - track_list.set_selection_mode(gtk::SelectionMode::None); - track_list.set_vexpand(false); - track_list.show(); - tracks_frame.add(&track_list); - - let this = Rc::new(Self { - source, - widget, - select_button, - selection: RefCell::new(Vec::new()), - selected_cb: RefCell::new(None), - 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(); - } - })); - - this.select_button.connect_clicked(clone!(@strong this => move |_| { - let navigator = this.navigator.borrow().clone(); - if let Some(navigator) = navigator { - navigator.pop(); - } - - if let Some(cb) = &*this.selected_cb.borrow() { - let selection = this.selection.borrow().clone(); - cb(selection); - } - })); - - for (index, track) in this.tracks.iter().enumerate() { - let check = gtk::CheckButton::new(); - - check.connect_toggled(clone!(@strong this => move |check| { - let mut selection = this.selection.borrow_mut(); - if check.get_active() { - selection.push(index); - } else { - if let Some(pos) = selection.iter().position(|part| *part == index) { - selection.remove(pos); - } - } - - if selection.is_empty() { - this.select_button.set_sensitive(false); - } else { - this.select_button.set_sensitive(true); - } - })); - - let row = libhandy::ActionRow::new(); - row.add_prefix(&check); - row.set_activatable_widget(Some(&check)); - row.set_title(Some(&track.description)); - row.show_all(); - - track_list.add(&row); - } - - this - } - - /// Set the closure to be called when the user has selected tracks. The - /// closure will be called with the indices of the selected tracks as its - /// argument. - pub fn set_selected_cb) + 'static>(&self, cb: F) { - self.selected_cb.replace(Some(Box::new(cb))); - } -} - -impl NavigatorScreen for TrackSelector { - fn attach_navigator(&self, navigator: Rc) { - self.navigator.replace(Some(navigator)); - } - - fn get_widget(&self) -> gtk::Widget { - self.widget.clone().upcast() - } - - fn detach_navigator(&self) { - self.navigator.replace(None); - } -} - -/// A screen for editing a single track. -struct TrackEditor { - widget: gtk::Box, - selection: RefCell>, - selected_cb: RefCell)>>>, - navigator: RefCell>>, -} - -impl TrackEditor { - /// Create a new track editor. - pub fn new(recording: Recording, selection: Vec) -> Rc { - // Create UI - - let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_editor.ui"); - - get_widget!(builder, gtk::Box, widget); - get_widget!(builder, gtk::Button, back_button); - get_widget!(builder, gtk::Button, select_button); - get_widget!(builder, gtk::Frame, parts_frame); - - let parts_list = gtk::ListBox::new(); - parts_list.set_selection_mode(gtk::SelectionMode::None); - parts_list.set_vexpand(false); - parts_list.show(); - parts_frame.add(&parts_list); - - let this = Rc::new(Self { - widget, - selection: RefCell::new(selection), - selected_cb: RefCell::new(None), - 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(); - } - })); - - select_button.connect_clicked(clone!(@strong this => move |_| { - let navigator = this.navigator.borrow().clone(); - if let Some(navigator) = navigator { - navigator.pop(); - } - - if let Some(cb) = &*this.selected_cb.borrow() { - let selection = this.selection.borrow().clone(); - cb(selection); - } - })); - - for (index, part) in recording.work.parts.iter().enumerate() { - let check = gtk::CheckButton::new(); - - check.connect_toggled(clone!(@strong this => move |check| { - let mut selection = this.selection.borrow_mut(); - if check.get_active() { - selection.push(index); - } else { - if let Some(pos) = selection.iter().position(|part| *part == index) { - selection.remove(pos); - } - } - })); - - let row = libhandy::ActionRow::new(); - row.add_prefix(&check); - row.set_activatable_widget(Some(&check)); - row.set_title(Some(&part.title)); - row.show_all(); - - parts_list.add(&row); - } - - this - } - - /// Set the closure to be called when the user has edited the track. - pub fn set_selected_cb) + 'static>(&self, cb: F) { - self.selected_cb.replace(Some(Box::new(cb))); - } -} - -impl NavigatorScreen for TrackEditor { - fn attach_navigator(&self, navigator: Rc) { - self.navigator.replace(Some(navigator)); - } - - fn get_widget(&self) -> gtk::Widget { - self.widget.clone().upcast() - } - - fn detach_navigator(&self) { - self.navigator.replace(None); - } -} - -/// A simple list of widgets. -struct List { - pub widget: gtk::ListBox, - make_widget: RefCell gtk::Widget>>>, -} - -impl List { - /// Create a new list. The list will be empty. - pub fn new(placeholder_text: &str) -> Self { - let placeholder_label = gtk::Label::new(Some(placeholder_text)); - placeholder_label.set_margin_top(6); - placeholder_label.set_margin_bottom(6); - placeholder_label.set_margin_start(6); - placeholder_label.set_margin_end(6); - placeholder_label.show(); - - let widget = gtk::ListBox::new(); - widget.set_selection_mode(gtk::SelectionMode::None); - widget.set_placeholder(Some(&placeholder_label)); - widget.show(); - - Self { - widget, - make_widget: RefCell::new(None), - } - } - - /// Set the closure to be called to construct widgets for the items. - pub fn set_make_widget gtk::Widget + 'static>(&self, make_widget: F) { - self.make_widget.replace(Some(Box::new(make_widget))); - } - - /// Call the make_widget function for each item. This will automatically - /// show all children by indices 0..length. - pub fn update(&self, length: usize) { - for child in self.widget.get_children() { - self.widget.remove(&child); - } - - if let Some(make_widget) = &*self.make_widget.borrow() { - for index in 0..length { - let row = make_widget(index); - self.widget.insert(&row, -1); - } - } - } -} diff --git a/musicus/src/editors/track_source.rs b/musicus/src/editors/track_source.rs deleted file mode 100644 index 3f09d05..0000000 --- a/musicus/src/editors/track_source.rs +++ /dev/null @@ -1,42 +0,0 @@ -use anyhow::Result; -use std::cell::Cell; -use std::path::Path; - -/// One track within a [`TrackSource`]. -#[derive(Debug, Clone)] -pub struct TrackState { - pub description: String, -} - -/// A live representation of a source of audio tracks. -pub struct TrackSource { - pub tracks: Vec, - pub ready: Cell, -} - -impl TrackSource { - /// Create a new track source for a folder. This will provide the folder's - /// files as selectable tracks and be ready immediately. - pub fn folder(path: &Path) -> Result { - let mut tracks = Vec::::new(); - - let entries = std::fs::read_dir(path)?; - for entry in entries { - let entry = entry?; - if entry.file_type()?.is_file() { - let file_name = entry.file_name(); - let track = TrackState { description: file_name.to_str().unwrap().to_owned() }; - tracks.push(track); - } - } - - tracks.sort_unstable_by(|a, b| { - a.description.cmp(&b.description) - }); - - Ok(Self { - tracks, - ready: Cell::new(true), - }) - } -} diff --git a/musicus/src/import/medium_editor.rs b/musicus/src/import/medium_editor.rs index 85b8a8d..742067c 100644 --- a/musicus/src/import/medium_editor.rs +++ b/musicus/src/import/medium_editor.rs @@ -1,6 +1,6 @@ use super::disc_source::DiscSource; +use super::track_set_editor::TrackSetEditor; use crate::backend::Backend; -// use crate::editors::{TrackSetEditor, TrackSource}; use crate::widgets::{Navigator, NavigatorScreen}; use crate::widgets::new_list::List; use glib::clone; @@ -12,7 +12,7 @@ use std::rc::Rc; /// A dialog for editing metadata while importing music into the music library. pub struct MediumEditor { backend: Rc, - source: DiscSource, + source: Rc, widget: gtk::Box, navigator: RefCell>>, } @@ -34,7 +34,7 @@ impl MediumEditor { let this = Rc::new(Self { backend, - source, + source: Rc::new(source), widget, navigator: RefCell::new(None), }); @@ -51,8 +51,8 @@ impl MediumEditor { add_button.connect_clicked(clone!(@strong this => move |_| { let navigator = this.navigator.borrow().clone(); if let Some(navigator) = navigator { - // let editor = TrackSetEditor::new(this.backend.clone(), this.source.clone()); - // navigator.push(editor); + let editor = TrackSetEditor::new(this.backend.clone(), Rc::clone(&this.source)); + navigator.push(editor); } })); diff --git a/musicus/src/import/mod.rs b/musicus/src/import/mod.rs index 248a7b7..2744611 100644 --- a/musicus/src/import/mod.rs +++ b/musicus/src/import/mod.rs @@ -1,5 +1,8 @@ mod disc_source; mod medium_editor; mod source_selector; +mod track_editor; +mod track_selector; +mod track_set_editor; pub use source_selector::SourceSelector; diff --git a/musicus/src/import/track_editor.rs b/musicus/src/import/track_editor.rs new file mode 100644 index 0000000..ff02c04 --- /dev/null +++ b/musicus/src/import/track_editor.rs @@ -0,0 +1,110 @@ +use crate::database::Recording; +use crate::widgets::{Navigator, NavigatorScreen}; +use crate::widgets::new_list::List; +use glib::clone; +use gtk::prelude::*; +use gtk_macros::get_widget; +use libhandy::prelude::*; +use std::cell::RefCell; +use std::rc::Rc; + +/// A screen for editing a single track. +pub struct TrackEditor { + widget: gtk::Box, + selection: RefCell>, + selected_cb: RefCell)>>>, + navigator: RefCell>>, +} + +impl TrackEditor { + /// Create a new track editor. + pub fn new(recording: Recording, selection: Vec) -> Rc { + // Create UI + + let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_editor.ui"); + + get_widget!(builder, gtk::Box, widget); + get_widget!(builder, gtk::Button, back_button); + get_widget!(builder, gtk::Button, select_button); + get_widget!(builder, gtk::Frame, parts_frame); + + let parts_list = gtk::ListBox::new(); + parts_list.set_selection_mode(gtk::SelectionMode::None); + parts_list.set_vexpand(false); + parts_list.show(); + parts_frame.add(&parts_list); + + let this = Rc::new(Self { + widget, + selection: RefCell::new(selection), + selected_cb: RefCell::new(None), + 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(); + } + })); + + select_button.connect_clicked(clone!(@strong this => move |_| { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + navigator.pop(); + } + + if let Some(cb) = &*this.selected_cb.borrow() { + let selection = this.selection.borrow().clone(); + cb(selection); + } + })); + + for (index, part) in recording.work.parts.iter().enumerate() { + let check = gtk::CheckButton::new(); + check.set_active(this.selection.borrow().contains(&index)); + + check.connect_toggled(clone!(@strong this => move |check| { + let mut selection = this.selection.borrow_mut(); + if check.get_active() { + selection.push(index); + } else { + if let Some(pos) = selection.iter().position(|part| *part == index) { + selection.remove(pos); + } + } + })); + + let row = libhandy::ActionRow::new(); + row.add_prefix(&check); + row.set_activatable_widget(Some(&check)); + row.set_title(Some(&part.title)); + row.show_all(); + + parts_list.add(&row); + } + + this + } + + /// Set the closure to be called when the user has edited the track. + pub fn set_selected_cb) + 'static>(&self, cb: F) { + self.selected_cb.replace(Some(Box::new(cb))); + } +} + +impl NavigatorScreen for TrackEditor { + fn attach_navigator(&self, navigator: Rc) { + self.navigator.replace(Some(navigator)); + } + + fn get_widget(&self) -> gtk::Widget { + self.widget.clone().upcast() + } + + fn detach_navigator(&self) { + self.navigator.replace(None); + } +} diff --git a/musicus/src/import/track_selector.rs b/musicus/src/import/track_selector.rs new file mode 100644 index 0000000..3404593 --- /dev/null +++ b/musicus/src/import/track_selector.rs @@ -0,0 +1,122 @@ +use super::disc_source::DiscSource; +use crate::widgets::{Navigator, NavigatorScreen}; +use glib::clone; +use gtk::prelude::*; +use gtk_macros::get_widget; +use libhandy::prelude::*; +use std::cell::RefCell; +use std::rc::Rc; + +/// A screen for selecting tracks from a medium. +pub struct TrackSelector { + source: Rc, + widget: gtk::Box, + select_button: gtk::Button, + selection: RefCell>, + selected_cb: RefCell)>>>, + navigator: RefCell>>, +} + +impl TrackSelector { + /// Create a new track selector. + pub fn new(source: Rc) -> Rc { + // Create UI + + let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_selector.ui"); + + get_widget!(builder, gtk::Box, widget); + get_widget!(builder, gtk::Button, back_button); + get_widget!(builder, gtk::Button, select_button); + get_widget!(builder, gtk::Frame, tracks_frame); + + let track_list = gtk::ListBox::new(); + track_list.set_selection_mode(gtk::SelectionMode::None); + track_list.set_vexpand(false); + track_list.show(); + tracks_frame.add(&track_list); + + let this = Rc::new(Self { + source, + widget, + select_button, + selection: RefCell::new(Vec::new()), + selected_cb: RefCell::new(None), + 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(); + } + })); + + this.select_button.connect_clicked(clone!(@strong this => move |_| { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + navigator.pop(); + } + + if let Some(cb) = &*this.selected_cb.borrow() { + let selection = this.selection.borrow().clone(); + cb(selection); + } + })); + + for (index, track) in this.source.tracks.iter().enumerate() { + let check = gtk::CheckButton::new(); + + check.connect_toggled(clone!(@strong this => move |check| { + let mut selection = this.selection.borrow_mut(); + if check.get_active() { + selection.push(index); + } else { + if let Some(pos) = selection.iter().position(|part| *part == index) { + selection.remove(pos); + } + } + + if selection.is_empty() { + this.select_button.set_sensitive(false); + } else { + this.select_button.set_sensitive(true); + } + })); + + let title = format!("Track {}", track.number); + + let row = libhandy::ActionRow::new(); + row.add_prefix(&check); + row.set_activatable_widget(Some(&check)); + row.set_title(Some(&title)); + row.show_all(); + + track_list.add(&row); + } + + this + } + + /// Set the closure to be called when the user has selected tracks. The + /// closure will be called with the indices of the selected tracks as its + /// argument. + pub fn set_selected_cb) + 'static>(&self, cb: F) { + self.selected_cb.replace(Some(Box::new(cb))); + } +} + +impl NavigatorScreen for TrackSelector { + fn attach_navigator(&self, navigator: Rc) { + self.navigator.replace(Some(navigator)); + } + + fn get_widget(&self) -> gtk::Widget { + self.widget.clone().upcast() + } + + fn detach_navigator(&self) { + self.navigator.replace(None); + } +} diff --git a/musicus/src/import/track_set_editor.rs b/musicus/src/import/track_set_editor.rs new file mode 100644 index 0000000..a418123 --- /dev/null +++ b/musicus/src/import/track_set_editor.rs @@ -0,0 +1,275 @@ +use super::disc_source::DiscSource; +use super::track_editor::TrackEditor; +use super::track_selector::TrackSelector; +use crate::backend::Backend; +use crate::database::{Recording, Track, TrackSet}; +use crate::selectors::{PersonSelector, RecordingSelector, WorkSelector}; +use crate::widgets::{Navigator, NavigatorScreen}; +use crate::widgets::new_list::List; +use gettextrs::gettext; +use glib::clone; +use gtk::prelude::*; +use gtk_macros::get_widget; +use libhandy::prelude::*; +use std::cell::{Cell, RefCell}; +use std::collections::HashSet; +use std::rc::Rc; + +/// A track set before being imported. +#[derive(Clone, Debug)] +pub struct TrackSetData { + pub recording: Recording, + pub tracks: Vec, +} + +/// A track before being imported. +#[derive(Clone, Debug)] +pub struct TrackData { + /// Index of the track source within the medium source's tracks. + pub track_source: usize, + + /// Actual track data. + pub work_parts: Vec, +} + +/// A screen for editing a set of tracks for one recording. +pub struct TrackSetEditor { + backend: Rc, + source: Rc, + widget: gtk::Box, + save_button: gtk::Button, + recording_row: libhandy::ActionRow, + track_list: List, + recording: RefCell>, + tracks: RefCell>, + done_cb: RefCell>>, + navigator: RefCell>>, +} + +impl TrackSetEditor { + /// Create a new track set editor. + pub fn new(backend: Rc, source: Rc) -> Rc { + // Create UI + + let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_set_editor.ui"); + + get_widget!(builder, gtk::Box, widget); + get_widget!(builder, gtk::Button, back_button); + get_widget!(builder, gtk::Button, save_button); + get_widget!(builder, libhandy::ActionRow, recording_row); + get_widget!(builder, gtk::Button, select_recording_button); + get_widget!(builder, gtk::Button, edit_tracks_button); + get_widget!(builder, gtk::Frame, tracks_frame); + + let track_list = List::new(&gettext!("No tracks added")); + tracks_frame.add(&track_list.widget); + + let this = Rc::new(Self { + backend, + source, + widget, + save_button, + recording_row, + track_list, + recording: RefCell::new(None), + tracks: RefCell::new(Vec::new()), + done_cb: RefCell::new(None), + 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(); + } + })); + + this.save_button.connect_clicked(clone!(@strong this => move |_| { + if let Some(cb) = &*this.done_cb.borrow() {} + })); + + select_recording_button.connect_clicked(clone!(@strong this => move |_| { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + let person_selector = PersonSelector::new(this.backend.clone()); + + person_selector.set_selected_cb(clone!(@strong this, @strong navigator => move |person| { + let work_selector = WorkSelector::new(this.backend.clone(), person.clone()); + + work_selector.set_selected_cb(clone!(@strong this, @strong navigator => move |work| { + let recording_selector = RecordingSelector::new(this.backend.clone(), work.clone()); + + recording_selector.set_selected_cb(clone!(@strong this, @strong navigator => move |recording| { + this.recording.replace(Some(recording.clone())); + this.recording_selected(); + + navigator.clone().pop(); + navigator.clone().pop(); + navigator.clone().pop(); + })); + + navigator.clone().push(recording_selector); + })); + + navigator.clone().push(work_selector); + })); + + navigator.clone().push(person_selector); + } + })); + + edit_tracks_button.connect_clicked(clone!(@strong this => move |_| { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + let selector = TrackSelector::new(Rc::clone(&this.source)); + + selector.set_selected_cb(clone!(@strong this => move |selection| { + 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(), + }; + + tracks.push(data); + } + + let length = tracks.len(); + this.tracks.replace(tracks); + this.track_list.update(length); + this.autofill_parts(); + })); + + navigator.push(selector); + } + })); + + this.track_list.set_make_widget(clone!(@strong this => move |index| { + let track = &this.tracks.borrow()[index]; + + let mut title_parts = Vec::::new(); + + if let Some(recording) = &*this.recording.borrow() { + for part in &track.work_parts { + title_parts.push(recording.work.parts[*part].title.clone()); + } + } + + let title = if title_parts.is_empty() { + gettext("Unknown") + } else { + title_parts.join(", ") + }; + + let number = this.source.tracks[track.track_source].number; + let subtitle = format!("Track {}", number); + + let edit_image = gtk::Image::from_icon_name(Some("document-edit-symbolic"), gtk::IconSize::Button); + let edit_button = gtk::Button::new(); + edit_button.set_relief(gtk::ReliefStyle::None); + edit_button.set_valign(gtk::Align::Center); + edit_button.add(&edit_image); + + let row = libhandy::ActionRow::new(); + row.set_activatable(true); + row.set_title(Some(&title)); + row.set_subtitle(Some(&subtitle)); + row.add(&edit_button); + row.set_activatable_widget(Some(&edit_button)); + row.show_all(); + + edit_button.connect_clicked(clone!(@strong this => move |_| { + let recording = this.recording.borrow().clone(); + let navigator = this.navigator.borrow().clone(); + + if let (Some(recording), Some(navigator)) = (recording, navigator) { + let track = &this.tracks.borrow()[index]; + + let editor = TrackEditor::new(recording, track.work_parts.clone()); + + editor.set_selected_cb(clone!(@strong this => move |selection| { + { + let mut tracks = this.tracks.borrow_mut(); + let mut track = &mut tracks[index]; + track.work_parts = selection; + }; + + this.update_tracks(); + })); + + navigator.push(editor); + } + })); + + row.upcast() + })); + + this + } + + /// Set the closure to be called when the user has created the track set. + pub fn set_done_cb(&self, cb: F) { + self.done_cb.replace(Some(Box::new(cb))); + } + + /// Set everything up after selecting a recording. + fn recording_selected(&self) { + if let Some(recording) = &*self.recording.borrow() { + self.recording_row.set_title(Some(&recording.work.get_title())); + self.recording_row.set_subtitle(Some(&recording.get_performers())); + self.save_button.set_sensitive(true); + } + + self.autofill_parts(); + } + + /// Automatically try to put work part information from the selected recording into the + /// selected tracks. + fn autofill_parts(&self) { + if let Some(recording) = &*self.recording.borrow() { + let mut tracks = self.tracks.borrow_mut(); + + for (index, _) in recording.work.parts.iter().enumerate() { + if let Some(mut track) = tracks.get_mut(index) { + track.work_parts = vec![index]; + } else { + break; + } + } + } + + self.update_tracks(); + } + + /// Update the track list. + fn update_tracks(&self) { + let length = self.tracks.borrow().len(); + self.track_list.update(length); + } +} + +impl NavigatorScreen for TrackSetEditor { + fn attach_navigator(&self, navigator: Rc) { + self.navigator.replace(Some(navigator)); + } + + fn get_widget(&self) -> gtk::Widget { + self.widget.clone().upcast() + } + + fn detach_navigator(&self) { + self.navigator.replace(None); + } +} + + + + + diff --git a/musicus/src/meson.build b/musicus/src/meson.build index ec71ba2..ae59ef2 100644 --- a/musicus/src/meson.build +++ b/musicus/src/meson.build @@ -63,7 +63,6 @@ sources = files( 'editors/performance.rs', 'editors/person.rs', 'editors/recording.rs', - 'editors/track_set.rs', 'editors/work.rs', 'editors/work_part.rs', 'editors/work_section.rs', From 035142d193919a744cf5b2f437a630288fa1a33f Mon Sep 17 00:00:00 2001 From: Elias Projahn Date: Wed, 13 Jan 2021 19:56:27 +0100 Subject: [PATCH 08/12] Hook up track set editor --- musicus/src/import/medium_editor.rs | 45 +++++++++++++++++++++++++- musicus/src/import/track_set_editor.rs | 18 +++++++++-- 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/musicus/src/import/medium_editor.rs b/musicus/src/import/medium_editor.rs index 742067c..57cec91 100644 --- a/musicus/src/import/medium_editor.rs +++ b/musicus/src/import/medium_editor.rs @@ -1,11 +1,12 @@ use super::disc_source::DiscSource; -use super::track_set_editor::TrackSetEditor; +use super::track_set_editor::{TrackSetData, TrackSetEditor}; use crate::backend::Backend; use crate::widgets::{Navigator, NavigatorScreen}; use crate::widgets::new_list::List; use glib::clone; use gtk::prelude::*; use gtk_macros::get_widget; +use libhandy::prelude::*; use std::cell::RefCell; use std::rc::Rc; @@ -14,6 +15,8 @@ pub struct MediumEditor { backend: Rc, source: Rc, widget: gtk::Box, + track_set_list: List, + track_sets: RefCell>, navigator: RefCell>>, } @@ -36,6 +39,8 @@ impl MediumEditor { backend, source: Rc::new(source), widget, + track_set_list: list, + track_sets: RefCell::new(Vec::new()), navigator: RefCell::new(None), }); @@ -52,10 +57,48 @@ impl MediumEditor { let navigator = this.navigator.borrow().clone(); if let Some(navigator) = navigator { let editor = TrackSetEditor::new(this.backend.clone(), Rc::clone(&this.source)); + + editor.set_done_cb(clone!(@strong this => move |track_set| { + let length = { + let mut track_sets = this.track_sets.borrow_mut(); + track_sets.push(track_set); + track_sets.len() + }; + + this.track_set_list.update(length); + })); + navigator.push(editor); } })); + this.track_set_list.set_make_widget(clone!(@strong this => move |index| { + let track_set = &this.track_sets.borrow()[index]; + + let title = track_set.recording.work.get_title(); + let subtitle = track_set.recording.get_performers(); + + let edit_image = gtk::Image::from_icon_name(Some("document-edit-symbolic"), gtk::IconSize::Button); + let edit_button = gtk::Button::new(); + edit_button.set_relief(gtk::ReliefStyle::None); + edit_button.set_valign(gtk::Align::Center); + edit_button.add(&edit_image); + + let row = libhandy::ActionRow::new(); + row.set_activatable(true); + row.set_title(Some(&title)); + row.set_subtitle(Some(&subtitle)); + row.add(&edit_button); + row.set_activatable_widget(Some(&edit_button)); + row.show_all(); + + edit_button.connect_clicked(clone!(@strong this => move |_| { + + })); + + row.upcast() + })); + this } } diff --git a/musicus/src/import/track_set_editor.rs b/musicus/src/import/track_set_editor.rs index a418123..6cef69b 100644 --- a/musicus/src/import/track_set_editor.rs +++ b/musicus/src/import/track_set_editor.rs @@ -42,7 +42,7 @@ pub struct TrackSetEditor { track_list: List, recording: RefCell>, tracks: RefCell>, - done_cb: RefCell>>, + done_cb: RefCell>>, navigator: RefCell>>, } @@ -87,7 +87,19 @@ impl TrackSetEditor { })); this.save_button.connect_clicked(clone!(@strong this => move |_| { - if let Some(cb) = &*this.done_cb.borrow() {} + if let Some(cb) = &*this.done_cb.borrow() { + let data = TrackSetData { + recording: this.recording.borrow().clone().unwrap(), + tracks: this.tracks.borrow().clone(), + }; + + cb(data); + } + + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + navigator.pop(); + } })); select_recording_button.connect_clicked(clone!(@strong this => move |_| { @@ -215,7 +227,7 @@ impl TrackSetEditor { } /// Set the closure to be called when the user has created the track set. - pub fn set_done_cb(&self, cb: F) { + pub fn set_done_cb(&self, cb: F) { self.done_cb.replace(Some(Box::new(cb))); } From d2ba34af1c3edce985e44e7938984ff80e29775d Mon Sep 17 00:00:00 2001 From: Elias Projahn Date: Wed, 13 Jan 2021 20:16:44 +0100 Subject: [PATCH 09/12] Add done button to medium editor --- musicus/res/ui/medium_editor.ui | 32 +++++++++++++++++++++++++++++ musicus/src/import/medium_editor.rs | 26 +++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/musicus/res/ui/medium_editor.ui b/musicus/res/ui/medium_editor.ui index 615b733..5c78ba8 100644 --- a/musicus/res/ui/medium_editor.ui +++ b/musicus/res/ui/medium_editor.ui @@ -36,6 +36,38 @@ end + + + True + True + False + + + True + crossfade + + + True + True + + + + + True + object-select-symbolic + + + + + + + + end + 0 + + diff --git a/musicus/src/import/medium_editor.rs b/musicus/src/import/medium_editor.rs index 57cec91..de78a43 100644 --- a/musicus/src/import/medium_editor.rs +++ b/musicus/src/import/medium_editor.rs @@ -4,6 +4,7 @@ use crate::backend::Backend; use crate::widgets::{Navigator, NavigatorScreen}; use crate::widgets::new_list::List; use glib::clone; +use glib::prelude::*; use gtk::prelude::*; use gtk_macros::get_widget; use libhandy::prelude::*; @@ -15,6 +16,9 @@ pub struct MediumEditor { backend: Rc, source: Rc, widget: gtk::Box, + done_button: gtk::Button, + done_stack: gtk::Stack, + done: gtk::Image, track_set_list: List, track_sets: RefCell>, navigator: RefCell>>, @@ -30,6 +34,9 @@ impl MediumEditor { get_widget!(builder, gtk::Box, widget); get_widget!(builder, gtk::Button, back_button); get_widget!(builder, gtk::Button, add_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::Frame, frame); let list = List::new("No recordings added."); @@ -39,6 +46,9 @@ impl MediumEditor { backend, source: Rc::new(source), widget, + done_button, + done_stack, + done, track_set_list: list, track_sets: RefCell::new(Vec::new()), navigator: RefCell::new(None), @@ -99,6 +109,22 @@ impl MediumEditor { row.upcast() })); + // Start ripping the CD in the background. + let context = glib::MainContext::default(); + let clone = this.clone(); + context.spawn_local(async move { + match clone.source.rip().await { + Err(error) => { + // TODO: Present error. + println!("Failed to rip: {}", error); + }, + Ok(_) => { + clone.done_stack.set_visible_child(&clone.done); + clone.done_button.set_sensitive(true); + } + } + }); + this } } From 5348b7750b239cd1d87632121b9127e263cce5d0 Mon Sep 17 00:00:00 2001 From: Elias Projahn Date: Wed, 13 Jan 2021 20:33:02 +0100 Subject: [PATCH 10/12] Add name entry to medium editor --- musicus/res/ui/medium_editor.ui | 104 ++++++++++++++++++++++------ musicus/src/import/medium_editor.rs | 5 +- 2 files changed, 87 insertions(+), 22 deletions(-) diff --git a/musicus/res/ui/medium_editor.ui b/musicus/res/ui/medium_editor.ui index 5c78ba8..7408cf5 100644 --- a/musicus/res/ui/medium_editor.ui +++ b/musicus/res/ui/medium_editor.ui @@ -21,21 +21,6 @@ - - - True - True - - - True - list-add-symbolic - - - - - end - - True @@ -65,7 +50,6 @@ end - 0 @@ -79,14 +63,92 @@ True - + True - start - in - 12 - 6 6 6 + 6 + vertical + + + True + start + 12 + 6 + Medium + + + + + + + + True + in + + + True + none + + + True + True + True + Name of the medium + name_entry + + + True + True + center + True + + + + + + + + + + + True + horizontal + 12 + 6 + + + True + start + end + True + Recordings + + + + + + + + True + True + none + + + True + list-add-symbolic + + + + + + + + + True + in + + diff --git a/musicus/src/import/medium_editor.rs b/musicus/src/import/medium_editor.rs index de78a43..4cf39b0 100644 --- a/musicus/src/import/medium_editor.rs +++ b/musicus/src/import/medium_editor.rs @@ -19,6 +19,7 @@ pub struct MediumEditor { done_button: gtk::Button, done_stack: gtk::Stack, done: gtk::Image, + name_entry: gtk::Entry, track_set_list: List, track_sets: RefCell>, navigator: RefCell>>, @@ -33,10 +34,11 @@ impl MediumEditor { get_widget!(builder, gtk::Box, widget); get_widget!(builder, gtk::Button, back_button); - get_widget!(builder, gtk::Button, add_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::Button, add_button); get_widget!(builder, gtk::Frame, frame); let list = List::new("No recordings added."); @@ -49,6 +51,7 @@ impl MediumEditor { done_button, done_stack, done, + name_entry, track_set_list: list, track_sets: RefCell::new(Vec::new()), navigator: RefCell::new(None), From aa6b5c6ac455d94b97557bd2fac796ab52ef1a3f Mon Sep 17 00:00:00 2001 From: Elias Projahn Date: Fri, 15 Jan 2021 22:27:43 +0100 Subject: [PATCH 11/12] Actually import from medium editor --- .../2020-09-27-201047_initial_schema/down.sql | 1 - .../2020-09-27-201047_initial_schema/up.sql | 8 +- musicus/res/ui/medium_editor.ui | 244 ++++++++++-------- musicus/src/database/files.rs | 54 ---- musicus/src/database/medium.rs | 64 ++++- musicus/src/database/mod.rs | 3 - musicus/src/database/schema.rs | 10 +- musicus/src/database/thread.rs | 48 +--- musicus/src/import/disc_source.rs | 4 + musicus/src/import/medium_editor.rs | 98 ++++++- musicus/src/import/track_set_editor.rs | 4 - musicus/src/meson.build | 1 - musicus/src/screens/recording_screen.rs | 28 +- 13 files changed, 328 insertions(+), 239 deletions(-) delete mode 100644 musicus/src/database/files.rs 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 From f69cb38b574186d40f70d76cc93969d5a9e06013 Mon Sep 17 00:00:00 2001 From: Elias Projahn Date: Sat, 16 Jan 2021 15:08:12 +0100 Subject: [PATCH 12/12] Use track set for recording screen --- musicus/src/database/medium.rs | 95 ++++++++++++------------- musicus/src/database/thread.rs | 12 ++-- musicus/src/player.rs | 5 +- musicus/src/screens/player_screen.rs | 16 ++--- musicus/src/screens/recording_screen.rs | 78 +++++++++++--------- musicus/src/widgets/player_bar.rs | 8 +-- 6 files changed, 108 insertions(+), 106 deletions(-) diff --git a/musicus/src/database/medium.rs b/musicus/src/database/medium.rs index f145ccf..0512c22 100644 --- a/musicus/src/database/medium.rs +++ b/musicus/src/database/medium.rs @@ -175,33 +175,22 @@ impl Database { Ok(()) } - /// Get all tracks for a recording. - pub fn get_tracks(&self, recording_id: &str) -> Result> { - let mut tracks: Vec = Vec::new(); + /// Get all available track sets for a recording. + pub fn get_track_sets(&self, recording_id: &str) -> Result> { + let mut track_sets: Vec = Vec::new(); - let rows = tracks::table - .inner_join(track_sets::table.on(track_sets::id.eq(tracks::track_set))) + let rows = track_sets::table .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)?; + .select(track_sets::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); + let track_set = self.get_track_set_from_row(row)?; + track_sets.push(track_set); } - Ok(tracks) + Ok(track_sets) } /// Retrieve all available information on a medium from related tables. @@ -214,36 +203,7 @@ impl Database { 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::(&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::>>()?; - - let track = Track { - work_parts, - path: track_row.path.clone(), - }; - - tracks.push(track); - } - - let track_set = TrackSet { recording, tracks }; - + let track_set = self.get_track_set_from_row(track_set_row)?; track_sets.push(track_set); } @@ -256,4 +216,39 @@ impl Database { Ok(medium) } + + /// Convert a track set row from the database to an actual track set. + fn get_track_set_from_row(&self, row: TrackSetRow) -> Result { + let recording_id = 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::track_set.eq(row.id)) + .order_by(tracks::index) + .load::(&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::>>()?; + + let track = Track { + work_parts, + path: track_row.path, + }; + + tracks.push(track); + } + + let track_set = TrackSet { recording, tracks }; + + Ok(track_set) + } } diff --git a/musicus/src/database/thread.rs b/musicus/src/database/thread.rs index 23955e8..2ce86fe 100644 --- a/musicus/src/database/thread.rs +++ b/musicus/src/database/thread.rs @@ -31,7 +31,7 @@ enum Action { UpdateMedium(Medium, Sender>), GetMedium(String, Sender>>), DeleteMedium(String, Sender>), - GetTracks(String, Sender>>), + GetTrackSets(String, Sender>>), Stop(Sender<()>), } @@ -134,8 +134,8 @@ impl DbThread { DeleteMedium(id, sender) => { sender.send(db.delete_medium(&id)).unwrap(); } - GetTracks(recording_id, sender) => { - sender.send(db.get_tracks(&recording_id)).unwrap(); + GetTrackSets(recording_id, sender) => { + sender.send(db.get_track_sets(&recording_id)).unwrap(); } Stop(sender) => { sender.send(()).unwrap(); @@ -339,10 +339,10 @@ impl DbThread { receiver.await? } - /// Get all tracks for a recording. - pub async fn get_tracks(&self, recording_id: &str) -> Result> { + /// Get all track sets for a recording. + pub async fn get_track_sets(&self, recording_id: &str) -> Result> { let (sender, receiver) = oneshot::channel(); - self.action_sender.send(GetTracks(recording_id.to_owned(), sender))?; + self.action_sender.send(GetTrackSets(recording_id.to_owned(), sender))?; receiver.await? } diff --git a/musicus/src/player.rs b/musicus/src/player.rs index f59371b..31ba92e 100644 --- a/musicus/src/player.rs +++ b/musicus/src/player.rs @@ -8,8 +8,7 @@ use std::rc::Rc; #[derive(Clone)] pub struct PlaylistItem { - pub tracks: TrackSet, - pub file_names: Vec, + pub track_set: TrackSet, pub indices: Vec, } @@ -249,7 +248,7 @@ impl Player { "file://{}", self.music_library_path .join( - self.playlist.borrow()[current_item].file_names[current_track].clone(), + self.playlist.borrow()[current_item].track_set.tracks[current_track].path.clone(), ) .to_str() .unwrap(), diff --git a/musicus/src/screens/player_screen.rs b/musicus/src/screens/player_screen.rs index 8a258e2..fe3ae81 100644 --- a/musicus/src/screens/player_screen.rs +++ b/musicus/src/screens/player_screen.rs @@ -215,17 +215,17 @@ impl PlayerScreen { elements.push(PlaylistElement { item: item_index, track: 0, - title: item.tracks.recording.work.get_title(), - subtitle: Some(item.tracks.recording.get_performers()), + title: item.track_set.recording.work.get_title(), + subtitle: Some(item.track_set.recording.get_performers()), playable: false, }); for track_index in &item.indices { - let track = &item.tracks.tracks[*track_index]; + let track = &item.track_set.tracks[*track_index]; let mut parts = Vec::::new(); for part in &track.work_parts { - parts.push(item.tracks.recording.work.parts[*part].title.clone()); + parts.push(item.track_set.recording.work.parts[*part].title.clone()); } let title = if parts.is_empty() { @@ -264,20 +264,20 @@ impl PlayerScreen { next_button.set_sensitive(player.has_next()); let item = &playlist.borrow()[current_item]; - let track = &item.tracks.tracks[current_track]; + let track = &item.track_set.tracks[current_track]; let mut parts = Vec::::new(); for part in &track.work_parts { - parts.push(item.tracks.recording.work.parts[*part].title.clone()); + parts.push(item.track_set.recording.work.parts[*part].title.clone()); } - let mut title = item.tracks.recording.work.get_title(); + let mut title = item.track_set.recording.work.get_title(); if !parts.is_empty() { title = format!("{}: {}", title, parts.join(", ")); } title_label.set_text(&title); - subtitle_label.set_text(&item.tracks.recording.get_performers()); + subtitle_label.set_text(&item.track_set.recording.get_performers()); position_label.set_text("0:00"); self_item.replace(current_item); diff --git a/musicus/src/screens/recording_screen.rs b/musicus/src/screens/recording_screen.rs index 18540fb..4d33aa9 100644 --- a/musicus/src/screens/recording_screen.rs +++ b/musicus/src/screens/recording_screen.rs @@ -17,7 +17,7 @@ pub struct RecordingScreen { recording: Recording, widget: gtk::Box, stack: gtk::Stack, - tracks: RefCell>, + track_sets: RefCell>, navigator: RefCell>>, } @@ -56,35 +56,43 @@ impl RecordingScreen { recording, widget, stack, - tracks: RefCell::new(Vec::new()), + track_sets: RefCell::new(Vec::new()), navigator: RefCell::new(None), }); - list.set_make_widget(clone!(@strong result => move |track: &Track| { - let mut title_parts = Vec::::new(); - for part in &track.work_parts { - title_parts.push(result.recording.work.parts[*part].title.clone()); - } - - let title = if title_parts.is_empty() { - gettext("Unknown") - } else { - title_parts.join(", ") - }; - - let title_label = gtk::Label::new(Some(&title)); - title_label.set_ellipsize(pango::EllipsizeMode::End); - title_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); - + list.set_make_widget(clone!(@strong result => move |track_set: &TrackSet| { let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0); vbox.set_border_width(6); - vbox.add(&title_label); - vbox.add(&file_name_label); + vbox.set_spacing(6); + + for track in &track_set.tracks { + let track_box = gtk::Box::new(gtk::Orientation::Vertical, 0); + + let mut title_parts = Vec::::new(); + for part in &track.work_parts { + title_parts.push(result.recording.work.parts[*part].title.clone()); + } + + let title = if title_parts.is_empty() { + gettext("Unknown") + } else { + title_parts.join(", ") + }; + + let title_label = gtk::Label::new(Some(&title)); + title_label.set_ellipsize(pango::EllipsizeMode::End); + title_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); + + track_box.add(&title_label); + track_box.add(&file_name_label); + + vbox.add(&track_box); + } vbox.upcast() })); @@ -97,12 +105,12 @@ impl RecordingScreen { })); add_to_playlist_button.connect_clicked(clone!(@strong result => move |_| { - if let Some(player) = result.backend.get_player() { - // player.add_item(PlaylistItem { - // recording: result.recording.clone(), - // tracks: result.tracks.borrow().clone(), - // }).unwrap(); - } + // if let Some(player) = result.backend.get_player() { + // player.add_item(PlaylistItem { + // track_set: result.track_sets.get(0).unwrap().clone(), + // indices: result.tracks.borrow().clone(), + // }).unwrap(); + // } })); edit_action.connect_activate(clone!(@strong result => move |_, _| { @@ -138,16 +146,16 @@ impl RecordingScreen { let context = glib::MainContext::default(); let clone = result.clone(); context.spawn_local(async move { - let tracks = clone + let track_sets = clone .backend .db() - .get_tracks(&clone.recording.id) + .get_track_sets(&clone.recording.id) .await .unwrap(); - list.show_items(tracks.clone()); + list.show_items(track_sets.clone()); clone.stack.set_visible_child_name("content"); - clone.tracks.replace(tracks); + clone.track_sets.replace(track_sets); }); result diff --git a/musicus/src/widgets/player_bar.rs b/musicus/src/widgets/player_bar.rs index c84d11b..1e61df6 100644 --- a/musicus/src/widgets/player_bar.rs +++ b/musicus/src/widgets/player_bar.rs @@ -112,20 +112,20 @@ impl PlayerBar { next_button.set_sensitive(player.has_next()); let item = &playlist.borrow()[current_item]; - let track = &item.tracks.tracks[current_track]; + let track = &item.track_set.tracks[current_track]; let mut parts = Vec::::new(); for part in &track.work_parts { - parts.push(item.tracks.recording.work.parts[*part].title.clone()); + parts.push(item.track_set.recording.work.parts[*part].title.clone()); } - let mut title = item.tracks.recording.work.get_title(); + let mut title = item.track_set.recording.work.get_title(); if !parts.is_empty() { title = format!("{}: {}", title, parts.join(", ")); } title_label.set_text(&title); - subtitle_label.set_text(&item.tracks.recording.get_performers()); + subtitle_label.set_text(&item.track_set.recording.get_performers()); position_label.set_text("0:00"); } ));