From 827ebf0cb02dab6fad141cfd95a13bb4560bc4ad Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Tue, 7 Jan 2025 13:52:07 +0100 Subject: [PATCH] loadfile: optionally save the watch history The history could be formatted as CSV, but this requires escaping the separator in the fields and doesn't work with paths and titles with newlines, or as JSON, but it is inefficient to reread and rewrite the whole history on each new file, and doing so overwrites the history with an empty file when writing without disk space left. So this uses a hybrid of one JSON array per line to get the best of both worlds. This is called NDJSON or JSONL. --- DOCS/interface-changes/watch-history.txt | 1 + DOCS/man/options.rst | 15 +++++++++ options/options.c | 4 +++ options/options.h | 2 ++ player/loadfile.c | 42 ++++++++++++++++++++++++ 5 files changed, 64 insertions(+) create mode 100644 DOCS/interface-changes/watch-history.txt diff --git a/DOCS/interface-changes/watch-history.txt b/DOCS/interface-changes/watch-history.txt new file mode 100644 index 0000000000000..28e5c674f816b --- /dev/null +++ b/DOCS/interface-changes/watch-history.txt @@ -0,0 +1 @@ +add `--save-watch-history` and `--watch-history-path` options diff --git a/DOCS/man/options.rst b/DOCS/man/options.rst index 4e932f1eff69c..ece0e1621641b 100644 --- a/DOCS/man/options.rst +++ b/DOCS/man/options.rst @@ -1148,6 +1148,21 @@ Watch Later Ignore path (i.e. use filename only) when using watch later feature. (Default: disabled) +Watch History +------------- + +``--save-watch-history`` + Whether to save which files are played. + + .. warning:: + + This option may expose privacy-sensitive information and is thus + disabled by default. + +``--watch-history-path=`` + The path in which to store the watch history. Default: + ``~~state/watch_history.jsonl`` (see `PATHS`_). + Video ----- diff --git a/options/options.c b/options/options.c index 3bb958c49a042..2d7cfce922a10 100644 --- a/options/options.c +++ b/options/options.c @@ -811,6 +811,9 @@ static const m_option_t mp_opts[] = { {"watch-later-directory", OPT_ALIAS("watch-later-dir")}, {"watch-later-options", OPT_STRINGLIST(watch_later_options)}, + {"save-watch-history", OPT_BOOL(save_watch_history)}, + {"watch-history-path", OPT_STRING(watch_history_path), .flags = M_OPT_FILE}, + {"ordered-chapters", OPT_BOOL(ordered_chapters)}, {"ordered-chapters-files", OPT_STRING(ordered_chapters_files), .flags = M_OPT_FILE}, @@ -986,6 +989,7 @@ static const struct MPOpts mp_default_opts = { .sync_max_factor = 5, .load_config = true, .position_resume = true, + .watch_history_path = "~~state/watch_history.jsonl", .autoload_files = true, .demuxer_thread = true, .demux_termination_timeout = 0.1, diff --git a/options/options.h b/options/options.h index 3f184c49cf5a9..ef8c4306845db 100644 --- a/options/options.h +++ b/options/options.h @@ -276,6 +276,8 @@ typedef struct MPOpts { bool ignore_path_in_watch_later_config; char *watch_later_dir; char **watch_later_options; + bool save_watch_history; + char *watch_history_path; bool pause; int keep_open; bool keep_open_pause; diff --git a/player/loadfile.c b/player/loadfile.c index 4f204c20e75fa..c24c411376dae 100644 --- a/player/loadfile.c +++ b/player/loadfile.c @@ -19,6 +19,7 @@ #include #include #include +#include #include @@ -44,6 +45,7 @@ #include "common/encode.h" #include "common/stats.h" #include "input/input.h" +#include "misc/json.h" #include "misc/language.h" #include "audio/out/ao.h" @@ -1521,6 +1523,44 @@ static void load_external_opts(struct MPContext *mpctx) mp_waiter_wait(&wait); } +static void append_to_watch_history(struct MPContext *mpctx) +{ + if (!mpctx->opts->save_watch_history) + return; + + void *ctx = talloc_new(NULL); + char *history_path = mp_get_user_path(ctx, mpctx->global, + mpctx->opts->watch_history_path); + FILE *history_file = fopen(history_path, "ab"); + + if (!history_file) { + MP_ERR(mpctx, "Failed to write to %s: %s\n", history_path, + mp_strerror(errno)); + goto done; + } + + char *title = (char *)find_non_filename_media_title(mpctx); + mpv_node items[] = { + {.format = MPV_FORMAT_INT64, .u = {.int64 = time(NULL)}}, + {.format = MPV_FORMAT_STRING, .u = {.string = mp_normalize_path(ctx, mpctx->filename)}}, + {.format = MPV_FORMAT_STRING, .u = {.string = title}}, + }; + mpv_node_list list = {.values = items, .num = title ? 3 : 2}; + mpv_node node = {.format = MPV_FORMAT_NODE_ARRAY, .u = {.list = &list}}; + char *dst = talloc_strdup(ctx, ""); + json_write(&dst, &node); + dst = talloc_strdup_append(dst, "\n"); + + bool fail = fwrite(dst, strlen(dst), 1, history_file) != 1; + if (fclose(history_file) || fail) { + MP_ERR(mpctx, "Failed to write to %s: %s\n", history_path, + mp_strerror(errno)); + } + +done: + talloc_free(ctx); +} + // Start playing the current playlist entry. // Handle initialization and deinitialization. static void play_current_file(struct MPContext *mpctx) @@ -1772,6 +1812,8 @@ static void play_current_file(struct MPContext *mpctx) if (watch_later) mp_delete_watch_later_conf(mpctx, mpctx->filename); + append_to_watch_history(mpctx); + if (mpctx->max_frames == 0) { if (!mpctx->stop_play) mpctx->stop_play = PT_NEXT_ENTRY;