Implement ML-143: Cart-style multi record import
Closes ML-143.
This commit is contained in:
@@ -1,10 +1,10 @@
|
|||||||
---
|
---
|
||||||
id: ML-143
|
id: ML-143
|
||||||
title: Cart-style multi-record import in Add Record modal
|
title: Cart-style multi-record import in Add Record modal
|
||||||
status: To Do
|
status: In Progress
|
||||||
assignee: []
|
assignee: []
|
||||||
created_date: '2026-04-20 10:00'
|
created_date: '2026-04-20 10:00'
|
||||||
updated_date: '2026-04-20 10:01'
|
updated_date: '2026-04-20 10:15'
|
||||||
labels:
|
labels:
|
||||||
- ui
|
- ui
|
||||||
- liveview
|
- liveview
|
||||||
@@ -17,6 +17,7 @@ documentation:
|
|||||||
- backlog/ml-143/plan.md
|
- backlog/ml-143/plan.md
|
||||||
- backlog/ml-143/mockups.html
|
- backlog/ml-143/mockups.html
|
||||||
priority: medium
|
priority: medium
|
||||||
|
ordinal: 1000
|
||||||
---
|
---
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
defmodule MusicLibrary.Worker.ImportFromMusicbrainzReleaseGroup do
|
||||||
|
@moduledoc """
|
||||||
|
Imports a record from a MusicBrainz release group in the background.
|
||||||
|
|
||||||
|
Used by the cart-style multi-record import when there are two or more
|
||||||
|
records to import at once.
|
||||||
|
"""
|
||||||
|
|
||||||
|
use Oban.Worker, queue: :music_brainz, max_attempts: 3
|
||||||
|
|
||||||
|
alias MusicLibrary.Records
|
||||||
|
|
||||||
|
@impl Oban.Worker
|
||||||
|
def perform(%Oban.Job{args: %{"release_group_id" => release_group_id} = args}) do
|
||||||
|
opts = [
|
||||||
|
format: args["format"],
|
||||||
|
purchased_at: parse_datetime(args["purchased_at"])
|
||||||
|
]
|
||||||
|
|
||||||
|
case Records.import_from_musicbrainz_release_group(release_group_id, opts) do
|
||||||
|
{:ok, _record} -> :ok
|
||||||
|
{:error, reason} -> {:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp parse_datetime(nil), do: nil
|
||||||
|
|
||||||
|
defp parse_datetime(str) do
|
||||||
|
{:ok, datetime, _offset} = DateTime.from_iso8601(str)
|
||||||
|
datetime
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,4 +1,16 @@
|
|||||||
defmodule MusicLibraryWeb.Components.AddRecord do
|
defmodule MusicLibraryWeb.Components.AddRecord do
|
||||||
|
@moduledoc """
|
||||||
|
Cart-style MusicBrainz import modal.
|
||||||
|
|
||||||
|
Users search MusicBrainz, stage `{release_group, format}` pairs into an ephemeral
|
||||||
|
cart, then import them all at once. A single-item cart runs synchronously via
|
||||||
|
`start_async`; two or more items enqueue one Oban job per item and close the modal.
|
||||||
|
|
||||||
|
The parent LiveView receives two messages and handles navigation/toasts:
|
||||||
|
- `{__MODULE__, {:imported_single, record}}`
|
||||||
|
- `{__MODULE__, {:imported_async, count}}`
|
||||||
|
"""
|
||||||
|
|
||||||
use MusicLibraryWeb, :live_component
|
use MusicLibraryWeb, :live_component
|
||||||
|
|
||||||
import MusicLibraryWeb.RecordComponents, only: [format_label: 1, type_label: 1]
|
import MusicLibraryWeb.RecordComponents, only: [format_label: 1, type_label: 1]
|
||||||
@@ -6,61 +18,220 @@ defmodule MusicLibraryWeb.Components.AddRecord do
|
|||||||
|
|
||||||
alias MusicBrainz.ReleaseGroupSearchResult
|
alias MusicBrainz.ReleaseGroupSearchResult
|
||||||
alias MusicLibrary.Records
|
alias MusicLibrary.Records
|
||||||
|
alias MusicLibrary.Worker.ImportFromMusicbrainzReleaseGroup
|
||||||
|
alias MusicLibraryWeb.ErrorMessages
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
@batch_size 20
|
@batch_size 20
|
||||||
|
@default_format :cd
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<div class="w-86 md:w-2xl">
|
<div class="grid grid-cols-1 md:grid-cols-5">
|
||||||
<.simple_form
|
<section class="md:col-span-3 p-4 md:border-r md:border-zinc-200 md:dark:border-zinc-800">
|
||||||
for={@form}
|
<.simple_form
|
||||||
id={:import_form}
|
for={@form}
|
||||||
phx-target={@myself}
|
id={:import_form}
|
||||||
phx-change="search"
|
phx-target={@myself}
|
||||||
phx-submit="search"
|
phx-change="search"
|
||||||
class="px-4"
|
phx-submit="search"
|
||||||
>
|
>
|
||||||
<.input
|
<.input
|
||||||
id={:mb_query}
|
id={:mb_query}
|
||||||
name={:mb_query}
|
name={:mb_query}
|
||||||
field={@form[:mb_query]}
|
field={@form[:mb_query]}
|
||||||
type="search"
|
type="search"
|
||||||
label={gettext("Search for a record")}
|
label={gettext("Search for a record")}
|
||||||
phx-debounce="500"
|
phx-debounce="500"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
autofocus
|
autofocus
|
||||||
/>
|
/>
|
||||||
</.simple_form>
|
</.simple_form>
|
||||||
<.alert :if={@error_message} color="danger" hide_close class="mx-4 mt-4">
|
<.alert :if={@error_message} color="danger" hide_close class="mt-4">
|
||||||
{@error_message}
|
{@error_message}
|
||||||
</.alert>
|
</.alert>
|
||||||
<ul
|
<ul
|
||||||
id="release-groups"
|
id="release-groups"
|
||||||
phx-update="stream"
|
phx-viewport-bottom={!@loaded_all_results? && "load-more"}
|
||||||
phx-viewport-bottom={!@loaded_all_results? && "load-more"}
|
phx-target={@myself}
|
||||||
phx-target={@myself}
|
role="list"
|
||||||
role="list"
|
class={[
|
||||||
class={[
|
"mt-5 divide-y divide-zinc-100 dark:divide-slate-300/30",
|
||||||
"mt-5 divide-y divide-zinc-100 dark:divide-slate-300/30",
|
"max-h-125 overflow-y-auto"
|
||||||
"max-h-125 overflow-y-auto"
|
]}
|
||||||
]}
|
>
|
||||||
>
|
<.result
|
||||||
<.result
|
:for={release_group <- @release_groups}
|
||||||
:for={{id, release_group} <- @streams.release_groups}
|
id={"musicbrainz_#{release_group.id}"}
|
||||||
id={id}
|
myself={@myself}
|
||||||
release_group={release_group}
|
in_cart?={in_cart?(@cart_pairs, release_group.id)}
|
||||||
icon_name={@icon_name}
|
cart_format={cart_format(@cart, release_group.id)}
|
||||||
/>
|
release_group={release_group}
|
||||||
</ul>
|
icon_name={@icon_name}
|
||||||
<div
|
/>
|
||||||
:if={@release_groups_count == 0}
|
</ul>
|
||||||
id="release-groups-empty"
|
<div
|
||||||
class="text-md flex h-32 items-center justify-center text-zinc-500 md:h-64"
|
:if={@release_groups_count == 0}
|
||||||
>
|
id="release-groups-empty"
|
||||||
{gettext("No results")}
|
class="text-md flex h-32 items-center justify-center text-zinc-500 md:h-64"
|
||||||
</div>
|
>
|
||||||
<.results_footer total_results={@release_groups_total_count} />
|
{gettext("No results")}
|
||||||
|
</div>
|
||||||
|
<.results_footer total_results={@release_groups_total_count} />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<aside class={[
|
||||||
|
"md:col-span-2 bg-zinc-50 dark:bg-zinc-900/60",
|
||||||
|
"border-t md:border-t-0 md:border-l md:border-zinc-200 md:dark:border-zinc-800",
|
||||||
|
"flex flex-col"
|
||||||
|
]}>
|
||||||
|
<div class="px-4 py-3 flex items-center justify-between border-b border-zinc-200 dark:border-zinc-800">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<p class="text-sm font-semibold text-zinc-700 dark:text-zinc-300">
|
||||||
|
{gettext("Cart")}
|
||||||
|
</p>
|
||||||
|
<span class="text-xs text-zinc-500 dark:text-zinc-400">
|
||||||
|
{ngettext("%{count} record", "%{count} records", length(@cart), count: length(@cart))}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
:if={@cart != []}
|
||||||
|
type="button"
|
||||||
|
phx-click="clear_cart"
|
||||||
|
phx-target={@myself}
|
||||||
|
class="text-xs text-zinc-500 hover:text-zinc-900 dark:hover:text-zinc-100"
|
||||||
|
>
|
||||||
|
{gettext("Clear all")}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
phx-click="toggle_cart"
|
||||||
|
phx-target={@myself}
|
||||||
|
class="rounded-md p-1 text-zinc-500 hover:bg-zinc-200 dark:hover:bg-zinc-800 md:hidden"
|
||||||
|
aria-label={gettext("Toggle cart")}
|
||||||
|
>
|
||||||
|
<.icon
|
||||||
|
name={if @cart_expanded?, do: "hero-chevron-down", else: "hero-chevron-up"}
|
||||||
|
class="size-4"
|
||||||
|
aria-hidden="true"
|
||||||
|
data-slot="icon"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class={["md:!block", not @cart_expanded? && "hidden"]}>
|
||||||
|
<div
|
||||||
|
:if={@cart == []}
|
||||||
|
id="cart-empty"
|
||||||
|
class="flex flex-col items-center justify-center gap-2 px-6 py-10 text-center"
|
||||||
|
>
|
||||||
|
<.icon
|
||||||
|
name="hero-shopping-bag"
|
||||||
|
class="size-8 text-zinc-400"
|
||||||
|
aria-hidden="true"
|
||||||
|
data-slot="icon"
|
||||||
|
/>
|
||||||
|
<p class="text-sm text-zinc-500 dark:text-zinc-400">
|
||||||
|
{gettext("Your cart is empty")}
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-zinc-500 dark:text-zinc-400">
|
||||||
|
{gettext("Add records from the search results to get started.")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul
|
||||||
|
:if={@cart != []}
|
||||||
|
id="cart-items"
|
||||||
|
class="divide-y divide-zinc-200 dark:divide-zinc-800 md:max-h-[calc(100vh-20rem)] overflow-y-auto"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
:for={item <- @cart}
|
||||||
|
id={"cart-item-#{item.cart_item_id}"}
|
||||||
|
class="flex gap-3 px-4 py-3"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="w-12 h-12 rounded-md object-cover"
|
||||||
|
alt={item.title}
|
||||||
|
src={item.thumb_url}
|
||||||
|
onerror={"this.src = '" <> ~p"/images/cover-not-found.png" <> "';"}
|
||||||
|
/>
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<p class="truncate text-xs text-zinc-500 dark:text-zinc-400">
|
||||||
|
{item.artists}
|
||||||
|
</p>
|
||||||
|
<p class="truncate text-sm font-medium text-zinc-700 dark:text-zinc-300">
|
||||||
|
{item.title}
|
||||||
|
</p>
|
||||||
|
<div class="mt-1 flex items-center gap-2">
|
||||||
|
<form phx-change="change_format" phx-target={@myself}>
|
||||||
|
<input type="hidden" name="cart_item_id" value={item.cart_item_id} />
|
||||||
|
<select
|
||||||
|
name="format"
|
||||||
|
aria-label={gettext("Format")}
|
||||||
|
class="rounded-md border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-950 text-xs px-1.5 py-0.5"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
:for={format <- Records.Record.formats()}
|
||||||
|
value={format}
|
||||||
|
selected={format == item.format}
|
||||||
|
>
|
||||||
|
{format_label(format)}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</form>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
phx-click="remove_from_cart"
|
||||||
|
phx-value-cart_item_id={item.cart_item_id}
|
||||||
|
phx-target={@myself}
|
||||||
|
class="text-xs text-zinc-500 hover:text-red-600 dark:hover:text-red-400"
|
||||||
|
>
|
||||||
|
{gettext("Remove")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div
|
||||||
|
:if={@cart != []}
|
||||||
|
class="border-t border-zinc-200 dark:border-zinc-800 px-4 py-3"
|
||||||
|
>
|
||||||
|
<.button
|
||||||
|
variant="solid"
|
||||||
|
phx-click="import_cart"
|
||||||
|
phx-target={@myself}
|
||||||
|
disabled={@importing?}
|
||||||
|
class="w-full"
|
||||||
|
>
|
||||||
|
<.icon
|
||||||
|
:if={@importing?}
|
||||||
|
name="hero-arrow-path"
|
||||||
|
class="icon animate-spin"
|
||||||
|
aria-hidden="true"
|
||||||
|
data-slot="icon"
|
||||||
|
/>
|
||||||
|
<.icon
|
||||||
|
:if={not @importing?}
|
||||||
|
name="hero-plus"
|
||||||
|
class="icon"
|
||||||
|
aria-hidden="true"
|
||||||
|
data-slot="icon"
|
||||||
|
/>
|
||||||
|
{ngettext(
|
||||||
|
"Import %{count} record",
|
||||||
|
"Import %{count} records",
|
||||||
|
length(@cart),
|
||||||
|
count: length(@cart)
|
||||||
|
)}
|
||||||
|
</.button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
@@ -68,6 +239,9 @@ defmodule MusicLibraryWeb.Components.AddRecord do
|
|||||||
attr :id, :string, required: true
|
attr :id, :string, required: true
|
||||||
attr :icon_name, :string, required: true
|
attr :icon_name, :string, required: true
|
||||||
attr :release_group, MusicBrainz.ReleaseGroupSearchResult, required: true
|
attr :release_group, MusicBrainz.ReleaseGroupSearchResult, required: true
|
||||||
|
attr :myself, :any, required: true
|
||||||
|
attr :in_cart?, :boolean, required: true
|
||||||
|
attr :cart_format, :atom, required: true
|
||||||
|
|
||||||
defp result(assigns) do
|
defp result(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
@@ -93,9 +267,17 @@ defmodule MusicLibraryWeb.Components.AddRecord do
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<span
|
||||||
|
:if={@in_cart?}
|
||||||
|
class="mr-2 inline-flex items-center gap-1 rounded-full bg-emerald-100 dark:bg-emerald-500/10 text-emerald-700 dark:text-emerald-300 text-xs font-medium px-2 py-0.5"
|
||||||
|
>
|
||||||
|
<.icon name="hero-check" class="size-3" aria-hidden="true" data-slot="icon" />
|
||||||
|
{gettext("In cart · %{format}", format: format_label(@cart_format))}
|
||||||
|
</span>
|
||||||
|
|
||||||
<.dropdown id={"actions-#{@release_group.id}"} placement="bottom-end">
|
<.dropdown id={"actions-#{@release_group.id}"} placement="bottom-end">
|
||||||
<:toggle>
|
<:toggle>
|
||||||
<span class="sr-only">{gettext("Choose which format to import")}</span>
|
<span class="sr-only">{gettext("Choose which format to add")}</span>
|
||||||
<.icon
|
<.icon
|
||||||
name="hero-plus"
|
name="hero-plus"
|
||||||
class="size-5 cursor-pointer text-zinc-500 dark:text-zinc-400"
|
class="size-5 cursor-pointer text-zinc-500 dark:text-zinc-400"
|
||||||
@@ -106,9 +288,19 @@ defmodule MusicLibraryWeb.Components.AddRecord do
|
|||||||
<.focus_wrap id={"actions-#{@release_group.id}-focus-wrap"}>
|
<.focus_wrap id={"actions-#{@release_group.id}-focus-wrap"}>
|
||||||
<.dropdown_link
|
<.dropdown_link
|
||||||
:for={format <- Records.Record.formats()}
|
:for={format <- Records.Record.formats()}
|
||||||
id={"actions-#{@release_group.id}-#{format}-import"}
|
id={"actions-#{@release_group.id}-#{format}-add"}
|
||||||
phx-click={
|
phx-click={
|
||||||
JS.push("import", value: %{id: @release_group.id, format: format}, page_loading: true)
|
JS.push("add_to_cart",
|
||||||
|
value: %{
|
||||||
|
id: @release_group.id,
|
||||||
|
format: format,
|
||||||
|
title: @release_group.title,
|
||||||
|
artists: @release_group.artists,
|
||||||
|
release_date: @release_group.release_date,
|
||||||
|
thumb_url: ReleaseGroupSearchResult.thumb_url(@release_group)
|
||||||
|
},
|
||||||
|
target: @myself
|
||||||
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{format_label(format)}
|
{format_label(format)}
|
||||||
@@ -124,14 +316,15 @@ defmodule MusicLibraryWeb.Components.AddRecord do
|
|||||||
def mount(socket) do
|
def mount(socket) do
|
||||||
{:ok,
|
{:ok,
|
||||||
socket
|
socket
|
||||||
|> stream_configure(:release_groups,
|
|> assign(:release_groups, [])
|
||||||
dom_id: fn rg -> "musicbrainz_#{rg.id}" end
|
|
||||||
)
|
|
||||||
|> assign(:release_groups_count, 0)
|
|> assign(:release_groups_count, 0)
|
||||||
|> assign(:release_groups_total_count, 0)
|
|> assign(:release_groups_total_count, 0)
|
||||||
|> stream(:release_groups, [])
|
|
||||||
|> assign(:loaded_all_results?, false)
|
|> assign(:loaded_all_results?, false)
|
||||||
|> assign(:error_message, nil)}
|
|> assign(:error_message, nil)
|
||||||
|
|> assign(:cart, [])
|
||||||
|
|> assign(:cart_pairs, MapSet.new())
|
||||||
|
|> assign(:cart_expanded?, true)
|
||||||
|
|> assign(:importing?, false)}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
@@ -148,7 +341,7 @@ defmodule MusicLibraryWeb.Components.AddRecord do
|
|||||||
|> assign(:error_message, nil)
|
|> assign(:error_message, nil)
|
||||||
|> assign(:release_groups_count, Enum.count(result.release_groups))
|
|> assign(:release_groups_count, Enum.count(result.release_groups))
|
||||||
|> assign(:release_groups_total_count, result.total_count)
|
|> assign(:release_groups_total_count, result.total_count)
|
||||||
|> stream(:release_groups, result.release_groups, reset: true)
|
|> assign(:release_groups, result.release_groups)
|
||||||
|
|
||||||
{:error, _reason} ->
|
{:error, _reason} ->
|
||||||
assign(
|
assign(
|
||||||
@@ -163,19 +356,19 @@ defmodule MusicLibraryWeb.Components.AddRecord do
|
|||||||
assign(socket,
|
assign(socket,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
icon_name: assigns.icon_name,
|
icon_name: assigns.icon_name,
|
||||||
|
purchased_at_fn: assigns.purchased_at_fn,
|
||||||
form: to_form(%{"mb_query" => mb_query})
|
form: to_form(%{"mb_query" => mb_query})
|
||||||
)}
|
)}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
|
||||||
def handle_event("search", %{"mb_query" => ""}, socket) do
|
def handle_event("search", %{"mb_query" => ""}, socket) do
|
||||||
{:noreply,
|
{:noreply,
|
||||||
socket
|
socket
|
||||||
|> assign(:offset, 0)
|
|> assign(:offset, 0)
|
||||||
|> assign(:release_groups_count, 0)
|
|> assign(:release_groups_count, 0)
|
||||||
|> assign(:release_groups_total_count, 0)
|
|> assign(:release_groups_total_count, 0)
|
||||||
|> stream(:release_groups, [], reset: true)
|
|> assign(:release_groups, [])
|
||||||
|> assign(:form, to_form(%{"mb_query" => ""}))}
|
|> assign(:form, to_form(%{"mb_query" => ""}))}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -188,7 +381,7 @@ defmodule MusicLibraryWeb.Components.AddRecord do
|
|||||||
|> assign(:offset, 0)
|
|> assign(:offset, 0)
|
||||||
|> assign(:release_groups_count, length(result.release_groups))
|
|> assign(:release_groups_count, length(result.release_groups))
|
||||||
|> assign(:release_groups_total_count, result.total_count)
|
|> assign(:release_groups_total_count, result.total_count)
|
||||||
|> stream(:release_groups, result.release_groups, reset: true)
|
|> assign(:release_groups, result.release_groups)
|
||||||
|> assign(:form, to_form(%{"mb_query" => mb_query}))}
|
|> assign(:form, to_form(%{"mb_query" => mb_query}))}
|
||||||
|
|
||||||
{:error, _reason} ->
|
{:error, _reason} ->
|
||||||
@@ -198,7 +391,7 @@ defmodule MusicLibraryWeb.Components.AddRecord do
|
|||||||
|> assign(:offset, 0)
|
|> assign(:offset, 0)
|
||||||
|> assign(:release_groups_count, 0)
|
|> assign(:release_groups_count, 0)
|
||||||
|> assign(:release_groups_total_count, 0)
|
|> assign(:release_groups_total_count, 0)
|
||||||
|> stream(:release_groups, [], reset: true)
|
|> assign(:release_groups, [])
|
||||||
|> assign(:form, to_form(%{"mb_query" => mb_query}))}
|
|> assign(:form, to_form(%{"mb_query" => mb_query}))}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -215,10 +408,209 @@ defmodule MusicLibraryWeb.Components.AddRecord do
|
|||||||
|> assign(:loaded_all_results?, length(result.release_groups) < @batch_size)
|
|> assign(:loaded_all_results?, length(result.release_groups) < @batch_size)
|
||||||
|> assign(:release_groups_count, offset + length(result.release_groups))
|
|> assign(:release_groups_count, offset + length(result.release_groups))
|
||||||
|> assign(:release_groups_total_count, result.total_count)
|
|> assign(:release_groups_total_count, result.total_count)
|
||||||
|> stream(:release_groups, result.release_groups)}
|
|> assign(
|
||||||
|
:release_groups,
|
||||||
|
socket.assigns.release_groups ++ result.release_groups
|
||||||
|
)}
|
||||||
|
|
||||||
{:error, _reason} ->
|
{:error, _reason} ->
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_event("add_to_cart", params, socket) do
|
||||||
|
case parse_format(params["format"]) do
|
||||||
|
{:ok, format} ->
|
||||||
|
{:noreply, add_to_cart(socket, params, format)}
|
||||||
|
|
||||||
|
:error ->
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("remove_from_cart", %{"cart_item_id" => id}, socket) do
|
||||||
|
id = cast_id(id)
|
||||||
|
{:noreply, remove_cart_item(socket, id)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("change_format", %{"cart_item_id" => id, "format" => format_str}, socket) do
|
||||||
|
id = cast_id(id)
|
||||||
|
|
||||||
|
case parse_format(format_str) do
|
||||||
|
{:ok, format} -> {:noreply, change_cart_format(socket, id, format)}
|
||||||
|
:error -> {:noreply, socket}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("clear_cart", _params, socket) do
|
||||||
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> assign(:cart, [])
|
||||||
|
|> assign(:cart_pairs, MapSet.new())}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("toggle_cart", _params, socket) do
|
||||||
|
{:noreply, assign(socket, :cart_expanded?, not socket.assigns.cart_expanded?)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("import_cart", _params, socket) do
|
||||||
|
case socket.assigns.cart do
|
||||||
|
[] ->
|
||||||
|
{:noreply, socket}
|
||||||
|
|
||||||
|
[single] ->
|
||||||
|
purchased_at = socket.assigns.purchased_at_fn.()
|
||||||
|
|
||||||
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> assign(:importing?, true)
|
||||||
|
|> start_async(:import_cart, fn ->
|
||||||
|
Records.import_from_musicbrainz_release_group(single.release_group_id,
|
||||||
|
format: single.format,
|
||||||
|
purchased_at: purchased_at
|
||||||
|
)
|
||||||
|
end)}
|
||||||
|
|
||||||
|
items ->
|
||||||
|
purchased_at = socket.assigns.purchased_at_fn.()
|
||||||
|
purchased_at_iso = purchased_at && DateTime.to_iso8601(purchased_at)
|
||||||
|
|
||||||
|
changesets =
|
||||||
|
Enum.map(items, fn item ->
|
||||||
|
ImportFromMusicbrainzReleaseGroup.new(%{
|
||||||
|
"release_group_id" => item.release_group_id,
|
||||||
|
"format" => Atom.to_string(item.format),
|
||||||
|
"purchased_at" => purchased_at_iso
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
|
||||||
|
Oban.insert_all(changesets)
|
||||||
|
notify_parent({:imported_async, length(items)})
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
# If the user closes the modal mid-import, this callback lands on a detached
|
||||||
|
# component and is silently dropped by Phoenix — no user-visible bug.
|
||||||
|
def handle_async(:import_cart, {:ok, {:ok, record}}, socket) do
|
||||||
|
notify_parent({:imported_single, record})
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_async(:import_cart, {:ok, {:error, reason}}, socket) do
|
||||||
|
put_toast!(
|
||||||
|
:error,
|
||||||
|
gettext("Error importing record") <> ": " <> ErrorMessages.friendly_message(reason)
|
||||||
|
)
|
||||||
|
|
||||||
|
{:noreply, assign(socket, :importing?, false)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_async(:import_cart, {:exit, reason}, socket) do
|
||||||
|
Logger.warning("Cart import crashed: #{inspect(reason)}")
|
||||||
|
put_toast!(:error, gettext("Error importing record"))
|
||||||
|
{:noreply, assign(socket, :importing?, false)}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp add_to_cart(socket, params, format) do
|
||||||
|
rg_id = params["id"]
|
||||||
|
pair = {rg_id, format}
|
||||||
|
|
||||||
|
if MapSet.member?(socket.assigns.cart_pairs, pair) do
|
||||||
|
socket
|
||||||
|
else
|
||||||
|
item = %{
|
||||||
|
cart_item_id: System.unique_integer([:positive]),
|
||||||
|
release_group_id: rg_id,
|
||||||
|
title: params["title"],
|
||||||
|
artists: params["artists"],
|
||||||
|
release_date: params["release_date"],
|
||||||
|
thumb_url: params["thumb_url"],
|
||||||
|
format: format
|
||||||
|
}
|
||||||
|
|
||||||
|
socket
|
||||||
|
|> assign(:cart, [item | socket.assigns.cart])
|
||||||
|
|> assign(:cart_pairs, MapSet.put(socket.assigns.cart_pairs, pair))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp remove_cart_item(socket, id) do
|
||||||
|
{removed, kept} = Enum.split_with(socket.assigns.cart, &(&1.cart_item_id == id))
|
||||||
|
|
||||||
|
pairs =
|
||||||
|
Enum.reduce(removed, socket.assigns.cart_pairs, fn item, acc ->
|
||||||
|
MapSet.delete(acc, {item.release_group_id, item.format})
|
||||||
|
end)
|
||||||
|
|
||||||
|
socket
|
||||||
|
|> assign(:cart, kept)
|
||||||
|
|> assign(:cart_pairs, pairs)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp change_cart_format(socket, id, new_format) do
|
||||||
|
case Enum.find(socket.assigns.cart, &(&1.cart_item_id == id)) do
|
||||||
|
nil ->
|
||||||
|
socket
|
||||||
|
|
||||||
|
%{format: ^new_format} ->
|
||||||
|
socket
|
||||||
|
|
||||||
|
item ->
|
||||||
|
new_pair = {item.release_group_id, new_format}
|
||||||
|
|
||||||
|
if MapSet.member?(socket.assigns.cart_pairs, new_pair) do
|
||||||
|
socket
|
||||||
|
else
|
||||||
|
updated = %{item | format: new_format}
|
||||||
|
|
||||||
|
cart =
|
||||||
|
Enum.map(socket.assigns.cart, fn
|
||||||
|
^item -> updated
|
||||||
|
other -> other
|
||||||
|
end)
|
||||||
|
|
||||||
|
pairs =
|
||||||
|
socket.assigns.cart_pairs
|
||||||
|
|> MapSet.delete({item.release_group_id, item.format})
|
||||||
|
|> MapSet.put(new_pair)
|
||||||
|
|
||||||
|
socket
|
||||||
|
|> assign(:cart, cart)
|
||||||
|
|> assign(:cart_pairs, pairs)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp parse_format(nil), do: {:ok, @default_format}
|
||||||
|
|
||||||
|
defp parse_format(format_str) when is_binary(format_str) do
|
||||||
|
formats = Records.Record.formats()
|
||||||
|
|
||||||
|
case Enum.find(formats, fn f -> Atom.to_string(f) == format_str end) do
|
||||||
|
nil -> :error
|
||||||
|
format -> {:ok, format}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp parse_format(format) when is_atom(format) do
|
||||||
|
if format in Records.Record.formats(), do: {:ok, format}, else: :error
|
||||||
|
end
|
||||||
|
|
||||||
|
defp cast_id(id) when is_integer(id), do: id
|
||||||
|
defp cast_id(id) when is_binary(id), do: String.to_integer(id)
|
||||||
|
|
||||||
|
defp in_cart?(cart_pairs, rg_id) do
|
||||||
|
Enum.any?(cart_pairs, fn {id, _format} -> id == rg_id end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp cart_format(cart, rg_id) do
|
||||||
|
case Enum.find(cart, &(&1.release_group_id == rg_id)) do
|
||||||
|
nil -> nil
|
||||||
|
item -> item.format
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -154,6 +154,7 @@ defmodule MusicLibraryWeb.CoreComponents do
|
|||||||
attr :id, :string, required: true
|
attr :id, :string, required: true
|
||||||
attr :on_close, :any, required: false, default: nil
|
attr :on_close, :any, required: false, default: nil
|
||||||
attr :open, :boolean, required: false, default: true
|
attr :open, :boolean, required: false, default: true
|
||||||
|
attr :width_class, :string, required: false, default: "md:max-w-3xl"
|
||||||
|
|
||||||
slot :inner_block, required: true
|
slot :inner_block, required: true
|
||||||
|
|
||||||
@@ -161,7 +162,7 @@ defmodule MusicLibraryWeb.CoreComponents do
|
|||||||
~H"""
|
~H"""
|
||||||
<Fluxon.Components.Modal.modal
|
<Fluxon.Components.Modal.modal
|
||||||
id={@id}
|
id={@id}
|
||||||
class="mx-auto mt-8 max-w-sm sm:min-w-2xl md:max-w-3xl"
|
class={"mx-auto mt-8 max-w-sm sm:min-w-2xl #{@width_class}"}
|
||||||
animation="transition duration-100 ease-in-out"
|
animation="transition duration-100 ease-in-out"
|
||||||
placement="top"
|
placement="top"
|
||||||
open={@open}
|
open={@open}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ defmodule MusicLibraryWeb.CollectionLive.Index do
|
|||||||
|
|
||||||
alias MusicLibrary.Chats
|
alias MusicLibrary.Chats
|
||||||
alias MusicLibrary.Collection
|
alias MusicLibrary.Collection
|
||||||
|
alias MusicLibraryWeb.Components.AddRecord
|
||||||
alias MusicLibraryWeb.LiveHelpers.IndexActions
|
alias MusicLibraryWeb.LiveHelpers.IndexActions
|
||||||
|
|
||||||
defp index_config do
|
defp index_config do
|
||||||
@@ -20,7 +21,6 @@ defmodule MusicLibraryWeb.CollectionLive.Index do
|
|||||||
import_page_title: gettext("Add new Record · Collection"),
|
import_page_title: gettext("Add new Record · Collection"),
|
||||||
section_page_title: gettext("Collection"),
|
section_page_title: gettext("Collection"),
|
||||||
import_success_toast: gettext("Record imported successfully"),
|
import_success_toast: gettext("Record imported successfully"),
|
||||||
import_error_toast: gettext("Error importing record"),
|
|
||||||
record_path_fn: fn id -> ~p"/collection/#{id}" end,
|
record_path_fn: fn id -> ~p"/collection/#{id}" end,
|
||||||
index_path_fn: fn qs -> ~p"/collection?#{qs}" end,
|
index_path_fn: fn qs -> ~p"/collection?#{qs}" end,
|
||||||
base_index_path: ~p"/collection"
|
base_index_path: ~p"/collection"
|
||||||
@@ -186,9 +186,10 @@ defmodule MusicLibraryWeb.CollectionLive.Index do
|
|||||||
:if={@live_action == :import}
|
:if={@live_action == :import}
|
||||||
id="record-modal"
|
id="record-modal"
|
||||||
on_close={JS.patch(back_path(@record_list_params))}
|
on_close={JS.patch(back_path(@record_list_params))}
|
||||||
|
width_class="md:max-w-4xl lg:max-w-5xl"
|
||||||
>
|
>
|
||||||
<.live_component
|
<.live_component
|
||||||
module={MusicLibraryWeb.Components.AddRecord}
|
module={AddRecord}
|
||||||
id={:search}
|
id={:search}
|
||||||
title={@page_title}
|
title={@page_title}
|
||||||
action={@live_action}
|
action={@live_action}
|
||||||
@@ -196,6 +197,7 @@ defmodule MusicLibraryWeb.CollectionLive.Index do
|
|||||||
patch={back_path(@record_list_params)}
|
patch={back_path(@record_list_params)}
|
||||||
initial_query={@import_query}
|
initial_query={@import_query}
|
||||||
icon_name="hero-plus"
|
icon_name="hero-plus"
|
||||||
|
purchased_at_fn={@index_config.purchased_at_fn}
|
||||||
/>
|
/>
|
||||||
</.structured_modal>
|
</.structured_modal>
|
||||||
|
|
||||||
@@ -284,6 +286,14 @@ defmodule MusicLibraryWeb.CollectionLive.Index do
|
|||||||
IndexActions.handle_record_saved(socket)
|
IndexActions.handle_record_saved(socket)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_info({AddRecord, {:imported_single, record}}, socket) do
|
||||||
|
IndexActions.handle_cart_imported_single(socket, record)
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info({AddRecord, {:imported_async, count}}, socket) do
|
||||||
|
IndexActions.handle_cart_imported_async(socket, count)
|
||||||
|
end
|
||||||
|
|
||||||
def handle_info({MusicLibraryWeb.Components.Chat, :chats_changed}, socket) do
|
def handle_info({MusicLibraryWeb.Components.Chat, :chats_changed}, socket) do
|
||||||
chat_count = Chats.count_chats(:collection, Chats.collection_musicbrainz_id())
|
chat_count = Chats.count_chats(:collection, Chats.collection_musicbrainz_id())
|
||||||
{:noreply, assign(socket, :chat_count, chat_count)}
|
{:noreply, assign(socket, :chat_count, chat_count)}
|
||||||
@@ -307,10 +317,6 @@ defmodule MusicLibraryWeb.CollectionLive.Index do
|
|||||||
IndexActions.handle_search(socket, query)
|
IndexActions.handle_search(socket, query)
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("import", %{"id" => musicbrainz_id, "format" => format}, socket) do
|
|
||||||
IndexActions.handle_import(socket, musicbrainz_id, format)
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_event("set_display", %{"mode" => mode}, socket) do
|
def handle_event("set_display", %{"mode" => mode}, socket) do
|
||||||
IndexActions.handle_set_display(socket, mode)
|
IndexActions.handle_set_display(socket, mode)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ defmodule MusicLibraryWeb.WishlistLive.Index do
|
|||||||
|
|
||||||
alias MusicLibrary.Records
|
alias MusicLibrary.Records
|
||||||
alias MusicLibrary.Wishlist
|
alias MusicLibrary.Wishlist
|
||||||
|
alias MusicLibraryWeb.Components.AddRecord
|
||||||
alias MusicLibraryWeb.LiveHelpers.IndexActions
|
alias MusicLibraryWeb.LiveHelpers.IndexActions
|
||||||
|
|
||||||
defp index_config do
|
defp index_config do
|
||||||
@@ -18,7 +19,6 @@ defmodule MusicLibraryWeb.WishlistLive.Index do
|
|||||||
import_page_title: gettext("Add new Record · Wishlist"),
|
import_page_title: gettext("Add new Record · Wishlist"),
|
||||||
section_page_title: gettext("Wishlist"),
|
section_page_title: gettext("Wishlist"),
|
||||||
import_success_toast: gettext("Record wishlisted successfully"),
|
import_success_toast: gettext("Record wishlisted successfully"),
|
||||||
import_error_toast: gettext("Error wishlisting record"),
|
|
||||||
record_path_fn: fn id -> ~p"/wishlist/#{id}" end,
|
record_path_fn: fn id -> ~p"/wishlist/#{id}" end,
|
||||||
index_path_fn: fn qs -> ~p"/wishlist?#{qs}" end,
|
index_path_fn: fn qs -> ~p"/wishlist?#{qs}" end,
|
||||||
base_index_path: ~p"/wishlist"
|
base_index_path: ~p"/wishlist"
|
||||||
@@ -154,9 +154,10 @@ defmodule MusicLibraryWeb.WishlistLive.Index do
|
|||||||
:if={@live_action == :import}
|
:if={@live_action == :import}
|
||||||
id="record-modal"
|
id="record-modal"
|
||||||
on_close={JS.patch(back_path(@record_list_params))}
|
on_close={JS.patch(back_path(@record_list_params))}
|
||||||
|
width_class="md:max-w-4xl lg:max-w-5xl"
|
||||||
>
|
>
|
||||||
<.live_component
|
<.live_component
|
||||||
module={MusicLibraryWeb.Components.AddRecord}
|
module={AddRecord}
|
||||||
id={:search}
|
id={:search}
|
||||||
title={@page_title}
|
title={@page_title}
|
||||||
action={@live_action}
|
action={@live_action}
|
||||||
@@ -164,6 +165,7 @@ defmodule MusicLibraryWeb.WishlistLive.Index do
|
|||||||
patch={back_path(@record_list_params)}
|
patch={back_path(@record_list_params)}
|
||||||
initial_query={@import_query}
|
initial_query={@import_query}
|
||||||
icon_name="hero-plus"
|
icon_name="hero-plus"
|
||||||
|
purchased_at_fn={@index_config.purchased_at_fn}
|
||||||
/>
|
/>
|
||||||
</.structured_modal>
|
</.structured_modal>
|
||||||
|
|
||||||
@@ -207,6 +209,14 @@ defmodule MusicLibraryWeb.WishlistLive.Index do
|
|||||||
IndexActions.handle_record_saved(socket)
|
IndexActions.handle_record_saved(socket)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_info({AddRecord, {:imported_single, record}}, socket) do
|
||||||
|
IndexActions.handle_cart_imported_single(socket, record)
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info({AddRecord, {:imported_async, count}}, socket) do
|
||||||
|
IndexActions.handle_cart_imported_async(socket, count)
|
||||||
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("delete", %{"id" => id}, socket) do
|
def handle_event("delete", %{"id" => id}, socket) do
|
||||||
IndexActions.handle_delete(socket, id)
|
IndexActions.handle_delete(socket, id)
|
||||||
@@ -216,10 +226,6 @@ defmodule MusicLibraryWeb.WishlistLive.Index do
|
|||||||
IndexActions.handle_search(socket, query)
|
IndexActions.handle_search(socket, query)
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("import", %{"id" => musicbrainz_id, "format" => format}, socket) do
|
|
||||||
IndexActions.handle_import(socket, musicbrainz_id, format)
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_event("add-to-collection", %{"id" => id}, socket) do
|
def handle_event("add-to-collection", %{"id" => id}, socket) do
|
||||||
record = Records.get_record!(id)
|
record = Records.get_record!(id)
|
||||||
current_time = DateTime.utc_now()
|
current_time = DateTime.utc_now()
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ defmodule MusicLibraryWeb.LiveHelpers.IndexActions do
|
|||||||
use Gettext, backend: MusicLibraryWeb.Gettext
|
use Gettext, backend: MusicLibraryWeb.Gettext
|
||||||
|
|
||||||
alias MusicLibrary.Records
|
alias MusicLibrary.Records
|
||||||
alias MusicLibraryWeb.ErrorMessages
|
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Applies the :index action. Reads config from `socket.assigns.index_config`
|
Applies the :index action. Reads config from `socket.assigns.index_config`
|
||||||
@@ -108,28 +107,30 @@ defmodule MusicLibraryWeb.LiveHelpers.IndexActions do
|
|||||||
{:noreply, push_patch(socket, to: config.index_path_fn.(qs))}
|
{:noreply, push_patch(socket, to: config.index_path_fn.(qs))}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_import(socket, musicbrainz_id, format) do
|
def handle_cart_imported_single(socket, record) do
|
||||||
config = socket.assigns.index_config
|
config = socket.assigns.index_config
|
||||||
|
|
||||||
case Records.import_from_musicbrainz_release_group(musicbrainz_id,
|
{:noreply,
|
||||||
format: format,
|
socket
|
||||||
purchased_at: config.purchased_at_fn.()
|
|> put_toast(:info, config.import_success_toast)
|
||||||
) do
|
|> push_navigate(to: config.record_path_fn.(record.id))}
|
||||||
{:ok, record} ->
|
end
|
||||||
{:noreply,
|
|
||||||
socket
|
|
||||||
|> put_toast(:info, config.import_success_toast)
|
|
||||||
|> push_navigate(to: config.record_path_fn.(record.id))}
|
|
||||||
|
|
||||||
{:error, reason} ->
|
def handle_cart_imported_async(socket, count) do
|
||||||
{:noreply,
|
config = socket.assigns.index_config
|
||||||
socket
|
|
||||||
|> put_toast(
|
msg =
|
||||||
:error,
|
ngettext(
|
||||||
config.import_error_toast <> ": " <> ErrorMessages.friendly_message(reason)
|
"Importing %{count} record in the background...",
|
||||||
)
|
"Importing %{count} records in the background...",
|
||||||
|> push_patch(to: config.base_index_path)}
|
count,
|
||||||
end
|
count: count
|
||||||
|
)
|
||||||
|
|
||||||
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> put_toast(:info, msg)
|
||||||
|
|> push_patch(to: config.base_index_path)}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_set_display(socket, mode) do
|
def handle_set_display(socket, mode) do
|
||||||
|
|||||||
@@ -64,12 +64,13 @@ msgstr ""
|
|||||||
msgid "Edit"
|
msgid "Edit"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/music_library_web/components/add_record.ex
|
||||||
#: lib/music_library_web/live/artist_live/show.ex
|
#: lib/music_library_web/live/artist_live/show.ex
|
||||||
#: lib/music_library_web/live/collection_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Error importing record"
|
msgid "Error importing record"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/music_library_web/components/add_record.ex
|
||||||
#: lib/music_library_web/components/record_form.ex
|
#: lib/music_library_web/components/record_form.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Format"
|
msgid "Format"
|
||||||
@@ -227,7 +228,6 @@ msgstr ""
|
|||||||
msgid "Scrobble activity"
|
msgid "Scrobble activity"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/music_library_web/components/add_record.ex
|
|
||||||
#: lib/music_library_web/components/scrobble_components.ex
|
#: lib/music_library_web/components/scrobble_components.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Choose which format to import"
|
msgid "Choose which format to import"
|
||||||
@@ -569,7 +569,6 @@ msgid "Add more · Artist"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/music_library_web/live/stats_live/index.ex
|
#: lib/music_library_web/live/stats_live/index.ex
|
||||||
#: lib/music_library_web/live/wishlist_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Error wishlisting record"
|
msgid "Error wishlisting record"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -1588,6 +1587,7 @@ msgstr ""
|
|||||||
msgid "Record set updated successfully"
|
msgid "Record set updated successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/music_library_web/components/add_record.ex
|
||||||
#: lib/music_library_web/live/record_set_live/index.ex
|
#: lib/music_library_web/live/record_set_live/index.ex
|
||||||
#: lib/music_library_web/live/record_set_live/show.ex
|
#: lib/music_library_web/live/record_set_live/show.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
@@ -2401,6 +2401,7 @@ msgid "Record has a selected release"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/music_library_web/components/barcode_scanner.ex
|
#: lib/music_library_web/components/barcode_scanner.ex
|
||||||
|
#: lib/music_library_web/live_helpers/index_actions.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Importing %{count} record in the background..."
|
msgid "Importing %{count} record in the background..."
|
||||||
msgid_plural "Importing %{count} records in the background..."
|
msgid_plural "Importing %{count} records in the background..."
|
||||||
@@ -2461,3 +2462,52 @@ msgstr ""
|
|||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "A rule for this album already exists"
|
msgid "A rule for this album already exists"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/music_library_web/components/add_record.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "%{count} record"
|
||||||
|
msgid_plural "%{count} records"
|
||||||
|
msgstr[0] ""
|
||||||
|
msgstr[1] ""
|
||||||
|
|
||||||
|
#: lib/music_library_web/components/add_record.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Add records from the search results to get started."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/music_library_web/components/add_record.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Cart"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/music_library_web/components/add_record.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Choose which format to add"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/music_library_web/components/add_record.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Clear all"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/music_library_web/components/add_record.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Import %{count} record"
|
||||||
|
msgid_plural "Import %{count} records"
|
||||||
|
msgstr[0] ""
|
||||||
|
msgstr[1] ""
|
||||||
|
|
||||||
|
#: lib/music_library_web/components/add_record.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "In cart · %{format}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/music_library_web/components/add_record.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Toggle cart"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/music_library_web/components/add_record.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Your cart is empty"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@@ -64,12 +64,13 @@ msgstr ""
|
|||||||
msgid "Edit"
|
msgid "Edit"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/music_library_web/components/add_record.ex
|
||||||
#: lib/music_library_web/live/artist_live/show.ex
|
#: lib/music_library_web/live/artist_live/show.ex
|
||||||
#: lib/music_library_web/live/collection_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Error importing record"
|
msgid "Error importing record"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/music_library_web/components/add_record.ex
|
||||||
#: lib/music_library_web/components/record_form.ex
|
#: lib/music_library_web/components/record_form.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Format"
|
msgid "Format"
|
||||||
@@ -227,7 +228,6 @@ msgstr ""
|
|||||||
msgid "Scrobble activity"
|
msgid "Scrobble activity"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/music_library_web/components/add_record.ex
|
|
||||||
#: lib/music_library_web/components/scrobble_components.ex
|
#: lib/music_library_web/components/scrobble_components.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Choose which format to import"
|
msgid "Choose which format to import"
|
||||||
@@ -569,7 +569,6 @@ msgid "Add more · Artist"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/music_library_web/live/stats_live/index.ex
|
#: lib/music_library_web/live/stats_live/index.ex
|
||||||
#: lib/music_library_web/live/wishlist_live/index.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Error wishlisting record"
|
msgid "Error wishlisting record"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -1588,6 +1587,7 @@ msgstr ""
|
|||||||
msgid "Record set updated successfully"
|
msgid "Record set updated successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/music_library_web/components/add_record.ex
|
||||||
#: lib/music_library_web/live/record_set_live/index.ex
|
#: lib/music_library_web/live/record_set_live/index.ex
|
||||||
#: lib/music_library_web/live/record_set_live/show.ex
|
#: lib/music_library_web/live/record_set_live/show.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
@@ -2401,6 +2401,7 @@ msgid "Record has a selected release"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/music_library_web/components/barcode_scanner.ex
|
#: lib/music_library_web/components/barcode_scanner.ex
|
||||||
|
#: lib/music_library_web/live_helpers/index_actions.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Importing %{count} record in the background..."
|
msgid "Importing %{count} record in the background..."
|
||||||
msgid_plural "Importing %{count} records in the background..."
|
msgid_plural "Importing %{count} records in the background..."
|
||||||
@@ -2461,3 +2462,52 @@ msgstr ""
|
|||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "A rule for this album already exists"
|
msgid "A rule for this album already exists"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/music_library_web/components/add_record.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "%{count} record"
|
||||||
|
msgid_plural "%{count} records"
|
||||||
|
msgstr[0] ""
|
||||||
|
msgstr[1] ""
|
||||||
|
|
||||||
|
#: lib/music_library_web/components/add_record.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Add records from the search results to get started."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/music_library_web/components/add_record.ex
|
||||||
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
|
msgid "Cart"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/music_library_web/components/add_record.ex
|
||||||
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
|
msgid "Choose which format to add"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/music_library_web/components/add_record.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Clear all"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/music_library_web/components/add_record.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Import %{count} record"
|
||||||
|
msgid_plural "Import %{count} records"
|
||||||
|
msgstr[0] ""
|
||||||
|
msgstr[1] ""
|
||||||
|
|
||||||
|
#: lib/music_library_web/components/add_record.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "In cart · %{format}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/music_library_web/components/add_record.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Toggle cart"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/music_library_web/components/add_record.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Your cart is empty"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
defmodule MusicLibrary.Worker.ImportFromMusicbrainzReleaseGroupTest do
|
||||||
|
use MusicLibrary.DataCase
|
||||||
|
|
||||||
|
import MusicBrainz.Fixtures.ReleaseGroup
|
||||||
|
import MusicLibrary.Fixtures.Records
|
||||||
|
|
||||||
|
alias MusicLibrary.Records.Record
|
||||||
|
alias MusicLibrary.Worker.ImportFromMusicbrainzReleaseGroup
|
||||||
|
|
||||||
|
describe "perform/1" do
|
||||||
|
test "imports a record from a MusicBrainz release group" do
|
||||||
|
release_group_data = release_group(:marbles)
|
||||||
|
release_group_id = release_group_id(:marbles)
|
||||||
|
release_group_releases_data = release_group_releases(:marbles)
|
||||||
|
|
||||||
|
cover_data = marbles_cover_data()
|
||||||
|
|
||||||
|
Req.Test.stub(MusicBrainz.API, fn conn ->
|
||||||
|
case conn.path_info do
|
||||||
|
[_ws, _version, "release-group", ^release_group_id] ->
|
||||||
|
Req.Test.json(conn, release_group_data)
|
||||||
|
|
||||||
|
[_ws, _version, "release"] ->
|
||||||
|
Req.Test.json(conn, release_group_releases_data)
|
||||||
|
|
||||||
|
[_release_group, ^release_group_id, "front"] ->
|
||||||
|
Plug.Conn.send_resp(conn, 200, cover_data)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
purchased_at = DateTime.utc_now()
|
||||||
|
|
||||||
|
assert :ok =
|
||||||
|
perform_job(ImportFromMusicbrainzReleaseGroup, %{
|
||||||
|
"release_group_id" => release_group_id,
|
||||||
|
"format" => "cd",
|
||||||
|
"purchased_at" => DateTime.to_iso8601(purchased_at)
|
||||||
|
})
|
||||||
|
|
||||||
|
imported_record = Repo.get_by!(Record, musicbrainz_id: release_group_id)
|
||||||
|
assert imported_record.title == "Marbles"
|
||||||
|
assert imported_record.format == :cd
|
||||||
|
assert imported_record.purchased_at == DateTime.truncate(purchased_at, :second)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "imports a wishlist record when purchased_at is nil" do
|
||||||
|
release_group_data = release_group(:marbles)
|
||||||
|
release_group_id = release_group_id(:marbles)
|
||||||
|
release_group_releases_data = release_group_releases(:marbles)
|
||||||
|
|
||||||
|
cover_data = marbles_cover_data()
|
||||||
|
|
||||||
|
Req.Test.stub(MusicBrainz.API, fn conn ->
|
||||||
|
case conn.path_info do
|
||||||
|
[_ws, _version, "release-group", ^release_group_id] ->
|
||||||
|
Req.Test.json(conn, release_group_data)
|
||||||
|
|
||||||
|
[_ws, _version, "release"] ->
|
||||||
|
Req.Test.json(conn, release_group_releases_data)
|
||||||
|
|
||||||
|
[_release_group, ^release_group_id, "front"] ->
|
||||||
|
Plug.Conn.send_resp(conn, 200, cover_data)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert :ok =
|
||||||
|
perform_job(ImportFromMusicbrainzReleaseGroup, %{
|
||||||
|
"release_group_id" => release_group_id,
|
||||||
|
"format" => "vinyl",
|
||||||
|
"purchased_at" => nil
|
||||||
|
})
|
||||||
|
|
||||||
|
imported_record = Repo.get_by!(Record, musicbrainz_id: release_group_id)
|
||||||
|
assert imported_record.purchased_at == nil
|
||||||
|
assert imported_record.format == :vinyl
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns error on transport failure" do
|
||||||
|
Req.Test.stub(MusicBrainz.API, fn conn ->
|
||||||
|
Req.Test.transport_error(conn, :timeout)
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert {:error, %Req.TransportError{reason: :timeout}} =
|
||||||
|
perform_job(ImportFromMusicbrainzReleaseGroup, %{
|
||||||
|
"release_group_id" => "nonexistent-release-group-id",
|
||||||
|
"format" => "cd",
|
||||||
|
"purchased_at" => DateTime.to_iso8601(DateTime.utc_now())
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,15 +1,17 @@
|
|||||||
defmodule MusicLibraryWeb.CollectionLive.IndexTest do
|
defmodule MusicLibraryWeb.CollectionLive.IndexTest do
|
||||||
use MusicLibraryWeb.ConnCase
|
use MusicLibraryWeb.ConnCase
|
||||||
|
use Oban.Testing, repo: MusicLibrary.BackgroundRepo
|
||||||
|
|
||||||
import MusicBrainz.Fixtures.Release
|
import MusicBrainz.Fixtures.Release
|
||||||
import MusicBrainz.Fixtures.ReleaseGroup
|
import MusicBrainz.Fixtures.ReleaseGroup
|
||||||
import MusicLibrary.Fixtures.Records
|
import MusicLibrary.Fixtures.Records
|
||||||
import MusicLibraryWeb.RecordComponents, only: [format_label: 1, type_label: 1]
|
import MusicLibraryWeb.RecordComponents, only: [format_label: 1, type_label: 1]
|
||||||
|
import Phoenix.LiveViewTest, only: [assert_redirect: 2]
|
||||||
|
|
||||||
alias MusicBrainz.ReleaseGroupSearchResult
|
|
||||||
alias MusicLibrary.Assets
|
alias MusicLibrary.Assets
|
||||||
alias MusicLibrary.Assets.{Image, Transform}
|
alias MusicLibrary.Assets.{Image, Transform}
|
||||||
alias MusicLibrary.Records.Record
|
alias MusicLibrary.Records.Record
|
||||||
|
alias MusicLibrary.Worker.ImportFromMusicbrainzReleaseGroup
|
||||||
|
|
||||||
# make it a multiple of 4 for easier calculations
|
# make it a multiple of 4 for easier calculations
|
||||||
@default_records_page_size 4
|
@default_records_page_size 4
|
||||||
@@ -281,93 +283,171 @@ defmodule MusicLibraryWeb.CollectionLive.IndexTest do
|
|||||||
|> assert_has("input[value='test query']")
|
|> assert_has("input[value='test query']")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "imports a record when selected", %{conn: conn} do
|
test "adds a record to the cart instead of importing immediately", %{conn: conn} do
|
||||||
release_group_search_results = Map.get(release_group_search_results(), "release-groups")
|
stub_release_group_search()
|
||||||
|
|
||||||
first_release_group_search_result = hd(release_group_search_results)
|
[first | _] = Map.get(release_group_search_results(), "release-groups")
|
||||||
first_release_group_search_result_id = first_release_group_search_result["id"]
|
first_id = first["id"]
|
||||||
|
|
||||||
release_group = release_group(:marbles)
|
conn
|
||||||
release_group_releases = release_group_releases(:marbles)
|
|> visit(~p"/collection/import")
|
||||||
|
|> fill_in("Search for a record", with: "Marillion Marbles")
|
||||||
|
|> click_link("#musicbrainz_#{first_id} a", "CD")
|
||||||
|
|> assert_has("#musicbrainz_#{first_id} span", text: "In cart · CD")
|
||||||
|
|> assert_has("#cart-items li", count: 1)
|
||||||
|
|
||||||
cover_data = marbles_cover_data()
|
assert MusicLibrary.Repo.all(Record) == []
|
||||||
|
refute_enqueued(worker: ImportFromMusicbrainzReleaseGroup)
|
||||||
|
end
|
||||||
|
|
||||||
Req.Test.stub(MusicBrainz.API, fn conn ->
|
test "deduplicates the same {release_group, format} pair", %{conn: conn} do
|
||||||
case conn.path_info do
|
stub_release_group_search()
|
||||||
[_ws, _version, "release-group", ^first_release_group_search_result_id] ->
|
|
||||||
Req.Test.json(conn, release_group)
|
|
||||||
|
|
||||||
[_ws, _version, "release-group"] ->
|
[first | _] = Map.get(release_group_search_results(), "release-groups")
|
||||||
Req.Test.json(conn, release_group_search_results())
|
first_id = first["id"]
|
||||||
|
|
||||||
[_ws, _version, "release"] ->
|
|
||||||
Req.Test.json(conn, release_group_releases)
|
|
||||||
|
|
||||||
[_release_group, ^first_release_group_search_result_id, "front"] ->
|
|
||||||
Plug.Conn.send_resp(conn, 200, cover_data)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
session =
|
session =
|
||||||
conn
|
conn
|
||||||
|> visit(~p"/collection/import")
|
|> visit(~p"/collection/import")
|
||||||
|> fill_in("Search for a record", with: "Marillion Marbles")
|
|> fill_in("Search for a record", with: "Marillion Marbles")
|
||||||
|
|> click_link("#musicbrainz_#{first_id} a", "CD")
|
||||||
|
|> click_link("#musicbrainz_#{first_id} a", "CD")
|
||||||
|
|
||||||
for release_group_search_result <- release_group_search_results do
|
assert_has(session, "#cart-items li", count: 1)
|
||||||
result = ReleaseGroupSearchResult.from_api_response(release_group_search_result)
|
end
|
||||||
|
|
||||||
session
|
test "allows same release group with different formats", %{conn: conn} do
|
||||||
|> assert_has("h1", result.artists)
|
stub_release_group_search()
|
||||||
|> assert_has("h2", result.title)
|
|
||||||
|> assert_has("p", Record.format_release_date(result.release_date))
|
[first | _] = Map.get(release_group_search_results(), "release-groups")
|
||||||
end
|
first_id = first["id"]
|
||||||
|
|
||||||
session =
|
session =
|
||||||
session
|
conn
|
||||||
|> click_link("#musicbrainz_#{first_release_group_search_result_id} a", "CD")
|
|> visit(~p"/collection/import")
|
||||||
|
|> fill_in("Search for a record", with: "Marillion Marbles")
|
||||||
|
|> click_link("#musicbrainz_#{first_id} a", "CD")
|
||||||
|
|> click_link("#musicbrainz_#{first_id} a", "Vinyl")
|
||||||
|
|
||||||
[record] = MusicLibrary.Repo.all(MusicLibrary.Records.Record)
|
assert_has(session, "#cart-items li", count: 2)
|
||||||
|
end
|
||||||
|
|
||||||
assert record.musicbrainz_id == first_release_group_search_result_id
|
test "removes an item from the cart", %{conn: conn} do
|
||||||
|
stub_release_group_search()
|
||||||
|
|
||||||
|
[first | _] = Map.get(release_group_search_results(), "release-groups")
|
||||||
|
first_id = first["id"]
|
||||||
|
|
||||||
|
session =
|
||||||
|
conn
|
||||||
|
|> visit(~p"/collection/import")
|
||||||
|
|> fill_in("Search for a record", with: "Marillion Marbles")
|
||||||
|
|> click_link("#musicbrainz_#{first_id} a", "CD")
|
||||||
|
|> click_button("#cart-items button", "Remove")
|
||||||
|
|
||||||
|
assert_has(session, "#cart-empty")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "imports a single cart item synchronously and navigates", %{conn: conn} do
|
||||||
|
alias Phoenix.LiveViewTest, as: LVT
|
||||||
|
|
||||||
|
stub_full_import()
|
||||||
|
|
||||||
|
[first | _] = Map.get(release_group_search_results(), "release-groups")
|
||||||
|
first_id = first["id"]
|
||||||
|
|
||||||
|
{:ok, view, _html} = LVT.live(conn, ~p"/collection/import")
|
||||||
|
|
||||||
|
view
|
||||||
|
|> LVT.form("#import_form", %{"mb_query" => "Marillion Marbles"})
|
||||||
|
|> LVT.render_change()
|
||||||
|
|
||||||
|
view
|
||||||
|
|> LVT.element("#musicbrainz_#{first_id} a", "CD")
|
||||||
|
|> LVT.render_click()
|
||||||
|
|
||||||
|
view
|
||||||
|
|> LVT.element("button", "Import 1 record")
|
||||||
|
|> LVT.render_click()
|
||||||
|
|
||||||
|
{path, _flash} = assert_redirect(view, 2_000)
|
||||||
|
"/collection/" <> record_id = path
|
||||||
|
|
||||||
|
record = MusicLibrary.Records.get_record!(record_id)
|
||||||
|
|
||||||
|
assert record.musicbrainz_id == first_id
|
||||||
assert record.title == "Marbles"
|
assert record.title == "Marbles"
|
||||||
assert record.release_date == "2004-05-03"
|
|
||||||
assert record.format == :cd
|
assert record.format == :cd
|
||||||
assert record.musicbrainz_data == release_group
|
assert record.purchased_at != nil
|
||||||
|
|
||||||
assert record.genres == [
|
|
||||||
"alternative rock",
|
|
||||||
"art rock",
|
|
||||||
"baroque pop",
|
|
||||||
"pop rock",
|
|
||||||
"progressive rock",
|
|
||||||
"psychedelic pop",
|
|
||||||
"rock"
|
|
||||||
]
|
|
||||||
|
|
||||||
assert record.cover_hash ==
|
|
||||||
"E7238C742E5B8711FC5BFF01A4A1F727D9E404A4D1420429A6B37ABFFC0B5960"
|
|
||||||
|
|
||||||
{:ok, resized_cover_data} = Image.resize(cover_data)
|
|
||||||
|
|
||||||
|
{:ok, resized_cover_data} = Image.resize(marbles_cover_data())
|
||||||
assets = Assets.get(record.cover_hash)
|
assets = Assets.get(record.cover_hash)
|
||||||
|
|
||||||
assert assets.content == resized_cover_data
|
assert assets.content == resized_cover_data
|
||||||
|
|
||||||
assert record.inserted_at !== nil
|
refute_enqueued(worker: ImportFromMusicbrainzReleaseGroup)
|
||||||
assert record.updated_at !== nil
|
|
||||||
assert record.purchased_at !== nil
|
|
||||||
|
|
||||||
[marillion] = record.artists
|
|
||||||
|
|
||||||
assert %MusicLibrary.Artists.Artist{
|
|
||||||
name: "Marillion",
|
|
||||||
sort_name: "Marillion",
|
|
||||||
disambiguation: "British progressive rock band",
|
|
||||||
musicbrainz_id: "1932f5b6-0b7b-4050-b1df-833ca89e5f44"
|
|
||||||
} = marillion
|
|
||||||
|
|
||||||
assert_path(session, ~p"/collection/#{record.id}")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "enqueues one job per cart item for 2+ items and closes modal", %{conn: conn} do
|
||||||
|
stub_release_group_search()
|
||||||
|
|
||||||
|
[first, second | _] = Map.get(release_group_search_results(), "release-groups")
|
||||||
|
first_id = first["id"]
|
||||||
|
second_id = second["id"]
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> visit(~p"/collection/import")
|
||||||
|
|> fill_in("Search for a record", with: "Marillion Marbles")
|
||||||
|
|> click_link("#musicbrainz_#{first_id} a", "CD")
|
||||||
|
|> click_link("#musicbrainz_#{second_id} a", "Vinyl")
|
||||||
|
|> click_button("Import 2 records")
|
||||||
|
|> assert_has("p", text: "Importing 2 records in the background...")
|
||||||
|
|
||||||
|
assert_enqueued(
|
||||||
|
worker: ImportFromMusicbrainzReleaseGroup,
|
||||||
|
args: %{"release_group_id" => first_id, "format" => "cd"}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_enqueued(
|
||||||
|
worker: ImportFromMusicbrainzReleaseGroup,
|
||||||
|
args: %{"release_group_id" => second_id, "format" => "vinyl"}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert MusicLibrary.Repo.all(Record) == []
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp stub_release_group_search do
|
||||||
|
Req.Test.stub(MusicBrainz.API, fn conn ->
|
||||||
|
case conn.path_info do
|
||||||
|
[_ws, _version, "release-group"] ->
|
||||||
|
Req.Test.json(conn, release_group_search_results())
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp stub_full_import do
|
||||||
|
[first | _] = Map.get(release_group_search_results(), "release-groups")
|
||||||
|
first_id = first["id"]
|
||||||
|
|
||||||
|
release_group = release_group(:marbles)
|
||||||
|
release_group_releases = release_group_releases(:marbles)
|
||||||
|
cover_data = marbles_cover_data()
|
||||||
|
|
||||||
|
Req.Test.stub(MusicBrainz.API, fn conn ->
|
||||||
|
case conn.path_info do
|
||||||
|
[_ws, _version, "release-group", ^first_id] ->
|
||||||
|
Req.Test.json(conn, release_group)
|
||||||
|
|
||||||
|
[_ws, _version, "release-group"] ->
|
||||||
|
Req.Test.json(conn, release_group_search_results())
|
||||||
|
|
||||||
|
[_ws, _version, "release"] ->
|
||||||
|
Req.Test.json(conn, release_group_releases)
|
||||||
|
|
||||||
|
[_release_group, ^first_id, "front"] ->
|
||||||
|
Plug.Conn.send_resp(conn, 200, cover_data)
|
||||||
|
end
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "Add via barcode scan" do
|
describe "Add via barcode scan" do
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
defmodule MusicLibraryWeb.WishlistLive.IndexTest do
|
defmodule MusicLibraryWeb.WishlistLive.IndexTest do
|
||||||
use MusicLibraryWeb.ConnCase
|
use MusicLibraryWeb.ConnCase
|
||||||
|
use Oban.Testing, repo: MusicLibrary.BackgroundRepo
|
||||||
|
|
||||||
|
import MusicBrainz.Fixtures.ReleaseGroup
|
||||||
import MusicLibrary.Fixtures.Records
|
import MusicLibrary.Fixtures.Records
|
||||||
|
|
||||||
|
alias MusicLibrary.Records.Record
|
||||||
|
alias MusicLibrary.Worker.ImportFromMusicbrainzReleaseGroup
|
||||||
|
|
||||||
defp fill_wishlist(_) do
|
defp fill_wishlist(_) do
|
||||||
records = Enum.map(1..5, fn _ -> record(%{purchased_at: nil}) end)
|
records = Enum.map(1..5, fn _ -> record(%{purchased_at: nil}) end)
|
||||||
%{wishlist: records}
|
%{wishlist: records}
|
||||||
@@ -27,4 +32,49 @@ defmodule MusicLibraryWeb.WishlistLive.IndexTest do
|
|||||||
assert purchased_record.purchased_at !== nil
|
assert purchased_record.purchased_at !== nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "Adding a new record" do
|
||||||
|
test "enqueues one job per cart item with purchased_at: nil for wishlist", %{conn: conn} do
|
||||||
|
Req.Test.stub(MusicBrainz.API, fn conn ->
|
||||||
|
case conn.path_info do
|
||||||
|
[_ws, _version, "release-group"] ->
|
||||||
|
Req.Test.json(conn, release_group_search_results())
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
[first, second | _] = Map.get(release_group_search_results(), "release-groups")
|
||||||
|
first_id = first["id"]
|
||||||
|
second_id = second["id"]
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> visit(~p"/wishlist/import")
|
||||||
|
|> fill_in("Search for a record", with: "Marillion Marbles")
|
||||||
|
|> click_link("#musicbrainz_#{first_id} a", "CD")
|
||||||
|
|> click_link("#musicbrainz_#{second_id} a", "Vinyl")
|
||||||
|
|> click_button("Import 2 records")
|
||||||
|
|> assert_has("p", text: "Importing 2 records in the background...")
|
||||||
|
|
||||||
|
assert_enqueued(
|
||||||
|
worker: ImportFromMusicbrainzReleaseGroup,
|
||||||
|
args: %{
|
||||||
|
"release_group_id" => first_id,
|
||||||
|
"format" => "cd",
|
||||||
|
"purchased_at" => nil
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_enqueued(
|
||||||
|
worker: ImportFromMusicbrainzReleaseGroup,
|
||||||
|
args: %{
|
||||||
|
"release_group_id" => second_id,
|
||||||
|
"format" => "vinyl",
|
||||||
|
"purchased_at" => nil
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
import Ecto.Query, only: [from: 2]
|
||||||
|
query = from(r in Record, where: not is_nil(r.purchased_at))
|
||||||
|
assert MusicLibrary.Repo.all(query) == []
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user