Rebase to flake parts #9

This commit is contained in:
2026-05-10 01:45:16 -03:00
parent 34b89af77f
commit f02606902c
46 changed files with 2382 additions and 166 deletions
+1 -1
View File
@@ -3,7 +3,7 @@ keys:
- &host_14900k age1elk6zwmcylwfk7gd4pjda7g29upftjvxys8py42s8d42jklnyv7s7dm9z2
- &host_uConsole age193gw802ytal7h5p5q37kpd9079k2vsflzmnvupcwfxh2kjdrwqtsk3g6rm
- &host_t2mbp age1yr7vurfxc3w8ewfw9djfm54atw6ayze69qglamecuft5q0n9gu2sadsa2m
- &host_ideapad age1m30m9xzszmcawte35m0yymz42gfx3x84w7d5l67mtdtajhgpfgssuc2plm
- &host_ideapad age1hya7pgpe8zal52w3pjf036tpapmehedatfm4r84h30t4wuh079ssedfd37
- &host_nix-server age1p05z980kdtngk9mw67hfev72h7xhslplpxfk9yskgmf0hl4lu3ls04zht9
creation_rules:
- path_regex: secrets/[^/]+\.(yaml|json|env|ini)$
+8
View File
@@ -58,6 +58,14 @@
xdg.portal.extraPortals =
[ pkgs.xdg-desktop-portal-gtk ]
++ lib.optionals cfg.plasma.enable [ pkgs.kdePackages.xdg-desktop-portal-kde ];
# Backlight control: `brightnessctl` + its udev rule (auto-loaded from the package's
# `lib/udev/rules.d`). The rule grants the `video` group write access to
# `/sys/class/backlight/*/brightness`; the user catalog adds desktop users to `video`.
# The Hyprland defaults already bind XF86MonBrightness* to brightnessctl, and the Niri
# base config does the same — this guarantees the binary is actually present.
environment.systemPackages = [ pkgs.brightnessctl ];
services.udev.packages = [ pkgs.brightnessctl ];
})
(lib.mkIf (guiEnabled && !useGreeter) {
services.displayManager.sddm = {
+5
View File
@@ -113,6 +113,11 @@ let
"toggle"
];
# Backlight: relies on `pkgs.brightnessctl` being on PATH (provided by `desktopGui` when
# any GUI session is enabled) and the user being in the `video` group (catalog default).
"XF86MonBrightnessUp".spawn = [ "brightnessctl" "set" "+5%" ];
"XF86MonBrightnessDown".spawn = [ "brightnessctl" "set" "5%-" ];
Print.screenshot = _: { };
"Ctrl+Print"."screenshot-screen" = _: { };
"Alt+Print"."screenshot-window" = _: { };
+3 -43
View File
@@ -4,28 +4,9 @@
#TODO[epic=Moderate] Clean this up, move to host's configuration.nix.
{ config, lib, ... }:
let
gpuPassthrough = config.chiasson.system.vm.gpuPassthrough.enable;
in
{
chiasson.desktop.niri.extraSettings = {
extraConfig =
if gpuPassthrough then
''
output "DP-1" {
mode "2560x1080@144"
scale 1.0
position x=1920 y=0
focus-at-startup
}
output "HDMI-A-2" {
mode "2560x1080@60"
scale 1.0
position x=0 y=0
}
''
else
''
extraConfig = ''
output "DP-2" {
mode "2560x1080@144"
scale 1.0
@@ -55,33 +36,12 @@ in
chiasson.desktop.hyprland.settings = lib.mkIf config.chiasson.desktop.hyprland.enable (
let
monitorList =
if gpuPassthrough then
[
"DP-1, 2560x1080@144, 0x0, 1"
"HDMI-A-2, 1920x1080@60, auto-up, 1"
]
else
[
monitorList = [
"DP-2, 2560x1080@144, 0x0, 1"
"DP-4, 1920x1080@144, 0x-1080, 1"
"HDMI-A-3, 1920x1080@60, -1920x0, 1"
];
workspaceList =
if gpuPassthrough then
[
"1, monitor:DP-1, default:true"
"2, monitor:DP-1"
"3, monitor:DP-1"
"4, monitor:DP-1"
"5, monitor:HDMI-A-2, default:true"
"6, monitor:HDMI-A-2"
"7, monitor:HDMI-A-2"
"8, monitor:HDMI-A-2"
"9, monitor:DP-1"
]
else
[
workspaceList = [
"1, monitor:DP-3, default:true"
"2, monitor:DP-3"
"3, monitor:DP-3"
@@ -26,8 +26,9 @@
mountdPort = 4000;
lockdPort = 4001;
statdPort = 4002;
# fsid= stabilizes file handles across server reboots/remounts of this tree (avoids client ESTALE).
exports = ''
/mnt/test/jellyfin 192.168.2.238(rw,sync,no_subtree_check,crossmnt,root_squash,all_squash,anonuid=990,anongid=990)
/mnt/test/jellyfin 192.168.2.238(rw,sync,no_subtree_check,crossmnt,root_squash,all_squash,anonuid=990,anongid=990,fsid=1)
'';
};
+4 -11
View File
@@ -1,18 +1,12 @@
# NVIDIA for host desktop; when `chiasson.system.vm.gpuPassthrough` is enabled, drop NVIDIA for VFIO (port later).
# NVIDIA for host desktop.
{ config, lib, pkgs, ... }:
let
passthrough = config.chiasson.system.vm.gpuPassthrough.enable;
in
{
boot.kernelParams = [ "snd_hda_core.gpu_bind=0" ];
boot.kernelPackages = lib.mkDefault pkgs.linuxPackages_latest;
services.xserver.videoDrivers = if passthrough then [ "modesetting" ] else [ "nvidia" ];
services.xserver.videoDrivers = [ "nvidia" ];
hardware.nvidia =
if passthrough then
lib.mkForce { }
else {
hardware.nvidia = {
modesetting.enable = true;
powerManagement.enable = false;
powerManagement.finegrained = false;
@@ -21,6 +15,5 @@ in
package = config.boot.kernelPackages.nvidiaPackages.stable;
};
# Needed for `docker compose` GPU passthrough (e.g. `--gpus all` / DEVICE=gpu).
hardware.nvidia-container-toolkit.enable = !passthrough;
hardware.nvidia-container-toolkit.enable = true;
}
+1 -7
View File
@@ -84,13 +84,7 @@ services.cloudflare-warp.enable = true;
};
chiasson.system = {
# libvirt/QEMU + VFIO; host uses Intel iGPU for Niri while NVIDIA is passed through (see
# `_private/nvidia.nix`, `_private/displays.nix`). If your GPU is not RTX 2070-class IDs, set
# `chiasson.system.vm.gpuPassthrough.vfioIds` from `lspci -nn` (GPU + HDA functions in the same group).
vm = {
enable = true;
gpuPassthrough.enable = false;
};
ytDlpTelequebecPatch.enable = true;
audio.enable = true;
docker.enable = true;
@@ -0,0 +1,139 @@
# Cameras on the Lenovo Duet 3 (`lenovo-wormdingler`) — TODO
The front and rear cameras do **not** work on the current Mobile NixOS image.
This file is a starting point for picking the work back up later, so we don't
have to re-diagnose from scratch.
## Current state (May 2026)
Empirical, taken on the running device:
```
$ uname -a
Linux ideapad 6.5.0 #1-mobile-nixos SMP Tue Jan 1 00:00:00 UTC 1980 aarch64 GNU/Linux
$ ls /dev/video*
/dev/video0 ← Qualcomm Venus video decoder (h.264/h.265 dec)
/dev/video1 ← Qualcomm Venus video encoder (h.264/h.265 enc)
$ ls /dev/media* # nothing — no media controller graph at all
$ lsmod | grep -i camss # nothing
$ wpctl status | grep -A1 Sources # nothing — PipeWire has no camera sources
```
Relevant kernel config bits in `/proc/config.gz` on the running image:
```
CONFIG_MEDIA_CAMERA_SUPPORT=y ← media framework is on
# CONFIG_VIDEO_QCOM_CAMSS is not set ← Qualcomm Camera Subsystem driver is OFF
# CONFIG_VIDEO_OV5675 is not set ← every relevant sensor driver is OFF
# CONFIG_VIDEO_OV13858 is not set
# CONFIG_VIDEO_HI556 is not set
# (full grep of `VIDEO_OV*` / `VIDEO_HI*` is all `not set`)
```
So the *media framework* is enabled, but the *ISP driver* (CAMSS) and every
plausible sensor driver are off, and no out-of-tree modules are shipped. The
two `/dev/video*` nodes are the Venus codecs, not cameras — that's why
`snapshot` reports "no camera found."
## Why this is non-trivial
1. **Kernel rebuild required.** `mobile-nixos` builds its own kernel for this
device (see `mobile.kernel.structuredConfig` in `hardware.nix`, where we
already enable `CIFS` and `EXFAT_FS`). Adding camera support means adding:
- `VIDEO_QCOM_CAMSS = module;`
- The right sensor driver(s) — and we don't currently know which ones the
Duet 3 actually uses. The `wormdingler` Chromium-OS DT references a pair
of OV-series sensors but the exact part numbers can vary by SKU and
production batch.
- Their dependencies (`I2C`, `V4L2`, `MEDIA_CONTROLLER`, etc. — most are
already pulled in by `MEDIA_CAMERA_SUPPORT`).
2. **Device-tree wiring.** Even with the drivers compiled in, the device tree
has to describe the CCI bus, the sensor I²C addresses, the regulators, the
reset/enable GPIOs, and the CSI port mapping. Mobile NixOS' DT for
wormdingler may or may not include these nodes — needs verification by
reading the upstream device file at
`${inputs.mobile-nixos}/devices/lenovo-wormdingler/`.
3. **libcamera is the user-space side.** CAMSS is not a "single v4l2 device"
driver — it exposes a media-controller graph that has to be configured by
libcamera (per-sensor IPA, format negotiation, etc.). Apps then talk to
libcamera via the `libcamerasrc` PipeWire module or directly. So the
user-space stack is:
- `pkgs.libcamera` (system-wide)
- `pkgs.pipewire` already running, but needs the libcamera module enabled
(`services.pipewire.libcamera = …` or equivalent — check current option
name)
- GUI: `snapshot`, `gnome-camera`, or anything that talks to PipeWire
video sources.
## Investigation checklist
When picking this up again, do these in order:
1. **Identify the actual sensors.** The cleanest way:
- Read the upstream Mobile NixOS device file for wormdingler
(`devices/lenovo-wormdingler/default.nix` and any `.dts` overlays).
- Cross-check with the Chromium OS overlay at
<https://chromium.googlesource.com/chromiumos/overlays/board-overlays/+/refs/heads/main/overlay-trogdor/>
and the upstream Linux DTS at
`arch/arm64/boot/dts/qcom/sc7180-trogdor-wormdingler-*.dts`.
- As a runtime cross-check, when CAMSS is eventually loaded, `dmesg | grep -i
-E 'cci|sensor|isp'` will print the I²C probe attempts.
2. **Enable kernel options** in `modules/hosts/ideapad/hardware.nix` under
`mobile.kernel.structuredConfig`:
```nix
(helpers: with helpers; {
VIDEO_QCOM_CAMSS = module;
# plus whatever sensors step 1 identified, e.g.:
# VIDEO_OV5675 = module;
# VIDEO_OV13858 = module;
})
```
Cross-build on the 14900k via the existing flow (binfmt aarch64 + push back).
Reboot, then check:
```
ls /dev/media* # expect at least /dev/media0
sudo dmesg | grep -i -E 'camss|sensor|isp|cci' # probe history
sudo modprobe -v qcom-camss # if not auto-loaded
```
3. **Install diagnostic tools** for this round of work (do **not** keep these
in the long-term config unless cameras actually work):
```nix
environment.systemPackages = with pkgs; [
v4l-utils # provides v4l2-ctl, media-ctl
libcamera # provides `cam`, `qcam`
];
```
Then:
```
v4l2-ctl --list-devices
media-ctl -p # dumps the full media-controller graph
cam -l # libcamera's view of available cameras
```
4. **Wire libcamera into PipeWire.** Once `cam -l` shows at least one camera,
enable PipeWire's libcamera module (option name may have shifted; current
nixpkgs typically has `services.pipewire.wireplumber.extraConfig` or
similar). Then `wpctl status` should show new Sources under "Video", and
`snapshot` will see them.
5. **Re-add a camera GUI** to `configuration.nix`. `snapshot` is the simplest
touch-first option; `gnome-camera` and `cheese` are alternatives.
## Why nothing else is being touched right now
Steps 14 above are speculative — there's no guarantee the Duet 3 cameras have
working mainline-Linux sensor drivers at all. The conservative move is to
leave the config tablet-usable without them, document the dead end, and revisit
when there's time for a real spike.
@@ -0,0 +1,45 @@
{ pkgs, ... }: {
# ─────────────────────── Power & thermal ───────────────────────
# Snapdragon 7c (sc7180) on a tablet form factor: aim for battery life. `schedutil` is the
# right modern cpufreq governor on ARM (responsive + power-aware); use it instead of
# `powersave` to avoid pinning the CPU at minimum frequency under interactive load.
powerManagement.cpuFreqGovernor = "schedutil";
powerManagement.enable = true;
# ─────────────────────── logind: lid & power button ───────────────────────
# Closing the lid suspends, even on AC — Duet 3 is a tablet, treat it like one.
# Short press on power: suspend (matches ChromeOS/iOS); long press: poweroff.
# The DMS bar power menu is the way to reboot / shut down explicitly.
services.logind.settings.Login = {
HandleLidSwitch = "suspend";
HandleLidSwitchExternalPower = "suspend";
HandleLidSwitchDocked = "ignore";
HandlePowerKey = "suspend";
HandlePowerKeyLongPress = "poweroff";
};
# ─────────────────────── Idle / suspend tuning ───────────────────────
# Allow suspend-to-RAM but disable hibernate (ARM swap-resume is unreliable, and we don't
# have a swap device by default anyway).
systemd.sleep.settings.Sleep = {
AllowHibernation = "no";
AllowHybridSleep = "no";
AllowSuspendThenHibernate = "no";
};
# upower picks the right battery percentages for low/critical out of the box; just make
# sure the action on critical is hibernate-then-poweroff fallback (we disabled hibernate
# so it'll go straight to poweroff). DMS reads upower for the bar widget.
services.upower = {
enable = true;
criticalPowerAction = "PowerOff";
percentageLow = 15;
percentageCritical = 7;
percentageAction = 3;
};
# ─────────────────────── Bluetooth audio quality of life ───────────────────────
# Duet has limited mic/speaker hw — keep wpa_supplicant power-save off so audio doesn't crackle
# over Bluetooth when CPU is idle. (Wi-Fi + BT share the chip; aggressive power-save = stutter.)
networking.networkmanager.wifi.powersave = false;
}
@@ -0,0 +1,321 @@
# Host-only: ideapad tablet ergonomics — touchscreen calibration, IIO sensors, virtual keyboard,
# and per-session helper daemons (tablet-mode toggle + auto-rotation via iio-sensor-proxy) for both
# Niri and Hyprland.
#
# Why all of this lives at the *NixOS* layer (not the home-manager catalog under wisdom/):
# - The hardware bits (`hardware.sensor.iio.enable`, the udev calibration matrix) are system-wide
# and tied to this exact device, so they belong with the host module.
# - The compositor helpers run via session-specific autostart hooks (Niri `spawn-at-startup`,
# Hyprland `exec-once`); the wiring is gated on the matching `chiasson.desktop.<wm>.enable`,
# so picking a different session at the greeter just leaves them dormant.
#
# Two compositor flavours of each daemon:
# - Hyprland (CW transforms via `hyprctl`) — original; matches the old NixOS-New setup.
# - Niri (CCW transforms via `niri msg output`) — needed because Niri is the V2 default.
{
config,
lib,
pkgs,
...
}:
let
# ─────────────────────── Hyprland helpers ───────────────────────
ideapadTabletModeDaemon = pkgs.writeShellScriptBin "ideapad-tablet-mode-daemon" ''
#!/usr/bin/env bash
set -euo pipefail
HYPRCTL="${pkgs.hyprland}/bin/hyprctl"
JQ="${pkgs.jq}/bin/jq"
PKILL="${pkgs.procps}/bin/pkill"
PGREP="${pkgs.procps}/bin/pgrep"
WVKBD="${pkgs.wvkbd}/bin/wvkbd-mobintl"
monitor_name="DSI-1"
keyboard_scale="1.25"
tablet_scale="1.6"
state_file="''${XDG_RUNTIME_DIR:-/run/user/$(${pkgs.coreutils}/bin/id -u)}/ideapad-input-mode.state"
current_transform() {
"$HYPRCTL" -j monitors 2>/dev/null | "$JQ" -r --arg mon "$monitor_name" '
([.[] | select(.name == $mon)][0].transform // 1 | tostring)
' 2>/dev/null || echo "1"
}
has_attached_pogo_dock() {
"$HYPRCTL" -j devices 2>/dev/null | "$JQ" -e '
any((([.keyboards[]?.name] + [.mice[]?.name])[]?); (ascii_downcase | test("google-inc\\.-hammer")))
' >/dev/null
}
# wvkbd is no longer auto-spawned at session start this daemon owns its lifecycle so we
# only have a virtual keyboard surface in memory when the pogo cover is detached.
start_wvkbd() {
"$PGREP" -x wvkbd-mobintl >/dev/null 2>&1 && return 0
"$WVKBD" --non-exclusive -H 520 -L 360 --fn 'DejaVu Sans 18' >/dev/null 2>&1 &
}
stop_wvkbd() {
"$PKILL" -x wvkbd-mobintl >/dev/null 2>&1 || true
}
apply_mode() {
mode="$1"
transform="$(current_transform)"
if [ "$mode" = "tablet" ]; then
"$HYPRCTL" keyword monitor "$monitor_name,1200x2000@60.0,0x0,$tablet_scale, transform, $transform" >/dev/null 2>&1 || true
start_wvkbd
else
"$HYPRCTL" keyword monitor "$monitor_name,1200x2000@60.0,0x0,$keyboard_scale, transform, $transform" >/dev/null 2>&1 || true
stop_wvkbd
fi
"$HYPRCTL" keyword input:touchdevice:output "$monitor_name" >/dev/null 2>&1 || true
"$HYPRCTL" keyword input:touchdevice:transform "$transform" >/dev/null 2>&1 || true
printf "%s\n" "$mode" > "$state_file"
}
# Always reapply on startup the cached state file may lie (e.g., session restart while
# pogo state changed) and wvkbd needs to be (re)spawned since nothing else launches it now.
previous_mode=""
while true; do
if has_attached_pogo_dock; then
mode="keyboard"
else
mode="tablet"
fi
if [ "$mode" != "$previous_mode" ]; then
apply_mode "$mode"
previous_mode="$mode"
fi
sleep 2
done
'';
ideapadAutoRotateDaemon = pkgs.writeShellScriptBin "ideapad-autorotate-daemon" ''
#!/usr/bin/env bash
set -euo pipefail
HYPRCTL="${pkgs.hyprland}/bin/hyprctl"
MONITOR_SENSOR="${pkgs.iio-sensor-proxy}/bin/monitor-sensor"
monitor_name="DSI-1"
keyboard_scale="1.25"
tablet_scale="1.6"
state_file="''${XDG_RUNTIME_DIR:-/run/user/$(${pkgs.coreutils}/bin/id -u)}/ideapad-input-mode.state"
scale_for_mode() {
mode="$(cat "$state_file" 2>/dev/null || echo keyboard)"
if [ "$mode" = "tablet" ]; then
printf "%s\n" "$tablet_scale"
else
printf "%s\n" "$keyboard_scale"
fi
}
transform_for_orientation() {
orientation="$1"
case "$orientation" in
normal) printf "0\n" ;;
bottom-up) printf "2\n" ;;
left-up) printf "1\n" ;;
right-up) printf "3\n" ;;
*) return 1 ;;
esac
}
apply_orientation() {
orientation="$1"
transform="$(transform_for_orientation "$orientation")" || return 0
scale="$(scale_for_mode)"
"$HYPRCTL" keyword monitor "$monitor_name,1200x2000@60.0,0x0,$scale, transform, $transform" >/dev/null 2>&1 || true
"$HYPRCTL" keyword input:touchdevice:output "$monitor_name" >/dev/null 2>&1 || true
"$HYPRCTL" keyword input:touchdevice:transform "$transform" >/dev/null 2>&1 || true
}
while true; do
"$MONITOR_SENSOR" --accel 2>/dev/null | while IFS= read -r line; do
case "$line" in
*normal*) apply_orientation "normal" ;;
*bottom-up*) apply_orientation "bottom-up" ;;
*left-up*) apply_orientation "left-up" ;;
*right-up*) apply_orientation "right-up" ;;
esac
done
sleep 2
done
'';
# ─────────────────────── Niri helpers ───────────────────────
# `niri msg output DSI-1 transform <T>` accepts: normal | 90 | 180 | 270.
#
# Empirical mapping for this exact device + ACCEL_MOUNT_MATRIX (lenovo-wormdingler family):
# physical position iio orientation correct niri transform
# ────────────────── ──────────────── ──────────────────────
# keyboard down (LS) left-up normal
# keyboard up (LS) right-up 180
# keyboard right (P) bottom-up 90
# keyboard left (P) normal 270
# iio's "normal" is *not* the natural landscape pose here — the panel-mount matrix in the
# mobile-nixos device file biases it toward portrait-with-keyboard-left. Don't trust the
# textbook iio→Wayland convention; trust the table above.
ideapadNiriTabletModeDaemon = pkgs.writeShellScriptBin "ideapad-niri-tablet-mode-daemon" ''
#!/usr/bin/env bash
set -euo pipefail
NIRI="${pkgs.niri}/bin/niri"
JQ="${pkgs.jq}/bin/jq"
PKILL="${pkgs.procps}/bin/pkill"
PGREP="${pkgs.procps}/bin/pgrep"
WVKBD="${pkgs.wvkbd}/bin/wvkbd-mobintl"
output_name="DSI-1"
keyboard_scale="1.25"
tablet_scale="1.6"
state_file="''${XDG_RUNTIME_DIR:-/run/user/$(${pkgs.coreutils}/bin/id -u)}/ideapad-input-mode.state"
has_attached_pogo_dock() {
# Niri doesn't expose input devices via IPC, so we look in `/dev/input/by-id` for the
# USB-class enumeration of the Google Hammer / Whiskers pogo keyboard. The directory
# contains symlinks; presence of either string means the cover is attached.
# NOTE: `find` ships in findutils, not coreutils using the wrong package here makes
# the pipeline fail silently (stderr is dropped) and pogo always reads as detached.
${pkgs.findutils}/bin/find /dev/input/by-id -maxdepth 1 -type l 2>/dev/null \
| ${pkgs.gnugrep}/bin/grep -i -E 'google.*hammer|google.*whiskers' >/dev/null
}
# wvkbd is owned by this daemon: spawn on pogo detach, kill on pogo attach. The DMS bar
# `pkill -SIGRTMIN -x wvkbd-mobintl` toggle still works while wvkbd is running (i.e., in
# tablet mode). When the pogo cover is on, the pill is a no-op that's intentional, the
# physical keyboard is the input.
start_wvkbd() {
"$PGREP" -x wvkbd-mobintl >/dev/null 2>&1 && return 0
"$WVKBD" --non-exclusive -H 520 -L 360 --fn 'DejaVu Sans 18' >/dev/null 2>&1 &
}
stop_wvkbd() {
"$PKILL" -x wvkbd-mobintl >/dev/null 2>&1 || true
}
apply_mode() {
mode="$1"
if [ "$mode" = "tablet" ]; then
"$NIRI" msg output "$output_name" scale "$tablet_scale" >/dev/null 2>&1 || true
start_wvkbd
else
"$NIRI" msg output "$output_name" scale "$keyboard_scale" >/dev/null 2>&1 || true
stop_wvkbd
fi
printf "%s\n" "$mode" > "$state_file"
}
# Always reapply on startup see Hyprland daemon's identical comment.
previous_mode=""
while true; do
if has_attached_pogo_dock; then
mode="keyboard"
else
mode="tablet"
fi
if [ "$mode" != "$previous_mode" ]; then
apply_mode "$mode"
previous_mode="$mode"
fi
sleep 2
done
'';
ideapadNiriAutoRotateDaemon = pkgs.writeShellScriptBin "ideapad-niri-autorotate-daemon" ''
#!/usr/bin/env bash
set -euo pipefail
NIRI="${pkgs.niri}/bin/niri"
MONITOR_SENSOR="${pkgs.iio-sensor-proxy}/bin/monitor-sensor"
output_name="DSI-1"
transform_for_orientation() {
orientation="$1"
case "$orientation" in
normal) printf "270\n" ;;
bottom-up) printf "90\n" ;;
left-up) printf "normal\n" ;;
right-up) printf "180\n" ;;
*) return 1 ;;
esac
}
apply_orientation() {
orientation="$1"
transform="$(transform_for_orientation "$orientation")" || return 0
"$NIRI" msg output "$output_name" transform "$transform" >/dev/null 2>&1 || true
}
while true; do
"$MONITOR_SENSOR" --accel 2>/dev/null | while IFS= read -r line; do
case "$line" in
*normal*) apply_orientation "normal" ;;
*bottom-up*) apply_orientation "bottom-up" ;;
*left-up*) apply_orientation "left-up" ;;
*right-up*) apply_orientation "right-up" ;;
esac
done
sleep 2
done
'';
in
{
# ─────────────────────── Hardware ───────────────────────
hardware.sensor.iio.enable = true;
# Touchscreen calibration — solved empirically with `niri msg output DSI-1 transform normal`
# in the natural kb-down pose. The panel's touch hardware reports raw coordinates already
# aligned with the panel-native frame (HW(visual_top_left) = (0,0), etc.), so identity is
# correct. `niri input.touch.map-to-output = "DSI-1"` then handles per-orientation rotation
# on top — never re-tune this matrix per orientation; rotate the *output* instead.
services.udev.extraRules = ''
SUBSYSTEM=="input", ENV{ID_INPUT_TOUCHSCREEN}=="1", ENV{LIBINPUT_CALIBRATION_MATRIX}="1 0 0 0 1 0"
'';
# ─────────────────────── User-facing tools ───────────────────────
# System-wide so any user session (Niri or Hyprland) can launch wvkbd / hyprctl / niri-msg helpers.
environment.systemPackages = [
pkgs.wvkbd
pkgs.iio-sensor-proxy
ideapadTabletModeDaemon
ideapadAutoRotateDaemon
ideapadNiriTabletModeDaemon
ideapadNiriAutoRotateDaemon
];
# ─────────────────────── Niri session autostart ───────────────────────
# Set on the NixOS layer; the `desktopNiri` HM module merges this into per-user `niri/config.kdl`.
# Touch input is glued to DSI-1 so it follows whatever transform the autorotate daemon sets.
# wvkbd is intentionally NOT spawned here — `ideapad-niri-tablet-mode-daemon` starts/kills it
# in response to pogo-cover attach/detach so we don't keep a virtual-keyboard surface alive
# while a physical keyboard is plugged in.
chiasson.desktop.niri.extraSettings = lib.mkIf config.chiasson.desktop.niri.enable {
input.touch.map-to-output = "DSI-1";
# wrapper-modules schema: each entry is a `command argv` list of strings (or a single string).
spawn-at-startup = [
[ "ideapad-niri-autorotate-daemon" ]
[ "ideapad-niri-tablet-mode-daemon" ]
];
};
# ─────────────────────── Hyprland session autostart ───────────────────────
# Same lifecycle policy as Niri: the tablet-mode daemon owns wvkbd, no exec-once for it.
chiasson.desktop.hyprland.settings = lib.mkIf config.chiasson.desktop.hyprland.enable {
exec-once = lib.mkAfter [
"${pkgs.procps}/bin/pkill -x ideapad-tablet-mode-daemon >/dev/null 2>&1 || true; ideapad-tablet-mode-daemon &"
"${pkgs.procps}/bin/pkill -x ideapad-autorotate-daemon >/dev/null 2>&1 || true; ideapad-autorotate-daemon &"
];
bind = lib.mkAfter [
"Super, K, exec, pkill -SIGRTMIN -x wvkbd-mobintl"
];
monitor = lib.mkAfter [
"DSI-1,1200x2000@60.0,0x0,1.25, transform, 1"
];
};
}
+201
View File
@@ -0,0 +1,201 @@
{ self, inputs, ... }: {
# Lenovo Chromebook Duet 3 (`lenovo-wormdingler`) on Mobile NixOS.
#
# Phase 1 (minimal bootstrap) lived here previously; we now run the full V2 stack:
# mobile-nixos device + Niri/Hyprland/DMS, DankGreeter, Waydroid (tablet-class), wvkbd,
# IIO sensors, touchscreen calibration, attic cache, sops, and the standard user catalog.
flake.nixosModules.ideapadConfiguration =
{
self,
config,
lib,
pkgs,
...
}:
{
imports = [
# Mobile NixOS device + family + depthcharge system-type.
(import "${inputs.mobile-nixos}/lib/configuration.nix" {
device = "lenovo-wormdingler";
})
self.nixosModules.ideapadHardware
inputs.home-manager.nixosModules.home-manager
inputs.sops-nix.nixosModules.sops
self.nixosModules.system
self.nixosModules.desktop
self.nixosModules.users
self.nixosModules."client-services"
# Host-only: IIO + touchscreen calibration + per-compositor tablet/autorotate helpers.
./_private/touch-tablet.nix
# Host-only: cpufreq, lid/power-button policy, upower thresholds.
./_private/platform.nix
];
# ─────────────────────── Sops ───────────────────────
# `host_ideapad` recipient in `.sops.yaml` derives from the new ed25519 host key (post-reflash).
sops = {
defaultSopsFile = ../../../secrets/secrets.yaml;
defaultSopsFormat = "yaml";
age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
};
sops.secrets."users/olivier/hashedPassword".neededForUsers = true;
sops.secrets."caching/attic/token" = {
owner = "olivier";
group = "users";
mode = "0400";
};
sops.secrets."swiftshare/API_KEY" = {
owner = "olivier";
group = "users";
mode = "0400";
};
# ─────────────────────── Mobile NixOS / firmware ───────────────────────
# mruby's test-suite breaks on aarch64 in the Nix sandbox; the overlay strips checks and
# rebuilds Mobile NixOS' script-loader against the patched mruby.
chiasson.system.ideapadMrubyOverlay.enable = true;
# Wi-Fi modem (qcom-wcn3990) + Bluetooth (QCA crnv32) need binary blobs.
nixpkgs.config.allowUnfreePredicate =
pkg: builtins.elem (lib.getName pkg) [
"chromeos-sc7180-unredistributable-firmware"
"chromeos-sc7180-unredistributable-firmware-zstd"
];
hardware.firmware = [ pkgs.chromeos-sc7180-unredistributable-firmware ];
hardware.enableRedistributableFirmware = true;
# ─────────────────────── Attic (substitution + push + CLI token) ───────
chiasson.system.caching.attic = {
enable = true;
cacheName = "nixos-new";
endpoint = "http://192.168.2.238:8080/";
publicKey = "nixos-new:8NySIcT0HP7KvGQKgBRWoWESxxRA8BVYo8S85UNpNX0=";
tokenFile = config.sops.secrets."caching/attic/token".path;
push.enable = true;
userCli.enable = true;
};
# ─────────────────────── System bits ───────────────────────
chiasson.system = {
audio.enable = true;
networking = {
hostName = "ideapad";
networkManager = {
enable = true;
unmanaged = [ ];
};
wifi.tools.enabled = true;
};
extraPackages = with pkgs; [
gitMinimal
sops
ssh-to-age
];
};
# ─────────────────────── Desktop ───────────────────────
# Both compositors enabled; DankGreeter lets you pick at login. Default = Niri (V2 convention),
# Hyprland session is where the tablet-mode + autorotate daemons in `_private/touch-tablet.nix`
# actually run (they hook `exec-once`).
chiasson.desktop = {
niri.enable = true;
hyprland.enable = true;
defaultSession = "niri";
shell = "dms";
shells.dms = {
enableWvkbdToggle = true;
# Cross-build on the 14900k via binfmt and push back over LAN — much faster than
# rebuilding aarch64 closure on the Snapdragon. Mirrors the old NixOS-New flow:
# ssh out to nixdesk, run nixos-rebuild --target-host pointing back at us.
rebuildCommand = [
"bash"
"-lc"
''
ssh -t olivier@nixdesk \
"nixos-rebuild switch --flake path:/home/olivier/NixOS-V2#ideapad --target-host olivier@ideapad --sudo --ask-sudo-password 2>&1"
''
];
};
# Tablet-class screen → constrain Waydroid to a sane portrait-ish frame and use gesture nav
# instead of 3-button so it feels like the ChromeOS tablet UI.
#waydroid = {
# enable = true;
# multiWindows = false;
# width = 1600;
# height = 960;
# navigationMode = "gestures";
#};
};
# ─────────────────────── Users / HM ───────────────────────
chiasson.users.enabled = [ "olivier" ];
# Touch-friendly application set, mirroring uConsole's selection (no heavy IDEs / gaming).
chiasson.users.extraModules.olivier = [
self.homeManagerModules.wisdomFilebrowsersDolphin
self.homeManagerModules.wisdomTerminalsKitty
self.homeManagerModules.wisdomBrowsersZen
self.homeManagerModules.wisdomEditorsKate
self.homeManagerModules.wisdomShellFish
self.homeManagerModules.wisdomShellOhMyPosh
self.homeManagerModules.wisdomAppsSpotify
self.homeManagerModules.wisdomAppsLocalsend
self.homeManagerModules.wisdomDesktopScreenshot
{
chiasson.home = {
shell = {
fish.enable = true;
ohMyPosh.enable = true;
};
terminals.kitty.enable = true;
filebrowsers.dolphin.enable = true;
browsers.zen.enable = true;
editors.kate.enable = true;
apps.spotify.enable = true;
apps.localsend.enable = true;
desktop = {
screenshot = {
enable = true;
swiftshareApiKeyFile = "/run/secrets/swiftshare/API_KEY"; #TODO[epic=sops] redo this by passing sops file output directly
};
};
};
}
# Tablet-class apps: kept inline rather than promoting to wisdom modules — these aren't
# part of the broader catalog (no use on uConsole / 14900k / servers) and adding a wisdom
# module per single-host package would just be ceremony. If a second tablet host ever
# appears, factor them out then.
#
# NOTE on cameras: no v4l2/libcamera GUI is installed. The Mobile NixOS kernel for
# `lenovo-wormdingler` ships with `CONFIG_VIDEO_QCOM_CAMSS` disabled and no
# `VIDEO_OV*`/`VIDEO_HI*` sensor drivers, so `/dev/video0`-`/dev/video1` only expose
# the Qualcomm Venus codecs (h.264/h.265 enc/dec) and there is no camera source for
# PipeWire / libcamera to pick up. See `_private/CAMERA-TODO.md` for the steps that
# would (potentially) bring the front/rear cameras online — it's a kernel-rebuild +
# device-tree + libcamera project, not a config tweak.
(
{ pkgs, ... }:
{
home.packages = with pkgs; [
# PDF viewer — fits the existing KDE app set (Dolphin + Kate).
kdePackages.okular
# ePub reader, GTK4, large touch targets.
foliate
];
}
)
];
system.stateVersion = "26.05";
};
}
+14
View File
@@ -0,0 +1,14 @@
{ self, inputs, ... }: {
flake.nixosConfigurations.ideapad = inputs.nixpkgs.lib.nixosSystem {
system = "aarch64-linux";
specialArgs = {
inherit self inputs;
host = "ideapad";
system = "aarch64-linux";
};
modules = [
self.nixosModules.ideapadConfiguration
];
};
}
+22
View File
@@ -0,0 +1,22 @@
{ ... }: {
flake.nixosModules.ideapadHardware =
# Mobile NixOS' depthcharge system-type wires up the disk image, kernel partitions and
# rootfs entirely; we don't need a generated `hardware-configuration.nix`. This module is a
# placeholder so the host follows the standard `<host>Configuration` / `<host>Hardware` shape
# and gives us a place to drop kernel-config knobs that aren't covered by the device family.
{ ... }:
{
# Useful on a portable: mounting USB sticks (often exFAT) and SMB shares.
boot.supportedFilesystems = [ "exfat" "cifs" ];
# Mobile NixOS builds its own kernel — the regular `boot.kernelModules` won't help if the
# module isn't compiled in. `mobile.kernel.structuredConfig` lives upstream in
# `modules/system-types/depthcharge/kernel/` and is the right layer to add features.
mobile.kernel.structuredConfig = [
(helpers: with helpers; {
CIFS = module;
EXFAT_FS = module;
})
];
};
}
@@ -0,0 +1,30 @@
{ config, ... }: {
sops = {
templates."atticd.env" = {
owner = "root";
group = "root";
mode = "0400";
content = ''
ATTIC_SERVER_TOKEN_RS256_SECRET_BASE64=${config.sops.placeholder."attic/server-token-rs256-secret-base64"}
'';
};
};
sops.secrets."attic/server-token-rs256-secret-base64" = {
sopsFile = ../../../../secrets/attic-secrets.yaml;
owner = "root";
group = "root";
mode = "0400";
};
services.atticd = {
enable = true;
environmentFile = config.sops.templates."atticd.env".path;
settings = {
listen = "[::]:8080";
jwt = { };
};
};
chiasson.system.networking.firewall.allowedTCPPorts = [ 8080 ];
}
@@ -0,0 +1,12 @@
# DDRM Flask backend (Widevine / PlayReady decrypt). Extension URL: http://<host>:58239
{ pkgs, inputs, ... }:
{
services.ddrm-media-server = {
enable = true;
port = 58239;
listenAddress = "0.0.0.0";
openFirewall = true;
package = inputs.ddrm.packages.${pkgs.stdenv.hostPlatform.system}.default;
# State: /var/lib/ddrm-media (venv + configs + CDMs on first run — needs network for pip).
};
}
@@ -0,0 +1,11 @@
{ ... }:
{
# FlareSolverr (Cloudflare / JS challenge solver for some indexers).
# Typically used by Prowlarr as an HTTP proxy.
#
# UI/endpoint: http://<host>:8191
services.flaresolverr.enable = true;
networking.firewall.allowedTCPPorts = [ 8191 ];
}
@@ -0,0 +1,29 @@
{ config, ... }:
let
secretFilePath = ../secrets.yaml;
in
{
sops.secrets."immich/database-password".sopsFile = secretFilePath;
# Placeholders are expanded only inside template `content` (not in arbitrary Nix strings).
sops.templates."immich-db.env" = {
content = ''
POSTGRES_PASSWORD=${config.sops.placeholder."immich/database-password"}
DB_PASSWORD=${config.sops.placeholder."immich/database-password"}
'';
};
chiasson.system.services.immich = {
enable = true;
host = "0.0.0.0";
port = 2283;
timezone = "America/Moncton";
uploadLocation = "/var/lib/immich/library";
environmentFiles = [ config.sops.templates."immich-db.env".path ];
postgres = {
user = "postgres";
#password = ""; # Defined in sops.templates."immich-db.env"
database = "immich";
};
};
}
@@ -0,0 +1,27 @@
# NFS read-only mount of nixdesk (14900k) bulk storage for extra Jellyfin libraries.
# Source: ssh inventory hostName for 14900k. Export is defined in
# modules/hosts/14900k/_private/jellyfin-nfs-export.nix
#
# In Jellyfin (in addition to local /var/lib/media/...), add e.g.:
# Movies → /mnt/nixdesk-jellyfin/movies
# Shows → /mnt/nixdesk-jellyfin/tv
{ ... }:
let
# Must match LAN IP of the NFS server (flake `sshInventory` → hosts."14900k".hostName).
nfsExportHost = "192.168.2.25";
in
{
fileSystems."/mnt/nixdesk-jellyfin" = {
device = "${nfsExportHost}:/mnt/test/jellyfin";
fsType = "nfs";
options = [
"rw"
"noatime"
"nofail"
"_netdev"
"x-systemd.automount"
"x-systemd.idle-timeout=600"
];
};
}
@@ -0,0 +1,60 @@
# Jellyfin (native NixOS service). Local media: /var/lib/media (group `media`; jellyfin + server).
# Dashboard: Movies → /var/lib/media/movies, Shows → /var/lib/media/tv (see jellyfin-remote-storage.nix
# for bulk libraries on nixdesk at /mnt/nixdesk-jellyfin/{movies,tv}).
# Do not use "Mixed Movies and Shows" (deprecated): https://jellyfin.org/docs/general/server/media/mixed-movies-and-shows
# Dedicated disk: fileSystems."/var/lib/media" in hardware.nix, then fix ownership.
{ lib, ... }:
{
nixpkgs.overlays = [
(final: prev: {
jellyfin-web = prev.jellyfin-web.overrideAttrs (oldAttrs: {
postInstall =
(oldAttrs.postInstall or "")
+ ''
# Blank default Jellyfin banner assets (read-only store otherwise). Wildcards
# track hashed filenames across jellyfin-web releases; bump if layout changes.
find "$out" -type f \( -name 'banner-light.*.png' -o -name 'banner-dark.*.png' \) \
-exec truncate -s 0 {} \;
'';
});
})
];
users.groups.media = { };
users.users.jellyfin.extraGroups = [ "media" ];
users.users.server.extraGroups = [ "media" ];
systemd.tmpfiles.settings."nix-server-var-lib-media" = {
"/var/lib/media"."d" = {
mode = "0775";
user = "root";
group = "media";
};
"/var/lib/media/movies"."d" = {
mode = "0775";
user = "root";
group = "media";
};
"/var/lib/media/tv"."d" = {
mode = "0775";
user = "root";
group = "media";
};
};
services.jellyfin = {
enable = true;
openFirewall = true;
};
# `users.users.jellyfin.extraGroups` does not affect systemd; the service must list
# supplementary groups explicitly. Without `media`, directories mode 775 root:media are
# not writable by uid jellyfin (it only had group `jellyfin`), so deletes fail.
systemd.services.jellyfin.serviceConfig = {
SupplementaryGroups = [ "media" ];
# Jellyfin libraries may live on NFS (e.g. /mnt/nixdesk-jellyfin). PrivateUsers breaks
# uid mapping for NFS auth in practice; disable so deletes use the real host jellyfin uid.
PrivateUsers = lib.mkForce false;
};
}
@@ -0,0 +1,53 @@
# Organizr — homelab dashboard (Docker). UI: http://<host>:8888
# Official image: https://github.com/organizr/docker-organizr
#
# Wizard errors like "API … /default/ not writable" are almost always host permissions on
# `/var/lib/organizr`: the first container run may leave root-owned files under `/config`.
{ lib, pkgs, ... }:
{
users.groups.organizr = { gid = 950; };
users.users.organizr = {
isSystemUser = true;
uid = 950;
group = "organizr";
};
systemd.tmpfiles.settings."nix-server-organizr-config" = {
"/var/lib/organizr"."d" = {
mode = "0755";
user = "organizr";
group = "organizr";
};
};
# Recursively reset ownership (handles root-owned files from an earlier container run).
systemd.tmpfiles.settings."nix-server-organizr-config-perms" = {
"/var/lib/organizr"."Z" = {
mode = "0755";
user = "organizr";
group = "organizr";
};
};
systemd.services.docker-organizr.preStart = lib.mkBefore ''
${pkgs.coreutils}/bin/mkdir -p /var/lib/organizr
${pkgs.coreutils}/bin/chown -R organizr:organizr /var/lib/organizr
'';
virtualisation.oci-containers.containers.organizr = {
image = "ghcr.io/organizr/organizr:latest";
ports = [ "8888:80" ];
volumes = [
"/var/lib/organizr:/config"
];
environment = {
PUID = "950";
PGID = "950";
TZ = "America/Moncton";
# v2-master / master are stable v2; optional override:
# branch = "v2-master";
};
};
networking.firewall.allowedTCPPorts = [ 8888 ];
}
@@ -0,0 +1,20 @@
{config, ...}: {
virtualisation = {
docker.enable = true;
oci-containers = {
backend = "docker";
containers = {
portainer = {
image = "portainer/portainer-ce:latest";
ports = [ "9443:9443" ];
volumes = [
"/var/run/docker.sock:/var/run/docker.sock"
"/var/lib/portainer:/data"
];
};
};
};
};
networking.firewall.allowedTCPPorts = [ 9443 ];
}
@@ -0,0 +1,22 @@
{ lib, ... }:
{
# Prowlarr (indexer manager). UI: http://<host>:9696
# Data dir is /var/lib/prowlarr (see systemd unit ExecStart -data=…), not ~/.config/Prowlarr.
services.prowlarr.enable = true;
# Useful when Prowlarr/Sonarr/Radarr need to write into shared areas (downloads, etc.).
users.groups.prowlarr = { };
users.users.prowlarr = {
isSystemUser = true;
group = "prowlarr";
extraGroups = [ "media" ];
};
systemd.services.prowlarr.preStart = lib.mkBefore ''
mkdir -p /var/lib/prowlarr/Definitions/Custom
ln -sf ${./prowlarr/torrent9-custom.yml} /var/lib/prowlarr/Definitions/Custom/torrent9-custom.yml
'';
networking.firewall.allowedTCPPorts = [ 9696 ];
}
@@ -0,0 +1,193 @@
---
id: torrent9-custom
name: Torrent9 (Custom URL)
description: "Torrent9 is a FRENCH Public site for MOVIES / TV / GENERAL"
language: fr-FR
type: public
encoding: UTF-8
followredirect: true
testlinktorrent: false
links:
- https://www.torrent9.club/
- https://www6.torrent9.to/
legacylinks:
- https://www.torrent9.pl/ # this is a proxy for torrent9clone
- https://torrent9.black-mirror.xyz/ # this is a proxy for torrent9clone
- https://torrent9.unblocked.casa/ # this is a proxy for torrent9clone
- https://torrent9.proxyportal.fun/ # this is a proxy for torrent9clone
- https://torrent9.uk-unblock.xyz/ # this is a proxy for torrent9clone
- https://torrent9.ind-unblock.xyz/ # this is a proxy for torrent9clone
- https://ww1.torrent9.to/
- https://www.torrent9.is/
- https://torrent9.li/ # not a proxy for torrent9 or torrent9clone
- https://www.oxtorrent.me/
- https://www.torrent9.gg/
- https://www.torrent9.fi/ # this is the torrent9clone domain
- https://www.torrent9.fm/
- https://torrent9.se/ # redirect to www.
- https://torrent9.ninjaproxy1.com/ # no response data
- https://torrent9.proxyninja.org/ # Error 1007
- https://www.torrent9.se/
- https://torrent9.unblockninja.com/ # 403 forbidden
- https://ww1.torrent9.fm/
- https://www.torrent9.zone/ # clone? details links are broken
- https://torrent9.to/
- https://ww2.torrent9.to/
- https://www5.torrent9.to/
caps:
# dont forget to update the search fields category case block
categorymappings:
- {id: films, cat: Movies, desc: "Movies"}
- {id: series, cat: TV, desc: "TV"}
- {id: musique, cat: Audio, desc: "Music"}
- {id: ebook, cat: Books, desc: "Books"}
- {id: logiciels, cat: PC, desc: "Software"}
- {id: jeux-pc, cat: PC/Games, desc: "PC Games"}
- {id: other, cat: Other, desc: "Other"} # dummy cat for results missing icon
modes:
search: [q]
tv-search: [q, season, ep]
movie-search: [q]
music-search: [q]
book-search: [q]
allowrawsearch: true
settings:
- name: info_flaresolverr
type: info_flaresolverr
- name: multilang
type: checkbox
label: Replace MULTi by another language in release name
default: false
- name: multilanguage
type: select
label: Replace MULTi by this language
default: FRENCH
options:
FRENCH: FRENCH
MULTi FRENCH: MULTi FRENCH
ENGLISH: ENGLISH
MULTi ENGLISH: MULTi ENGLISH
VOSTFR: VOSTFR
MULTi VOSTFR: MULTi VOSTFR
- name: vostfr
type: checkbox
label: Replace VOSTFR and SUBFRENCH with ENGLISH
default: false
- name: sort
type: select
label: Sort requested from site (Only works for searches with Keywords)
default: ".html"
options:
".html": best
".html,trie-date-d": created desc
".html,trie-date-a": created asc
".html,trie-seeds-d": seeders desc
".html,trie-seeds-a": seeders asc
".html,trie-poid-d": size desc
".html,trie-poid-a": size asc
".html,trie-nom-d": title desc
".html,trie-nom-a": title asc
download:
selectors:
- selector: a[href^="magnet:?"]
attribute: href
- selector: script:contains("magnet:?")
filters:
- name: regexp
args: "\\s'(magnet:\\?.+?)';"
search:
paths:
- path: "{{ if .Keywords }}search_torrent/{{ .Keywords }}{{ .Config.sort }}{{ else }}home/{{ end }}"
keywordsfilters:
# if searching for season packs with S01 to saison 1 #9712
- name: re_replace
args: ["(?i)(S0)(\\d{1,2})$", "saison $2"]
- name: re_replace
args: ["(?i)(S)(\\d{1,3})$", "saison $2"]
- name: replace
args: [" ", "-"]
headers:
# site blocks all Linux User-Agents, so use a slightly altered Windows Jackett UA here (e.g. Safari/537.36 > Safari/537.35)
User-Agent: ["Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.35"]
rows:
selector: table.table-striped > tbody > tr
filters:
- name: andmatch
fields:
category_optional:
selector: td:nth-child(1) i
optional: true
case:
i[class="fa fa-video-camera"]: films
i[class="fa fa-tv"]: series # search by name
i[class="fa fa-desktop"]: series # keywordless search
i[class="fa fa-music"]: musique
i[class="fa fa-gamepad"]: jeux-pc
i[class="fa fa-laptop"]: logiciels
i[class="fa fa-book"]: ebook
category:
text: "{{ if .Result.category_optional }}{{ .Result.category_optional }}{{ else }}other{{ end }}"
title_default:
selector: td:nth-child(1) a
title_optional:
selector: td:nth-child(1) a[title]
attribute: title
optional: true
title_phase1:
text: "{{ if .Result.title_optional }}{{ .Result.title_optional }}{{ else }}{{ .Result.title_default }}{{ end }}"
filters:
- name: re_replace
args: ["(?i)\\b(FRENCH|MULTI|TRUEFRENCH|VOSTFR|SUBFRENCH)\\b(.+?)(\\b((19|20)\\d{2})\\b)$", "$3 $1$2"]
title_vostfr:
text: "{{ .Result.title_phase1 }}"
filters:
- name: re_replace
args: ["(?i)\\b(vostfr|subfrench)\\b", "ENGLISH"]
title_phase2:
text: "{{ if .Config.vostfr }}{{ .Result.title_vostfr }}{{ else }}{{ .Result.title_phase1 }}{{ end }}"
title_multilang:
text: "{{ .Result.title_phase2 }}"
filters:
- name: re_replace
args: ["(?i)\\b(MULTI(?!.*(?:FRENCH|ENGLISH|VOSTFR)))\\b", "{{ .Config.multilanguage }}"]
title:
text: "{{ if .Config.multilang }}{{ .Result.title_multilang }}{{ else }}{{ .Result.title_phase2 }}{{ end }}"
details:
selector: td:nth-child(1) a
attribute: href
download:
selector: td:nth-child(1) a
attribute: href
date:
selector: td:nth-child(2):contains("/")
optional: true
default: now
filters:
- name: dateparse
args: "dd/MM/yyyy"
size:
selector: "{{ if .Keywords }}td:nth-child(3){{ else }}td:nth-child(2){{ end }}"
filters:
- name: re_replace
args: ["(\\w)o", "$1B"]
seeders:
selector: "{{ if .Keywords }}td:nth-child(4){{ else }}td:nth-child(3){{ end }}"
optional: true
default: 0
leechers:
selector: "{{ if .Keywords }}td:nth-child(5){{ else }}td:nth-child(4){{ end }}"
optional: true
default: 0
downloadvolumefactor:
text: 0
uploadvolumefactor:
text: 1
# engine n/a
@@ -0,0 +1,36 @@
{ config, lib, ... }:
let
webPort = 8081;
btPort = 51413;
downloadsDir = "/var/lib/downloads";
in
{
# qBittorrent (headless). Web UI: http://<host>:8081
services.qbittorrent = {
enable = true;
openFirewall = true;
webuiPort = webPort;
# Prefer a stable port for NAT/firewall and for easier debugging.
torrentingPort = btPort;
};
users.groups.qbittorrent = { };
users.users.qbittorrent = {
isSystemUser = true;
group = "qbittorrent";
extraGroups = [ "media" ];
};
systemd.tmpfiles.settings."nix-server-downloads-dir" = {
"${downloadsDir}"."d" = {
mode = "2775";
user = "root";
group = "media";
};
};
# Some NixOS versions don't open UDP for torrenting even when openFirewall=true.
networking.firewall.allowedTCPPorts = [ webPort btPort ];
networking.firewall.allowedUDPPorts = [ btPort ];
}
@@ -0,0 +1,16 @@
{ ... }:
{
# Radarr (movie automation). UI: http://<host>:7878
services.radarr.enable = true;
# Keep permissions aligned with Jellyfin (/var/lib/media via group `media`).
users.groups.radarr = { };
users.users.radarr = {
isSystemUser = true;
group = "radarr";
extraGroups = [ "media" ];
};
networking.firewall.allowedTCPPorts = [ 7878 ];
}
@@ -0,0 +1,29 @@
{ ... }:
{
# Blank default Seerr branding assets (read-only store otherwise).
nixpkgs.overlays = [
(final: prev: {
seerr = prev.seerr.overrideAttrs (oldAttrs: {
postInstall =
(oldAttrs.postInstall or "")
+ ''
find "$out/share/public" -maxdepth 1 -type f \( -name 'logo_full.svg' -o -name 'logo_stacked.svg' \) \
-exec truncate -s 0 {} \;
'';
});
})
];
# "Seerr" request management. For Jellyfin, Jellyseerr is the right choice.
# UI: http://<host>:5055
services.seerr.enable = true;
users.groups.jellyseerr = { };
users.users.jellyseerr = {
isSystemUser = true;
group = "jellyseerr";
};
networking.firewall.allowedTCPPorts = [ 5055 ];
}
@@ -0,0 +1,16 @@
{ ... }:
{
# Sonarr (TV automation). UI: http://<host>:8989
services.sonarr.enable = true;
# Ensure Sonarr can manage the same libraries as Jellyfin.
users.groups.sonarr = { };
users.users.sonarr = {
isSystemUser = true;
group = "sonarr";
extraGroups = [ "media" ];
};
networking.firewall.allowedTCPPorts = [ 8989 ];
}
@@ -0,0 +1,113 @@
{ config, ... }:
let
secretFilePath = ../secrets.yaml;
in
{
sops.secrets."swiftshare/ghcr-token".sopsFile = secretFilePath;
sops.secrets."swiftshare/database-password".sopsFile = secretFilePath;
sops.secrets."swiftshare/oauth-discord-client-secret".sopsFile = secretFilePath;
sops.secrets."swiftshare/oauth-github-client-secret".sopsFile = secretFilePath;
sops.secrets."swiftshare/auth-secret".sopsFile = secretFilePath;
sops.secrets."swiftshare/oauth-google-client-id".sopsFile = secretFilePath;
sops.secrets."swiftshare/oauth-google-client-secret".sopsFile = secretFilePath;
sops.secrets."swiftshare/smtp-pass".sopsFile = secretFilePath;
sops.secrets."swiftshare/minio-access-key".sopsFile = secretFilePath;
sops.secrets."swiftshare/minio-secret-key".sopsFile = secretFilePath;
# Docker `--env-file` expects `KEY=value`. Separate snippets for DB/MinIO so only `swiftshare.env` hits the app container.
sops.templates."swiftshare-postgres.env" = {
content = ''
POSTGRES_PASSWORD=${config.sops.placeholder."swiftshare/database-password"}
'';
};
sops.templates."swiftshare-minio.env" = {
content = ''
MINIO_ROOT_USER=${config.sops.placeholder."swiftshare/minio-access-key"}
MINIO_ROOT_PASSWORD=${config.sops.placeholder."swiftshare/minio-secret-key"}
'';
};
sops.templates."swiftshare.env" = {
content = ''
DATABASE_URL=postgresql://swiftshare:${config.sops.placeholder."swiftshare/database-password"}@swiftshare-db:5432/swiftshare
AUTH_SECRET=${config.sops.placeholder."swiftshare/auth-secret"}
AUTH_DISCORD_SECRET=${config.sops.placeholder."swiftshare/oauth-discord-client-secret"}
AUTH_GITHUB_SECRET=${config.sops.placeholder."swiftshare/oauth-github-client-secret"}
AUTH_GOOGLE_SECRET=${config.sops.placeholder."swiftshare/oauth-google-client-secret"}
AUTH_GOOGLE_ID=${config.sops.placeholder."swiftshare/oauth-google-client-id"}
SMTP_PASS=${config.sops.placeholder."swiftshare/smtp-pass"}
STORAGE_ACCESS_KEY=${config.sops.placeholder."swiftshare/minio-access-key"}
STORAGE_SECRET_KEY=${config.sops.placeholder."swiftshare/minio-secret-key"}
'';
};
services.swiftshare = {
enable = true;
app = {
image = "ghcr.io/olivierchiasson/swiftshare:main";
ghcr = {
username = "olivierchiasson";
passwordFile = config.sops.secrets."swiftshare/ghcr-token".path;
};
origin = "https://swiftshare.cloud";
port = 3000;
uploadBodySizeLimit = "100mb";
disableTelemetry = true;
environmentFiles = [ config.sops.templates."swiftshare.env".path ];
};
database = {
user = "swiftshare";
#password = ""; # Defined in sops.templates."swiftshare-postgres.env"
name = "swiftshare";
environmentFiles = [ config.sops.templates."swiftshare-postgres.env".path ];
#exposePort.enable = true;
};
auth = {
#secret = "";
discord = {
clientId = "1400660345068191855";
#clientSecret = ""; # Defined in sops.templates."swiftshare.env"
};
# GitHub OAuth App (https://github.com/settings/developers) — replace placeholders.
github = {
clientId = "Ov23lifcVKR6B1iYDicU";
#clientSecret = ""; # Defined in sops.templates."swiftshare.env"
};
# Google Cloud OAuth 2.0 client — replace placeholders.
#google = {
# clientId = ""; # Defined in sops.templates."swiftshare.env"
# clientSecret = ""; # Defined in sops.templates."swiftshare.env"
#};
# SMTP for Better Auth email verification / password reset.
smtp = {
host = "smtp.purelymail.com";
port = 465;
secure = true;
user = "noreply@swiftshare.cloud";
#pass = ""; # Defined in sops.templates."swiftshare.env"
from = "noreply@swiftshare.cloud";
};
};
minio = {
#accessKey = ""; # Defined in sops.templates."swiftshare-minio.env"
#secretKey = ""; # Defined in sops.templates."swiftshare-minio.env"
bucketName = "swiftshare-assets";
environmentFiles = [ config.sops.templates."swiftshare-minio.env".path ];
};
umami = {
websiteId = "b4e1240d-a9d8-4075-b64d-0d3e0329cac8";
scriptUrl = "https://analytics.chiasson.cloud/script.js";
};
};
}
@@ -9,14 +9,25 @@
}:
{
imports = [
inputs.ddrm.nixosModules.default
self.nixosModules.nix-serverHardware
inputs.sops-nix.nixosModules.sops
self.nixosModules.system
self.nixosModules.users
./_services/attic-cache-server.nix
./_services/portainer.nix
./_services/organizr.nix
./_services/swiftshare.nix
./_services/immich.nix
./_services/jellyfin.nix
./_services/jellyfin-remote-storage.nix
./_services/ddrm-media-server.nix
./_services/sonarr.nix
./_services/prowlarr.nix
./_services/flaresolverr.nix
./_services/radarr.nix
./_services/qbittorrent.nix
./_services/seerr.nix
];
boot.loader.grub = {
+1 -1
View File
@@ -9,7 +9,7 @@
};
ideapad = {
hostName = "192.168.2.113";
hostName = "192.168.2.229";
aliases = [ "ideapad" ];
publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIQwaaI90xIMjZ46EcMyO8kBwGCxf7qVL75IYhw8Ssze ideapad";
};
+383
View File
@@ -0,0 +1,383 @@
diff --git a/yt_dlp/extractor/_extractors.py b/yt_dlp/extractor/_extractors.py
index 1a29a93ed..94434d5e7 100644
--- a/yt_dlp/extractor/_extractors.py
+++ b/yt_dlp/extractor/_extractors.py
@@ -2034,6 +2034,7 @@
TeleQuebecEmissionIE,
TeleQuebecIE,
TeleQuebecLiveIE,
+ TeleQuebecSeasonIE,
TeleQuebecSquatIE,
TeleQuebecVideoIE,
)
diff --git a/yt_dlp/extractor/telequebec.py b/yt_dlp/extractor/telequebec.py
index 7f5d5d29b..be927d6c5 100644
--- a/yt_dlp/extractor/telequebec.py
+++ b/yt_dlp/extractor/telequebec.py
@@ -1,7 +1,12 @@
+import json
+import re
+
from .common import InfoExtractor
from ..utils import (
+ ExtractorError,
int_or_none,
smuggle_url,
+ traverse_obj,
try_get,
unified_timestamp,
)
@@ -28,81 +33,174 @@ class TeleQuebecIE(TeleQuebecBaseIE):
)/(?P<id>\d+)
'''
_TESTS = [{
- # available till 01.01.2023
- 'url': 'http://zonevideo.telequebec.tv/media/37578/un-petit-choc-et-puis-repart/un-chef-a-la-cabane',
- 'info_dict': {
- 'id': '6155972771001',
- 'ext': 'mp4',
- 'title': 'Un petit choc et puis repart!',
- 'description': 'md5:b04a7e6b3f74e32d7b294cffe8658374',
- 'timestamp': 1589262469,
- 'uploader_id': '6150020952001',
- 'upload_date': '20200512',
- },
- 'add_ie': ['BrightcoveNew'],
+ 'url': 'https://zonevideo.telequebec.tv/media/1/exemple',
+ 'only_matching': True,
}, {
- 'url': 'https://zonevideo.telequebec.tv/media/55267/le-soleil/passe-partout',
+ 'url': 'https://coucou.telequebec.tv/videos/1004958/top-cornichon/l-anniversaire-de-top-cornichon',
'info_dict': {
- 'id': '6167180337001',
+ 'id': '6370144678112',
'ext': 'mp4',
- 'title': 'Le soleil',
- 'description': 'md5:64289c922a8de2abbe99c354daffde02',
+ 'title': 'L\'anniversaire de Top Cornichon',
+ 'description': 'md5:fc4fb2967dcea0baa8b6d39a11da917b',
'uploader_id': '6150020952001',
- 'upload_date': '20200625',
- 'timestamp': 1593090307,
+ 'duration': 360.107,
+ 'thumbnail': 'md5:027b0d8b371bc86d5ac9c024acfeb6f2',
+ 'timestamp': 1742217124,
+ 'upload_date': '20250317',
+ 'series': 'Top Cornichon',
+ 'episode': 'L\'anniversaire de Top Cornichon',
+ },
+ 'params': {
+ 'skip_download': True,
},
'add_ie': ['BrightcoveNew'],
- }, {
- # no description
- 'url': 'http://zonevideo.telequebec.tv/media/30261',
- 'only_matching': True,
- }, {
- 'url': 'https://coucou.telequebec.tv/videos/41788/idee-de-genie/l-heure-du-bain',
- 'only_matching': True,
}]
def _real_extract(self, url):
media_id = self._match_id(url)
- media = self._download_json(
+ meta = self._download_json(
'https://mnmedias.api.telequebec.tv/api/v3/media/' + media_id,
- media_id)['media']
- source_id = next(source_info['sourceId'] for source_info in media['streamInfos'] if source_info.get('source') == 'Brightcove')
- info = self._brightcove_result(source_id, '22gPKdt7f')
- product = media.get('product') or {}
- season = product.get('season') or {}
+ media_id, fatal=False)
+ media = meta.get('media') if meta else None
+ stream_infos = try_get(media, lambda m: m['streamInfos']) or []
+
+ if media and any(si and si.get('source') == 'Brightcove' for si in stream_infos):
+ source_id = next(
+ si['sourceId'] for si in stream_infos
+ if si and si.get('source') == 'Brightcove')
+ info = self._brightcove_result(source_id, '22gPKdt7f')
+ product = media.get('product') or {}
+ season = product.get('season') or {}
+ info.update({
+ 'description': try_get(media, lambda x: x['descriptions'][-1]['text'], str),
+ 'series': try_get(season, lambda x: x['serie']['titre']),
+ 'season': season.get('name'),
+ 'season_number': int_or_none(season.get('seasonNo')),
+ 'episode': product.get('titre'),
+ 'episode_number': int_or_none(product.get('episodeNo')),
+ })
+ return info
+
+ # Coucou Next.js: Brightcove id is in __NEXT_DATA__ when mnmedias has no catalogue row.
+ webpage = self._download_webpage(url, media_id)
+ next_json = self._search_regex(
+ r'<script id="__NEXT_DATA__" type="application/json">(?P<json>[\s\S]+?)</script>',
+ webpage, '__NEXT_DATA__')
+ next_data = self._parse_json(next_json, media_id)
+ media_obj = traverse_obj(next_data, ('props', 'pageProps', 'media')) or {}
+ bc_video_id = media_obj.get('mediaId')
+ if not bc_video_id:
+ raise ExtractorError('Unable to extract Brightcove video id')
+
+ info = self._brightcove_result(str(bc_video_id), '22gPKdt7f')
+ hero = traverse_obj(next_data, ('props', 'pageProps', 'hero')) or {}
info.update({
- 'description': try_get(media, lambda x: x['descriptions'][-1]['text'], str),
- 'series': try_get(season, lambda x: x['serie']['titre']),
- 'season': season.get('name'),
- 'season_number': int_or_none(season.get('seasonNo')),
- 'episode': product.get('titre'),
- 'episode_number': int_or_none(product.get('episodeNo')),
+ 'description': media_obj.get('description'),
+ 'episode': media_obj.get('titre'),
+ 'series': hero.get('nom'),
})
return info
-class TeleQuebecSquatIE(InfoExtractor):
- _VALID_URL = r'https?://squat\.telequebec\.tv/videos/(?P<id>\d+)'
+class TeleQuebecSeasonIE(InfoExtractor):
+ """telequebec.tv/contenu/{slug}/saison/{n} — expands to episode /regarder/ URLs via GraphQL."""
+
+ _VALID_URL = r'https?://(?:www\.)?telequebec\.tv/contenu/(?P<slug>[^/?#]+)/saison/(?P<season>\d+)'
_TESTS = [{
- 'url': 'https://squat.telequebec.tv/videos/9314',
+ 'url': 'https://telequebec.tv/contenu/macaroni-tout-garni/saison/1',
+ 'playlist_mincount': 15,
'info_dict': {
- 'id': 'd59ae78112d542e793d83cc9d3a5b530',
- 'ext': 'mp4',
- 'title': 'Poupeflekta',
- 'description': 'md5:2f0718f8d2f8fece1646ee25fb7bce75',
- 'duration': 1351,
- 'timestamp': 1569057600,
- 'upload_date': '20190921',
- 'series': 'Miraculous : Les Aventures de Ladybug et Chat Noir',
- 'season': 'Saison 3',
- 'season_number': 3,
- 'episode_number': 57,
+ 'id': 'macaroni-tout-garni-s1',
+ 'title': 'Macaroni tout garni',
},
'params': {
'skip_download': True,
},
+ }, {
+ 'url': 'https://www.telequebec.tv/contenu/macaroni-tout-garni/saison/1/',
+ 'only_matching': True,
}]
+ def _real_extract(self, url):
+ mobj = self._match_valid_url(url)
+ slug, season = mobj.group('slug'), int(mobj.group('season'))
+ playlist_id = f'{slug}-s{season}'
+
+ query = '''query ($slug: String!, $season: Int!) {
+ productPage(rootProductSlug: $slug, seasonNumber: $season) {
+ ... on ArtisanPage {
+ blocks {
+ ... on ArtisanBlocksProductPlayableProductsStrip {
+ blockConfiguration {
+ rootProduct { title }
+ season { title }
+ products { title videoCanonicalUrl }
+ }
+ }
+ }
+ }
+ }
+ }'''
+
+ resp = self._download_json(
+ 'https://api.pc-cms.tele.quebec/graphql', playlist_id,
+ data=json.dumps({
+ 'query': query,
+ 'variables': {'slug': slug, 'season': season},
+ }, separators=(',', ':')).encode(),
+ headers={
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json',
+ 'Origin': 'https://telequebec.tv',
+ })
+
+ errs = traverse_obj(resp, ('errors', ..., 'message', {str}))
+ if errs:
+ raise ExtractorError(', '.join(errs), expected=True)
+
+ playlist_title = None
+ products = []
+ for block in traverse_obj(resp, ('data', 'productPage', 'blocks')) or []:
+ cfg = traverse_obj(block, 'blockConfiguration')
+ if not cfg:
+ continue
+ prods = cfg.get('products')
+ if not prods:
+ continue
+ products = prods
+ playlist_title = traverse_obj(cfg, ('season', 'title')) or traverse_obj(cfg, ('rootProduct', 'title'))
+ break
+
+ if not products:
+ raise ExtractorError('No playable episodes in this season', expected=True)
+
+ def _episode_sort_key(p):
+ vu = p.get('videoCanonicalUrl') or ''
+ m = re.search(r'/(\d+)/?(?:[?#]|$)', vu)
+ return int(m.group(1)) if m else 0
+
+ products = sorted(products, key=_episode_sort_key)
+
+ entries = [
+ self.url_result(
+ p['videoCanonicalUrl'],
+ ie=TeleQuebecEmissionIE.ie_key(),
+ video_title=p.get('title'))
+ for p in products
+ if p.get('videoCanonicalUrl')
+ ]
+
+ return self.playlist_result(
+ entries,
+ playlist_id=playlist_id,
+ playlist_title=playlist_title or slug)
+
+
+class TeleQuebecSquatIE(InfoExtractor):
+ _WORKING = False
+ _VALID_URL = r'https?://squat\.telequebec\.tv/videos/(?P<id>\d+)'
+ _TESTS = []
+
def _real_extract(self, url):
video_id = self._match_id(url)
@@ -136,25 +234,47 @@ class TeleQuebecEmissionIE(InfoExtractor):
)
(?P<id>[^?#&]+)
'''
+
+ @classmethod
+ def suitable(cls, url):
+ if re.match(r'https?://(?:www\.)?telequebec\.tv/contenu/[^/?#]+/saison/\d+', url):
+ return False
+ return super().suitable(url)
+
_TESTS = [{
- 'url': 'http://lindicemcsween.telequebec.tv/emissions/100430013/des-soins-esthetiques-a-377-d-interets-annuels-ca-vous-tente',
+ 'url': 'https://telequebec.tv/regarder/donner-lgout/2/6',
'info_dict': {
- 'id': '6154476028001',
+ 'id': 'ref:100832979',
'ext': 'mp4',
- 'title': 'Des soins esthétiques à 377 % dintérêts annuels, ça vous tente?',
- 'description': 'md5:cb4d378e073fae6cce1f87c00f84ae9f',
- 'upload_date': '20200505',
- 'timestamp': 1588713424,
+ 'title': 'La Grèce à Québec',
+ 'description': 'md5:c506e07b90426ad391e18a753c021516',
'uploader_id': '6150020952001',
+ 'duration': 2695.083,
+ 'thumbnail': 'md5:17aead23a395fb3f56a376524eb9f23c',
+ 'timestamp': 1777782475,
+ 'upload_date': '20260503',
},
+ 'params': {
+ 'skip_download': True,
+ },
+ 'add_ie': ['BrightcoveNew'],
+ }, {
+ 'url': 'https://telequebec.tv/regarder/donner-lgout/2/1',
+ 'only_matching': True,
+ }, {
+ 'url': 'https://telequebec.tv/regarder/kamikazes/2/7',
+ 'only_matching': True,
}, {
- 'url': 'http://bancpublic.telequebec.tv/emissions/emission-49/31986/jeunes-meres-sous-pression',
+ 'url': 'https://telequebec.tv/regarder/5050',
'only_matching': True,
}, {
- 'url': 'http://www.telequebec.tv/masha-et-michka/epi059masha-et-michka-3-053-078',
+ 'url': 'https://telequebec.tv/regarder/les-magnetiques',
'only_matching': True,
}, {
- 'url': 'http://www.telequebec.tv/documentaire/bebes-sur-mesure/',
+ 'url': 'https://telequebec.tv/regarder/les-dalton/2/107',
+ 'only_matching': True,
+ }, {
+ 'url': 'https://telequebec.tv/regarder/macaroni-tout-garni/1/1',
'only_matching': True,
}]
@@ -164,31 +284,33 @@ def _real_extract(self, url):
webpage = self._download_webpage(url, display_id)
media_id = self._search_regex(
- r'mediaId\s*:\s*(?P<id>\d+)', webpage, 'media id')
+ (
+ r'mediaId\s*:\s*(?P<id>\d+)',
+ r'"mediaId"\s*,\s*"(?P<id>\d+)"',
+ r'"mediaId"\s*,\s*(?P<id>\d+)',
+ r'mediaId\\"\s*,\s*\\"(?P<id>\d+)',
+ ),
+ webpage, 'media id')
+
+ meta = self._download_json(
+ 'https://mnmedias.api.telequebec.tv/api/v3/media/' + media_id,
+ media_id, fatal=False)
+ media = meta.get('media') if meta else None
+ stream_infos = try_get(media, lambda m: m['streamInfos']) or []
+ if media and any(si and si.get('source') == 'Brightcove' for si in stream_infos):
+ return self.url_result(
+ 'http://zonevideo.telequebec.tv/media/' + media_id,
+ TeleQuebecIE.ie_key())
- return self.url_result(
- 'http://zonevideo.telequebec.tv/media/' + media_id,
- TeleQuebecIE.ie_key())
+ # New telequebec.tv stack no longer mirrors catalogue ids into mnmedias; Brightcove loads
+ # videoId=ref:{mediaId} (see web player bundle: videoId:`ref:${mediaId}`).
+ return TeleQuebecBaseIE._brightcove_result(f'ref:{media_id}', 'ja7RtbSne')
class TeleQuebecLiveIE(TeleQuebecBaseIE):
+ _WORKING = False
_VALID_URL = r'https?://zonevideo\.telequebec\.tv/(?P<id>endirect)'
- _TEST = {
- 'url': 'http://zonevideo.telequebec.tv/endirect/',
- 'info_dict': {
- 'id': '6159095684001',
- 'ext': 'mp4',
- 'title': 're:^Télé-Québec [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
- 'is_live': True,
- 'description': 'Canal principal de Télé-Québec',
- 'uploader_id': '6150020952001',
- 'timestamp': 1590439901,
- 'upload_date': '20200525',
- },
- 'params': {
- 'skip_download': True,
- },
- }
+ _TESTS = []
def _real_extract(self, url):
return self._brightcove_result('6159095684001', 'skCsmi2Uw')
@@ -198,15 +320,7 @@ class TeleQuebecVideoIE(TeleQuebecBaseIE):
_VALID_URL = r'https?://video\.telequebec\.tv/player(?:-live)?/(?P<id>\d+)'
_TESTS = [{
'url': 'https://video.telequebec.tv/player/31110/stream',
- 'info_dict': {
- 'id': '6202570652001',
- 'ext': 'mp4',
- 'title': 'Le coût du véhicule le plus vendu au Canada / Tous les frais liés à la procréation assistée',
- 'description': 'md5:685a7e4c450ba777c60adb6e71e41526',
- 'upload_date': '20201019',
- 'timestamp': 1603115930,
- 'uploader_id': '6101674910001',
- },
+ 'only_matching': True,
}, {
'url': 'https://video.telequebec.tv/player-live/28527',
'only_matching': True,
+120
View File
@@ -0,0 +1,120 @@
# Pull everywhere; enable push only on the host that runs `nixos-rebuild --target-host` (needs sops `cachix/auth-token`).
{ inputs, ... }: {
flake.nixosModules.systemCachingCachix =
{ config, lib, pkgs, ... }:
let
cfg = config.chiasson.system.caching.cachix;
in
{
imports = [ inputs.sops-nix.nixosModules.sops ];
options.chiasson.system.caching.cachix = with lib; {
enable = mkOption {
type = types.bool;
default = false;
description = "Enable Cachix cache integration for this host.";
};
cacheName = mkOption {
type = types.str;
default = "";
example = "chiasson-nixosnew";
description = "Your Cachix cache name (from app.cachix.org). Leave empty to disable.";
};
publicKey = mkOption {
type = types.str;
default = "";
example = "chiasson-nixosnew.cachix.org-1:xxxx=";
description = "Public key for the cache (shown on the cache page).";
};
enablePush = mkOption {
type = types.bool;
default = false;
description = "Push every build to Cachix (enable only on the machine that runs nixos-rebuild --target-host).";
};
pushExcludePatterns = mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "wallpaper" "hm_wallpapers" ".iso" "-source" "large-assets" ];
description = "Substring patterns: store paths containing any of these are not pushed to Cachix (saves cache space for heavy outputs).";
};
};
config = let
enabled = cfg.enable && cfg.cacheName != "" && cfg.publicKey != "";
cacheUrl = "https://${cfg.cacheName}.cachix.org";
in
lib.mkMerge [
(lib.mkIf enabled {
nix.settings = {
substituters = lib.mkAfter [ cacheUrl ];
trusted-public-keys = lib.mkAfter [ cfg.publicKey ];
};
})
(lib.mkIf (enabled && cfg.enablePush) {
nix.settings.post-build-hook = pkgs.writeShellScript "upload-to-cachix" ''
set -eu
set -f
token_path="${config.sops.secrets."cachix/auth-token".path}"
if [ ! -r "$token_path" ]; then
echo "cachix: skipping push (token not readable at $token_path)" >&2
exit 0
fi
export PATH="${lib.makeBinPath [ pkgs.cachix ]}:$PATH"
export CACHIX_AUTH_TOKEN=$(cat "$token_path")
push_paths=""
skipped_roots=0
pushed_roots=0
for path in $OUT_PATHS; do
skip=0
skip_reason=""
closure_paths="$(nix-store -qR "$path" 2>/dev/null || true)"
for candidate in "$path" $closure_paths; do
while IFS= read -r pat; do
pat="$(printf '%s' "$pat" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
[ -z "$pat" ] && continue
case "$candidate" in
*"$pat"*)
skip=1
skip_reason="$pat ($candidate)"
break
;;
esac
done << 'EXCLUDE_PATTERNS'
${lib.concatStringsSep "\n" cfg.pushExcludePatterns}
EXCLUDE_PATTERNS
if [ "$skip" -eq 1 ]; then
break
fi
done
if [ "$skip" -eq 0 ]; then
push_paths="$push_paths $path"
pushed_roots=$((pushed_roots + 1))
else
skipped_roots=$((skipped_roots + 1))
echo "cachix: skipping root $path (matches exclude pattern via $skip_reason)" >&2
fi
done
echo "cachix: root paths selected for push: $pushed_roots, skipped: $skipped_roots" >&2
if [ -n "$push_paths" ]; then
if ! cachix push ${lib.escapeShellArg cfg.cacheName} $push_paths; then
echo "cachix: push failed (build succeeded; check token/network)" >&2
fi
fi
exit 0
'';
sops.secrets."cachix/auth-token" = {
owner = "root";
group = "root";
mode = "0400";
};
})
];
};
}
+9
View File
@@ -0,0 +1,9 @@
# `system.caching.*` — Attic + Cachix; exported again as `nixosModules.systemCaching`.
{ self, ... }: {
flake.nixosModules.systemCaching = {
imports = [
self.nixosModules.systemCachingAttic
self.nixosModules.systemCachingCachix
];
};
}
+1 -1
View File
@@ -13,6 +13,7 @@
self.nixosModules.systemFlatpak
self.nixosModules.systemAudio
self.nixosModules.systemIdeapadMrubyOverlay
self.nixosModules.systemYtDlpTelequebecPatch
self.nixosModules.systemGaming
self.nixosModules.systemUconsoleKernelBuilder
self.nixosModules.systemLibrepods
@@ -20,7 +21,6 @@
self.nixosModules.systemCaching
inputs.swiftshare.nixosModules.systemServiceSwiftshare
self.nixosModules.systemServiceImmich
self.nixosModules.systemVM
];
};
}
+4 -1
View File
@@ -11,11 +11,14 @@
extraGroups = [
"networkmanager"
"wheel"
"libvirtd"
"docker"
"fuse"
"uinput"
"kvm"
# `video` is required for the brightnessctl/light udev rules to grant write access
# to /sys/class/backlight/*/brightness without sudo. Harmless on hosts without a
# backlight (servers, desktop towers): the group simply has no devices to own.
"video"
];
# Host must set `sops.secrets."users/olivier/hashedPassword".neededForUsers = true`.
@@ -0,0 +1,35 @@
# Tele-Québec extractor fixes until upstream yt-dlp carries them.
#
# Regenerate `modules/patches/yt-dlp-telequebec.patch` from yt-dlp checkout
# (branch with fixes vs master), redirecting into this flake's modules/patches/:
#
# git diff master..ie/telequebec-update -- \
# yt_dlp/extractor/telequebec.py yt_dlp/extractor/_extractors.py \
# > path/to/NixOS-V2/modules/patches/yt-dlp-telequebec.patch
#
{ ... }: {
flake.nixosModules.systemYtDlpTelequebecPatch =
{ config, lib, ... }:
let
cfg = config.chiasson.system.ytDlpTelequebecPatch;
patchFile = ../patches/yt-dlp-telequebec.patch;
in
{
options.chiasson.system.ytDlpTelequebecPatch = {
enable = lib.mkEnableOption ''
Patch yt-dlp with Tele-Québec extractor updates (telequebec.tv, Coucou, season playlists).
Regenerate `modules/patches/yt-dlp-telequebec.patch` when nixpkgs bumps yt-dlp if the build fails.
'';
};
config = lib.mkIf cfg.enable {
nixpkgs.overlays = lib.mkOrder 1000 [
(final: prev: {
yt-dlp = prev.yt-dlp.overrideAttrs (old: {
patches = (old.patches or [ ]) ++ [ patchFile ];
});
})
];
};
};
}
+44
View File
@@ -0,0 +1,44 @@
# HM side of the flake; option tree is `chiasson.home.*` (docs/module-conventions.md).
{ self, inputs, ... }: {
imports = [
./apps/discord.nix
./apps/localsend.nix
./apps/pokeclicker
./apps/spotify.nix
./browsers/orion.nix
./desktop/screenshot.nix
./hardware/uconsole-gamepad.nix
];
# Root module: chiasson.home.enable + bash. Everything else is separate `wisdom*` exports —
# pull those into `home.users.<name>.extraModules` on each host as needed.
flake.homeManagerModules.wisdom =
{ config, lib, ... }:
let
cfg = config.chiasson.home;
in
{
imports = [
self.homeManagerModules.wisdomShellBash
];
options.chiasson.home = {
enable = lib.mkEnableOption ''
HM profile root for this flake (bash on by default). Wire other `wisdom*` modules in
`home.users.<name>.extraModules` and flip their `chiasson.home.*.enable` options on the host.
'' // {
default = true;
};
extraPackages = lib.mkOption {
type = lib.types.listOf lib.types.package;
default = [ ];
description = ''
Extra `home.packages` (e.g. `pkgs.parsec-bin`) when you do not want a separate wisdom module.
'';
};
};
config = lib.mkIf cfg.enable { home.packages = cfg.extraPackages; };
};
}
+17
View File
@@ -0,0 +1,17 @@
{ ... }: {
flake.homeManagerModules.wisdomEditorsKate =
{ config, lib, pkgs, ... }:
let
root = config.chiasson.home;
cfg = config.chiasson.home.editors.kate;
in
{
options.chiasson.home.editors.kate.enable = lib.mkEnableOption "Kate.";
config = lib.mkIf (root.enable && cfg.enable) {
home.packages = lib.optional (
lib.meta.availableOn pkgs.stdenv.hostPlatform pkgs.kdePackages.kate
) pkgs.kdePackages.kate;
};
};
}
+19
View File
@@ -0,0 +1,19 @@
{ ... }: {
flake.homeManagerModules.wisdomEditorsObsidian =
{ config, lib, pkgs, ... }:
let
root = config.chiasson.home;
cfg = config.chiasson.home.editors.obsidian;
in
{
options.chiasson.home.editors.obsidian.enable = lib.mkEnableOption ''
Obsidian (unfree); skipped if unavailable here.
'';
config = lib.mkIf (root.enable && cfg.enable) {
home.packages = lib.optional (
lib.meta.availableOn pkgs.stdenv.hostPlatform pkgs.obsidian
) pkgs.obsidian;
};
};
}
+37
View File
@@ -0,0 +1,37 @@
{ ... }: {
flake.homeManagerModules.wisdomShellBash =
{ config, lib, ... }:
let
root = config.chiasson.home;
cfg = config.chiasson.home.shell.bash;
in
{
options.chiasson.home.shell.bash.enable = lib.mkEnableOption "Bash + rbw SSH socket env in profile/init." // {
default = true;
};
config = lib.mkIf (root.enable && cfg.enable) {
programs.bash = {
enable = true;
profileExtra = ''
if [ -z "''${SSH_AUTH_SOCK:-}" ]; then
if [ -n "''${XDG_RUNTIME_DIR:-}" ]; then
export SSH_AUTH_SOCK="$XDG_RUNTIME_DIR/rbw/ssh-agent-socket"
else
export SSH_AUTH_SOCK="/run/user/$(id -u)/rbw/ssh-agent-socket"
fi
fi
'';
initExtra = ''
if [ -z "''${SSH_AUTH_SOCK:-}" ]; then
if [ -n "''${XDG_RUNTIME_DIR:-}" ]; then
export SSH_AUTH_SOCK="$XDG_RUNTIME_DIR/rbw/ssh-agent-socket"
else
export SSH_AUTH_SOCK="/run/user/$(id -u)/rbw/ssh-agent-socket"
fi
fi
'';
};
};
};
}
+37
View File
@@ -0,0 +1,37 @@
{ ... }: {
flake.homeManagerModules.wisdomShellFish =
{ config, lib, pkgs, ... }:
let
root = config.chiasson.home;
cfg = config.chiasson.home.shell.fish;
in
{
options.chiasson.home.shell.fish.enable = lib.mkEnableOption "Fish + grc, quiet greeting, rbw SSH socket defaults.";
config = lib.mkIf (root.enable && cfg.enable) {
# `fishPlugins.grc` only installs the plugin; the wrapper invokes the `grc` binary.
home.packages = [ pkgs.grc ];
programs.fish = {
enable = true;
interactiveShellInit = ''
set fish_greeting ""
if test -z "$SSH_AUTH_SOCK"
if test -n "$XDG_RUNTIME_DIR"
set -gx SSH_AUTH_SOCK "$XDG_RUNTIME_DIR/rbw/ssh-agent-socket"
else
set -l _hm_uid (${pkgs.coreutils}/bin/id -u)
set -gx SSH_AUTH_SOCK "/run/user/$_hm_uid/rbw/ssh-agent-socket"
end
end
'';
plugins = [
{
name = "grc";
src = pkgs.fishPlugins.grc.src;
}
];
};
};
};
}
+41
View File
@@ -0,0 +1,41 @@
{ ... }: {
flake.homeManagerModules.wisdomShellOhMyPosh =
{ config, lib, ... }:
let
root = config.chiasson.home;
cfg = config.chiasson.home.shell.ohMyPosh;
in
{
options.chiasson.home.shell.ohMyPosh = {
enable = lib.mkEnableOption "[Oh My Posh](https://ohmyposh.dev/) prompt.";
builtinTheme = lib.mkOption {
type = lib.types.str;
default = "jandedobbeleer";
description = ''
Stock theme when DMS is not overriding `configFile` (DMS replaces this with matugen output).
'';
};
enableFishIntegration = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Run `oh-my-posh init fish` in Fish `shellInit` (Home Manager).";
};
enableBashIntegration = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Run `oh-my-posh init bash` in bash `initExtra`.";
};
};
config = lib.mkIf (root.enable && cfg.enable) {
programs.oh-my-posh = {
enable = true;
inherit (cfg) enableFishIntegration enableBashIntegration;
useTheme = lib.mkDefault cfg.builtinTheme;
};
};
};
}
+59
View File
@@ -0,0 +1,59 @@
{ ... }: {
# Optional fragment: import from `home.users.<name>.extraModules` only on hosts that need Yazi so this
# module (and its `pkgs.yazi` wiring) is not evaluated when omitted.
flake.homeManagerModules.wisdomShellYazi =
{ config, lib, pkgs, ... }:
let
root = config.chiasson.home;
cfg = config.chiasson.home.shell.yazi;
in
{
options.chiasson.home.shell.yazi.enable = lib.mkEnableOption "Yazi as `y` + 7zz with rar.";
config = lib.mkIf (root.enable && cfg.enable) {
programs.yazi = {
enable = true;
package = pkgs.yazi.override {
_7zz = pkgs._7zz-rar;
};
shellWrapperName = "y";
settings = {
manager = {
ratio = [
1
4
3
];
sort_by = "natural";
sort_sensitive = true;
sort_reverse = false;
sort_dir_first = true;
linemode = "none";
show_hidden = true;
show_symlink = true;
};
preview = {
image_filter = "lanczos3";
image_quality = 90;
tab_size = 1;
max_width = 600;
max_height = 900;
cache_dir = "";
ueberzug_scale = 1;
ueberzug_offset = [
0
0
0
0
];
};
tasks = {
micro_workers = 5;
macro_workers = 10;
bizarre_retry = 5;
};
};
};
};
};
}
+31
View File
@@ -0,0 +1,31 @@
{ ... }: {
flake.homeManagerModules.wisdomTerminalsKitty =
{ config, lib, ... }:
let
root = config.chiasson.home;
cfg = config.chiasson.home.terminals.kitty;
in
{
options.chiasson.home.terminals.kitty.enable = lib.mkEnableOption ''
Kitty + DMS includes; activation strips `*-theme.auto.conf` so matugen colors stick.
'';
config = lib.mkIf (root.enable && cfg.enable) {
programs.kitty = {
enable = true;
extraConfig = ''
include dank-tabs.conf
include dank-theme.conf
allow_remote_control yes
listen_on unix:@kitty
'';
};
home.activation.kittyRemoveAutoThemes = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
rm -f "$HOME/.config/kitty/dark-theme.auto.conf" \
"$HOME/.config/kitty/light-theme.auto.conf" \
"$HOME/.config/kitty/no-preference-theme.auto.conf"
'';
};
};
}
+31 -31
View File
@@ -5,56 +5,56 @@ sops:
- recipient: age1yyzgmazjxkvwtfcv9re3lqmt2ru5dcrfu3sauysm0wzfwzvyap8qkjkq32
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBuWEtXVW8ydkNhQTZSdkdE
MUswOHZNcGE5YTFrZkY5YmpCRFM0QkYxUGpFCml6MWJyakxjRzhlR2VHRGVlU3lB
U2lNMjZsWW1hZ1o1WFZqekl4a3crVU0KLS0tIEtFMkFrWVBvY2hZVGNwYnY4R2Jn
dWlSOWhySWZydFQ0eWFseUlyOGRhYjQKRDM5k5q4AmyuPQ0EPzt5yrTmcNhW0umW
dxYNO68NoHtFfK9efokxPKUnv9XqdbujTt/AWrtbxiyH4iOwt5N6fg==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBZbmNOZm45NFYvdCtsRlpr
Sk05ZENGUjQ2UHhkZnFxUFZyd2VNR0twZ3c4CmEwSFRMOUZiNVFRTEhSUEFFK1BO
VDdINkZRSmdFWmVQRzVSRVQ3cENFZ2MKLS0tIDZTSCtmQzZFdzNwbytJNmVsWmVn
RmFQakVYaHd0TDVSd0lsOGJqOVYremMKHUINhxbHI0lxG5kHoCH1bZSm5Gkr4qRp
uPazF1+7kxrfMEBiZixq3K94HKzo+KYJQrcmD2EpS/p2cGN9oNvKHQ==
-----END AGE ENCRYPTED FILE-----
- recipient: age1elk6zwmcylwfk7gd4pjda7g29upftjvxys8py42s8d42jklnyv7s7dm9z2
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBha3BFaFJWTytOTlpTNXVV
UXE1eENkZFRkQytzTVNLVkYreG5KNktrMVJRCk1iYnRQYW5IZFE1blB0T0JkUEhS
YkRISDJBR1REY1cwVkNhSGtPbmMzcjAKLS0tIEJEamFvQjRvck1pMXJFNkxSVEVT
WUd0b0Z5RXNCcVdKL09Ic2liMmtvMkkKtWcKmBSCOTqfaa52OxoHM3jA039l5o42
Quvwzq0mc78/lyJbRAGCEBgvUvUnHVGlRf1NJOYyTb/zRkv/HGplOw==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBlVWRvMm5BQWFYRlYzbjU0
dHBlaHZCWnR5UXJSZDFiOWRjS2w0L0V4TVFNCkpYVk5CQ0RwNjlmckV6amNia1pl
MFp5blAybncxazNneTYxRTdqNEdBaEEKLS0tIGtpVjQxSWMzaDVPS1dkZnVJMFFi
aWNRZTBSQllPSkQ2ZjYyOHpNcTEzWjgKK6XZaHV8GhLAr10VfvdP3+wc+ngBvLnv
PpdC4mPcHeueqv6fgLyUe0eUTulQ+oTXP51HG1/yClM/VGurA+Oxkg==
-----END AGE ENCRYPTED FILE-----
- recipient: age193gw802ytal7h5p5q37kpd9079k2vsflzmnvupcwfxh2kjdrwqtsk3g6rm
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB5U3NscUtHRzJmV2crQ3da
UmpDOGMybER3M2p4Y291SjBPT0pRZnpUMHlFClFhRkgzbEFJaXVaRnFJUHhXdWVw
QS9lMHVKeHFuZlRGNnZwUDI0WDkxdzgKLS0tIDYxMzBWcjNlbUZURGQ4VjlJek1Z
Zk5zUlA2cEpSb1dwbDdlYVFFcXpvK2sKbOgfT2ocpG/WcBIAfoJ1w6OhRQh43bw6
QaQZ3H+WykP07HNKjXw+ZH38LzdfPidDUaxa85Pcomu53E4v1cbF6w==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwMTBsYjB3K2taN3BSL3Zx
VDhnVlhnKzJXeUZhT1ArZG8xZGFSQmlyczNjCkhQck92Q3R6UVR2WDlhREFRcVJB
UXRDTGkyV3VNenp5Z2c1ZGh2dmdwZDAKLS0tIGlUZ3M2VFptSm1qTGlhc3NUTFEv
ckdsQTkxbzk2WDNRTGhWUS9YeTM1encK0+SFVSrpC3YmeAAlnmD1c3eYUXVOt5/I
VjYtU9IBcoUzQXM9o+wl3FyowOR/Q4Ws+I0sWtYuXigHHnDubL3Ylw==
-----END AGE ENCRYPTED FILE-----
- recipient: age1yr7vurfxc3w8ewfw9djfm54atw6ayze69qglamecuft5q0n9gu2sadsa2m
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBZZHpENjduTTJxTWNIQ1NH
Z3h5RjFrVHVZbnZqcVc1QjBpSEh3bkNpdW1zCi91NkpkR1RnSjJucWxZM3Zpc1Jo
Q0cyV0pVU0M3UjBqNWFFRDdMaDRUWjQKLS0tIDN4MjVmeFNXV3FjdVdiZXNGNWxM
TzdPUlE4R3RDcnJzTmFTSG9BVFZBL00Kr1PcmgTjo7Du66eocglrOtwpPIQ/itp4
EWMCz7J7rRUf0690bb39VU72L7sj15wBhYbyWK80IJjtUl+4w+EyQw==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBiNk0zK0ZVVTNBR0p2aFNn
WkgyVkNzV1pPOFlSUll5cXduVVJ4eFR3cmwwCkxOZUNxQjU2OEtlYUlGR0tXZ2lv
UkxXcnJPNkNxMVd6TFpvRmFFWFJ2a3MKLS0tIGt5TkgxeFQwTlZEYmN5aUk0Zk5C
ZTlkOVEwd3NSVDNtY2owQWRjRU5KdkkKC9AxSP/e5/St79taNHVTmgWx8fGpsntm
4mdRILdGGnkwTpPyipSxwX1X71O5kBs45cyoQ0brysP0O7b2aXOvdA==
-----END AGE ENCRYPTED FILE-----
- recipient: age1m30m9xzszmcawte35m0yymz42gfx3x84w7d5l67mtdtajhgpfgssuc2plm
- recipient: age1hya7pgpe8zal52w3pjf036tpapmehedatfm4r84h30t4wuh079ssedfd37
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBzY250TU53Y2hjYTk3eExX
Vk5EZ2lJSDI2NzZaTVNWU1hIZVdieGNwc1FJCk5PaVlDMytNbm0zdlhyaWdoVkJ5
T0Y4dVIyeFRvNktqMDcvZ0h3bDN4bzQKLS0tIEtMR3p5azBlTEV6LytvU3JqLzd4
em14dlFUUmY4aXRoK3A3Y2kyd3V4ZkUKu2XT/sL/syXYXC1B0Yah07QniAVvCik7
j1Pnn3buQ8ZwP9fSRuE9uYQzqW89CmZanq2X9P32Pdw3JC9NfvaILA==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA5disxY3F2U1dmUENIbjhI
bnRkTElIbjE2ZlRZZTBPWWpjQXEyVUEvTlhBCklXMDNVT0tYUysybndqaHlqbDAz
akpLdE9CVGhOZ2cxQmsyNklWY3grSDgKLS0tIEE4TWMyZHdqR2w1ZWpnTUpuS25Z
RjVRM051U2pvSU1id2x5VjFzQ1dkd0UKhGU97Sqf1PG2/ZLz7jqFTsLTq5cx0h/7
jdWXSRUmmv2Cz3G3CxmF/lOi+NYRPTkx7DRAmoX3S7flc68onlSgcQ==
-----END AGE ENCRYPTED FILE-----
- recipient: age1p05z980kdtngk9mw67hfev72h7xhslplpxfk9yskgmf0hl4lu3ls04zht9
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBKaDRHNVdQM0Q5SFIwVTMr
SzJmQkVsLzZ1dkNMYnU0WjlOU1dsem5zdVZFClg4QllIZGl0a3ZmMVBKc2pobFFm
SVFaRU52T2FUWXQ4UFdDWDNUZTdGMTAKLS0tIFY0OXlDaTJOOEsrVm1TUkFQMENW
MThLYTVYWER6Wm8yRlh0c0cwU2lDQjgKP8TdNo46f6rnYXjx+kBHwahv6UIlHgNq
+aMTovb79YrY937wSk2Agtt0KlHnlsgvxsmWOpeAoboN+4FBalTySg==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA3cjE2dVpSOUZYRk5WTFFi
TDliOEJsNEdTUzFGck40K1NtdlQ0YjJ1Vng4CjVTeFU0ZDN2ZmVIT3FGZU1ia1Aw
RmNTOTN6WDVMR25uSzRRbnFzeDdTYmMKLS0tIGtla2owUW9wTTFaTERTaUMxb0k0
bjlyQ3BDOVRkMFROdk1veEgyYVZuK3cKAbhQPk1T0o6bGYBT9EggACViunccjSgG
G1vZMTJbLCOU9G3JNFGWVEnpIkY3fqLJGDpVTinRLh1fN3VNpNRowQ==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-03-24T00:25:39Z"
mac: ENC[AES256_GCM,data:fl3MByuB+MrsRdsmpvLbH8ebnJ+4RKfKLu26aO50tRpdvMqi8lqpcYb9FKwTksGM1qb84rU/Q/NK4/mkwqGr3hAftLJ1J2pcR+GcnBbnihJs5uA3jfb59fine855QLaWbfk61LbQk3GWJs45jRGMAGSCqZMqXZNM5N55KSWSmjw=,iv:j3ev3sVc41TsyPVP1570uGxOOmmogYPQHPPDklt9qtM=,tag:JwXmI7/FMdyZg4w6v6Rq4g==,type:str]
+31 -31
View File
@@ -15,56 +15,56 @@ sops:
- recipient: age1yyzgmazjxkvwtfcv9re3lqmt2ru5dcrfu3sauysm0wzfwzvyap8qkjkq32
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBDT3hZams2RW9tYnpuclNU
V1BSN2E3L1VicldXb2E4U2cwMEMvekw4MDJRCklyTlhwa3FhVTNibVNkcTFoNThC
MzJKdFdhMlRqVlpvQjVmbmo0Z1JuZ3MKLS0tIHcwTk10WklKU3ZEYWg4dlVuRTFy
T1NKaW1CdEppOU44c2t4Tm45MzhCRTAKeUTQB64sXl2NT3VlQqfDZVH1iqGI0HMN
egV/S6pqKV0mC4aKrn2AKQUws4E6XF60z7ICM2ouBh+uth5xQ4U42g==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBtc25Wd0dlZldBVHZKTDRW
ZlVHamluT0hPbVArdWx0Qlk5czR6ZldIaEhRCnVkUVp6cHRwdnNsRDBhV285Q0tV
RitNWHNrUy9pN0dpanFSVHJjK0VuM1kKLS0tIHVLRWFRNmZyR2JOR3FwYjZsME9v
L2ZKbFIydkJ5L25GSWFBamhtbHZXV0UKAPIGMBbIdR7sCxsCYJa/9kajqmceOAJa
/jjxxcaWDkv5cmq0u8qFZEDkTjY3PKoJofBcx5q0npuJmrIA/oLeJA==
-----END AGE ENCRYPTED FILE-----
- recipient: age1elk6zwmcylwfk7gd4pjda7g29upftjvxys8py42s8d42jklnyv7s7dm9z2
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBIN1Zkd0FFS2FkbEZVQWhM
aWhka2lWY0tIbEtGK25maGtVcnRmL09Yc0NrCnF1RnYyakozM1RxTC91UUtlVHRN
TTRxdTBFaHJ4RXRvQ3RFN3VXV1FDTzgKLS0tIEdYNkhEc21rdFF3eldOaFFTQWNR
NUtkbTdVd0JNeEd4ZVBDb3YybGpUVUUKKhyOptpf1xD4z2nEYH2QKfEC0RPI9TSy
XiQ66Wzh1fkdqTlOkNdRzBniSObKg03oRaiZVXBtPx3iIrZw0XwywA==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBvcm5rZmtlUy9yQSs3Vnhh
eDVYaUxvcCtMYXhEa0ViTmFYK3JONnFsTWw0Cmdad2ZKdGcxeGQzeGVnTXRUeGJZ
R1kxQ1ZDK0FsQzc4S1EwMFN4VVZaTzAKLS0tIDJ5S3QycG5ZdThIVlZEVjBnMG9K
V1RSem1LdWpKT2o2aHh5dC9wVGJRakUK+5/eE+9WtSXmwoJ2Nqk4ni01GS4c3gLQ
p+wcpiOsxwnnisZTxag2yCn4hlv6FcOUWOcISq5H/sxwKgjBaeeuRQ==
-----END AGE ENCRYPTED FILE-----
- recipient: age193gw802ytal7h5p5q37kpd9079k2vsflzmnvupcwfxh2kjdrwqtsk3g6rm
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBxUlFTVmQwbGVCWjQxbjF4
bUcvSTRZY3hMTFdiaUlwN3hCb2QrWEhETlJZCjlVN3JDR0xUbFI4ZjFwVXowUmVE
WklhVEI1Vjh6Ukg3VzlSSXM0WWtHVkEKLS0tIDloaDB5VGxVYmhSU3hjbE4vZ3Zi
bTFaVVcyRVJjQ1dFVm9ITnhYRzBKbGcKAIw03NgRWOsmd/Kc8r8j+8fRRA3syHCz
fmMgs8w9lpuJj24XDmcpIkN48PnCTV5XzTuEg5nvIJdb3TG2t0KWvA==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6TjZVN3dFbmg5MGhNVFdD
YTZ4WVdOUWRGTzkrUmRGVUpobmg0Y0ozaUFNClh6cXhKWVkxcTlRYmZ5NmNqeEJJ
YzdYR1FyRExabXBDSnVSbXNxZy91am8KLS0tIHZLa0dXWHZhMDU5Q3BNR2U3NG00
eFp4Vmxha0xOVTVwd09PVzgrdzFNL2sKDEofAS4W4i8+VBU0wl1yTWmOogNbGHhY
azvb0QmxrYpurxjep3BYsc/5Co6U4mwowidoyzQLsiBJWDWy3wPdLQ==
-----END AGE ENCRYPTED FILE-----
- recipient: age1yr7vurfxc3w8ewfw9djfm54atw6ayze69qglamecuft5q0n9gu2sadsa2m
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBxSkdOakRYSm5FOFEwRzdW
UmZsREltNlhYdDhKWkE0blV0NVVwRWdvczBVCmVhSjB5Ymx4SEJ5S1NaU2RNSzV1
RjRjQzRpaHlMeE9ZbVozQTBiVmVncUEKLS0tIDNTTE9JMG9jczE4cGI3TW1aVk9o
anN5Wmgzd1NnWEFIY1lMU205REZjOHcKI3Jjobw0KmN6gK85QUsSW3m6IDtC960K
eO6fk2WHT1jSPjWH87JuqNzbrkR7XJRB2CW+MXIDDb8h7euLWp7Png==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB2d3dLVitDTVpyNllCTEJN
R1RLc2NkRnBrandDakhxeTJWeEdMVkZyS0JRCnl5Uk9FSk1iZmxCZUI1ZWdmYlJa
dEFlbjkxYjI4Y2FDZmswOGZqMUxGUXcKLS0tIGdqVWNaU1NWcGs4QXZyTkc5Z2VK
MGpaWmtHdzdpRDJxQXNveTd5WFkzTTgKxKuoC36uLqy+QoSGVcQekv5wn69yF0qH
2qZPAm3wnf0KzZ8Wo/B8nXkjkq4llfKHbwfePiMRL4RObKXAejYhLA==
-----END AGE ENCRYPTED FILE-----
- recipient: age1m30m9xzszmcawte35m0yymz42gfx3x84w7d5l67mtdtajhgpfgssuc2plm
- recipient: age1hya7pgpe8zal52w3pjf036tpapmehedatfm4r84h30t4wuh079ssedfd37
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBvRmZ5ZFAweWFuWFJrSDJY
SFJUR0FPYnZXdGVucVpBRUZRVEFhSktlMFZzCmg4T1VTY3l4cm1BK3ZGRC80ckRI
RjFHWllwamdEanAwU3BjNjgyZGR2bmcKLS0tIEUxK2NDV0IvRW0ycjA5cFdWaVhj
L0ZJODdNTjJiNDNqd3k2eGs4SktBN1kKhPba1fZ/fIMr6ys+sUc4bi71O/oE9Cns
7tSKVUXUnP5aNSW217gMwPBoc/5vTl92cYJaGKFlQ2IpECCYJKCi7g==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB4am5wWWRkL3J0dlZVaWJR
eVdMNnhTdGJqeFlNdnppTHNLdjdjcVNYbUQ4CjcwdGM5Nmdvck1wb0RSRjMwL01D
STlFdkdDallLLzNnbGY4WndiLzdRMzAKLS0tIHRENzRFTVA2MWE5djVHUy9peVdG
dkZDTTJvWU1vZHN2ZUR1R1Z3UWpkSlEKjo621j7xjyyHcc/ij8/X0H5uNLD6SnZa
o+apPj44+fTJFL5+VjP/XOsx0rCKTQhnV0gGfZU2SPtfH2BUiDjV0g==
-----END AGE ENCRYPTED FILE-----
- recipient: age1p05z980kdtngk9mw67hfev72h7xhslplpxfk9yskgmf0hl4lu3ls04zht9
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBiMDh5MmVyOHlJaUZiRVc4
RENYRDBORExRb1NETld4TnZkZ21DR0NIRm13ClVQeWVaNWYxc3FTcjFRcnRYZm1R
a2loWlpoYlc4bzc4Z2V0dWd5V0d2c2cKLS0tIG0wUGxTQUxqb0Z1cGZYSHpjQWRW
am1lYUp4VFpmKzJESjMrdUZwVFN0MWcK7wHDAEqHcMWcBcZyw+wL1dWH7R9xFq5H
grGFxgoPv2sn+4eQNKagC7jACm+l2vUFX5UuH0qJRVTpatXHnYb6SA==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA5YmF1K1ZtQmhoMGRVM0Fk
bW1PL0RPQ1ZUZEg4VU5VUi9BdlNhM2hPZkRNCnVIaUJJR1dFeXA1SWk2UFBkQW9a
cjc4bFRDb2p0eXJodG5IWk01ekdCdG8KLS0tIHFzU3l3WGcyTmZ0QjRyTmQwcDRZ
Mnp5K1VjcncrWWt4SEUrUVlCTTc3OVkKtRqPoatEp8NvZW4Z73nfCUshdz90SCad
VFgYF/2DYc7lSDP7otbsjBzGlauQQTWF1wfgEVOkw2EzOt2LCoflbg==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-03-24T00:15:02Z"
mac: ENC[AES256_GCM,data:dYTwO5DtkKinTKfBXGuvXRFxl8yavxXMKTw27M5/GcK/kkstHBG119IRk9B9KC6s6IHTY81U3MeUxE9XwdBiE7q4m15+ZO2vmdBVhN8wAh+82P9BP0HSaxLkjWLeKWBfULyLX/YXmQVsr09/NUEVSZcugJ6m40Ta+X9AQgO+cyA=,iv:FmsznsKTuIr61s3Zn0QZKSKvb/e2AljEB1ijKE52RKk=,tag:rHF2Xi4iP9VF33rxpBr5pg==,type:str]