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.
175 lines
7.0 KiB
C
175 lines
7.0 KiB
C
#include "insecam.h"
|
|
#include "httpstream.h"
|
|
#include "sdkconfig.h"
|
|
#include "esp_heap_caps.h"
|
|
#include "esp_log.h"
|
|
#include "esp_random.h"
|
|
#include <stdbool.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
static const char *TAG = "insecam";
|
|
#define PAGE_CAP (192 * 1024)
|
|
|
|
// ASCII folding for U+00C0..U+00FF and U+0100..U+017F so LVGL's Montserrat can show them.
|
|
static const char latin1[] = "AAAAAAACEEEEIIIIDNOOOOOxOUUUUYTsaaaaaaaceeeeiiiidnooooo/ouuuuyty";
|
|
static const char latin_ext_a[] =
|
|
"AaAaAa" "CcCcCcCc" "DdDd" "EeEeEeEeEe" "GgGgGgGg" "HhHh" "IiIiIiIiIiIi" "Jj" "Kkk" "LlLlLlLlLl"
|
|
"NnNnNnnNn" "OoOoOoOo" "RrRrRr" "SsSsSsSs" "TtTtTt" "UuUuUuUuUuUu" "Ww" "YyY" "ZzZzZz" "s";
|
|
_Static_assert(sizeof latin1 == 65, "latin1 table");
|
|
_Static_assert(sizeof latin_ext_a == 129, "latin ext-a table");
|
|
|
|
// Decodes the common HTML entities and folds UTF-8 to ASCII, in place, bounded by cap.
|
|
void insecam_clean_text(char *s, size_t cap)
|
|
{
|
|
char *out = s;
|
|
const unsigned char *in = (const unsigned char *)s;
|
|
size_t n = 0;
|
|
while (*in && n + 1 < cap) {
|
|
unsigned char c = *in;
|
|
char ch = 0;
|
|
if (c == '&') {
|
|
if (!strncmp((const char *)in, "&", 5)) { ch = '&'; in += 5; }
|
|
else if (!strncmp((const char *)in, """, 6)) { ch = '"'; in += 6; }
|
|
else if (!strncmp((const char *)in, "'", 5)) { ch = '\''; in += 5; }
|
|
else if (!strncmp((const char *)in, "<", 4)) { ch = '<'; in += 4; }
|
|
else if (!strncmp((const char *)in, ">", 4)) { ch = '>'; in += 4; }
|
|
else if (in[1] == '#') {
|
|
char *end;
|
|
long cp = strtol((const char *)in + 2, &end, 10);
|
|
if (*end == ';') { ch = (cp >= 32 && cp < 127) ? (char)cp : 0; in = (const unsigned char *)end + 1; }
|
|
else { ch = '&'; in++; }
|
|
} else { ch = '&'; in++; }
|
|
} else if (c < 0x80) {
|
|
ch = (c >= 32) ? c : ' ';
|
|
in++;
|
|
} else {
|
|
uint32_t cp = 0;
|
|
int extra = (c & 0xE0) == 0xC0 ? 1 : (c & 0xF0) == 0xE0 ? 2 : (c & 0xF8) == 0xF0 ? 3 : 0;
|
|
cp = extra == 1 ? c & 0x1F : extra == 2 ? c & 0x0F : extra == 3 ? c & 0x07 : 0;
|
|
in++;
|
|
for (int k = 0; k < extra && (*in & 0xC0) == 0x80; k++) cp = (cp << 6) | (*in++ & 0x3F);
|
|
if (cp >= 0xC0 && cp <= 0xFF) ch = latin1[cp - 0xC0];
|
|
else if (cp >= 0x100 && cp <= 0x17F) ch = latin_ext_a[cp - 0x100];
|
|
}
|
|
if (ch) out[n++] = ch;
|
|
}
|
|
out[n] = 0;
|
|
// trim trailing spaces
|
|
while (n && out[n - 1] == ' ') out[--n] = 0;
|
|
}
|
|
|
|
static bool copy_attr(const char *from, const char *limit, const char *attr, char *dst, size_t cap)
|
|
{
|
|
const char *p = strstr(from, attr);
|
|
if (!p || p >= limit) return false;
|
|
p += strlen(attr);
|
|
const char *q = strchr(p, '"');
|
|
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;
|
|
}
|
|
|
|
void insecam_effective_url(const cam_info_t *cam, char *out, size_t cap)
|
|
{
|
|
strlcpy(out, cam->url, cap);
|
|
char *c;
|
|
while ((c = strstr(out, "COUNTER"))) { // the site's JS swaps this for a cache-buster
|
|
char digits[8];
|
|
snprintf(digits, sizeof digits, "%07lu", (unsigned long)(esp_random() % 10000000));
|
|
memcpy(c, digits, 7);
|
|
}
|
|
}
|
|
|
|
static int parse_page(char *html, cam_info_t *out, int max, int *total_pages)
|
|
{
|
|
const char *nav = strstr(html, "pagenavigator(\"?page=\", ");
|
|
if (nav) {
|
|
int tp = atoi(nav + strlen("pagenavigator(\"?page=\", "));
|
|
if (tp > 0) *total_pages = tp;
|
|
}
|
|
int count = 0;
|
|
// Anchor on the view link so header images can't masquerade as cameras.
|
|
static const char anchor[] = "class=\"thumbnail-item__wrap\" href=\"/en/view/";
|
|
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 + strlen(html);
|
|
cam_info_t *c = &out[count];
|
|
memset(c, 0, sizeof *c);
|
|
c->provider = PROVIDER_INSECAM;
|
|
char tmp[128];
|
|
if (copy_attr(item, limit, "href=\"/en/view/", tmp, sizeof tmp)) c->id = strtoul(tmp, NULL, 10);
|
|
if (!copy_attr(item, limit, " src=\"", c->url, sizeof c->url) || strncmp(c->url, "http://", 7)) {
|
|
item = next;
|
|
continue;
|
|
}
|
|
for (int pass = 0; pass < 3 && strstr(c->url, "&"); pass++) insecam_clean_text(c->url, sizeof c->url);
|
|
insecam_clean_text(c->url, sizeof c->url);
|
|
// <img ... title="Live camera Mobotix in Imola, Italy"> ; fall back to the link title
|
|
const char *img = strstr(item, "<img");
|
|
bool ok = img && img < limit && copy_attr(img, limit, "title=\"Live camera ", tmp, sizeof tmp);
|
|
const char *in = ok ? strstr(tmp, " in ") : NULL;
|
|
if (in) {
|
|
size_t ml = in - tmp;
|
|
if (ml >= sizeof c->maker) ml = sizeof c->maker - 1;
|
|
memcpy(c->maker, tmp, ml);
|
|
c->maker[ml] = 0;
|
|
strlcpy(c->place, in + 4, sizeof c->place);
|
|
} else if (copy_attr(item, limit, "title=\"Live camera in ", tmp, sizeof tmp)) {
|
|
strlcpy(c->place, tmp, sizeof c->place); // "Country, City" order on the link
|
|
}
|
|
insecam_clean_text(c->place, sizeof c->place);
|
|
insecam_clean_text(c->maker, sizeof c->maker);
|
|
if (!c->place[0]) strlcpy(c->place, "Unknown location", sizeof c->place);
|
|
count++;
|
|
item = next;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
esp_err_t insecam_fetch_page(insecam_listing_t listing, int page, cam_info_t *out, int max,
|
|
int *count, int *total_pages)
|
|
{
|
|
char url[96];
|
|
snprintf(url, sizeof url, "http://www.insecam.org/en/%s/?page=%d",
|
|
listing == INSECAM_BY_NEW ? "bynew" : "byrating", page);
|
|
*count = 0;
|
|
|
|
char *html = heap_caps_malloc(PAGE_CAP, MALLOC_CAP_SPIRAM);
|
|
if (!html) return ESP_ERR_NO_MEM;
|
|
httpstream_t s;
|
|
esp_err_t err = httpstream_open(&s, url, CONFIG_WIDGET_USER_AGENT, 5000, 8000);
|
|
if (err != ESP_OK) {
|
|
ESP_LOGW(TAG, "open %s failed: %s", url, esp_err_to_name(err));
|
|
free(html);
|
|
return err;
|
|
}
|
|
if (s.status != 200) {
|
|
ESP_LOGW(TAG, "%s -> HTTP %d", url, s.status);
|
|
httpstream_close(&s);
|
|
free(html);
|
|
return ESP_FAIL;
|
|
}
|
|
size_t len = 0;
|
|
while (len < PAGE_CAP - 1) {
|
|
int r = httpstream_read(&s, (uint8_t *)html + len, PAGE_CAP - 1 - len);
|
|
if (r == HTTPSTREAM_EOF) break;
|
|
if (r < 0) { ESP_LOGW(TAG, "read error %d after %u bytes", r, (unsigned)len); break; }
|
|
len += r;
|
|
}
|
|
httpstream_close(&s);
|
|
for (size_t i = 0; i < len; i++) if (!html[i]) html[i] = ' ';
|
|
html[len] = 0;
|
|
|
|
*count = parse_page(html, out, max, total_pages);
|
|
free(html);
|
|
ESP_LOGI(TAG, "%s page %d: %u bytes, %d cameras, %d pages total",
|
|
listing == INSECAM_BY_NEW ? "bynew" : "byrating", page, (unsigned)len, *count, *total_pages);
|
|
return *count ? ESP_OK : ESP_ERR_NOT_FOUND;
|
|
}
|