defmodule Algora.Admin.GenerateBlurbThumbnails do import Ecto.Query alias Algora.Ads.ProductReview alias Algora.{Clipper, Repo} def run do for product_review <- fetch_product_reviews() do :ok = process_product_review(product_review) end end defp fetch_product_reviews do Repo.all( from pr in ProductReview, join: v in assoc(pr, :video), where: is_nil(pr.thumbnail_url), preload: [video: v] ) end defp process_product_review(product_review) do with {:ok, thumbnail_path} <- create_thumbnail(product_review), {:ok, thumbnail_url} <- upload_thumbnail(thumbnail_path), {:ok, _updated_review} <- update_product_review(product_review, thumbnail_url) do IO.puts("Successfully processed ProductReview #{product_review.id}") else {:error, step, reason} -> IO.puts( "Error processing ProductReview #{product_review.id} at step #{step}: #{inspect(reason)}" ) :error end end defp create_thumbnail(product_review) do video_path = generate_thumbnails(product_review) output_path = "/tmp/thumbnail_#{product_review.id}.jpg" case Thumbnex.create_thumbnail(video_path, output_path, time: product_review.clip_from) do :ok -> {:ok, output_path} error -> {:error, :create_thumbnail, error} end end defp upload_thumbnail(file_path) do uuid = Ecto.UUID.generate() remote_path = "blurbs/#{uuid}.jpg" case Algora.Storage.upload_from_filename(file_path, remote_path, fn _ -> nil end, content_type: "image/jpeg" ) do {:ok, _} -> bucket = Algora.config([:buckets, :media]) %{scheme: scheme, host: host} = Application.fetch_env!(:ex_aws, :s3) |> Enum.into(%{}) thumbnail_url = "#{scheme}#{host}/#{bucket}/#{remote_path}" {:ok, thumbnail_url} error -> {:error, :upload_thumbnail, error} end end defp update_product_review(product_review, thumbnail_url) do product_review |> Ecto.Changeset.change(thumbnail_url: thumbnail_url) |> Repo.update() |> case do {:ok, updated_review} -> {:ok, updated_review} error -> {:error, :update_product_review, error} end end def generate_thumbnails(product_review) do # Generate clipped manifest %{playlist: playlist, ss: _ss} = Clipper.clip(product_review.video, product_review.clip_from, product_review.clip_to) # Find MediaInit and first Segment {init_tag, segment_tag} = find_init_and_segment(playlist.timeline) # Download and concatenate files video_path = download_and_concatenate(init_tag, segment_tag, product_review.video) video_path end defp find_init_and_segment(timeline) do init_tag = Enum.find(timeline, &match?(%ExM3U8.Tags.MediaInit{}, &1)) segments = Enum.filter(timeline, &match?(%ExM3U8.Tags.Segment{}, &1)) segment_tag = case segments do [_, second | _] -> second [first | _] -> first _ -> nil end {init_tag, segment_tag} end defp download_and_concatenate(init_tag, segment_tag, video) do temp_dir = Path.join(System.tmp_dir!(), video.uuid) File.mkdir_p!(temp_dir) output_path = Path.join(temp_dir, "output.mp4") init_url = maybe_to_absolute(init_tag.uri, video) segment_url = maybe_to_absolute(segment_tag.uri, video) init_path = download_file(init_url, Path.join(temp_dir, "init.mp4")) segment_path = download_file(segment_url, Path.join(temp_dir, "segment.m4s")) ffmpeg_command = "ffmpeg -y -i \"concat:#{init_path}|#{segment_path}\" -c copy #{output_path}" System.cmd("sh", ["-c", ffmpeg_command]) output_path end defp download_file(url, path) do {:ok, %{body: body}} = HTTPoison.get(url) File.write!(path, body) path end defp maybe_to_absolute(uri, video) do if URI.parse(uri).scheme do uri else Algora.Clipper.to_absolute(:video, video.uuid, uri) end end end