2021-10-29 16:12:23 +00:00
|
|
|
|
defmodule LiveBeats.MediaLibrary do
|
|
|
|
|
@moduledoc """
|
|
|
|
|
The MediaLibrary context.
|
|
|
|
|
"""
|
|
|
|
|
|
2021-11-05 19:57:33 +00:00
|
|
|
|
require Logger
|
2021-10-29 16:12:23 +00:00
|
|
|
|
import Ecto.Query, warn: false
|
2021-11-05 00:49:19 +00:00
|
|
|
|
alias LiveBeats.{Repo, MP3Stat, Accounts}
|
2021-10-29 16:12:23 +00:00
|
|
|
|
alias LiveBeats.MediaLibrary.{Song, Genre}
|
2021-11-05 19:57:33 +00:00
|
|
|
|
alias Ecto.{Multi, Changeset}
|
2021-10-29 16:12:23 +00:00
|
|
|
|
|
2021-11-05 00:49:19 +00:00
|
|
|
|
@pubsub LiveBeats.PubSub
|
|
|
|
|
|
2021-11-05 19:57:33 +00:00
|
|
|
|
defdelegate stopped?(song), to: Song
|
|
|
|
|
defdelegate playing?(song), to: Song
|
|
|
|
|
defdelegate paused?(song), to: Song
|
|
|
|
|
|
2021-11-05 00:49:19 +00:00
|
|
|
|
def subscribe(%Accounts.User{} = user) do
|
|
|
|
|
Phoenix.PubSub.subscribe(@pubsub, topic(user.id))
|
|
|
|
|
end
|
|
|
|
|
|
2021-11-06 03:02:31 +00:00
|
|
|
|
def local_filepath(filename_uuid) when is_binary(filename_uuid) do
|
|
|
|
|
Path.join("priv/uploads/songs", filename_uuid)
|
|
|
|
|
end
|
|
|
|
|
|
2021-11-05 00:49:19 +00:00
|
|
|
|
def play_song(%Song{id: id}), do: play_song(id)
|
|
|
|
|
|
|
|
|
|
def play_song(id) do
|
|
|
|
|
song = get_song!(id)
|
2021-11-05 19:57:33 +00:00
|
|
|
|
|
|
|
|
|
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,
|
2021-11-10 15:10:43 +00:00
|
|
|
|
where: s.user_id == ^song.user_id and s.status in [:playing, :paused],
|
2021-11-05 19:57:33 +00:00
|
|
|
|
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}})
|
2021-11-05 00:49:19 +00:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def pause_song(%Song{} = song) do
|
2021-11-05 19:57:33 +00:00
|
|
|
|
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()
|
|
|
|
|
|
2021-11-05 00:49:19 +00:00
|
|
|
|
Phoenix.PubSub.broadcast!(@pubsub, topic(song.user_id), {:pause, song})
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
defp topic(user_id), do: "room:#{user_id}"
|
|
|
|
|
|
2021-11-01 19:57:53 +00:00
|
|
|
|
def store_mp3(%Song{} = song, tmp_path) do
|
2021-11-05 19:57:33 +00:00
|
|
|
|
File.mkdir_p!(Path.dirname(song.mp3_filepath))
|
|
|
|
|
File.cp!(tmp_path, song.mp3_filepath)
|
2021-11-01 19:57:53 +00:00
|
|
|
|
end
|
|
|
|
|
|
2021-11-05 00:49:19 +00:00
|
|
|
|
def put_stats(%Ecto.Changeset{} = changeset, %MP3Stat{} = stat) do
|
2021-11-10 15:10:43 +00:00
|
|
|
|
chset = Song.put_duration(changeset, stat.duration)
|
|
|
|
|
if error = chset.errors[:duration] do
|
|
|
|
|
{:error, %{duration: error}}
|
|
|
|
|
else
|
|
|
|
|
{:ok, chset}
|
|
|
|
|
end
|
2021-11-05 00:49:19 +00:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def import_songs(%Accounts.User{} = user, changesets, consume_file)
|
|
|
|
|
when is_map(changesets) and is_function(consume_file, 2) do
|
2021-11-05 19:57:33 +00:00
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
case LiveBeats.Repo.transaction(multi) do
|
2021-11-01 19:57:53 +00:00
|
|
|
|
{:ok, results} ->
|
|
|
|
|
{:ok,
|
|
|
|
|
results
|
|
|
|
|
|> Enum.filter(&match?({{:song, _ref}, _}, &1))
|
|
|
|
|
|> Enum.map(fn {{:song, ref}, song} ->
|
2021-11-05 00:49:19 +00:00
|
|
|
|
consume_file.(ref, fn tmp_path -> store_mp3(song, tmp_path) end)
|
2021-11-01 19:57:53 +00:00
|
|
|
|
{ref, song}
|
|
|
|
|
end)
|
|
|
|
|
|> Enum.into(%{})}
|
|
|
|
|
|
|
|
|
|
{:error, _failed_op, _failed_val, _changes} ->
|
|
|
|
|
{:error, :invalid}
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def parse_file_name(name) do
|
|
|
|
|
case Regex.split(~r/[-–]/, Path.rootname(name), parts: 2) do
|
|
|
|
|
[title] -> %{title: String.trim(title), artist: nil}
|
|
|
|
|
[title, artist] -> %{title: String.trim(title), artist: String.trim(artist)}
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2021-10-29 16:12:23 +00:00
|
|
|
|
def create_genre(attrs \\ %{}) do
|
|
|
|
|
%Genre{}
|
|
|
|
|
|> Genre.changeset(attrs)
|
|
|
|
|
|> Repo.insert()
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def list_genres do
|
|
|
|
|
Repo.all(Genre, order_by: [asc: :title])
|
|
|
|
|
end
|
|
|
|
|
|
2021-11-05 00:49:19 +00:00
|
|
|
|
def list_songs(limit \\ 100) do
|
2021-11-05 19:57:33 +00:00
|
|
|
|
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
|
2021-10-29 16:12:23 +00:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def get_song!(id), do: Repo.get!(Song, id)
|
|
|
|
|
|
|
|
|
|
def create_song(attrs \\ %{}) do
|
|
|
|
|
%Song{}
|
|
|
|
|
|> Song.changeset(attrs)
|
|
|
|
|
|> Repo.insert()
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def update_song(%Song{} = song, attrs) do
|
|
|
|
|
song
|
|
|
|
|
|> Song.changeset(attrs)
|
|
|
|
|
|> Repo.update()
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def delete_song(%Song{} = song) do
|
2021-11-05 19:57:33 +00:00
|
|
|
|
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
|
|
|
|
|
|
2021-10-29 16:12:23 +00:00
|
|
|
|
Repo.delete(song)
|
|
|
|
|
end
|
|
|
|
|
|
2021-11-08 19:53:02 +00:00
|
|
|
|
def change_song(song_or_changeset, attrs \\ %{})
|
2021-11-08 18:46:23 +00:00
|
|
|
|
|
2021-11-08 19:53:02 +00:00
|
|
|
|
def change_song(%Song{} = song, attrs) do
|
|
|
|
|
Song.changeset(song, attrs)
|
2021-10-29 16:12:23 +00:00
|
|
|
|
end
|
2021-11-08 18:46:23 +00:00
|
|
|
|
|
2021-11-08 19:53:02 +00:00
|
|
|
|
def change_song(%Ecto.Changeset{} = prev_changeset, attrs) do
|
|
|
|
|
%Song{}
|
|
|
|
|
|> change_song(attrs)
|
|
|
|
|
|> Ecto.Changeset.change(Map.take(prev_changeset.changes, [:duration]))
|
|
|
|
|
end
|
2021-10-29 16:12:23 +00:00
|
|
|
|
end
|