Skip to content

Commit

Permalink
select.lua: select from the watch history with g-h
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
guidocella committed Jan 6, 2025
1 parent 7a59a12 commit 02ef821
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 0 deletions.
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
11 changes: 11 additions & 0 deletions DOCS/man/options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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=<path>``
The path in which to store the watch history. Default:
``~~state/watch_history.jsonl`` (see `PATHS`_).

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
107 changes: 107 additions & 0 deletions player/lua/select.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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

table.insert(items, 1, date .. item)
table.insert(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")

Expand Down

0 comments on commit 02ef821

Please sign in to comment.