|
1 | 1 | defmodule Memorable.Data.Image do |
2 | | - use Memento.Table, attributes: [:id, :collection_id, :path, :timestamp, :upload_date] |
| 2 | + @derive {Inspect, except: []} |
| 3 | + use Memento.Table, attributes: [:id, :collection_id, :path, :imported_datetime] |
| 4 | + |
| 5 | + @type t :: %__MODULE__{ |
| 6 | + id: Memorable.Util.id(), |
| 7 | + collection_id: Memorable.Data.Collection.id(), |
| 8 | + path: Path.t(), |
| 9 | + imported_datetime: DateTime.t() |
| 10 | + } |
| 11 | + @type metadata :: %{String.t() => any()} |
| 12 | + |
| 13 | + @spec read_metadata(t()) :: {:ok, metadata()} | {:error, any()} |
| 14 | + def read_metadata(%__MODULE__{path: path}) do |
| 15 | + with {:file_read, {:ok, data}} <- {:file_read, File.read(path)}, |
| 16 | + %{status: 0, stdout: stdout} <- Subprocess.exiftool_json(data), |
| 17 | + {:json_decode, {:ok, result}} <- {:json_decode, JSON.decode(to_string(stdout))} do |
| 18 | + {:ok, List.first(result)} |
| 19 | + else |
| 20 | + {:file_read, {:error, reason}} -> {:error, {:file_read, reason}} |
| 21 | + %{status: nil} -> {:error, :exiftool_exit_signal} |
| 22 | + %{status: other} -> {:error, {:exiftool_exit_status, other}} |
| 23 | + {:json_decode, {:error, reason}} -> {:error, {:json_decode, reason}} |
| 24 | + end |
| 25 | + end |
| 26 | +end |
| 27 | + |
| 28 | +defmodule Memorable.Data.Image.DerivedMetadata do |
| 29 | + alias Memorable.Data.Image |
| 30 | + |
| 31 | + # `image_id` is 1:1 with an Image `id` |
| 32 | + use Memento.Table, |
| 33 | + attributes: [ |
| 34 | + :image_id, |
| 35 | + :file_hash, |
| 36 | + :original_datetime, |
| 37 | + :lens_model, |
| 38 | + :body_model, |
| 39 | + :focal_length, |
| 40 | + :aperture, |
| 41 | + :exposure_time, |
| 42 | + :iso |
| 43 | + ] |
| 44 | + |
| 45 | + @type t :: %__MODULE__{ |
| 46 | + image_id: Image.id(), |
| 47 | + file_hash: binary(), |
| 48 | + original_datetime: NaiveDateTime.t(), |
| 49 | + lens_model: String.t(), |
| 50 | + body_model: String.t(), |
| 51 | + focal_length: float(), |
| 52 | + aperture: float(), |
| 53 | + exposure_time: String.t(), |
| 54 | + iso: integer() |
| 55 | + } |
| 56 | + |
| 57 | + @spec from_image(Image.t()) :: {:ok, t()} | {:error, any()} |
| 58 | + def from_image(%Image{id: image_id, path: path} = image) do |
| 59 | + with {:read_file, {:ok, data}} <- {:read_file, File.read(path)}, |
| 60 | + {:read_metadata, {:ok, metadata}} <- |
| 61 | + {:read_metadata, Image.read_metadata(image)} do |
| 62 | + %__MODULE__{ |
| 63 | + image_id: image_id, |
| 64 | + file_hash: :crypto.hash(:sha256, data), |
| 65 | + original_datetime: original_datetime(metadata), |
| 66 | + lens_model: Map.get(metadata, "LensID"), |
| 67 | + body_model: Map.get(metadata, "Model"), |
| 68 | + focal_length: focal_length(metadata), |
| 69 | + aperture: Map.get(metadata, "Aperture"), |
| 70 | + exposure_time: Map.get(metadata, "ExposureTime"), |
| 71 | + iso: Map.get(metadata, "ISO") |
| 72 | + } |
| 73 | + else |
| 74 | + {:read_file, error} -> {:error, {:read_file, error}} |
| 75 | + {:read_metadata, error} -> {:error, {:read_metadata, error}} |
| 76 | + end |
| 77 | + end |
| 78 | + |
| 79 | + @spec original_datetime(Image.metadata()) :: NaiveDateTime.t() | nil |
| 80 | + defp original_datetime(metadata) do |
| 81 | + with result when result != nil <- Map.get(metadata, "DateTimeOriginal") do |
| 82 | + NaiveDateTime.from_iso8601!(result) |
| 83 | + end |
| 84 | + end |
| 85 | + |
| 86 | + @spec focal_length(Image.metadata()) :: float() | nil |
| 87 | + defp focal_length(metadata) do |
| 88 | + with result when result != nil <- Map.get(metadata, "FocalLength") do |
| 89 | + case Float.parse(result) do |
| 90 | + {result, " mm"} -> result |
| 91 | + {_, _} -> raise "FocalLength is not in mm: #{result}" |
| 92 | + :error -> raise "FocalLength does not parse as float: #{result}" |
| 93 | + end |
| 94 | + end |
| 95 | + end |
3 | 96 | end |
0 commit comments