Skip to content

Commit 95e6722

Browse files
committed
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.
1 parent 7a59a12 commit 95e6722

File tree

6 files changed

+108
-0
lines changed

6 files changed

+108
-0
lines changed

DOCS/man/mpv.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,9 @@ g-l
328328
g-d
329329
Select an audio device.
330330

331+
g-h
332+
Select a file from the watch history. Requires ``--save-watch-history``.
333+
331334
g-w
332335
Select a file from watch later config files (see `RESUMING PLAYBACK`_) to
333336
resume playing. Requires ``--write-filename-in-watch-later-config``. This

DOCS/man/options.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1148,6 +1148,17 @@ Watch Later
11481148
Ignore path (i.e. use filename only) when using watch later feature.
11491149
(Default: disabled)
11501150

1151+
Watch History
1152+
-------------
1153+
1154+
``--save-watch-history``
1155+
Whether to save which files are played (default: no). These can be then
1156+
selected with the default ``g-h`` key binding.
1157+
1158+
``--watch-history-path=<path>``
1159+
The path in which to store the watch history. Default:
1160+
``~~state/watch_history.jsonl`` (see `PATHS`_).
1161+
11511162
Video
11521163
-----
11531164

etc/input.conf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@
187187
#g-e script-binding select/select-edition
188188
#g-l script-binding select/select-subtitle-line
189189
#g-d script-binding select/select-audio-device
190+
#g-h script-binding select/select-watch-history
190191
#g-w script-binding select/select-watch-later
191192
#g-b script-binding select/select-binding
192193
#g-r script-binding select/show-properties

options/options.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -811,6 +811,9 @@ static const m_option_t mp_opts[] = {
811811
{"watch-later-directory", OPT_ALIAS("watch-later-dir")},
812812
{"watch-later-options", OPT_STRINGLIST(watch_later_options)},
813813

814+
{"save-watch-history", OPT_BOOL(save_watch_history)},
815+
{"watch-history-path", OPT_STRING(watch_history_path), .flags = M_OPT_FILE},
816+
814817
{"ordered-chapters", OPT_BOOL(ordered_chapters)},
815818
{"ordered-chapters-files", OPT_STRING(ordered_chapters_files),
816819
.flags = M_OPT_FILE},
@@ -986,6 +989,7 @@ static const struct MPOpts mp_default_opts = {
986989
.sync_max_factor = 5,
987990
.load_config = true,
988991
.position_resume = true,
992+
.watch_history_path = "~~state/watch_history.jsonl",
989993
.autoload_files = true,
990994
.demuxer_thread = true,
991995
.demux_termination_timeout = 0.1,

options/options.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,8 @@ typedef struct MPOpts {
276276
bool ignore_path_in_watch_later_config;
277277
char *watch_later_dir;
278278
char **watch_later_options;
279+
bool save_watch_history;
280+
char *watch_history_path;
279281
bool pause;
280282
int keep_open;
281283
bool keep_open_pause;

player/lua/select.lua

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,93 @@ mp.add_key_binding(nil, "select-audio-device", function ()
353353
})
354354
end)
355355

356+
local history_file_path =
357+
mp.command_native({"expand-path", mp.get_property("watch-history-path")})
358+
359+
mp.register_event("file-loaded", function ()
360+
if not mp.get_property_native("save-watch-history") then
361+
return
362+
end
363+
364+
local history_file, error_message = io.open(history_file_path, "a")
365+
if not history_file then
366+
show_error("Failed to write the watch history: " .. error_message)
367+
return
368+
end
369+
370+
local path = mp.command_native({"normalize-path", mp.get_property("path")})
371+
local title = mp.get_property("playlist/" .. mp.get_property("playlist-pos") .. "/title")
372+
373+
history_file:write(utils.format_json({os.time(), path, title}) .. "\n")
374+
history_file:close()
375+
end)
376+
377+
local function add_history_entry(line, items, paths, osd_playlist_entry)
378+
local entry = utils.parse_json(line)
379+
380+
if not entry then
381+
mp.msg.warn(line .. " in " .. history_file_path .. " is not valid JSON.")
382+
return
383+
end
384+
385+
local time, path, title = unpack(entry)
386+
387+
local status, date = pcall(os.date, "(%Y-%m-%d %H:%M) ", time)
388+
389+
if not status or not path then
390+
mp.msg.warn(line .. " in " .. history_file_path .. " has invalid data.")
391+
return
392+
end
393+
394+
local item = path
395+
if title and osd_playlist_entry == "title" then
396+
item = title
397+
elseif title and osd_playlist_entry == "both" then
398+
item = title .. " (" .. path .. ")"
399+
end
400+
401+
table.insert(items, 1, date .. item)
402+
table.insert(paths, 1, path)
403+
end
404+
405+
mp.add_key_binding(nil, "select-watch-history", function ()
406+
local history_file, error_message = io.open(history_file_path)
407+
if not history_file then
408+
show_warning(mp.get_property_native("save-watch-history")
409+
and error_message
410+
or "Enable --save-watch-history")
411+
return
412+
end
413+
414+
local items = {}
415+
local paths = {}
416+
local osd_playlist_entry = mp.get_property("osd-playlist-entry")
417+
418+
for line in history_file:lines() do
419+
add_history_entry(line, items, paths, osd_playlist_entry)
420+
end
421+
422+
items[#items+1] = "Clear history"
423+
424+
input.select({
425+
prompt = "Select a file:",
426+
items = items,
427+
submit = function (i)
428+
if paths[i] then
429+
mp.commandv("loadfile", paths[i])
430+
return
431+
end
432+
433+
error_message = select(2, os.remove(history_file_path))
434+
if error_message then
435+
show_error(error_message)
436+
else
437+
mp.osd_message("History cleared.")
438+
end
439+
end,
440+
})
441+
end)
442+
356443
mp.add_key_binding(nil, "select-watch-later", function ()
357444
local watch_later_dir = mp.get_property("current-watch-later-dir")
358445

0 commit comments

Comments
 (0)