Skip to content

Commit f19b961

Browse files
committed
create wizard automation, system config, and users services
1 parent 7511b19 commit f19b961

9 files changed

Lines changed: 646 additions & 37 deletions

File tree

modules/jellyfin/apiKeysService.nix

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ in {
1111
config = mkIf (nixflix.enable && cfg.enable) {
1212
systemd.services.jellyfin-api-keys = {
1313
description = "Jellyfin API Keys Initialization";
14-
after = ["jellyfin.service"];
15-
wants = ["jellyfin.service"];
14+
after = ["jellyfin-initialization.service"];
15+
wants = ["jellyfin-initialization.service"];
1616
wantedBy = ["multi-user.target"];
1717

1818
serviceConfig = {

modules/jellyfin/default.nix

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ with lib; let
1717
in {
1818
imports = [
1919
./options
20+
./initializationService.nix
2021
./apiKeysService.nix
22+
./setupWizardService.nix
23+
./systemConfigService.nix
24+
./usersConfigService.nix
2125
];
2226

2327
config = mkIf (nixflix.enable && cfg.enable) {
@@ -27,8 +31,8 @@ in {
2731
message = "Cannot enable VPN routing for Jellyfin (nixflix.jellyfin.vpn.enable = true) when Mullvad VPN is disabled. Please set nixflix.mullvad.enable = true.";
2832
}
2933
{
30-
assertion = cfg.apikeys ? default;
31-
message = "Jellyfin requires at least a 'default' API key. Please configure nixflix.jellyfin.apikeys.default.";
34+
assertion = any (user: user.policy.isAdministrator) (attrValues cfg.users);
35+
message = "At least one Jellyfin user must have policy.isAdministrator = true.";
3236
}
3337
];
3438

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
{
2+
config,
3+
lib,
4+
pkgs,
5+
...
6+
}:
7+
with lib; let
8+
inherit (config) nixflix;
9+
cfg = config.nixflix.jellyfin;
10+
in {
11+
config = mkIf (nixflix.enable && cfg.enable) {
12+
systemd.services.jellyfin-initialization = {
13+
description = "Wait for Jellyfin Initialization";
14+
after = ["jellyfin.service"];
15+
wants = ["jellyfin.service"];
16+
wantedBy = ["multi-user.target"];
17+
18+
serviceConfig = {
19+
Type = "oneshot";
20+
RemainAfterExit = true;
21+
};
22+
23+
script = ''
24+
set -eu
25+
26+
LOG_FILE="${cfg.logDir}/log_$(${pkgs.coreutils}/bin/date +%Y%m%d).log"
27+
BASE_URL="http://127.0.0.1:${toString cfg.network.internalHttpPort}${cfg.network.baseUrl}"
28+
29+
echo "Waiting for Jellyfin to complete startup..."
30+
for i in {1..60}; do
31+
if [ -f "$LOG_FILE" ]; then
32+
if ${pkgs.gnugrep}/bin/grep -q "Startup complete" "$LOG_FILE"; then
33+
echo "Jellyfin startup complete"
34+
break
35+
fi
36+
fi
37+
if [[ $i -eq 60 ]]; then
38+
echo "Jellyfin did not complete startup after 120 seconds" >&2
39+
exit 1
40+
fi
41+
sleep 2
42+
done
43+
44+
echo "Waiting for Jellyfin API to be available..."
45+
for i in {1..90}; do
46+
if ${pkgs.curl}/bin/curl -s -f "$BASE_URL/System/Ping" >/dev/null 2>&1; then
47+
echo "Jellyfin API is available"
48+
exit 0
49+
fi
50+
if [[ $i -eq 90 ]]; then
51+
echo "Jellyfin API not available after 90 seconds" >&2
52+
exit 1
53+
fi
54+
sleep 1
55+
done
56+
'';
57+
};
58+
};
59+
}

modules/jellyfin/options/system.nix

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ in {
155155
'';
156156
};
157157

158-
inactiveSessionThreshhold = mkOption {
158+
inactiveSessionThreshold = mkOption {
159159
type = types.int;
160160
default = 0;
161161
};
@@ -170,6 +170,14 @@ in {
170170
default = 30;
171171
};
172172

173+
cacheSize = mkOption {
174+
type = types.int;
175+
default = 0;
176+
description = ''
177+
Cache size in MB. 0 means no limit.
178+
'';
179+
};
180+
173181
imageSavingConvention = mkOption {
174182
type = types.enum ["Legacy"];
175183
default = "Legacy";
@@ -291,7 +299,18 @@ in {
291299
saveMetadataHidden = mkEnableOption "";
292300

293301
contentTypes = mkOption {
294-
type = with types; listOf str;
302+
type = with types; listOf (submodule {
303+
options = {
304+
name = mkOption {
305+
type = str;
306+
description = "Content type name";
307+
};
308+
value = mkOption {
309+
type = str;
310+
description = "Content type value";
311+
};
312+
};
313+
});
295314
default = [];
296315
};
297316

@@ -302,7 +321,9 @@ in {
302321

303322
enableFolderView = mkEnableOption "";
304323

305-
enableGroupingIntoCollections = mkEnableOption "";
324+
enableGroupingMoviesIntoCollections = mkEnableOption "";
325+
326+
enableGroupingShowsIntoCollections = mkEnableOption "";
306327

307328
displaySpecialsWithinSeasons = mkOption {
308329
type = types.bool;
@@ -341,7 +362,18 @@ in {
341362
};
342363

343364
pathSubstitutions = mkOption {
344-
type = with types; listOf str;
365+
type = with types; listOf (submodule {
366+
options = {
367+
from = mkOption {
368+
type = str;
369+
description = "Path to substitute from";
370+
};
371+
to = mkOption {
372+
type = str;
373+
description = "Path to substitute to";
374+
};
375+
};
376+
});
345377
default = [];
346378
};
347379

@@ -549,5 +581,13 @@ in {
549581
'';
550582
};
551583
};
584+
585+
enableLegacyAuthorization = mkOption {
586+
type = types.bool;
587+
default = false;
588+
description = ''
589+
Enable legacy authorization mode for backwards compatibility.
590+
'';
591+
};
552592
};
553593
}

modules/jellyfin/options/users.nix

Lines changed: 118 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,9 @@
22
with lib; let
33
preferenceOpts = _: {
44
options = {
5-
# NOTE: renamed from internal EnabledFolders, since
6-
# it makes more sense to call it library not folder
75
enabledLibraries = mkOption {
86
type = types.listOf types.str;
9-
default = []; # empty means all are enabled
7+
default = [];
108
description = ''
119
A list of libraries this user as access to.
1210
If it is empty, it means that the user has access to all libraries.
@@ -18,6 +16,26 @@ with lib; let
1816
"Family Photos"
1917
];
2018
};
19+
20+
groupedFolders = mkOption {
21+
type = types.listOf types.str;
22+
default = [];
23+
};
24+
25+
orderedViews = mkOption {
26+
type = types.listOf types.str;
27+
default = [];
28+
};
29+
30+
latestItemsExcludes = mkOption {
31+
type = types.listOf types.str;
32+
default = [];
33+
};
34+
35+
myMediaExcludes = mkOption {
36+
type = types.listOf types.str;
37+
default = [];
38+
};
2139
};
2240
};
2341
# See: https://github.com/jellyfin/jellyfin/blob/master/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/PermissionKind.cs
@@ -144,6 +162,95 @@ with lib; let
144162
default = false;
145163
description = "Whether the server should force transcoding on remote connections for the user";
146164
};
165+
166+
maxParentalRating = mkOption {
167+
type = types.nullOr types.int;
168+
default = null;
169+
};
170+
171+
blockedTags = mkOption {
172+
type = types.listOf types.str;
173+
default = [];
174+
};
175+
176+
allowedTags = mkOption {
177+
type = types.listOf types.str;
178+
default = [];
179+
};
180+
181+
blockUnratedItems = mkOption {
182+
type = types.listOf types.str;
183+
default = [];
184+
};
185+
186+
enabledDevices = mkOption {
187+
type = types.listOf types.str;
188+
default = [];
189+
};
190+
191+
enabledChannels = mkOption {
192+
type = types.listOf types.str;
193+
default = [];
194+
};
195+
196+
blockedMediaFolders = mkOption {
197+
type = types.listOf types.str;
198+
default = [];
199+
};
200+
201+
blockedChannels = mkOption {
202+
type = types.listOf types.str;
203+
default = [];
204+
};
205+
206+
enableContentDeletionFromFolders = mkOption {
207+
type = types.listOf types.str;
208+
default = [];
209+
};
210+
211+
accessSchedules = mkOption {
212+
type = types.listOf (types.submodule {
213+
options = {
214+
dayOfWeek = mkOption {
215+
type = types.enum [
216+
"Sunday"
217+
"Monday"
218+
"Tuesday"
219+
"Wednesday"
220+
"Thursday"
221+
"Friday"
222+
"Saturday"
223+
];
224+
};
225+
startHour = mkOption {
226+
type = types.ints.between 0 23;
227+
};
228+
endHour = mkOption {
229+
type = types.ints.between 0 23;
230+
};
231+
};
232+
});
233+
default = [];
234+
};
235+
236+
syncPlayAccess = mkOption {
237+
type = types.bool;
238+
description = "Whether or not this user has access to SyncPlay";
239+
example = true;
240+
default = false;
241+
};
242+
243+
authenticationProviderId = mkOption {
244+
type = types.str;
245+
default = "Jellyfin.Server.Implementations.Users.DefaultAuthenticationProvider";
246+
description = "Authentication provider ID";
247+
};
248+
249+
passwordResetProviderId = mkOption {
250+
type = types.str;
251+
default = "Jellyfin.Server.Implementations.Users.DefaultPasswordResetProvider";
252+
description = "Password reset provider ID";
253+
};
147254
};
148255
};
149256
userOpts = _: {
@@ -160,8 +267,8 @@ with lib; let
160267
];
161268
};
162269
};
163-
permissions = mkOption {
164-
description = "Permissions for this user";
270+
policy = mkOption {
271+
description = "Policy for this user";
165272
default = {};
166273
type = with types; submodule permissionOpts;
167274
example = {
@@ -181,6 +288,10 @@ with lib; let
181288
and no nix configuration changes will have any effect.
182289
If false however, all options are overwritten as specified in the nix configuration,
183290
which means any change through the Jellyfin GUI will have no effect after a rebuild.
291+
292+
Note: Passwords are only set during user creation and are never updated
293+
declaratively, regardless of the mutable setting. To change a user's password,
294+
use the Jellyfin web interface.
184295
'';
185296
default = true;
186297
};
@@ -262,24 +373,10 @@ with lib; let
262373
type = with types; nullOr int;
263374
default = null;
264375
};
265-
hashedPassword = mkOption {
266-
type = types.nullOr types.str;
267-
default = null;
268-
description = ''
269-
A pbkdf2-sha512 hash of the user password.
270-
Generate using: nix run github:kiriwalawren/nixflix#genhash -- -k <password> -i 210000 -l 128 -u
271-
'';
272-
example = "$PBKDF2-SHA512$iterations=210000$D12C02D1DD15949D867BCA9971BE9987$67E75CDCD14E7F6FDDF96BAACBE9E84E5197FB9FE454FB039F5CD773D7DF558B57DC81DB42B6F7CF0E6B8207A771E5C0EE0DBFD91CE5BAF804FE53F70E61CD2E";
273-
};
274-
hashedPasswordFile = mkOption {
376+
passwordFile = mkOption {
275377
type = types.nullOr types.path;
276378
description = ''
277-
Path to a file containing a pbkdf2-sha512 hash in PHC string format.
278-
Generate using: nix run github:kiriwalawren/nixflix#genhash -- -k <password> -i 210000 -l 128 -u > /path/to/hash
279-
'';
280-
example = ''
281-
# the format is: $<id>[$<param>=<value>(,<param>=<value>)*][$<salt>[$<hash>]]
282-
$PBKDF2-SHA512$iterations=210000$D12C02D1DD15949D867BCA9971BE9987$67E75CDCD14E7F6FDDF96BAACBE9E84E5197FB9FE454FB039F5CD773D7DF558B57DC81DB42B6F7CF0E6B8207A771E5C0EE0DBFD91CE5BAF804FE53F70E61CD2E
379+
Path to a file containing the user's password in plain text.
283380
'';
284381
default = null;
285382
};
@@ -329,13 +426,6 @@ with lib; let
329426
'';
330427
default = "default";
331428
};
332-
syncPlayAccess = mkOption {
333-
type = types.bool;
334-
description = "Whether or not this user has access to SyncPlay";
335-
example = true;
336-
default = false;
337-
};
338-
# Something to do with chromecast, don't know tbh
339429
castReceiverId = mkOption {
340430
type = types.str;
341431
default = "F007D354";

0 commit comments

Comments
 (0)