Show barcodes that are not found inside the results UI
Includes a refactor to extract/cleanup barcode scan logic from the component to a separate context with a better API.
This commit is contained in:
@@ -0,0 +1,48 @@
|
|||||||
|
defmodule MusicLibrary.BarcodeScan do
|
||||||
|
alias MusicLibrary.BarcodeScan.Result
|
||||||
|
alias MusicLibrary.Records
|
||||||
|
|
||||||
|
def scan(number) do
|
||||||
|
case MusicBrainz.search_release_by_barcode(number) do
|
||||||
|
{:ok, [best_match_release | _other_releases]} ->
|
||||||
|
format = MusicBrainz.ReleaseSearchResult.format(best_match_release)
|
||||||
|
|
||||||
|
case Records.get_release_status(best_match_release.id, format) do
|
||||||
|
:new ->
|
||||||
|
{:ok, Result.new(number, best_match_release)}
|
||||||
|
|
||||||
|
{:wishlisted, record_id} ->
|
||||||
|
{:ok, Result.wishlisted(number, record_id, best_match_release)}
|
||||||
|
|
||||||
|
{:collected, record_id} ->
|
||||||
|
{:ok, Result.collected(number, record_id, best_match_release)}
|
||||||
|
end
|
||||||
|
|
||||||
|
{:ok, []} ->
|
||||||
|
{:ok, Result.not_found(number)}
|
||||||
|
|
||||||
|
error ->
|
||||||
|
error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def import(scan_result, current_time) do
|
||||||
|
case scan_result.status do
|
||||||
|
:new ->
|
||||||
|
Records.import_from_musicbrainz_release(scan_result.release.id,
|
||||||
|
format: MusicBrainz.ReleaseSearchResult.format(scan_result.release),
|
||||||
|
purchased_at: current_time
|
||||||
|
)
|
||||||
|
|
||||||
|
:wishlisted ->
|
||||||
|
record = Records.get_record!(scan_result.record_id)
|
||||||
|
Records.update_record(record, %{"purchased_at" => current_time})
|
||||||
|
|
||||||
|
:collected ->
|
||||||
|
{:error, :already_collected}
|
||||||
|
|
||||||
|
:not_found ->
|
||||||
|
{:error, :not_found}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
defmodule MusicLibrary.BarcodeScan.Result do
|
||||||
|
defstruct [:status, :number, :record_id, :release]
|
||||||
|
|
||||||
|
def new(number, release) do
|
||||||
|
%__MODULE__{
|
||||||
|
number: number,
|
||||||
|
status: :new,
|
||||||
|
release: release
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def wishlisted(number, record_id, release) do
|
||||||
|
%__MODULE__{
|
||||||
|
number: number,
|
||||||
|
status: :wishlisted,
|
||||||
|
record_id: record_id,
|
||||||
|
release: release
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def collected(number, record_id, release) do
|
||||||
|
%__MODULE__{
|
||||||
|
number: number,
|
||||||
|
status: :collected,
|
||||||
|
record_id: record_id,
|
||||||
|
release: release
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def not_found(number) do
|
||||||
|
%__MODULE__{
|
||||||
|
number: number,
|
||||||
|
status: :not_found
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -107,7 +107,11 @@ defmodule MusicLibrary.Records do
|
|||||||
purchased_at: fragment("records.purchased_at")
|
purchased_at: fragment("records.purchased_at")
|
||||||
}
|
}
|
||||||
|
|
||||||
Repo.one(q)
|
case Repo.one(q) do
|
||||||
|
nil -> :new
|
||||||
|
%{record_id: record_id, purchased_at: nil} -> {:wishlisted, record_id}
|
||||||
|
%{record_id: record_id} -> {:collected, record_id}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_artist_records(musicbrainz_id) do
|
def get_artist_records(musicbrainz_id) do
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ defmodule MusicLibraryWeb.BarcodeScannerComponent do
|
|||||||
use MusicLibraryWeb, :live_component
|
use MusicLibraryWeb, :live_component
|
||||||
|
|
||||||
alias MusicBrainz.ReleaseGroupSearchResult
|
alias MusicBrainz.ReleaseGroupSearchResult
|
||||||
|
alias MusicLibrary.BarcodeScan
|
||||||
alias MusicLibrary.Records
|
alias MusicLibrary.Records
|
||||||
alias MusicLibraryWeb.RecordComponents
|
alias MusicLibraryWeb.RecordComponents
|
||||||
|
|
||||||
@@ -12,7 +13,7 @@ defmodule MusicLibraryWeb.BarcodeScannerComponent do
|
|||||||
{:ok,
|
{:ok,
|
||||||
socket
|
socket
|
||||||
|> assign(:camera, :pending)
|
|> assign(:camera, :pending)
|
||||||
|> assign(:releases, [])}
|
|> assign(:scan_results, [])}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
@@ -31,8 +32,8 @@ defmodule MusicLibraryWeb.BarcodeScannerComponent do
|
|||||||
|
|
||||||
<ul class="divide-y divide-zinc-100 dark:divide-slate-300/30 mt-5">
|
<ul class="divide-y divide-zinc-100 dark:divide-slate-300/30 mt-5">
|
||||||
<li
|
<li
|
||||||
:for={{status, record_id, release} <- @releases}
|
:for={scan_result <- @scan_results}
|
||||||
id={release.id}
|
id={scan_result.number}
|
||||||
class="flex justify-between gap-x-6 py-5"
|
class="flex justify-between gap-x-6 py-5"
|
||||||
phx-mounted={
|
phx-mounted={
|
||||||
JS.transition(
|
JS.transition(
|
||||||
@@ -42,13 +43,13 @@ defmodule MusicLibraryWeb.BarcodeScannerComponent do
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<.release status={status} record_id={record_id} release={release} />
|
<.scan_result scan_result={scan_result} />
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="mt-4 flex justify-center">
|
<div class="mt-4 flex justify-center">
|
||||||
<.button
|
<.button
|
||||||
disabled={length(@releases) == 0}
|
disabled={length(@scan_results) == 0}
|
||||||
phx-disable-with={gettext("Importing...")}
|
phx-disable-with={gettext("Importing...")}
|
||||||
phx-click={JS.push("import_releases", target: "#barcode-scanner")}
|
phx-click={JS.push("import_releases", target: "#barcode-scanner")}
|
||||||
>
|
>
|
||||||
@@ -94,6 +95,35 @@ defmodule MusicLibraryWeb.BarcodeScannerComponent do
|
|||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
attr :scan_result, BarcodeScan.Result, required: true
|
||||||
|
|
||||||
|
defp scan_result(assigns) do
|
||||||
|
~H"""
|
||||||
|
<.barcode_not_found :if={@scan_result.status == :not_found} number={@scan_result.number} />
|
||||||
|
<.release
|
||||||
|
:if={@scan_result.status != :not_found}
|
||||||
|
release={@scan_result.release}
|
||||||
|
record_id={@scan_result.record_id}
|
||||||
|
status={@scan_result.status}
|
||||||
|
/>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
attr :number, :string, required: true
|
||||||
|
|
||||||
|
defp barcode_not_found(assigns) do
|
||||||
|
~H"""
|
||||||
|
<div class="w-full bg-red-50 dark:bg-red-950 p-4">
|
||||||
|
<h1 class="text-sm leading-6 text-zinc-700 dark:text-zinc-400">
|
||||||
|
{gettext("Barcode not found")}
|
||||||
|
</h1>
|
||||||
|
<h2 class="mt-1 flex font-semibold text-sm sm:text-base leading-5 text-zinc-700 dark:text-zinc-300 text-wrap">
|
||||||
|
{@number}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
attr :release, MusicBrainz.ReleaseSearchResult, required: true
|
attr :release, MusicBrainz.ReleaseSearchResult, required: true
|
||||||
attr :record_id, :string
|
attr :record_id, :string
|
||||||
attr :status, :atom, required: true, values: [:collected, :wishlisted, :new]
|
attr :status, :atom, required: true, values: [:collected, :wishlisted, :new]
|
||||||
@@ -133,35 +163,23 @@ defmodule MusicLibraryWeb.BarcodeScannerComponent do
|
|||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("camera_allowed", _params, socket) do
|
def handle_event("camera_allowed", _params, socket) do
|
||||||
Logger.debug(fn -> "Camera access allowed" end)
|
|
||||||
{:noreply, assign(socket, camera: :allowed)}
|
{:noreply, assign(socket, camera: :allowed)}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("camera_denied", _params, socket) do
|
def handle_event("camera_denied", _params, socket) do
|
||||||
Logger.debug(fn -> "Camera access denied" end)
|
|
||||||
{:noreply, assign(socket, camera: :denied)}
|
{:noreply, assign(socket, camera: :denied)}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("barcode_scanned", %{"number" => number}, socket) do
|
def handle_event("barcode_scanned", %{"number" => number}, socket) do
|
||||||
Logger.debug(fn -> "Scanned barcode #{number}" end)
|
|
||||||
|
|
||||||
socket =
|
socket =
|
||||||
case MusicBrainz.search_release_by_barcode(number) do
|
case BarcodeScan.scan(number) do
|
||||||
{:ok, [best_match_release | _other_releases]} ->
|
{:ok, scan_result} ->
|
||||||
Logger.debug(fn -> "Found release #{best_match_release.id}" end)
|
assign(socket, :scan_results, [scan_result | socket.assigns.scan_results])
|
||||||
assign_release_with_status(best_match_release, socket)
|
|
||||||
|
|
||||||
{:ok, []} ->
|
{:error, reason} ->
|
||||||
Logger.debug(fn -> "No release found for barcode #{number}" end)
|
Logger.error(fn ->
|
||||||
|
"Failed to search release for barcode #{number}: #{inspect(reason)}"
|
||||||
put_flash(
|
end)
|
||||||
socket,
|
|
||||||
:error,
|
|
||||||
gettext("No release found for barcode %{number}", number: number)
|
|
||||||
)
|
|
||||||
|
|
||||||
{:error, _reason} ->
|
|
||||||
Logger.error(fn -> "Failed to search release for barcode #{number}" end)
|
|
||||||
|
|
||||||
put_flash(
|
put_flash(
|
||||||
socket,
|
socket,
|
||||||
@@ -177,47 +195,19 @@ defmodule MusicLibraryWeb.BarcodeScannerComponent do
|
|||||||
current_time = DateTime.utc_now()
|
current_time = DateTime.utc_now()
|
||||||
# TODO: error handling when a release fails to import
|
# TODO: error handling when a release fails to import
|
||||||
:ok =
|
:ok =
|
||||||
Enum.each(socket.assigns.releases, fn {status, record_id, release} ->
|
Enum.each(socket.assigns.scan_results, fn scan_result ->
|
||||||
if status == :new do
|
BarcodeScan.import(scan_result, current_time)
|
||||||
Records.import_from_musicbrainz_release(release.id,
|
|
||||||
format: MusicBrainz.ReleaseSearchResult.format(release),
|
|
||||||
purchased_at: current_time
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
if status == :wishlisted do
|
|
||||||
record = Records.get_record!(record_id)
|
|
||||||
Records.update_record(record, %{"purchased_at" => current_time})
|
|
||||||
end
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
qs = %{order: :purchase}
|
qs = %{order: :purchase}
|
||||||
|
|
||||||
{:noreply,
|
{:noreply,
|
||||||
socket
|
socket
|
||||||
|> assign(:releases, [])
|
|> assign(:scan_results, [])
|
||||||
|> put_flash(:info, gettext("Records imported successfully"))
|
|> put_flash(:info, gettext("Records imported successfully"))
|
||||||
|> push_patch(to: ~p"/collection?#{qs}")}
|
|> push_patch(to: ~p"/collection?#{qs}")}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp assign_release_with_status(release, socket) do
|
|
||||||
format = MusicBrainz.ReleaseSearchResult.format(release)
|
|
||||||
|
|
||||||
release_with_status =
|
|
||||||
case Records.get_release_status(release.id, format) do
|
|
||||||
nil ->
|
|
||||||
{:new, nil, release}
|
|
||||||
|
|
||||||
%{record_id: record_id, purchased_at: nil} ->
|
|
||||||
{:wishlisted, record_id, release}
|
|
||||||
|
|
||||||
%{record_id: record_id} ->
|
|
||||||
{:collected, record_id, release}
|
|
||||||
end
|
|
||||||
|
|
||||||
assign(socket, :releases, [release_with_status | socket.assigns.releases])
|
|
||||||
end
|
|
||||||
|
|
||||||
defp release_format_label(release) do
|
defp release_format_label(release) do
|
||||||
release
|
release
|
||||||
|> MusicBrainz.ReleaseSearchResult.format()
|
|> MusicBrainz.ReleaseSearchResult.format()
|
||||||
|
|||||||
+14
-14
@@ -338,13 +338,13 @@ msgstr ""
|
|||||||
msgid "Choose which format to import"
|
msgid "Choose which format to import"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/music_library_web/components/barcode_scanner_component.ex:128
|
#: lib/music_library_web/components/barcode_scanner_component.ex:158
|
||||||
#: lib/music_library_web/live/stats_live/index.html.heex:141
|
#: lib/music_library_web/live/stats_live/index.html.heex:141
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Collected"
|
msgid "Collected"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/music_library_web/components/barcode_scanner_component.ex:125
|
#: lib/music_library_web/components/barcode_scanner_component.ex:155
|
||||||
#: lib/music_library_web/live/stats_live/index.html.heex:147
|
#: lib/music_library_web/live/stats_live/index.html.heex:147
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Wishlisted"
|
msgid "Wishlisted"
|
||||||
@@ -616,42 +616,37 @@ msgstr ""
|
|||||||
msgid "Scan barcodes · Collection"
|
msgid "Scan barcodes · Collection"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/music_library_web/components/barcode_scanner_component.ex:24
|
#: lib/music_library_web/components/barcode_scanner_component.ex:25
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Scan one or more barcodes"
|
msgid "Scan one or more barcodes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/music_library_web/components/barcode_scanner_component.ex:91
|
#: lib/music_library_web/components/barcode_scanner_component.ex:92
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Open camera"
|
msgid "Open camera"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/music_library_web/components/barcode_scanner_component.ex:55
|
#: lib/music_library_web/components/barcode_scanner_component.ex:56
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Import releases"
|
msgid "Import releases"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/music_library_web/components/barcode_scanner_component.ex:169
|
#: lib/music_library_web/components/barcode_scanner_component.ex:187
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Failed to search release for barcode %{number}"
|
msgid "Failed to search release for barcode %{number}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/music_library_web/components/barcode_scanner_component.ex:160
|
#: lib/music_library_web/components/barcode_scanner_component.ex:53
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "No release found for barcode %{number}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/music_library_web/components/barcode_scanner_component.ex:52
|
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Importing..."
|
msgid "Importing..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/music_library_web/components/barcode_scanner_component.ex:199
|
#: lib/music_library_web/components/barcode_scanner_component.ex:207
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Records imported successfully"
|
msgid "Records imported successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/music_library_web/components/barcode_scanner_component.ex:123
|
#: lib/music_library_web/components/barcode_scanner_component.ex:153
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "New"
|
msgid "New"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -690,3 +685,8 @@ msgstr ""
|
|||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Live Dashboard"
|
msgid "Live Dashboard"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/music_library_web/components/barcode_scanner_component.ex:118
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Barcode not found"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@@ -385,6 +385,7 @@ defmodule MusicLibraryWeb.CollectionLive.IndexTest do
|
|||||||
|> visit(~p"/collection/scan")
|
|> visit(~p"/collection/scan")
|
||||||
|> trigger_hook("#barcode-scanner", "barcode_scanned", %{"number" => barcode})
|
|> trigger_hook("#barcode-scanner", "barcode_scanned", %{"number" => barcode})
|
||||||
|> assert_has("h2", text: "Marbles")
|
|> assert_has("h2", text: "Marbles")
|
||||||
|
|> assert_has("span", text: "New")
|
||||||
|> click_button("Import releases")
|
|> click_button("Import releases")
|
||||||
|
|
||||||
[record] = MusicLibrary.Repo.all(MusicLibrary.Records.Record)
|
[record] = MusicLibrary.Repo.all(MusicLibrary.Records.Record)
|
||||||
|
|||||||
Reference in New Issue
Block a user