summaryrefslogtreecommitdiff
path: root/mac/.config/mpv/scripts/fuzzydir.lua
blob: 22119da565c431c21e63430a17461738ce635822 (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
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
--[[
	fuzzydir / by sibwaf / https://github.com/sibwaf/mpv-scripts

	Allows using "**" wildcards in sub-file-paths and audio-file-paths
    so you don't have to specify all the possible directory names.

    Basically, allows you to do this and never have the need to edit any paths ever again:
    audio-file-paths = **
    sub-file-paths = **

	MIT license - do whatever you want, but I'm not responsible for any possible problems.
	Please keep the URL to the original repository. Thanks!
]]

--[[
    Configuration:

    # max_search_depth

    Determines the max depth of recursive search, should be >= 1

    Examples for "sub-file-paths = **":
    "max_search_depth = 1" => mpv will be able to find [xyz.ass, subs/xyz.ass]
    "max_search_depth = 2" => mpv will be able to find [xyz.ass, subs/xyz.ass, subs/moresubs/xyz.ass]

    Please be careful when setting this value too high as it can result in awful performance or even stack overflow


    # discovery_threshold

    fuzzydir will skip paths which contain more than discovery_threshold directories in them

    This is done to keep at least some garbage from getting into *-file-paths properties in case of big collections:
    - dir1 <- will be ignored on opening video.mp4 as it's probably unrelated to the file
    - ...
    - dir999 <- will be ignored
    - video.mp4

    Use 0 to disable this behavior completely


    # use_powershell

    fuzzydir will use PowerShell to traverse directories when it's available

    Can be faster in some cases, but can also be significantly slower
]]

local max_search_depth = 3
local discovery_threshold = 10
local use_powershell = false

----------

local utils = require("mp.utils")
local msg = require("mp.msg")

local default_audio_paths = mp.get_property_native("options/audio-file-paths")
local default_sub_paths = mp.get_property_native("options/sub-file-paths")

function foreach(list, action)
	for _, item in pairs(list) do
		action(item)
	end
end

function starts_with(str, prefix)
	return string.sub(str, 1, string.len(prefix)) == prefix
end

function ends_with(str, suffix)
	return suffix == "" or string.sub(str, -string.len(suffix)) == suffix
end

function add_all(to, from)
	for index, element in pairs(from) do
		table.insert(to, element)
	end
end

function contains(t, e)
	for index, element in pairs(t) do
		if element == e then
			return true
		end
	end
	return false
end

function normalize(path)
	if path == "." then
		return ""
	end

	if starts_with(path, "./") or starts_with(path, ".\\") then
		path = string.sub(path, 3, -1)
	end
	if ends_with(path, "/") or ends_with(path, "\\") then
		path = string.sub(path, 1, -2)
	end

	return path
end

function call_command(command)
	local command_string = ""
	for _, part in pairs(command) do
		command_string = command_string .. part .. " "
	end

	msg.trace("Calling external command:", command_string)

	local process = mp.command_native({
		name = "subprocess",
		playback_only = false,
		capture_stdout = true,
		capture_stderr = true,
		args = command,
	})

	if process.status ~= 0 then
		msg.verbose("External command failed with status " .. process.status .. ": " .. command_string)
		if process.stderr ~= "" then
			msg.debug(process.stderr)
		end

		return nil
	end

	local result = {}
	for line in string.gmatch(process.stdout, "([^\r\n]+)") do
		table.insert(result, line)
	end
	return result
end

-- Platform-dependent optimization

local powershell_version = nil
if use_powershell then
	powershell_version = call_command({
		"powershell",
		"-NoProfile",
		"-Command",
		"$Host.Version.Major",
	})
end
if powershell_version ~= nil then
	powershell_version = tonumber(powershell_version[1])
end
if powershell_version == nil then
	powershell_version = -1
end
msg.debug("PowerShell version", powershell_version)

function fast_readdir(path)
	if powershell_version >= 3 then
		msg.trace("Scanning", path, "with PowerShell")
		result = call_command({
			"powershell",
			"-NoProfile",
			"-Command",
			[[
            $dirs = Get-ChildItem -LiteralPath ]] .. string.format("%q", path) .. [[ -Directory
            foreach($dir in $dirs) {
                $u8clip = [System.Text.Encoding]::UTF8.GetBytes($dir.Name)
                [Console]::OpenStandardOutput().Write($u8clip, 0, $u8clip.Length)
                Write-Host ""
            } ]],
		})
		msg.trace("Finished scanning", path, "with PowerShell")
		return result
	end

	msg.trace("Scanning", path, "with default readdir")
	result = utils.readdir(path, "dirs")
	msg.trace("Finished scanning", path, "with default readdir")
	return result
end

-- Platform-dependent optimization end

function traverse(search_path, current_path, level, cache)
	local full_path = utils.join_path(search_path, current_path)

	if level > max_search_depth then
		msg.trace("Traversed too deep, skipping scan for", full_path)
		return {}
	end

	if cache[full_path] ~= nil then
		msg.trace("Returning from cache for", full_path)
		return cache[full_path]
	end

	local result = {}

	local discovered_paths = fast_readdir(full_path)
	if discovered_paths == nil then
		-- noop
		msg.debug("Unable to scan " .. full_path .. ", skipping")
	elseif discovery_threshold > 0 and #discovered_paths > discovery_threshold then
		-- noop
		msg.debug("Too many directories in " .. full_path .. ", skipping")
	else
		for _, discovered_path in pairs(discovered_paths) do
			local new_path = utils.join_path(current_path, discovered_path)

			table.insert(result, new_path)
			add_all(result, traverse(search_path, new_path, level + 1, cache))
		end
	end

	cache[full_path] = result

	return result
end

function explode(raw_paths, search_path, cache)
	local result = {}
	for _, raw_path in pairs(raw_paths) do
		local parent, leftover = utils.split_path(raw_path)
		if leftover == "**" then
			msg.trace("Expanding wildcard for", raw_path)
			table.insert(result, parent)
			add_all(result, traverse(search_path, parent, 1, cache))
		else
			msg.trace("Path", raw_path, "doesn't have a wildcard, keeping as-is")
			table.insert(result, raw_path)
		end
	end

	local normalized = {}
	for index, path in pairs(result) do
		local normalized_path = normalize(path)
		if not contains(normalized, normalized_path) and normalized_path ~= "" then
			table.insert(normalized, normalized_path)
		end
	end

	return normalized
end

function explode_all()
	msg.debug("max_search_depth = " .. max_search_depth .. ", discovery_threshold = " .. discovery_threshold)

	local video_path = mp.get_property("path")
	local search_path, _ = utils.split_path(video_path)
	msg.debug("search_path = " .. search_path)

	local cache = {}

	foreach(default_audio_paths, function(it)
		msg.debug("audio-file-paths:", it)
	end)
	local audio_paths = explode(default_audio_paths, search_path, cache)
	foreach(audio_paths, function(it)
		msg.debug("Adding to audio-file-paths:", it)
	end)
	mp.set_property_native("options/audio-file-paths", audio_paths)

	msg.verbose("Done expanding audio-file-paths")

	foreach(default_sub_paths, function(it)
		msg.debug("sub-file-paths:", it)
	end)
	local sub_paths = explode(default_sub_paths, search_path, cache)
	foreach(sub_paths, function(it)
		msg.debug("Adding to sub-file-paths:", it)
	end)
	mp.set_property_native("options/sub-file-paths", sub_paths)

	msg.verbose("Done expanding sub-file-paths")

	msg.debug("Done expanding paths")
end

mp.add_hook("on_load", 50, explode_all)