From 653d5cd629afcea772e37f37315aedb2e1b80ba7 Mon Sep 17 00:00:00 2001 From: Elias Projahn Date: Sat, 15 Mar 2025 07:08:19 +0100 Subject: [PATCH] program: Penalize recently played composers and instruments --- data/de.johrpan.Musicus.gschema.xml.in | 6 +- src/library.rs | 131 ++++++++++++++++++------- src/program.rs | 42 +++++++- 3 files changed, 136 insertions(+), 43 deletions(-) diff --git a/data/de.johrpan.Musicus.gschema.xml.in b/data/de.johrpan.Musicus.gschema.xml.in index 4d36e4e..a8bb49a 100644 --- a/data/de.johrpan.Musicus.gschema.xml.in +++ b/data/de.johrpan.Musicus.gschema.xml.in @@ -19,17 +19,17 @@ - '{"title":"Just play some music","description":"Randomly select some music. Customize programs using the button in the top right.","design":"Program1","prefer_recently_added":0.0,"prefer_least_recently_played":0.1,"play_full_recordings":true}' + '{"title":"Just play some music","description":"Randomly select some music. Customize programs using the button in the top right.","design":"Program1","prefer_recently_added":0.0,"prefer_least_recently_played":0.1,"avoid_repeated_composers_seconds":3600,"avoid_repeated_instruments_seconds":3600,"play_full_recordings":true}' Default settings for program 1 - '{"title":"What\'s new?","description":"Recordings that you recently added to your music library.","design":"Program2","prefer_recently_added":1.0,"prefer_least_recently_played":0.0,"play_full_recordings":true}' + '{"title":"What\'s new?","description":"Recordings that you recently added to your music library.","design":"Program2","prefer_recently_added":1.0,"prefer_least_recently_played":0.0,"avoid_repeated_composers_seconds":3600,"avoid_repeated_instruments_seconds":3600,"play_full_recordings":true}' Default settings for program 2 - '{"title":"A long time ago","description":"Works that you haven\'t listened to for a long time.","design":"Program3","prefer_recently_added":0.0,"prefer_least_recently_played":1.0,"play_full_recordings":true}' + '{"title":"A long time ago","description":"Works that you haven\'t listened to for a long time.","design":"Program3","prefer_recently_added":0.0,"prefer_least_recently_played":1.0,"avoid_repeated_composers_seconds":3600,"avoid_repeated_instruments_seconds":3600,"play_full_recordings":true}' Default settings for program 3 diff --git a/src/library.rs b/src/library.rs index 393dc87..a7f511e 100644 --- a/src/library.rs +++ b/src/library.rs @@ -532,8 +532,8 @@ impl Library { let mut query = recordings::table .inner_join( works::table - .left_join(work_persons::table) - .left_join(work_instruments::table), + .left_join(work_persons::table.inner_join(persons::table)) + .left_join(work_instruments::table.inner_join(instruments::table)), ) .left_join(recording_persons::table) .left_join( @@ -576,42 +576,82 @@ impl Library { query = query.filter(album_recordings::album_id.eq(album_id)); } - if program.prefer_least_recently_played() > 0.0 || program.prefer_recently_added() > 0.0 { - // Orders recordings using a dynamically calculated priority score that includes: - // - a random base value between 0.0 and 1.0 giving equal probability to each recording - // - weighted by the average of two scores between 0.0 and 1.0 based on - // 1. how long ago the last playback is - // 2. how recently the recording was added to the library - // Both scores are individually modified based on the following formula: - // e^(10 * a * (score - 1)) - // This assigns a new score between 0.0 and 1.0 that favors higher scores with "a" being - // a user defined constant to determine the bias. - query = query.order( - diesel::dsl::sql::("( \ - WITH \ - global_bounds AS ( \ - SELECT \ - MIN(UNIXEPOCH(last_played_at)) AS min_last_played_at, \ - NULLIF(MAX(UNIXEPOCH(last_played_at)) - MIN(UNIXEPOCH(last_played_at)), 0.0) AS last_played_at_range, \ - MIN(UNIXEPOCH(created_at)) AS min_created_at, \ - NULLIF(MAX(UNIXEPOCH(created_at)) - MIN(UNIXEPOCH(created_at)), 0.0) AS created_at_range \ - FROM recordings \ - ), \ - normalized AS ( \ - SELECT \ - IFNULL(1.0 - (UNIXEPOCH(recordings.last_played_at) - min_last_played_at) * 1.0 / last_played_at_range, 1.0) AS least_recently_played, \ - IFNULL((UNIXEPOCH(recordings.created_at) - min_created_at) * 1.0 / created_at_range, 1.0) AS recently_created \ - FROM global_bounds \ - ) \ - SELECT (RANDOM() / 9223372036854775808.0 + 1.0) / 2.0 * (EXP(10.0 * ") - .bind::(program.prefer_least_recently_played()) - .sql(" * (least_recently_played - 1.0)) + EXP(10.0 * ") - .bind::(program.prefer_recently_added()) - .sql(" * (recently_created - 1.0))) / 2.0 FROM normalized) DESC") - ); - } else { - query = query.order(random()); - } + // Orders recordings using a dynamically calculated priority score that includes: + // - a random base value between 0.0 and 1.0 giving equal probability to each recording + // - weighted by the average of two scores between 0.0 and 1.0 based on + // 1. how long ago the last playback is + // 2. how recently the recording was added to the library + // Both scores are individually modified based on the following formula: + // e^(10 * a * (score - 1)) + // This assigns a new score between 0.0 and 1.0 that favors higher scores with "a" being + // a user defined constant to determine the bias. + query = query.order( + diesel::dsl::sql::("( \ + WITH global_bounds AS ( + SELECT MIN(UNIXEPOCH(last_played_at)) AS min_last_played_at, + NULLIF( + MAX(UNIXEPOCH(last_played_at)) - MIN(UNIXEPOCH(last_played_at)), + 0.0 + ) AS last_played_at_range, + MIN(UNIXEPOCH(created_at)) AS min_created_at, + NULLIF( + MAX(UNIXEPOCH(created_at)) - MIN(UNIXEPOCH(created_at)), + 0.0 + ) AS created_at_range + FROM recordings + ), + normalized AS ( + SELECT IFNULL( + 1.0 - ( + UNIXEPOCH(recordings.last_played_at) - min_last_played_at + ) * 1.0 / last_played_at_range, + 1.0 + ) AS least_recently_played, + IFNULL( + ( + UNIXEPOCH(recordings.created_at) - min_created_at + ) * 1.0 / created_at_range, + 1.0 + ) AS recently_created + FROM global_bounds + ) + SELECT (RANDOM() / 9223372036854775808.0 + 1.0) / 2.0 * MIN( + ( + EXP(10.0 * ") + .bind::(program.prefer_least_recently_played()) + .sql(" * (least_recently_played - 1.0)) + EXP(10.0 * ") + .bind::(program.prefer_recently_added()) + .sql(" * (recently_created - 1.0)) + ) / 2.0, + FIRST_VALUE( + MIN( + IFNULL( + ( + UNIXEPOCH('now', 'localtime') - UNIXEPOCH(instruments.last_played_at) + ) * 1.0 / ") + .bind::(program.avoid_repeated_instruments_seconds()) + .sql(", + 1.0 + ), + IFNULL( + ( + UNIXEPOCH('now', 'localtime') - UNIXEPOCH(persons.last_played_at) + ) * 1.0 / ").bind::(program.avoid_repeated_composers_seconds()).sql(", + 1.0 + ), + 1.0 + ) + ) OVER ( + PARTITION BY recordings.recording_id + ORDER BY MAX( + IFNULL(instruments.last_played_at, 0), + IFNULL(persons.last_played_at, 0) + ) + ) + ) + FROM normalized + ) DESC") + ); let row = query .select(tables::Recording::as_select()) @@ -668,6 +708,21 @@ impl Library { .set(works::last_played_at.eq(now)) .execute(connection)?; + diesel::update(instruments::table) + .filter(exists( + work_instruments::table + .inner_join( + works::table.inner_join(recordings::table.inner_join(tracks::table)), + ) + .filter( + tracks::track_id + .eq(track_id) + .and(work_instruments::instrument_id.eq(instruments::instrument_id)), + ), + )) + .set(instruments::last_played_at.eq(now)) + .execute(connection)?; + diesel::update(persons::table) .filter( exists( diff --git a/src/program.rs b/src/program.rs index fdb91b9..804e71c 100644 --- a/src/program.rs +++ b/src/program.rs @@ -11,6 +11,7 @@ mod imp { #[derive(Properties, Serialize, Deserialize, Default)] #[properties(wrapper_type = super::Program)] + #[serde(default)] pub struct Program { #[property(get, set)] pub title: RefCell>, @@ -45,6 +46,12 @@ mod imp { #[property(get, set)] pub prefer_least_recently_played: Cell, + #[property(get, set)] + pub avoid_repeated_composers_seconds: Cell, + + #[property(get, set)] + pub avoid_repeated_instruments_seconds: Cell, + #[property(get, set)] pub play_full_recordings: Cell, } @@ -74,12 +81,35 @@ impl Program { pub fn from_query(query: LibraryQuery) -> Self { glib::Object::builder() - .property("composer-id", query.composer.map(|p| p.person_id)) + .property( + "composer-id", + query.composer.as_ref().map(|p| p.person_id.clone()), + ) .property("performer-id", query.performer.map(|p| p.person_id)) .property("ensemble-id", query.ensemble.map(|e| e.ensemble_id)) - .property("instrument-id", query.instrument.map(|i| i.instrument_id)) + .property( + "instrument-id", + query.instrument.as_ref().map(|i| i.instrument_id.clone()), + ) + .property("work-id", query.work.as_ref().map(|w| w.work_id.clone())) .property("prefer-recently-added", 0.0) .property("prefer-least-recently-played", 0.5) + .property( + "avoid-repeated-composers-seconds", + if query.composer.is_none() && query.work.is_none() { + 3600 + } else { + 0 + }, + ) + .property( + "avoid-repeated-instruments-seconds", + if query.instrument.is_none() && query.work.is_none() { + 3600 + } else { + 0 + }, + ) .property("play-full-recordings", true) .build() } @@ -96,6 +126,14 @@ impl Program { "prefer-least-recently-played", data.prefer_least_recently_played.get(), ) + .property( + "avoid-repeated-composers-seconds", + data.avoid_repeated_composers_seconds.get(), + ) + .property( + "avoid-repeated-instruments-seconds", + data.avoid_repeated_instruments_seconds.get(), + ) .property("play-full-recordings", data.play_full_recordings.get()) .build();