This commit is contained in:
@@ -1,4 +1,6 @@
|
|||||||
defmodule CobblemonUi.TierListScraper do
|
defmodule CobblemonUi.TierListScraper do
|
||||||
|
require Logger
|
||||||
|
|
||||||
@url "https://rankedboost.com/pokemon/tier-list/"
|
@url "https://rankedboost.com/pokemon/tier-list/"
|
||||||
@filename "pokemon_tier_list.json"
|
@filename "pokemon_tier_list.json"
|
||||||
|
|
||||||
@@ -8,26 +10,49 @@ defmodule CobblemonUi.TierListScraper do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def run do
|
def run do
|
||||||
|
Logger.info("[TierListScraper] Starting scrape from #{@url}")
|
||||||
|
|
||||||
with {:ok, html} <- fetch_page(),
|
with {:ok, html} <- fetch_page(),
|
||||||
pokemon <- parse(html),
|
{:ok, pokemon} <- parse(html),
|
||||||
:ok <- write_json(pokemon) do
|
:ok <- write_json(pokemon) do
|
||||||
|
Logger.info("[TierListScraper] Successfully scraped and saved #{length(pokemon)} pokemon")
|
||||||
{:ok, pokemon}
|
{:ok, pokemon}
|
||||||
|
else
|
||||||
|
{:error, reason} = err ->
|
||||||
|
Logger.error("[TierListScraper] Scrape failed: #{inspect(reason)}")
|
||||||
|
err
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_page do
|
def fetch_page do
|
||||||
case Req.get(@url) do
|
Logger.debug("[TierListScraper] Fetching #{@url}")
|
||||||
{:ok, %Req.Response{status: 200, body: body}} -> {:ok, body}
|
case Req.get(@url, headers: [{"user-agent", "Mozilla/5.0 (compatible; CobblemonUI/1.0)"}]) do
|
||||||
{:ok, %Req.Response{status: status}} -> {:error, {:http_error, status}}
|
{:ok, %Req.Response{status: 200, body: body}} ->
|
||||||
{:error, err} -> {:error, err}
|
Logger.debug("[TierListScraper] Fetch OK, body size: #{byte_size(body)} bytes")
|
||||||
|
{:ok, body}
|
||||||
|
{:ok, %Req.Response{status: status}} ->
|
||||||
|
Logger.warning("[TierListScraper] Unexpected HTTP status: #{status}")
|
||||||
|
{:error, {:http_error, status}}
|
||||||
|
{:error, err} ->
|
||||||
|
Logger.error("[TierListScraper] HTTP request failed: #{inspect(err)}")
|
||||||
|
{:error, err}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def parse(html) do
|
def parse(html) do
|
||||||
html
|
nodes = html |> Floki.parse_document!() |> Floki.find(".pokemon-tier")
|
||||||
|> Floki.parse_document!()
|
Logger.debug("[TierListScraper] Found #{length(nodes)} .pokemon-tier nodes")
|
||||||
|> Floki.find(".pokemon-tier")
|
|
||||||
|> Enum.map(&extract_pokemon/1)
|
case nodes do
|
||||||
|
[] ->
|
||||||
|
Logger.warning("[TierListScraper] No .pokemon-tier elements found — page structure may have changed or content is JS-rendered")
|
||||||
|
{:error, :no_pokemon_found}
|
||||||
|
_ ->
|
||||||
|
pokemon = Enum.map(nodes, &extract_pokemon/1)
|
||||||
|
valid = Enum.filter(pokemon, fn %{name: n} -> n != "" end)
|
||||||
|
Logger.info("[TierListScraper] Parsed #{length(valid)} valid pokemon (#{length(pokemon) - length(valid)} skipped with empty names)")
|
||||||
|
{:ok, valid}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp extract_pokemon(node) do
|
defp extract_pokemon(node) do
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
defmodule CobblemonUiWeb.DashboardLive do
|
defmodule CobblemonUiWeb.DashboardLive do
|
||||||
use CobblemonUiWeb, :live_view
|
use CobblemonUiWeb, :live_view
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
import CobblemonUiWeb.PokemonComponents
|
import CobblemonUiWeb.PokemonComponents
|
||||||
import CobblemonUiWeb.BattleComponents
|
import CobblemonUiWeb.BattleComponents
|
||||||
|
|
||||||
@@ -94,9 +96,6 @@ defmodule CobblemonUiWeb.DashboardLive do
|
|||||||
{:noreply, assign(socket, view_mode: String.to_existing_atom(mode), selected_pokemon: nil)}
|
{:noreply, assign(socket, view_mode: String.to_existing_atom(mode), selected_pokemon: nil)}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("refresh", _params, socket) do
|
|
||||||
{:noreply, do_refresh(socket)}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_info(:tick, socket) do
|
def handle_info(:tick, socket) do
|
||||||
@@ -106,8 +105,10 @@ defmodule CobblemonUiWeb.DashboardLive do
|
|||||||
def handle_info(:scrape_tier_list, socket) do
|
def handle_info(:scrape_tier_list, socket) do
|
||||||
lv = self()
|
lv = self()
|
||||||
Task.start(fn ->
|
Task.start(fn ->
|
||||||
CobblemonUi.TierListScraper.run()
|
case CobblemonUi.TierListScraper.run() do
|
||||||
send(lv, :reload_tier_list)
|
{:ok, _} -> send(lv, :reload_tier_list)
|
||||||
|
{:error, reason} -> Logger.error("[DashboardLive] Tier list scrape failed: #{inspect(reason)}")
|
||||||
|
end
|
||||||
end)
|
end)
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
@@ -156,13 +157,6 @@ defmodule CobblemonUiWeb.DashboardLive do
|
|||||||
<p class="text-sm text-base-content/50">Who are you?</p>
|
<p class="text-sm text-base-content/50">Who are you?</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
id="refresh-btn"
|
|
||||||
phx-click="refresh"
|
|
||||||
class="btn btn-ghost btn-sm gap-2 hover:bg-base-300/50 transition-colors"
|
|
||||||
>
|
|
||||||
<.icon name="hero-arrow-path" class="size-4" /> Refresh
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -211,13 +205,6 @@ defmodule CobblemonUiWeb.DashboardLive do
|
|||||||
<p class="text-sm text-base-content/50">Player Data Explorer</p>
|
<p class="text-sm text-base-content/50">Player Data Explorer</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
id="refresh-btn"
|
|
||||||
phx-click="refresh"
|
|
||||||
class="btn btn-ghost btn-sm gap-2 hover:bg-base-300/50 transition-colors"
|
|
||||||
>
|
|
||||||
<.icon name="hero-arrow-path" class="size-4" /> Refresh
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%!-- Scrollable content --%>
|
<%!-- Scrollable content --%>
|
||||||
|
|||||||
@@ -46,6 +46,17 @@ defmodule CobblemonUiWeb.PokemonComponents do
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1 shrink-0">
|
<div class="flex items-center gap-1 shrink-0">
|
||||||
<.tier_badge :if={@tier} tier={@tier} species={@pokemon.species} compact={@compact} />
|
<.tier_badge :if={@tier} tier={@tier} species={@pokemon.species} compact={@compact} />
|
||||||
|
<div
|
||||||
|
:if={!@tier}
|
||||||
|
title="Tier data not downloaded"
|
||||||
|
class={[
|
||||||
|
"inline-flex items-center justify-center font-black rounded leading-none shrink-0",
|
||||||
|
"bg-base-300/20 text-base-content/25 ring-1 ring-base-300/30",
|
||||||
|
if(@compact, do: "text-[9px] w-4 h-4", else: "text-[10px] w-5 h-5")
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
?
|
||||||
|
</div>
|
||||||
<div :if={@pokemon.shiny} title="Shiny">
|
<div :if={@pokemon.shiny} title="Shiny">
|
||||||
<.icon
|
<.icon
|
||||||
name="hero-sparkles"
|
name="hero-sparkles"
|
||||||
@@ -126,6 +137,13 @@ defmodule CobblemonUiWeb.PokemonComponents do
|
|||||||
<h3 class="font-bold text-base-content capitalize text-lg flex items-center gap-2">
|
<h3 class="font-bold text-base-content capitalize text-lg flex items-center gap-2">
|
||||||
{@pokemon.species || "Unknown"}
|
{@pokemon.species || "Unknown"}
|
||||||
<.tier_badge :if={@tier} tier={@tier} species={@pokemon.species} />
|
<.tier_badge :if={@tier} tier={@tier} species={@pokemon.species} />
|
||||||
|
<span
|
||||||
|
:if={!@tier}
|
||||||
|
class="inline-flex items-center gap-1 text-xs text-base-content/30 font-normal italic"
|
||||||
|
title="Tier data not downloaded"
|
||||||
|
>
|
||||||
|
<.icon name="hero-arrow-down-tray" class="size-3" /> tier unavailable
|
||||||
|
</span>
|
||||||
<span :if={@pokemon.shiny} class="text-warning text-sm" title="Shiny">★</span>
|
<span :if={@pokemon.shiny} class="text-warning text-sm" title="Shiny">★</span>
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-xs text-base-content/40">
|
<p class="text-xs text-base-content/40">
|
||||||
|
|||||||
Reference in New Issue
Block a user