Initial containerized Frigate camera control bridge

This commit is contained in:
2026-08-03 00:21:18 +02:00
commit 26e042d2d7
15 changed files with 2466 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
.git
__pycache__/
*.pyc
*.pyo
.DS_Store
.env
*.env
!*.env.example
camera-control.env
dist/
build/
+10
View File
@@ -0,0 +1,10 @@
__pycache__/
*.py[cod]
*.mo
.DS_Store
.env
*.env
!.env.example
camera-control.env
dist/
build/
+22
View File
@@ -0,0 +1,22 @@
ARG BASE_IMAGE=python:3.11-slim
FROM ${BASE_IMAGE}
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV LOCALE_DIR=/app/locale
ENV HTTP_PORT=5011
ENV UI_LANGUAGE=en
COPY frigate-camera-control-bridge.py /app/frigate-camera-control-bridge.py
COPY locale /app/locale
COPY tools/compile_gettext.py /app/tools/compile_gettext.py
RUN python3 /app/tools/compile_gettext.py /app/locale
EXPOSE 5011
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 CMD python3 -c "import os,urllib.request; urllib.request.urlopen('http://127.0.0.1:%s/readyz' % os.environ.get('HTTP_PORT', '5011'), timeout=3).read()"
ENTRYPOINT ["python3", "/app/frigate-camera-control-bridge.py"]
+14
View File
@@ -0,0 +1,14 @@
ARG BASE_IMAGE=python:3.11-slim
FROM ${BASE_IMAGE}
WORKDIR /workspace
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV LOCALE_DIR=/workspace/locale
ENV HTTP_PORT=5011
ENV UI_LANGUAGE=en
COPY tools/compile_gettext.py /usr/local/bin/compile_gettext.py
CMD ["sh"]
+194
View File
@@ -0,0 +1,194 @@
# Frigate Camera Control Bridge
## PL
Ten projekt jest uniwersalnym sidecarem dla pojedynczej instancji Frigate.
Kontener pobiera konfiguracje Frigate przez `/api/config`, wykrywa kamery z
`go2rtc` oraz z bezposrednich URL-i `ffmpeg.inputs[].path`, a potem wystawia
sterowanie swiatlem/syrena przez MQTT Discovery dla Home Assistant i awaryjne
GUI HTTP.
Najwazniejsze zalozenia:
- Ten sam obraz obsluguje Podman dzisiaj i Kubernetes pozniej.
- Konfiguracja runtime jest przez zmienne srodowiskowe, bez montowania pliku
konfiguracyjnego Frigate.
- Sidecar czeka na Frigate przy starcie i ponawia odczyt `/api/config`.
- Jesli `ffmpeg.inputs[].path` wskazuje na `rtsp://127.0.0.1:8554/...`,
sidecar traktuje to jako brame go2rtc i szuka prawdziwego URL-a kamery w
`go2rtc.streams`.
- Aktualny Podman uzywa pliku env per sidecar, np.
`/etc/frigate-camera-control-bridge/frigate-main.env`.
- Kubernetes powinien podawac `MQTT_HOST`, `MQTT_USER`, `MQTT_PASSWORD`,
`DEFAULT_CAMERA_USER`, `DEFAULT_CAMERA_PASSWORD` przez `Secret`/`ConfigMap`.
- W trybie env brak tych wartosci jest bledem startu; sidecar nie szuka
credentiali w Quadletach ani w konfiguracji Frigate.
- GUI fallback domyslnie slucha na `5011`, bo `5001` jest juz uzywany przez
czesc instancji Frigate.
- Jezyki GUI sa standardowym gettext: `locale/<lang>/LC_MESSAGES/*.po` i `.mo`.
Podstawowe zmienne:
| Zmienna | Znaczenie |
| --- | --- |
| `FRIGATE_NAME` | Nazwa tej instancji, np. `frigate-main`. |
| `FRIGATE_URL` | URL API Frigate, np. `http://frigate-main.example.test:5000`. |
| `MQTT_HOST`, `MQTT_PORT` | Broker MQTT. |
| `MQTT_USER`, `MQTT_PASSWORD` | Credentiale MQTT. |
| `DEFAULT_CAMERA_USER`, `DEFAULT_CAMERA_PASSWORD` | Domyslne credentiale kamer odkrytych z Frigate. |
| `BASE_TOPIC` | Bazowy topic MQTT, np. `frigate_camera_control/frigate-main`. |
| `DISCOVERY_PREFIX` | Prefix Home Assistant MQTT Discovery, zwykle `homeassistant`. |
| `UI_LANGUAGE` | Domyslny jezyk GUI i encji HA, domyslnie `en`. |
| `HTTP_PORT` | Port GUI fallback, domyslnie `5011`. |
| `HTTP_TOKEN` | Opcjonalny token dla GUI/API HTTP. |
| `SNAPSHOT_RETRY_SECONDS` | Co ile probowac ponownie, gdy migawki brakuja, domyslnie `600`. |
| `SNAPSHOT_REFRESH_SECONDS` | Co ile odswiezac poprawnie pobrane migawki, domyslnie `3600`. |
| `SNAPSHOT_LIVE_ON_GUI` | Czy pobierac swieza migawke z Frigate przy otwieraniu GUI, domyslnie `true`. |
Przyklad `/etc/frigate-camera-control-bridge/frigate-main.env`:
```bash
MQTT_HOST=mqtt.example.test
MQTT_PORT=1883
MQTT_USER=...
MQTT_PASSWORD=...
DEFAULT_CAMERA_USER=admin
DEFAULT_CAMERA_PASSWORD=...
```
Budowanie:
```bash
docker build -t frigate-camera-control-bridge:1.0 .
docker build -f Dockerfile.dev -t frigate-camera-control-bridge:dev .
```
Przyklady uruchomienia:
- Podman Quadlet: `examples/podman-quadlet/`
- Kubernetes sidecar: `examples/kubernetes-sidecar.yaml`
Lokalny test kontenera:
```bash
docker run --rm \
-e FRIGATE_NAME=example \
-e FRIGATE_URL=http://frigate:5000 \
-e MQTT_HOST=mqtt.local \
-e MQTT_USER=user \
-e MQTT_PASSWORD=password \
-e DEFAULT_CAMERA_USER=admin \
-e DEFAULT_CAMERA_PASSWORD=password \
-p 5011:5011 \
frigate-camera-control-bridge:1.0 --check
```
Jezyki:
1. Dodaj katalog `locale/de/LC_MESSAGES/`.
2. Skopiuj `frigate_camera_control_bridge.po` z `locale/en/LC_MESSAGES/`.
3. Przetlumacz `msgstr`.
4. Uruchom `python3 tools/compile_gettext.py locale`.
5. Po przebudowaniu obrazu GUI samo pokaze nowy jezyk.
Manualne komendy w kontenerze:
```bash
/app/frigate-camera-control-bridge.py --list
/app/frigate-camera-control-bridge.py --check
/app/frigate-camera-control-bridge.py --all-off
/app/frigate-camera-control-bridge.py --action frigate_main_camera_a_red_blue off
```
HTTP fallback:
```text
http://<ip-frigate>:5011/
http://<ip-frigate>:5011/?lang=pl
http://<ip-frigate>:5011/api/actions
http://<ip-frigate>:5011/snapshot/<action_id>.jpg
http://<ip-frigate>:5011/healthz
http://<ip-frigate>:5011/readyz
```
Jesli ustawiono `HTTP_TOKEN`, dodaj `?token=<token>` albo naglowek
`X-Bridge-Token: <token>`.
## EN
This project is a generic sidecar for one Frigate instance. The container reads
Frigate configuration from `/api/config`, discovers cameras from `go2rtc` and
direct `ffmpeg.inputs[].path` stream URLs, then exposes light/siren controls
through Home Assistant MQTT Discovery plus an emergency HTTP fallback GUI.
Main design points:
- One image works for Podman now and Kubernetes later.
- Runtime configuration is environment-based; no Frigate config mount is needed.
- The sidecar waits for Frigate startup and retries `/api/config`.
- If `ffmpeg.inputs[].path` points at `rtsp://127.0.0.1:8554/...`, the sidecar
treats it as a go2rtc gateway and resolves the real camera URL from
`go2rtc.streams`.
- Current Podman deployment uses one env file per sidecar, for example:
`/etc/frigate-camera-control-bridge/frigate-main.env`.
- Kubernetes should pass `MQTT_HOST`, `MQTT_USER`, `MQTT_PASSWORD`,
`DEFAULT_CAMERA_USER`, and `DEFAULT_CAMERA_PASSWORD` from a `Secret`/`ConfigMap`.
- In env mode, missing values are startup errors; the sidecar does not look up
credentials in Quadlets or Frigate config.
- The fallback GUI listens on `5011` by default because `5001` is already used
by some Frigate instances.
- GUI languages use standard gettext files:
`locale/<lang>/LC_MESSAGES/*.po` and `.mo`.
Core env vars:
| Variable | Meaning |
| --- | --- |
| `FRIGATE_NAME` | Instance name, for example `frigate-main`. |
| `FRIGATE_URL` | Frigate API URL, for example `http://frigate-main.example.test:5000`. |
| `MQTT_HOST`, `MQTT_PORT` | MQTT broker. |
| `MQTT_USER`, `MQTT_PASSWORD` | MQTT credentials. |
| `DEFAULT_CAMERA_USER`, `DEFAULT_CAMERA_PASSWORD` | Default credentials for cameras discovered from Frigate. |
| `BASE_TOPIC` | MQTT base topic, for example `frigate_camera_control/frigate-main`. |
| `DISCOVERY_PREFIX` | Home Assistant MQTT Discovery prefix, usually `homeassistant`. |
| `UI_LANGUAGE` | Default GUI and HA entity language, defaults to `en`. |
| `HTTP_PORT` | Fallback GUI port, defaults to `5011`. |
| `HTTP_TOKEN` | Optional HTTP GUI/API token. |
| `SNAPSHOT_RETRY_SECONDS` | Retry interval while a snapshot is missing, defaults to `600`. |
| `SNAPSHOT_REFRESH_SECONDS` | Refresh interval after snapshots are available, defaults to `3600`. |
| `SNAPSHOT_LIVE_ON_GUI` | Fetch a fresh Frigate snapshot when the GUI loads, defaults to `true`. |
Build:
```bash
docker build -t frigate-camera-control-bridge:1.0 .
docker build -f Dockerfile.dev -t frigate-camera-control-bridge:dev .
```
Run examples:
- Podman Quadlet: `examples/podman-quadlet/`
- Kubernetes sidecar: `examples/kubernetes-sidecar.yaml`
Development container:
```bash
docker run --rm -it \
-v "$PWD":/workspace \
frigate-camera-control-bridge:dev
```
HTTP fallback:
```text
http://<frigate-ip>:5011/
http://<frigate-ip>:5011/?lang=pl
http://<frigate-ip>:5011/api/actions
http://<frigate-ip>:5011/snapshot/<action_id>.jpg
http://<frigate-ip>:5011/healthz
http://<frigate-ip>:5011/readyz
```
Kubernetes sidecar notes:
- Put the bridge container in the same pod as Frigate, or point `FRIGATE_URL`
at the Frigate service.
- Use `readinessProbe` on `/readyz`.
- Pass secrets through env vars from `Secret`.
- Keep `/var/lib/frigate-camera-control-bridge` on a small persistent volume if
you want stale Home Assistant discovery cleanup to survive pod recreation.
+22
View File
@@ -0,0 +1,22 @@
# Per-instance runtime env for one Frigate camera-control sidecar.
# Example target on Podman: /etc/frigate-camera-control-bridge/frigate-main.env
# Kubernetes equivalent: env values from Secret/ConfigMap for that Frigate pod.
MQTT_HOST=mqtt.example.test
MQTT_PORT=1883
MQTT_USER=change-me
MQTT_PASSWORD=change-me
DEFAULT_CAMERA_USER=admin
DEFAULT_CAMERA_PASSWORD=change-me
# Optional. If set, HTTP GUI/API require ?token=<value> or X-Bridge-Token.
# HTTP_TOKEN=change-me
# Optional. Snapshot refresh is intentionally slow; it is only for GUI context.
SNAPSHOT_ENABLED=true
SNAPSHOT_HEIGHT=180
SNAPSHOT_CACHE_SECONDS=300
SNAPSHOT_RETRY_SECONDS=600
SNAPSHOT_REFRESH_SECONDS=3600
SNAPSHOT_LIVE_ON_GUI=true
+86
View File
@@ -0,0 +1,86 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: frigate-with-camera-control
spec:
replicas: 1
selector:
matchLabels:
app: frigate
template:
metadata:
labels:
app: frigate
spec:
containers:
- name: frigate
image: ghcr.io/blakeblackshear/frigate:stable
ports:
- name: http
containerPort: 5000
- name: camera-control
image: registry.example.local/frigate-camera-control-bridge:1.0
env:
- name: FRIGATE_NAME
value: frigate-main
- name: FRIGATE_URL
value: http://127.0.0.1:5000
- name: MQTT_HOST
value: mqtt.default.svc.cluster.local
- name: BASE_TOPIC
value: frigate_camera_control/frigate-main
- name: UI_LANGUAGE
value: en
- name: SNAPSHOT_RETRY_SECONDS
value: "600"
- name: SNAPSHOT_REFRESH_SECONDS
value: "3600"
- name: SNAPSHOT_LIVE_ON_GUI
value: "true"
- name: MQTT_USER
valueFrom:
secretKeyRef:
name: frigate-camera-control
key: mqtt-user
- name: MQTT_PASSWORD
valueFrom:
secretKeyRef:
name: frigate-camera-control
key: mqtt-password
- name: DEFAULT_CAMERA_USER
valueFrom:
secretKeyRef:
name: frigate-camera-control
key: camera-user
- name: DEFAULT_CAMERA_PASSWORD
valueFrom:
secretKeyRef:
name: frigate-camera-control
key: camera-password
- name: HTTP_TOKEN
valueFrom:
secretKeyRef:
name: frigate-camera-control
key: http-token
optional: true
ports:
- name: control-http
containerPort: 5011
readinessProbe:
httpGet:
path: /readyz
port: control-http
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /healthz
port: control-http
initialDelaySeconds: 20
periodSeconds: 30
volumeMounts:
- name: camera-control-state
mountPath: /var/lib/frigate-camera-control-bridge
volumes:
- name: camera-control-state
emptyDir: {}
+57
View File
@@ -0,0 +1,57 @@
# Podman Quadlet Example
## PL
To jest generyczny przyklad uruchomienia kontenera
`frigate-camera-control-bridge` przez Podman Quadlet. Pliki sa przykladem dla
jednej instancji Frigate. Dla wielu instancji skopiuj `.container` oraz `.env`
i zmien `FRIGATE_NAME`, `FRIGATE_URL`, `BASE_TOPIC`, port publikowany przez
`PublishPort` oraz credentiale MQTT.
Przykladowa instalacja:
```bash
install -d /etc/frigate-camera-control-bridge
install -m 0600 frigate-camera-control-bridge.env.example \
/etc/frigate-camera-control-bridge/frigate-main.env
install -m 0644 frigate-camera-control-bridge.container \
/etc/containers/systemd/frigate-camera-control-bridge.container
systemctl daemon-reload
systemctl enable --now frigate-camera-control-bridge.service
```
W pliku env uzupelnij:
- `MQTT_HOST`, `MQTT_USER`, `MQTT_PASSWORD`
- `DEFAULT_CAMERA_USER`, `DEFAULT_CAMERA_PASSWORD`
- `FRIGATE_NAME`, `FRIGATE_URL`, `BASE_TOPIC`
Jesli sidecar ma startowac razem z istniejaca usluga Frigate, dopasuj w
`.container` nazwe `frigate.service` w polach `After`, `Requires`, `BindsTo`
i `PartOf`.
## EN
This is a generic example for running `frigate-camera-control-bridge` with
Podman Quadlet. The files model one Frigate instance. For multiple instances,
copy the `.container` and `.env` files and change `FRIGATE_NAME`,
`FRIGATE_URL`, `BASE_TOPIC`, the `PublishPort` binding, and MQTT credentials.
Example install:
```bash
install -d /etc/frigate-camera-control-bridge
install -m 0600 frigate-camera-control-bridge.env.example \
/etc/frigate-camera-control-bridge/frigate-main.env
install -m 0644 frigate-camera-control-bridge.container \
/etc/containers/systemd/frigate-camera-control-bridge.container
systemctl daemon-reload
systemctl enable --now frigate-camera-control-bridge.service
```
Fill these env values:
- `MQTT_HOST`, `MQTT_USER`, `MQTT_PASSWORD`
- `DEFAULT_CAMERA_USER`, `DEFAULT_CAMERA_PASSWORD`
- `FRIGATE_NAME`, `FRIGATE_URL`, `BASE_TOPIC`
If the sidecar should follow an existing Frigate service, adjust the
`frigate.service` references in `After`, `Requires`, `BindsTo`, and `PartOf`.
@@ -0,0 +1,25 @@
[Unit]
Description=Frigate camera control bridge sidecar
Wants=network-online.target
After=network-online.target frigate.service
Requires=frigate.service
BindsTo=frigate.service
PartOf=frigate.service
RequiresMountsFor=/var/lib/frigate-camera-control-bridge
[Container]
ContainerName=frigate-camera-control-bridge
Image=localhost/frigate-camera-control-bridge:1.0
PublishPort=0.0.0.0:5011:5011
Volume=/var/lib/frigate-camera-control-bridge:/var/lib/frigate-camera-control-bridge
Volume=/etc/localtime:/etc/localtime:ro
PodmanArgs=--env-file=/etc/frigate-camera-control-bridge/frigate-main.env
[Service]
Restart=on-failure
RestartSec=5s
TimeoutStartSec=180
[Install]
WantedBy=multi-user.target
WantedBy=frigate.service
@@ -0,0 +1,24 @@
FRIGATE_NAME=frigate-main
FRIGATE_URL=http://127.0.0.1:5000
BASE_TOPIC=frigate_camera_control/frigate-main
MQTT_HOST=mqtt.local
MQTT_PORT=1883
MQTT_USER=change-me
MQTT_PASSWORD=change-me
DEFAULT_CAMERA_USER=admin
DEFAULT_CAMERA_PASSWORD=change-me
HTTP_PORT=5011
UI_LANGUAGE=en
WAIT_FOR_FRIGATE=true
FRIGATE_RETRY_SECONDS=5
FRIGATE_STARTUP_WAIT_SECONDS=0
SNAPSHOT_ENABLED=true
SNAPSHOT_HEIGHT=180
SNAPSHOT_CACHE_SECONDS=300
SNAPSHOT_RETRY_SECONDS=600
SNAPSHOT_REFRESH_SECONDS=3600
SNAPSHOT_LIVE_ON_GUI=true
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,51 @@
msgid ""
msgstr ""
"Project-Id-Version: frigate-camera-control-bridge 1.0\n"
"Language: en\n"
"Language-Name: English\n"
"Content-Type: text/plain; charset=UTF-8\n"
msgid "title"
msgstr "Frigate Camera Control"
msgid "language"
msgstr "Language"
msgid "id"
msgstr "ID"
msgid "name"
msgstr "Name"
msgid "type"
msgstr "Type"
msgid "state"
msgstr "State"
msgid "actions"
msgstr "Actions"
msgid "snapshot"
msgstr "Snapshot"
msgid "button.on"
msgstr "ON"
msgid "button.off"
msgstr "OFF"
msgid "button.press"
msgstr "PRESS"
msgid "action.all_off"
msgstr "{target} all off"
msgid "action.manual_white"
msgstr "{target} manual white light"
msgid "action.red_blue"
msgstr "{target} red-blue light"
msgid "action.siren"
msgstr "{target} siren"
@@ -0,0 +1,51 @@
msgid ""
msgstr ""
"Project-Id-Version: frigate-camera-control-bridge 1.0\n"
"Language: pl\n"
"Language-Name: Polski\n"
"Content-Type: text/plain; charset=UTF-8\n"
msgid "title"
msgstr "Frigate Camera Control"
msgid "language"
msgstr "Jezyk"
msgid "id"
msgstr "ID"
msgid "name"
msgstr "Nazwa"
msgid "type"
msgstr "Typ"
msgid "state"
msgstr "Stan"
msgid "actions"
msgstr "Akcje"
msgid "snapshot"
msgstr "Migawka"
msgid "button.on"
msgstr "WLACZ"
msgid "button.off"
msgstr "WYLACZ"
msgid "button.press"
msgstr "URUCHOM"
msgid "action.all_off"
msgstr "{target} wylacz wszystko"
msgid "action.manual_white"
msgstr "{target} biale swiatlo reczne"
msgid "action.red_blue"
msgstr "{target} czerwono-niebieskie"
msgid "action.siren"
msgstr "{target} syrena"
+86
View File
@@ -0,0 +1,86 @@
import importlib.util
import unittest
from pathlib import Path
def load_bridge_module():
root = Path(__file__).resolve().parents[1]
path = root / "frigate-camera-control-bridge.py"
spec = importlib.util.spec_from_file_location("bridge", path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
class DiscoveryMappingTest(unittest.TestCase):
def setUp(self):
self.bridge = load_bridge_module()
def test_loopback_ffmpeg_path_resolves_through_go2rtc(self):
camera_cfg = {
"ffmpeg": {
"inputs": [
{"path": "rtsp://127.0.0.1:8554/stream-a?video=copy"},
]
}
}
streams = {
"stream-a": [
"rtsp://camera-a.example.test:554/Streaming/Channels/101",
]
}
urls = self.bridge.CameraControlBridge.camera_candidate_urls("camera-a", camera_cfg, streams)
self.assertEqual(urls, ["rtsp://camera-a.example.test:554/Streaming/Channels/101"])
self.assertEqual(self.bridge.CameraControlBridge.hosts_from_urls(urls), ["camera-a.example.test"])
def test_direct_ffmpeg_path_is_used_without_go2rtc(self):
camera_cfg = {
"ffmpeg": {
"inputs": [
{"path": "rtsp://camera-b.example.test:554/cam/realmonitor?channel=1&subtype=0"},
]
}
}
urls = self.bridge.CameraControlBridge.camera_candidate_urls("camera-b", camera_cfg, {})
self.assertEqual(urls, ["rtsp://camera-b.example.test:554/cam/realmonitor?channel=1&subtype=0"])
self.assertEqual(self.bridge.CameraControlBridge.hosts_from_urls(urls), ["camera-b.example.test"])
def test_direct_path_wins_when_loopback_is_also_present(self):
camera_cfg = {
"ffmpeg": {
"inputs": [
{"path": "rtsp://127.0.0.1:8554/stream-a"},
{"path": "rtsp://direct-camera.example.test:554/Streaming/Channels/102"},
]
}
}
streams = {
"stream-a": {
"streams": [
"rtsp://camera-a.example.test:554/Streaming/Channels/101",
]
}
}
urls = self.bridge.CameraControlBridge.camera_candidate_urls("camera-a", camera_cfg, streams)
self.assertEqual(
urls,
[
"rtsp://direct-camera.example.test:554/Streaming/Channels/102",
],
)
self.assertEqual(
self.bridge.CameraControlBridge.hosts_from_urls(urls),
[
"direct-camera.example.test",
],
)
if __name__ == "__main__":
unittest.main()
+77
View File
@@ -0,0 +1,77 @@
#!/usr/bin/env python3
import ast
import struct
import sys
from pathlib import Path
def read_po(path):
messages = {}
current = None
buffers = {"msgid": [], "msgstr": []}
def flush():
if buffers["msgid"] or buffers["msgstr"]:
messages["".join(buffers["msgid"])] = "".join(buffers["msgstr"])
buffers["msgid"] = []
buffers["msgstr"] = []
for raw_line in path.read_text(encoding="utf-8").splitlines():
line = raw_line.strip()
if not line or line.startswith("#"):
continue
if line.startswith("msgid "):
flush()
current = "msgid"
buffers[current].append(ast.literal_eval(line[6:].strip()))
continue
if line.startswith("msgstr "):
current = "msgstr"
buffers[current].append(ast.literal_eval(line[7:].strip()))
continue
if line.startswith('"') and current:
buffers[current].append(ast.literal_eval(line))
flush()
return messages
def write_mo(messages, path):
keys = sorted(messages)
ids = b"\x00".join(key.encode("utf-8") for key in keys) + b"\x00"
strs = b"\x00".join(messages[key].encode("utf-8") for key in keys) + b"\x00"
keystart = 7 * 4 + len(keys) * 16
valuestart = keystart + len(ids)
koffsets = []
voffsets = []
offset = keystart
for key in keys:
encoded = key.encode("utf-8")
koffsets.append((len(encoded), offset))
offset += len(encoded) + 1
offset = valuestart
for key in keys:
encoded = messages[key].encode("utf-8")
voffsets.append((len(encoded), offset))
offset += len(encoded) + 1
output = [
struct.pack("Iiiiiii", 0x950412DE, 0, len(keys), 7 * 4, 7 * 4 + len(keys) * 8, 0, 0)
]
output.extend(struct.pack("ii", length, offset) for length, offset in koffsets)
output.extend(struct.pack("ii", length, offset) for length, offset in voffsets)
output.append(ids)
output.append(strs)
path.write_bytes(b"".join(output))
def main():
root = Path(sys.argv[1] if len(sys.argv) > 1 else "locale")
for po in root.glob("*/LC_MESSAGES/*.po"):
mo = po.with_suffix(".mo")
write_mo(read_po(po), mo)
print(mo)
if __name__ == "__main__":
main()