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 ontaken_atwould 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
"manual"— set by a person. Never replaced automatically."exif"— an image'sDateTimeOriginal, elseDateTimeDigitized, withOffsetTimeOriginal/OffsetTimeDigitizedwhen present."container"— a video's QuickTime creation date (a local time with its offset), else the container'screation_time(UTC). Same strength."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)."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.
Summary
Functions
attrs without its capture-date keys when writing them over current
would be a downgrade (replace?/2), unchanged otherwise.
The four columns a capture date occupies on phoenix_kit_files.
A capture date from EXIF tags: DateTimeOriginal, else DateTimeDigitized,
each with its own offset tag when present. nil when neither is a
plausible date.
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.
The upload time as a capture date: the fallback every file has.
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.
The EXIF tags ImageMagick reads from the first frame at path, as
%{"DateTimeOriginal" => "2018:07:31 23:04:05", …}.
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.
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.
The capture date of file, reading its bytes at path when given.
Every valid taken_at_source, strongest first.
Types
@type source() :: String.t()
Where a capture date came from. See the moduledoc.
@type t() :: %{ taken_at: DateTime.t(), taken_on: Date.t(), taken_at_offset: integer() | nil, taken_at_source: source() }
Functions
attrs without its capture-date keys when writing them over current
would be a downgrade (replace?/2), unchanged otherwise.
@spec fields() :: [atom()]
The four columns a capture date occupies on phoenix_kit_files.
A capture date from EXIF tags: DateTimeOriginal, else DateTimeDigitized,
each with its own offset tag when present. nil when neither is a
plausible date.
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.
@spec from_inserted_at(DateTime.t() | NaiveDateTime.t()) :: t()
The upload time as a capture date: the fallback every file has.
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.
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.
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.
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.
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.
@spec sources() :: [source()]
Every valid taken_at_source, strongest first.