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