me like nix
1{
2 pkgs,
3 config,
4 inputs,
5 ...
6}:
7
8let
9 jellyfinKodiSyncQueue = pkgs.fetchzip {
10 url = "https://repo.jellyfin.org/releases/plugin/kodi-sync-queue/kodi-sync-queue_15.0.0.0.zip";
11 stripRoot = false;
12 hash = "sha256-xtlG3UQ/WClt/Hvxe+oId2CeJ+PWMDXBUJXh5+k+mZQ=";
13 };
14in
15{
16 imports = [
17 # Include the results of the hardware scan.
18 ./hardware-configuration.nix
19 ../common/common.nix
20 ];
21
22 networking.hostName = "mira"; # Define your hostname.
23
24 # Prevent NetworkManager from managing USB Ethernet
25 networking.networkmanager.unmanaged = [ "interface-name:enp0s20f0u4u3" ];
26
27 # ZRAM swap to prevent OOM freezes
28 zramSwap = {
29 enable = true;
30 memoryPercent = 50;
31 };
32
33 # Kill runaway processes before the system locks up
34 services.earlyoom = {
35 enable = true;
36 freeMemThreshold = 5;
37 freeSwapThreshold = 5;
38 enableNotifications = true;
39 };
40 # networking.wireless.enable = true; # Enables wireless support via wpa_supplicant.
41
42 # Configure network proxy if necessary
43 # networking.proxy.default = "http://user:password@proxy:port/";
44 # networking.proxy.noProxy = "127.0.0.1,localhost,internal.domain";
45
46 # this is like a network devices discovery thing
47 services.avahi = {
48 enable = true;
49 nssmdns4 = true;
50 openFirewall = true;
51 };
52
53 services.copyparty.enable = true;
54
55 services.openssh = {
56 enable = true;
57 settings = {
58 PasswordAuthentication = false;
59 KbdInteractiveAuthentication = false;
60 PermitRootLogin = "no";
61 AllowUsers = [ "sean" ];
62 };
63 };
64
65 users.users.sean.openssh.authorizedKeys.keys = [
66 "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDCIqgZ7kedxo+mOW7YG73Vp3zel3h180y3GKvHtRsXfGlpIIvRDy7pgCBQ4AGXYD4y78URQmFohYSAPqCPOPaWcU2un3XG9KvCzEsHmsbskPonitUmCiKvrKkb6oW4jCBtd7AEtBn+AiajAQFtPZ7NN2Df3AmTypvR6Irg7R+nxnfc9NTIHmGvxSDyWcbb4pguL20sctUSqGL6xGh8q/bqhdOThSimM+z9bEUNxK/5rPhwkNniMrp4pJcUrUiAh5/4DiRFG6KT+oeg+/myoz/Z1sPvAs7u/8JDQI4RshRD8Hu0oTkRBN6Hxj478q2SXbeBUZlD6IdjP3RhGpmSecoDdtWqKbpuV3eVRtQtba3KL86GBeV/bugaOdJ1Aud+1SOFJreAAuvxzMMKT+cdQZk6oOPP148DA/No+mDm/2S43lcdCXh79wA6YRAmKQ8jmZxTCtPutrvuZK1rguvvUlEoG/vhdNHh7eDa4Td07V6bjCRPUl8qk/e4M0E3pwsTlZc="
67 "no-touch-required sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAILdilHXHdAP/V8Zq28EzHKtLAMMaFPu4+1det2N50QfhAAAABHNzaDo= sean@framework16"
68 ];
69
70 # List services that you want to enable:
71 services.flaresolverr.enable = true;
72 nixarr = {
73 enable = true;
74 mediaDir = "/mnt/storage1/nixarr/media";
75 vpn = {
76 enable = true;
77 wgConf = "/mnt/storage1/nixarr/wireguard.conf";
78 };
79
80 jellyfin = {
81 enable = true;
82 openFirewall = true;
83 };
84
85 transmission = {
86 enable = true;
87 vpn.enable = true;
88 };
89 sabnzbd = {
90 enable = true;
91 vpn.enable = true;
92 openFirewall = true;
93 };
94
95 prowlarr.enable = true;
96 radarr.enable = true;
97 sonarr.enable = true;
98 jellyseerr = {
99 enable = true;
100 openFirewall = true;
101 };
102
103 recyclarr = {
104 enable = true;
105 configuration = {
106 sonarr = {
107 series = {
108 base_url = "http://localhost:8989";
109 api_key = "!env_var SONARR_API_KEY";
110 quality_definition = {
111 type = "series";
112 };
113 delete_old_custom_formats = true;
114 custom_formats = [
115 {
116 trash_ids = [
117 "85c61753df5da1fb2aab6f2a47426b09" # BR-DISK
118 "9c11cd3f07101cdba90a2d81cf0e56b4" # LQ
119 ];
120 assign_scores_to = [
121 {
122 name = "WEB-DL (1080p)";
123 score = -10000;
124 }
125 ];
126 }
127 ];
128 };
129 };
130 radarr = {
131 movies = {
132 base_url = "http://localhost:7878";
133 api_key = "!env_var RADARR_API_KEY";
134 quality_definition = {
135 type = "movie";
136 };
137 delete_old_custom_formats = true;
138 custom_formats = [
139 {
140 trash_ids = [
141 "570bc9ebecd92723d2d21500f4be314c" # Remaster
142 "eca37840c13c6ef2dd0262b141a5482f" # 4K Remaster
143 ];
144 assign_scores_to = [
145 {
146 name = "HD Bluray + WEB";
147 score = 25;
148 }
149 ];
150 }
151 ];
152 };
153 };
154 };
155 };
156 };
157
158 # Install Kodi Sync Queue plugin into Jellyfin
159 systemd.services.jellyfin.serviceConfig.ExecStartPre =
160 let
161 pluginDir = "/data/.state/nixarr/jellyfin/data/plugins/Kodi Sync Queue/15.0.0.0";
162 in
163 pkgs.writeShellScript "install-jellyfin-plugins" ''
164 mkdir -p "${pluginDir}"
165 cp -f ${jellyfinKodiSyncQueue}/*.dll ${jellyfinKodiSyncQueue}/meta.json "${pluginDir}/"
166 '';
167
168 # MQTT broker for Home Assistant (Tasmota devices, Frigate)
169 services.mosquitto = {
170 enable = true;
171 listeners = [
172 {
173 acl = [ "pattern readwrite #" ];
174 omitPasswordAuth = true;
175 settings.allow_anonymous = true;
176 }
177 ];
178 };
179
180 # Frigate NVR for camera recording and AI object detection
181 services.frigate = {
182 enable = true;
183 hostname = "frigate";
184 settings = {
185 mqtt = {
186 enabled = true;
187 host = "localhost";
188 port = 1883;
189 };
190
191 detectors = {
192 cpu = {
193 type = "cpu";
194 num_threads = 4;
195 };
196 };
197
198 cameras = {
199 picam = {
200 enabled = true;
201 ffmpeg = {
202 hwaccel_args = "preset-nvidia-h264";
203 inputs = [
204 {
205 path = "rtsp://pi:8554/picam";
206 roles = [ "detect" "record" ];
207 }
208 ];
209 };
210 detect = {
211 enabled = true;
212 width = 1920;
213 height = 1080;
214 fps = 5;
215 };
216 record = {
217 enabled = true;
218 retain = {
219 days = 7;
220 mode = "active_objects";
221 };
222 };
223 snapshots = {
224 enabled = true;
225 bounding_box = true;
226 retain = {
227 default = 14;
228 };
229 };
230 objects = {
231 track = [ "person" "dog" "cat" "car" ];
232 };
233 zones = {
234 driveway = {
235 coordinates = "0,0.243,1,0.544,1,1,0,1,0,0.75";
236 objects = [ "person" "car" "dog" "cat" ];
237 };
238 };
239 };
240
241 pizerocam = {
242 enabled = true;
243 ffmpeg = {
244 hwaccel_args = "preset-nvidia-h264";
245 inputs = [
246 {
247 path = "rtsp://pizero:8554/pizerocam";
248 roles = [ "record" ];
249 }
250 ];
251 };
252 detect = {
253 enabled = false;
254 };
255 record = {
256 enabled = true;
257 retain = {
258 days = 2;
259 mode = "all";
260 };
261 };
262 };
263 };
264
265 record = {
266 enabled = true;
267 retain = {
268 days = 7;
269 mode = "active_objects";
270 };
271 };
272 };
273 };
274
275 # Home Assistant service
276 services.home-assistant = {
277 enable = true;
278 customComponents = with pkgs.home-assistant-custom-components; [
279 frigate
280 ];
281 extraComponents = [
282 "esphome"
283 "met"
284 "radio_browser"
285 "homekit"
286 "homekit_controller"
287 "isal"
288 "mqtt"
289 "tasmota"
290 "wiz"
291 "google_translate" # TTS - was missing gtts module
292 "ecobee" # Was missing pyecobee module
293 "ibeacon" # Was missing ibeacon_ble module
294 "go2rtc" # Camera streaming
295 "generic" # Generic camera integration
296 ];
297 config = {
298 homeassistant = {
299 time_zone = "America/Toronto";
300 };
301 default_config = { };
302 zeroconf = { };
303 # MQTT configuration - broker must be set up via UI
304 mqtt = { };
305 # Automations
306 automation = [
307 {
308 id = "1761448856909";
309 alias = "Lower heat at night";
310 trigger = [
311 {
312 platform = "time";
313 at = "23:00:00";
314 }
315 ];
316 condition = [ ];
317 action = [
318 {
319 action = "climate.set_temperature";
320 target.device_id = "bfe22d32a4532f8ae991d6daffb48267";
321 data = {
322 hvac_mode = "heat";
323 temperature = 18;
324 };
325 }
326 ];
327 mode = "single";
328 }
329 {
330 id = "1766200000001";
331 alias = "Raise heat in morning";
332 trigger = [
333 {
334 platform = "time";
335 at = "06:00:00";
336 }
337 ];
338 condition = [ ];
339 action = [
340 {
341 action = "climate.set_temperature";
342 target.device_id = "bfe22d32a4532f8ae991d6daffb48267";
343 data = {
344 hvac_mode = "heat";
345 temperature = 21;
346 };
347 }
348 ];
349 mode = "single";
350 }
351 {
352 id = "1766153071796";
353 alias = "Close Garage Door";
354 trigger = [
355 {
356 platform = "device";
357 device_id = "d8dedd8cd0ce1488d9830c455bb0a761";
358 domain = "cover";
359 entity_id = "cf36763543169888aa106b1acb02ad72";
360 type = "opened";
361 for = {
362 hours = 0;
363 minutes = 10;
364 seconds = 0;
365 };
366 }
367 ];
368 condition = [ ];
369 action = [
370 {
371 device_id = "d8dedd8cd0ce1488d9830c455bb0a761";
372 domain = "cover";
373 entity_id = "cf36763543169888aa106b1acb02ad72";
374 type = "close";
375 }
376 ];
377 mode = "single";
378 }
379 ];
380 };
381 };
382
383 # Invidious (self-hosted YouTube frontend for Kodi addon)
384 services.invidious = {
385 enable = true;
386 port = 3001;
387 address = "0.0.0.0";
388 settings = {
389 registration_enabled = false;
390 local = true;
391 invidious_companion = [
392 {
393 private_url = "http://127.0.0.1:8282/companion";
394 }
395 ];
396 invidious_companion_key = "k9x2mP4qR7wL3nYz";
397 };
398 };
399
400 # Invidious Companion (replaces deprecated inv-sig-helper)
401 virtualisation.oci-containers.containers.invidious-companion = {
402 image = "quay.io/invidious/invidious-companion:latest";
403 ports = [ "127.0.0.1:8282:8282" ];
404 environment = {
405 SERVER_SECRET_KEY = "k9x2mP4qR7wL3nYz";
406 };
407 extraOptions = [
408 "--read-only"
409 "--cap-drop=ALL"
410 ];
411 };
412
413 # Enable the OpenSSH daemon.
414 # services.openssh.enable = true;
415
416 security.pam.loginLimits = [
417 {
418 domain = "*";
419 type = "soft";
420 item = "nofile";
421 value = "8192";
422 }
423 ];
424
425 # trmnl-rs server
426 systemd.services.trmnl-rs = {
427 description = "TRMNL Server";
428 wantedBy = [ "multi-user.target" ];
429 wants = [ "network-online.target" ];
430 after = [ "network-online.target" "nss-lookup.target" ];
431 serviceConfig = {
432 ExecStart = "${inputs.trmnl-rs.packages.x86_64-linux.default}/bin/server";
433 Restart = "on-failure";
434 RestartSec = 5;
435 DynamicUser = true;
436 StateDirectory = "trmnl-rs";
437 WorkingDirectory = "/var/lib/trmnl-rs";
438 };
439 };
440
441 # Open ports in the firewall.
442 networking.firewall.allowedTCPPorts = [
443 8096 # jellyfin
444 5055 # jellyseer
445 3000 # vite dev port
446 3001 # Invidious
447 1883 # MQTT for Tasmota devices
448 2300 # trmnl
449 5000 # Frigate web UI
450 8971 # Frigate API
451 config.services.home-assistant.config.http.server_port
452 ];
453 networking.firewall.allowedUDPPorts = [
454 ];
455 # networking.firewall.enable = false;
456
457 # This value determines the NixOS release from which the default
458 # settings for stateful data, like file locations and database versions
459 # on your system were taken. It‘s perfectly fine and recommended to leave
460 # this value at the release version of the first install of this system.
461 # Before changing this value read the documentation for this option
462 # (e.g. man configuration.nix or on https://nixos.org/nixos/options.html).
463 system.stateVersion = "25.05"; # Did you read the comment?
464
465}