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