program: Penalize recently played composers and instruments

This commit is contained in:
Elias Projahn 2025-03-15 07:08:19 +01:00
parent e5e41619f2
commit 653d5cd629
3 changed files with 136 additions and 43 deletions

View file

@ -19,17 +19,17 @@
</key>
<key name="program1" type="s">
<!-- Translators: Configuration for the default programs in JSON. Please only translate the values of "title" and "description". -->
<default l10n="messages">'{"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}'</default>
<default l10n="messages">'{"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>
<summary>Default settings for program 1</summary>
</key>
<key name="program2" type="s">
<!-- Translators: Configuration for the default programs in JSON. Please only translate the values of "title" and "description". -->
<default l10n="messages">'{"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}'</default>
<default l10n="messages">'{"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>
<summary>Default settings for program 2</summary>
</key>
<key name="program3" type="s">
<!-- Translators: Configuration for the default programs in JSON. Please only translate the values of "title" and "description". -->
<default l10n="messages">'{"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}'</default>
<default l10n="messages">'{"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>
<summary>Default settings for program 3</summary>
</key>
</schema>

View file

@ -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::<sql_types::Untyped>("( \
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::<sql_types::Double, _>(program.prefer_least_recently_played())
.sql(" * (least_recently_played - 1.0)) + EXP(10.0 * ")
.bind::<sql_types::Double, _>(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::<sql_types::Untyped>("( \
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::<sql_types::Double, _>(program.prefer_least_recently_played())
.sql(" * (least_recently_played - 1.0)) + EXP(10.0 * ")
.bind::<sql_types::Double, _>(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::<sql_types::Integer, _>(program.avoid_repeated_instruments_seconds())
.sql(",
1.0
),
IFNULL(
(
UNIXEPOCH('now', 'localtime') - UNIXEPOCH(persons.last_played_at)
) * 1.0 / ").bind::<sql_types::Integer, _>(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(

View file

@ -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<Option<String>>,
@ -45,6 +46,12 @@ mod imp {
#[property(get, set)]
pub prefer_least_recently_played: Cell<f64>,
#[property(get, set)]
pub avoid_repeated_composers_seconds: Cell<i32>,
#[property(get, set)]
pub avoid_repeated_instruments_seconds: Cell<i32>,
#[property(get, set)]
pub play_full_recordings: Cell<bool>,
}
@ -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();