scanning.lua (7149B)
1 local mp = require 'mp' 2 local msg = require 'mp.msg' 3 local utils = require 'mp.utils' 4 5 local g = require 'modules.globals' 6 local fb_utils = require 'modules.utils' 7 local cursor = require 'modules.navigation.cursor' 8 local ass = require 'modules.ass' 9 10 local parse_state_API = require 'modules.apis.parse-state' 11 12 ---@class scanning 13 local scanning = {} 14 15 ---@enum NavigationType 16 local NavType = { 17 DOWN = 1, 18 UP = -1, 19 REDIRECT = 2, 20 GOTO = 3, 21 RESCAN = 4, 22 } 23 24 scanning.NavType = NavType 25 26 ---@param directory_stack? boolean 27 local function clear_non_adjacent_state(directory_stack) 28 g.state.directory_label = nil 29 if directory_stack then 30 g.directory_stack.stack = {g.state.directory} 31 g.directory_stack.position = 1 32 end 33 end 34 35 ---parses the given directory or defers to the next parser if nil is returned 36 ---@async 37 ---@param directory string 38 ---@param index number 39 ---@return List? 40 ---@return Opts? 41 function scanning.choose_and_parse(directory, index) 42 msg.debug(("finding parser for %q"):format(directory)) 43 ---@type Parser, List?, Opts? 44 local parser, list, opts 45 local parse_state = g.parse_states[coroutine.running() or ""] 46 while list == nil and not parse_state.already_deferred and index <= #g.parsers do 47 parser = g.parsers[index] 48 if parser:can_parse(directory, parse_state) then 49 msg.debug("attempting parser:", parser:get_id()) 50 list, opts = parser:parse(directory, parse_state) 51 end 52 index = index + 1 53 end 54 if not list then return nil, {} end 55 56 msg.debug("list returned from:", parser:get_id()) 57 opts = opts or {} 58 if list then opts.id = opts.id or parser:get_id() end 59 return list, opts 60 end 61 62 ---Sets up the parse_state table and runs the parse operation. 63 ---@async 64 ---@param directory string 65 ---@param parse_state_template ParseStateTemplate 66 ---@return List|nil 67 ---@return Opts 68 local function run_parse(directory, parse_state_template) 69 msg.verbose(("scanning files in %q"):format(directory)) 70 71 ---@type ParseStateFields 72 local parse_state = { 73 source = parse_state_template.source, 74 directory = directory, 75 properties = parse_state_template.properties or {} 76 } 77 78 local co = coroutine.running() 79 g.parse_states[co] = fb_utils.set_prototype(parse_state, parse_state_API) --[[@as ParseState]] 80 81 local list, opts = scanning.choose_and_parse(directory, 1) 82 83 if list == nil then return msg.debug("no successful parsers found"), {} end 84 opts = opts or {} 85 opts.parser = g.parsers[opts.id] 86 87 if not opts.filtered then fb_utils.filter(list) end 88 if not opts.sorted then fb_utils.sort(list) end 89 return list, opts 90 end 91 92 ---Returns the contents of the given directory using the given parse state. 93 ---If a coroutine has already been used for a parse then create a new coroutine so that 94 ---the every parse operation has a unique thread ID. 95 ---@async 96 ---@param directory string 97 ---@param parse_state ParseStateTemplate 98 ---@return List|nil 99 ---@return Opts 100 function scanning.scan_directory(directory, parse_state) 101 local co = fb_utils.coroutine.assert("scan_directory must be executed from within a coroutine - aborting scan "..utils.to_string(parse_state)) 102 if not g.parse_states[co] then return run_parse(directory, parse_state) end 103 104 --if this coroutine is already is use by another parse operation then we create a new 105 --one and hand execution over to that 106 ---@async 107 local new_co = coroutine.create(function() 108 fb_utils.coroutine.resume_err(co, run_parse(directory, parse_state)) 109 end) 110 111 --queue the new coroutine on the mpv event queue 112 mp.add_timeout(0, function() 113 local success, err = coroutine.resume(new_co) 114 if not success then 115 fb_utils.traceback(err, new_co) 116 fb_utils.coroutine.resume_err(co) 117 end 118 end) 119 return g.parse_states[co]:yield() 120 end 121 122 ---Sends update requests to the different parsers. 123 ---@async 124 ---@param moving_adjacent? number|boolean 125 ---@param parse_properties? ParseProperties 126 local function update_list(moving_adjacent, parse_properties) 127 msg.verbose('opening directory: ' .. g.state.directory) 128 129 g.state.selected = 1 130 g.state.selection = {} 131 132 local directory = g.state.directory 133 local list, opts = scanning.scan_directory(g.state.directory, { source = "browser", properties = parse_properties }) 134 135 --if the running coroutine isn't the one stored in the state variable, then the user 136 --changed directories while the coroutine was paused, and this operation should be aborted 137 if coroutine.running() ~= g.state.co then 138 msg.verbose(g.ABORT_ERROR.msg) 139 msg.debug("expected:", g.state.directory, "received:", directory) 140 return 141 end 142 143 --apply fallbacks if the scan failed 144 if not list then 145 msg.warn("could not read directory", g.state.directory) 146 list, opts = {}, {} 147 opts.empty_text = g.style.warning..'Error: could not parse directory' 148 end 149 150 g.state.list = list 151 g.state.parser = opts.parser 152 153 --setting custom options from parsers 154 g.state.directory_label = opts.directory_label 155 g.state.empty_text = opts.empty_text or g.state.empty_text 156 157 --we assume that directory is only changed when redirecting to a different location 158 --therefore we need to change the `moving_adjacent` flag and clear some state values 159 if opts.directory then 160 g.state.directory = opts.directory 161 moving_adjacent = false 162 clear_non_adjacent_state(true) 163 end 164 165 if opts.selected_index then 166 g.state.selected = opts.selected_index or g.state.selected 167 if g.state.selected > #g.state.list then g.state.selected = #g.state.list 168 elseif g.state.selected < 1 then g.state.selected = 1 end 169 end 170 171 if moving_adjacent then cursor.select_prev_directory() 172 else cursor.select_playing_item() end 173 g.state.prev_directory = g.state.directory 174 end 175 176 ---rescans the folder and updates the list. 177 ---@param nav_type? NavigationType 178 ---@param cb? function 179 ---@param parse_properties? ParseProperties 180 ---@return thread # The coroutine for the triggered parse operation. May be aborted early if directory is in the cache. 181 function scanning.rescan(nav_type, cb, parse_properties) 182 if nav_type == nil then nav_type = NavType.RESCAN end 183 184 --we can only make assumptions about the directory label when moving from adjacent directories 185 if nav_type == NavType.GOTO or nav_type == NavType.REDIRECT then 186 clear_non_adjacent_state(nav_type == NavType.GOTO) 187 end 188 189 g.state.empty_text = "~" 190 g.state.list = {} 191 cursor.disable_select_mode() 192 ass.update_ass() 193 194 --the directory is always handled within a coroutine to allow addons to 195 --pause execution for asynchronous operations 196 ---@async 197 local co = fb_utils.coroutine.queue(function() 198 update_list(nav_type, parse_properties) 199 if g.state.empty_text == "~" then g.state.empty_text = "empty directory" end 200 201 ass.update_ass() 202 if cb then fb_utils.coroutine.run(cb) end 203 end) 204 205 g.state.co = co 206 return co 207 end 208 209 210 return scanning