Add profiles

This commit is contained in:
Chris McCord 2021-11-11 22:42:10 -05:00
parent 5fa2944627
commit c45510cb6e
11 changed files with 350 additions and 109 deletions

View file

@ -1,7 +1,7 @@
import "phoenix_html" import "phoenix_html"
import {Socket} from "phoenix" import {Socket} from "phoenix"
// import {LiveSocket} from "./phoenix_live_view" import {LiveSocket} from "./phoenix_live_view"
import {LiveSocket} from "phoenix_live_view" // import {LiveSocket} from "phoenix_live_view"
import topbar from "../vendor/topbar" import topbar from "../vendor/topbar"
let nowSeconds = () => Math.round(Date.now() / 1000) let nowSeconds = () => Math.round(Date.now() / 1000)
@ -54,9 +54,8 @@ Hooks.AudioPlayer = {
this.play({sync: true}) this.play({sync: true})
} }
}) })
this.handleEvent("pause", () => { this.handleEvent("pause", () => this.pause())
this.pause() this.handleEvent("stop", () => this.stop())
})
}, },
play(opts = {}){ play(opts = {}){
@ -76,10 +75,19 @@ Hooks.AudioPlayer = {
this.player.pause() this.player.pause()
}, },
stop(){
clearInterval(this.progressTimer)
this.player.pause()
this.player.currentTime = 0
this.updateProgress()
this.duration.innerText = ""
this.currentTime.innerText = ""
},
updateProgress(){ updateProgress(){
if(isNaN(this.player.duration)){ return false } if(isNaN(this.player.duration)){ return false }
if(this.player.currentTime >= this.player.duration){ if(this.player.currentTime >= this.player.duration){
this.pushEvent("next-song-auto") this.pushEvent("next_song_auto")
clearInterval(this.progressTimer) clearInterval(this.progressTimer)
return return
} }

View file

@ -6,6 +6,17 @@ defmodule LiveBeats.Accounts do
alias LiveBeats.Accounts.{User, Identity} alias LiveBeats.Accounts.{User, Identity}
@admin_emails ["chris@chrismccord.com"] @admin_emails ["chris@chrismccord.com"]
@pubsub LiveBeats.PubSub
def subscribe(user_id) do
Phoenix.PubSub.subscribe(@pubsub, topic(user_id))
end
def unsubscribe(user_id) do
Phoenix.PubSub.unsubscribe(@pubsub, topic(user_id))
end
defp topic(user_id), do: "user:#{user_id}"
def list_users(opts) do def list_users(opts) do
Repo.all(from u in User, limit: ^Keyword.fetch!(opts, :limit)) Repo.all(from u in User, limit: ^Keyword.fetch!(opts, :limit))
@ -47,6 +58,27 @@ defmodule LiveBeats.Accounts do
""" """
def get_user!(id), do: Repo.get!(User, id) def get_user!(id), do: Repo.get!(User, id)
def get_user_by!(fields), do: Repo.get_by!(User, fields)
def update_active_profile(%User{active_profile_user_id: same_id} = current_user, same_id) do
current_user
end
def update_active_profile(%User{} = current_user, profile_user_id) do
{1, _} =
Repo.update_all(from(u in User, where: u.id == ^current_user.id),
set: [active_profile_user_id: profile_user_id]
)
Phoenix.PubSub.broadcast!(
@pubsub,
topic(current_user.id),
{__MODULE__, :active_profile_changed, current_user, %{user_id: profile_user_id}}
)
%User{current_user | active_profile_user_id: profile_user_id}
end
## User registration ## User registration
@doc """ @doc """

View file

@ -11,6 +11,7 @@ defmodule LiveBeats.Accounts.User do
field :confirmed_at, :naive_datetime field :confirmed_at, :naive_datetime
field :role, :string, default: "subscriber" field :role, :string, default: "subscriber"
field :profile_tagline, :string field :profile_tagline, :string
field :active_profile_user_id, :id
has_many :identities, Identity has_many :identities, Identity

View file

@ -6,7 +6,7 @@ defmodule LiveBeats.MediaLibrary do
require Logger require Logger
import Ecto.Query, warn: false import Ecto.Query, warn: false
alias LiveBeats.{Repo, MP3Stat, Accounts} alias LiveBeats.{Repo, MP3Stat, Accounts}
alias LiveBeats.MediaLibrary.{Song, Genre} alias LiveBeats.MediaLibrary.{Profile, Song, Genre}
alias Ecto.{Multi, Changeset} alias Ecto.{Multi, Changeset}
@pubsub LiveBeats.PubSub @pubsub LiveBeats.PubSub
@ -16,15 +16,25 @@ defmodule LiveBeats.MediaLibrary do
defdelegate playing?(song), to: Song defdelegate playing?(song), to: Song
defdelegate paused?(song), to: Song defdelegate paused?(song), to: Song
def subscribe(%Accounts.User{} = user) do def subscribe_to_profile(%Profile{} = profile, from \\ nil) do
Phoenix.PubSub.subscribe(@pubsub, topic(user.id)) Phoenix.PubSub.subscribe(@pubsub, topic(profile.user_id))
end
def unsubscribe_to_profile(%Profile{} = profile) do
Phoenix.PubSub.unsubscribe(@pubsub, topic(profile.user_id))
end end
def local_filepath(filename_uuid) when is_binary(filename_uuid) do def local_filepath(filename_uuid) when is_binary(filename_uuid) do
Path.join("priv/uploads/songs", filename_uuid) Path.join("priv/uploads/songs", filename_uuid)
end end
def play_song(%Song{id: id}), do: play_song(id) def can_control_playback?(%Accounts.User{} = user, %Song{} = song) do
user.id == song.user_id
end
def play_song(%Song{id: id}) do
play_song(id)
end
def play_song(id) do def play_song(id) do
song = get_song!(id) song = get_song!(id)
@ -60,7 +70,14 @@ defmodule LiveBeats.MediaLibrary do
|> Repo.transaction() |> Repo.transaction()
elapsed = elapsed_playback(new_song) elapsed = elapsed_playback(new_song)
Phoenix.PubSub.broadcast!(@pubsub, topic(song.user_id), {:play, song, %{elapsed: elapsed}})
Phoenix.PubSub.broadcast!(
@pubsub,
topic(song.user_id),
{__MODULE__, :play, song, %{elapsed: elapsed}}
)
new_song
end end
def pause_song(%Song{} = song) do def pause_song(%Song{} = song) do
@ -79,36 +96,36 @@ defmodule LiveBeats.MediaLibrary do
|> Multi.update_all(:now_paused, fn _ -> pause_query end, []) |> Multi.update_all(:now_paused, fn _ -> pause_query end, [])
|> Repo.transaction() |> Repo.transaction()
Phoenix.PubSub.broadcast!(@pubsub, topic(song.user_id), {:pause, song}) Phoenix.PubSub.broadcast!(@pubsub, topic(song.user_id), {__MODULE__, :pause, song})
end end
def play_next_song_auto(user_id) do def play_next_song_auto(%Profile{} = profile) do
song = get_current_active_song(user_id) || get_first_song(user_id) song = get_current_active_song(profile) || get_first_song(profile)
if song && elapsed_playback(song) >= song.duration - @auto_next_threshold_seconds do if song && elapsed_playback(song) >= song.duration - @auto_next_threshold_seconds do
song song
|> get_next_song() |> get_next_song(profile)
|> play_song() |> play_song()
end end
end end
def play_prev_song(user_id) do def play_prev_song(%Profile{} = profile) do
song = get_current_active_song(user_id) || get_first_song(user_id) song = get_current_active_song(profile) || get_first_song(profile)
if prev_song = get_prev_song(song) do if prev_song = get_prev_song(song, profile) do
play_song(prev_song) play_song(prev_song)
end end
end end
def play_next_song(user_id) do def play_next_song(%Profile{} = profile) do
song = get_current_active_song(user_id) || get_first_song(user_id) song = get_current_active_song(profile) || get_first_song(profile)
if next_song = get_next_song(song) do if next_song = get_next_song(song, profile) do
play_song(next_song) play_song(next_song)
end end
end end
defp topic(user_id), do: "room:#{user_id}" defp topic(user_id) when is_integer(user_id), do: "profile:#{user_id}"
def store_mp3(%Song{} = song, tmp_path) do def store_mp3(%Song{} = song, tmp_path) do
File.mkdir_p!(Path.dirname(song.mp3_filepath)) File.mkdir_p!(Path.dirname(song.mp3_filepath))
@ -170,16 +187,28 @@ defmodule LiveBeats.MediaLibrary do
Repo.all(Genre, order_by: [asc: :title]) Repo.all(Genre, order_by: [asc: :title])
end end
def list_songs(limit \\ 100) do def list_profile_songs(%Profile{} = profile, limit \\ 100) do
from(s in Song, limit: ^limit) from(s in Song, where: s.user_id == ^profile.user_id, limit: ^limit)
|> order_by_playlist(:asc) |> order_by_playlist(:asc)
|> Repo.all() |> Repo.all()
end end
def get_current_active_song(user_id) do def get_current_active_song(%Profile{user_id: user_id}) do
Repo.one(from s in Song, where: s.user_id == ^user_id and s.status in [:playing, :paused]) Repo.one(from s in Song, where: s.user_id == ^user_id and s.status in [:playing, :paused])
end end
def get_profile!(%Accounts.User{} = user) do
%Profile{user_id: user.id, username: user.username, tagline: user.profile_tagline}
end
def owns_profile?(%Accounts.User{} = user, %Profile{} = profile) do
user.id == profile.user_id
end
def owns_song?(%Profile{} = profile, %Song{} = song) do
profile.user_id == song.user_id
end
def elapsed_playback(%Song{} = song) do def elapsed_playback(%Song{} = song) do
cond do cond do
playing?(song) -> playing?(song) ->
@ -196,7 +225,7 @@ defmodule LiveBeats.MediaLibrary do
def get_song!(id), do: Repo.get!(Song, id) def get_song!(id), do: Repo.get!(Song, id)
def get_first_song(user_id) do def get_first_song(%Profile{user_id: user_id}) do
from(s in Song, from(s in Song,
where: s.user_id == ^user_id, where: s.user_id == ^user_id,
limit: 1 limit: 1
@ -205,7 +234,7 @@ defmodule LiveBeats.MediaLibrary do
|> Repo.one() |> Repo.one()
end end
def get_last_song(user_id) do def get_last_song(%Profile{user_id: user_id}) do
from(s in Song, from(s in Song,
where: s.user_id == ^user_id, where: s.user_id == ^user_id,
limit: 1 limit: 1
@ -214,7 +243,7 @@ defmodule LiveBeats.MediaLibrary do
|> Repo.one() |> Repo.one()
end end
def get_next_song(%Song{} = song) do def get_next_song(%Song{} = song, %Profile{} = profile) do
next = next =
from(s in Song, from(s in Song,
where: s.user_id == ^song.user_id and s.id > ^song.id, where: s.user_id == ^song.user_id and s.id > ^song.id,
@ -223,10 +252,10 @@ defmodule LiveBeats.MediaLibrary do
|> order_by_playlist(:asc) |> order_by_playlist(:asc)
|> Repo.one() |> Repo.one()
next || get_first_song(song.user_id) next || get_first_song(profile)
end end
def get_prev_song(%Song{} = song) do def get_prev_song(%Song{} = song, %Profile{} = profile) do
prev = prev =
from(s in Song, from(s in Song,
where: s.user_id == ^song.user_id and s.id < ^song.id, where: s.user_id == ^song.user_id and s.id < ^song.id,
@ -236,7 +265,7 @@ defmodule LiveBeats.MediaLibrary do
|> order_by_playlist(:desc) |> order_by_playlist(:desc)
|> Repo.one() |> Repo.one()
prev || get_last_song(song.user_id) prev || get_last_song(profile)
end end
def create_song(attrs \\ %{}) do def create_song(attrs \\ %{}) do

View file

@ -0,0 +1,3 @@
defmodule LiveBeats.MediaLibrary.Profile do
defstruct user_id: nil, username: nil, tagline: nil
end

View file

@ -5,9 +5,17 @@ defmodule LiveBeatsWeb.FileController do
def show(conn, %{"id" => filename_uuid, "token" => token}) do def show(conn, %{"id" => filename_uuid, "token" => token}) do
case Phoenix.Token.verify(conn, "file", token, max_age: :timer.minutes(10)) do case Phoenix.Token.verify(conn, "file", token, max_age: :timer.minutes(10)) do
{:ok, ^filename_uuid} -> send_file(conn, 200, MediaLibrary.local_filepath(filename_uuid)) {:ok, ^filename_uuid} -> do_send_file(conn, MediaLibrary.local_filepath(filename_uuid))
{:ok, _} -> send_resp(conn, :unauthorized, "") {:ok, _} -> send_resp(conn, :unauthorized, "")
{:error, _} -> send_resp(conn, :unauthorized, "") {:error, _} -> send_resp(conn, :unauthorized, "")
end end
end
defp do_send_file(conn, path) do
# accept-ranges headers required for chrome to seek via currentTime
conn
|> put_resp_header("content-type", "audio/mp3")
|> put_resp_header("accept-ranges", "bytes")
|> send_file(200, path)
end end
end end

View file

@ -1,7 +1,7 @@
defmodule LiveBeatsWeb.HomeLive do defmodule LiveBeatsWeb.HomeLive do
use LiveBeatsWeb, :live_view use LiveBeatsWeb, :live_view
alias LiveBeats.MediaLibrary # alias LiveBeats.MediaLibrary
def render(assigns) do def render(assigns) do
~H""" ~H"""
@ -346,6 +346,6 @@ defmodule LiveBeatsWeb.HomeLive do
end end
defp fetch_songs(_socket) do defp fetch_songs(_socket) do
MediaLibrary.list_songs() []
end end
end end

View file

@ -1,7 +1,7 @@
defmodule LiveBeatsWeb.PlayerLive do defmodule LiveBeatsWeb.PlayerLive do
use LiveBeatsWeb, {:live_view, container: {:div, []}} use LiveBeatsWeb, {:live_view, container: {:div, []}}
alias LiveBeats.MediaLibrary alias LiveBeats.{Accounts, MediaLibrary}
alias LiveBeats.MediaLibrary.Song alias LiveBeats.MediaLibrary.Song
on_mount {LiveBeatsWeb.UserAuth, :current_user} on_mount {LiveBeatsWeb.UserAuth, :current_user}
@ -36,14 +36,20 @@ defmodule LiveBeatsWeb.PlayerLive do
</div> </div>
</div> </div>
<div class="bg-gray-50 text-black dark:bg-gray-900 dark:text-white px-1 sm:px-3 lg:px-1 xl:px-3 grid grid-cols-5 items-center"> <div class="bg-gray-50 text-black dark:bg-gray-900 dark:text-white px-1 sm:px-3 lg:px-1 xl:px-3 grid grid-cols-5 items-center">
<button type="button" class="mx-auto scale-75"> <%= if @profile do %>
<svg width="24" height="24" fill="none"> <.link
<path d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" /> redirect_to={Routes.song_index_path(@socket, :index, @profile.username)}
</svg> class="mx-auto flex outline border-2 border-white border-opacity-20 rounded-md p-1 pr-2"
</button> >
<span class="mt-1"><.icon name={:user_circle}/></span>
<p class="ml-2"><%= @profile.username %></p>
</.link>
<% else %>
<div class="mx-auto flex"></div>
<% end %>
<!-- prev --> <!-- prev -->
<button type="button" class="sm:block xl:block mx-auto scale-75" phx-click="prev-song"> <button type="button" class="sm:block xl:block mx-auto scale-75" phx-click={js_prev(@own_profile?)}>
<svg width="17" height="18"> <svg width="17" height="18">
<path d="M0 0h2v18H0V0zM4 9l13-9v18L4 9z" fill="currentColor" /> <path d="M0 0h2v18H0V0zM4 9l13-9v18L4 9z" fill="currentColor" />
</svg> </svg>
@ -51,7 +57,7 @@ defmodule LiveBeatsWeb.PlayerLive do
<!-- /prev --> <!-- /prev -->
<!-- pause --> <!-- pause -->
<button type="button" class="mx-auto scale-75" phx-click={JS.push("play_pause") |> js_play_pause()}> <button type="button" class="mx-auto scale-75" phx-click={js_play_pause(@own_profile?)}>
<%= if @playing do %> <%= if @playing do %>
<svg id="player-pause" width="50" height="50" fill="none"> <svg id="player-pause" width="50" height="50" fill="none">
<circle class="text-gray-300 dark:text-gray-500" cx="25" cy="25" r="24" stroke="currentColor" stroke-width="1.5" /> <circle class="text-gray-300 dark:text-gray-500" cx="25" cy="25" r="24" stroke="currentColor" stroke-width="1.5" />
@ -67,7 +73,7 @@ defmodule LiveBeatsWeb.PlayerLive do
<!-- /pause --> <!-- /pause -->
<!-- next --> <!-- next -->
<button type="button" class="mx-auto scale-75" phx-click="next-song"> <button type="button" class="mx-auto scale-75" phx-click={js_next(@own_profile?)}>
<svg width="17" height="18" viewBox="0 0 17 18" fill="none"> <svg width="17" height="18" viewBox="0 0 17 18" fill="none">
<path d="M17 0H15V18H17V0Z" fill="currentColor" /> <path d="M17 0H15V18H17V0Z" fill="currentColor" />
<path d="M13 9L0 0V18L13 9Z" fill="currentColor" /> <path d="M13 9L0 0V18L13 9Z" fill="currentColor" />
@ -85,84 +91,153 @@ defmodule LiveBeatsWeb.PlayerLive do
Your browser needs a click event to enable playback Your browser needs a click event to enable playback
<:confirm>Listen Now</:confirm> <:confirm>Listen Now</:confirm>
</.modal> </.modal>
<%= if @profile do %>
<.modal id="not-authorized" on_confirm={hide_modal("not-authorized")}>
<:title>You can't do that</:title>
Only <%= @profile.username %> can control playback
<:confirm>Ok</:confirm>
</.modal>
<% end %>
</div> </div>
<!-- /player --> <!-- /player -->
""" """
end end
def mount(_parmas, _session, socket) do def mount(_parmas, _session, socket) do
if connected?(socket) and socket.assigns.current_user do %{current_user: current_user} = socket.assigns
MediaLibrary.subscribe(socket.assigns.current_user)
send(self(), :play_current) if connected?(socket) do
Accounts.subscribe(current_user.id)
end end
socket = socket =
assign(socket, socket
|> assign(
song: nil, song: nil,
playing: false, playing: false,
current_user_id: socket.assigns.current_user.id, profile: nil,
# todo use actual room user id current_user_id: current_user.id,
room_user_id: socket.assigns.current_user.id own_profile?: false
) )
|> switch_profile(current_user.active_profile_user_id || current_user.id)
{:ok, socket, layout: false, temporary_assigns: [current_user: nil]} {:ok, socket, layout: false, temporary_assigns: []}
end
defp switch_profile(socket, nil) do
current_user = Accounts.update_active_profile(socket.assigns.current_user, nil)
socket
|> assign(current_user: current_user)
|> assign_profile(nil)
end
defp switch_profile(socket, profile_user_id) do
profile = get_profile(profile_user_id)
if connected?(socket) and profile do
current_user = Accounts.update_active_profile(socket.assigns.current_user, profile.user_id)
send(self(), :play_current)
socket
|> assign(current_user: current_user)
|> assign_profile(profile)
else
assign_profile(socket, nil)
end
end
defp assign_profile(socket, profile) do
if prev_profile = connected?(socket) && socket.assigns.profile do
MediaLibrary.unsubscribe_to_profile(prev_profile)
end
if profile, do: MediaLibrary.subscribe_to_profile(profile)
assign(socket,
profile: profile,
own_profile?: profile && MediaLibrary.owns_profile?(socket.assigns.current_user, profile)
)
end
defp assign_inactive_profile(socket, %Song{} = song) do
if socket.assigns.profile && MediaLibrary.owns_song?(socket.assigns.profile, song) do
socket
else
switch_profile(socket, song.user_id)
end
end end
def handle_event("play_pause", _, socket) do def handle_event("play_pause", _, socket) do
%{song: song, playing: playing} = socket.assigns %{song: song, playing: playing, current_user: current_user} = socket.assigns
song = MediaLibrary.get_song!(song.id)
cond do cond do
song && playing -> song && playing and MediaLibrary.can_control_playback?(current_user, song) ->
MediaLibrary.pause_song(song) MediaLibrary.pause_song(song)
{:noreply, assign(socket, playing: false)} {:noreply, assign(socket, playing: false)}
song -> song && MediaLibrary.can_control_playback?(current_user, song) ->
MediaLibrary.play_song(song) MediaLibrary.play_song(song)
{:noreply, assign(socket, playing: true)} {:noreply, assign(socket, playing: true)}
true -> true ->
{:noreply, assign(socket, playing: false)} {:noreply, socket}
end end
end end
def handle_event("next-song", _, socket) do def handle_event("switch_profile", %{"user_id" => user_id}, socket) do
if socket.assigns.song do {:noreply, switch_profile(socket, user_id)}
MediaLibrary.play_next_song(socket.assigns.song.user_id) end
def handle_event("next_song", _, socket) do
%{song: song, current_user: current_user} = socket.assigns
if song && MediaLibrary.can_control_playback?(current_user, song) do
MediaLibrary.play_next_song(socket.assigns.profile)
end end
{:noreply, socket} {:noreply, socket}
end end
def handle_event("prev-song", _, socket) do def handle_event("prev_song", _, socket) do
if socket.assigns.song do %{song: song, current_user: current_user} = socket.assigns
MediaLibrary.play_prev_song(socket.assigns.song.user_id)
if song && MediaLibrary.can_control_playback?(current_user, song) do
MediaLibrary.play_prev_song(socket.assigns.profile)
end end
{:noreply, socket} {:noreply, socket}
end end
def handle_event("next-song-auto", _, socket) do def handle_event("next_song_auto", _, socket) do
if socket.assigns.song do if socket.assigns.song do
MediaLibrary.play_next_song_auto(socket.assigns.song.user_id) MediaLibrary.play_next_song_auto(socket.assigns.profile)
end end
{:noreply, socket} {:noreply, socket}
end end
def handle_info({Accounts, :active_profile_changed, _cur_user, %{user_id: user_id}}, socket) do
if user_id do
{:noreply, assign(socket, profile: get_profile(user_id))}
else
{:noreply, socket |> assign_profile(nil) |> stop_song()}
end
end
def handle_info(:play_current, socket) do def handle_info(:play_current, socket) do
# we raced a pubsub, noop {:noreply, play_current_song(socket)}
if socket.assigns.song do
{:noreply, socket}
else
{:noreply, play_current_song(socket)}
end
end end
def handle_info({:pause, _}, socket) do def handle_info({MediaLibrary, :pause, _}, socket) do
{:noreply, {:noreply, push_pause(socket)}
socket
|> push_event("pause", %{})
|> assign(playing: false)}
end end
def handle_info({:play, %Song{} = song, %{elapsed: elapsed}}, socket) do def handle_info({MediaLibrary, :play, %Song{} = song, %{elapsed: elapsed}}, socket) do
{:noreply, play_song(socket, song, elapsed)} {:noreply, play_song(socket, song, elapsed)}
end end
@ -170,18 +245,17 @@ defmodule LiveBeatsWeb.PlayerLive do
socket socket
|> push_play(song, elapsed) |> push_play(song, elapsed)
|> assign(song: song, playing: true) |> assign(song: song, playing: true)
|> assign_inactive_profile(song)
end end
defp js_play_pause(%JS{} = js) do defp stop_song(socket) do
JS.dispatch(js, "js:play_pause", to: "#audio-player") socket
end |> push_event("stop", %{})
|> assign(song: nil, playing: false)
defp js_listen_now(js \\ %JS{}) do
JS.dispatch(js, "js:listen_now", to: "#audio-player")
end end
defp play_current_song(socket) do defp play_current_song(socket) do
song = MediaLibrary.get_current_active_song(socket.assigns.room_user_id) song = MediaLibrary.get_current_active_song(socket.assigns.profile)
cond do cond do
song && MediaLibrary.playing?(song) -> song && MediaLibrary.playing?(song) ->
@ -197,6 +271,7 @@ defmodule LiveBeatsWeb.PlayerLive do
defp push_play(socket, %Song{} = song, elapsed) do defp push_play(socket, %Song{} = song, elapsed) do
token = Phoenix.Token.sign(socket.endpoint, "file", song.mp3_filename) token = Phoenix.Token.sign(socket.endpoint, "file", song.mp3_filename)
push_event(socket, "play", %{ push_event(socket, "play", %{
paused: Song.paused?(song), paused: Song.paused?(song),
elapsed: elapsed, elapsed: elapsed,
@ -204,4 +279,43 @@ defmodule LiveBeatsWeb.PlayerLive do
url: song.mp3_url url: song.mp3_url
}) })
end end
defp push_pause(socket) do
socket
|> push_event("pause", %{})
|> assign(playing: false)
end
defp js_play_pause(own_profile?) do
if own_profile? do
JS.push("play_pause")
|> JS.dispatch("js:play_pause", to: "#audio-player")
else
show_modal("not-authorized")
end
end
defp js_prev(own_profile?) do
if own_profile? do
JS.push("prev_song")
else
show_modal("not-authorized")
end
end
defp js_next(own_profile?) do
if own_profile? do
JS.push("next_song")
else
show_modal("not-authorized")
end
end
defp js_listen_now(js \\ %JS{}) do
JS.dispatch(js, "js:listen_now", to: "#audio-player")
end
defp get_profile(user_id) do
user_id && Accounts.get_user!(user_id) |> MediaLibrary.get_profile!()
end
end end

View file

@ -1,17 +1,34 @@
defmodule LiveBeatsWeb.SongLive.Index do defmodule LiveBeatsWeb.SongLive.Index do
use LiveBeatsWeb, :live_view use LiveBeatsWeb, :live_view
alias LiveBeats.{MediaLibrary, MP3Stat} alias LiveBeats.{Accounts, MediaLibrary, MP3Stat}
alias LiveBeatsWeb.LayoutComponent alias LiveBeatsWeb.LayoutComponent
alias LiveBeatsWeb.SongLive.{SongRowComponent, UploadFormComponent} alias LiveBeatsWeb.SongLive.{SongRowComponent, UploadFormComponent}
def render(assigns) do def render(assigns) do
~H""" ~H"""
<.title_bar> <.title_bar>
Listing Songs <%= @profile.tagline %> <%= if @owns_profile? do %>(you)<% end %>
<:actions> <:actions>
<.button primary patch_to={Routes.song_index_path(@socket, :new)}>Upload Songs</.button> <%= if @active_profile_id == @profile.user_id do %>
<.button primary
phx-click={JS.push("switch_profile", value: %{user_id: nil}, target: "#player", loading: "#player")}
>
<.icon name={:stop}/><span class="ml-2">Stop Listening</span>
</.button>
<% else %>
<.button primary
phx-click={JS.push("switch_profile", value: %{user_id: @profile.user_id}, target: "#player", loading: "#player")}
>
<.icon name={:play}/><span class="ml-2">Listen</span>
</.button>
<% end %>
<%= if @owns_profile? do %>
<.button primary patch_to={Routes.song_index_path(@socket, :new)}>
<.icon name={:upload}/><span class="ml-2">Upload Songs</span>
</.button>
<% end %>
</:actions> </:actions>
</.title_bar> </.title_bar>
@ -45,20 +62,35 @@ defmodule LiveBeatsWeb.SongLive.Index do
""" """
end end
def mount(_params, _session, socket) do def mount(%{"profile_username" => profile_username}, _session, socket) do
%{current_user: current_user} = socket.assigns %{current_user: current_user} = socket.assigns
profile =
Accounts.get_user_by!(username: profile_username)
|> MediaLibrary.get_profile!()
if connected?(socket) do if connected?(socket) do
MediaLibrary.subscribe(current_user) MediaLibrary.subscribe_to_profile(profile, __MODULE__)
Accounts.subscribe(current_user.id)
end end
active_id = active_song_id =
if song = MediaLibrary.get_current_active_song(current_user.id) do if song = MediaLibrary.get_current_active_song(profile) do
SongRowComponent.send_status(song.id, song.status) SongRowComponent.send_status(song.id, song.status)
song.id song.id
end end
{:ok, assign(socket, songs: list_songs(), active_id: active_id), temporary_assigns: [songs: []]} socket =
socket
|> assign(
active_song_id: active_song_id,
active_profile_id: current_user.active_profile_user_id,
profile: profile,
owns_profile?: MediaLibrary.owns_profile?(current_user, profile)
)
|> list_songs()
{:ok, socket, temporary_assigns: [songs: []]}
end end
def handle_params(params, _url, socket) do def handle_params(params, _url, socket) do
@ -67,10 +99,17 @@ defmodule LiveBeatsWeb.SongLive.Index do
def handle_event("play_or_pause", %{"id" => id}, socket) do def handle_event("play_or_pause", %{"id" => id}, socket) do
song = MediaLibrary.get_song!(id) song = MediaLibrary.get_song!(id)
if socket.assigns.active_id == id and MediaLibrary.playing?(song) do can_playback? = MediaLibrary.can_control_playback?(socket.assigns.current_user, song)
MediaLibrary.pause_song(song)
else cond do
MediaLibrary.play_song(id) can_playback? and socket.assigns.active_song_id == id and MediaLibrary.playing?(song) ->
MediaLibrary.pause_song(song)
can_playback? ->
MediaLibrary.play_song(id)
true ->
:noop
end end
{:noreply, socket} {:noreply, socket}
@ -78,23 +117,29 @@ defmodule LiveBeatsWeb.SongLive.Index do
def handle_event("delete", %{"id" => id}, socket) do def handle_event("delete", %{"id" => id}, socket) do
song = MediaLibrary.get_song!(id) song = MediaLibrary.get_song!(id)
{:ok, _} = MediaLibrary.delete_song(song) if song.user_id == socket.assigns.current_user.id do
{:ok, _} = MediaLibrary.delete_song(song)
end
{:noreply, socket} {:noreply, socket}
end end
def handle_info({:play, %MediaLibrary.Song{} = song, _meta}, socket) do def handle_info({Accounts, :active_profile_changed, _cur_user, %{user_id: user_id}}, socket) do
{:noreply, assign(socket, active_profile_id: user_id)}
end
def handle_info({MediaLibrary, :play, %MediaLibrary.Song{} = song, _meta}, socket) do
{:noreply, play_song(socket, song)} {:noreply, play_song(socket, song)}
end end
def handle_info({:pause, %MediaLibrary.Song{} = song}, socket) do def handle_info({MediaLibrary, :pause, %MediaLibrary.Song{} = song}, socket) do
{:noreply, pause_song(socket, song.id)} {:noreply, pause_song(socket, song.id)}
end end
defp stop_song(socket, song_id) do defp stop_song(socket, song_id) do
SongRowComponent.send_status(song_id, :stopped) SongRowComponent.send_status(song_id, :stopped)
if socket.assigns.active_id == song_id do if socket.assigns.active_song_id == song_id do
assign(socket, :active_id, nil) assign(socket, :active_song_id, nil)
else else
socket socket
end end
@ -106,23 +151,23 @@ defmodule LiveBeatsWeb.SongLive.Index do
end end
defp play_song(socket, %MediaLibrary.Song{} = song) do defp play_song(socket, %MediaLibrary.Song{} = song) do
%{active_id: active_id} = socket.assigns %{active_song_id: active_song_id} = socket.assigns
cond do cond do
active_id == song.id -> active_song_id == song.id ->
SongRowComponent.send_status(song.id, :playing) SongRowComponent.send_status(song.id, :playing)
socket socket
active_id -> active_song_id ->
SongRowComponent.send_status(song.id, :playing) SongRowComponent.send_status(song.id, :playing)
socket socket
|> stop_song(active_id) |> stop_song(active_song_id)
|> assign(active_id: song.id) |> assign(active_song_id: song.id)
true -> true ->
SongRowComponent.send_status(song.id, :playing) SongRowComponent.send_status(song.id, :playing)
assign(socket, active_id: song.id) assign(socket, active_song_id: song.id)
end end
end end
@ -155,7 +200,7 @@ defmodule LiveBeatsWeb.SongLive.Index do
|> assign(:song, nil) |> assign(:song, nil)
end end
defp list_songs do defp list_songs(socket) do
MediaLibrary.list_songs(50) assign(socket, songs: MediaLibrary.list_profile_songs(socket.assigns.profile, 50))
end end
end end

View file

@ -56,7 +56,7 @@ defmodule LiveBeatsWeb.Router do
live_session :authenticated, live_session :authenticated,
on_mount: [{LiveBeatsWeb.UserAuth, :ensure_authenticated}, LiveBeatsWeb.Nav] do on_mount: [{LiveBeatsWeb.UserAuth, :ensure_authenticated}, LiveBeatsWeb.Nav] do
live "/songs/new", SongLive.Index, :new live "/songs/new", SongLive.Index, :new
live "/:user_id", SongLive.Index, :index live "/:profile_username", SongLive.Index, :index
live "/profile/settings", SettingsLive, :edit live "/profile/settings", SettingsLive, :edit
end end
end end

View file

@ -11,6 +11,7 @@ defmodule LiveBeats.Repo.Migrations.CreateUserAuth do
add :role, :string, null: false add :role, :string, null: false
add :confirmed_at, :naive_datetime add :confirmed_at, :naive_datetime
add :profile_tagline, :string add :profile_tagline, :string
add :active_profile_user_id, references(:users, on_delete: :nilify_all)
timestamps() timestamps()
end end