From 6afdfe202bea3f680c13bc25b7914d1c3e5318d2 Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Mon, 6 Jan 2025 14:27:12 +0100 Subject: [PATCH] select.lua: select from the watch history with g-h Implement saving watched paths and selecting them. --osd-playlist-entry determines whether titles and/or filenames are shown. But unlike in show-text ${playlist} and select-playlist, "file" and "both" print full paths because history is much more likely to have files from completely different directories, so showing the directory conveys where files are located. This is particularly helpful for filenames like 1.jpg. The last entry in the selector deletes the history file, as requested by Samillion. 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. I went with an hybrid of one JSON array per line to get the best of both worlds. And I discovered afterwards that this was an existing thing called NDJSON or JSONL. --- DOCS/man/mpv.rst | 3 ++ DOCS/man/options.rst | 11 +++++ etc/input.conf | 1 + options/options.c | 4 ++ options/options.h | 2 + player/lua/select.lua | 107 ++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 128 insertions(+) diff --git a/DOCS/man/mpv.rst b/DOCS/man/mpv.rst index 7c67cf0d96d18..ecb54f42abec9 100644 --- a/DOCS/man/mpv.rst +++ b/DOCS/man/mpv.rst @@ -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 diff --git a/DOCS/man/options.rst b/DOCS/man/options.rst index 4e932f1eff69c..cb4a28e74630a 100644 --- a/DOCS/man/options.rst +++ b/DOCS/man/options.rst @@ -1148,6 +1148,17 @@ 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 (default: no). These can be then + selected with the default ``g-h`` key binding of select.lua. + +``--watch-history-path=`` + The path in which to store the watch history. Default: + ``~~state/watch_history.jsonl`` (see `PATHS`_). + Video ----- diff --git a/etc/input.conf b/etc/input.conf index 2faa88c233b28..adbb824a0d838 100644 --- a/etc/input.conf +++ b/etc/input.conf @@ -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 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/lua/select.lua b/player/lua/select.lua index 7f20602aff098..8bec32bfb5cbb 100644 --- a/player/lua/select.lua +++ b/player/lua/select.lua @@ -353,6 +353,113 @@ mp.add_key_binding(nil, "select-audio-device", function () }) end) +local history_file_path = + mp.command_native({"expand-path", mp.get_property("watch-history-path")}) + +mp.register_event("file-loaded", function () + if not mp.get_property_native("save-watch-history") then + return + end + + local history_file, error_message = io.open(history_file_path, "a") + if not history_file then + show_error("Failed to write the watch history: " .. error_message) + return + end + + local path = mp.command_native({"normalize-path", mp.get_property("path")}) + local title = mp.get_property("playlist/" .. mp.get_property("playlist-pos") .. "/title") + + history_file:write(utils.format_json({os.time(), path, title}) .. "\n") + history_file:close() +end) + +local function add_history_entry(line, items, paths, seen, osd_playlist_entry) + local entry = utils.parse_json(line) + + if not entry then + mp.msg.warn(line .. " in " .. history_file_path .. " is not valid JSON.") + return + end + + local time, path, title = unpack(entry) + + if seen[path] then + return + end + seen[path] = true + + local status, date = pcall(os.date, "(%Y-%m-%d %H:%M) ", time) + + if not status or not path then + mp.msg.warn(line .. " in " .. history_file_path .. " has invalid data.") + return + end + + for i, seen_path in ipairs(paths) do + if seen_path == path then + table.remove(items, i) + table.remove(paths, i) + break + end + end + + local item = path + if title and osd_playlist_entry == "title" then + item = title + elseif title and osd_playlist_entry == "both" then + item = title .. " (" .. path .. ")" + end + + items[#items + 1] = date .. item + paths[#paths + 1] = path +end + +mp.add_key_binding(nil, "select-watch-history", function () + 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 lines = {} + local items = {} + local paths = {} + local seen = {} + local osd_playlist_entry = mp.get_property("osd-playlist-entry") + + for line in history_file:lines() do + table.insert(lines, line) + end + history_file:close() + + for i = #lines, 1, -1 do + add_history_entry(lines[i], items, paths, seen, osd_playlist_entry) + end + + items[#items+1] = "Clear history" + + input.select({ + prompt = "Select a file:", + items = items, + submit = function (i) + if paths[i] then + mp.commandv("loadfile", paths[i]) + return + end + + error_message = select(2, os.remove(history_file_path)) + 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")