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 object per line to get the best of both worlds. This
is called NDJSON or JSONL.

Co-authored-by: Kacper Michajłow <kasper93@gmail.com>
This commit is contained in:
Guido Cella
2025-01-07 13:52:07 +01:00
committed by Kacper Michajłow
parent a3cc06f754
commit b75ed73f4f
5 changed files with 113 additions and 0 deletions

View File

@@ -0,0 +1 @@
add `--save-watch-history` and `--watch-history-path` options

View File

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

View File

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

View File

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

View File

@@ -19,6 +19,7 @@
#include <stdbool.h>
#include <inttypes.h>
#include <assert.h>
#include <time.h>
#include <libavutil/avutil.h>
@@ -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;