394 lines
16 KiB
PHP
394 lines
16 KiB
PHP
|
|
<?php
|
|||
|
|
|
|||
|
|
declare(strict_types=1);
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Instagram-Beitrag in den Feed aufnehmen (nur CLI).
|
|||
|
|
*
|
|||
|
|
* php bin/instagram-add.php --profil ALLE neuen Beiträge holen (Normalfall)
|
|||
|
|
* php bin/instagram-add.php <post-url> [<post-url> …]
|
|||
|
|
* php bin/instagram-add.php --liste=links.txt eine URL je Zeile
|
|||
|
|
* php bin/instagram-add.php --entfernen=<shortcode> Eintrag und Bild löschen
|
|||
|
|
* php bin/instagram-add.php --bild=<pfad> <url> Bild von Hand, siehe „Notfall“ unten
|
|||
|
|
*
|
|||
|
|
* ERSETZT bin/instagram-sync.php (gelöscht am 17.09.2026). Der Scraper holte die letzten
|
|||
|
|
* Beiträge des öffentlichen Profils; das geht seit dem 03.09.2026 nicht mehr: Instagram
|
|||
|
|
* antwortet auf `feed/user` und `web_profile_info` mit 401 `require_login`, das Profil-HTML
|
|||
|
|
* enthält keine Beitragsdaten mehr, und auch die `/embed/`-Seiten liefern nur noch eine leere
|
|||
|
|
* App-Hülle, die ihren Inhalt erst im Browser nachlädt. Von einem privaten Anschluss geprüft,
|
|||
|
|
* es liegt also NICHT an der Server-IP — ein Lauf von woanders hilft nicht.
|
|||
|
|
*
|
|||
|
|
* WAS NOCH GEHT: der oEmbed-Endpunkt für EINEN Beitrag antwortet weiterhin ohne Token und
|
|||
|
|
* liefert alles, was wir brauchen — Bild, Caption und über die media_id sogar das Datum.
|
|||
|
|
* Deshalb dieser Weg: Felix wirft die Links der Beiträge in den Chat, dieses Skript holt den
|
|||
|
|
* Rest. Die Datei data/instagram.json behält ihre Struktur EXAKT bei, an Startseite,
|
|||
|
|
* News-Feed und Stadionzeitung ändert sich dadurch nichts.
|
|||
|
|
*
|
|||
|
|
* --profil NIMMT DAS LINKSAMMELN AB. Was per curl tot ist, zeigt ein echter Browser weiterhin:
|
|||
|
|
* headless Chrome bekommt auf der ausgeloggten Profilseite die Liste der letzten zwölf
|
|||
|
|
* Beitrags-Codes (nachgewiesen am 17.09.2026 — im reinen HTML stehen sie NICHT, sie entstehen
|
|||
|
|
* erst durch das JavaScript der Seite). Das Skript liest sie aus dem gerenderten DOM und holt
|
|||
|
|
* zu jedem neuen Code Bild, Text und Datum über oEmbed. Der Aufruf, den der Browser dafür
|
|||
|
|
* macht, ist mit Sitzungs-Token abgesichert und bewusst NICHT nachgebaut: das wäre eine
|
|||
|
|
* Bastelei, die beim nächsten Meta-Update wieder bricht.
|
|||
|
|
*
|
|||
|
|
* KEIN CRON. Auf dem All-Inkl-Webspace gibt es kein Chrome, --profil läuft also nur lokal
|
|||
|
|
* auf einem Arbeitsrechner — wie bin/logo-build.php. Das ist die ehrliche Grenze: die
|
|||
|
|
* Aktualisierung braucht einen Anstoß im Chat, dafür hängt die Seite an nichts mehr, das
|
|||
|
|
* unbemerkt von außen kaputtgehen kann. Ohne Chrome bleiben die Einzel-URLs.
|
|||
|
|
*
|
|||
|
|
* Notfall: verschwindet der oEmbed-Endpunkt (er ist Instagrams eigener, nicht der offiziell
|
|||
|
|
* dokumentierte — der liefert kein Bild), bleibt alles Eingetragene liegen und nichts bricht
|
|||
|
|
* rückwirkend. Für neue Beiträge dann `--bild=<pfad>` mit einem Screenshot; Caption und Datum
|
|||
|
|
* werden in dem Fall im Chat nachgetragen.
|
|||
|
|
*/
|
|||
|
|
if (PHP_SAPI !== 'cli') {
|
|||
|
|
exit(1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
require dirname(__DIR__) . '/app/bootstrap.php';
|
|||
|
|
require_once __DIR__ . '/chrome-lib.php';
|
|||
|
|
|
|||
|
|
$verbose = in_array('-v', $argv, true);
|
|||
|
|
$username = (string) config('instagram.username', 'tsv08kulmbach');
|
|||
|
|
$imgDir = PUBLIC_PATH . '/assets/img/instagram';
|
|||
|
|
$cacheFile = DATA_PATH . '/instagram.json';
|
|||
|
|
|
|||
|
|
// Schreibt über die zentrale log_write() (helpers.php) nach storage/logs/instagram.log;
|
|||
|
|
// ERROR landet zusätzlich in error.log. Mit -v zusätzlich auf stdout.
|
|||
|
|
$log = static function (string $msg, string $level = 'INFO') use ($verbose): void {
|
|||
|
|
log_write('instagram', $level, $msg);
|
|||
|
|
if ($verbose || $level === 'ERROR' || $level === 'WARN') {
|
|||
|
|
echo '[' . date('c') . "] {$level} {$msg}\n";
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
$fail = static function (string $msg) use ($log): never {
|
|||
|
|
$log("{$msg} — bestehende Daten bleiben unangetastet.", 'ERROR');
|
|||
|
|
exit(1);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// --- Argumente ---
|
|||
|
|
$urls = [];
|
|||
|
|
$removeCode = '';
|
|||
|
|
$manualImg = '';
|
|||
|
|
$vomProfil = false;
|
|||
|
|
foreach (array_slice($argv, 1) as $arg) {
|
|||
|
|
if ($arg === '-v') {
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
if ($arg === '--profil') {
|
|||
|
|
$vomProfil = true;
|
|||
|
|
} elseif (preg_match('/^--entfernen=(.+)$/', $arg, $m) === 1) {
|
|||
|
|
$removeCode = trim($m[1]);
|
|||
|
|
} elseif (preg_match('/^--bild=(.+)$/', $arg, $m) === 1) {
|
|||
|
|
$manualImg = $m[1];
|
|||
|
|
} elseif (preg_match('/^--liste=(.+)$/', $arg, $m) === 1) {
|
|||
|
|
if (!is_file($m[1])) {
|
|||
|
|
$fail("Liste nicht gefunden: {$m[1]}");
|
|||
|
|
}
|
|||
|
|
foreach (file($m[1], FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [] as $line) {
|
|||
|
|
$line = trim($line);
|
|||
|
|
if ($line !== '' && !str_starts_with($line, '#')) {
|
|||
|
|
$urls[] = $line;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} elseif (str_starts_with($arg, '--')) {
|
|||
|
|
$fail("Unbekannter Schalter: {$arg}");
|
|||
|
|
} else {
|
|||
|
|
$urls[] = $arg;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if ($urls === [] && $removeCode === '' && !$vomProfil) {
|
|||
|
|
fwrite(STDERR, "Nutzung: php bin/instagram-add.php --profil | <post-url> [...] | --liste=<datei> | --entfernen=<shortcode>\n");
|
|||
|
|
exit(1);
|
|||
|
|
}
|
|||
|
|
if ($manualImg !== '' && count($urls) !== 1) {
|
|||
|
|
$fail('--bild gilt genau für EINE URL.');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --- Beitragsliste vom Profil holen (--profil) ---
|
|||
|
|
// Der einzige Schritt, der einen echten Browser braucht: die Codes stehen nicht im
|
|||
|
|
// ausgelieferten HTML, sie entstehen erst durch das JavaScript der Profilseite.
|
|||
|
|
if ($vomProfil) {
|
|||
|
|
$chrome = chrome_binary();
|
|||
|
|
if ($chrome === null) {
|
|||
|
|
$fail('--profil braucht Chrome, hier ist keines installiert (auf dem Server gibt es keines). '
|
|||
|
|
. 'Entweder Pfad setzen: CHROME="/Pfad/zu/Chrome" php bin/instagram-add.php --profil — '
|
|||
|
|
. 'oder die Beitrags-URLs einzeln übergeben.');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$dom = chrome_run($chrome, [
|
|||
|
|
'--virtual-time-budget=15000',
|
|||
|
|
'--dump-dom',
|
|||
|
|
escapeshellarg('https://www.instagram.com/' . $username . '/'),
|
|||
|
|
]);
|
|||
|
|
|
|||
|
|
if (preg_match_all('#/p/([A-Za-z0-9_-]{8,})/#', $dom, $treffer) !== 0) {
|
|||
|
|
// Reihenfolge im DOM ist die des Profils (neueste zuerst), Duplikate raus.
|
|||
|
|
foreach (array_unique($treffer[1]) as $code) {
|
|||
|
|
$urls[] = "https://www.instagram.com/p/{$code}/";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if ($urls === []) {
|
|||
|
|
$fail('Profilseite gelesen, aber keine Beiträge gefunden — hat Instagram die Seite wieder geändert? '
|
|||
|
|
. 'Notfalls die Beitrags-URLs einzeln übergeben.');
|
|||
|
|
}
|
|||
|
|
$log('Profil gelesen: ' . count($urls) . ' Beiträge gefunden.');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --- Lock gegen parallele Läufe ---
|
|||
|
|
$lock = fopen(STORAGE_PATH . '/cache/instagram.lock', 'c');
|
|||
|
|
if (!$lock || !flock($lock, LOCK_EX | LOCK_NB)) {
|
|||
|
|
$log('Läuft bereits (Lock belegt) — Lauf übersprungen.', 'WARN');
|
|||
|
|
exit(1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --- HTTP-Helfer, unverändert aus bin/instagram-sync.php übernommen ---
|
|||
|
|
$fetch = static function (string $url, array $headers = []): string|false {
|
|||
|
|
$ch = curl_init($url);
|
|||
|
|
curl_setopt_array($ch, [
|
|||
|
|
CURLOPT_RETURNTRANSFER => true,
|
|||
|
|
CURLOPT_FOLLOWLOCATION => true,
|
|||
|
|
CURLOPT_MAXREDIRS => 5,
|
|||
|
|
CURLOPT_TIMEOUT => 20,
|
|||
|
|
CURLOPT_ENCODING => '',
|
|||
|
|
CURLOPT_USERAGENT => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
|
|||
|
|
CURLOPT_HTTPHEADER => array_merge(['Accept-Language: de-DE,de;q=0.9'], $headers),
|
|||
|
|
]);
|
|||
|
|
$body = curl_exec($ch);
|
|||
|
|
$status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
|
|||
|
|
return ($body !== false && $status === 200) ? $body : false;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Aufnahmezeitpunkt aus der media_id rechnen.
|
|||
|
|
*
|
|||
|
|
* Instagram kodiert den Zeitpunkt in den oberen Bits der 64-Bit-ID: die unteren 23 Bit sind
|
|||
|
|
* Shard und Sequenz, darüber stehen Millisekunden seit der Instagram-Epoche (01.09.2011).
|
|||
|
|
* Gegen die echten taken_at-Werte aller neun Beiträge aus dem alten Cache gerechnet: sechs
|
|||
|
|
* exakt, drei mit 2 bis 8 Minuten Abweichung (Aufnahme- gegen Veröffentlichungszeit).
|
|||
|
|
* Sichtbar ist ohnehin nur der Tag.
|
|||
|
|
*/
|
|||
|
|
const IG_EPOCH_MS = 1314220021721;
|
|||
|
|
|
|||
|
|
$takenAtFromMediaId = static function (string $mediaId): int {
|
|||
|
|
$id = (int) explode('_', $mediaId)[0];
|
|||
|
|
|
|||
|
|
return $id > 0 ? intdiv(($id >> 23) + IG_EPOCH_MS, 1000) : time();
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/** Shortcode aus einer Beitrags-URL. Reels tragen /reel/ statt /p/. */
|
|||
|
|
$shortcodeFromUrl = static function (string $url): string {
|
|||
|
|
return preg_match('#/(?:p|reel|tv)/([A-Za-z0-9_-]+)#', $url, $m) === 1 ? $m[1] : '';
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// --- Bestand laden ---
|
|||
|
|
$cache = is_file($cacheFile)
|
|||
|
|
? (json_decode((string) file_get_contents($cacheFile), true) ?: [])
|
|||
|
|
: [];
|
|||
|
|
$posts = $cache['posts'] ?? [];
|
|||
|
|
|
|||
|
|
// Shortcode → Position, das ist die ganze Dedupe-Logik: der Shortcode ist Instagrams eigener
|
|||
|
|
// Primärschlüssel, ein Textvergleich oder eine Heuristik wäre nur eine Fehlerquelle mehr.
|
|||
|
|
$index = [];
|
|||
|
|
foreach ($posts as $i => $post) {
|
|||
|
|
if (!empty($post['shortcode'])) {
|
|||
|
|
$index[$post['shortcode']] = $i;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$speichern = static function () use (&$posts, $cacheFile, $username, $fail): void {
|
|||
|
|
// Neueste zuerst — dieselbe Reihenfolge, die der alte Sync von Instagram bekam.
|
|||
|
|
usort($posts, static fn (array $a, array $b): int => ($b['taken_at'] ?? 0) <=> ($a['taken_at'] ?? 0));
|
|||
|
|
|
|||
|
|
$payload = json_encode([
|
|||
|
|
'fetched_at' => time(),
|
|||
|
|
'username' => $username,
|
|||
|
|
'posts' => array_values($posts),
|
|||
|
|
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
|||
|
|
|
|||
|
|
if ($payload === false) {
|
|||
|
|
$fail('Cache-Encode fehlgeschlagen');
|
|||
|
|
}
|
|||
|
|
$tmp = $cacheFile . '.tmp';
|
|||
|
|
if (file_put_contents($tmp, $payload . "\n") === false || !rename($tmp, $cacheFile)) {
|
|||
|
|
@unlink($tmp);
|
|||
|
|
$fail("Cache-Schreiben fehlgeschlagen ({$cacheFile})");
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// --- Entfernen ---
|
|||
|
|
if ($removeCode !== '') {
|
|||
|
|
if (!isset($index[$removeCode])) {
|
|||
|
|
$fail("Kein Eintrag mit Shortcode „{$removeCode}“.");
|
|||
|
|
}
|
|||
|
|
unset($posts[$index[$removeCode]]);
|
|||
|
|
$bild = "{$imgDir}/{$removeCode}.jpg";
|
|||
|
|
if (is_file($bild)) {
|
|||
|
|
unlink($bild);
|
|||
|
|
}
|
|||
|
|
$speichern();
|
|||
|
|
$log("Entfernt: {$removeCode} (Eintrag und Bild).");
|
|||
|
|
if ($urls === []) {
|
|||
|
|
exit(0);
|
|||
|
|
}
|
|||
|
|
// Index neu aufbauen, falls im selben Lauf noch etwas dazukommt.
|
|||
|
|
$index = [];
|
|||
|
|
foreach (array_values($posts) as $i => $post) {
|
|||
|
|
$index[$post['shortcode']] = $i;
|
|||
|
|
}
|
|||
|
|
$posts = array_values($posts);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Bild auf Instagram-Portrait 4:5 (640×800) bringen.
|
|||
|
|
* Unverändert aus bin/instagram-sync.php übernommen — die einzige Stelle im Projekt, die GD
|
|||
|
|
* direkt bedient, weil resize_image_variants() nur proportional skaliert und keinen mittigen
|
|||
|
|
* Crop auf ein festes Seitenverhältnis kann.
|
|||
|
|
*/
|
|||
|
|
$bildSchreiben = static function (string $rohdaten, string $ziel): bool {
|
|||
|
|
$src = imagecreatefromstring($rohdaten);
|
|||
|
|
if ($src === false) {
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
$w = imagesx($src);
|
|||
|
|
$h = imagesy($src);
|
|||
|
|
if ($w / $h > 4 / 5) {
|
|||
|
|
$cropH = $h;
|
|||
|
|
$cropW = (int) round($h * 4 / 5);
|
|||
|
|
} else {
|
|||
|
|
$cropW = $w;
|
|||
|
|
$cropH = (int) round($w * 5 / 4);
|
|||
|
|
}
|
|||
|
|
$out = imagecreatetruecolor(640, 800);
|
|||
|
|
imagecopyresampled($out, $src, 0, 0, (int) (($w - $cropW) / 2), (int) (($h - $cropH) / 2), 640, 800, $cropW, $cropH);
|
|||
|
|
|
|||
|
|
return imagejpeg($out, $ziel, 80);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
if (!is_dir($imgDir) && !mkdir($imgDir, 0755, true) && !is_dir($imgDir)) {
|
|||
|
|
$fail("Bildordner nicht anlegbar: {$imgDir}");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --- Beiträge aufnehmen ---
|
|||
|
|
$neu = 0;
|
|||
|
|
$aktualisiert = 0;
|
|||
|
|
$uebersprungen = 0;
|
|||
|
|
|
|||
|
|
$bekannt = 0;
|
|||
|
|
foreach ($urls as $url) {
|
|||
|
|
$shortcode = $shortcodeFromUrl($url);
|
|||
|
|
// Beim Profil-Lauf zählt nur, was neu ist: die Liste enthält jedes Mal dieselben zwölf
|
|||
|
|
// Beiträge, und jeder bekannte wäre ein überflüssiger Abruf bei Instagram.
|
|||
|
|
if ($vomProfil && $shortcode !== '' && isset($index[$shortcode])) {
|
|||
|
|
$bekannt++;
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
if ($shortcode === '') {
|
|||
|
|
$log("Keine Beitrags-URL: {$url} — übersprungen.", 'WARN');
|
|||
|
|
$uebersprungen++;
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$body = $fetch('https://www.instagram.com/api/v1/oembed/?url=' . rawurlencode("https://www.instagram.com/p/{$shortcode}/"));
|
|||
|
|
$data = $body !== false ? json_decode($body, true) : null;
|
|||
|
|
|
|||
|
|
if (!is_array($data) && $manualImg === '') {
|
|||
|
|
$log("oEmbed liefert nichts für {$shortcode} — übersprungen. Ist der Beitrag öffentlich? Notfalls mit --bild=<pfad> nachreichen.", 'WARN');
|
|||
|
|
$uebersprungen++;
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
$data ??= [];
|
|||
|
|
|
|||
|
|
// Fremde Beiträge abweisen: ein versehentlich eingeworfener Link soll nicht still im
|
|||
|
|
// Vereinsfeed landen.
|
|||
|
|
$autor = (string) ($data['author_name'] ?? '');
|
|||
|
|
if ($autor !== '' && strcasecmp($autor, $username) !== 0) {
|
|||
|
|
$log("Beitrag {$shortcode} stammt von @{$autor}, nicht von @{$username} — abgelehnt.", 'WARN');
|
|||
|
|
$uebersprungen++;
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$bestand = isset($index[$shortcode]) ? $posts[$index[$shortcode]] : null;
|
|||
|
|
$bild = "{$imgDir}/{$shortcode}.jpg";
|
|||
|
|
|
|||
|
|
// Bild nur holen, wenn es fehlt (oder von Hand geliefert wird) — ein zweiter Lauf mit
|
|||
|
|
// demselben Link kostet damit nichts.
|
|||
|
|
if ($manualImg !== '') {
|
|||
|
|
if (!is_file($manualImg)) {
|
|||
|
|
$fail("Bilddatei nicht gefunden: {$manualImg}");
|
|||
|
|
}
|
|||
|
|
if (!$bildSchreiben((string) file_get_contents($manualImg), $bild)) {
|
|||
|
|
$fail("Bild unlesbar: {$manualImg}");
|
|||
|
|
}
|
|||
|
|
} elseif (!is_file($bild)) {
|
|||
|
|
$quelle = (string) ($data['thumbnail_url'] ?? '');
|
|||
|
|
if ($quelle === '') {
|
|||
|
|
$log("Kein Bild in der oEmbed-Antwort für {$shortcode} — übersprungen.", 'WARN');
|
|||
|
|
$uebersprungen++;
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
$rohdaten = $fetch($quelle);
|
|||
|
|
if ($rohdaten === false || !$bildSchreiben($rohdaten, $bild)) {
|
|||
|
|
$log("Bild-Download fehlgeschlagen: {$shortcode} — übersprungen.", 'WARN');
|
|||
|
|
$uebersprungen++;
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Caption: aus oEmbed, sonst die bestehende behalten (Notfall-Pfad mit --bild).
|
|||
|
|
$caption = (string) ($data['title'] ?? ($bestand['caption'] ?? ''));
|
|||
|
|
$alt = trim(strtok($caption, "\n") ?: '');
|
|||
|
|
$alt = $alt !== '' ? mb_substr($alt, 0, 120) : 'Instagram-Beitrag des TSV 08 Kulmbach';
|
|||
|
|
|
|||
|
|
$takenAt = !empty($data['media_id'])
|
|||
|
|
? $takenAtFromMediaId((string) $data['media_id'])
|
|||
|
|
: (int) ($bestand['taken_at'] ?? time());
|
|||
|
|
|
|||
|
|
$eintrag = [
|
|||
|
|
'shortcode' => $shortcode,
|
|||
|
|
'caption' => $caption,
|
|||
|
|
'alt' => $alt,
|
|||
|
|
'taken_at' => $takenAt,
|
|||
|
|
// oEmbed sagt nicht, ob es ein Video ist. Steuert nur das ▶ auf der Kachel und wird
|
|||
|
|
// bei Bedarf im Chat nachgetragen; ein bestehender Wert bleibt erhalten.
|
|||
|
|
'is_video' => (bool) ($bestand['is_video'] ?? false),
|
|||
|
|
'image' => "img/instagram/{$shortcode}.jpg",
|
|||
|
|
'url' => "https://www.instagram.com/p/{$shortcode}/",
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
if ($bestand !== null) {
|
|||
|
|
$posts[$index[$shortcode]] = $eintrag;
|
|||
|
|
$aktualisiert++;
|
|||
|
|
$log("Aktualisiert: {$shortcode}");
|
|||
|
|
} else {
|
|||
|
|
$posts[] = $eintrag;
|
|||
|
|
$index[$shortcode] = array_key_last($posts);
|
|||
|
|
$neu++;
|
|||
|
|
$log("Aufgenommen: {$shortcode} — " . mb_substr($alt, 0, 60));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if ($neu === 0 && $aktualisiert === 0) {
|
|||
|
|
if ($vomProfil && $uebersprungen === 0) {
|
|||
|
|
// Kein Fehler: das Profil hatte schlicht nichts Neues.
|
|||
|
|
$log("Keine neuen Beiträge — alle {$bekannt} vom Profil sind schon im Feed.");
|
|||
|
|
echo "Nichts Neues. Der Feed ist auf dem aktuellen Stand (" . count($posts) . " Beiträge).\n";
|
|||
|
|
exit(0);
|
|||
|
|
}
|
|||
|
|
$log("Nichts aufgenommen ({$uebersprungen} übersprungen).", 'WARN');
|
|||
|
|
exit(1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$speichern();
|
|||
|
|
$log("Fertig: {$neu} neu, {$aktualisiert} aktualisiert, {$bekannt} schon bekannt, {$uebersprungen} übersprungen — "
|
|||
|
|
. count($posts) . ' Beiträge im Feed.');
|
|||
|
|
|
|||
|
|
echo "\nBeiträge in data/instagram.json: " . count($posts) . "\n";
|
|||
|
|
foreach (array_slice($posts, 0, 5) as $post) {
|
|||
|
|
printf(" %-14s %s %s\n", $post['shortcode'], date('d.m.Y', $post['taken_at']), mb_substr($post['alt'], 0, 50));
|
|||
|
|
}
|