1{
2 config,
3 pkgs,
4 lib,
5 ...
6}:
7
8let
9 inherit (lib)
10 mkOption
11 mkEnableOption
12 mkIf
13 types
14 ;
15
16 cfg = config.services.lavalink;
17
18 format = pkgs.formats.yaml { };
19in
20
21{
22 options.services.lavalink = {
23 enable = mkEnableOption "Lavalink";
24
25 package = lib.mkPackageOption pkgs "lavalink" { };
26
27 password = mkOption {
28 type = types.nullOr types.str;
29 default = null;
30 example = "s3cRe!p4SsW0rD";
31 description = ''
32 The password for Lavalink's authentication in plain text.
33 '';
34 };
35
36 port = mkOption {
37 type = types.port;
38 default = 2333;
39 example = 4567;
40 description = ''
41 The port that Lavalink will use.
42 '';
43 };
44
45 address = mkOption {
46 type = types.str;
47 default = "0.0.0.0";
48 example = "127.0.0.1";
49 description = ''
50 The network address to bind to.
51 '';
52 };
53
54 openFirewall = mkOption {
55 type = types.bool;
56 default = false;
57 example = true;
58 description = ''
59 Whether to expose the port to the network.
60 '';
61 };
62
63 user = mkOption {
64 type = types.str;
65 default = "lavalink";
66 example = "root";
67 description = ''
68 The user of the service.
69 '';
70 };
71
72 group = mkOption {
73 type = types.str;
74 default = "lavalink";
75 example = "medias";
76 description = ''
77 The group of the service.
78 '';
79 };
80
81 home = mkOption {
82 type = types.str;
83 default = "/var/lib/lavalink";
84 example = "/home/lavalink";
85 description = ''
86 The home directory for lavalink.
87 '';
88 };
89
90 enableHttp2 = mkEnableOption "HTTP/2 support";
91
92 jvmArgs = mkOption {
93 type = types.str;
94 default = "-Xmx4G";
95 example = "-Djava.io.tmpdir=/var/lib/lavalink/tmp -Xmx6G";
96 description = ''
97 Set custom JVM arguments.
98 '';
99 };
100
101 environmentFile = mkOption {
102 type = types.nullOr types.str;
103 default = null;
104 example = "/run/secrets/lavalink/passwordEnvFile";
105 description = ''
106 Add custom environment variables from a file.
107 See <https://lavalink.dev/configuration/index.html#example-environment-variables> for the full documentation.
108 '';
109 };
110
111 plugins = mkOption {
112 type = types.listOf (
113 types.submodule {
114 options = {
115 dependency = mkOption {
116 type = types.str;
117 example = "dev.lavalink.youtube:youtube-plugin:1.8.0";
118 description = ''
119 The coordinates of the plugin.
120 '';
121 };
122
123 repository = mkOption {
124 type = types.str;
125 example = "https://maven.example.com/releases";
126 default = "https://maven.lavalink.dev/releases";
127 description = ''
128 The plugin repository. Defaults to the lavalink releases repository.
129
130 To use the snapshots repository, use <https://maven.lavalink.dev/snapshots> instead
131 '';
132 };
133
134 hash = mkOption {
135 type = types.str;
136 example = lib.fakeHash;
137 description = ''
138 The hash of the plugin.
139 '';
140 };
141
142 configName = mkOption {
143 type = types.nullOr types.str;
144 example = "youtube";
145 default = null;
146 description = ''
147 The name of the plugin to use as the key for the plugin configuration.
148 '';
149 };
150
151 extraConfig = mkOption {
152 type = types.submodule { freeformType = format.type; };
153 default = { };
154 description = ''
155 The configuration for the plugin.
156
157 The {option}`services.lavalink.plugins.*.configName` option must be set.
158 '';
159 };
160 };
161 }
162 );
163 default = [ ];
164
165 example = lib.literalExpression ''
166 [
167 {
168 dependency = "dev.lavalink.youtube:youtube-plugin:1.8.0";
169 repository = "https://maven.lavalink.dev/snapshots";
170 hash = lib.fakeHash;
171 configName = "youtube";
172 extraConfig = {
173 enabled = true;
174 allowSearch = true;
175 allowDirectVideoIds = true;
176 allowDirectPlaylistIds = true;
177 };
178 }
179 ]
180 '';
181
182 description = ''
183 A list of plugins for lavalink.
184 '';
185 };
186
187 extraConfig = mkOption {
188 type = types.submodule { freeformType = format.type; };
189
190 description = ''
191 Configuration to write to {file}`application.yml`.
192 See <https://lavalink.dev/configuration/#example-applicationyml> for the full documentation.
193
194 Individual configuration parameters can be overwritten using environment variables.
195 See <https://lavalink.dev/configuration/#example-environment-variables> for more information.
196 '';
197
198 default = { };
199
200 example = lib.literalExpression ''
201 {
202 lavalink.server = {
203 sources.twitch = true;
204
205 filters.volume = true;
206 };
207
208 logging.file.path = "./logs/";
209 }
210 '';
211 };
212 };
213
214 config =
215 let
216 pluginSymlinks = lib.concatStringsSep "\n" (
217 map (
218 pluginCfg:
219 let
220 pluginParts = lib.match ''^(.*?:(.*?):)([0-9]+\.[0-9]+\.[0-9]+)$'' pluginCfg.dependency;
221
222 pluginWebPath = lib.replaceStrings [ "." ":" ] [ "/" "/" ] (lib.elemAt pluginParts 0);
223
224 pluginFileName = lib.elemAt pluginParts 1;
225 pluginVersion = lib.elemAt pluginParts 2;
226
227 pluginFile = "${pluginFileName}-${pluginVersion}.jar";
228 pluginUrl = "${pluginCfg.repository}/${pluginWebPath}${pluginVersion}/${pluginFile}";
229
230 plugin = pkgs.fetchurl {
231 url = pluginUrl;
232 inherit (pluginCfg) hash;
233 };
234 in
235 "ln -sf ${plugin} ${cfg.home}/plugins/${pluginFile}"
236 ) cfg.plugins
237 );
238
239 pluginExtraConfigs = builtins.listToAttrs (
240 builtins.map (
241 pluginConfig: lib.attrsets.nameValuePair pluginConfig.configName pluginConfig.extraConfig
242 ) (lib.lists.filter (pluginCfg: pluginCfg.configName != null) cfg.plugins)
243 );
244
245 config = lib.attrsets.recursiveUpdate cfg.extraConfig {
246 server = {
247 inherit (cfg) port address;
248 http2.enabled = cfg.enableHttp2;
249 };
250
251 plugins = pluginExtraConfigs;
252 lavalink.plugins = (
253 builtins.map (
254 pluginConfig:
255 builtins.removeAttrs pluginConfig [
256 "name"
257 "extraConfig"
258 "hash"
259 ]
260 ) cfg.plugins
261 );
262 };
263
264 configWithPassword = lib.attrsets.recursiveUpdate config (
265 lib.attrsets.optionalAttrs (cfg.password != null) { lavalink.server.password = cfg.password; }
266 );
267
268 configFile = format.generate "application.yml" configWithPassword;
269 in
270 mkIf cfg.enable {
271 assertions = [
272 {
273 assertion =
274 !(lib.lists.any (
275 pluginCfg: pluginCfg.extraConfig != { } && pluginCfg.configName == null
276 ) cfg.plugins);
277 message = "Plugins with extra configuration need to have the `configName` attribute defined";
278 }
279 ];
280
281 networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ];
282
283 users.groups = mkIf (cfg.group == "lavalink") { lavalink = { }; };
284 users.users = mkIf (cfg.user == "lavalink") {
285 lavalink = {
286 inherit (cfg) home;
287 group = "lavalink";
288 description = "The user for the Lavalink server";
289 isSystemUser = true;
290 };
291 };
292
293 systemd.tmpfiles.settings."10-lavalink" =
294 let
295 dirConfig = {
296 inherit (cfg) user group;
297 mode = "0700";
298 };
299 in
300 {
301 "${cfg.home}/plugins".d = mkIf (cfg.plugins != [ ]) dirConfig;
302 ${cfg.home}.d = dirConfig;
303 };
304
305 systemd.services.lavalink = {
306 description = "Lavalink Service";
307
308 wantedBy = [ "multi-user.target" ];
309 after = [
310 "syslog.target"
311 "network.target"
312 ];
313
314 script = ''
315 ${pluginSymlinks}
316
317 ln -sf ${configFile} ${cfg.home}/application.yml
318 export _JAVA_OPTIONS="${cfg.jvmArgs}"
319
320 ${lib.getExe cfg.package}
321 '';
322
323 serviceConfig = {
324 User = cfg.user;
325 Group = cfg.group;
326
327 Type = "simple";
328 Restart = "on-failure";
329
330 EnvironmentFile = cfg.environmentFile;
331 WorkingDirectory = cfg.home;
332 };
333 };
334 };
335}