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,476 @@
1
+ # Apply a function to the first track of a source
2
+ # @category Source / Track processing
3
+ # @flag extra
4
+ # @param ~id Force the value of the source ID.
5
+ # @param fn The applied function.
6
+ # @param s The input source.
7
+ def map_first_track(~id=null("map_first_track"), fn, s) =
8
+ fallback(id=id, [fn((once(s) : source)), s])
9
+ end
10
+
11
+ # Same operator as rotate but merges tracks from each sources.
12
+ # For instance, `rotate.merge([intro,main,outro])` creates a source
13
+ # that plays a sequence `[intro,main,outro]` as single track and loops back.
14
+ # @category Source / Track processing
15
+ # @flag extra
16
+ # @param ~id Force the value of the source ID.
17
+ # @param sources Sequence of sources to be merged.
18
+ def rotate.merge(~id=null("rotate.merge"), sources) =
19
+ sources =
20
+ (sources :
21
+ [
22
+ source.{
23
+ on_select?: (
24
+ {ending: source?, replay_metadata: bool, starting: source}
25
+ )->source,
26
+ on_leave?: ({source: source, track_sensitive: bool})->unit,
27
+ single?: bool,
28
+ track_sensitive?: getter(bool),
29
+ weight?: getter(int)
30
+ }
31
+ ]
32
+ )
33
+
34
+ ready = ref(true)
35
+ duration = frame.duration()
36
+
37
+ def to_first({starting, ..._}) =
38
+ ready := (not ready())
39
+ (sequence(merge=true, [blank(duration=duration), (starting : source)]) :
40
+ source
41
+ )
42
+ end
43
+
44
+ sources =
45
+ list.mapi(
46
+ fun (i, (s:source)) ->
47
+ if i == 0 then s.{on_select = to_first} else (s : source) end,
48
+ sources
49
+ )
50
+
51
+ s = rotate(sources)
52
+ let {track_marks = _, ...tracks} = source.tracks(s)
53
+ s = source(tracks).{replay_metadata = false}
54
+ switch(id=id, [(ready, s), ({not ready()}, s)])
55
+ end
56
+
57
+ # Rotate between overlapping sources. Next track starts according
58
+ # to 'liq_start_next' offset metadata.
59
+ # @category Source / Track processing
60
+ # @flag extra
61
+ # @param ~id Force the value of the source ID.
62
+ # @param ~start_next Metadata field indicating when the next track should start, relative to current track's time.
63
+ # @param ~weights Relative weight of the sources in the sum. The empty list stands for the homogeneous distribution.
64
+ # @param sources Sources to toggle from
65
+ def overlap_sources(
66
+ ~id=null("overlap_sources"),
67
+ ~normalize=false,
68
+ ~start_next="liq_start_next",
69
+ ~weights=[],
70
+ sources
71
+ ) =
72
+ position = ref(0)
73
+ length = list.length(sources)
74
+
75
+ def current_position() =
76
+ pos = position()
77
+ position := (pos + 1) mod length
78
+ pos
79
+ end
80
+
81
+ ready_list = list.map(fun (_) -> ref(false), sources)
82
+ grab_ready = fun (n) -> list.nth(default=ref(false), ready_list, n)
83
+
84
+ def set_ready(pos, b) =
85
+ is_ready = grab_ready(pos)
86
+ is_ready := b
87
+ end
88
+
89
+ # Start next track on_offset
90
+ def on_start_next(_, _) =
91
+ set_ready(current_position(), true)
92
+ end
93
+
94
+ def on_offset(s) =
95
+ let (s, offset) = metadata.getter.source.float(-1., start_next, s)
96
+ s.on_position(
97
+ synchronous=true,
98
+ allow_partial=true,
99
+ position=offset,
100
+ on_start_next
101
+ )
102
+ end
103
+
104
+ list.iter(on_offset, sources)
105
+
106
+ # Disable after each track
107
+ def disable(pos, s) =
108
+ def disable(_) =
109
+ set_ready(pos, false)
110
+ end
111
+
112
+ s.on_track(synchronous=true, disable)
113
+ end
114
+
115
+ list.iteri(disable, sources)
116
+
117
+ # Relay metadata from all sources
118
+ send_to_main_source = ref(fun (_) -> ())
119
+
120
+ def relay_metadata(m) =
121
+ fn = send_to_main_source()
122
+ fn(m)
123
+ end
124
+
125
+ list.iter(fun (s) -> s.on_metadata(synchronous=true, relay_metadata), sources)
126
+
127
+ def drop_metadata(s) =
128
+ let {metadata = _, ...tracks} = source.tracks(s)
129
+ source(tracks)
130
+ end
131
+
132
+ # Now drop all metadata
133
+ sources = list.map(drop_metadata, sources)
134
+
135
+ # Wrap sources into switches.
136
+ def make_switch(pos, source) =
137
+ is_ready = grab_ready(pos)
138
+ switch([(is_ready, source)])
139
+ end
140
+
141
+ sources = list.mapi(make_switch, sources)
142
+
143
+ # Initiate the whole thing.
144
+ set_ready(current_position(), true)
145
+
146
+ # Create main source
147
+ s = add(id=id, normalize=normalize, weights=weights, sources)
148
+
149
+ # Set send_to_main_source
150
+ send_to_main_source := fun (m) -> s.insert_metadata(m)
151
+ s
152
+ end
153
+
154
+ # Append speech-synthesized tracks reading the metadata.
155
+ # @category Metadata
156
+ # @flag extra
157
+ # @param ~pattern Pattern to use
158
+ # @param s The source to use
159
+ def source.say_metadata =
160
+ def pattern(m) =
161
+ artist = m["artist"]
162
+ title = m["title"]
163
+ artist_predicate =
164
+ if
165
+ artist != ""
166
+ then
167
+ "It was #{artist} playing "
168
+ else
169
+ ""
170
+ end
171
+
172
+ say_metadata = "#{artist_predicate}#{title}"
173
+ say_metadata = r/:/g.replace(fun (_) -> '$(colon)', say_metadata)
174
+ say_metadata =
175
+ say_metadata == ""
176
+ ? "Sorry, I do not know what this song title was"
177
+ : say_metadata
178
+
179
+ "say:#{say_metadata}"
180
+ end
181
+
182
+ fun (~id=null("source.say_metadata"), ~pattern=pattern, s) ->
183
+ append(id=id, s, fun (m) -> once(single(pattern(m))))
184
+ end
185
+
186
+ # Regularly insert track boundaries in a stream (useful for testing tracks).
187
+ # @category Source / Track processing
188
+ # @flag extra
189
+ # @param ~every Duration of a track (in seconds).
190
+ # @param ~metadata Metadata for tracks.
191
+ # @param s The stream.
192
+ def chop(~every=getter(3.), ~metadata=getter([]), s) =
193
+ # Track time in the source's context:
194
+ time = ref(0.)
195
+
196
+ s = source.methods(s)
197
+
198
+ is_first = ref(true)
199
+
200
+ def f() =
201
+ time := time() + settings.frame.duration()
202
+ if
203
+ is_first() or getter.get(every) <= time()
204
+ then
205
+ is_first := false
206
+ time := 0.
207
+ s.insert_metadata(new_track=true, getter.get(metadata))
208
+ end
209
+ end
210
+
211
+ s.on_frame(synchronous=true, f)
212
+ s
213
+ end
214
+
215
+ # Regularly skip tracks from a source (useful for testing skipping).
216
+ # @category Source / Track processing
217
+ # @flag extra
218
+ # @param ~every How often to skip tracks.
219
+ # @param s The stream.
220
+ # @flag extra
221
+ def skipper(~every=getter(5.), s) =
222
+ start_time = ref(0.)
223
+
224
+ def f() =
225
+ if
226
+ getter.get(every) <= s.time() - start_time()
227
+ then
228
+ start_time := s.time()
229
+ s.skip()
230
+ end
231
+ end
232
+
233
+ s.on_frame(f)
234
+ s
235
+ end
236
+
237
+ # Special track insensitive fallback that always skips current song before
238
+ # switching.
239
+ # @category Source / Track processing
240
+ # @flag extra
241
+ # @param s The main source.
242
+ # @param ~fallback The fallback source. Defaults to `blank` if `null`.
243
+ def fallback.skip(s, ~fallback:f=null) =
244
+ f = f ?? (blank() : source)
245
+ avail = ref(true)
246
+
247
+ def check() =
248
+ old = avail()
249
+ avail := source.is_ready(s)
250
+ if not old and avail() then source.skip(f) end
251
+ end
252
+
253
+ s = fallback([s, f])
254
+
255
+ # TODO: could we have something more efficient that checking on every frame
256
+ s.on_frame(synchronous=true, check)
257
+ s
258
+ end
259
+
260
+ # Generate a CUE file for the source. This function will generate a new track in
261
+ # the file for each metadata of the source. This function tries to map metadata to
262
+ # the appropriate CUE file standard values. You can use the `map_metadata` argument
263
+ # to add your own pre-processing. The following metadata are recognized on tracks:
264
+ # `"title"`, `"artist"`, `"album"`, `"isrc"`, and `"cue_year"`.
265
+ # @category Source / Track processing
266
+ # @flag extra
267
+ # @param filename Path where the CUE file should be written.
268
+ # @param ~last_tracks Only report the number of last tracks.
269
+ # @param ~title Title of the stream.
270
+ # @param ~file File where the stream is stored.
271
+ # @param ~file_type Format in which the stream is stored.
272
+ # @param ~comment Comment about the stream.
273
+ # @param ~year Year for the stream.
274
+ # @param ~map_metadata Function to apply to metadata before writing the CUE file (useful for pre-processing metadata).
275
+ # @param ~temp_dir Temporary directory for atomic write.
276
+ # @param ~deduplicate_using To avoid duplicate entries, duplicate metadata are \
277
+ # filtered. Set this to a list of labels to use for detecting \
278
+ # duplicated metadata.
279
+ # @param ~delete Delete the CUE files when starting if it exists.
280
+ def source.cue(
281
+ ~title=null,
282
+ ~performer=null,
283
+ ~file:f=getter(null),
284
+ ~file_type=null,
285
+ ~comment=null,
286
+ ~year=null,
287
+ ~map_metadata=fun (m) -> (m : [(string * string)]),
288
+ ~last_tracks=null,
289
+ ~temp_dir=null,
290
+ ~deduplicate_using=["title", "artist", "album", "isrc", "cue_year"],
291
+ ~delete=true,
292
+ filename,
293
+ s
294
+ ) =
295
+ is_first = ref(true)
296
+ current_filename = ref("")
297
+
298
+ def write(~append, entries) =
299
+ filename = current_filename()
300
+ f = getter.get(f)
301
+ file_type = file_type ?? file.extension(leading_dot=false, f ?? "")
302
+
303
+ if
304
+ append
305
+ then
306
+ log(
307
+ label="source.cue",
308
+ level=4,
309
+ "Writing new entry to #{filename}"
310
+ )
311
+ else
312
+ log(
313
+ label="source.cue",
314
+ level=4,
315
+ "Writing full CUE file at #{filename}"
316
+ )
317
+ if delete and file.exists(filename) then file.remove(filename) end
318
+ end
319
+
320
+ write =
321
+ file.write.stream(append=append, atomic=true, temp_dir=temp_dir, filename)
322
+
323
+ # Append to the file.
324
+ def w(data) =
325
+ write(data ^ "\n")
326
+ end
327
+
328
+ # Write a tag.
329
+ def tag(~indent=0, ~quote=true, name, (value:string?)) =
330
+ quote = if quote then fun (v) -> string.quote(v) else fun (v) -> v end
331
+
332
+ if
333
+ null.defined(value)
334
+ then
335
+ s =
336
+ "#{string.spaces(indent)}#{name} #{quote(null.get(value))}"
337
+ w(s)
338
+ end
339
+ end
340
+
341
+ if
342
+ is_first() or not append
343
+ then
344
+ is_first := false
345
+
346
+ tag("TITLE", title)
347
+ tag("PERFORMER", performer)
348
+ tag(
349
+ "REM COMMENT",
350
+ comment
351
+ )
352
+ tag(
353
+ quote=false,
354
+ "REM DATE",
355
+ null.map(string.of_int, year)
356
+ )
357
+ if
358
+ null.defined(f)
359
+ then
360
+ w(
361
+ "FILE \"#{(null.get(f) : string)}\" #{string.uppercase(file_type)}"
362
+ )
363
+ end
364
+ end
365
+
366
+ list.iter(
367
+ fun (entry) ->
368
+ begin
369
+ let {position = p, time = t, metadata = m} = entry
370
+
371
+ tag(
372
+ indent=2,
373
+ quote=false,
374
+ "TRACK",
375
+ "#{string.of_int(digits=2, p)} AUDIO"
376
+ )
377
+ tag(indent=4, "TITLE", list.assoc.nullable("title", m))
378
+ tag(indent=4, "PERFORMER", list.assoc.nullable("artist", m))
379
+ tag(
380
+ indent=4,
381
+ "REM ALBUM",
382
+ list.assoc.nullable("album", m)
383
+ )
384
+ tag(
385
+ indent=4,
386
+ quote=false,
387
+ "REM DATE",
388
+ list.assoc.nullable("cue_year", m)
389
+ )
390
+ tag(indent=4, quote=false, "ISRC", list.assoc.nullable("isrc", m))
391
+
392
+ frames = int_of_float((t - floor(t)) * 75.)
393
+ t = int_of_float(t)
394
+ minutes = t / 60
395
+ seconds = t mod 60
396
+ m = string.of_int(digits=2, minutes)
397
+ s = string.of_int(digits=2, seconds)
398
+ f = string.of_int(digits=2, frames)
399
+ tag(
400
+ indent=4,
401
+ quote=false,
402
+ "INDEX 01",
403
+ "#{m}:#{s}:#{f}"
404
+ )
405
+ end,
406
+ entries
407
+ )
408
+
409
+ write("")
410
+ end
411
+
412
+ entries = ref([])
413
+ current_position = ref(1)
414
+ t0 = ref(source.time(s))
415
+
416
+ def handle_metadata(m) =
417
+ filename = getter.get(filename)
418
+ if
419
+ current_filename() != filename
420
+ then
421
+ log(
422
+ label="source.cue",
423
+ level=4,
424
+ "Opening new CUE sheet at #{filename}"
425
+ )
426
+ t0 := source.time(s)
427
+ current_position := 1
428
+ current_filename := filename
429
+ is_first := true
430
+ end
431
+
432
+ m = map_metadata(m)
433
+ entry =
434
+ {
435
+ position = current_position(),
436
+ time = source.time(s) - t0(),
437
+ metadata = m
438
+ }
439
+ ref.incr(current_position)
440
+
441
+ if
442
+ null.defined(last_tracks)
443
+ then
444
+ current_entries =
445
+ null.case(
446
+ last_tracks,
447
+ entries,
448
+ fun (last_tracks) ->
449
+ list.rev(list.prefix(last_tracks - 1, list.rev(entries())))
450
+ )
451
+ entries := [...current_entries, entry]
452
+ write(append=false, entries())
453
+ else
454
+ write(append=true, [entry])
455
+ end
456
+ end
457
+
458
+ s = metadata.deduplicate(using=deduplicate_using, s)
459
+ s.on_metadata(synchronous=true, handle_metadata)
460
+ s.on_frame(
461
+ before=false,
462
+ synchronous=true,
463
+ fun () ->
464
+ begin
465
+ # If filename has changed but no metadata handler was executed,
466
+ # insert one
467
+ if
468
+ current_filename() != getter.get(filename)
469
+ then
470
+ s.insert_metadata(new_track=true, s.last_metadata() ?? [])
471
+ end
472
+ end
473
+ )
474
+
475
+ s
476
+ end