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.
Files changed (67) hide show
  1. package/.github/workflows/check-formatting.yml +32 -0
  2. package/README.md +31 -5
  3. package/package.json +1 -1
  4. package/src/cli.js +104 -9
  5. package/tests/liq/audio.liq +460 -0
  6. package/tests/liq/autocue.liq +1081 -0
  7. package/tests/liq/clock.liq +14 -0
  8. package/tests/liq/cron.liq +74 -0
  9. package/tests/liq/error.liq +48 -0
  10. package/tests/liq/extra/audio.liq +677 -0
  11. package/tests/liq/extra/audioscrobbler.liq +482 -0
  12. package/tests/liq/extra/deprecations.liq +976 -0
  13. package/tests/liq/extra/externals.liq +196 -0
  14. package/tests/liq/extra/fades.liq +260 -0
  15. package/tests/liq/extra/file.liq +66 -0
  16. package/tests/liq/extra/http.liq +160 -0
  17. package/tests/liq/extra/interactive.liq +917 -0
  18. package/tests/liq/extra/metadata.liq +75 -0
  19. package/tests/liq/extra/native.liq +201 -0
  20. package/tests/liq/extra/openai.liq +150 -0
  21. package/tests/liq/extra/server.liq +177 -0
  22. package/tests/liq/extra/source.liq +476 -0
  23. package/tests/liq/extra/spinitron.liq +272 -0
  24. package/tests/liq/extra/telnet.liq +266 -0
  25. package/tests/liq/extra/video.liq +59 -0
  26. package/tests/liq/extra/visualization.liq +68 -0
  27. package/tests/liq/fades.liq +941 -0
  28. package/tests/liq/ffmpeg.liq +605 -0
  29. package/tests/liq/file.liq +387 -0
  30. package/tests/liq/getter.liq +74 -0
  31. package/tests/liq/hls.liq +329 -0
  32. package/tests/liq/http.liq +1048 -0
  33. package/tests/liq/http_codes.liq +447 -0
  34. package/tests/liq/icecast.liq +58 -0
  35. package/tests/liq/io.liq +106 -0
  36. package/tests/liq/liquidsoap.liq +31 -0
  37. package/tests/liq/list.liq +440 -0
  38. package/tests/liq/log.liq +47 -0
  39. package/tests/liq/lufs.liq +295 -0
  40. package/tests/liq/math.liq +23 -0
  41. package/tests/liq/medialib.liq +752 -0
  42. package/tests/liq/metadata.liq +253 -0
  43. package/tests/liq/nfo.liq +258 -0
  44. package/tests/liq/null.liq +71 -0
  45. package/tests/liq/playlist.liq +1347 -0
  46. package/tests/liq/predicate.liq +106 -0
  47. package/tests/liq/process.liq +93 -0
  48. package/tests/liq/profiler.liq +5 -0
  49. package/tests/liq/protocols.liq +1139 -0
  50. package/tests/liq/ref.liq +28 -0
  51. package/tests/liq/replaygain.liq +135 -0
  52. package/tests/liq/request.liq +467 -0
  53. package/tests/liq/resolvers.liq +33 -0
  54. package/tests/liq/runtime.liq +70 -0
  55. package/tests/liq/server.liq +99 -0
  56. package/tests/liq/settings.liq +41 -0
  57. package/tests/liq/socket.liq +33 -0
  58. package/tests/liq/source.liq +362 -0
  59. package/tests/liq/sqlite.liq +161 -0
  60. package/tests/liq/stdlib.liq +172 -0
  61. package/tests/liq/string.liq +476 -0
  62. package/tests/liq/switches.liq +197 -0
  63. package/tests/liq/testing.liq +37 -0
  64. package/tests/liq/thread.liq +161 -0
  65. package/tests/liq/tracks.liq +100 -0
  66. package/tests/liq/utils.liq +81 -0
  67. 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