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,196 @@
|
|
|
1
|
+
# Enable the external ffmpeg decoder.
|
|
2
|
+
# @category Liquidsoap
|
|
3
|
+
# @param ~file_extensions File extensions to decode. Should not be empty
|
|
4
|
+
# @param ~mimes Mime types to decode. Empty list means any type.
|
|
5
|
+
# @param ~binary Path to the `ffmpeg` binary.
|
|
6
|
+
def enable_external_ffmpeg_decoder(~binary="ffmpeg", ~mimes, ~file_extensions) =
|
|
7
|
+
decoder.add(
|
|
8
|
+
name="FFMPEG",
|
|
9
|
+
description="Decode files using the ffmpeg decoder binary",
|
|
10
|
+
mimes=mimes,
|
|
11
|
+
file_extensions=file_extensions,
|
|
12
|
+
fun (~rlog, ~maxtime:_, fname) ->
|
|
13
|
+
begin
|
|
14
|
+
# File is cleaned up as part of the request workflow.
|
|
15
|
+
outfile = file.temp(cleanup=false, "ffmpeg", ".wav")
|
|
16
|
+
try
|
|
17
|
+
let {status = {code}} =
|
|
18
|
+
process.run(
|
|
19
|
+
"#{binary} -i #{process.quote(fname)} #{process.quote(outfile)}"
|
|
20
|
+
)
|
|
21
|
+
code == 0 ? outfile : null
|
|
22
|
+
catch err do
|
|
23
|
+
file.remove(outfile)
|
|
24
|
+
rlog(
|
|
25
|
+
"Error while decoding #{fname} using ffmpeg: #{err}"
|
|
26
|
+
)
|
|
27
|
+
null
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Enable the external openmpt123 decoder
|
|
34
|
+
# @category Liquidsoap
|
|
35
|
+
# @param ~file_extensions File extensions to decode.
|
|
36
|
+
# @param ~mimes Mime types to decode. Empty list means any type.
|
|
37
|
+
# @param ~options Extra options.
|
|
38
|
+
# @param ~binary Path to the `ffmpeg` binary.
|
|
39
|
+
def enable_external_openmpt123_decoder(
|
|
40
|
+
~binary="openmpt123",
|
|
41
|
+
~mimes=[
|
|
42
|
+
"audio/it",
|
|
43
|
+
"audio/xm",
|
|
44
|
+
"audio/s3m",
|
|
45
|
+
"audio/x-mod",
|
|
46
|
+
"audio/mod",
|
|
47
|
+
"audio/module-xm",
|
|
48
|
+
"audio/x-mod",
|
|
49
|
+
"application/playerpro",
|
|
50
|
+
"audio/x-s3m",
|
|
51
|
+
"application/soundapp",
|
|
52
|
+
"audio/med",
|
|
53
|
+
"audio/x-xm"
|
|
54
|
+
],
|
|
55
|
+
~file_extensions=[
|
|
56
|
+
"xm",
|
|
57
|
+
"mtm",
|
|
58
|
+
"amf",
|
|
59
|
+
"stm",
|
|
60
|
+
"ult",
|
|
61
|
+
"wow",
|
|
62
|
+
"dmf",
|
|
63
|
+
"it",
|
|
64
|
+
"s3m",
|
|
65
|
+
"far",
|
|
66
|
+
"mod",
|
|
67
|
+
"mt2",
|
|
68
|
+
"okt",
|
|
69
|
+
"med",
|
|
70
|
+
"669"
|
|
71
|
+
],
|
|
72
|
+
~options=""
|
|
73
|
+
) =
|
|
74
|
+
decoder.add(
|
|
75
|
+
name="OPENMPT123",
|
|
76
|
+
description="Decode files using the openmpt123 decoder binary",
|
|
77
|
+
mimes=mimes,
|
|
78
|
+
file_extensions=file_extensions,
|
|
79
|
+
fun (~rlog, ~maxtime:_, infile) ->
|
|
80
|
+
begin
|
|
81
|
+
ret =
|
|
82
|
+
process.read.lines(
|
|
83
|
+
"#{binary} --info #{process.quote(infile)} 2>&1"
|
|
84
|
+
)
|
|
85
|
+
def get_meta(l, s) =
|
|
86
|
+
ret = string.extract(pattern="^(\\w+).+:\\s(.+)$", s)
|
|
87
|
+
if
|
|
88
|
+
list.length(ret) > 2
|
|
89
|
+
then
|
|
90
|
+
label = ret[1]
|
|
91
|
+
val = ret[2]
|
|
92
|
+
label = "openmpt:#{string.case(lower=true, label)}"
|
|
93
|
+
["#{string.quote(label)}=#{string.quote(val)}", ...l]
|
|
94
|
+
else
|
|
95
|
+
l
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
meta = list.fold(get_meta, [], ret)
|
|
99
|
+
|
|
100
|
+
prefix =
|
|
101
|
+
if
|
|
102
|
+
meta == []
|
|
103
|
+
then
|
|
104
|
+
""
|
|
105
|
+
else
|
|
106
|
+
"annotate:#{string.concat(separator=',', meta)}:"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# File is cleaned up as part of the request workflow.
|
|
110
|
+
outfile = file.temp(cleanup=false, "openmpt", ".wav")
|
|
111
|
+
try
|
|
112
|
+
let {status = {code}} =
|
|
113
|
+
process.run(
|
|
114
|
+
"#{binary} --assume-terminal --quiet --force #{options} --output #{
|
|
115
|
+
process.quote(outfile)
|
|
116
|
+
} #{process.quote(infile)}"
|
|
117
|
+
)
|
|
118
|
+
code == 0 ? "#{prefix}#{outfile}" : null
|
|
119
|
+
catch err do
|
|
120
|
+
file.remove(outfile)
|
|
121
|
+
rlog(
|
|
122
|
+
"Error while decoding #{infile} using ffmpeg: #{err}"
|
|
123
|
+
)
|
|
124
|
+
null
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Standard function for displaying metadata.
|
|
131
|
+
# Shows artist and title, using "Unknown" when a field is empty.
|
|
132
|
+
# @param m Metadata packet to be displayed.
|
|
133
|
+
# @category String
|
|
134
|
+
def string_of_metadata(m) =
|
|
135
|
+
artist = m["artist"]
|
|
136
|
+
title = m["title"]
|
|
137
|
+
artist = if "" == artist then "Unknown" else artist end
|
|
138
|
+
title = if "" == title then "Unknown" else title end
|
|
139
|
+
"#{artist} -- #{title}"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Use X On Screen Display to display metadata info.
|
|
143
|
+
# @flag extra
|
|
144
|
+
# @param ~color Color of the text.
|
|
145
|
+
# @param ~position Position of the text (top|middle|bottom).
|
|
146
|
+
# @param ~font Font used (xfontsel is your friend...)
|
|
147
|
+
# @param ~display Function used to display a metadata packet.
|
|
148
|
+
# @category Source / Track processing
|
|
149
|
+
def osd_metadata(
|
|
150
|
+
~color="green",
|
|
151
|
+
~position="top",
|
|
152
|
+
~font="-*-courier-*-r-*-*-*-240-*-*-*-*-*-*",
|
|
153
|
+
~display=string_of_metadata,
|
|
154
|
+
s
|
|
155
|
+
) =
|
|
156
|
+
osd =
|
|
157
|
+
'osd_cat -p #{position} --font #{process.quote(font)}' ^
|
|
158
|
+
' --color #{color}'
|
|
159
|
+
|
|
160
|
+
def feedback(m) =
|
|
161
|
+
ignore(
|
|
162
|
+
process.run(
|
|
163
|
+
"echo #{process.quote(display(m))} | #{osd} &"
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
s.on_metadata(synchronous=false, feedback)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Use notify to display metadata info.
|
|
172
|
+
# @flag extra
|
|
173
|
+
# @param ~urgency Urgency (low|normal|critical).
|
|
174
|
+
# @param ~icon Icon filename or stock icon to display.
|
|
175
|
+
# @param ~timeout Timeout in seconds.
|
|
176
|
+
# @param ~display Function used to display a metadata packet.
|
|
177
|
+
# @param ~title Title of the notification message.
|
|
178
|
+
# @category Source / Track processing
|
|
179
|
+
def notify_metadata(
|
|
180
|
+
~urgency="low",
|
|
181
|
+
~icon="stock_smiley-22",
|
|
182
|
+
~timeout=3.,
|
|
183
|
+
~display=string_of_metadata,
|
|
184
|
+
~title="Liquidsoap: new track",
|
|
185
|
+
s
|
|
186
|
+
) =
|
|
187
|
+
time = int_of_float(timeout * 1000.)
|
|
188
|
+
send =
|
|
189
|
+
'notify-send -i #{icon} -u #{urgency}' ^
|
|
190
|
+
' -t #{time} #{process.quote(title)} '
|
|
191
|
+
|
|
192
|
+
s.on_metadata(
|
|
193
|
+
synchronous=false,
|
|
194
|
+
fun (m) -> ignore(process.run(send ^ process.quote(display(m))))
|
|
195
|
+
)
|
|
196
|
+
end
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# Plot the first crossfade transition. Used for visualizing and testing
|
|
2
|
+
# crossfade transitions.
|
|
3
|
+
# @category Source / Track processing
|
|
4
|
+
# @flag extra
|
|
5
|
+
def cross.plot(~png=null, ~dir=null, ~font=null, ~font_size=10, s) =
|
|
6
|
+
dir =
|
|
7
|
+
if
|
|
8
|
+
null.defined(dir)
|
|
9
|
+
then
|
|
10
|
+
null.get(dir)
|
|
11
|
+
else
|
|
12
|
+
dir = file.temp_dir("plot")
|
|
13
|
+
on_cleanup({file.rmdir(dir)})
|
|
14
|
+
dir
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
old_txt = path.concat(dir, "old.txt")
|
|
18
|
+
new_txt = path.concat(dir, "new.txt")
|
|
19
|
+
|
|
20
|
+
def gnuplot_cmd(filename) =
|
|
21
|
+
term =
|
|
22
|
+
if
|
|
23
|
+
null.defined(font)
|
|
24
|
+
then
|
|
25
|
+
'pngcairo font "#{null.get(font)},#{font_size}"'
|
|
26
|
+
else
|
|
27
|
+
"png"
|
|
28
|
+
end
|
|
29
|
+
'set term #{term}; set output "#{(filename : string)}"; plot "#{new_txt}" \
|
|
30
|
+
using 1:2 with lines title "new track", "#{old_txt}" using 1:2 with lines \
|
|
31
|
+
title "old track"'
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def store_rms(~id, s) =
|
|
35
|
+
s = rms(duration=settings.frame.duration(), s)
|
|
36
|
+
t0 = ref(null)
|
|
37
|
+
|
|
38
|
+
s.on_frame(
|
|
39
|
+
synchronous=true,
|
|
40
|
+
before=false,
|
|
41
|
+
{
|
|
42
|
+
let t0 =
|
|
43
|
+
if
|
|
44
|
+
null.defined(t0())
|
|
45
|
+
then
|
|
46
|
+
null.get(t0())
|
|
47
|
+
else
|
|
48
|
+
t0 := source.time(s)
|
|
49
|
+
null.get(t0())
|
|
50
|
+
end
|
|
51
|
+
let v = float.truncate(digits=4, mode="floor", s.rms())
|
|
52
|
+
let p = float.truncate(digits=4, mode="floor", source.time(s) - t0)
|
|
53
|
+
fname = id == "old" ? old_txt : new_txt
|
|
54
|
+
file.write(append=true, data="#{p}\t#{v}\n", fname)
|
|
55
|
+
}
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
s
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
plotted = ref(false)
|
|
62
|
+
|
|
63
|
+
def transition(old, new) =
|
|
64
|
+
old = store_rms(id="old", fade.out(old.source))
|
|
65
|
+
new = store_rms(id="new", fade.in(new.source))
|
|
66
|
+
|
|
67
|
+
s = blank(duration=0.1)
|
|
68
|
+
s.on_frame(
|
|
69
|
+
synchronous=true,
|
|
70
|
+
{
|
|
71
|
+
if
|
|
72
|
+
null.defined(png) and not plotted()
|
|
73
|
+
then
|
|
74
|
+
ignore(
|
|
75
|
+
process.run(
|
|
76
|
+
"gnuplot -e #{process.quote(gnuplot_cmd(null.get(png)))}"
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
plotted := true
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
sequence(single_track=false, [add(normalize=false, [new, old]), once(s)])
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
cross(transition, s)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Plot two sine tracks with given autocue params.
|
|
91
|
+
# Used for visualizing and testing autocue crossfade transitions.
|
|
92
|
+
# @category Source / Track processing
|
|
93
|
+
# @flag extra
|
|
94
|
+
def autocue.plot(
|
|
95
|
+
~png=null,
|
|
96
|
+
~dir=null,
|
|
97
|
+
~font=null,
|
|
98
|
+
~font_size=10,
|
|
99
|
+
~old_autocue,
|
|
100
|
+
~new_autocue,
|
|
101
|
+
~sync="auto",
|
|
102
|
+
~on_stop
|
|
103
|
+
) =
|
|
104
|
+
dir =
|
|
105
|
+
if
|
|
106
|
+
null.defined(dir)
|
|
107
|
+
then
|
|
108
|
+
null.get(dir)
|
|
109
|
+
else
|
|
110
|
+
dir = file.temp_dir("plot")
|
|
111
|
+
on_cleanup({file.rmdir(dir)})
|
|
112
|
+
dir
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
old_txt = path.concat(dir, "old.txt")
|
|
116
|
+
new_txt = path.concat(dir, "new.txt")
|
|
117
|
+
|
|
118
|
+
def gnuplot_cmd(filename) =
|
|
119
|
+
term =
|
|
120
|
+
if
|
|
121
|
+
null.defined(font)
|
|
122
|
+
then
|
|
123
|
+
'pngcairo font "#{null.get(font)},#{font_size}"'
|
|
124
|
+
else
|
|
125
|
+
"png"
|
|
126
|
+
end
|
|
127
|
+
'set term #{term}; set output "#{(filename : string)}"; plot "#{new_txt}" \
|
|
128
|
+
using 1:2 with lines title "new track", "#{old_txt}" using 1:2 with lines \
|
|
129
|
+
title "old track"'
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def autocue_annotate(a, uri) =
|
|
133
|
+
meta = autocue.metadata(implementation="autocue.plot", a)
|
|
134
|
+
meta =
|
|
135
|
+
list.map(fun ((label, value)) -> "#{label}=#{string.quote(value)}", meta)
|
|
136
|
+
meta = string.concat(separator=",", meta)
|
|
137
|
+
"annotate:#{meta}:#{uri}"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
old_uri =
|
|
141
|
+
process.uri(
|
|
142
|
+
extname="wav",
|
|
143
|
+
"ffmpeg -f lavfi -i \"sine=frequency=1000:duration=10\" -y $(output) < \
|
|
144
|
+
/dev/null"
|
|
145
|
+
)
|
|
146
|
+
old_uri = autocue_annotate(old_autocue, old_uri)
|
|
147
|
+
old_source = request.once(request.create(old_uri))
|
|
148
|
+
|
|
149
|
+
new_uri =
|
|
150
|
+
process.uri(
|
|
151
|
+
extname="wav",
|
|
152
|
+
"ffmpeg -f lavfi -i \"sine=frequency=500:duration=10\" -y $(output) < \
|
|
153
|
+
/dev/null"
|
|
154
|
+
)
|
|
155
|
+
new_uri = autocue_annotate(new_autocue, new_uri)
|
|
156
|
+
new_source = request.once(request.create(new_uri))
|
|
157
|
+
|
|
158
|
+
s = sequence([old_source, new_source])
|
|
159
|
+
|
|
160
|
+
def store_rms(~fname, s) =
|
|
161
|
+
s = rms(duration=settings.frame.duration(), s)
|
|
162
|
+
t0 = ref(null)
|
|
163
|
+
|
|
164
|
+
s.on_frame(
|
|
165
|
+
synchronous=true,
|
|
166
|
+
before=false,
|
|
167
|
+
{
|
|
168
|
+
let t0 =
|
|
169
|
+
if
|
|
170
|
+
null.defined(t0())
|
|
171
|
+
then
|
|
172
|
+
null.get(t0())
|
|
173
|
+
else
|
|
174
|
+
t0 := source.time(s)
|
|
175
|
+
null.get(t0())
|
|
176
|
+
end
|
|
177
|
+
let t = float.truncate(digits=4, mode="floor", source.time(s) - t0)
|
|
178
|
+
let v = float.truncate(digits=4, mode="floor", s.rms())
|
|
179
|
+
file.write(append=true, data="#{t}\t#{v}\n", fname)
|
|
180
|
+
}
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
s
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
is_first_ending = ref(true)
|
|
187
|
+
is_first_starting = ref(true)
|
|
188
|
+
|
|
189
|
+
def transition(old, new) =
|
|
190
|
+
list.iter(
|
|
191
|
+
fun (x) ->
|
|
192
|
+
log(
|
|
193
|
+
level=4,
|
|
194
|
+
"Before: #{x}"
|
|
195
|
+
),
|
|
196
|
+
old.metadata
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
list.iter(
|
|
200
|
+
fun (x) ->
|
|
201
|
+
log(
|
|
202
|
+
level=4,
|
|
203
|
+
"After : #{x}"
|
|
204
|
+
),
|
|
205
|
+
new.metadata
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
def ending_map(s) =
|
|
209
|
+
if
|
|
210
|
+
is_first_ending()
|
|
211
|
+
then
|
|
212
|
+
is_first_ending := false
|
|
213
|
+
store_rms(fname=old_txt, s)
|
|
214
|
+
else
|
|
215
|
+
s
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def starting_map(s) =
|
|
220
|
+
if
|
|
221
|
+
is_first_starting()
|
|
222
|
+
then
|
|
223
|
+
is_first_starting := false
|
|
224
|
+
store_rms(fname=new_txt, s)
|
|
225
|
+
else
|
|
226
|
+
s
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
cross.simple(
|
|
231
|
+
initial_fade_in_metadata=new.metadata,
|
|
232
|
+
initial_fade_out_metadata=old.metadata,
|
|
233
|
+
ending_map=ending_map,
|
|
234
|
+
starting_map=starting_map,
|
|
235
|
+
old.source,
|
|
236
|
+
new.source
|
|
237
|
+
)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
s = cross(transition, s)
|
|
241
|
+
|
|
242
|
+
clock.assign_new(sync=sync, [s])
|
|
243
|
+
|
|
244
|
+
def on_stop() =
|
|
245
|
+
if
|
|
246
|
+
null.defined(png)
|
|
247
|
+
then
|
|
248
|
+
ignore(
|
|
249
|
+
process.run(
|
|
250
|
+
"gnuplot -e #{process.quote(gnuplot_cmd(null.get(png)))}"
|
|
251
|
+
)
|
|
252
|
+
)
|
|
253
|
+
end
|
|
254
|
+
on_stop()
|
|
255
|
+
clock(s.clock).stop()
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
o = output.dummy(fallible=true, s)
|
|
259
|
+
o.on_stop(synchronous=true, on_stop)
|
|
260
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Keep a record of played files. This is primarily useful to know when a song
|
|
2
|
+
# was last played and avoid repetitions.
|
|
3
|
+
# @flag extra
|
|
4
|
+
# @param ~duration Duration (in seconds) after which songs are forgotten. By default, songs are not forgotten which means that the playlog will contain all the songs ever played.
|
|
5
|
+
# @param ~hash Function to extract an identifier from the metadata. By default, the filename is used but we could return the artist to know when a song from a given artist was last played for instance.
|
|
6
|
+
# @param ~persistency Set a file name where the values are stored and loaded in case the script is restarted.
|
|
7
|
+
# @category Source / Track processing
|
|
8
|
+
# @method add Record that file with given metadata has been played.
|
|
9
|
+
# @method last How long ago a file was played (in seconds), `infinity` is returned if the song has never been played.
|
|
10
|
+
def playlog(
|
|
11
|
+
~duration=infinity,
|
|
12
|
+
~persistency=null,
|
|
13
|
+
~hash=fun (m) -> m["filename"]
|
|
14
|
+
) =
|
|
15
|
+
l = ref([])
|
|
16
|
+
|
|
17
|
+
# Load from persistency file
|
|
18
|
+
if
|
|
19
|
+
null.defined(persistency)
|
|
20
|
+
then
|
|
21
|
+
if
|
|
22
|
+
file.exists(null.get(persistency))
|
|
23
|
+
then
|
|
24
|
+
let json.parse (parsed : [(string * float)]?) =
|
|
25
|
+
file.contents(null.get(persistency))
|
|
26
|
+
|
|
27
|
+
if null.defined(parsed) then l := null.get(parsed) end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Save into persistency file
|
|
32
|
+
def save() =
|
|
33
|
+
if
|
|
34
|
+
null.defined(persistency)
|
|
35
|
+
then
|
|
36
|
+
data = json.stringify(l())
|
|
37
|
+
file.write(data=data, null.get(persistency))
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Remove too old elements
|
|
42
|
+
def prune() =
|
|
43
|
+
if
|
|
44
|
+
duration != infinity
|
|
45
|
+
then
|
|
46
|
+
t = time()
|
|
47
|
+
l := list.assoc.filter(fun (_, tf) -> t - tf <= duration, l())
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Add a new entry
|
|
52
|
+
def add(m) =
|
|
53
|
+
prune()
|
|
54
|
+
f = hash(m)
|
|
55
|
+
l := (f, time())::l()
|
|
56
|
+
save()
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Last time this entry was played
|
|
60
|
+
def last(m) =
|
|
61
|
+
f = hash(m)
|
|
62
|
+
time() - list.assoc(default=0. - infinity, f, l())
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
{add = add, last = last}
|
|
66
|
+
end
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Harbor middleware to add CORS headers
|
|
2
|
+
# @category Internet
|
|
3
|
+
# @flag extra
|
|
4
|
+
# @param ~origin Configures the Access-Control-Allow-Origin CORS header
|
|
5
|
+
# @param ~origin_callback Origin callback for advanced uses. If passed, overrides `origin` argument. Takes the request as input and returns the allowed origin. Return `null` to skip all CORS headers.
|
|
6
|
+
# @param ~methods Configures the Access-Control-Allow-Methods CORS header.
|
|
7
|
+
# @param ~allowed_headers Configures the Access-Control-Allow-Headers CORS header. If not specified, defaults to reflecting the headers specified in the request's Access-Control-Request-Headers header.
|
|
8
|
+
# @param ~exposed_headers Configures the Access-Control-Expose-Headers CORS header. If not specified, no custom headers are exposed.
|
|
9
|
+
# @param ~credentials Configures the Access-Control-Allow-Credentials CORS header. Set to true to pass the header, otherwise it is omitted.
|
|
10
|
+
# @param ~max_age Configures the Access-Control-Max-Age CORS header. Set to an integer to pass the header, otherwise it is omitted.
|
|
11
|
+
# @param ~preflight_continue Pass the CORS preflight response to the nexnhandler.
|
|
12
|
+
# @param ~options_status_code Provides a status code to use for successful OPTIONS requests, since some legacy browsers (IE11, various SmartTVs) choke on 204.
|
|
13
|
+
def harbor.http.middleware.cors(
|
|
14
|
+
~origin=null("*"),
|
|
15
|
+
~origin_callback=null,
|
|
16
|
+
~methods=["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"],
|
|
17
|
+
~allowed_headers=null,
|
|
18
|
+
~exposed_headers=[],
|
|
19
|
+
~credentials=false,
|
|
20
|
+
~max_age=null,
|
|
21
|
+
~preflight_continue=false,
|
|
22
|
+
~options_status_code=204
|
|
23
|
+
) =
|
|
24
|
+
fun (req, res, next) ->
|
|
25
|
+
begin
|
|
26
|
+
# This is for typing purposes
|
|
27
|
+
res = if false then http.response() else res end
|
|
28
|
+
if
|
|
29
|
+
false
|
|
30
|
+
then
|
|
31
|
+
harbor.http.register(
|
|
32
|
+
"/foo",
|
|
33
|
+
fun (r, _) -> ignore(if false then r else req end)
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
vary = ref([])
|
|
38
|
+
|
|
39
|
+
def add_vary() =
|
|
40
|
+
if
|
|
41
|
+
vary() != []
|
|
42
|
+
then
|
|
43
|
+
res.header("Vary", string.concat(separator=",", vary()))
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def vary(v) =
|
|
48
|
+
vary := v::vary()
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def add_origin(origin) =
|
|
52
|
+
res.header("Access-Control-Allow-Origin", origin)
|
|
53
|
+
if origin != "*" then vary("Origin") end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def add_methods() =
|
|
57
|
+
if
|
|
58
|
+
methods != []
|
|
59
|
+
then
|
|
60
|
+
res.header(
|
|
61
|
+
"Access-Control-Allow-Methods",
|
|
62
|
+
string.concat(separator=",", methods)
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def add_credentials() =
|
|
68
|
+
if
|
|
69
|
+
credentials
|
|
70
|
+
then
|
|
71
|
+
res.header("Access-Control-Allow-Credentials", "true")
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def add_allowed_headers() =
|
|
76
|
+
allowed_headers =
|
|
77
|
+
if
|
|
78
|
+
null.defined(allowed_headers)
|
|
79
|
+
then
|
|
80
|
+
string.concat(separator=",", null.get(allowed_headers))
|
|
81
|
+
elsif
|
|
82
|
+
list.assoc.mem("access-control-request-headers", req.headers)
|
|
83
|
+
then
|
|
84
|
+
req.headers["access-control-request-headers"]
|
|
85
|
+
else
|
|
86
|
+
null
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
if
|
|
90
|
+
null.defined(allowed_headers)
|
|
91
|
+
then
|
|
92
|
+
res.header("Access-Control-Allow-Headers", null.get(allowed_headers))
|
|
93
|
+
vary("Access-Control-Request-Headers")
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def add_exposed_headers() =
|
|
98
|
+
if
|
|
99
|
+
exposed_headers != []
|
|
100
|
+
then
|
|
101
|
+
res.header(
|
|
102
|
+
"Access-Control-Expose-Headers",
|
|
103
|
+
string.concat(separator=",", exposed_headers)
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def add_max_age() =
|
|
109
|
+
if
|
|
110
|
+
null.defined(max_age)
|
|
111
|
+
then
|
|
112
|
+
res.header("Access-Control-Max-Age", "#{(null.get(max_age) : int)}")
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
origin =
|
|
117
|
+
if
|
|
118
|
+
null.defined(origin_callback)
|
|
119
|
+
then
|
|
120
|
+
fn = null.get(origin_callback)
|
|
121
|
+
fn(req)
|
|
122
|
+
else
|
|
123
|
+
getter.get(origin)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
if
|
|
127
|
+
not null.defined(origin)
|
|
128
|
+
then
|
|
129
|
+
next(req, res)
|
|
130
|
+
else
|
|
131
|
+
add_origin(null.get(origin))
|
|
132
|
+
if
|
|
133
|
+
req.method == "OPTIONS"
|
|
134
|
+
then
|
|
135
|
+
add_credentials()
|
|
136
|
+
add_methods()
|
|
137
|
+
add_allowed_headers()
|
|
138
|
+
add_max_age()
|
|
139
|
+
add_exposed_headers()
|
|
140
|
+
add_vary()
|
|
141
|
+
if
|
|
142
|
+
preflight_continue
|
|
143
|
+
then
|
|
144
|
+
next(req, res)
|
|
145
|
+
else
|
|
146
|
+
res.status_code(options_status_code)
|
|
147
|
+
res.header("Content-length", "0")
|
|
148
|
+
end
|
|
149
|
+
else
|
|
150
|
+
add_credentials()
|
|
151
|
+
add_allowed_headers()
|
|
152
|
+
add_vary()
|
|
153
|
+
next(req, res)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# This is for typing purposes
|
|
160
|
+
if false then harbor.http.middleware.register(harbor.http.middleware.cors()) end
|