From f84e67ed5e829d4bd5f0763356a05241393995f7 Mon Sep 17 00:00:00 2001 From: chme Date: Sun, 19 Aug 2018 09:45:35 +0200 Subject: [PATCH] [db/conf] Add automatic calculation of ratings based on played/skipped If activated in the configuration file, rating is updated after playing or skipping a song. The calculation is based on how it is done in the mpdstats plugin for beets (https://beets.readthedocs.io/en/latest/plugins/mpdstats.html). --- forked-daapd.conf.in | 11 +++++++++++ src/conffile.c | 1 + src/db.c | 39 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/forked-daapd.conf.in b/forked-daapd.conf.in index d8d3a4d1..56a950e1 100644 --- a/forked-daapd.conf.in +++ b/forked-daapd.conf.in @@ -178,6 +178,17 @@ library { # there is data to be read. To exclude specific pipes from watching, # consider using the above _ignore options. # pipe_autostart = true + + # Enable automatic rating updates + # If enabled, rating is automatically updated after a song has either been + # played or skipped (only skipping to the next song is taken into account). + # The calculation is taken from the beets plugin "mpdstats" (see + # https://beets.readthedocs.io/en/latest/plugins/mpdstats.html). + # It consist of calculating a stable rating based only on the play- and + # skipcount and a rolling rating based on the current rating and the action + # (played or skipped). Both results are combined with a mix-factor of 0.75: + # new rating = 0.75 * stable rating + 0.25 * rolling rating) +# rating_updates = false } # Local audio output diff --git a/src/conffile.c b/src/conffile.c index 85219afe..e38521d2 100644 --- a/src/conffile.c +++ b/src/conffile.c @@ -100,6 +100,7 @@ static cfg_opt_t sec_library[] = CFG_STR_LIST("no_decode", NULL, CFGF_NONE), CFG_STR_LIST("force_decode", NULL, CFGF_NONE), CFG_BOOL("pipe_autostart", cfg_true, CFGF_NONE), + CFG_BOOL("rating_updates", cfg_false, CFGF_NONE), CFG_END() }; diff --git a/src/db.c b/src/db.c index 54b9dfcd..04904853 100644 --- a/src/db.c +++ b/src/db.c @@ -411,6 +411,7 @@ db_data_kind_label(enum data_kind data_kind) struct rng_ctx shuffle_rng; static char *db_path; +static bool db_rating_updates; static __thread sqlite3 *hdl; @@ -2333,9 +2334,31 @@ void db_file_inc_playcount(int id) { #define Q_TMPL "UPDATE files SET play_count = play_count + 1, time_played = %" PRIi64 ", seek = 0 WHERE id = %d;" + /* + * Rating calculation is taken from from the beets plugin "mpdstats" (see https://beets.readthedocs.io/en/latest/plugins/mpdstats.html) + * and adapted to the forked-daapd rating rage (0 to 100). + * + * Rating consist of the stable rating and a rolling rating. + * The stable rating is calculated based on the number was played and skipped: + * stable rating = (play_count + 1.0) / (play_count + skip_count + 2.0) * 100 + * The rolling rating is calculated based on the current action (played or skipped): + * rolling rating for played = rating + ((100.0 - rating) / 2.0) + * rolling rating for skipped = rating - (rating / 2.0) + * + * The new rating is a mix of stable and rolling rating (factor 0.75): + * new rating = stable rating * 0.75 + rolling rating * 0.25 + */ +#define Q_TMPL_WITH_RATING \ + "UPDATE files "\ + " SET play_count = play_count + 1, time_played = %" PRIi64 ", seek = 0, "\ + " rating = CAST(((play_count + 1.0) / (play_count + skip_count + 2.0) * 100 * 0.75) + ((rating + ((100.0 - rating) / 2.0)) * 0.25) AS INT)" \ + " WHERE id = %d;" char *query; - query = sqlite3_mprintf(Q_TMPL, (int64_t)time(NULL), id); + if (db_rating_updates) + query = sqlite3_mprintf(Q_TMPL_WITH_RATING, (int64_t)time(NULL), id); + else + query = sqlite3_mprintf(Q_TMPL, (int64_t)time(NULL), id); if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); @@ -2345,15 +2368,25 @@ db_file_inc_playcount(int id) db_query_run(query, 1, 0); #undef Q_TMPL +#undef Q_TMPL_WITH_RATING } void db_file_inc_skipcount(int id) { #define Q_TMPL "UPDATE files SET skip_count = skip_count + 1, time_skipped = %" PRIi64 " WHERE id = %d;" + // see db_file_inc_playcount for a description of how the rating is calculated +#define Q_TMPL_WITH_RATING \ + "UPDATE files "\ + " SET skip_count = skip_count + 1, time_skipped = %" PRIi64 ", seek = 0, "\ + " rating = CAST(((play_count + 1.0) / (play_count + skip_count + 2.0) * 100 * 0.75) + ((rating - (rating / 2.0)) * 0.25) AS INT)" \ + " WHERE id = %d;" char *query; - query = sqlite3_mprintf(Q_TMPL, (int64_t)time(NULL), id); + if (db_rating_updates) + query = sqlite3_mprintf(Q_TMPL_WITH_RATING, (int64_t)time(NULL), id); + else + query = sqlite3_mprintf(Q_TMPL, (int64_t)time(NULL), id); if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); @@ -2363,6 +2396,7 @@ db_file_inc_skipcount(int id) db_query_run(query, 1, 0); #undef Q_TMPL +#undef Q_TMPL_WITH_RATING } void @@ -6841,6 +6875,7 @@ db_init(void) } db_path = cfg_getstr(cfg_getsec(cfg, "general"), "db_path"); + db_rating_updates = cfg_getbool(cfg_getsec(cfg, "library"), "rating_updates"); DPRINTF(E_LOG, L_DB, "Configured to use database file '%s'\n", db_path);