mirror of
https://github.com/fly-apps/live_beats.git
synced 2024-11-21 23:50:59 +00:00
Add attribution field and handle async duration race
This commit is contained in:
parent
ff7b064660
commit
eda99fa903
11 changed files with 72 additions and 21 deletions
|
@ -49,7 +49,7 @@ defmodule LiveBeats.MediaLibrary do
|
||||||
|
|
||||||
stopped_query =
|
stopped_query =
|
||||||
from s in Song,
|
from s in Song,
|
||||||
where: s.user_id == ^song.user_id and s.status == :playing,
|
where: s.user_id == ^song.user_id and s.status in [:playing, :paused],
|
||||||
update: [set: [status: :stopped]]
|
update: [set: [status: :stopped]]
|
||||||
|
|
||||||
{:ok, %{now_playing: new_song}} =
|
{:ok, %{now_playing: new_song}} =
|
||||||
|
@ -89,7 +89,12 @@ defmodule LiveBeats.MediaLibrary do
|
||||||
end
|
end
|
||||||
|
|
||||||
def put_stats(%Ecto.Changeset{} = changeset, %MP3Stat{} = stat) do
|
def put_stats(%Ecto.Changeset{} = changeset, %MP3Stat{} = stat) do
|
||||||
Ecto.Changeset.put_change(changeset, :duration, stat.duration)
|
chset = Song.put_duration(changeset, stat.duration)
|
||||||
|
if error = chset.errors[:duration] do
|
||||||
|
{:error, %{duration: error}}
|
||||||
|
else
|
||||||
|
{:ok, chset}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def import_songs(%Accounts.User{} = user, changesets, consume_file)
|
def import_songs(%Accounts.User{} = user, changesets, consume_file)
|
||||||
|
|
|
@ -15,6 +15,7 @@ defmodule LiveBeats.MediaLibrary.Song do
|
||||||
field :duration, :integer
|
field :duration, :integer
|
||||||
field :status, Ecto.Enum, values: [stopped: 1, playing: 2, paused: 3]
|
field :status, Ecto.Enum, values: [stopped: 1, playing: 2, paused: 3]
|
||||||
field :title, :string
|
field :title, :string
|
||||||
|
field :attribution, :string
|
||||||
field :mp3_url, :string
|
field :mp3_url, :string
|
||||||
field :mp3_filepath, :string
|
field :mp3_filepath, :string
|
||||||
field :mp3_filename, :string
|
field :mp3_filename, :string
|
||||||
|
@ -31,15 +32,24 @@ defmodule LiveBeats.MediaLibrary.Song do
|
||||||
@doc false
|
@doc false
|
||||||
def changeset(song, attrs) do
|
def changeset(song, attrs) do
|
||||||
song
|
song
|
||||||
|> cast(attrs, [:album_artist, :artist, :title, :date_recorded, :date_released])
|
|> cast(attrs, [:album_artist, :artist, :title, :attribution, :date_recorded, :date_released])
|
||||||
|> validate_required([:artist, :title])
|
|> validate_required([:artist, :title])
|
||||||
|> validate_number(:duration, greater_than: 0, less_than: 1200)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def put_user(%Ecto.Changeset{} = changeset, %Accounts.User{} = user) do
|
def put_user(%Ecto.Changeset{} = changeset, %Accounts.User{} = user) do
|
||||||
put_assoc(changeset, :user, user)
|
put_assoc(changeset, :user, user)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def put_duration(%Ecto.Changeset{} = changeset, duration) when is_integer(duration) do
|
||||||
|
changeset
|
||||||
|
|> Ecto.Changeset.change(%{duration: duration})
|
||||||
|
|> Ecto.Changeset.validate_number(:duration,
|
||||||
|
greater_than: 0,
|
||||||
|
less_than: 1200,
|
||||||
|
message: "must be less than 20 minutes"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
def put_mp3_path(%Ecto.Changeset{} = changeset) do
|
def put_mp3_path(%Ecto.Changeset{} = changeset) do
|
||||||
if changeset.valid? do
|
if changeset.valid? do
|
||||||
filename = Ecto.UUID.generate() <> ".mp3"
|
filename = Ecto.UUID.generate() <> ".mp3"
|
||||||
|
|
|
@ -315,7 +315,7 @@ defmodule LiveBeatsWeb.LiveHelpers do
|
||||||
<%= for {row, i} <- Enum.with_index(@rows) do %>
|
<%= for {row, i} <- Enum.with_index(@rows) do %>
|
||||||
<tr id={@row_id && @row_id.(row)} class="hover:bg-gray-50">
|
<tr id={@row_id && @row_id.(row)} class="hover:bg-gray-50">
|
||||||
<%= for col <- @col do %>
|
<%= for col <- @col do %>
|
||||||
<td class={"px-6 py-3 whitespace-nowrap text-sm font-medium text-gray-900 #{if i == 0, do: "max-w-0 w-full"}"}>
|
<td class={"px-6 py-3 whitespace-nowrap text-sm font-medium text-gray-900 #{if i == 0, do: "max-w-0 w-full"} #{col[:class]}"}>
|
||||||
<div class="flex items-center space-x-3 lg:pl-2">
|
<div class="flex items-center space-x-3 lg:pl-2">
|
||||||
<%= render_slot(col, row) %>
|
<%= render_slot(col, row) %>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -16,7 +16,7 @@ defmodule LiveBeatsWeb.PlayerLive do
|
||||||
<div class="bg-white dark:bg-gray-800 p-4">
|
<div class="bg-white dark:bg-gray-800 p-4">
|
||||||
<div class="flex items-center space-x-3.5 sm:space-x-5 lg:space-x-3.5 xl:space-x-5">
|
<div class="flex items-center space-x-3.5 sm:space-x-5 lg:space-x-3.5 xl:space-x-5">
|
||||||
<div class="pr-5">
|
<div class="pr-5">
|
||||||
<div class="min-w-0 flex-col space-y-0.5">
|
<div class="min-w-0 max-w-xs flex-col space-y-0.5">
|
||||||
<h2 class="text-black dark:text-white text-sm sm:text-sm lg:text-sm xl:text-sm font-semibold truncate">
|
<h2 class="text-black dark:text-white text-sm sm:text-sm lg:text-sm xl:text-sm font-semibold truncate">
|
||||||
<%= if @song, do: @song.title, else: raw(" ") %>
|
<%= if @song, do: @song.title, else: raw(" ") %>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
|
@ -33,6 +33,7 @@ defmodule LiveBeatsWeb.SongLive.Index do
|
||||||
>
|
>
|
||||||
<:col let={%{song: song}} label="Title"><%= song.title %></:col>
|
<:col let={%{song: song}} label="Title"><%= song.title %></:col>
|
||||||
<:col let={%{song: song}} label="Artist"><%= song.artist %></:col>
|
<:col let={%{song: song}} label="Artist"><%= song.artist %></:col>
|
||||||
|
<:col let={%{song: song}} label="Attribution" class="max-w-5xl break-words text-gray-600 font-light"><%= song.attribution %></:col>
|
||||||
<:col let={%{song: song}} label="Duration"><%= MP3Stat.to_mmss(song.duration) %></:col>
|
<:col let={%{song: song}} label="Duration"><%= MP3Stat.to_mmss(song.duration) %></:col>
|
||||||
<:col let={%{song: song}} label="">
|
<:col let={%{song: song}} label="">
|
||||||
<.link phx-click={show_modal("delete-modal-#{song.id}")} class="inline-flex items-center px-3 py-2 text-sm leading-4 font-medium">
|
<.link phx-click={show_modal("delete-modal-#{song.id}")} class="inline-flex items-center px-3 py-2 text-sm leading-4 font-medium">
|
||||||
|
|
|
@ -33,6 +33,18 @@ defmodule LiveBeatsWeb.SongLive.SongEntryComponent do
|
||||||
<.error input_name={"songs[#{@ref}][title]"} field={:title} errors={@errors} class="-mt-1"/>
|
<.error input_name={"songs[#{@ref}][title]"} field={:title} errors={@errors} class="-mt-1"/>
|
||||||
<.error input_name={"songs[#{@ref}][artist]"} field={:artist} errors={@errors} class="-mt-1"/>
|
<.error input_name={"songs[#{@ref}][artist]"} field={:artist} errors={@errors} class="-mt-1"/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="border col-span-full border-gray-300 rounded-md px-3 py-2 mt-2 shadow-sm focus-within:ring-1 focus-within:ring-indigo-600 focus-within:border-indigo-600">
|
||||||
|
<label for="name" class="block text-xs font-medium text-gray-900">
|
||||||
|
License Attribution <span class="text-gray-400">(as required by artist)</span>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
name={"songs[#{@ref}][attribution]"}
|
||||||
|
class="block w-full border-0 p-0 text-gray-900 placeholder-gray-500 focus:ring-0 sm:text-xs"
|
||||||
|
><%= @attribution %></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-full sm:grid sm:grid-cols-2 sm:gap-2 sm:items-start">
|
||||||
|
<.error input_name={"songs[#{@ref}][attribution]"} field={:attribution} errors={@errors} class="-mt-1"/>
|
||||||
|
</div>
|
||||||
<div style={"width: #{@progress}%;"} class="col-span-full bg-purple-500 dark:bg-purple-400 h-1.5 w-0 p-0">
|
<div style={"width: #{@progress}%;"} class="col-span-full bg-purple-500 dark:bg-purple-400 h-1.5 w-0 p-0">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -51,6 +63,7 @@ defmodule LiveBeatsWeb.SongLive.SongEntryComponent do
|
||||||
|> assign(title: Ecto.Changeset.get_field(changeset, :title))
|
|> assign(title: Ecto.Changeset.get_field(changeset, :title))
|
||||||
|> assign(artist: Ecto.Changeset.get_field(changeset, :artist))
|
|> assign(artist: Ecto.Changeset.get_field(changeset, :artist))
|
||||||
|> assign(duration: Ecto.Changeset.get_field(changeset, :duration))
|
|> assign(duration: Ecto.Changeset.get_field(changeset, :duration))
|
||||||
|
|> assign(attribution: Ecto.Changeset.get_field(changeset, :attribution))
|
||||||
|> assign_new(:progress, fn -> 0 end)}
|
|> assign_new(:progress, fn -> 0 end)}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,7 +10,7 @@ defmodule LiveBeatsWeb.SongLive.SongRowComponent do
|
||||||
<tr id={@id} class={@class}}>
|
<tr id={@id} class={@class}}>
|
||||||
<%= for {col, i} <- Enum.with_index(@col) do %>
|
<%= for {col, i} <- Enum.with_index(@col) do %>
|
||||||
<td
|
<td
|
||||||
class={"px-6 py-3 whitespace-nowrap text-sm font-medium text-gray-900 #{if i == 0, do: "max-w-0 w-full cursor-pointer"}"}
|
class={"px-6 py-3 text-sm font-medium text-gray-900 #{if i == 0, do: "w-80 cursor-pointer"} #{col[:class]}"}
|
||||||
phx-click={JS.push("play_or_pause", value: %{id: @song.id})}
|
phx-click={JS.push("play_or_pause", value: %{id: @song.id})}
|
||||||
>
|
>
|
||||||
<div class="flex items-center space-x-3 lg:pl-2">
|
<div class="flex items-center space-x-3 lg:pl-2">
|
||||||
|
|
|
@ -46,18 +46,26 @@ defmodule LiveBeatsWeb.SongLive.UploadFormComponent do
|
||||||
%{current_user: current_user} = socket.assigns
|
%{current_user: current_user} = socket.assigns
|
||||||
changesets = socket.assigns.changesets
|
changesets = socket.assigns.changesets
|
||||||
|
|
||||||
case MediaLibrary.import_songs(current_user, changesets, &consume_entry(socket, &1, &2)) do
|
if pending_stats?(socket) do
|
||||||
{:ok, songs} ->
|
{:noreply, socket}
|
||||||
{:noreply,
|
else
|
||||||
socket
|
case MediaLibrary.import_songs(current_user, changesets, &consume_entry(socket, &1, &2)) do
|
||||||
|> put_flash(:info, "#{map_size(songs)} song(s) uploaded")
|
{:ok, songs} ->
|
||||||
|> push_redirect(to: Routes.song_index_path(socket, :index))}
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> put_flash(:info, "#{map_size(songs)} song(s) uploaded")
|
||||||
|
|> push_redirect(to: home_path(socket))}
|
||||||
|
|
||||||
{:error, _reason} ->
|
{:error, _reason} ->
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp pending_stats?(socket) do
|
||||||
|
Enum.find(socket.assigns.changesets, fn {_ref, chset} -> !chset.changes[:duration] end)
|
||||||
|
end
|
||||||
|
|
||||||
defp consume_entry(socket, ref, store_func) when is_function(store_func) do
|
defp consume_entry(socket, ref, store_func) when is_function(store_func) do
|
||||||
{entries, []} = uploaded_entries(socket, :mp3)
|
{entries, []} = uploaded_entries(socket, :mp3)
|
||||||
entry = Enum.find(entries, fn entry -> entry.ref == ref end)
|
entry = Enum.find(entries, fn entry -> entry.ref == ref end)
|
||||||
|
@ -121,6 +129,7 @@ defmodule LiveBeatsWeb.SongLive.UploadFormComponent do
|
||||||
|
|
||||||
consume_uploaded_entry(socket, entry, fn %{path: path} ->
|
consume_uploaded_entry(socket, entry, fn %{path: path} ->
|
||||||
Task.Supervisor.start_child(LiveBeats.TaskSupervisor, fn ->
|
Task.Supervisor.start_child(LiveBeats.TaskSupervisor, fn ->
|
||||||
|
Process.sleep(5000)
|
||||||
send_update(lv, __MODULE__,
|
send_update(lv, __MODULE__,
|
||||||
id: socket.assigns.id,
|
id: socket.assigns.id,
|
||||||
action: {:duration, entry.ref, LiveBeats.MP3Stat.parse(path)}
|
action: {:duration, entry.ref, LiveBeats.MP3Stat.parse(path)}
|
||||||
|
@ -136,9 +145,20 @@ defmodule LiveBeatsWeb.SongLive.UploadFormComponent do
|
||||||
defp file_error(%{kind: :not_accepted} = assigns), do: ~H|not a valid MP3 file|
|
defp file_error(%{kind: :not_accepted} = assigns), do: ~H|not a valid MP3 file|
|
||||||
defp file_error(%{kind: :too_many_files} = assigns), do: ~H|too many files|
|
defp file_error(%{kind: :too_many_files} = assigns), do: ~H|too many files|
|
||||||
|
|
||||||
|
defp file_error(%{kind: {msg, opts}} = assigns) when is_binary(msg) and is_list(opts) do
|
||||||
|
~H|<%= LiveBeatsWeb.ErrorHelpers.translate_error(@kind) %>|
|
||||||
|
end
|
||||||
|
|
||||||
defp put_stats(socket, entry_ref, %MP3Stat{} = stat) do
|
defp put_stats(socket, entry_ref, %MP3Stat{} = stat) do
|
||||||
if changeset = get_changeset(socket, entry_ref) do
|
if changeset = get_changeset(socket, entry_ref) do
|
||||||
update_changeset(socket, MediaLibrary.put_stats(changeset, stat), entry_ref)
|
case MediaLibrary.put_stats(changeset, stat) do
|
||||||
|
{:ok, new_changeset} ->
|
||||||
|
update_changeset(socket, new_changeset, entry_ref)
|
||||||
|
|
||||||
|
{:error, %{duration: error}} ->
|
||||||
|
IO.inspect({:duration, error})
|
||||||
|
cancel_changeset_upload(socket, entry_ref, error)
|
||||||
|
end
|
||||||
else
|
else
|
||||||
socket
|
socket
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,4 +1,10 @@
|
||||||
<main>
|
<main>
|
||||||
|
<.live_component module={LiveBeatsWeb.LayoutComponent} id="layout" />
|
||||||
|
|
||||||
|
<%= if @current_user do %>
|
||||||
|
<%= live_render(@socket, LiveBeatsWeb.PlayerLive, id: "player", session: %{}, sticky: true) %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<p class="alert alert-info fade-in-scale" role="alert"
|
<p class="alert alert-info fade-in-scale" role="alert"
|
||||||
phx-click="lv:clear-flash"
|
phx-click="lv:clear-flash"
|
||||||
phx-value-key="info"><%= live_flash(@flash, :info) %></p>
|
phx-value-key="info"><%= live_flash(@flash, :info) %></p>
|
||||||
|
@ -7,7 +13,5 @@
|
||||||
phx-click="lv:clear-flash"
|
phx-click="lv:clear-flash"
|
||||||
phx-value-key="error"><%= live_flash(@flash, :error) %></p>
|
phx-value-key="error"><%= live_flash(@flash, :error) %></p>
|
||||||
|
|
||||||
<.live_component module={LiveBeatsWeb.LayoutComponent} id="layout" />
|
|
||||||
|
|
||||||
<%= @inner_content %>
|
<%= @inner_content %>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
@ -301,9 +301,6 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<main class="flex-1 relative z-0 overflow-y-auto focus:outline-none">
|
<main class="flex-1 relative z-0 overflow-y-auto focus:outline-none">
|
||||||
<%= if @current_user do %>
|
|
||||||
<%= live_render(@conn, LiveBeatsWeb.PlayerLive, session: %{}) %>
|
|
||||||
<% end %>
|
|
||||||
<%= @inner_content %>
|
<%= @inner_content %>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -10,6 +10,7 @@ defmodule LiveBeats.Repo.Migrations.CreateSongs do
|
||||||
add :played_at, :utc_datetime
|
add :played_at, :utc_datetime
|
||||||
add :paused_at, :utc_datetime
|
add :paused_at, :utc_datetime
|
||||||
add :title, :string, null: false
|
add :title, :string, null: false
|
||||||
|
add :attribution, :string
|
||||||
add :mp3_url, :string, null: false
|
add :mp3_url, :string, null: false
|
||||||
add :mp3_filename, :string, null: false
|
add :mp3_filename, :string, null: false
|
||||||
add :mp3_filepath, :string, null: false
|
add :mp3_filepath, :string, null: false
|
||||||
|
|
Loading…
Reference in a new issue