summaryrefslogtreecommitdiff
path: root/mpv/scripts/file-browser/modules/addons/cache.lua
blob: b1011fdf5e50571434918078507dba2060dd447b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
--[[
    This file is an internal file-browser addon.
    It should not be imported like a normal module.

    Maintains a cache of the accessed directories to improve
    parsing speed. Disabled by default.
]]

local mp = require 'mp'
local msg = require 'mp.msg'
local utils = require 'mp.utils'

local fb = require 'file-browser'

---@type ParserConfig
local cacheParser = {
    name = 'cache',
    priority = 0,
    api_version = '1.9',
}

---@class CacheEntry
---@field list List
---@field opts Opts?
---@field timeout MPTimer

---@type table<string,CacheEntry>
local cache = {}

---@type table<string,(async fun(list: List?, opts: Opts?))[]>
local pending_parses = {}

---@param directories? string[]
local function clear_cache(directories)
    if directories then
        msg.debug('clearing cache for', #directories, 'directorie(s)')
        for _, dir in ipairs(directories) do
            if cache[dir] then
                msg.trace('clearing cache for', dir)
                cache[dir].timeout:kill()
                cache[dir] = nil
            end
        end
    else
        msg.debug('clearing cache')
        for _, entry in pairs(cache) do
            entry.timeout:kill()
        end
        cache = {}
    end
end

---@type string
local prev_directory = ''

function cacheParser:can_parse(directory, parse_state)
    -- allows the cache to be forcibly used or bypassed with the
    -- cache/use parse property.
    if parse_state.properties.cache and parse_state.properties.cache.use ~= nil then
        if parse_state.source == 'browser' then prev_directory = directory end
        return parse_state.properties.cache.use
    end

    -- the script message is guaranteed to always bypass the cache
    if parse_state.source == 'script-message' then return false end
    if not fb.get_opt('cache') or directory == '' then return false end

    -- clear the cache if reloading the current directory in the browser
    -- this means that fb.rescan() should maintain expected behaviour
    if parse_state.source == 'browser' then
        if prev_directory == directory then clear_cache({directory}) end
        prev_directory = directory
    end

    return true
end

---@async
function cacheParser:parse(directory)
    if cache[directory] then
        msg.verbose('fetching', directory, 'contents from cache')
        cache[directory].timeout:kill()
        cache[directory].timeout:resume()
        return cache[directory].list, cache[directory].opts
    end

    ---@type List?, Opts?
    local list, opts

    -- if another parse is already running on the same directory, then wait and use the same result
    if not pending_parses[directory] then
        pending_parses[directory] = {}
        list, opts = self:defer(directory)
    else
        msg.debug('parse for', directory, 'already running - waiting for other parse to finish...')
        table.insert(pending_parses[directory], fb.coroutine.callback(30))
        list, opts = coroutine.yield()
    end

    local pending = pending_parses[directory]
    -- need to clear the pending parses before resuming them or they will also attempt to resume the parses
    pending_parses[directory] = nil
    if pending and #pending > 0 then
        msg.debug('resuming', #pending, 'pending parses for', directory)
        for _, cb in ipairs(pending) do
            cb(list, opts)
        end
    end

    if not list then return end

    -- pending will be truthy for the original parse and falsy for any parses that were pending
    if pending then
        msg.debug('storing', directory, 'contents in cache')
        cache[directory] = {
            list = list,
            opts = opts,
            timeout = mp.add_timeout(120, function() cache[directory] = nil end),
        }
    end

    return list, opts
end

cacheParser.keybinds = {
    {
        key = 'Ctrl+Shift+r',
        name = 'clear',
        command = function() clear_cache() ; fb.rescan() end,
    }
}

-- provide method of clearing the cache through script messages
mp.register_script_message('cache/clear', function(dirs)
    if not dirs then
        return clear_cache()
    end

    ---@type string[]?
    local directories = utils.parse_json(dirs)
    if not directories then msg.error('unable to parse', dirs) end

    clear_cache(directories)
end)

return cacheParser