mirror of
https://github.com/fly-apps/live_beats.git
synced 2024-11-25 01:10:59 +00:00
Synced playing fixup
This commit is contained in:
parent
3ff4ae2bea
commit
287f78ab2a
13 changed files with 322 additions and 167 deletions
|
@ -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(){
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -87,7 +87,7 @@ defmodule LiveBeatsWeb.PlayerLive do
|
|||
</div>
|
||||
|
||||
<%= 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</:title>
|
||||
Your browser needs a click event to enable playback
|
||||
<:confirm>Listen Now</:confirm>
|
||||
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
defmodule LiveBeatsWeb.SongLive.SongEntryComponent do
|
||||
defmodule LiveBeatsWeb.SongLive.SongEntry do
|
||||
use LiveBeatsWeb, :live_component
|
||||
|
||||
alias LiveBeats.MP3Stat
|
|
@ -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"""
|
||||
<tr id={@id} class={@class}}>
|
||||
<%= for {col, i} <- Enum.with_index(@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 cursor-pointer"}"}
|
||||
phx-click={JS.push("play-song", 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">
|
||||
<%= if i == 0 do %>
|
||||
<%= if @active do %>
|
||||
<%= if @status == :playing do %>
|
||||
<span class="flex pt-1 relative mr-2 w-4">
|
||||
<span class="w-3 h-3 animate-ping bg-purple-400 rounded-full absolute"></span>
|
||||
<.icon name={:volume_up} class="h-5 w-5 -mt-1 -ml-1"/>
|
||||
</span>
|
||||
<% else %>
|
||||
<% end %>
|
||||
<%= if @status == :paused do %>
|
||||
<span class="flex pt-1 relative mr-2 w-4">
|
||||
<.icon name={:volume_up} class="h-5 w-5 -mt-1 -ml-1 text-gray-400"/>
|
||||
</span>
|
||||
<% end %>
|
||||
<%= if @status == :stopped do %>
|
||||
<span class="flex relative w-6 -translate-x-1">
|
||||
<.icon name={:play} class="h-5 w-5 text-gray-400"/>
|
||||
</span>
|
||||
|
@ -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
|
|
@ -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|
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
<h2><%= @title %></h2>
|
||||
|
||||
<.form
|
||||
let={f}
|
||||
for={:songs}
|
||||
id="song-form"
|
||||
class="space-y-8"
|
||||
|
@ -13,7 +12,7 @@
|
|||
<div class="space-y-8 divide-y divide-gray-200 sm:space-y-5">
|
||||
<div class="space-y-2 sm:space-y-2">
|
||||
<%= for {ref, changeset} <- @changesets do %>
|
||||
<.live_component id={ref} module={SongEntryComponent} changeset={changeset} />
|
||||
<.live_component id={ref} module={SongEntry} changeset={changeset} />
|
||||
<% end %>
|
||||
|
||||
<!-- upload -->
|
||||
|
|
|
@ -307,7 +307,9 @@
|
|||
</div>
|
||||
|
||||
<main class="flex-1 relative z-0 overflow-y-auto focus:outline-none">
|
||||
<%= live_render(@conn, LiveBeatsWeb.PlayerLive, session: %{}) %>
|
||||
<%= if @current_user do %>
|
||||
<%= live_render(@conn, LiveBeatsWeb.PlayerLive, session: %{}) %>
|
||||
<% end %>
|
||||
<%= @inner_content %>
|
||||
</main>
|
||||
</div>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue