diff --git a/.gitignore b/.gitignore index ecb706c..2ce926b 100644 --- a/.gitignore +++ b/.gitignore @@ -260,6 +260,7 @@ prowlarr/Backups/ prowlarr/logs/ prowlarr/*.pid + # qBittorrent runtime data qbit-vpn/config/.bash_history qbit-vpn/config/.wget-hsts @@ -275,3 +276,6 @@ side-agent/manifest/ # Claude Code session data and instructions .claude/ CLAUDE.md + +# OTHERS +log-ingester/state/state.json diff --git a/frigate/config.yml.bak b/frigate/config.yml.bak deleted file mode 100644 index fe66f38..0000000 --- a/frigate/config.yml.bak +++ /dev/null @@ -1,428 +0,0 @@ -# =================================================================== -# Frigate Configuration v0.16.3 - Finalny config (Fixed Alerts) -# =================================================================== - -mqtt: - host: 192.168.1.132 - port: 1883 - topic_prefix: frigate - client_id: frigate - user: mosquitto - password: guilermo3 - stats_interval: 60 - -logger: - default: info - logs: - frigate.embeddings: error - frigate.embeddings.onnx: error - -detectors: - coral: - type: edgetpu - device: usb - -ffmpeg: - hwaccel_args: [] - -go2rtc: - streams: - birdseye: - - ffmpeg:birdseye#video=h264#audio=opus - rtsp: - transport: tcp - -birdseye: - enabled: true - width: 1920 - height: 1080 - quality: 8 - mode: continuous - -detect: - enabled: true - width: 640 - height: 360 - fps: 7 - -semantic_search: - enabled: false - model_size: large - -face_recognition: - enabled: false - model_size: large - -lpr: - enabled: false - -cameras: - frigate_reolink_1: - ffmpeg: - inputs: - - path: - rtsp://admin:{FRIGATE_REO_PW}@192.168.1.101:554/h264Preview_01_main - roles: [record] - - path: - rtsp://admin:{FRIGATE_REO_PW}@192.168.1.101:554/h264Preview_01_sub - roles: [detect] - detect: - enabled: true - width: 640 - height: 360 - fps: 8 - objects: - track: [person, car] - filters: - person: - # POPRAWKA: Zmniejszono min_area i usunięto max_area, aby lepiej wykrywać ludzi - min_area: 1500 - min_score: 0.75 - car: {min_area: 3000, min_score: 0.80} - motion: - threshold: 25 - contour_area: 80 - improve_contrast: true - mask: - 0.002,0.004,0.002,0.706,0.417,0.539,0.32,0.241,0.377,0.185,0.673,0.372,0.996,0.538,0.997,0.009 - zones: - parking: {coordinates: '0.443,0.568,0.445,1,1,1,0.994,0.57,0.581,0.333', - inertia: 3, loitering_time: 5} - driveway: {coordinates: '0.325,0.252,0.423,0.535,0.453,0.529,0.575,0.322,0.437,0.232,0.38,0.201', - loitering_time: 5, inertia: 2} - r1_my-garden: - coordinates: 0.005,0.717,0.435,0.547,0.44,1,0,1 - loitering_time: 0 - objects: - - person - inertia: 1 - snapshots: {enabled: true, timestamp: false, bounding_box: true} - record: - enabled: true - retain: {days: 10, mode: motion} - alerts: {retain: {days: 14}} - detections: {retain: {days: 14}} - review: - alerts: - labels: [person, car] - required_zones: [] - detections: - labels: [person, car] - required_zones: [] - - frigate_reolink_3: - ffmpeg: - inputs: - - path: rtsp://admin:{FRIGATE_REO_PW}@192.168.1.150:554/Preview_01_main - roles: [record] - - path: rtsp://admin:{FRIGATE_REO_PW}@192.168.1.150:554/Preview_01_sub - roles: [detect] - detect: - enabled: true - width: 640 - height: 360 - fps: 7 - max_disappeared: 30 - stationary: {interval: 40, threshold: 30} - objects: - track: [person, car] - filters: - person: {min_area: 2000, max_area: 20000, min_score: 0.73} - car: {min_area: 4000, min_score: 0.7} - motion: - threshold: 25 - contour_area: 80 - improve_contrast: true - mask: - 0.001,0.673,0.005,0.009,0.935,0.012,0.929,0.345,0.932,0.417,0.776,0.247,0.691,0.156,0.448,0.028 - zones: - terrace: - coordinates: 0,0.966,0.149,0.898,0.275,0.625,0.46,1,0,1 - inertia: 3 - loitering_time: 10 - kennel: - coordinates: 0,0.729,0.108,0.531,0.152,0.75,0.041,0.9 - inertia: 4 - loitering_time: 0 - objects: - - person - my-garden: - coordinates: - 0.179,0.799,0.135,0.547,0.451,0.036,0.685,0.157,0.932,0.433,0.992,0.978,0.482,0.991,0.39,0.852,0.274,0.606 - loitering_time: 0 - snapshots: {enabled: true, timestamp: false, bounding_box: true} - record: - enabled: true - retain: {days: 10, mode: motion} - alerts: {retain: {days: 14}} - detections: {retain: {days: 14}} - review: - alerts: - labels: [person] - required_zones: [] - detections: - labels: [person] - required_zones: [terrace] - - frigate_reolink_4: - ffmpeg: - inputs: - - path: rtsp://admin:{FRIGATE_REO_PW}@192.168.1.168:554/Preview_01_main - roles: [record] - - path: rtsp://admin:{FRIGATE_REO_PW}@192.168.1.168:554/Preview_01_sub - roles: [detect] - detect: - enabled: true - width: 640 - height: 360 - fps: 7 - max_disappeared: 30 - stationary: {interval: 40, threshold: 30} - objects: - track: [person, car] - filters: - person: {min_area: 1800, min_score: 0.78, mask: '0,0,1,0,1,0.15,0.7,0.15,0.5,0.12,0.32,0.05,0,0.05'} - car: {min_area: 3000, min_score: 0.7} - motion: - threshold: 25 - contour_area: 80 - improve_contrast: true - mask: 0,0,1,0,1,0.804,0.692,0.318,0.558,0.13,0.32,0.05,0,0.152 - zones: - main_area: {coordinates: '0.37,0.091,0.519,0.139,0.822,1,0.199,1', - loitering_time: 10, inertia: 3} - entrance: {coordinates: '0.415,0.169,0.491,0.253,0.515,0.432,0.439,0.661', - objects: [person], inertia: 3, loitering_time: 0} - snapshots: {enabled: true, timestamp: false, bounding_box: true} - record: - enabled: true - retain: {days: 2, mode: motion} - alerts: {retain: {days: 2}} - detections: {retain: {days: 2}} - review: - alerts: - labels: [person] - required_zones: [] - detections: - labels: [person] - required_zones: [] - - frigate_hikvision_4: - ffmpeg: - inputs: - - path: rtsp://admin:{FRIGATE_DOMOFON_HIK_PW}@192.168.1.127:554/Streaming/Channels/101 - roles: [record] - - path: rtsp://admin:{FRIGATE_DOMOFON_HIK_PW}@192.168.1.127:554/Streaming/Channels/102 - roles: [detect] - detect: - enabled: true - width: 640 - height: 360 - fps: 10 - motion: {threshold: 50, contour_area: 40, improve_contrast: true, lightning_threshold: 0.8} - objects: - track: [person] - filters: - person: {min_score: 0.67, threshold: 0.70, min_area: 600, max_area: 250000, min_ratio: 0.20, max_ratio: 0.85} - snapshots: {enabled: true, timestamp: false, bounding_box: true} - record: - enabled: true - retain: {days: 10, mode: motion} - alerts: {retain: {days: 10}} - detections: {retain: {days: 10}} - zones: - street-left: - coordinates: 0.906,0.491,0.733,0.482,0.755,0.59,0.805,0.66,1,0.694,0.997,0.624,0.997,0.49 - objects: [person] - inertia: 2 - loitering_time: 0 - robo: - coordinates: 0.513,0.371,0.504,0.796,0.655,0.867,0.821,0.814,0.904,0.777,0.952,0.689,0.806,0.675,0.75,0.594,0.717,0.42 - objects: [person] - loitering_time: 0 - inertia: 3 - at-gate: - coordinates: 0,0,1,0,1,0.482,0.733,0.478,0.73,0.429,0.517,0.36,0.507,0.358,0.498,0.801,0.653,0.876,0.819,0.823,0.907,0.781,0.953,0.695,0.998,0.704,1,1,0,1 - objects: [person] - loitering_time: 0 - inertia: 3 - review: - alerts: - labels: [person] - required_zones: [] - detections: - labels: [person, car] - required_zones: [] - - frigate_reolink_2: - ffmpeg: - inputs: - - path: - rtsp://admin:{FRIGATE_REO_PW}@192.168.1.62:554/h264Preview_01_sub - roles: [detect, record] - detect: - enabled: true - width: 640 - height: 480 - fps: 8 - max_disappeared: 30 - stationary: {interval: 40, threshold: 30} - objects: - track: [person] - filters: - person: {min_area: 1000, min_score: 0.72} - car: {min_area: 2500, min_score: 0.7} - motion: - threshold: 22 - contour_area: 80 - improve_contrast: true - mask: 0.006,0.004,0,0.415,0.223,0.323,0.338,0.228,0.552,0.078,0.621,0 - zones: - my_sidewalk: - coordinates: - 0,0.415,0.225,0.325,0.303,0.265,0.551,0.082,0.621,0,0.7,0,0.973,1,0,1 - inertia: 3 - loitering_time: 8 - snapshots: {enabled: true, timestamp: false, bounding_box: true} - record: - enabled: true - retain: {days: 10, mode: motion} - alerts: {retain: {days: 10}} - detections: {retain: {days: 10}} - review: - alerts: - labels: [person] - required_zones: [] - detections: - labels: [person] - required_zones: [] - - frigate_reolink_5: - ffmpeg: - inputs: - - path: rtsp://admin:{FRIGATE_REO_PW}@192.168.1.172:554/Preview_01_sub - roles: [detect, record] - detect: - enabled: true - width: 640 - height: 360 - fps: 6 - max_disappeared: 30 - stationary: {interval: 40, threshold: 30} - objects: - track: [person, car] - filters: - person: {min_area: 1800, min_score: 0.75, mask: '0,0.95,0.12,0.72,0.2,0.4,0.35,0.22,0.5,0.18,0.85,0.22,1,0,0,0'} - car: {min_area: 3000, min_score: 0.7} - motion: - threshold: 25 - contour_area: 80 - improve_contrast: true - mask: 0,0.95,0.12,0.72,0.2,0.4,0.35,0.22,0.5,0.18,0.85,0.22,1,0,0,0 - zones: - terrace: - coordinates: - 0.442,0.214,0.626,0.218,0.669,0.215,0.819,0.243,0.788,0.669,0.763,1,0.038,1,0.131,0.775,0.238,0.465 - inertia: 3 - loitering_time: 10 - snapshots: {enabled: true, timestamp: false, bounding_box: true} - record: - enabled: true - retain: {days: 2, mode: motion} - alerts: {retain: {days: 2}} - detections: {retain: {days: 2}} - review: - alerts: - labels: [person] - required_zones: [] - detections: - labels: [person] - required_zones: [] - - frigate_hikvision_2: - ffmpeg: - inputs: - - path: - rtsp://admin:{FRIGATE_HIK_PW}@192.168.1.65:554/Streaming/Channels/102/ - roles: [detect, record] - detect: - enabled: true - width: 640 - height: 360 - fps: 7 - objects: - track: [person, car] - filters: - person: - min_score: 0.74 - mask: - 0.139,1,0.091,0.685,0.658,0.48,0.635,0.331,1,0.272,0.996,0.007,0.015,0.014,0.028,0.718 - car: - min_score: 0.8 - mask: 0.646,0.335,0.668,0.522,0.858,0.473,0.844,0.302 - motion: {threshold: 25, contour_area: 150, improve_contrast: false} - snapshots: {enabled: true, timestamp: false, bounding_box: true} - zones: - my-garden: - coordinates: - 0.651,0.477,0.404,0.577,0.421,0.666,0.094,0.795,0.054,0.99,0.902,0.997,0.859,0.42,0.845,0.278,0.622,0.298 - inertia: 2 - loitering_time: 0 - objects: - - person - record: - enabled: true - retain: {days: 10, mode: motion} - alerts: {retain: {days: 10}} - detections: {retain: {days: 10}} - review: - alerts: - labels: [person] - required_zones: [] - detections: - labels: [person] - required_zones: [] - - mercusys_mc200: - ffmpeg: - inputs: - - path: - rtsp://{FRIGATE_MERCUSYS_USER}:{FRIGATE_MERCUSYS_PW}@192.168.20.33:554/stream2 - roles: [detect, record] - detect: - enabled: true - width: 640 - height: 360 - fps: 6 - objects: - track: [person, cat] - filters: - person: {min_area: 1500, max_area: 20000, min_score: 0.75} - cat: {min_area: 300, max_area: 6000, min_score: 0.73} - motion: {threshold: 25, contour_area: 80, improve_contrast: true} - record: - enabled: true - retain: {days: 7, mode: motion} - alerts: {retain: {days: 7}} - detections: {retain: {days: 7}} - snapshots: - enabled: true - timestamp: true - retain: {default: 3} - review: - alerts: - labels: [person, cat] - required_zones: [] - detections: - labels: [person, cat] - required_zones: [] - -snapshots: - retain: - default: 365 - objects: - person: 3650 - car: 180 - quality: 90 - clean_copy: true -version: 0.16-0 diff --git a/ha-dashboard/app.py b/ha-dashboard/app.py index 5fac622..1a8cc7b 100644 --- a/ha-dashboard/app.py +++ b/ha-dashboard/app.py @@ -732,6 +732,111 @@ def api_zigbee(): return jsonify({"error": str(e)}), 500 +@app.route("/niepalenie") +def niepalenie(): + return render_template("niepalenie.html") + + +# ── Rzucanie - persistent profiles ──────────────────────────────────────────── + +import sqlite3 as _sqlite3 +import secrets as _secrets + +_RZ_DB = os.path.join(AUTH_DIR, "rzucanie.db") + + +def _rz_conn(): + conn = _sqlite3.connect(_RZ_DB) + conn.row_factory = _sqlite3.Row + return conn + + +def _rz_init(): + with _rz_conn() as c: + c.execute(""" + CREATE TABLE IF NOT EXISTS profiles ( + token TEXT PRIMARY KEY, + name TEXT DEFAULT '', + dziennie REAL NOT NULL, + paczka REAL NOT NULL DEFAULT 20, + cena REAL NOT NULL, + rzucone TEXT NOT NULL, + created_at TEXT NOT NULL + ) + """) + + +_rz_init() + + +def _rz_get(token): + with _rz_conn() as c: + row = c.execute("SELECT * FROM profiles WHERE token = ?", (token,)).fetchone() + return dict(row) if row else None + + +@app.route("/rzucanie", methods=["GET", "POST"]) +def rzucanie_setup(): + if request.method == "POST": + try: + dziennie = float(request.form["dziennie"]) + paczka = float(request.form["paczka"]) + cena = float(request.form["cena"]) + rzucone = request.form["rzucone"] # datetime-local string + name = request.form.get("name", "").strip() + except (KeyError, ValueError): + return render_template("rzucanie.html", config=None, error="Uzupełnij wszystkie pola.") + token = _secrets.token_urlsafe(8) + with _rz_conn() as c: + c.execute( + "INSERT INTO profiles (token,name,dziennie,paczka,cena,rzucone,created_at) VALUES (?,?,?,?,?,?,?)", + (token, name, dziennie, paczka, cena, rzucone, datetime.utcnow().isoformat()), + ) + return redirect(url_for("rzucanie_dash", token=token)) + return render_template("rzucanie.html", config=None, error=None) + + +@app.route("/rzucanie/", methods=["GET"]) +def rzucanie_dash(token): + config = _rz_get(token) + if not config: + return redirect(url_for("rzucanie_setup")) + return render_template("rzucanie.html", config=config, token=token, error=None) + + +@app.route("/api/rzucanie/profiles") +@login_required +def rzucanie_profiles(): + with _rz_conn() as c: + rows = c.execute( + "SELECT token, name, dziennie, cena, rzucone, created_at FROM profiles ORDER BY created_at DESC" + ).fetchall() + return jsonify([dict(r) for r in rows]) + + +@app.route("/api/rzucanie/", methods=["DELETE"]) +@login_required +def rzucanie_delete(token): + with _rz_conn() as c: + c.execute("DELETE FROM profiles WHERE token = ?", (token,)) + return jsonify({"ok": True}) + + +@app.route("/api/rzucanie/", methods=["POST"]) +def rzucanie_update(token): + if not _rz_get(token): + return jsonify({"error": "not found"}), 404 + data = request.get_json(force=True) + fields = {k: data[k] for k in ("name", "dziennie", "paczka", "cena", "rzucone") if k in data} + if not fields: + return jsonify({"error": "no fields"}), 400 + set_clause = ", ".join(f"{k} = ?" for k in fields) + with _rz_conn() as c: + c.execute(f"UPDATE profiles SET {set_clause} WHERE token = ?", + (*fields.values(), token)) + return jsonify(_rz_get(token)) + + @app.route("/") @login_required def index(): diff --git a/ha-dashboard/bin/rzucanie-delete b/ha-dashboard/bin/rzucanie-delete new file mode 100755 index 0000000..bfd2520 --- /dev/null +++ b/ha-dashboard/bin/rzucanie-delete @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# Usuwa profil rzucania palenia po tokenie +# Użycie: rzucanie-delete TOKEN [URL] +# Zmienne środowiskowe: HADASH_USER, HADASH_PASS, HADASH_URL + +set -euo pipefail + +TOKEN="${1:-}" +URL="${2:-${HADASH_URL:-http://localhost:9081}}" +USER="${HADASH_USER:-}" +PASS="${HADASH_PASS:-}" +COOKIE=$(mktemp /tmp/hadash-cookie.XXXXXX) +trap 'rm -f "$COOKIE"' EXIT + +if [[ -z "$TOKEN" ]]; then + echo "Użycie: $(basename "$0") TOKEN [URL]" >&2 + exit 1 +fi + +if [[ -z "$USER" ]]; then + read -rp "Login: " USER +fi +if [[ -z "$PASS" ]]; then + read -rsp "Hasło: " PASS; echo +fi + +# Login (-d implikuje POST; -L śledzi redirect do / już jako GET) +HTTP=$(curl -s -o /dev/null -w "%{http_code}" -L -c "$COOKIE" -b "$COOKIE" \ + "$URL/login" \ + -d "username=${USER}&password=${PASS}") + +if [[ "$HTTP" != "200" ]]; then + echo "Błąd logowania (HTTP $HTTP)" >&2 + exit 1 +fi + +# Usuń profil +RESULT=$(curl -s -b "$COOKIE" -X DELETE "$URL/api/rzucanie/${TOKEN}") + +if echo "$RESULT" | python3 -c "import sys,json; d=json.load(sys.stdin); sys.exit(0 if d.get('ok') else 1)" 2>/dev/null; then + echo "Usunięto profil: $TOKEN" +else + echo "Błąd: $RESULT" >&2 + exit 1 +fi diff --git a/ha-dashboard/bin/rzucanie-list b/ha-dashboard/bin/rzucanie-list new file mode 100755 index 0000000..c94a793 --- /dev/null +++ b/ha-dashboard/bin/rzucanie-list @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# Lista profili rzucania palenia +# Użycie: rzucanie-list [URL] +# Zmienne środowiskowe: HADASH_USER, HADASH_PASS, HADASH_URL + +set -euo pipefail + +URL="${1:-${HADASH_URL:-http://localhost:9081}}" +USER="${HADASH_USER:-}" +PASS="${HADASH_PASS:-}" +COOKIE=$(mktemp /tmp/hadash-cookie.XXXXXX) +trap 'rm -f "$COOKIE"' EXIT + +if [[ -z "$USER" ]]; then + read -rp "Login: " USER +fi +if [[ -z "$PASS" ]]; then + read -rsp "Hasło: " PASS; echo +fi + +# Login (-d implikuje POST; -L śledzi redirect do / już jako GET) +HTTP=$(curl -s -o /dev/null -w "%{http_code}" -L -c "$COOKIE" -b "$COOKIE" \ + "$URL/login" \ + -d "username=${USER}&password=${PASS}") + +if [[ "$HTTP" != "200" ]]; then + echo "Błąd logowania (HTTP $HTTP)" >&2 + exit 1 +fi + +# Pobierz profile +curl -s -b "$COOKIE" "$URL/api/rzucanie/profiles" | python3 -c " +import sys, json +from datetime import datetime, timezone + +profiles = json.load(sys.stdin) +if 'error' in profiles: + print('Błąd:', profiles['error']) + sys.exit(1) + +if not profiles: + print('Brak profili.') + sys.exit(0) + +print(f' {len(profiles)} profil(e/ów):') +print() +print(f' {\"TOKEN\":<14} {\"IMIĘ\":<14} {\"RZUCIŁ/A\":<17} {\"REJESTRACJA\":<17} {\"DZIENNIE\":>9} {\"CENA\":>8}') +print(' ' + '-'*90) +for p in profiles: + name = (p.get('name') or '(brak)') [:14] + token = p['token'][:14] + rzucone = p['rzucone'][:16].replace('T', ' ') + created = p['created_at'][:16].replace('T', ' ') + dziennie = f\"{p['dziennie']:.0f} szt.\" + cena = f\"{p['cena']:.2f} zł\" + print(f' {token:<14} {name:<14} {rzucone:<17} {created:<17} {dziennie:>9} {cena:>8}') +" diff --git a/ha-dashboard/templates/niepalenie.html b/ha-dashboard/templates/niepalenie.html new file mode 100644 index 0000000..30d08df --- /dev/null +++ b/ha-dashboard/templates/niepalenie.html @@ -0,0 +1,705 @@ + + + + + + Nie palę 🚭 + + + + + + +
+ ← Dashboard +

🚭 Nie palę

+
+ +
+ + +
+
Rzuciłem palenie
+
23 sierpnia 2011 · z przerwą w 2021
+
+
+
+
lat
+
+
+
+
dni
+
+
+
+
godzin
+
+
+
+
minut
+
+
+
+
sekund
+
+
+
+ + +
+
Zaoszczędzono
+
+
+
złotych
+
+
+
+ + +
+
Nie wypaliłem
+
+
+
papierosów
+
– zł/tyg · – zł/mies
+
+
+ + +
+
Wypaliłem łącznie
+
+
+
papierosów
+
– przed 2011 · – nawrót
+
+
+ + +
+
Następny jubileusz
+
+
+
+
za – dni
+
+
+ + +
+
Kamienie milowe
+
+
+ +
+
–%
+
+
+
+
+ +
+
+
+ + +
+
Oszczędności w czasie (zł)
+
+ +
+
+ + +
+
Szczegóły oszczędności
+
+
Na godzinę
+
Dziennie
+
Tygodniowo
+
Miesięcznie
+
Rocznie
+
Cena paczki (dziś)
+
Paliłem dziennie19 szt.
+
+ Równowartość CZK + +
+
+ kurs: – + +
+
+
+ + +
+
Historia
+
+
+
+
+
+
+
+
19.11.1997
+
Pierwszy papieros
+
Zaczął się nałóg
+
+
+
+
+
+
+
+
+
23.08.2011 · 01:01 UTC
+
🚭 Rzuciłem (1. raz)
+
– dni bez papierosa
+
+
+
+
+
+
+
+
+
01.02.2021
+
💨 Nawrót
+
– dni nawrotu
+
+
+
+
+
+
+
+
+
25.12.2021
+
🚭 Rzuciłem (2. raz)
+
– dni bez papierosa
+
+
+
+
+
+
+
+
dziś
+
🎯 Teraz
+
łącznie – dni
+
+
+
+
+ + +
+
Okresy niepalenia
+
+
+ Okres 1 (2011-2021) + +
+
+ Nawrót (2021) + +
+
+ Okres 2 (2021-teraz) + +
+
+ Łącznie bez papierosa + +
+
+ Palenie ogółem (1997-2011) + – lat +
+
+
+ +
+ + + + diff --git a/ha-dashboard/templates/rzucanie.html b/ha-dashboard/templates/rzucanie.html new file mode 100644 index 0000000..e00738b --- /dev/null +++ b/ha-dashboard/templates/rzucanie.html @@ -0,0 +1,704 @@ + + + + + + Rzuć palenie 🚭 + + + + + + +{% if not config %} + +
+
+
+
🚭
+

Zaplanuj rzucenie palenia

+

Podaj swoje dane - dostaniesz swój prywatny link do śledzenia postępów i motywacji.

+
+ + {% if error %} +
{{ error }}
+ {% endif %} + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + + +
+ +
+ + +
+
+ + + +{% else %} + + + + +
+ +
+
+ 🚭 +

Nie palę!

+
+
+ + +
+
+ +
+ +
+
Hej, Nie palisz od:
+
+
+
0
lat
+
0
dni
+
00
godzin
+
00
minut
+
00
sekund
+
+
+ +
+
Zaoszczędziłam/em
+
+
+
złotych
+
+
+
+ +
+
Nie wypaliłam/em
+
+
+
papierosów
+
+
+
+ +
+
Następny krok zdrowotny
+
+
+
+
+
+ +
+
Prognoza oszczędności
+
+
+
1 rok:
+
+
+
+
+
2 lata:
+
+
+
+
+
5 lat:
+
+
+
+
+
+ +
+
Co dzieje się z Twoim ciałem 🫁
+
+
+ +
+
Oszczędności w czasie (zł)
+
+
+ +
+
Za te pieniądze mogłaś/mogłeś kupić... 💸
+
+
+ +
+
Trudny moment?
+
+
+ Masz ochotę zapalić? Kliknij tutaj
zanim sięgniesz po papierosa. +
+ +
+ Ochota na papierosa trwa zwykle 3-5 minut i mija sama. +
+
+
+ +
+
+ + + + + + + + + + + +{% endif %} + + diff --git a/homeassistant/config/pyscript/stefi_custody_days_calculator.py b/homeassistant/config/pyscript/stefi_custody_days_calculator.py index 0447f76..d3ddb85 100644 --- a/homeassistant/config/pyscript/stefi_custody_days_calculator.py +++ b/homeassistant/config/pyscript/stefi_custody_days_calculator.py @@ -155,8 +155,11 @@ def check_calendar_override(now_dt): # Parsuj czas zakonczenia end_str = ev.get("end", "") if "T" in end_str: - # Timed event: "2026-04-04T09:00:00" + # Timed event: "2026-04-04T09:00:00" or "2026-04-04T09:00:00+02:00" end_dt = datetime.fromisoformat(end_str) + # Google Calendar moze zwracac czas z timezone - ujednolicamy do naive local + if end_dt.tzinfo is not None: + end_dt = end_dt.astimezone().replace(tzinfo=None) else: # All-day: "2026-04-05" oznacza koniec o 00:00 nastepnego dnia # -> ustaw na 23:59:59 dnia poprzedniego (ostatni moment dnia eventu) diff --git a/log-ingester/state/state.json b/log-ingester/state/state.json index 31650cf..5859a37 100644 --- a/log-ingester/state/state.json +++ b/log-ingester/state/state.json @@ -1,13 +1,13 @@ { - "ha_log_pos": 4554206, - "ha_history_last_ts": 1775331867.057616, + "ha_log_pos": 5032700, + "ha_history_last_ts": 1775723911.899412, "frigate_last_id": "1774114966.094129-j8nxv6", - "docker_since_traefik": 1775301395, - "docker_since_homeassistant": 1775332084, - "docker_since_immich_server": 1775305360, - "docker_since_frigate": 1775332093, - "docker_since_ha-dashboard": 1775332095, - "docker_since_esphome": 1775332092, - "docker_since_mosquitto": 1774010092, - "docker_since_crowdsec": 1775332092 + "docker_since_traefik": 1775724569, + "docker_since_homeassistant": 1775728798, + "docker_since_immich_server": 1775723966, + "docker_since_frigate": 1775459569, + "docker_since_ha-dashboard": 1775461944, + "docker_since_esphome": 1775728798, + "docker_since_mosquitto": 1775461949, + "docker_since_crowdsec": 1775461949 } \ No newline at end of file diff --git a/prowlarr/Definitions/1337x.yml b/prowlarr/Definitions/1337x.yml index 1d64331..2f8bd72 100644 --- a/prowlarr/Definitions/1337x.yml +++ b/prowlarr/Definitions/1337x.yml @@ -5,7 +5,7 @@ description: "1337x is a Public torrent site that offers verified torrent downlo language: en-US type: public encoding: UTF-8 -requestDelay: 2 +requestDelay: 3 # get status and news on domains at the official site https://1337x-status.org/ links: - https://1337x.to/ diff --git a/prowlarr/Definitions/concertos.yml b/prowlarr/Definitions/concertos.yml index cad9cf2..a6ccb65 100644 --- a/prowlarr/Definitions/concertos.yml +++ b/prowlarr/Definitions/concertos.yml @@ -13,6 +13,7 @@ caps: - {id: 1, cat: TV, desc: "Live Concert"} - {id: 2, cat: Audio/Video, desc: "Music Video"} - {id: 4, cat: TV, desc: "Music Documentary"} + - {id: 11, cat: TV, desc: "Music TV Series"} - {id: 5, cat: Audio/Lossless, desc: "Pure Audio"} - {id: 7, cat: TV, desc: "Opera"} - {id: 8, cat: TV, desc: "Musical"} diff --git a/prowlarr/Definitions/f1gp.yml b/prowlarr/Definitions/f1gp.yml index 7096c01..51bb071 100644 --- a/prowlarr/Definitions/f1gp.yml +++ b/prowlarr/Definitions/f1gp.yml @@ -155,6 +155,6 @@ search: minimumratio: text: 1.4 minimumseedtime: - # 7 days (as seconds = 7 x 24 x 60 x 60) - text: 604800 + # 5 days (as seconds = 5 x 24 x 60 x 60) + text: 432000 # xbtitFM v4.1.32 diff --git a/prowlarr/Definitions/lastfiles.yml b/prowlarr/Definitions/lastfiles.yml index 6587b9e..e2f45d1 100644 --- a/prowlarr/Definitions/lastfiles.yml +++ b/prowlarr/Definitions/lastfiles.yml @@ -144,43 +144,43 @@ search: attribute: src case: # not all categories are available at the moment, so some are deduced and unconfirmed - img[src$="/anime.png"]: 1 - img[src$="/anime-ro.png"]: 2 - img[src$="/bluray.png"]: 5 - img[src$="/bluray-ro.png"]: 6 - img[src$="/dvd.png"]: 9 - img[src$="/dvd-ro.png"]: 10 - img[src$="/hd.png"]: 11 - img[src$="/hd-ro.png"]: 12 - img[src$="/hdtve.png"]: 13 - img[src$="/hdtve-ro.png"]: 14 - img[src$="/oldies.png"]: 16 - img[src$="/oldies-ro.png"]: 17 - img[src$="/pack.png"]: 18 - img[src$="/pack-ro.png"]: 19 - img[src$="/tvepisode.png"]: 20 - img[src$="/tvepisode-ro.png"]: 21 - img[src$="/rocontent.png"]: 22 - img[src$="/xvid.png"]: 24 - img[src$="/xvid-ro.png"]: 25 - img[src$="/soft.png"]: 26 - img[src$="/xxx.jpeg"]: 27 - img[src$="/music.png"]: 28 - img[src$="/pciso.png"]: 30 - img[src$="/4k.png"]: 31 - img[src$="/4kRO.png"]: 32 - img[src$="/gpack.png"]: 33 - img[src$="/xxxpack.jpeg"]: 34 - img[src$="/sport.png"]: 42 - img[src$="/docs.png"]: 43 - img[src$="/images.png"]: 44 - img[src$="/misc.png"]: 49 - img[src$="/web-DL.png"]: 54 - img[src$="/web-DLRO.png"]: 55 - img[src$="/doc.png"]: 56 - img[src$="/doc-ro.png"]: 57 - img[src$="/x265ro.png"]: 81 - img[src$="/x265.png"]: 82 + img[src*="/anime.png"]: 1 + img[src*="/anime-ro.png"]: 2 + img[src*="/bluray.png"]: 5 + img[src*="/bluray-ro.png"]: 6 + img[src*="/dvd.png"]: 9 + img[src*="/dvd-ro.png"]: 10 + img[src*="/hd.png"]: 11 + img[src*="/hd-ro.png"]: 12 + img[src*="/hdtve.png"]: 13 + img[src*="/hdtve-ro.png"]: 14 + img[src*="/oldies.png"]: 16 + img[src*="/oldies-ro.png"]: 17 + img[src*="/pack.png"]: 18 + img[src*="/pack-ro.png"]: 19 + img[src*="/tvepisode.png"]: 20 + img[src*="/tvepisode-ro.png"]: 21 + img[src*="/rocontent.png"]: 22 + img[src*="/xvid.png"]: 24 + img[src*="/xvid-ro.png"]: 25 + img[src*="/soft.png"]: 26 + img[src*="/xxx.jpeg"]: 27 + img[src*="/music.png"]: 28 + img[src*="/pciso.png"]: 30 + img[src*="/4k.png"]: 31 + img[src*="/4kRO.png"]: 32 + img[src*="/gpack.png"]: 33 + img[src*="/xxxpack.jpeg"]: 34 + img[src*="/sport.png"]: 42 + img[src*="/docs.png"]: 43 + img[src*="/images.png"]: 44 + img[src*="/misc.png"]: 49 + img[src*="/web-DL.png"]: 54 + img[src*="/web-DLRO.png"]: 55 + img[src*="/doc.png"]: 56 + img[src*="/doc-ro.png"]: 57 + img[src*="/x265ro.png"]: 81 + img[src*="/x265.png"]: 82 title: selector: a[href*="/torrents/"] details: diff --git a/prowlarr/Definitions/magnetcat.yml b/prowlarr/Definitions/magnetcat.yml index f3a6dce..695fc18 100644 --- a/prowlarr/Definitions/magnetcat.yml +++ b/prowlarr/Definitions/magnetcat.yml @@ -9,16 +9,12 @@ encoding: UTF-8 links: - https://magnetcatcat.com/ - https://clmclm.com/ - - https://www.8800552.xyz/ - - https://www.8800554.xyz/ - - https://www.8800556.xyz/ - - https://www.8800557.xyz/ + - https://www.8800558.xyz/ + - https://www.8800559.xyz/ + - https://www.8800560.xyz/ + - https://www.8800561.xyz/ legacylinks: - https://www.clm472.sbs/ - - https://www.8800536.xyz/ - - https://www.8800537.xyz/ - - https://www.8800538.xyz/ - - https://www.8800539.xyz/ - https://www.8800540.xyz/ - https://www.8800541.xyz/ - https://www.8800542.xyz/ @@ -31,6 +27,10 @@ legacylinks: - https://www.8800549.xyz/ - https://www.8800550.xyz/ - https://www.8800551.xyz/ + - https://www.8800552.xyz/ + - https://www.8800554.xyz/ + - https://www.8800556.xyz/ + - https://www.8800557.xyz/ caps: categorymappings: diff --git a/prowlarr/Definitions/nexum-api.yml b/prowlarr/Definitions/nexum-api.yml index 90ecec0..a0abf3a 100644 --- a/prowlarr/Definitions/nexum-api.yml +++ b/prowlarr/Definitions/nexum-api.yml @@ -17,6 +17,7 @@ caps: - {id: 3, cat: TV/Documentary, desc: "Documentaires"} - {id: 4, cat: TV/Anime, desc: "Animés"} - {id: 5, cat: TV/Other, desc: "Concerts / Spectacles"} + - {id: 7, cat: TV/Sport, desc: "Sports"} modes: search: [q] diff --git a/prowlarr/Definitions/nortorrent.yml b/prowlarr/Definitions/nortorrent.yml index e0ce86b..187cc07 100644 --- a/prowlarr/Definitions/nortorrent.yml +++ b/prowlarr/Definitions/nortorrent.yml @@ -10,7 +10,7 @@ type: public encoding: UTF-8 # to fetch current domain use https://www.rantop.org/ links: - - https://www.nortorrent.org/ + - https://www.nortorrent.cc/ legacylinks: # latest domains list - https://www.rantop.xyz/ @@ -29,7 +29,6 @@ legacylinks: - https://www.torrent-site.com/ - https://www.rantop.org/ # actual legacylinks - - https://www.nortorrent4.com/ - https://www.nortorrent5.com/ - https://www.nortorrent6.com/ - https://www.nortorrent.net/ @@ -44,6 +43,7 @@ legacylinks: - https://www-nortorrent.com/ - https://ww2-nortorrent.com/ - https://ww3-nortorrent.com/ + - https://www.nortorrent.org/ caps: categorymappings: diff --git a/prowlarr/Definitions/p2pbg.yml b/prowlarr/Definitions/p2pbg.yml index 51b1810..a01406e 100644 --- a/prowlarr/Definitions/p2pbg.yml +++ b/prowlarr/Definitions/p2pbg.yml @@ -137,7 +137,7 @@ search: by: "{{ .Config.type }}" rows: - selector: table.lista[width="100%"] tr > td > table.lista[width="100%"] tr:has(a[href*="category="]) + selector: table.torrent-index__table:not(table.torrent-index__table--recommended) > tbody > tr.torrent-index__row fields: category: diff --git a/prowlarr/Definitions/scenetime-api.yml b/prowlarr/Definitions/scenetime-api.yml new file mode 100644 index 0000000..958dd0b --- /dev/null +++ b/prowlarr/Definitions/scenetime-api.yml @@ -0,0 +1,139 @@ +--- +id: scenetime-api +name: SceneTime (API) +description: "SceneTime is a Private Torrent Tracker for MOVIES / TV / GENERAL" +language: en-US +type: private +encoding: UTF-8 +links: + - https://www.scenetime.com/ + +caps: + categorymappings: + # from https://www.scenetime.com/rss.php?apikey=YOUR-API-KEY&t=caps + - {id: 1000, cat: Console, desc: "Console"} + - {id: 100015, cat: Console/Other, desc: "Games Dreamcast"} + - {id: 100055, cat: Console/NDS, desc: "Games Nintendo"} + - {id: 100012, cat: Console/PS4, desc: "Games PS"} + - {id: 100051, cat: Console/Wii, desc: "Games Wii"} + - {id: 100048, cat: Console/XBox, desc: "Games XBOX"} + - {id: 2000, cat: Movies, desc: "Movies"} + - {id: 100047, cat: Movies, desc: "Movie Packs"} + - {id: 100064, cat: Movies/3D, desc: "Movies 3D"} + - {id: 100082, cat: Movies/Other, desc: "Movies CAM/TS"} + - {id: 100059, cat: Movies/HD, desc: "Movies HD"} + - {id: 100057, cat: Movies/SD, desc: "Movies SD"} + - {id: 100016, cat: Movies/UHD, desc: "Movies UHD"} + - {id: 3000, cat: Audio, desc: "Audio"} + - {id: 100004, cat: Audio, desc: "Music"} + - {id: 100116, cat: Audio, desc: "Music Packs"} + - {id: 4000, cat: PC, desc: "PC"} + - {id: 100006, cat: PC/Games, desc: "Games PC-ISO"} + - {id: 100053, cat: PC/0day, desc: "Apps"} + - {id: 100052, cat: PC/Mac, desc: "Mac/Linux"} + - {id: 5000, cat: TV, desc: "TV"} + - {id: 100001, cat: TV/Anime, desc: "TV ANIME"} + - {id: 100009, cat: TV/HD, desc: "TV HD"} + - {id: 100043, cat: TV, desc: "TV Packs"} + - {id: 100077, cat: TV/SD, desc: "TV SD"} + - {id: 100002, cat: TV/UHD, desc: "TV UHD"} + - {id: 6000, cat: XXX, desc: "XXX"} + - {id: 100010, cat: XXX, desc: "Movies Adult"} + - {id: 7000, cat: Books, desc: "Books"} + - {id: 100007, cat: Books, desc: "Books and Magazines"} + - {id: 100065, cat: Books/Comics, desc: "Comic Books"} + - {id: 8000, cat: Other, desc: "Other"} + - {id: 100024, cat: PC/Mobile-Other, desc: "Mobile Apps"} + + modes: + search: [q] + tv-search: [q, season, ep, imdbid] + movie-search: [q, imdbid] + music-search: [q] + book-search: [q] + +settings: + - name: apikey + type: text + label: API Key + - name: info_key + type: info + label: About your API key + default: "You can find your API Key by accessing the SceneTime RSS page, scrolling down to the Torznab API section, and copying the API Key." + - name: freeleech + type: checkbox + label: Search freeleech only + default: false + - name: info_activity + type: info + label: Account Inactivity + default: "Unused accounts (accounts with no activity) and inactive unparked accounts may be deleted." + +login: + # returns "401 Unauthorized, check your credentials" if passkey is invalid + path: "rss.php?apikey={{ .Config.apikey }}&view=html" + method: get + +search: + paths: + - path: rss.php + response: + type: xml + + inputs: + apikey: "{{ .Config.apikey }}" + t: "{{ if or .Query.Season .Query.Ep }}tvsearch{{ else }}search{{ end }}" + q: "{{ .Keywords }}" + cat: "{{ join .Categories \",\" }}" + season: "{{ .Query.Season }}" + ep: "{{ .Query.Ep }}" + imdbid: "{{ .Query.IMDBID }}" + limit: 100 + downloadvolumefactor: "{{ if .Config.freeleech }}0{{ else }}{{ end }}" + + rows: + selector: rss > channel > item + + fields: + category: + selector: '[name="category"]' + attribute: value + title: + selector: title + details: + selector: link + download: + selector: enclosure + attribute: url + imdbid: + selector: '[name="imdbid"]' + attribute: value + date: + # Tue, 07 Apr 2026 20:41:08 +0000 + selector: pubDate + filters: + - name: dateparse + args: "ddd, dd MMM yyyy HH:mm:ss zzz" + size: + selector: size + seeders: + selector: '[name="seeders"]' + attribute: value + leechers: + selector: '[name="leechers"]' + attribute: value + grabs: + selector: '[name="grabs"]' + attribute: value + downloadvolumefactor: + selector: '[name="downloadvolumefactor"]' + attribute: value + uploadvolumefactor: + selector: '[name="uploadvolumefactor"]' + attribute: value + minimumratio: + text: 1.0 + minimumseedtime: + # 3 days (as seconds = 3 x 24 x 60 x 60) + text: 259200 +# torznab xml diff --git a/prowlarr/Definitions/unionfansub.yml b/prowlarr/Definitions/unionfansub.yml index 576b46b..1c95bfb 100644 --- a/prowlarr/Definitions/unionfansub.yml +++ b/prowlarr/Definitions/unionfansub.yml @@ -63,17 +63,17 @@ settings: asc: asc login: - path: https://foro.unionfansub.com/member.php - method: post + path: https://foro.unionfansub.com/member.php?action=login + method: form + form: form[action="member.php"] inputs: - quick_username: "{{ .Config.username }}" - quick_password: "{{ .Config.password }}" + username: "{{ .Config.username }}" + password: "{{ .Config.password }}" action: do_login url: "{{ .Config.sitelink }}" - quick_login: 1 - quick_remember: yes + remember: yes error: - - selector: div.error + - selector: td:contains("usuario/contraseña inválida") test: path: / selector: div#statusbar diff --git a/prowlarr/Definitions/zktorrent.yml b/prowlarr/Definitions/zktorrent.yml index a36e14c..094c581 100644 --- a/prowlarr/Definitions/zktorrent.yml +++ b/prowlarr/Definitions/zktorrent.yml @@ -9,7 +9,7 @@ type: public encoding: UTF-8 # to fetch current domain use https://www.rantop.org/ links: - - https://www.zktorrent.org/ + - https://www.zktorrent.cc/ legacylinks: # latest domains list - https://www.rantop.xyz/ @@ -28,7 +28,6 @@ legacylinks: - https://www.torrent-site.com/ - https://www.rantop.org/ # actual legacylinks - - https://www.zktorrent4.com/ - https://www.zktorrent5.com/ - https://www.zktorrent.net/ - https://www.zktorrent6.com/ @@ -43,6 +42,7 @@ legacylinks: - https://www-zktorrent.com/ - https://ww2-zktorrent.com/ - https://www.zktorrent.com/ + - https://www.zktorrent.org/ caps: categorymappings: diff --git a/timelapse_service/timelapse_maker.py.bak b/timelapse_service/timelapse_maker.py.bak deleted file mode 100644 index 3b2de39..0000000 --- a/timelapse_service/timelapse_maker.py.bak +++ /dev/null @@ -1,174 +0,0 @@ -import argparse -import os -import subprocess -import logging -import sys -from datetime import datetime, timedelta -from multiprocessing import Pool - -# --- Configuration --- -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(levelname)s - %(message)s', - handlers=[logging.StreamHandler(sys.stdout)] -) - -BASEDIR = "/media/seagata16t/hikvision16t" -TIMELAPSE_BASEDIR = "/media/seagata16t/all_timelapses/" -DIRS = ["reolink_4", "hikvision_2", "reolink_5", "hikvision_4", "reolink_1", "reolink_2", "reolink_3"] -DEFAULT_FPS = 30 -TARGET_LEVEL = "root" - -# --- Helpers --- - -def get_video_duration(filepath): - """Pobiera czas trwania wideo za pomocą ffprobe.""" - cmd = [ - 'ffprobe', '-v', 'error', '-show_entries', 'format=duration', - '-of', 'default=noprint_wrappers=1:nokey=1', filepath - ] - try: - result = subprocess.run(cmd, capture_output=True, text=True, check=True) - return float(result.stdout.strip()) - except Exception: - return 0.0 - -def _chronological_dates(dates): - return sorted(dates) - -# --- Core Functions --- - -def create_from_images(cam_dir, date_list, period_name): - """Tworzy timelapse z surowych zdjęć JPG.""" - tmp_file_path = f'/tmp/{cam_dir}_{period_name}_images.txt' - image_count = 0 - - with open(tmp_file_path, 'w') as f: - for target_date in _chronological_dates(date_list): - source_path = os.path.join(BASEDIR, cam_dir, target_date) - if not os.path.exists(source_path): - continue - - jpgs = sorted([os.path.join(source_path, img) for img in os.listdir(source_path) - if img.lower().endswith(".jpg") and os.path.getsize(os.path.join(source_path, img)) > 0]) - - for jpg_file in jpgs: - f.write(f"file '{jpg_file}'\n") - image_count += 1 - - if image_count == 0: - logging.warning(f"[{cam_dir}] Brak obrazów dla {period_name}") - return - - output_dir = TIMELAPSE_BASEDIR if TARGET_LEVEL == 'root' else os.path.join(TIMELAPSE_BASEDIR, cam_dir) - os.makedirs(output_dir, exist_ok=True) - output_file = os.path.join(output_dir, f"timelapse_{cam_dir}_{period_name}.mp4") - - ffmpeg_cmd = [ - 'ffmpeg', '-f', 'concat', '-safe', '0', '-i', tmp_file_path, - '-r', str(DEFAULT_FPS), '-s', 'hd1080', '-vcodec', 'libx264', - '-pix_fmt', 'yuv420p', '-y', output_file - ] - logging.info(f"[{cam_dir}] Start renderowania {image_count} zdjęć...") - subprocess.run(ffmpeg_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) - -def merge_and_target_duration(cam_dir, date_list, period_name, target_duration_sec): - """Łączy wideo MP4 i przyspiesza do celu.""" - output_dir = TIMELAPSE_BASEDIR if TARGET_LEVEL == 'root' else os.path.join(TIMELAPSE_BASEDIR, cam_dir) - - video_files = [] - total_input_duration = 0.0 - - for target_date in _chronological_dates(date_list): - expected_video = os.path.join(output_dir, f"timelapse_{cam_dir}_{target_date}.mp4") - if os.path.exists(expected_video): - dur = get_video_duration(expected_video) - if dur > 0: - video_files.append(expected_video) - total_input_duration += dur - - if not video_files: - logging.warning(f"[{cam_dir}] Brak plików MP4 w {output_dir}") - return - - speed_multiplier = total_input_duration / float(target_duration_sec) - pts_value = 1 / speed_multiplier - - logging.info(f"[{cam_dir}] Wejście: {total_input_duration:.1f}s | Cel: {target_duration_sec}s | Speed: {speed_multiplier:.2f}x") - - list_file = f'/tmp/{cam_dir}_{period_name}_videos.txt' - with open(list_file, 'w') as f: - for v in video_files: - f.write(f"file '{v}'\n") - - output_file = os.path.join(output_dir, f"timelapse_{cam_dir}_{period_name}.mp4") - - ffmpeg_cmd = [ - 'ffmpeg', '-f', 'concat', '-safe', '0', '-i', list_file, - '-vf', f"setpts={pts_value}*PTS", - '-r', str(DEFAULT_FPS), '-an', - '-vcodec', 'libx264', '-crf', '23', '-preset', 'ultrafast', '-y', - output_file - ] - - subprocess.run(ffmpeg_cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) - logging.info(f"[{cam_dir}] Gotowe: {output_file}") - -# --- Paralelizacja --- - -def camera_worker(args): - cam_dir, date_list, period_name, date_arg = args - try: - if date_arg == 'year': - merge_and_target_duration(cam_dir, date_list, period_name, 300) - elif date_arg == 'month': - merge_and_target_duration(cam_dir, date_list, period_name, 180) - elif date_arg == 'week': - merge_and_target_duration(cam_dir, date_list, period_name, 120) - else: - create_from_images(cam_dir, date_list, period_name) - except Exception as e: - logging.error(f"Błąd w procesie kamery {cam_dir}: {e}") - -def make_timelapses(date_arg): - today = datetime.now() - date_list = [] - - if date_arg == 'today': - date_list = [today.strftime('%Y-%m-%d')] - period_name = date_list[0] - elif date_arg == 'week': - date_list = [(today - timedelta(days=i)).strftime('%Y-%m-%d') for i in range(7)] - period_name = f"week-{today.year}-{today.isocalendar()[1]}" - elif date_arg == 'month': - first = today.replace(day=1).date() - last_prev = first - timedelta(days=1) - date_list = [(last_prev.replace(day=1) + timedelta(days=i)).strftime('%Y-%m-%d') for i in range(last_prev.day)] - period_name = f"month-{last_prev.year}-{last_prev.month:02d}" - elif date_arg == 'year': - ly = today.year - 1 # Szuka 2025 - date_list = [(datetime(ly, 1, 1).date() + timedelta(days=i)).strftime('%Y-%m-%d') for i in range(365)] - period_name = f"year-{ly}" - else: - date_list = [date_arg] - period_name = date_arg - - # Kolejka zadań - tasks = [(cam, date_list, period_name, date_arg) for cam in DIRS] - - # processes=2 - przetwarza 2 kamery naraz (optymalne dla dysku) - with Pool(processes=2) as pool: - pool.map(camera_worker, tasks) - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("date") - args = parser.parse_args() - - start_time = datetime.now() - logging.info(f"Rozpoczęto zadanie: {args.date}") - make_timelapses(args.date) - duration = datetime.now() - start_time - logging.info(f"Zakończono wszystko w czasie: {duration}") - - diff --git a/watchdog.sh b/watchdog.sh new file mode 100755 index 0000000..cfa9983 --- /dev/null +++ b/watchdog.sh @@ -0,0 +1,55 @@ +#!/bin/bash +# Watchdog: sprawdza kontenery compose, restartuje dead/exited, wysyła Telegram + +COMPOSE_DIR="/home/seba/mydocker" +LOG="/var/log/docker-watchdog.log" +STATE_FILE="/tmp/docker-watchdog-state" + +# Telegram - z .env (grep żeby uniknąć nadpisania pustymi duplikatami) +TELEGRAM_BOT_TOKEN=$(grep -m1 '^TELEGRAM_BOT_TOKEN=.\+' "$COMPOSE_DIR/.env" | cut -d= -f2-) +TELEGRAM_CHAT_ID=$(grep -m1 '^TELEGRAM_CHAT_ID=.\+' "$COMPOSE_DIR/.env" | cut -d= -f2-) + +telegram_notify() { + local msg="$1" + curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \ + -d chat_id="${TELEGRAM_CHAT_ID}" \ + -d text="$msg" \ + -d parse_mode="HTML" > /dev/null 2>&1 +} + +cd "$COMPOSE_DIR" || exit 1 + +# Pobierz listę kontenerów i ich stan +declare -A current_states +while IFS= read -r line; do + name=$(echo "$line" | awk '{print $1}') + state=$(echo "$line" | awk '{print $2}') + [ -n "$name" ] && current_states["$name"]="$state" +done < <(docker compose ps -a --format "{{.Name}} {{.State}}" 2>/dev/null | grep -v "^$") + +restarted=() + +for name in "${!current_states[@]}"; do + state="${current_states[$name]}" + if [[ "$state" == "exited" || "$state" == "dead" || "$state" == "created" ]]; then + echo "[$(date)] $name jest $state - restartuję..." >> "$LOG" + docker compose up -d "$name" >> "$LOG" 2>&1 + new_state=$(docker inspect --format '{{.State.Status}}' "$name" 2>/dev/null) + if [[ "$new_state" == "running" ]]; then + echo "[$(date)] $name uruchomiony." >> "$LOG" + restarted+=("$name (był: $state)") + else + echo "[$(date)] $name - nie udało się uruchomić (stan: $new_state)." >> "$LOG" + restarted+=("$name BŁĄD (był: $state, teraz: $new_state)") + fi + fi +done + +if [ ${#restarted[@]} -gt 0 ]; then + msg="🐳 Docker watchdog - acemagic%0A" + msg+="Zrestartowane kontenery:%0A" + for r in "${restarted[@]}"; do + msg+="- $r%0A" + done + telegram_notify "$msg" +fi