> storage/logs/instagram.log 2>&1 * * Strategie-Kette (erster Erfolg gewinnt): * 1. feed/user-API (JSON, items[] — antwortet auch dann, wenn web_profile_info 400 wirft) * 2. web_profile_info-API (JSON, edges[]) * 3. 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; }; /** * Posts aus der feed/user-Struktur ziehen (items[] mit image_versions2 statt edges[]). * @return array> */ $extractFeedItems = static function (array $data) use ($maxPosts): array { $posts = []; foreach (array_slice($data['items'] ?? [], 0, $maxPosts) as $item) { // Größten Bild-Kandidaten nehmen — wird unten ohnehin auf 640x800 gecroppt. $url = ''; $bestWidth = 0; foreach ($item['image_versions2']['candidates'] ?? [] as $candidate) { $width = (int) ($candidate['width'] ?? 0); if ($width > $bestWidth && !empty($candidate['url'])) { $bestWidth = $width; $url = (string) $candidate['url']; } } if (empty($item['code']) || $url === '') { continue; } $posts[] = [ 'shortcode' => (string) $item['code'], 'caption' => trim((string) ($item['caption']['text'] ?? '')), 'taken_at' => (int) ($item['taken_at'] ?? 0), // media_type: 1 = Bild, 2 = Video/Reel, 8 = Karussell. 'is_video' => (int) ($item['media_type'] ?? 1) === 2 || !empty($item['video_versions']), 'display_url' => $url, ]; } return $posts; }; $apiHeaders = [ 'x-ig-app-id: 936619743392459', 'Accept: application/json', 'Referer: https://www.instagram.com/' . $username . '/', ]; // --- Strategie 1: feed/user-API --- $posts = []; $body = $fetch("https://www.instagram.com/api/v1/feed/user/{$username}/username/?count=12", $apiHeaders); if ($body !== false) { $data = json_decode($body, true); if (is_array($data)) { $posts = $extractFeedItems($data); $log('Strategie 1 (feed/user): ' . count($posts) . ' Posts'); } } // --- Strategie 2: web_profile_info-API --- if ($posts === []) { $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 2 (web_profile_info): ' . count($posts) . ' Posts'); } } } // --- Strategie 3: 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('/