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-11-12 11:42:07 +00:00
|
|
|
|
alias LiveBeats.MediaLibrary.{Profile, Song, Events, 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-10 19:29:53 +00:00
|
|
|
|
@auto_next_threshold_seconds 5
|
2021-11-18 21:36:51 +00:00
|
|
|
|
@max_songs 30
|
2021-11-05 00:49:19 +00:00
|
|
|
|
|
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-12 17:41:16 +00:00
|
|
|
|
def attach do
|
2021-11-12 19:20:37 +00:00
|
|
|
|
LiveBeats.attach(__MODULE__, to: {Accounts, Accounts.Events.PublicSettingsChanged})
|
2021-11-12 17:41:16 +00:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def handle_execute({Accounts, %Accounts.Events.PublicSettingsChanged{user: user}}) do
|
|
|
|
|
profile = get_profile!(user)
|
|
|
|
|
broadcast!(user.id, %Events.PublicProfileUpdated{profile: profile})
|
|
|
|
|
end
|
|
|
|
|
|
2021-11-12 11:21:12 +00:00
|
|
|
|
def subscribe_to_profile(%Profile{} = profile) do
|
2021-11-12 03:42:10 +00:00
|
|
|
|
Phoenix.PubSub.subscribe(@pubsub, topic(profile.user_id))
|
|
|
|
|
end
|
|
|
|
|
|
2022-01-29 01:40:48 +00:00
|
|
|
|
def broadcast_ping(%Accounts.User{} = user, rtt, region) do
|
2022-01-31 13:21:27 +00:00
|
|
|
|
broadcast!(
|
|
|
|
|
user.active_profile_user_id,
|
|
|
|
|
{:ping, %{user: user, rtt: rtt, region: region}}
|
|
|
|
|
)
|
2022-01-29 01:40:48 +00:00
|
|
|
|
end
|
|
|
|
|
|
2021-11-12 03:42:10 +00:00
|
|
|
|
def unsubscribe_to_profile(%Profile{} = profile) do
|
|
|
|
|
Phoenix.PubSub.unsubscribe(@pubsub, topic(profile.user_id))
|
2021-11-05 00:49:19 +00:00
|
|
|
|
end
|
|
|
|
|
|
2021-11-06 03:02:31 +00:00
|
|
|
|
def local_filepath(filename_uuid) when is_binary(filename_uuid) do
|
2021-11-17 03:11:11 +00:00
|
|
|
|
dir = LiveBeats.config([:files, :uploads_dir])
|
|
|
|
|
Path.join([dir, "songs", filename_uuid])
|
2021-11-06 03:02:31 +00:00
|
|
|
|
end
|
|
|
|
|
|
2021-11-12 03:42:10 +00:00
|
|
|
|
def can_control_playback?(%Accounts.User{} = user, %Song{} = song) do
|
|
|
|
|
user.id == song.user_id
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def play_song(%Song{id: id}) do
|
|
|
|
|
play_song(id)
|
|
|
|
|
end
|
2021-11-05 00:49:19 +00:00
|
|
|
|
|
|
|
|
|
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)
|
2021-11-12 03:42:10 +00:00
|
|
|
|
|
2021-11-12 17:41:16 +00:00
|
|
|
|
broadcast!(song.user_id, %Events.Play{song: song, elapsed: elapsed})
|
2021-11-12 03:42:10 +00:00
|
|
|
|
|
|
|
|
|
new_song
|
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-12 17:41:16 +00:00
|
|
|
|
broadcast!(song.user_id, %Events.Pause{song: song})
|
2021-11-05 00:49:19 +00:00
|
|
|
|
end
|
|
|
|
|
|
2021-11-12 03:42:10 +00:00
|
|
|
|
def play_next_song_auto(%Profile{} = profile) do
|
|
|
|
|
song = get_current_active_song(profile) || get_first_song(profile)
|
2021-11-10 19:29:53 +00:00
|
|
|
|
|
|
|
|
|
if song && elapsed_playback(song) >= song.duration - @auto_next_threshold_seconds do
|
|
|
|
|
song
|
2021-11-12 03:42:10 +00:00
|
|
|
|
|> get_next_song(profile)
|
2021-11-10 19:29:53 +00:00
|
|
|
|
|> play_song()
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2021-11-12 03:42:10 +00:00
|
|
|
|
def play_prev_song(%Profile{} = profile) do
|
|
|
|
|
song = get_current_active_song(profile) || get_first_song(profile)
|
2021-11-10 19:59:13 +00:00
|
|
|
|
|
2021-11-12 03:42:10 +00:00
|
|
|
|
if prev_song = get_prev_song(song, profile) do
|
2021-11-10 19:49:38 +00:00
|
|
|
|
play_song(prev_song)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2021-11-12 03:42:10 +00:00
|
|
|
|
def play_next_song(%Profile{} = profile) do
|
|
|
|
|
song = get_current_active_song(profile) || get_first_song(profile)
|
2021-11-10 19:59:13 +00:00
|
|
|
|
|
2021-11-12 03:42:10 +00:00
|
|
|
|
if next_song = get_next_song(song, profile) do
|
2021-11-10 19:49:38 +00:00
|
|
|
|
play_song(next_song)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
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
|
2022-01-27 18:03:42 +00:00
|
|
|
|
chset = Song.put_stats(changeset, stat)
|
2021-11-10 19:29:53 +00:00
|
|
|
|
|
2021-11-10 15:10:43 +00:00
|
|
|
|
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-19 15:51:50 +00:00
|
|
|
|
# refetch user for fresh song count
|
|
|
|
|
user = Accounts.get_user!(user.id)
|
|
|
|
|
|
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()
|
2021-11-17 15:40:31 +00:00
|
|
|
|
|> Song.put_server_ip()
|
2021-11-05 19:57:33 +00:00
|
|
|
|
|
|
|
|
|
Ecto.Multi.insert(acc, {:song, ref}, chset)
|
|
|
|
|
end)
|
2021-11-19 15:51:50 +00:00
|
|
|
|
|> Ecto.Multi.run(:valid_songs_count, fn _repo, changes ->
|
2021-11-18 21:36:51 +00:00
|
|
|
|
new_songs_count = changes |> Enum.filter(&match?({{:song, _ref}, _}, &1)) |> Enum.count()
|
2021-11-19 15:51:50 +00:00
|
|
|
|
validate_songs_limit(user.songs_count, new_songs_count)
|
2021-11-18 21:36:51 +00:00
|
|
|
|
end)
|
|
|
|
|
|> Ecto.Multi.update_all(
|
2021-11-19 15:51:50 +00:00
|
|
|
|
:update_songs_count,
|
|
|
|
|
fn %{valid_songs_count: new_count} ->
|
2021-11-18 21:36:51 +00:00
|
|
|
|
from(u in Accounts.User,
|
2021-11-19 15:51:50 +00:00
|
|
|
|
where: u.id == ^user.id and u.songs_count == ^user.songs_count,
|
|
|
|
|
update: [inc: [songs_count: ^new_count]]
|
2021-11-18 21:36:51 +00:00
|
|
|
|
)
|
|
|
|
|
end,
|
|
|
|
|
[]
|
|
|
|
|
)
|
2021-11-19 15:51:50 +00:00
|
|
|
|
|> Ecto.Multi.run(:is_songs_count_updated?, fn _repo, %{update_songs_count: result} ->
|
2021-11-18 21:36:51 +00:00
|
|
|
|
case result do
|
|
|
|
|
{1, _} -> {:ok, user}
|
|
|
|
|
_ -> {:error, :invalid}
|
|
|
|
|
end
|
2021-11-17 21:31:34 +00:00
|
|
|
|
end)
|
2021-11-05 19:57:33 +00:00
|
|
|
|
|
|
|
|
|
case LiveBeats.Repo.transaction(multi) do
|
2021-11-01 19:57:53 +00:00
|
|
|
|
{:ok, results} ->
|
2021-12-16 02:51:09 +00:00
|
|
|
|
songs =
|
|
|
|
|
results
|
|
|
|
|
|> Enum.filter(&match?({{:song, _ref}, _}, &1))
|
|
|
|
|
|> Enum.map(fn {{:song, ref}, song} ->
|
|
|
|
|
consume_file.(ref, fn tmp_path -> store_mp3(song, tmp_path) end)
|
|
|
|
|
{ref, song}
|
|
|
|
|
end)
|
2022-01-11 18:59:31 +00:00
|
|
|
|
|
|
|
|
|
broadcast_imported(user, songs)
|
2021-12-16 02:51:09 +00:00
|
|
|
|
|
|
|
|
|
{:ok, Enum.into(songs, %{})}
|
2021-11-01 19:57:53 +00:00
|
|
|
|
|
2021-11-17 21:31:34 +00:00
|
|
|
|
{:error, failed_op, failed_val, _changes} ->
|
2021-11-18 21:36:51 +00:00
|
|
|
|
failed_op =
|
|
|
|
|
case failed_op do
|
2021-11-19 19:01:15 +00:00
|
|
|
|
{:song, _number} -> "Invalid song (#{failed_val.changes.title})"
|
2021-11-19 15:51:50 +00:00
|
|
|
|
:is_songs_count_updated? -> :invalid
|
2021-11-18 21:36:51 +00:00
|
|
|
|
failed_op -> failed_op
|
2021-11-17 21:31:34 +00:00
|
|
|
|
end
|
|
|
|
|
|
2021-11-18 21:36:51 +00:00
|
|
|
|
{:error, {failed_op, failed_val}}
|
2021-11-01 19:57:53 +00:00
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2021-12-16 02:51:09 +00:00
|
|
|
|
defp broadcast_imported(%Accounts.User{} = user, songs) do
|
|
|
|
|
songs = Enum.map(songs, fn {_ref, song} -> song end)
|
|
|
|
|
broadcast!(user.id, %Events.SongsImported{user_id: user.id, songs: songs})
|
|
|
|
|
end
|
|
|
|
|
|
2021-11-01 19:57:53 +00:00
|
|
|
|
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
|
2022-01-28 01:42:26 +00:00
|
|
|
|
Repo.replica().all(Genre, order_by: [asc: :title])
|
2021-10-29 16:12:23 +00:00
|
|
|
|
end
|
|
|
|
|
|
2021-11-12 03:42:10 +00:00
|
|
|
|
def list_profile_songs(%Profile{} = profile, limit \\ 100) do
|
|
|
|
|
from(s in Song, where: s.user_id == ^profile.user_id, limit: ^limit)
|
2021-11-10 19:59:13 +00:00
|
|
|
|
|> order_by_playlist(:asc)
|
2022-01-28 01:42:26 +00:00
|
|
|
|
|> Repo.replica().all()
|
2021-11-05 19:57:33 +00:00
|
|
|
|
end
|
|
|
|
|
|
2021-11-12 15:10:04 +00:00
|
|
|
|
def list_active_profiles(opts) do
|
|
|
|
|
from(s in Song,
|
|
|
|
|
inner_join: u in LiveBeats.Accounts.User,
|
|
|
|
|
on: s.user_id == u.id,
|
|
|
|
|
where: s.status in [:playing],
|
|
|
|
|
limit: ^Keyword.fetch!(opts, :limit),
|
|
|
|
|
order_by: [desc: s.updated_at],
|
2021-11-16 16:58:22 +00:00
|
|
|
|
select: struct(u, [:id, :username, :profile_tagline, :avatar_url, :external_homepage_url])
|
2021-11-12 15:10:04 +00:00
|
|
|
|
)
|
2022-01-28 01:42:26 +00:00
|
|
|
|
|> Repo.replica().all()
|
2021-11-12 15:10:04 +00:00
|
|
|
|
|> Enum.map(&get_profile!/1)
|
|
|
|
|
end
|
|
|
|
|
|
2021-11-12 03:42:10 +00:00
|
|
|
|
def get_current_active_song(%Profile{user_id: user_id}) do
|
2022-01-29 01:40:48 +00:00
|
|
|
|
Repo.replica().one(
|
|
|
|
|
from s in Song, where: s.user_id == ^user_id and s.status in [:playing, :paused]
|
|
|
|
|
)
|
2021-11-05 19:57:33 +00:00
|
|
|
|
end
|
|
|
|
|
|
2021-11-12 03:42:10 +00:00
|
|
|
|
def get_profile!(%Accounts.User{} = user) do
|
2021-11-16 16:58:22 +00:00
|
|
|
|
%Profile{
|
|
|
|
|
user_id: user.id,
|
|
|
|
|
username: user.username,
|
|
|
|
|
tagline: user.profile_tagline,
|
|
|
|
|
avatar_url: user.avatar_url,
|
|
|
|
|
external_homepage_url: user.external_homepage_url
|
|
|
|
|
}
|
2021-11-12 03:42:10 +00:00
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def owns_profile?(%Accounts.User{} = user, %Profile{} = profile) do
|
|
|
|
|
user.id == profile.user_id
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def owns_song?(%Profile{} = profile, %Song{} = song) do
|
|
|
|
|
profile.user_id == song.user_id
|
|
|
|
|
end
|
|
|
|
|
|
2021-11-05 19:57:33 +00:00
|
|
|
|
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
|
|
|
|
|
|
2022-01-28 01:42:26 +00:00
|
|
|
|
def get_song!(id), do: Repo.replica().get!(Song, id)
|
2021-10-29 16:12:23 +00:00
|
|
|
|
|
2021-11-12 03:42:10 +00:00
|
|
|
|
def get_first_song(%Profile{user_id: user_id}) do
|
2021-11-10 19:59:13 +00:00
|
|
|
|
from(s in Song,
|
|
|
|
|
where: s.user_id == ^user_id,
|
|
|
|
|
limit: 1
|
2021-11-10 19:29:53 +00:00
|
|
|
|
)
|
2021-11-10 19:59:13 +00:00
|
|
|
|
|> order_by_playlist(:asc)
|
2022-01-28 01:42:26 +00:00
|
|
|
|
|> Repo.replica().one()
|
2021-11-10 19:29:53 +00:00
|
|
|
|
end
|
|
|
|
|
|
2021-11-12 03:42:10 +00:00
|
|
|
|
def get_last_song(%Profile{user_id: user_id}) do
|
2021-11-10 19:59:13 +00:00
|
|
|
|
from(s in Song,
|
|
|
|
|
where: s.user_id == ^user_id,
|
|
|
|
|
limit: 1
|
2021-11-10 19:49:38 +00:00
|
|
|
|
)
|
2021-11-10 19:59:13 +00:00
|
|
|
|
|> order_by_playlist(:desc)
|
2022-01-28 01:42:26 +00:00
|
|
|
|
|> Repo.replica().one()
|
2021-11-10 19:49:38 +00:00
|
|
|
|
end
|
2021-11-10 19:59:13 +00:00
|
|
|
|
|
2021-11-12 03:42:10 +00:00
|
|
|
|
def get_next_song(%Song{} = song, %Profile{} = profile) do
|
2021-11-10 19:59:13 +00:00
|
|
|
|
next =
|
|
|
|
|
from(s in Song,
|
2021-11-10 19:29:53 +00:00
|
|
|
|
where: s.user_id == ^song.user_id and s.id > ^song.id,
|
|
|
|
|
limit: 1
|
2021-11-10 19:59:13 +00:00
|
|
|
|
)
|
|
|
|
|
|> order_by_playlist(:asc)
|
2022-01-28 01:42:26 +00:00
|
|
|
|
|> Repo.replica().one()
|
2021-11-10 19:59:13 +00:00
|
|
|
|
|
2021-11-12 03:42:10 +00:00
|
|
|
|
next || get_first_song(profile)
|
2021-11-10 19:29:53 +00:00
|
|
|
|
end
|
|
|
|
|
|
2021-11-12 03:42:10 +00:00
|
|
|
|
def get_prev_song(%Song{} = song, %Profile{} = profile) do
|
2021-11-10 19:59:13 +00:00
|
|
|
|
prev =
|
|
|
|
|
from(s in Song,
|
2021-11-10 19:49:38 +00:00
|
|
|
|
where: s.user_id == ^song.user_id and s.id < ^song.id,
|
|
|
|
|
order_by: [desc: s.inserted_at, desc: s.id],
|
|
|
|
|
limit: 1
|
2021-11-10 19:59:13 +00:00
|
|
|
|
)
|
|
|
|
|
|> order_by_playlist(:desc)
|
2022-01-28 01:42:26 +00:00
|
|
|
|
|> Repo.replica().one()
|
2021-11-10 19:59:13 +00:00
|
|
|
|
|
2021-11-12 03:42:10 +00:00
|
|
|
|
prev || get_last_song(profile)
|
2021-11-10 19:49:38 +00:00
|
|
|
|
end
|
|
|
|
|
|
2021-10-29 16:12:23 +00:00
|
|
|
|
def update_song(%Song{} = song, attrs) do
|
|
|
|
|
song
|
|
|
|
|
|> Song.changeset(attrs)
|
|
|
|
|
|> Repo.update()
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def delete_song(%Song{} = song) do
|
2022-01-18 17:12:44 +00:00
|
|
|
|
delete_song_file(song)
|
2021-11-05 19:57:33 +00:00
|
|
|
|
|
2021-11-17 21:32:09 +00:00
|
|
|
|
Ecto.Multi.new()
|
|
|
|
|
|> Ecto.Multi.delete(:delete, song)
|
2022-01-18 17:12:44 +00:00
|
|
|
|
|> update_user_songs_count(song.user_id, -1)
|
2021-11-17 21:32:09 +00:00
|
|
|
|
|> Repo.transaction()
|
2021-12-14 15:35:51 +00:00
|
|
|
|
|> case do
|
|
|
|
|
{:ok, _} -> :ok
|
|
|
|
|
other -> other
|
|
|
|
|
end
|
2021-10-29 16:12:23 +00:00
|
|
|
|
end
|
|
|
|
|
|
2022-01-17 19:13:59 +00:00
|
|
|
|
def expire_songs_older_than(count, interval) when interval in [:month, :day, :second] do
|
2022-01-31 13:41:34 +00:00
|
|
|
|
server_ip = LiveBeats.config([:files, :server_ip])
|
|
|
|
|
|
2022-01-11 18:59:31 +00:00
|
|
|
|
Ecto.Multi.new()
|
|
|
|
|
|> Ecto.Multi.delete_all(
|
|
|
|
|
:delete_expired_songs,
|
|
|
|
|
from(s in Song,
|
2022-01-17 19:13:59 +00:00
|
|
|
|
where: s.inserted_at < from_now(^(-count), ^to_string(interval)),
|
2022-01-31 13:41:34 +00:00
|
|
|
|
where: s.server_ip == ^server_ip,
|
2022-01-11 18:59:31 +00:00
|
|
|
|
select: %{user_id: s.user_id, mp3_filepath: s.mp3_filepath}
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|> Ecto.Multi.merge(&update_users_songs_count(&1))
|
|
|
|
|
|> Repo.transaction()
|
|
|
|
|
|> case do
|
|
|
|
|
{:ok, transaction_result} ->
|
|
|
|
|
{_deleted_songs_count, deleted_songs} = transaction_result.delete_expired_songs
|
|
|
|
|
Enum.each(deleted_songs, &delete_song_file/1)
|
|
|
|
|
|
|
|
|
|
error ->
|
|
|
|
|
error
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
defp update_users_songs_count(%{delete_expired_songs: results}) do
|
|
|
|
|
{_deleted_songs_count, deleted_songs} = results
|
|
|
|
|
|
|
|
|
|
deleted_songs
|
|
|
|
|
|> Enum.reduce(%{}, &acc_user_songs/2)
|
|
|
|
|
|> Enum.reduce(Ecto.Multi.new(), &decrement_user_songs_count/2)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
defp decrement_user_songs_count({user_id, deleted_songs_count}, multi) do
|
|
|
|
|
update_user_songs_count(multi, user_id, deleted_songs_count * -1)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
defp update_user_songs_count(multi, user_id, songs_count) do
|
|
|
|
|
Ecto.Multi.update_all(
|
|
|
|
|
multi,
|
|
|
|
|
"update_songs_count_user_#{user_id}",
|
|
|
|
|
fn _ ->
|
|
|
|
|
from(u in Accounts.User,
|
|
|
|
|
where: u.id == ^user_id,
|
|
|
|
|
update: [inc: [songs_count: ^songs_count]]
|
|
|
|
|
)
|
|
|
|
|
end,
|
|
|
|
|
[]
|
|
|
|
|
)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
defp acc_user_songs(%{user_id: user_id} = _song, songs_acc) do
|
|
|
|
|
if Map.has_key?(songs_acc, user_id) do
|
|
|
|
|
Map.put(songs_acc, user_id, songs_acc[user_id] + 1)
|
|
|
|
|
else
|
|
|
|
|
Map.put_new(songs_acc, user_id, 1)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
defp delete_song_file(song) do
|
|
|
|
|
case File.rm(song.mp3_filepath) do
|
|
|
|
|
:ok ->
|
|
|
|
|
:ok
|
|
|
|
|
|
|
|
|
|
{:error, reason} ->
|
|
|
|
|
Logger.info(
|
|
|
|
|
"unable to delete song #{song.id} at #{song.mp3_filepath}, got: #{inspect(reason)}"
|
|
|
|
|
)
|
|
|
|
|
end
|
|
|
|
|
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
|
|
|
|
|
2022-01-27 18:03:42 +00:00
|
|
|
|
@keep_changes [:duration, :mp3_filesize, :mp3_filepath]
|
2021-11-08 19:53:02 +00:00
|
|
|
|
def change_song(%Ecto.Changeset{} = prev_changeset, attrs) do
|
|
|
|
|
%Song{}
|
|
|
|
|
|> change_song(attrs)
|
2022-01-27 18:03:42 +00:00
|
|
|
|
|> Ecto.Changeset.change(Map.take(prev_changeset.changes, @keep_changes))
|
2021-11-08 19:53:02 +00:00
|
|
|
|
end
|
2021-11-10 19:59:13 +00:00
|
|
|
|
|
|
|
|
|
defp order_by_playlist(%Ecto.Query{} = query, direction) when direction in [:asc, :desc] do
|
|
|
|
|
from(s in query, order_by: [{^direction, s.inserted_at}, {^direction, s.id}])
|
|
|
|
|
end
|
2021-11-12 17:41:16 +00:00
|
|
|
|
|
|
|
|
|
defp broadcast!(user_id, msg) when is_integer(user_id) do
|
|
|
|
|
Phoenix.PubSub.broadcast!(@pubsub, topic(user_id), {__MODULE__, msg})
|
|
|
|
|
end
|
2021-11-15 17:42:12 +00:00
|
|
|
|
|
|
|
|
|
defp topic(user_id) when is_integer(user_id), do: "profile:#{user_id}"
|
2021-11-18 21:50:05 +00:00
|
|
|
|
|
|
|
|
|
defp validate_songs_limit(user_songs, new_songs_count) do
|
|
|
|
|
if user_songs + new_songs_count <= @max_songs do
|
|
|
|
|
{:ok, new_songs_count}
|
|
|
|
|
else
|
|
|
|
|
{:error, :songs_limit_exceeded}
|
|
|
|
|
end
|
|
|
|
|
end
|
2021-10-29 16:12:23 +00:00
|
|
|
|
end
|