liquidsoap-prettier 1.8.2 → 1.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/check-formatting.yml +32 -0
- package/README.md +31 -5
- package/package.json +1 -1
- package/src/cli.js +104 -9
- package/tests/liq/audio.liq +460 -0
- package/tests/liq/autocue.liq +1081 -0
- package/tests/liq/clock.liq +14 -0
- package/tests/liq/cron.liq +74 -0
- package/tests/liq/error.liq +48 -0
- package/tests/liq/extra/audio.liq +677 -0
- package/tests/liq/extra/audioscrobbler.liq +482 -0
- package/tests/liq/extra/deprecations.liq +976 -0
- package/tests/liq/extra/externals.liq +196 -0
- package/tests/liq/extra/fades.liq +260 -0
- package/tests/liq/extra/file.liq +66 -0
- package/tests/liq/extra/http.liq +160 -0
- package/tests/liq/extra/interactive.liq +917 -0
- package/tests/liq/extra/metadata.liq +75 -0
- package/tests/liq/extra/native.liq +201 -0
- package/tests/liq/extra/openai.liq +150 -0
- package/tests/liq/extra/server.liq +177 -0
- package/tests/liq/extra/source.liq +476 -0
- package/tests/liq/extra/spinitron.liq +272 -0
- package/tests/liq/extra/telnet.liq +266 -0
- package/tests/liq/extra/video.liq +59 -0
- package/tests/liq/extra/visualization.liq +68 -0
- package/tests/liq/fades.liq +941 -0
- package/tests/liq/ffmpeg.liq +605 -0
- package/tests/liq/file.liq +387 -0
- package/tests/liq/getter.liq +74 -0
- package/tests/liq/hls.liq +329 -0
- package/tests/liq/http.liq +1048 -0
- package/tests/liq/http_codes.liq +447 -0
- package/tests/liq/icecast.liq +58 -0
- package/tests/liq/io.liq +106 -0
- package/tests/liq/liquidsoap.liq +31 -0
- package/tests/liq/list.liq +440 -0
- package/tests/liq/log.liq +47 -0
- package/tests/liq/lufs.liq +295 -0
- package/tests/liq/math.liq +23 -0
- package/tests/liq/medialib.liq +752 -0
- package/tests/liq/metadata.liq +253 -0
- package/tests/liq/nfo.liq +258 -0
- package/tests/liq/null.liq +71 -0
- package/tests/liq/playlist.liq +1347 -0
- package/tests/liq/predicate.liq +106 -0
- package/tests/liq/process.liq +93 -0
- package/tests/liq/profiler.liq +5 -0
- package/tests/liq/protocols.liq +1139 -0
- package/tests/liq/ref.liq +28 -0
- package/tests/liq/replaygain.liq +135 -0
- package/tests/liq/request.liq +467 -0
- package/tests/liq/resolvers.liq +33 -0
- package/tests/liq/runtime.liq +70 -0
- package/tests/liq/server.liq +99 -0
- package/tests/liq/settings.liq +41 -0
- package/tests/liq/socket.liq +33 -0
- package/tests/liq/source.liq +362 -0
- package/tests/liq/sqlite.liq +161 -0
- package/tests/liq/stdlib.liq +172 -0
- package/tests/liq/string.liq +476 -0
- package/tests/liq/switches.liq +197 -0
- package/tests/liq/testing.liq +37 -0
- package/tests/liq/thread.liq +161 -0
- package/tests/liq/tracks.liq +100 -0
- package/tests/liq/utils.liq +81 -0
- package/tests/liq/video.liq +918 -0
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
# A library to store the metadata of files in given folders and query them. This
|
|
2
|
+
# is useful to generate playlists based on metadata.
|
|
3
|
+
# @category File
|
|
4
|
+
# @param ~persistency Store the database in given file, which is reuse to populate the database on next run.
|
|
5
|
+
# @param ~refresh Scan directories for new files every given number of seconds (by default the database is never updated).
|
|
6
|
+
# @param ~standardize Function mapped on metadata when indexing. It can be used to change the field names to standard ones, pretreat data, etc.
|
|
7
|
+
# @param ~initial_progress Show progress of library being indexed at startup.
|
|
8
|
+
# @param ~directories Directories to look for files in.
|
|
9
|
+
# @param dir Directory to look for files in.
|
|
10
|
+
# @method find Find files according to conditions on metadata.
|
|
11
|
+
# @method refresh Update metadatas and look for new files.
|
|
12
|
+
# @method add_directory Add a new directory which should be scanned.
|
|
13
|
+
# @method clear Remove all known metadata.
|
|
14
|
+
def medialib(
|
|
15
|
+
~id=null,
|
|
16
|
+
~persistency=null,
|
|
17
|
+
~refresh=null,
|
|
18
|
+
~standardize=fun (m) -> m,
|
|
19
|
+
~initial_progress=true,
|
|
20
|
+
~directories=[],
|
|
21
|
+
dir=null
|
|
22
|
+
) =
|
|
23
|
+
id = string.id.default(default="medialib", id)
|
|
24
|
+
refresh_time = refresh
|
|
25
|
+
directories = ref(directories)
|
|
26
|
+
if null.defined(dir) then directories := null.get(dir)::directories() end
|
|
27
|
+
db = ref([])
|
|
28
|
+
|
|
29
|
+
def dt(t) =
|
|
30
|
+
string.float(decimal_places=2, time() - t)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Read metadata from file.
|
|
34
|
+
def metadata(f) =
|
|
35
|
+
m = file.metadata.native(f)
|
|
36
|
+
m = standardize(m)
|
|
37
|
+
|
|
38
|
+
# Sanitize
|
|
39
|
+
m = metadata.cover.remove(m)
|
|
40
|
+
m = list.assoc.filter(fun (k, _) -> not list.mem(k, ["priv", "rva2"]), m)
|
|
41
|
+
|
|
42
|
+
# Add more metadata
|
|
43
|
+
m = ("basename", path.basename(f))::m
|
|
44
|
+
m =
|
|
45
|
+
(
|
|
46
|
+
"last scan",
|
|
47
|
+
string.float(time())
|
|
48
|
+
)::m
|
|
49
|
+
m
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Whether an entry needs to be updated.
|
|
53
|
+
def needs_update(f, m) =
|
|
54
|
+
file.mtime(f) >
|
|
55
|
+
string.to_float(
|
|
56
|
+
m[
|
|
57
|
+
"last scan"
|
|
58
|
+
]
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Add a file to the database.
|
|
63
|
+
def add(f) =
|
|
64
|
+
# If file doesn't exist remove it
|
|
65
|
+
if
|
|
66
|
+
not (file.exists(f))
|
|
67
|
+
then
|
|
68
|
+
db := list.assoc.remove(f, db())
|
|
69
|
+
else
|
|
70
|
+
# New file or not recent enough metadata
|
|
71
|
+
if
|
|
72
|
+
not list.assoc.mem(f, db()) or needs_update(f, list.assoc(f, db()))
|
|
73
|
+
then
|
|
74
|
+
db := (f, metadata(f))::list.assoc.remove(f, db())
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Update database by renewing metadata and removing removed files.
|
|
80
|
+
def update(~progress=fun (_, _) -> ()) =
|
|
81
|
+
len = list.length(db())
|
|
82
|
+
n = ref(0)
|
|
83
|
+
nu = ref(0)
|
|
84
|
+
|
|
85
|
+
def u(fm) =
|
|
86
|
+
let (f, m) = fm
|
|
87
|
+
ref.incr(n)
|
|
88
|
+
progress(n(), len)
|
|
89
|
+
if
|
|
90
|
+
not (file.exists(f))
|
|
91
|
+
then
|
|
92
|
+
null
|
|
93
|
+
elsif
|
|
94
|
+
needs_update(f, m)
|
|
95
|
+
then
|
|
96
|
+
ref.incr(nu)
|
|
97
|
+
(f, metadata(f))
|
|
98
|
+
else
|
|
99
|
+
(f, m)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
db := list.filter_map(u, db())
|
|
104
|
+
log.debug(
|
|
105
|
+
label=id,
|
|
106
|
+
"Updated #{nu()} files."
|
|
107
|
+
)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Make sure that new files from directories are registered.
|
|
111
|
+
def scan(~progress=fun (_, _) -> ()) =
|
|
112
|
+
l =
|
|
113
|
+
list.map(
|
|
114
|
+
fun (d) -> file.ls(absolute=true, recursive=true, d),
|
|
115
|
+
directories()
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
l = list.flatten(l)
|
|
119
|
+
n = ref(0)
|
|
120
|
+
len = list.length(l)
|
|
121
|
+
|
|
122
|
+
def add(f) =
|
|
123
|
+
ref.incr(n)
|
|
124
|
+
progress(n(), len)
|
|
125
|
+
add(f)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
list.iter(add, l)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Increment when the format of the db changes
|
|
132
|
+
db_version = 1
|
|
133
|
+
|
|
134
|
+
# Load from the persistent file.
|
|
135
|
+
def load() =
|
|
136
|
+
db := []
|
|
137
|
+
if
|
|
138
|
+
null.defined(persistency)
|
|
139
|
+
then
|
|
140
|
+
f = null.get(persistency)
|
|
141
|
+
if
|
|
142
|
+
file.exists(f)
|
|
143
|
+
then
|
|
144
|
+
try
|
|
145
|
+
let json.parse ((v, parsed) :
|
|
146
|
+
(int * [(string * [(string * string)])]?)
|
|
147
|
+
) = file.contents(f)
|
|
148
|
+
|
|
149
|
+
if
|
|
150
|
+
v == db_version and null.defined(parsed)
|
|
151
|
+
then
|
|
152
|
+
db := null.get(parsed)
|
|
153
|
+
end
|
|
154
|
+
catch e do
|
|
155
|
+
log.important(
|
|
156
|
+
label=id,
|
|
157
|
+
"Failed to parse persistent file #{f}: #{e.kind}: #{e.message}"
|
|
158
|
+
)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Store the file in a persistent file.
|
|
165
|
+
def store() =
|
|
166
|
+
if
|
|
167
|
+
null.defined(persistency)
|
|
168
|
+
then
|
|
169
|
+
f = null.get(persistency)
|
|
170
|
+
data = json.stringify(compact=true, (db_version, db()))
|
|
171
|
+
file.write(data=data, f)
|
|
172
|
+
log.info(
|
|
173
|
+
label=id,
|
|
174
|
+
"Wrote persistent file #{f}"
|
|
175
|
+
)
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Refresh the library.
|
|
180
|
+
def refresh() =
|
|
181
|
+
log.info(
|
|
182
|
+
label=id,
|
|
183
|
+
"Refreshing the library..."
|
|
184
|
+
)
|
|
185
|
+
t = time()
|
|
186
|
+
update()
|
|
187
|
+
scan()
|
|
188
|
+
store()
|
|
189
|
+
log.info(
|
|
190
|
+
label=id,
|
|
191
|
+
"Refreshed the library in #{dt(t)}s."
|
|
192
|
+
)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Find all files matching given criteria.
|
|
196
|
+
def find(
|
|
197
|
+
~case_sensitive=true,
|
|
198
|
+
~artist=null,
|
|
199
|
+
~artist_contains=null,
|
|
200
|
+
~artist_matches=null,
|
|
201
|
+
~album=null,
|
|
202
|
+
~genre=null,
|
|
203
|
+
~title=null,
|
|
204
|
+
~title_contains=null,
|
|
205
|
+
~filename=null,
|
|
206
|
+
~filename_contains=null,
|
|
207
|
+
~filename_matches=null,
|
|
208
|
+
~year=null,
|
|
209
|
+
~year_ge=null,
|
|
210
|
+
~year_lt=null,
|
|
211
|
+
~bpm=null,
|
|
212
|
+
~bpm_ge=null,
|
|
213
|
+
~bpm_lt=null,
|
|
214
|
+
~predicate=(fun (_) -> true)
|
|
215
|
+
) =
|
|
216
|
+
def p(m) =
|
|
217
|
+
def eq(s, t) =
|
|
218
|
+
if case_sensitive then s == t else string.case(s) == string.case(t) end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def contains(s, t) =
|
|
222
|
+
if
|
|
223
|
+
case_sensitive
|
|
224
|
+
then
|
|
225
|
+
string.contains(substring=s, t)
|
|
226
|
+
else
|
|
227
|
+
string.contains(substring=string.case(s), string.case(t))
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def eqf(k, v) =
|
|
232
|
+
null.defined(v) ? eq(m[k], null.get(v)) : true
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def ctf(k, v) =
|
|
236
|
+
null.defined(v) ? contains(null.get(v), m[k]) : true
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def mtf(k, v) =
|
|
240
|
+
null.defined(v) ? string.match(pattern=null.get(v), m[k]) : true
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
eqf("artist", artist)
|
|
244
|
+
and ctf("artist", artist_contains)
|
|
245
|
+
and mtf("artist", artist_matches)
|
|
246
|
+
and eqf("album", album)
|
|
247
|
+
and eqf("genre", genre)
|
|
248
|
+
and eqf("title", title)
|
|
249
|
+
and ctf("title", title_contains)
|
|
250
|
+
and eqf("filename", filename)
|
|
251
|
+
and ctf("basename", filename_contains)
|
|
252
|
+
and mtf("basename", filename_matches)
|
|
253
|
+
and if
|
|
254
|
+
null.defined(year) or null.defined(year_ge) or null.defined(year_lt)
|
|
255
|
+
then
|
|
256
|
+
if
|
|
257
|
+
string.is_int(m["year"])
|
|
258
|
+
then
|
|
259
|
+
y = string.to_int(m["year"])
|
|
260
|
+
(null.defined(year) ? y == null.get(year) : true)
|
|
261
|
+
and (null.defined(year_ge) ? y >= null.get(year_ge) : true)
|
|
262
|
+
and (null.defined(year_lt) ? y < null.get(year_lt) : true)
|
|
263
|
+
else
|
|
264
|
+
false
|
|
265
|
+
end
|
|
266
|
+
else
|
|
267
|
+
true
|
|
268
|
+
end
|
|
269
|
+
and if
|
|
270
|
+
null.defined(bpm) or null.defined(bpm_ge) or null.defined(bpm_lt)
|
|
271
|
+
then
|
|
272
|
+
if
|
|
273
|
+
string.is_int(m["bpm"])
|
|
274
|
+
then
|
|
275
|
+
b = string.to_int(m["bpm"])
|
|
276
|
+
(null.defined(bpm) ? b == null.get(bpm) : true)
|
|
277
|
+
and (null.defined(bpm_ge) ? b >= null.get(bpm_ge) : true)
|
|
278
|
+
and (null.defined(bpm_lt) ? b < null.get(bpm_lt) : true)
|
|
279
|
+
else
|
|
280
|
+
false
|
|
281
|
+
end
|
|
282
|
+
else
|
|
283
|
+
true
|
|
284
|
+
end
|
|
285
|
+
and predicate(m)
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
l = list.filter(fun (fm) -> p(snd(fm)), db())
|
|
289
|
+
l = list.map(fst, l)
|
|
290
|
+
l
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
t = time()
|
|
294
|
+
load()
|
|
295
|
+
log.important(
|
|
296
|
+
label=id,
|
|
297
|
+
"Loaded library from #{persistency} in #{dt(t)}s: #{list.length(db())} \
|
|
298
|
+
entries"
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
t = time()
|
|
302
|
+
progress =
|
|
303
|
+
if
|
|
304
|
+
initial_progress
|
|
305
|
+
then
|
|
306
|
+
fun (n, l) ->
|
|
307
|
+
print(
|
|
308
|
+
newline=false,
|
|
309
|
+
"#{id}: updating #{n * 100 / l}%...\r"
|
|
310
|
+
)
|
|
311
|
+
else
|
|
312
|
+
fun (_, _) -> ()
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
update(progress=progress)
|
|
316
|
+
log.important(
|
|
317
|
+
label=id,
|
|
318
|
+
"Updated library in #{dt(t)}s: #{list.length(db())} entries"
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
t = time()
|
|
322
|
+
progress =
|
|
323
|
+
if
|
|
324
|
+
initial_progress
|
|
325
|
+
then
|
|
326
|
+
fun (n, l) ->
|
|
327
|
+
print(
|
|
328
|
+
newline=false,
|
|
329
|
+
"#{id}: scanning #{n * 100 / l}%...\r"
|
|
330
|
+
)
|
|
331
|
+
else
|
|
332
|
+
fun (_, _) -> ()
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
scan(progress=progress)
|
|
336
|
+
log.important(
|
|
337
|
+
label=id,
|
|
338
|
+
"Scanned new files in #{dt(t)}s: #{list.length(db())} entries"
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
store()
|
|
342
|
+
log.important(
|
|
343
|
+
label=id,
|
|
344
|
+
"Stored library"
|
|
345
|
+
)
|
|
346
|
+
if
|
|
347
|
+
null.defined(refresh_time)
|
|
348
|
+
then
|
|
349
|
+
thread.run(
|
|
350
|
+
delay=null.get(refresh_time),
|
|
351
|
+
every=null.get(refresh_time),
|
|
352
|
+
refresh
|
|
353
|
+
)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def clear() =
|
|
357
|
+
db := []
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def add_directory(d) =
|
|
361
|
+
directories := d::directories()
|
|
362
|
+
scan()
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
{find = find, refresh = refresh, add_directory = add_directory, clear = clear}
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
%ifdef sqlite
|
|
369
|
+
# A library to store the metadata of files in given folders and query
|
|
370
|
+
# them. This is useful to generate playlists based on metadata. This version
|
|
371
|
+
# use an SQL implementation which should be much faster and less memory
|
|
372
|
+
# consuming than the basic one.
|
|
373
|
+
# @category File
|
|
374
|
+
# @param ~persistency Store the database in given file, which is reuse to populate the database on next run.
|
|
375
|
+
# @param ~refresh Scan directories for new files every given number of seconds (by default the database is never updated).
|
|
376
|
+
# @param ~standardize Function mapped on metadata when indexing. It can be used to change the field names to standard ones, pretreat data, etc.
|
|
377
|
+
# @param ~initial_progress Show progress of library being indexed at startup.
|
|
378
|
+
# @param ~directories Directories to look for files in.
|
|
379
|
+
# @param dir Directory to look for files in.
|
|
380
|
+
# @method find Find files according to conditions on metadata.
|
|
381
|
+
# @method refresh Update metadatas and look for new files.
|
|
382
|
+
# @method add_directory Add a new directory which should be scanned.
|
|
383
|
+
# @method clear Remove all known metadata.
|
|
384
|
+
def medialib.sqlite(
|
|
385
|
+
~id=null,
|
|
386
|
+
~database,
|
|
387
|
+
~refresh=null,
|
|
388
|
+
~standardize=fun (m) -> m,
|
|
389
|
+
~initial_progress=true,
|
|
390
|
+
~directories=[],
|
|
391
|
+
dir=null
|
|
392
|
+
) =
|
|
393
|
+
id = string.id.default(default="medialib.sqlite", id)
|
|
394
|
+
refresh_time = refresh
|
|
395
|
+
directories = ref(directories)
|
|
396
|
+
if null.defined(dir) then directories := null.get(dir)::directories() end
|
|
397
|
+
|
|
398
|
+
fields_string = ["artist", "title", "album", "genre", "basename"]
|
|
399
|
+
fields_int = ["year", "bpm"]
|
|
400
|
+
fields_float = ["last_scan"]
|
|
401
|
+
|
|
402
|
+
db = sqlite(database)
|
|
403
|
+
begin
|
|
404
|
+
fields_string = list.map(fun (l) -> (l, "STRING"), fields_string)
|
|
405
|
+
fields_int = list.map(fun (l) -> (l, "INT"), fields_int)
|
|
406
|
+
fields_float = list.map(fun (l) -> (l, "FLOAT"), fields_float)
|
|
407
|
+
db.table.create(
|
|
408
|
+
"metadata",
|
|
409
|
+
preserve=true,
|
|
410
|
+
[
|
|
411
|
+
(
|
|
412
|
+
"file",
|
|
413
|
+
"STRING PRIMARY KEY"
|
|
414
|
+
),
|
|
415
|
+
...fields_string,
|
|
416
|
+
...fields_int,
|
|
417
|
+
...fields_float
|
|
418
|
+
]
|
|
419
|
+
)
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def dt(t) =
|
|
423
|
+
string.float(decimal_places=2, time() - t)
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
# Read metadata from file.
|
|
427
|
+
def metadata(f) =
|
|
428
|
+
m = file.metadata.native(f)
|
|
429
|
+
m = standardize(m)
|
|
430
|
+
|
|
431
|
+
# Sanitize
|
|
432
|
+
m = metadata.cover.remove(m)
|
|
433
|
+
m = list.assoc.filter(fun (k, _) -> not list.mem(k, ["priv", "rva2"]), m)
|
|
434
|
+
|
|
435
|
+
# Add more metadata
|
|
436
|
+
m = ("basename", path.basename(f))::m
|
|
437
|
+
m = ("last_scan", string.float(time()))::m
|
|
438
|
+
m
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
# Whether an entry needs to be updated.
|
|
442
|
+
def needs_update(f, last_scan) =
|
|
443
|
+
file.mtime(f) > last_scan
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
# Remove file from the database
|
|
447
|
+
def remove(f) =
|
|
448
|
+
db.delete(table="metadata", where="file=#{sqlite.escape(f)}")
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
# Add a file to the database.
|
|
452
|
+
def add(f) =
|
|
453
|
+
# If file doesn't exist remove it
|
|
454
|
+
if
|
|
455
|
+
not (file.exists(f))
|
|
456
|
+
then
|
|
457
|
+
remove(f)
|
|
458
|
+
else
|
|
459
|
+
count = db.count(table="metadata", where="file=#{sqlite.escape(f)}")
|
|
460
|
+
def last_scan() =
|
|
461
|
+
let sqlite.query ([{last_scan}] : [{last_scan: float}]) =
|
|
462
|
+
db.select(
|
|
463
|
+
"last_scan",
|
|
464
|
+
table="metadata",
|
|
465
|
+
where="file=#{sqlite.escape(f)}"
|
|
466
|
+
)
|
|
467
|
+
last_scan
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
# New file or not recent enough metadata
|
|
471
|
+
if
|
|
472
|
+
count == 0 or needs_update(f, last_scan())
|
|
473
|
+
then
|
|
474
|
+
m = metadata(f)
|
|
475
|
+
def field(~map, k) =
|
|
476
|
+
def map(x) =
|
|
477
|
+
# Harden
|
|
478
|
+
try
|
|
479
|
+
map(x)
|
|
480
|
+
catch _ do
|
|
481
|
+
null
|
|
482
|
+
end
|
|
483
|
+
end
|
|
484
|
+
if list.assoc.mem(k, m) then map(list.assoc(k, m)) else null end
|
|
485
|
+
end
|
|
486
|
+
id = fun (x) -> x
|
|
487
|
+
m =
|
|
488
|
+
{
|
|
489
|
+
file = f,
|
|
490
|
+
artist = field(map=id, "artist"),
|
|
491
|
+
title = field(map=id, "title"),
|
|
492
|
+
album = field(map=id, "album"),
|
|
493
|
+
genre = field(map=id, "genre"),
|
|
494
|
+
basename = field(map=id, "basename"),
|
|
495
|
+
last_scan = field(map=float_of_string, "last_scan"),
|
|
496
|
+
year = field(map=int_of_string, "year"),
|
|
497
|
+
bpm = field(map=int_of_string, "bpm")
|
|
498
|
+
}
|
|
499
|
+
db.insert(table="metadata", replace=true, m)
|
|
500
|
+
end
|
|
501
|
+
end
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
# Number of entries in the database
|
|
505
|
+
def count() =
|
|
506
|
+
db.count(table="metadata")
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
# Update database by renewing metadata and removing removed files.
|
|
510
|
+
def update(~progress=fun (_, _) -> ()) =
|
|
511
|
+
len = count()
|
|
512
|
+
n = ref(0)
|
|
513
|
+
nu = ref(0)
|
|
514
|
+
|
|
515
|
+
def u(row) =
|
|
516
|
+
ref.incr(n)
|
|
517
|
+
progress(n(), len)
|
|
518
|
+
let sqlite.row ({file} : {file: string}) = row
|
|
519
|
+
add(file)
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
db.select.iter(u, "file", table="metadata")
|
|
523
|
+
log.debug(
|
|
524
|
+
label=id,
|
|
525
|
+
"Updated #{nu()} files."
|
|
526
|
+
)
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
# Make sure that new files from directories are registered.
|
|
530
|
+
def scan(~progress=fun (_, _) -> ()) =
|
|
531
|
+
l =
|
|
532
|
+
list.map(
|
|
533
|
+
fun (d) -> file.ls(absolute=true, recursive=true, d),
|
|
534
|
+
directories()
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
l = list.flatten(l)
|
|
538
|
+
n = ref(0)
|
|
539
|
+
len = list.length(l)
|
|
540
|
+
|
|
541
|
+
def add(f) =
|
|
542
|
+
ref.incr(n)
|
|
543
|
+
progress(n(), len)
|
|
544
|
+
add(f)
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
list.iter(add, l)
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
# Refresh the library.
|
|
551
|
+
def refresh() =
|
|
552
|
+
log.info(
|
|
553
|
+
label=id,
|
|
554
|
+
"Refreshing the library..."
|
|
555
|
+
)
|
|
556
|
+
t = time()
|
|
557
|
+
update()
|
|
558
|
+
scan()
|
|
559
|
+
log.info(
|
|
560
|
+
label=id,
|
|
561
|
+
"Refreshed the library in #{dt(t)}s."
|
|
562
|
+
)
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
# Find all files matching given criteria.
|
|
566
|
+
def find(
|
|
567
|
+
~case_sensitive=true,
|
|
568
|
+
~artist=null,
|
|
569
|
+
~artist_contains=null,
|
|
570
|
+
~artist_matches=null,
|
|
571
|
+
~album=null,
|
|
572
|
+
~genre=null,
|
|
573
|
+
~title=null,
|
|
574
|
+
~title_contains=null,
|
|
575
|
+
~filename=null,
|
|
576
|
+
~filename_contains=null,
|
|
577
|
+
~filename_matches=null,
|
|
578
|
+
~year=null,
|
|
579
|
+
~year_ge=null,
|
|
580
|
+
~year_lt=null,
|
|
581
|
+
~bpm=null,
|
|
582
|
+
~bpm_ge=null,
|
|
583
|
+
~bpm_lt=null,
|
|
584
|
+
~condition=null
|
|
585
|
+
) =
|
|
586
|
+
predicates = ref([])
|
|
587
|
+
def pred(p) =
|
|
588
|
+
predicates := p::predicates()
|
|
589
|
+
end
|
|
590
|
+
if null.defined(condition) then pred(null.get(condition)) end
|
|
591
|
+
def cmp(op, k, v) =
|
|
592
|
+
if
|
|
593
|
+
null.defined(v)
|
|
594
|
+
then
|
|
595
|
+
v = null.get(v)
|
|
596
|
+
p =
|
|
597
|
+
if
|
|
598
|
+
case_sensitive
|
|
599
|
+
then
|
|
600
|
+
(
|
|
601
|
+
"#{k} #{op} #{sqlite.escape(v)}"
|
|
602
|
+
)
|
|
603
|
+
else
|
|
604
|
+
(
|
|
605
|
+
"UPPER(#{k}) #{op} UPPER(#{sqlite.escape(v)})"
|
|
606
|
+
)
|
|
607
|
+
end
|
|
608
|
+
pred(p)
|
|
609
|
+
end
|
|
610
|
+
end
|
|
611
|
+
def eqf(k, v) =
|
|
612
|
+
cmp("=", k, v)
|
|
613
|
+
end
|
|
614
|
+
def ctf(k, v) =
|
|
615
|
+
if
|
|
616
|
+
null.defined(v)
|
|
617
|
+
then
|
|
618
|
+
v = null.get(v)
|
|
619
|
+
cmp("LIKE", k, "%" ^ v ^ "%")
|
|
620
|
+
end
|
|
621
|
+
end
|
|
622
|
+
def mtf(k, v) =
|
|
623
|
+
cmp("MATCHES", k, v)
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
eqf("artist", artist)
|
|
627
|
+
ctf("artist", artist_contains)
|
|
628
|
+
mtf("artist", artist_matches)
|
|
629
|
+
eqf("album", album)
|
|
630
|
+
eqf("genre", genre)
|
|
631
|
+
eqf("title", title)
|
|
632
|
+
ctf("title", title_contains)
|
|
633
|
+
eqf("filename", filename)
|
|
634
|
+
ctf("basename", filename_contains)
|
|
635
|
+
mtf("basename", filename_matches)
|
|
636
|
+
if
|
|
637
|
+
null.defined(year)
|
|
638
|
+
then
|
|
639
|
+
year = null.get(year)
|
|
640
|
+
pred("year=#{year}")
|
|
641
|
+
end
|
|
642
|
+
if
|
|
643
|
+
null.defined(year_ge)
|
|
644
|
+
then
|
|
645
|
+
year_ge = null.get(year_ge)
|
|
646
|
+
pred(
|
|
647
|
+
"year >= #{year_ge}"
|
|
648
|
+
)
|
|
649
|
+
end
|
|
650
|
+
if
|
|
651
|
+
null.defined(year_lt)
|
|
652
|
+
then
|
|
653
|
+
year_lt = null.get(year_lt)
|
|
654
|
+
pred(
|
|
655
|
+
"year < #{year_lt}"
|
|
656
|
+
)
|
|
657
|
+
end
|
|
658
|
+
if
|
|
659
|
+
null.defined(bpm)
|
|
660
|
+
then
|
|
661
|
+
bpm = null.get(bpm)
|
|
662
|
+
pred("bpm=#{bpm}")
|
|
663
|
+
end
|
|
664
|
+
if
|
|
665
|
+
null.defined(bpm_ge)
|
|
666
|
+
then
|
|
667
|
+
bpm_ge = null.get(bpm_ge)
|
|
668
|
+
pred(
|
|
669
|
+
"bpm >= #{bpm_ge}"
|
|
670
|
+
)
|
|
671
|
+
end
|
|
672
|
+
if
|
|
673
|
+
null.defined(bpm_lt)
|
|
674
|
+
then
|
|
675
|
+
bpm_lt = null.get(bpm_lt)
|
|
676
|
+
pred(
|
|
677
|
+
"bpm < #{bpm_lt}"
|
|
678
|
+
)
|
|
679
|
+
end
|
|
680
|
+
predicates =
|
|
681
|
+
string.concat(
|
|
682
|
+
separator=" AND ",
|
|
683
|
+
predicates()
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
let sqlite.query (l : [{file: string}]) =
|
|
687
|
+
db.select("file", table="metadata", where=predicates)
|
|
688
|
+
list.map((fun (l) -> l.file), l)
|
|
689
|
+
end
|
|
690
|
+
|
|
691
|
+
t = time()
|
|
692
|
+
progress =
|
|
693
|
+
if
|
|
694
|
+
initial_progress
|
|
695
|
+
then
|
|
696
|
+
fun (n, l) ->
|
|
697
|
+
print(
|
|
698
|
+
newline=false,
|
|
699
|
+
"#{id}: updating #{n * 100 / l}%...\r"
|
|
700
|
+
)
|
|
701
|
+
else
|
|
702
|
+
fun (_, _) -> ()
|
|
703
|
+
end
|
|
704
|
+
|
|
705
|
+
update(progress=progress)
|
|
706
|
+
log.important(
|
|
707
|
+
label=id,
|
|
708
|
+
"Updated library in #{dt(t)}s: #{count()} entries"
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
t = time()
|
|
712
|
+
progress =
|
|
713
|
+
if
|
|
714
|
+
initial_progress
|
|
715
|
+
then
|
|
716
|
+
fun (n, l) ->
|
|
717
|
+
print(
|
|
718
|
+
newline=false,
|
|
719
|
+
"#{id}: scanning #{n * 100 / l}%...\r"
|
|
720
|
+
)
|
|
721
|
+
else
|
|
722
|
+
fun (_, _) -> ()
|
|
723
|
+
end
|
|
724
|
+
|
|
725
|
+
scan(progress=progress)
|
|
726
|
+
log.important(
|
|
727
|
+
label=id,
|
|
728
|
+
"Scanned new files in #{dt(t)}s: #{count()} entries"
|
|
729
|
+
)
|
|
730
|
+
|
|
731
|
+
if
|
|
732
|
+
null.defined(refresh_time)
|
|
733
|
+
then
|
|
734
|
+
thread.run(
|
|
735
|
+
delay=null.get(refresh_time),
|
|
736
|
+
every=null.get(refresh_time),
|
|
737
|
+
refresh
|
|
738
|
+
)
|
|
739
|
+
end
|
|
740
|
+
|
|
741
|
+
def clear() =
|
|
742
|
+
db.delete(table="metadata")
|
|
743
|
+
end
|
|
744
|
+
|
|
745
|
+
def add_directory(d) =
|
|
746
|
+
directories := d::directories()
|
|
747
|
+
scan()
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
{find = find, refresh = refresh, add_directory = add_directory, clear = clear}
|
|
751
|
+
end
|
|
752
|
+
%endif
|