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