nirish

config files for niri window manager
Log | Files | Refs | README | LICENSE

SmartSkip.lua (52859B)


      1 -- Copyright (c) 2025, Eisa AlAwadhi
      2 -- License: BSD 2-Clause License
      3 -- Creator: Eisa AlAwadhi
      4 -- Project: SmartSkip
      5 -- Version: 1.3.4
      6 -- Date: 14-May-2025
      7 
      8 -- Related forked projects:
      9 --  https://github.com/detuur/mpv-scripts/blob/master/skiptosilence.lua
     10 --  https://github.com/mar04/chapters_for_mpv
     11 --  https://github.com/po5/chapterskip/blob/master/chapterskip.lua
     12 
     13 local o = {
     14 	-----Silence Skip Settings-----
     15 	silence_audio_level = -40,
     16 	silence_duration = 0.65,
     17 	ignore_silence_duration=5,
     18 	min_skip_duration = 0,
     19 	max_skip_duration = 180,
     20 	keybind_twice_cancel_skip = true,
     21 	silence_skip_to_end = "playlist-next",
     22 	add_chapter_on_skip = true,
     23 	force_mute_on_skip = false,
     24 	-----Smart Skip Settings-----
     25 	last_chapter_skip_behavior=[[ [ ["no-chapters", "silence-skip"], ["internal-chapters", "playlist-next"], ["external-chapters", "silence-skip"] ] ]],
     26 	smart_next_proceed_countdown = true,
     27 	smart_prev_cancel_countdown = true,
     28 	-----Chapters Settings-----
     29     external_chapters_autoload = true,
     30     modified_chapters_autosave=[[ ["no-chapters", "external-chapters"] ]],
     31     global_chapters = true,
     32     global_chapters_path = "/:dir%mpvconf%/chapters",
     33     hash_global_chapters = true,
     34     add_chapter_ask_title = false,
     35 	add_chapter_pause_for_input = false,
     36     add_chapter_placeholder_title = "Chapter ",
     37 	-----Auto-Skip Settings-----
     38     autoskip_chapter = true,
     39 	autoskip_countdown = 3,
     40 	autoskip_countdown_bulk = false,
     41 	autoskip_countdown_graceful = false,
     42     skip_once = false,
     43 	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"] ] ]],
     44 	skip=[[ [ ["internal-chapters", "toggle;toggle_idx;opening;ending;preview"], ["external-chapters", "toggle;toggle_idx"] ] ]],
     45 	invert_autoskip_exclusion = false,
     46 	autoskip_exclusion=[[
     47 	[""]
     48 	]],
     49 	-----OSD Messages Settings-----
     50 	osd_duration = 2500,
     51 	silence_skip_osd = "osd-msg-bar",
     52 	chapter_osd = "osd-msg-bar",
     53 	autoskip_osd = "osd-msg-bar",
     54 	playlist_osd = true,
     55 	osd_msg = true,
     56 	-----Keybind Settings-----
     57 	toggle_autoskip_keybind=[[ ["ctrl+."] ]],
     58 	toggle_category_autoskip_keybind=[[ ["alt+."] ]],
     59 	cancel_autoskip_countdown_keybind=[[ ["esc", "n"] ]],
     60 	proceed_autoskip_countdown_keybind=[[ ["enter", "y"] ]],
     61 	add_chapter_keybind=[[ ["n"] ]],
     62 	remove_chapter_keybind=[[ ["alt+n"] ]],
     63 	edit_chapter_keybind=[[ [""] ]],
     64 	write_chapters_keybind=[[ ["ctrl+n"] ]],
     65 	bake_chapters_keybind=[[ [""] ]],
     66 	chapter_next_keybind=[[ ["ctrl+right"] ]],
     67 	chapter_prev_keybind=[[ ["ctrl+left"] ]],
     68 	smart_next_keybind=[[ [">"] ]],
     69 	smart_prev_keybind=[[ ["<"] ]],
     70 	silence_skip_keybind=[[ ["?"] ]],
     71     cancel_silence_skip_keybind=[[ ["esc"] ]],
     72 }
     73 
     74 local mp = require 'mp'
     75 local msg = require 'mp.msg'
     76 local utils = require 'mp.utils'
     77 local options = require 'mp.options'
     78 options.read_options(o)
     79 
     80 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
     81 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
     82 o.last_chapter_skip_behavior = utils.parse_json(o.last_chapter_skip_behavior)
     83 if utils.parse_json(o.skip) ~= nil then o.skip = utils.parse_json(o.skip) end
     84 if utils.parse_json(o.categories) ~= nil then o.categories = utils.parse_json(o.categories) end
     85 if o.skip_once ~= false and o.skip_once ~= true then o.skip_once = utils.parse_json(o.skip_once) end
     86 
     87 if o.global_chapters_path:match('/:dir%%mpvconf%%') then
     88 	o.global_chapters_path = o.global_chapters_path:gsub('/:dir%%mpvconf%%', mp.find_config_file('.'))
     89 elseif o.global_chapters_path:match('/:dir%%script%%') then
     90 	o.global_chapters_path = o.global_chapters_path:gsub('/:dir%%script%%', debug.getinfo(1).source:match('@?(.*/)'))
     91 elseif o.global_chapters_path:match('/:var%%(.*)%%') then
     92 	local os_variable = o.global_chapters_path:match('/:var%%(.*)%%')
     93 	o.global_chapters_path = o.global_chapters_path:gsub('/:var%%(.*)%%', os.getenv(os_variable))
     94 end
     95 
     96 o.toggle_autoskip_keybind = utils.parse_json(o.toggle_autoskip_keybind)
     97 o.cancel_autoskip_countdown_keybind = utils.parse_json(o.cancel_autoskip_countdown_keybind)
     98 o.proceed_autoskip_countdown_keybind = utils.parse_json(o.proceed_autoskip_countdown_keybind)
     99 o.toggle_category_autoskip_keybind = utils.parse_json(o.toggle_category_autoskip_keybind)
    100 o.add_chapter_keybind = utils.parse_json(o.add_chapter_keybind)
    101 o.remove_chapter_keybind = utils.parse_json(o.remove_chapter_keybind)
    102 o.write_chapters_keybind = utils.parse_json(o.write_chapters_keybind)
    103 o.edit_chapter_keybind = utils.parse_json(o.edit_chapter_keybind)
    104 o.bake_chapters_keybind = utils.parse_json(o.bake_chapters_keybind)
    105 o.chapter_prev_keybind = utils.parse_json(o.chapter_prev_keybind)
    106 o.chapter_next_keybind = utils.parse_json(o.chapter_next_keybind)
    107 o.smart_prev_keybind = utils.parse_json(o.smart_prev_keybind)
    108 o.smart_next_keybind = utils.parse_json(o.smart_next_keybind)
    109 o.silence_skip_keybind = utils.parse_json(o.silence_skip_keybind)
    110 o.cancel_silence_skip_keybind = utils.parse_json(o.cancel_silence_skip_keybind)
    111 o.autoskip_exclusion = utils.parse_json(o.autoskip_exclusion) --1.3.4# option autoskip_exclusion parsing
    112 
    113 package.path = mp.command_native({"expand-path", "~~/script-modules/?.lua;"}) .. package.path
    114 local user_input_module, input = pcall(require, "user-input-module")
    115 
    116 mp.set_property('user-data/smartskip/silence-skip', 'no')
    117 
    118 if o.osd_duration == -1 then o.osd_duration = (mp.get_property_number('osd-duration') or 1000) end
    119 local speed_state = 1
    120 local pause_state = false
    121 local mute_state = false
    122 local sub_state = nil
    123 local secondary_sub_state = nil
    124 local vid_state = nil
    125 local skip_flag = false
    126 local window_state = nil
    127 local force_silence_skip = false
    128 local initial_skip_time = 0
    129 local initial_chapter_count = 0
    130 local chapter_state = 'no-chapters'
    131 local file_length = 0
    132 local keep_open_state = "yes"
    133 if mp.get_property("config") ~= "no" then keep_open_state = mp.get_property("keep-open") end
    134 local osd_duration_default = (mp.get_property_number('osd-duration') or 1000)
    135 local geometry_default = mp.get_property_native("geometry")
    136 local autoskip_chapter = o.autoskip_chapter
    137 local playlist_osd = false
    138 local autoskip_playlist_osd = false
    139 local g_playlist_pos = 0 
    140 local g_opt_categories = o.categories
    141 local g_opt_skip_once = false
    142 o.autoskip_countdown = math.floor(o.autoskip_countdown)
    143 local g_autoskip_countdown = o.autoskip_countdown
    144 local g_autoskip_countdown_flag = false
    145 local g_protocols = {'https?:', 'magnet:', 'rtmps?:', 'smb:', 'ftps?:', 'sftp:'} --1.3.4# default global protocols needed to check for protocols / website exclusion
    146 local g_filepath = '' --1.3.4# define initial global variable for filepath
    147 local g_autoskip_disabled = false --1.3.4# set global variable for autoskip disabled to be used for autoskip exclusion_check function
    148 local categories = {
    149 	toggle = "",
    150 	toggle_idx = "",
    151 }
    152 
    153 -- utility functions --
    154 function has_value(tab, val, array2d)
    155 	if not tab then return msg.error('check value passed') end
    156 	if not val then return msg.error('check value passed') end
    157 	if not array2d then
    158 		for index, value in ipairs(tab) do
    159 			if string.lower(value) == string.lower(val) then
    160 				return true
    161 			end
    162 		end
    163 	end
    164 	if array2d then
    165 		for i=1, #tab do
    166 			if tab[i] and string.lower(tab[i][array2d]) == string.lower(val) then
    167 				return true
    168 			end
    169 		end
    170 	end
    171 	
    172 	return false
    173 end
    174 
    175 function esc_string(str)
    176 	return str:gsub("([%p])", "%%%1")
    177 end
    178 
    179 function esc_lua_pattern(str) --1.3.4# helper function to escape pattern needed for exclusion_check function
    180     return str:gsub("([%^%$%(%)%%%.%[%]%+%-%?])", "%%%1")
    181 end
    182 
    183 function starts_protocol(tab, val) --1.3.4# helper function to check if file is protocol needed for exclusion_check function
    184 	for index, value in ipairs(tab) do
    185 		if (val:find(value) == 1) then
    186 			return true
    187 		end
    188 	end
    189 	return false
    190 end
    191 
    192 function prompt_msg(text, duration, osd)
    193 	if not text then return end
    194 	msg.info(text)
    195 
    196 	if not o.osd_msg then return end
    197 	if osd == "no-osd" or osd == "osd-bar" then return end
    198 	
    199 	if not duration then duration = o.osd_duration end
    200 	mp.commandv("show-text", text, duration) 
    201 end
    202 
    203 function prompt_progress(osd, duration)
    204 	if not o.osd_msg or not osd then return end
    205 	if osd == "no-osd" then return end
    206 
    207 	if not duration then duration = o.osd_duration end
    208 	mp.set_property("osd-duration", o.osd_duration)
    209 	mp.commandv(osd, "show-progress")
    210 	mp.add_timeout(0.07, function () mp.set_property('osd-duration', osd_duration_default) end)
    211 end
    212 
    213 function bind_keys(keys, name, func, opts)
    214 	if not keys then
    215 		mp.add_forced_key_binding(keys, name, func, opts)
    216 		return
    217 	end
    218 	
    219 	for i = 1, #keys do
    220 		if i == 1 then 
    221 			mp.add_forced_key_binding(keys[i], name, func, opts)
    222 		else
    223 			mp.add_forced_key_binding(keys[i], name .. i, func, opts)
    224 		end
    225 	end
    226 end
    227 
    228 function unbind_keys(keys, name)
    229 	if not keys then
    230 		mp.remove_key_binding(name)
    231 		return
    232 	end
    233 	
    234 	for i = 1, #keys do
    235 		if i == 1 then
    236 			mp.remove_key_binding(name)
    237 		else
    238 			mp.remove_key_binding(name .. i)
    239 		end
    240 	end
    241 end
    242 
    243 function exclusion_check()
    244 	if g_filepath == "" then return false end
    245 	if o.autoskip_exclusion == nil then msg.warn('ignoring autoskip_exclusion option due to an inappropriate value') return false end
    246 	if not o.autoskip_exclusion[1] or #o.autoskip_exclusion == 1 and o.autoskip_exclusion[1] == "" then return false end
    247 
    248 	local invertable_return = {true, false}
    249 	local exclusion_msg = 'Auto-Skip disabled because of exclusion'
    250 	if o.invert_autoskip_exclusion then 
    251 		invertable_return = {false, true} 
    252 		exclusion_msg = 'Auto-Skip enabled because of inclusion'
    253 	end
    254 
    255 	if has_value(o.autoskip_exclusion, g_filepath, nil) then
    256 		msg.info(exclusion_msg)
    257 		return invertable_return[1]
    258 	elseif not starts_protocol(g_protocols, g_filepath) then
    259 		if has_value(o.autoskip_exclusion, g_filepath:match('^(.-)([^\\/]-)%.([^\\/%.]-)%.?$'), nil) or 
    260 		has_value(o.autoskip_exclusion, g_filepath:match('^(.-)([^\\/]-)%.([^\\/%.]-)%.?$'):gsub('\\$', ''), nil) then
    261 			msg.info(exclusion_msg)
    262 			return invertable_return[1]
    263 		elseif has_value(o.autoskip_exclusion, g_filepath:match('%.([^%.]+)$'), nil) or
    264 		has_value(o.autoskip_exclusion, "."..g_filepath:match('%.([^%.]+)$'), nil) then
    265 			msg.info(exclusion_msg)
    266 			return invertable_return[1]
    267 		else
    268 			for i=1, #o.autoskip_exclusion do
    269 				local esc_autoskip_exclusion = esc_lua_pattern(o.autoskip_exclusion[i])
    270 				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
    271 					msg.info(exclusion_msg)
    272 					return invertable_return[1]
    273 				end
    274 			end
    275 		end
    276 	elseif starts_protocol(g_protocols, g_filepath) then
    277 		if has_value(o.autoskip_exclusion, g_filepath:match('(.-)(:)'), nil) or
    278 		has_value(o.autoskip_exclusion, g_filepath:match('(.-:)'), nil) or
    279 		has_value(o.autoskip_exclusion, g_filepath:match('(.-:/?/?)'), nil) then
    280 			msg.info(exclusion_msg)
    281 			return invertable_return[1]
    282 		elseif g_filepath:find('https?://') == 1 then
    283 			local difchk_1, difchk_2 = g_filepath:match("(https?://)w?w?w?%.?([%w%.%:]*)")
    284 			local different_check_temp = difchk_1..difchk_2
    285 			local different_checks = {different_check_temp, g_filepath:match("https?://w?w?w?%.?([%w%.%:]*)"), g_filepath:match("https?://([%w%.%:]*)"), g_filepath:match("(https?://[%w%.%:]*)") }
    286 			for i = 1, #different_checks do
    287 				if different_checks[i] and has_value(o.autoskip_exclusion, different_checks[i], nil)
    288 				or different_checks[i]..'/' and has_value(o.autoskip_exclusion, different_checks[i]..'/', nil) then
    289 					msg.info(exclusion_msg)
    290 					return invertable_return[1]
    291 				end
    292 			end
    293 		end
    294 	end
    295 	
    296 	return invertable_return[2]
    297 end
    298 
    299 -- skip-silence utility functions --
    300 function restoreProp(timepos,pause)
    301 	if not timepos then timepos = mp.get_property_number("time-pos") end
    302 	if not pause then pause = pause_state end
    303 	
    304 	mp.set_property("vid", vid_state)
    305 	mp.set_property("force-window", window_state)
    306 	mp.set_property_bool("mute", mute_state)
    307 	mp.set_property("speed", speed_state)
    308 	mp.unobserve_property(foundSilence)
    309 	mp.command("no-osd af remove @skiptosilence")
    310 	mp.set_property_bool("pause", pause)
    311 	mp.set_property_number("time-pos", timepos)
    312 	mp.set_property("sub-visibility", sub_state)
    313 	mp.set_property("secondary-sub-visibility", secondary_sub_state)
    314     unbind_keys(o.cancel_silence_skip_keybind, 'cancel-silence-skip')
    315     mp.set_property('user-data/smartskip/silence-skip', 'no')
    316 	timer:kill()
    317 	skip_flag = false
    318 end
    319 
    320 function handleMinMaxDuration(timepos)
    321 		if not skip_flag then return end
    322 		if not timepos then timepos = mp.get_property_number("time-pos") end
    323 		
    324 		skip_duration = timepos - initial_skip_time
    325 		if o.min_skip_duration > 0 and skip_duration <= o.min_skip_duration then
    326 			restoreProp(initial_skip_time)
    327 			prompt_msg('Skipping Cancelled\nSilence less than minimum', o.osd_duration, o.silence_skip_osd)
    328 			return true
    329 		end
    330 		if o.max_skip_duration > 0 and skip_duration >= o.max_skip_duration then
    331 			restoreProp(initial_skip_time)
    332 			prompt_msg('Skipping Cancelled\nSilence is more than configured maximum', o.osd_duration, o.silence_skip_osd)
    333 			return true
    334 		end
    335 		return false
    336 end
    337 
    338 function setKeepOpenState()
    339 	if o.silence_skip_to_end == "playlist-next" then
    340 		mp.set_property("keep-open", "yes")
    341 	else
    342 		mp.set_property("keep-open", "always")
    343 	end
    344 end
    345 
    346 function eofHandler(name, val)
    347     if val and skip_flag then
    348 		if o.silence_skip_to_end == 'playlist-next' then
    349 			restoreProp((mp.get_property_native('duration') or 0))
    350 			if mp.get_property_native('playlist-playing-pos')+1 == mp.get_property_native('playlist-count') then
    351 				prompt_msg('Skipped to end at ' .. mp.get_property_osd('duration'), o.osd_duration, o.silence_skip_osd)
    352 			else
    353 				mp.commandv("playlist-next")
    354 			end
    355 		elseif o.silence_skip_to_end == 'cancel' then	
    356 			prompt_msg('Skipping Cancelled\nSilence not detected', o.osd_duration, o.silence_skip_osd)
    357 			restoreProp(initial_skip_time)
    358 		elseif o.silence_skip_to_end == 'pause' then
    359 			prompt_msg('Skipped to end at ' .. mp.get_property_osd('duration'), o.osd_duration, o.silence_skip_osd)
    360 			restoreProp((mp.get_property_native('duration') or 0), true)
    361 		end
    362 	end
    363 end
    364 
    365 -- smart-skip main code --
    366 function smartNext()
    367 	if g_autoskip_countdown_flag and o.smart_next_proceed_countdown then proceed_autoskip(true) return end
    368 	local next_action = "silence-skip"
    369 	local chapters_count = (mp.get_property_number('chapters') or 0)
    370     local chapter  = (mp.get_property_number('chapter') or 0)
    371 	local current_playlist = (mp.get_property_native('playlist-playing-pos')+1 or 0)
    372 	local total_playlist = (mp.get_property_native('playlist-count') or 0)
    373 	
    374 	if chapter+2 <= chapters_count then
    375 		next_action = 'chapter-next'
    376 	elseif chapter+2 > chapters_count and (initial_chapter_count == 0 or chapters_count == 0 or force_silence_skip) then
    377 		if chapters_count == 0 then force_silence_skip = true end
    378 		next_action = 'silence-skip'
    379 	elseif chapter+1 >= chapters_count then
    380 		for i = 1, #o.last_chapter_skip_behavior do
    381 			if o.last_chapter_skip_behavior[i] and o.last_chapter_skip_behavior[i][1] == chapter_state then
    382 				next_action = o.last_chapter_skip_behavior[i][2]
    383 				break
    384 			end
    385 		end
    386 	end
    387 	
    388 	if next_action == 'playlist-next' and current_playlist == total_playlist then
    389 		next_action = 'chapter-next'
    390 	end
    391 	
    392 	if next_action == 'silence-skip' then
    393 		silenceSkip()
    394 	end
    395 	if next_action == 'chapter-next' then
    396 		mp.set_property('osd-duration', o.osd_duration)
    397 		mp.commandv(o.chapter_osd, 'add', 'chapter', 1)
    398 		mp.add_timeout(0.07, function () mp.set_property('osd-duration', osd_duration_default) end)
    399 	end
    400 	if next_action == 'playlist-next' then
    401 		mp.command('playlist_next')
    402 	end
    403 end
    404 
    405 function smartPrev()
    406 	if skip_flag then restoreProp(initial_skip_time) return end
    407 	if g_autoskip_countdown_flag and o.smart_prev_cancel_countdown then kill_chapterskip_countdown('osd') return end
    408 	local chapters_count = (mp.get_property_number('chapters') or 0)
    409     local chapter  = (mp.get_property_number('chapter') or 0)
    410 	local timepos = (mp.get_property_native("time-pos") or 0)
    411 
    412 	if chapter-1 < 0 and timepos > 1 and chapters_count == 0 then
    413 		mp.commandv('seek', 0, 'absolute', 'exact')
    414 		prompt_progress(o.chapter_osd, o.osd_duration)
    415 	elseif chapter-1 < 0 and timepos < 1 then
    416         mp.command('playlist_prev')
    417     elseif chapter-1 <= chapters_count then
    418 		mp.set_property('osd-duration', o.osd_duration)
    419         mp.commandv(o.chapter_osd, 'add', 'chapter', -1)
    420 		mp.add_timeout(0.07, function () mp.set_property('osd-duration', osd_duration_default) end)
    421     end
    422 end
    423 
    424 -- chapter-next/prev main code --
    425 function chapterSeek(direction)
    426 	if skip_flag and direction == -1 then restoreProp(initial_skip_time) return end
    427 
    428 	local chapters_count = (mp.get_property_number('chapters') or 0)
    429     local chapter  = (mp.get_property_number('chapter') or 0)
    430 	local timepos = (mp.get_property_native("time-pos") or 0)
    431 
    432     if chapter+direction < 0 and timepos > 1 and chapters_count == 0 then
    433 		mp.commandv('seek', 0, 'absolute', 'exact')
    434 		prompt_progress(o.chapter_osd, o.osd_duration)
    435 	elseif chapter+direction < 0 and timepos < 1 then
    436 	    mp.command('playlist_prev')
    437     elseif chapter+direction >= chapters_count then
    438 		mp.command('playlist_next')
    439     else
    440         mp.set_property('osd-duration', o.osd_duration)
    441         mp.commandv(o.chapter_osd, 'add', 'chapter', direction)
    442 		mp.add_timeout(0.07, function () mp.set_property('osd-duration', osd_duration_default) end)
    443     end
    444 end
    445 
    446 -- silence skip main code --
    447 function silenceSkip(action)
    448     bind_keys(o.cancel_silence_skip_keybind, "cancel-silence-skip", function() restoreProp(initial_skip_time) return end)
    449     if skip_flag then if o.keybind_twice_cancel_skip then restoreProp(initial_skip_time) end return end
    450 	initial_skip_time = (mp.get_property_native("time-pos") or 0)
    451 	if math.floor(initial_skip_time) == math.floor(mp.get_property_native('duration') or 0) then return end
    452     mp.set_property('user-data/smartskip/silence-skip', 'yes')
    453 	kill_chapterskip_countdown()
    454 
    455 	local width = mp.get_property_native("osd-width")
    456 	local height = mp.get_property_native("osd-height")
    457     local window_maximized = mp.get_property_native("window-maximized")
    458 
    459 	window_state = mp.get_property("force-window")
    460 	vid_state = mp.get_property("vid")
    461 	sub_state = mp.get_property("sub-visibility")
    462 	secondary_sub_state = mp.get_property("secondary-sub-visibility")
    463 	pause_state = mp.get_property_native("pause")
    464 	speed_state = mp.get_property_native("speed")
    465 	
    466     if not window_maximized then mp.set_property_native("geometry", ("%dx%d"):format(width, height)) end
    467 	
    468 	if o.silence_skip_osd ~= 'no-osd' then mp.commandv(o.silence_skip_osd, "show-progress") end
    469 	
    470 	mp.command(
    471 		"no-osd af add @skiptosilence:lavfi=[silencedetect=noise=" ..
    472 		o.silence_audio_level .. "dB:d=" .. o.silence_duration .. "]"
    473 	)
    474 
    475 	mp.observe_property("af-metadata/skiptosilence", "string", foundSilence)
    476 	mp.set_property("sub-visibility", "no")
    477 	mp.set_property("secondary-sub-visibility", "no")
    478 	mp.set_property("force-window", "yes")
    479 	mp.set_property("vid", "no")
    480 	mute_state = mp.get_property_native("mute")
    481     if o.force_mute_on_skip then
    482         mp.set_property_bool("mute", true)
    483     end
    484 	mp.set_property_bool("pause", false)
    485 	mp.set_property("speed", 100)
    486 	setKeepOpenState()
    487 	skip_flag = true
    488 	
    489 	timer = mp.add_periodic_timer(0.5, function()
    490 		local video_time = (mp.get_property_native("time-pos") or 0)
    491 		handleMinMaxDuration(video_time)
    492 		if skip_flag and o.silence_skip_osd ~= 'no-osd' then mp.commandv(o.silence_skip_osd, "show-progress") end
    493 	end)
    494 end
    495 
    496 function foundSilence(name, value)
    497 	if value == "{}" or value == nil then
    498 		return
    499 	end
    500 	
    501 	timecode = tonumber(string.match(value, "%d+%.?%d+"))
    502 	if timecode == nil or timecode < initial_skip_time + o.ignore_silence_duration then
    503 		return
    504 	end
    505 	
    506 	if handleMinMaxDuration(timecode) then return end
    507 	
    508 	restoreProp(timecode)
    509 
    510 	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)
    511 	if o.add_chapter_on_skip == true or has_value(o.add_chapter_on_skip, chapter_state) then
    512 		mp.add_timeout(0.05, add_chapter)
    513 	end
    514 	skip_flag = false
    515 end
    516 
    517 -- modified fork of chapters_for_mpv --
    518 --[[
    519 Copyright (c) 2023 Mariusz Libera <[email protected]>
    520 
    521 Permission is hereby granted, free of charge, to any person obtaining a copy
    522 of this software and associated documentation files (the "Software"), to deal
    523 in the Software without restriction, including without limitation the rights
    524 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    525 copies of the Software, and to permit persons to whom the Software is
    526 furnished to do so, subject to the following conditions:
    527 
    528 The above copyright notice and this permission notice shall be included in all
    529 copies or substantial portions of the Software.
    530 
    531 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    532 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    533 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    534 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    535 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    536 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    537 SOFTWARE.
    538 --]]
    539 
    540 
    541 -- to debug run mpv with arg: --msg-level=SmartSkip=debug
    542 -- to test o.run mpv with arg: --script-opts=SmartSkip-OPTION=VALUE
    543 
    544 
    545 
    546 local chapters_modified = false
    547 
    548 msg.debug("options:", utils.to_string(options))
    549 
    550 
    551 -- CHAPTER MANIPULATION --------------------------------------------------------
    552 
    553 
    554 function change_title_callback(user_input, err, chapter_index)
    555     if user_input == nil or err ~= nil then
    556         msg.warn("no chapter title provided:", err)
    557         return
    558     end
    559 
    560     local chapter_list = mp.get_property_native("chapter-list")
    561 
    562     if chapter_index > mp.get_property_number("chapter-list/count") then
    563         msg.warn("can't set chapter title")
    564         return
    565     end
    566 
    567     chapter_list[chapter_index].title = user_input
    568 
    569     mp.set_property_native("chapter-list", chapter_list)
    570     chapters_modified = true
    571 end
    572 
    573 
    574 function edit_chapter()
    575     local mpv_chapter_index = mp.get_property_number("chapter")
    576     local chapter_list = mp.get_property_native("chapter-list")
    577 
    578     if mpv_chapter_index == nil or mpv_chapter_index == -1 then
    579         msg.verbose("no chapter selected, nothing to edit")
    580         return
    581     end
    582 
    583     if not user_input_module then
    584         msg.error("no mpv-user-input, can't get user input, install: https://github.com/CogentRedTester/mpv-user-input")
    585         return
    586     end
    587     input.get_user_input(change_title_callback, {
    588         request_text = "title of the chapter:",
    589         default_input = chapter_list[mpv_chapter_index + 1].title,
    590         cursor_pos = #(chapter_list[mpv_chapter_index + 1].title) + 1,
    591     }, mpv_chapter_index + 1)
    592 
    593     if o.add_chapter_pause_for_input then
    594         mp.set_property_bool("pause", true)
    595         mp.osd_message(" ", 0.1)
    596     end
    597 end
    598 
    599 
    600 function add_chapter(timepos)
    601 	if not timepos then timepos = mp.get_property_number("time-pos") end
    602     local chapter_list = mp.get_property_native("chapter-list")
    603 	
    604 	if #chapter_list > 0 then
    605 		for i = 1, #chapter_list do
    606 			if math.floor(chapter_list[i].time) == math.floor(timepos) then
    607 				msg.debug("failed to add chapter, chapter exists in same position")
    608 				return
    609 			end
    610 		end
    611 	end
    612 	
    613     local chapter_index = (mp.get_property_number("chapter") or -1) + 2
    614 
    615     table.insert(chapter_list, chapter_index, {title = "", time = timepos})
    616 
    617     msg.debug("inserting new chapter at ", chapter_index, " chapter_", " time: ", timepos)
    618 
    619     mp.set_property_native("chapter-list", chapter_list)
    620     chapters_modified = true
    621 
    622     if o.add_chapter_ask_title then
    623         if not user_input_module then
    624             msg.error("no mpv-user-input, can't get user input, install: https://github.com/CogentRedTester/mpv-user-input")
    625             return
    626         end
    627         -- ask user for chapter title
    628         input.get_user_input(change_title_callback, {
    629             request_text = "title of the chapter:",
    630             default_input = o.placeholder_title .. chapter_index,
    631             cursor_pos = #(o.placeholder_title .. chapter_index) + 1,
    632         }, chapter_index)
    633 
    634         if o.add_chapter_pause_for_input then
    635             mp.set_property_bool("pause", true)
    636             -- FIXME: for whatever reason osd gets hidden when we pause the
    637             -- playback like that, workaround to make input prompt appear
    638             -- right away without requiring mouse or keyboard action
    639             mp.osd_message(" ", 0.1)
    640         end
    641     end
    642 end
    643 
    644 
    645 function remove_chapter()
    646     local chapter_count = mp.get_property_number("chapter-list/count")
    647 
    648     if chapter_count < 1 then
    649         msg.verbose("no chapters to remove")
    650         return
    651     end
    652 
    653     local chapter_list = mp.get_property_native("chapter-list")
    654     local current_chapter = mp.get_property_number("chapter") + 1
    655 
    656     table.remove(chapter_list, current_chapter)
    657     msg.debug("removing chapter", current_chapter)
    658 
    659     mp.set_property_native("chapter-list", chapter_list)
    660     chapters_modified = true
    661 end
    662 
    663 
    664 -- UTILITY FUNCTIONS -----------------------------------------------------------
    665 
    666 
    667 function detect_os()
    668     if package.config:sub(1,1) == "/" then
    669         return "unix"
    670     else
    671         return "windows"
    672     end
    673 end
    674 
    675 
    676 -- for unix use only
    677 -- returns a table of command path and varargs, or nil if command was not found
    678 function command_exists(command, ...)
    679     msg.debug("looking for command:", command)
    680     -- msg.debug("args:", )
    681     local process = mp.command_native({
    682         name = "subprocess",
    683         capture_stdout = true,
    684         capture_stderr = true,
    685         playback_only = false,
    686         args = {"sh", "-c", "command -v -- " .. command}
    687     })
    688 
    689     if process.status == 0 then
    690         local command_path = process.stdout:gsub("\n", "")
    691         msg.debug("command found:", command_path)
    692         return {command_path, ...}
    693     else
    694         msg.debug("command not found:", command)
    695         return nil
    696     end
    697 end
    698 
    699 function mkdir(path)
    700     local args = nil
    701 
    702     if detect_os() == "unix" then
    703         args = {"mkdir", "-p", "--", path}
    704     else
    705         args = {"powershell", "-NoProfile", "-Command", "mkdir", path}
    706     end
    707 
    708     local process = mp.command_native({
    709         name = 'subprocess',
    710         playback_only = false,
    711         capture_stdout = true,
    712         capture_stderr = true,
    713         args = args,
    714     })
    715 
    716     if process.status == 0 then
    717         msg.debug("mkdir success:", path)
    718         return true
    719     else
    720         msg.error("mkdir failure:", path)
    721         return false
    722     end
    723 end
    724 
    725 
    726 -- returns md5 hash of the full path of the current media file
    727 function hash()
    728     local path = mp.get_property("path")
    729     if path == nil then
    730         msg.debug("something is wrong with the path, can't get full_path, can't hash it")
    731         return
    732     end
    733 
    734     msg.debug("hashing:", path)
    735 
    736     local cmd = {
    737         name = 'subprocess',
    738         capture_stdout = true,
    739         playback_only = false,
    740     }
    741     local args = nil
    742 
    743     if detect_os() == "unix" then
    744         local md5 = command_exists("md5sum") or command_exists("md5") or command_exists("openssl", "md5 | cut -d ' ' -f 2")
    745         if md5 == nil then
    746             msg.warn("no md5 command found, can't generate hash")
    747             return
    748         end
    749         md5 = table.concat(md5, " ")
    750         cmd["stdin_data"] = path
    751         args = {"sh", "-c", md5 .. " | cut -d ' ' -f 1 | tr '[:lower:]' '[:upper:]'" }
    752     else --windows
    753         -- https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/get-filehash?view=powershell-7.3
    754         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"
    755         args = {"powershell", "-NoProfile", "-Command", hash_command}
    756     end
    757     cmd["args"] = args
    758     msg.debug("hash cmd:", utils.to_string(cmd))
    759     local process = mp.command_native(cmd)
    760 
    761     if process.status == 0 then
    762         local hash = process.stdout:gsub("%s+", "")
    763         msg.debug("hash:", hash)
    764         return hash
    765     else
    766         msg.warn("hash function failed")
    767         return
    768     end
    769 end
    770 
    771 function construct_ffmetadata()
    772 	local path = mp.get_property("path")
    773     if path == nil then
    774         msg.debug("something is wrong with the path, can't get full_path")
    775         return
    776     end
    777 	
    778     local chapter_count = mp.get_property_number("chapter-list/count")
    779     local all_chapters = mp.get_property_native("chapter-list")
    780 
    781     local ffmetadata = ";FFMETADATA1\n;file=" .. path
    782 
    783     for i, c in ipairs(all_chapters) do
    784         local c_title = c.title
    785         local c_start = c.time * 1000000000
    786         local c_end
    787 
    788         if i < chapter_count then
    789             c_end = all_chapters[i+1].time * 1000000000
    790         else
    791             c_end = (mp.get_property_number("duration") or c.time) * 1000000000
    792         end
    793 
    794         msg.debug(i, "c_title", c_title, "c_start:", c_start, "c_end", c_end)
    795 
    796         ffmetadata = ffmetadata .. "\n[CHAPTER]\nSTART=" .. c_start .. "\nEND=" .. c_end .. "\ntitle=" .. c_title
    797     end
    798 
    799     return ffmetadata
    800 end
    801 
    802 
    803 -- FILE IO ---------------------------------------------------------------------
    804 
    805 
    806 -- args:
    807 --      osd - if true, display an osd message
    808 --      force -- if true write chapters file even if there are no changes
    809 -- on success returns path of the chapters file, nil on failure
    810 function write_chapters(...)
    811 	local chapters_count = (mp.get_property_number('chapters') or 0)
    812 	local osd, force = ...
    813     if not force and not chapters_modified then
    814         msg.debug("nothing to write")
    815         return
    816     end
    817 	if initial_chapter_count == 0 and chapters_count == 0 then return end
    818 
    819 
    820     -- figure out the directory
    821     local chapters_dir
    822     if o.global_chapters then
    823         local dir = utils.file_info(o.global_chapters_path)
    824         if dir then
    825             if dir.is_dir then
    826                 msg.debug("o.global_chapters_path exists:", o.global_chapters_path)
    827                 chapters_dir = o.global_chapters_path
    828             else
    829                 msg.error("o.global_chapters_path is not a directory")
    830                 return
    831             end
    832         else
    833             msg.verbose("o.global_chapters_path doesn't exists:", o.global_chapters_path)
    834             if mkdir(o.global_chapters_path) then
    835                 chapters_dir = o.global_chapters_path
    836             else
    837                 return
    838             end
    839         end
    840     else
    841         chapters_dir = utils.split_path(mp.get_property("path"))
    842     end
    843 
    844     -- and the name
    845     local name = mp.get_property("filename")
    846     if o.hash_global_chapters and o.global_chapters then
    847         name = hash()
    848         if name == nil then
    849             msg.warn("hash function failed, fallback to filename")
    850             name = mp.get_property("filename")
    851         end
    852     end
    853 
    854     local chapters_file_path = utils.join_path(chapters_dir, name .. ".ffmetadata")
    855 	--1.09#HERE I SHOULD ADD SOME SORT OF DELETE FUNCTION IN CASE CHAPTER COUNT IS 0
    856     msg.debug("opening for writing:", chapters_file_path)
    857     local chapters_file = io.open(chapters_file_path, "w")
    858     if chapters_file == nil then
    859         msg.error("could not open chapter file for writing")
    860         return
    861     end
    862 
    863     local success, error = chapters_file:write(construct_ffmetadata())
    864     chapters_file:close()
    865 
    866     if success then
    867         if osd then
    868             prompt_msg('Chapters written to:' .. chapters_file_path)
    869         end
    870         return chapters_file_path
    871     else
    872         msg.error("error writing chapters file:", error)
    873         return
    874     end
    875 end
    876 
    877 function load_chapters()
    878     local path = mp.get_property("path")
    879     local expected_chapters_file = utils.join_path(utils.split_path(path), mp.get_property("filename") .. ".ffmetadata")
    880 
    881     msg.debug("looking for:", expected_chapters_file)
    882 
    883     local file = utils.file_info(expected_chapters_file)
    884 	
    885     if file then
    886         msg.debug("found in the local directory, loading..")
    887         mp.set_property("file-local-options/chapters-file", expected_chapters_file)
    888 		chapter_state = 'external-chapters'
    889         return
    890     end
    891 
    892     if not o.global_chapters then
    893         msg.debug("not in local, global chapters not enabled, aborting search")
    894         return
    895     end
    896 
    897     msg.debug("looking in the global directory")
    898 
    899     if o.hash_global_chapters then
    900         local hashed_path = hash()
    901         if hashed_path then
    902             expected_chapters_file = utils.join_path(o.global_chapters_path, hashed_path .. ".ffmetadata")
    903         else
    904             msg.debug("hash function failed, fallback to path")
    905             expected_chapters_file = utils.join_path(o.global_chapters_path, mp.get_property("filename") .. ".ffmetadata")
    906         end
    907     else
    908         expected_chapters_file = utils.join_path(o.global_chapters_path, mp.get_property("filename") .. ".ffmetadata")
    909     end
    910 
    911     msg.debug("looking for:", expected_chapters_file)
    912 
    913     file = utils.file_info(expected_chapters_file)
    914 
    915     if file then
    916         msg.debug("found in the global directory, loading..")
    917         mp.set_property("file-local-options/chapters-file", expected_chapters_file)
    918 		chapter_state = 'external-chapters'
    919         return
    920     end
    921 
    922     msg.debug("chapters file not found")
    923 end
    924 
    925 
    926 function bake_chapters()
    927     if mp.get_property_number("chapter-list/count") == 0 then
    928         msg.verbose("no chapters present")
    929         return
    930     end
    931 
    932     local chapters_file_path = write_chapters(false, true)
    933     if not chapters_file_path then
    934         msg.error("no chapters file")
    935         return
    936     end
    937 
    938     local filename = mp.get_property("filename")
    939     local output_name
    940 
    941     -- extract file extension
    942     local reverse_dot_index = filename:reverse():find(".", 1, true)
    943     if reverse_dot_index == nil then
    944         msg.warning("file has no extension, fallback to .mkv")
    945         output_name = filename .. ".chapters.mkv"
    946     else
    947         local dot_index = #filename + 1 - reverse_dot_index
    948         local ext = filename:sub(dot_index + 1)
    949         msg.debug("ext:", ext)
    950         if ext ~= "mkv" and ext ~= "mp4" and ext ~= "webm" then
    951             msg.debug("fallback to .mkv")
    952             ext = "mkv"
    953         end
    954         output_name = filename:sub(1, dot_index) .. "chapters." .. ext
    955     end
    956 
    957     local file_path = mp.get_property("path")
    958     local output_path = utils.join_path(utils.split_path(file_path), output_name)
    959 
    960     local args = {"ffmpeg", "-y", "-i", file_path, "-i", chapters_file_path, "-map_metadata", "1", "-codec", "copy", output_path}
    961 
    962     msg.debug("args:", utils.to_string(args))
    963 
    964     local process = mp.command_native({
    965         name = 'subprocess',
    966         playback_only = false,
    967         capture_stdout = true,
    968         capture_stderr = true,
    969         args = args
    970     })
    971 
    972     if process.status == 0 then
    973 		prompt_msg('file written to ' .. output_path)
    974     else
    975         msg.error("failed to write file:\n", process.stderr)
    976     end
    977 end
    978 
    979 --modified fork of chapterskip.lua--
    980 
    981 function matches(i, title)
    982 	local opt_skip = o.skip
    983 	if type(o.skip) == 'table' then
    984 		for i=1, #o.skip do
    985 			if o.skip[i] and o.skip[i][1] == chapter_state then
    986 				opt_skip = o.skip[i][2]
    987 				break
    988 			end
    989 		end
    990 	end
    991 
    992     for category in string.gmatch(opt_skip, " *([^;]*[^; ]) *") do
    993 		if categories[category:lower()] then
    994             if category:lower() == "idx-" or category:lower() == "toggle_idx" then
    995 			   for pattern in string.gmatch(categories[category:lower()], "([^/]+)") do
    996                     if tonumber(pattern) == i then
    997                         return true
    998                     end
    999                 end
   1000 			else
   1001                 if title then
   1002                     for pattern in string.gmatch(categories[category:lower()], "([^/]+)") do
   1003                         if string.match(title, pattern) then
   1004                             return true
   1005                         end
   1006                     end
   1007                 end
   1008             end
   1009 		end
   1010     end
   1011 end
   1012 
   1013 local skipped = {}
   1014 local parsed = {}
   1015 
   1016 function prep_chapterskip_var()
   1017 	if chapter_state == 'no-chapters' then return end
   1018 	g_opt_categories = o.categories
   1019 	
   1020 	g_opt_skip_once = false
   1021 	if o.skip_once == true or o.skip_once == false then
   1022 		g_opt_skip_once = o.skip_once
   1023 	elseif has_value(o.skip_once, chapter_state) then
   1024 		g_opt_skip_once = true;
   1025 	end
   1026 	
   1027 	if type(o.categories) == 'table' then
   1028 		for i=1, #o.categories do
   1029 			if o.categories[i] and o.categories[i][1] == chapter_state then
   1030 				g_opt_categories = o.categories[i][2]
   1031 				break
   1032 			end
   1033 		end
   1034 	end
   1035 	
   1036 	for category in string.gmatch(g_opt_categories, "([^;]+)") do
   1037         local name, patterns = string.match(category, " *([^+>]*[^+> ]) *[+>](.*)")
   1038         if name then
   1039             categories[name:lower()] = patterns
   1040         elseif not parsed[category] then
   1041             mp.msg.warn("Improper category definition: " .. category)
   1042         end
   1043         parsed[category] = true
   1044     end
   1045 end
   1046 
   1047 function start_chapterskip_countdown(text, duration, osd)
   1048 	g_autoskip_countdown_flag = true
   1049     g_autoskip_countdown = g_autoskip_countdown - 1
   1050 	
   1051 	if o.autoskip_countdown_graceful and (g_autoskip_countdown <= 0) then kill_chapterskip_countdown(); mp.osd_message('',0) return end
   1052 	
   1053 	if (g_autoskip_countdown < 0) then kill_chapterskip_countdown(); mp.osd_message('',0) return end
   1054 	
   1055 	text = text:gsub("%%countdown%%", g_autoskip_countdown)
   1056 	prompt_msg(text, duration, osd)
   1057 end
   1058 
   1059 function kill_chapterskip_countdown(action)
   1060 	if not g_autoskip_countdown_flag then return end
   1061 	if action == 'osd' then
   1062 		prompt_msg('○ Auto-Skip Cancelled', o.osd_duration, o.autoskip_osd)
   1063 	end
   1064 	if g_autoskip_timer ~= nil then
   1065 		g_autoskip_timer:kill()
   1066 	end
   1067 	unbind_keys(o.cancel_autoskip_countdown_keybind, 'cancel-autoskip-countdown')
   1068 	unbind_keys(o.proceed_autoskip_countdown_keybind, 'proceed-autoskip-countdown')
   1069 	g_autoskip_countdown = o.autoskip_countdown
   1070 	g_autoskip_countdown_flag = false
   1071 end
   1072 
   1073 function chapterskip(_, current, countdown)
   1074 	if g_autoskip_disabled then return end --1.3.4# do not autoskip if g_autoskip_disabled is in disabled state
   1075     if skip_flag then return end
   1076 	if chapter_state == 'no-chapters' then return end
   1077     if not autoskip_chapter then return end
   1078 	if not current then return end
   1079 	if g_autoskip_countdown_flag then kill_chapterskip_countdown('osd') end
   1080 	if not countdown then countdown = o.autoskip_countdown end
   1081 
   1082 	local chapters = mp.get_property_native("chapter-list")
   1083     local skip = false
   1084 	local consecutive_i = 0
   1085 	
   1086     for i=0, #chapters do
   1087 		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
   1088 		    if i == current + 1 or skip == i - 1 then
   1089                 if skip then
   1090                     skipped[skip] = true
   1091                 end
   1092                 skip = i
   1093 				consecutive_i = consecutive_i+1
   1094             end
   1095         elseif (not g_opt_skip_once or not skipped[i]) and chapters[i] and matches(i, chapters[i].title) then
   1096             if i == current + 1 or skip == i - 1 then
   1097                 if skip then
   1098                     skipped[skip] = true
   1099                 end
   1100                 skip = i
   1101 				consecutive_i = consecutive_i+1
   1102             end
   1103         elseif skip and countdown <= 0 then
   1104 			if o.autoskip_osd == 'osd-bar' or o.autoskip_osd == 'osd-msg-bar' then prompt_progress('osd-bar', o.osd_duration) end
   1105 			
   1106 			if consecutive_i > 1 then
   1107 				local autoskip_osd_string = ''
   1108 				for j=consecutive_i, 1, -1  do
   1109 					local chapter_title = ''
   1110 					if chapters[i-j] then chapter_title = chapters[i-j].title end
   1111 					autoskip_osd_string=(autoskip_osd_string..'\n  ➤ Chapter ('..i-j..') '..chapter_title)
   1112 				end
   1113 				prompt_msg('● Auto-Skip'..autoskip_osd_string, o.osd_duration, o.autoskip_osd)
   1114 			else
   1115 				prompt_msg('➤ Auto-Skip: Chapter '.. mp.command_native({'expand-text', '${chapter}'}), o.osd_duration, o.autoskip_osd)
   1116 			end
   1117 			mp.commandv('no-osd', 'set', 'chapter', i-1)
   1118             skipped[skip] = true
   1119             return
   1120         elseif skip and countdown > 0 then
   1121 			g_autoskip_countdown_flag = true
   1122 			bind_keys(o.cancel_autoskip_countdown_keybind, "cancel-autoskip-countdown", function() kill_chapterskip_countdown('osd') return end)
   1123 			
   1124 			local autoskip_osd_string = ''
   1125 			local autoskip_graceful_osd = ''
   1126 			if o.autoskip_countdown_graceful then autoskip_graceful_osd = 'Press Keybind to:\n' end
   1127 			
   1128 			if consecutive_i > 1 and o.autoskip_countdown_bulk then
   1129 				local autoskip_osd_string = ''
   1130 				for j=consecutive_i, 1, -1  do
   1131 					local chapter_title = ''
   1132 					if chapters[i-j] then chapter_title = chapters[i-j].title end
   1133 					autoskip_osd_string=(autoskip_osd_string..'\n  ▷ Chapter ('..i-j..') '..chapter_title)
   1134 				end
   1135 				prompt_msg(autoskip_graceful_osd..'○ Auto-Skip'..' in "'..o.autoskip_countdown..'"'..autoskip_osd_string, 2000, o.autoskip_osd)
   1136 				g_autoskip_timer = mp.add_periodic_timer(1, function () 
   1137 					start_chapterskip_countdown(autoskip_graceful_osd..'○ Auto-Skip'..' in "%countdown%"'..autoskip_osd_string, 2000, o.autoskip_osd)
   1138 				end)
   1139 			else
   1140 				prompt_msg(autoskip_graceful_osd..'▷ Auto-Skip in "'..o.autoskip_countdown..'": Chapter '.. mp.command_native({'expand-text', '${chapter}'}), 2000, o.autoskip_osd)
   1141 				g_autoskip_timer = mp.add_periodic_timer(1, function () 
   1142 					start_chapterskip_countdown(autoskip_graceful_osd..'▷ Auto-Skip in "%countdown%": Chapter '.. mp.command_native({'expand-text', '${chapter}'}), 2000, o.autoskip_osd)
   1143 				end)
   1144 			end
   1145 
   1146 			function proceed_autoskip(force)
   1147 				if not g_autoskip_countdown_flag then kill_chapterskip_countdown() return end
   1148 				if g_autoskip_countdown > 1 and not force then return end
   1149 				
   1150 				if o.autoskip_osd == 'osd-bar' or o.autoskip_osd == 'osd-msg-bar' then prompt_progress('osd-bar', o.osd_duration) end
   1151 
   1152 				if consecutive_i > 1 and o.autoskip_countdown_bulk then
   1153 					local autoskip_osd_string = ''
   1154 					for j=consecutive_i, 1, -1  do
   1155 						local chapter_title = ''
   1156 						if chapters[i-j] then chapter_title = chapters[i-j].title end
   1157 						autoskip_osd_string=(autoskip_osd_string..'\n  ➤ Chapter ('..i-j..') '..chapter_title)
   1158 					end
   1159 					prompt_msg('● Auto-Skip'..autoskip_osd_string, o.osd_duration, o.autoskip_osd)
   1160 				else
   1161 					prompt_msg('➤ Auto-Skip: Chapter '.. mp.command_native({'expand-text', '${chapter}'}), o.osd_duration, o.autoskip_osd)
   1162 				end
   1163 
   1164 				if consecutive_i > 1 and o.autoskip_countdown_bulk then
   1165 					mp.commandv('no-osd', 'set', 'chapter', i-1)
   1166 				else
   1167 					mp.commandv('no-osd', 'add', 'chapter', 1)
   1168 				end
   1169 				skipped[skip] = true
   1170 				kill_chapterskip_countdown()
   1171 			end
   1172 			bind_keys(o.proceed_autoskip_countdown_keybind, "proceed-autoskip-countdown", function() proceed_autoskip(true) return end)
   1173 			if o.autoskip_countdown_graceful then return end
   1174 			mp.add_timeout(countdown, proceed_autoskip)
   1175             return
   1176         end
   1177     end
   1178     if skip and countdown <= 0 then
   1179         if mp.get_property_native("playlist-count") == mp.get_property_native("playlist-pos-1") then
   1180             return mp.set_property("time-pos", mp.get_property_native("duration"))
   1181         end
   1182         mp.commandv("playlist-next")
   1183 		if o.autoskip_osd ~= 'no-osd' or o.autoskip_osd ~= 'osd-bar' then autoskip_playlist_osd = true end
   1184     elseif skip and countdown > 0 then
   1185 		g_autoskip_countdown_flag = true
   1186 		bind_keys(o.cancel_autoskip_countdown_keybind, "cancel-autoskip-countdown", function() kill_chapterskip_countdown('osd') return end)
   1187 		local autoskip_graceful_osd = ''
   1188 		if o.autoskip_countdown_graceful then autoskip_graceful_osd = 'Press Keybind to:\n' end
   1189 		if consecutive_i > 1 and o.autoskip_countdown_bulk then
   1190 			local i = (mp.get_property_number('chapters')+1 or 0)
   1191 			local autoskip_osd_string = ''
   1192 			for j=consecutive_i, 1, -1  do
   1193 				local chapter_title = ''
   1194 				if chapters[i-j] then chapter_title = chapters[i-j].title end
   1195 				autoskip_osd_string=(autoskip_osd_string..'\n  ▷ Chapter ('..i-j..') '..chapter_title)
   1196 			end
   1197 			prompt_msg(autoskip_graceful_osd..'○ Auto-Skip'..' in "'..o.autoskip_countdown..'"'..autoskip_osd_string, 2000, o.autoskip_osd)
   1198 			g_autoskip_timer = mp.add_periodic_timer(1, function ()
   1199 				start_chapterskip_countdown(autoskip_graceful_osd..'○ Auto-Skip'..' in "%countdown%"'..autoskip_osd_string, 2000, o.autoskip_osd)
   1200 			end)
   1201 		else
   1202 			prompt_msg(autoskip_graceful_osd..'▷ Auto-Skip in "'..o.autoskip_countdown..'": Chapter '.. mp.command_native({'expand-text', '${chapter}'}), 2000, o.autoskip_osd)
   1203 			g_autoskip_timer = mp.add_periodic_timer(1, function () 
   1204 				start_chapterskip_countdown(autoskip_graceful_osd..'▷ Auto-Skip in "%countdown%": Chapter '.. mp.command_native({'expand-text', '${chapter}'}), 2000, o.autoskip_osd)
   1205 			end)
   1206 		end
   1207 		function proceed_autoskip(force)
   1208 			if not g_autoskip_countdown_flag then return end
   1209 			if g_autoskip_countdown > 1 and not force then return end
   1210 
   1211 			if o.autoskip_osd == 'osd-bar' or o.autoskip_osd == 'osd-msg-bar' then prompt_progress('osd-bar', o.osd_duration) end
   1212 
   1213 			if consecutive_i > 1 and o.autoskip_countdown_bulk then
   1214 				if mp.get_property_native("playlist-count") == mp.get_property_native("playlist-pos-1") then
   1215 					return mp.set_property("time-pos", mp.get_property_native("duration"))
   1216 				end
   1217 				mp.commandv("playlist-next")
   1218 			else
   1219 				local current_chapter = (mp.get_property_number("chapter") + 1 or 0)
   1220 				local chapters_count = (mp.get_property_number('chapters') or 0)
   1221 				
   1222 				if current_chapter == chapters_count then
   1223 					if mp.get_property_native("playlist-count") == mp.get_property_native("playlist-pos-1") then
   1224 						return mp.set_property("time-pos", mp.get_property_native("duration"))
   1225 					end
   1226 					mp.commandv("playlist-next")
   1227 				else
   1228 					mp.commandv('no-osd', 'add', 'chapter', 1)
   1229 				end
   1230 			end
   1231 			if o.autoskip_osd ~= 'no-osd' or o.autoskip_osd ~= 'osd-bar' then autoskip_playlist_osd = true end
   1232 			kill_chapterskip_countdown()
   1233 		end
   1234 		bind_keys(o.proceed_autoskip_countdown_keybind, "proceed-autoskip-countdown", function() proceed_autoskip(true) return end)
   1235 		if o.autoskip_countdown_graceful then return end
   1236 		mp.add_timeout(countdown, proceed_autoskip)
   1237     end
   1238 end
   1239 
   1240 function toggle_autoskip()
   1241 	if autoskip_chapter == true then
   1242 		prompt_msg('○ Auto-Skip Disabled', o.osd_duration, o.autoskip_osd)
   1243 		autoskip_chapter = false
   1244 		if g_autoskip_countdown_flag then kill_chapterskip_countdown() end
   1245 	elseif autoskip_chapter == false then
   1246 		prompt_msg('● Auto-Skip Enabled', o.osd_duration, o.autoskip_osd)
   1247 		autoskip_chapter = true
   1248 	end
   1249 end
   1250 
   1251 function toggle_category_autoskip()
   1252 	if chapter_state == 'no-chapters' then return end
   1253 	if not mp.get_property_number("chapter") then return end
   1254 	local chapters = mp.get_property_native("chapter-list")
   1255 	local current_chapter = (mp.get_property_number("chapter") + 1 or 0)
   1256 	
   1257 	local chapter_title = tostring(current_chapter)
   1258 	if current_chapter > 0 and chapters[current_chapter].title and chapters[current_chapter].title ~= '' then
   1259 		chapter_title = chapters[current_chapter].title
   1260 	end
   1261 
   1262 	local found_i = 0
   1263 	if matches(current_chapter, chapter_title) then
   1264 		for category in string.gmatch(g_opt_categories, "([^;]+)") do
   1265 			local name, patterns = string.match(category, " *([^+>]*[^+> ]) *[+>](.*)")
   1266 
   1267 			for pattern in string.gmatch(patterns, "([^/]+)") do
   1268 				if string.match(chapter_title:lower(), pattern:lower()) then
   1269 					g_opt_categories = g_opt_categories:gsub(esc_string(pattern)..'/?', "")
   1270 					found_i = found_i + 1
   1271 				end
   1272 			end
   1273 		end
   1274 		
   1275 		for category in string.gmatch(g_opt_categories, "([^;]+)") do
   1276 			local name, patterns = string.match(category, " *([^+>]*[^+> ]) *[+>](.*)")
   1277 			if name then
   1278 				categories[name:lower()] = patterns
   1279 			elseif not parsed[category] then
   1280 				mp.msg.warn("Improper category definition: " .. category)
   1281 			end
   1282 			parsed[category] = true
   1283 		end
   1284 		
   1285 		if type(o.categories) == 'table' then
   1286 			for i=1, #o.categories do
   1287 				if o.categories[i] and o.categories[i][1] == chapter_state then
   1288 					o.categories[i][2] = g_opt_categories
   1289 					break
   1290 				end
   1291 			end
   1292 		else
   1293 			o.categories = g_opt_categories
   1294 		end
   1295 	end
   1296 	if current_chapter > 0 and chapters[current_chapter].title and chapters[current_chapter].title ~= '' then
   1297 		if found_i > 0 or string.match(categories.toggle, esc_string(chapter_title)) then
   1298 			prompt_msg('○ Removed from Auto-Skip\n  ▷ Chapter: '..chapter_title, o.osd_duration, o.autoskip_osd)
   1299 			categories.toggle = categories.toggle:gsub(esc_string("^"..chapter_title.."/"), "")
   1300 			if g_autoskip_countdown_flag then kill_chapterskip_countdown() end
   1301 		else
   1302 			prompt_msg('● Added to Auto-Skip\n  ➔ Chapter: '..chapter_title, o.osd_duration, o.autoskip_osd)
   1303 			categories.toggle = categories.toggle.."^"..chapter_title.."/"
   1304 		end
   1305 	else
   1306 		if found_i > 0 or string.match(categories.toggle_idx, esc_string(chapter_title)) then
   1307 			prompt_msg('○ Removed from Auto-Skip\n  ▷ Chapter: '..chapter_title, o.osd_duration, o.autoskip_osd)
   1308 			categories.toggle_idx = categories.toggle_idx:gsub(esc_string(chapter_title.."/"), "")
   1309 			if g_autoskip_countdown_flag then kill_chapterskip_countdown() end
   1310 		else
   1311 			prompt_msg('● Added to Auto-Skip\n  ➔ Chapter: '..chapter_title, o.osd_duration, o.autoskip_osd)
   1312 			categories.toggle_idx = categories.toggle_idx..chapter_title.."/"
   1313 		end
   1314 	end
   1315 end
   1316 
   1317 -- HOOKS --------------------------------------------------------------------
   1318 if user_input_module then mp.add_hook("on_unload", 50, function () input.cancel_user_input() end) end -- chapters.lua
   1319 mp.observe_property("chapter", "number", chapterskip) -- chapterskip.lua
   1320 
   1321 -- smart skip events / properties / hooks --
   1322 
   1323 mp.register_event('file-loaded', function()
   1324 	file_length = (mp.get_property_native('duration') or 0)
   1325 	g_filepath = (mp.get_property('path') or '') --1.3.4# get filepath needed for exclusion_check function
   1326 	if exclusion_check() then g_autoskip_disabled = true end --1.3.4# disable / enable autoskip based on exclusion_check
   1327 
   1328 	if o.playlist_osd and g_playlist_pos > 0 then playlist_osd = true end
   1329 	if playlist_osd and not autoskip_playlist_osd then
   1330 		prompt_msg('['..mp.command_native({'expand-text', '${playlist-pos-1}'})..'/'..mp.command_native({'expand-text', '${playlist-count}'})..'] '..mp.command_native({'expand-text', '${filename}'}))
   1331 	end
   1332 	if autoskip_playlist_osd then
   1333 		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)
   1334 	end
   1335 	playlist_osd = false
   1336 	autoskip_playlist_osd = false
   1337 	force_silence_skip = false
   1338 	skipped = {}
   1339 	initial_chapter_count = mp.get_property_number("chapter-list/count")
   1340 	if initial_chapter_count > 0 and chapter_state ~= 'external-chapters' then chapter_state = 'internal-chapters' end
   1341 	prep_chapterskip_var()
   1342 end)
   1343 
   1344 mp.add_hook("on_load", 50, function()
   1345 	if o.external_chapters_autoload then load_chapters() end
   1346 end)
   1347 
   1348 mp.observe_property('pause', 'bool', function(name, value)
   1349 	if value and skip_flag then
   1350 		restoreProp(initial_skip_time, true)
   1351 	end
   1352 	if g_autoskip_countdown_flag then kill_chapterskip_countdown('osd') end
   1353 end)
   1354 
   1355 mp.add_hook('on_unload', 9, function()
   1356 	if geometry_default == "" then mp.set_property("geometry","") end
   1357 	if o.modified_chapters_autosave == true or has_value(o.modified_chapters_autosave, chapter_state) then write_chapters(false) end
   1358 	mp.set_property("keep-open", keep_open_state)
   1359 	chapter_state = 'no-chapters'
   1360 	g_playlist_pos = (mp.get_property_native('playlist-playing-pos')+1 or 0)
   1361 	kill_chapterskip_countdown()
   1362 end)
   1363 
   1364 mp.register_event('seek', function()
   1365 	if g_autoskip_countdown_flag then kill_chapterskip_countdown('osd') end
   1366 end)
   1367 
   1368 mp.observe_property('eof-reached', 'bool', eofHandler)
   1369 
   1370 -- BINDINGS --------------------------------------------------------------------
   1371 
   1372 bind_keys(o.toggle_autoskip_keybind, "toggle-autoskip", toggle_autoskip)
   1373 bind_keys(o.toggle_category_autoskip_keybind, "toggle-category-autoskip", toggle_category_autoskip)
   1374 bind_keys(o.add_chapter_keybind, "add-chapter", add_chapter)
   1375 bind_keys(o.remove_chapter_keybind, "remove-chapter", remove_chapter)
   1376 bind_keys(o.write_chapters_keybind, "write-chapters", function () write_chapters(true) end)
   1377 bind_keys(o.edit_chapter_keybind, "edit-chapter", edit_chapter)
   1378 bind_keys(o.bake_chapters_keybind, "bake-chapters", bake_chapters)
   1379 bind_keys(o.chapter_prev_keybind, "chapter-prev", function() chapterSeek(-1) end)
   1380 bind_keys(o.chapter_next_keybind, "chapter-next", function() chapterSeek(1) end)
   1381 bind_keys(o.smart_prev_keybind, "smart-prev", smartPrev)
   1382 bind_keys(o.smart_next_keybind, "smart-next", smartNext)
   1383 bind_keys(o.silence_skip_keybind, "silence-skip", silenceSkip)