Skill: unixweb/photo-webcam (version 1.0.6)

--- name: photo-webcam description: List and snapshot retrieval for webcams (especially foto-webcam.eu). Use this skill when the user types “webcam <number>”, wants to manage a webcam favorites list, or needs a current snapshot JPG from a foto-webcam.eu webcam sent to chat. --- # Photo-Webcam Snapshots Goal: Fetch a current image from a saved favorites list (number -> webcam page) and send it to the user. ## Data source (favorites) Default file in the workspace: - `docs/webcams/favorites-muenchen.json` Format: - `items[].id` (int) - `items[].name` (string) - `items[].page` (URL of the webcam page) - optional `items[].image` (direct image URL) ## Typical user commands - webcam 1 - webcam 3+4+5 - liste - liste webcams - fuege <name> <url> hinzu ## Workflow: webcam N -> send image 1) Load favorites list from docs/webcams/favorites-muenchen.json. 2) Find entry with id equal to N. 3) Fetch snapshot: - If image is set: load that URL directly - Otherwise: derive current 1200 jpg from page URL 4) Save image to /tmp/webcamN.jpg 5) Send image via openclaw CLI: openclaw message send --channel telegram --target <CHAT_ID> --message “Webcam N Name” --media /tmp/webcamN.jpg ## Workflow: webcam 3+4+5 -> multiple images Maximum 6 images per request; ask first if more are requested. Run a separate openclaw call for each ID — never bundle multiple images into a single command or response. Example for “webcam 1+3”: python3 ... --id 1 --out /tmp/webcam1.jpg -> read name from script output openclaw message send --channel telegram --target <CHAT_ID> --message “Webcam 1 <name>” --media /tmp/webcam1.jpg python3 ... --id 3 --out /tmp/webcam3.jpg -> read name from script output openclaw message send --channel telegram --target <CHAT_ID> --message “Webcam 3 <name>” --media /tmp/webcam3.jpg Each openclaw command runs separately. The caption for each image comes exclusively from the script output (field “name”) of the respective call. ## Workflow: liste -> send favorites list Send a plain text list: Webcam 1 Name Webcam 2 Name etc. No formatting, plain text only. ## Resolving the image URL (foto-webcam.eu) For a webcam page like: - `https://www.foto-webcam.eu/webcam/zugspitze/` there is usually a direct “current” image at: - `https://www.foto-webcam.eu/webcam/zugspitze/current/1200.jpg` In practice: fetch the HTML with a browser User-Agent and search for a link matching `.../current/<digits>.jpg`. ## Script Use the script: - `skills/public/foto-webcam/scripts/foto_webcam_snapshot.py` Examples: - Snapshot via favorites ID: - `python3 skills/public/foto-webcam/scripts/foto_webcam_snapshot.py --favorites docs/webcams/favorites-muenchen.json --id 4 --out /tmp/webcam4.jpg` - Snapshot via URL: - `python3 skills/public/foto-webcam/scripts/foto_webcam_snapshot.py --url https://www.foto-webcam.eu/webcam/zugspitze/ --out /tmp/zugspitze.jpg` ## Maintenance / adding webcams - Add a new webcam: append to `favorites-muenchen.json` (new `id`, `name`, `page`). - If a source is unreliable, set `image` to a direct JPG link. Important: chat responses must be plain text only (no Markdown). For audio, use clean speech only (no special characters or formatting). --- > Eine deutsche Version dieser Skill-Beschreibung ist weiter unten zu finden. --- # Foto-Webcam Schnappschüsse (Deutsch) Ziel: Ein aktuelles Bild aus einer gespeicherten Favoritenliste (Nummer → Webcam-Seite) abrufen und an den Benutzer senden. ## Datenquelle (Favoriten) Standarddatei im Workspace: - `docs/webcams/favorites-muenchen.json` Format: - `items[].id` (int) - `items[].name` (string) - `items[].page` (URL der Webcam-Seite) - optional `items[].image` (direkte Bild-URL) ## Typische Benutzerbefehle - webcam 1 - webcam 3+4+5 - liste - liste webcams - fuege <name> <url> hinzu ## Ablauf: webcam N -> Bild senden 1) Favoritenliste aus docs/webcams/favorites-muenchen.json laden. 2) Eintrag mit der entsprechenden ID finden. 3) Schnappschuss abrufen: - Falls `image` gesetzt ist: diese URL direkt laden - Andernfalls: aktuelles 1200-JPG aus der Seiten-URL ableiten 4) Bild unter /tmp/webcamN.jpg speichern 5) Bild per openclaw CLI senden: openclaw message send --channel telegram --target <CHAT_ID> --message "Webcam N Name" --media /tmp/webcamN.jpg ## Ablauf: webcam 3+4+5 -> mehrere Bilder Maximal 6 Bilder pro Anfrage; bei mehr zuerst nachfragen. Für jede ID einen separaten openclaw-Aufruf ausführen — niemals mehrere Bilder in einem einzigen Befehl oder einer Antwort bündeln. Beispiel für „webcam 1+3": python3 ... --id 1 --out /tmp/webcam1.jpg -> Name aus Script-Ausgabe lesen openclaw message send --channel telegram --target <CHAT_ID> --message "Webcam 1 <name>" --media /tmp/webcam1.jpg python3 ... --id 3 --out /tmp/webcam3.jpg -> Name aus Script-Ausgabe lesen openclaw message send --channel telegram --target <CHAT_ID> --message "Webcam 3 <name>" --media /tmp/webcam3.jpg Jeder openclaw-Befehl läuft separat. Die Bildunterschrift kommt ausschließlich aus der Script-Ausgabe (Feld „name") des jeweiligen Aufrufs. ## Ablauf: liste -> Favoritenliste senden Einfache Textliste senden: Webcam 1 Name Webcam 2 Name usw. Keine Formatierung, nur reiner Text. ## Bild-URL auflösen (foto-webcam.eu) Für eine Webcam-Seite wie: - `https://www.foto-webcam.eu/webcam/zugspitze/` gibt es normalerweise ein aktuelles Bild unter: - `https://www.foto-webcam.eu/webcam/zugspitze/current/1200.jpg` In der Praxis: HTML mit Browser-User-Agent abrufen und nach einem Link suchen, der auf `.../current/<Ziffern>.jpg` passt. ## Script Das Script verwenden: - `skills/public/foto-webcam/scripts/foto_webcam_snapshot.py` Beispiele: - Schnappschuss per Favoriten-ID: - `python3 skills/public/foto-webcam/scripts/foto_webcam_snapshot.py --favorites docs/webcams/favorites-muenchen.json --id 4 --out /tmp/webcam4.jpg` - Schnappschuss per URL: - `python3 skills/public/foto-webcam/scripts/foto_webcam_snapshot.py --url https://www.foto-webcam.eu/webcam/zugspitze/ --out /tmp/zugspitze.jpg` ## Pflege / Webcams hinzufügen - Neue Webcam hinzufügen: Eintrag in `favorites-muenchen.json` ergänzen (neue `id`, `name`, `page`). - Bei unzuverlässiger Quelle: `image` auf eine direkte JPG-URL setzen. Wichtig: Chat-Antworten nur als reiner Text (kein Markdown). Für Sprachausgabe: sauberer Text ohne Sonderzeichen oder Formatierung.

path: _meta.json
content: ownerId: kn7236ymjfttm5mjvctmzazf1d80qd1r
slug: foto-webcam
version: 1.0.8
publishedAt: 1770470873007

path: docs/webcams/favorites-muenchen.json
content: {
  "items": [
    {
      "id": 1,
      "name": "München Großhadern (LMU-Klinikum) – Blick nach Nordost / Innenstadt",
      "page": "https://www.foto-webcam.eu/webcam/muenchen/"
    },
    {
      "id": 2,
      "name": "München Großhadern (LMU-Klinikum) – Blick nach Süden",
      "page": "https://www.foto-webcam.eu/webcam/lmu-klinikum/"
    },
    {
      "id": 3,
      "name": "München Olympiapark – Olympiaturm / TUM Campus Baustelle",
      "page": "https://www.foto-webcam.eu/webcam/tum-olympiapark/"
    },
    {
      "id": 4,
      "name": "München-Freimann – Studentenstadt (Blick Richtung A9/Allianz Arena)",
      "page": "https://www.foto-webcam.eu/webcam/freimann/"
    },
    {
      "id": 5,
      "name": "München – LMU Neubau Geo- und Umweltwissenschaften (Baustelle)",
      "page": "https://www.foto-webcam.eu/webcam/lmu-neubau/"
    },
    {
      "id": 6,
      "name": "Dachau – Rathaus (Blick nach Südosten)",
      "page": "https://www.foto-webcam.eu/webcam/dachau/"
    },
    {
      "id": 7,
      "name": "Fürstenfeldbruck – Veranstaltungsforum Fürstenfeld (Blick zur Klosterkirche)",
      "page": "https://www.foto-webcam.eu/webcam/fuerstenfeld/"
    },
    {
      "id": 8,
      "name": "Salzburg – Blick über Salzburg nach Osten (Hochstaufen/Reichenhaller Haus)",
      "page": "https://www.foto-webcam.eu/webcam/salzburg/"
    },
    {
      "id": 9,
      "name": "Zugspitze – Seilbahn Zugspitze Talstation (Bayerische Zugspitzbahn)",
      "page": "https://www.foto-webcam.eu/webcam/bzb-tal/"
    },
    {
      "id": 10,
      "name": "Eibsee – Eibsee-Hotel (Blick zur Zugspitze)",
      "page": "https://www.foto-webcam.eu/webcam/eibsee-hotel/"
    },
    {
      "id": 11,
      "name": "Eibsee – Seilbahn-Zugspitze Stütze (Blick über den Eibsee nach Norden)",
      "page": "https://www.foto-webcam.eu/webcam/eibsee-nord/"
    },
    {
      "id": 12,
      "name": "Chiemsee – Kampenwand/Andachtskreuz (Blick nach Norden über den Chiemsee)",
      "page": "https://www.foto-webcam.eu/webcam/kampenwand/"
    },
    {
      "id": 13,
      "name": "Herzogstand/Fahrenbergkopf – Blick über den Kochelsee (links ferne: Starnberger See)",
      "page": "https://www.foto-webcam.eu/webcam/kochelsee/"
    }
  ]
}


path: scripts/foto_webcam_snapshot.py
content: #!/usr/bin/env python3
"""foto_webcam_snapshot.py

Fetch a current snapshot image for a foto-webcam.eu webcam page or from a favorites list.

Usage examples:
  python3 foto_webcam_snapshot.py --url https://www.foto-webcam.eu/webcam/zugspitze/ --out /tmp/zugspitze.jpg
  python3 foto_webcam_snapshot.py --favorites docs/webcams/favorites-muenchen.json --id 4 --out /tmp/webcam4.jpg

Outputs:
  - Writes JPG bytes to --out
  - Prints a single JSON line to stdout: {ok, out, sourceUrl, pageUrl, name, id}

Notes:
  - Uses a browser-like User-Agent.
  - Best-effort HTML parsing via regex.
"""

from __future__ import annotations

import argparse
import json
import os
import re
import sys
from urllib.parse import urljoin

import requests

UA = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120 Safari/537.36"


def read_favorites(path: str) -> dict:
    with open(path, "r", encoding="utf-8") as f:
        return json.load(f)


def resolve_current_image_from_page(page_url: str) -> str:
    r = requests.get(page_url, headers={"User-Agent": UA, "Accept": "text/html"}, timeout=30)
    r.raise_for_status()
    html = r.text

    # Prefer explicit current/<digits>.jpg occurrences
    m = re.search(r"https://www\.foto-webcam\.eu/webcam/[^\s\"']+/current/\d+\.jpg", html)
    if m:
        return m.group(0)

    # Fallback: build from /webcam/<slug>/
    m2 = re.search(r"https://www\.foto-webcam\.eu/webcam/([^/]+)/", page_url)
    if m2:
        slug = m2.group(1)
        return f"https://www.foto-webcam.eu/webcam/{slug}/current/1200.jpg"

    # Generic fallback: search for any jpg
    m3 = re.search(r"https?://[^\s\"']+\.jpg", html, flags=re.IGNORECASE)
    if m3:
        return m3.group(0)

    raise RuntimeError("Could not resolve current image URL")


def download_image(url: str) -> bytes:
    r = requests.get(url, headers={"User-Agent": UA, "Accept": "image/avif,image/webp,image/apng,image/*,*/*"}, timeout=30)
    r.raise_for_status()
    return r.content


def main() -> int:
    p = argparse.ArgumentParser()
    p.add_argument("--url", help="foto-webcam page URL")
    p.add_argument("--favorites", help="favorites json file")
    p.add_argument("--id", type=int, help="favorites id")
    p.add_argument("--out", required=True, help="output jpg path")

    a = p.parse_args()

    page_url = a.url
    fav_name = None
    fav_id = a.id
    source_url = None

    if a.favorites and a.id is not None:
        fav = read_favorites(a.favorites)
        items = fav.get("items") or []
        hit = next((it for it in items if int(it.get("id")) == int(a.id)), None)
        if not hit:
            raise RuntimeError(f"Favorite id not found: {a.id}")
        fav_name = hit.get("name")
        page_url = hit.get("page")
        source_url = hit.get("image")

    if not page_url:
        raise RuntimeError("Missing --url or --favorites+--id")

    if not source_url:
        source_url = resolve_current_image_from_page(page_url)

    data = download_image(source_url)
    os.makedirs(os.path.dirname(a.out) or ".", exist_ok=True)
    with open(a.out, "wb") as f:
        f.write(data)

    sys.stdout.write(
        json.dumps(
            {
                "ok": True,
                "out": a.out,
                "sourceUrl": source_url,
                "pageUrl": page_url,
                "name": fav_name,
                "id": fav_id,
                "bytes": len(data),
            }
        )
        + "\n"
    )
    return 0


if __name__ == "__main__":
    try:
        raise SystemExit(main())
    except Exception as e:
        sys.stdout.write(json.dumps({"ok": False, "error": str(e)}) + "\n")
        raise


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)