This commit is contained in:
2026-03-27 07:06:16 +01:00
commit 1541961403
340 changed files with 151916 additions and 0 deletions
@@ -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