defmodule Ammoprices.Scraping.Retailers.PalmettoStateArmory do @moduledoc false @behaviour Ammoprices.Scraping.Scraper alias Ammoprices.Scraping.TextDetector @slug_to_path %{ "9mm-luger" => "/9mm-ammo.html", "45-acp" => "/45-acp-ammo.html", "380-acp" => "/380-acp-ammo.html", "40-sw" => "/40-s-w-ammo.html", "38-special" => "/38-special-ammo.html", "357-magnum" => "/357-magnum-ammo.html", "10mm-auto" => "/10mm-auto-ammo.html", "223-rem" => "/223-ammo.html", "556-nato" => "/5-56-ammo.html", "308-win" => "/308-winchester-ammo.html", "762x39" => "/7-62x39mm-ammo.html", "30-06" => "/30-06-springfield-ammo.html", "300-blackout" => "/300-aac-blackout-ammo.html", "65-creedmoor" => "/6-5-creedmoor-ammo.html", "22-lr" => "/22-lr-ammo.html", "22-wmr" => "/22-wmr-ammo.html", "17-hmr" => "/17-hmr-ammo.html", "12-gauge" => "/12-gauge-ammo.html", "16-gauge" => "/16-gauge-ammo.html", "20-gauge" => "/20-gauge-ammo.html" } @impl true def retailer_slug, do: "palmetto-state-armory" @impl true def category_url(caliber) do Map.get(@slug_to_path, caliber.slug) end @impl true def parse_products(html) do html |> Floki.parse_document!() |> Floki.find("li.product-item") |> Enum.map(&parse_item/1) |> Enum.reject(&is_nil/1) end defp parse_item(item) do with {:ok, title, url} <- extract_link_info(item), {:ok, price_cents} <- extract_price(item), {:ok, ppr_cents} <- extract_unit_price(item) do %{ title: title, url: url, price_cents: price_cents, price_per_round_cents: ppr_cents, brand: extract_brand(title), grain_weight: extract_grain_weight(title), round_count: extract_round_count(title), casing: TextDetector.detect_casing(title), subsonic: TextDetector.detect_subsonic(title), in_stock: true } else _ -> nil end end defp extract_link_info(item) do case Floki.find(item, ".product-item-link") do [{_, attrs, _} = node] -> title = node |> Floki.text() |> String.trim() case List.keyfind(attrs, "href", 0) do {"href", href} -> {:ok, title, href} _ -> :error end _ -> :error end end defp extract_price(item) do case Floki.find(item, "[data-price-type=\"finalPrice\"]") do [{_, attrs, _} | _] -> case List.keyfind(attrs, "data-price-amount", 0) do {"data-price-amount", amount} -> {:ok, amount |> String.to_float() |> Kernel.*(100) |> round()} _ -> :error end _ -> :error end end defp extract_unit_price(item) do case Floki.find(item, "[data-price-type=\"unit_price\"]") do [{_, attrs, _} | _] -> case List.keyfind(attrs, "data-price-amount", 0) do {"data-price-amount", amount} -> {:ok, amount |> String.to_float() |> Kernel.*(100) |> round()} _ -> :error end _ -> :error end end defp extract_brand(title) do case String.split(title, " ", parts: 2) do [brand | _] -> brand _ -> nil end end defp extract_grain_weight(title) do case Regex.run(~r/(\d+)\s*(?:gr(?:ain)?)/i, title) do [_, weight] -> String.to_integer(weight) _ -> nil end end defp extract_round_count(title) do cond do match = Regex.run(~r/(\d[\d,]*)\s*(?:rds?|rounds?)/i, title) -> match |> Enum.at(1) |> String.replace(",", "") |> String.to_integer() match = Regex.run(~r/(\d+)\s*rd\/box/i, title) -> match |> Enum.at(1) |> String.to_integer() true -> nil end end end