[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:
whatdoineed2do 2024-01-24 22:30:02 +00:00 committed by GitHub
parent 9491a3b980
commit 2dc448fa30
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 648 additions and 169 deletions

View File

@ -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.

View File

@ -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
View File

@ -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)

View File

@ -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);

View File

@ -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);
}

View File

@ -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;

View File

@ -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, &param);
}
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 **

View File

@ -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);

View File

@ -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,

View File

@ -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__ */

View File

@ -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;
}

View File

@ -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);