summaryrefslogtreecommitdiff
path: root/mac/.config/mpv/scripts/gallery-thumbgen.lua
diff options
context:
space:
mode:
Diffstat (limited to 'mac/.config/mpv/scripts/gallery-thumbgen.lua')
-rw-r--r--mac/.config/mpv/scripts/gallery-thumbgen.lua342
1 files changed, 342 insertions, 0 deletions
diff --git a/mac/.config/mpv/scripts/gallery-thumbgen.lua b/mac/.config/mpv/scripts/gallery-thumbgen.lua
new file mode 100644
index 0000000..dc0db1a
--- /dev/null
+++ b/mac/.config/mpv/scripts/gallery-thumbgen.lua
@@ -0,0 +1,342 @@
+--[[
+mpv-gallery-view | https://github.com/occivink/mpv-gallery-view
+
+This mpv script implements a worker for generating gallery thumbnails.
+It is meant to be used by other scripts.
+Multiple copies of this script can be loaded by mpv.
+
+File placement: inside scripts directory
+Settings: script-opts/gallery_worker.conf
+]]
+
+local utils = require("mp.utils")
+local msg = require("mp.msg")
+
+local jobs_queue = {} -- queue of thumbnail jobs
+local failed = {} -- list of failed output paths, to avoid redoing them
+local script_id = mp.get_script_name() .. utils.getpid()
+
+local opts = {
+ ytdl_exclude = "",
+};
+(require("mp.options")).read_options(opts, "gallery_worker")
+
+local ytdl = {
+ path = "youtube-dl",
+ searched = false,
+ blacklisted = {}, -- Add patterns of URLs you want blacklisted from youtube-dl,
+ -- see gallery_worker.conf or ytdl_hook-exclude in the mpv manpage for more info
+}
+
+function append_table(lhs, rhs)
+ for i = 1, #rhs do
+ lhs[#lhs + 1] = rhs[i]
+ end
+ return lhs
+end
+
+local function file_exists(path)
+ local info = utils.file_info(path)
+ return info ~= nil and info.is_file
+end
+
+local video_extensions = { "mkv", "webm", "mp4", "avi", "wmv" }
+
+function is_video(input_path)
+ local extension = string.match(input_path, "%.([^.]+)$")
+ if extension then
+ extension = string.lower(extension)
+ for _, ext in ipairs(video_extensions) do
+ if extension == ext then
+ return true
+ end
+ end
+ end
+ return false
+end
+
+function is_blacklisted(url)
+ if opts.ytdl_exclude == "" then
+ return false
+ end
+ if #ytdl.blacklisted == 0 then
+ local joined = opts.ytdl_exclude
+ while joined:match("%|?[^|]+") do
+ local _, e, substring = joined:find("%|?([^|]+)")
+ table.insert(ytdl.blacklisted, substring)
+ joined = joined:sub(e + 1)
+ end
+ end
+ if #ytdl.blacklisted > 0 then
+ url = url:match("https?://(.+)")
+ for _, exclude in ipairs(ytdl.blacklisted) do
+ if url:match(exclude) then
+ msg.verbose("URL matches excluded substring. Skipping.")
+ return true
+ end
+ end
+ end
+ return false
+end
+
+function ytdl_thumbnail_url(input_path)
+ local function exec(args)
+ local ret = utils.subprocess({ args = args, cancellable = false })
+ return ret.status, ret.stdout, ret
+ end
+ local function first_non_nil(x, ...)
+ if x ~= nil then
+ return x
+ end
+ return first_non_nil(...)
+ end
+
+ -- if input_path is youtube, generate our own URL
+ youtube_id1 = string.match(input_path, "https?://youtu%.be/([%a%d%-_]+).*")
+ youtube_id2 = string.match(input_path, "https?://w?w?w?%.?youtube%.com/v/([%a%d%-_]+).*")
+ youtube_id3 = string.match(input_path, "https?://w?w?w?%.?youtube%.com/watch%?v=([%a%d%-_]+).*")
+ youtube_id4 = string.match(input_path, "https?://w?w?w?%.?youtube%.com/embed/([%a%d%-_]+).*")
+ youtube_id = youtube_id1 or youtube_id2 or youtube_id3 or youtube_id4
+
+ if youtube_id then
+ -- the hqdefault.jpg thumbnail should always exist, since it's used on the search result page
+ return "https://i.ytimg.com/vi/" .. youtube_id .. "/hqdefault.jpg"
+ end
+
+ --otherwise proceed with the slower `youtube-dl -J` method
+ if not ytdl.searched then --search for youtude-dl in mpv's config directory
+ local exesuf = (package.config:sub(1, 1) == "\\") and ".exe" or ""
+ local ytdl_mcd = mp.find_config_file("youtube-dl")
+ if not (ytdl_mcd == nil) then
+ msg.error("found youtube-dl at: " .. ytdl_mcd)
+ ytdl.path = ytdl_mcd
+ end
+ ytdl.searched = true
+ end
+ local command = { ytdl.path, "--no-warnings", "--no-playlist", "-J", input_path }
+ local es, json, result = exec(command)
+
+ if (es < 0) or (json == nil) or (json == "") then
+ msg.error("fetching thumbnail url with youtube-dl failed for" .. input_path)
+ return input_path
+ end
+ local json, err = utils.parse_json(json)
+ if json == nil then
+ msg.error("failed to parse json for youtube-dl thumbnail: " .. err)
+ return input_path
+ end
+
+ if (json.thumbnail == nil) or (json.thumbnail == "") then
+ msg.error("no thumbnail url from youtube-dl.")
+ return input_path
+ end
+ return json.thumbnail
+end
+
+function thumbnail_command(input_path, width, height, take_thumbnail_at, output_path, accurate, with_mpv)
+ local vf = string.format(
+ "%s,%s",
+ string.format("scale=iw*min(1\\,min(%d/iw\\,%d/ih)):-2", width, height),
+ string.format("pad=%d:%d:(%d-iw)/2:(%d-ih)/2:color=0x00000000", width, height, width, height)
+ )
+ local out = {}
+ local add = function(table)
+ out = append_table(out, table)
+ end
+
+ if input_path:find("^https?://") and not is_blacklisted(input_path) then
+ -- returns the original input_path on failure
+ input_path = ytdl_thumbnail_url(input_path)
+ end
+
+ if input_path:find("^archive://") or input_path:find("^edl://") then
+ with_mpv = true
+ end
+
+ if not with_mpv then
+ out = { "ffmpeg" }
+ if is_video(input_path) then
+ if string.sub(take_thumbnail_at, -1) == "%" then
+ --if only fucking ffmpeg supported percent-style seeking
+ local res = utils.subprocess({
+ args = {
+ "ffprobe",
+ "-v",
+ "error",
+ "-show_entries",
+ "format=duration",
+ "-of",
+ "default=noprint_wrappers=1:nokey=1",
+ input_path,
+ },
+ cancellable = false,
+ })
+ if res.status == 0 then
+ local duration = tonumber(string.match(res.stdout, "^%s*(.-)%s*$"))
+ if duration then
+ local percent = tonumber(string.sub(take_thumbnail_at, 1, -2))
+ local start = tostring(duration * percent / 100)
+ add({ "-ss", start })
+ end
+ end
+ else
+ add({ "-ss", take_thumbnail_at })
+ end
+ end
+ if not accurate then
+ add({ "-noaccurate_seek" })
+ end
+ add({
+ "-i",
+ input_path,
+ "-vf",
+ vf,
+ "-map",
+ "v:0",
+ "-f",
+ "rawvideo",
+ "-pix_fmt",
+ "bgra",
+ "-c:v",
+ "rawvideo",
+ "-frames:v",
+ "1",
+ "-y",
+ "-loglevel",
+ "quiet",
+ output_path,
+ })
+ else
+ out = { "mpv", input_path }
+ if take_thumbnail_at ~= "0" and is_video(input_path) then
+ if not accurate then
+ add({ "--hr-seek=no" })
+ end
+ add({ "--start=" .. take_thumbnail_at })
+ end
+ add({
+ "--no-config",
+ "--msg-level=all=no",
+ "--vf=lavfi=[" .. vf .. ",format=bgra]",
+ "--audio=no",
+ "--sub=no",
+ "--frames=1",
+ "--image-display-duration=0",
+ "--of=rawvideo",
+ "--ovc=rawvideo",
+ "--o=" .. output_path,
+ })
+ end
+ return out
+end
+
+function generate_thumbnail(thumbnail_job)
+ if file_exists(thumbnail_job.output_path) then
+ return true
+ end
+
+ local dir, _ = utils.split_path(thumbnail_job.output_path)
+ local tmp_output_path = utils.join_path(dir, script_id)
+
+ local command = thumbnail_command(
+ thumbnail_job.input_path,
+ thumbnail_job.width,
+ thumbnail_job.height,
+ thumbnail_job.take_thumbnail_at,
+ tmp_output_path,
+ thumbnail_job.accurate,
+ thumbnail_job.with_mpv
+ )
+
+ local res = utils.subprocess({ args = command, cancellable = false })
+ --"atomically" generate the output to avoid loading half-generated thumbnails (results in crashes)
+ if res.status == 0 then
+ local info = utils.file_info(tmp_output_path)
+ if not info or not info.is_file or info.size == 0 then
+ return false
+ end
+ if os.rename(tmp_output_path, thumbnail_job.output_path) then
+ return true
+ end
+ end
+ return false
+end
+
+function handle_events(wait)
+ e = mp.wait_event(wait)
+ while e.event ~= "none" do
+ if e.event == "shutdown" then
+ return false
+ elseif e.event == "client-message" then
+ if e.args[1] == "push-thumbnail-front" or e.args[1] == "push-thumbnail-back" then
+ local thumbnail_job = {
+ requester = e.args[2],
+ input_path = e.args[3],
+ width = tonumber(e.args[4]),
+ height = tonumber(e.args[5]),
+ take_thumbnail_at = e.args[6],
+ output_path = e.args[7],
+ accurate = (e.args[8] == "true"),
+ with_mpv = (e.args[9] == "true"),
+ }
+ if e.args[1] == "push-thumbnail-front" then
+ jobs_queue[#jobs_queue + 1] = thumbnail_job
+ else
+ table.insert(jobs_queue, 1, thumbnail_job)
+ end
+ end
+ end
+ e = mp.wait_event(0)
+ end
+ return true
+end
+
+local registration_timeout = 2 -- seconds
+local registration_period = 0.2
+
+-- shitty custom event loop because I can't figure out a better way
+-- works pretty well though
+function mp_event_loop()
+ local start_time = mp.get_time()
+ local sleep_time = registration_period
+ local last_broadcast_time = -registration_period
+ local broadcast_func
+ broadcast_func = function()
+ local now = mp.get_time()
+ if now >= start_time + registration_timeout then
+ mp.commandv("script-message", "thumbnails-generator-broadcast", mp.get_script_name())
+ sleep_time = 1e20
+ broadcast_func = function() end
+ elseif now >= last_broadcast_time + registration_period then
+ mp.commandv("script-message", "thumbnails-generator-broadcast", mp.get_script_name())
+ last_broadcast_time = now
+ end
+ end
+
+ while true do
+ if not handle_events(sleep_time) then
+ return
+ end
+ broadcast_func()
+ while #jobs_queue > 0 do
+ local thumbnail_job = jobs_queue[#jobs_queue]
+ if not failed[thumbnail_job.output_path] then
+ if generate_thumbnail(thumbnail_job) then
+ mp.commandv(
+ "script-message-to",
+ thumbnail_job.requester,
+ "thumbnail-generated",
+ thumbnail_job.output_path
+ )
+ else
+ failed[thumbnail_job.output_path] = true
+ end
+ end
+ jobs_queue[#jobs_queue] = nil
+ if not handle_events(0) then
+ return
+ end
+ broadcast_func()
+ end
+ end
+end