-- Copyright (c) 2025, Eisa AlAwadhi -- License: BSD 2-Clause License -- Creator: Eisa AlAwadhi -- Project: SmartSkip -- Version: 1.3.4 -- Date: 14-May-2025 -- Related forked projects: -- https://github.com/detuur/mpv-scripts/blob/master/skiptosilence.lua -- https://github.com/mar04/chapters_for_mpv -- https://github.com/po5/chapterskip/blob/master/chapterskip.lua local o = { -----Silence Skip Settings----- silence_audio_level = -40, silence_duration = 0.65, ignore_silence_duration=5, min_skip_duration = 0, max_skip_duration = 180, keybind_twice_cancel_skip = true, silence_skip_to_end = "playlist-next", add_chapter_on_skip = true, force_mute_on_skip = false, -----Smart Skip Settings----- last_chapter_skip_behavior=[[ [ ["no-chapters", "silence-skip"], ["internal-chapters", "playlist-next"], ["external-chapters", "silence-skip"] ] ]], smart_next_proceed_countdown = true, smart_prev_cancel_countdown = true, -----Chapters Settings----- external_chapters_autoload = true, modified_chapters_autosave=[[ ["no-chapters", "external-chapters"] ]], global_chapters = true, global_chapters_path = "/:dir%mpvconf%/chapters", hash_global_chapters = true, add_chapter_ask_title = false, add_chapter_pause_for_input = false, add_chapter_placeholder_title = "Chapter ", -----Auto-Skip Settings----- autoskip_chapter = true, autoskip_countdown = 3, autoskip_countdown_bulk = false, autoskip_countdown_graceful = false, skip_once = false, categories=[[ [ ["internal-chapters", "prologue>Prologue/^Intro; opening>^OP/ OP$/^Opening; ending>^ED/ ED$/^Ending/Credits Start; preview>PV/ PV$/^Preview/Preview Start"], ["external-chapters", "idx->0/2"] ] ]], skip=[[ [ ["internal-chapters", "toggle;toggle_idx;opening;ending;preview"], ["external-chapters", "toggle;toggle_idx"] ] ]], invert_autoskip_exclusion = false, autoskip_exclusion=[[ [""] ]], -----OSD Messages Settings----- osd_duration = 2500, silence_skip_osd = "osd-msg-bar", chapter_osd = "osd-msg-bar", autoskip_osd = "osd-msg-bar", playlist_osd = true, osd_msg = true, -----Keybind Settings----- toggle_autoskip_keybind=[[ ["ctrl+."] ]], toggle_category_autoskip_keybind=[[ ["alt+."] ]], cancel_autoskip_countdown_keybind=[[ ["esc", "n"] ]], proceed_autoskip_countdown_keybind=[[ ["enter", "y"] ]], add_chapter_keybind=[[ ["n"] ]], remove_chapter_keybind=[[ ["alt+n"] ]], edit_chapter_keybind=[[ [""] ]], write_chapters_keybind=[[ ["ctrl+n"] ]], bake_chapters_keybind=[[ [""] ]], chapter_next_keybind=[[ ["ctrl+right"] ]], chapter_prev_keybind=[[ ["ctrl+left"] ]], smart_next_keybind=[[ [">"] ]], smart_prev_keybind=[[ ["<"] ]], silence_skip_keybind=[[ ["?"] ]], cancel_silence_skip_keybind=[[ ["esc"] ]], } local mp = require 'mp' local msg = require 'mp.msg' local utils = require 'mp.utils' local options = require 'mp.options' options.read_options(o) if o.add_chapter_on_skip ~= false and o.add_chapter_on_skip ~= true then o.add_chapter_on_skip = utils.parse_json(o.add_chapter_on_skip) end if o.modified_chapters_autosave ~= false and o.modified_chapters_autosave ~= true then o.modified_chapters_autosave = utils.parse_json(o.modified_chapters_autosave) end o.last_chapter_skip_behavior = utils.parse_json(o.last_chapter_skip_behavior) if utils.parse_json(o.skip) ~= nil then o.skip = utils.parse_json(o.skip) end if utils.parse_json(o.categories) ~= nil then o.categories = utils.parse_json(o.categories) end if o.skip_once ~= false and o.skip_once ~= true then o.skip_once = utils.parse_json(o.skip_once) end if o.global_chapters_path:match('/:dir%%mpvconf%%') then o.global_chapters_path = o.global_chapters_path:gsub('/:dir%%mpvconf%%', mp.find_config_file('.')) elseif o.global_chapters_path:match('/:dir%%script%%') then o.global_chapters_path = o.global_chapters_path:gsub('/:dir%%script%%', debug.getinfo(1).source:match('@?(.*/)')) elseif o.global_chapters_path:match('/:var%%(.*)%%') then local os_variable = o.global_chapters_path:match('/:var%%(.*)%%') o.global_chapters_path = o.global_chapters_path:gsub('/:var%%(.*)%%', os.getenv(os_variable)) end o.toggle_autoskip_keybind = utils.parse_json(o.toggle_autoskip_keybind) o.cancel_autoskip_countdown_keybind = utils.parse_json(o.cancel_autoskip_countdown_keybind) o.proceed_autoskip_countdown_keybind = utils.parse_json(o.proceed_autoskip_countdown_keybind) o.toggle_category_autoskip_keybind = utils.parse_json(o.toggle_category_autoskip_keybind) o.add_chapter_keybind = utils.parse_json(o.add_chapter_keybind) o.remove_chapter_keybind = utils.parse_json(o.remove_chapter_keybind) o.write_chapters_keybind = utils.parse_json(o.write_chapters_keybind) o.edit_chapter_keybind = utils.parse_json(o.edit_chapter_keybind) o.bake_chapters_keybind = utils.parse_json(o.bake_chapters_keybind) o.chapter_prev_keybind = utils.parse_json(o.chapter_prev_keybind) o.chapter_next_keybind = utils.parse_json(o.chapter_next_keybind) o.smart_prev_keybind = utils.parse_json(o.smart_prev_keybind) o.smart_next_keybind = utils.parse_json(o.smart_next_keybind) o.silence_skip_keybind = utils.parse_json(o.silence_skip_keybind) o.cancel_silence_skip_keybind = utils.parse_json(o.cancel_silence_skip_keybind) o.autoskip_exclusion = utils.parse_json(o.autoskip_exclusion) --1.3.4# option autoskip_exclusion parsing package.path = mp.command_native({"expand-path", "~~/script-modules/?.lua;"}) .. package.path local user_input_module, input = pcall(require, "user-input-module") mp.set_property('user-data/smartskip/silence-skip', 'no') if o.osd_duration == -1 then o.osd_duration = (mp.get_property_number('osd-duration') or 1000) end local speed_state = 1 local pause_state = false local mute_state = false local sub_state = nil local secondary_sub_state = nil local vid_state = nil local skip_flag = false local window_state = nil local force_silence_skip = false local initial_skip_time = 0 local initial_chapter_count = 0 local chapter_state = 'no-chapters' local file_length = 0 local keep_open_state = "yes" if mp.get_property("config") ~= "no" then keep_open_state = mp.get_property("keep-open") end local osd_duration_default = (mp.get_property_number('osd-duration') or 1000) local geometry_default = mp.get_property_native("geometry") local autoskip_chapter = o.autoskip_chapter local playlist_osd = false local autoskip_playlist_osd = false local g_playlist_pos = 0 local g_opt_categories = o.categories local g_opt_skip_once = false o.autoskip_countdown = math.floor(o.autoskip_countdown) local g_autoskip_countdown = o.autoskip_countdown local g_autoskip_countdown_flag = false local g_protocols = {'https?:', 'magnet:', 'rtmps?:', 'smb:', 'ftps?:', 'sftp:'} --1.3.4# default global protocols needed to check for protocols / website exclusion local g_filepath = '' --1.3.4# define initial global variable for filepath local g_autoskip_disabled = false --1.3.4# set global variable for autoskip disabled to be used for autoskip exclusion_check function local categories = { toggle = "", toggle_idx = "", } -- utility functions -- function has_value(tab, val, array2d) if not tab then return msg.error('check value passed') end if not val then return msg.error('check value passed') end if not array2d then for index, value in ipairs(tab) do if string.lower(value) == string.lower(val) then return true end end end if array2d then for i=1, #tab do if tab[i] and string.lower(tab[i][array2d]) == string.lower(val) then return true end end end return false end function esc_string(str) return str:gsub("([%p])", "%%%1") end function esc_lua_pattern(str) --1.3.4# helper function to escape pattern needed for exclusion_check function return str:gsub("([%^%$%(%)%%%.%[%]%+%-%?])", "%%%1") end function starts_protocol(tab, val) --1.3.4# helper function to check if file is protocol needed for exclusion_check function for index, value in ipairs(tab) do if (val:find(value) == 1) then return true end end return false end function prompt_msg(text, duration, osd) if not text then return end msg.info(text) if not o.osd_msg then return end if osd == "no-osd" or osd == "osd-bar" then return end if not duration then duration = o.osd_duration end mp.commandv("show-text", text, duration) end function prompt_progress(osd, duration) if not o.osd_msg or not osd then return end if osd == "no-osd" then return end if not duration then duration = o.osd_duration end mp.set_property("osd-duration", o.osd_duration) mp.commandv(osd, "show-progress") mp.add_timeout(0.07, function () mp.set_property('osd-duration', osd_duration_default) end) end function bind_keys(keys, name, func, opts) if not keys then mp.add_forced_key_binding(keys, name, func, opts) return end for i = 1, #keys do if i == 1 then mp.add_forced_key_binding(keys[i], name, func, opts) else mp.add_forced_key_binding(keys[i], name .. i, func, opts) end end end function unbind_keys(keys, name) if not keys then mp.remove_key_binding(name) return end for i = 1, #keys do if i == 1 then mp.remove_key_binding(name) else mp.remove_key_binding(name .. i) end end end function exclusion_check() if g_filepath == "" then return false end if o.autoskip_exclusion == nil then msg.warn('ignoring autoskip_exclusion option due to an inappropriate value') return false end if not o.autoskip_exclusion[1] or #o.autoskip_exclusion == 1 and o.autoskip_exclusion[1] == "" then return false end local invertable_return = {true, false} local exclusion_msg = 'Auto-Skip disabled because of exclusion' if o.invert_autoskip_exclusion then invertable_return = {false, true} exclusion_msg = 'Auto-Skip enabled because of inclusion' end if has_value(o.autoskip_exclusion, g_filepath, nil) then msg.info(exclusion_msg) return invertable_return[1] elseif not starts_protocol(g_protocols, g_filepath) then if has_value(o.autoskip_exclusion, g_filepath:match('^(.-)([^\\/]-)%.([^\\/%.]-)%.?$'), nil) or has_value(o.autoskip_exclusion, g_filepath:match('^(.-)([^\\/]-)%.([^\\/%.]-)%.?$'):gsub('\\$', ''), nil) then msg.info(exclusion_msg) return invertable_return[1] elseif has_value(o.autoskip_exclusion, g_filepath:match('%.([^%.]+)$'), nil) or has_value(o.autoskip_exclusion, "."..g_filepath:match('%.([^%.]+)$'), nil) then msg.info(exclusion_msg) return invertable_return[1] else for i=1, #o.autoskip_exclusion do local esc_autoskip_exclusion = esc_lua_pattern(o.autoskip_exclusion[i]) if string.lower(g_filepath):match(string.lower(esc_autoskip_exclusion)) and o.autoskip_exclusion[i]:sub(-2) == '\\*' and string.lower(o.autoskip_exclusion[i]:sub(1, -2)) ~= string.lower(g_filepath):match("(.*[\\/])") then msg.info(exclusion_msg) return invertable_return[1] end end end elseif starts_protocol(g_protocols, g_filepath) then if has_value(o.autoskip_exclusion, g_filepath:match('(.-)(:)'), nil) or has_value(o.autoskip_exclusion, g_filepath:match('(.-:)'), nil) or has_value(o.autoskip_exclusion, g_filepath:match('(.-:/?/?)'), nil) then msg.info(exclusion_msg) return invertable_return[1] elseif g_filepath:find('https?://') == 1 then local difchk_1, difchk_2 = g_filepath:match("(https?://)w?w?w?%.?([%w%.%:]*)") local different_check_temp = difchk_1..difchk_2 local different_checks = {different_check_temp, g_filepath:match("https?://w?w?w?%.?([%w%.%:]*)"), g_filepath:match("https?://([%w%.%:]*)"), g_filepath:match("(https?://[%w%.%:]*)") } for i = 1, #different_checks do if different_checks[i] and has_value(o.autoskip_exclusion, different_checks[i], nil) or different_checks[i]..'/' and has_value(o.autoskip_exclusion, different_checks[i]..'/', nil) then msg.info(exclusion_msg) return invertable_return[1] end end end end return invertable_return[2] end -- skip-silence utility functions -- function restoreProp(timepos,pause) if not timepos then timepos = mp.get_property_number("time-pos") end if not pause then pause = pause_state end mp.set_property("vid", vid_state) mp.set_property("force-window", window_state) mp.set_property_bool("mute", mute_state) mp.set_property("speed", speed_state) mp.unobserve_property(foundSilence) mp.command("no-osd af remove @skiptosilence") mp.set_property_bool("pause", pause) mp.set_property_number("time-pos", timepos) mp.set_property("sub-visibility", sub_state) mp.set_property("secondary-sub-visibility", secondary_sub_state) unbind_keys(o.cancel_silence_skip_keybind, 'cancel-silence-skip') mp.set_property('user-data/smartskip/silence-skip', 'no') timer:kill() skip_flag = false end function handleMinMaxDuration(timepos) if not skip_flag then return end if not timepos then timepos = mp.get_property_number("time-pos") end skip_duration = timepos - initial_skip_time if o.min_skip_duration > 0 and skip_duration <= o.min_skip_duration then restoreProp(initial_skip_time) prompt_msg('Skipping Cancelled\nSilence less than minimum', o.osd_duration, o.silence_skip_osd) return true end if o.max_skip_duration > 0 and skip_duration >= o.max_skip_duration then restoreProp(initial_skip_time) prompt_msg('Skipping Cancelled\nSilence is more than configured maximum', o.osd_duration, o.silence_skip_osd) return true end return false end function setKeepOpenState() if o.silence_skip_to_end == "playlist-next" then mp.set_property("keep-open", "yes") else mp.set_property("keep-open", "always") end end function eofHandler(name, val) if val and skip_flag then if o.silence_skip_to_end == 'playlist-next' then restoreProp((mp.get_property_native('duration') or 0)) if mp.get_property_native('playlist-playing-pos')+1 == mp.get_property_native('playlist-count') then prompt_msg('Skipped to end at ' .. mp.get_property_osd('duration'), o.osd_duration, o.silence_skip_osd) else mp.commandv("playlist-next") end elseif o.silence_skip_to_end == 'cancel' then prompt_msg('Skipping Cancelled\nSilence not detected', o.osd_duration, o.silence_skip_osd) restoreProp(initial_skip_time) elseif o.silence_skip_to_end == 'pause' then prompt_msg('Skipped to end at ' .. mp.get_property_osd('duration'), o.osd_duration, o.silence_skip_osd) restoreProp((mp.get_property_native('duration') or 0), true) end end end -- smart-skip main code -- function smartNext() if g_autoskip_countdown_flag and o.smart_next_proceed_countdown then proceed_autoskip(true) return end local next_action = "silence-skip" local chapters_count = (mp.get_property_number('chapters') or 0) local chapter = (mp.get_property_number('chapter') or 0) local current_playlist = (mp.get_property_native('playlist-playing-pos')+1 or 0) local total_playlist = (mp.get_property_native('playlist-count') or 0) if chapter+2 <= chapters_count then next_action = 'chapter-next' elseif chapter+2 > chapters_count and (initial_chapter_count == 0 or chapters_count == 0 or force_silence_skip) then if chapters_count == 0 then force_silence_skip = true end next_action = 'silence-skip' elseif chapter+1 >= chapters_count then for i = 1, #o.last_chapter_skip_behavior do if o.last_chapter_skip_behavior[i] and o.last_chapter_skip_behavior[i][1] == chapter_state then next_action = o.last_chapter_skip_behavior[i][2] break end end end if next_action == 'playlist-next' and current_playlist == total_playlist then next_action = 'chapter-next' end if next_action == 'silence-skip' then silenceSkip() end if next_action == 'chapter-next' then mp.set_property('osd-duration', o.osd_duration) mp.commandv(o.chapter_osd, 'add', 'chapter', 1) mp.add_timeout(0.07, function () mp.set_property('osd-duration', osd_duration_default) end) end if next_action == 'playlist-next' then mp.command('playlist_next') end end function smartPrev() if skip_flag then restoreProp(initial_skip_time) return end if g_autoskip_countdown_flag and o.smart_prev_cancel_countdown then kill_chapterskip_countdown('osd') return end local chapters_count = (mp.get_property_number('chapters') or 0) local chapter = (mp.get_property_number('chapter') or 0) local timepos = (mp.get_property_native("time-pos") or 0) if chapter-1 < 0 and timepos > 1 and chapters_count == 0 then mp.commandv('seek', 0, 'absolute', 'exact') prompt_progress(o.chapter_osd, o.osd_duration) elseif chapter-1 < 0 and timepos < 1 then mp.command('playlist_prev') elseif chapter-1 <= chapters_count then mp.set_property('osd-duration', o.osd_duration) mp.commandv(o.chapter_osd, 'add', 'chapter', -1) mp.add_timeout(0.07, function () mp.set_property('osd-duration', osd_duration_default) end) end end -- chapter-next/prev main code -- function chapterSeek(direction) if skip_flag and direction == -1 then restoreProp(initial_skip_time) return end local chapters_count = (mp.get_property_number('chapters') or 0) local chapter = (mp.get_property_number('chapter') or 0) local timepos = (mp.get_property_native("time-pos") or 0) if chapter+direction < 0 and timepos > 1 and chapters_count == 0 then mp.commandv('seek', 0, 'absolute', 'exact') prompt_progress(o.chapter_osd, o.osd_duration) elseif chapter+direction < 0 and timepos < 1 then mp.command('playlist_prev') elseif chapter+direction >= chapters_count then mp.command('playlist_next') else mp.set_property('osd-duration', o.osd_duration) mp.commandv(o.chapter_osd, 'add', 'chapter', direction) mp.add_timeout(0.07, function () mp.set_property('osd-duration', osd_duration_default) end) end end -- silence skip main code -- function silenceSkip(action) bind_keys(o.cancel_silence_skip_keybind, "cancel-silence-skip", function() restoreProp(initial_skip_time) return end) if skip_flag then if o.keybind_twice_cancel_skip then restoreProp(initial_skip_time) end return end initial_skip_time = (mp.get_property_native("time-pos") or 0) if math.floor(initial_skip_time) == math.floor(mp.get_property_native('duration') or 0) then return end mp.set_property('user-data/smartskip/silence-skip', 'yes') kill_chapterskip_countdown() local width = mp.get_property_native("osd-width") local height = mp.get_property_native("osd-height") local window_maximized = mp.get_property_native("window-maximized") window_state = mp.get_property("force-window") vid_state = mp.get_property("vid") sub_state = mp.get_property("sub-visibility") secondary_sub_state = mp.get_property("secondary-sub-visibility") pause_state = mp.get_property_native("pause") speed_state = mp.get_property_native("speed") if not window_maximized then mp.set_property_native("geometry", ("%dx%d"):format(width, height)) end if o.silence_skip_osd ~= 'no-osd' then mp.commandv(o.silence_skip_osd, "show-progress") end mp.command( "no-osd af add @skiptosilence:lavfi=[silencedetect=noise=" .. o.silence_audio_level .. "dB:d=" .. o.silence_duration .. "]" ) mp.observe_property("af-metadata/skiptosilence", "string", foundSilence) mp.set_property("sub-visibility", "no") mp.set_property("secondary-sub-visibility", "no") mp.set_property("force-window", "yes") mp.set_property("vid", "no") mute_state = mp.get_property_native("mute") if o.force_mute_on_skip then mp.set_property_bool("mute", true) end mp.set_property_bool("pause", false) mp.set_property("speed", 100) setKeepOpenState() skip_flag = true timer = mp.add_periodic_timer(0.5, function() local video_time = (mp.get_property_native("time-pos") or 0) handleMinMaxDuration(video_time) if skip_flag and o.silence_skip_osd ~= 'no-osd' then mp.commandv(o.silence_skip_osd, "show-progress") end end) end function foundSilence(name, value) if value == "{}" or value == nil then return end timecode = tonumber(string.match(value, "%d+%.?%d+")) if timecode == nil or timecode < initial_skip_time + o.ignore_silence_duration then return end if handleMinMaxDuration(timecode) then return end restoreProp(timecode) mp.add_timeout(0.05, function() prompt_msg('Skipped to silence 🕒 ' .. mp.get_property_osd("time-pos"), o.osd_duration, o.silence_skip_osd) end) if o.add_chapter_on_skip == true or has_value(o.add_chapter_on_skip, chapter_state) then mp.add_timeout(0.05, add_chapter) end skip_flag = false end -- modified fork of chapters_for_mpv -- --[[ Copyright (c) 2023 Mariusz Libera Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --]] -- to debug run mpv with arg: --msg-level=SmartSkip=debug -- to test o.run mpv with arg: --script-opts=SmartSkip-OPTION=VALUE local chapters_modified = false msg.debug("options:", utils.to_string(options)) -- CHAPTER MANIPULATION -------------------------------------------------------- function change_title_callback(user_input, err, chapter_index) if user_input == nil or err ~= nil then msg.warn("no chapter title provided:", err) return end local chapter_list = mp.get_property_native("chapter-list") if chapter_index > mp.get_property_number("chapter-list/count") then msg.warn("can't set chapter title") return end chapter_list[chapter_index].title = user_input mp.set_property_native("chapter-list", chapter_list) chapters_modified = true end function edit_chapter() local mpv_chapter_index = mp.get_property_number("chapter") local chapter_list = mp.get_property_native("chapter-list") if mpv_chapter_index == nil or mpv_chapter_index == -1 then msg.verbose("no chapter selected, nothing to edit") return end if not user_input_module then msg.error("no mpv-user-input, can't get user input, install: https://github.com/CogentRedTester/mpv-user-input") return end input.get_user_input(change_title_callback, { request_text = "title of the chapter:", default_input = chapter_list[mpv_chapter_index + 1].title, cursor_pos = #(chapter_list[mpv_chapter_index + 1].title) + 1, }, mpv_chapter_index + 1) if o.add_chapter_pause_for_input then mp.set_property_bool("pause", true) mp.osd_message(" ", 0.1) end end function add_chapter(timepos) if not timepos then timepos = mp.get_property_number("time-pos") end local chapter_list = mp.get_property_native("chapter-list") if #chapter_list > 0 then for i = 1, #chapter_list do if math.floor(chapter_list[i].time) == math.floor(timepos) then msg.debug("failed to add chapter, chapter exists in same position") return end end end local chapter_index = (mp.get_property_number("chapter") or -1) + 2 table.insert(chapter_list, chapter_index, {title = "", time = timepos}) msg.debug("inserting new chapter at ", chapter_index, " chapter_", " time: ", timepos) mp.set_property_native("chapter-list", chapter_list) chapters_modified = true if o.add_chapter_ask_title then if not user_input_module then msg.error("no mpv-user-input, can't get user input, install: https://github.com/CogentRedTester/mpv-user-input") return end -- ask user for chapter title input.get_user_input(change_title_callback, { request_text = "title of the chapter:", default_input = o.placeholder_title .. chapter_index, cursor_pos = #(o.placeholder_title .. chapter_index) + 1, }, chapter_index) if o.add_chapter_pause_for_input then mp.set_property_bool("pause", true) -- FIXME: for whatever reason osd gets hidden when we pause the -- playback like that, workaround to make input prompt appear -- right away without requiring mouse or keyboard action mp.osd_message(" ", 0.1) end end end function remove_chapter() local chapter_count = mp.get_property_number("chapter-list/count") if chapter_count < 1 then msg.verbose("no chapters to remove") return end local chapter_list = mp.get_property_native("chapter-list") local current_chapter = mp.get_property_number("chapter") + 1 table.remove(chapter_list, current_chapter) msg.debug("removing chapter", current_chapter) mp.set_property_native("chapter-list", chapter_list) chapters_modified = true end -- UTILITY FUNCTIONS ----------------------------------------------------------- function detect_os() if package.config:sub(1,1) == "/" then return "unix" else return "windows" end end -- for unix use only -- returns a table of command path and varargs, or nil if command was not found function command_exists(command, ...) msg.debug("looking for command:", command) -- msg.debug("args:", ) local process = mp.command_native({ name = "subprocess", capture_stdout = true, capture_stderr = true, playback_only = false, args = {"sh", "-c", "command -v -- " .. command} }) if process.status == 0 then local command_path = process.stdout:gsub("\n", "") msg.debug("command found:", command_path) return {command_path, ...} else msg.debug("command not found:", command) return nil end end function mkdir(path) local args = nil if detect_os() == "unix" then args = {"mkdir", "-p", "--", path} else args = {"powershell", "-NoProfile", "-Command", "mkdir", path} end local process = mp.command_native({ name = 'subprocess', playback_only = false, capture_stdout = true, capture_stderr = true, args = args, }) if process.status == 0 then msg.debug("mkdir success:", path) return true else msg.error("mkdir failure:", path) return false end end -- returns md5 hash of the full path of the current media file function hash() local path = mp.get_property("path") if path == nil then msg.debug("something is wrong with the path, can't get full_path, can't hash it") return end msg.debug("hashing:", path) local cmd = { name = 'subprocess', capture_stdout = true, playback_only = false, } local args = nil if detect_os() == "unix" then local md5 = command_exists("md5sum") or command_exists("md5") or command_exists("openssl", "md5 | cut -d ' ' -f 2") if md5 == nil then msg.warn("no md5 command found, can't generate hash") return end md5 = table.concat(md5, " ") cmd["stdin_data"] = path args = {"sh", "-c", md5 .. " | cut -d ' ' -f 1 | tr '[:lower:]' '[:upper:]'" } else --windows -- https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/get-filehash?view=powershell-7.3 local hash_command ="$s = [System.IO.MemoryStream]::new(); $w = [System.IO.StreamWriter]::new($s); $w.write(\"" .. path .. "\"); $w.Flush(); $s.Position = 0; Get-FileHash -Algorithm MD5 -InputStream $s | Select-Object -ExpandProperty Hash" args = {"powershell", "-NoProfile", "-Command", hash_command} end cmd["args"] = args msg.debug("hash cmd:", utils.to_string(cmd)) local process = mp.command_native(cmd) if process.status == 0 then local hash = process.stdout:gsub("%s+", "") msg.debug("hash:", hash) return hash else msg.warn("hash function failed") return end end function construct_ffmetadata() local path = mp.get_property("path") if path == nil then msg.debug("something is wrong with the path, can't get full_path") return end local chapter_count = mp.get_property_number("chapter-list/count") local all_chapters = mp.get_property_native("chapter-list") local ffmetadata = ";FFMETADATA1\n;file=" .. path for i, c in ipairs(all_chapters) do local c_title = c.title local c_start = c.time * 1000000000 local c_end if i < chapter_count then c_end = all_chapters[i+1].time * 1000000000 else c_end = (mp.get_property_number("duration") or c.time) * 1000000000 end msg.debug(i, "c_title", c_title, "c_start:", c_start, "c_end", c_end) ffmetadata = ffmetadata .. "\n[CHAPTER]\nSTART=" .. c_start .. "\nEND=" .. c_end .. "\ntitle=" .. c_title end return ffmetadata end -- FILE IO --------------------------------------------------------------------- -- args: -- osd - if true, display an osd message -- force -- if true write chapters file even if there are no changes -- on success returns path of the chapters file, nil on failure function write_chapters(...) local chapters_count = (mp.get_property_number('chapters') or 0) local osd, force = ... if not force and not chapters_modified then msg.debug("nothing to write") return end if initial_chapter_count == 0 and chapters_count == 0 then return end -- figure out the directory local chapters_dir if o.global_chapters then local dir = utils.file_info(o.global_chapters_path) if dir then if dir.is_dir then msg.debug("o.global_chapters_path exists:", o.global_chapters_path) chapters_dir = o.global_chapters_path else msg.error("o.global_chapters_path is not a directory") return end else msg.verbose("o.global_chapters_path doesn't exists:", o.global_chapters_path) if mkdir(o.global_chapters_path) then chapters_dir = o.global_chapters_path else return end end else chapters_dir = utils.split_path(mp.get_property("path")) end -- and the name local name = mp.get_property("filename") if o.hash_global_chapters and o.global_chapters then name = hash() if name == nil then msg.warn("hash function failed, fallback to filename") name = mp.get_property("filename") end end local chapters_file_path = utils.join_path(chapters_dir, name .. ".ffmetadata") --1.09#HERE I SHOULD ADD SOME SORT OF DELETE FUNCTION IN CASE CHAPTER COUNT IS 0 msg.debug("opening for writing:", chapters_file_path) local chapters_file = io.open(chapters_file_path, "w") if chapters_file == nil then msg.error("could not open chapter file for writing") return end local success, error = chapters_file:write(construct_ffmetadata()) chapters_file:close() if success then if osd then prompt_msg('Chapters written to:' .. chapters_file_path) end return chapters_file_path else msg.error("error writing chapters file:", error) return end end function load_chapters() local path = mp.get_property("path") local expected_chapters_file = utils.join_path(utils.split_path(path), mp.get_property("filename") .. ".ffmetadata") msg.debug("looking for:", expected_chapters_file) local file = utils.file_info(expected_chapters_file) if file then msg.debug("found in the local directory, loading..") mp.set_property("file-local-options/chapters-file", expected_chapters_file) chapter_state = 'external-chapters' return end if not o.global_chapters then msg.debug("not in local, global chapters not enabled, aborting search") return end msg.debug("looking in the global directory") if o.hash_global_chapters then local hashed_path = hash() if hashed_path then expected_chapters_file = utils.join_path(o.global_chapters_path, hashed_path .. ".ffmetadata") else msg.debug("hash function failed, fallback to path") expected_chapters_file = utils.join_path(o.global_chapters_path, mp.get_property("filename") .. ".ffmetadata") end else expected_chapters_file = utils.join_path(o.global_chapters_path, mp.get_property("filename") .. ".ffmetadata") end msg.debug("looking for:", expected_chapters_file) file = utils.file_info(expected_chapters_file) if file then msg.debug("found in the global directory, loading..") mp.set_property("file-local-options/chapters-file", expected_chapters_file) chapter_state = 'external-chapters' return end msg.debug("chapters file not found") end function bake_chapters() if mp.get_property_number("chapter-list/count") == 0 then msg.verbose("no chapters present") return end local chapters_file_path = write_chapters(false, true) if not chapters_file_path then msg.error("no chapters file") return end local filename = mp.get_property("filename") local output_name -- extract file extension local reverse_dot_index = filename:reverse():find(".", 1, true) if reverse_dot_index == nil then msg.warning("file has no extension, fallback to .mkv") output_name = filename .. ".chapters.mkv" else local dot_index = #filename + 1 - reverse_dot_index local ext = filename:sub(dot_index + 1) msg.debug("ext:", ext) if ext ~= "mkv" and ext ~= "mp4" and ext ~= "webm" then msg.debug("fallback to .mkv") ext = "mkv" end output_name = filename:sub(1, dot_index) .. "chapters." .. ext end local file_path = mp.get_property("path") local output_path = utils.join_path(utils.split_path(file_path), output_name) local args = {"ffmpeg", "-y", "-i", file_path, "-i", chapters_file_path, "-map_metadata", "1", "-codec", "copy", output_path} msg.debug("args:", utils.to_string(args)) local process = mp.command_native({ name = 'subprocess', playback_only = false, capture_stdout = true, capture_stderr = true, args = args }) if process.status == 0 then prompt_msg('file written to ' .. output_path) else msg.error("failed to write file:\n", process.stderr) end end --modified fork of chapterskip.lua-- function matches(i, title) local opt_skip = o.skip if type(o.skip) == 'table' then for i=1, #o.skip do if o.skip[i] and o.skip[i][1] == chapter_state then opt_skip = o.skip[i][2] break end end end for category in string.gmatch(opt_skip, " *([^;]*[^; ]) *") do if categories[category:lower()] then if category:lower() == "idx-" or category:lower() == "toggle_idx" then for pattern in string.gmatch(categories[category:lower()], "([^/]+)") do if tonumber(pattern) == i then return true end end else if title then for pattern in string.gmatch(categories[category:lower()], "([^/]+)") do if string.match(title, pattern) then return true end end end end end end end local skipped = {} local parsed = {} function prep_chapterskip_var() if chapter_state == 'no-chapters' then return end g_opt_categories = o.categories g_opt_skip_once = false if o.skip_once == true or o.skip_once == false then g_opt_skip_once = o.skip_once elseif has_value(o.skip_once, chapter_state) then g_opt_skip_once = true; end if type(o.categories) == 'table' then for i=1, #o.categories do if o.categories[i] and o.categories[i][1] == chapter_state then g_opt_categories = o.categories[i][2] break end end end for category in string.gmatch(g_opt_categories, "([^;]+)") do local name, patterns = string.match(category, " *([^+>]*[^+> ]) *[+>](.*)") if name then categories[name:lower()] = patterns elseif not parsed[category] then mp.msg.warn("Improper category definition: " .. category) end parsed[category] = true end end function start_chapterskip_countdown(text, duration, osd) g_autoskip_countdown_flag = true g_autoskip_countdown = g_autoskip_countdown - 1 if o.autoskip_countdown_graceful and (g_autoskip_countdown <= 0) then kill_chapterskip_countdown(); mp.osd_message('',0) return end if (g_autoskip_countdown < 0) then kill_chapterskip_countdown(); mp.osd_message('',0) return end text = text:gsub("%%countdown%%", g_autoskip_countdown) prompt_msg(text, duration, osd) end function kill_chapterskip_countdown(action) if not g_autoskip_countdown_flag then return end if action == 'osd' then prompt_msg('○ Auto-Skip Cancelled', o.osd_duration, o.autoskip_osd) end if g_autoskip_timer ~= nil then g_autoskip_timer:kill() end unbind_keys(o.cancel_autoskip_countdown_keybind, 'cancel-autoskip-countdown') unbind_keys(o.proceed_autoskip_countdown_keybind, 'proceed-autoskip-countdown') g_autoskip_countdown = o.autoskip_countdown g_autoskip_countdown_flag = false end function chapterskip(_, current, countdown) if g_autoskip_disabled then return end --1.3.4# do not autoskip if g_autoskip_disabled is in disabled state if skip_flag then return end if chapter_state == 'no-chapters' then return end if not autoskip_chapter then return end if not current then return end if g_autoskip_countdown_flag then kill_chapterskip_countdown('osd') end if not countdown then countdown = o.autoskip_countdown end local chapters = mp.get_property_native("chapter-list") local skip = false local consecutive_i = 0 for i=0, #chapters do if (not g_opt_skip_once or not skipped[i]) and i == 0 and chapters[i+1] and matches(i, chapters[i+1].title) then if i == current + 1 or skip == i - 1 then if skip then skipped[skip] = true end skip = i consecutive_i = consecutive_i+1 end elseif (not g_opt_skip_once or not skipped[i]) and chapters[i] and matches(i, chapters[i].title) then if i == current + 1 or skip == i - 1 then if skip then skipped[skip] = true end skip = i consecutive_i = consecutive_i+1 end elseif skip and countdown <= 0 then if o.autoskip_osd == 'osd-bar' or o.autoskip_osd == 'osd-msg-bar' then prompt_progress('osd-bar', o.osd_duration) end if consecutive_i > 1 then local autoskip_osd_string = '' for j=consecutive_i, 1, -1 do local chapter_title = '' if chapters[i-j] then chapter_title = chapters[i-j].title end autoskip_osd_string=(autoskip_osd_string..'\n ➤ Chapter ('..i-j..') '..chapter_title) end prompt_msg('● Auto-Skip'..autoskip_osd_string, o.osd_duration, o.autoskip_osd) else prompt_msg('➤ Auto-Skip: Chapter '.. mp.command_native({'expand-text', '${chapter}'}), o.osd_duration, o.autoskip_osd) end mp.commandv('no-osd', 'set', 'chapter', i-1) skipped[skip] = true return elseif skip and countdown > 0 then g_autoskip_countdown_flag = true bind_keys(o.cancel_autoskip_countdown_keybind, "cancel-autoskip-countdown", function() kill_chapterskip_countdown('osd') return end) local autoskip_osd_string = '' local autoskip_graceful_osd = '' if o.autoskip_countdown_graceful then autoskip_graceful_osd = 'Press Keybind to:\n' end if consecutive_i > 1 and o.autoskip_countdown_bulk then local autoskip_osd_string = '' for j=consecutive_i, 1, -1 do local chapter_title = '' if chapters[i-j] then chapter_title = chapters[i-j].title end autoskip_osd_string=(autoskip_osd_string..'\n ▷ Chapter ('..i-j..') '..chapter_title) end prompt_msg(autoskip_graceful_osd..'○ Auto-Skip'..' in "'..o.autoskip_countdown..'"'..autoskip_osd_string, 2000, o.autoskip_osd) g_autoskip_timer = mp.add_periodic_timer(1, function () start_chapterskip_countdown(autoskip_graceful_osd..'○ Auto-Skip'..' in "%countdown%"'..autoskip_osd_string, 2000, o.autoskip_osd) end) else prompt_msg(autoskip_graceful_osd..'▷ Auto-Skip in "'..o.autoskip_countdown..'": Chapter '.. mp.command_native({'expand-text', '${chapter}'}), 2000, o.autoskip_osd) g_autoskip_timer = mp.add_periodic_timer(1, function () start_chapterskip_countdown(autoskip_graceful_osd..'▷ Auto-Skip in "%countdown%": Chapter '.. mp.command_native({'expand-text', '${chapter}'}), 2000, o.autoskip_osd) end) end function proceed_autoskip(force) if not g_autoskip_countdown_flag then kill_chapterskip_countdown() return end if g_autoskip_countdown > 1 and not force then return end if o.autoskip_osd == 'osd-bar' or o.autoskip_osd == 'osd-msg-bar' then prompt_progress('osd-bar', o.osd_duration) end if consecutive_i > 1 and o.autoskip_countdown_bulk then local autoskip_osd_string = '' for j=consecutive_i, 1, -1 do local chapter_title = '' if chapters[i-j] then chapter_title = chapters[i-j].title end autoskip_osd_string=(autoskip_osd_string..'\n ➤ Chapter ('..i-j..') '..chapter_title) end prompt_msg('● Auto-Skip'..autoskip_osd_string, o.osd_duration, o.autoskip_osd) else prompt_msg('➤ Auto-Skip: Chapter '.. mp.command_native({'expand-text', '${chapter}'}), o.osd_duration, o.autoskip_osd) end if consecutive_i > 1 and o.autoskip_countdown_bulk then mp.commandv('no-osd', 'set', 'chapter', i-1) else mp.commandv('no-osd', 'add', 'chapter', 1) end skipped[skip] = true kill_chapterskip_countdown() end bind_keys(o.proceed_autoskip_countdown_keybind, "proceed-autoskip-countdown", function() proceed_autoskip(true) return end) if o.autoskip_countdown_graceful then return end mp.add_timeout(countdown, proceed_autoskip) return end end if skip and countdown <= 0 then if mp.get_property_native("playlist-count") == mp.get_property_native("playlist-pos-1") then return mp.set_property("time-pos", mp.get_property_native("duration")) end mp.commandv("playlist-next") if o.autoskip_osd ~= 'no-osd' or o.autoskip_osd ~= 'osd-bar' then autoskip_playlist_osd = true end elseif skip and countdown > 0 then g_autoskip_countdown_flag = true bind_keys(o.cancel_autoskip_countdown_keybind, "cancel-autoskip-countdown", function() kill_chapterskip_countdown('osd') return end) local autoskip_graceful_osd = '' if o.autoskip_countdown_graceful then autoskip_graceful_osd = 'Press Keybind to:\n' end if consecutive_i > 1 and o.autoskip_countdown_bulk then local i = (mp.get_property_number('chapters')+1 or 0) local autoskip_osd_string = '' for j=consecutive_i, 1, -1 do local chapter_title = '' if chapters[i-j] then chapter_title = chapters[i-j].title end autoskip_osd_string=(autoskip_osd_string..'\n ▷ Chapter ('..i-j..') '..chapter_title) end prompt_msg(autoskip_graceful_osd..'○ Auto-Skip'..' in "'..o.autoskip_countdown..'"'..autoskip_osd_string, 2000, o.autoskip_osd) g_autoskip_timer = mp.add_periodic_timer(1, function () start_chapterskip_countdown(autoskip_graceful_osd..'○ Auto-Skip'..' in "%countdown%"'..autoskip_osd_string, 2000, o.autoskip_osd) end) else prompt_msg(autoskip_graceful_osd..'▷ Auto-Skip in "'..o.autoskip_countdown..'": Chapter '.. mp.command_native({'expand-text', '${chapter}'}), 2000, o.autoskip_osd) g_autoskip_timer = mp.add_periodic_timer(1, function () start_chapterskip_countdown(autoskip_graceful_osd..'▷ Auto-Skip in "%countdown%": Chapter '.. mp.command_native({'expand-text', '${chapter}'}), 2000, o.autoskip_osd) end) end function proceed_autoskip(force) if not g_autoskip_countdown_flag then return end if g_autoskip_countdown > 1 and not force then return end if o.autoskip_osd == 'osd-bar' or o.autoskip_osd == 'osd-msg-bar' then prompt_progress('osd-bar', o.osd_duration) end if consecutive_i > 1 and o.autoskip_countdown_bulk then if mp.get_property_native("playlist-count") == mp.get_property_native("playlist-pos-1") then return mp.set_property("time-pos", mp.get_property_native("duration")) end mp.commandv("playlist-next") else local current_chapter = (mp.get_property_number("chapter") + 1 or 0) local chapters_count = (mp.get_property_number('chapters') or 0) if current_chapter == chapters_count then if mp.get_property_native("playlist-count") == mp.get_property_native("playlist-pos-1") then return mp.set_property("time-pos", mp.get_property_native("duration")) end mp.commandv("playlist-next") else mp.commandv('no-osd', 'add', 'chapter', 1) end end if o.autoskip_osd ~= 'no-osd' or o.autoskip_osd ~= 'osd-bar' then autoskip_playlist_osd = true end kill_chapterskip_countdown() end bind_keys(o.proceed_autoskip_countdown_keybind, "proceed-autoskip-countdown", function() proceed_autoskip(true) return end) if o.autoskip_countdown_graceful then return end mp.add_timeout(countdown, proceed_autoskip) end end function toggle_autoskip() if autoskip_chapter == true then prompt_msg('○ Auto-Skip Disabled', o.osd_duration, o.autoskip_osd) autoskip_chapter = false if g_autoskip_countdown_flag then kill_chapterskip_countdown() end elseif autoskip_chapter == false then prompt_msg('● Auto-Skip Enabled', o.osd_duration, o.autoskip_osd) autoskip_chapter = true end end function toggle_category_autoskip() if chapter_state == 'no-chapters' then return end if not mp.get_property_number("chapter") then return end local chapters = mp.get_property_native("chapter-list") local current_chapter = (mp.get_property_number("chapter") + 1 or 0) local chapter_title = tostring(current_chapter) if current_chapter > 0 and chapters[current_chapter].title and chapters[current_chapter].title ~= '' then chapter_title = chapters[current_chapter].title end local found_i = 0 if matches(current_chapter, chapter_title) then for category in string.gmatch(g_opt_categories, "([^;]+)") do local name, patterns = string.match(category, " *([^+>]*[^+> ]) *[+>](.*)") for pattern in string.gmatch(patterns, "([^/]+)") do if string.match(chapter_title:lower(), pattern:lower()) then g_opt_categories = g_opt_categories:gsub(esc_string(pattern)..'/?', "") found_i = found_i + 1 end end end for category in string.gmatch(g_opt_categories, "([^;]+)") do local name, patterns = string.match(category, " *([^+>]*[^+> ]) *[+>](.*)") if name then categories[name:lower()] = patterns elseif not parsed[category] then mp.msg.warn("Improper category definition: " .. category) end parsed[category] = true end if type(o.categories) == 'table' then for i=1, #o.categories do if o.categories[i] and o.categories[i][1] == chapter_state then o.categories[i][2] = g_opt_categories break end end else o.categories = g_opt_categories end end if current_chapter > 0 and chapters[current_chapter].title and chapters[current_chapter].title ~= '' then if found_i > 0 or string.match(categories.toggle, esc_string(chapter_title)) then prompt_msg('○ Removed from Auto-Skip\n ▷ Chapter: '..chapter_title, o.osd_duration, o.autoskip_osd) categories.toggle = categories.toggle:gsub(esc_string("^"..chapter_title.."/"), "") if g_autoskip_countdown_flag then kill_chapterskip_countdown() end else prompt_msg('● Added to Auto-Skip\n ➔ Chapter: '..chapter_title, o.osd_duration, o.autoskip_osd) categories.toggle = categories.toggle.."^"..chapter_title.."/" end else if found_i > 0 or string.match(categories.toggle_idx, esc_string(chapter_title)) then prompt_msg('○ Removed from Auto-Skip\n ▷ Chapter: '..chapter_title, o.osd_duration, o.autoskip_osd) categories.toggle_idx = categories.toggle_idx:gsub(esc_string(chapter_title.."/"), "") if g_autoskip_countdown_flag then kill_chapterskip_countdown() end else prompt_msg('● Added to Auto-Skip\n ➔ Chapter: '..chapter_title, o.osd_duration, o.autoskip_osd) categories.toggle_idx = categories.toggle_idx..chapter_title.."/" end end end -- HOOKS -------------------------------------------------------------------- if user_input_module then mp.add_hook("on_unload", 50, function () input.cancel_user_input() end) end -- chapters.lua mp.observe_property("chapter", "number", chapterskip) -- chapterskip.lua -- smart skip events / properties / hooks -- mp.register_event('file-loaded', function() file_length = (mp.get_property_native('duration') or 0) g_filepath = (mp.get_property('path') or '') --1.3.4# get filepath needed for exclusion_check function if exclusion_check() then g_autoskip_disabled = true end --1.3.4# disable / enable autoskip based on exclusion_check if o.playlist_osd and g_playlist_pos > 0 then playlist_osd = true end if playlist_osd and not autoskip_playlist_osd then prompt_msg('['..mp.command_native({'expand-text', '${playlist-pos-1}'})..'/'..mp.command_native({'expand-text', '${playlist-count}'})..'] '..mp.command_native({'expand-text', '${filename}'})) end if autoskip_playlist_osd then prompt_msg('➤ Auto-Skip\n['..mp.command_native({'expand-text', '${playlist-pos-1}'})..'/'..mp.command_native({'expand-text', '${playlist-count}'})..'] '..mp.command_native({'expand-text', '${filename}'}), o.osd_duration, o.autoskip_osd) end playlist_osd = false autoskip_playlist_osd = false force_silence_skip = false skipped = {} initial_chapter_count = mp.get_property_number("chapter-list/count") if initial_chapter_count > 0 and chapter_state ~= 'external-chapters' then chapter_state = 'internal-chapters' end prep_chapterskip_var() end) mp.add_hook("on_load", 50, function() if o.external_chapters_autoload then load_chapters() end end) mp.observe_property('pause', 'bool', function(name, value) if value and skip_flag then restoreProp(initial_skip_time, true) end if g_autoskip_countdown_flag then kill_chapterskip_countdown('osd') end end) mp.add_hook('on_unload', 9, function() if geometry_default == "" then mp.set_property("geometry","") end if o.modified_chapters_autosave == true or has_value(o.modified_chapters_autosave, chapter_state) then write_chapters(false) end mp.set_property("keep-open", keep_open_state) chapter_state = 'no-chapters' g_playlist_pos = (mp.get_property_native('playlist-playing-pos')+1 or 0) kill_chapterskip_countdown() end) mp.register_event('seek', function() if g_autoskip_countdown_flag then kill_chapterskip_countdown('osd') end end) mp.observe_property('eof-reached', 'bool', eofHandler) -- BINDINGS -------------------------------------------------------------------- bind_keys(o.toggle_autoskip_keybind, "toggle-autoskip", toggle_autoskip) bind_keys(o.toggle_category_autoskip_keybind, "toggle-category-autoskip", toggle_category_autoskip) bind_keys(o.add_chapter_keybind, "add-chapter", add_chapter) bind_keys(o.remove_chapter_keybind, "remove-chapter", remove_chapter) bind_keys(o.write_chapters_keybind, "write-chapters", function () write_chapters(true) end) bind_keys(o.edit_chapter_keybind, "edit-chapter", edit_chapter) bind_keys(o.bake_chapters_keybind, "bake-chapters", bake_chapters) bind_keys(o.chapter_prev_keybind, "chapter-prev", function() chapterSeek(-1) end) bind_keys(o.chapter_next_keybind, "chapter-next", function() chapterSeek(1) end) bind_keys(o.smart_prev_keybind, "smart-prev", smartPrev) bind_keys(o.smart_next_keybind, "smart-next", smartNext) bind_keys(o.silence_skip_keybind, "silence-skip", silenceSkip)