From b75ed73f4f1a8e835e17798bb58a2cc644b50a10 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 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 object per line to get the best of both worlds. This is called NDJSON or JSONL. Co-authored-by: Kacper Michajłow --- DOCS/interface-changes/watch-history.txt | 1 + DOCS/man/options.rst | 19 ++++++ options/options.c | 4 ++ options/options.h | 2 + player/loadfile.c | 87 ++++++++++++++++++++++++ 5 files changed, 113 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 0000000000..28e5c674f8 --- /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 3735e6767e..e2de939e4d 100644 --- a/DOCS/man/options.rst +++ b/DOCS/man/options.rst @@ -1148,6 +1148,25 @@ 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`_). + + This file contains one JSON object per line. Its ``time`` field is the UNIX + timestamp when the file was opened, its ``path`` field is the normalized + path, and its ``title`` field is the title when it was available. + Video ----- diff --git a/options/options.c b/options/options.c index 915670bf25..f9159affd8 100644 --- a/options/options.c +++ b/options/options.c @@ -813,6 +813,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}, @@ -988,6 +991,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 31e72cfcec..899c9062e3 100644 --- a/options/options.h +++ b/options/options.h @@ -277,6 +277,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 a199e1f5b9..ea6fd5c558 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,89 @@ 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 open history file: %s\n", + mp_strerror(errno)); + goto done; + } + + char *title = (char *)mp_find_non_filename_media_title(mpctx); + + mpv_node_list *list = talloc_zero(ctx, mpv_node_list); + mpv_node node = { + .format = MPV_FORMAT_NODE_MAP, + .u.list = list, + }; + list->num = title ? 3 : 2; + list->keys = talloc_array(ctx, char*, list->num); + list->values = talloc_array(ctx, mpv_node, list->num); + list->keys[0] = "time"; + list->values[0] = (struct mpv_node) { + .format = MPV_FORMAT_INT64, + .u.int64 = time(NULL), + }; + list->keys[1] = "path"; + list->values[1] = (struct mpv_node) { + .format = MPV_FORMAT_STRING, + .u.string = mp_normalize_path(ctx, mpctx->filename), + }; + if (title) { + list->keys[2] = "title"; + list->values[2] = (struct mpv_node) { + .format = MPV_FORMAT_STRING, + .u.string = title, + }; + } + + bstr dst = {0}; + json_append(&dst, &node, -1); + talloc_steal(ctx, dst.start); + if (!dst.len) { + MP_ERR(mpctx, "Failed to serialize history entry\n"); + goto done; + } + bstr_xappend0(ctx, &dst, "\n"); + + int seek = fseek(history_file, 0, SEEK_END); + off_t history_size = ftell(history_file); + if (seek != 0 || history_size == -1) { + MP_ERR(mpctx, "Failed to get history file size: %s\n", + mp_strerror(errno)); + fclose(history_file); + goto done; + } + + bool failed = fwrite(dst.start, dst.len, 1, history_file) != 1 || + fflush(history_file) != 0; + + if (failed) { + MP_ERR(mpctx, "Failed to write to history file: %s\n", + mp_strerror(errno)); + + int fd = fileno(history_file); + if (fd == -1 || ftruncate(fd, history_size) == -1) + MP_ERR(mpctx, "Failed to roll-back history file: %s\n", + mp_strerror(errno)); + } + + if (fclose(history_file) != 0) + MP_ERR(mpctx, "Failed to close history file: %s\n", + 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 +1857,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;