SoundCloud leitet die Feed-Adresse auf einen signierten Link um, der nach
zwei Minuten abläuft. Das Audio-Element fragt für jeden weiteren Teil der
Datei diesen Link an, wird abgewiesen (MEDIA_ERR_NETWORK) und blieb danach
kaputt, bis die App neu startete. Jetzt wird die Folge bei einem Fehler an
derselben Stelle neu geladen (bis zu dreimal pro Minute), und nach dem
Aufgeben beim nächsten Abspielen frisch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Der Server findet die Pausen jetzt nach den Regeln der App (PauseCutter):
10-ms-Fenster, Rauschboden als gleitendes Perzentil, ein Fünftel jeder Pause
bleibt (60–250 ms), halb davor, halb danach. Ältere Ergebnisse werden neu
berechnet (v: 2).
Der Player läuft durch diese Teile höchstens 4× schnell statt 8×: Chromium
schaltet darüber stumm, und weil currentTime in 10–20-ms-Schritten läuft,
landete der Wechsel zurück bei 8× bis zu 200 ms in der Sprache. Pro Pause
wird das Tempo gewählt, das am meisten spart und trotzdem rechtzeitig endet.
Das Tempo des Podcasts setzt die Rate während einer Pause nicht mehr zurück.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ein Sprung im gestreamten Podcast wartet aufs Netz (hörbares Haken). Jetzt
läuft eine Pause mit 4-facher Geschwindigkeit (höchstens 8x), je 0,12 s
bleiben an beiden Enden; nur Pausen über 3 s werden noch übersprungen.
Geprüft alle 40 ms statt mit timeupdate (alle 250 ms). Browser-Tab im
Hintergrund (gedrosselte Timer): weiter mit Sprüngen, sonst liefe Sprache
sekundenlang vierfach. Gemessen in der Desktop-App: Pausen 52,85–53,34 und
57,03–57,58 s schnell durchlaufen, kein Sprung.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Änderungen an native-ui.js (diora-android) erneuern jetzt auch die im
IndexedDB gespeicherte Aufbereitung (window.__dioraPrepareSig).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Die Leiste nimmt ihre Breite links weg; dieselbe Breite kommt rechts als
Abstand dazu, soweit Platz ist (die Spalte wird dafür nie schmaler). Beides
gleitet gleichzeitig, der Text bleibt stehen. Neu berechnet beim Öffnen und
Schließen der Leiste, bei Fenstergröße und Zeilenlänge/Schrift.
e2e: Test dafür (breit: Spalte bleibt, schmal: weicht aus, bleibt gleich
breit, liegt nicht unter der Leiste); Service Worker in den Tests aus (sein
Neuladen störte mitten im Test).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- e2e/reader.spec.js: ein Test-EPUB (e2e/make_epub.py) hochladen, öffnen ohne
zu senden, lesen und Lesestand nach Neuladen, Fußnote, Markierung und
Notizleiste, Hell/Dunkel samt Reader, alle Tabs ohne Seitenfehler.
- e2e/env.js: E2E_PORT und E2E_PYTHON; nie einen laufenden Server
wiederverwenden (dahinter läge eine echte Datenbank).
- CI: eigener Job e2e (Playwright + Chromium); blockiert den Docker-Build nicht.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mit offener Notizleiste kostete jeder Scroll-Schritt in Das Kapital (26.000
Blöcke, 8 Markierungen) 90–110 ms, jetzt rund 3 ms:
- resolveAnchorRange nimmt die zwischengespeicherte Blockliste (_epubBlocks)
statt für jede Markierung alle Blöcke neu zu suchen.
- Die Position jeder Markierung im Buch wird gemerkt, bis sich das Layout
ändert.
- Die Karten liegen auf einer Ebene in Buchkoordinaten; beim Scrollen wird
nur diese verschoben. Gebaut und vermessen wird nur, wenn sich Markierungen
oder Layout ändern, und dann alle Karten in einem Durchgang.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Farbvariablen für beide Schemata in app.css (--bg --fg --muted --card --panel
--hover --raised --border --border-strong --accent --on-accent --scrim
--shadow), Hell als html[data-theme=light]. Umschalter im Kopf jeder Seite
(base.html, vor dem ersten Zeichnen): Dunkel / Hell / Automatisch, ☾☀◐ in der
Navigation; __dioraTheme / __dioraSetTheme / Ereignis diora-theme.
- Reader: neues Standard-Theme "Wie die App" (Dunkel/Hell folgt der App).
- ◑ (Kontrast zum Hintergrundbild) nur noch mit Hintergrundbild.
- home.js auf die Variablen.
- window.__dioraTts (state/toggle/stop/next/prev) und Ereignis diora-tts für
die Playerleiste; mit __dioraTtsDock keine eigene Vorlese-Leiste mehr.
- Reader-Hinweise über der Playerleiste, wenn sie im Reader steht.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/progress-sync.md ist der Vertrag für Server, Web, Android und Desktop.
- Server: read_at (Zeit, zu der dort gelesen wurde, Uhrfehler des Geräts per
sent_at herausgerechnet, nie nach "jetzt"), Gerät, weiteste Stelle. Ältere
Schreibvorgänge (offline nachgereicht, liegengelassener Tab) gewinnen nicht.
Apps ohne read_at: nur strikt vorwärts. Antwort = Zustand nach dem Schreiben.
/books/ mit X-Server-Now, /api/sync/ mit server_now.
- Web-Reader: Zeit einer Stelle ist der Moment, in dem man durch eigenes Tun
hinkam (Scrollen, Blättern, Tasten, Vorlesen); unverändert Liegendes
schickt nichts bzw. nur seine alte Zeit. Vor dem Wiederherstellen und
während eines Blicks (Fußnote, "Weit gescrollt") wird nicht gespeichert.
Eigene Stellen in einem Ausgang (localStorage) bis der Server sie hat.
Angebote "Auf <Gerät> weitergelesen" und "Weiteste Stelle".
- Tests: books/tests.py (Reihenfolge, Uhrfehler, Zukunft, alte Apps, weiteste
Stelle, Liste und Snapshot).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bisher ließ sich eine Verbindung nur anlegen und löschen, der Startordner
also nicht nachträglich setzen. Das Passwort wird nicht angezeigt; leer
lassen behält das gespeicherte.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
getPositionAnchor maß jeden Block vom Buchanfang bis zur Leseposition und suchte
die Blöcke bei jedem Aufruf neu – im E-Reader-Modus bei jedem Umblättern (die
Prozentanzeige), beim Speichern des Fortschritts sowieso. In Das Kapital ab
Kapitel 15: 0,8 s bei 6-fach gedrosselter CPU; auf dem tolino filtert compat61
das :has() dazu in JavaScript.
- Blockliste samt Kapitel je Block einmal je geöffnetem Buch (verworfen bei
jeder Änderung am Inhalt, MutationObserver)
- Binärsuche nur über die angezeigten Kapitel; gleiche Regel wie vorher
(letzter Block mit Höhe, der an/über der Oberkante beginnt, bei gleicher Höhe
der erste; sonst der erste darunter)
- Geprüft an 480 Stellen (Kapital, Hyperpolitik; E-Reader und Scrollen): gleiche
Anker wie vorher; 53/281 ms → 4 ms; Umblättern 21–48 ms statt bis 0,9 s
- sw v98
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- <a id="x"/> (XHTML) blieb beim Parsen als HTML offen und umschloss den
folgenden Text, in jedem weiteren Absatz neu (Suhrkamp/transpect): die
inline-Elemente werden vorher geschlossen.
- Seitenzahlen der Druckausgabe (epub:type="pagebreak") als kleine graue
Marke statt am Text klebend („3Anton Jäger“).
- Fußnoten-Popover: Absatz der Notiz auch bei id auf einem inline-Element,
ohne eigene Nummer und Seitenzahl, mehrabsätzige Noten (Gedichte).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Gemessen mit Das Kapital (148 Kapitel, 26 000 Absätze) im diora Reader, CPU
6-fach gedrosselt wie auf einem E-Reader: Öffnen 2,4 s → 1,2 s.
- parseEpub entpackt alle Kapitel auf einmal statt eines nach dem anderen
(jedes wartete auf JSZips eigene Ticks) und pausiert nach ~50 ms Arbeit statt
alle 8 Kapitel einen Frame; ebenso das Einfügen der Kapitel
- Vorbereitete Bücher (entpackt und bereinigt) liegen verschlüsselt mit dem
Buchschlüssel in IndexedDB („parsed“): gültig für eine Fassung der Buchdatei
(IV) und des Vorbereitungs-Codes (Hash seines Quelltexts – eine Änderung am
Sanitizer bereitet jedes Buch neu vor); Bilder bleiben in der Buchdatei, die
Kapitel tragen „diora-img:<Pfad>“ statt Blob-URLs. Nach 4 Wochen ohne Öffnen weg.
Geprüft: Text, Absätze, Bilder, Fußnotenlinks, Inhaltsverzeichnis identisch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PDFs liefern die echte Seitenzahl, bei EPUBs wird sie aus der Zeichenzahl
geschätzt (1800 Zeichen je Seite). Gespeichert in den verschlüsselten Metadaten
beim Öffnen des Buchs.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Die Buch-Synchronisation der Apps braucht vom Schnappschuss nur Bücher,
Fortschritt, Markierungen und Lesezeichen; der Podcast-Teil (jede je gespielte
Folge) ist für ein Konto aus AntennaPod 11 MB groß und wurde bei jedem Abgleich
mitgeladen – über mobile Daten Gigabytes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Source.folders (Etiketten, Groß/Klein egal, max. 40 Zeichen); die bisherige
Kategorie eines Blogs wird sein erster Ordner (Migration)
- /feeds/folders/rename/ und /dissolve/; in den Blog-Einstellungen Häkchen je
Ordner plus „Neuer Ordner“; langer Druck/Rechtsklick auf einen Ordner: öffnen,
umbenennen, auflösen; Ordnerliste in „Feeds verwalten“
- Lesefortschritt je Post (Item.progress, geräteübergreifend): die Leseansicht
springt zurück an die Stelle, ab 90 % gilt ein Blogpost als gelesen (Nachrichten
weiter beim Öffnen); Fortschrittsbalken und „Weiterlesen“ oben in den Blogs
- sw v95
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Source.notify; ein neuer Post wird beim Eintreffen markiert (Item.notify),
nie beim ersten Abruf eines Blogs, nie älter als 2 Tage
- GET /feeds/notifications/?after=<id>: ungelesene markierte Posts von Blogs mit
Glocke nach der zuletzt gesehenen id; ohne after nur der Startpunkt
- Glocken-Schalter in der Blog-Leiste und in den Einstellungen des Blogs;
window.__dioraFeedsOpenItem öffnet einen Post (Antippen der Benachrichtigung)
- sw v94
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Abschnitte Heute/Gestern/Diese Woche/Diesen Monat/Älter; je Abschnitt bis zu
5 Posts je Favoriten-Blog zuerst, dann „Seltene zuerst“ (mittlerer Abstand der
letzten 10 Posts, ein Post = 30 Tage) oder chronologisch
- Blog „im Hauptfeed ausblenden“: nicht unter Alle und nicht gezählt, im Ordner da
- NEU (jünger als 24 h, ungelesen) am Post, als Zahl am Tab, Punkt am Ordner
- Chips Ungelesen/Alle, ★ Favoriten, Seltene zuerst; Schalter je Blog
- Lesezeit (Wörter/230) an jedem Post; words wird beim Speichern gezählt
- sw v93
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- In den Einstellungen einer Quelle: Zugangsdaten der Seite; der Server prüft
die Anmeldung sofort, speichert das Passwort verschlüsselt (Fernet, Schlüssel
aus SECRET_KEY) und gibt es nie wieder heraus
- Artikel werden mit der angemeldeten Sitzung geholt; hinter der Bezahlschranke
bleibt der freie Teil (paywalled) und wird nach der Anmeldung neu geholt
- Adapter je Seite (feeds/logins.py), zuerst jungewelt.de
- sw v92
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Die Kategorie eines Blogs ist sein Ordner: Ordner-Chips mit Ungelesen-Zahl in
der Blog-Ansicht, /feeds/blogs/?folder=, Ordner im OPML-Export und -Import
- Neue Feeds ohne (brauchbare) Daten: die Reihenfolge des Feeds gilt als Zeit,
nur die ersten 3 ungelesen; ein neuer Blog bringt 7 statt 14 Tage ungelesen
- sw v91
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
static/js/feeds.js für Web, Desktop und beide Android-Apps:
- Nachrichten: „Seit letztem Mal“ (Top-Themen, dann nach Ressort, „Alles
gelesen“), Stories nach Tag, Ressort-Filter
- Blogs: Ungelesen-Liste, je Blog filterbar, „Alle gelesen“
- Artikel im eigenen Lesemodus (Volltext, Schriftgröße, merken, alle Berichte
einer Story), Browser nur auf Wunsch
- Feeds verwalten: Seite oder Feed suchen, als Nachrichten/Blog abonnieren,
Ressort, Volltext-Regel, OPML-Import pro Ordner, Export, Aufbewahrung
- diora Reader (ohne Tab-Leiste) bekommt eine kleine Leiste Bücher/Feeds
Server: Feed-Namen ohne Slogan, Feeds mit einem Zeitstempel für alle Einträge
(Deutschlandfunk) zählen ab dem ersten Sehen; sw v89
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Neue App feeds: Quellen sind entweder Nachrichten oder Blog.
- Abruf wie bei Podcasts (ETag, Body-Hash, Backoff), refresh_newsfeeds für den Cron
- Anrisstexte: der Artikel wird von der Seite geholt (trafilatura), HTML bereinigt,
relative Links absolut
- Nachrichten: gleiche Meldung aus mehreren Quellen wird eine Story (tf-idf, je
Artikel verglichen), Briefing „Seit letztem Mal“ (Top-Themen, dann nach Ressort),
verfallen nach news_keep_days (Standard 2)
- Blogs: Ungelesen-Liste je Blog, bleiben bis gelesen
- API unter /feeds/: Quellen suchen/abonnieren/OPML, News, Stories, Blogs,
Artikel, gelesen, merken
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- static/js/home.js rendert #tab-home aus dem, was die Seite schon hat:
zuletzt gelesenes Buch (und drei weitere), laufende oder nächste Folge mit
Restzeit und „N neue Folgen“, Favoriten-Sender (sonst die ersten
gespeicherten) und zuletzt gehörter Sender, Drift mit der zuletzt
gewählten Klangwelt und Sleep-Timer. Läuft im Browser, in diora desktop und
in der Android-App (dort mit static/js/ kopiert).
- app.js: Tab 'home' (TOP_TABS), window.__dioraAmbientStart() lädt Drift bei
Bedarf und startet es; die gewählte Klangwelt wird gemerkt und beim Laden
wiederhergestellt.
- podcasts-web.js: Startseite Home, wenn nichts anderes gewählt ist.
- native-podcast.js aus diora-android 7dcc0c6 (Home in der Startseiten-Wahl,
__dioraPodcastView).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- podcasts/silence.py: ffmpeg (imageio-ffmpeg) findet die Pausen einer Folge einmal
(silencedetect -35 dB, ab 0,4 s), im Hintergrund, höchstens 2 zugleich; gespeichert in
PodcastEpisode.silences (Migration 0018); GET episodes/<id>/silences/
- podcasts-web.js: skip_silence als Einstellung (Gerät, pro Podcast vom Server), springt
beim Abspielen über die Pausen (0,15 s bleiben)
- Drift: Ringpuffer für die Körner als AudioWorklet statt ScriptProcessor im Hauptthread
(gehäufte Aussetzer am Desktop, wenn die Seite kurz stockte)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Auf dem S21 lagen App- und Renderer-Prozess bei je ~105 % CPU. Auf Telefonen jetzt:
höchstens 10 Stimmen zugleich, eine Säge und ein Wavetable-Zyklus pro Stimme, FM nur
deutlich, keine offline gerenderten Rückwärts-Swells (Rechenspitzen), Bild in halber
Auflösung mit ~20 Bildern/s und einfachen Partikeln.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Der stündliche Abruf der 502 Feeds dauerte 114 s, fast nur Parsen: viele Server
ignorieren ETag/Last-Modified und schicken jedes Mal den ganzen Feed (Lage der Nation
8,4 MB = 5,9 s). ⟳ in der App lief damit in ihr 60-s-Zeitlimit und schlug fehl.
- Fingerabdruck des Feed-Inhalts (ohne lastBuildDate/Kommentare, http_body_hash,
Migration 0017): derselbe Inhalt zählt als unverändert und wird nicht geparst
- _refresh_feeds: ein Pool für alle Feeds (16 parallel), jeder Feed wird geschrieben,
sobald er da ist, optionales Zeitlimit
- refresh-all wartet höchstens 40 s (done/left in der Antwort); der Browser-Host
holt den Stand dann eine Minute später noch einmal, und alle 15 Minuten
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Zwei Achsen kurz nacheinander (3,1 m), 33 m Schiene bis zum nächsten Paar, dumpferer
Anschlag, Karosserie klingt mit zwei Moden nach. Dioras eigene Drift-Änderungen bleiben.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- app.js: __dioraAmbientInfo (spielt, zuletzt gestartet, Klangwelt), meldet Moduswechsel
an die Leiste; Radio/Folge übernimmt sie wieder
- Drift (Telefone): Musik-Bett, Delay und Hall werden vom Ausgang getrennt, wenn eine
Klangwelt ohne sie läuft (Braunes Rauschen …); Zugfahrt und Herzschlag planen 2,5 s voraus
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Auf dem S21 lag die App mit Drift bei ~185 % CPU, der Ton stockte. Auf Telefonen jetzt:
Bild in einfacher Auflösung und ~30 Bilder/s, 3,5 s statt 7 s Hall, kein Oversampling,
kein ScriptProcessor (Körner) im Hauptthread.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Drift: AudioContext mit latencyHint 'playback' – der Niedriglatenz-Weg (AAudio/MMAP)
blieb auf dem Samsung des Nutzers stumm, obwohl Drift lief
- Drift: cmd 'volume' (0..1) von der Seite ist die Lautstärke; eingebettet ist der eigene
Regler ausgeblendet
- app.js: setVolume() und das Laden des Frames schicken die Lautstärke an Drift (nicht in
der Android-App: dort gelten die Lautstärketasten)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Aus der Session "Ambient-Synthesizer-Webapp": Ambient-Musik ist jetzt ein Modus neben
weißem/pinkem/braunem/blauem/violettem/grauem Rauschen, Regen, Gewitter, Meer, Wind, Bach,
Lagerfeuer, Sommernacht, Ventilator, Flugzeugkabine, Zugfahrt, binauralen Beats,
Klangschalen und Herzschlag. Alles live synthetisiert, offline; Schnittstelle zu diora
unverändert (neu: cmd 'mode').
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- app.js: __dioraAmbientCmd, meldet Start/Stopp an die App (DioraNative.ambientState,
sie hält Drift bei ausgeschaltetem Bildschirm am Laufen) und an den Browser-Host
- podcasts-web.js: der Sleep-Timer zählt auch, solange Ambient spielt, stoppt es beim
Ablauf; Nachtautomatik auch beim Ambient-Start
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- static/ambient/index.html: Drift (aus der Session "Ambient-Synthesizer-Webapp",
eine Datei, Web Audio, offline), eingebettet per iframe (?embed=1&bg=#000)
- app.js: Tab 'ambient', iframe lädt beim ersten Öffnen und füllt den Platz bis
zur Leiste; Radio/Podcast und Ambient wechseln sich ab (postMessage
{type:'ambient', cmd|state})
- sw.js: Ambient im Offline-Cache (ignoreSearch für die Shell-Dateien)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>