Basic support for random programs

This commit is contained in:
Elias Projahn 2024-06-10 16:57:36 +02:00
parent 016bace36e
commit 815dede141
4 changed files with 170 additions and 104 deletions

View file

@ -4,7 +4,6 @@ use crate::{
editor::{person_editor::MusicusPersonEditor, work_editor::MusicusWorkEditor}, editor::{person_editor::MusicusPersonEditor, work_editor::MusicusWorkEditor},
library::{LibraryQuery, MusicusLibrary}, library::{LibraryQuery, MusicusLibrary},
player::MusicusPlayer, player::MusicusPlayer,
playlist_item::PlaylistItem,
program::Program, program::Program,
program_tile::MusicusProgramTile, program_tile::MusicusProgramTile,
recording_tile::MusicusRecordingTile, recording_tile::MusicusRecordingTile,
@ -38,6 +37,7 @@ mod imp {
#[property(get, construct_only)] #[property(get, construct_only)]
pub player: OnceCell<MusicusPlayer>, pub player: OnceCell<MusicusPlayer>,
pub programs: RefCell<Vec<Program>>,
pub composers: RefCell<Vec<Person>>, pub composers: RefCell<Vec<Person>>,
pub performers: RefCell<Vec<Person>>, pub performers: RefCell<Vec<Person>>,
pub ensembles: RefCell<Vec<Ensemble>>, pub ensembles: RefCell<Vec<Ensemble>>,
@ -110,18 +110,19 @@ mod imp {
.build(); .build();
let settings = gio::Settings::new("de.johrpan.musicus"); let settings = gio::Settings::new("de.johrpan.musicus");
let program1 = Program::deserialize(&settings.string("program1")).unwrap();
let program2 = Program::deserialize(&settings.string("program2")).unwrap();
let program3 = Program::deserialize(&settings.string("program3")).unwrap();
self.programs_flow_box let programs = vec![
.append(&MusicusProgramTile::new(program1)); Program::deserialize(&settings.string("program1")).unwrap(),
Program::deserialize(&settings.string("program2")).unwrap(),
Program::deserialize(&settings.string("program3")).unwrap(),
];
self.programs_flow_box for program in &programs {
.append(&MusicusProgramTile::new(program2)); self.programs_flow_box
.append(&MusicusProgramTile::new(program.to_owned()));
}
self.programs_flow_box self.programs.replace(programs);
.append(&MusicusProgramTile::new(program3));
self.obj().query(&LibraryQuery::default()); self.obj().query(&LibraryQuery::default());
} }
@ -179,6 +180,10 @@ impl MusicusHomePage {
#[template_callback] #[template_callback]
fn play(&self, _: &gtk::Button) { fn play(&self, _: &gtk::Button) {
log::info!("Play button clicked"); log::info!("Play button clicked");
let program = Program::from_query(self.imp().search_entry.query());
self.player().set_program(program);
self.player().play(); self.player().play();
} }
@ -187,7 +192,9 @@ impl MusicusHomePage {
let imp = self.imp(); let imp = self.imp();
if imp.programs_flow_box.is_visible() { if imp.programs_flow_box.is_visible() {
log::info!("Program selected"); if let Some(program) = imp.programs.borrow().first().cloned() {
self.player().set_program(program);
}
} else { } else {
let (composer, performer, ensemble, work, recording, album) = { let (composer, performer, ensemble, work, recording, album) = {
( (
@ -209,7 +216,7 @@ impl MusicusHomePage {
} else if let Some(work) = work { } else if let Some(work) = work {
search_entry.add_tag(Tag::Work(work)); search_entry.add_tag(Tag::Work(work));
} else if let Some(recording) = recording { } else if let Some(recording) = recording {
self.play_recording(&recording); self.player().play_recording(&recording);
} else if let Some(album) = album { } else if let Some(album) = album {
self.show_album(&album); self.show_album(&album);
} }
@ -218,10 +225,8 @@ impl MusicusHomePage {
#[template_callback] #[template_callback]
fn program_selected(&self, tile: &gtk::FlowBoxChild, _: &gtk::FlowBox) { fn program_selected(&self, tile: &gtk::FlowBoxChild, _: &gtk::FlowBox) {
log::info!( self.player()
"Program selected: {:?}", .set_program(tile.downcast_ref::<MusicusProgramTile>().unwrap().program());
tile.downcast_ref::<MusicusProgramTile>().unwrap().program()
);
} }
#[template_callback] #[template_callback]
@ -233,7 +238,7 @@ impl MusicusHomePage {
#[template_callback] #[template_callback]
fn recording_selected(&self, tile: &gtk::FlowBoxChild, _: &gtk::FlowBox) { fn recording_selected(&self, tile: &gtk::FlowBoxChild, _: &gtk::FlowBox) {
self.play_recording( self.player().play_recording(
tile.downcast_ref::<MusicusRecordingTile>() tile.downcast_ref::<MusicusRecordingTile>()
.unwrap() .unwrap()
.recording(), .recording(),
@ -245,74 +250,6 @@ impl MusicusHomePage {
self.show_album(tile.downcast_ref::<MusicusAlbumTile>().unwrap().album()); self.show_album(tile.downcast_ref::<MusicusAlbumTile>().unwrap().album());
} }
fn play_recording(&self, recording: &Recording) {
let tracks = &recording.tracks;
if tracks.is_empty() {
log::warn!("Ignoring recording without tracks being added to the playlist.");
return;
}
let title = format!(
"{}: {}",
recording.work.composers_string(),
recording.work.name.get(),
);
let performances = recording.performers_string();
let mut items = Vec::new();
if tracks.len() == 1 {
items.push(PlaylistItem::new(
true,
&title,
Some(&performances),
None,
&tracks[0].path,
));
} else {
let mut tracks = tracks.into_iter();
let first_track = tracks.next().unwrap();
let track_title = |track: &Track, number: usize| -> String {
let title = track
.works
.iter()
.map(|w| w.name.get().to_string())
.collect::<Vec<String>>()
.join(", ");
if title.is_empty() {
format!("Track {number}")
} else {
title
}
};
items.push(PlaylistItem::new(
true,
&title,
Some(&performances),
Some(&track_title(&first_track, 1)),
&first_track.path,
));
for (index, track) in tracks.enumerate() {
items.push(PlaylistItem::new(
false,
&title,
Some(&performances),
// track number = track index + 1 (first track) + 1 (zero based)
Some(&track_title(&track, index + 2)),
&track.path,
));
}
}
self.player().append(items);
}
fn show_album(&self, _album: &Album) { fn show_album(&self, _album: &Album) {
todo!("Show album"); todo!("Show album");
} }

View file

@ -8,7 +8,10 @@ use chrono::prelude::*;
use diesel::{dsl::exists, prelude::*, QueryDsl, SqliteConnection}; use diesel::{dsl::exists, prelude::*, QueryDsl, SqliteConnection};
use gtk::{glib, glib::Properties, prelude::*, subclass::prelude::*}; use gtk::{glib, glib::Properties, prelude::*, subclass::prelude::*};
use crate::db::{self, models::*, schema::*, tables, TranslatedString}; use crate::{
db::{self, models::*, schema::*, tables, TranslatedString},
program::Program,
};
diesel::define_sql_function! { diesel::define_sql_function! {
/// Represents the SQL RANDOM() function. /// Represents the SQL RANDOM() function.
@ -380,19 +383,45 @@ impl MusicusLibrary {
}) })
} }
pub fn random_recording(&self, query: &LibraryQuery) -> Result<Recording> { pub fn generate_recording(&self, program: &Program) -> Result<Recording> {
let mut binding = self.imp().connection.borrow_mut(); let mut binding = self.imp().connection.borrow_mut();
let connection = &mut *binding.as_mut().unwrap(); let connection = &mut *binding.as_mut().unwrap();
match query { let mut query = recordings::table
LibraryQuery { .. } => Recording::from_table( .inner_join(works::table.inner_join(work_persons::table))
recordings::table .inner_join(recording_persons::table)
.order(random()) .inner_join(recording_ensembles::table)
.first::<tables::Recording>(connection)?, .inner_join(album_recordings::table)
&self.folder(), .into_boxed();
connection,
), if let Some(composer_id) = program.composer_id() {
query = query.filter(work_persons::person_id.eq(composer_id));
} }
if let Some(performer_id) = program.performer_id() {
query = query.filter(recording_persons::person_id.eq(performer_id));
}
if let Some(ensemble_id) = program.ensemble_id() {
query = query.filter(recording_ensembles::ensemble_id.eq(ensemble_id));
}
if let Some(work_id) = program.work_id() {
query = query.filter(recordings::work_id.eq(work_id));
}
if let Some(album_id) = program.album_id() {
query = query.filter(album_recordings::album_id.eq(album_id));
}
// TODO: Implement prefer_recently_added and prefer_least_recently_played.
let row = query
.order(random())
.select(tables::Recording::as_select())
.first::<tables::Recording>(connection)?;
Recording::from_table(row, &self.folder(), connection)
} }
pub fn search_persons(&self, search: &str) -> Result<Vec<Person>> { pub fn search_persons(&self, search: &str) -> Result<Vec<Person>> {

View file

@ -1,4 +1,8 @@
use crate::playlist_item::PlaylistItem; use std::{
cell::{Cell, OnceCell, RefCell},
sync::Arc,
};
use fragile::Fragile; use fragile::Fragile;
use gstreamer_play::gst; use gstreamer_play::gst;
use gtk::{ use gtk::{
@ -7,25 +11,30 @@ use gtk::{
prelude::*, prelude::*,
subclass::prelude::*, subclass::prelude::*,
}; };
use mpris_player::{MprisPlayer, PlaybackStatus}; use mpris_player::{Metadata, MprisPlayer, PlaybackStatus};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use std::{
cell::{Cell, OnceCell}, use crate::{
sync::Arc, db::models::{Recording, Track},
library::MusicusLibrary,
playlist_item::PlaylistItem,
program::Program,
}; };
mod imp { mod imp {
use mpris_player::Metadata;
use super::*; use super::*;
#[derive(Properties, Debug, Default)] #[derive(Properties, Debug, Default)]
#[properties(wrapper_type = super::MusicusPlayer)] #[properties(wrapper_type = super::MusicusPlayer)]
pub struct MusicusPlayer { pub struct MusicusPlayer {
#[property(get, set)]
pub library: RefCell<Option<MusicusLibrary>>,
#[property(get, set)] #[property(get, set)]
pub active: Cell<bool>, pub active: Cell<bool>,
#[property(get, set)] #[property(get, set)]
pub playing: Cell<bool>, pub playing: Cell<bool>,
#[property(get, set = Self::set_program)]
pub program: RefCell<Option<Program>>,
#[property(get, construct_only)] #[property(get, construct_only)]
pub playlist: OnceCell<gio::ListStore>, pub playlist: OnceCell<gio::ListStore>,
#[property(get, set = Self::set_current_index)] #[property(get, set = Self::set_current_index)]
@ -41,6 +50,17 @@ mod imp {
} }
impl MusicusPlayer { impl MusicusPlayer {
pub fn set_program(&self, program: &Program) {
self.program.replace(Some(program.to_owned()));
if !self.obj().active() {
self.obj().set_active(true);
self.obj().generate_items(program);
self.obj().set_current_index(0);
self.obj().play();
}
}
pub fn set_current_index(&self, index: u32) { pub fn set_current_index(&self, index: u32) {
let playlist = self.playlist.get().unwrap(); let playlist = self.playlist.get().unwrap();
@ -144,10 +164,10 @@ mod imp {
if let Some(duration) = duration { if let Some(duration) = duration {
let obj = obj.get(); let obj = obj.get();
let imp = obj.imp(); let imp = obj.imp();
imp.position_ms.set(0); imp.position_ms.set(0);
obj.notify_position_ms(); obj.notify_position_ms();
imp.duration_ms.set(duration.mseconds()); imp.duration_ms.set(duration.mseconds());
obj.notify_duration_ms(); obj.notify_duration_ms();
} }
@ -183,6 +203,74 @@ impl MusicusPlayer {
}) })
} }
pub fn play_recording(&self, recording: &Recording) {
let tracks = &recording.tracks;
if tracks.is_empty() {
log::warn!("Ignoring recording without tracks being added to the playlist.");
return;
}
let title = format!(
"{}: {}",
recording.work.composers_string(),
recording.work.name.get(),
);
let performances = recording.performers_string();
let mut items = Vec::new();
if tracks.len() == 1 {
items.push(PlaylistItem::new(
true,
&title,
Some(&performances),
None,
&tracks[0].path,
));
} else {
let mut tracks = tracks.into_iter();
let first_track = tracks.next().unwrap();
let track_title = |track: &Track, number: usize| -> String {
let title = track
.works
.iter()
.map(|w| w.name.get().to_string())
.collect::<Vec<String>>()
.join(", ");
if title.is_empty() {
format!("Track {number}")
} else {
title
}
};
items.push(PlaylistItem::new(
true,
&title,
Some(&performances),
Some(&track_title(&first_track, 1)),
&first_track.path,
));
for (index, track) in tracks.enumerate() {
items.push(PlaylistItem::new(
false,
&title,
Some(&performances),
// track number = track index + 1 (first track) + 1 (zero based)
Some(&track_title(&track, index + 2)),
&track.path,
));
}
}
self.append(items);
}
pub fn append(&self, tracks: Vec<PlaylistItem>) { pub fn append(&self, tracks: Vec<PlaylistItem>) {
let playlist = self.playlist(); let playlist = self.playlist();
@ -245,6 +333,9 @@ impl MusicusPlayer {
pub fn next(&self) { pub fn next(&self) {
if self.current_index() < self.playlist().n_items() - 1 { if self.current_index() < self.playlist().n_items() - 1 {
self.set_current_index(self.current_index() + 1); self.set_current_index(self.current_index() + 1);
} else if let Some(program) = self.program() {
self.generate_items(&program);
self.set_current_index(self.current_index() + 1);
} }
} }
@ -253,6 +344,14 @@ impl MusicusPlayer {
self.set_current_index(self.current_index() - 1); self.set_current_index(self.current_index() - 1);
} }
} }
fn generate_items(&self, program: &Program) {
if let Some(library) = self.library() {
// TODO: if program.play_full_recordings() {
let recording = library.generate_recording(program).unwrap();
self.play_recording(&recording);
}
}
} }
impl Default for MusicusPlayer { impl Default for MusicusPlayer {

View file

@ -154,6 +154,7 @@ impl MusicusWindow {
fn load_library(&self, path: impl AsRef<Path>) { fn load_library(&self, path: impl AsRef<Path>) {
let library = MusicusLibrary::new(path); let library = MusicusLibrary::new(path);
self.imp().player.set_library(&library);
let navigation = self.imp().navigation_view.get(); let navigation = self.imp().navigation_view.get();
navigation.replace(&[MusicusHomePage::new(&navigation, &library, &self.imp().player).into()]); navigation.replace(&[MusicusHomePage::new(&navigation, &library, &self.imp().player).into()]);