Skip to content

Commit

Permalink
[scan/library] Media rating sync (#1681)
Browse files Browse the repository at this point in the history
Automatically read/write ratings to files in the library, if options read_rating/
write_rating are enabled. Also adds a max_rating so the user can set the rating
scale.

Doesn't sync automatic rating updates, because that could lead to whole-playlist
file rewriting.

Closes #1678 

---------

Co-authored-by: whatdoineed2do/Ray <[email protected]>
Co-authored-by: ejurgensen <[email protected]>
  • Loading branch information
3 people authored Jan 24, 2024
1 parent 9491a3b commit 2dc448f
Show file tree
Hide file tree
Showing 12 changed files with 648 additions and 169 deletions.
10 changes: 10 additions & 0 deletions owntone.conf.in
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,16 @@ library {
# new rating = 0.75 * stable rating + 0.25 * rolling rating)
# rating_updates = false

# By default, ratings are only saved in the server's database. Enable
# the below to make the server also read ratings from file metadata and
# write on update (requires write access). To avoid excessive writing to
# the library, automatic rating updates are not written, even with the
# write_rating option enabled.
# read_rating = false
# write_rating = false
# The scale used when reading/writing ratings to files
# max_rating = 100

# Allows creating, deleting and modifying m3u playlists in the library directories.
# Only supported by the player web interface and some mpd clients
# Defaults to being disabled.
Expand Down
3 changes: 3 additions & 0 deletions src/conffile.c
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ static cfg_opt_t sec_library[] =
CFG_INT("pipe_sample_rate", 44100, CFGF_NONE),
CFG_INT("pipe_bits_per_sample", 16, CFGF_NONE),
CFG_BOOL("rating_updates", cfg_false, CFGF_NONE),
CFG_BOOL("read_rating", cfg_false, CFGF_NONE),
CFG_BOOL("write_rating", cfg_false, CFGF_NONE),
CFG_INT("max_rating", 100, CFGF_NONE),
CFG_BOOL("allow_modifying_stored_playlists", cfg_false, CFGF_NONE),
CFG_STR("default_playlist_directory", NULL, CFGF_NONE),
CFG_BOOL("clear_queue_on_stop_disable", cfg_false, CFGF_NONE),
Expand Down
123 changes: 55 additions & 68 deletions src/db.c
Original file line number Diff line number Diff line change
Expand Up @@ -2921,7 +2921,9 @@ db_file_inc_playcount_byfilter(const char *filter)
return;
}

ret = db_query_run(query, 1, 0);
// Perhaps this should in principle emit LISTENER_DATABASE, but that would
// cause a lot of useless cache updates
ret = db_query_run(query, 1, db_rating_updates ? LISTENER_RATING : 0);
if (ret == 0)
db_admin_setint64(DB_ADMIN_DB_MODIFIED, (int64_t) time(NULL));
#undef Q_TMPL
Expand Down Expand Up @@ -2987,7 +2989,7 @@ db_file_inc_skipcount(int id)
return;
}

ret = db_query_run(query, 1, 0);
ret = db_query_run(query, 1, db_rating_updates ? LISTENER_RATING : 0);
if (ret == 0)
db_admin_setint64(DB_ADMIN_DB_MODIFIED, (int64_t) time(NULL));
#undef Q_TMPL
Expand Down Expand Up @@ -3155,6 +3157,30 @@ db_file_id_byquery(const char *query)
return ret;
}

bool
db_file_id_exists(int id)
{
#define Q_TMPL "SELECT f.id FROM files f WHERE f.id = %d;"
char *query;
int ret;

query = sqlite3_mprintf(Q_TMPL, id);
if (!query)
{
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");

return 0;
}

ret = db_file_id_byquery(query);

sqlite3_free(query);

return (id == ret);

#undef Q_TMPL
}

int
db_file_id_bypath(const char *path)
{
Expand Down Expand Up @@ -3228,13 +3254,37 @@ db_file_id_byurl(const char *url)
}

int
db_file_id_by_virtualpath_match(const char *path)
db_file_id_byvirtualpath(const char *virtual_path)
{
#define Q_TMPL "SELECT f.id FROM files f WHERE f.virtual_path = %Q;"
char *query;
int ret;

query = sqlite3_mprintf(Q_TMPL, virtual_path);
if (!query)
{
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");

return 0;
}

ret = db_file_id_byquery(query);

sqlite3_free(query);

return ret;

#undef Q_TMPL
}

int
db_file_id_byvirtualpath_match(const char *virtual_path)
{
#define Q_TMPL "SELECT f.id FROM files f WHERE f.virtual_path LIKE '%%%q%%';"
char *query;
int ret;

query = sqlite3_mprintf(Q_TMPL, path);
query = sqlite3_mprintf(Q_TMPL, virtual_path);
if (!query)
{
DPRINTF(E_LOG, L_DB, "Out of memory for query string\n");
Expand Down Expand Up @@ -3454,67 +3504,6 @@ db_file_seek_update(int id, uint32_t seek)
#undef Q_TMPL
}

static int
db_file_rating_update(char *query)
{
int ret;

ret = db_query_run(query, 1, 0);

if (ret == 0)
{
db_admin_setint64(DB_ADMIN_DB_MODIFIED, (int64_t) time(NULL));
listener_notify(LISTENER_RATING);
}

return ((ret < 0) ? -1 : sqlite3_changes(hdl));
}

int
db_file_rating_update_byid(uint32_t id, uint32_t rating)
{
#define Q_TMPL "UPDATE files SET rating = %d WHERE id = %d;"
char *query;

query = sqlite3_mprintf(Q_TMPL, rating, id);

return db_file_rating_update(query);
#undef Q_TMPL
}

int
db_file_rating_update_byvirtualpath(const char *virtual_path, uint32_t rating)
{
#define Q_TMPL "UPDATE files SET rating = %d WHERE virtual_path = %Q;"
char *query;

query = sqlite3_mprintf(Q_TMPL, rating, virtual_path);

return db_file_rating_update(query);
#undef Q_TMPL
}

int
db_file_usermark_update_byid(uint32_t id, uint32_t usermark)
{
#define Q_TMPL "UPDATE files SET usermark = %d WHERE id = %d;"
char *query;
int ret;

query = sqlite3_mprintf(Q_TMPL, usermark, id);

ret = db_query_run(query, 1, 0);

if (ret == 0)
{
db_admin_setint64(DB_ADMIN_DB_MODIFIED, (int64_t) time(NULL));
listener_notify(LISTENER_UPDATE);
}

return ((ret < 0) ? -1 : sqlite3_changes(hdl));
#undef Q_TMPL
}

void
db_file_delete_bypath(const char *path)
{
Expand Down Expand Up @@ -6350,8 +6339,6 @@ db_watch_get_byquery(struct watch_info *wi, char *query)
ret = db_blocking_step(stmt);
if (ret != SQLITE_ROW)
{
DPRINTF(E_WARN, L_DB, "Watch not found: '%s'\n", query);

sqlite3_finalize(stmt);
sqlite3_free(query);
return -1;
Expand Down Expand Up @@ -6577,7 +6564,7 @@ db_watch_enum_fetchwd(struct watch_enum *we, uint32_t *wd)
ret = db_blocking_step(we->stmt);
if (ret == SQLITE_DONE)
{
DPRINTF(E_INFO, L_DB, "End of watch enum results\n");
DPRINTF(E_DBG, L_DB, "End of watch enum results\n");
return 0;
}
else if (ret != SQLITE_ROW)
Expand Down
17 changes: 7 additions & 10 deletions src/db.h
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,9 @@ db_file_ping_bymatch(const char *path, int isdir);
char *
db_file_path_byid(int id);

bool
db_file_id_exists(int id);

int
db_file_id_bypath(const char *path);

Expand All @@ -695,7 +698,10 @@ int
db_file_id_byurl(const char *url);

int
db_file_id_by_virtualpath_match(const char *path);
db_file_id_byvirtualpath(const char *virtual_path);

int
db_file_id_byvirtualpath_match(const char *virtual_path);

struct media_file_info *
db_file_fetch_byid(int id);
Expand All @@ -712,15 +718,6 @@ db_file_update(struct media_file_info *mfi);
void
db_file_seek_update(int id, uint32_t seek);

int
db_file_rating_update_byid(uint32_t id, uint32_t rating);

int
db_file_usermark_update_byid(uint32_t id, uint32_t usermark);

int
db_file_rating_update_byvirtualpath(const char *virtual_path, uint32_t rating);

void
db_file_delete_bypath(const char *path);

Expand Down
27 changes: 2 additions & 25 deletions src/httpd_dacp.c
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
#include "conffile.h"
#include "artwork.h"
#include "dmap_common.h"
#include "library.h"
#include "db.h"
#include "player.h"
#include "listener.h"
Expand Down Expand Up @@ -1106,31 +1107,7 @@ dacp_propset_userrating(const char *value, struct httpd_request *hreq)
return;
}

ret = db_file_rating_update_byid(itemid, rating);

/* If no mfi, it may be because we sent an invalid nowplaying itemid. In this
* case request the real one from the player and default to that.
*/
if (ret == 0)
{
DPRINTF(E_WARN, L_DACP, "Invalid id %d for rating, defaulting to player id\n", itemid);

ret = player_playing_now(&itemid);
if (ret < 0)
{
DPRINTF(E_WARN, L_DACP, "Could not find an id for rating\n");

return;
}

ret = db_file_rating_update_byid(itemid, rating);
if (ret <= 0)
{
DPRINTF(E_WARN, L_DACP, "Could not find an id for rating\n");

return;
}
}
library_item_attrib_save(itemid, LIBRARY_ATTRIB_RATING, rating);
}


Expand Down
61 changes: 15 additions & 46 deletions src/httpd_jsonapi.c
Original file line number Diff line number Diff line change
Expand Up @@ -334,25 +334,6 @@ track_to_json(struct db_media_file_info *dbmfi)
return item;
}

// TODO Only partially implemented. A full implementation should use a mapping
// table, which should also be used above in track_to_json(). It should also
// return errors if there are incorrect/mispelled fields, but not sure how to
// walk a json object with json-c.
static int
json_to_track(struct media_file_info *mfi, json_object *json)
{
if (jparse_contains_key(json, "id", json_type_int))
mfi->id = jparse_int_from_obj(json, "id");
if (jparse_contains_key(json, "usermark", json_type_int))
mfi->usermark = jparse_int_from_obj(json, "usermark");
if (jparse_contains_key(json, "rating", json_type_int))
mfi->rating = jparse_int_from_obj(json, "rating");
if (jparse_contains_key(json, "play_count", json_type_int))
mfi->play_count = jparse_int_from_obj(json, "play_count");

return HTTP_OK;
}

static json_object *
playlist_to_json(struct db_playlist_info *dbpli)
{
Expand Down Expand Up @@ -3217,7 +3198,6 @@ jsonapi_reply_library_tracks_put(struct httpd_request *hreq)
json_object *request = NULL;
json_object *tracks;
json_object *track = NULL;
struct media_file_info *mfi = NULL;
int ret;
int err;
int32_t track_id;
Expand Down Expand Up @@ -3251,30 +3231,21 @@ jsonapi_reply_library_tracks_put(struct httpd_request *hreq)
goto error;
}

mfi = db_file_fetch_byid(track_id);
if (!mfi)
if (!db_file_id_exists(track_id))
{
DPRINTF(E_LOG, L_WEB, "Unknown track_id %d in json tracks request\n", track_id);
err = HTTP_NOTFOUND;
goto error;
}

ret = json_to_track(mfi, track);
if (ret != HTTP_OK)
{
err = ret;
goto error;
}
// These are async, so no error check
if (jparse_contains_key(track, "rating", json_type_int))
library_item_attrib_save(track_id, LIBRARY_ATTRIB_RATING, jparse_int_from_obj(track, "rating"));
if (jparse_contains_key(track, "usermark", json_type_int))
library_item_attrib_save(track_id, LIBRARY_ATTRIB_USERMARK, jparse_int_from_obj(track, "usermark"));
if (jparse_contains_key(track, "play_count", json_type_int))
library_item_attrib_save(track_id, LIBRARY_ATTRIB_PLAY_COUNT, jparse_int_from_obj(track, "play_count"));

ret = db_file_update(mfi);
if (ret < 0)
{
err = HTTP_INTERNAL;
goto error;
}

free_mfi(mfi, 0);
mfi = NULL;
i++;
}

Expand All @@ -3286,7 +3257,6 @@ jsonapi_reply_library_tracks_put(struct httpd_request *hreq)
jparse_free(request);
if (track)
db_transaction_rollback();
free_mfi(mfi, 0);
return err;
}

Expand All @@ -3299,8 +3269,11 @@ jsonapi_reply_library_tracks_put_byid(struct httpd_request *hreq)
int ret;

ret = safe_atoi32(hreq->path_parts[3], &track_id);
if (ret < 0)
return HTTP_INTERNAL;
if (ret < 0 || !db_file_id_exists(track_id))
{
DPRINTF(E_WARN, L_WEB, "Invalid or unknown track id in request '%s'\n", hreq->path);
return HTTP_NOTFOUND;
}

param = httpd_query_value_find(hreq->query, "play_count");
if (param)
Expand Down Expand Up @@ -3330,9 +3303,7 @@ jsonapi_reply_library_tracks_put_byid(struct httpd_request *hreq)
return HTTP_BADREQUEST;
}

ret = db_file_rating_update_byid(track_id, val);
if (ret < 0)
return HTTP_INTERNAL;
library_item_attrib_save(track_id, LIBRARY_ATTRIB_RATING, val);
}

// Retreive marked tracks via "/api/search?type=tracks&expression=usermark+=+1"
Expand All @@ -3346,9 +3317,7 @@ jsonapi_reply_library_tracks_put_byid(struct httpd_request *hreq)
return HTTP_BADREQUEST;
}

ret = db_file_usermark_update_byid(track_id, val);
if (ret < 0)
return HTTP_INTERNAL;
library_item_attrib_save(track_id, LIBRARY_ATTRIB_USERMARK, val);
}

return HTTP_OK;
Expand Down
Loading

0 comments on commit 2dc448f

Please sign in to comment.