mirror of
https://github.com/owntone/owntone-server.git
synced 2025-01-27 06:33:21 -05:00
[scan/library] Media rating sync (#1681)
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 <whatdoineed2do@nospam.gmail.com> Co-authored-by: ejurgensen <espenjurgensen@gmail.com>
This commit is contained in:
parent
9491a3b980
commit
2dc448fa30
@ -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.
|
||||
|
@ -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),
|
||||
|
123
src/db.c
123
src/db.c
@ -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
|
||||
@ -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
|
||||
@ -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)
|
||||
{
|
||||
@ -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");
|
||||
@ -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)
|
||||
{
|
||||
@ -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;
|
||||
@ -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)
|
||||
|
17
src/db.h
17
src/db.h
@ -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);
|
||||
|
||||
@ -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);
|
||||
@ -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);
|
||||
|
||||
|
@ -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"
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
@ -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)
|
||||
{
|
||||
@ -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;
|
||||
@ -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++;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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)
|
||||
@ -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"
|
||||
@ -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;
|
||||
|
124
src/library.c
124
src/library.c
@ -67,6 +67,14 @@ struct queue_item_add_param
|
||||
int *new_item_id;
|
||||
};
|
||||
|
||||
struct item_param
|
||||
{
|
||||
const char *path;
|
||||
uint32_t id;
|
||||
enum library_attrib attrib;
|
||||
uint32_t value;
|
||||
};
|
||||
|
||||
static struct commands_base *cmdbase;
|
||||
static pthread_t tid_library;
|
||||
|
||||
@ -119,6 +127,8 @@ static struct library_callback_register library_cb_register[LIBRARY_MAX_CALLBACK
|
||||
int
|
||||
library_media_save(struct media_file_info *mfi)
|
||||
{
|
||||
int ret;
|
||||
|
||||
if (!mfi->path || !mfi->fname || !mfi->scan_kind)
|
||||
{
|
||||
DPRINTF(E_LOG, L_LIB, "Ignoring media file with missing values (path='%s', fname='%s', scan_kind='%d', data_kind='%d')\n",
|
||||
@ -134,9 +144,11 @@ library_media_save(struct media_file_info *mfi)
|
||||
}
|
||||
|
||||
if (mfi->id == 0)
|
||||
return db_file_add(mfi);
|
||||
ret = db_file_add(mfi);
|
||||
else
|
||||
return db_file_update(mfi);
|
||||
ret = db_file_update(mfi);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
int
|
||||
@ -579,11 +591,11 @@ queue_save(void *arg, int *retval)
|
||||
static enum command_state
|
||||
item_add(void *arg, int *retval)
|
||||
{
|
||||
const char *path = arg;
|
||||
struct item_param *param = arg;
|
||||
int i;
|
||||
int ret = LIBRARY_ERROR;
|
||||
|
||||
DPRINTF(E_DBG, L_LIB, "Adding item to library '%s'\n", path);
|
||||
DPRINTF(E_DBG, L_LIB, "Adding item to library '%s'\n", param->path);
|
||||
|
||||
for (i = 0; sources[i]; i++)
|
||||
{
|
||||
@ -593,11 +605,11 @@ item_add(void *arg, int *retval)
|
||||
continue;
|
||||
}
|
||||
|
||||
ret = sources[i]->item_add(path);
|
||||
ret = sources[i]->item_add(param->path);
|
||||
|
||||
if (ret == LIBRARY_OK)
|
||||
{
|
||||
DPRINTF(E_DBG, L_LIB, "Add item to path '%s' with library source '%s'\n", path, db_scan_kind_label(sources[i]->scan_kind));
|
||||
DPRINTF(E_DBG, L_LIB, "Add item to path '%s' with library source '%s'\n", param->path, db_scan_kind_label(sources[i]->scan_kind));
|
||||
listener_notify(LISTENER_DATABASE);
|
||||
break;
|
||||
}
|
||||
@ -617,6 +629,87 @@ item_add(void *arg, int *retval)
|
||||
return COMMAND_END;
|
||||
}
|
||||
|
||||
static int
|
||||
write_metadata(struct media_file_info *mfi)
|
||||
{
|
||||
int ret;
|
||||
int i;
|
||||
|
||||
for (i = 0; sources[i]; i++)
|
||||
{
|
||||
if (sources[i]->disabled || !sources[i]->write_metadata)
|
||||
continue;
|
||||
|
||||
ret = sources[i]->write_metadata(mfi);
|
||||
if (ret == LIBRARY_OK)
|
||||
return ret;
|
||||
}
|
||||
|
||||
return LIBRARY_PATH_INVALID;
|
||||
}
|
||||
|
||||
static enum command_state
|
||||
item_attrib_save(void *arg, int *retval)
|
||||
{
|
||||
struct item_param *param = arg;
|
||||
struct media_file_info *mfi = NULL;
|
||||
int ret;
|
||||
|
||||
if (scanning)
|
||||
goto error;
|
||||
|
||||
mfi = db_file_fetch_byid(param->id);
|
||||
if (!mfi)
|
||||
goto error;
|
||||
|
||||
*retval = LIBRARY_OK;
|
||||
|
||||
switch (param->attrib)
|
||||
{
|
||||
case LIBRARY_ATTRIB_RATING:
|
||||
if (param->value < 0 || param->value > DB_FILES_RATING_MAX)
|
||||
goto error;
|
||||
|
||||
mfi->rating = param->value;
|
||||
|
||||
if (cfg_getbool(cfg_getsec(cfg, "library"), "write_rating"))
|
||||
*retval = write_metadata(mfi);
|
||||
|
||||
listener_notify(LISTENER_RATING);
|
||||
break;
|
||||
|
||||
case LIBRARY_ATTRIB_USERMARK:
|
||||
if (param->value < 0)
|
||||
goto error;
|
||||
|
||||
mfi->usermark = param->value;
|
||||
break;
|
||||
|
||||
case LIBRARY_ATTRIB_PLAY_COUNT:
|
||||
if (param->value < 0)
|
||||
goto error;
|
||||
|
||||
mfi->play_count = param->value;
|
||||
break;
|
||||
|
||||
default:
|
||||
goto error;
|
||||
}
|
||||
|
||||
ret = db_file_update(mfi);
|
||||
if (ret < 0)
|
||||
goto error;
|
||||
|
||||
free_mfi(mfi, 0);
|
||||
return COMMAND_END;
|
||||
|
||||
error:
|
||||
DPRINTF(E_LOG, L_LIB, "Error updating attribute %d to %d for file with id %d\n", param->attrib, param->value, param->id);
|
||||
*retval = LIBRARY_ERROR;
|
||||
free_mfi(mfi, 0);
|
||||
return COMMAND_END;
|
||||
}
|
||||
|
||||
// Callback to notify listeners of database changes
|
||||
static void
|
||||
update_trigger_cb(int fd, short what, void *arg)
|
||||
@ -861,6 +954,8 @@ library_queue_item_add(const char *path, int position, char reshuffle, uint32_t
|
||||
int
|
||||
library_item_add(const char *path)
|
||||
{
|
||||
struct item_param param;
|
||||
|
||||
if (scanning)
|
||||
{
|
||||
DPRINTF(E_INFO, L_LIB, "Scan already running, ignoring request to add item '%s'\n", path);
|
||||
@ -869,7 +964,22 @@ library_item_add(const char *path)
|
||||
|
||||
scanning = true;
|
||||
|
||||
return commands_exec_sync(cmdbase, item_add, NULL, (char *)path);
|
||||
param.path = path;
|
||||
|
||||
return commands_exec_sync(cmdbase, item_add, NULL, ¶m);
|
||||
}
|
||||
|
||||
void
|
||||
library_item_attrib_save(uint32_t id, enum library_attrib attrib, uint32_t value)
|
||||
{
|
||||
struct item_param *param;
|
||||
|
||||
param = malloc(sizeof(struct item_param));
|
||||
param->id = id;
|
||||
param->attrib = attrib;
|
||||
param->value = value;
|
||||
|
||||
commands_exec_async(cmdbase, item_attrib_save, param);
|
||||
}
|
||||
|
||||
struct library_source **
|
||||
|
@ -47,6 +47,13 @@ enum library_cb_action
|
||||
LIBRARY_CB_DELETE,
|
||||
};
|
||||
|
||||
enum library_attrib
|
||||
{
|
||||
LIBRARY_ATTRIB_RATING,
|
||||
LIBRARY_ATTRIB_USERMARK,
|
||||
LIBRARY_ATTRIB_PLAY_COUNT,
|
||||
};
|
||||
|
||||
/*
|
||||
* Definition of a library source
|
||||
*
|
||||
@ -88,6 +95,11 @@ struct library_source
|
||||
*/
|
||||
int (*fullrescan)(void);
|
||||
|
||||
/*
|
||||
* Write metadata to an item in the library
|
||||
*/
|
||||
int (*write_metadata)(struct media_file_info *mfi);
|
||||
|
||||
/*
|
||||
* Add an item to the library
|
||||
*/
|
||||
@ -219,6 +231,14 @@ library_queue_item_add(const char *path, int position, char reshuffle, uint32_t
|
||||
int
|
||||
library_item_add(const char *path);
|
||||
|
||||
/*
|
||||
* Async function to set selected attributes for an item in the library. In case
|
||||
* of ratrings also writes the rating to the source if the "write_rating" config
|
||||
* option is enabled.
|
||||
*/
|
||||
void
|
||||
library_item_attrib_save(uint32_t id, enum library_attrib attrib, uint32_t value);
|
||||
|
||||
struct library_source **
|
||||
library_sources(void);
|
||||
|
||||
|
@ -76,6 +76,13 @@
|
||||
#define F_SCAN_TYPE_AUDIOBOOK (1 << 2)
|
||||
#define F_SCAN_TYPE_COMPILATION (1 << 3)
|
||||
|
||||
#ifdef __linux__
|
||||
#define INOTIFY_FLAGS (IN_ATTRIB | IN_CREATE | IN_DELETE | IN_CLOSE_WRITE | IN_MOVE | IN_DELETE | IN_MOVE_SELF)
|
||||
#else
|
||||
#define INOTIFY_FLAGS (IN_CREATE | IN_DELETE | IN_MOVE)
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
enum file_type {
|
||||
FILE_UNKNOWN = 0,
|
||||
@ -906,11 +913,7 @@ process_directory(char *path, int parent_id, int flags)
|
||||
|
||||
// Add inotify watch (for FreeBSD we limit the flags so only dirs will be
|
||||
// opened, otherwise we will be opening way too many files)
|
||||
#ifdef __linux__
|
||||
wi.wd = inotify_add_watch(inofd, path, IN_ATTRIB | IN_CREATE | IN_DELETE | IN_CLOSE_WRITE | IN_MOVE | IN_DELETE | IN_MOVE_SELF);
|
||||
#else
|
||||
wi.wd = inotify_add_watch(inofd, path, IN_CREATE | IN_DELETE | IN_MOVE);
|
||||
#endif
|
||||
wi.wd = inotify_add_watch(inofd, path, INOTIFY_FLAGS);
|
||||
if (wi.wd < 0)
|
||||
{
|
||||
DPRINTF(E_WARN, L_SCAN, "Could not create inotify watch for %s: %s\n", path, strerror(errno));
|
||||
@ -1719,6 +1722,12 @@ filescanner_fullrescan()
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
filescanner_write_metadata(struct media_file_info *mfi)
|
||||
{
|
||||
return write_metadata_ffmpeg(mfi);
|
||||
}
|
||||
|
||||
static int
|
||||
queue_item_file_add(const char *sub_uri, int position, char reshuffle, uint32_t item_id, int *count, int *new_item_id)
|
||||
{
|
||||
@ -2217,6 +2226,7 @@ struct library_source filescanner =
|
||||
.rescan = filescanner_rescan,
|
||||
.metarescan = filescanner_metarescan,
|
||||
.fullrescan = filescanner_fullrescan,
|
||||
.write_metadata = filescanner_write_metadata,
|
||||
.playlist_item_add = playlist_item_add,
|
||||
.playlist_remove = playlist_remove,
|
||||
.queue_save = queue_save,
|
||||
|
@ -77,4 +77,10 @@ playlist_fill(struct playlist_info *pli, const char *path);
|
||||
int
|
||||
playlist_add(const char *path);
|
||||
|
||||
|
||||
/* --------------------------------- Other -------------------------------- */
|
||||
|
||||
int
|
||||
write_metadata_ffmpeg(const struct media_file_info *mfi);
|
||||
|
||||
#endif /* !__FILESCANNER_H__ */
|
||||
|
@ -22,10 +22,22 @@
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <stdint.h>
|
||||
// For fstat()
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
// For file copy
|
||||
#include <fcntl.h>
|
||||
#if defined(__APPLE__) || defined(__FreeBSD__)
|
||||
#include <copyfile.h>
|
||||
#else
|
||||
#include <sys/sendfile.h>
|
||||
#endif
|
||||
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libavformat/avformat.h>
|
||||
@ -174,6 +186,28 @@ parse_albumid(struct media_file_info *mfi, const char *id_string)
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int
|
||||
parse_rating(struct media_file_info *mfi, const char *rating_string)
|
||||
{
|
||||
cfg_t *library = cfg_getsec(cfg, "library");
|
||||
int max_rating;
|
||||
|
||||
if (!cfg_getbool(library, "read_rating"))
|
||||
return 0;
|
||||
|
||||
if (safe_atou32(rating_string, &mfi->rating) < 0)
|
||||
return 0;
|
||||
|
||||
// Make sure mfi->rating is in proper range
|
||||
max_rating = cfg_getint(library, "max_rating");
|
||||
if (max_rating < 5) // Invalid config
|
||||
max_rating = DB_FILES_RATING_MAX;
|
||||
|
||||
mfi->rating = MIN(DB_FILES_RATING_MAX * mfi->rating / max_rating, DB_FILES_RATING_MAX);
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
/* Lookup is case-insensitive, first occurrence takes precedence */
|
||||
static const struct metadata_map md_map_generic[] =
|
||||
{
|
||||
@ -198,6 +232,7 @@ static const struct metadata_map md_map_generic[] =
|
||||
{ "album-sort", 0, mfi_offsetof(album_sort), NULL },
|
||||
{ "compilation", 1, mfi_offsetof(compilation), NULL },
|
||||
{ "lyrics", 0, mfi_offsetof(lyrics), NULL, AV_DICT_IGNORE_SUFFIX },
|
||||
{ "rating", 1, mfi_offsetof(rating), parse_rating },
|
||||
|
||||
// ALAC sort tags
|
||||
{ "sort_name", 0, mfi_offsetof(title_sort), NULL },
|
||||
@ -768,3 +803,352 @@ scan_metadata_ffmpeg(struct media_file_info *mfi, const char *file)
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/* ----------------------- Writing metadata to files ------------------------ */
|
||||
|
||||
// Adapted from https://stackoverflow.com/questions/2180079/how-can-i-copy-a-file-on-unix-using-c
|
||||
static int
|
||||
fast_copy(int fd_dst, int fd_src)
|
||||
{
|
||||
// Here we use kernel-space copying for performance reasons
|
||||
#if defined(__APPLE__) || defined(__FreeBSD__)
|
||||
// fcopyfile works on FreeBSD and OS X 10.5+
|
||||
return fcopyfile(fd_src, fd_dst, 0, COPYFILE_ALL);
|
||||
#else
|
||||
// sendfile will work with non-socket output (i.e. regular file) on Linux 2.6.33+
|
||||
struct stat fileinfo = { 0 };
|
||||
fstat(fd_src, &fileinfo);
|
||||
return sendfile(fd_dst, fd_src, NULL, fileinfo.st_size);
|
||||
#endif
|
||||
}
|
||||
|
||||
static int
|
||||
file_copy(const char *dst, const char *src)
|
||||
{
|
||||
int fd_src = -1;
|
||||
int fd_dst = -1;
|
||||
int ret;
|
||||
|
||||
fd_src = open(src, O_RDONLY);
|
||||
if (fd_src < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_SCAN, "Error opening source '%s' for copy: %s\n", src, strerror(errno));
|
||||
goto error;
|
||||
}
|
||||
|
||||
fd_dst = open(dst, O_WRONLY);
|
||||
if (fd_src < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_SCAN, "Error opening destination '%s' for copy: %s\n", dst, strerror(errno));
|
||||
goto error;
|
||||
}
|
||||
|
||||
ret = fast_copy(fd_dst, fd_src);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_SCAN, "Error copying '%s' to file '%s': %s\n", src, dst, strerror(errno));
|
||||
goto error;
|
||||
}
|
||||
|
||||
close(fd_src);
|
||||
close(fd_dst);
|
||||
return 0;
|
||||
|
||||
error:
|
||||
if (fd_src != -1)
|
||||
close(fd_src);
|
||||
if (fd_dst != -1)
|
||||
close(fd_dst);
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int
|
||||
file_copy_to_tmp(char *dst, size_t dst_size, const char *src)
|
||||
{
|
||||
int fd_src = -1;
|
||||
int fd_dst = -1;
|
||||
const char *ext;
|
||||
int ret;
|
||||
|
||||
ext = strrchr(src, '.');
|
||||
if (!ext || strlen(ext) < 2)
|
||||
return -1;
|
||||
|
||||
// Obviously, copying only requires read access, but we will need write access
|
||||
// later, so let's fail early if it isn't going to work.
|
||||
fd_src = open(src, O_RDWR);
|
||||
if (fd_src < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_SCAN, "Error opening '%s' for metadata update: %s\n", src, strerror(errno));
|
||||
goto error;
|
||||
}
|
||||
|
||||
ret = snprintf(dst, dst_size, "/tmp/owntone.tmpXXXXXX%s", ext);
|
||||
if (ret < 0 || ret >= dst_size)
|
||||
{
|
||||
DPRINTF(E_LOG, L_SCAN, "Error creating tmp file name\n");
|
||||
goto error;
|
||||
}
|
||||
|
||||
fd_dst = mkstemps(dst, strlen(ext));
|
||||
if (fd_dst < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_SCAN, "Error creating tmp file '%s' for metadata update: %s\n", dst, strerror(errno));
|
||||
goto error;
|
||||
}
|
||||
|
||||
ret = fast_copy(fd_dst, fd_src);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_SCAN, "Error copying '%s' to tmp file '%s': %s\n", src, dst, strerror(errno));
|
||||
goto error;
|
||||
}
|
||||
|
||||
close(fd_src);
|
||||
close(fd_dst);
|
||||
return 0;
|
||||
|
||||
error:
|
||||
if (fd_src != -1)
|
||||
close(fd_src);
|
||||
if (fd_dst != -1)
|
||||
close(fd_dst);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// based on FFmpeg's doc/examples and in particular mux.c
|
||||
static int
|
||||
file_write_rating(const char *dst, const char *src, const char *rating)
|
||||
{
|
||||
AVFormatContext *in_fmt_ctx = NULL;
|
||||
AVFormatContext *out_fmt_ctx = NULL;
|
||||
AVPacket pkt;
|
||||
const AVDictionaryEntry *tag;
|
||||
AVStream *out_stream;
|
||||
AVStream *in_stream;
|
||||
#if (LIBAVCODEC_VERSION_MAJOR > 59) || ((LIBAVCODEC_VERSION_MAJOR == 59) && (LIBAVCODEC_VERSION_MINOR >= 0) && (LIBAVCODEC_VERSION_MICRO >= 100))
|
||||
const AVOutputFormat *out_fmt;
|
||||
#else
|
||||
AVOutputFormat *out_fmt;
|
||||
#endif
|
||||
bool restore_src = false;
|
||||
int ret;
|
||||
int i;
|
||||
int stream_idx;
|
||||
int *stream_mapping = NULL;
|
||||
|
||||
ret = avformat_open_input(&in_fmt_ctx, src, NULL, NULL);
|
||||
if (ret != 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_SCAN, "Error opening tmpfile '%s' for rating metadata update: %s\n", src, av_err2str(ret));
|
||||
goto error;
|
||||
}
|
||||
|
||||
av_dict_set(&in_fmt_ctx->metadata, "rating", rating, 0);
|
||||
|
||||
ret = avformat_find_stream_info(in_fmt_ctx, NULL);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_SCAN, "Error reading input stream information from '%s': %s\n", in_fmt_ctx->url, av_err2str(ret));
|
||||
goto error;
|
||||
}
|
||||
|
||||
out_fmt = av_guess_format(in_fmt_ctx->iformat->name, in_fmt_ctx->url, in_fmt_ctx->iformat->mime_type);
|
||||
if (out_fmt == NULL)
|
||||
{
|
||||
DPRINTF(E_LOG, L_SCAN, "Could not determine output format from '%s'\n", in_fmt_ctx->url);
|
||||
goto error;
|
||||
}
|
||||
|
||||
ret = avformat_alloc_output_context2(&out_fmt_ctx, out_fmt, NULL, NULL);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_SCAN, "Could not create output context '%s' - %s\n", in_fmt_ctx->url, av_err2str(ret));
|
||||
goto error;
|
||||
}
|
||||
|
||||
CHECK_NULL(L_SCAN, stream_mapping = av_calloc(in_fmt_ctx->nb_streams, sizeof(*stream_mapping)));
|
||||
|
||||
tag = NULL;
|
||||
while ((tag = av_dict_iterate(in_fmt_ctx->metadata, tag)))
|
||||
{
|
||||
av_dict_set(&(out_fmt_ctx->metadata), tag->key, tag->value, 0);
|
||||
}
|
||||
|
||||
stream_idx = 0;
|
||||
for (i = 0; i < in_fmt_ctx->nb_streams; i++)
|
||||
{
|
||||
in_stream = in_fmt_ctx->streams[i];
|
||||
stream_mapping[i] = stream_idx++;
|
||||
|
||||
out_stream = avformat_new_stream(out_fmt_ctx, NULL);
|
||||
if (!out_stream)
|
||||
{
|
||||
DPRINTF(E_LOG, L_SCAN, "Error allocating output stream for '%s'\n", in_fmt_ctx->url);
|
||||
goto error;
|
||||
}
|
||||
|
||||
ret = avcodec_parameters_copy(out_stream->codecpar, in_stream->codecpar);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_SCAN, "Error copying codec parameters from '%s': %s\n", in_fmt_ctx->url, av_err2str(ret));
|
||||
goto error;
|
||||
}
|
||||
|
||||
if (in_stream->metadata)
|
||||
{
|
||||
tag = NULL;
|
||||
while ((tag = av_dict_iterate(in_stream->metadata, tag)))
|
||||
{
|
||||
av_dict_set(&(out_stream->metadata), tag->key, tag->value, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ret = avio_open(&out_fmt_ctx->pb, dst, AVIO_FLAG_WRITE);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_SCAN, "Could not open output rating file '%s': %s\n", dst, av_err2str(ret));
|
||||
goto error;
|
||||
}
|
||||
|
||||
ret = avformat_write_header(out_fmt_ctx, NULL);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_SCAN, "Error occurred when writing output header to '%s': %s\n", dst, av_err2str(ret));
|
||||
goto error;
|
||||
}
|
||||
|
||||
while (1)
|
||||
{
|
||||
ret = av_read_frame(in_fmt_ctx, &pkt);
|
||||
if (ret < 0)
|
||||
{
|
||||
if (ret == AVERROR_EOF)
|
||||
break;
|
||||
|
||||
DPRINTF(E_LOG, L_SCAN, "Error reading '%s': %s\n", in_fmt_ctx->url, av_err2str(ret));
|
||||
restore_src = true;
|
||||
goto error;
|
||||
}
|
||||
|
||||
in_stream = in_fmt_ctx->streams[pkt.stream_index];
|
||||
if (pkt.stream_index >= in_fmt_ctx->nb_streams || stream_mapping[pkt.stream_index] < 0)
|
||||
{
|
||||
av_packet_unref(&pkt);
|
||||
continue;
|
||||
}
|
||||
|
||||
pkt.stream_index = stream_mapping[pkt.stream_index];
|
||||
out_stream = out_fmt_ctx->streams[pkt.stream_index];
|
||||
|
||||
/* copy packet */
|
||||
pkt.pts = av_rescale_q_rnd(pkt.pts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX);
|
||||
pkt.dts = av_rescale_q_rnd(pkt.dts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX);
|
||||
pkt.duration = av_rescale_q(pkt.duration, in_stream->time_base, out_stream->time_base);
|
||||
pkt.pos = -1;
|
||||
|
||||
ret = av_interleaved_write_frame(out_fmt_ctx, &pkt);
|
||||
av_packet_unref(&pkt);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_SCAN, "Error muxing pkt for rating '%s': %s\n", in_fmt_ctx->url, av_err2str(ret));
|
||||
restore_src = true;
|
||||
goto error;
|
||||
}
|
||||
}
|
||||
|
||||
av_write_trailer(out_fmt_ctx);
|
||||
|
||||
if (out_fmt_ctx && !(out_fmt_ctx->oformat->flags & AVFMT_NOFILE))
|
||||
avio_closep(&out_fmt_ctx->pb);
|
||||
avformat_free_context(out_fmt_ctx);
|
||||
av_freep(&stream_mapping);
|
||||
return 0;
|
||||
|
||||
error:
|
||||
if (out_fmt_ctx && !(out_fmt_ctx->oformat->flags & AVFMT_NOFILE))
|
||||
avio_closep(&out_fmt_ctx->pb);
|
||||
avformat_free_context(out_fmt_ctx);
|
||||
av_freep(&stream_mapping);
|
||||
if (restore_src)
|
||||
file_copy(dst, src);
|
||||
return -1;
|
||||
}
|
||||
|
||||
static bool
|
||||
file_rating_matches(const char *path, const char *rating)
|
||||
{
|
||||
AVFormatContext *in_fmt_ctx = NULL;
|
||||
AVDictionaryEntry *entry;
|
||||
bool has_rating;
|
||||
int ret;
|
||||
|
||||
ret = avformat_open_input(&in_fmt_ctx, path, NULL, NULL);
|
||||
if (ret != 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_SCAN, "Failed to open library file for rating metadata update '%s' - %s\n", path, av_err2str(ret));
|
||||
return true; // Return true so called aborts
|
||||
}
|
||||
|
||||
entry = av_dict_get(in_fmt_ctx->metadata, "rating", NULL, 0);
|
||||
has_rating = (entry && entry->value && strcmp(entry->value, rating) == 0);
|
||||
|
||||
avformat_close_input(&in_fmt_ctx);
|
||||
|
||||
return has_rating;
|
||||
}
|
||||
|
||||
// ffmpeg's metadata update is limited - some formats do not support rating
|
||||
// update even though the write completes; keep this in sync with supported
|
||||
// formats
|
||||
static bool
|
||||
format_is_supported(const char *format)
|
||||
{
|
||||
if (strcmp(format, "mp3") == 0)
|
||||
return true;
|
||||
if (strcmp(format, "flac") == 0)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
int
|
||||
write_metadata_ffmpeg(struct media_file_info *mfi)
|
||||
{
|
||||
char rating_str[32];
|
||||
char tmpfile[PATH_MAX];
|
||||
int max_rating;
|
||||
int file_rating;
|
||||
int ret;
|
||||
|
||||
if (mfi->data_kind != DATA_KIND_FILE || !format_is_supported(mfi->type))
|
||||
{
|
||||
DPRINTF(E_WARN, L_SCAN, "Update of rating metadata requires file in MP3 or FLAC format: '%s'\n", mfi->path);
|
||||
return -1;
|
||||
}
|
||||
|
||||
max_rating = cfg_getint(cfg_getsec(cfg, "library"), "max_rating");
|
||||
if (max_rating < 5) // Invalid config
|
||||
max_rating = DB_FILES_RATING_MAX;
|
||||
file_rating = mfi->rating * max_rating / DB_FILES_RATING_MAX;
|
||||
snprintf(rating_str, sizeof(rating_str), "%d", file_rating);
|
||||
|
||||
// Save a write if metadata of the underlying file matches requested rating
|
||||
if (file_rating_matches(mfi->path, rating_str))
|
||||
return 0;
|
||||
|
||||
ret = file_copy_to_tmp(tmpfile, sizeof(tmpfile), mfi->path);
|
||||
if (ret < 0)
|
||||
return -1;
|
||||
|
||||
ret = file_write_rating(mfi->path, tmpfile, rating_str);
|
||||
unlink(tmpfile);
|
||||
if (ret < 0)
|
||||
return -1;
|
||||
|
||||
DPRINTF(E_DBG, L_SCAN, "Wrote rating metadata to '%s'\n", mfi->path);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
20
src/mpd.c
20
src/mpd.c
@ -3269,7 +3269,8 @@ static int
|
||||
mpd_sticker_set(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, const char *virtual_path)
|
||||
{
|
||||
uint32_t rating;
|
||||
int ret = 0;
|
||||
int id;
|
||||
int ret;
|
||||
|
||||
if (strcmp(argv[4], "rating") != 0)
|
||||
{
|
||||
@ -3291,20 +3292,22 @@ mpd_sticker_set(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, co
|
||||
return ACK_ERROR_ARG;
|
||||
}
|
||||
|
||||
ret = db_file_rating_update_byvirtualpath(virtual_path, rating);
|
||||
if (ret <= 0)
|
||||
id = db_file_id_byvirtualpath(virtual_path);
|
||||
if (id <= 0)
|
||||
{
|
||||
*errmsg = safe_asprintf("Invalid path '%s'", virtual_path);
|
||||
return ACK_ERROR_ARG;
|
||||
}
|
||||
|
||||
library_item_attrib_save(id, LIBRARY_ATTRIB_RATING, rating);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
mpd_sticker_delete(struct evbuffer *evbuf, int argc, char **argv, char **errmsg, const char *virtual_path)
|
||||
{
|
||||
int ret = 0;
|
||||
int id;
|
||||
|
||||
if (strcmp(argv[4], "rating") != 0)
|
||||
{
|
||||
@ -3312,12 +3315,15 @@ mpd_sticker_delete(struct evbuffer *evbuf, int argc, char **argv, char **errmsg,
|
||||
return ACK_ERROR_NO_EXIST;
|
||||
}
|
||||
|
||||
ret = db_file_rating_update_byvirtualpath(virtual_path, 0);
|
||||
if (ret <= 0)
|
||||
id = db_file_id_byvirtualpath(virtual_path);
|
||||
if (id <= 0)
|
||||
{
|
||||
*errmsg = safe_asprintf("Invalid path '%s'", virtual_path);
|
||||
return ACK_ERROR_ARG;
|
||||
}
|
||||
|
||||
library_item_attrib_save(id, LIBRARY_ATTRIB_RATING, 0);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@ -4714,7 +4720,7 @@ artwork_cb(struct evhttp_request *req, void *arg)
|
||||
|
||||
DPRINTF(E_DBG, L_MPD, "Artwork request for path: %s\n", decoded_path);
|
||||
|
||||
itemid = db_file_id_by_virtualpath_match(decoded_path);
|
||||
itemid = db_file_id_byvirtualpath_match(decoded_path);
|
||||
if (!itemid)
|
||||
{
|
||||
DPRINTF(E_WARN, L_MPD, "No item found for path '%s' from request uri '%s'\n", decoded_path, uri);
|
||||
|
Loading…
x
Reference in New Issue
Block a user