Files
tsv08kulmbach-website/bin/anzeige-add.php

186 lines
6.8 KiB
PHP
Raw Normal View History

Stadionzeitung, Veranstaltungen, News-Artikel, Lightbox und Anzeigen-Verteilung 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
2026-07-31 14:55:27 +02:00
<?php
declare(strict_types=1);
/**
* Sponsoren-Anzeige in den Bestand aufnehmen oder ihr Motiv erneuern (nur CLI).
*
* php bin/anzeige-add.php <bild-oder-ordner> [-v]
*
* Beispiele:
* php bin/anzeige-add.php ~/Desktop/anzeigen/ alle Bilder im Ordner
* php bin/anzeige-add.php ~/Desktop/eldorado.jpg nur diese eine
*
* DER DATEINAME IST DER SLUG: `eldorado.jpg` wird zu `img/anzeigen/eldorado-*.jpg`
* und findet damit den bestehenden Eintrag wieder. Ein neues Motiv für einen
* bekannten Sponsor heißt also einfach wieder `eldorado.jpg`.
*
* Anzeigen sind ein DAUERHAFTER Bestand (data/anzeigen.json), kein Zubehör einer
* einzelnen Ausgabe: Sponsorenmotive ändern sich selten, die Stadionzeitung
* erscheint oft. bin/stadionzeitung-add.php holt sie sich von hier, pro Heft ist
* nichts abzuliefern. Vorher lag jede Anzeige als Kopie unter jeder Ausgabe und
* ihr Alt-Text musste jedes Mal neu geschrieben werden.
*
* Das Skript schreibt ausschließlich `base` und `widths`. `sponsor`, `alt`,
* `aktiv` und die REIHENFOLGE der Liste (= Reihenfolge im Heft) sind
* chat-gepflegt und überleben jeden Lauf unangetastet dasselbe Prinzip wie bei
* bin/veranstaltung-bilder.php. Bei einem neuen Sponsor wird `sponsor` aus dem
* Dateinamen vorbelegt und muss im Chat richtiggestellt werden; das Skript sagt,
* bei welchen.
*/
if (PHP_SAPI !== 'cli') {
exit(1);
}
require dirname(__DIR__) . '/app/bootstrap.php';
$args = array_values(array_filter(array_slice($argv, 1), static fn (string $a): bool => $a !== '-v'));
$verbose = in_array('-v', $argv, true);
$log = static function (string $msg, string $level = 'INFO') use ($verbose): void {
log_write('stadionzeitung', $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);
};
if ($args === []) {
fwrite(STDERR, "Nutzung: php bin/anzeige-add.php <bild-oder-ordner> [-v]\n");
exit(1);
}
// --- Quelldateien einsammeln ---
$exts = 'jpg,jpeg,png,webp,JPG,JPEG,PNG,WEBP';
$quelle = $args[0];
if (is_dir($quelle)) {
$sources = glob(rtrim($quelle, '/') . "/*.{{$exts}}", GLOB_BRACE) ?: [];
natsort($sources);
$sources = array_values($sources);
} elseif (is_file($quelle)) {
$sources = [$quelle];
} else {
$fail("Nicht gefunden: {$quelle}");
}
if ($sources === []) {
$fail("Keine Bilder in {$quelle} gefunden.");
}
// Selbst-Import verhindern: zeigt jemand auf den Zielordner, wären die Quellen
// die erzeugten Varianten (`eldorado-1440.jpg`), und aus jeder Breite entstünde
// ein eigener Sponsor („eldorado 1440"). Einmal reingelaufen, deshalb der Riegel.
$zielReal = realpath(PUBLIC_PATH . '/assets/img/anzeigen');
foreach ($sources as $src) {
if ($zielReal !== false && realpath(dirname($src)) === $zielReal) {
$fail('Quelle liegt im Zielordner public/assets/img/anzeigen/ — das sind die erzeugten Varianten, nicht die Originale.');
}
}
// --- Lock gegen parallele Läufe ---
if (!is_dir(STORAGE_PATH . '/cache')) {
mkdir(STORAGE_PATH . '/cache', 0755, true);
}
$lock = fopen(STORAGE_PATH . '/cache/anzeigen.lock', 'c');
if (!$lock || !flock($lock, LOCK_EX | LOCK_NB)) {
$log('Läuft bereits (Lock belegt) — Lauf übersprungen.', 'WARN');
exit(1);
}
$dataFile = DATA_PATH . '/anzeigen.json';
$data = is_file($dataFile)
? (json_decode((string) file_get_contents($dataFile), true) ?: ['anzeigen' => []])
: ['anzeigen' => []];
$destDir = PUBLIC_PATH . '/assets/img/anzeigen';
if (!is_dir($destDir)) {
mkdir($destDir, 0755, true);
}
$widths = [480, 960, 1440];
/** Dateiname zu Slug: Kleinschreibung, Umlaute weg, alles andere zu Bindestrichen. */
$slugify = static function (string $text): string {
$text = strtr($text, ['ä' => 'ae', 'ö' => 'oe', 'ü' => 'ue', 'Ä' => 'Ae', 'Ö' => 'Oe', 'Ü' => 'Ue', 'ß' => 'ss']);
$text = strtolower($text);
return trim(preg_replace('/[^a-z0-9]+/', '-', $text) ?? '', '-');
};
$neu = [];
$erneuert = [];
foreach ($sources as $src) {
$slug = $slugify(pathinfo($src, PATHINFO_FILENAME));
if ($slug === '') {
$log('Dateiname ergibt keinen Slug, übersprungen: ' . basename($src), 'WARN');
continue;
}
// Alte Varianten dieses Slugs weg, sonst bleiben bei einem kleineren Motiv
// Breiten von vorher liegen und die JSON verspricht Dateien, die zum Bild
// nicht mehr passen.
Sportfest-Ausgabe der Stadionzeitung zum Heimspiel gegen ATS Wartenfels Neue Live-Seiten, geschaltet vom chat-gepflegten Feld `veranstaltung` (Slug aus data/veranstaltungen.json) — Programm und Turnierpläne kommen damit aus derselben Quelle wie die Website, nicht als Kopie ins Heft: - Programm-Seite (programm[], rendert zugleich auf /veranstaltungen/sportfest-2026) - vier Turnierplan-Seiten (turniere[]) mit leerer Ergebnisspalte und leerer Abschlusstabelle zum Eintragen am Platz - Gegnerseite je Mannschaft mit Heimspiel am Fest statt nur der tragenden: stadionzeitung_gegner_fixtures() ist die einzige Stelle, die das entscheidet, Seitenplanung und Snapshot lesen beide dort. `gegner` ist deshalb eine Map je Team-Key; die Vorschau überspringt jedes Spiel mit eigener Gegnerseite - Danke-Seite mit Partner-Logo-Wand aus data/partners.json und QR auf /partner-werden, dazu das Anzeigen-Feld `platzierung` für Motive mit festem Platz auf einer Live-Seite (Emons über der Logo-Wand). Vorher klebte der Dank als leere Fläche unten an einem Anzeigen-Bild - Cover einer Sonderausgabe: das Fest als Titelgeschichte statt einer der drei Begegnungen, dazu das Hauptplakat schräg gelegt an Stelle des Spielerfotos Ausgabe 2026-09-04-ats-wartenfels als ENTWURF angelegt, 48 Seiten (12 Bögen), Tabellen auf gerader Seite, längster Anzeigenblock zwei. Veröffentlicht wird auf dem Server, dort liegt der frische Matchcenter-Stand. Nebenbei behoben: bin/anzeige-add.php räumte beim Anlegen alle Motive weg, deren Slug mit demselben Text beginnt (`emons` löschte `emons-partnerdank` und `emons-stuebinger`). Muster jetzt auf die Breiten-Endung eingeschränkt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KubsajzuV6N6QsSd3xKNjC
2026-09-03 10:57:52 +02:00
// Muster bewusst auf die BREITEN-Endung eingeschränkt (`-<zahl>.jpg`): mit
// `{$slug}-*.jpg` löscht ein Slug die Motive jedes Slugs, der ihn als Präfix
// hat — `emons` räumte so die Dateien von `emons-partnerdank` mit weg
// (03.09.2026 beobachtet, dort zufällig gewollt, generell Datenverlust).
foreach (glob("{$destDir}/{$slug}-[0-9]*.jpg") ?: [] as $alt) {
Stadionzeitung, Veranstaltungen, News-Artikel, Lightbox und Anzeigen-Verteilung 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
2026-07-31 14:55:27 +02:00
unlink($alt);
}
try {
// Qualität 82 wie bei den Plakaten: Anzeigen tragen Text, und Ringing um
// Buchstaben sieht man.
$variants = resize_image_variants($src, "{$destDir}/{$slug}", $widths, 82);
} catch (RuntimeException $e) {
$fail('Anzeige nicht verarbeitbar (' . basename($src) . '): ' . $e->getMessage());
}
$generated = array_values(array_unique(array_column($variants, 'width')));
sort($generated);
// Bestehenden Eintrag suchen und NUR base/widths ersetzen.
$index = null;
foreach ($data['anzeigen'] as $i => $eintrag) {
if (($eintrag['slug'] ?? '') === $slug) {
$index = $i;
break;
}
}
if ($index === null) {
// Neuer Sponsor: Platzhalter-Name aus dem Slug, im Chat zu ersetzen.
$data['anzeigen'][] = [
'slug' => $slug,
'sponsor' => str_replace('-', ' ', $slug),
'aktiv' => true,
'base' => "img/anzeigen/{$slug}",
'widths' => $generated,
];
$neu[] = $slug;
} else {
$data['anzeigen'][$index]['base'] = "img/anzeigen/{$slug}";
$data['anzeigen'][$index]['widths'] = $generated;
$erneuert[] = $slug;
}
$log("Anzeige {$slug}: " . implode('/', $generated) . 'px');
}
$payload = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$tmp = $dataFile . '.tmp';
file_put_contents($tmp, $payload . "\n");
if (!rename($tmp, $dataFile)) {
$fail('Konnte data/anzeigen.json nicht schreiben.');
}
$log(sprintf('Anzeigen-Bestand aktualisiert: %d neu, %d erneuert.', count($neu), count($erneuert)));
echo 'Anzeigen-Bestand: ' . count($data['anzeigen']) . ' Motive, ' . count($neu) . ' neu, ' . count($erneuert) . " erneuert.\n";
if ($neu !== []) {
echo "Sponsorenname noch Platzhalter bei: " . implode(', ', $neu) . "\n";
echo "In data/anzeigen.json richtigstellen (daraus entsteht der Alt-Text „Anzeige: …\").\n";
}