1{ config, pkgs, lib, ... }:
2
3with lib;
4
5let
6 cfg = config.services.plex;
7in
8{
9 imports = [
10 (mkRemovedOptionModule [ "services" "plex" "managePlugins" ] "Please omit or define the option: `services.plex.extraPlugins' instead.")
11 ];
12
13 options = {
14 services.plex = {
15 enable = mkEnableOption "Plex Media Server";
16
17 dataDir = mkOption {
18 type = types.str;
19 default = "/var/lib/plex";
20 description = ''
21 The directory where Plex stores its data files.
22 '';
23 };
24
25 openFirewall = mkOption {
26 type = types.bool;
27 default = false;
28 description = ''
29 Open ports in the firewall for the media server.
30 '';
31 };
32
33 user = mkOption {
34 type = types.str;
35 default = "plex";
36 description = ''
37 User account under which Plex runs.
38 '';
39 };
40
41 group = mkOption {
42 type = types.str;
43 default = "plex";
44 description = ''
45 Group under which Plex runs.
46 '';
47 };
48
49 extraPlugins = mkOption {
50 type = types.listOf types.path;
51 default = [];
52 description = ''
53 A list of paths to extra plugin bundles to install in Plex's plugin
54 directory. Every time the systemd unit for Plex starts up, all of the
55 symlinks in Plex's plugin directory will be cleared and this module
56 will symlink all of the paths specified here to that directory.
57 '';
58 example = literalExpression ''
59 [
60 (builtins.path {
61 name = "Audnexus.bundle";
62 path = pkgs.fetchFromGitHub {
63 owner = "djdembeck";
64 repo = "Audnexus.bundle";
65 rev = "v0.2.8";
66 sha256 = "sha256-IWOSz3vYL7zhdHan468xNc6C/eQ2C2BukQlaJNLXh7E=";
67 };
68 })
69 ]
70 '';
71 };
72
73 extraScanners = mkOption {
74 type = types.listOf types.path;
75 default = [];
76 description = ''
77 A list of paths to extra scanners to install in Plex's scanners
78 directory.
79
80 Every time the systemd unit for Plex starts up, all of the symlinks
81 in Plex's scanners directory will be cleared and this module will
82 symlink all of the paths specified here to that directory.
83 '';
84 example = literalExpression ''
85 [
86 (fetchFromGitHub {
87 owner = "ZeroQI";
88 repo = "Absolute-Series-Scanner";
89 rev = "773a39f502a1204b0b0255903cee4ed02c46fde0";
90 sha256 = "4l+vpiDdC8L/EeJowUgYyB3JPNTZ1sauN8liFAcK+PY=";
91 })
92 ]
93 '';
94 };
95
96 accelerationDevices = mkOption {
97 type = types.listOf types.str;
98 default = ["*"];
99 example = [ "/dev/dri/renderD128" ];
100 description = ''
101 A list of device paths to hardware acceleration devices that Plex should
102 have access to. This is useful when transcoding media files.
103 The special value `"*"` will allow all devices.
104 '';
105 };
106
107 package = mkPackageOption pkgs "plex" {
108 extraDescription = ''
109 Plex subscribers may wish to use their own package here,
110 pointing to subscriber-only server versions.
111 '';
112 };
113 };
114 };
115
116 config = mkIf cfg.enable {
117 # Most of this is just copied from the RPM package's systemd service file.
118 systemd.services.plex = {
119 description = "Plex Media Server";
120 after = [ "network.target" ];
121 wantedBy = [ "multi-user.target" ];
122
123 serviceConfig = {
124 Type = "simple";
125 User = cfg.user;
126 Group = cfg.group;
127
128 # Run the pre-start script with full permissions (the "!" prefix) so it
129 # can create the data directory if necessary.
130 ExecStartPre = let
131 preStartScript = pkgs.writeScript "plex-run-prestart" ''
132 #!${pkgs.bash}/bin/bash
133
134 # Create data directory if it doesn't exist
135 if ! test -d "$PLEX_DATADIR"; then
136 echo "Creating initial Plex data directory in: $PLEX_DATADIR"
137 install -d -m 0755 -o "${cfg.user}" -g "${cfg.group}" "$PLEX_DATADIR"
138 fi
139 '';
140 in
141 "!${preStartScript}";
142
143 ExecStart = "${cfg.package}/bin/plexmediaserver";
144 KillSignal = "SIGQUIT";
145 PIDFile = "${cfg.dataDir}/Plex Media Server/plexmediaserver.pid";
146 Restart = "on-failure";
147
148 # Hardening
149 NoNewPrivileges = true;
150 PrivateTmp = true;
151 PrivateDevices = cfg.accelerationDevices == [];
152 DeviceAllow = mkIf (cfg.accelerationDevices != [] && !lib.elem "*" cfg.accelerationDevices) cfg.accelerationDevices;
153 ProtectSystem = true;
154 ProtectHome = true;
155 ProtectControlGroups = true;
156 ProtectKernelModules = true;
157 ProtectKernelTunables = true;
158 RestrictAddressFamilies = ["AF_UNIX" "AF_INET" "AF_INET6" "AF_NETLINK"];
159 # This could be made to work if the namespaces needed were known
160 # RestrictNamespaces = true;
161 RestrictRealtime = true;
162 RestrictSUIDSGID = true;
163 MemoryDenyWriteExecute = true;
164 LockPersonality = true;
165 };
166
167 environment = {
168 # Configuration for our FHS userenv script
169 PLEX_DATADIR=cfg.dataDir;
170 PLEX_PLUGINS=concatMapStringsSep ":" builtins.toString cfg.extraPlugins;
171 PLEX_SCANNERS=concatMapStringsSep ":" builtins.toString cfg.extraScanners;
172
173 # The following variables should be set by the FHS userenv script:
174 # PLEX_MEDIA_SERVER_APPLICATION_SUPPORT_DIR
175 # PLEX_MEDIA_SERVER_HOME
176
177 # Allow access to GPU acceleration; the Plex LD_LIBRARY_PATH is added
178 # by the FHS userenv script.
179 LD_LIBRARY_PATH="/run/opengl-driver/lib";
180
181 PLEX_MEDIA_SERVER_MAX_PLUGIN_PROCS="6";
182 PLEX_MEDIA_SERVER_TMPDIR="/tmp";
183 PLEX_MEDIA_SERVER_USE_SYSLOG="true";
184 LC_ALL="en_US.UTF-8";
185 LANG="en_US.UTF-8";
186 };
187 };
188
189 networking.firewall = mkIf cfg.openFirewall {
190 allowedTCPPorts = [ 32400 3005 8324 32469 ];
191 allowedUDPPorts = [ 1900 5353 32410 32412 32413 32414 ];
192 };
193
194 users.users = mkIf (cfg.user == "plex") {
195 plex = {
196 group = cfg.group;
197 uid = config.ids.uids.plex;
198 };
199 };
200
201 users.groups = mkIf (cfg.group == "plex") {
202 plex = {
203 gid = config.ids.gids.plex;
204 };
205 };
206 };
207}