summaryrefslogtreecommitdiff
path: root/mac/.config/mpv/scripts/SmartSkip.lua
diff options
context:
space:
mode:
Diffstat (limited to 'mac/.config/mpv/scripts/SmartSkip.lua')
-rw-r--r--mac/.config/mpv/scripts/SmartSkip.lua1936
1 files changed, 1936 insertions, 0 deletions
diff --git a/mac/.config/mpv/scripts/SmartSkip.lua b/mac/.config/mpv/scripts/SmartSkip.lua
new file mode 100644
index 0000000..05ce108
--- /dev/null
+++ b/mac/.config/mpv/scripts/SmartSkip.lua
@@ -0,0 +1,1936 @@
+-- Copyright (c) 2023, Eisa AlAwadhi
+-- License: BSD 2-Clause License
+-- Creator: Eisa AlAwadhi
+-- Project: SmartSkip
+-- Version: 1.2
+-- Date: 23-09-2023
+
+-- Related forked projects:
+-- https://github.com/detuur/mpv-scripts/blob/master/skiptosilence.lua
+-- https://raw.githubusercontent.com/mpv-player/mpv/master/TOOLS/lua/autoload.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 = 130,
+ 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; preview>Preview$"], ["external-chapters", "idx->0/2"] ] ]],
+ skip = [[ [ ["internal-chapters", "toggle;toggle_idx;opening;ending;preview"], ["external-chapters", "toggle;toggle_idx"] ] ]],
+ -----Autoload Settings-----
+ autoload_playlist = true,
+ autoload_max_entries = 5000,
+ autoload_max_dir_stack = 20,
+ ignore_hidden = true,
+ same_type = false,
+ directory_mode = "auto",
+ images = true,
+ videos = true,
+ audio = true,
+ additional_image_exts = "",
+ additional_video_exts = "",
+ additional_audio_exts = "",
+ -----OSD Messages Settings-----
+ osd_duration = 2500,
+ seek_osd = "osd-msg-bar",
+ chapter_osd = "osd-msg-bar",
+ autoskip_osd = "osd-msg-bar",
+ playlist_osd = true,
+ osd_msg = true,
+ -----Keybind Settings-----
+ toggle_autoload_keybind = [[ [""] ]],
+ 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 = [[ ["?"] ]],
+}
+
+local mp = require("mp")
+local msg = require("mp.msg")
+local utils = require("mp.utils")
+local options = require("mp.options")
+options.read_options(o, nil, function(list)
+ split_option_exts(list.additional_video_exts, list.additional_audio_exts, list.additional_image_exts)
+ if
+ list.videos
+ or list.additional_video_exts
+ or list.audio
+ or list.additional_audio_exts
+ or list.images
+ or list.additional_image_exts
+ then
+ create_extensions()
+ end
+ if list.directory_mode then
+ validate_directory_mode()
+ end
+end)
+
+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 --1.2# add variables for specifying path via user-config
+ 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_autoload_keybind = utils.parse_json(o.toggle_autoload_keybind)
+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)
+
+package.path = mp.command_native({ "expand-path", "~~/script-modules/?.lua;" }) .. package.path
+local user_input_module, input = pcall(require, "user-input-module")
+
+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 autoload_playlist = o.autoload_playlist
+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 categories = {
+ toggle = "",
+ toggle_idx = "",
+}
+local autoskip_osd = o.autoskip_osd
+if o.autoskip_osd == "osd-msg-bar" then
+ autoskip_osd = "osd-bar"
+end
+if o.autoskip_osd == "osd-msg" then
+ autoskip_osd = "no-osd"
+end
+
+-- 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 prompt_msg(text, duration)
+ if not text then
+ return
+ end
+ if not duration then
+ duration = o.osd_duration
+ end
+ if o.osd_msg then
+ mp.commandv("show-text", text, duration)
+ end
+ msg.info(text)
+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
+
+-- 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)
+ 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")
+ 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")
+ 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"))
+ else
+ mp.commandv("playlist-next")
+ end
+ elseif o.silence_skip_to_end == "cancel" then
+ prompt_msg("Skipping Cancelled\nSilence not detected")
+ restoreProp(initial_skip_time)
+ elseif o.silence_skip_to_end == "pause" then
+ prompt_msg("Skipped to end at " .. mp.get_property_osd("duration"))
+ 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")
+
+ mp.set_property("osd-duration", o.osd_duration)
+ mp.commandv(o.seek_osd, "show-progress")
+ mp.add_timeout(0.07, function()
+ mp.set_property("osd-duration", osd_duration_default)
+ end)
+ 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")
+
+ mp.set_property("osd-duration", o.osd_duration)
+ mp.commandv(o.seek_osd, "show-progress")
+ mp.add_timeout(0.07, function()
+ mp.set_property("osd-duration", osd_duration_default)
+ end)
+ 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)
+ 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
+ local width = mp.get_property_native("osd-width")
+ local height = mp.get_property_native("osd-height")
+ mp.set_property_native("geometry", ("%dx%d"):format(width, height))
+ mp.commandv(o.seek_osd, "show-progress")
+
+ 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)
+
+ sub_state = mp.get_property("sub-visibility")
+ mp.set_property("sub-visibility", "no")
+ secondary_sub_state = mp.get_property("secondary-sub-visibility")
+ mp.set_property("secondary-sub-visibility", "no")
+ window_state = mp.get_property("force-window")
+ mp.set_property("force-window", "yes")
+ vid_state = mp.get_property("vid")
+ 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
+ pause_state = mp.get_property_native("pause")
+ mp.set_property_bool("pause", false)
+ speed_state = mp.get_property_native("speed")
+ 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 then
+ mp.commandv(o.seek_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"))
+ 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 autoload script--
+function toggle_autoload()
+ if autoload_playlist == true then
+ prompt_msg("○ Auto-Load Disabled")
+ autoload_playlist = false
+ elseif autoload_playlist == false then
+ prompt_msg("● Auto-Load Enabled")
+ autoload_playlist = true
+ end
+ if autoload_playlist then
+ find_and_add_entries()
+ end
+end
+
+function Set(t)
+ local set = {}
+ for _, v in pairs(t) do
+ set[v] = true
+ end
+ return set
+end
+
+function SetUnion(a, b)
+ for k in pairs(b) do
+ a[k] = true
+ end
+ return a
+end
+
+function Split(s)
+ local set = {}
+ for v in string.gmatch(s, "([^,]+)") do
+ set[v] = true
+ end
+ return set
+end
+
+EXTENSIONS_VIDEO = Set({
+ "3g2",
+ "3gp",
+ "avi",
+ "flv",
+ "m2ts",
+ "m4v",
+ "mj2",
+ "mkv",
+ "mov",
+ "mp4",
+ "mpeg",
+ "mpg",
+ "ogv",
+ "rmvb",
+ "webm",
+ "wmv",
+ "y4m",
+})
+
+EXTENSIONS_AUDIO = Set({
+ "aiff",
+ "ape",
+ "au",
+ "flac",
+ "m4a",
+ "mka",
+ "mp3",
+ "oga",
+ "ogg",
+ "ogm",
+ "opus",
+ "wav",
+ "wma",
+})
+
+EXTENSIONS_IMAGES = Set({
+ "avif",
+ "bmp",
+ "gif",
+ "j2k",
+ "jp2",
+ "jpeg",
+ "jpg",
+ "jxl",
+ "png",
+ "svg",
+ "tga",
+ "tif",
+ "tiff",
+ "webp",
+})
+
+function split_option_exts(video, audio, image)
+ if video then
+ o.additional_video_exts = Split(o.additional_video_exts)
+ end
+ if audio then
+ o.additional_audio_exts = Split(o.additional_audio_exts)
+ end
+ if image then
+ o.additional_image_exts = Split(o.additional_image_exts)
+ end
+end
+split_option_exts(true, true, true)
+
+function create_extensions()
+ EXTENSIONS = {}
+ if o.videos then
+ SetUnion(SetUnion(EXTENSIONS, EXTENSIONS_VIDEO), o.additional_video_exts)
+ end
+ if o.audio then
+ SetUnion(SetUnion(EXTENSIONS, EXTENSIONS_AUDIO), o.additional_audio_exts)
+ end
+ if o.images then
+ SetUnion(SetUnion(EXTENSIONS, EXTENSIONS_IMAGES), o.additional_image_exts)
+ end
+end
+create_extensions()
+
+function validate_directory_mode()
+ if o.directory_mode ~= "recursive" and o.directory_mode ~= "lazy" and o.directory_mode ~= "ignore" then
+ o.directory_mode = nil
+ end
+end
+validate_directory_mode()
+
+function add_files(files)
+ local oldcount = mp.get_property_number("playlist-count", 1)
+ for i = 1, #files do
+ mp.commandv("loadfile", files[i][1], "append")
+ mp.commandv("playlist-move", oldcount + i - 1, files[i][2])
+ end
+end
+
+function get_extension(path)
+ match = string.match(path, "%.([^%.]+)$")
+ if match == nil then
+ return "nomatch"
+ else
+ return match
+ end
+end
+
+table.filter = function(t, iter)
+ for i = #t, 1, -1 do
+ if not iter(t[i]) then
+ table.remove(t, i)
+ end
+ end
+end
+
+table.append = function(t1, t2)
+ local t1_size = #t1
+ for i = 1, #t2 do
+ t1[t1_size + i] = t2[i]
+ end
+end
+
+-- alphanum sorting for humans in Lua
+-- http://notebook.kulchenko.com/algorithms/alphanumeric-natural-sorting-for-humans-in-lua
+
+function alphanumsort(filenames)
+ local function padnum(n, d)
+ return #d > 0 and ("%03d%s%.12f"):format(#n, n, tonumber(d) / (10 ^ #d)) or ("%03d%s"):format(#n, n)
+ end
+
+ local tuples = {}
+ for i, f in ipairs(filenames) do
+ tuples[i] = { f:lower():gsub("0*(%d+)%.?(%d*)", padnum), f }
+ end
+ table.sort(tuples, function(a, b)
+ return a[1] == b[1] and #b[2] < #a[2] or a[1] < b[1]
+ end)
+ for i, tuple in ipairs(tuples) do
+ filenames[i] = tuple[2]
+ end
+ return filenames
+end
+
+local autoloaded = nil
+local added_entries = {}
+local autoloaded_dir = nil
+
+function scan_dir(path, current_file, dir_mode, separator, dir_depth, total_files, extensions)
+ if dir_depth == o.autoload_max_dir_stack then
+ return
+ end
+ msg.trace("scanning: " .. path)
+ local files = utils.readdir(path, "files") or {}
+ local dirs = dir_mode ~= "ignore" and utils.readdir(path, "dirs") or {}
+ local prefix = path == "." and "" or path
+ table.filter(files, function(v)
+ -- The current file could be a hidden file, ignoring it doesn't load other
+ -- files from the current directory.
+ if o.ignore_hidden and not (prefix .. v == current_file) and string.match(v, "^%.") then
+ return false
+ end
+ local ext = get_extension(v)
+ if ext == nil then
+ return false
+ end
+ return extensions[string.lower(ext)]
+ end)
+ table.filter(dirs, function(d)
+ return not (o.ignore_hidden and string.match(d, "^%."))
+ end)
+ alphanumsort(files)
+ alphanumsort(dirs)
+
+ for i, file in ipairs(files) do
+ files[i] = prefix .. file
+ end
+
+ table.append(total_files, files)
+ if dir_mode == "recursive" then
+ for _, dir in ipairs(dirs) do
+ scan_dir(
+ prefix .. dir .. separator,
+ current_file,
+ dir_mode,
+ separator,
+ dir_depth + 1,
+ total_files,
+ extensions
+ )
+ end
+ else
+ for i, dir in ipairs(dirs) do
+ dirs[i] = prefix .. dir
+ end
+ table.append(total_files, dirs)
+ end
+end
+
+function find_and_add_entries()
+ local path = mp.get_property("path", "")
+ local dir, filename = utils.split_path(path)
+ msg.trace(("dir: %s, filename: %s"):format(dir, filename))
+ if not autoload_playlist then
+ msg.verbose("stopping: autoload_playlist is disabled")
+ return
+ elseif #dir == 0 then
+ msg.verbose("stopping: not a local path")
+ return
+ end
+
+ local pl_count = mp.get_property_number("playlist-count", 1)
+ this_ext = get_extension(filename)
+ -- check if this is a manually made playlist
+ if (pl_count > 1 and autoloaded == nil) or (pl_count == 1 and EXTENSIONS[string.lower(this_ext)] == nil) then
+ msg.verbose("stopping: manually made playlist")
+ return
+ else
+ if pl_count == 1 then
+ autoloaded = true
+ autoloaded_dir = dir
+ added_entries = {}
+ end
+ end
+
+ local extensions = {}
+ if o.same_type then
+ if EXTENSIONS_VIDEO[string.lower(this_ext)] ~= nil then
+ extensions = EXTENSIONS_VIDEO
+ elseif EXTENSIONS_AUDIO[string.lower(this_ext)] ~= nil then
+ extensions = EXTENSIONS_AUDIO
+ else
+ extensions = EXTENSIONS_IMAGES
+ end
+ else
+ extensions = EXTENSIONS
+ end
+
+ local pl = mp.get_property_native("playlist", {})
+ local pl_current = mp.get_property_number("playlist-pos-1", 1)
+ msg.trace(("playlist-pos-1: %s, playlist: %s"):format(pl_current, utils.to_string(pl)))
+
+ local files = {}
+ do
+ local dir_mode = o.directory_mode or mp.get_property("directory-mode", "lazy")
+ local separator = mp.get_property_native("platform") == "windows" and "\\" or "/"
+ scan_dir(autoloaded_dir, path, dir_mode, separator, 0, files, extensions)
+ end
+
+ if next(files) == nil then
+ msg.verbose("no other files or directories in directory")
+ return
+ end
+
+ -- Find the current pl entry (dir+"/"+filename) in the sorted dir list
+ local current
+ for i = 1, #files do
+ if files[i] == path then
+ current = i
+ break
+ end
+ end
+ if current == nil then
+ return
+ end
+ msg.trace("current file position in files: " .. current)
+
+ -- treat already existing playlist entries, independent of how they got added
+ -- as if they got added by autoload
+ for _, entry in ipairs(pl) do
+ added_entries[entry.filename] = true
+ end
+
+ local append = { [-1] = {}, [1] = {} }
+ for direction = -1, 1, 2 do -- 2 iterations, with direction = -1 and +1
+ for i = 1, o.autoload_max_entries do
+ local pos = current + i * direction
+ local file = files[pos]
+ if file == nil or file[1] == "." then
+ break
+ end
+
+ -- skip files that are/were already in the playlist
+ if not added_entries[file] then
+ if direction == -1 then
+ msg.info("Prepending " .. file)
+ table.insert(append[-1], 1, { file, pl_current + i * direction + 1 })
+ else
+ msg.info("Adding " .. file)
+ if pl_count > 1 then
+ table.insert(append[1], { file, pl_current + i * direction - 1 })
+ else
+ mp.commandv("loadfile", file, "append")
+ end
+ end
+ end
+ added_entries[file] = true
+ end
+ if pl_count == 1 and direction == -1 and #append[-1] > 0 then
+ for i = 1, #append[-1] do
+ mp.commandv("loadfile", append[-1][i][1], "append")
+ end
+ mp.commandv("playlist-move", 0, current)
+ end
+ end
+
+ if pl_count > 1 then
+ add_files(append[1])
+ add_files(append[-1])
+ 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)
+ 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, 2000)
+end
+
+function kill_chapterskip_countdown(action)
+ if not g_autoskip_countdown_flag then
+ return
+ end
+ if action == "osd" and o.autoskip_osd ~= "no-osd" then
+ prompt_msg("○ Auto-Skip Cancelled")
+ 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 chapter_state == "no-chapters" then
+ return
+ end
+ if not autoskip_chapter 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
+ mp.set_property("osd-duration", o.osd_duration)
+ mp.commandv(autoskip_osd, "show-progress")
+ mp.add_timeout(0.07, function()
+ mp.set_property("osd-duration", osd_duration_default)
+ end)
+
+ if o.autoskip_osd == "osd-msg-bar" or o.autoskip_osd == "osd-msg" then
+ 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)
+ else
+ prompt_msg("➤ Auto-Skip: Chapter " .. mp.command_native({ "expand-text", "${chapter}" }))
+ end
+ end
+ mp.set_property("time-pos", chapters[i].time)
+ 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 o.autoskip_osd == "osd-msg-bar" or o.autoskip_osd == "osd-msg" then
+ 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
+ )
+ g_autoskip_timer = mp.add_periodic_timer(1, function()
+ start_chapterskip_countdown(
+ autoskip_graceful_osd .. "○ Auto-Skip" .. ' in "%countdown%"' .. autoskip_osd_string,
+ 2000
+ )
+ end)
+ else
+ prompt_msg(
+ autoskip_graceful_osd
+ .. '▷ Auto-Skip in "'
+ .. o.autoskip_countdown
+ .. '": Chapter '
+ .. mp.command_native({ "expand-text", "${chapter}" }),
+ 2000
+ )
+ 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
+ )
+ end)
+ 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
+
+ mp.set_property("osd-duration", o.osd_duration)
+ mp.commandv(autoskip_osd, "show-progress")
+ mp.add_timeout(0.07, function()
+ mp.set_property("osd-duration", osd_duration_default)
+ end)
+ if o.autoskip_osd == "osd-msg-bar" or o.autoskip_osd == "osd-msg" then
+ 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)
+ else
+ prompt_msg("➤ Auto-Skip: Chapter " .. mp.command_native({ "expand-text", "${chapter}" }))
+ end
+ end
+ if consecutive_i > 1 and o.autoskip_countdown_bulk then
+ mp.set_property("time-pos", chapters[i].time)
+ 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" 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)
+
+ if o.autoskip_osd == "osd-msg-bar" or o.autoskip_osd == "osd-msg" then
+ 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
+ )
+ g_autoskip_timer = mp.add_periodic_timer(1, function()
+ start_chapterskip_countdown(
+ autoskip_graceful_osd .. "○ Auto-Skip" .. ' in "%countdown%"' .. autoskip_osd_string,
+ 2000
+ )
+ end)
+ else
+ prompt_msg(
+ autoskip_graceful_osd
+ .. '▷ Auto-Skip in "'
+ .. o.autoskip_countdown
+ .. '": Chapter '
+ .. mp.command_native({ "expand-text", "${chapter}" }),
+ 2000
+ )
+ 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
+ )
+ end)
+ 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
+
+ mp.set_property("osd-duration", o.osd_duration)
+ mp.commandv(autoskip_osd, "show-progress")
+ mp.add_timeout(0.07, function()
+ mp.set_property("osd-duration", osd_duration_default)
+ 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" 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")
+ autoskip_chapter = false
+ if g_autoskip_countdown_flag then
+ kill_chapterskip_countdown()
+ end
+ elseif autoskip_chapter == false then
+ prompt_msg("● Auto-Skip Enabled")
+ 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)
+ 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)
+ 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)
+ 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)
+ 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.register_event("start-file", find_and_add_entries) -- autoload.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)
+ 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}" })
+ )
+ 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 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_autoload_keybind, "toggle-autoload", toggle_autoload)
+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)