1import ./make-test-python.nix (
2 { lib, pkgs, ... }:
3
4 {
5 name = "jellyfin";
6 meta.maintainers = with lib.maintainers; [ minijackson ];
7
8 nodes.machine =
9 { ... }:
10 {
11 services.jellyfin.enable = true;
12 environment.systemPackages = with pkgs; [ ffmpeg ];
13 };
14
15 # Documentation of the Jellyfin API: https://api.jellyfin.org/
16 # Beware, this link can be resource intensive
17 testScript =
18 let
19 payloads = {
20 auth = pkgs.writeText "auth.json" (
21 builtins.toJSON {
22 Username = "jellyfin";
23 }
24 );
25 empty = pkgs.writeText "empty.json" (builtins.toJSON { });
26 };
27 in
28 ''
29 import json
30 from urllib.parse import urlencode
31
32 machine.wait_for_unit("jellyfin.service")
33 machine.wait_for_open_port(8096)
34 machine.succeed("curl --fail http://localhost:8096/")
35
36 machine.wait_until_succeeds("curl --fail http://localhost:8096/health | grep Healthy")
37
38 auth_header = 'MediaBrowser Client="NixOS Integration Tests", DeviceId="1337", Device="Apple II", Version="20.09"'
39
40
41 def api_get(path):
42 return f"curl --fail 'http://localhost:8096{path}' -H 'X-Emby-Authorization:{auth_header}'"
43
44
45 def api_post(path, json_file=None):
46 if json_file:
47 return f"curl --fail -X post 'http://localhost:8096{path}' -d '@{json_file}' -H Content-Type:application/json -H 'X-Emby-Authorization:{auth_header}'"
48 else:
49 return f"curl --fail -X post 'http://localhost:8096{path}' -H 'X-Emby-Authorization:{auth_header}'"
50
51
52 with machine.nested("Wizard completes"):
53 machine.wait_until_succeeds(api_get("/Startup/Configuration"))
54 machine.succeed(api_get("/Startup/FirstUser"))
55 machine.succeed(api_post("/Startup/Complete"))
56
57 with machine.nested("Can login"):
58 auth_result_str = machine.succeed(
59 api_post(
60 "/Users/AuthenticateByName",
61 "${payloads.auth}",
62 )
63 )
64 auth_result = json.loads(auth_result_str)
65 auth_token = auth_result["AccessToken"]
66 auth_header += f", Token={auth_token}"
67
68 sessions_result_str = machine.succeed(api_get("/Sessions"))
69 sessions_result = json.loads(sessions_result_str)
70
71 this_session = [
72 session for session in sessions_result if session["DeviceId"] == "1337"
73 ]
74 if len(this_session) != 1:
75 raise Exception("Session not created")
76
77 me_str = machine.succeed(api_get("/Users/Me"))
78 me = json.loads(me_str)["Id"]
79
80 with machine.nested("Can add library"):
81 tempdir = machine.succeed("mktemp -d -p /var/lib/jellyfin").strip()
82 machine.succeed(f"chmod 755 '{tempdir}'")
83
84 # Generate a dummy video that we can test later
85 videofile = f"{tempdir}/Big Buck Bunny (2008) [1080p].mkv"
86 machine.succeed(f"ffmpeg -f lavfi -i testsrc2=duration=5 '{videofile}'")
87
88 add_folder_query = urlencode(
89 {
90 "name": "My Library",
91 "collectionType": "Movies",
92 "paths": tempdir,
93 "refreshLibrary": "true",
94 }
95 )
96
97 machine.succeed(
98 api_post(
99 f"/Library/VirtualFolders?{add_folder_query}",
100 "${payloads.empty}",
101 )
102 )
103
104
105 def is_refreshed(_):
106 folders_str = machine.succeed(api_get("/Library/VirtualFolders"))
107 folders = json.loads(folders_str)
108 print(folders)
109 return all(folder["RefreshStatus"] == "Idle" for folder in folders)
110
111
112 retry(is_refreshed)
113
114 with machine.nested("Can identify videos"):
115 items = []
116
117 # For some reason, having the folder refreshed doesn't mean the
118 # movie was scanned
119 def has_movie(_):
120 global items
121
122 items_str = machine.succeed(
123 api_get(f"/Users/{me}/Items?IncludeItemTypes=Movie&Recursive=true")
124 )
125 items = json.loads(items_str)["Items"]
126
127 return len(items) == 1
128
129 retry(has_movie)
130
131 video = items[0]["Id"]
132
133 item_info_str = machine.succeed(api_get(f"/Users/{me}/Items/{video}"))
134 item_info = json.loads(item_info_str)
135
136 if item_info["Name"] != "Big Buck Bunny":
137 raise Exception("Jellyfin failed to properly identify file")
138
139 with machine.nested("Can read videos"):
140 media_source_id = item_info["MediaSources"][0]["Id"]
141
142 machine.succeed(
143 "ffmpeg"
144 + f" -headers 'X-Emby-Authorization:{auth_header}'"
145 + f" -i http://localhost:8096/Videos/{video}/master.m3u8?mediaSourceId={media_source_id}"
146 + " /tmp/test.mkv"
147 )
148
149 duration = machine.succeed(
150 "ffprobe /tmp/test.mkv"
151 + " -show_entries format=duration"
152 + " -of compact=print_section=0:nokey=1"
153 )
154
155 if duration.strip() != "5.000000":
156 raise Exception("Downloaded video has wrong duration")
157 '';
158 }
159)