defmodule BDS.Scripting.Capabilities.Media do @moduledoc false import Ecto.Query import BDS.Scripting.Capabilities.Util alias BDS.Media alias BDS.Media.Media, as: MediaRecord alias BDS.Media.Translation, as: MediaTranslation alias BDS.Repo alias BDS.Search def import_media(project_id, attrs) do attrs |> normalize_map() |> normalize_media_attrs() |> Map.put("project_id", project_id) |> Media.import_media() |> unwrap_result() end def update_media(project_id, media_id, attrs) do case fetch_media(project_id, media_id) do %MediaRecord{} -> Media.update_media(media_id, attrs |> normalize_map() |> normalize_media_attrs()) |> unwrap_result() _other -> nil end end def delete_media(project_id, media_id) do case fetch_media(project_id, media_id) do %MediaRecord{} -> boolean_result(Media.delete_media(media_id)) _other -> false end end def load_media(project_id, media_id) do fetch_media(project_id, media_id) |> sanitize_nilable() end def list_media(project_id) do Repo.all( from(media in MediaRecord, where: media.project_id == ^project_id, order_by: [asc: media.created_at] ) ) |> Enum.map(&sanitize/1) end def load_media_translation(project_id, media_id, language) do case fetch_media(project_id, media_id) do %MediaRecord{id: id} -> Repo.one( from(translation in MediaTranslation, where: translation.translation_for == ^id and translation.language == ^(string_or_nil(language) || ""), limit: 1 ) ) |> sanitize_nilable() _other -> nil end end def list_media_translations(project_id, media_id) do case fetch_media(project_id, media_id) do %MediaRecord{id: id} -> Repo.all( from(translation in MediaTranslation, where: translation.translation_for == ^id, order_by: [asc: translation.language] ) ) |> Enum.map(&sanitize/1) _other -> [] end end def upsert_media_translation(project_id, media_id, language, attrs) do case fetch_media(project_id, media_id) do %MediaRecord{} -> Media.upsert_media_translation( media_id, string_or_nil(language) || "", normalize_media_translation_attrs(normalize_map(attrs)) ) |> unwrap_result() _other -> nil end end def delete_media_translation(project_id, media_id, language) do case fetch_media(project_id, media_id) do %MediaRecord{} -> case Media.delete_media_translation(media_id, string_or_nil(language) || "") do {:ok, deleted?} -> deleted? {:error, _reason} -> false end _other -> false end end def filter_media(project_id, filters) do filters = normalize_map(filters) list_media(project_id) |> Enum.filter(fn media -> media_matches_filters?(media, filters) end) end def media_counts_by_year_month(project_id) do list_media(project_id) |> Enum.reduce(%{}, fn media, acc -> datetime = media_datetime(media) key = {datetime.year, datetime.month} Map.update(acc, key, 1, &(&1 + 1)) end) |> Enum.map(fn {{year, month}, count} -> %{"year" => year, "month" => month, "count" => count} end) |> Enum.sort_by(fn row -> {-row["year"], -row["month"]} end) end def media_file_path(project_id, media_id) do case fetch_media(project_id, media_id) do %MediaRecord{} = media -> Path.join(project_path(project_id), media.file_path) _other -> nil end end def media_tags(project_id), do: media_tags_with_counts(project_id) |> Enum.map(& &1["tag"]) def media_tags_with_counts(project_id) do Repo.all( from(media in MediaRecord, where: media.project_id == ^project_id, order_by: [asc: media.created_at] ) ) |> Enum.flat_map(&(&1.tags || [])) |> Enum.reduce(%{}, fn tag, acc -> Map.update(acc, tag, 1, &(&1 + 1)) end) |> Enum.map(fn {tag, count} -> %{"tag" => tag, "count" => count} end) |> Enum.sort_by(fn row -> {-row["count"], String.downcase(row["tag"])} end) end def media_thumbnail(project_id, media_id, size) do with %MediaRecord{} = media <- fetch_media(project_id, media_id), relative_path <- Media.thumbnail_paths(media)[thumbnail_size(size)], absolute_path <- Path.join(project_path(project_id), relative_path), true <- File.exists?(absolute_path), {:ok, binary} <- File.read(absolute_path) do "data:#{thumbnail_mime(absolute_path)};base64," <> Base.encode64(binary) else _other -> nil end end def media_url(project_id, media_id) do case fetch_media(project_id, media_id) do %MediaRecord{} = media -> "/" <> String.trim_leading(media.file_path, "/") _other -> nil end end def rebuild_media_from_files(project_id) do project_id |> Media.rebuild_media_from_files() |> unwrap_result(fn media -> Enum.map(media, &sanitize/1) end) end def regenerate_missing_thumbnails(project_id) do Media.regenerate_missing_thumbnails(project_id) |> sanitize() end def regenerate_media_thumbnails(project_id, media_id) do case fetch_media(project_id, media_id) do %MediaRecord{} = media -> case Media.regenerate_thumbnails(media.id) do {:ok, _media} -> Media.thumbnail_paths(media) |> Enum.map(fn {size, relative_path} -> {to_string(size), Path.join(project_path(project_id), relative_path)} end) |> Map.new() {:error, _reason} -> nil end _other -> nil end end def replace_media_file(project_id, media_id, source_path) do case fetch_media(project_id, media_id) do %MediaRecord{} -> Media.replace_media_file(media_id, string_or_nil(source_path) || "") |> unwrap_result() _other -> nil end end def search_media(project_id, query) do project_id |> Search.search_media(string_or_nil(query) || "") |> unwrap_result(fn %{media: media} -> Enum.map(media, &sanitize/1) end) end def fetch_media(project_id, media_id) do Repo.one( from(media in MediaRecord, where: media.project_id == ^project_id and media.id == ^(string_or_nil(media_id) || ""), limit: 1 ) ) end def normalize_media_attrs(attrs) do attrs |> maybe_put_normalized_list("tags") end def normalize_media_translation_attrs(attrs) do attrs |> Map.take(["title", "alt", "caption"]) end def media_matches_filters?(media, filters) do created_at = media_datetime(media) tags = Map.get(media, "tags", []) language = Map.get(media, "language") matches_year = compare_optional(Map.get(filters, "year"), fn year -> created_at.year == integer_or_default(year, created_at.year) end) matches_month = compare_optional(Map.get(filters, "month"), fn month -> created_at.month == integer_or_default(month, created_at.month) end) matches_language = compare_optional(blank_to_nil(Map.get(filters, "language")), fn value -> language == value end) matches_tags = compare_optional(Map.get(filters, "tags"), fn required_tags -> Enum.all?(normalize_string_list(required_tags), &(&1 in tags)) end) matches_from = compare_optional( parse_datetime(Map.get(filters, "from") || Map.get(filters, "start_date")), fn from_dt -> DateTime.compare(created_at, from_dt) != :lt end ) matches_to = compare_optional( parse_datetime(Map.get(filters, "to") || Map.get(filters, "end_date")), fn to_dt -> DateTime.compare(created_at, to_dt) != :gt end ) matches_year and matches_month and matches_language and matches_tags and matches_from and matches_to end def media_datetime(media) do media |> Map.get("created_at") |> case do value when is_binary(value) -> case DateTime.from_iso8601(value) do {:ok, datetime, _offset} -> datetime _other -> DateTime.utc_now() end value when is_integer(value) -> DateTime.from_unix!(value, :millisecond) _other -> DateTime.utc_now() end end end