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,476 @@
|
|
|
1
|
+
# Apply a function to the first track of a source
|
|
2
|
+
# @category Source / Track processing
|
|
3
|
+
# @flag extra
|
|
4
|
+
# @param ~id Force the value of the source ID.
|
|
5
|
+
# @param fn The applied function.
|
|
6
|
+
# @param s The input source.
|
|
7
|
+
def map_first_track(~id=null("map_first_track"), fn, s) =
|
|
8
|
+
fallback(id=id, [fn((once(s) : source)), s])
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Same operator as rotate but merges tracks from each sources.
|
|
12
|
+
# For instance, `rotate.merge([intro,main,outro])` creates a source
|
|
13
|
+
# that plays a sequence `[intro,main,outro]` as single track and loops back.
|
|
14
|
+
# @category Source / Track processing
|
|
15
|
+
# @flag extra
|
|
16
|
+
# @param ~id Force the value of the source ID.
|
|
17
|
+
# @param sources Sequence of sources to be merged.
|
|
18
|
+
def rotate.merge(~id=null("rotate.merge"), sources) =
|
|
19
|
+
sources =
|
|
20
|
+
(sources :
|
|
21
|
+
[
|
|
22
|
+
source.{
|
|
23
|
+
on_select?: (
|
|
24
|
+
{ending: source?, replay_metadata: bool, starting: source}
|
|
25
|
+
)->source,
|
|
26
|
+
on_leave?: ({source: source, track_sensitive: bool})->unit,
|
|
27
|
+
single?: bool,
|
|
28
|
+
track_sensitive?: getter(bool),
|
|
29
|
+
weight?: getter(int)
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
ready = ref(true)
|
|
35
|
+
duration = frame.duration()
|
|
36
|
+
|
|
37
|
+
def to_first({starting, ..._}) =
|
|
38
|
+
ready := (not ready())
|
|
39
|
+
(sequence(merge=true, [blank(duration=duration), (starting : source)]) :
|
|
40
|
+
source
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
sources =
|
|
45
|
+
list.mapi(
|
|
46
|
+
fun (i, (s:source)) ->
|
|
47
|
+
if i == 0 then s.{on_select = to_first} else (s : source) end,
|
|
48
|
+
sources
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
s = rotate(sources)
|
|
52
|
+
let {track_marks = _, ...tracks} = source.tracks(s)
|
|
53
|
+
s = source(tracks).{replay_metadata = false}
|
|
54
|
+
switch(id=id, [(ready, s), ({not ready()}, s)])
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Rotate between overlapping sources. Next track starts according
|
|
58
|
+
# to 'liq_start_next' offset metadata.
|
|
59
|
+
# @category Source / Track processing
|
|
60
|
+
# @flag extra
|
|
61
|
+
# @param ~id Force the value of the source ID.
|
|
62
|
+
# @param ~start_next Metadata field indicating when the next track should start, relative to current track's time.
|
|
63
|
+
# @param ~weights Relative weight of the sources in the sum. The empty list stands for the homogeneous distribution.
|
|
64
|
+
# @param sources Sources to toggle from
|
|
65
|
+
def overlap_sources(
|
|
66
|
+
~id=null("overlap_sources"),
|
|
67
|
+
~normalize=false,
|
|
68
|
+
~start_next="liq_start_next",
|
|
69
|
+
~weights=[],
|
|
70
|
+
sources
|
|
71
|
+
) =
|
|
72
|
+
position = ref(0)
|
|
73
|
+
length = list.length(sources)
|
|
74
|
+
|
|
75
|
+
def current_position() =
|
|
76
|
+
pos = position()
|
|
77
|
+
position := (pos + 1) mod length
|
|
78
|
+
pos
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
ready_list = list.map(fun (_) -> ref(false), sources)
|
|
82
|
+
grab_ready = fun (n) -> list.nth(default=ref(false), ready_list, n)
|
|
83
|
+
|
|
84
|
+
def set_ready(pos, b) =
|
|
85
|
+
is_ready = grab_ready(pos)
|
|
86
|
+
is_ready := b
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Start next track on_offset
|
|
90
|
+
def on_start_next(_, _) =
|
|
91
|
+
set_ready(current_position(), true)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def on_offset(s) =
|
|
95
|
+
let (s, offset) = metadata.getter.source.float(-1., start_next, s)
|
|
96
|
+
s.on_position(
|
|
97
|
+
synchronous=true,
|
|
98
|
+
allow_partial=true,
|
|
99
|
+
position=offset,
|
|
100
|
+
on_start_next
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
list.iter(on_offset, sources)
|
|
105
|
+
|
|
106
|
+
# Disable after each track
|
|
107
|
+
def disable(pos, s) =
|
|
108
|
+
def disable(_) =
|
|
109
|
+
set_ready(pos, false)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
s.on_track(synchronous=true, disable)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
list.iteri(disable, sources)
|
|
116
|
+
|
|
117
|
+
# Relay metadata from all sources
|
|
118
|
+
send_to_main_source = ref(fun (_) -> ())
|
|
119
|
+
|
|
120
|
+
def relay_metadata(m) =
|
|
121
|
+
fn = send_to_main_source()
|
|
122
|
+
fn(m)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
list.iter(fun (s) -> s.on_metadata(synchronous=true, relay_metadata), sources)
|
|
126
|
+
|
|
127
|
+
def drop_metadata(s) =
|
|
128
|
+
let {metadata = _, ...tracks} = source.tracks(s)
|
|
129
|
+
source(tracks)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Now drop all metadata
|
|
133
|
+
sources = list.map(drop_metadata, sources)
|
|
134
|
+
|
|
135
|
+
# Wrap sources into switches.
|
|
136
|
+
def make_switch(pos, source) =
|
|
137
|
+
is_ready = grab_ready(pos)
|
|
138
|
+
switch([(is_ready, source)])
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
sources = list.mapi(make_switch, sources)
|
|
142
|
+
|
|
143
|
+
# Initiate the whole thing.
|
|
144
|
+
set_ready(current_position(), true)
|
|
145
|
+
|
|
146
|
+
# Create main source
|
|
147
|
+
s = add(id=id, normalize=normalize, weights=weights, sources)
|
|
148
|
+
|
|
149
|
+
# Set send_to_main_source
|
|
150
|
+
send_to_main_source := fun (m) -> s.insert_metadata(m)
|
|
151
|
+
s
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Append speech-synthesized tracks reading the metadata.
|
|
155
|
+
# @category Metadata
|
|
156
|
+
# @flag extra
|
|
157
|
+
# @param ~pattern Pattern to use
|
|
158
|
+
# @param s The source to use
|
|
159
|
+
def source.say_metadata =
|
|
160
|
+
def pattern(m) =
|
|
161
|
+
artist = m["artist"]
|
|
162
|
+
title = m["title"]
|
|
163
|
+
artist_predicate =
|
|
164
|
+
if
|
|
165
|
+
artist != ""
|
|
166
|
+
then
|
|
167
|
+
"It was #{artist} playing "
|
|
168
|
+
else
|
|
169
|
+
""
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
say_metadata = "#{artist_predicate}#{title}"
|
|
173
|
+
say_metadata = r/:/g.replace(fun (_) -> '$(colon)', say_metadata)
|
|
174
|
+
say_metadata =
|
|
175
|
+
say_metadata == ""
|
|
176
|
+
? "Sorry, I do not know what this song title was"
|
|
177
|
+
: say_metadata
|
|
178
|
+
|
|
179
|
+
"say:#{say_metadata}"
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
fun (~id=null("source.say_metadata"), ~pattern=pattern, s) ->
|
|
183
|
+
append(id=id, s, fun (m) -> once(single(pattern(m))))
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Regularly insert track boundaries in a stream (useful for testing tracks).
|
|
187
|
+
# @category Source / Track processing
|
|
188
|
+
# @flag extra
|
|
189
|
+
# @param ~every Duration of a track (in seconds).
|
|
190
|
+
# @param ~metadata Metadata for tracks.
|
|
191
|
+
# @param s The stream.
|
|
192
|
+
def chop(~every=getter(3.), ~metadata=getter([]), s) =
|
|
193
|
+
# Track time in the source's context:
|
|
194
|
+
time = ref(0.)
|
|
195
|
+
|
|
196
|
+
s = source.methods(s)
|
|
197
|
+
|
|
198
|
+
is_first = ref(true)
|
|
199
|
+
|
|
200
|
+
def f() =
|
|
201
|
+
time := time() + settings.frame.duration()
|
|
202
|
+
if
|
|
203
|
+
is_first() or getter.get(every) <= time()
|
|
204
|
+
then
|
|
205
|
+
is_first := false
|
|
206
|
+
time := 0.
|
|
207
|
+
s.insert_metadata(new_track=true, getter.get(metadata))
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
s.on_frame(synchronous=true, f)
|
|
212
|
+
s
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Regularly skip tracks from a source (useful for testing skipping).
|
|
216
|
+
# @category Source / Track processing
|
|
217
|
+
# @flag extra
|
|
218
|
+
# @param ~every How often to skip tracks.
|
|
219
|
+
# @param s The stream.
|
|
220
|
+
# @flag extra
|
|
221
|
+
def skipper(~every=getter(5.), s) =
|
|
222
|
+
start_time = ref(0.)
|
|
223
|
+
|
|
224
|
+
def f() =
|
|
225
|
+
if
|
|
226
|
+
getter.get(every) <= s.time() - start_time()
|
|
227
|
+
then
|
|
228
|
+
start_time := s.time()
|
|
229
|
+
s.skip()
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
s.on_frame(f)
|
|
234
|
+
s
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Special track insensitive fallback that always skips current song before
|
|
238
|
+
# switching.
|
|
239
|
+
# @category Source / Track processing
|
|
240
|
+
# @flag extra
|
|
241
|
+
# @param s The main source.
|
|
242
|
+
# @param ~fallback The fallback source. Defaults to `blank` if `null`.
|
|
243
|
+
def fallback.skip(s, ~fallback:f=null) =
|
|
244
|
+
f = f ?? (blank() : source)
|
|
245
|
+
avail = ref(true)
|
|
246
|
+
|
|
247
|
+
def check() =
|
|
248
|
+
old = avail()
|
|
249
|
+
avail := source.is_ready(s)
|
|
250
|
+
if not old and avail() then source.skip(f) end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
s = fallback([s, f])
|
|
254
|
+
|
|
255
|
+
# TODO: could we have something more efficient that checking on every frame
|
|
256
|
+
s.on_frame(synchronous=true, check)
|
|
257
|
+
s
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Generate a CUE file for the source. This function will generate a new track in
|
|
261
|
+
# the file for each metadata of the source. This function tries to map metadata to
|
|
262
|
+
# the appropriate CUE file standard values. You can use the `map_metadata` argument
|
|
263
|
+
# to add your own pre-processing. The following metadata are recognized on tracks:
|
|
264
|
+
# `"title"`, `"artist"`, `"album"`, `"isrc"`, and `"cue_year"`.
|
|
265
|
+
# @category Source / Track processing
|
|
266
|
+
# @flag extra
|
|
267
|
+
# @param filename Path where the CUE file should be written.
|
|
268
|
+
# @param ~last_tracks Only report the number of last tracks.
|
|
269
|
+
# @param ~title Title of the stream.
|
|
270
|
+
# @param ~file File where the stream is stored.
|
|
271
|
+
# @param ~file_type Format in which the stream is stored.
|
|
272
|
+
# @param ~comment Comment about the stream.
|
|
273
|
+
# @param ~year Year for the stream.
|
|
274
|
+
# @param ~map_metadata Function to apply to metadata before writing the CUE file (useful for pre-processing metadata).
|
|
275
|
+
# @param ~temp_dir Temporary directory for atomic write.
|
|
276
|
+
# @param ~deduplicate_using To avoid duplicate entries, duplicate metadata are \
|
|
277
|
+
# filtered. Set this to a list of labels to use for detecting \
|
|
278
|
+
# duplicated metadata.
|
|
279
|
+
# @param ~delete Delete the CUE files when starting if it exists.
|
|
280
|
+
def source.cue(
|
|
281
|
+
~title=null,
|
|
282
|
+
~performer=null,
|
|
283
|
+
~file:f=getter(null),
|
|
284
|
+
~file_type=null,
|
|
285
|
+
~comment=null,
|
|
286
|
+
~year=null,
|
|
287
|
+
~map_metadata=fun (m) -> (m : [(string * string)]),
|
|
288
|
+
~last_tracks=null,
|
|
289
|
+
~temp_dir=null,
|
|
290
|
+
~deduplicate_using=["title", "artist", "album", "isrc", "cue_year"],
|
|
291
|
+
~delete=true,
|
|
292
|
+
filename,
|
|
293
|
+
s
|
|
294
|
+
) =
|
|
295
|
+
is_first = ref(true)
|
|
296
|
+
current_filename = ref("")
|
|
297
|
+
|
|
298
|
+
def write(~append, entries) =
|
|
299
|
+
filename = current_filename()
|
|
300
|
+
f = getter.get(f)
|
|
301
|
+
file_type = file_type ?? file.extension(leading_dot=false, f ?? "")
|
|
302
|
+
|
|
303
|
+
if
|
|
304
|
+
append
|
|
305
|
+
then
|
|
306
|
+
log(
|
|
307
|
+
label="source.cue",
|
|
308
|
+
level=4,
|
|
309
|
+
"Writing new entry to #{filename}"
|
|
310
|
+
)
|
|
311
|
+
else
|
|
312
|
+
log(
|
|
313
|
+
label="source.cue",
|
|
314
|
+
level=4,
|
|
315
|
+
"Writing full CUE file at #{filename}"
|
|
316
|
+
)
|
|
317
|
+
if delete and file.exists(filename) then file.remove(filename) end
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
write =
|
|
321
|
+
file.write.stream(append=append, atomic=true, temp_dir=temp_dir, filename)
|
|
322
|
+
|
|
323
|
+
# Append to the file.
|
|
324
|
+
def w(data) =
|
|
325
|
+
write(data ^ "\n")
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# Write a tag.
|
|
329
|
+
def tag(~indent=0, ~quote=true, name, (value:string?)) =
|
|
330
|
+
quote = if quote then fun (v) -> string.quote(v) else fun (v) -> v end
|
|
331
|
+
|
|
332
|
+
if
|
|
333
|
+
null.defined(value)
|
|
334
|
+
then
|
|
335
|
+
s =
|
|
336
|
+
"#{string.spaces(indent)}#{name} #{quote(null.get(value))}"
|
|
337
|
+
w(s)
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
if
|
|
342
|
+
is_first() or not append
|
|
343
|
+
then
|
|
344
|
+
is_first := false
|
|
345
|
+
|
|
346
|
+
tag("TITLE", title)
|
|
347
|
+
tag("PERFORMER", performer)
|
|
348
|
+
tag(
|
|
349
|
+
"REM COMMENT",
|
|
350
|
+
comment
|
|
351
|
+
)
|
|
352
|
+
tag(
|
|
353
|
+
quote=false,
|
|
354
|
+
"REM DATE",
|
|
355
|
+
null.map(string.of_int, year)
|
|
356
|
+
)
|
|
357
|
+
if
|
|
358
|
+
null.defined(f)
|
|
359
|
+
then
|
|
360
|
+
w(
|
|
361
|
+
"FILE \"#{(null.get(f) : string)}\" #{string.uppercase(file_type)}"
|
|
362
|
+
)
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
list.iter(
|
|
367
|
+
fun (entry) ->
|
|
368
|
+
begin
|
|
369
|
+
let {position = p, time = t, metadata = m} = entry
|
|
370
|
+
|
|
371
|
+
tag(
|
|
372
|
+
indent=2,
|
|
373
|
+
quote=false,
|
|
374
|
+
"TRACK",
|
|
375
|
+
"#{string.of_int(digits=2, p)} AUDIO"
|
|
376
|
+
)
|
|
377
|
+
tag(indent=4, "TITLE", list.assoc.nullable("title", m))
|
|
378
|
+
tag(indent=4, "PERFORMER", list.assoc.nullable("artist", m))
|
|
379
|
+
tag(
|
|
380
|
+
indent=4,
|
|
381
|
+
"REM ALBUM",
|
|
382
|
+
list.assoc.nullable("album", m)
|
|
383
|
+
)
|
|
384
|
+
tag(
|
|
385
|
+
indent=4,
|
|
386
|
+
quote=false,
|
|
387
|
+
"REM DATE",
|
|
388
|
+
list.assoc.nullable("cue_year", m)
|
|
389
|
+
)
|
|
390
|
+
tag(indent=4, quote=false, "ISRC", list.assoc.nullable("isrc", m))
|
|
391
|
+
|
|
392
|
+
frames = int_of_float((t - floor(t)) * 75.)
|
|
393
|
+
t = int_of_float(t)
|
|
394
|
+
minutes = t / 60
|
|
395
|
+
seconds = t mod 60
|
|
396
|
+
m = string.of_int(digits=2, minutes)
|
|
397
|
+
s = string.of_int(digits=2, seconds)
|
|
398
|
+
f = string.of_int(digits=2, frames)
|
|
399
|
+
tag(
|
|
400
|
+
indent=4,
|
|
401
|
+
quote=false,
|
|
402
|
+
"INDEX 01",
|
|
403
|
+
"#{m}:#{s}:#{f}"
|
|
404
|
+
)
|
|
405
|
+
end,
|
|
406
|
+
entries
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
write("")
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
entries = ref([])
|
|
413
|
+
current_position = ref(1)
|
|
414
|
+
t0 = ref(source.time(s))
|
|
415
|
+
|
|
416
|
+
def handle_metadata(m) =
|
|
417
|
+
filename = getter.get(filename)
|
|
418
|
+
if
|
|
419
|
+
current_filename() != filename
|
|
420
|
+
then
|
|
421
|
+
log(
|
|
422
|
+
label="source.cue",
|
|
423
|
+
level=4,
|
|
424
|
+
"Opening new CUE sheet at #{filename}"
|
|
425
|
+
)
|
|
426
|
+
t0 := source.time(s)
|
|
427
|
+
current_position := 1
|
|
428
|
+
current_filename := filename
|
|
429
|
+
is_first := true
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
m = map_metadata(m)
|
|
433
|
+
entry =
|
|
434
|
+
{
|
|
435
|
+
position = current_position(),
|
|
436
|
+
time = source.time(s) - t0(),
|
|
437
|
+
metadata = m
|
|
438
|
+
}
|
|
439
|
+
ref.incr(current_position)
|
|
440
|
+
|
|
441
|
+
if
|
|
442
|
+
null.defined(last_tracks)
|
|
443
|
+
then
|
|
444
|
+
current_entries =
|
|
445
|
+
null.case(
|
|
446
|
+
last_tracks,
|
|
447
|
+
entries,
|
|
448
|
+
fun (last_tracks) ->
|
|
449
|
+
list.rev(list.prefix(last_tracks - 1, list.rev(entries())))
|
|
450
|
+
)
|
|
451
|
+
entries := [...current_entries, entry]
|
|
452
|
+
write(append=false, entries())
|
|
453
|
+
else
|
|
454
|
+
write(append=true, [entry])
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
s = metadata.deduplicate(using=deduplicate_using, s)
|
|
459
|
+
s.on_metadata(synchronous=true, handle_metadata)
|
|
460
|
+
s.on_frame(
|
|
461
|
+
before=false,
|
|
462
|
+
synchronous=true,
|
|
463
|
+
fun () ->
|
|
464
|
+
begin
|
|
465
|
+
# If filename has changed but no metadata handler was executed,
|
|
466
|
+
# insert one
|
|
467
|
+
if
|
|
468
|
+
current_filename() != getter.get(filename)
|
|
469
|
+
then
|
|
470
|
+
s.insert_metadata(new_track=true, s.last_metadata() ?? [])
|
|
471
|
+
end
|
|
472
|
+
end
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
s
|
|
476
|
+
end
|