mirror of
https://github.com/mpv-player/mpv.git
synced 2025-12-26 21:00:21 +00:00
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:
committed by
Kacper Michajłow
parent
a3cc06f754
commit
b75ed73f4f
1
DOCS/interface-changes/watch-history.txt
Normal file
1
DOCS/interface-changes/watch-history.txt
Normal file
@@ -0,0 +1 @@
|
||||
add `--save-watch-history` and `--watch-history-path` options
|
||||
@@ -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
|
||||
-----
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user