me like nix
1{ pkgs, lib, ... }:
2
3let
4 # Patch Invidious addon to prepend instance URL to relative stream/dash URLs
5 patchedInvidious = pkgs.kodiPackages.invidious.overrideAttrs (old: {
6 postPatch = (old.postPatch or "") + ''
7 substituteInPlace resources/lib/invidious_plugin.py \
8 --replace-fail \
9 'url = video_info["dashUrl"]' \
10 'url = video_info["dashUrl"]; url = self.api_client.instance_url + url if url.startswith("/") else url' \
11 --replace-fail \
12 'url = video_info["formatStreams"][-1]["url"]' \
13 'url = video_info["formatStreams"][-1]["url"]; url = self.api_client.instance_url + url if url.startswith("/") else url'
14 '';
15 });
16
17 myKodi = pkgs.kodi-gbm.withPackages (kodiPkgs: with kodiPkgs; [
18 jellyfin
19 patchedInvidious
20 ]);
21
22 kodiAdvancedSettings = pkgs.writeText "advancedsettings.xml" ''
23 <advancedsettings version="1.0">
24 <videoplayer>
25 <useprimedecoder>true</useprimedecoder>
26 <useprimedecoderforhw>true</useprimedecoderforhw>
27 </videoplayer>
28 </advancedsettings>
29 '';
30in
31{
32 imports = [ ../pi-common/wifi.nix ];
33
34 networking.hostName = "kodi-pi";
35
36 # Use the new generational bootloader (matches nixos-raspberrypi installer)
37 boot.loader.raspberry-pi.bootloader = "kernel";
38
39 # Force 4K30 output when EDID read fails (TV may not be ready at boot)
40 boot.kernelParams = [ "video=HDMI-A-1:3840x2160@30D" ];
41
42 # Filesystems (matching nixos-raspberrypi installer layout)
43 fileSystems."/" = {
44 device = "/dev/disk/by-label/NIXOS_SD";
45 fsType = "ext4";
46 options = [ "noatime" ];
47 };
48 fileSystems."/boot/firmware" = {
49 device = "/dev/disk/by-label/FIRMWARE";
50 fsType = "vfat";
51 options = [ "noatime" "noauto" "x-systemd.automount" "x-systemd.idle-timeout=1min" ];
52 };
53
54 # Graphics
55 hardware.graphics.enable = true;
56
57 # Pi firmware config.txt settings for Kodi
58 hardware.raspberry-pi.config.all.options = {
59 gpu_mem = { enable = true; value = 256; };
60 hdmi_force_hotplug = { enable = true; value = true; };
61 };
62
63 # Audio: ALSA only for HDMI passthrough
64 services.pipewire.enable = false;
65 services.pulseaudio.enable = false;
66
67 # Kodi via GBM (direct DRM, no compositor needed)
68 systemd.services.kodi = {
69 description = "Kodi media center";
70 wantedBy = [ "multi-user.target" ];
71 after = [ "network-online.target" "sound.target" "systemd-user-sessions.service" ];
72 wants = [ "network-online.target" ];
73 serviceConfig = {
74 Type = "simple";
75 User = "kodi";
76 ExecStartPre = pkgs.writeShellScript "kodi-setup" ''
77 mkdir -p /home/kodi/.kodi/userdata
78 cp -f ${kodiAdvancedSettings} /home/kodi/.kodi/userdata/advancedsettings.xml
79
80 # Patch Jellyfin addon settings (Pi 5 has HW HEVC decoder, no need to transcode)
81 JFSETTINGS=/home/kodi/.kodi/userdata/addon_data/plugin.video.jellyfin/settings.xml
82 if [ -f "$JFSETTINGS" ]; then
83 ${pkgs.gnused}/bin/sed -i \
84 -e 's|"transcode_h265">true|"transcode_h265">false|' \
85 -e 's|"transcodeHi10P">true|"transcodeHi10P">false|' \
86 -e 's|"kodiCompanion">false|"kodiCompanion">true|' \
87 "$JFSETTINGS"
88 fi
89
90 # Configure Invidious addon to use a specific instance (auto-detect is unreliable)
91 INVSETTINGS=/home/kodi/.kodi/userdata/addon_data/plugin.video.invidious/settings.xml
92 if [ -f "$INVSETTINGS" ]; then
93 ${pkgs.gnused}/bin/sed -i \
94 -e 's|"auto_instance">true|"auto_instance">false|' \
95 -e 's|"instance_url"[^<]*<|"instance_url">http://mira:3001<|' \
96 -e 's|"disable_dash"[^>]*>[^<]*<|"disable_dash">false<|' \
97 "$INVSETTINGS"
98 else
99 mkdir -p /home/kodi/.kodi/userdata/addon_data/plugin.video.invidious
100 cat > "$INVSETTINGS" <<'INVEOF'
101<settings version="2">
102 <setting id="auto_instance">false</setting>
103 <setting id="instance_url">http://mira:3001</setting>
104 <setting id="disable_dash">false</setting>
105</settings>
106INVEOF
107 fi
108
109 '';
110 ExecStart = "${myKodi}/bin/kodi-standalone";
111 Restart = "always";
112 TimeoutStopSec = "15s";
113 TimeoutStopFailureMode = "kill";
114 };
115 };
116
117 # Kodi user
118 users.users.kodi = {
119 isNormalUser = true;
120 extraGroups = [ "video" "audio" "input" "render" ];
121 };
122
123 # Admin user
124 users.users.sean = {
125 isNormalUser = true;
126 extraGroups = [ "wheel" ];
127 openssh.authorizedKeys.keys = [
128 "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDCIqgZ7kedxo+mOW7YG73Vp3zel3h180y3GKvHtRsXfGlpIIvRDy7pgCBQ4AGXYD4y78URQmFohYSAPqCPOPaWcU2un3XG9KvCzEsHmsbskPonitUmCiKvrKkb6oW4jCBtd7AEtBn+AiajAQFtPZ7NN2Df3AmTypvR6Irg7R+nxnfc9NTIHmGvxSDyWcbb4pguL20sctUSqGL6xGh8q/bqhdOThSimM+z9bEUNxK/5rPhwkNniMrp4pJcUrUiAh5/4DiRFG6KT+oeg+/myoz/Z1sPvAs7u/8JDQI4RshRD8Hu0oTkRBN6Hxj478q2SXbeBUZlD6IdjP3RhGpmSecoDdtWqKbpuV3eVRtQtba3KL86GBeV/bugaOdJ1Aud+1SOFJreAAuvxzMMKT+cdQZk6oOPP148DA/No+mDm/2S43lcdCXh79wA6YRAmKQ8jmZxTCtPutrvuZK1rguvvUlEoG/vhdNHh7eDa4Td07V6bjCRPUl8qk/e4M0E3pwsTlZc="
129 "sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIOIgEteUEW06dnBHe2z8vNLwz2iMKe8bba6JgMmOUpcBAAAABHNzaDo= sean@framework16"
130 ];
131 };
132
133 # SSH
134 services.openssh = {
135 enable = true;
136 settings = {
137 PasswordAuthentication = false;
138 PermitRootLogin = "no";
139 };
140 };
141
142 # Accept unsigned store paths from local builds
143 nix.settings.require-sigs = false;
144
145 # Binary caches
146 nix.settings.substituters = [
147 "https://nixos-raspberrypi.cachix.org"
148 "https://seanaye.cachix.org"
149 ];
150 nix.settings.trusted-public-keys = [
151 "nixos-raspberrypi.cachix.org-1:4iMO9LXa8BqhU+Rpg6LQKiGa2lsNh/j2oiYLNOQ5sPI="
152 "seanaye.cachix.org-1:0Qf3cZ1SwnTwqaiNGltYySksjGHnemzRPiodThnvibA="
153 ];
154
155 # Networking
156 networking.useDHCP = true;
157 security.sudo.wheelNeedsPassword = false;
158
159 # Firewall
160 networking.firewall.allowedTCPPorts = [
161 22 # SSH
162 8080 # Kodi web remote
163 ];
164
165 environment.systemPackages = [ myKodi ];
166
167 system.stateVersion = "24.11";
168}