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,28 @@
1
+ # Create a reference from a pair of get / set functions.
2
+ # @category Programming
3
+ # @param get Function to retrieve the value of the reference.
4
+ # @param set Function to change the value of the reference.
5
+ def ref.make(get, set) =
6
+ (get.{set = set} : ref)
7
+ end
8
+
9
+ # Create a getter from a reference (sometimes useful to remove the `set`
10
+ # method).
11
+ # @category Programming
12
+ def ref.getter((r:ref)) =
13
+ {r()}
14
+ end
15
+
16
+ # Map functions to a reference.
17
+ # @category Programming
18
+ # @param g Function to apply to the getter.
19
+ # @param s Function to apply to the setter.
20
+ def ref.map(g, s, (r:ref)) =
21
+ ref.make({g(r())}, fun (x) -> r.set(s(x)))
22
+ end
23
+
24
+ # Increment a reference to an integer.
25
+ # @category Programming
26
+ def ref.incr(r) =
27
+ r := r() + 1
28
+ end
@@ -0,0 +1,135 @@
1
+ let file.replaygain = ()
2
+
3
+ # Compute the ReplayGain for a file (in dB).
4
+ # @category File
5
+ # @param ~id Force the value of the source ID.
6
+ # @param ~ratio Decoding ratio. A value of `50` means try to decode the file `50x` faster than real time, if possible.
7
+ # Use this setting to lower CPU peaks when computing replaygain tags.
8
+ # @param file_name File name.
9
+ # @flag hidden
10
+ def file.replaygain.compute(~ratio=50., file_name) =
11
+ _request = request.create(resolve_metadata=false, file_name)
12
+ if
13
+ request.resolve(_request)
14
+ then
15
+ get_gain = ref(fun () -> null)
16
+ def process(s) =
17
+ s = source.replaygain.compute(s)
18
+ get_gain := {s.gain()}
19
+ s
20
+ end
21
+
22
+ request.process(ratio=ratio, process=process, _request)
23
+
24
+ fn = get_gain()
25
+ fn()
26
+ else
27
+ null
28
+ end
29
+ end
30
+
31
+ # Extract the ReplayGain from the metadata (in dB).
32
+ # @category Metadata
33
+ # @param _metadata Metadata from which the ReplayGain should be extracted.
34
+ def metadata.replaygain(_metadata) =
35
+ if
36
+ list.assoc.mem("r128_track_gain", _metadata)
37
+ then
38
+ try
39
+ float_of_string(_metadata["r128_track_gain"]) / 256.
40
+ catch _ do
41
+ null
42
+ end
43
+ elsif
44
+ list.assoc.mem("replaygain_track_gain", _metadata)
45
+ then
46
+ replaygain_metadata = _metadata["replaygain_track_gain"]
47
+ match = r/([+-]?\d*\.?\d*)/.exec(replaygain_metadata)
48
+ try
49
+ float_of_string(list.assoc(1, match))
50
+ catch _ do
51
+ null
52
+ end
53
+ else
54
+ null
55
+ end
56
+ end
57
+
58
+ # Get the ReplayGain for a file (in dB).
59
+ # @category File
60
+ # @param ~id Force the value of the source ID.
61
+ # @param ~compute Compute ReplayGain if metadata tag is empty.
62
+ # @param ~ratio Decoding ratio. A value of `50` means try to decode the file `50x` faster than real time, if possible.
63
+ # Use this setting to lower CPU peaks when computing replaygain tags.
64
+ # @param file_name File name.
65
+ def replaces file.replaygain(~id=null, ~compute=true, ~ratio=50., file_name) =
66
+ id = string.id.default(default="file.replaygain", id)
67
+ file_name_quoted = string.quote(file_name)
68
+
69
+ _metadata = file.metadata(exclude=decoder.metadata.reentrant(), file_name)
70
+ gain = metadata.replaygain(_metadata)
71
+
72
+ if
73
+ gain != null
74
+ then
75
+ log.info(
76
+ label=id,
77
+ "Detected track replaygain #{gain} dB for #{file_name_quoted}."
78
+ )
79
+ gain
80
+ elsif
81
+ compute
82
+ then
83
+ log.info(
84
+ label=id,
85
+ "Computing replay gain for #{file_name_quoted}."
86
+ )
87
+ start_time = time()
88
+ gain = file.replaygain.compute(ratio=ratio, file_name)
89
+ elapsed_time = time() - start_time
90
+ if
91
+ gain != null
92
+ then
93
+ log.info(
94
+ label=id,
95
+ "Computed replay gain #{gain} dB for #{file_name_quoted} (time: #{
96
+ elapsed_time
97
+ } s)."
98
+ )
99
+ end
100
+ gain
101
+ else
102
+ null
103
+ end
104
+ end
105
+
106
+ # Enable ReplayGain metadata resolver. This resolver will process any file
107
+ # decoded by Liquidsoap and add a `replaygain_track_gain` metadata when this
108
+ # value could be computed. For a finer-grained replay gain processing, use the
109
+ # `replaygain:` protocol.
110
+ # @param ~compute Compute replaygain if metadata tag is empty.
111
+ # @param ~ratio Decoding ratio. A value of `50.` means try to decode the file `50x` faster than real time, if possible. Use this setting to lower CPU peaks when computing replaygain tags.
112
+ # @category Liquidsoap
113
+ def enable_replaygain_metadata(~compute=true, ~ratio=50.) =
114
+ def replaygain_metadata(~metadata:_, file_name) =
115
+ gain = file.replaygain(compute=compute, ratio=ratio, file_name)
116
+ if
117
+ gain != null
118
+ then
119
+ [
120
+ (
121
+ settings.normalize_track_gain_metadata(),
122
+ "#{null.get(gain)} dB"
123
+ )
124
+ ]
125
+ else
126
+ []
127
+ end
128
+ end
129
+
130
+ decoder.metadata.add(
131
+ reentrant=true,
132
+ "replaygain_track_gain",
133
+ replaygain_metadata
134
+ )
135
+ end
@@ -0,0 +1,467 @@
1
+ %ifdef native
2
+ let stdlib_native = native
3
+ %endif
4
+
5
+ # @docof request.dynamic
6
+ # @param ~native Use native implementation, when available.
7
+ def replaces request.dynamic(%argsof(request.dynamic), ~native=false, f) =
8
+ ignore(native)
9
+ default = request.dynamic(%argsof(request.dynamic), f)
10
+ %ifdef native
11
+ if
12
+ native
13
+ then
14
+ stdlib_native.request.dynamic(%argsof(request.dynamic), f)
15
+ else
16
+ default
17
+ end
18
+ %else
19
+ default
20
+ %endif
21
+ end
22
+
23
+ # Play a queue of requests (the first added request gets played first).
24
+ # @category Source / Track processing
25
+ # @param ~id Force the value of the source ID.
26
+ # @param ~interactive Should the queue be controllable via telnet?
27
+ # @param ~prefetch How many requests should be queued in advance.
28
+ # @param ~native Use native implementation, when available.
29
+ # @param ~queue Initial queue of requests.
30
+ # @param ~timeout Timeout (in sec.) to resolve the request.
31
+ # @method add This method is internal and should not be used. Consider using `push` instead.
32
+ # @method push Push a request on the request queue.
33
+ # @method length Length of the queue.
34
+ def request.queue(
35
+ ~id=null,
36
+ ~interactive=true,
37
+ ~prefetch=null,
38
+ ~native=false,
39
+ ~queue=[],
40
+ ~timeout=null
41
+ ) =
42
+ ignore(native)
43
+ id = string.id.default(default="request.queue", id)
44
+ initial_queue = ref(queue)
45
+ queue = ref([])
46
+ fetch = ref(fun () -> ())
47
+ started = ref(false)
48
+
49
+ def next() =
50
+ if
51
+ queue() != []
52
+ then
53
+ let [r, ...q] = queue()
54
+ queue := q
55
+ (r : request)
56
+ else
57
+ null
58
+ end
59
+ end
60
+
61
+ def push(r) =
62
+ if
63
+ started()
64
+ then
65
+ log.info(
66
+ label=id,
67
+ "Pushing #{r} on the queue."
68
+ )
69
+ queue := [...queue(), r]
70
+ fn = fetch()
71
+ fn()
72
+ else
73
+ log.info(
74
+ label=id,
75
+ "Pushing #{r} on the initial queue."
76
+ )
77
+ initial_queue := [...initial_queue(), r]
78
+ end
79
+ end
80
+
81
+ def push_uri(uri) =
82
+ r = request.create(uri)
83
+ push(r)
84
+ end
85
+
86
+ default =
87
+ fun () ->
88
+ request.dynamic(
89
+ id=id,
90
+ prefetch=prefetch,
91
+ timeout=timeout,
92
+ available={not list.is_empty(queue())},
93
+ next
94
+ )
95
+
96
+ s =
97
+ %ifdef native
98
+ if
99
+ native
100
+ then
101
+ stdlib_native.request.dynamic(id=id, timeout=timeout, next)
102
+ else
103
+ default()
104
+ end
105
+ %else
106
+ default()
107
+ %endif
108
+
109
+ def add(r) =
110
+ log.severe(
111
+ label=s.id(),
112
+ "Please use #{s.id()}.push instead of #{s.id()}.add()"
113
+ )
114
+
115
+ s.add(r)
116
+ end
117
+
118
+ def set_queue(q) =
119
+ if started() then queue := q else initial_queue := q end
120
+ s.set_queue([])
121
+ end
122
+
123
+ def get_queue() =
124
+ [...s.queue(), ...initial_queue(), ...queue()]
125
+ end
126
+
127
+ s =
128
+ s.{
129
+ add = add,
130
+ push = push.{uri = push_uri},
131
+ length = {list.length(queue()) + list.length(s.queue())},
132
+ set_queue = set_queue,
133
+ queue = get_queue
134
+ }
135
+
136
+ s.on_wake_up(
137
+ synchronous=true,
138
+ fun () ->
139
+ begin
140
+ started := true
141
+ s.set_queue(initial_queue())
142
+ initial_queue := []
143
+ end
144
+ )
145
+
146
+ fetch := s.fetch
147
+
148
+ if
149
+ interactive
150
+ then
151
+ def push(uri) =
152
+ r = request.create(uri)
153
+ push(r)
154
+ "#{request.id(r)}"
155
+ end
156
+
157
+ s.register_command(
158
+ description="Flush the queue and skip the current track",
159
+ "flush_and_skip",
160
+ fun (_) ->
161
+ try
162
+ s.set_queue([])
163
+ s.skip()
164
+ "Done."
165
+ catch err do
166
+ "Error while flushing and skipping source: #{err}"
167
+ end
168
+ )
169
+
170
+ s.register_command(
171
+ description="Push a new request in the queue.",
172
+ usage="push <uri>",
173
+ "push",
174
+ push
175
+ )
176
+
177
+ def show_queue(_) =
178
+ queue = s.queue()
179
+ string.concat(
180
+ separator=" ",
181
+ list.map(fun (r) -> string(request.id(r)), queue)
182
+ )
183
+ end
184
+
185
+ s.register_command(
186
+ description="Display current queue content.",
187
+ usage="queue",
188
+ "queue",
189
+ show_queue
190
+ )
191
+
192
+ def skip(_) =
193
+ s.skip()
194
+ "Done."
195
+ end
196
+
197
+ s.register_command(
198
+ description="Skip current song.",
199
+ usage="skip",
200
+ "skip",
201
+ skip
202
+ )
203
+ end
204
+
205
+ s
206
+ end
207
+
208
+ # Play a request once and become unavailable.
209
+ # @category Source / Input / Passive
210
+ # @param ~timeout Timeout in seconds for resolving the request.
211
+ # @param r Request to play.
212
+ def request.once(~id=null("request.once"), ~timeout=null, r) =
213
+ id = string.id.default(default="request.once", id)
214
+
215
+ done = ref(false)
216
+ fail = fallback([])
217
+
218
+ def next() =
219
+ if
220
+ done()
221
+ then
222
+ null
223
+ else
224
+ done := true
225
+ if
226
+ request.resolve(r, timeout=timeout)
227
+ then
228
+ request.queue(queue=[r])
229
+ else
230
+ log.critical(
231
+ label=id,
232
+ "Failed to prepare track: request not ready."
233
+ )
234
+ request.destroy(r)
235
+ fail
236
+ end
237
+ end
238
+ end
239
+
240
+ s = source.dynamic(self_sync=false, track_sensitive=true, next)
241
+ (s : source_methods).{
242
+ resolve = fun () -> request.resolve(r, timeout=timeout),
243
+ request = r
244
+ }
245
+ end
246
+
247
+ # Loop on a request. It never fails if the request is static, meaning
248
+ # that it can be fetched once. Typically, http, ftp, say requests are
249
+ # static, and time is not.
250
+ # @category Source / Input / Passive
251
+ # @param ~prefetch How many requests should be queued in advance.
252
+ # @param ~timeout Timeout (in sec.) to resolve the request.
253
+ # @param ~fallible Enforce fallibility of the request.
254
+ # @param r Request
255
+ def request.single(
256
+ ~id=null("request.single"),
257
+ ~prefetch=null,
258
+ ~timeout=null,
259
+ ~fallible=null,
260
+ r
261
+ ) =
262
+ id = string.id.default(default="single", id)
263
+
264
+ fallible = fallible ?? not getter.is_constant(r)
265
+
266
+ infallible =
267
+ if
268
+ not fallible
269
+ then
270
+ if
271
+ not getter.is_constant(r)
272
+ then
273
+ error.raise(
274
+ error.invalid,
275
+ "Infallible sources cannot change their underlying file."
276
+ )
277
+ end
278
+
279
+ initial_request = getter.get(r)
280
+ uri = request.uri(initial_request)
281
+ request.is_static(uri)
282
+ else
283
+ false
284
+ end
285
+
286
+ if
287
+ not fallible and not infallible
288
+ then
289
+ log.severe(
290
+ label=id,
291
+ "Source was marked a infallible but its request is not a static file. The \
292
+ source is considered fallible for backward compatibility but this will \
293
+ fail in future versions!"
294
+ )
295
+ end
296
+
297
+ static_request = ref(null)
298
+
299
+ def on_wake_up(s) =
300
+ if
301
+ infallible
302
+ then
303
+ initial_request = getter.get(r)
304
+ uri = request.uri(initial_request)
305
+
306
+ log.important(
307
+ label=id,
308
+ "#{uri} is static, resolving once for all..."
309
+ )
310
+
311
+ if
312
+ not request.resolve(initial_request, timeout=timeout, content_type=s)
313
+ then
314
+ request.destroy(initial_request)
315
+ error.raise(
316
+ error.invalid,
317
+ "Could not resolve uri: #{uri}"
318
+ )
319
+ else
320
+ static_request := initial_request
321
+ end
322
+ end
323
+ end
324
+
325
+ def on_shutdown() =
326
+ if
327
+ null.defined(static_request())
328
+ then
329
+ request.destroy(null.get(static_request()))
330
+ end
331
+ static_request := null
332
+ end
333
+
334
+ def next() =
335
+ static_request() ?? getter.get(r)
336
+ end
337
+
338
+ def mk_source(id) =
339
+ request.dynamic(id=id, prefetch=prefetch, synchronous=infallible, next)
340
+ end
341
+
342
+ # We want to mark infallible source as such. `source.dynamic` is a nice
343
+ # way to do it as it will raise a user-friendly error in case the underlying
344
+ # source does not respect the conditions for being infallible.
345
+ s =
346
+ if
347
+ infallible
348
+ then
349
+ s = mk_source("#{id}.actual")
350
+ source.dynamic(
351
+ id=id,
352
+ infallible=infallible,
353
+ self_sync=false,
354
+ track_sensitive=true,
355
+ {s}
356
+ )
357
+ else
358
+ mk_source(id)
359
+ end
360
+
361
+ s.on_wake_up(synchronous=true, fun () -> on_wake_up(s))
362
+ s.on_shutdown(synchronous=true, on_shutdown)
363
+ (s : source_methods)
364
+ end
365
+
366
+ # Loop on a URI. It never fails if the request is static, meaning
367
+ # that it can be fetched once. Typically, http, ftp, say requests are
368
+ # static, and time is not.
369
+ # @category Source / Input / Passive
370
+ # @param ~prefetch How many requests should be queued in advance.
371
+ # @param ~timeout Timeout (in sec.) to resolve the request.
372
+ # @param ~fallible Enforce fallibility of the request.
373
+ # @param ~cue_in_metadata Metadata for cue in points. Disabled if `null`.
374
+ # @param ~cue_out_metadata Metadata for cue out points. Disabled if `null`.
375
+ # @param uri URI where to find the file
376
+ def single(
377
+ %argsof(request.single[!id,!fallible]),
378
+ ~id=null("single"),
379
+ ~fallible=false,
380
+ ~cue_in_metadata=null("liq_cue_in"),
381
+ ~cue_out_metadata=null("liq_cue_out"),
382
+ uri
383
+ ) =
384
+ r =
385
+ if
386
+ fallible
387
+ then
388
+ getter(
389
+ {
390
+ request.create(
391
+ cue_in_metadata=cue_in_metadata,
392
+ cue_out_metadata=cue_out_metadata,
393
+ uri
394
+ )
395
+ }
396
+ )
397
+ else
398
+ request.create(
399
+ persistent=true,
400
+ cue_in_metadata=cue_in_metadata,
401
+ cue_out_metadata=cue_out_metadata,
402
+ uri
403
+ )
404
+ end
405
+
406
+ request.single(%argsof(request.single), r)
407
+ end
408
+
409
+ # Create a source on which plays immediately requests given with the `play`
410
+ # method.
411
+ # @category Source / Track processing
412
+ # @param ~simultaneous Allow multiple requests to play simultaneously. If `false` a new request replaces the previous one.
413
+ # @method play Play a request.
414
+ # @method length Number of currently playing requests.
415
+ def request.player(~simultaneous=true) =
416
+ if
417
+ simultaneous
418
+ then
419
+ l = ref([])
420
+ s = ref(add(normalize=false, l()))
421
+
422
+ # Perform some garbage collection in order to avoid that the list grows too
423
+ # much.
424
+ def collect(~reload_source) =
425
+ len = list.length(l())
426
+ l := list.filter(source.is_ready, l())
427
+ if
428
+ reload_source and list.length(l()) != len
429
+ then
430
+ s := add(normalize=false, l())
431
+ end
432
+ end
433
+
434
+ def play(r) =
435
+ collect(reload_source=false)
436
+ l := [request.once(r), ...l()]
437
+ s := add(normalize=false, l())
438
+ end
439
+
440
+ source.dynamic(s).{
441
+ play = play,
442
+ length =
443
+ {
444
+ collect(reload_source=true)
445
+ list.length(l())
446
+ }
447
+ }
448
+ else
449
+ next_source = ref(null)
450
+
451
+ def next() =
452
+ s = next_source()
453
+ next_source := null
454
+ s
455
+ end
456
+
457
+ s = source.dynamic(next)
458
+
459
+ def play(r) =
460
+ r = request.once(r)
461
+ r = s.prepare(r)
462
+ next_source := r
463
+ end
464
+
465
+ s.{play = play, length = {1}}
466
+ end
467
+ end
@@ -0,0 +1,33 @@
1
+ # @flag hidden
2
+ def youtube_playlist_parser(~pwd="", url) =
3
+ ignore(pwd)
4
+ binary = null.get(settings.protocol.youtube_dl.path())
5
+
6
+ def parse_line(line) =
7
+ let json.parse (parsed : {url: string}?) = line
8
+ parsed = parsed ?? {url = "foo"}
9
+ url = parsed.url
10
+ ([], "youtube-dl:#{url}")
11
+ end
12
+
13
+ if
14
+ r/^youtube-pl/.test(url)
15
+ then
16
+ uri = list.nth(default="", r/:/.split(url), 1)
17
+ list.map(
18
+ parse_line,
19
+ process.read.lines(
20
+ "#{binary} -j --flat-playlist #{uri}"
21
+ )
22
+ )
23
+ else
24
+ []
25
+ end
26
+ end
27
+
28
+ playlist.parse.register(
29
+ name="youtube-dl",
30
+ mimes=[],
31
+ strict=true,
32
+ youtube_playlist_parser
33
+ )