Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

select.lua: select from the watch history with g-h #15655

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions DOCS/interface-changes/watch-history.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
add `--save-watch-history` and `--watch-history-path` options
3 changes: 3 additions & 0 deletions DOCS/man/mpv.rst
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,9 @@ g-l
g-d
Select an audio device.

g-h
Select a file from the watch history. Requires ``--save-watch-history``.

g-w
Select a file from watch later config files (see `RESUMING PLAYBACK`_) to
resume playing. Requires ``--write-filename-in-watch-later-config``. This
Expand Down
20 changes: 20 additions & 0 deletions DOCS/man/options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1148,6 +1148,26 @@ Watch Later
Ignore path (i.e. use filename only) when using watch later feature.
(Default: disabled)

Watch History
-------------

``--save-watch-history``
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the file format should be documented.

Whether to save which files are played. These can be then selected with the
default ``g-h`` key binding.

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

Expand Down
1 change: 1 addition & 0 deletions etc/input.conf
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@
#g-e script-binding select/select-edition
#g-l script-binding select/select-subtitle-line
#g-d script-binding select/select-audio-device
#g-h script-binding select/select-watch-history
#g-w script-binding select/select-watch-later
#g-b script-binding select/select-binding
#g-r script-binding select/show-properties
Expand Down
4 changes: 4 additions & 0 deletions options/options.c
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions options/options.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
21 changes: 0 additions & 21 deletions player/command.c
Original file line number Diff line number Diff line change
Expand Up @@ -577,27 +577,6 @@ static int mp_property_file_size(void *ctx, struct m_property *prop,
return m_property_int64_ro(action, arg, size);
}

static const char *find_non_filename_media_title(MPContext *mpctx)
{
const char *name = mpctx->opts->media_title;
if (name && name[0])
return name;
if (mpctx->demuxer) {
name = mp_tags_get_str(mpctx->demuxer->metadata, "service_name");
if (name && name[0])
return name;
name = mp_tags_get_str(mpctx->demuxer->metadata, "title");
if (name && name[0])
return name;
name = mp_tags_get_str(mpctx->demuxer->metadata, "icy-title");
if (name && name[0])
return name;
}
if (mpctx->playing && mpctx->playing->title)
return mpctx->playing->title;
return NULL;
}

static int mp_property_media_title(void *ctx, struct m_property *prop,
int action, void *arg)
{
Expand Down
1 change: 1 addition & 0 deletions player/core.h
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,7 @@ int stream_dump(struct MPContext *mpctx, const char *source_filename);
double get_track_seek_offset(struct MPContext *mpctx, struct track *track);
bool str_in_list(bstr str, char **list);
char *mp_format_track_metadata(void *ctx, struct track *t, bool add_lang);
const char *find_non_filename_media_title(MPContext *mpctx);

// osd.c
void set_osd_bar(struct MPContext *mpctx, int type,
Expand Down
62 changes: 62 additions & 0 deletions player/loadfile.c
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include <stdbool.h>
#include <inttypes.h>
#include <assert.h>
#include <time.h>

#include <libavutil/avutil.h>

Expand All @@ -44,6 +45,7 @@
#include "common/encode.h"
#include "common/stats.h"
#include "input/input.h"
#include "misc/json.h"
kasper93 marked this conversation as resolved.
Show resolved Hide resolved
#include "misc/language.h"

#include "audio/out/ao.h"
Expand Down Expand Up @@ -1521,6 +1523,64 @@ 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,
Copy link
Contributor

@kasper93 kasper93 Jan 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be null when no-config is used. Or empty string, either way it has to be checked, before fopen.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is watch_history.jsonl with --no-config --save-watch-history

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So what happens it saves history in cwd?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep.

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_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,
};
}

char *dst = talloc_strdup(ctx, "");
json_write(&dst, &node);
dst = talloc_strdup_append(dst, "\n");

bool failed = fwrite(dst, strlen(dst), 1, history_file) != 1;
if (fclose(history_file) || failed) {
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)
Expand Down Expand Up @@ -1772,6 +1832,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;
Expand Down
78 changes: 78 additions & 0 deletions player/lua/select.lua
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ License along with mpv. If not, see <http://www.gnu.org/licenses/>.
local utils = require "mp.utils"
local input = require "mp.input"

local options = {
history_date_format = "%Y-%m-%d %H:%M:%S",
hide_history_duplicates = true,
}

require "mp.options".read_options(options, nil, function () end)

local function show_warning(message)
mp.msg.warn(message)
if mp.get_property_native("vo-configured") then
Expand Down Expand Up @@ -353,6 +360,77 @@ mp.add_key_binding(nil, "select-audio-device", function ()
})
end)

local function format_history_entry(entry)
local status
status, entry.time = pcall(os.date, options.history_date_format, entry.time)

if not status then
mp.msg.warn(entry.time)
end

return "(" .. entry.time .. ") " ..
(entry.title or select(2, utils.split_path(entry.path))) ..
" (" .. entry.path .. ")"
end

mp.add_key_binding(nil, "select-watch-history", function ()
local history_file_path = mp.command_native(
{"expand-path", mp.get_property("watch-history-path")})
local history_file, error_message = io.open(history_file_path)
if not history_file then
show_warning(mp.get_property_native("save-watch-history")
and error_message
or "Enable --save-watch-history")
return
end

local all_entries = {}
local line_num = 1
for line in history_file:lines() do
local entry = utils.parse_json(line)
if entry and entry.path then
all_entries[#all_entries + 1] = entry
else
mp.msg.warn(history_file_path .. ": Parse error at line " .. line_num)
end
line_num = line_num + 1
end
history_file:close()

local entries = {}
local items = {}
local seen = {}

for i = #all_entries, 1, -1 do
local entry = all_entries[i]
if not seen[entry.path] or not options.hide_history_duplicates then
seen[entry.path] = true
entries[#entries + 1] = entry
items[#items + 1] = format_history_entry(entry)
end
end
kasper93 marked this conversation as resolved.
Show resolved Hide resolved

items[#items+1] = "Clear history"
kasper93 marked this conversation as resolved.
Show resolved Hide resolved

input.select({
prompt = "Select a file:",
items = items,
kasper93 marked this conversation as resolved.
Show resolved Hide resolved
submit = function (i)
if entries[i] then
mp.commandv("loadfile", entries[i].path)
return
end

error_message = select(2, os.remove(history_file_path))
kasper93 marked this conversation as resolved.
Show resolved Hide resolved
if error_message then
show_error(error_message)
else
mp.osd_message("History cleared.")
end
end,
})
end)

mp.add_key_binding(nil, "select-watch-later", function ()
local watch_later_dir = mp.get_property("current-watch-later-dir")

Expand Down
21 changes: 21 additions & 0 deletions player/misc.c
Original file line number Diff line number Diff line change
Expand Up @@ -413,3 +413,24 @@ char *mp_format_track_metadata(void *ctx, struct track *t, bool add_lang)

return bstrto0(ctx, dst);
}

const char *find_non_filename_media_title(MPContext *mpctx)
{
const char *name = mpctx->opts->media_title;
if (name && name[0])
return name;
if (mpctx->demuxer) {
name = mp_tags_get_str(mpctx->demuxer->metadata, "service_name");
if (name && name[0])
return name;
name = mp_tags_get_str(mpctx->demuxer->metadata, "title");
if (name && name[0])
return name;
name = mp_tags_get_str(mpctx->demuxer->metadata, "icy-title");
if (name && name[0])
return name;
}
if (mpctx->playing && mpctx->playing->title)
return mpctx->playing->title;
return NULL;
}
Loading