defmodule BDS.Desktop.ShellLive.GalleryImport do @moduledoc false require Logger alias BDS.{AI, Media, Metadata} @doc """ Starts the image import pipeline: for each selected path, imports the file, runs AI analysis, updates metadata, links to the post, and translates to all configured blog languages. Processes images with a concurrency cap via a sliding window. """ @spec start(list(String.t()), String.t(), String.t(), String.t(), integer(), pid()) :: :ok def start(paths, project_id, post_id, language, concurrency_limit, parent) do {:ok, metadata} = Metadata.get_project_metadata(project_id) main_language = metadata.main_language || language blog_languages = metadata.blog_languages || [] translate_targets = [main_language | blog_languages] |> Enum.reject(&(&1 == language or is_nil(&1))) |> Enum.uniq() {in_flight, remaining} = Enum.split(paths, concurrency_limit) tasks = Enum.map(in_flight, fn path -> Task.async(fn -> process_single_image(path, project_id, post_id, language, translate_targets, parent) end) end) known_refs = MapSet.new(tasks, & &1.ref) drain_tasks( remaining, tasks, known_refs, project_id, post_id, language, translate_targets, parent ) send(parent, {:add_images_complete, length(paths)}) end defp drain_tasks( [], tasks, _known_refs, _project_id, _post_id, _language, _translate_targets, _parent ) do Enum.each(tasks, fn task -> Task.await(task, :infinity) end) end defp drain_tasks( [next_path | rest], tasks, known_refs, project_id, post_id, language, translate_targets, parent ) do receive do {ref, _result} when is_reference(ref) -> if MapSet.member?(known_refs, ref) do {_completed_task, remaining_tasks} = pop_task_by_ref(tasks, ref) new_task = Task.async(fn -> process_single_image( next_path, project_id, post_id, language, translate_targets, parent ) end) drain_tasks( rest, [new_task | remaining_tasks], MapSet.put(MapSet.delete(known_refs, ref), new_task.ref), project_id, post_id, language, translate_targets, parent ) else drain_tasks( [next_path | rest], tasks, known_refs, project_id, post_id, language, translate_targets, parent ) end {:DOWN, ref, :process, _pid, _reason} when is_reference(ref) -> if MapSet.member?(known_refs, ref) do {_completed_task, remaining_tasks} = pop_task_by_ref(tasks, ref) new_task = Task.async(fn -> process_single_image( next_path, project_id, post_id, language, translate_targets, parent ) end) drain_tasks( rest, [new_task | remaining_tasks], MapSet.put(MapSet.delete(known_refs, ref), new_task.ref), project_id, post_id, language, translate_targets, parent ) else drain_tasks( [next_path | rest], tasks, known_refs, project_id, post_id, language, translate_targets, parent ) end end end defp pop_task_by_ref(tasks, ref) do Enum.reduce(tasks, {nil, []}, fn %{ref: ^ref} = task, {nil, rest} -> {task, rest} task, {found, rest} -> {found, [task | rest]} end) end defp process_single_image( path, project_id, post_id, language, translate_targets, parent ) do with {:ok, media} <- Media.import_media(%{project_id: project_id, source_path: path}), true <- String.starts_with?(media.mime_type || "", "image/"), {:ok, result} <- AI.analyze_image(media.id, language: language), {:ok, _updated} <- Media.update_media(media.id, %{ title: result.title, alt: result.alt, caption: result.caption }), {:ok, _link} <- Media.link_media_to_post(media.id, post_id) do translate_media_translations(media.id, translate_targets) title = result.title || media.original_name send(parent, {:add_image_processed, title}) else false -> :ok {:error, reason} -> Logger.warning("Image pipeline error for #{path}: #{inspect(reason)}") send(parent, {:add_image_error, path, reason}) end end defp translate_media_translations(_media_id, []), do: :ok defp translate_media_translations(media_id, [target | rest]) do case AI.translate_media(media_id, target) do {:ok, translation} -> Media.upsert_media_translation(media_id, target, %{ title: translation.title, alt: translation.alt, caption: translation.caption }) translate_media_translations(media_id, rest) {:error, reason} -> Logger.warning( "Media translation failed for #{media_id} -> #{target}: #{inspect(reason)}" ) translate_media_translations(media_id, rest) end end end