liquidsoap-prettier 1.8.1 → 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 (68) hide show
  1. package/.github/workflows/check-formatting.yml +32 -0
  2. package/README.md +31 -5
  3. package/dist/liquidsoap.cjs +4155 -3090
  4. package/package.json +1 -1
  5. package/src/cli.js +104 -9
  6. package/tests/liq/audio.liq +460 -0
  7. package/tests/liq/autocue.liq +1081 -0
  8. package/tests/liq/clock.liq +14 -0
  9. package/tests/liq/cron.liq +74 -0
  10. package/tests/liq/error.liq +48 -0
  11. package/tests/liq/extra/audio.liq +677 -0
  12. package/tests/liq/extra/audioscrobbler.liq +482 -0
  13. package/tests/liq/extra/deprecations.liq +976 -0
  14. package/tests/liq/extra/externals.liq +196 -0
  15. package/tests/liq/extra/fades.liq +260 -0
  16. package/tests/liq/extra/file.liq +66 -0
  17. package/tests/liq/extra/http.liq +160 -0
  18. package/tests/liq/extra/interactive.liq +917 -0
  19. package/tests/liq/extra/metadata.liq +75 -0
  20. package/tests/liq/extra/native.liq +201 -0
  21. package/tests/liq/extra/openai.liq +150 -0
  22. package/tests/liq/extra/server.liq +177 -0
  23. package/tests/liq/extra/source.liq +476 -0
  24. package/tests/liq/extra/spinitron.liq +272 -0
  25. package/tests/liq/extra/telnet.liq +266 -0
  26. package/tests/liq/extra/video.liq +59 -0
  27. package/tests/liq/extra/visualization.liq +68 -0
  28. package/tests/liq/fades.liq +941 -0
  29. package/tests/liq/ffmpeg.liq +605 -0
  30. package/tests/liq/file.liq +387 -0
  31. package/tests/liq/getter.liq +74 -0
  32. package/tests/liq/hls.liq +329 -0
  33. package/tests/liq/http.liq +1048 -0
  34. package/tests/liq/http_codes.liq +447 -0
  35. package/tests/liq/icecast.liq +58 -0
  36. package/tests/liq/io.liq +106 -0
  37. package/tests/liq/liquidsoap.liq +31 -0
  38. package/tests/liq/list.liq +440 -0
  39. package/tests/liq/log.liq +47 -0
  40. package/tests/liq/lufs.liq +295 -0
  41. package/tests/liq/math.liq +23 -0
  42. package/tests/liq/medialib.liq +752 -0
  43. package/tests/liq/metadata.liq +253 -0
  44. package/tests/liq/nfo.liq +258 -0
  45. package/tests/liq/null.liq +71 -0
  46. package/tests/liq/playlist.liq +1347 -0
  47. package/tests/liq/predicate.liq +106 -0
  48. package/tests/liq/process.liq +93 -0
  49. package/tests/liq/profiler.liq +5 -0
  50. package/tests/liq/protocols.liq +1139 -0
  51. package/tests/liq/ref.liq +28 -0
  52. package/tests/liq/replaygain.liq +135 -0
  53. package/tests/liq/request.liq +467 -0
  54. package/tests/liq/resolvers.liq +33 -0
  55. package/tests/liq/runtime.liq +70 -0
  56. package/tests/liq/server.liq +99 -0
  57. package/tests/liq/settings.liq +41 -0
  58. package/tests/liq/socket.liq +33 -0
  59. package/tests/liq/source.liq +362 -0
  60. package/tests/liq/sqlite.liq +161 -0
  61. package/tests/liq/stdlib.liq +172 -0
  62. package/tests/liq/string.liq +476 -0
  63. package/tests/liq/switches.liq +197 -0
  64. package/tests/liq/testing.liq +37 -0
  65. package/tests/liq/thread.liq +161 -0
  66. package/tests/liq/tracks.liq +100 -0
  67. package/tests/liq/utils.liq +81 -0
  68. package/tests/liq/video.liq +918 -0
@@ -0,0 +1,482 @@
1
+ let error.audioscrobbler = error.register("audioscrobbler")
2
+
3
+ let settings.audioscrobbler =
4
+ settings.make.void(
5
+ "Audioscrobbler settings"
6
+ )
7
+
8
+ let settings.audioscrobbler.api_key =
9
+ settings.make(
10
+ description="Default API key for audioscrobbler",
11
+ ""
12
+ )
13
+
14
+ let settings.audioscrobbler.api_secret =
15
+ settings.make(
16
+ description="Default API secret for audioscrobbler",
17
+ ""
18
+ )
19
+
20
+ audioscrobbler = ()
21
+
22
+ def audioscrobbler.request(
23
+ ~base_url="http://ws.audioscrobbler.com/2.0",
24
+ ~api_key=null,
25
+ ~api_secret=null,
26
+ params
27
+ ) =
28
+ api_key = api_key ?? settings.audioscrobbler.api_key()
29
+ api_secret = api_secret ?? settings.audioscrobbler.api_secret()
30
+
31
+ if
32
+ api_key == "" or api_secret == ""
33
+ then
34
+ error.raise(
35
+ error.audioscrobbler,
36
+ "`api_key` or `api_secret` missing!"
37
+ )
38
+ end
39
+
40
+ params = [("api_key", api_key), ...params]
41
+
42
+ sig_params = list.sort(fun (v, v') -> string.compare(fst(v), fst(v')), params)
43
+ sig_params = list.map(fun (v) -> "#{fst(v)}#{(snd(v) : string)}", sig_params)
44
+ sig_params = string.concat(separator="", sig_params)
45
+
46
+ api_sig = string.digest("#{sig_params}#{api_secret}")
47
+
48
+ http.post(
49
+ base_url,
50
+ headers=[("Content-Type", "application/x-www-form-urlencoded")],
51
+ data=http.www_form_urlencoded([...params, ("api_sig", api_sig)])
52
+ )
53
+ end
54
+
55
+ def audioscrobbler.check_response(resp) =
56
+ let xml.parse ({lfm = {xml_params = {status}}} :
57
+ {lfm: {xml_params: {status: string}}}
58
+ ) = resp
59
+ if
60
+ (status == "failed")
61
+ then
62
+ error_ref = error
63
+ let xml.parse ({lfm = {error = {xml_params = {code}}}} :
64
+ {lfm: {error: string.{ xml_params: {code: int} }}}
65
+ ) = resp
66
+ error_ref.raise(
67
+ error_ref.audioscrobbler,
68
+ "Error #{code}: #{error}"
69
+ )
70
+ end
71
+ end
72
+
73
+ def audioscrobbler.auth(~username, ~password, ~api_key=null, ~api_secret=null) =
74
+ resp =
75
+ audioscrobbler.request(
76
+ api_key=api_key,
77
+ api_secret=api_secret,
78
+ [
79
+ ("method", "auth.getMobileSession"),
80
+ ("username", username),
81
+ ("password", password)
82
+ ]
83
+ )
84
+
85
+ audioscrobbler.check_response(resp)
86
+
87
+ try
88
+ let xml.parse ({lfm = {session = {key}}} :
89
+ {lfm: {session: {name: string, key: string}}}
90
+ ) = resp
91
+ key
92
+ catch err do
93
+ error.raise(
94
+ error.invalid,
95
+ "Invalid response: #{resp}, error: #{err}"
96
+ )
97
+ end
98
+ end
99
+
100
+ let audioscrobbler.api = {track = ()}
101
+
102
+ # Submit a track to the audioscrobbler
103
+ # `track.updateNowPlaying` API.
104
+ # @category Interaction
105
+ def audioscrobbler.api.track.updateNowPlaying(
106
+ ~username,
107
+ ~password,
108
+ ~session_key=null,
109
+ ~api_key=null,
110
+ ~api_secret=null,
111
+ ~artist,
112
+ ~track,
113
+ ~album=null,
114
+ ~context=null,
115
+ ~trackNumber=null,
116
+ ~mbid=null,
117
+ ~albumArtist=null,
118
+ ~duration=null
119
+ ) =
120
+ session_key =
121
+ session_key
122
+ ?? audioscrobbler.auth(
123
+ username=username,
124
+ password=password,
125
+ api_key=api_key,
126
+ api_secret=api_secret
127
+ )
128
+
129
+ params =
130
+ [
131
+ ("track", track),
132
+ ("artist", artist),
133
+ ...(null.defined(album) ? [("album", null.get(album))] : []),
134
+ ...(null.defined(context) ? [("context", null.get(context))] : []),
135
+ ...(
136
+ null.defined(trackNumber)
137
+ ? [("trackNumber", string((null.get(trackNumber) : int)))]
138
+ : []
139
+ ),
140
+ ...(null.defined(mbid) ? [("mbid", null.get(mbid))] : []),
141
+ ...(
142
+ null.defined(albumArtist)
143
+ ? [("albumArtist", null.get(albumArtist))]
144
+ : []
145
+ ),
146
+ ...(
147
+ null.defined(duration)
148
+ ? [("duration", string((null.get(duration) : int)))]
149
+ : []
150
+ )
151
+ ]
152
+
153
+ log.info(
154
+ label="audioscrobbler.api.track.updateNowPlaying",
155
+ "Submitting updateNowPlaying with: #{params}"
156
+ )
157
+
158
+ resp =
159
+ audioscrobbler.request(
160
+ api_key=api_key,
161
+ api_secret=api_secret,
162
+ [...params, ("method", "track.updateNowPlaying"), ("sk", session_key)]
163
+ )
164
+
165
+ audioscrobbler.check_response(resp)
166
+
167
+ try
168
+ let xml.parse (v :
169
+ {
170
+ lfm: {
171
+ nowplaying: {
172
+ track: string.{ xml_params: {corrected: int} },
173
+ artist: string.{ xml_params: {corrected: int} },
174
+ album: string?.{ xml_params: {corrected: int} },
175
+ albumArtist: string?.{ xml_params: {corrected: int} },
176
+ ignoredMessage: {xml_params: {code: int}}
177
+ },
178
+ xml_params: {status: string}
179
+ }
180
+ }
181
+ ) = resp
182
+
183
+ log.info(
184
+ label="audioscrobbler.api.track.updateNowPlaying",
185
+ "Done submitting updateNowPlaying with: #{params}"
186
+ )
187
+
188
+ v
189
+ catch err do
190
+ error.raise(
191
+ error.invalid,
192
+ "Invalid response: #{resp}, error: #{err}"
193
+ )
194
+ end
195
+ end
196
+
197
+ # @flag hidden
198
+ def audioscrobbler.api.apply_meta(
199
+ ~name,
200
+ ~username,
201
+ ~password,
202
+ ~api_key,
203
+ ~api_secret,
204
+ ~session_key,
205
+ fn,
206
+ m
207
+ ) =
208
+ def c(v) =
209
+ v == "" ? null : v
210
+ end
211
+ track = m["title"]
212
+ artist = m["artist"]
213
+
214
+ if
215
+ track == "" or artist == ""
216
+ then
217
+ log.info(
218
+ label=name,
219
+ "No artist or track present: metadata submission disabled!"
220
+ )
221
+ else
222
+ album = c(m["album"])
223
+ trackNumber =
224
+ try
225
+ null.map(int_of_string, c(m["tracknumber"]))
226
+ catch _ do
227
+ null
228
+ end
229
+ albumArtist = c(m["albumartist"])
230
+ ignore(
231
+ fn(
232
+ username=username,
233
+ password=password,
234
+ api_key=api_key,
235
+ api_secret=api_secret,
236
+ session_key=session_key,
237
+ track=track,
238
+ artist=artist,
239
+ album=album,
240
+ trackNumber=trackNumber,
241
+ albumArtist=albumArtist
242
+ )
243
+ )
244
+ end
245
+ end
246
+
247
+ # Submit a track using its metadata to the audioscrobbler
248
+ # `track.updateNowPlaying` API.
249
+ # @category Interaction
250
+ def audioscrobbler.api.track.updateNowPlaying.metadata(
251
+ ~username,
252
+ ~password,
253
+ ~session_key=null,
254
+ ~api_key=null,
255
+ ~api_secret=null,
256
+ m
257
+ ) =
258
+ audioscrobbler.api.apply_meta(
259
+ username=username,
260
+ password=password,
261
+ session_key=session_key,
262
+ api_key=api_key,
263
+ api_secret=api_secret,
264
+ name="audioscrobbler.api.track.updateNowPlaying",
265
+ audioscrobbler.api.track.updateNowPlaying,
266
+ m
267
+ )
268
+ end
269
+
270
+ # Submit a track to the audioscrobbler
271
+ # `track.scrobble` API.
272
+ # @category Interaction
273
+ def audioscrobbler.api.track.scrobble(
274
+ ~username,
275
+ ~password,
276
+ ~session_key=null,
277
+ ~api_key=null,
278
+ ~api_secret=null,
279
+ ~artist,
280
+ ~track,
281
+ ~timestamp=null,
282
+ ~album=null,
283
+ ~context=null,
284
+ ~streamId=null,
285
+ ~chosenByUser=true,
286
+ ~trackNumber=null,
287
+ ~mbid=null,
288
+ ~albumArtist=null,
289
+ ~duration=null
290
+ ) =
291
+ session_key =
292
+ session_key
293
+ ?? audioscrobbler.auth(
294
+ username=username,
295
+ password=password,
296
+ api_key=api_key,
297
+ api_secret=api_secret
298
+ )
299
+
300
+ params =
301
+ [
302
+ ("track", track),
303
+ ("artist", artist),
304
+ ("timestamp", string(timestamp ?? time())),
305
+ ...(null.defined(album) ? [("album", null.get(album))] : []),
306
+ ...(null.defined(context) ? [("context", null.get(context))] : []),
307
+ ...(null.defined(streamId) ? [("streamId", null.get(streamId))] : []),
308
+ ("chosenByUser", chosenByUser ? "1" : "0"),
309
+ ...(
310
+ null.defined(trackNumber)
311
+ ? [("trackNumber", string((null.get(trackNumber) : int)))]
312
+ : []
313
+ ),
314
+ ...(null.defined(mbid) ? [("mbid", null.get(mbid))] : []),
315
+ ...(
316
+ null.defined(albumArtist)
317
+ ? [("albumArtist", null.get(albumArtist))]
318
+ : []
319
+ ),
320
+ ...(
321
+ null.defined(duration)
322
+ ? [("duration", string((null.get(duration) : int)))]
323
+ : []
324
+ )
325
+ ]
326
+
327
+ log.info(
328
+ label="audioscrobbler.api.track.scrobble",
329
+ "Submitting updateNowPlaying with: #{params}"
330
+ )
331
+
332
+ resp =
333
+ audioscrobbler.request(
334
+ api_key=api_key,
335
+ api_secret=api_secret,
336
+ [...params, ("method", "track.scrobble"), ("sk", session_key)]
337
+ )
338
+
339
+ audioscrobbler.check_response(resp)
340
+
341
+ try
342
+ let xml.parse (v :
343
+ {
344
+ lfm: {
345
+ scrobbles: {
346
+ scrobble: {
347
+ track: string.{ xml_params: {corrected: int} },
348
+ artist: string.{ xml_params: {corrected: int} },
349
+ album: string?.{ xml_params: {corrected: int} },
350
+ albumArtist: string?.{ xml_params: {corrected: int} },
351
+ timestamp: float,
352
+ ignoredMessage: {xml_params: {code: int}}
353
+ },
354
+ xml_params: {ignored: int, accepted: int}
355
+ },
356
+ xml_params: {status: string}
357
+ }
358
+ }
359
+ ) = resp
360
+
361
+ log.info(
362
+ label="audioscrobbler.api.track.scrobble",
363
+ "Done submitting scrobble with: #{params}"
364
+ )
365
+
366
+ v
367
+ catch err do
368
+ error.raise(
369
+ error.invalid,
370
+ "Invalid response: #{resp}, error: #{err}"
371
+ )
372
+ end
373
+ end
374
+
375
+ # Submit a track to the audioscrobbler
376
+ # `track.scrobble` API using its metadata.
377
+ # @category Interaction
378
+ def audioscrobbler.api.track.scrobble.metadata(
379
+ ~username,
380
+ ~password,
381
+ ~session_key=null,
382
+ ~api_key=null,
383
+ ~api_secret=null,
384
+ m
385
+ ) =
386
+ audioscrobbler.api.apply_meta(
387
+ username=username,
388
+ password=password,
389
+ session_key=session_key,
390
+ api_key=api_key,
391
+ api_secret=api_secret,
392
+ name="audioscrobbler.api.track.scrobble",
393
+ audioscrobbler.api.track.scrobble,
394
+ m
395
+ )
396
+ end
397
+
398
+ # Submit songs using audioscrobbler, respecting the full protocol:
399
+ # First signal song as now playing when starting, and
400
+ # then submit song when it ends.
401
+ # @category Interaction
402
+ # @flag extra
403
+ # @param ~source Source for tracks. Should be one of: "broadcast", "user", "recommendation" or "unknown". Since liquidsoap is intended for radio broadcasting, this is the default. Sources other than user don't need duration to be set.
404
+ # @param ~delay Submit song when there is only this delay left, in seconds.
405
+ # @param ~force If remaining time is null, the song will be assumed to be skipped or cut, and not submitted. Set this to `true` to prevent this behavior
406
+ # @param ~metadata_preprocessor Metadata pre-processor callback. Can be used to change metadata on-the-fly before sending to nowPlaying/scrobble. If returning an empty metadata, nothing is sent at all.
407
+ def audioscrobbler.submit(
408
+ ~username,
409
+ ~password,
410
+ ~api_key=null,
411
+ ~api_secret=null,
412
+ ~delay=10.,
413
+ ~force=false,
414
+ ~metadata_preprocessor=fun (m) -> m,
415
+ s
416
+ ) =
417
+ session_key =
418
+ audioscrobbler.auth(
419
+ username=username,
420
+ password=password,
421
+ api_key=api_key,
422
+ api_secret=api_secret
423
+ )
424
+
425
+ def now_playing(m) =
426
+ try
427
+ audioscrobbler.api.track.updateNowPlaying.metadata(
428
+ username=username,
429
+ password=password,
430
+ api_key=api_key,
431
+ api_secret=api_secret,
432
+ session_key=session_key,
433
+ metadata_preprocessor(m)
434
+ )
435
+ catch err do
436
+ log.important(
437
+ "Error while submitting nowplaying info for #{source.id(s)}: #{err}"
438
+ )
439
+ end
440
+ end
441
+
442
+ s = source.methods(s)
443
+ s.on_metadata(synchronous=false, now_playing)
444
+
445
+ f =
446
+ fun (rem, m) ->
447
+ # Avoid skipped songs
448
+ if
449
+ rem > 0. or force
450
+ then
451
+ thread.run(
452
+ delay=0.,
453
+ {
454
+ try
455
+ audioscrobbler.api.track.scrobble.metadata(
456
+ username=username,
457
+ password=password,
458
+ api_key=api_key,
459
+ api_secret=api_secret,
460
+ session_key=session_key,
461
+ metadata_preprocessor(m)
462
+ )
463
+ catch err do
464
+ log.important(
465
+ "Error while submitting scrobble info for #{source.id(s)}: #{
466
+ err
467
+ }"
468
+ )
469
+ end
470
+ }
471
+ )
472
+ else
473
+ log(
474
+ label="audioscrobbler.submit",
475
+ level=4,
476
+ "Remaining time null: will not submit song (song skipped ?)"
477
+ )
478
+ end
479
+ s.on_position(synchronous=true, remaining=true, position=delay, f)
480
+
481
+ (s : source)
482
+ end