mirror of
https://github.com/algora-io/tv.git
synced 2025-03-17 20:17:45 +02:00
177 lines
4.9 KiB
Elixir
177 lines
4.9 KiB
Elixir
defmodule AlgoraWeb.AdLive.Analytics do
|
|
use AlgoraWeb, :live_view
|
|
alias AlgoraWeb.Components.TechIcon
|
|
alias AlgoraWeb.RTMPDestinationIconComponent
|
|
|
|
alias Algora.{Ads, Library}
|
|
alias AlgoraWeb.PlayerComponent
|
|
|
|
@impl true
|
|
def mount(%{"slug" => slug}, _session, socket) do
|
|
ad = Ads.get_ad_by_slug!(slug)
|
|
|
|
%{
|
|
stats: stats,
|
|
appearances: appearances,
|
|
product_reviews: product_reviews,
|
|
product_review: product_review
|
|
} = fetch_ad_stats(ad)
|
|
|
|
blurb =
|
|
if product_review,
|
|
do: %{
|
|
video: Library.get_video!(product_review.video_id),
|
|
current_time: product_review.clip_from
|
|
}
|
|
|
|
if connected?(socket) do
|
|
if blurb do
|
|
send_update(PlayerComponent, %{
|
|
id: "analytics-player",
|
|
video: blurb.video,
|
|
current_user: socket.assigns.current_user,
|
|
current_time: blurb.current_time
|
|
})
|
|
end
|
|
end
|
|
|
|
{:ok,
|
|
socket
|
|
|> assign(ad: ad)
|
|
|> assign(stats: stats)
|
|
|> assign(appearances: appearances)
|
|
|> assign(product_review: product_review)
|
|
|> assign(product_reviews: product_reviews)
|
|
|> assign(blurb: blurb)}
|
|
end
|
|
|
|
@impl true
|
|
def handle_info(_arg, socket) do
|
|
{:noreply, socket}
|
|
end
|
|
|
|
@impl true
|
|
def handle_params(_params, _url, socket) do
|
|
socket =
|
|
cond do
|
|
socket.assigns.ad.og_image_url ->
|
|
assign(socket, :page_image, socket.assigns.ad.og_image_url)
|
|
|
|
socket.assigns.product_review ->
|
|
assign(socket, :page_image, socket.assigns.product_review.thumbnail_url)
|
|
|
|
true ->
|
|
socket
|
|
end
|
|
|
|
{:noreply,
|
|
socket
|
|
|> assign(:page_title, socket.assigns.ad.name)
|
|
|> assign(
|
|
:page_description,
|
|
"Discover the appearances of #{socket.assigns.ad.name} in livestreams and videos"
|
|
)}
|
|
end
|
|
|
|
defp fetch_ad_stats(ad) do
|
|
appearances = Ads.list_appearances(ad)
|
|
content_metrics = Ads.list_content_metrics(appearances)
|
|
|
|
product_reviews =
|
|
Ads.list_product_reviews(ad) |> Enum.sort_by(&(&1.clip_to - &1.clip_from), :desc)
|
|
|
|
twitch_views = Enum.reduce(content_metrics, 0, fn cm, acc -> acc + cm.twitch_views end)
|
|
youtube_views = Enum.reduce(content_metrics, 0, fn cm, acc -> acc + cm.youtube_views end)
|
|
twitter_views = Enum.reduce(content_metrics, 0, fn cm, acc -> acc + cm.twitter_views end)
|
|
|
|
tech_stack_data =
|
|
appearances
|
|
|> group_data_by_tech_stack(content_metrics)
|
|
|> Enum.sort_by(fn {_, d} -> d.views end, :desc)
|
|
|
|
product_review = List.first(product_reviews)
|
|
|
|
views =
|
|
%{
|
|
"Twitch" => twitch_views,
|
|
"YouTube" => youtube_views,
|
|
"Twitter" => twitter_views
|
|
}
|
|
|> Enum.sort_by(fn {_, v} -> v end, :desc)
|
|
|
|
%{
|
|
stats: %{
|
|
views: views,
|
|
total_views: twitch_views + youtube_views + twitter_views,
|
|
airtime: calculate_total_airtime(appearances),
|
|
streams: length(appearances),
|
|
creators: length(Enum.uniq_by(appearances, & &1.video.user.id)),
|
|
tech_stack_data: tech_stack_data
|
|
},
|
|
appearances: appearances,
|
|
product_reviews: product_reviews,
|
|
product_review: product_review
|
|
}
|
|
end
|
|
|
|
defp group_data_by_tech_stack(appearances, content_metrics) do
|
|
appearances
|
|
|> Enum.zip(content_metrics)
|
|
|> Enum.reduce(%{}, fn {appearance, metrics}, acc ->
|
|
tech_stack = get_tech_stack(appearance.video.user.id)
|
|
total_views = metrics.twitch_views + metrics.youtube_views + metrics.twitter_views
|
|
creator = appearance.video.user
|
|
|
|
Map.update(acc, tech_stack, %{views: total_views, creators: [creator]}, fn existing ->
|
|
%{
|
|
views: existing.views + total_views,
|
|
creators: [creator | existing.creators] |> Enum.uniq()
|
|
}
|
|
end)
|
|
end)
|
|
end
|
|
|
|
# TODO: This is a hack, we need to get the tech stack from the user's profile
|
|
defp get_tech_stack(user_id) do
|
|
case user_id do
|
|
7 -> "TypeScript"
|
|
8 -> "TypeScript"
|
|
109 -> "TypeScript"
|
|
307 -> "PHP"
|
|
588 -> "Elixir"
|
|
_ -> "Other"
|
|
end
|
|
end
|
|
|
|
defp calculate_total_airtime(appearances) do
|
|
appearances
|
|
|> Enum.reduce(0, fn appearance, acc -> acc + appearance.airtime end)
|
|
|> format_duration()
|
|
end
|
|
|
|
defp format_duration(seconds) do
|
|
hours = div(seconds, 3600)
|
|
minutes = div(rem(seconds, 3600), 60)
|
|
remaining_seconds = rem(seconds, 60)
|
|
|
|
cond do
|
|
hours > 0 -> "#{hours}h #{minutes}m #{remaining_seconds}s"
|
|
minutes > 0 -> "#{minutes}m #{remaining_seconds}s"
|
|
true -> "#{remaining_seconds}s"
|
|
end
|
|
end
|
|
|
|
defp format_number(number) when number >= 1_000_000 do
|
|
:io_lib.format("~.1fM", [number / 1_000_000]) |> to_string()
|
|
end
|
|
|
|
defp format_number(number) when number >= 1_000 do
|
|
:io_lib.format("~.1fK", [number / 1_000]) |> to_string()
|
|
end
|
|
|
|
defp format_number(number), do: to_string(number)
|
|
|
|
defp tech_icon(assigns), do: TechIcon.tech_icon(assigns)
|
|
defp source_icon(assigns), do: RTMPDestinationIconComponent.icon(assigns)
|
|
end
|