# `PhoenixKit.Modules.Storage.CaptureDate`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v2.37.1/lib/modules/storage/services/capture_date.ex#L1)

When a photo or video was taken, as opposed to when it was uploaded.

Four columns on `phoenix_kit_files` (V200) hold it:

  * `taken_at` — the moment, in UTC. Exact when the offset is known. When it
    is not — most EXIF carries only a local wall-clock time — it is that
    local time stored as if it were UTC: it still orders a library
    correctly, but it is not a true instant.
  * `taken_on` — the local calendar **date**, which is what a library
    groups by. A photo taken at 23:00 on 31 July in California is a July
    photo, although in UTC it is already 1 August; grouping on `taken_at`
    would file it under the wrong month.
  * `taken_at_offset` — seconds east of UTC, when known.
  * `taken_at_source` — where the date came from (see below).

## Where a date comes from, strongest first

  1. `"manual"` — set by a person. Never replaced automatically.
  2. `"exif"` — an image's `DateTimeOriginal`, else `DateTimeDigitized`,
     with `OffsetTimeOriginal` / `OffsetTimeDigitized` when present.
     `"container"` — a video's QuickTime creation date (a local time with
     its offset), else the container's `creation_time` (UTC). Same strength.
  3. `"filename"` — a date in the uploaded file's name, the way phones and
     cameras name files (`IMG_20180701_120000.jpg`,
     `PXL_20240315_081100123.jpg`, `2018-07-01 12.34.56.jpg`).
  4. `"inserted_at"` — the upload time. Always available, so every file
     resolves to *some* date.

A file's modification time is deliberately not a source: originals live on
local disk or in object storage, where it is the upload time — the same
information `inserted_at` already carries.

## Never downgraded

An image edit re-encodes the file keeping only its ICC profile (see
`PhoenixKit.Modules.Storage.ImageEdit`), so an edited original carries no
EXIF, and anything that reads a date from those bytes finds the file name or
the upload time at best. `replace?/2` is the guard every automatic writer
goes through: a date is replaced only by one from an equally strong or
stronger source, and a manual date never.

# `source`

```elixir
@type source() :: String.t()
```

Where a capture date came from. See the moduledoc.

# `t`

```elixir
@type t() :: %{
  taken_at: DateTime.t(),
  taken_on: Date.t(),
  taken_at_offset: integer() | nil,
  taken_at_source: source()
}
```

# `admit`

```elixir
@spec admit(map(), map()) :: map()
```

`attrs` without its capture-date keys when writing them over `current`
would be a downgrade (`replace?/2`), unchanged otherwise.

# `fields`

```elixir
@spec fields() :: [atom()]
```

The four columns a capture date occupies on `phoenix_kit_files`.

# `from_exif`

```elixir
@spec from_exif(%{required(String.t()) =&gt; String.t()}) :: t() | nil
```

A capture date from EXIF tags: `DateTimeOriginal`, else `DateTimeDigitized`,
each with its own offset tag when present. `nil` when neither is a
plausible date.

# `from_filename`

```elixir
@spec from_filename(String.t() | nil) :: t() | nil
```

A capture date from the date in a file name, or `nil`. Only the base name
is read, and only dates in the shapes phones and cameras write.

# `from_inserted_at`

```elixir
@spec from_inserted_at(DateTime.t() | NaiveDateTime.t()) :: t()
```

The upload time as a capture date: the fallback every file has.

# `from_video_tags`

```elixir
@spec from_video_tags(%{required(String.t()) =&gt; String.t()}) :: t() | nil
```

A capture date from container tags: QuickTime's creation date, which keeps
the local time and its offset (what an iPhone writes), else the container's
`creation_time`, which is UTC with no offset — the instant is exact, but the
local date can only be taken from UTC.

# `read_exif`

```elixir
@spec read_exif(Path.t()) :: %{required(String.t()) =&gt; String.t()}
```

The EXIF tags ImageMagick reads from the first frame at `path`, as
`%{"DateTimeOriginal" => "2018:07:31 23:04:05", …}`.

`%[EXIF:*]` rather than one `%[EXIF:Tag]` per tag: ImageMagick 7 warns on
stderr for every named tag a file lacks, and most files lack some. An empty
map when the file has no EXIF, is not an image, or `identify` is missing.

# `read_video_tags`

```elixir
@spec read_video_tags(Path.t()) :: %{required(String.t()) =&gt; String.t()}
```

The creation tags `ffprobe` reads from the container at `path`, as
`%{"creation_time" => …, "com.apple.quicktime.creationdate" => …}`. An
empty map when there are none or `ffprobe` is missing.

# `replace?`

```elixir
@spec replace?(source() | nil, source()) :: boolean()
```

Whether an automatic writer may replace a date from `current` with one
from `new`: only by an equally strong or stronger source, and a manual date
never. `nil` (no date yet) is always replaceable.

# `resolve`

```elixir
@spec resolve(Path.t() | nil, map()) :: t()
```

The capture date of `file`, reading its bytes at `path` when given.

Always returns a date: embedded metadata, else the original file name, else
the upload time. `path` is `nil` when the bytes could not be read, which
skips straight to the file name.

# `sources`

```elixir
@spec sources() :: [source()]
```

Every valid `taken_at_source`, strongest first.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
