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