-- 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 <mariusz.libera@gmail.com>

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)
