Skip to content

Commit

Permalink
Use LiveView async features for Audible book import
Browse files Browse the repository at this point in the history
  • Loading branch information
doughsay committed Mar 6, 2024
1 parent 72aabf8 commit 23de8d9
Show file tree
Hide file tree
Showing 8 changed files with 219 additions and 203 deletions.
2 changes: 2 additions & 0 deletions lib/ambry_scraping/audible/products.ex
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ defmodule AmbryScraping.Audible.Products do
@doc """
Returns product details for a given title search query.
"""
def search(""), do: {:ok, []}

def search(query) do
query =
URI.encode_query(%{
Expand Down
2 changes: 2 additions & 0 deletions lib/ambry_scraping/goodreads/books/search.ex
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ defmodule AmbryScraping.GoodReads.Books.Search do
defstruct [:src, :data_url]
end

def search(""), do: {:ok, []}

def search(query_string) do
query = URI.encode_query(%{utf8: "✓", query: query_string})
path = "/search" |> URI.new!() |> URI.append_query(query) |> URI.to_string()
Expand Down
29 changes: 10 additions & 19 deletions lib/ambry_web/live/admin/book_live/form.ex
Original file line number Diff line number Diff line change
Expand Up @@ -70,25 +70,6 @@ defmodule AmbryWeb.Admin.BookLive.Form do
{:noreply, assign_form(socket, changeset)}
end

# FIXME: Don't use form submit event for this
def handle_event("submit", %{"import" => import_type, "book" => book_params}, socket) do
changeset =
socket.assigns.book
|> Books.change_book(book_params)
|> Map.put(:action, :validate)

if Keyword.has_key?(changeset.errors, :title) do
{:noreply, assign_form(socket, changeset)}
else
socket =
assign(socket,
import: %{type: String.to_existing_atom(import_type), query: book_params["title"]}
)

{:noreply, socket}
end
end

def handle_event("submit", %{"book" => book_params}, socket) do
with {:ok, _book} <-
socket.assigns.book
Expand All @@ -104,6 +85,14 @@ defmodule AmbryWeb.Admin.BookLive.Form do
end
end

def handle_event("open-import-form", %{"type" => type}, socket) do
query = socket.assigns.form.params["title"]
import_type = String.to_existing_atom(type)
socket = assign(socket, import: %{type: import_type, query: query})

{:noreply, socket}
end

def handle_event("cancel-upload", %{"ref" => ref}, socket) do
{:noreply, cancel_upload(socket, :image, ref)}
end
Expand Down Expand Up @@ -181,4 +170,6 @@ defmodule AmbryWeb.Admin.BookLive.Form do

defp import_form(:goodreads), do: GoodreadsImportForm
defp import_form(:audible), do: AudibleImportForm

defp open_import_form(type), do: JS.push("open-import-form", value: %{"type" => type})
end
10 changes: 8 additions & 2 deletions lib/ambry_web/live/admin/book_live/form.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,16 @@
<div class="flex items-center gap-2">
<.input field={@form[:title]} show_errors={false} container_class="grow" />
<.label>Import from:</.label>
<.button :if={@scraping_available} color={:zinc} class="flex items-center gap-1" name="import" value="goodreads">
<.button
:if={@scraping_available}
color={:zinc}
class="flex items-center gap-1"
type="button"
phx-click={open_import_form("goodreads")}
>
<FA.icon name="goodreads" type="brands" class="h-4 w-4 fill-current" /> GoodReads
</.button>
<.button color={:zinc} class="flex items-center gap-1" name="import" value="audible">
<.button color={:zinc} class="flex items-center gap-1" type="button" phx-click={open_import_form("audible")}>
<FA.icon name="audible" type="brands" class="h-4 w-4 fill-current" /> Audible
</.button>
</div>
Expand Down
148 changes: 89 additions & 59 deletions lib/ambry_web/live/admin/book_live/form/audible_import_form.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ defmodule AmbryWeb.Admin.BookLive.Form.AudibleImportForm do
alias Ambry.People.Person
alias Ambry.Search
alias Ambry.Series.Series
alias Phoenix.LiveView.AsyncResult

@impl Phoenix.LiveComponent
def mount(socket) do
Expand All @@ -17,36 +18,93 @@ defmodule AmbryWeb.Admin.BookLive.Form.AudibleImportForm do

@impl Phoenix.LiveComponent
def update(assigns, socket) do
socket =
case Map.pop(assigns, :info) do
{nil, assigns} ->
socket
|> assign(assigns)
|> async_search(assigns.query)

{forwarded_info_payload, assigns} ->
socket
|> assign(assigns)
|> then(fn socket ->
handle_forwarded_info(forwarded_info_payload, socket)
end)
end
{:ok,
socket
|> assign(assigns)
|> assign(
books: AsyncResult.loading(),
selected_book: AsyncResult.loading(),
search_form: to_form(%{"query" => assigns.query}, as: :search),
select_book_form: to_form(%{}, as: :select_book),
form: to_form(init_import_form_params(assigns.book), as: :import)
)
|> start_async(:search, fn -> search(assigns.query) end)}
end

{:ok, socket}
@impl Phoenix.LiveComponent
def handle_async(:search, {:ok, books}, socket) do
[first_book | _rest] = books

{:noreply,
socket
|> assign(books: AsyncResult.ok(socket.assigns.books, books))
|> assign(select_book_form: to_form(%{"book_id" => first_book.id}, as: :select_book))
|> start_async(:select_book, fn -> select_book(first_book) end)}
end

def handle_async(:search, {:exit, {:shutdown, :cancel}}, socket) do
{:noreply, assign(socket, books: AsyncResult.loading())}
end

def handle_async(:search, {:exit, {exception, _stacktrace}}, socket) do
{:noreply, assign(socket, books: AsyncResult.failed(socket.assigns.books, exception.message))}
end

def handle_async(:select_book, {:ok, results}, socket) do
%{
selected_book: selected_book,
matching_authors: matching_authors,
matching_series: matching_series
} = results

{:noreply,
assign(socket,
selected_book: AsyncResult.ok(socket.assigns.selected_book, selected_book),
matching_authors: matching_authors,
matching_series: matching_series
)}
end

def handle_async(:select_book, {:exit, {:shutdown, :cancel}}, socket) do
{:noreply, assign(socket, selected_book: AsyncResult.loading())}
end

def handle_async(:select_book, {:exit, {exception, _stacktrace}}, socket) do
{:noreply,
assign(socket,
selected_book: AsyncResult.failed(socket.assigns.selected_book, exception.message)
)}
end

@impl Phoenix.LiveComponent
def handle_event("search", %{"search" => %{"query" => query}}, socket) do
{:noreply, async_search(socket, query)}
{:noreply,
socket
|> assign(
books: AsyncResult.loading(),
selected_book: AsyncResult.loading(),
search_form: to_form(%{"query" => query}, as: :search)
)
|> cancel_async(:search)
|> cancel_async(:select_book)
|> start_async(:search, fn -> search(query) end)}
end

def handle_event("select-book", %{"select_book" => %{"book_id" => book_id}}, socket) do
book = Enum.find(socket.assigns.books, &(&1.id == book_id))
{:noreply, select_book(socket, book)}
book = Enum.find(socket.assigns.books.result, &(&1.id == book_id))

{:noreply,
socket
|> assign(
selected_book: AsyncResult.loading(),
select_book_form: to_form(%{"book_id" => book.id}, as: :select_book)
)
|> cancel_async(:select_book)
|> start_async(:select_book, fn -> select_book(book) end)}
end

def handle_event("import", %{"import" => import_params}, socket) do
book = socket.assigns.selected_book
book = socket.assigns.selected_book.result

params =
Enum.reduce(import_params, %{}, fn
Expand All @@ -60,14 +118,14 @@ defmodule AmbryWeb.Admin.BookLive.Form.AudibleImportForm do
Map.put(
acc,
"book_authors",
build_authors_params(book.authors, socket.assigns.matching_authors)
build_authors_params(book.authors, socket.assigns.matching_authors.result)
)

{"use_series", "true"}, acc ->
Map.put(
acc,
"series_books",
build_series_params(book.series, socket.assigns.matching_series)
build_series_params(book.series, socket.assigns.matching_series.result)
)

{"use_cover_image", "true"}, acc ->
Expand Down Expand Up @@ -119,7 +177,15 @@ defmodule AmbryWeb.Admin.BookLive.Form.AudibleImportForm do
end)
end

defp select_book(socket, book) do
defp search(query) do
case "#{query}" |> String.trim() |> String.downcase() |> Audible.search_books() do
{:ok, []} -> raise "No books found"
{:ok, books} -> books
{:error, reason} -> raise "Unhandled error: #{inspect(reason)}"
end
end

defp select_book(book) do
matching_authors =
Enum.map(book.authors, fn author ->
Search.find_first(author.name, Person)
Expand All @@ -130,43 +196,7 @@ defmodule AmbryWeb.Admin.BookLive.Form.AudibleImportForm do
Search.find_first(series.title, Series)
end)

assign(socket,
selected_book: book,
matching_authors: matching_authors,
matching_series: matching_series,
select_book_form: to_form(%{"book_id" => book.id}, as: :select_book)
)
end

defp handle_forwarded_info({:search, {:ok, books}}, socket) do
socket = assign(socket, search_loading: false, books: books)

case books do
[] -> socket
[first_result | _rest] -> select_book(socket, first_result)
end
end

defp handle_forwarded_info({:search, {:error, _reason}}, socket) do
socket
|> put_flash(:error, "search failed")
|> assign(search_loading: false)
end

defp async_search(socket, query) do
Task.async(fn ->
response = Audible.search_books(query |> String.trim() |> String.downcase())
{{:for, __MODULE__, socket.assigns.id}, {:search, response}}
end)

assign(socket,
search_form: to_form(%{"query" => query}, as: :search),
search_loading: true,
books: [],
select_book_form: to_form(%{}, as: :select_book),
selected_book: nil,
form: to_form(init_import_form_params(socket.assigns.book), as: :import)
)
%{selected_book: book, matching_authors: matching_authors, matching_series: matching_series}
end

defp init_import_form_params(book) do
Expand Down
Loading

0 comments on commit 23de8d9

Please sign in to comment.