PHP

5717,57 Stunden

5717,57 Stunden habe ich seit dem 16. Januar 2023, 8:58 Uhr, in meinem Tracking-Tool aufgezeichnet. Also war es mal an der Zeit, das Tool in ein öffentliches Repo zu verfrachten.

Es hat sich neben Stift und Papier als das verlässlichste Zeiterfassungswerkzeug herausgestellt. Und warum? Weil die Erfassung möglichst schmerzfrei ist. Ich kann über die Weboberfläche oder via Rasycast oder Shell mit einer Zeile ein Tracking aufnehmen. Und wenn ich den Text dann noch mit einer Youtrack-ID beginne, kann ich die erfasste Zeit als Workitem bei Youtrack über die API verbuchen. Die Erfassung der Länge erfolgt über die Differenz zum vorigen Tracking. Das heißt, ich beginne meinen Arbeitstag mit einem Tracking, das den Arbeitsbeginn festlegt, und alle anderen Trackings bauen darauf auf.

Movie Tracker als Standalone Projekt

Ich habe mal den Movie Tracker aus der Blogsoftware herausgepfriemelt und als eigenständiges Projekt auf GitHub bereitgestellt. Eigentlich sind es nur vier Views:

Login

Movie Tracker Login
Movie Tracker Login

Der Single-User-Login wird über die .env konfiguriert.

AUTH_USERNAME=mk
AUTH_PASSWORD_HASH='<password_hash>'

Der Passwort-Hash kann mit einem mitgelieferten PHP-Skript erzeugt werden:

$ php bin/create-password-hash.php mein-passwort
In die .env übernehmen (inklusive Anführungszeichen):
AUTH_PASSWORD_HASH='$2y$12$eKomaqWXRQNHJa0pdrw9huODslOQMADsLwGCfYRqrJu76NlO2Zuxu'

Movies Übersicht

Movie Tracker Movie List
Movie Tracker Movie List

Im Listview werden die angeschauten Filme und Serien in chronologischer Reihenfolge angezeigt. Ein Klick auf einen Eintrag öffnet den Editview.


Hinzufügen/Bearbeiten

Movie Tracker Movie Edit
Movie Tracker Movie Edit

Beim Anlegen eines neuen Elements reicht es, den Titel einzugeben und auf „Von TMDB laden“ zu klicken. Soweit der Film gefunden wird, werden Darsteller, Originaltitel, Jahr und URL automatisch ergänzt.


Cast

Movie Tracker Cast
Movie Tracker Cast

Die Darstellerliste enthält den Cast, der mit einem Film gespeichert wurde.

Der schnellste Weg, um die Darsteller mit einer URL auszustatten, geht über ein kleines Skript, das die URL in der TMDb abfragt.

Aber die URLs können auch von Hand bearbeitet werden und man kann eine andere als den automatisch ergänzten Person-Link von TMDb nehmen.

$ php bin/update-movie-cast-urls.php
[1 von 11] Brad Pitt: https://www.themoviedb.org/person/287
[2 von 11] Charlotte Gainsbourg: https://www.themoviedb.org/person/4273
[3 von 11] Jessie T. Usher: https://www.themoviedb.org/person/198847
[4 von 11] LaKeith Stanfield: https://www.themoviedb.org/person/1200864
[5 von 11] Maika Monroe: https://www.themoviedb.org/person/1094091
[6 von 11] Mark Wahlberg: https://www.themoviedb.org/person/13240
[7 von 11] Rosa Salazar: https://www.themoviedb.org/person/973667
[8 von 11] Thomas Jane: https://www.themoviedb.org/person/11155
[9 von 11] Travis Tope: https://www.themoviedb.org/person/1278725
[10 von 11] William Fichtner: https://www.themoviedb.org/person/886
[11 von 11] 杨颖: https://www.themoviedb.org/person/150899
Fertig. 11 von 11 aktualisiert.

Simple Anti-FOMO RSS-Reader

Das erste Mal, dass ich ein GitHub Repo public mache. Im RSS-Club hatte ich ja schon mal angedeutet, dass ich einen einzigen RSS-Reader geschrieben habe, der per LLM FeedItem für mich nach Relevanz sortieren soll. Ich muss nicht alles lesen, was mir vor die Augen kommt. Man wird heutzutage ohnehin schon wie blöd mit News vollgepumpt. Quasi ein Anti-FOMO RSS-Reader. Der Name ist noch etwas sperrig.

Feature sind überschaubar:

  • Feeds werden einer OPML-Datei entnommen, gelesen und die Items in einer MySQL Tabelle hinterlegt.
  • Wenn ein ANTHROPIC_API_KEY hinterlegt ist, werden neu gelesene Items mit dem claude-haiku-4-5-20251001-Model klassifiziert. Dazu wird eine Markdown-Datei mit einer Liste von Kategorien herangezogen. Diese enthält je Zeile eine Kategorie mit Schlüsselwörtern/Definition. Die Reihenfolge der Kategorien entspricht der Relevanz. Was nicht vom LLM klassifiziert werden kann, ist „Nicht kategorisiert“ und steht unten in der Liste. Das sind dann in der Regel Posts, die sich nicht mit meinem Interessenbereich decken. Bei der Klassifizierung entstehen geringfügige Kosten.
  • Neue Items können als gelesen markiert werden.
  • Alte Items werden nach 5 entsorgt. * Items können favorisiert werden und bleiben dann über 5 Tage hinaus behalten.

Mehr gibt es noch nicht. Ich könnte mir vorstellen, die index.php zum Lesen auch wegzulassen und stattdessen das Ganze als RSS-Feed mit (soweit verfügbar) vollständigen Posts einfach nur als Feed bereitzustellen, den ich dann mit NetNewsWire lesen kann. Mal sehen …

Eine DDEV-Config ist im Repo enthalten.

Naja, es ist eher eine Art Proof of Concept als eine ernst gemeinte Anwendung. Ich denke, dass man so etwas vielleicht auch mit ein paar Klicks in Inoreader machen könnte. Aber wo bleibt denn da der Spaß.

Zeilenweise Text zum Browser streamen.

Ich wollte im Browser eine zeilenweise Ausgabe erzwingen und habe gemogelt, was das Zeug hält nur um es allein in PHP umzusetzen.

Naja, es funktioniert auf jeden Fall.

set_time_limit(0);

// Streaming-Setup: gzip/deflate aus, alle Puffer-Ebenen schließen, implicit flush an.
if (function_exists('apache_setenv')) {
    apache_setenv('no-gzip', '1');
}
@ini_set('zlib.output_compression', 'Off');
header('Content-Encoding: none');
while (ob_get_level() > 0) {
    ob_end_flush();
}
ob_implicit_flush(true);

// Mini-HTML-Preamble + Padding, 
// damit Firefox/Safari den Render-Threshold (~1 KB) erreichen.
echo "<!doctype html><meta charset=utf-8><title></title>\n";
echo str_repeat(' ', 1024) . "\n";
flush();

// Apache mod_proxy_fcgi puffert FastCGI-Pakete ohne flushpackets=on;
// per-line padding drückt jede Zeile über die Puffer-Schwelle und erzwingt Auslieferung.
$line = static function (string $text): void {
    echo $text . str_repeat(' ', 4096) . "\n";
};

// Ausgabe
for ($i = 1; $i <= 10; $i++) {
    $line($i.'<br>');
    sleep(1);
}

Suchbegriffe in HTML highlighten

Ich denke, ich habe einen recht brauchbaren RegEx gefunden, um Suchbegriffe in HTML zu highlighten und dabei den Inhalt von Tags zu ignorieren. Befindet sich der Suchbegriff beispielsweise in einem href-Attribut eines a-Tags, sollte der natürlich nicht „gehighlighted“ werden.

$html = preg_replace(
    pattern: '/(?![^<>]*>)'.preg_quote($searchTerm, '/').'/ui',
    replacement: '<mark class="tag">$0</mark>',
    subject: $html
);

Naja, wenn es um Tags geht, könnte ich evtl. auf preg_quote() verzichten. Aber der Vollständigkeit halber… 

So im Nachhinein denke ich mir natürlich: „Hmm… Profan!“ Aber ich bin mir sicher, in einem Jahr müsste ich mir wieder den Kopf zerbrechen. Ich habe das gesucht, um in der HTML-Ansicht meines Zettelkastens die assoziierten Tags im Fließtext hervorzuheben.

FTP Deployment

Wenn man einen Virtual Host ohne SSH & Git benutzt, ist es etwas lästig, Änderungen an einer Seite von Hand mit einem FTP Client hochzuladen. Das Projekt ftp-deplyoment kommt einem da stark entgegen.

Wenn man ein Composer Projekt hat, kann man es mit composer require dg/ftp-deployment in das Projekt einbinden. Dann eine deplyoment.php oder deplyoment.ini erstellen, wie es dort beschrieben ist, und vendor/bin/deployment deployment.php vereinfacht das Leben.

Bin ich eigentlich der Einzige, der von DDEV manchmal unglaublich genervt ist. Immer wieder stolpere ich über die beiden folgenden Probleme. Vollkommen egal, ob ich Colima oder OrbStack benutze.

  • Irgendwann hört der Mutagen-Spaß auf zu synchronisieren, oder die Synchronisation ist so langsam, dass effektiv keine Arbeit möglich ist. Obwohl in Anbetracht der ignorierten Verzeichnisse in der .ddev/mutagen/mutagen.yml nur eine Hand voll Dateien übrig bleiben, die überhaupt synchronisiert werden müssten.
  • Xdebug beachtet grundsätzlich nur Breakpoints in der index.php eine Projekts. Alle anderen Breakpoints werden ignoriert.

Es ist zwar nervig, den vollständigen Stack in den passenden Versionen lokal zu installieren. Aber arbeiten konnte ich damit immer sehr gut. Ich denke, ich will dahin zurück…

The Power Of Glob

Mir war nicht klar, dass man mit nur einem Aufruf von glob eine Verzeichnis Struktur rekursiv scannen kann, wenn man zuvor die Tiefe kennt.

glob("$basedir/{,*/,*/*/,*/*/*/}*.jpg", GLOB_BRACE);

Das GLOB_BRACE-Flag ist schon ein Knaller.