Instagram-Sync reparieren: feed/user-Endpunkt als Strategie 1

web_profile_info antwortet seit kurzem mit HTTP 400 und einem
Instagram-internen Schema-Fehler ("Asset asset://laser.provider/
ig_business_category_subvertical has been deleted") — kein IP-Block.
Gleichzeitig enthält das Profil-HTML kein edge_owner_to_timeline_media
mehr, womit auch der zweite Fallback tot war und der Cache seit
20.06. nicht mehr aktualisiert wurde.

api/v1/feed/user/{username}/username/ liefert weiterhin 200 mit den
letzten Posts, nur in anderer Form (items[] mit image_versions2 statt
edges[] mit display_url). Neuer Extractor dafür, eingehängt als
Strategie 1; die bisherigen zwei bleiben als Fallback 2 und 3.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-26 23:30:11 +02:00
parent 819bdf6173
commit 1c4f5cdbc5

View File

@@ -12,8 +12,9 @@ declare(strict_types=1);
* Cron: 17 6,18 * * * cd /pfad/zur/site && /usr/bin/php bin/instagram-sync.php >> 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
* 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);
@@ -89,23 +90,68 @@ $extractPosts = static function (array $data) use ($maxPosts): array {
return $posts;
};
// --- Strategie 1: web_profile_info-API ---
/**
* Posts aus der feed/user-Struktur ziehen (items[] mit image_versions2 statt edges[]).
* @return array<int, array<string, mixed>>
*/
$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 1 (web_profile_info): ' . count($posts) . ' Posts');
$log('Strategie 2 (web_profile_info): ' . count($posts) . ' Posts');
}
}
}
// --- Strategie 2: Profil-HTML nach eingebettetem JSON parsen ---
// --- Strategie 3: Profil-HTML nach eingebettetem JSON parsen ---
if ($posts === []) {
$html = $fetch("https://www.instagram.com/{$username}/");
if ($html !== false) {
@@ -138,7 +184,7 @@ if ($posts === []) {
}
}
}
$log('Strategie 2 (HTML-Parsing): ' . count($posts) . ' Posts');
$log('Strategie 3 (HTML-Parsing): ' . count($posts) . ' Posts');
}
}