Files
Juli bd05fca812 Add opentopia.com as a second camera provider
Listing pages (rated/newest) and per-camera pages are fetched over HTTPS
with the certificate bundle; the camera page's Live Feed mode yields the
camera's own MJPEG URL (Current Still as fallback), resolved just before
playing and cached in favourites. New Provider menu row: insecam /
opentopia / mixed. cam_info_t gains provider + title fields and the
settings blob a version byte; older favourites and settings still load.
2026-09-17 02:04:57 +02:00

153 lines
6.4 KiB
C

#include "opentopia.h"
#include "webpage.h"
#include "sdkconfig.h"
#include "esp_log.h"
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static const char *TAG = "opentopia";
#define PAGE_CAP (160 * 1024)
void insecam_clean_text(char *s, size_t cap); // entity decode + ASCII fold, shared with insecam.c
static bool between(const char *from, const char *limit, const char *open, const char *close, char *dst, size_t cap)
{
const char *p = strstr(from, open);
if (!p || p >= limit) return false;
p += strlen(open);
const char *q = strstr(p, close);
if (!q || q > limit) return false;
size_t n = q - p;
if (n >= cap) n = cap - 1;
memcpy(dst, p, n);
dst[n] = 0;
return true;
}
static void trim(char *s)
{
size_t n = strlen(s);
while (n && (s[n - 1] == ' ' || s[n - 1] == '\n' || s[n - 1] == '\r' || s[n - 1] == '\t')) s[--n] = 0;
size_t i = 0;
while (s[i] == ' ' || s[i] == '\n' || s[i] == '\r' || s[i] == '\t') i++;
if (i) memmove(s, s + i, n - i + 1);
}
esp_err_t opentopia_fetch_page(insecam_listing_t listing, int page, cam_info_t *out, int max,
int *count, int *total_pages)
{
char url[160];
snprintf(url, sizeof url, "https://www.opentopia.com/hiddencam.php?showmode=standard&country=%%2A&seewhat=%s&p=%d",
listing == INSECAM_BY_NEW ? "newest" : "highlyrated", page);
*count = 0;
char *html;
size_t len;
esp_err_t err = webpage_get(url, CONFIG_WIDGET_USER_AGENT, PAGE_CAP, &html, &len);
if (err != ESP_OK) return err;
// paginator: take the largest &p=N we can find
int maxp = 0;
for (const char *p = html; (p = strstr(p, "&p=")); p += 3) {
int v = atoi(p + 3);
if (v > maxp) maxp = v;
}
if (maxp > 0) *total_pages = maxp;
static const char anchor[] = "href=\"/webcam/";
const char *item = strstr(html, anchor);
while (item && *count < max) {
const char *next = strstr(item + sizeof anchor - 1, anchor);
const char *limit = next ? next : html + len;
cam_info_t *c = &out[*count];
memset(c, 0, sizeof *c);
c->provider = PROVIDER_OPENTOPIA;
c->id = strtoul(item + sizeof anchor - 1, NULL, 10);
char name[80] = "", country[48] = "", region[48] = "", city[48] = "";
bool has_name = between(item, limit, "class=\"viewcamsname\">", "<", name, sizeof name);
const char *loc = strstr(item, "class=\"location\">");
if (c->id && has_name && loc && loc < limit) {
between(loc, limit, "<span>", "</span>", country, sizeof country);
const char *r = strstr(loc, "</span>");
if (r && r < limit) between(r, limit, "<span>", "</span>", region, sizeof region);
const char *r2 = r ? strstr(r + 7, "</span>") : NULL;
if (r2 && r2 < limit) between(r2, limit, "<span>", "</span>", city, sizeof city);
trim(name); trim(country); trim(region); trim(city);
const char *town = city[0] ? city : region;
if (town[0] && country[0]) snprintf(c->place, sizeof c->place, "%s, %s", town, country);
else strlcpy(c->place, country[0] ? country : "Somewhere", sizeof c->place);
strlcpy(c->title, name, sizeof c->title);
insecam_clean_text(c->place, sizeof c->place);
insecam_clean_text(c->title, sizeof c->title);
(*count)++;
}
item = next;
}
free(html);
ESP_LOGI(TAG, "%s page %d: %u bytes, %d cameras, %d pages total",
listing == INSECAM_BY_NEW ? "newest" : "highlyrated", page, (unsigned)len, *count, *total_pages);
return *count ? ESP_OK : ESP_ERR_NOT_FOUND;
}
// The camera page embeds the camera's own URL in <div class="big">; "livevideo" gives the
// MJPEG stream, "livestill" a plain JPEG. Both are http:// straight to the camera.
static bool extract_big_src(const char *html, char *dst, size_t cap)
{
const char *big = strstr(html, "<div class=\"big\">");
if (!big) return false;
const char *img = strstr(big, "<img");
if (!img || img - big > 600) return false;
const char *src = strstr(img, "src=\"");
if (!src || src - img > 300) return false;
src += 5;
const char *end = strchr(src, '"');
if (!end) return false;
size_t n = end - src;
if (n >= cap || strncmp(src, "http://", 7)) return false; // https/cached snapshots aren't streams
memcpy(dst, src, n);
dst[n] = 0;
return true;
}
esp_err_t opentopia_resolve(cam_info_t *cam)
{
static const char *modes[] = {"livevideo", "livestill"};
for (int m = 0; m < 2; m++) {
char url[96];
snprintf(url, sizeof url, "https://www.opentopia.com/webcam/%lu?viewmode=%s", (unsigned long)cam->id, modes[m]);
char *html;
size_t len;
esp_err_t err = webpage_get(url, CONFIG_WIDGET_USER_AGENT, PAGE_CAP, &html, &len);
if (err != ESP_OK) return err;
// only trust the live-video page when the camera actually offers that mode
bool ok = (m == 1 || strstr(html, "value=\"livevideo\"")) && extract_big_src(html, cam->url, sizeof cam->url);
if (ok) {
// the listing clips names to 10 characters; the page has them in full
char city[48] = "", country[48] = "", facility[64] = "";
const char *end = html + len;
between(html, end, "class=\"right locality\">", "<", city, sizeof city);
between(html, end, "class=\"right country-name\">", "<", country, sizeof country);
between(html, end, "<label class=\"left\">Facility:</label> <label class=\"right\">", "<", facility, sizeof facility);
trim(city); trim(country); trim(facility);
if (country[0]) {
if (city[0]) snprintf(cam->place, sizeof cam->place, "%s, %s", city, country);
else strlcpy(cam->place, country, sizeof cam->place);
insecam_clean_text(cam->place, sizeof cam->place);
}
if (facility[0]) {
strlcpy(cam->title, facility, sizeof cam->title);
insecam_clean_text(cam->title, sizeof cam->title);
}
}
free(html);
if (ok) {
insecam_clean_text(cam->url, sizeof cam->url);
ESP_LOGI(TAG, "#%lu %s -> %s", (unsigned long)cam->id, modes[m], cam->url);
return ESP_OK;
}
}
cam->url[0] = 0;
return ESP_ERR_NOT_FOUND;
}