diff options
Diffstat (limited to 'mpv/scripts/file-browser/modules/navigation/scanning.lua')
| -rw-r--r-- | mpv/scripts/file-browser/modules/navigation/scanning.lua | 210 |
1 files changed, 210 insertions, 0 deletions
diff --git a/mpv/scripts/file-browser/modules/navigation/scanning.lua b/mpv/scripts/file-browser/modules/navigation/scanning.lua new file mode 100644 index 0000000..0923ba9 --- /dev/null +++ b/mpv/scripts/file-browser/modules/navigation/scanning.lua @@ -0,0 +1,210 @@ +local mp = require 'mp' +local msg = require 'mp.msg' +local utils = require 'mp.utils' + +local g = require 'modules.globals' +local fb_utils = require 'modules.utils' +local cursor = require 'modules.navigation.cursor' +local ass = require 'modules.ass' + +local parse_state_API = require 'modules.apis.parse-state' + +---@class scanning +local scanning = {} + +---@enum NavigationType +local NavType = { + DOWN = 1, + UP = -1, + REDIRECT = 2, + GOTO = 3, + RESCAN = 4, +} + +scanning.NavType = NavType + +---@param directory_stack? boolean +local function clear_non_adjacent_state(directory_stack) + g.state.directory_label = nil + if directory_stack then + g.directory_stack.stack = {g.state.directory} + g.directory_stack.position = 1 + end +end + +---parses the given directory or defers to the next parser if nil is returned +---@async +---@param directory string +---@param index number +---@return List? +---@return Opts? +function scanning.choose_and_parse(directory, index) + msg.debug(("finding parser for %q"):format(directory)) + ---@type Parser, List?, Opts? + local parser, list, opts + local parse_state = g.parse_states[coroutine.running() or ""] + while list == nil and not parse_state.already_deferred and index <= #g.parsers do + parser = g.parsers[index] + if parser:can_parse(directory, parse_state) then + msg.debug("attempting parser:", parser:get_id()) + list, opts = parser:parse(directory, parse_state) + end + index = index + 1 + end + if not list then return nil, {} end + + msg.debug("list returned from:", parser:get_id()) + opts = opts or {} + if list then opts.id = opts.id or parser:get_id() end + return list, opts +end + +---Sets up the parse_state table and runs the parse operation. +---@async +---@param directory string +---@param parse_state_template ParseStateTemplate +---@return List|nil +---@return Opts +local function run_parse(directory, parse_state_template) + msg.verbose(("scanning files in %q"):format(directory)) + + ---@type ParseStateFields + local parse_state = { + source = parse_state_template.source, + directory = directory, + properties = parse_state_template.properties or {} + } + + local co = coroutine.running() + g.parse_states[co] = fb_utils.set_prototype(parse_state, parse_state_API) --[[@as ParseState]] + + local list, opts = scanning.choose_and_parse(directory, 1) + + if list == nil then return msg.debug("no successful parsers found"), {} end + opts = opts or {} + opts.parser = g.parsers[opts.id] + + if not opts.filtered then fb_utils.filter(list) end + if not opts.sorted then fb_utils.sort(list) end + return list, opts +end + +---Returns the contents of the given directory using the given parse state. +---If a coroutine has already been used for a parse then create a new coroutine so that +---the every parse operation has a unique thread ID. +---@async +---@param directory string +---@param parse_state ParseStateTemplate +---@return List|nil +---@return Opts +function scanning.scan_directory(directory, parse_state) + local co = fb_utils.coroutine.assert("scan_directory must be executed from within a coroutine - aborting scan "..utils.to_string(parse_state)) + if not g.parse_states[co] then return run_parse(directory, parse_state) end + + --if this coroutine is already is use by another parse operation then we create a new + --one and hand execution over to that + ---@async + local new_co = coroutine.create(function() + fb_utils.coroutine.resume_err(co, run_parse(directory, parse_state)) + end) + + --queue the new coroutine on the mpv event queue + mp.add_timeout(0, function() + local success, err = coroutine.resume(new_co) + if not success then + fb_utils.traceback(err, new_co) + fb_utils.coroutine.resume_err(co) + end + end) + return g.parse_states[co]:yield() +end + +---Sends update requests to the different parsers. +---@async +---@param moving_adjacent? number|boolean +---@param parse_properties? ParseProperties +local function update_list(moving_adjacent, parse_properties) + msg.verbose('opening directory: ' .. g.state.directory) + + g.state.selected = 1 + g.state.selection = {} + + local directory = g.state.directory + local list, opts = scanning.scan_directory(g.state.directory, { source = "browser", properties = parse_properties }) + + --if the running coroutine isn't the one stored in the state variable, then the user + --changed directories while the coroutine was paused, and this operation should be aborted + if coroutine.running() ~= g.state.co then + msg.verbose(g.ABORT_ERROR.msg) + msg.debug("expected:", g.state.directory, "received:", directory) + return + end + + --apply fallbacks if the scan failed + if not list then + msg.warn("could not read directory", g.state.directory) + list, opts = {}, {} + opts.empty_text = g.style.warning..'Error: could not parse directory' + end + + g.state.list = list + g.state.parser = opts.parser + + --setting custom options from parsers + g.state.directory_label = opts.directory_label + g.state.empty_text = opts.empty_text or g.state.empty_text + + --we assume that directory is only changed when redirecting to a different location + --therefore we need to change the `moving_adjacent` flag and clear some state values + if opts.directory then + g.state.directory = opts.directory + moving_adjacent = false + clear_non_adjacent_state(true) + end + + if opts.selected_index then + g.state.selected = opts.selected_index or g.state.selected + if g.state.selected > #g.state.list then g.state.selected = #g.state.list + elseif g.state.selected < 1 then g.state.selected = 1 end + end + + if moving_adjacent then cursor.select_prev_directory() + else cursor.select_playing_item() end + g.state.prev_directory = g.state.directory +end + +---rescans the folder and updates the list. +---@param nav_type? NavigationType +---@param cb? function +---@param parse_properties? ParseProperties +---@return thread # The coroutine for the triggered parse operation. May be aborted early if directory is in the cache. +function scanning.rescan(nav_type, cb, parse_properties) + if nav_type == nil then nav_type = NavType.RESCAN end + + --we can only make assumptions about the directory label when moving from adjacent directories + if nav_type == NavType.GOTO or nav_type == NavType.REDIRECT then + clear_non_adjacent_state(nav_type == NavType.GOTO) + end + + g.state.empty_text = "~" + g.state.list = {} + cursor.disable_select_mode() + ass.update_ass() + + --the directory is always handled within a coroutine to allow addons to + --pause execution for asynchronous operations + ---@async + local co = fb_utils.coroutine.queue(function() + update_list(nav_type, parse_properties) + if g.state.empty_text == "~" then g.state.empty_text = "empty directory" end + + ass.update_ass() + if cb then fb_utils.coroutine.run(cb) end + end) + + g.state.co = co + return co +end + + +return scanning |
