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