> storage/logs/instagram.log 2>&1 * * Strategie-Kette (erster Erfolg gewinnt): * 1. web_profile_info-API (JSON, inkl. der letzten 12 Posts) * 2. Profil-HTML nach eingebettetem JSON parsen */ if (PHP_SAPI !== 'cli') { exit(1); } require dirname(__DIR__) . '/app/bootstrap.php'; $verbose = in_array('-v', $argv, true); $username = (string) config('instagram.username', 'tsv08kulmbach'); $maxPosts = (int) config('instagram.max_posts', 9); $imgDir = PUBLIC_PATH . '/assets/img/instagram'; $cacheFile = DATA_PATH . '/instagram.json'; $log = static function (string $msg) use ($verbose): void { $line = '[' . date('c') . "] {$msg}\n"; file_put_contents(STORAGE_PATH . '/logs/instagram.log', $line, FILE_APPEND); if ($verbose) { echo $line; } }; $fail = static function (string $msg) use ($log): never { $log("FEHLER: {$msg} — bestehender Cache bleibt unangetastet."); exit(1); }; // --- Lock gegen parallele Läufe --- $lock = fopen(STORAGE_PATH . '/cache/instagram.lock', 'c'); if (!$lock || !flock($lock, LOCK_EX | LOCK_NB)) { $fail('Läuft bereits (Lock belegt)'); } // --- HTTP-Helfer mit Browser-typischen Headern --- $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; }; /** * Posts aus der web_profile_info-JSON-Struktur ziehen. * @return array> */ $extractPosts = static function (array $data) use ($maxPosts): array { $edges = $data['data']['user']['edge_owner_to_timeline_media']['edges'] ?? $data['graphql']['user']['edge_owner_to_timeline_media']['edges'] ?? []; $posts = []; foreach (array_slice($edges, 0, $maxPosts) as $edge) { $node = $edge['node'] ?? []; if (empty($node['shortcode']) || empty($node['display_url'])) { continue; } $caption = trim((string) ($node['edge_media_to_caption']['edges'][0]['node']['text'] ?? '')); $posts[] = [ 'shortcode' => (string) $node['shortcode'], 'caption' => $caption, 'taken_at' => (int) ($node['taken_at_timestamp'] ?? 0), 'is_video' => (bool) ($node['is_video'] ?? false), 'display_url' => (string) $node['display_url'], ]; } return $posts; }; // --- Strategie 1: web_profile_info-API --- $posts = []; $apiHeaders = [ 'x-ig-app-id: 936619743392459', 'Accept: application/json', 'Referer: https://www.instagram.com/' . $username . '/', ]; $body = $fetch("https://www.instagram.com/api/v1/users/web_profile_info/?username={$username}", $apiHeaders); if ($body !== false) { $data = json_decode($body, true); if (is_array($data)) { $posts = $extractPosts($data); $log('Strategie 1 (web_profile_info): ' . count($posts) . ' Posts'); } } // --- Strategie 2: Profil-HTML nach eingebettetem JSON parsen --- if ($posts === []) { $html = $fetch("https://www.instagram.com/{$username}/"); if ($html !== false) { // Eingebettete JSON-Blöcke (script type application/json) nach Timeline-Daten durchsuchen. if (preg_match_all('/