From 182776185352234198072d1285ca3f0c07a10ceb Mon Sep 17 00:00:00 2001 From: f0e <7321764+f0e@users.noreply.github.com> Date: Fri, 30 Jan 2026 13:50:27 +1000 Subject: [PATCH 1/3] feat: lossy ffmpeg args & render+copy keybind --- mpv-lossless-cut.conf | 3 +- mpv-lossless-cut.lua | 88 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 83 insertions(+), 8 deletions(-) diff --git a/mpv-lossless-cut.conf b/mpv-lossless-cut.conf index b22fc09..c762738 100644 --- a/mpv-lossless-cut.conf +++ b/mpv-lossless-cut.conf @@ -1,3 +1,4 @@ -lossless=yes output_dir=. multi_cut_mode=separate +lossless=yes +lossy_ffmpeg_args=-c:v libx264 -crf 18 -preset fast -c:a aac -b:a 192k \ No newline at end of file diff --git a/mpv-lossless-cut.lua b/mpv-lossless-cut.lua index a4e059c..d7b5b26 100644 --- a/mpv-lossless-cut.lua +++ b/mpv-lossless-cut.lua @@ -3,9 +3,10 @@ mp.utils = require("mp.utils") mp.options = require("mp.options") local options = { - lossless = true, output_dir = ".", multi_cut_mode = "separate", + lossless = true, + lossy_ffmpeg_args = "-c:v libx264 -crf 18 -preset fast -c:a aac -b:a 192k", } mp.options.read_options(options, "mpv-lossless-cut") @@ -161,6 +162,49 @@ local function delete_file(file_path) return res.status == 0 end +local function copy_to_clipboard(file_path) + local args + if os_name == "windows" then + -- Windows: Copy file path to clipboard + args = { + "powershell", + "-command", + string.format('Set-Clipboard -Path "%s"', file_path:gsub("/", "\\")), + } + elseif os_name == "mac" then + -- macOS: Copy file to clipboard + args = { "osascript", "-e", string.format('set the clipboard to POSIX file "%s"', file_path) } + else + -- Linux: Try xclip or wl-copy + local xclip_check = mp.utils.subprocess({ args = { "which", "xclip" }, cancellable = false }) + local wl_copy_check = mp.utils.subprocess({ args = { "which", "wl-copy" }, cancellable = false }) + + if xclip_check.status == 0 then + args = { "xclip", "-selection", "clipboard", "-t", "text/uri-list", "-i" } + -- Need to pipe the file URI + local uri = "file://" .. file_path + local result = mp.utils.subprocess({ + args = args, + stdin = uri, + cancellable = false, + }) + return result.status == 0 + elseif wl_copy_check.status == 0 then + args = { "wl-copy", file_path } + else + log("No clipboard utility found (xclip or wl-copy)") + return false + end + end + + if args then + local result = mp.utils.subprocess({ args = args, cancellable = false }) + return result.status == 0 + end + + return false +end + local function set_file_times(file_path, mtime) if not mtime then mp.msg.warn("No mtime provided for: " .. file_path) @@ -245,7 +289,7 @@ local function run_ffmpeg(args) return result.status == 0, result.stdout, result.stderr end -local function render_cut(input, outpath, start, duration, input_mtime) +local function render_cut(input, outpath, start, duration, input_mtime, use_lossless) local args = { -- seek to start before loading file (faster) https://trac.ffmpeg.org/wiki/Seeking#Inputseeking "-ss", @@ -262,9 +306,13 @@ local function render_cut(input, outpath, start, duration, input_mtime) "make_zero", } - if options.lossless then + if use_lossless then table.insert(args, "-c") table.insert(args, "copy") + else + for arg in options.lossy_ffmpeg_args:gmatch("%S+") do + table.insert(args, arg) + end end table.insert(args, outpath) @@ -351,7 +399,7 @@ local function dump_cache(outpath) return cache_start end -local function cut_render() +local function cut_render(use_lossless, copy_clipboard) if #cuts == 0 or not cuts[#cuts].end_time then log("No complete cuts to render") return @@ -367,7 +415,15 @@ local function cut_render() local is_stream = input_info == nil local outdir - if options.output_dir == "@cwd" or is_stream then + if copy_clipboard then + if os_name == "windows" then + outdir = os.getenv("TEMP") or os.getenv("TMP") or "C:\\Windows\\Temp" + else + outdir = os.getenv("TMPDIR") or "/tmp" + end + + outdir = join_paths(outdir, "mpv-lossless-cut") + elseif options.output_dir == "@cwd" or is_stream then outdir = mp.utils.getcwd() else input_dir = mp.utils.split_path(input) @@ -433,7 +489,7 @@ local function cut_render() log(string.format("(%d/%d) Rendering cut to %s", i, #cuts, cut_path)) local mtime = input_info and input_info.mtime or nil - local success = render_cut(input, cut_path, cut.start_time - cache_offset, duration, mtime) + local success = render_cut(input, cut_path, cut.start_time - cache_offset, duration, mtime, use_lossless) if success then table.insert(cut_paths, cut_path) log(string.format("(%d/%d) Rendered cut to %s", i, #cuts, cut_path)) @@ -443,6 +499,8 @@ local function cut_render() end end + local final_output = nil + if #cut_paths > 1 and options.multi_cut_mode == "merge" then local merge_name = string.format("(%d merged cuts) %s%s", #cut_paths, filename_noext, ext) @@ -454,9 +512,20 @@ local function cut_render() if success then log("Successfully merged cuts") + final_output = merge_path else log("Failed to merge cuts") end + elseif #cut_paths == 1 then + final_output = cut_paths[1] + end + + if copy_clipboard and final_output then + if copy_to_clipboard(final_output) then + log("Copied to clipboard: " .. final_output) + else + log("Failed to copy to clipboard") + end end if is_stream then @@ -534,7 +603,12 @@ end) mp.add_key_binding("ctrl+g", "cut_toggle_mode", cut_toggle_mode) mp.add_key_binding("ctrl+h", "cut_clear", cut_clear) -mp.add_key_binding("r", "cut_render", cut_render) +mp.add_key_binding("r", "cut_render", function() + cut_render(options.lossless, false) +end) +mp.add_key_binding("ctrl+r", "cut_render_clipboard", function() + cut_render(false, true) +end) mp.register_event("end-file", function() cut_clear(true) From d5f61434959f3eeff4894cca272b3eceffe12433 Mon Sep 17 00:00:00 2001 From: f0e <7321764+f0e@users.noreply.github.com> Date: Fri, 30 Jan 2026 14:44:47 +1000 Subject: [PATCH 2/3] fix: properly parse quoted ffmpeg arguments --- mpv-lossless-cut.lua | 81 ++++++++++++++++++++------------------------ 1 file changed, 36 insertions(+), 45 deletions(-) diff --git a/mpv-lossless-cut.lua b/mpv-lossless-cut.lua index d7b5b26..e45c489 100644 --- a/mpv-lossless-cut.lua +++ b/mpv-lossless-cut.lua @@ -57,6 +57,39 @@ local function to_hms(secs) return #str == 0 and "0" or table.concat(str, "") end +local function parse_ffmpeg_args(args_string) + local args = {} + local in_quote = false + local quote_char = nil + local current_arg = "" + + for i = 1, #args_string do + local char = args_string:sub(i, i) + + if (char == '"' or char == "'") and not in_quote then + in_quote = true + quote_char = char + elseif char == quote_char and in_quote then + in_quote = false + quote_char = nil + elseif char:match("%s") and not in_quote then + if current_arg ~= "" then + table.insert(args, current_arg) + current_arg = "" + end + else + current_arg = current_arg .. char + end + end + + -- add the last argument + if current_arg ~= "" then + table.insert(args, current_arg) + end + + return args +end + function join_paths(path1, path2) if not path1 or path1 == "" then return path2 or "" @@ -162,49 +195,6 @@ local function delete_file(file_path) return res.status == 0 end -local function copy_to_clipboard(file_path) - local args - if os_name == "windows" then - -- Windows: Copy file path to clipboard - args = { - "powershell", - "-command", - string.format('Set-Clipboard -Path "%s"', file_path:gsub("/", "\\")), - } - elseif os_name == "mac" then - -- macOS: Copy file to clipboard - args = { "osascript", "-e", string.format('set the clipboard to POSIX file "%s"', file_path) } - else - -- Linux: Try xclip or wl-copy - local xclip_check = mp.utils.subprocess({ args = { "which", "xclip" }, cancellable = false }) - local wl_copy_check = mp.utils.subprocess({ args = { "which", "wl-copy" }, cancellable = false }) - - if xclip_check.status == 0 then - args = { "xclip", "-selection", "clipboard", "-t", "text/uri-list", "-i" } - -- Need to pipe the file URI - local uri = "file://" .. file_path - local result = mp.utils.subprocess({ - args = args, - stdin = uri, - cancellable = false, - }) - return result.status == 0 - elseif wl_copy_check.status == 0 then - args = { "wl-copy", file_path } - else - log("No clipboard utility found (xclip or wl-copy)") - return false - end - end - - if args then - local result = mp.utils.subprocess({ args = args, cancellable = false }) - return result.status == 0 - end - - return false -end - local function set_file_times(file_path, mtime) if not mtime then mp.msg.warn("No mtime provided for: " .. file_path) @@ -310,7 +300,8 @@ local function render_cut(input, outpath, start, duration, input_mtime, use_loss table.insert(args, "-c") table.insert(args, "copy") else - for arg in options.lossy_ffmpeg_args:gmatch("%S+") do + local parsed_args = parse_ffmpeg_args(options.lossy_ffmpeg_args) + for _, arg in ipairs(parsed_args) do table.insert(args, arg) end end @@ -521,7 +512,7 @@ local function cut_render(use_lossless, copy_clipboard) end if copy_clipboard and final_output then - if copy_to_clipboard(final_output) then + if mp.set_property('clipboard/text', final_output) then log("Copied to clipboard: " .. final_output) else log("Failed to copy to clipboard") From a7a55c49f18cf861a129ad4adb097fcf658fbb3f Mon Sep 17 00:00:00 2001 From: f0e <7321764+f0e@users.noreply.github.com> Date: Fri, 30 Jan 2026 15:54:05 +1000 Subject: [PATCH 3/3] refactor: simplify code --- mpv-lossless-cut.lua | 93 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 78 insertions(+), 15 deletions(-) diff --git a/mpv-lossless-cut.lua b/mpv-lossless-cut.lua index e45c489..ade80c9 100644 --- a/mpv-lossless-cut.lua +++ b/mpv-lossless-cut.lua @@ -11,9 +11,22 @@ local options = { mp.options.read_options(options, "mpv-lossless-cut") +-- https://github.com/CogentRedTester/mpv-clipboard/blob/master/clipboard.lua +local function detect_platform() + local o = {} + if mp.get_property_native("options/vo-mmcss-profile", o) ~= o then + return "windows" + elseif mp.get_property_native("options/macos-force-dedicated-gpu", o) ~= o then + return "macos" + elseif os.getenv("WAYLAND_DISPLAY") then + return "wayland" + end + return "x11" +end + +local platform = detect_platform() + local cuts = {} -local os_name = package.config:sub(1, 1) == "\\" and "windows" - or (io.popen("uname"):read("*a"):match("Darwin") and "mac" or "linux") -- utility functions local function log(message) @@ -62,10 +75,10 @@ local function parse_ffmpeg_args(args_string) local in_quote = false local quote_char = nil local current_arg = "" - + for i = 1, #args_string do local char = args_string:sub(i, i) - + if (char == '"' or char == "'") and not in_quote then in_quote = true quote_char = char @@ -81,12 +94,12 @@ local function parse_ffmpeg_args(args_string) current_arg = current_arg .. char end end - + -- add the last argument if current_arg ~= "" then table.insert(args, current_arg) end - + return args end @@ -99,7 +112,7 @@ function join_paths(path1, path2) end local separator - if os_name == "windows" then + if platform == "windows" then separator = "\\" else separator = "/" @@ -160,12 +173,63 @@ function join_paths(path1, path2) return resolve_path(path1, path2) end +local function command_exists(cmd) + local pipe = io.popen("type " .. cmd .. ' > /dev/null 2> /dev/null; printf "$?"', "r") + exists = pipe:read() == "0" + pipe:close() + return exists +end + +local function copy_path_to_clipboard(file_path) + local result, args + + if platform == "windows" then + result = mp.utils.subprocess({ + args = { + "powershell", + "-command", + string.format("Set-Clipboard -LiteralPath '%s'", file_path:gsub("'", "''")), + }, + cancellable = false, + }) + elseif platform == "macos" then + result = mp.utils.subprocess({ + args = { + "osascript", + "-e", + string.format('set the clipboard to POSIX file "%s"', file_path), + }, + cancellable = false, + }) + else + local uri = "file://" .. file_path + + if command_exists("xclip") then + result = mp.utils.subprocess({ + args = { "xclip", "-selection", "clipboard", "-t", "text/uri-list" }, + stdin = uri, + cancellable = false, + }) + elseif command_exists("wl-copy") then + result = mp.utils.subprocess({ + args = { "wl-copy", "--type", "text/uri-list" }, + stdin = uri, + cancellable = false, + }) + else + log("No clipboard utility found (xclip or wl-copy)") + end + end + + return result and result.status == 0 +end + -- file operations local function ensure_directory_exists(dir) local dir_info = mp.utils.file_info(dir) if not dir_info or not dir_info.is_dir then local args - if os_name == "windows" then + if platform == "windows" then args = { "cmd", "/c", "mkdir", dir } else args = { "mkdir", "-p", dir } @@ -185,7 +249,7 @@ local function delete_file(file_path) end local args - if os_name == "windows" then + if platform == "windows" then args = { "cmd", "/c", "del", file_path } else args = { "rm", file_path } @@ -207,11 +271,10 @@ local function set_file_times(file_path, mtime) return false end - local normalized_path = file_path:gsub([[\]], "/") local success = false local result - if os_name == "windows" then + if platform == "windows" then result = mp.utils.subprocess({ args = { "powershell", @@ -221,7 +284,7 @@ local function set_file_times(file_path, mtime) .. '$date = (Get-Date "1970-01-01 00:00:00").AddSeconds(%d).ToLocalTime(); ' .. "$file.CreationTime = $date; " .. "$file.LastWriteTime = $date", - normalized_path:gsub("/", "\\"):gsub("'", "''"), + file_path:gsub("'", "''"), mtime ), }, @@ -233,7 +296,7 @@ local function set_file_times(file_path, mtime) "touch", "-t", os.date("!%Y%m%d%H%M.%S", mtime), - normalized_path, + file_path, }, cancellable = false, }) @@ -407,7 +470,7 @@ local function cut_render(use_lossless, copy_clipboard) local outdir if copy_clipboard then - if os_name == "windows" then + if platform == "windows" then outdir = os.getenv("TEMP") or os.getenv("TMP") or "C:\\Windows\\Temp" else outdir = os.getenv("TMPDIR") or "/tmp" @@ -512,7 +575,7 @@ local function cut_render(use_lossless, copy_clipboard) end if copy_clipboard and final_output then - if mp.set_property('clipboard/text', final_output) then + if copy_path_to_clipboard(final_output) then log("Copied to clipboard: " .. final_output) else log("Failed to copy to clipboard")