diff --git a/assets/js/app.js b/assets/js/app.js index 6cefdf1..c7ab771 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -3,15 +3,7 @@ import {Socket} from "phoenix" import {LiveSocket} from "./phoenix_live_view" import topbar from "../vendor/topbar" -let render = (webComponent, html) => { - let shadow = webComponent.attachShadow({mode: "open"}) - document.querySelectorAll("link").forEach(link => shadow.appendChild(link.cloneNode())) - let div = document.createElement("div") - div.setAttribute("class", webComponent.getAttribute("class")) - div.innerHTML = html || webComponent.innerHTML - shadow.appendChild(div) - return div -} +let nowSeconds = () => Math.round(Date.now() / 1000) let Hooks = {} @@ -40,23 +32,30 @@ Hooks.AudioPlayer = { this.player.pause() } document.addEventListener("click", enableAudio) + this.el.addEventListener("js:listen_now", () => this.play({sync: true})) this.el.addEventListener("js:play_pause", () => { - this.play() + if(this.player.paused){ + this.play() + } }) - this.handleEvent("play", ({url, began_at}) => { - this.playbackBeganAt = began_at - this.player.src = url - this.play() + this.handleEvent("play", ({url, elapsed}) => { + this.playbackBeganAt = nowSeconds() - elapsed + if(this.player.src === url && this.player.paused){ + this.play({sync: true}) + } else if(this.player.src !== url) { + this.player.src = url + this.play({sync: true}) + } }) this.handleEvent("pause", () => { - console.log("Server Pause!") this.pause() }) }, - play(){ + play(opts = {}){ + let {sync} = opts this.player.play().then(() => { - this.player.currentTime = (Date.now() - this.playbackBeganAt) / 1000 + if(sync){ this.player.currentTime = nowSeconds() - this.playbackBeganAt } this.progressTimer = setInterval(() => this.updateProgress(), 100) this.pushEvent("audio-accepted", {}) }, error => { @@ -65,8 +64,8 @@ Hooks.AudioPlayer = { }, pause(){ - this.player.pause() clearInterval(this.progressTimer) + this.player.pause() }, updateProgress(){ diff --git a/lib/live_beats/id3.ex b/lib/live_beats/id3.ex deleted file mode 100644 index 9c2b1a6..0000000 --- a/lib/live_beats/id3.ex +++ /dev/null @@ -1,25 +0,0 @@ -defmodule LiveBeats.ID3 do - alias LiveBeats.ID3 - - defstruct title: nil, - artist: nil, - album: nil, - year: nil - - def parse(path) do - with {:ok, parsed} <- :id3_tag_reader.read_tag(path) do - {:ok, parsed} - # %ID3{ - # title: strip(title), - # artist: strip(artist), - # album: strip(album), - # year: 2028 - # }} - else - other -> - {:error, other} - end - end - - defp strip(binary), do: String.trim_trailing(binary, <<0>>) -end diff --git a/lib/live_beats/media_library.ex b/lib/live_beats/media_library.ex index 20ef0be..aba6671 100644 --- a/lib/live_beats/media_library.ex +++ b/lib/live_beats/media_library.ex @@ -3,12 +3,18 @@ defmodule LiveBeats.MediaLibrary do The MediaLibrary context. """ + require Logger import Ecto.Query, warn: false alias LiveBeats.{Repo, MP3Stat, Accounts} alias LiveBeats.MediaLibrary.{Song, Genre} + alias Ecto.{Multi, Changeset} @pubsub LiveBeats.PubSub + defdelegate stopped?(song), to: Song + defdelegate playing?(song), to: Song + defdelegate paused?(song), to: Song + def subscribe(%Accounts.User{} = user) do Phoenix.PubSub.subscribe(@pubsub, topic(user.id)) end @@ -17,19 +23,65 @@ defmodule LiveBeats.MediaLibrary do def play_song(id) do song = get_song!(id) - Phoenix.PubSub.broadcast!(@pubsub, topic(song.user_id), {:play, song, %{began_at: now_ms()}}) + + played_at = + cond do + playing?(song) -> + song.played_at + + paused?(song) -> + elapsed = DateTime.diff(song.paused_at, song.played_at, :second) + DateTime.add(DateTime.utc_now(), -elapsed) + + true -> + DateTime.utc_now() + end + + changeset = + Changeset.change(song, %{ + played_at: DateTime.truncate(played_at, :second), + status: :playing + }) + + stopped_query = + from s in Song, + where: s.user_id == ^song.user_id and s.status == :playing, + update: [set: [status: :stopped]] + + {:ok, %{now_playing: new_song}} = + Multi.new() + |> Multi.update_all(:now_stopped, fn _ -> stopped_query end, []) + |> Multi.update(:now_playing, changeset) + |> Repo.transaction() + + elapsed = elapsed_playback(new_song) + Phoenix.PubSub.broadcast!(@pubsub, topic(song.user_id), {:play, song, %{elapsed: elapsed}}) end def pause_song(%Song{} = song) do + now = DateTime.truncate(DateTime.utc_now(), :second) + set = [status: :paused, paused_at: now] + pause_query = from(s in Song, where: s.id == ^song.id, update: [set: ^set]) + + stopped_query = + from s in Song, + where: s.user_id == ^song.user_id and s.status in [:playing, :paused], + update: [set: [status: :stopped]] + + {:ok, _} = + Multi.new() + |> Multi.update_all(:now_stopped, fn _ -> stopped_query end, []) + |> Multi.update_all(:now_paused, fn _ -> pause_query end, []) + |> Repo.transaction() + Phoenix.PubSub.broadcast!(@pubsub, topic(song.user_id), {:pause, song}) end defp topic(user_id), do: "room:#{user_id}" def store_mp3(%Song{} = song, tmp_path) do - dir = "priv/static/uploads/songs" - File.mkdir_p!(dir) - File.cp!(tmp_path, Path.join(dir, song.mp3_filename)) + File.mkdir_p!(Path.dirname(song.mp3_filepath)) + File.cp!(tmp_path, song.mp3_filepath) end def put_stats(%Ecto.Changeset{} = changeset, %MP3Stat{} = stat) do @@ -38,18 +90,17 @@ defmodule LiveBeats.MediaLibrary do def import_songs(%Accounts.User{} = user, changesets, consume_file) when is_map(changesets) and is_function(consume_file, 2) do - changesets - |> Enum.reduce(Ecto.Multi.new(), fn {ref, chset}, acc -> - chset = - chset - |> Song.put_user(user) - |> Song.put_mp3_path() - |> Map.put(:action, nil) + multi = + Enum.reduce(changesets, Ecto.Multi.new(), fn {ref, chset}, acc -> + chset = + chset + |> Song.put_user(user) + |> Song.put_mp3_path() - Ecto.Multi.insert(acc, {:song, ref}, chset) - end) - |> LiveBeats.Repo.transaction() - |> case do + Ecto.Multi.insert(acc, {:song, ref}, chset) + end) + + case LiveBeats.Repo.transaction(multi) do {:ok, results} -> {:ok, results @@ -83,7 +134,25 @@ defmodule LiveBeats.MediaLibrary do end def list_songs(limit \\ 100) do - Repo.all(from s in Song, limit: ^limit, order_by: [asc: s.inserted_at]) + Repo.all(from s in Song, limit: ^limit, order_by: [asc: s.inserted_at, asc: s.id]) + end + + def get_current_active_song(user_id) do + Repo.one(from s in Song, where: s.user_id == ^user_id and s.status in [:playing, :paused]) + end + + def elapsed_playback(%Song{} = song) do + cond do + playing?(song) -> + start_seconds = song.played_at |> DateTime.to_unix() + System.os_time(:second) - start_seconds + + paused?(song) -> + DateTime.diff(song.paused_at, song.played_at, :second) + + stopped?(song) -> + 0 + end end def get_song!(id), do: Repo.get!(Song, id) @@ -101,12 +170,20 @@ defmodule LiveBeats.MediaLibrary do end def delete_song(%Song{} = song) do + case File.rm(song.mp3_filepath) do + :ok -> + :ok + + {:error, reason} -> + Logger.info( + "unable to delete song #{song.id} at #{song.mp3_filepath}, got: #{inspect(reason)}" + ) + end + Repo.delete(song) end def change_song(%Song{} = song, attrs \\ %{}) do Song.changeset(song, attrs) end - - defp now_ms, do: System.system_time() |> System.convert_time_unit(:native, :millisecond) end diff --git a/lib/live_beats/media_library/song.ex b/lib/live_beats/media_library/song.ex index 1cf59c3..3859d4c 100644 --- a/lib/live_beats/media_library/song.ex +++ b/lib/live_beats/media_library/song.ex @@ -2,23 +2,31 @@ defmodule LiveBeats.MediaLibrary.Song do use Ecto.Schema import Ecto.Changeset + alias LiveBeats.MediaLibrary.Song alias LiveBeats.Accounts schema "songs" do field :album_artist, :string field :artist, :string + field :played_at, :utc_datetime + field :paused_at, :utc_datetime field :date_recorded, :naive_datetime field :date_released, :naive_datetime field :duration, :integer + field :status, Ecto.Enum, values: [stopped: 1, playing: 2, paused: 3] field :title, :string field :mp3_path, :string - field :mp3_filename, :string + field :mp3_filepath, :string belongs_to :user, Accounts.User belongs_to :genre, LiveBeats.MediaLibrary.Genre timestamps() end + def playing?(%Song{} = song), do: song.status == :playing + def paused?(%Song{} = song), do: song.status == :paused + def stopped?(%Song{} = song), do: song.status == :stopped + @doc false def changeset(song, attrs) do song @@ -34,10 +42,11 @@ defmodule LiveBeats.MediaLibrary.Song do def put_mp3_path(%Ecto.Changeset{} = changeset) do if changeset.valid? do filename = Ecto.UUID.generate() <> ".mp3" + filepath = Path.join("priv/static/uploads/songs", filename) changeset - |> Ecto.Changeset.put_change(:mp3_filename, filename) - |> Ecto.Changeset.put_change(:mp3_path, "uploads/songs/#{filename}") + |> Ecto.Changeset.put_change(:mp3_filepath, filepath) + |> Ecto.Changeset.put_change(:mp3_path, Path.join("uploads/songs", filename)) else changeset end diff --git a/lib/live_beats_web/live/player_live.ex b/lib/live_beats_web/live/player_live.ex index 87fd56e..b8e03d9 100644 --- a/lib/live_beats_web/live/player_live.ex +++ b/lib/live_beats_web/live/player_live.ex @@ -87,7 +87,7 @@ defmodule LiveBeatsWeb.PlayerLive do <%= if @error do %> - <.modal show id="enable-audio" on_confirm={js_play_pause() |> hide_modal("enable-audio")}> + <.modal show id="enable-audio" on_confirm={js_listen_now() |> hide_modal("enable-audio")}> <:title>Start Listening now Your browser needs a click event to enable playback <:confirm>Listen Now @@ -101,16 +101,25 @@ defmodule LiveBeatsWeb.PlayerLive do def mount(_parmas, _session, socket) do if connected?(socket) and socket.assigns.current_user do MediaLibrary.subscribe(socket.assigns.current_user) + send(self(), :play_current) end - {:ok, assign(socket, song: nil, playing: false, error: false), layout: false} + socket = + assign(socket, + song: nil, + playing: false, + error: false, + current_user_id: socket.assigns.current_user.id, + # todo use actual room user id + room_user_id: socket.assigns.current_user.id + ) + + {:ok, socket, layout: false, temporary_assigns: [current_user: nil]} end def handle_event("play_pause", _, socket) do %{song: song, playing: playing} = socket.assigns - IO.inspect({:play_pause, playing}) - cond do song && playing -> MediaLibrary.pause_song(song) @@ -133,6 +142,15 @@ defmodule LiveBeatsWeb.PlayerLive do {:noreply, assign(socket, error: false)} end + def handle_info(:play_current, socket) do + # we raced a pubsub, noop + if socket.assigns.song do + {:noreply, socket} + else + {:noreply, play_current_song(socket)} + end + end + def handle_info({:pause, _}, socket) do {:noreply, socket @@ -140,14 +158,44 @@ defmodule LiveBeatsWeb.PlayerLive do |> assign(playing: false)} end - def handle_info({:play, %Song{} = song, %{began_at: at}}, socket) do - {:noreply, - socket - |> push_event("play", %{began_at: at, url: Path.join(LiveBeatsWeb.Endpoint.url(), song.mp3_path)}) - |> assign(song: song, playing: true)} + def handle_info({:play, %Song{} = song, %{elapsed: elapsed}}, socket) do + {:noreply, play_song(socket, song, elapsed)} end - defp js_play_pause(js \\ %JS{}) do + defp play_song(socket, %Song{} = song, elapsed) do + socket + |> push_play(song, elapsed) + |> assign(song: song, playing: true) + end + + defp js_play_pause(%JS{} = js) do JS.dispatch(js, "js:play_pause", to: "#audio-player") end + + defp js_listen_now(js \\ %JS{}) do + JS.dispatch(js, "js:listen_now", to: "#audio-player") + end + + defp play_current_song(socket) do + song = MediaLibrary.get_current_active_song(socket.assigns.room_user_id) + + cond do + song && MediaLibrary.playing?(song) -> + play_song(socket, song, MediaLibrary.elapsed_playback(song)) + + song && MediaLibrary.paused?(song) -> + assign(socket, song: song, playing: false) + + true -> + socket + end + end + + defp push_play(socket, %Song{} = song, elapsed) do + push_event(socket, "play", %{ + paused: Song.paused?(song), + elapsed: elapsed, + url: Path.join(LiveBeatsWeb.Endpoint.url(), song.mp3_path) + }) + end end diff --git a/lib/live_beats_web/live/song_live/index.ex b/lib/live_beats_web/live/song_live/index.ex index c2ee5c8..a7ddad8 100644 --- a/lib/live_beats_web/live/song_live/index.ex +++ b/lib/live_beats_web/live/song_live/index.ex @@ -2,8 +2,8 @@ defmodule LiveBeatsWeb.SongLive.Index do use LiveBeatsWeb, :live_view alias LiveBeats.{MediaLibrary, MP3Stat} - alias LiveBeats.MediaLibrary.Song alias LiveBeatsWeb.LayoutComponent + alias LiveBeatsWeb.SongLive.{SongRowComponent, UploadFormComponent} def render(assigns) do ~H""" @@ -27,7 +27,7 @@ defmodule LiveBeatsWeb.SongLive.Index do <% end %> <.live_table - module={LiveBeatsWeb.SongLive.SongRow} + module={SongRowComponent} rows={@songs} row_id={fn song -> "song-#{song.id}" end} > @@ -45,19 +45,33 @@ defmodule LiveBeatsWeb.SongLive.Index do end def mount(_params, _session, socket) do + %{current_user: current_user} = socket.assigns + if connected?(socket) do - MediaLibrary.subscribe(socket.assigns.current_user) + MediaLibrary.subscribe(current_user) end - {:ok, assign(socket, songs: list_songs(), active_id: nil), temporary_assigns: [songs: []]} + active_id = + if song = MediaLibrary.get_current_active_song(current_user.id) do + SongRowComponent.send_status(song.id, song.status) + song.id + end + + {:ok, assign(socket, songs: list_songs(), active_id: active_id), temporary_assigns: [songs: []]} end def handle_params(params, _url, socket) do {:noreply, socket |> apply_action(socket.assigns.live_action, params) |> maybe_show_modal()} end - def handle_event("play-song", %{"id" => id}, socket) do - MediaLibrary.play_song(id) + def handle_event("play_or_pause", %{"id" => id}, socket) do + song = MediaLibrary.get_song!(id) + if socket.assigns.active_id == id and MediaLibrary.playing?(song) do + MediaLibrary.pause_song(song) + else + MediaLibrary.play_song(id) + end + {:noreply, socket} end @@ -67,42 +81,59 @@ defmodule LiveBeatsWeb.SongLive.Index do {:noreply, socket} end - def handle_info({:play, %Song{} = song, _meta}, socket) do + def handle_info({:play, %MediaLibrary.Song{} = song, _meta}, socket) do {:noreply, play_song(socket, song)} end - def handle_info({:pause, %Song{} = song}, socket) do + def handle_info({:pause, %MediaLibrary.Song{} = song}, socket) do {:noreply, pause_song(socket, song.id)} end + defp stop_song(socket, song_id) do + SongRowComponent.send_status(song_id, :stopped) + + if socket.assigns.active_id == song_id do + assign(socket, :active_id, nil) + else + socket + end + end + defp pause_song(socket, song_id) do - send_update(LiveBeatsWeb.SongLive.SongRow, id: "song-#{song_id}", action: :deactivate) + SongRowComponent.send_status(song_id, :paused) socket end - defp play_song(socket, %Song{} = song) do - send_update(LiveBeatsWeb.SongLive.SongRow, id: "song-#{song.id}", action: :activate) + defp play_song(socket, %MediaLibrary.Song{} = song) do + %{active_id: active_id} = socket.assigns - if socket.assigns.active_id do - socket - |> pause_song(socket.assigns.active_id) - |> assign(active_id: song.id) - else - assign(socket, active_id: song.id) + cond do + active_id == song.id -> + SongRowComponent.send_status(song.id, :playing) + socket + + active_id -> + SongRowComponent.send_status(song.id, :playing) + + socket + |> stop_song(active_id) + |> assign(active_id: song.id) + + true -> + SongRowComponent.send_status(song.id, :playing) + assign(socket, active_id: song.id) end end defp maybe_show_modal(socket) do - if socket.assigns.live_action in [:new, :edit] do - LayoutComponent.show_modal(LiveBeatsWeb.SongLive.UploadFormComponent, %{ + if socket.assigns.live_action in [:new] do + LayoutComponent.show_modal(UploadFormComponent, %{ + id: :new, confirm: {"Save", type: "submit", form: "song-form"}, patch_to: Routes.song_index_path(socket, :index), - id: socket.assigns.song.id || :new, - title: socket.assigns.page_title, - action: socket.assigns.live_action, song: socket.assigns.song, - current_user: socket.assigns.current_user, - genres: socket.assigns.genres + title: socket.assigns.page_title, + current_user: socket.assigns.current_user }) else LayoutComponent.hide_modal() @@ -114,7 +145,7 @@ defmodule LiveBeatsWeb.SongLive.Index do defp apply_action(socket, :new, _params) do socket |> assign(:page_title, "Add Songs") - |> assign(:song, %Song{}) + |> assign(:song, %MediaLibrary.Song{}) end defp apply_action(socket, :index, _params) do diff --git a/lib/live_beats_web/live/song_live/song_entry_component.ex b/lib/live_beats_web/live/song_live/song_entry.ex similarity index 97% rename from lib/live_beats_web/live/song_live/song_entry_component.ex rename to lib/live_beats_web/live/song_live/song_entry.ex index fa2ae22..a737f89 100644 --- a/lib/live_beats_web/live/song_live/song_entry_component.ex +++ b/lib/live_beats_web/live/song_live/song_entry.ex @@ -1,4 +1,4 @@ -defmodule LiveBeatsWeb.SongLive.SongEntryComponent do +defmodule LiveBeatsWeb.SongLive.SongEntry do use LiveBeatsWeb, :live_component alias LiveBeats.MP3Stat diff --git a/lib/live_beats_web/live/song_live/song_row.ex b/lib/live_beats_web/live/song_live/song_row_component.ex similarity index 59% rename from lib/live_beats_web/live/song_live/song_row.ex rename to lib/live_beats_web/live/song_live/song_row_component.ex index 222385c..6494e89 100644 --- a/lib/live_beats_web/live/song_live/song_row.ex +++ b/lib/live_beats_web/live/song_live/song_row_component.ex @@ -1,22 +1,32 @@ -defmodule LiveBeatsWeb.SongLive.SongRow do +defmodule LiveBeatsWeb.SongLive.SongRowComponent do use LiveBeatsWeb, :live_component + def send_status(id, status) when status in [:playing, :paused, :stopped] do + send_update(__MODULE__, id: "song-#{id}", action: :send, status: status) + end + def render(assigns) do ~H""" <%= for {col, i} <- Enum.with_index(@col) do %>
<%= if i == 0 do %> - <%= if @active do %> + <%= if @status == :playing do %> <.icon name={:volume_up} class="h-5 w-5 -mt-1 -ml-1"/> - <% else %> + <% end %> + <%= if @status == :paused do %> + + <.icon name={:volume_up} class="h-5 w-5 -mt-1 -ml-1 text-gray-400"/> + + <% end %> + <%= if @status == :stopped do %> <.icon name={:play} class="h-5 w-5 text-gray-400"/> @@ -30,16 +40,8 @@ defmodule LiveBeatsWeb.SongLive.SongRow do """ end - def update(%{action: :activate}, socket) do - {:ok, assign(socket, active: true)} - end - - def update(%{action: :deactivate}, socket) do - {:ok, assign(socket, active: false)} - end - - def update(%{action: action}, _socket) do - raise ArgumentError, "unkown action #{inspect(action)}" + def update(%{action: :send, status: status}, socket) when status in [:playing, :paused, :stopped] do + {:ok, assign(socket, status: status)} end def update(assigns, socket) do @@ -50,7 +52,7 @@ defmodule LiveBeatsWeb.SongLive.SongRow do col: assigns.col, class: assigns.class, index: assigns.index, - active: false + status: :stopped )} end end diff --git a/lib/live_beats_web/live/song_live/upload_form_component.ex b/lib/live_beats_web/live/song_live/upload_form_component.ex index 8104622..442da3c 100644 --- a/lib/live_beats_web/live/song_live/upload_form_component.ex +++ b/lib/live_beats_web/live/song_live/upload_form_component.ex @@ -2,7 +2,7 @@ defmodule LiveBeatsWeb.SongLive.UploadFormComponent do use LiveBeatsWeb, :live_component alias LiveBeats.{MediaLibrary, MP3Stat} - alias LiveBeatsWeb.SongLive.SongEntryComponent + alias LiveBeatsWeb.SongLive.SongEntry @max_songs 10 @@ -37,29 +37,12 @@ defmodule LiveBeatsWeb.SongLive.UploadFormComponent do {:noreply, drop_invalid_uploads(socket)} end - def handle_event("validate", %{"songs" => songs_params, "_target" => ["songs", _, _]}, socket) do - new_socket = - Enum.reduce(songs_params, socket, fn {ref, song_params}, acc -> - new_changeset = - acc - |> get_changeset(ref) - |> Ecto.Changeset.apply_changes() - |> MediaLibrary.change_song(song_params) - |> Map.put(:action, :validate) - - update_changeset(acc, new_changeset, ref) - end) - - {:noreply, new_socket} + def handle_event("validate", %{"songs" => params, "_target" => ["songs", _, _]}, socket) do + {:noreply, apply_params(socket, params, :validate)} end - defp consume_entry(socket, ref, store_func) when is_function(store_func) do - {entries, []} = uploaded_entries(socket, :mp3) - entry = Enum.find(entries, fn entry -> entry.ref == ref end) - consume_uploaded_entry(socket, entry, fn meta -> {:ok, store_func.(meta.path)} end) - end - - def handle_event("save", %{"songs" => song_params}, socket) do + def handle_event("save", %{"songs" => params}, socket) do + socket = apply_params(socket, params, :insert) %{current_user: current_user} = socket.assigns changesets = socket.assigns.changesets @@ -75,6 +58,25 @@ defmodule LiveBeatsWeb.SongLive.UploadFormComponent do end end + defp consume_entry(socket, ref, store_func) when is_function(store_func) do + {entries, []} = uploaded_entries(socket, :mp3) + entry = Enum.find(entries, fn entry -> entry.ref == ref end) + consume_uploaded_entry(socket, entry, fn meta -> {:ok, store_func.(meta.path)} end) + end + + defp apply_params(socket, params, action) when action in [:validate, :insert] do + Enum.reduce(params, socket, fn {ref, song_params}, acc -> + new_changeset = + acc + |> get_changeset(ref) + |> Ecto.Changeset.apply_changes() + |> MediaLibrary.change_song(song_params) + |> Map.put(:action, action) + + update_changeset(acc, new_changeset, ref) + end) + end + defp get_changeset(socket, entry_ref) do case Enum.find(socket.assigns.changesets, fn {ref, _changeset} -> ref === entry_ref end) do {^entry_ref, changeset} -> changeset @@ -102,27 +104,30 @@ defmodule LiveBeatsWeb.SongLive.UploadFormComponent do end defp handle_progress(:mp3, entry, socket) do - send_update(SongEntryComponent, id: entry.ref, progress: entry.progress) - lv = self() + send_update(SongEntry, id: entry.ref, progress: entry.progress) if entry.done? do - consume_uploaded_entry(socket, entry, fn %{path: path} -> - Task.Supervisor.start_child(LiveBeats.TaskSupervisor, fn -> - result = LiveBeats.MP3Stat.parse(path) - - send_update(lv, __MODULE__, - id: socket.assigns.id, - action: {:duration, entry.ref, result} - ) - end) - - {:postpone, :ok} - end) + async_calculate_duration(socket, entry) end {:noreply, put_new_changeset(socket, entry)} end + defp async_calculate_duration(socket, %Phoenix.LiveView.UploadEntry{} = entry) do + lv = self() + + consume_uploaded_entry(socket, entry, fn %{path: path} -> + Task.Supervisor.start_child(LiveBeats.TaskSupervisor, fn -> + send_update(lv, __MODULE__, + id: socket.assigns.id, + action: {:duration, entry.ref, LiveBeats.MP3Stat.parse(path)} + ) + end) + + {:postpone, :ok} + end) + end + defp file_error(%{kind: :dropped} = assigns), do: ~H|dropped (exceeds limit of 10 files)| defp file_error(%{kind: :too_large} = assigns), do: ~H|larger than 10MB| defp file_error(%{kind: :not_accepted} = assigns), do: ~H|not a valid MP3 file| diff --git a/lib/live_beats_web/live/song_live/upload_form_component.html.heex b/lib/live_beats_web/live/song_live/upload_form_component.html.heex index 50be1ad..7ddaf3d 100644 --- a/lib/live_beats_web/live/song_live/upload_form_component.html.heex +++ b/lib/live_beats_web/live/song_live/upload_form_component.html.heex @@ -2,7 +2,6 @@

<%= @title %>

<.form - let={f} for={:songs} id="song-form" class="space-y-8" @@ -13,7 +12,7 @@
<%= for {ref, changeset} <- @changesets do %> - <.live_component id={ref} module={SongEntryComponent} changeset={changeset} /> + <.live_component id={ref} module={SongEntry} changeset={changeset} /> <% end %> diff --git a/lib/live_beats_web/templates/layout/root.html.heex b/lib/live_beats_web/templates/layout/root.html.heex index 62d2d21..0cbc0d8 100644 --- a/lib/live_beats_web/templates/layout/root.html.heex +++ b/lib/live_beats_web/templates/layout/root.html.heex @@ -307,7 +307,9 @@
- <%= live_render(@conn, LiveBeatsWeb.PlayerLive, session: %{}) %> + <%= if @current_user do %> + <%= live_render(@conn, LiveBeatsWeb.PlayerLive, session: %{}) %> + <% end %> <%= @inner_content %>
diff --git a/priv/repo/migrations/20211027201102_create_songs.exs b/priv/repo/migrations/20211027201102_create_songs.exs index 1ec8387..54a2a19 100644 --- a/priv/repo/migrations/20211027201102_create_songs.exs +++ b/priv/repo/migrations/20211027201102_create_songs.exs @@ -4,11 +4,14 @@ defmodule LiveBeats.Repo.Migrations.CreateSongs do def change do create table(:songs) do add :album_artist, :string - add :artist, :string - add :duration, :integer - add :title, :string - add :mp3_path, :string - add :mp3_filename, :string + add :artist, :string, null: false + add :duration, :integer, default: 0, null: false + add :status, :integer, null: false, default: 1 + add :played_at, :utc_datetime + add :paused_at, :utc_datetime + add :title, :string, null: false + add :mp3_path, :string, null: false + add :mp3_filepath, :string, null: false add :date_recorded, :naive_datetime add :date_released, :naive_datetime add :user_id, references(:users, on_delete: :nothing) @@ -20,5 +23,6 @@ defmodule LiveBeats.Repo.Migrations.CreateSongs do create unique_index(:songs, [:user_id, :title, :artist]) create index(:songs, [:user_id]) create index(:songs, [:genre_id]) + create index(:songs, [:status]) end end diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index 904ddbe..3bb5ab8 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -14,11 +14,15 @@ # {:ok, _} = LiveBeats.MediaLibrary.create_genre(%{title: title}) # end -for i <- 1..200 do - {:ok, _} = - LiveBeats.MediaLibrary.create_song(%{ - artist: "Bonobo", - title: "Black Sands #{i}", - duration: 180_000 - }) -end +# for i <- 1..200 do +# filename = Ecto.UUID.generate() + +# {:ok, _} = +# LiveBeats.Repo.insert(%LiveBeats.MediaLibrary.Song{ +# artist: "Bonobo", +# title: "Black Sands #{i}", +# duration: 180_000, +# mp3_filename: filename, +# mp3_path: "uploads/songs/#{filename}" +# }) +# end