diff --git a/Cargo.toml b/Cargo.toml index 9ee79bb..73e529b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2018" anyhow = "1.0.33" diesel = { version = "1.4.5", features = ["sqlite"] } diesel_migrations = "1.4.0" +fragile = "1.0.0" futures = "0.3.6" futures-channel = "0.3.5" gettext-rs = "0.5.0" @@ -14,6 +15,8 @@ gio = "0.9.1" glib = "0.10.2" gtk = { version = "0.9.2", features = ["v3_24"] } gtk-macros = "0.2.0" +gstreamer = "0.16.4" +gstreamer-player = "0.16.3" libhandy = "0.7.0" pango = "0.9.1" rand = "0.7.3" diff --git a/meson.build b/meson.build index c9281cd..0c4ba05 100644 --- a/meson.build +++ b/meson.build @@ -6,6 +6,7 @@ project('musicus', 'rust', dependency('glib-2.0', version: '>= 2.56') dependency('gio-2.0', version: '>= 2.56') +dependency('gstreamer-1.0', version: '>= 1.12') dependency('gtk+-3.0', version: '>= 3.24.7') dependency('libhandy-1', version: '>= 1.0.0') dependency('pango', version: '>= 1.0') diff --git a/res/musicus.gresource.xml b/res/musicus.gresource.xml index 6b77e61..b446cce 100644 --- a/res/musicus.gresource.xml +++ b/res/musicus.gresource.xml @@ -12,6 +12,7 @@ ui/person_list.ui ui/person_screen.ui ui/person_selector.ui + ui/player_bar.ui ui/poe_list.ui ui/preferences.ui ui/recording_editor.ui diff --git a/res/ui/player_bar.ui b/res/ui/player_bar.ui new file mode 100644 index 0000000..1fe2112 --- /dev/null +++ b/res/ui/player_bar.ui @@ -0,0 +1,230 @@ + + + + + + True + False + media-playback-start-symbolic + + + True + False + slide-up + + + True + False + vertical + + + True + False + + + False + True + 0 + + + + + True + False + 6 + 12 + + + True + False + center + 6 + + + True + False + True + True + + + True + False + media-skip-backward-symbolic + + + + + False + True + 0 + + + + + True + True + True + + + True + False + media-playback-pause-symbolic + + + + + False + True + 1 + + + + + True + False + True + True + + + True + False + media-skip-forward-symbolic + + + + + False + True + 2 + + + + + False + True + 0 + + + + + True + False + vertical + + + True + False + start + Title + end + + + + + + False + True + 0 + + + + + True + False + start + Subtitle + end + + + False + True + 1 + + + + + True + True + 1 + + + + + True + True + True + center + + + True + False + view-list-bullet-symbolic + + + + + False + True + end + 1 + + + + + True + False + 2 + + + True + False + 0:00 + + + False + True + 0 + + + + + True + False + / + + + False + True + 1 + + + + + True + False + 0:00 + + + False + True + 2 + + + + + False + True + 3 + + + + + False + True + 1 + + + + + + diff --git a/res/ui/recording_screen.ui b/res/ui/recording_screen.ui index 782bd98..eaac798 100644 --- a/res/ui/recording_screen.ui +++ b/res/ui/recording_screen.ui @@ -122,6 +122,23 @@ 1 + + + Add to playlist + True + True + True + end + + + + False + True + 2 + + diff --git a/res/ui/window.ui b/res/ui/window.ui index ca4574f..3bf0610 100644 --- a/res/ui/window.ui +++ b/res/ui/window.ui @@ -26,9 +26,9 @@ False center center + 18 vertical 18 - 18 True @@ -230,78 +230,90 @@ - + True False + vertical - - 250 + True False - False - vertical - + + 250 True False - Musicus + False + vertical - + True - True - True + False + Musicus - + True - False - list-add-symbolic + True + True + + + True + False + list-add-symbolic + + - - - - - True - True - False - True - menu - + True - False - open-menu-symbolic + True + False + True + menu + + + True + False + open-menu-symbolic + + + + end + 1 + - end - 1 + False + True + 0 - False - True - 0 + sidebar + + + + + True + False + vertical + + + + False - sidebar - - - - - True - False - vertical - - - - False + True + True + 0 diff --git a/src/backend.rs b/src/backend.rs index ffa3472..09a9cb1 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -1,4 +1,5 @@ use super::database::*; +use crate::player::*; use anyhow::{anyhow, Result}; use futures_channel::oneshot::Sender; use futures_channel::{mpsc, oneshot}; @@ -50,6 +51,7 @@ pub struct Backend { action_sender: RefCell>>, settings: gio::Settings, music_library_path: RefCell>, + player: RefCell>>, } impl Backend { @@ -62,6 +64,7 @@ impl Backend { action_sender: RefCell::new(None), settings: gio::Settings::new("de.johrpan.musicus"), music_library_path: RefCell::new(None), + player: RefCell::new(None), } } @@ -267,10 +270,16 @@ impl Backend { self.music_library_path.borrow().clone() } + pub fn get_player(&self) -> Option> { + self.player.borrow().clone() + } + async fn set_music_library_path_priv(&self, path: PathBuf) -> Result<()> { - self.music_library_path.replace(Some(path.clone())); self.set_state(BackendState::Loading); + self.music_library_path.replace(Some(path.clone())); + self.player.replace(Some(Player::new(path.clone()))); + if let Some(action_sender) = self.action_sender.borrow_mut().take() { action_sender.send(Stop)?; } diff --git a/src/main.rs b/src/main.rs index 5dd64d2..e3555c8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,6 +15,7 @@ mod config; mod backend; mod database; mod dialogs; +mod player; mod screens; mod widgets; diff --git a/src/meson.build b/src/meson.build index 275424e..b7a72cf 100644 --- a/src/meson.build +++ b/src/meson.build @@ -63,10 +63,14 @@ sources = files( 'widgets/mod.rs', 'widgets/navigator.rs', 'widgets/person_list.rs', + 'widgets/player_bar.rs', 'widgets/poe_list.rs', 'widgets/selector_row.rs', 'backend.rs', + 'config.rs', + 'config.rs.in', 'main.rs', + 'player.rs', 'resources.rs', 'resources.rs.in', 'window.rs', diff --git a/src/player.rs b/src/player.rs new file mode 100644 index 0000000..bfb0bf9 --- /dev/null +++ b/src/player.rs @@ -0,0 +1,291 @@ +use crate::database::*; +use anyhow::anyhow; +use anyhow::Result; +use gstreamer_player::prelude::*; +use std::cell::{Cell, RefCell}; +use std::path::PathBuf; +use std::rc::Rc; + +#[derive(Clone)] +pub struct PlaylistItem { + pub recording: RecordingDescription, + pub tracks: Vec, +} + +pub struct Player { + music_library_path: PathBuf, + player: gstreamer_player::Player, + playlist: RefCell>, + current_item: Cell>, + current_track: Cell>, + playing: Cell, + playlist_cb: RefCell) -> ()>>>, + track_cb: RefCell ()>>>, + duration_cb: RefCell ()>>>, + playing_cb: RefCell ()>>>, + position_cb: RefCell ()>>>, +} + +impl Player { + pub fn new(music_library_path: PathBuf) -> Rc { + let dispatcher = gstreamer_player::PlayerGMainContextSignalDispatcher::new(None); + let player = gstreamer_player::Player::new(None, Some(&dispatcher.upcast())); + let mut config = player.get_config(); + config.set_position_update_interval(250); + player.set_config(config).unwrap(); + player.set_video_track_enabled(false); + + let result = Rc::new(Self { + music_library_path, + player: player.clone(), + playlist: RefCell::new(Vec::new()), + current_item: Cell::new(None), + current_track: Cell::new(None), + playing: Cell::new(false), + playlist_cb: RefCell::new(None), + track_cb: RefCell::new(None), + duration_cb: RefCell::new(None), + playing_cb: RefCell::new(None), + position_cb: RefCell::new(None), + }); + + let clone = fragile::Fragile::new(result.clone()); + player.connect_end_of_stream(move |_| { + let clone = clone.get(); + if clone.has_next() { + clone.next().unwrap(); + } else { + clone.player.stop(); + + if let Some(cb) = &*clone.playing_cb.borrow() { + cb(false); + } + } + }); + + let clone = fragile::Fragile::new(result.clone()); + player.connect_position_updated(move |_, position| { + if let Some(cb) = &*clone.get().position_cb.borrow() { + cb(position.mseconds().unwrap()); + } + }); + + let clone = fragile::Fragile::new(result.clone()); + player.connect_duration_changed(move |_, duration| { + if let Some(cb) = &*clone.get().duration_cb.borrow() { + cb(duration.mseconds().unwrap()); + } + }); + + result + } + + pub fn set_playlist_cb) -> () + 'static>(&self, cb: F) { + self.playlist_cb.replace(Some(Box::new(cb))); + } + + pub fn set_track_cb () + 'static>(&self, cb: F) { + self.track_cb.replace(Some(Box::new(cb))); + } + + pub fn set_duration_cb () + 'static>(&self, cb: F) { + self.duration_cb.replace(Some(Box::new(cb))); + } + + pub fn set_playing_cb () + 'static>(&self, cb: F) { + self.playing_cb.replace(Some(Box::new(cb))); + } + + pub fn set_position_cb () + 'static>(&self, cb: F) { + self.position_cb.replace(Some(Box::new(cb))); + } + + pub fn get_playlist(&self) -> Vec { + self.playlist.borrow().clone() + } + + pub fn get_current_item(&self) -> Option { + self.current_item.get() + } + + pub fn get_current_track(&self) -> Option { + self.current_track.get() + } + + pub fn get_duration(&self) -> gstreamer::ClockTime { + self.player.get_duration() + } + + pub fn is_playing(&self) -> bool { + self.playing.get() + } + + pub fn add_item(&self, item: PlaylistItem) -> Result<()> { + if item.tracks.is_empty() { + Err(anyhow!( + "Tried to add playlist item without tracks to playlist!" + )) + } else { + let was_empty = { + let mut playlist = self.playlist.borrow_mut(); + let was_empty = playlist.is_empty(); + + playlist.push(item); + + was_empty + }; + + if let Some(cb) = &*self.playlist_cb.borrow() { + cb(self.playlist.borrow().clone()); + } + + if was_empty { + self.set_track(0, 0)?; + self.player.play(); + self.playing.set(true); + + if let Some(cb) = &*self.playing_cb.borrow() { + cb(true); + } + } + + Ok(()) + } + } + + pub fn play_pause(&self) { + if self.is_playing() { + self.player.pause(); + self.playing.set(false); + + if let Some(cb) = &*self.playing_cb.borrow() { + cb(false); + } + } else { + self.player.play(); + self.playing.set(true); + + if let Some(cb) = &*self.playing_cb.borrow() { + cb(true); + } + } + } + + pub fn has_previous(&self) -> bool { + if let Some(current_item) = self.current_item.get() { + if let Some(current_track) = self.current_track.get() { + current_track > 0 || current_item > 0 + } else { + false + } + } else { + false + } + } + + pub fn previous(&self) -> Result<()> { + let mut current_item = self.current_item.get().ok_or(anyhow!("No current item!"))?; + let mut current_track = self + .current_track + .get() + .ok_or(anyhow!("No current track!"))?; + + let playlist = self.playlist.borrow(); + if current_track > 0 { + current_track -= 1; + } else if current_item > 0 { + current_item -= 1; + current_track = playlist[current_item].tracks.len() - 1; + } else { + return Err(anyhow!("No previous track!")); + } + + self.set_track(current_item, current_track) + } + + pub fn has_next(&self) -> bool { + if let Some(current_item) = self.current_item.get() { + if let Some(current_track) = self.current_track.get() { + let playlist = self.playlist.borrow(); + let item = &playlist[current_item]; + + current_track + 1 < item.tracks.len() || current_item + 1 < playlist.len() + } else { + false + } + } else { + false + } + } + + pub fn next(&self) -> Result<()> { + let mut current_item = self.current_item.get().ok_or(anyhow!("No current item!"))?; + let mut current_track = self + .current_track + .get() + .ok_or(anyhow!("No current track!"))?; + + let playlist = self.playlist.borrow(); + let item = &playlist[current_item]; + if current_track + 1 < item.tracks.len() { + current_track += 1; + } else if current_item + 1 < playlist.len() { + current_item += 1; + current_track = 0; + } else { + return Err(anyhow!("No next track!")); + } + + self.set_track(current_item, current_track) + } + + pub fn set_track(&self, current_item: usize, current_track: usize) -> Result<()> { + let uri = format!( + "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(), + ) + .to_str() + .unwrap(), + ); + + self.player.set_uri(&uri); + if self.is_playing() { + self.player.play(); + } + + self.current_item.set(Some(current_item)); + self.current_track.set(Some(current_track)); + + if let Some(cb) = &*self.track_cb.borrow() { + cb(current_item, current_track); + } + + Ok(()) + } + + pub fn clear(&self) { + self.player.stop(); + self.playing.set(false); + self.current_item.set(None); + self.current_track.set(None); + self.playlist.replace(Vec::new()); + + if let Some(cb) = &*self.playing_cb.borrow() { + cb(false); + } + + if let Some(cb) = &*self.playlist_cb.borrow() { + cb(Vec::new()); + } + } +} diff --git a/src/screens/recording_screen.rs b/src/screens/recording_screen.rs index c67d491..c852de9 100644 --- a/src/screens/recording_screen.rs +++ b/src/screens/recording_screen.rs @@ -1,5 +1,6 @@ use crate::backend::*; use crate::database::*; +use crate::player::*; use crate::widgets::*; use gettextrs::gettext; use glib::clone; @@ -13,13 +14,13 @@ pub struct RecordingScreen { backend: Rc, widget: gtk::Box, stack: gtk::Stack, + tracks: RefCell>, navigator: RefCell>>, } impl RecordingScreen { pub fn new(backend: Rc, recording: RecordingDescription) -> Rc { - let builder = - gtk::Builder::from_resource("/de/johrpan/musicus/ui/recording_screen.ui"); + let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/recording_screen.ui"); get_widget!(builder, gtk::Box, widget); get_widget!(builder, libhandy::HeaderBar, header); @@ -27,6 +28,7 @@ impl RecordingScreen { get_widget!(builder, gtk::MenuButton, menu_button); get_widget!(builder, gtk::Stack, stack); get_widget!(builder, gtk::Frame, frame); + get_widget!(builder, gtk::Button, add_to_playlist_button); header.set_title(Some(&recording.work.get_title())); header.set_subtitle(Some(&recording.get_performers())); @@ -88,6 +90,7 @@ impl RecordingScreen { backend, widget, stack, + tracks: RefCell::new(Vec::new()), navigator: RefCell::new(None), }); @@ -98,13 +101,25 @@ impl RecordingScreen { } })); + add_to_playlist_button.connect_clicked( + clone!(@strong result, @strong recording => move |_| { + if let Some(player) = result.backend.get_player() { + player.add_item(PlaylistItem { + recording: (*recording).clone(), + tracks: result.tracks.borrow().clone(), + }).unwrap(); + } + }), + ); + let context = glib::MainContext::default(); let clone = result.clone(); let id = recording.id; context.spawn_local(async move { let tracks = clone.backend.get_tracks(id).await.unwrap(); - list.show_items(tracks); + list.show_items(tracks.clone()); clone.stack.set_visible_child_name("content"); + clone.tracks.replace(tracks); }); result diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index b637738..0578fa3 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -7,6 +7,9 @@ pub use navigator::*; pub mod person_list; pub use person_list::*; +pub mod player_bar; +pub use player_bar::*; + pub mod poe_list; pub use poe_list::*; diff --git a/src/widgets/player_bar.rs b/src/widgets/player_bar.rs new file mode 100644 index 0000000..edd07a8 --- /dev/null +++ b/src/widgets/player_bar.rs @@ -0,0 +1,161 @@ +use crate::player::*; +use glib::clone; +use gtk::prelude::*; +use gtk_macros::get_widget; +use std::cell::RefCell; +use std::rc::Rc; + +pub struct PlayerBar { + pub widget: gtk::Revealer, + title_label: gtk::Label, + subtitle_label: gtk::Label, + previous_button: gtk::Button, + play_button: gtk::Button, + next_button: gtk::Button, + position_label: gtk::Label, + duration_label: gtk::Label, + play_image: gtk::Image, + pause_image: gtk::Image, + player: Rc>>>, +} + +impl PlayerBar { + pub fn new() -> Self { + let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/player_bar.ui"); + + get_widget!(builder, gtk::Revealer, widget); + get_widget!(builder, gtk::Label, title_label); + get_widget!(builder, gtk::Label, subtitle_label); + get_widget!(builder, gtk::Button, previous_button); + get_widget!(builder, gtk::Button, play_button); + get_widget!(builder, gtk::Button, next_button); + get_widget!(builder, gtk::Label, position_label); + get_widget!(builder, gtk::Label, duration_label); + get_widget!(builder, gtk::Image, play_image); + get_widget!(builder, gtk::Image, pause_image); + + let player = Rc::new(RefCell::new(None::>)); + + previous_button.connect_clicked(clone!(@strong player => move |_| { + if let Some(player) = &*player.borrow() { + player.previous().unwrap(); + } + })); + + play_button.connect_clicked(clone!(@strong player => move |_| { + if let Some(player) = &*player.borrow() { + player.play_pause(); + } + })); + + next_button.connect_clicked(clone!(@strong player => move |_| { + if let Some(player) = &*player.borrow() { + player.next().unwrap(); + } + })); + + Self { + widget, + title_label, + subtitle_label, + previous_button, + play_button, + next_button, + position_label, + duration_label, + play_image, + pause_image, + player: player, + } + } + + pub fn set_player(&self, player: Option>) { + self.player.replace(player.clone()); + + if let Some(player) = player { + let playlist = Rc::new(RefCell::new(Vec::::new())); + + player.set_playlist_cb(clone!( + @strong player, + @strong self.widget as widget, + @strong self.previous_button as previous_button, + @strong self.next_button as next_button, + @strong playlist + => move |new_playlist| { + widget.set_reveal_child(!new_playlist.is_empty()); + playlist.replace(new_playlist); + previous_button.set_sensitive(player.has_previous()); + next_button.set_sensitive(player.has_next()); + } + )); + + player.set_track_cb(clone!( + @strong player, + @strong playlist, + @strong self.previous_button as previous_button, + @strong self.next_button as next_button, + @strong self.title_label as title_label, + @strong self.subtitle_label as subtitle_label, + @strong self.position_label as position_label + => move |current_item, current_track| { + previous_button.set_sensitive(player.has_previous()); + next_button.set_sensitive(player.has_next()); + + let item = &playlist.borrow()[current_item]; + let track = &item.tracks[current_track]; + + let mut parts = Vec::::new(); + for part in &track.work_parts { + parts.push(item.recording.work.parts[*part].title.clone()); + } + + let mut title = item.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()); + position_label.set_text("0:00"); + } + )); + + player.set_duration_cb(clone!( + @strong self.duration_label as duration_label + => move |ms| { + let min = ms / 60000; + let sec = (ms % 60000) / 1000; + duration_label.set_text(&format!("{}:{:02}", min, sec)); + } + )); + + player.set_playing_cb(clone!( + @strong self.play_button as play_button, + @strong self.play_image as play_image, + @strong self.pause_image as pause_image + => move |playing| { + if let Some(child) = play_button.get_child() { + play_button.remove( &child); + } + + play_button.add(if playing { + &pause_image + } else { + &play_image + }); + } + )); + + player.set_position_cb(clone!( + @strong self.position_label as position_label + => move |ms| { + let min = ms / 60000; + let sec = (ms % 60000) / 1000; + position_label.set_text(&format!("{}:{:02}", min, sec)); + } + )); + } else { + self.widget.set_reveal_child(false); + } + } +} diff --git a/src/window.rs b/src/window.rs index aa99658..b433d74 100644 --- a/src/window.rs +++ b/src/window.rs @@ -19,6 +19,7 @@ pub struct Window { sidebar_box: gtk::Box, poe_list: Rc, navigator: Rc, + player_bar: PlayerBar, } impl Window { @@ -28,6 +29,7 @@ impl Window { get_widget!(builder, libhandy::ApplicationWindow, window); get_widget!(builder, gtk::Stack, stack); get_widget!(builder, gtk::Button, select_music_library_path_button); + get_widget!(builder, gtk::Box, content_box); get_widget!(builder, libhandy::Leaflet, leaflet); get_widget!(builder, gtk::Button, add_button); get_widget!(builder, gtk::Box, sidebar_box); @@ -42,6 +44,9 @@ impl Window { leaflet.set_visible_child(&sidebar_box); })); + let player_bar = PlayerBar::new(); + content_box.add(&player_bar.widget); + let result = Rc::new(Self { backend, window, @@ -50,6 +55,7 @@ impl Window { sidebar_box, poe_list, navigator, + player_bar, }); result.window.set_application(Some(app)); @@ -290,6 +296,9 @@ impl Window { BackendState::Ready => { clone.stack.set_visible_child_name("content"); clone.poe_list.clone().reload(); + + let player = clone.backend.get_player().unwrap(); + clone.player_bar.set_player(Some(player)); } } }