local msg = require "mp.msg" local utils = require "mp.utils" local options = require "mp.options" local cut_pos = nil local copy_audio = true local ext_map = { ["mpegts"] = "ts", } local o = { ffmpeg_path = "ffmpeg", target_dir = "~~/cutfragments", overwrite = false, -- whether to overwrite exist files vcodec = "copy", acodec = "copy", debug = false, } options.read_options(o) Command = {} local function is_protocol(path) return type(path) == 'string' and (path:find('^%a[%w.+-]-://') ~= nil or path:find('^%a[%w.+-]-:%?') ~= nil) end function Command:new(name) local o = {} setmetatable(o, self) self.__index = self o.name = "" o.args = { "" } if name then o.name = name o.args[1] = name end return o end function Command:arg(...) for _, v in ipairs({...}) do self.args[#self.args + 1] = v end return self end function Command:as_str() return table.concat(self.args, " ") end function Command:run() local res, err = mp.command_native({ name = "subprocess", args = self.args, capture_stdout = true, capture_stderr = true, }) return res, err end local function file_format() local fmt = mp.get_property("file-format") if not fmt:find(',') then return fmt end local path = mp.get_property('path') if is_protocol(path) then return nil end local filename = mp.get_property('filename') return filename:match('%.([^.]+)$') end local function get_ext() local fmt = file_format() if fmt and ext_map[fmt] ~= nil then return ext_map[fmt] else return fmt end end local function timestamp(duration) local hours = math.floor(duration / 3600) local minutes = math.floor(duration % 3600 / 60) local seconds = duration % 60 return string.format("%02d:%02d:%06.3f", hours, minutes, seconds) end local function osd(str) return mp.osd_message(str, 3) end local function info(s) msg.info(s) osd(s) end local function get_outname(path, shift, endpos) local name = mp.get_property("filename/no-ext") if is_protocol(path) then name = mp.get_property("media-title") end local ext = get_ext() or "mkv" name = string.format("%s_%s-%s.%s", name, timestamp(shift), timestamp(endpos), ext) return name:gsub(":", "-") end local function cut(shift, endpos) local duration = endpos - shift local path = mp.get_property("path") local inpath = mp.get_property("stream-open-filename") local outpath = utils.join_path( o.target_dir, get_outname(path, shift, endpos) ) local cache = mp.get_property_native("cache") local cache_state = mp.get_property_native("demuxer-cache-state") local cache_ranges = cache_state and cache_state["seekable-ranges"] or {} if path and is_protocol(path) or cache == "auto" and #cache_ranges > 0 then local pid = mp.get_property_native('pid') local temp_path = os.getenv("TEMP") or "/tmp/" local temp_video_file = utils.join_path(temp_path, "mpv_dump_" .. pid .. ".mkv") mp.commandv("dump-cache", shift, endpos, temp_video_file) shift = 0 inpath = temp_video_file end local cmds = Command:new(o.ffmpeg_path) :arg("-v", "warning") :arg(o.overwrite and "-y" or "-n") :arg("-stats") cmds:arg("-ss", tostring(shift)) cmds:arg("-accurate_seek") cmds:arg("-i", inpath) cmds:arg("-t", tostring(duration)) cmds:arg("-c:v", o.vcodec) cmds:arg("-c:a", o.acodec) cmds:arg("-c:s", "copy") cmds:arg("-map", string.format("v:%s?", math.max(mp.get_property_number("current-tracks/video/id", 0) - 1, 0))) cmds:arg("-map", string.format("a:%s?", math.max(mp.get_property_number("current-tracks/audio/id", 0) - 1, 0))) cmds:arg("-map", string.format("s:%s?", math.max(mp.get_property_number("current-tracks/sub/id", 0) - 1, 0))) cmds:arg(not copy_audio and "-an" or nil) cmds:arg("-avoid_negative_ts", "make_zero") cmds:arg("-async", "1") cmds:arg(outpath) msg.info("Run commands: " .. cmds:as_str()) local screenx, screeny, aspect = mp.get_osd_size() mp.set_osd_ass(screenx, screeny, "{\\an9}● ") local res, err = cmds:run() mp.set_osd_ass(screenx, screeny, "") if err then msg.error(utils.to_string(err)) mp.osd_message("Failed. Refer console for details.") elseif res.status ~= 0 then if res.stderr ~= "" or res.stdout ~= "" then msg.info("stderr: " .. (res.stderr:gsub("^%s*(.-)%s*$", "%1"))) -- trim stderr msg.info("stdout: " .. (res.stdout:gsub("^%s*(.-)%s*$", "%1"))) -- trim stdout mp.osd_message("Failed. Refer console for details.") end elseif res.status == 0 then if o.debug and (res.stderr ~= "" or res.stdout ~= "") then msg.info("stderr: " .. (res.stderr:gsub("^%s*(.-)%s*$", "%1"))) -- trim stderr msg.info("stdout: " .. (res.stdout:gsub("^%s*(.-)%s*$", "%1"))) -- trim stdout end msg.info("Trim file successfully created: " .. outpath) mp.add_timeout(1, function() mp.osd_message("Trim file successfully created!") end) end end local function toggle_mark() local pos, err = mp.get_property_number("time-pos") if not pos then osd("Failed to get timestamp") msg.error("Failed to get timestamp: " .. err) return end if cut_pos then local shift, endpos = cut_pos, pos if shift > endpos then shift, endpos = endpos, shift elseif shift == endpos then osd("Cut fragment is empty") return end cut_pos = nil info(string.format("Cut fragment: %s-%s", timestamp(shift), timestamp(endpos))) cut(shift, endpos) else cut_pos = pos info(string.format("Marked %s as start position", timestamp(pos))) end end local function toggle_audio() copy_audio = not copy_audio info("Audio capturing is " .. (copy_audio and "enabled" or "disabled")) end local function clear_toggle_mark() cut_pos = nil info("Cleared cut fragment") end o.target_dir = o.target_dir:gsub('"', "") local file, _ = utils.file_info(mp.command_native({ "expand-path", o.target_dir })) if not file then --create target_dir if it doesn't exist local savepath = mp.command_native({ "expand-path", o.target_dir }) local is_windows = package.config:sub(1, 1) == "\\" local windows_args = { 'powershell', '-NoProfile', '-Command', 'mkdir', string.format("\"%s\"", savepath) } local unix_args = { 'mkdir', '-p', savepath } local args = is_windows and windows_args or unix_args local res = mp.command_native({name = "subprocess", capture_stdout = true, playback_only = false, args = args}) if res.status ~= 0 then msg.error("Failed to create target_dir save directory "..savepath..". Error: "..(res.error or "unknown")) return end elseif not file.is_dir then osd("target_dir is a file") msg.warn(string.format("target_dir `%s` is a file", o.target_dir)) end o.target_dir = mp.command_native({ "expand-path", o.target_dir }) mp.add_key_binding("c", "slicing_mark", toggle_mark) mp.add_key_binding("a", "slicing_audio", toggle_audio) mp.add_key_binding("C", "clear_slicing_mark", clear_toggle_mark)