mirror of
https://github.com/fly-apps/live_beats.git
synced 2024-11-24 17:01:00 +00:00
commit
75d82eed3f
4 changed files with 205 additions and 28 deletions
|
@ -25,7 +25,8 @@ defmodule LiveBeats.Application do
|
||||||
presence: LiveBeatsWeb.Presence,
|
presence: LiveBeatsWeb.Presence,
|
||||||
name: PresenceClient},
|
name: PresenceClient},
|
||||||
# Start the Endpoint (http/https)
|
# Start the Endpoint (http/https)
|
||||||
LiveBeatsWeb.Endpoint
|
LiveBeatsWeb.Endpoint,
|
||||||
|
{LiveBeats.SongsCleaner, count: 7, interval: :day}
|
||||||
|
|
||||||
# Start a worker by calling: LiveBeats.Worker.start_link(arg)
|
# Start a worker by calling: LiveBeats.Worker.start_link(arg)
|
||||||
# {LiveBeats.Worker, arg}
|
# {LiveBeats.Worker, arg}
|
||||||
|
|
|
@ -191,7 +191,8 @@ defmodule LiveBeats.MediaLibrary do
|
||||||
consume_file.(ref, fn tmp_path -> store_mp3(song, tmp_path) end)
|
consume_file.(ref, fn tmp_path -> store_mp3(song, tmp_path) end)
|
||||||
{ref, song}
|
{ref, song}
|
||||||
end)
|
end)
|
||||||
broadcast_imported(user, songs)
|
|
||||||
|
broadcast_imported(user, songs)
|
||||||
|
|
||||||
{:ok, Enum.into(songs, %{})}
|
{:ok, Enum.into(songs, %{})}
|
||||||
|
|
||||||
|
@ -336,6 +337,74 @@ defmodule LiveBeats.MediaLibrary do
|
||||||
end
|
end
|
||||||
|
|
||||||
def delete_song(%Song{} = song) do
|
def delete_song(%Song{} = song) do
|
||||||
|
delete_song_file(song)
|
||||||
|
|
||||||
|
Ecto.Multi.new()
|
||||||
|
|> Ecto.Multi.delete(:delete, song)
|
||||||
|
|> update_user_songs_count(song.user_id, -1)
|
||||||
|
|> Repo.transaction()
|
||||||
|
|> case do
|
||||||
|
{:ok, _} -> :ok
|
||||||
|
other -> other
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def expire_songs_older_than(count, interval) when interval in [:month, :day, :second] do
|
||||||
|
Ecto.Multi.new()
|
||||||
|
|> Ecto.Multi.delete_all(
|
||||||
|
:delete_expired_songs,
|
||||||
|
from(s in Song,
|
||||||
|
where: s.inserted_at < from_now(^(-count), ^to_string(interval)),
|
||||||
|
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
|
case File.rm(song.mp3_filepath) do
|
||||||
:ok ->
|
:ok ->
|
||||||
:ok
|
:ok
|
||||||
|
@ -345,24 +414,6 @@ defmodule LiveBeats.MediaLibrary do
|
||||||
"unable to delete song #{song.id} at #{song.mp3_filepath}, got: #{inspect(reason)}"
|
"unable to delete song #{song.id} at #{song.mp3_filepath}, got: #{inspect(reason)}"
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
Ecto.Multi.new()
|
|
||||||
|> Ecto.Multi.delete(:delete, song)
|
|
||||||
|> Ecto.Multi.update_all(
|
|
||||||
:update_songs_count,
|
|
||||||
fn _ ->
|
|
||||||
from(u in Accounts.User,
|
|
||||||
where: u.id == ^song.user_id,
|
|
||||||
update: [inc: [songs_count: -1]]
|
|
||||||
)
|
|
||||||
end,
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
|> Repo.transaction()
|
|
||||||
|> case do
|
|
||||||
{:ok, _} -> :ok
|
|
||||||
other -> other
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def change_song(song_or_changeset, attrs \\ %{})
|
def change_song(song_or_changeset, attrs \\ %{})
|
||||||
|
|
34
lib/live_beats/songs_cleaner.ex
Normal file
34
lib/live_beats/songs_cleaner.ex
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
defmodule LiveBeats.SongsCleaner do
|
||||||
|
@moduledoc """
|
||||||
|
Expire user songs using a polling interval.
|
||||||
|
"""
|
||||||
|
use GenServer
|
||||||
|
|
||||||
|
alias LiveBeats.MediaLibrary
|
||||||
|
|
||||||
|
@poll_interval :timer.minutes(60)
|
||||||
|
|
||||||
|
def start_link(opts) do
|
||||||
|
GenServer.start_link(__MODULE__, opts)
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def init(opts) do
|
||||||
|
count = Keyword.fetch!(opts, :count)
|
||||||
|
interval = Keyword.fetch!(opts, :interval)
|
||||||
|
MediaLibrary.expire_songs_older_than(count, interval)
|
||||||
|
|
||||||
|
{:ok, schedule_cleanup(%{count: count, interval: interval})}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info(:remove_songs, %{count: count, interval: interval} = state) do
|
||||||
|
MediaLibrary.expire_songs_older_than(count, interval)
|
||||||
|
{:noreply, schedule_cleanup(state)}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp schedule_cleanup(state) do
|
||||||
|
Process.send_after(self(), :remove_songs, @poll_interval)
|
||||||
|
state
|
||||||
|
end
|
||||||
|
end
|
|
@ -2,14 +2,20 @@ defmodule LiveBeats.MediaLibraryTest do
|
||||||
use LiveBeats.DataCase
|
use LiveBeats.DataCase
|
||||||
|
|
||||||
alias LiveBeats.MediaLibrary
|
alias LiveBeats.MediaLibrary
|
||||||
|
alias LiveBeats.Accounts
|
||||||
|
alias LiveBeats.MediaLibrary.Song
|
||||||
|
import LiveBeats.AccountsFixtures
|
||||||
|
import LiveBeats.MediaLibraryFixtures
|
||||||
|
|
||||||
describe "songs" do
|
describe "songs" do
|
||||||
alias LiveBeats.MediaLibrary.Song
|
@invalid_attrs %{
|
||||||
|
album_artist: nil,
|
||||||
import LiveBeats.AccountsFixtures
|
artist: nil,
|
||||||
import LiveBeats.MediaLibraryFixtures
|
date_recorded: nil,
|
||||||
|
date_released: nil,
|
||||||
@invalid_attrs %{album_artist: nil, artist: nil, date_recorded: nil, date_released: nil, duration: nil, title: nil}
|
duration: nil,
|
||||||
|
title: nil
|
||||||
|
}
|
||||||
|
|
||||||
test "list_profile_songs/1 returns all songs for a profile" do
|
test "list_profile_songs/1 returns all songs for a profile" do
|
||||||
user = user_fixture()
|
user = user_fixture()
|
||||||
|
@ -25,7 +31,15 @@ defmodule LiveBeats.MediaLibraryTest do
|
||||||
|
|
||||||
test "update_song/2 with valid data updates the song" do
|
test "update_song/2 with valid data updates the song" do
|
||||||
song = song_fixture()
|
song = song_fixture()
|
||||||
update_attrs = %{album_artist: "some updated album_artist", artist: "some updated artist", date_recorded: ~N[2021-10-27 20:11:00], date_released: ~N[2021-10-27 20:11:00], duration: 43, title: "some updated title"}
|
|
||||||
|
update_attrs = %{
|
||||||
|
album_artist: "some updated album_artist",
|
||||||
|
artist: "some updated artist",
|
||||||
|
date_recorded: ~N[2021-10-27 20:11:00],
|
||||||
|
date_released: ~N[2021-10-27 20:11:00],
|
||||||
|
duration: 43,
|
||||||
|
title: "some updated title"
|
||||||
|
}
|
||||||
|
|
||||||
assert {:ok, %Song{} = song} = MediaLibrary.update_song(song, update_attrs)
|
assert {:ok, %Song{} = song} = MediaLibrary.update_song(song, update_attrs)
|
||||||
assert song.album_artist == "some updated album_artist"
|
assert song.album_artist == "some updated album_artist"
|
||||||
|
@ -42,11 +56,17 @@ defmodule LiveBeats.MediaLibraryTest do
|
||||||
assert song == MediaLibrary.get_song!(song.id)
|
assert song == MediaLibrary.get_song!(song.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "delete_song/1 deletes the song" do
|
test "delete_song/1 deletes the song and decrement the user's songs_count" do
|
||||||
user = user_fixture()
|
user = user_fixture()
|
||||||
|
|
||||||
|
user
|
||||||
|
|> Ecto.Changeset.change(songs_count: 10)
|
||||||
|
|> LiveBeats.Repo.update()
|
||||||
|
|
||||||
song = song_fixture(%{user_id: user.id})
|
song = song_fixture(%{user_id: user.id})
|
||||||
assert :ok = MediaLibrary.delete_song(song)
|
assert :ok = MediaLibrary.delete_song(song)
|
||||||
assert_raise Ecto.NoResultsError, fn -> MediaLibrary.get_song!(song.id) end
|
assert_raise Ecto.NoResultsError, fn -> MediaLibrary.get_song!(song.id) end
|
||||||
|
assert Accounts.get_user(user.id).songs_count == 9
|
||||||
end
|
end
|
||||||
|
|
||||||
test "change_song/1 returns a song changeset" do
|
test "change_song/1 returns a song changeset" do
|
||||||
|
@ -54,4 +74,75 @@ defmodule LiveBeats.MediaLibraryTest do
|
||||||
assert %Ecto.Changeset{} = MediaLibrary.change_song(song)
|
assert %Ecto.Changeset{} = MediaLibrary.change_song(song)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "expire_songs_older_than/2" do
|
||||||
|
setup do
|
||||||
|
today = DateTime.utc_now()
|
||||||
|
|
||||||
|
creation_dates = Enum.map([-1, -3, -4], &add_n_months(today, &1))
|
||||||
|
|
||||||
|
%{creation_dates: creation_dates}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "deletes the songs expired before the required interval", %{
|
||||||
|
creation_dates: [one_month_ago, three_months_ago, four_months_ago]
|
||||||
|
} do
|
||||||
|
user = user_fixture()
|
||||||
|
|
||||||
|
expired_song_1 =
|
||||||
|
song_fixture(user_id: user.id, title: "song1", inserted_at: four_months_ago)
|
||||||
|
|
||||||
|
expired_song_2 =
|
||||||
|
song_fixture(user_id: user.id, title: "song2", inserted_at: three_months_ago)
|
||||||
|
|
||||||
|
active_song = song_fixture(user_id: user.id, title: "song3", inserted_at: one_month_ago)
|
||||||
|
|
||||||
|
MediaLibrary.expire_songs_older_than(2, :month)
|
||||||
|
|
||||||
|
assert_raise Ecto.NoResultsError, fn -> MediaLibrary.get_song!(expired_song_1.id) end
|
||||||
|
assert_raise Ecto.NoResultsError, fn -> MediaLibrary.get_song!(expired_song_2.id) end
|
||||||
|
assert active_song == MediaLibrary.get_song!(active_song.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "Users song_count is decremented when user songs are deleted", %{
|
||||||
|
creation_dates: creation_dates
|
||||||
|
} do
|
||||||
|
user = user_fixture()
|
||||||
|
|
||||||
|
songs_changesets =
|
||||||
|
["1", "2", "3"]
|
||||||
|
|> Enum.reduce(%{}, fn song_number, acc ->
|
||||||
|
song_changeset =
|
||||||
|
Song.changeset(%Song{}, %{title: "song#{song_number}", artist: "artist_one"})
|
||||||
|
|
||||||
|
Map.put_new(acc, song_number, song_changeset)
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert {:ok, results} =
|
||||||
|
MediaLibrary.import_songs(user, songs_changesets, fn one, two -> {one, two} end)
|
||||||
|
|
||||||
|
assert Accounts.get_user(user.id).songs_count == 3
|
||||||
|
|
||||||
|
created_songs = Enum.reduce(results, [], fn {_key, song}, acc -> [song | acc] end)
|
||||||
|
|
||||||
|
for {song, date} <- Enum.zip(created_songs, creation_dates) do
|
||||||
|
song
|
||||||
|
|> Ecto.Changeset.change(inserted_at: date)
|
||||||
|
|> LiveBeats.Repo.update()
|
||||||
|
end
|
||||||
|
|
||||||
|
MediaLibrary.expire_songs_older_than(2, :month)
|
||||||
|
|
||||||
|
assert Accounts.get_user(user.id).songs_count == 1
|
||||||
|
end
|
||||||
|
|
||||||
|
defp add_n_months(datetime, n) do
|
||||||
|
seconds = 30 * (60 * 60 * 24) * n
|
||||||
|
|
||||||
|
datetime
|
||||||
|
|> DateTime.add(seconds, :second)
|
||||||
|
|> DateTime.to_naive()
|
||||||
|
|> NaiveDateTime.truncate(:second)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue