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 = 2;
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 = [
207 "detect"
208 "record"
209 ];
210 }
211 ];
212 };
213 detect = {
214 enabled = true;
215 width = 1920;
216 height = 1080;
217 fps = 3;
218 };
219 record = {
220 enabled = true;
221 retain = {
222 days = 7;
223 mode = "active_objects";
224 };
225 };
226 snapshots = {
227 enabled = true;
228 bounding_box = true;
229 retain = {
230 default = 14;
231 };
232 };
233 objects = {
234 track = [
235 "person"
236 "dog"
237 "cat"
238 "car"
239 ];
240 };
241 zones = {
242 driveway = {
243 coordinates = "0,0.243,1,0.544,1,1,0,1,0,0.75";
244 objects = [
245 "person"
246 "car"
247 "dog"
248 "cat"
249 ];
250 };
251 };
252 };
253
254 pizerocam = {
255 enabled = true;
256 ffmpeg = {
257 hwaccel_args = "preset-nvidia-h264";
258 inputs = [
259 {
260 path = "rtsp://pizero:8554/pizerocam";
261 roles = [ "record" ];
262 }
263 ];
264 };
265 detect = {
266 enabled = false;
267 };
268 record = {
269 enabled = true;
270 retain = {
271 days = 2;
272 mode = "all";
273 };
274 };
275 };
276 };
277
278 record = {
279 enabled = true;
280 retain = {
281 days = 7;
282 mode = "active_objects";
283 };
284 };
285 };
286 };
287
288 # Home Assistant service
289 services.home-assistant = {
290 enable = true;
291 customComponents = with pkgs.home-assistant-custom-components; [
292 frigate
293 ];
294 extraComponents = [
295 "esphome"
296 "met"
297 "radio_browser"
298 "homekit"
299 "homekit_controller"
300 "isal"
301 "mqtt"
302 "tasmota"
303 "wiz"
304 "google_translate" # TTS - was missing gtts module
305 "ecobee" # Was missing pyecobee module
306 "ibeacon" # Was missing ibeacon_ble module
307 "go2rtc" # Camera streaming
308 "generic" # Generic camera integration
309 ];
310 config = {
311 homeassistant = {
312 time_zone = "America/Toronto";
313 };
314 default_config = { };
315 zeroconf = { };
316 # MQTT configuration - broker must be set up via UI
317 mqtt = { };
318 # Automations
319 automation = [
320 {
321 id = "1761448856909";
322 alias = "Lower heat at night";
323 trigger = [
324 {
325 platform = "time";
326 at = "23:00:00";
327 }
328 ];
329 condition = [ ];
330 action = [
331 {
332 action = "climate.set_temperature";
333 target.device_id = "bfe22d32a4532f8ae991d6daffb48267";
334 data = {
335 hvac_mode = "heat";
336 temperature = 18;
337 };
338 }
339 ];
340 mode = "single";
341 }
342 {
343 id = "1766200000001";
344 alias = "Raise heat in morning";
345 trigger = [
346 {
347 platform = "time";
348 at = "06:00:00";
349 }
350 ];
351 condition = [ ];
352 action = [
353 {
354 action = "climate.set_temperature";
355 target.device_id = "bfe22d32a4532f8ae991d6daffb48267";
356 data = {
357 hvac_mode = "heat";
358 temperature = 21;
359 };
360 }
361 ];
362 mode = "single";
363 }
364 {
365 id = "1766153071796";
366 alias = "Close Garage Door";
367 trigger = [
368 {
369 platform = "device";
370 device_id = "d8dedd8cd0ce1488d9830c455bb0a761";
371 domain = "cover";
372 entity_id = "cf36763543169888aa106b1acb02ad72";
373 type = "opened";
374 for = {
375 hours = 0;
376 minutes = 10;
377 seconds = 0;
378 };
379 }
380 ];
381 condition = [ ];
382 action = [
383 {
384 device_id = "d8dedd8cd0ce1488d9830c455bb0a761";
385 domain = "cover";
386 entity_id = "cf36763543169888aa106b1acb02ad72";
387 type = "close";
388 }
389 ];
390 mode = "single";
391 }
392 ];
393 };
394 };
395
396 # Invidious (self-hosted YouTube frontend for Kodi addon)
397 services.invidious = {
398 enable = true;
399 port = 3001;
400 address = "0.0.0.0";
401 settings = {
402 registration_enabled = false;
403 local = true;
404 invidious_companion = [
405 {
406 private_url = "http://127.0.0.1:8282/companion";
407 }
408 ];
409 invidious_companion_key = "k9x2mP4qR7wL3nYz";
410 };
411 };
412
413 # Invidious Companion (replaces deprecated inv-sig-helper)
414 virtualisation.oci-containers.containers.invidious-companion = {
415 image = "quay.io/invidious/invidious-companion:latest";
416 ports = [ "127.0.0.1:8282:8282" ];
417 environment = {
418 SERVER_SECRET_KEY = "k9x2mP4qR7wL3nYz";
419 };
420 extraOptions = [
421 "--read-only"
422 "--cap-drop=ALL"
423 ];
424 };
425
426 environment.systemPackages = [
427 pkgs.lm_sensors
428 ];
429
430 # Enable the OpenSSH daemon.
431 # services.openssh.enable = true;
432
433 security.pam.loginLimits = [
434 {
435 domain = "*";
436 type = "soft";
437 item = "nofile";
438 value = "8192";
439 }
440 ];
441
442 # trmnl-rs server
443 systemd.services.trmnl-rs = {
444 description = "TRMNL Server";
445 wantedBy = [ "multi-user.target" ];
446 wants = [ "network-online.target" ];
447 after = [
448 "network-online.target"
449 "nss-lookup.target"
450 ];
451 serviceConfig = {
452 ExecStart = "${inputs.trmnl-rs.packages.x86_64-linux.default}/bin/server";
453 Restart = "on-failure";
454 RestartSec = 5;
455 DynamicUser = true;
456 StateDirectory = "trmnl-rs";
457 WorkingDirectory = "/var/lib/trmnl-rs";
458 };
459 };
460
461 # Open ports in the firewall.
462 networking.firewall.allowedTCPPorts = [
463 8096 # jellyfin
464 5055 # jellyseer
465 3000 # vite dev port
466 3001 # Invidious
467 1883 # MQTT for Tasmota devices
468 2300 # trmnl
469 5000 # Frigate web UI
470 8971 # Frigate API
471 config.services.home-assistant.config.http.server_port
472 ];
473 networking.firewall.allowedUDPPorts = [
474 ];
475 # networking.firewall.enable = false;
476
477 # This value determines the NixOS release from which the default
478 # settings for stateful data, like file locations and database versions
479 # on your system were taken. It‘s perfectly fine and recommended to leave
480 # this value at the release version of the first install of this system.
481 # Before changing this value read the documentation for this option
482 # (e.g. man configuration.nix or on https://nixos.org/nixos/options.html).
483 system.stateVersion = "25.05"; # Did you read the comment?
484
485}