web(review): confidence-aware date guesser with combined filename + path signals

Refactor suggestDateFromPath to combine multiple signals instead of
trying patterns in priority order:

- Filename Y-M-D corroborated by path Y-M/Y-M-D → HIGH (filename-
  agrees-path). Fixes the case where a Samsung-style 20240226_xxx.jpg
  under 2024/02/ was returning the path-only 2024-02-01.
- Filename Y-M-D with no path signal → HIGH (filename-only).
- 10/13-digit Unix epoch in basename → HIGH (unix-timestamp) —
  covers WeChat (mmexport...) and FB saves.
- Path Y-M-D → HIGH (path-ymd).
- Path Y-M only → MEDIUM (path-ym-default-day, synthesised day=01).
  Sidebar row labels these "(estimated day)" so the user knows.

Filename parser now accepts `.` and space separators (covers macOS
screenshots, manual 2024.02.26 renames). Path parser accepts `.` too.
OriginalName participates as a secondary filename signal when present
and different from the on-disk basename.

Patterns we explicitly DO NOT parse, to avoid silent date flips:
DD-MM-YYYY / MM-DD-YYYY, 2-digit years, bare camera sequence numbers.

Add photoNameAndDir(p) helper next to primaryFile so RightSidebar,
BulkActionBar, photoActions, and gridKeyNav all derive {fileName,
path} the same way — fixes the bug where photo.FileName was
undefined on the single-photo detail endpoint and the basename branch
was being skipped entirely.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-20 12:04:48 +02:00
parent a54d90a2d9
commit c134afe023
6 changed files with 257 additions and 65 deletions

View File

@@ -38,7 +38,7 @@
import { isAuthenticated } from '$lib/stores/session.svelte';
import { push as pushUndo } from '$lib/stores/undo.svelte';
import { getMetadataSectionOpen, setMetadataSection } from '$lib/stores/view.svelte';
import { primaryFile, type PpPhoto } from '$lib/types/photoprism';
import { photoNameAndDir, primaryFile, type PpPhoto } from '$lib/types/photoprism';
import { COLOR_SWATCHES } from '$lib/utils/tagGroups';
import { suggestDateFromPath } from '$lib/utils/suggestDateFromPath';
@@ -139,15 +139,20 @@
page.url.pathname === '/review' &&
page.url.searchParams.get('tab') === 'stripped_exif'
);
const dateSuggestion = $derived(
suggestDateFromPath({ fileName: photo.FileName, path: photo.Path })
);
const dateSuggestion = $derived.by(() => {
const { fileName, path } = photoNameAndDir(photo);
return suggestDateFromPath({
fileName,
originalName: photo.OriginalName,
path
});
});
const showDateSuggestion = $derived(
onExifStrippedTab && !!dateSuggestion && dateSuggestion !== takenAt
onExifStrippedTab && !!dateSuggestion && dateSuggestion.iso !== takenAt
);
function applyDateSuggestion() {
if (!dateSuggestion) return;
takenAt = dateSuggestion;
takenAt = dateSuggestion.iso;
commitTakenAt();
}
function commitTakenAt() {
@@ -344,17 +349,23 @@
/>
</div>
<!-- Date suggestion derived from the file/folder path. Only shown
on the EXIF Stripped review tab; amber styling marks it as
unconfirmed. Apply writes the value into the date input above
and commits as a manual TakenAt edit. -->
{#if showDateSuggestion}
<!-- Date suggestion derived from filename / folder signals. Only
shown on the EXIF Stripped review tab; amber styling marks
it as unconfirmed. `(estimated day)` hint appears when the
day was synthesised because only Y-M was available — same
row, just so the user knows that part is fabricated. Apply
writes the value into the date input above and commits as
a manual TakenAt edit. -->
{#if showDateSuggestion && dateSuggestion}
<div
class="flex items-center gap-2 rounded border border-amber-300/70 bg-amber-50/40 px-1.5 py-1 text-[11px] text-amber-700 dark:border-amber-500/40 dark:bg-amber-500/10 dark:text-amber-300"
>
<Folder class="h-3.5 w-3.5 shrink-0" />
<span class="min-w-0 flex-1 truncate">
Suggested from path: <span class="font-medium">{dateSuggestion}</span>
Suggested from path: <span class="font-medium">{dateSuggestion.iso}</span>
{#if dateSuggestion.source === 'path-ym-default-day'}
<span class="text-amber-600/80 dark:text-amber-400/70">(estimated day)</span>
{/if}
</span>
<button
type="button"