osint-dashboard/app/camera_models.py
Sirius DevOps 69a30115cc
Some checks failed
build-and-deploy / build (push) Failing after 5s
Add open IP camera discovery scraper (OSINT map)
- camera_scraper: public-directory-only discovery (Insecam-style HTML +
  plain-text lists), hard private-range guard (fail closed), per-host
  rate limiting, Nominatim geocoding at <=1 req/s, TTL'd local snapshot
  cache, sha256 url_hash dedupe with Postgres upsert
- cameras table (migration 002) + /api/cameras?bbox= + snapshot endpoint
  with cache passthrough in main.py
- run_camera_service: long-running cycle worker following NATS->ingester
  pattern; publishes events.camera for shared ingester
- docker-compose camera-service profile, .env.example knobs

Verified E2E against TimescaleDB+PostGIS: private-range entries dropped,
dedupe across cycles holds, bbox query returns expected rows.
2026-08-24 14:54:51 -04:00

43 lines
1.6 KiB
Python

"""Open IP camera discovery — models and migration helpers.
Cameras are stored in their own `cameras` table (TimescaleDB hypertable on
last_seen) alongside the existing `events` feed, deduped by URL hash.
"""
from __future__ import annotations
from sqlalchemy import (
Column, Float, Index, Integer, String, Text, DateTime, JSON,
func, Table, BigInteger,
)
from sqlalchemy.dialects.postgresql import UUID
import uuid
from database import metadata
cameras = Table(
"cameras",
metadata,
Column("id", UUID(as_uuid=True), primary_key=True, default=uuid.uuid4),
# sha256 hex of the camera's source/snapshot URL — the dedupe key.
Column("url_hash", String(64), nullable=False, index=True),
Column("source_url", Text, nullable=False),
# Snapshot endpoint (may differ from the directory listing URL).
Column("snapshot_url", Text),
# Which discovery feed produced this record (e.g. "insecam", "camera_list").
Column("discovery_source", String(128), nullable=False, index=True),
Column("location_lat", Float),
Column("location_lon", Float),
Column("location_name", String(512)),
# Vendor / device type when detectable from the directory page or URL.
Column("vendor", String(128)),
Column("device_type", String(64)), # e.g. ip-cam, rtsp, mjpeg
Column("first_seen", DateTime(timezone=True), server_default=func.now(), nullable=False),
Column("last_seen", DateTime(timezone=True), server_default=func.now(), nullable=False),
Column("raw", JSON),
)
Index("ix_cameras_location", cameras.c.location_lat, cameras.c.location_lon)
Index("ix_cameras_last_seen", cameras.c.last_seen)