Stadionzeitung: Live-Seiten (Tabellen, Ergebnisse, Vorschau, Termine, News, Kontakt, Historie, Sportheim, Partner, Momente, Impressum), automatisches Cover, Swipe-Viewer mit Vollbildmodus und die Druckfassung als Falt-Heft. Anzeigen kommen jetzt aus dem Bestand und werden gleichmäßig verteilt, nie mehr als zwei hintereinander (vorher vier Blöcke à fünf). Die Doppelseiten-Regel rechnet das Skript selbst, statt sie von Hand zu prüfen. Veranstaltungen: Bereich /veranstaltungen mit Detailseite pro Fest, Lebenszyklus über das Datum (Ankündigung vor dem Fest, Rückblick danach), Galerie mit Lightbox. veranstaltungen.json ist dritte Termin-Quelle, damit ein Fest nie doppelt gepflegt wird. News-Artikel als dritte dynamische Prefix-Route, gemeinsamer detail-head. Behobene Fehler: - Wappen-Kontexte setzten Zeilen-Layout auf .crest statt auf die Zeile; das Wappen wurde zum Grid, Vereinsname und Tore rutschten darunter zusammen (Ergebnis-Rückblick, Vorschau, Cover). - --header-h (60px) unterschätzt die Kopfleiste um 32px (Logo 72px + 20px Versatz): neues abgeleitetes --header-space, sonst klebten Zurück-Links unter dem Logo. - base_url in der Produktions-Config ohne www, während .htaccess auf www umleitet; preflight prüft das jetzt. - .lightbox brauchte display:none mit (0,2,0), sonst lag das Overlay offen. Neue Werkzeuge: bin/anzeige-add.php (Anzeigen-Bestand), bin/veranstaltung-bilder.php (Plakate und Galerien, Alt-Texte folgen der Quelldatei), bin/qr.php (Druck-QR mit Warnung, wenn base_url nicht auf die echte Domain zeigt). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
228 lines
8.5 KiB
PHP
228 lines
8.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* Bilder einer Veranstaltung einarbeiten (nur CLI).
|
|
*
|
|
* php bin/veranstaltung-bilder.php <slug> <ordner> [-v]
|
|
*
|
|
* Erwarteter Ordnerinhalt (beides optional, mindestens eines nötig):
|
|
* <ordner>/plakate/*.{jpg|jpeg|png|webp} Plakate, natsort = Reihenfolge.
|
|
* Das ERSTE ist das Hauptplakat und
|
|
* landet auf Karten und im OG-Bild.
|
|
* <ordner>/bilder/*.{jpg|…} Fotos vom Fest, natsort = Reihenfolge
|
|
*
|
|
* DER SLUG MUSS SCHON IN data/veranstaltungen.json STEHEN. Umgekehrt zur
|
|
* Stadionzeitung (dort legt das Skript die Ausgabe an und der Chat reichert sie
|
|
* an) gilt hier: der Eintrag entsteht im Chat, das Skript reichert ihn an. Genau
|
|
* daraus folgt die Garantie, dass chat-gepflegte Felder nie verloren gehen —
|
|
* das Skript ändert am Eintrag ausschließlich `plakate` und `galerie` und lässt
|
|
* alles andere unangetastet (auch `status`: nachgelieferte Fotos sind kein Grund,
|
|
* eine veröffentlichte Seite offline zu nehmen — anders als bei
|
|
* bin/stadionzeitung-add.php, wo ersetzte Seiten den Stand neu prüfbar machen).
|
|
*
|
|
* DER ORDNER IST IMMER DIE VOLLSTÄNDIGE GALERIE. Ein erneuter Lauf ersetzt sie
|
|
* komplett (alte Dateien werden vorher gelöscht). Wer nur drei Fotos nachliefert,
|
|
* verliert die anderen zwanzig — der Ordner muss also alles enthalten. Diese
|
|
* Wahl ist Absicht: nur so lassen sich Fotos auch entfernen und umsortieren.
|
|
*
|
|
* Alt-Texte schreibt Claude im Chat, nicht dieses Skript. Damit sie einen
|
|
* erneuten Lauf überleben, trägt jeder Galerie-Eintrag `source` (den
|
|
* Originaldateinamen) — daran werden bestehende Alt-Texte wiedererkannt, auch
|
|
* wenn sich die Reihenfolge durch ein neues Foto verschiebt. Neue Fotos bekommen
|
|
* den Veranstaltungstitel als Platzhalter; das Skript nennt sie am Ende
|
|
* namentlich, damit die Texte in derselben Chat-Runde entstehen.
|
|
*/
|
|
if (PHP_SAPI !== 'cli') {
|
|
exit(1);
|
|
}
|
|
|
|
require dirname(__DIR__) . '/app/bootstrap.php';
|
|
|
|
$args = array_values(array_filter($argv, static fn (string $a): bool => $a !== '-v'));
|
|
$verbose = in_array('-v', $argv, true);
|
|
array_shift($args); // Skriptname
|
|
|
|
if (count($args) < 2) {
|
|
fwrite(STDERR, "Nutzung: php bin/veranstaltung-bilder.php <slug> <ordner> [-v]\n");
|
|
exit(1);
|
|
}
|
|
|
|
[$slug, $sourceDir] = $args;
|
|
|
|
$log = static function (string $msg, string $level = 'INFO') use ($verbose): void {
|
|
log_write('veranstaltungen', $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);
|
|
};
|
|
|
|
// --- Eingaben validieren ---
|
|
if (!is_dir($sourceDir)) {
|
|
$fail("Ordner nicht gefunden: {$sourceDir}");
|
|
}
|
|
|
|
$dataFile = DATA_PATH . '/veranstaltungen.json';
|
|
$data = is_file($dataFile)
|
|
? (json_decode((string) file_get_contents($dataFile), true) ?: ['veranstaltungen' => []])
|
|
: ['veranstaltungen' => []];
|
|
|
|
$index = null;
|
|
foreach ($data['veranstaltungen'] ?? [] as $i => $entry) {
|
|
if (($entry['slug'] ?? '') === $slug) {
|
|
$index = $i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ($index === null) {
|
|
$known = implode(', ', array_column($data['veranstaltungen'] ?? [], 'slug'));
|
|
$fail("Unbekannter Slug '{$slug}'. Der Eintrag muss zuerst in data/veranstaltungen.json angelegt werden. Bekannt: {$known}");
|
|
}
|
|
|
|
$veranstaltung = $data['veranstaltungen'][$index];
|
|
$titel = (string) ($veranstaltung['title'] ?? $slug);
|
|
|
|
// --- Quelldateien einsammeln ---
|
|
$exts = 'jpg,jpeg,png,webp,JPG,JPEG,PNG,WEBP';
|
|
$findAll = static function (string $subdir) use ($sourceDir, $exts): array {
|
|
$dir = rtrim($sourceDir, '/') . "/{$subdir}";
|
|
if (!is_dir($dir)) {
|
|
return [];
|
|
}
|
|
$files = glob("{$dir}/*.{{$exts}}", GLOB_BRACE) ?: [];
|
|
// natsort, damit IMG_9 vor IMG_10 kommt (strcmp würde umgekehrt sortieren).
|
|
natsort($files);
|
|
return array_values($files);
|
|
};
|
|
|
|
$plakatSources = $findAll('plakate');
|
|
$fotoSources = $findAll('bilder');
|
|
|
|
if ($plakatSources === [] && $fotoSources === []) {
|
|
$fail("Weder plakate/* noch bilder/* in {$sourceDir} gefunden.");
|
|
}
|
|
|
|
// --- Lock gegen parallele Läufe ---
|
|
if (!is_dir(STORAGE_PATH . '/cache')) {
|
|
mkdir(STORAGE_PATH . '/cache', 0755, true);
|
|
}
|
|
$lock = fopen(STORAGE_PATH . '/cache/veranstaltungen.lock', 'c');
|
|
if (!$lock || !flock($lock, LOCK_EX | LOCK_NB)) {
|
|
$log('Läuft bereits (Lock belegt) — Lauf übersprungen.', 'WARN');
|
|
exit(1);
|
|
}
|
|
|
|
// --- Bestehende Alt-Texte sichern, bevor irgendetwas ersetzt wird ---
|
|
$altBySource = [];
|
|
foreach (array_merge($veranstaltung['plakate'] ?? [], $veranstaltung['galerie'] ?? []) as $bild) {
|
|
if (!empty($bild['source']) && !empty($bild['alt'])) {
|
|
$altBySource[(string) $bild['source']] = (string) $bild['alt'];
|
|
}
|
|
}
|
|
|
|
// --- Ziel-Ordner vorbereiten: bei erneutem Lauf alte Dateien wegräumen ---
|
|
$destDir = PUBLIC_PATH . "/assets/img/veranstaltungen/{$slug}";
|
|
if (!is_dir($destDir)) {
|
|
mkdir($destDir, 0755, true);
|
|
} else {
|
|
foreach (array_merge(
|
|
glob("{$destDir}/plakat-*.jpg") ?: [],
|
|
glob("{$destDir}/bild-*.jpg") ?: []
|
|
) as $old) {
|
|
unlink($old);
|
|
}
|
|
}
|
|
|
|
// Plakate brauchen die große Fassung: sie tragen Text, und in der Großansicht
|
|
// will man Uhrzeiten lesen können. Fotos nicht — dort ist die Großansicht nur
|
|
// „größer ansehen", und die dritte Stufe kostet bei 40 Fotos mehr Platz als sie
|
|
// bringt (gemessen: über die Hälfte des gesamten Bildvolumens).
|
|
$plakatWidths = [640, 1000, 1600];
|
|
$fotoWidths = [640, 1200];
|
|
|
|
/**
|
|
* Eine Quelldatei in die responsiven Breiten rendern und den Bild-Eintrag für
|
|
* die JSON bauen. Die `widths` im Eintrag sind die TATSÄCHLICH erzeugten —
|
|
* resize_image_variants() skaliert nie hoch, eine kleine Quelle liefert also
|
|
* weniger Varianten und img.php bleibt ehrlich.
|
|
*/
|
|
$render = static function (string $src, string $destBase, string $relBase, string $alt, int $quality, array $widths) use ($fail): array {
|
|
try {
|
|
$variants = resize_image_variants($src, $destBase, $widths, $quality);
|
|
} catch (RuntimeException $e) {
|
|
$fail('Bild nicht verarbeitbar (' . basename($src) . '): ' . $e->getMessage());
|
|
}
|
|
$generated = array_values(array_unique(array_column($variants, 'width')));
|
|
sort($generated);
|
|
|
|
return ['base' => $relBase, 'widths' => $generated, 'alt' => $alt];
|
|
};
|
|
|
|
$ohneAlt = [];
|
|
|
|
/**
|
|
* Eine Liste von Quelldateien zu Bild-Einträgen rendern. $prefix bestimmt den
|
|
* Dateinamen (plakat-01…, bild-01…), $quality die JPEG-Qualität: Plakate tragen
|
|
* Text, da ist Ringing um Buchstaben sichtbar — Fotos brauchen das nicht.
|
|
*/
|
|
$renderAlle = static function (array $sources, string $prefix, int $quality, array $widths, string $fallbackAlt)
|
|
use ($render, $destDir, $slug, $altBySource, &$ohneAlt): array {
|
|
$eintraege = [];
|
|
foreach ($sources as $i => $src) {
|
|
$source = basename($src);
|
|
$nummer = str_pad((string) ($i + 1), 2, '0', STR_PAD_LEFT);
|
|
$alt = $altBySource[$source] ?? '';
|
|
if ($alt === '') {
|
|
$ohneAlt[] = $source;
|
|
}
|
|
|
|
$eintrag = $render(
|
|
$src,
|
|
"{$destDir}/{$prefix}-{$nummer}",
|
|
"img/veranstaltungen/{$slug}/{$prefix}-{$nummer}",
|
|
$alt !== '' ? $alt : $fallbackAlt,
|
|
$quality,
|
|
$widths
|
|
);
|
|
$eintrag['source'] = $source;
|
|
$eintraege[] = $eintrag;
|
|
}
|
|
return $eintraege;
|
|
};
|
|
|
|
$plakate = $renderAlle($plakatSources, 'plakat', 82, $plakatWidths, "Plakat zum {$titel}");
|
|
$galerie = $renderAlle($fotoSources, 'bild', 78, $fotoWidths, $titel);
|
|
|
|
// --- Nur plakate + galerie ersetzen, alles andere bleibt, wie es ist ---
|
|
$data['veranstaltungen'][$index]['plakate'] = $plakate;
|
|
$data['veranstaltungen'][$index]['galerie'] = $galerie;
|
|
|
|
$payload = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
|
$tmp = $dataFile . '.tmp';
|
|
file_put_contents($tmp, $payload);
|
|
if (!rename($tmp, $dataFile)) {
|
|
$fail('Konnte data/veranstaltungen.json nicht schreiben.');
|
|
}
|
|
|
|
$log(sprintf(
|
|
'%s: Bilder komplett ersetzt (%d Plakate, %d Fotos).',
|
|
$slug,
|
|
count($plakate),
|
|
count($galerie)
|
|
));
|
|
|
|
echo "Veranstaltung {$slug}: Bilder komplett ersetzt, jetzt "
|
|
. count($plakate) . " Plakat(e) und " . count($galerie) . " Foto(s).\n";
|
|
|
|
if ($ohneAlt !== []) {
|
|
echo count($ohneAlt) . " Bild(er) noch ohne Alt-Text: " . implode(', ', $ohneAlt) . "\n";
|
|
echo "Alt-Texte in data/veranstaltungen.json nachtragen (Wir-Form, siehe CLAUDE.md).\n";
|
|
}
|