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