Compare commits
2
Commits
0cd2c3951a
...
30d3127b21
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30d3127b21 | ||
|
|
88fd24c22b |
@@ -0,0 +1,5 @@
|
|||||||
|
.venv
|
||||||
|
.git
|
||||||
|
data
|
||||||
|
.env
|
||||||
|
**/__pycache__
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# API-Key von https://api-manager.opentransportdata.swiss (Produkt "GTFS-RT").
|
||||||
|
# Leer lassen = Karte läuft nur nach Fahrplan, ohne Verspätungen.
|
||||||
|
TRAINPULSE_RT_KEY=
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
.venv/
|
.venv/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
|
data/
|
||||||
|
.env
|
||||||
|
|||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
# pfaedle: rechnet aus OpenStreetMap die echten Streckenverläufe
|
||||||
|
FROM debian:trixie AS pfaedle
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
ca-certificates git cmake g++ make libzip-dev zlib1g-dev libbz2-dev \
|
||||||
|
&& git clone --depth 1 --recurse-submodules --shallow-submodules https://github.com/ad-freiburg/pfaedle /src \
|
||||||
|
&& cmake -S /src -B /build -DCMAKE_BUILD_TYPE=Release \
|
||||||
|
&& make -C /build -j"$(nproc)" pfaedle \
|
||||||
|
&& make -C /build install
|
||||||
|
|
||||||
|
# pmtiles: schneidet die Kartenkacheln für die Region aus
|
||||||
|
FROM golang:1.25-trixie AS pmtiles
|
||||||
|
RUN go install github.com/protomaps/go-pmtiles@v1.31.2
|
||||||
|
|
||||||
|
FROM python:3.13-slim-trixie
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends libzip5 zlib1g libbz2-1.0 libgomp1 \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
COPY --from=pfaedle /usr/local/bin/pfaedle /usr/local/bin/
|
||||||
|
COPY --from=pfaedle /usr/local/etc/pfaedle /usr/local/etc/pfaedle
|
||||||
|
COPY --from=pmtiles /go/bin/go-pmtiles /usr/local/bin/pmtiles
|
||||||
|
WORKDIR /app
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
COPY trainpulse trainpulse
|
||||||
|
COPY web web
|
||||||
|
COPY tests tests
|
||||||
|
ENV TRAINPULSE_DATA=/data PYTHONUNBUFFERED=1
|
||||||
|
EXPOSE 8080
|
||||||
|
CMD ["uvicorn", "trainpulse.server:app", "--host", "0.0.0.0", "--port", "8080", "--proxy-headers"]
|
||||||
@@ -7,11 +7,11 @@ Ziel, Verspätung und Fahrtverlauf mit Gleis. Hintergrund umschaltbar zwischen K
|
|||||||
## Wie es funktioniert
|
## Wie es funktioniert
|
||||||
|
|
||||||
```
|
```
|
||||||
opentransportdata.swiss ──GTFS (Mo+Do)──▶ trainpulse-update (stündlich)
|
opentransportdata.swiss ──GTFS (Mo+Do)──▶ updater-Container (stündlich)
|
||||||
├─ Filter: nur Bahn (route_type 100–199)
|
├─ Filter: nur Bahn (route_type 100–199)
|
||||||
OpenStreetMap (Geofabrik) ─────────────────▶├─ pfaedle: echte Streckenverläufe
|
OpenStreetMap (Geofabrik) ─────────────────▶├─ pfaedle: echte Streckenverläufe
|
||||||
└─▶ /var/lib/trainpulse/feeds/<version>/
|
└─▶ data/feeds/<version>/
|
||||||
opentransportdata.swiss ──GTFS-RT (35 s)──▶ trainpulse (Webserver, Port 8080)
|
opentransportdata.swiss ──GTFS-RT (35 s)──▶ web-Container (Port 8080)
|
||||||
├─ Position = Fahrplan + Verspätung, entlang Strecke
|
├─ Position = Fahrplan + Verspätung, entlang Strecke
|
||||||
└─ /api/vehicles, /api/trip/…, /api/status, Weboberfläche
|
└─ /api/vehicles, /api/trip/…, /api/status, Weboberfläche
|
||||||
```
|
```
|
||||||
@@ -23,33 +23,43 @@ Wechsel des Fahrplans: Neue Fahrpläne erscheinen Montag und Donnerstag zwischen
|
|||||||
die Echtzeitdaten stellen erst um 15 Uhr um. Der Server lädt deshalb immer den Fahrplan, auf den
|
die Echtzeitdaten stellen erst um 15 Uhr um. Der Server lädt deshalb immer den Fahrplan, auf den
|
||||||
sich die Echtzeitdaten gerade beziehen (`feed_version`), und schaltet automatisch mit um.
|
sich die Echtzeitdaten gerade beziehen (`feed_version`), und schaltet automatisch mit um.
|
||||||
|
|
||||||
## Installation auf Proxmox
|
## Installation auf Proxmox (Docker)
|
||||||
|
|
||||||
|
TrainPulse läuft als zwei Docker-Container aus demselben Image:
|
||||||
|
`web` (Webserver, Port 8080) und `updater` (holt stündlich neue Fahrpläne und beim ersten Start
|
||||||
|
die Kartenkacheln). Alle Daten liegen im Ordner `data/` neben der `compose.yaml`.
|
||||||
|
|
||||||
1. **Container anlegen** (Proxmox-Oberfläche → *Create CT*):
|
1. **Container anlegen** (Proxmox-Oberfläche → *Create CT*):
|
||||||
Debian 12 oder 13 Template, unprivileged, 2 Kerne, **4 GB RAM** (pfaedle braucht beim Aufbereiten bis ~2 GB, der Server ~0,5 GB), 20 GB Disk, Netz per DHCP.
|
Debian 12 oder 13 Template, unprivileged, 2 Kerne, **4 GB RAM** (pfaedle braucht beim
|
||||||
2. **Projekt in den Container kopieren**, z. B. `scp trainpulse.zip root@<ip>:` und dort
|
Aufbereiten bis ~2 GB, der Server ~0,5 GB), **20 GB Disk**, Netz per DHCP.
|
||||||
`apt install -y unzip && unzip trainpulse.zip`.
|
Unter *Options → Features* **Nesting** und **keyctl** aktivieren – ohne das läuft Docker
|
||||||
3. **Installieren:** `bash trainpulse/deploy/install.sh`
|
im LXC nicht.
|
||||||
Baut pfaedle, lädt die Kartenkacheln für die Schweiz, richtet die Dienste ein und startet
|
2. **Installieren** (im Container als root):
|
||||||
die erste Fahrplan-Aufbereitung im Hintergrund (`journalctl -fu trainpulse-update`).
|
```bash
|
||||||
4. **Echtzeit freischalten:** Auf <https://api-manager.opentransportdata.swiss> registrieren,
|
apt install -y git
|
||||||
|
git clone https://git.scarriffle.com/Scarriffle/Trainpulse.git
|
||||||
|
bash Trainpulse/deploy/install.sh
|
||||||
|
```
|
||||||
|
Das Skript installiert Docker, falls es fehlt, baut das Image und startet beide Container.
|
||||||
|
3. **Echtzeit freischalten:** Auf <https://api-manager.opentransportdata.swiss> registrieren,
|
||||||
eine Applikation anlegen, das Produkt *GTFS-RT* abonnieren und den API-Key in
|
eine Applikation anlegen, das Produkt *GTFS-RT* abonnieren und den API-Key in
|
||||||
`/etc/trainpulse.env` bei `TRAINPULSE_RT_KEY=` eintragen. Dann `systemctl restart trainpulse`.
|
`Trainpulse/.env` bei `TRAINPULSE_RT_KEY=` eintragen. Dann `docker compose up -d`.
|
||||||
|
|
||||||
Danach ist die Karte unter `http://<ip>:8080` erreichbar. Für den Zugriff von aussen einfach
|
Danach ist die Karte unter `http://<ip>:8080` erreichbar. Für den Zugriff von aussen einfach
|
||||||
hinter deinen bestehenden Reverse Proxy hängen.
|
hinter deinen bestehenden Reverse Proxy hängen.
|
||||||
|
|
||||||
Ein Update des Programms: neue Version in den Container kopieren, `install.sh` nochmals ausführen.
|
**Update:** `git pull && bash deploy/install.sh` – baut das Image neu, Daten bleiben.
|
||||||
|
|
||||||
## Nützliche Befehle
|
## Nützliche Befehle (im Ordner `Trainpulse`)
|
||||||
|
|
||||||
| Was | Befehl |
|
| Was | Befehl |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Status | `curl localhost:8080/api/status` |
|
| Status | `curl localhost:8080/api/status` |
|
||||||
| Logs Webserver | `journalctl -fu trainpulse` |
|
| Logs Webserver | `docker compose logs -f web` |
|
||||||
| Logs Fahrplan-Update | `journalctl -fu trainpulse-update` |
|
| Logs Fahrplan-Update | `docker compose logs -f updater` |
|
||||||
| Fahrplan sofort neu aufbereiten | `sudo -u trainpulse env $(cat /etc/trainpulse.env) /opt/trainpulse/.venv/bin/python -m trainpulse.prepare --force` |
|
| Fahrplan sofort neu aufbereiten | `docker compose exec updater python -m trainpulse.prepare --force` |
|
||||||
| Selbsttest | `/opt/trainpulse/.venv/bin/python tests/test_core.py` (im Projektordner) |
|
| Selbsttest | `docker compose exec web python tests/test_core.py` |
|
||||||
|
| Stoppen / Starten | `docker compose down` / `docker compose up -d` |
|
||||||
|
|
||||||
## Datenquellen und Lizenzen
|
## Datenquellen und Lizenzen
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
services:
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
image: trainpulse
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
env_file: .env
|
||||||
|
volumes:
|
||||||
|
- ./data:/data
|
||||||
|
|
||||||
|
# holt stündlich neue Fahrpläne (und beim ersten Start die Kartenkacheln)
|
||||||
|
updater:
|
||||||
|
build: .
|
||||||
|
image: trainpulse
|
||||||
|
depends_on: [web]
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file: .env
|
||||||
|
volumes:
|
||||||
|
- ./data:/data
|
||||||
|
command: ["sh", "-c", "while true; do python -m trainpulse.prepare; sleep 3600; done"]
|
||||||
+26
-63
@@ -1,79 +1,42 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# TrainPulse – Installation in einem Debian-LXC (Proxmox). Im Container als root:
|
# TrainPulse – Installation mit Docker (z. B. in einem Debian-LXC auf Proxmox). Als root:
|
||||||
# bash deploy/install.sh
|
# bash deploy/install.sh
|
||||||
# Erneut ausführen = Update des Programmcodes (Daten, Karte und Einstellungen bleiben).
|
# Erneut ausführen = Update (Image neu bauen, Daten und Einstellungen bleiben).
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
APP=/opt/trainpulse
|
[ "$(id -u)" = 0 ] || { echo "Bitte als root ausführen" >&2; exit 1; }
|
||||||
DATA=/var/lib/trainpulse
|
command -v apt-get >/dev/null || { echo "Nur für Debian/Ubuntu (apt) gedacht" >&2; exit 1; }
|
||||||
SRC=$(cd "$(dirname "$0")/.." && pwd)
|
mem=$(awk '/MemTotal/ {print int($2/1024)}' /proc/meminfo)
|
||||||
BBOX=5.8,45.7,10.6,47.9 # Schweiz mit etwas Rand
|
[ "$mem" -ge 3500 ] || echo "WARNUNG: nur ${mem} MB RAM – für die Fahrplan-Aufbereitung werden ~4 GB empfohlen" >&2
|
||||||
|
|
||||||
echo "==> Pakete"
|
if ! command -v docker >/dev/null || ! docker compose version >/dev/null 2>&1; then
|
||||||
|
echo "==> Docker installieren (offizielles Skript von get.docker.com)"
|
||||||
apt-get update -qq
|
apt-get update -qq
|
||||||
apt-get install -y -qq python3-venv git cmake g++ make libzip-dev zlib1g-dev libbz2-dev curl ca-certificates >/dev/null
|
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq curl ca-certificates >/dev/null
|
||||||
|
curl -fsSL https://get.docker.com | sh
|
||||||
echo "==> Programm nach $APP"
|
|
||||||
id trainpulse &>/dev/null || useradd --system --home-dir "$DATA" --shell /usr/sbin/nologin trainpulse
|
|
||||||
mkdir -p "$APP" "$DATA/tiles"
|
|
||||||
rm -rf "$APP/trainpulse" "$APP/web"
|
|
||||||
cp -r "$SRC/trainpulse" "$SRC/web" "$SRC/requirements.txt" "$APP/"
|
|
||||||
[ -d "$APP/.venv" ] || python3 -m venv "$APP/.venv"
|
|
||||||
"$APP/.venv/bin/pip" install -q --upgrade -r "$APP/requirements.txt"
|
|
||||||
|
|
||||||
if ! command -v pfaedle >/dev/null; then
|
|
||||||
echo "==> pfaedle bauen (Streckenverläufe aus OpenStreetMap, dauert ein paar Minuten)"
|
|
||||||
tmp=$(mktemp -d)
|
|
||||||
git clone -q --depth 1 --recurse-submodules --shallow-submodules https://github.com/ad-freiburg/pfaedle "$tmp/src"
|
|
||||||
cmake -S "$tmp/src" -B "$tmp/build" -DCMAKE_BUILD_TYPE=Release >/dev/null
|
|
||||||
make -C "$tmp/build" -j"$(nproc)" >/dev/null
|
|
||||||
make -C "$tmp/build" install >/dev/null
|
|
||||||
rm -rf "$tmp"
|
|
||||||
fi
|
fi
|
||||||
|
if ! docker run --rm hello-world >/dev/null 2>&1; then
|
||||||
if ! command -v pmtiles >/dev/null; then
|
cat >&2 <<'EOF'
|
||||||
echo "==> pmtiles-Werkzeug"
|
Docker ist installiert, kann aber keine Container starten.
|
||||||
url=$(curl -fsSL https://api.github.com/repos/protomaps/go-pmtiles/releases/latest | python3 -c \
|
In einem Proxmox-LXC: Container herunterfahren, unter Optionen → Features
|
||||||
'import json,sys; print(next(a["browser_download_url"] for a in json.load(sys.stdin)["assets"] if a["name"].endswith("Linux_x86_64.tar.gz")))')
|
"Nesting" und "keyctl" aktivieren, Container starten, Skript nochmals ausführen.
|
||||||
curl -fsSL "$url" | tar xz -C /usr/local/bin pmtiles
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -s "$DATA/tiles/region.pmtiles" ]; then
|
|
||||||
echo "==> Kartenkacheln (OpenStreetMap via Protomaps) für $BBOX"
|
|
||||||
for i in 1 2 3 4 5 6 7; do
|
|
||||||
d=$(date -d "-$i day" +%Y%m%d)
|
|
||||||
if pmtiles extract "https://build.protomaps.com/$d.pmtiles" "$DATA/tiles/region.tmp.pmtiles" --bbox=$BBOX --maxzoom=15; then
|
|
||||||
mv "$DATA/tiles/region.tmp.pmtiles" "$DATA/tiles/region.pmtiles"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
[ -s "$DATA/tiles/region.pmtiles" ] || { echo "Kartenkacheln konnten nicht geladen werden" >&2; exit 1; }
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -f /etc/trainpulse.env ]; then
|
|
||||||
cat > /etc/trainpulse.env <<EOF
|
|
||||||
TRAINPULSE_DATA=$DATA
|
|
||||||
# API-Key von https://api-manager.opentransportdata.swiss (Produkt "GTFS-RT")
|
|
||||||
TRAINPULSE_RT_KEY=
|
|
||||||
EOF
|
EOF
|
||||||
chmod 640 /etc/trainpulse.env
|
exit 1
|
||||||
chgrp trainpulse /etc/trainpulse.env
|
|
||||||
fi
|
fi
|
||||||
chown -R trainpulse:trainpulse "$DATA"
|
|
||||||
|
|
||||||
echo "==> Dienste"
|
[ -f .env ] || cp .env.example .env
|
||||||
cp "$SRC"/deploy/trainpulse.service "$SRC"/deploy/trainpulse-update.service "$SRC"/deploy/trainpulse-update.timer /etc/systemd/system/
|
mkdir -p data
|
||||||
systemctl daemon-reload
|
|
||||||
systemctl enable --now trainpulse-update.timer
|
echo "==> Image bauen und starten (das erste Mal dauert der Bau von pfaedle ein paar Minuten)"
|
||||||
systemctl enable trainpulse.service
|
docker compose up -d --build
|
||||||
systemctl restart trainpulse.service
|
|
||||||
systemctl start --no-block trainpulse-update.service
|
|
||||||
|
|
||||||
ip=$(hostname -I | awk '{print $1}')
|
ip=$(hostname -I | awk '{print $1}')
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
|
|
||||||
Fertig. Die Karte läuft unter http://$ip:8080
|
Fertig. Die Karte läuft unter http://$ip:8080
|
||||||
Der erste Fahrplan wird jetzt im Hintergrund geladen und aufbereitet (ca. 10–15 min).
|
Der Updater lädt jetzt Kartenkacheln (~1,2 GB) und den Fahrplan und berechnet die
|
||||||
Fortschritt: journalctl -fu trainpulse-update
|
Streckenverläufe. Bis die ersten Züge erscheinen, dauert es ca. 15 Minuten.
|
||||||
Echtzeit: API-Key in /etc/trainpulse.env eintragen, dann systemctl restart trainpulse
|
Fortschritt: docker compose logs -f updater
|
||||||
|
Echtzeit: API-Key in $(pwd)/.env eintragen, dann docker compose up -d
|
||||||
EOF
|
EOF
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=TrainPulse: neuen Fahrplan holen und Streckenverläufe berechnen
|
|
||||||
After=network-online.target
|
|
||||||
Wants=network-online.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=oneshot
|
|
||||||
User=trainpulse
|
|
||||||
EnvironmentFile=/etc/trainpulse.env
|
|
||||||
WorkingDirectory=/opt/trainpulse
|
|
||||||
ExecStart=/opt/trainpulse/.venv/bin/python -m trainpulse.prepare
|
|
||||||
Nice=10
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=TrainPulse: stündlich auf neuen Fahrplan prüfen
|
|
||||||
|
|
||||||
[Timer]
|
|
||||||
OnBootSec=2min
|
|
||||||
OnCalendar=*-*-* *:20:00
|
|
||||||
Persistent=true
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=timers.target
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=TrainPulse Webserver
|
|
||||||
After=network-online.target
|
|
||||||
Wants=network-online.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
User=trainpulse
|
|
||||||
EnvironmentFile=/etc/trainpulse.env
|
|
||||||
WorkingDirectory=/opt/trainpulse
|
|
||||||
ExecStart=/opt/trainpulse/.venv/bin/uvicorn trainpulse.server:app --host 0.0.0.0 --port 8080 --proxy-headers
|
|
||||||
Restart=always
|
|
||||||
RestartSec=5
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
@@ -13,4 +13,6 @@ RT_INTERVAL = 35 # Sekunden; Limit sind 2 Abfragen/Minute (Sliding Window)
|
|||||||
|
|
||||||
OSM_URL = os.environ.get("TRAINPULSE_OSM_URL", "https://download.geofabrik.de/europe/switzerland-latest.osm.pbf")
|
OSM_URL = os.environ.get("TRAINPULSE_OSM_URL", "https://download.geofabrik.de/europe/switzerland-latest.osm.pbf")
|
||||||
PFAEDLE = os.environ.get("TRAINPULSE_PFAEDLE", "pfaedle")
|
PFAEDLE = os.environ.get("TRAINPULSE_PFAEDLE", "pfaedle")
|
||||||
|
PMTILES = os.environ.get("TRAINPULSE_PMTILES", "pmtiles")
|
||||||
|
TILES_BBOX = "5.8,45.7,10.6,47.9" # Schweiz mit etwas Rand
|
||||||
USER_AGENT = "trainpulse/0.1 (+https://scarriffle.com)"
|
USER_AGENT = "trainpulse/0.1 (+https://scarriffle.com)"
|
||||||
|
|||||||
@@ -201,6 +201,24 @@ def pack_shapes(d: Path) -> None:
|
|||||||
src.unlink()
|
src.unlink()
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_tiles() -> None:
|
||||||
|
"""Kartenkacheln (OpenStreetMap, von Protomaps) einmalig für die Region herunterladen."""
|
||||||
|
dest = C.DATA_DIR / "tiles" / "region.pmtiles"
|
||||||
|
if dest.exists() and dest.stat().st_size:
|
||||||
|
return
|
||||||
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
tmp = dest.with_suffix(".tmp")
|
||||||
|
for days in range(1, 8): # Protomaps baut täglich, ältere Builds werden gelöscht
|
||||||
|
d = (dt.date.today() - dt.timedelta(days=days)).strftime("%Y%m%d")
|
||||||
|
log.info("Lade Kartenkacheln vom %s …", d)
|
||||||
|
r = subprocess.run([C.PMTILES, "extract", f"https://build.protomaps.com/{d}.pmtiles", str(tmp),
|
||||||
|
f"--bbox={C.TILES_BBOX}", "--maxzoom=15"])
|
||||||
|
if r.returncode == 0 and tmp.exists() and tmp.stat().st_size:
|
||||||
|
tmp.rename(dest)
|
||||||
|
return
|
||||||
|
log.error("Kartenkacheln konnten nicht geladen werden – Karte bleibt ohne Hintergrund")
|
||||||
|
|
||||||
|
|
||||||
def publish(src: Path, meta: dict) -> None:
|
def publish(src: Path, meta: dict) -> None:
|
||||||
(src / "meta.json").write_text(json.dumps(meta))
|
(src / "meta.json").write_text(json.dumps(meta))
|
||||||
dest = C.FEEDS_DIR / meta["feed_version"]
|
dest = C.FEEDS_DIR / meta["feed_version"]
|
||||||
@@ -219,6 +237,8 @@ def main() -> None:
|
|||||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
|
||||||
|
|
||||||
C.FEEDS_DIR.mkdir(parents=True, exist_ok=True)
|
C.FEEDS_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
if not args.zip:
|
||||||
|
ensure_tiles()
|
||||||
state_file = C.DATA_DIR / "state.json"
|
state_file = C.DATA_DIR / "state.json"
|
||||||
state = json.loads(state_file.read_text()) if state_file.exists() else {}
|
state = json.loads(state_file.read_text()) if state_file.exists() else {}
|
||||||
work = C.DATA_DIR / "work"
|
work = C.DATA_DIR / "work"
|
||||||
|
|||||||
Reference in New Issue
Block a user