From 6f583c8f5b88305598a5d70a6490d95c6ec76b14 Mon Sep 17 00:00:00 2001 From: Ferdinand Urban Date: Tue, 29 Sep 2026 09:29:43 +0000 Subject: [PATCH] Videos nach Chatreihenfolge statt nach Zeitstempel ordnen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WhatsApp stempelt beim Synchronisieren gelegentlich alte Nachrichten neu, im echten Export mehrere Nachrichten am 19.08.26 um 13:17. Die Position im Chat bildet die tatsächliche Reihenfolge verlässlich ab. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/lib/whatsapp.js | 18 +++++++++++------- test/whatsapp.test.js | 22 +++++++++++++++++++--- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/src/lib/whatsapp.js b/src/lib/whatsapp.js index 3b9064d..5c80a4f 100644 --- a/src/lib/whatsapp.js +++ b/src/lib/whatsapp.js @@ -111,12 +111,16 @@ export function timestampFromFileName(name) { return null; } +// Die Chatreihenfolge ist verlässlicher als die Zeitstempel: WhatsApp stempelt +// beim Synchronisieren gelegentlich alte Nachrichten mit einer neuen Zeit. function compareClips(a, b) { - return ( - a.timestamp.localeCompare(b.timestamp) || - a.order - b.order || - a.file.localeCompare(b.file) - ); + return a.order - b.order || a.file.localeCompare(b.file); +} + +// Videos ohne Chatzeile werden per Zeitstempel zwischen die Nachrichten einsortiert. +function orderForTimestamp(messages, timestamp) { + const next = messages.find((message) => message.timestamp > timestamp); + return next ? next.index - 0.5 : messages.length; } // Doppelt gepostete Videos sind bytegleich; die Dateigröße genügt als Merkmal. @@ -131,7 +135,7 @@ function markDuplicates(clips) { } // files: [{ name, size }] aus dem Export (Pfade erlaubt). -// Liefert die Videos chronologisch, dazu im Chat genannte, aber fehlende Dateien. +// Liefert die Videos in Chatreihenfolge, dazu im Chat genannte, aber fehlende Dateien. export function collectClips(messages, files) { const videos = new Map(); for (const file of files) { @@ -176,7 +180,7 @@ export function collectClips(messages, files) { timestamp, sender: null, caption: '', - order: Number.MAX_SAFE_INTEGER, + order: orderForTimestamp(messages, timestamp), }); } diff --git a/test/whatsapp.test.js b/test/whatsapp.test.js index 5aa238b..a389669 100644 --- a/test/whatsapp.test.js +++ b/test/whatsapp.test.js @@ -16,7 +16,7 @@ const IOS_CHAT = [ '[21.09.26, 10:16:00] Ben Muster: Stark!', 'Zweite Zeile der Nachricht', '\u200e[22.09.26, 08:00:00] ~\u202fCarla: \u200e', - // Außer der Reihe: im Chat später, zeitlich aber früher + // Außer der Reihe: im Chat später, laut Zeitstempel aber früher '\u200e[21.09.26, 07:30:00] Ben Muster: Frühaufsteher \u200e', '\u200e[22.09.26, 08:00:05] ~\u202fCarla: \u200e', '[22.09.26, 09:00:00] Ben Muster: \u200eDiese Nachricht wurde gelöscht.', @@ -50,20 +50,36 @@ test('iPhone-Export: Nachrichten, Absender, Zeitstempel und Anhänge', () => { assert.deepEqual(messages[6].attachments, []); }); -test('Videos werden nach Zeitstempel sortiert, nicht nach Chatreihenfolge', () => { +test('Videos folgen der Chatreihenfolge, Videos ohne Chatzeile dem Zeitstempel', () => { const { clips } = collectClips(parseChat(IOS_CHAT), IOS_FILES); assert.deepEqual( clips.map((clip) => clip.file), [ - '00000103-VIDEO-2026-09-21-07-30-00.mp4', '00000101-VIDEO-2026-09-21-10-15-30.mp4', '00000102-VIDEO-2026-09-22-08-00-00.mp4', + '00000103-VIDEO-2026-09-21-07-30-00.mp4', '00000107-VIDEO-2026-09-22-15-00-00.mp4', '00000106-VIDEO-2026-09-28-12-00-00.mp4', ], ); }); +test('Ein falsch nachgestempeltes Video bleibt an seinem Platz im Chat', () => { + // Wie im echten Export: WhatsApp hat das mittlere Video auf den 19. umgestempelt. + const chat = [ + '\u200e[17.08.26, 09:48:18] Anna: Eins \u200e', + '\u200e[19.08.26, 13:17:37] Ben: Zwei \u200e', + '\u200e[17.08.26, 09:52:15] Carla: Drei \u200e', + ].join('\n'); + const files = [ + { name: '00001338-VIDEO-2026-08-17-09-48-18.mp4', size: 1 }, + { name: '00001339-VIDEO-2026-08-19-13-17-37.mp4', size: 2 }, + { name: '00001340-VIDEO-2026-08-17-09-52-15.mp4', size: 3 }, + ]; + const { clips } = collectClips(parseChat(chat), files); + assert.deepEqual(clips.map((clip) => clip.caption), ['Eins', 'Zwei', 'Drei']); +}); + test('Clips tragen Absender, Beschriftung und Pfad', () => { const { clips } = collectClips(parseChat(IOS_CHAT), IOS_FILES); const clip = clips.find((c) => c.file.startsWith('00000101'));