492 lines
19 KiB
PHP
492 lines
19 KiB
PHP
|
|
<?php
|
|||
|
|
|
|||
|
|
declare(strict_types=1);
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Fundstücke für die Vereinschronik 2028 einsammeln und ablegen.
|
|||
|
|
*
|
|||
|
|
* Holt zu einer Web-Adresse Überschrift, Datum, Teaser, Bildnachweis und das Aufmacherbild,
|
|||
|
|
* legt beides im richtigen Jahrzehnt-Ordner unter Vereinshistorie/ ab, schreibt einen
|
|||
|
|
* Quellen-Beleg daneben und eine Zeile ins Register (docs/chronik/register.csv).
|
|||
|
|
*
|
|||
|
|
* Warum überhaupt ein Skript und kein „Rechtsklick, Bild speichern": das Bild allein ist
|
|||
|
|
* in fünf Jahren wertlos. Woher es stammt, wann es entstand und wem es gehört, steht nirgends
|
|||
|
|
* im JPEG — und genau das braucht man, wenn die Chronik in den Druck geht. Das Skript hält
|
|||
|
|
* den Zusammenhang fest, in dem Moment, in dem er noch bekannt ist.
|
|||
|
|
*
|
|||
|
|
* Es wird NICHTS massenhaft abgegriffen: pro Aufruf eine Seite, mit Pause zwischen den
|
|||
|
|
* Abrufen. Die Presseportale haben keine Übersichtsseite mehr, über die man den Verein
|
|||
|
|
* vollständig abfragen könnte — die Fundstellen kommen aus gezielter Suche, nicht aus
|
|||
|
|
* einem Crawler.
|
|||
|
|
*
|
|||
|
|
* NUR CLI, nie im Request-Pfad (wie alle bin/-Skripte).
|
|||
|
|
*
|
|||
|
|
* Aufruf:
|
|||
|
|
* php bin/chronik-add.php <URL> [--datum=1972-07-15] [--titel="…"] [--art=Zeitungsartikel]
|
|||
|
|
* [--notiz="…"] [--urheber="…"] [--ohne-bild] [--nur-register]
|
|||
|
|
* php bin/chronik-add.php --liste=urls.txt Zeilen: URL | Datum | Titel (| optional)
|
|||
|
|
* php bin/chronik-add.php --datei=<pfad> --datum=1983-06-11 --titel="Festzug 75 Jahre"
|
|||
|
|
* [--quelle="Album Familie Rausch"] [--urheber="…"] [--art=Foto]
|
|||
|
|
* [--rechte-klaeren]
|
|||
|
|
* php bin/chronik-add.php --rechte nur 00_RECHTE-ZU-KLAEREN.txt neu schreiben
|
|||
|
|
* php bin/chronik-add.php --bericht Registerübersicht auf stdout
|
|||
|
|
*
|
|||
|
|
* Optionen --datum/--titel gewinnen immer gegen das, was die Seite selbst angibt.
|
|||
|
|
* --datei benennt die Datei um und verschiebt sie in den passenden Jahrzehnt-Ordner.
|
|||
|
|
*/
|
|||
|
|
if (PHP_SAPI !== 'cli') {
|
|||
|
|
exit(1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
require dirname(__DIR__) . '/app/bootstrap.php';
|
|||
|
|
require __DIR__ . '/chronik-lib.php';
|
|||
|
|
|
|||
|
|
$opts = [];
|
|||
|
|
$positional = [];
|
|||
|
|
foreach (array_slice($argv, 1) as $arg) {
|
|||
|
|
if (preg_match('/^--([a-z-]+)(?:=(.*))?$/s', $arg, $m)) {
|
|||
|
|
$opts[$m[1]] = $m[2] ?? true;
|
|||
|
|
} else {
|
|||
|
|
$positional[] = $arg;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$root = chronik_root();
|
|||
|
|
if (!is_dir($root)) {
|
|||
|
|
fwrite(STDERR, "Vereinshistorie/ fehlt — erst 'php bin/chronik-init.php' laufen lassen.\n");
|
|||
|
|
exit(1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --- Nebenmodi -------------------------------------------------------------
|
|||
|
|
|
|||
|
|
if (isset($opts['rechte'])) {
|
|||
|
|
$n = chronik_write_rights_todo();
|
|||
|
|
echo "00_RECHTE-ZU-KLAEREN.txt geschrieben — {$n} offene Posten.\n";
|
|||
|
|
exit(0);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (isset($opts['bericht'])) {
|
|||
|
|
chronik_report();
|
|||
|
|
exit(0);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --- HTTP ------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Browser-typische Header. Ohne sie antworten mehrere der Quellen mit 403 —
|
|||
|
|
* dieselbe Erfahrung wie beim BFV-Widget (dort HTTP 418), siehe matchcenter-sync.php.
|
|||
|
|
*/
|
|||
|
|
function chronik_fetch(string $url, ?string $referer = null): array
|
|||
|
|
{
|
|||
|
|
$ch = curl_init($url);
|
|||
|
|
curl_setopt_array($ch, [
|
|||
|
|
CURLOPT_RETURNTRANSFER => true,
|
|||
|
|
CURLOPT_FOLLOWLOCATION => true,
|
|||
|
|
CURLOPT_MAXREDIRS => 5,
|
|||
|
|
CURLOPT_TIMEOUT => 25,
|
|||
|
|
CURLOPT_ENCODING => '',
|
|||
|
|
CURLOPT_USERAGENT => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 '
|
|||
|
|
. '(KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36',
|
|||
|
|
CURLOPT_HTTPHEADER => array_filter([
|
|||
|
|
'Accept: text/html,application/xhtml+xml,image/avif,image/webp,*/*;q=0.8',
|
|||
|
|
'Accept-Language: de-DE,de;q=0.9',
|
|||
|
|
$referer !== null ? 'Referer: ' . $referer : null,
|
|||
|
|
]),
|
|||
|
|
]);
|
|||
|
|
$body = curl_exec($ch);
|
|||
|
|
$status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
|
|||
|
|
$type = (string) curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
|
|||
|
|
$final = (string) curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
|
|||
|
|
|
|||
|
|
return [
|
|||
|
|
'ok' => $body !== false && $status === 200,
|
|||
|
|
'status' => $status,
|
|||
|
|
'body' => $body === false ? '' : $body,
|
|||
|
|
'type' => $type,
|
|||
|
|
'url' => $final !== '' ? $final : $url,
|
|||
|
|
];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** Ersten Treffer eines Musters liefern, HTML-Entities aufgelöst. */
|
|||
|
|
function chronik_match(string $html, string $pattern): ?string
|
|||
|
|
{
|
|||
|
|
if (!preg_match($pattern, $html, $m)) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
$val = html_entity_decode(trim($m[1]), ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
|||
|
|
return $val !== '' ? $val : null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Fließtext einer Seite herausschälen.
|
|||
|
|
*
|
|||
|
|
* Grob und absichtlich: Skripte und Styles raus, Tags raus, alles behalten, was länger als
|
|||
|
|
* ein Navigationspunkt ist. Für saubere Extraktion bräuchte es pro Portal eine eigene Regel —
|
|||
|
|
* das lohnt hier nicht, weil der Text als Beleg dient und nicht als Layout.
|
|||
|
|
*/
|
|||
|
|
function chronik_body_text(string $html): string
|
|||
|
|
{
|
|||
|
|
$html = (string) preg_replace('#<(script|style|nav|header|footer|form)\b.*?</\1>#is', ' ', $html);
|
|||
|
|
$html = (string) preg_replace('#<(br|/p|/div|/h[1-6]|/li|/td|/tr)[^>]*>#i', "\n", $html);
|
|||
|
|
$text = html_entity_decode(strip_tags($html), ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
|||
|
|
|
|||
|
|
$keep = [];
|
|||
|
|
$seen = [];
|
|||
|
|
foreach (explode("\n", $text) as $line) {
|
|||
|
|
$line = trim((string) preg_replace('/[ \t\x{00A0}]+/u', ' ', $line));
|
|||
|
|
// 45 Zeichen trennt Fließtext zuverlässig von Menüpunkten und Buttons.
|
|||
|
|
if (mb_strlen($line) < 45 || isset($seen[$line])) {
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
$seen[$line] = true;
|
|||
|
|
$keep[] = $line;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return implode("\n\n", $keep);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** Seite auswerten: was an Metadaten drinsteht, ohne Layout-Annahmen. */
|
|||
|
|
function chronik_parse(string $html): array
|
|||
|
|
{
|
|||
|
|
$strip = static fn(string $s): string => trim((string) preg_replace('/\s+/', ' ', strip_tags($s)));
|
|||
|
|
|
|||
|
|
$titel = chronik_match($html, '/<meta property="og:title" content="([^"]+)"/i')
|
|||
|
|
?? chronik_match($html, '/<h1[^>]*>(.*?)<\/h1>/is')
|
|||
|
|
?? chronik_match($html, '/<title[^>]*>(.*?)<\/title>/is');
|
|||
|
|
if ($titel !== null) {
|
|||
|
|
$titel = $strip($titel);
|
|||
|
|
// Seitentitel tragen oft " | Portalname" mit — für den Dateinamen stört das nur.
|
|||
|
|
$titel = (string) preg_replace('/\s*[|–-]\s*(inFranken\.de|BFV|FUSSBALL\.DE|Deutsche Turnliga)\s*$/iu', '', $titel);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$datum = chronik_match($html, '/<meta property="article:published_time" content="(\d{4}-\d{2}-\d{2})/i')
|
|||
|
|
?? chronik_match($html, '/"datePublished"\s*:\s*"(\d{4}-\d{2}-\d{2})/i')
|
|||
|
|
?? chronik_match($html, '/datetime="(\d{4}-\d{2}-\d{2})/i');
|
|||
|
|
|
|||
|
|
$teaser = chronik_match($html, '/<meta property="og:description" content="([^"]+)"/i')
|
|||
|
|
?? chronik_match($html, '/<meta name="description" content="([^"]+)"/i');
|
|||
|
|
|
|||
|
|
$bild = chronik_match($html, '/<meta property="og:image" content="([^"]+)"/i');
|
|||
|
|
|
|||
|
|
// Bildnachweis steht bei Presseseiten fast immer in der Bildunterschrift
|
|||
|
|
// („Foto: Max Mustermann") — genau die Angabe, die später fehlt.
|
|||
|
|
$urheber = null;
|
|||
|
|
if (preg_match_all('/<figcaption[^>]*>(.*?)<\/figcaption>/is', $html, $caps)) {
|
|||
|
|
foreach ($caps[1] as $cap) {
|
|||
|
|
$cap = $strip(html_entity_decode($cap, ENT_QUOTES | ENT_HTML5, 'UTF-8'));
|
|||
|
|
if (preg_match('/\b(Foto|Bild|Quelle|©)\s*:?\s*(.{2,80})$/ui', $cap, $m)) {
|
|||
|
|
$urheber = trim($m[2], " .;");
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
$urheber ??= chronik_match($html, '/<meta name="author" content="([^"]+)"/i');
|
|||
|
|
|
|||
|
|
return compact('titel', 'datum', 'teaser', 'bild', 'urheber');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --- Ein Fundstück verarbeiten ---------------------------------------------
|
|||
|
|
|
|||
|
|
function chronik_add(string $url, array $opts, array $override = []): bool
|
|||
|
|
{
|
|||
|
|
$root = chronik_root();
|
|||
|
|
$heute = date('Y-m-d');
|
|||
|
|
|
|||
|
|
if (chronik_register_has_url($url) && !isset($opts['erneut'])) {
|
|||
|
|
echo "· schon im Register: {$url}\n";
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Bei Wayback-Adressen zählt für Rechte und Quellenkürzel die ARCHIVIERTE Seite,
|
|||
|
|
// nicht das Archiv — abgerufen wird trotzdem der Schnappschuss.
|
|||
|
|
$quellUrl = chronik_origin_url($url);
|
|||
|
|
$host = (string) (parse_url($quellUrl, PHP_URL_HOST) ?: '');
|
|||
|
|
if ($host === '') {
|
|||
|
|
fwrite(STDERR, "✗ Keine gültige Adresse: {$url}\n");
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$rights = chronik_rights($host);
|
|||
|
|
|
|||
|
|
$meta = ['titel' => null, 'datum' => null, 'teaser' => null, 'bild' => null, 'urheber' => null];
|
|||
|
|
$volltext = '';
|
|||
|
|
if (!isset($opts['nur-register'])) {
|
|||
|
|
$res = chronik_fetch($url);
|
|||
|
|
if (!$res['ok']) {
|
|||
|
|
fwrite(STDERR, "✗ HTTP {$res['status']}: {$url}\n");
|
|||
|
|
log_write('chronik', 'WARN', "Abruf fehlgeschlagen (HTTP {$res['status']}): {$url}");
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
$meta = chronik_parse($res['body']);
|
|||
|
|
// Die Rechtelage entscheidet, wie viel Text bleibt: eigenes Vereinsmaterial sichern
|
|||
|
|
// wir vollständig (sonst ist es weg — die alten Vereinsseiten sind offline), fremde
|
|||
|
|
// Presseartikel nur als Nachweis mit Überschrift und Anriss.
|
|||
|
|
if (!$rights['klaeren']) {
|
|||
|
|
$volltext = chronik_body_text($res['body']);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$titel = (string) ($override['titel'] ?? $opts['titel'] ?? $meta['titel'] ?? 'Ohne Titel');
|
|||
|
|
$datum = $override['datum'] ?? $opts['datum'] ?? $meta['datum'] ?? null;
|
|||
|
|
$urheber = (string) ($opts['urheber'] ?? $meta['urheber'] ?? '');
|
|||
|
|
$art = (string) ($opts['art'] ?? 'Zeitungsartikel');
|
|||
|
|
$notiz = (string) ($opts['notiz'] ?? '');
|
|||
|
|
if ($quellUrl !== $url) {
|
|||
|
|
$notiz = trim($notiz . ' Archivfassung (Internet Archive), Original nicht mehr online: ' . $quellUrl);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$tag = chronik_source_tag($host);
|
|||
|
|
$bucket = chronik_bucket(is_string($datum) ? $datum : null);
|
|||
|
|
$dnorm = chronik_date_norm(is_string($datum) ? $datum : null);
|
|||
|
|
$base = $dnorm . '_' . chronik_slug($titel) . '_' . $tag;
|
|||
|
|
|
|||
|
|
$dir = $root . '/' . $bucket;
|
|||
|
|
if (!is_dir($dir)) {
|
|||
|
|
mkdir($dir, 0775, true);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Namenskollision auflösen. Passiert wirklich: zwei Seiten „Bundesliga" und
|
|||
|
|
// „Bundesliga-1" mit gleicher Überschrift und gleichem Jahr ergeben denselben Namen —
|
|||
|
|
// ohne Suffix überschreibt der zweite Fund den ersten stillschweigend.
|
|||
|
|
// Anker ist die .quelle.txt, die zu jedem Fundstück entsteht.
|
|||
|
|
if (is_file($dir . '/' . $base . '.quelle.txt')) {
|
|||
|
|
$n = 2;
|
|||
|
|
while (is_file($dir . '/' . $base . '-' . $n . '.quelle.txt')) {
|
|||
|
|
$n++;
|
|||
|
|
}
|
|||
|
|
$base .= '-' . $n;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --- Aufmacherbild ---
|
|||
|
|
$bildDatei = '';
|
|||
|
|
if (!isset($opts['ohne-bild']) && !isset($opts['nur-register']) && is_string($meta['bild'])) {
|
|||
|
|
$img = chronik_fetch($meta['bild'], $url);
|
|||
|
|
$ext = match (true) {
|
|||
|
|
str_contains($img['type'], 'png') => 'png',
|
|||
|
|
str_contains($img['type'], 'webp') => 'webp',
|
|||
|
|
str_contains($img['type'], 'gif') => 'gif',
|
|||
|
|
default => 'jpg',
|
|||
|
|
};
|
|||
|
|
// 6 KB Untergrenze: Portale liefern bei fehlendem Aufmacher ein Platzhalter-Pixel aus.
|
|||
|
|
if ($img['ok'] && strlen($img['body']) > 6144) {
|
|||
|
|
$bildDatei = $base . '.' . $ext;
|
|||
|
|
file_put_contents($dir . '/' . $bildDatei, $img['body']);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --- Textbeleg: das Fundstück bleibt auffindbar, auch wenn die Seite verschwindet ---
|
|||
|
|
$belegDatei = $base . '.artikel.txt';
|
|||
|
|
$beleg = implode("\n", array_filter([
|
|||
|
|
$titel,
|
|||
|
|
str_repeat('=', min(72, max(8, strlen($titel)))),
|
|||
|
|
'',
|
|||
|
|
$dnorm !== '0000-00-00' ? 'Erschienen: ' . $dnorm : null,
|
|||
|
|
'Quelle: ' . $host,
|
|||
|
|
'URL: ' . $url,
|
|||
|
|
'Abgerufen: ' . $heute,
|
|||
|
|
'',
|
|||
|
|
// Bei Volltext ist der Teaser nur dessen erster Absatz — nicht doppelt hinschreiben.
|
|||
|
|
($meta['teaser'] !== null && $volltext === '') ? wordwrap((string) $meta['teaser'], 88, "\n") : null,
|
|||
|
|
$volltext === '' ? '' : null,
|
|||
|
|
'--',
|
|||
|
|
$volltext !== ''
|
|||
|
|
? "Eigenes Vereinsmaterial — vollständig gesichert, weil die Quellseite offline ist."
|
|||
|
|
: 'Nur Überschrift, Datum und Anrisstext festgehalten — der vollständige Artikeltext',
|
|||
|
|
$volltext !== ''
|
|||
|
|
? ''
|
|||
|
|
: 'gehört dem Verlag und wird hier nicht gespeichert. Für die Chronik über die URL',
|
|||
|
|
$volltext !== '' ? '' : 'oder das Zeitungsarchiv im Original nachlesen.',
|
|||
|
|
$volltext !== '' ? "\n" . wordwrap($volltext, 88, "\n") : null,
|
|||
|
|
], static fn($l): bool => $l !== null));
|
|||
|
|
file_put_contents($dir . '/' . $belegDatei, $beleg . "\n");
|
|||
|
|
|
|||
|
|
// --- Register + Beleg-Sidecar ---
|
|||
|
|
$row = [
|
|||
|
|
'datei' => $bucket . '/' . ($bildDatei !== '' ? $bildDatei : $belegDatei),
|
|||
|
|
'datum' => $dnorm,
|
|||
|
|
'titel' => $titel,
|
|||
|
|
'art' => $art,
|
|||
|
|
'quelle' => $host,
|
|||
|
|
'url' => $url,
|
|||
|
|
'urheber' => $urheber,
|
|||
|
|
'rechte' => $rights['status'],
|
|||
|
|
'klaeren' => $rights['klaeren'] ? 'ja' : 'nein',
|
|||
|
|
'abgerufen' => $heute,
|
|||
|
|
'notiz' => $notiz,
|
|||
|
|
];
|
|||
|
|
chronik_register_append($row);
|
|||
|
|
file_put_contents($dir . '/' . $base . '.quelle.txt', chronik_sidecar_text($row, $rights['hinweis']));
|
|||
|
|
|
|||
|
|
$flag = $rights['klaeren'] ? ' [Rechte klären]' : '';
|
|||
|
|
echo "✓ {$bucket}/{$base}" . ($bildDatei !== '' ? ' (+Bild)' : ' (nur Text)') . $flag . "\n";
|
|||
|
|
log_write('chronik', 'INFO', "Fundstück aufgenommen: {$bucket}/{$base} ({$host})");
|
|||
|
|
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Eine Datei aufnehmen, die schon da ist — Scan aus einer Festschrift, Foto aus einem
|
|||
|
|
* privaten Album, per Hand gespeichertes Pressebild.
|
|||
|
|
*
|
|||
|
|
* Genau derselbe Weg wie bei einer Web-Adresse: umbenennen nach Konvention, ins richtige
|
|||
|
|
* Jahrzehnt einsortieren, Beleg daneben, Zeile ins Register. Ohne das landen die Scans aus
|
|||
|
|
* dem Sammelaufruf als IMG_4711.jpg im Ordner und sind in einem Jahr nicht mehr zuzuordnen.
|
|||
|
|
*/
|
|||
|
|
function chronik_add_file(string $path, array $opts): bool
|
|||
|
|
{
|
|||
|
|
if (!is_file($path)) {
|
|||
|
|
fwrite(STDERR, "✗ Datei nicht gefunden: {$path}\n");
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$root = chronik_root();
|
|||
|
|
$heute = date('Y-m-d');
|
|||
|
|
|
|||
|
|
$datum = is_string($opts['datum'] ?? null) ? $opts['datum'] : null;
|
|||
|
|
$titel = (string) ($opts['titel'] ?? pathinfo($path, PATHINFO_FILENAME));
|
|||
|
|
$quelle = (string) ($opts['quelle'] ?? 'Vereinsbestand');
|
|||
|
|
$urheber = (string) ($opts['urheber'] ?? '');
|
|||
|
|
$art = (string) ($opts['art'] ?? 'Foto');
|
|||
|
|
$notiz = (string) ($opts['notiz'] ?? '');
|
|||
|
|
|
|||
|
|
// Ohne --quelle gilt: es kommt aus dem Verein und ist nutzbar. Mit --quelle=<Host>
|
|||
|
|
// greift dieselbe Einschätzung wie bei einem Abruf aus dem Netz.
|
|||
|
|
$rights = str_contains($quelle, '.')
|
|||
|
|
? chronik_rights($quelle)
|
|||
|
|
: ['status' => 'Vereinsbestand — Herkunft ' . $quelle, 'klaeren' => false,
|
|||
|
|
'hinweis' => 'Aus dem Bestand des Vereins bzw. von Mitgliedern. Bei erkennbaren Personen '
|
|||
|
|
. 'die Einwilligung zur Veröffentlichung einholen, bei Kindern die Eltern fragen.'];
|
|||
|
|
if (isset($opts['rechte-klaeren'])) {
|
|||
|
|
$rights['klaeren'] = true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$tag = str_contains($quelle, '.') ? chronik_source_tag($quelle) : strtoupper(chronik_slug($quelle, 12));
|
|||
|
|
$bucket = chronik_bucket($datum);
|
|||
|
|
$dnorm = chronik_date_norm($datum);
|
|||
|
|
$ext = strtolower(pathinfo($path, PATHINFO_EXTENSION)) ?: 'bin';
|
|||
|
|
$base = $dnorm . '_' . chronik_slug($titel) . '_' . $tag;
|
|||
|
|
|
|||
|
|
$dir = $root . '/' . $bucket;
|
|||
|
|
if (!is_dir($dir) && !mkdir($dir, 0775, true) && !is_dir($dir)) {
|
|||
|
|
fwrite(STDERR, "✗ Zielordner nicht anlegbar: {$bucket}\n");
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$ziel = $dir . '/' . $base . '.' . $ext;
|
|||
|
|
if (realpath($path) !== realpath($ziel) && !rename($path, $ziel)) {
|
|||
|
|
fwrite(STDERR, "✗ Verschieben fehlgeschlagen: {$path}\n");
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$row = [
|
|||
|
|
'datei' => $bucket . '/' . $base . '.' . $ext,
|
|||
|
|
'datum' => $dnorm,
|
|||
|
|
'titel' => $titel,
|
|||
|
|
'art' => $art,
|
|||
|
|
'quelle' => $quelle,
|
|||
|
|
'url' => (string) ($opts['url'] ?? ''),
|
|||
|
|
'urheber' => $urheber,
|
|||
|
|
'rechte' => $rights['status'],
|
|||
|
|
'klaeren' => $rights['klaeren'] ? 'ja' : 'nein',
|
|||
|
|
'abgerufen' => $heute,
|
|||
|
|
'notiz' => $notiz,
|
|||
|
|
];
|
|||
|
|
chronik_register_append($row);
|
|||
|
|
file_put_contents($dir . '/' . $base . '.quelle.txt', chronik_sidecar_text($row, $rights['hinweis']));
|
|||
|
|
|
|||
|
|
echo "✓ {$bucket}/{$base}.{$ext}" . ($rights['klaeren'] ? ' [Rechte klären]' : '') . "\n";
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** Registerübersicht — was haben wir, wo klafft die Lücke. */
|
|||
|
|
function chronik_report(): void
|
|||
|
|
{
|
|||
|
|
$path = chronik_register_path();
|
|||
|
|
if (!is_file($path)) {
|
|||
|
|
echo "Register ist noch leer.\n";
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
$fh = fopen($path, 'r');
|
|||
|
|
fgetcsv($fh, 0, ';', '"', '\\');
|
|||
|
|
|
|||
|
|
$perBucket = array_fill_keys(array_merge(array_values(CHRONIK_BUCKETS), [CHRONIK_DOKUMENTE, CHRONIK_UNDATIERT]), 0);
|
|||
|
|
$perQuelle = [];
|
|||
|
|
$offen = 0;
|
|||
|
|
$gesamt = 0;
|
|||
|
|
while (($r = fgetcsv($fh, 0, ';', '"', '\\')) !== false) {
|
|||
|
|
$gesamt++;
|
|||
|
|
$bucket = explode('/', (string) ($r[0] ?? ''))[0];
|
|||
|
|
if (isset($perBucket[$bucket])) {
|
|||
|
|
$perBucket[$bucket]++;
|
|||
|
|
}
|
|||
|
|
$perQuelle[$r[4] ?? '?'] = ($perQuelle[$r[4] ?? '?'] ?? 0) + 1;
|
|||
|
|
if (($r[8] ?? '') === 'ja') {
|
|||
|
|
$offen++;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
fclose($fh);
|
|||
|
|
|
|||
|
|
echo "Chronik-Register — {$gesamt} Fundstücke, {$offen} mit offenen Rechten\n\n";
|
|||
|
|
echo "Nach Zeitraum:\n";
|
|||
|
|
foreach ($perBucket as $b => $n) {
|
|||
|
|
printf(" %-28s %3d %s\n", $b, $n, $n === 0 ? '← Lücke' : str_repeat('▌', min(40, $n)));
|
|||
|
|
}
|
|||
|
|
echo "\nNach Quelle:\n";
|
|||
|
|
arsort($perQuelle);
|
|||
|
|
foreach ($perQuelle as $q => $n) {
|
|||
|
|
printf(" %-28s %3d\n", $q, $n);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --- Hauptlauf -------------------------------------------------------------
|
|||
|
|
|
|||
|
|
if (isset($opts['datei'])) {
|
|||
|
|
$ok = chronik_add_file((string) $opts['datei'], $opts);
|
|||
|
|
$offen = chronik_write_rights_todo();
|
|||
|
|
echo "\n{$offen} Fundstücke mit offenen Rechten — siehe Vereinshistorie/" . CHRONIK_RECHTE . "\n";
|
|||
|
|
exit($ok ? 0 : 1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$jobs = [];
|
|||
|
|
if (isset($opts['liste'])) {
|
|||
|
|
$listFile = (string) $opts['liste'];
|
|||
|
|
if (!is_file($listFile)) {
|
|||
|
|
fwrite(STDERR, "Liste nicht gefunden: {$listFile}\n");
|
|||
|
|
exit(1);
|
|||
|
|
}
|
|||
|
|
foreach (file($listFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
|||
|
|
$line = trim($line);
|
|||
|
|
if ($line === '' || str_starts_with($line, '#')) {
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
$parts = array_map('trim', explode('|', $line));
|
|||
|
|
$jobs[] = [
|
|||
|
|
'url' => $parts[0],
|
|||
|
|
'datum' => ($parts[1] ?? '') !== '' ? $parts[1] : null,
|
|||
|
|
'titel' => ($parts[2] ?? '') !== '' ? $parts[2] : null,
|
|||
|
|
];
|
|||
|
|
}
|
|||
|
|
} elseif ($positional !== []) {
|
|||
|
|
$jobs[] = ['url' => $positional[0], 'datum' => null, 'titel' => null];
|
|||
|
|
} else {
|
|||
|
|
fwrite(STDERR, "Aufruf: php bin/chronik-add.php <URL> | --liste=urls.txt | --rechte | --bericht\n");
|
|||
|
|
exit(1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$ok = 0;
|
|||
|
|
$fehler = 0;
|
|||
|
|
foreach ($jobs as $i => $job) {
|
|||
|
|
if ($i > 0) {
|
|||
|
|
// Pause zwischen den Abrufen: das hier ist Recherche, kein Crawl.
|
|||
|
|
usleep(1_500_000);
|
|||
|
|
}
|
|||
|
|
$override = array_filter(['datum' => $job['datum'], 'titel' => $job['titel']], static fn($v): bool => $v !== null);
|
|||
|
|
chronik_add($job['url'], $opts, $override) ? $ok++ : $fehler++;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$offen = chronik_write_rights_todo();
|
|||
|
|
|
|||
|
|
echo "\n{$ok} aufgenommen" . ($fehler > 0 ? ", {$fehler} fehlgeschlagen" : '') . ".\n";
|
|||
|
|
echo "{$offen} Fundstücke mit offenen Rechten — siehe Vereinshistorie/" . CHRONIK_RECHTE . "\n";
|
|||
|
|
exit($fehler > 0 ? 1 : 0);
|