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