1{ config, lib, options, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.airsonic;
7 opt = options.services.airsonic;
8in {
9 options = {
10
11 services.airsonic = {
12 enable = mkEnableOption (lib.mdDoc "Airsonic, the Free and Open Source media streaming server (fork of Subsonic and Libresonic)");
13
14 user = mkOption {
15 type = types.str;
16 default = "airsonic";
17 description = lib.mdDoc "User account under which airsonic runs.";
18 };
19
20 home = mkOption {
21 type = types.path;
22 default = "/var/lib/airsonic";
23 description = lib.mdDoc ''
24 The directory where Airsonic will create files.
25 Make sure it is writable.
26 '';
27 };
28
29 virtualHost = mkOption {
30 type = types.nullOr types.str;
31 default = null;
32 description = lib.mdDoc ''
33 Name of the nginx virtualhost to use and setup. If null, do not setup any virtualhost.
34 '';
35 };
36
37 listenAddress = mkOption {
38 type = types.str;
39 default = "127.0.0.1";
40 description = lib.mdDoc ''
41 The host name or IP address on which to bind Airsonic.
42 The default value is appropriate for first launch, when the
43 default credentials are easy to guess. It is also appropriate
44 if you intend to use the virtualhost option in the service
45 module. In other cases, you may want to change this to a
46 specific IP or 0.0.0.0 to listen on all interfaces.
47 '';
48 };
49
50 port = mkOption {
51 type = types.port;
52 default = 4040;
53 description = lib.mdDoc ''
54 The port on which Airsonic will listen for
55 incoming HTTP traffic. Set to 0 to disable.
56 '';
57 };
58
59 contextPath = mkOption {
60 type = types.path;
61 default = "/";
62 description = lib.mdDoc ''
63 The context path, i.e., the last part of the Airsonic
64 URL. Typically '/' or '/airsonic'. Default '/'
65 '';
66 };
67
68 maxMemory = mkOption {
69 type = types.int;
70 default = 100;
71 description = lib.mdDoc ''
72 The memory limit (max Java heap size) in megabytes.
73 Default: 100
74 '';
75 };
76
77 transcoders = mkOption {
78 type = types.listOf types.path;
79 default = [ "${pkgs.ffmpeg.bin}/bin/ffmpeg" ];
80 defaultText = literalExpression ''[ "''${pkgs.ffmpeg.bin}/bin/ffmpeg" ]'';
81 description = lib.mdDoc ''
82 List of paths to transcoder executables that should be accessible
83 from Airsonic. Symlinks will be created to each executable inside
84 ''${config.${opt.home}}/transcoders.
85 '';
86 };
87
88 jre = mkOption {
89 type = types.package;
90 default = pkgs.jre8;
91 defaultText = literalExpression "pkgs.jre8";
92 description = lib.mdDoc ''
93 JRE package to use.
94
95 Airsonic only supports Java 8, airsonic-advanced requires at least
96 Java 11.
97 '';
98 };
99
100 war = mkOption {
101 type = types.path;
102 default = "${pkgs.airsonic}/webapps/airsonic.war";
103 defaultText = literalExpression ''"''${pkgs.airsonic}/webapps/airsonic.war"'';
104 description = lib.mdDoc "Airsonic war file to use.";
105 };
106
107 jvmOptions = mkOption {
108 description = lib.mdDoc ''
109 Extra command line options for the JVM running AirSonic.
110 Useful for sending jukebox output to non-default alsa
111 devices.
112 '';
113 default = [
114 ];
115 type = types.listOf types.str;
116 example = [
117 "-Djavax.sound.sampled.Clip='#CODEC [plughw:1,0]'"
118 "-Djavax.sound.sampled.Port='#Port CODEC [hw:1]'"
119 "-Djavax.sound.sampled.SourceDataLine='#CODEC [plughw:1,0]'"
120 "-Djavax.sound.sampled.TargetDataLine='#CODEC [plughw:1,0]'"
121 ];
122 };
123
124 };
125 };
126
127 config = mkIf cfg.enable {
128 systemd.services.airsonic = {
129 description = "Airsonic Media Server";
130 after = [ "network.target" ];
131 wantedBy = [ "multi-user.target" ];
132
133 preStart = ''
134 # Install transcoders.
135 rm -rf ${cfg.home}/transcode
136 mkdir -p ${cfg.home}/transcode
137 for exe in ${toString cfg.transcoders}; do
138 ln -sf "$exe" ${cfg.home}/transcode
139 done
140 '';
141 serviceConfig = {
142 ExecStart = ''
143 ${cfg.jre}/bin/java -Xmx${toString cfg.maxMemory}m \
144 -Dairsonic.home=${cfg.home} \
145 -Dserver.address=${cfg.listenAddress} \
146 -Dserver.port=${toString cfg.port} \
147 -Dairsonic.contextPath=${cfg.contextPath} \
148 -Djava.awt.headless=true \
149 ${optionalString (cfg.virtualHost != null)
150 "-Dserver.use-forward-headers=true"} \
151 ${toString cfg.jvmOptions} \
152 -verbose:gc \
153 -jar ${cfg.war}
154 '';
155 Restart = "always";
156 User = "airsonic";
157 UMask = "0022";
158 };
159 };
160
161 services.nginx = mkIf (cfg.virtualHost != null) {
162 enable = true;
163 recommendedProxySettings = true;
164 virtualHosts.${cfg.virtualHost} = {
165 locations.${cfg.contextPath}.proxyPass = "http://${cfg.listenAddress}:${toString cfg.port}";
166 };
167 };
168
169 users.users.airsonic = {
170 description = "Airsonic service user";
171 group = "airsonic";
172 name = cfg.user;
173 home = cfg.home;
174 createHome = true;
175 isSystemUser = true;
176 };
177 users.groups.airsonic = {};
178 };
179}