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,387 @@
1
+ # @docof file.temp
2
+ # @param ~cleanup Delete the file on shutdown
3
+ def file.temp(~cleanup=false, %argsof(file.temp), prefix, suffix) =
4
+ f = file.temp(%argsof(file.temp), prefix, suffix)
5
+ if cleanup then on_cleanup({file.remove(f)}) end
6
+ f
7
+ end
8
+
9
+ # @docof file.temp_dir
10
+ # @param ~cleanup Delete the file on shutdown
11
+ def file.temp_dir(~cleanup=false, prefix, suffix="") =
12
+ dir = file.temp_dir(prefix, suffix)
13
+ if cleanup then on_cleanup({file.rmdir(dir)}) end
14
+ dir
15
+ end
16
+
17
+ # Read the content of a file. Returns a function of type `()->string`. File is
18
+ # done reading when function returns the empty string `""`.
19
+ # @category File
20
+ # @method close Close the underlying file descriptor without waiting for the whole file to be read.
21
+ def file.read(fname) =
22
+ fd = file.open(write=false, fname)
23
+ is_done = ref(false)
24
+
25
+ def close() =
26
+ if
27
+ not is_done()
28
+ then
29
+ is_done := true
30
+ fd.close()
31
+ end
32
+ end
33
+
34
+ def read() =
35
+ if
36
+ is_done()
37
+ then
38
+ ""
39
+ else
40
+ s = fd.read()
41
+ if s == "" then close() end
42
+ s
43
+ end
44
+ end
45
+
46
+ read.{close = close}
47
+ end
48
+
49
+ let file.write = ()
50
+
51
+ # Stream data to a file. Returns a callback to write to the file. Execute
52
+ # with `null` or `""` to signify the end of the writing operation.
53
+ # @category File
54
+ # @param ~append Append data if file exists.
55
+ # @param ~perms Default file rights if created. Default: `0o644`.
56
+ # @param ~atomic Make the write atomic by writing to a temporary file and moving \
57
+ # the file to destination once writing has succeeded.
58
+ # @param ~temp_dir Temporary directory for atomic write.
59
+ # @param path Path to write to
60
+ def file.write.stream(
61
+ ~perms=0o644,
62
+ ~append=false,
63
+ ~atomic=false,
64
+ ~temp_dir=null,
65
+ p
66
+ ) =
67
+ let (fd, exec) =
68
+ if
69
+ atomic
70
+ then
71
+ let (temp_dir, ensure) =
72
+ if
73
+ null.defined(temp_dir)
74
+ then
75
+ (null.get(temp_dir), {()})
76
+ else
77
+ temp_dir = file.temp_dir("temp", "dir")
78
+ (temp_dir, {file.rmdir(temp_dir)})
79
+ end
80
+ tmp = path.concat(temp_dir, "atomic.write")
81
+ if append and file.exists(p) then file.copy(p, tmp) end
82
+ def exec() =
83
+ try
84
+ file.move(atomic=true, tmp, p)
85
+ catch _ : [error.file.cross_device] do
86
+ log(
87
+ label="file.write",
88
+ "Atomic rename failed! Directory for temporary files appears to be \
89
+ on a different file system. Please set it to the same one using \
90
+ `temp_dir` argument to guarantee atomic file operations!"
91
+ )
92
+ file.copy(force=true, tmp, p)
93
+ finally
94
+ ensure()
95
+ end
96
+ end
97
+ fd = file.open(write=true, append=append, perms=perms, tmp)
98
+ (fd, exec)
99
+ else
100
+ (file.open(write=true, append=append, perms=perms, p), {()})
101
+ end
102
+
103
+ def write(s) =
104
+ s = s ?? ""
105
+ if
106
+ s == ""
107
+ then
108
+ if
109
+ not fd.closed()
110
+ then
111
+ fd.close()
112
+ exec()
113
+ end
114
+ else
115
+ fd.write(s)
116
+ end
117
+ end
118
+
119
+ write
120
+ end
121
+
122
+ # Write data to a file.
123
+ # @category File
124
+ # @param ~data Data to write. If passing a callback `() -> string?`, the callback \
125
+ # must return `null` or `""` when it has finished sending all its data.
126
+ # @param ~append Append data if file exists.
127
+ # @param ~perms Default file rights if created. Default: `0o644`.
128
+ # @param ~atomic Make the write atomic by writing to a temporary file and moving \
129
+ # the file to destination once writing has succeeded.
130
+ # @param ~temp_dir Temporary directory for atomic write.
131
+ # @param path Path to write to.
132
+ def replaces file.write(
133
+ ~data,
134
+ ~perms=0o644,
135
+ ~append=false,
136
+ ~atomic=false,
137
+ ~temp_dir=null,
138
+ path
139
+ ) =
140
+ cb =
141
+ file.write.stream(
142
+ append=append,
143
+ perms=perms,
144
+ atomic=atomic,
145
+ temp_dir=temp_dir,
146
+ path
147
+ )
148
+
149
+ try
150
+ s = ref(getter.get(data) ?? "")
151
+ cb(s())
152
+ if
153
+ getter.is_constant(data)
154
+ then
155
+ cb("")
156
+ else
157
+ while s() != "" do
158
+ s := getter.get(data) ?? ""
159
+ cb(s())
160
+
161
+ end
162
+ end
163
+ catch err do
164
+ cb("")
165
+ error.raise(err)
166
+ end
167
+ end
168
+
169
+ # Ensure that a file exists, creating it empty if it does not.
170
+ # @category File
171
+ # @param path Path of the file.
172
+ def file.touch(~perms=0o644, path) =
173
+ file.write(data="", perms=perms, append=true, path)
174
+ end
175
+
176
+ # Read the whole contents of a file.
177
+ # @category File
178
+ def file.contents(fname) =
179
+ fn = file.read(fname)
180
+
181
+ cur = ref("")
182
+ next = ref(fn())
183
+ while next() != "" do
184
+ cur := "#{cur()}#{next()}"
185
+ next := fn()
186
+
187
+ end
188
+
189
+ cur()
190
+ end
191
+
192
+ # Get the list of lines of a file.
193
+ # @category File
194
+ def file.lines(fname) =
195
+ r/\n/.split(file.contents(fname))
196
+ end
197
+
198
+ # Iterate over the lines of a file.
199
+ # @category File
200
+ def file.lines.iterator(fname) =
201
+ list.iterator(file.lines(fname))
202
+ end
203
+
204
+ # Iterate over the contents of a file.
205
+ # @category File
206
+ def file.iterator(fname) =
207
+ f = file.read(fname)
208
+ fun () ->
209
+ begin
210
+ s = f()
211
+ (s == "") ? null : s
212
+ end
213
+ end
214
+
215
+ # Get a file's mime type by calling the `file` command line binary.
216
+ # @category File
217
+ def file.mime.cli(fname) =
218
+ mime =
219
+ list.hd(
220
+ default="",
221
+ process.read.lines(
222
+ process.quote.command("file", args=["-b", "--mime-type", fname])
223
+ )
224
+ )
225
+
226
+ mime == "" ? null : mime
227
+ end
228
+
229
+ # Get a file's mime type. Uses libmagic if enabled, otherwise try
230
+ # to get the value using the file binary. Returns `null` if no value
231
+ # can be found.
232
+ # @category File
233
+ # @param file The file to test
234
+ def replaces file.mime(fname) =
235
+ fn = file.mime.cli
236
+ %ifdef file.mime.libmagic
237
+ ignore(fn)
238
+ fn = file.mime.libmagic
239
+ %endif
240
+
241
+ fn(fname)
242
+ end
243
+
244
+ # Getter to the contents of a file.
245
+ # @category File
246
+ # @param fname Name of the file from which the contents should be taken.
247
+ def file.getter(fname) =
248
+ contents = ref("")
249
+
250
+ def update() =
251
+ contents := file.contents(fname)
252
+ end
253
+
254
+ update()
255
+ ignore(file.watch(fname, update))
256
+ ref.getter(contents)
257
+ end
258
+
259
+ # Float getter from a file.
260
+ # @category File
261
+ # @param fname Name of the file from which the contents should be taken.
262
+ # @param ~default Default value when the file contains invalid data.
263
+ def file.getter.float(~default=0., fname) =
264
+ x = file.getter(fname)
265
+
266
+ def f(x) =
267
+ float_of_string(default=default, string.trim(x))
268
+ end
269
+
270
+ getter.map.memoize(f, x)
271
+ end
272
+
273
+ %ifndef file.metadata.flac
274
+ let file.metadata.flac = fun (_) -> []
275
+ %endif
276
+
277
+ let file.metadata.flac.cover = ()
278
+
279
+ # Decode a flac-encoded cover metadata string.
280
+ # @category String
281
+ def file.metadata.flac.cover.decode(s) =
282
+ # See https://xiph.org/flac/format.html#metadata_block_picture
283
+ i = ref(0)
284
+
285
+ def read_int() =
286
+ ret =
287
+ string.binary.to_int(
288
+ little_endian=false,
289
+ string.sub(encoding="ascii", s, start=i(), length=4)
290
+ )
291
+
292
+ i := i() + 4
293
+ ret
294
+ end
295
+
296
+ def read_string(len) =
297
+ ret = string.sub(encoding="ascii", s, start=i(), length=len)
298
+ i := i() + len
299
+ (ret : string)
300
+ end
301
+
302
+ pic_type = read_int()
303
+ mime_len = read_int()
304
+ mime = mime_len == 0 ? "image/" : read_string(mime_len)
305
+ desc_len = read_int()
306
+ desc = read_string(desc_len)
307
+ width = read_int()
308
+ height = read_int()
309
+ color_depth = read_int()
310
+ number_of_colors = read_int()
311
+ number_of_colors = number_of_colors > 0 ? null(number_of_colors) : null
312
+ data_len = read_int()
313
+ data = string.sub(encoding="ascii", s, start=i(), length=data_len)
314
+ if
315
+ data == ""
316
+ then
317
+ log.info(
318
+ "Failed to read cover metadata"
319
+ )
320
+ null
321
+ else
322
+ null(
323
+ data.{
324
+ picture_type = pic_type,
325
+ mime = mime,
326
+ description = desc,
327
+ width = width,
328
+ height = height,
329
+ color_depth = color_depth,
330
+ number_of_colors = number_of_colors
331
+ }
332
+ )
333
+ end
334
+ end
335
+
336
+ # Encode cover metadata for embedding with flac files.
337
+ # @category String
338
+ def file.metadata.flac.cover.encode(
339
+ ~picture_type,
340
+ ~mime,
341
+ ~description="",
342
+ ~width,
343
+ ~height,
344
+ ~color_depth,
345
+ ~number_of_colors=null,
346
+ data
347
+ ) =
348
+ def encode_string(s) =
349
+ len = 1 + (string.bytes.length(s) / 8)
350
+ str_len = string.binary.of_int(little_endian=false, pad=4, len)
351
+ if
352
+ string.bytes.length(str_len) > 4
353
+ then
354
+ error.raise(
355
+ error.invalid,
356
+ "Data length too long for APIC format!"
357
+ )
358
+ end
359
+
360
+ pad = string.make(char_code=0, len * 8 - string.bytes.length(s))
361
+ (str_len, "#{s}#{pad}")
362
+ end
363
+
364
+ pic_type = string.binary.of_int(little_endian=false, pad=4, picture_type)
365
+ let (mime_len, mime) = encode_string(mime)
366
+ let (desc_len, description) = encode_string(description)
367
+ width = string.binary.of_int(little_endian=false, pad=4, width)
368
+ height = string.binary.of_int(little_endian=false, pad=4, height)
369
+ color_depth = string.binary.of_int(little_endian=false, pad=4, color_depth)
370
+ number_of_colors =
371
+ string.binary.of_int(little_endian=false, pad=4, number_of_colors ?? 0)
372
+
373
+ let (data_len, data) = encode_string(data)
374
+ "#{pic_type}#{mime_len}#{mime}#{desc_len}#{description}#{width}#{height}#{
375
+ color_depth
376
+ }#{number_of_colors}#{data_len}#{data}"
377
+ end
378
+
379
+ # Download file using a regular http.get request. Returns `true` on success.
380
+ # @category File
381
+ # @param ~filename Downloaded filename.
382
+ # @param ~timeout Timeout in seconds
383
+ def file.download(~filename, ~timeout=5., url) =
384
+ file_writer = file.write.stream(filename)
385
+ response = http.get.stream(on_body_data=file_writer, timeout=timeout, url)
386
+ response.status_code < 400
387
+ end
@@ -0,0 +1,74 @@
1
+ # Construct a function returning the value of a getter.
2
+ # @category Getter
3
+ def getter.function(x) =
4
+ {getter.get(x)}
5
+ end
6
+
7
+ # Determine if a getter is a constant.
8
+ # @category Getter
9
+ def getter.is_constant(x) =
10
+ getter.case(x, fun (_) -> true, fun (_) -> false)
11
+ end
12
+
13
+ # Convert an int getter to a float getter.
14
+ # @category Getter
15
+ def getter.float_of_int(x) =
16
+ getter.map(float_of_int, x)
17
+ end
18
+
19
+ # Convert a float getter to a int getter.
20
+ # @category Getter
21
+ def getter.int_of_float(x) =
22
+ getter.map(int_of_float, x)
23
+ end
24
+
25
+ # Execute a function when the value of the getter changes.
26
+ # @category Getter
27
+ def getter.on_change(f, x) =
28
+ x = {getter.get(x)}
29
+ old = ref(x())
30
+ fun () ->
31
+ begin
32
+ new = x()
33
+ if
34
+ old() != new
35
+ then
36
+ old := new
37
+ f(new)
38
+ end
39
+
40
+ new
41
+ end
42
+ end
43
+
44
+ # Detect whether the value of the getter changes.
45
+ # @category Getter
46
+ def getter.changes(x) =
47
+ old = ref(getter.get(x))
48
+ fun () ->
49
+ begin
50
+ new = getter.get(x)
51
+ if
52
+ old() != new
53
+ then
54
+ old := new
55
+ true
56
+ else
57
+ false
58
+ end
59
+ end
60
+ end
61
+
62
+ # Give the latest value among two getters.
63
+ # @category Getter
64
+ def getter.merge(x, y) =
65
+ v = ref(getter.get(x))
66
+ x = getter.on_change(fun (x) -> v := x, x)
67
+ y = getter.on_change(fun (y) -> v := y, y)
68
+ fun () ->
69
+ begin
70
+ ignore(x())
71
+ ignore(y())
72
+ v()
73
+ end
74
+ end