Updates for LiveView 1.0-rc

This commit is contained in:
Chris McCord 2024-04-08 20:53:14 -04:00
parent 57f5c0f142
commit 056071ef6e
38 changed files with 562 additions and 440 deletions

View file

@ -70,7 +70,7 @@ RUN mix release
# the compiled release and other runtime necessities
FROM ${RUNNER_IMAGE}
RUN apt-get update -y && apt-get install -y libstdc++6 openssl libncurses5 locales curl ffmpeg \
RUN apt-get update -y && apt-get install -y libstdc++6 openssl libncurses5 locales curl ffmpeg s3fs \
&& apt-get clean && rm -f /var/lib/apt/lists/*_*
# Set the locale
@ -86,9 +86,10 @@ ENV BUMBLEBEE_CACHE_DIR="/app/.bumblebee"
# Only copy the final release from the build stage
COPY --from=builder --chown=nobody:root /app/_build/prod/rel/live_beats ./
# COPY --from=builder --chown=nobody:root /app/.postgresql/ ./.postgresql
COPY --from=builder --chown=nobody:root /app/.bumblebee/ ./.bumblebee
USER nobody
USER root
# Set the runtime ENV
ENV ECTO_IPV6="true"

View file

@ -82,16 +82,25 @@
display: none;
}
tbody.phx-click-loading { animation: none; }
.phx-click-loading {
opacity: 0.5;
transition: opacity 1s ease-out;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% {
opacity: 0.8;
}
50% {
opacity: 0.5;
}
}
.phx-loading{
cursor: wait;
}
.phx-modal {
opacity: 1!important;
position: fixed;

View file

@ -3,6 +3,7 @@ import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
import topbar from "../vendor/topbar"
import Sortable from "../vendor/sortable"
import phxFeedbackDom from "./phx_feedback_dom"
let nowSeconds = () => Math.round(Date.now() / 1000)
let rand = (min, max) => Math.floor(Math.random() * (max - min) + min)
@ -199,7 +200,7 @@ Hooks.Ping = {
this.handleEvent("pong", () => {
let rtt = Date.now() - this.nowMs
this.el.innerText = `ping: ${rtt}ms`
// this.timer = setTimeout(() => this.ping(rtt), 1000)
this.timer = setTimeout(() => this.ping(rtt), 5000)
})
this.ping(null)
},
@ -277,15 +278,16 @@ let Focus = {
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {
timeout: 20000,
hooks: Hooks,
params: {_csrf_token: csrfToken},
dom: {
dom: phxFeedbackDom({
onNodeAdded(node){
if(node instanceof HTMLElement && node.autofocus){
node.focus()
}
}
}
})
})
let routeUpdated = () => {
@ -300,7 +302,8 @@ window.addEventListener("phx:page-loading-stop", info => topbar.hide())
// Accessible routing
window.addEventListener("phx:page-loading-stop", routeUpdated)
window.addEventListener("js:exec", e => e.target[e.detail.call](...e.detail.args))
window.addEventListener("phx:js:exec", e => liveSocket.execJS(liveSocket.main.el, e.detail.cmd))
window.addEventListener("js:call", e => e.target[e.detail.call](...e.detail.args))
window.addEventListener("js:focus", e => {
let parent = document.querySelector(e.detail.parent)
if(parent && isVisible(parent)){ e.target.focus() }

View file

@ -0,0 +1,48 @@
// maintain backwards compatibility of phx-feedback-for, which was removed in LiveView 1.0
// find all phx-feedbck-for containers and show/hide phx-no-feedback class based on used inputs
import {isUsedInput} from "phoenix_live_view"
let resetFeedbacks = (container, feedbacks) => {
feedbacks = feedbacks || Array.from(container.querySelectorAll("[phx-feedback-for]"))
.map(el => [el, el.getAttribute("phx-feedback-for")])
feedbacks.forEach(([feedbackEl, name]) => {
let query = `[name="${name}"], [name="${name}[]"]`
let isUsed = Array.from(container.querySelectorAll(query)).find(input => isUsedInput(input))
if(isUsed || !feedbackEl.hasAttribute("phx-feedback-for")){
feedbackEl.classList.remove("phx-no-feedback")
} else {
feedbackEl.classList.add("phx-no-feedback")
}
})
}
export default phxFeedbackDom = (dom) => {
window.addEventListener("reset", e => resetFeedbacks(document))
let feedbacks
// extend provided dom options with our own.
// accumulate phx-feedback-for containers for each patch and reset feedbacks when patch ends
return {
onPatchStart(container){
feedbacks = []
dom.onPatchStart && dom.onPatchStart(container)
},
onNodeAdded(node){
if(node.hasAttribute && node.hasAttribute("phx-feedback-for")){
feedbacks.push([node, node.getAttribute("phx-feedback-for")])
}
dom.onNodeAdded && dom.onNodeAdded(node)
},
onBeforeElUpdated(from, to){
let fromFor = from.getAttribute("phx-feedback-for")
let toFor = to.getAttribute("phx-feedback-for")
if(fromFor || toFor){ feedbacks.push([from, fromFor || toFor], [to, toFor || fromFor]) }
dom.onBeforeElUpdated && dom.onBeforeElUpdated(from, to)
},
onPatchEnd(container){
resetFeedbacks(container, feedbacks)
dom.onPatchEnd && dom.onPatchEnd(container)
}
}
}

View file

@ -1,16 +1,29 @@
// See the Tailwind configuration guide for advanced usage
// https://tailwindcss.com/docs/configuration
const plugin = require("tailwindcss/plugin")
module.exports = {
content: [
'./js/**/*.js',
'../lib/*_web.ex',
'../lib/*_web/**/*.*ex'
"./js/**/*.js",
"../lib/*_web.ex",
"../lib/*_web/**/*.*ex"
],
theme: {
extend: {},
},
plugins: [
require('@tailwindcss/forms')
require('@tailwindcss/forms'),
// Allows prefixing tailwind classes with LiveView classes to add rules
// only when LiveView classes are applied, for example:
//
// <div class="phx-click-loading:animate-ping">
//
plugin(({addVariant}) => addVariant("phx-no-feedback", [".phx-no-feedback&", ".phx-no-feedback &"])),
plugin(({addVariant}) => addVariant("phx-click-loading", [".phx-click-loading&"])),
plugin(({addVariant}) => addVariant("phxp-click-loading", [".phx-click-loading &"])),
plugin(({addVariant}) => addVariant("phx-submit-loading", [".phx-submit-loading &"])),
plugin(({addVariant}) => addVariant("phx-submitter-loading", [".phx-submit-loading&"])),
plugin(({addVariant}) => addVariant("phx-change-loading", [".phx-change-loading&", ".phx-change-loading &"])),
plugin(({addVariant}) => addVariant("phx-error", [".phx-error&", ".phx-error &"]))
]
}

View file

@ -8,7 +8,6 @@
import Config
config :live_beats,
replica: LiveBeats.ReplicaRepo,
ecto_repos: [LiveBeats.Repo]
config :live_beats, :files, admin_usernames: []
@ -27,7 +26,7 @@ config :live_beats, LiveBeatsWeb.Endpoint,
config :esbuild,
version: "0.12.18",
default: [
args: ~w(js/app.js --bundle --target=es2016 --outdir=../priv/static/assets),
args: ~w(js/app.js --bundle --outdir=../priv/static/assets),
cd: Path.expand("../assets", __DIR__),
env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
]

View file

@ -2,7 +2,7 @@ import Config
config :live_beats, :files,
uploads_dir: Path.expand("../priv/uploads", __DIR__),
host: [scheme: "http", host: "localhost", port: 4001],
host: [scheme: "http", host: "localhost", port: 4000],
server_ip: "127.0.0.1",
hostname: "localhost",
transport_opts: []
@ -13,23 +13,16 @@ config :live_beats, :github,
# Configure your database
config :live_beats, LiveBeats.Repo,
username: "postgres",
password: "postgres",
database: "live_beats_dev",
username: "root",
password: nil,
hostname: "localhost",
port: 26257,
database: "live_beats_dev",
migration_lock: false,
stacktrace: true,
show_sensitive_data_on_connection_error: true,
pool_size: 10
# Configure your replica database
config :live_beats, LiveBeats.ReplicaRepo,
username: "postgres",
password: "postgres",
database: "live_beats_dev",
hostname: "localhost",
show_sensitive_data_on_connection_error: true,
pool_size: 10,
priv: "priv/repo"
# For development, we disable any cache and enable
# debugging and code reloading.
#

View file

@ -12,6 +12,22 @@ if System.get_env("PHX_SERVER") && System.get_env("RELEASE_NAME") do
end
if config_env() == :prod do
config :flame, :terminator, log: :info
config :flame, :backend, FLAME.FlyBackend
config :flame, FLAME.FlyBackend, cpu_kind: "performance", cpus: 4, memory_mb: 8192
config :flame, FLAME.FlyBackend,
token: System.get_env("FLY_API_TOKEN"),
memory_mb: 8192,
env: %{
"BUCKET_MOUNT" => System.get_env("BUCKET_MOUNT"),
"DATABASE_URL" => System.get_env("DATABASE_URL"),
"LIVE_BEATS_GITHUB_CLIENT_ID" => System.fetch_env!("LIVE_BEATS_GITHUB_CLIENT_ID"),
"LIVE_BEATS_GITHUB_CLIENT_SECRET" => System.get_env("LIVE_BEATS_GITHUB_CLIENT_SECRET")
}
config :live_beats, dns_cluster_query: System.get_env("DNS_CLUSTER_QUERY")
database_url =
System.get_env("DATABASE_URL") ||
raise """
@ -19,27 +35,20 @@ if config_env() == :prod do
For example: ecto://USER:PASS@HOST/DATABASE
"""
replica_database_url = System.get_env("REPLICA_DATABASE_URL") || database_url
maybe_ipv6 = if System.get_env("ECTO_IPV6") in ~w(true 1), do: [:inet6], else: []
host = System.get_env("PHX_HOST") || "example.com"
ecto_ipv6? = System.get_env("ECTO_IPV6") == "true"
app_name =
System.get_env("FLY_APP_NAME") ||
raise "FLY_APP_NAME not available"
config :live_beats, LiveBeats.Repo,
# ssl: true,
socket_options: if(ecto_ipv6?, do: [:inet6], else: []),
migration_lock: false,
timeout: 60_000,
url: database_url,
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")
config :live_beats, LiveBeats.ReplicaRepo,
# ssl: true,
priv: "priv/repo",
socket_options: if(ecto_ipv6?, do: [:inet6], else: []),
url: replica_database_url,
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")
queue_interval: 60_000,
socket_options: maybe_ipv6,
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "5")
secret_key_base =
System.get_env("SECRET_KEY_BASE") ||
@ -58,13 +67,14 @@ if config_env() == :prod do
ip: {0, 0, 0, 0, 0, 0, 0, 0},
port: String.to_integer(System.get_env("PORT") || "4000")
],
secret_key_base: secret_key_base
secret_key_base: secret_key_base,
check_origin: false
config :live_beats, :files,
admin_usernames: ~w(chrismccord mrkurt),
uploads_dir: "/app/uploads",
uploads_dir: Path.join(System.fetch_env!("BUCKET_MOUNT"), "/app/uploads"),
host: [scheme: "https", host: host, port: 443],
server_ip: System.fetch_env!("LIVE_BEATS_SERVER_IP"),
server_ip: System.get_env("LIVE_BEATS_SERVER_IP"),
hostname: "livebeats.local",
transport_opts: [inet6: true]

View file

@ -1,8 +1,5 @@
import Config
config :live_beats,
replica: LiveBeats.Repo
config :live_beats, :files,
uploads_dir: Path.expand("../tmp/test-uploads", __DIR__),
host: [scheme: "http", host: "localhost", port: 4000],
@ -21,15 +18,6 @@ config :live_beats, LiveBeats.Repo,
pool: Ecto.Adapters.SQL.Sandbox,
pool_size: 10
config :live_beats, LiveBeats.ReplicaRepo,
username: "postgres",
password: "postgres",
database: "live_beats_test#{System.get_env("MIX_TEST_PARTITION")}",
hostname: "localhost",
show_sensitive_data_on_connection_error: true,
pool_size: 10,
priv: "priv/repo"
# We don't run a server during test. If one is required,
# you can enable the server option below.
config :live_beats, LiveBeatsWeb.Endpoint,

View file

@ -4,7 +4,7 @@
#
app = "livebeats"
primary_region = "ord"
primary_region = "iad"
kill_signal = "SIGTERM"
kill_timeout = "5s"
@ -15,13 +15,10 @@ kill_timeout = "5s"
release_command = "/app/bin/migrate"
[env]
BUCKET_MOUNT = "/tigris"
BUMBLEBEE_CACHE_DIR = "/app/.bumblebee"
PHX_HOST = "livebeats.fly.dev"
[mounts]
source="data"
destination="/app/uploads"
[[services]]
protocol = "tcp"
internal_port = 4000

View file

@ -18,15 +18,15 @@ defmodule LiveBeats.Accounts do
defp topic(user_id), do: "user:#{user_id}"
def list_users(opts) do
Repo.replica().all(from u in User, limit: ^Keyword.fetch!(opts, :limit))
Repo.all(from u in User, limit: ^Keyword.fetch!(opts, :limit))
end
def get_users_map(user_ids) when is_list(user_ids) do
Repo.replica().all(from u in User, where: u.id in ^user_ids, select: {u.id, u})
Repo.all(from u in User, where: u.id in ^user_ids, select: {u.id, u})
end
def lists_users_by_active_profile(id, opts) do
Repo.replica().all(
Repo.all(
from u in User, where: u.active_profile_user_id == ^id, limit: ^Keyword.fetch!(opts, :limit)
)
end
@ -84,11 +84,16 @@ defmodule LiveBeats.Accounts do
** (Ecto.NoResultsError)
"""
def get_user!(id), do: Repo.replica().get!(User, id)
def get_user!(id) do
get_user(id) ||
raise Ecto.NoResultsError, queryable: from(u in User, where: u.id == ^id, limit: 1)
end
def get_user(id), do: Repo.replica().get(User, id)
def get_user(id) do
Repo.one(from(u in User, where: u.id == ^id, limit: 1))
end
def get_user_by!(fields), do: Repo.replica().get_by!(User, fields)
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

View file

@ -65,6 +65,7 @@ defmodule LiveBeats.Accounts.User do
|> validate_required([:email])
|> validate_format(:email, ~r/^[^\s]+@[^\s]+$/, message: "must have the @ sign and no spaces")
|> validate_length(:email, max: 160)
|> update_change(:email, &String.downcase/1)
|> unsafe_validate_unique(:email, LiveBeats.Repo)
|> unique_constraint(:email)
end

View file

@ -19,32 +19,39 @@ defmodule LiveBeats.Application do
@impl true
def start(_type, _args) do
parent = FLAME.Parent.get()
LiveBeats.MediaLibrary.attach()
topologies = Application.get_env(:libcluster, :topologies) || []
children =
[
{Nx.Serving, name: WhisperServing, serving: load_serving()},
{Cluster.Supervisor, [topologies, [name: LiveBeats.ClusterSupervisor]]},
{Nx.Serving, name: LiveBeats.WhisperServing, serving: load_serving()},
!parent && {DNSCluster, query: Application.get_env(:wps, :dns_cluster_query) || :ignore},
{Task.Supervisor, name: LiveBeats.TaskSupervisor},
{Task.Supervisor, name: Fly.Machine.TaskSupervisor},
# Start the Ecto repository
LiveBeats.Repo,
LiveBeats.ReplicaRepo,
# Start the Telemetry supervisor
LiveBeatsWeb.Telemetry,
# Start the PubSub system
{Phoenix.PubSub, name: LiveBeats.PubSub},
# start presence
LiveBeatsWeb.Presence,
!parent && LiveBeatsWeb.Presence,
{Finch, name: LiveBeats.Finch},
{FLAME.Pool,
name: LiveBeats.WhisperRunner,
min: 0,
max: 5,
max_concurrency: 10,
min_idle_shutdown_after: :timer.seconds(30),
idle_shutdown_after: :timer.seconds(30),
log: :info},
# Start the Endpoint (http/https)
LiveBeatsWeb.Endpoint,
!parent && LiveBeatsWeb.Endpoint,
# Expire songs every six hours
{LiveBeats.SongsCleaner, interval: {3600 * 6, :second}}
!parent && {LiveBeats.SongsCleaner, interval: {3600 * 6, :second}}
# Start a worker by calling: LiveBeats.Worker.start_link(arg)
# {LiveBeats.Worker, arg}
]
|> Enum.filter(& &1)
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options

View file

@ -7,7 +7,7 @@ defmodule LiveBeats.Audio do
fn ss ->
args = ~w(-ac 1 -ar 16k -f f32le -ss #{ss} -t #{chunk_time} -v quiet -)
{data, 0} = System.cmd("ffmpeg", ["-i", path] ++ args)
{ss, Nx.Serving.batched_run(WhisperServing, Nx.from_binary(data, :f32))}
{ss, Nx.Serving.batched_run({:local, LiveBeats.WhisperServing}, Nx.from_binary(data, :f32))}
end,
max_concurrency: 2,
timeout: :infinity

View file

@ -145,7 +145,7 @@ defmodule LiveBeats.MediaLibrary do
def store_mp3(%Song{} = song, tmp_path) do
File.mkdir_p!(Path.dirname(song.mp3_filepath))
File.cp!(tmp_path, song.mp3_filepath)
{:ok, _} = File.copy(tmp_path, song.mp3_filepath)
end
def put_stats(%Ecto.Changeset{} = changeset, %MP3Stat{} = stat) do
@ -160,9 +160,6 @@ defmodule LiveBeats.MediaLibrary do
def import_songs(%Accounts.User{} = user, changesets, consume_file)
when is_map(changesets) and is_function(consume_file, 2) do
# refetch user for fresh song count
user = Accounts.get_user!(user.id)
multi =
Ecto.Multi.new()
|> lock_playlist(user.id)
@ -179,19 +176,24 @@ defmodule LiveBeats.MediaLibrary do
chset
|> Song.put_user(user)
|> Song.put_mp3_path()
|> Song.put_server_ip()
|> Ecto.Changeset.put_change(:position, pos_start + i + 1)
end)
end)
|> Ecto.Multi.run(:valid_songs_count, fn _repo, changes ->
|> Ecto.Multi.run(:valid_songs_count, fn repo, changes ->
new_songs_count = changes |> Enum.filter(&match?({{:song, _ref}, _}, &1)) |> Enum.count()
validate_songs_limit(user.songs_count, new_songs_count)
songs_count =
repo.one(
from(u in Accounts.User, where: u.id == ^user.id, select: u.songs_count, limit: 1)
)
validate_songs_limit(songs_count, new_songs_count)
end)
|> Ecto.Multi.update_all(
:update_songs_count,
fn %{valid_songs_count: new_count} ->
from(u in Accounts.User,
where: u.id == ^user.id and u.songs_count == ^user.songs_count,
where: u.id == ^user.id,
update: [inc: [songs_count: ^new_count]]
)
end,
@ -233,17 +235,18 @@ defmodule LiveBeats.MediaLibrary do
end
defp async_transcribe(%Song{} = song, %Accounts.User{} = user) do
Task.Supervisor.start_child(LiveBeats.TaskSupervisor, fn ->
# Task.Supervisor.start_child(LiveBeats.TaskSupervisor, fn ->
FLAME.cast(LiveBeats.WhisperRunner, fn ->
segments =
LiveBeats.Audio.speech_to_text(song.mp3_filepath, 20, fn ss, text ->
segment = %Song.TranscriptSegment{ss: ss, text: text}
segment = %Song.Transcript.Segment{ss: ss, text: text}
broadcast!(user.id, {segment, song.id})
segment
end)
Repo.update_all(from(s in Song, where: s.id == ^song.id),
set: [transcript_segments: segments]
set: [transcript: %Song.Transcript{segments: segments}]
)
end)
end
@ -267,7 +270,7 @@ defmodule LiveBeats.MediaLibrary do
end
def list_genres do
Repo.replica().all(Genre, order_by: [asc: :title])
Repo.all(Genre, order_by: [asc: :title])
end
def list_profile_songs(%Profile{} = profile, limit \\ 100) do
@ -276,7 +279,7 @@ defmodule LiveBeats.MediaLibrary do
limit: ^limit
)
|> order_by_playlist(:asc)
|> Repo.replica().all()
|> Repo.all()
end
def list_active_profiles(opts) do
@ -288,13 +291,13 @@ defmodule LiveBeats.MediaLibrary do
order_by: [desc: s.updated_at],
select: struct(u, [:id, :username, :profile_tagline, :avatar_url, :external_homepage_url])
)
|> Repo.replica().all()
|> Repo.all()
|> Enum.map(&get_profile!/1)
end
def get_current_active_song(%Profile{user_id: user_id}) do
Repo.replica().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], limit: 1)
)
end
@ -330,7 +333,7 @@ defmodule LiveBeats.MediaLibrary do
end
end
def get_song!(id), do: Repo.replica().get!(Song, id)
def get_song!(id), do: Repo.get!(Song, id)
def get_first_song(%Profile{user_id: user_id}) do
from(s in Song,
@ -338,7 +341,7 @@ defmodule LiveBeats.MediaLibrary do
limit: 1
)
|> order_by_playlist(:asc)
|> Repo.replica().one()
|> Repo.one()
end
def get_last_song(%Profile{user_id: user_id}) do
@ -347,7 +350,7 @@ defmodule LiveBeats.MediaLibrary do
limit: 1
)
|> order_by_playlist(:desc)
|> Repo.replica().one()
|> Repo.one()
end
def get_next_song(%Song{} = song, %Profile{} = profile) do
@ -357,7 +360,7 @@ defmodule LiveBeats.MediaLibrary do
limit: 1
)
|> order_by_playlist(:asc)
|> Repo.replica().one()
|> Repo.one()
next || get_first_song(profile)
end
@ -369,7 +372,7 @@ defmodule LiveBeats.MediaLibrary do
limit: 1
)
|> order_by_playlist(:desc)
|> Repo.replica().one()
|> Repo.one()
prev || get_last_song(profile)
end
@ -379,7 +382,7 @@ defmodule LiveBeats.MediaLibrary do
multi =
Ecto.Multi.new()
|> lock_playlist(song.user_id)
# |> lock_playlist(song.user_id)
|> Ecto.Multi.run(:index, fn repo, _changes ->
case repo.one(from(s in Song, where: s.user_id == ^song.user_id, select: count(s.id))) do
count when new_index < count -> {:ok, new_index}
@ -409,7 +412,10 @@ defmodule LiveBeats.MediaLibrary do
case LiveBeats.Repo.transaction(multi) do
{:ok, _} ->
broadcast!(song.user_id, %Events.NewPosition{song: %Song{song | position: new_index}})
broadcast_from!(self(), song.user_id, %Events.NewPosition{
song: %Song{song | position: new_index}
})
:ok
{:error, failed_op, _failed_val, _changes} ->
@ -428,7 +434,7 @@ defmodule LiveBeats.MediaLibrary do
old_index = song.position
Ecto.Multi.new()
|> lock_playlist(song.user_id)
# |> lock_playlist(song.user_id)
|> Ecto.Multi.delete(:delete, song)
|> multi_update_all(:dec_positions, fn _ ->
from(s in Song,
@ -451,7 +457,7 @@ defmodule LiveBeats.MediaLibrary do
def expire_songs_older_than(count, interval) when interval in [:month, :day, :second] do
admin_usernames = LiveBeats.config([:files, :admin_usernames])
server_ip = LiveBeats.config([:files, :server_ip])
# server_ip = LiveBeats.config([:files, :server_ip])
Ecto.Multi.new()
|> Ecto.Multi.delete_all(
@ -459,7 +465,7 @@ defmodule LiveBeats.MediaLibrary do
from(s in Song,
join: u in assoc(s, :user),
where: s.inserted_at < from_now(^(-count), ^to_string(interval)),
where: s.server_ip == ^server_ip,
# where: s.server_ip == ^server_ip,
where: u.username not in ^admin_usernames,
select: %{user_id: s.user_id, mp3_filepath: s.mp3_filepath}
)
@ -543,6 +549,10 @@ defmodule LiveBeats.MediaLibrary do
Phoenix.PubSub.broadcast!(@pubsub, topic(user_id), {__MODULE__, msg})
end
defp broadcast_from!(pid, user_id, msg) when is_integer(user_id) do
Phoenix.PubSub.broadcast_from!(@pubsub, pid, topic(user_id), {__MODULE__, msg})
end
defp topic(user_id) when is_integer(user_id), do: "profile:#{user_id}"
defp validate_songs_limit(user_songs, new_songs_count) do
@ -558,6 +568,9 @@ defmodule LiveBeats.MediaLibrary do
end
defp lock_playlist(%Ecto.Multi{} = multi, user_id) do
Repo.multi_transaction_lock(multi, :playlist, user_id)
Ecto.Multi.run(multi, :playlist_lock, fn repo, _changes ->
repo.all(from(u in "users", where: u.id == ^user_id, select: u.id, lock: "FOR UPDATE"))
{:ok, user_id}
end)
end
end

View file

@ -20,14 +20,16 @@ defmodule LiveBeats.MediaLibrary.Song do
field :mp3_filepath, :string
field :mp3_filename, :string
field :mp3_filesize, :integer, default: 0
field :server_ip, EctoNetwork.INET
field :server_ip, :string
field :position, :integer, default: 0
belongs_to :user, Accounts.User
belongs_to :genre, LiveBeats.MediaLibrary.Genre
embeds_many :transcript_segments, TranscriptSegment do
field :ss, :integer
field :text, :string
embeds_one :transcript, Transcript do
embeds_many :segments, Segment do
field :ss, :integer
field :text, :string
end
end
timestamps()

View file

@ -23,6 +23,7 @@ defmodule LiveBeats.Release do
end
defp load_app do
Application.ensure_all_started(:ssl)
Application.load(@app)
end
end

View file

@ -3,8 +3,6 @@ defmodule LiveBeats.Repo do
otp_app: :live_beats,
adapter: Ecto.Adapters.Postgres
def replica, do: LiveBeats.config([:replica])
@locks %{playlist: 1}
def multi_transaction_lock(multi, scope, id) when is_atom(scope) and is_integer(id) do
@ -15,9 +13,3 @@ defmodule LiveBeats.Repo do
end)
end
end
defmodule LiveBeats.ReplicaRepo do
use Ecto.Repo,
otp_app: :live_beats,
adapter: Ecto.Adapters.Postgres
end

View file

@ -14,8 +14,17 @@ defmodule LiveBeats.SongsCleaner do
@impl true
def init(opts) do
{count, interval} = Keyword.fetch!(opts, :interval)
{:ok, schedule_cleanup(%{count: count, interval: interval}, 0)}
region = System.get_env("FLY_REGION")
primary_region = System.get_env("PRIMARY_REGION")
case region do
region when region in [nil, primary_region] ->
{count, interval} = Keyword.fetch!(opts, :interval)
{:ok, schedule_cleanup(%{count: count, interval: interval}, 0)}
_ ->
:ignore
end
end
@impl true

View file

@ -41,6 +41,8 @@ defmodule LiveBeatsWeb do
import Phoenix.Controller,
only: [get_csrf_token: 0, view_module: 1, view_template: 1]
use Phoenix.Component
# Include general helpers for rendering HTML
unquote(html_helpers())
end
@ -100,9 +102,6 @@ defmodule LiveBeatsWeb do
# Use all HTML functionality (forms, tags, etc)
use Phoenix.HTML
# Import LiveView and .heex helpers (live_render, live_patch, <.form>, etc)
use Phoenix.Component
import LiveBeatsWeb.CoreComponents
import LiveBeatsWeb.Gettext
alias LiveBeatsWeb.Router.Helpers, as: Routes

View file

@ -166,7 +166,7 @@ defmodule LiveBeatsWeb.Presence.BadgeComponent do
<%= if @region do %>
<img
class="inline w-7 h-7 absolute right-3 top-3"
src={"https://fly.io/ui/images/#{@region}.svg"}
src={"https://fly.io/phx/ui/images/#{@region}.svg"}
title={region_name(@region)}
/>
<% end %>

View file

@ -33,6 +33,7 @@ defmodule LiveBeatsWeb.CoreComponents do
<div
id="connection-status"
class="hidden rounded-md bg-red-50 p-4 fixed top-1 right-1 w-96 fade-in-scale z-50"
phx-disconnected={JS.show()}
js-show={show("#connection-status")}
js-hide={hide("#connection-status")}
>
@ -105,7 +106,6 @@ defmodule LiveBeatsWeb.CoreComponents do
class="rounded-md bg-green-50 p-4 fixed top-1 right-1 w-96 fade-in-scale z-50"
phx-click={JS.push("lv:clear-flash") |> JS.remove_class("fade-in-scale") |> hide("#flash")}
phx-value-key="info"
phx-hook="Flash"
>
<div class="flex justify-between items-center space-x-3 text-green-700">
<.icon name={:check_circle} class="w-5 h-5" />
@ -278,7 +278,7 @@ defmodule LiveBeatsWeb.CoreComponents do
{"transition ease-in-out duration-300 transform", "-translate-x-full", "translate-x-0"}
)
|> JS.hide(to: "#show-mobile-sidebar", transition: "fade-out")
|> JS.dispatch("js:exec", to: "#hide-mobile-sidebar", detail: %{call: "focus", args: []})
|> JS.dispatch("js:call", to: "#hide-mobile-sidebar", detail: %{call: "focus", args: []})
end
def hide_mobile_sidebar(js \\ %JS{}) do
@ -291,7 +291,7 @@ defmodule LiveBeatsWeb.CoreComponents do
{"transition ease-in-out duration-300 transform", "translate-x-0", "-translate-x-full"}
)
|> JS.show(to: "#show-mobile-sidebar", transition: "fade-in")
|> JS.dispatch("js:exec", to: "#show-mobile-sidebar", detail: %{call: "focus", args: []})
|> JS.dispatch("js:call", to: "#show-mobile-sidebar", detail: %{call: "focus", args: []})
end
def show(js \\ %JS{}, selector) do
@ -348,6 +348,7 @@ defmodule LiveBeatsWeb.CoreComponents do
def show_modal(js \\ %JS{}, id) when is_binary(id) do
js
|> JS.remove_attribute("disabled", to: "##{id}-confirm")
|> JS.show(
to: "##{id}",
display: "inline-block",
@ -360,11 +361,12 @@ defmodule LiveBeatsWeb.CoreComponents do
{"ease-out duration-300", "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95",
"opacity-100 translate-y-0 sm:scale-100"}
)
|> js_exec("##{id}-confirm", "focus", [])
|> js_call("##{id}-confirm", "focus", [])
end
def hide_modal(js \\ %JS{}, id) do
js
|> JS.set_attribute({"disabled", ""}, to: "##{id}-confirm")
|> JS.remove_class("fade-in", to: "##{id}")
|> JS.hide(
to: "##{id}",
@ -388,7 +390,10 @@ defmodule LiveBeatsWeb.CoreComponents do
attr :rest, :global
slot :title
slot :confirm
slot :confirm do
attr :type, :string
attr :form, :string
end
slot :cancel
def modal(assigns) do
@ -447,9 +452,8 @@ defmodule LiveBeatsWeb.CoreComponents do
<%= for confirm <- @confirm do %>
<button
id={"#{@id}-confirm"}
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm"
class="phx-submit-loading:opacity-50 disabled:opacity-50 w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm"
phx-click={@on_confirm}
phx-disable-with
{assigns_to_attributes(confirm)}
>
<%= render_slot(confirm) %>
@ -527,15 +531,13 @@ defmodule LiveBeatsWeb.CoreComponents do
def button(%{patch: _} = assigns) do
~H"""
<%= if @primary do %>
<%= live_patch [to: @patch, class: "order-0 inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 sm:order-1 sm:ml-3"] ++
Map.to_list(@rest) do %>
<.link patch={@patch} class="order-0 inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 sm:order-1 sm:ml-3" {@rest}>
<%= render_slot(@inner_block) %>
<% end %>
</.link>
<% else %>
<%= live_patch [to: @patch, class: "order-1 inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 sm:order-0 sm:ml-0 lg:ml-3"] ++
assigns_to_attributes(assigns, [:primary, :patch]) do %>
<.link patch={@patch} class="order-1 inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 sm:order-0 sm:ml-0 lg:ml-3" {assigns_to_attributes(assigns, [:primary, :patch])}>
<%= render_slot(@inner_block) %>
<% end %>
</.link>
<% end %>
"""
end
@ -598,7 +600,7 @@ defmodule LiveBeatsWeb.CoreComponents do
data-drop={@sortable_drop}
>
<tr
:for={{row, i} <- Enum.with_index(@rows)}
:for={row <- @rows}
id={@row_id && @row_id.(row)}
phx-remove={@row_remove && @row_remove.(row)}
class="hover:bg-gray-50"
@ -606,10 +608,11 @@ defmodule LiveBeatsWeb.CoreComponents do
<td
:for={col <- @col}
phx-click={@row_click && @row_click.(row)}
class={
class={[
"phxp-click-loading:cursor-not-allowed phxp-click-loading:pointer-events-none",
col[:class!] ||
"px-6 py-3 whitespace-nowrap text-sm font-medium text-gray-900 #{col[:class]}"
}
]}
>
<div class="flex items-center space-x-3 lg:pl-2">
<%= render_slot(col, row) %>
@ -623,59 +626,17 @@ defmodule LiveBeatsWeb.CoreComponents do
"""
end
attr :id, :any, required: true
attr :module, :atom, required: true
attr :row_id, :any, default: false
attr :rows, :list, required: true
attr :owns_profile?, :boolean, default: false
attr :active_id, :any, default: nil
slot :col do
attr :label, :string
attr :class, :string
end
def live_table(assigns) do
~H"""
<div class="hidden mt-8 sm:block">
<div class="align-middle inline-block min-w-full border-b border-gray-200">
<table class="min-w-full">
<thead>
<tr class="border-t border-gray-200">
<%= for col <- @col do %>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<span class="lg:pl-2"><%= col.label %></span>
</th>
<% end %>
</tr>
</thead>
<tbody id={@id} class="bg-white divide-y divide-gray-100" phx-update="append">
<%= for {row, i} <- Enum.with_index(@rows) do %>
<.live_component
module={@module}
id={@row_id.(row)}
row={row}
col={@col}
index={i}
active_id={@active_id}
class="hover:bg-gray-50"
owns_profile?={@owns_profile?}
/>
<% end %>
</tbody>
</table>
</div>
</div>
"""
end
@doc """
Calls a wired up event listener to call a function with arguments.
window.addEventListener("js:exec", e => e.target[e.detail.call](...e.detail.args))
window.addEventListener("js:call", e => e.target[e.detail.call](...e.detail.args))
"""
def js_exec(js \\ %JS{}, to, call, args) do
JS.dispatch(js, "js:exec", to: to, detail: %{call: call, args: args})
def js_call(js \\ %JS{}, to, call, args) do
JS.dispatch(js, "js:call", to: to, detail: %{call: call, args: args})
end
def push_js_cmd(socket, %JS{ops: ops}) do
Phoenix.LiveView.push_event(socket, "js:exec", %{cmd: Phoenix.json_library().encode!(ops)})
end
def focus(js \\ %JS{}, parent, to) do

View file

@ -219,8 +219,6 @@
Re-establishing connection...
</.connection_status>
<.live_component module={LiveBeatsWeb.LayoutComponent} id="layout" />
<%= if @current_user do %>
<%= live_render(@socket, LiveBeatsWeb.PlayerLive, id: "player", session: %{}, sticky: true) %>
<% end %>
@ -231,16 +229,10 @@
<div class="relative">
<div
id="ping-container"
class="fixed bottom-0 right-0 bg-gray-900 text-gray-200 px-2 rounded-tl-md text-sm w-[114px] min-w-max"
class="fixed bottom-0 right-0 bg-gray-900 text-gray-200 px-2 rounded-tl-md text-sm w-[90px] min-w-max"
phx-update="ignore"
>
<span id="ping" phx-hook="Ping"></span>
<%= if @region do %>
<img
class="inline w-5 h-5 absolute right-0"
src={"https://fly.io/ui/images/#{@region}.svg"}
/>
<% end %>
</div>
</div>
</div>

View file

@ -9,6 +9,17 @@ defmodule LiveBeatsWeb.FileController do
require Logger
def show(conn, %{"id" => filename_uuid, "token" => token}) do
case Phoenix.Token.decrypt(conn, "file", token, max_age: :timer.minutes(1)) do
{:ok, %{vsn: 1, uuid: ^filename_uuid, size: _size}} ->
path = MediaLibrary.local_filepath(filename_uuid)
do_send_file(conn, path)
_ ->
send_resp(conn, :unauthorized, "")
end
end
def old_show(conn, %{"id" => filename_uuid, "token" => token}) do
path = MediaLibrary.local_filepath(filename_uuid)
mime_type = MIME.from_path(path)

View file

@ -10,7 +10,9 @@ defmodule LiveBeatsWeb.Endpoint do
signing_salt: "9OALgV62"
]
socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]
socket "/live", Phoenix.LiveView.Socket,
websocket: [connect_info: [session: @session_options]],
longpoll: [connect_info: [session: @session_options]]
# Serve at "/" the static files from "priv/static" directory.
#

View file

@ -1,55 +0,0 @@
defmodule LiveBeatsWeb.LayoutComponent do
@moduledoc """
Component for rendering content inside layout without full DOM patch.
"""
use LiveBeatsWeb, :live_component
def show_modal(module, attrs) do
send_update(__MODULE__, id: "layout", show: Enum.into(attrs, %{module: module}))
end
def hide_modal do
send_update(__MODULE__, id: "layout", show: nil)
end
def update(%{id: id} = assigns, socket) do
show =
case assigns[:show] do
%{module: _module, confirm: {text, attrs}} = show ->
show
|> Map.put_new(:title, show[:title])
|> Map.put_new(:on_cancel, show[:on_cancel] || %JS{})
|> Map.put_new(:on_confirm, show[:on_confirm] || %JS{})
|> Map.put_new(:patch, nil)
|> Map.put_new(:navigate, nil)
|> Map.merge(%{confirm_text: text, confirm_attrs: attrs})
nil ->
nil
end
{:ok, assign(socket, id: id, show: show)}
end
def render(assigns) do
~H"""
<div class={unless @show, do: "hidden"}>
<%= if @show do %>
<.modal
show
id={@id}
navigate={@show.navigate}
patch={@show.patch}
on_cancel={@show.on_cancel}
on_confirm={@show.on_confirm}
>
<:title><%= @show.title %></:title>
<.live_component module={@show.module} {@show} />
<:cancel>Cancel</:cancel>
<:confirm {@show.confirm_attrs}><%= @show.confirm_text %></:confirm>
</.modal>
<% end %>
</div>
"""
end
end

View file

@ -2,13 +2,29 @@ defmodule LiveBeatsWeb.ProfileLive do
use LiveBeatsWeb, :live_view
alias LiveBeats.{Accounts, MediaLibrary, MP3Stat}
alias LiveBeatsWeb.{LayoutComponent, Presence}
alias LiveBeatsWeb.Presence
alias LiveBeatsWeb.ProfileLive.{UploadFormComponent}
@max_presences 20
def render(assigns) do
~H"""
<.modal
id="upload"
patch={profile_path(@current_user)}
on_cancel={JS.push("cancel", target: "#upload-form")}
phx-mounted={@live_action == :new && show_modal("upload")}
>
<:title>Add Music</:title>
<.live_component
id="upload-form"
module={UploadFormComponent}
current_user={@current_user}
on_complete={hide_modal("upload")}
/>
<:confirm type="submit" form="song-form">Save</:confirm>
<:cancel>Cancel</:cancel>
</.modal>
<.title_bar>
<div>
<div class="block">
@ -16,7 +32,9 @@ defmodule LiveBeatsWeb.ProfileLive do
<%= if @owns_profile? do %>
(you)
<% end %>
<%= ngettext("%{count} song", "%{count} songs", @songs_count) %>
<span>
<%= ngettext("%{count} song", "%{count} songs", @songs_count) %>
</span>
</div>
<.link href={@profile.external_homepage_url} target="_blank" class="text-sm text-gray-600">
<.icon name={:code} /> <span class=""><%= url_text(@profile.external_homepage_url) %></span>
@ -37,7 +55,7 @@ defmodule LiveBeatsWeb.ProfileLive do
primary
phx-click={
JS.push("switch_profile",
value: %{user_id: @profile.user_id},
value: %{user_id: to_string(@profile.user_id)},
target: "#player",
loading: "#player"
)
@ -46,11 +64,14 @@ defmodule LiveBeatsWeb.ProfileLive do
<.icon name={:play} /><span class="ml-2">Listen</span>
</.button>
<% end %>
<%= if @owns_profile? do %>
<.button id="upload-btn" primary patch={profile_upload_path(@current_user)}>
<.icon name={:upload} /><span class="ml-2">Upload Songs</span>
</.button>
<% end %>
<.button
:if={@owns_profile?}
id="upload-btn"
primary
phx-click={show_modal("upload") |> JS.patch(profile_upload_path(@current_user))}
>
<.icon name={:upload} /><span class="ml-2">Upload Songs</span>
</.button>
</:actions>
</.title_bar>
@ -67,89 +88,105 @@ defmodule LiveBeatsWeb.ProfileLive do
</div>
</div>
<div id="dialogs" phx-update="stream">
<%= for {_id, song} <- if(@owns_profile?, do: @streams.songs, else: []), id = "delete-modal-#{song.id}" do %>
<.modal
id={id}
on_confirm={
JS.push("delete", value: %{id: song.id})
|> hide_modal(id)
|> focus_closest("#song-#{song.id}")
|> hide("#song-#{song.id}")
}
on_cancel={focus("##{id}", "#delete-song-#{song.id}")}
>
Are you sure you want to delete "<%= song.title %>"?
<:cancel>Cancel</:cancel>
<:confirm>Delete</:confirm>
</.modal>
<% end %>
<div :if={@owns_profile?} id="dialogs" phx-update="stream">
<.modal
:for={{_id, song} <- @streams.songs}
:if={song.id}
id={"delete-modal-#{song.id}"}
on_confirm={
JS.push("delete", value: %{id: to_string(song.id)})
|> hide_modal("delete-modal-#{song.id}")
|> focus_closest("#song-#{song.id}")
|> hide("#songs-#{song.id}")
}
on_cancel={focus("#delete-modal-#{song.id}", "#delete-song-#{song.id}")}
>
Are you sure you want to delete "<%= song.title %>"?
<:cancel>Cancel</:cancel>
<:confirm>Delete</:confirm>
</.modal>
</div>
<.table
id="songs"
rows={@streams.songs}
row_id={fn {id, _song} -> id end}
row_click={fn {_id, song} -> JS.push("play_or_pause", value: %{id: song.id}) end}
streamable
sortable_drop="row_dropped"
>
<:col
:let={{_id, song}}
label="Title"
class!="px-6 py-3 text-sm font-medium text-gray-900 min-w-[200px] md:min-w-[20rem] cursor-pointer"
<div>
<.table
id="songs"
rows={@streams.songs}
row_id={fn {id, _song} -> id end}
row_click={
fn {id, song} ->
JS.push("play_or_pause",
loading: "#songs tbody, ##{id}",
value: %{id: to_string(song.id)}
)
end
}
streamable
sortable_drop="row_dropped"
>
<span :if={song.status == :playing} 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" aria-label="Playing" role="button" />
</span>
<span :if={song.status == :paused} 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"
aria-label="Paused"
role="button"
/>
</span>
<span :if={song.status == :stopped} class="flex relative w-6 -translate-x-1">
<.icon
:if={@owns_profile?}
name={:play}
class="h-5 w-5 text-gray-400"
aria-label="Play"
role="button"
/>
</span>
<%= song.title %>
</:col>
<:col :let={{_id, song}} label="Artist"><%= song.artist %></:col>
<:col
:let={{_id, song}}
label="Attribution"
class="max-w-5xl break-words text-gray-600 font-light"
>
<%= song.attribution %>
</:col>
<:col :let={{_id, song}} label="Duration"><%= MP3Stat.to_mmss(song.duration) %></:col>
<:col :let={{_id, song}} :if={@owns_profile?} label="">
<.link
id={"delete-song-#{song.id}"}
phx-click={show_modal("delete-modal-#{song.id}")}
class="inline-flex items-center px-3 py-2 text-sm leading-4 font-medium"
<:col
:let={{_id, song}}
label="Title"
class!="px-6 py-3 text-sm font-medium text-gray-900 min-w-[200px] md:min-w-[20rem] cursor-pointer"
>
<.icon name={:trash} class="-ml-0.5 mr-2 h-4 w-4" /> Delete
</.link>
</:col>
</.table>
<span :if={song.status == :playing} 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" aria-label="Playing" role="button" />
</span>
<span :if={song.status == :paused} 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"
aria-label="Paused"
role="button"
/>
</span>
<span :if={song.status == :stopped} class="flex relative w-6 -translate-x-1">
<.icon
:if={@owns_profile?}
name={:play}
class="h-5 w-5 text-gray-400"
aria-label="Play"
role="button"
/>
</span>
<%= song.title %>
</:col>
<:col :let={{_id, song}} label="Artist"><%= song.artist %></:col>
<:col
:let={{_id, song}}
label="Attribution"
class="max-w-5xl break-words text-gray-600 font-light"
>
<%= song.attribution %>
</:col>
<:col :let={{_id, song}} label="Duration">
<%= MP3Stat.to_mmss(song.duration) %>
</:col>
<:col :let={{_id, song}} :if={@owns_profile?} label="">
<.link
id={"delete-song-#{song.id}"}
phx-click={show_modal("delete-modal-#{song.id}")}
class="inline-flex items-center px-3 py-2 text-sm leading-4 font-medium"
>
<.icon name={:trash} class="-ml-0.5 mr-2 h-4 w-4" /> Delete
</.link>
</:col>
</.table>
</div>
"""
end
def mount(%{"profile_username" => profile_username}, _session, socket) do
%{current_user: current_user} = socket.assigns
profile =
Accounts.get_user_by!(username: profile_username)
|> MediaLibrary.get_profile!()
profile_user =
if current_user.username == profile_username do
current_user
else
Accounts.get_user_by!(username: profile_username)
end
profile = MediaLibrary.get_profile!(profile_user)
if connected?(socket) do
MediaLibrary.subscribe_to_profile(profile)
@ -157,48 +194,68 @@ defmodule LiveBeatsWeb.ProfileLive do
Presence.subscribe(profile)
end
active_song = MediaLibrary.get_current_active_song(profile)
segments = if active_song, do: active_song.transcript_segments, else: []
songs = MediaLibrary.list_profile_songs(profile, 50)
socket =
socket
|> assign(
active_song_id: active_song && active_song.id,
active_song_id: nil,
active_profile_id: current_user.active_profile_user_id,
profile: profile,
owns_profile?: MediaLibrary.owns_profile?(current_user, profile),
songs_count: Enum.count(songs)
owns_profile?: MediaLibrary.owns_profile?(current_user, profile)
)
|> stream(:songs, songs)
|> stream(:transcript_segments, segments, dom_id: &"ss-#{&1.ss}")
|> stream_configure(:transcript_segments, dom_id: &"ss-#{&1.ss}")
|> stream(:transcript_segments, [])
|> stream_songs()
|> assign_presences()
{:ok, socket, temporary_assigns: [presences: %{}]}
end
def stream_songs(socket) do
%{profile: profile} = socket.assigns
songs = MediaLibrary.list_profile_songs(profile, 50)
active_song = Enum.find(songs, fn song -> song.status in [:playing, :paused] end)
segments = if active_song, do: active_song.transcript.segments, else: []
socket
|> assign(
songs: songs,
active_song: active_song,
active_song_id: active_song && active_song.id,
segments: segments,
songs_count: Enum.count(songs)
)
|> stream(:songs, songs, reset: true)
end
def handle_params(params, _url, socket) do
LayoutComponent.hide_modal()
{:noreply, socket |> apply_action(socket.assigns.live_action, params)}
end
def handle_event("play_or_pause", %{"id" => id}, socket) do
%{active_song_id: active_song_id} = socket.assigns
song = MediaLibrary.get_song!(id)
can_playback? = MediaLibrary.can_control_playback?(socket.assigns.current_user, song)
cond do
can_playback? and socket.assigns.active_song_id == id and MediaLibrary.playing?(song) ->
can_playback? and active_song_id == song.id and MediaLibrary.playing?(song) ->
MediaLibrary.pause_song(song)
receive do
{MediaLibrary, %MediaLibrary.Events.Pause{song: song}} ->
{:noreply, pause_song(socket, song)}
end
can_playback? ->
MediaLibrary.play_song(id)
true ->
:noop
end
receive do
{MediaLibrary, %MediaLibrary.Events.Play{song: song}} ->
{:noreply, play_song(socket, song)}
end
{:noreply, socket}
true ->
{:noreply, socket}
end
end
def handle_event("delete", %{"id" => id}, socket) do
@ -253,7 +310,10 @@ defmodule LiveBeatsWeb.ProfileLive do
end
def handle_info({MediaLibrary, %MediaLibrary.Events.NewPosition{song: song}}, socket) do
{:noreply, stream_insert(socket, :songs, song, at: song.position)}
{:noreply,
socket
|> stream_delete(:songs, song)
|> stream_insert(:songs, song, at: song.position)}
end
def handle_info({MediaLibrary, %MediaLibrary.Events.Play{song: song}}, socket) do
@ -264,7 +324,10 @@ defmodule LiveBeatsWeb.ProfileLive do
{:noreply, pause_song(socket, song)}
end
def handle_info({MediaLibrary, {%MediaLibrary.Song.TranscriptSegment{} = seg, song_id}}, socket) do
def handle_info(
{MediaLibrary, {%MediaLibrary.Song.Transcript.Segment{} = seg, song_id}},
socket
) do
if socket.assigns.active_song_id == song_id do
{:noreply, stream_insert(socket, :transcript_segments, seg)}
else
@ -348,36 +411,20 @@ defmodule LiveBeatsWeb.ProfileLive do
end
end
defp apply_action(socket, :new, _params) do
if socket.assigns.owns_profile? do
socket
|> assign(:page_title, "Add Music")
|> assign(:song, %MediaLibrary.Song{})
|> show_upload_modal()
else
socket
|> put_flash(:error, "You can't do that")
|> redirect(to: profile_path(socket.assigns.current_user))
end
end
defp apply_action(socket, :show, _params) do
socket
|> assign(:page_title, "Listing Songs")
|> assign(:song, nil)
end
defp show_upload_modal(socket) do
LayoutComponent.show_modal(UploadFormComponent, %{
id: :new,
confirm: {"Save", type: "submit", form: "song-form"},
patch: profile_path(socket.assigns.current_user),
song: socket.assigns.song,
title: socket.assigns.page_title,
current_user: socket.assigns.current_user
})
socket
defp apply_action(socket, :new, _params) do
if socket.assigns.owns_profile? do
assign(socket, :page_title, "Add Music")
else
socket
|> put_flash(:error, "You can't do that")
|> redirect(to: profile_path(socket.assigns.current_user))
end
end
defp assign_presences(socket) do

View file

@ -7,6 +7,10 @@ defmodule LiveBeatsWeb.ProfileLive.UploadFormComponent do
@max_songs 10
@impl true
def update(%{action: :reset}, socket) do
{:ok, reset_assigns(socket)}
end
def update(%{action: {:duration, entry_ref, result}}, socket) do
case result do
{:ok, %MP3Stat{} = stat} ->
@ -17,20 +21,37 @@ defmodule LiveBeatsWeb.ProfileLive.UploadFormComponent do
end
end
def update(%{song: song} = assigns, socket) do
def update(%{} = assigns, socket) do
{:ok,
socket
|> assign(assigns)
|> assign(changesets: %{}, error_messages: [])
|> allow_upload(:mp3,
song_id: song.id,
auto_upload: true,
progress: &handle_progress/3,
accept: ~w(.mp3),
max_entries: @max_songs,
max_file_size: 20_000_000,
chunk_size: 64_000 * 3
)}
|> reset_assigns()}
end
defp reset_assigns(socket) do
socket =
assign(socket,
song: %MediaLibrary.Song{},
changesets: %{},
error_messages: []
)
uploads = socket.assigns[:uploads][:mp3]
if uploads do
Enum.reduce(uploads.entries, socket, fn entry, acc ->
cancel_upload(acc, :mp3, entry.ref)
end)
else
allow_upload(socket, :mp3,
auto_upload: true,
progress: &handle_progress/3,
accept: ~w(.mp3),
max_entries: @max_songs,
max_file_size: 20_000_000,
chunk_size: 64_000 * 3
)
end
end
@impl true
@ -57,12 +78,15 @@ defmodule LiveBeatsWeb.ProfileLive.UploadFormComponent do
else
case MediaLibrary.import_songs(current_user, changesets, &consume_entry(socket, &1, &2)) do
{:ok, songs} ->
send_update(socket.assigns.myself, %{action: :reset})
{:noreply,
socket
|> put_flash(:info, "#{map_size(songs)} song(s) uploaded")
|> push_patch(to: profile_path(current_user))}
|> push_js_cmd(socket.assigns.on_complete)}
{:error, {failed_op, reason}} ->
IO.inspect({failed_op, reason})
{:noreply, put_error(socket, {failed_op, reason})}
end
end
@ -72,6 +96,10 @@ defmodule LiveBeatsWeb.ProfileLive.UploadFormComponent do
{:noreply, socket}
end
def handle_event("cancel", _, socket) do
{:noreply, reset_assigns(socket)}
end
defp pending_stats?(socket) do
Enum.find(socket.assigns.changesets, fn {_ref, chset} -> !chset.changes[:duration] end)
end

View file

@ -1,4 +1,4 @@
<div>
<div id="upload-form">
<p class="inline text-gray-500 text-sm">(songs expire every six hours)</p>
<.form
@ -8,7 +8,8 @@
class="space-y-8"
phx-target={@myself}
phx-change="validate"
phx-submit="save"
phx-auto-recover="ignore"
phx-submit={JS.push("save", target: @myself, loading: "#song-form, #upload")}
>
<div class="space-y-8 divide-y divide-gray-200 sm:space-y-5">
<div class="space-y-2 sm:space-y-2">
@ -62,7 +63,7 @@
for="file-upload"
class="relative cursor-pointer bg-white rounded-md font-medium text-indigo-600 hover:text-indigo-500 focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-indigo-500"
>
<span phx-click={js_exec("##{@uploads.mp3.ref}", "click", [])}>
<span phx-click={js_call("##{@uploads.mp3.ref}", "click", [])}>
Upload files
</span>
<.live_file_input upload={@uploads.mp3} class="sr-only" tabindex="0" />

View file

@ -11,10 +11,15 @@ defmodule LiveBeatsWeb.SettingsLive do
<div class="max-w-3xl px-4 mx-auto mt-6">
<.form
id="settings-form"
:let={f}
id="settings-form"
for={@changeset}
phx-change="validate"
phx-change={
JS.push("validate",
loading:
"#settings-form, #settings-form button, #settings-form input, #settings-form [phx-feedback-for]"
)
}
phx-submit="save"
class="space-y-8 divide-y divide-gray-200"
>
@ -103,6 +108,8 @@ defmodule LiveBeatsWeb.SettingsLive do
{:ok, assign(socket, changeset: changeset)}
end
def handle_params(_, _, socket), do: {:noreply, socket}
def handle_event("validate", %{"user" => params}, socket) do
changeset = Accounts.change_settings(socket.assigns.current_user, params)
{:noreply, assign(socket, changeset: Map.put(changeset, :action, :validate))}

13
mix.exs
View file

@ -4,7 +4,7 @@ defmodule LiveBeats.MixProject do
def project do
[
app: :live_beats,
version: "0.1.0",
version: "0.1.1",
elixir: "~> 1.12",
elixirc_paths: elixirc_paths(Mix.env()),
start_permanent: Mix.env() == :prod,
@ -33,10 +33,11 @@ defmodule LiveBeats.MixProject do
defp deps do
[
{:phoenix, "~> 1.7.1"},
{:phoenix_live_view, "~> 0.18.17"},
{:phoenix_live_dashboard, "~> 0.7.2"},
{:dns_cluster, ">= 0.0.0"},
{:phoenix_live_view, github: "phoenixframework/phoenix_live_view", override: true},
{:phoenix_live_dashboard, "~> 0.8"},
{:phoenix_ecto, "~> 4.4"},
{:ecto_sql, "~> 3.6"},
{:ecto_sql, "~> 3.11"},
{:ecto_network, "~> 1.3.0"},
{:postgrex, ">= 0.0.0"},
{:phoenix_html, "~> 3.3", override: true},
@ -54,10 +55,10 @@ defmodule LiveBeats.MixProject do
{:heroicons, "~> 0.2.2"},
{:castore, "~> 0.1.13"},
{:tailwind, "~> 0.2.0"},
{:libcluster, "~> 3.3.1"},
{:bumblebee, github: "elixir-nx/bumblebee"},
{:exla, ">= 0.0.0"},
{:req, "~> 0.3.7"}
{:req, "~> 0.4"},
{:flame, "~> 0.1.12"}
]
end

View file

@ -2,60 +2,75 @@
"axon": {:hex, :axon, "0.5.1", "1ae3a2193df45e51fca912158320b2ca87cb7fba4df242bd3ebe245504d0ea1a", [:mix], [{:kino, "~> 0.7", [hex: :kino, repo: "hexpm", optional: true]}, {:kino_vega_lite, "~> 0.1.7", [hex: :kino_vega_lite, repo: "hexpm", optional: true]}, {:nx, "~> 0.5.0", [hex: :nx, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1", [hex: :table_rex, repo: "hexpm", optional: true]}], "hexpm", "d36f2a11c34c6c2b458f54df5c71ffdb7ed91c6a9ccd908faba909c84cc6a38e"},
"bumblebee": {:git, "https://github.com/elixir-nx/bumblebee.git", "fd9a8b1d149cdcebd5170e103b49e9f6f64ee482", []},
"castore": {:hex, :castore, "0.1.22", "4127549e411bedd012ca3a308dede574f43819fe9394254ca55ab4895abfa1a2", [:mix], [], "hexpm", "c17576df47eb5aa1ee40cc4134316a99f5cad3e215d5c77b8dd3cfef12a22cac"},
"certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"},
"complex": {:hex, :complex, "0.5.0", "af2d2331ff6170b61bb738695e481b27a66780e18763e066ee2cd863d0b1dd92", [:mix], [], "hexpm", "2683bd3c184466cfb94fad74cbfddfaa94b860e27ad4ca1bffe3bff169d91ef1"},
"con_cache": {:hex, :con_cache, "1.1.0", "45c7c6cd6dc216e47636232e8c683734b7fe293221fccd9454fa1757bc685044", [:mix], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8655f2ae13a1e56c8aef304d250814c7ed929c12810f126fc423ecc8e871593b"},
"connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"},
"cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"},
"cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"},
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
"cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"},
"db_connection": {:hex, :db_connection, "2.4.2", "f92e79aff2375299a16bcb069a14ee8615c3414863a6fef93156aee8e86c2ff3", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4fe53ca91b99f55ea249693a0229356a08f4d1a7931d8ffa79289b145fe83668"},
"decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"},
"ecto": {:hex, :ecto, "3.9.4", "3ee68e25dbe0c36f980f1ba5dd41ee0d3eb0873bccae8aeaf1a2647242bffa35", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "de5f988c142a3aa4ec18b85a4ec34a2390b65b24f02385c1144252ff6ff8ee75"},
"cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"},
"db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"},
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
"dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"},
"ecto": {:hex, :ecto, "3.11.2", "e1d26be989db350a633667c5cda9c3d115ae779b66da567c68c80cfb26a8c9ee", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3c38bca2c6f8d8023f2145326cc8a80100c3ffe4dcbd9842ff867f7fc6156c65"},
"ecto_network": {:hex, :ecto_network, "1.3.0", "1e77fa37c20e0f6a426d3862732f3317b0fa4c18f123d325f81752a491d7304e", [:mix], [{:ecto_sql, ">= 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:phoenix_html, ">= 0.0.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.14.0", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm", "053a5e46ef2837e8ea5ea97c82fa0f5494699209eddd764e663c85f11b2865bd"},
"ecto_sql": {:hex, :ecto_sql, "3.9.0", "2bb21210a2a13317e098a420a8c1cc58b0c3421ab8e3acfa96417dab7817918c", [:mix], [{:db_connection, "~> 2.5 or ~> 2.4.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a8f3f720073b8b1ac4c978be25fa7960ed7fd44997420c304a4a2e200b596453"},
"ecto_sql": {:hex, :ecto_sql, "3.11.1", "e9abf28ae27ef3916b43545f9578b4750956ccea444853606472089e7d169470", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ce14063ab3514424276e7e360108ad6c2308f6d88164a076aac8a387e1fea634"},
"elixir_make": {:hex, :elixir_make, "0.7.5", "784cc00f5fa24239067cc04d449437dcc5f59353c44eb08f188b2b146568738a", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "c3d63e8d5c92fa3880d89ecd41de59473fa2e83eeb68148155e25e8b95aa2887"},
"erlexec": {:hex, :erlexec, "2.0.2", "995e40477de94c37ec1264cc3e52eb6273938e80c9bcc4f94110a3f1c0d9aba3", [:rebar3], [], "hexpm", "cc829a7c6c23d399832da2e998ea5ebc552232a6fe3eb1edb400178ec8287dcb"},
"esbuild": {:hex, :esbuild, "0.5.0", "d5bb08ff049d7880ee3609ed5c4b864bd2f46445ea40b16b4acead724fb4c4a3", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "f183a0b332d963c4cfaf585477695ea59eef9a6f2204fdd0efa00e099694ffe5"},
"ex_aws": {:hex, :ex_aws, "2.5.0", "1785e69350b16514c1049330537c7da10039b1a53e1d253bbd703b135174aec3", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "971b86e5495fc0ae1c318e35e23f389e74cf322f2c02d34037c6fc6d405006f1"},
"ex_aws_s3": {:hex, :ex_aws_s3, "2.5.2", "cee302b8e9ee198cc0d89f1de2a7d6a8921e1a556574476cf5590d2156590fe3", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "cc5bd945a22a99eece4721d734ae2452d3717e81c357a781c8574663254df4a1"},
"exla": {:hex, :exla, "0.5.1", "8832aa299fe06ed9b772e004760b7c97e9d8dcbe40e9a4bfcbbe10b320b9c342", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nx, "~> 0.5.1", [hex: :nx, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:xla, "~> 0.4.4", [hex: :xla, repo: "hexpm", optional: false]}], "hexpm", "48a990dbaf02bf5f288aa1360b5237c2f55db8bf52d4f63072f2b6a15d4e8375"},
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
"finch": {:hex, :finch, "0.13.0", "c881e5460ec563bf02d4f4584079e62201db676ed4c0ef3e59189331c4eddf7b", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "49957dcde10dcdc042a123a507a9c5ec5a803f53646d451db2f7dea696fba6cc"},
"floki": {:hex, :floki, "0.34.0", "002d0cc194b48794d74711731db004fafeb328fe676976f160685262d43706a8", [:mix], [], "hexpm", "9c3a9f43f40dde00332a589bd9d389b90c1f518aef500364d00636acc5ebc99c"},
"finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"},
"flame": {:hex, :flame, "0.1.12", "4d46b706d35d6eb22505d0e060fe41174052eaa38f778a4762fc74dd2c9df301", [:mix], [{:req, "~> 0.4.13", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "aa25de6614455ac01e33409c08db4560fa54dd837ad116aae16c8f7c011ccd76"},
"floki": {:hex, :floki, "0.36.2", "a7da0193538c93f937714a6704369711998a51a6164a222d710ebd54020aa7a3", [:mix], [], "hexpm", "a8766c0bc92f074e5cb36c4f9961982eda84c5d2b8e979ca67f5c268ec8ed580"},
"gettext": {:hex, :gettext, "0.20.0", "75ad71de05f2ef56991dbae224d35c68b098dd0e26918def5bb45591d5c8d429", [:mix], [], "hexpm", "1c03b177435e93a47441d7f681a7040bd2a816ece9e2666d1c9001035121eb3d"},
"hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"},
"heroicons": {:hex, :heroicons, "0.2.4", "12824795f25340415bffa9a501b96c89dc856ed732e82acccdab635fee92411b", [:mix], [{:phoenix_html, "~> 2.14 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}], "hexpm", "7d6c51dc8ecaadb37943247da83ec2fbf3f6b479a23f4cf0195bd61ec2268128"},
"hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"},
"jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
"libcluster": {:hex, :libcluster, "3.3.1", "e7a4875cd1290cee7a693d6bd46076863e9e433708b01339783de6eff5b7f0aa", [:mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "b575ca63c1cd84e01f3fa0fc45e6eb945c1ee7ae8d441d33def999075e9e5398"},
"mime": {:hex, :mime, "2.0.3", "3676436d3d1f7b81b5a2d2bd8405f412c677558c81b1c92be58c00562bb59095", [:mix], [], "hexpm", "27a30bf0db44d25eecba73755acf4068cbfe26a4372f9eb3e4ea3a45956bff6b"},
"mint": {:hex, :mint, "1.4.2", "50330223429a6e1260b2ca5415f69b0ab086141bc76dc2fbf34d7c389a6675b2", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "ce75a5bbcc59b4d7d8d70f8b2fc284b1751ffb35c7b6a6302b5192f8ab4ddd80"},
"nimble_options": {:hex, :nimble_options, "0.4.0", "c89babbab52221a24b8d1ff9e7d838be70f0d871be823165c94dd3418eea728f", [:mix], [], "hexpm", "e6701c1af326a11eea9634a3b1c62b475339ace9456c1a23ec3bc9a847bca02d"},
"nimble_pool": {:hex, :nimble_pool, "0.2.6", "91f2f4c357da4c4a0a548286c84a3a28004f68f05609b4534526871a22053cde", [:mix], [], "hexpm", "1c715055095d3f2705c4e236c18b618420a35490da94149ff8b580a2144f653f"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
"mint": {:hex, :mint, "1.5.2", "4805e059f96028948870d23d7783613b7e6b0e2fb4e98d720383852a760067fd", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "d77d9e9ce4eb35941907f1d3df38d8f750c357865353e21d335bdcdf6d892a02"},
"nimble_options": {:hex, :nimble_options, "1.1.0", "3b31a57ede9cb1502071fade751ab0c7b8dbe75a9a4c2b5bbb0943a690b63172", [:mix], [], "hexpm", "8bbbb3941af3ca9acc7835f5655ea062111c9c27bcac53e004460dfd19008a99"},
"nimble_ownership": {:hex, :nimble_ownership, "0.3.1", "99d5244672fafdfac89bfad3d3ab8f0d367603ce1dc4855f86a1c75008bce56f", [:mix], [], "hexpm", "4bf510adedff0449a1d6e200e43e57a814794c8b5b6439071274d248d272a549"},
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
"nx": {:hex, :nx, "0.5.1", "118134b8c97c2a8f86c87aa8434994c1cbbe139a306b89cca04e08dd46228067", [:mix], [{:complex, "~> 0.5", [hex: :complex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ceb8fbbe19b3c4252a7188d8b0e059fac9da0f4a4f3bb770fc665fdd0b29f0c5"},
"nx_image": {:hex, :nx_image, "0.1.0", "ae10fa41fa95126f934d6160ef4320f7db583535fb868415f2562fe19969d245", [:mix], [{:nx, "~> 0.4", [hex: :nx, repo: "hexpm", optional: false]}], "hexpm", "60a2928164cdca540b4c180ff25579b97a5f2a650fc890d40db3e1a7798c93ad"},
"nx_signal": {:hex, :nx_signal, "0.1.0", "403ac73140e2f368e827e0aca1a3035abaf6d890b00376742b359a6838e00d7f", [:mix], [{:nx, "~> 0.5", [hex: :nx, repo: "hexpm", optional: false]}], "hexpm", "1c68f2f0d186700819287f37ee6154a11e06bf5dbb30b73fcc92776293309a05"},
"phoenix": {:hex, :phoenix, "1.7.1", "a029bde19d9c3b559e5c3d06c78b76e81396bedd456a6acedb42f9c7b2e535a9", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.4", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "ea9d4a85c3592e37efa07d0dc013254fda445885facaefddcbf646375c116457"},
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
"phoenix": {:hex, :phoenix, "1.7.12", "1cc589e0eab99f593a8aa38ec45f15d25297dd6187ee801c8de8947090b5a9d3", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "d646192fbade9f485b01bc9920c139bfdd19d0f8df3d73fd8eaf2dfbe0d2837c"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.4.0", "0672ed4e4808b3fbed494dded89958e22fb882de47a97634c0b13e7b0b5f7720", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "09864e558ed31ee00bd48fcc1d4fc58ae9678c9e81649075431e69dbabb43cc1"},
"phoenix_html": {:hex, :phoenix_html, "3.3.1", "4788757e804a30baac6b3fc9695bf5562465dd3f1da8eb8460ad5b404d9a2178", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "bed1906edd4906a15fd7b412b85b05e521e1f67c9a85418c55999277e553d0d3"},
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.7.2", "97cc4ff2dba1ebe504db72cb45098cb8e91f11160528b980bd282cc45c73b29c", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.18.3", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "0e5fdf063c7a3b620c566a30fcf68b7ee02e5e46fe48ee46a6ec3ba382dc05b7"},
"phoenix_html": {:hex, :phoenix_html, "3.3.4", "42a09fc443bbc1da37e372a5c8e6755d046f22b9b11343bf885067357da21cb3", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0249d3abec3714aff3415e7ee3d9786cb325be3151e6c4b3021502c585bf53fb"},
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.3", "7ff51c9b6609470f681fbea20578dede0e548302b0c8bdf338b5a753a4f045bf", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "f9470a0a8bae4f56430a23d42f977b5a6205fdba6559d76f932b876bfaec652d"},
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.4.0", "4fe222c0be55fdc3f9c711e24955fc42a7cd9b7a2f5f406f2580a567c335a573", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "bebf0fc2d2113b61cb5968f585367234b7b4c21d963d691de7b4b2dc6cdaae6f"},
"phoenix_live_view": {:hex, :phoenix_live_view, "0.18.17", "74938b02f3c531bed3f87fe1ea39af6b5b2d26ab1405e77e76b8ef5df9ffa8a1", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f4b5710e19a29b8dc93b7af4bab4739c067a3cb759af01ffc3057165453dce38"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"},
"phoenix_template": {:hex, :phoenix_template, "1.0.1", "85f79e3ad1b0180abb43f9725973e3b8c2c3354a87245f91431eec60553ed3ef", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "157dc078f6226334c91cb32c1865bf3911686f8bcd6bcff86736f6253e6993ee"},
"plug": {:hex, :plug, "1.14.0", "ba4f558468f69cbd9f6b356d25443d0b796fbdc887e03fa89001384a9cac638f", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "bf020432c7d4feb7b3af16a0c2701455cbbbb95e5b6866132cb09eb0c29adc14"},
"plug_cowboy": {:hex, :plug_cowboy, "2.6.0", "d1cf12ff96a1ca4f52207c5271a6c351a4733f413803488d75b70ccf44aebec2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "073cf20b753ce6682ed72905cd62a2d4bd9bad1bf9f7feb02a1b8e525bd94fa6"},
"plug_crypto": {:hex, :plug_crypto, "1.2.3", "8f77d13aeb32bfd9e654cb68f0af517b371fb34c56c9f2b58fe3df1235c1251a", [:mix], [], "hexpm", "b5672099c6ad5c202c45f5a403f21a3411247f164e4a8fab056e5cd8a290f4a2"},
"postgrex": {:hex, :postgrex, "0.16.5", "fcc4035cc90e23933c5d69a9cd686e329469446ef7abba2cf70f08e2c4b69810", [:mix], [{:connection, "~> 1.1", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "edead639dc6e882618c01d8fc891214c481ab9a3788dfe38dd5e37fd1d5fb2e8"},
"phoenix_live_view": {:git, "https://github.com/phoenixframework/phoenix_live_view.git", "2785f5bb2df6eb2863567e3c9da622b4499968fc", []},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
"phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
"plug": {:hex, :plug, "1.16.0", "1d07d50cb9bb05097fdf187b31cf087c7297aafc3fed8299aac79c128a707e47", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cbf53aa1f5c4d758a7559c0bd6d59e286c2be0c6a1fac8cc3eee2f638243b93e"},
"plug_cowboy": {:hex, :plug_cowboy, "2.7.1", "87677ffe3b765bc96a89be7960f81703223fe2e21efa42c125fcd0127dd9d6b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "02dbd5f9ab571b864ae39418db7811618506256f6d13b4a45037e5fe78dc5de3"},
"plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"},
"postgrex": {:hex, :postgrex, "0.17.5", "0483d054938a8dc069b21bdd636bf56c487404c241ce6c319c1f43588246b281", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "50b8b11afbb2c4095a3ba675b4f055c416d0f3d7de6633a595fc131a828a67eb"},
"progress_bar": {:hex, :progress_bar, "2.0.1", "7b40200112ae533d5adceb80ff75fbe66dc753bca5f6c55c073bfc122d71896d", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "2519eb58a2f149a3a094e729378256d8cb6d96a259ec94841bd69fdc71f18f87"},
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
"req": {:hex, :req, "0.3.7", "e4ea5d73e3f434c0a15601bb85330ffd0e57860c098283e98c28d21172a1f749", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.9", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "a7d3c0bec7d2d23198ef12676d2c950bec258308c6a5123eb98465030205f39c"},
"req": {:hex, :req, "0.4.14", "103de133a076a31044e5458e0f850d5681eef23dfabf3ea34af63212e3b902e2", [:mix], [{:aws_signature, "~> 0.3.2", [hex: :aws_signature, repo: "hexpm", optional: true]}, {:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:nimble_ownership, "~> 0.2.0 or ~> 0.3.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "2ddd3d33f9ab714ced8d3c15fd03db40c14dbf129003c4a3eb80fac2cc0b1b08"},
"rustler_precompiled": {:hex, :rustler_precompiled, "0.6.1", "160b545bce8bf9a3f1b436b2c10f53574036a0db628e40f393328cbbe593602f", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "0dd269fa261c4e3df290b12031c575fff07a542749f7b0e8b744d72d66c43600"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
"sweet_xml": {:hex, :sweet_xml, "0.7.4", "a8b7e1ce7ecd775c7e8a65d501bc2cd933bff3a9c41ab763f5105688ef485d08", [:mix], [], "hexpm", "e7c4b0bdbf460c928234951def54fe87edf1a170f6896675443279e2dbeba167"},
"swoosh": {:hex, :swoosh, "1.8.2", "af9a22ab2c0d20b266f61acca737fa11a121902de9466a39e91bacdce012101c", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d058ba750eafadb6c09a84a352c14c5d1eeeda6e84945fcc95785b7f3067b7db"},
"tailwind": {:hex, :tailwind, "0.2.0", "95f9e4a32020c5bec480f1d6a43a49ac8030b13183127b577605f506d6e13a66", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "385e939fcd7fe4654be5130b187e358aaabade385513f9d200ffecdbb9552a9e"},
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"},
"telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"},
"tokenizers": {:hex, :tokenizers, "0.3.0", "1aebe61c68cf36e3ea4423a357b196e226c18a8b3206afe59d257f038833deb4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.6", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "6c6de26911fd875fc1aead92ee40efb3e4271f54a312cc52f073012f2a134201"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
"unpickler": {:hex, :unpickler, "0.1.0", "c2262c0819e6985b761e7107546cef96a485f401816be5304a65fdd200d5bd6a", [:mix], [], "hexpm", "e2b3f61e62406187ac52afead8a63bfb4e49394028993f3c4c42712743cab79e"},
"unzip": {:hex, :unzip, "0.8.0", "ee21d87c21b01567317387dab4228ac570ca15b41cfc221a067354cbf8e68c4d", [:mix], [], "hexpm", "ffa67a483efcedcb5876971a50947222e104d5f8fea2c4a0441e6f7967854827"},
"websock": {:hex, :websock, "0.5.0", "f6bbce90226121d62a0715bca7c986c5e43de0ccc9475d79c55381d1796368cc", [:mix], [], "hexpm", "b51ac706df8a7a48a2c622ee02d09d68be8c40418698ffa909d73ae207eb5fb8"},
"websock_adapter": {:hex, :websock_adapter, "0.4.5", "30038a3715067f51a9580562c05a3a8d501126030336ffc6edb53bf57d6d2d26", [:mix], [{:bandit, "~> 0.6", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.4", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "1d9812dc7e703c205049426fd4fe0852a247a825f91b099e53dc96f68bafe4c8"},
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
"websock_adapter": {:hex, :websock_adapter, "0.5.6", "0437fe56e093fd4ac422de33bf8fc89f7bc1416a3f2d732d8b2c8fd54792fe60", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "e04378d26b0af627817ae84c92083b7e97aca3121196679b73c73b99d0d133ea"},
"xla": {:hex, :xla, "0.4.4", "c3a8ed1f579bda949df505e49ff65415c8281d991fbd6ae1d8f3c5d0fd155f54", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "484f3f9011db3c9f1ff1e98eecefd382f3882a07ada540fd58803db1d2dab671"},
}

View file

@ -1,11 +1,20 @@
defmodule LiveBeats.Repo.Migrations.CreateUserAuth do
use Ecto.Migration
import Ecto.Query
def change do
execute "CREATE EXTENSION IF NOT EXISTS citext", ""
# email_type =
# if repo().exists?(
# from(e in "pg_available_extensions", where: e.name == "citext", select: e.name)
# ) do
# execute "CREATE EXTENSION IF NOT EXISTS citext", ""
# :citext
# else
# :string
# end
create table(:users) do
add :email, :citext, null: false
add :email, :string, null: false
add :username, :string, null: false
add :name, :string
add :role, :string, null: false
@ -16,8 +25,8 @@ defmodule LiveBeats.Repo.Migrations.CreateUserAuth do
timestamps()
end
create unique_index(:users, [:email])
create unique_index(:users, [:username])
create unique_index(:users, ["lower(email)"], name: :users_email_index)
create unique_index(:users, ["lower(username)"], name: :users_username_index)
create table(:identities) do
add :user_id, references(:users, on_delete: :delete_all), null: false

View file

@ -3,7 +3,7 @@ defmodule LiveBeats.Repo.Migrations.AddServerIpToSongs do
def change do
alter table(:songs) do
add :server_ip, :inet
add :server_ip, :string
end
end
end

View file

@ -5,11 +5,6 @@ defmodule LiveBeats.Repo.Migrations.AddSongsNumberToUsers do
alter table(:users) do
add :songs_count, :integer, null: false, default: 0
end
execute("
UPDATE users set songs_count =
(SELECT count (*) from songs
where songs.user_id = users.id)")
end
def down do

View file

@ -3,7 +3,7 @@ defmodule LiveBeats.Repo.Migrations.AddTranscriptsToSongs do
def change do
alter table(:songs) do
add :transcript_segments, {:array, :map}, null: false, default: []
add :transcript, :jsonb, null: false, default: "{\"segments\": []}"
end
end
end

View file

@ -1,4 +1,5 @@
ip=$(grep fly-local-6pn /etc/hosts | cut -f 1)
export RELEASE_DISTRIBUTION=name
export RELEASE_NODE=$FLY_APP_NAME@$ip
export LIVE_BEATS_SERVER_IP=$ip
export ERL_AFLAGS="-proto_dist inet6_tcp"
export ECTO_IPV6="true"
export DNS_CLUSTER_QUERY="${FLY_APP_NAME}.internal"
export RELEASE_DISTRIBUTION="name"
export RELEASE_NODE="${FLY_APP_NAME}-${FLY_IMAGE_REF##*-}@${FLY_PRIVATE_IP}"

View file

@ -1,3 +1,20 @@
#!/bin/sh
if [ -n "${BUCKET_MOUNT}" ]; then
mkdir -p "${BUCKET_MOUNT}"
chown nobody:nogroup "${BUCKET_MOUNT}"
echo "Mounting S3 bucket at ${BUCKET_MOUNT}"
echo $AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY > ${HOME}/.passwd-s3fs
chmod 600 ${HOME}/.passwd-s3fs
s3fs livebeats-store "${BUCKET_MOUNT}" \
-o url="${AWS_ENDPOINT_URL_S3}" \
-o dbglevel=info \
-o curldbg \
-o use_path_request_style \
-o allow_other \
-o _netdev \
-o uid="$(id -u nobody)" \
-o gid="$(id -g nobody)"
fi
cd -P -- "$(dirname -- "$0")"
PHX_SERVER=true exec ./live_beats start
su -s /bin/bash nobody -c 'PHX_SERVER=true exec ./live_beats start'