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 age.secrets.wireguard.file = ../../secrets/wireguard.age;
124
125 nixarr = {
126 enable = true;
127 mediaDir = "/mnt/storage1/nixarr/media";
128 vpn = {
129 enable = true;
130 wgConf = config.age.secrets.wireguard.path;
131 };
132
133 jellyfin = {
134 enable = true;
135 openFirewall = true;
136 };
137
138 transmission = {
139 enable = true;
140 vpn.enable = true;
141 };
142 sabnzbd = {
143 enable = true;
144 vpn.enable = true;
145 openFirewall = true;
146 };
147
148 prowlarr.enable = true;
149 radarr.enable = true;
150 sonarr.enable = true;
151 jellyseerr = {
152 enable = true;
153 openFirewall = true;
154 };
155
156 recyclarr = {
157 enable = true;
158 configuration = {
159 sonarr = {
160 series = {
161 base_url = "http://localhost:8989";
162 api_key = "!env_var SONARR_API_KEY";
163 quality_definition = {
164 type = "series";
165 };
166 delete_old_custom_formats = true;
167 custom_formats = [
168 {
169 trash_ids = [
170 "85c61753df5da1fb2aab6f2a47426b09" # BR-DISK
171 "9c11cd3f07101cdba90a2d81cf0e56b4" # LQ
172 ];
173 assign_scores_to = [
174 {
175 name = "WEB-DL (1080p)";
176 score = -10000;
177 }
178 ];
179 }
180 ];
181 };
182 };
183 radarr = {
184 movies = {
185 base_url = "http://localhost:7878";
186 api_key = "!env_var RADARR_API_KEY";
187 quality_definition = {
188 type = "movie";
189 };
190 delete_old_custom_formats = true;
191 custom_formats = [
192 {
193 trash_ids = [
194 "570bc9ebecd92723d2d21500f4be314c" # Remaster
195 "eca37840c13c6ef2dd0262b141a5482f" # 4K Remaster
196 ];
197 assign_scores_to = [
198 {
199 name = "HD Bluray + WEB";
200 score = 25;
201 }
202 ];
203 }
204 ];
205 };
206 };
207 };
208 };
209 };
210
211 # Install Kodi Sync Queue plugin into Jellyfin
212 systemd.services.jellyfin.serviceConfig.ExecStartPre =
213 let
214 pluginDir = "/data/.state/nixarr/jellyfin/data/plugins/Kodi Sync Queue/15.0.0.0";
215 in
216 pkgs.writeShellScript "install-jellyfin-plugins" ''
217 mkdir -p "${pluginDir}"
218 cp -f ${jellyfinKodiSyncQueue}/*.dll ${jellyfinKodiSyncQueue}/meta.json "${pluginDir}/"
219 '';
220
221 # MQTT broker for Home Assistant (Tasmota devices, Frigate)
222 services.mosquitto = {
223 enable = true;
224 listeners = [
225 {
226 acl = [ "pattern readwrite #" ];
227 omitPasswordAuth = true;
228 settings.allow_anonymous = true;
229 }
230 ];
231 };
232
233 # Frigate NVR for camera recording and AI object detection
234 services.frigate = {
235 enable = true;
236 hostname = "frigate";
237 settings = {
238 mqtt = {
239 enabled = true;
240 host = "localhost";
241 port = 1883;
242 };
243
244 detectors = {
245 cpu = {
246 type = "cpu";
247 num_threads = 2;
248 };
249 };
250
251 cameras = {
252 picam = {
253 enabled = true;
254 ffmpeg = {
255 hwaccel_args = "preset-nvidia-h264";
256 inputs = [
257 {
258 path = "rtsp://pi:8554/picam";
259 roles = [
260 "detect"
261 "record"
262 ];
263 }
264 ];
265 };
266 detect = {
267 enabled = true;
268 width = 1920;
269 height = 1080;
270 fps = 3;
271 };
272 record = {
273 enabled = true;
274 retain = {
275 days = 7;
276 mode = "active_objects";
277 };
278 };
279 snapshots = {
280 enabled = true;
281 bounding_box = true;
282 retain = {
283 default = 14;
284 };
285 };
286 objects = {
287 track = [
288 "person"
289 "dog"
290 "cat"
291 "car"
292 ];
293 };
294 zones = {
295 driveway = {
296 coordinates = "0,0.243,1,0.544,1,1,0,1,0,0.75";
297 objects = [
298 "person"
299 "car"
300 "dog"
301 "cat"
302 ];
303 };
304 };
305 };
306
307 pizerocam = {
308 enabled = true;
309 ffmpeg = {
310 hwaccel_args = "preset-nvidia-h264";
311 inputs = [
312 {
313 path = "rtsp://pizero:8554/pizerocam";
314 roles = [ "record" ];
315 }
316 ];
317 };
318 detect = {
319 enabled = false;
320 };
321 record = {
322 enabled = true;
323 retain = {
324 days = 2;
325 mode = "all";
326 };
327 };
328 };
329 };
330
331 record = {
332 enabled = true;
333 retain = {
334 days = 7;
335 mode = "active_objects";
336 };
337 };
338 };
339 };
340
341 # Home Assistant service
342 services.home-assistant = {
343 enable = true;
344 customComponents = with pkgs.home-assistant-custom-components; [
345 frigate
346 ];
347 extraComponents = [
348 "esphome"
349 "met"
350 "radio_browser"
351 "homekit"
352 "homekit_controller"
353 "isal"
354 "mqtt"
355 "tasmota"
356 "wiz"
357 "google_translate" # TTS - was missing gtts module
358 "ecobee" # Was missing pyecobee module
359 "ibeacon" # Was missing ibeacon_ble module
360 "go2rtc" # Camera streaming
361 "generic" # Generic camera integration
362 ];
363 config = {
364 homeassistant = {
365 time_zone = "America/Toronto";
366 };
367 default_config = { };
368 zeroconf = { };
369 # MQTT configuration - broker must be set up via UI
370 mqtt = { };
371 # Automations
372 automation = [
373 {
374 id = "1761448856909";
375 alias = "Lower heat at night";
376 trigger = [
377 {
378 platform = "time";
379 at = "23:00:00";
380 }
381 ];
382 condition = [ ];
383 action = [
384 {
385 action = "climate.set_temperature";
386 target.device_id = "bfe22d32a4532f8ae991d6daffb48267";
387 data = {
388 hvac_mode = "heat";
389 temperature = 18;
390 };
391 }
392 ];
393 mode = "single";
394 }
395 {
396 id = "1766200000001";
397 alias = "Raise heat in morning";
398 trigger = [
399 {
400 platform = "time";
401 at = "06:00:00";
402 }
403 ];
404 condition = [ ];
405 action = [
406 {
407 action = "climate.set_temperature";
408 target.device_id = "bfe22d32a4532f8ae991d6daffb48267";
409 data = {
410 hvac_mode = "heat";
411 temperature = 21;
412 };
413 }
414 ];
415 mode = "single";
416 }
417 {
418 id = "1766153071796";
419 alias = "Close Garage Door";
420 trigger = [
421 {
422 platform = "device";
423 device_id = "d8dedd8cd0ce1488d9830c455bb0a761";
424 domain = "cover";
425 entity_id = "cf36763543169888aa106b1acb02ad72";
426 type = "opened";
427 for = {
428 hours = 0;
429 minutes = 10;
430 seconds = 0;
431 };
432 }
433 ];
434 condition = [ ];
435 action = [
436 {
437 device_id = "d8dedd8cd0ce1488d9830c455bb0a761";
438 domain = "cover";
439 entity_id = "cf36763543169888aa106b1acb02ad72";
440 type = "close";
441 }
442 ];
443 mode = "single";
444 }
445 ];
446 };
447 };
448
449 # Invidious (self-hosted YouTube frontend for Kodi addon)
450 services.invidious = {
451 enable = false;
452 port = 3001;
453 address = "0.0.0.0";
454 database.port = 5433;
455 settings = {
456 registration_enabled = false;
457 local = true;
458 invidious_companion = [
459 {
460 private_url = "http://127.0.0.1:8282/companion";
461 }
462 ];
463 invidious_companion_key = "k9x2mP4qR7wL3nYz";
464 };
465 };
466
467 # Invidious Companion (replaces deprecated inv-sig-helper)
468 virtualisation.oci-containers.containers.invidious-companion = {
469 image = "quay.io/invidious/invidious-companion:latest";
470 ports = [ "127.0.0.1:8282:8282" ];
471 environment = {
472 SERVER_SECRET_KEY = "k9x2mP4qR7wL3nYz";
473 };
474 extraOptions = [
475 "--read-only"
476 "--cap-drop=ALL"
477 ];
478 };
479
480 environment.systemPackages = [
481 pkgs.lm_sensors
482 bambu-studio
483 ];
484
485 # Enable the OpenSSH daemon.
486 # services.openssh.enable = true;
487
488 security.pam.loginLimits = [
489 {
490 domain = "*";
491 type = "soft";
492 item = "nofile";
493 value = "8192";
494 }
495 ];
496
497 # trmnl-rs server
498 systemd.services.trmnl-rs = {
499 description = "TRMNL Server";
500 wantedBy = [ "multi-user.target" ];
501 wants = [ "network-online.target" ];
502 after = [
503 "network-online.target"
504 "nss-lookup.target"
505 ];
506 serviceConfig = {
507 ExecStart = "${inputs.trmnl-rs.packages.x86_64-linux.default}/bin/server";
508 Restart = "on-failure";
509 RestartSec = 5;
510 DynamicUser = true;
511 StateDirectory = "trmnl-rs";
512 WorkingDirectory = "/var/lib/trmnl-rs";
513 };
514 };
515
516 # Open ports in the firewall.
517 networking.firewall.allowedTCPPorts = [
518 8096 # jellyfin
519 5055 # jellyseer
520 3000 # vite dev port
521 3001 # Invidious
522 1883 # MQTT for Tasmota devices
523 2300 # trmnl
524 5000 # Frigate web UI
525 8971 # Frigate API
526 config.services.home-assistant.config.http.server_port
527 ];
528 networking.firewall.allowedUDPPorts = [
529 ];
530 # networking.firewall.enable = false;
531
532 # This value determines the NixOS release from which the default
533 # settings for stateful data, like file locations and database versions
534 # on your system were taken. It‘s perfectly fine and recommended to leave
535 # this value at the release version of the first install of this system.
536 # Before changing this value read the documentation for this option
537 # (e.g. man configuration.nix or on https://nixos.org/nixos/options.html).
538 system.stateVersion = "25.05"; # Did you read the comment?
539
540}