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,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
|