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.
Files changed (67) hide show
  1. package/.github/workflows/check-formatting.yml +32 -0
  2. package/README.md +31 -5
  3. package/package.json +1 -1
  4. package/src/cli.js +104 -9
  5. package/tests/liq/audio.liq +460 -0
  6. package/tests/liq/autocue.liq +1081 -0
  7. package/tests/liq/clock.liq +14 -0
  8. package/tests/liq/cron.liq +74 -0
  9. package/tests/liq/error.liq +48 -0
  10. package/tests/liq/extra/audio.liq +677 -0
  11. package/tests/liq/extra/audioscrobbler.liq +482 -0
  12. package/tests/liq/extra/deprecations.liq +976 -0
  13. package/tests/liq/extra/externals.liq +196 -0
  14. package/tests/liq/extra/fades.liq +260 -0
  15. package/tests/liq/extra/file.liq +66 -0
  16. package/tests/liq/extra/http.liq +160 -0
  17. package/tests/liq/extra/interactive.liq +917 -0
  18. package/tests/liq/extra/metadata.liq +75 -0
  19. package/tests/liq/extra/native.liq +201 -0
  20. package/tests/liq/extra/openai.liq +150 -0
  21. package/tests/liq/extra/server.liq +177 -0
  22. package/tests/liq/extra/source.liq +476 -0
  23. package/tests/liq/extra/spinitron.liq +272 -0
  24. package/tests/liq/extra/telnet.liq +266 -0
  25. package/tests/liq/extra/video.liq +59 -0
  26. package/tests/liq/extra/visualization.liq +68 -0
  27. package/tests/liq/fades.liq +941 -0
  28. package/tests/liq/ffmpeg.liq +605 -0
  29. package/tests/liq/file.liq +387 -0
  30. package/tests/liq/getter.liq +74 -0
  31. package/tests/liq/hls.liq +329 -0
  32. package/tests/liq/http.liq +1048 -0
  33. package/tests/liq/http_codes.liq +447 -0
  34. package/tests/liq/icecast.liq +58 -0
  35. package/tests/liq/io.liq +106 -0
  36. package/tests/liq/liquidsoap.liq +31 -0
  37. package/tests/liq/list.liq +440 -0
  38. package/tests/liq/log.liq +47 -0
  39. package/tests/liq/lufs.liq +295 -0
  40. package/tests/liq/math.liq +23 -0
  41. package/tests/liq/medialib.liq +752 -0
  42. package/tests/liq/metadata.liq +253 -0
  43. package/tests/liq/nfo.liq +258 -0
  44. package/tests/liq/null.liq +71 -0
  45. package/tests/liq/playlist.liq +1347 -0
  46. package/tests/liq/predicate.liq +106 -0
  47. package/tests/liq/process.liq +93 -0
  48. package/tests/liq/profiler.liq +5 -0
  49. package/tests/liq/protocols.liq +1139 -0
  50. package/tests/liq/ref.liq +28 -0
  51. package/tests/liq/replaygain.liq +135 -0
  52. package/tests/liq/request.liq +467 -0
  53. package/tests/liq/resolvers.liq +33 -0
  54. package/tests/liq/runtime.liq +70 -0
  55. package/tests/liq/server.liq +99 -0
  56. package/tests/liq/settings.liq +41 -0
  57. package/tests/liq/socket.liq +33 -0
  58. package/tests/liq/source.liq +362 -0
  59. package/tests/liq/sqlite.liq +161 -0
  60. package/tests/liq/stdlib.liq +172 -0
  61. package/tests/liq/string.liq +476 -0
  62. package/tests/liq/switches.liq +197 -0
  63. package/tests/liq/testing.liq +37 -0
  64. package/tests/liq/thread.liq +161 -0
  65. package/tests/liq/tracks.liq +100 -0
  66. package/tests/liq/utils.liq +81 -0
  67. package/tests/liq/video.liq +918 -0
@@ -0,0 +1,75 @@
1
+ # Store and retrieve file covers using metadata. This returns a set of
2
+ # getter/setter methods that can be used to store and retrieve cover art.
3
+ # Typical usage is to set cover art in a `on_metadata` handler and retrieve
4
+ # it in a `video.add_image` operator. See `video.add_cover` for an implementation
5
+ # example.
6
+ # @category Metadata
7
+ # @flag extra
8
+ # @param ~default Default cover file when no cover is available
9
+ # @param ~mime_types Recognized mime types and their corresponding file extensions.
10
+ def file.cover.manager(
11
+ ~id=null,
12
+ ~mime_types=[
13
+ ("image/gif", "gif"),
14
+ ("image/jpg", "jpeg"),
15
+ ("image/jpeg", "jpeg"),
16
+ ("image/png", "png"),
17
+ ("image/webp", "webp")
18
+ ],
19
+ ~default
20
+ ) =
21
+ id = string.id.default(id, default="cover")
22
+ default = request.create(persistent=true, default)
23
+
24
+ current_cover_request = ref(default)
25
+
26
+ def extract_cover_from_metadata(_metadata) =
27
+ filename = _metadata["filename"]
28
+ log.info(
29
+ label=id,
30
+ "Extracting cover from #{string.quote(filename)}."
31
+ )
32
+ cover = metadata.cover(_metadata)
33
+
34
+ new_cover =
35
+ if
36
+ not null.defined(cover)
37
+ then
38
+ log.important(
39
+ label=id,
40
+ "File #{string.quote(filename)} has no cover."
41
+ )
42
+ null
43
+ else
44
+ cover = null.get(cover)
45
+ extension = mime_types[cover.mime]
46
+ if
47
+ extension == ""
48
+ then
49
+ log.important(
50
+ label=id,
51
+ "File #{string.quote(filename)} has unknown mime type #{
52
+ string.quote(cover.mime)
53
+ }."
54
+ )
55
+ null
56
+ else
57
+ cover_file = file.temp("#{id}_", ".#{extension}")
58
+ file.write(cover_file, data=cover)
59
+
60
+ log.important(
61
+ label=id,
62
+ "Cover for #{string.quote(filename)} saved to #{
63
+ string.quote(cover_file)
64
+ }."
65
+ )
66
+
67
+ request.create(temporary=true, cover_file)
68
+ end
69
+ end
70
+
71
+ current_cover_request := (new_cover ?? default)
72
+ end
73
+
74
+ current_cover_request.{set = extract_cover_from_metadata}
75
+ end
@@ -0,0 +1,201 @@
1
+ # Native reimplementation of track functions.
2
+ # @category Source / Track processing
3
+ let native = ()
4
+
5
+ # Create a source that plays only one track of the input source.
6
+ # @category Source / Track processing
7
+ # @flag extra
8
+ def native.once(s) =
9
+ # source.available(track_sensitive=true, s, predicate.first({true}))
10
+ a = ref(true)
11
+ s.on_track(synchronous=true, fun (_) -> a := false)
12
+ source.available(s, a)
13
+ end
14
+
15
+ # At the beginning of each track, select the first ready child.
16
+ # @category Source / Track processing
17
+ # @flag extra
18
+ # @param ~id Force the value of the source ID.
19
+ # @param ~track_sensitive Re-select only on end of tracks.
20
+ def native.fallback(~id=null, ~track_sensitive=true, sources) =
21
+ fail = (source.fail() : source)
22
+
23
+ def s() =
24
+ list.find(default=fail, source.is_ready, getter.get(sources))
25
+ end
26
+
27
+ infallible =
28
+ getter.is_constant(sources)
29
+ and not (list.exists(source.fallible, getter.get(sources)))
30
+
31
+ source.dynamic(
32
+ id=id,
33
+ infallible=infallible,
34
+ track_sensitive=track_sensitive,
35
+ s
36
+ )
37
+ end
38
+
39
+ # Play only one track of every successive source, except for the last one which
40
+ # is played as much as available.
41
+ # @category Source / Track processing
42
+ # @flag extra
43
+ # @param ~id Force the value of the source ID.
44
+ # @param sources List of sources to play tracks from.
45
+ def native.sequence(~id=null, sources) =
46
+ len = list.length(sources)
47
+ n = ref(0)
48
+ fail = source.fail()
49
+
50
+ def rec s() =
51
+ sn = list.nth(default=list.last(default=fail, sources), sources, n())
52
+ if
53
+ source.is_ready(sn) or n() >= len - 1
54
+ then
55
+ sn
56
+ else
57
+ ref.incr(n)
58
+ s()
59
+ end
60
+ end
61
+
62
+ infallible = not source.fallible(list.last(default=fail, sources))
63
+ s = source.dynamic(id=id, infallible=infallible, track_sensitive=true, s)
64
+ first = ref(true)
65
+
66
+ def ot(_) =
67
+ # Drop the first track
68
+ if first() then first := false else ref.incr(n) end
69
+ end
70
+
71
+ s.on_track(synchronous=true, ot)
72
+ s
73
+ end
74
+
75
+ # Select the first source whose predicate is true in a list. If the second
76
+ # argument is a getter, the source will be dynamically created.
77
+ # @category Source / Track processing
78
+ # @flag extra
79
+ # @param ~id Force the value of the source ID.
80
+ # @param ~track_sensitive Re-select only on end of tracks.
81
+ # @param sources Sources with the predicate telling when they can be played.
82
+ def native.switch(~id=null, ~track_sensitive=true, sources) =
83
+ sources = list.map(fun (ps) -> source.available(snd(ps), fst(ps)), sources)
84
+ native.fallback(id=id, track_sensitive=track_sensitive, sources)
85
+ end
86
+
87
+ # This allows doing `open native`
88
+ # @category Source / Track processing
89
+ let native.request = request
90
+
91
+ # @docof request.dynamic
92
+ def native.request.dynamic(%argsof(request.dynamic), f) =
93
+ ignore(available)
94
+ ignore(timeout)
95
+ ignore(native)
96
+ ignore(synchronous)
97
+
98
+ def f() =
99
+ try
100
+ f()
101
+ catch _ do
102
+ null
103
+ end
104
+ end
105
+
106
+ # Prepared requests
107
+ queue = ref([])
108
+
109
+ def get_queue() =
110
+ def get_request(s) =
111
+ s.request
112
+ end
113
+
114
+ list.map(get_request, queue())
115
+ end
116
+
117
+ def add(r) =
118
+ s = request.once(r)
119
+ if
120
+ s.resolve()
121
+ then
122
+ queue := [...queue(), s]
123
+ true
124
+ else
125
+ false
126
+ end
127
+ end
128
+
129
+ def set_queue(l) =
130
+ queue := []
131
+ list.iter(fun (r) -> ignore(add(r)), l)
132
+ end
133
+
134
+ current = ref(null)
135
+
136
+ def get_current() =
137
+ if
138
+ null.defined(current())
139
+ then
140
+ s = null.get(current())
141
+ s.request
142
+ else
143
+ null
144
+ end
145
+ end
146
+
147
+ # Prefetch thread
148
+ def fetch() =
149
+ r = f()
150
+ if
151
+ null.defined(r)
152
+ then
153
+ r = null.get(r)
154
+ s = request.once(r)
155
+ if
156
+ s.resolve()
157
+ then
158
+ log.info(
159
+ "Added request on queue: #{request.uri(r)}."
160
+ )
161
+ queue := [...queue(), s]
162
+ true
163
+ else
164
+ log.important(
165
+ "Failed to resolve request #{request.uri(r)}."
166
+ )
167
+ false
168
+ end
169
+ else
170
+ false
171
+ end
172
+ end
173
+
174
+ def fill() =
175
+ if list.length(queue()) < prefetch then ignore(fetch()) end
176
+ end
177
+
178
+ thread.run(every=retry_delay, fill)
179
+
180
+ # Source
181
+ def s() =
182
+ if
183
+ not list.is_empty(queue())
184
+ then
185
+ let [s, ...rest] = queue()
186
+ queue := rest
187
+ current := s
188
+ s
189
+ else
190
+ source.fail()
191
+ end
192
+ end
193
+
194
+ source.dynamic(id=id, track_sensitive=true, s).{
195
+ fetch = fetch,
196
+ queue = get_queue,
197
+ add = add,
198
+ set_queue = set_queue,
199
+ current = get_current
200
+ }
201
+ end
@@ -0,0 +1,150 @@
1
+ let error.openai = error.register("openai")
2
+
3
+ openai = ()
4
+
5
+ # @flag hidden
6
+ def parse_openai_error(ans, err) =
7
+ try
8
+ let json.parse (e : {error: {message: string, type: string}}) = ans
9
+
10
+ e = e.error
11
+
12
+ error.raise(
13
+ error.openai,
14
+ "#{e.type}: #{e.message}"
15
+ )
16
+ catch _ do
17
+ error.raise(err)
18
+ end
19
+ end
20
+
21
+ # Query ChatGPT API.
22
+ # @param ~base_url Base URL for the API query
23
+ # @param ~key OpenAI API key.
24
+ # @param ~model Language model.
25
+ # @param ~timeout Timeout for network operations in seconds.
26
+ # @param messages Messages initially exchanged.
27
+ # @category Internet
28
+ # @flag extra
29
+ def openai.chat(
30
+ ~key,
31
+ ~base_url="https://api.openai.com",
32
+ ~model="gpt-3.5-turbo",
33
+ ~timeout=null(30.),
34
+ (
35
+ messages:
36
+ [
37
+ {
38
+ role: string,
39
+ content: string,
40
+ name?: string,
41
+ tool_calls?: [
42
+ {id: string, type: string, function: {name: string, arguments: string}}
43
+ ],
44
+ tool_call_id?: string
45
+ }
46
+ ]
47
+ )
48
+ ) =
49
+ payload = {model = model, messages = messages}
50
+
51
+ ans =
52
+ http.post(
53
+ data=json.stringify(payload),
54
+ timeout=timeout,
55
+ headers=[
56
+ ("Content-Type", "application/json"),
57
+ (
58
+ "Authorization",
59
+ "Bearer #{(key : string)}"
60
+ )
61
+ ],
62
+ "#{base_url}/v1/chat/completions"
63
+ )
64
+
65
+ if
66
+ ans.status_code != 200
67
+ then
68
+ error.raise(
69
+ error.http,
70
+ "#{ans.status_code}: #{ans.status_message}"
71
+ )
72
+ end
73
+
74
+ try
75
+ let json.parse (ans :
76
+ {
77
+ choices: [
78
+ {
79
+ finish_reason: string,
80
+ index: int,
81
+ message: {content: string, role: string}
82
+ }
83
+ ],
84
+ created: int,
85
+ model: string,
86
+ object: string,
87
+ usage: {completion_tokens: int, prompt_tokens: int, total_tokens: int}
88
+ }
89
+ ) = ans
90
+ ans
91
+ catch err : [error.json] do
92
+ parse_openai_error(ans, err)
93
+ end
94
+ end
95
+
96
+ # Generate speech using openai. Returns the encoded audio data.
97
+ # @param ~base_url Base URL for the API query
98
+ # @param ~key OpenAI API key.
99
+ # @param ~model Language model.
100
+ # @param ~timeout Timeout for network operations in seconds.
101
+ # @param ~voice The voice to use when generating the audio. Supported voices are `"alloy"`, `"echo"`, `"fable"`, `"onyx"`, `"nova"`, and `"shimmer"`
102
+ # @param ~response_format The format to audio in. Supported formats are: `"mp3"`, `"opus"`, `"aac"`, and `"flac"`.
103
+ # @param ~speed The speed of the generated audio. Select a value from `0.25` to `4.0`. `1.0` is the default.
104
+ # @params ~on_data Function executed when receiving the audio data.
105
+ # @category Internet
106
+ # @flag extra
107
+ def openai.speech(
108
+ ~key,
109
+ ~base_url="https://api.openai.com",
110
+ ~model="tts-1",
111
+ ~timeout=null(30.),
112
+ ~voice,
113
+ ~response_format="mp3",
114
+ ~speed=1.,
115
+ ~on_data,
116
+ (input:string)
117
+ ) =
118
+ payload =
119
+ {
120
+ model = model,
121
+ input = input,
122
+ voice = (voice : string),
123
+ response_format = response_format,
124
+ speed = speed
125
+ }
126
+
127
+ ans =
128
+ http.post.stream(
129
+ data=json.stringify(payload),
130
+ timeout=timeout,
131
+ headers=[
132
+ ("Content-Type", "application/json"),
133
+ (
134
+ "Authorization",
135
+ "Bearer #{(key : string)}"
136
+ )
137
+ ],
138
+ on_body_data=on_data,
139
+ "#{base_url}/v1/audio/speech"
140
+ )
141
+
142
+ if
143
+ ans.status_code != 200
144
+ then
145
+ error.raise(
146
+ error.http,
147
+ "#{ans.status_code}: #{ans.status_message}"
148
+ )
149
+ end
150
+ end
@@ -0,0 +1,177 @@
1
+ # Register a command that outputs the RMS of the returned source.
2
+ # @flag extra
3
+ # @category Source / Visualization
4
+ # @param ~id Force the value of the source ID.
5
+ def server.rms(~id=null, s) =
6
+ let s = rms(id=id, s)
7
+
8
+ def rms(_) =
9
+ rms = s.rms()
10
+ "#{rms}"
11
+ end
12
+
13
+ s.register_command(
14
+ description="Return the current RMS of the source.",
15
+ usage="rms",
16
+ "rms",
17
+ rms
18
+ )
19
+
20
+ s
21
+ end
22
+
23
+ # Register a server/telnet command to update a source's metadata. Returns a new
24
+ # source, which will receive the updated metadata. The command has the following
25
+ # format: insert key1="val1",key2="val2",...
26
+ # @flag extra
27
+ # @category Source / Track processing
28
+ # @param ~id Force the value of the source ID.
29
+ def server.insert_metadata(s) =
30
+ def insert(s) =
31
+ let (meta, _) = string.annotate.parse("#{s}:")
32
+ if
33
+ meta != []
34
+ then
35
+ s.insert_metadata(meta)
36
+ "Done"
37
+ else
38
+ "Syntax error or no metadata given. Use key1=\"val1\",key2=\"val2\",.."
39
+ end
40
+ end
41
+
42
+ s.register_command(
43
+ description="Insert a metadata chunk.",
44
+ usage="insert key1=\"val1\",key2=\"val2\",..",
45
+ "insert",
46
+ insert
47
+ )
48
+
49
+ s
50
+ end
51
+
52
+ # Start an interface for the "telnet" server over http.
53
+ # @category Internet
54
+ # @flag extra
55
+ # @param ~port Port of the server.
56
+ # @param ~transport Http transport. Use `http.transport.ssl` or http.transport.secure_transport`, when available, to enable HTTPS output
57
+ # @param ~uri URI of the server.
58
+ def server.harbor(~transport=http.transport.unix, ~port=8000, ~uri="/telnet") =
59
+ def webpage(_, response) =
60
+ response.html(
61
+ "
62
+ <html>
63
+ <head>
64
+ <meta charset='utf-8'/>
65
+ <title>Liquidsoap telnet server</title>
66
+ <style>
67
+ body {
68
+ font-family: sans-serif;
69
+ }
70
+ h1 {
71
+ text-align: center;
72
+ }
73
+ textarea {
74
+ display: block;
75
+ margin: 0 auto;
76
+ color: lightgreen;
77
+ background-color: black;
78
+ padding: 1ex;
79
+ }
80
+ </style>
81
+ <script>
82
+ window.onload = function () {
83
+ c = document.getElementById('console');
84
+
85
+ function send() {
86
+ var lines = c.value.substr(0, c.selectionStart).split('\\n');
87
+ var line = lines[lines.length-1];
88
+ var data = line;
89
+ console.log('send ' + line);
90
+ var xmlHttp = new XMLHttpRequest();
91
+ xmlHttp.open('POST', '#{
92
+ uri
93
+ }');
94
+ xmlHttp.onreadystatechange = function () {
95
+ if(xmlHttp.readyState === XMLHttpRequest.DONE) {
96
+ var status = xmlHttp.status;
97
+ if (status === 0 || (status >= 200 && status < 400)) {
98
+ c.value += xmlHttp.responseText + 'END\\n';
99
+ c.scrollTop = c.scrollHeight;
100
+ } else {
101
+ console.log('Failed to send values.')
102
+ }
103
+ }
104
+ }
105
+ xmlHttp.send(data);
106
+ }
107
+
108
+ c.addEventListener('keypress', function(e) {if (e.which == 13) {send()}})
109
+ }
110
+ </script>
111
+ </head>
112
+ <body>
113
+ <h1>Liquidsoap telnet server</h1>
114
+ <textarea id='console' cols='80' rows='25'></textarea>
115
+ <p style='text-align: center'>Type <code>help</code> if you are lost.</p>
116
+ </body>
117
+ </html>
118
+ "
119
+ )
120
+ end
121
+
122
+ harbor.http.register(
123
+ transport=transport,
124
+ port=port,
125
+ method="GET",
126
+ uri,
127
+ webpage
128
+ )
129
+
130
+ def setter(request, response) =
131
+ log.info(
132
+ "Executing command: #{request.data}"
133
+ )
134
+ answer = server.execute(request.body())
135
+ answer = string.concat(separator="\n", answer) ^ "\n"
136
+ response.data(answer)
137
+ end
138
+
139
+ harbor.http.register(
140
+ transport=transport,
141
+ port=port,
142
+ method="POST",
143
+ uri,
144
+ setter
145
+ )
146
+
147
+ log.important(
148
+ label="server.harbor",
149
+ "Website should be ready at <http://localhost:#{port}#{uri}>."
150
+ )
151
+ end
152
+
153
+ # Add a skip telnet command to a source when it does not have one by default.
154
+ # @category Interaction
155
+ # @flag extra
156
+ # @param s The source to attach the command to.
157
+ def add_skip_command(s) =
158
+ # A command to skip
159
+ def skip(_) =
160
+ s.skip()
161
+ "Done!"
162
+ end
163
+
164
+ s.on_wake_up(
165
+ synchronous=false,
166
+ {
167
+ # Register the command:
168
+ server.register(
169
+ namespace="#{source.id(s)}",
170
+ usage="skip",
171
+ description="Skip the current song.",
172
+ "skip",
173
+ skip
174
+ )
175
+ }
176
+ )
177
+ end