Skip to content

Instantly share code, notes, and snippets.

@rtldg
Last active January 6, 2025 01:00
Show Gist options
  • Select an option

  • Save rtldg/04057fea36f8b4c9333fcb8107f3944a to your computer and use it in GitHub Desktop.

Select an option

Save rtldg/04057fea36f8b4c9333fcb8107f3944a to your computer and use it in GitHub Desktop.
better screenshot filenames for mpv and also helps with long screenshot filenames
-- SPDX-License-Identifier: WTFPL
-- Copyright 2025 rtldg <rtldg@protonmail.com>
--[[
basically this:
screenshot-template="%F %P %n %{sub-text}"
Only includes sub-text in filename if the picture includes subs.
Things this basically does:
- Trim screenshot filenames to remove trailing '.', '_', and (c <= ' ').
- #define ILLEGAL_FILENAME_CHARS "?\"/\\<>*|:"
- ~~prepend \\?\ to filenames...~~ (NOT DONE)
- trim basenames to be less than 251. (trim invalid utf-8 too)
]]
mp.msg = require("mp.msg")
mp.utils = require("mp.utils")
---------------------------
----- STATE VARIABLES -----
local screenshot_counter = 1
local current_filename = ""
---------------------------
local function clean_string(s)
local ILLEGAL_FILENAME_CHARS = "?\"/\\<>*|:"
local clean = ""
for c in s:gmatch(".") do
if string.find(ILLEGAL_FILENAME_CHARS, c, 1, true) or string.byte(c) < 32 then
clean = clean .. "_"
else
clean = clean .. c
end
end
clean = clean:reverse()
while clean ~= "" do
local c = clean:sub(1,1)
if c == "." or c == "_" or string.byte(c) <= 32 then
clean = clean:sub(2)
else
break
end
end
return clean:reverse()
end
local function split_filename_and_extension(filename)
local reversed = filename:reverse()
local dotidx = reversed:find("%.")
if dotidx and dotidx ~= 1 then
return reversed:sub(dotidx+1):reverse(), reversed:sub(1, dotidx-1):reverse()
else
return filename, ""
end
end
local function truncate_utf8_string(str, maxlen)
str = str:sub(1, maxlen)
-- not thoroughly test but should work 😇
if string.byte(str:sub(-3)) >= 240 then return str:sub(1, str:len()-3) end
if string.byte(str:sub(-2)) >= 224 then return str:sub(1, str:len()-2) end
if string.byte(str:sub(-1)) >= 192 then return str:sub(1, str:len()-1) end
return str
end
local function time_to_HH_MM_SS_mmm(time)
local ms = math.fmod(time * 1000, 1000)
local integer = math.floor(time)
local h = math.floor(integer / (60 * 60))
local m = math.floor(integer / 60) - (h * 60)
local s = math.fmod(integer, 60)
--print(h, m, s, ms)
return string.format("%02d:%02d:%02d.%03d", h, m, s, ms)
end
local function construct_screenshot_path(with_subs)
local screenshot_extension = "." .. mp.get_property("screenshot-format", "jpg")
local output_directory = mp.get_property("screenshot-directory", "")
if output_directory ~= "" then
local last_byte = output_directory:sub(-1)
if last_byte ~= "/" and last_byte ~= "\\" then
output_directory = output_directory .. "/"
end
end
local playback_time = mp.get_property_number("playback-time/full", 0)
local timestamp = time_to_HH_MM_SS_mmm(playback_time)
local sub_text = ""
if with_subs then
sub_text = clean_string(mp.get_property("sub-text", ""))
end
for i=1, 12345 do
local basename = string.format("%s %s %04d %s", current_filename, timestamp, screenshot_counter, sub_text)
basename = clean_string(basename)
basename = truncate_utf8_string(basename, 255 - screenshot_extension:len())
local path = string.format("%s%s%s", output_directory, basename, screenshot_extension)
local info, err = mp.utils.file_info(path)
screenshot_counter = screenshot_counter + 1
-- file doesn't exist so we can screenshot safely
-- (if the file does exist then we'd loop and try again with a higher screenshot_counter)
if not info then
return path
end
end
return ""
end
local function do_screenshot(with_subs)
local path = construct_screenshot_path(with_subs)
if path == "" then
mp.osd_message("failed to build a screenshot path...")
mp.msg.error("failed to build a screenshot path...")
return
end
local cmd = {"screenshot-to-file", path, with_subs and "subtitles" or "video"}
local ret = mp.command_native_async(cmd, function(success, result, err)
if success then
mp.osd_message(string.format("Screenshot: '%s'", path))
else
if err ~= nil then
mp.osd_message(string.format("Failed to save '%s'\nReason: %s", path, err))
mp.msg.error(err)
else
mp.osd_message(string.format("Failed to save '%s'\nReason: unknown", path))
mp.msg.error(err)
end
end
end)
end
mp.register_event("file-loaded", function(event)
current_filename, _ = split_filename_and_extension(mp.get_property("filename", ""))
current_filename = clean_string(current_filename)
end)
-- screenshot with subtitles
mp.add_forced_key_binding("s", function()
do_screenshot(true)
end)
-- screenshot without subtitles
mp.add_forced_key_binding("S", function()
do_screenshot(false)
end)
@rtldg
Copy link
Author

rtldg commented Jan 6, 2025

Basically these four commits ported to lua mpv-player/mpv@master...rtldg:mpv:stuff-again

A PR for the 'long filename handling improvements' commit was never merged into the mpv repo so I gave up and starting building mpv locally with my patches.

I don't enjoy writing lua code (and I was hopeful a PR would one day be accepted) so I put off porting to lua for a long time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment