From d80ee92d9e6f9da621bd46419666dd1a3ef5c7ae Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Mon, 6 Jan 2025 00:18:32 +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. watch_history_path is awkwardly documented along with the key binding because I don't think it's worth adding a select.lua section to the manual just for this. I will add it and move it there if I add more script-opts in the future. --- DOCS/man/mpv.rst | 6 +++ etc/input.conf | 1 + player/lua/select.lua | 93 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+) diff --git a/DOCS/man/mpv.rst b/DOCS/man/mpv.rst index 7c67cf0d96d18..b198d3df84ca3 100644 --- a/DOCS/man/mpv.rst +++ b/DOCS/man/mpv.rst @@ -328,6 +328,12 @@ g-l g-d Select an audio device. +g-h + Select a file from the watch history. Requires + ``--script-opt=select-save_watch_history=yes``. The history path is + configured with ``--script-opt=select-watch_history_path=...``, and defaults + to ``~~state/watch_history.jsonl`` (see `PATHS`_). + 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/etc/input.conf b/etc/input.conf index 2faa88c233b28..8dbd42d53555f 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-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/player/lua/select.lua b/player/lua/select.lua index 7f20602aff098..0afec30a8d0b1 100644 --- a/player/lua/select.lua +++ b/player/lua/select.lua @@ -18,6 +18,13 @@ License along with mpv. If not, see . local utils = require "mp.utils" local input = require "mp.input" +local options = { + save_watch_history = false, + watch_history_path = "~~state/watch_history.jsonl", +} + +require "mp.options".read_options(options) + local function show_warning(message) mp.msg.warn(message) if mp.get_property_native("vo-configured") then @@ -353,6 +360,92 @@ mp.add_key_binding(nil, "select-audio-device", function () }) end) +local history_file_path = mp.command_native({"expand-path", options.watch_history_path}) + +local function save_to_watch_history() + 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 + +if options.save_watch_history then + mp.register_event("file-loaded", save_to_watch_history) +end + +local function add_history_entry(line, items, paths, 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) + + 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 + + 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 + + table.insert(items, 1, date .. item) + table.insert(paths, 1, path) +end + +mp.add_key_binding(nil, "select-history", function () + local history_file, error_message = io.open(history_file_path) + if not history_file then + show_warning(options.save_watch_history + and error_message + or "Enable --script-opt=select-save_watch_history=yes") + return + end + + local items = {} + local paths = {} + local osd_playlist_entry = mp.get_property("osd-playlist-entry") + + for line in history_file:lines() do + add_history_entry(line, items, paths, osd_playlist_entry) + end + + items[#items+1] = "Clear the 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")