Compare commits
60 Commits
0987290dca
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| f6b4a211b8 | |||
| 4c8364328c | |||
| 5192978917 | |||
| 4dfae60875 | |||
| b7723c91df | |||
| 3993f97de1 | |||
| f0a7fc1303 | |||
| 8ed22bebf0 | |||
| e364b20b1b | |||
| 305b9237c8 | |||
| 39c729b420 | |||
| d290a2c457 | |||
| 078682fd48 | |||
| 56ba989ad8 | |||
| 667bf76591 | |||
| 2768e4434a | |||
| caf0e44f3e | |||
| 11c8aade4c | |||
| 71cb4c8868 | |||
| 3c817b910a | |||
| be2f02560c | |||
| e0aa1b2476 | |||
| 90d54f85d9 | |||
| fdca3e37d1 | |||
| d2463fd1f3 | |||
| 854756c5dd | |||
| 50837411d3 | |||
| 9e7bd07ae0 | |||
| c09c4a0664 | |||
| eb5e1cf41d | |||
| 3e443617e9 | |||
| 648dacdd1a | |||
| 7a613ac191 | |||
| 1414439ceb | |||
| 036c0cdd5b | |||
| ccac1fec15 | |||
| bba1913d65 | |||
| d24dc4726b | |||
| 0c0c7f1230 | |||
| 1c06ffb8a8 | |||
| 8cfc39e381 | |||
| 6d89ed657c | |||
| 3e3017f31a | |||
| 37dcd67bdc | |||
| ffb31bf2b1 | |||
| 4c8f7fd11d | |||
| d07e604cbe | |||
| 75ec6a49e9 | |||
| c3de834f57 | |||
| a5cb6dc299 | |||
| 56af93b14e | |||
| 45f72cb836 | |||
| 656edf30a7 | |||
| aafb805194 | |||
| bd8148f3e2 | |||
| 4823e68700 | |||
| 4371701ca9 | |||
| 60bf927c67 | |||
| f79f0db862 | |||
| 7e30db3864 |
@@ -1 +1,52 @@
|
||||
# LoLAnalytics.Umbrella
|
||||
# LoLAnalytics
|
||||
|
||||
## Requirements
|
||||
|
||||
- PostgreSQL
|
||||
- Elixir
|
||||
- RabbitMQ
|
||||
- MinIO
|
||||
|
||||
A `docker-compose` file is provided to run them locally.
|
||||
|
||||
## Environment variables
|
||||
|
||||
The followign environment variables are required:
|
||||
|
||||
```
|
||||
export RIOT_API_KEY="{API-KEY}"
|
||||
|
||||
export EX_AWS_SECRET_KEY="{SECRET}"
|
||||
export EX_AWS_ACCESS_KEY="{ACCESS}"
|
||||
export EX_AWS_ENDPOINT="{HOST}"
|
||||
export EX_AWS_PORT="{PORT}" # minio defaults to 9000
|
||||
export DATABASE_URL="ecto://{USERNAME}:{PASSWORD}@{HOST}/lol-analytics"
|
||||
export SECRET_KEY_BASE="SECRET-KEY"
|
||||
```
|
||||
|
||||
## Running
|
||||
|
||||
```
|
||||
mix deps.get
|
||||
mix ecto.create && mix ecto.migrate
|
||||
iex -S mix phx.server
|
||||
```
|
||||
|
||||
## Queueing a player
|
||||
|
||||
```
|
||||
Scrapper.Queues.PlayerQueue.enqueue_puuid("PUUID")
|
||||
```
|
||||
|
||||
The scrapper will retrieve player ranked history, enqueue it's teammates, and store every match in minio.
|
||||
|
||||
## Processing
|
||||
|
||||
A `Task` to process the matches stored for a patch can be spawned by running:
|
||||
```
|
||||
LolAnalytics.MatchesProcessor.process_for_patch "14.12.594.4901"
|
||||
```
|
||||
|
||||
## Web site
|
||||
|
||||
A web site is exposed at `localhost:4000`.
|
||||
@@ -10,7 +10,10 @@ defmodule LoLAnalytics.Application do
|
||||
children = [
|
||||
LoLAnalytics.Repo,
|
||||
{DNSCluster, query: Application.get_env(:lol_analytics, :dns_cluster_query) || :ignore},
|
||||
{Phoenix.PubSub, name: LoLAnalytics.PubSub}
|
||||
{Phoenix.PubSub, name: LoLAnalytics.PubSub},
|
||||
{Task.Supervisor, name: LoLAnalytics.TaskSupervisor},
|
||||
# {LolAnalytics.MatchProcessor.MatchesBroadwayProcessor, []},
|
||||
{LolAnalytics.MatchProcessor.MatchesProducer, []}
|
||||
# Start a worker by calling: LoLAnalytics.Worker.start_link(arg)
|
||||
# {LoLAnalytics.Worker, arg}
|
||||
]
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
defmodule LolAnalytics.ChampionWinRate.ChampionWinRateRepo do
|
||||
import Ecto.Query
|
||||
alias LolAnalytics.ChampionWinRate.ChampionWinRateSchema
|
||||
alias LoLAnalytics.Repo
|
||||
|
||||
@spec add_champion_win_rate(
|
||||
champion_id :: String.t(),
|
||||
patch :: String.t(),
|
||||
position :: String.t(),
|
||||
win? :: boolean
|
||||
) :: {:ok, ChampionWinRateSchema.t()} | {:error, Ecto.Changeset.t()}
|
||||
def add_champion_win_rate(champion_id, patch, position, win?) do
|
||||
Repo.transaction(fn ->
|
||||
champion_query =
|
||||
from cwr in LolAnalytics.ChampionWinRate.ChampionWinRateSchema,
|
||||
where: cwr.champion_id == ^champion_id and cwr.position == ^position,
|
||||
lock: "FOR UPDATE"
|
||||
|
||||
champion_data = Repo.one(champion_query)
|
||||
|
||||
case champion_data do
|
||||
nil ->
|
||||
ChampionWinRateSchema.changeset(%ChampionWinRateSchema{}, %{
|
||||
champion_id: champion_id,
|
||||
patch: patch,
|
||||
total_games: 1,
|
||||
position: position,
|
||||
total_wins: if(win?, do: 1, else: 0)
|
||||
})
|
||||
|> Repo.insert!()
|
||||
|
||||
_ ->
|
||||
total_games = champion_data.total_games + 1
|
||||
total_wins = champion_data.total_wins + if win?, do: 1, else: 0
|
||||
|
||||
ChampionWinRateSchema.changeset(champion_data, %{
|
||||
total_games: total_games,
|
||||
total_wins: total_wins
|
||||
})
|
||||
|> Repo.update!()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
def list_win_rates() do
|
||||
Repo.all(ChampionWinRateSchema)
|
||||
end
|
||||
|
||||
def get_champion_win_rate(champion_id, _patch) do
|
||||
champion_query =
|
||||
from cwr in LolAnalytics.ChampionWinRate.ChampionWinRateSchema,
|
||||
where: cwr.champion_id == ^champion_id
|
||||
|
||||
Repo.one(champion_query)
|
||||
end
|
||||
end
|
||||
@@ -1,20 +0,0 @@
|
||||
defmodule LolAnalytics.ChampionWinRate.ChampionWinRateSchema do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
schema "champion_win_rate" do
|
||||
field :champion_id, :integer
|
||||
field :total_games, :integer
|
||||
field :patch, :string
|
||||
field :position, :string
|
||||
field :total_wins, :integer
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
def changeset(%__MODULE__{} = champion_win_rate, attrs) do
|
||||
champion_win_rate
|
||||
|> cast(attrs, [:champion_id, :total_games, :patch, :total_wins, :position])
|
||||
|> validate_required([:champion_id, :total_games, :patch, :total_wins, :position])
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,22 @@
|
||||
defmodule LolAnalytics.Dimensions.Item.ItemMetadata do
|
||||
alias LolAnalytics.Dimensions.Item.ItemRepo
|
||||
@items_data_url "https://ddragon.leagueoflegends.com/cdn/14.11.1/data/en_US/item.json"
|
||||
|
||||
def update_metadata() do
|
||||
get_items()
|
||||
|> Enum.each(&save_metadata/1)
|
||||
end
|
||||
|
||||
defp get_items() do
|
||||
with {:ok, resp} <- HTTPoison.get(@items_data_url),
|
||||
%{"data" => data} <- Poison.decode!(resp.body) do
|
||||
data
|
||||
else
|
||||
_ -> {:error, :get_items_error}
|
||||
end
|
||||
end
|
||||
|
||||
defp save_metadata({item_id, metadata}) do
|
||||
ItemRepo.update(item_id, %{metadata: metadata})
|
||||
end
|
||||
end
|
||||
@@ -2,13 +2,15 @@ defmodule LolAnalytics.Dimensions.Item.ItemRepo do
|
||||
alias LolAnalytics.Dimensions.Item.ItemSchema
|
||||
alias LoLAnalytics.Repo
|
||||
|
||||
def get_or_create(item_id) do
|
||||
item = Repo.get(ItemSchema, item_id: item_id)
|
||||
import Ecto.Query
|
||||
|
||||
case item do
|
||||
def get_or_create(item_id) do
|
||||
query = from i in ItemSchema, where: i.item_id == ^item_id
|
||||
|
||||
case Repo.one(query) do
|
||||
nil ->
|
||||
item_changeset = ItemSchema.changeset(%ItemSchema{}, %{item_id: item_id})
|
||||
Repo.insert(item_changeset)
|
||||
Repo.insert!(item_changeset)
|
||||
|
||||
item ->
|
||||
item
|
||||
@@ -18,4 +20,11 @@ defmodule LolAnalytics.Dimensions.Item.ItemRepo do
|
||||
def list_items() do
|
||||
Repo.all(ItemSchema)
|
||||
end
|
||||
|
||||
@spec update(item_id :: String.t(), attrs :: map()) :: any()
|
||||
def update(item_id, attrs) do
|
||||
get_or_create(item_id)
|
||||
|> ItemSchema.changeset(attrs)
|
||||
|> Repo.update()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,14 +2,17 @@ defmodule LolAnalytics.Dimensions.Item.ItemSchema do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
@args [:item_id, :metadata]
|
||||
|
||||
schema "dim_item" do
|
||||
field :item_id, :integer
|
||||
field :metadata, :map
|
||||
timestamps()
|
||||
end
|
||||
|
||||
def changeset(item = %__MODULE__{}, attrs \\ %{}) do
|
||||
item
|
||||
|> cast(attrs, [:item_id])
|
||||
|> cast(attrs, @args)
|
||||
|> validate_required([:item_id])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,30 +1,85 @@
|
||||
defmodule LolAnalytics.Dimensions.Match.MatchRepo do
|
||||
alias LolAnalytics.Dimensions.Patch.PatchRepo
|
||||
alias LolAnalytics.Dimensions.Match.MatchSchema
|
||||
alias LoLAnalytics.Repo
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
@spec get_or_create(String.t()) :: %MatchSchema{}
|
||||
def get_or_create(match_id) do
|
||||
@spec get_or_create(%{
|
||||
:match_id => String.t(),
|
||||
:queue_id => integer(),
|
||||
:patch_number => String.t()
|
||||
}) :: %MatchSchema{}
|
||||
def get_or_create(%{match_id: match_id, queue_id: queue_id, patch_number: patch_number}) do
|
||||
_patch = PatchRepo.get_or_create(patch_number)
|
||||
query = from m in MatchSchema, where: m.match_id == ^match_id
|
||||
match = Repo.one(query)
|
||||
|
||||
case match do
|
||||
nil ->
|
||||
match_changeset =
|
||||
MatchSchema.changeset(
|
||||
%MatchSchema{},
|
||||
%{match_id: match_id}
|
||||
)
|
||||
|
||||
Repo.insert(match_changeset)
|
||||
%MatchSchema{}
|
||||
|> MatchSchema.changeset(%{
|
||||
match_id: match_id,
|
||||
patch_number: patch_number,
|
||||
queue_id: queue_id,
|
||||
fact_champion_played_game_status: 0,
|
||||
fact_champion_picked_item_status: 0,
|
||||
fact_champion_picked_summoner_spell_status: 0
|
||||
})
|
||||
|> Repo.insert!()
|
||||
|
||||
match ->
|
||||
match
|
||||
end
|
||||
end
|
||||
|
||||
@spec get(String.t()) :: nil | %MatchSchema{}
|
||||
def get(match_id) do
|
||||
query = from m in MatchSchema, where: m.match_id == ^match_id
|
||||
|
||||
Repo.one(query)
|
||||
end
|
||||
|
||||
@type update_attrs :: %{
|
||||
optional(:fact_champion_played_game_status) => process_status(),
|
||||
optional(:fact_champion_picked_item_status) => process_status(),
|
||||
optional(:fact_champion_picked_summoner_spell_status) => process_status()
|
||||
}
|
||||
@spec update(%MatchSchema{}, update_attrs()) :: %MatchSchema{}
|
||||
def update(match, attrs) do
|
||||
mapped_attrs =
|
||||
attrs
|
||||
|> Enum.map(fn {k, v} -> {k, process_status_atom_to_db(v)} end)
|
||||
|> Map.new()
|
||||
|
||||
match
|
||||
|> MatchSchema.changeset(mapped_attrs)
|
||||
|> Repo.update!()
|
||||
end
|
||||
|
||||
def list_matches() do
|
||||
Repo.all(MatchSchema)
|
||||
end
|
||||
|
||||
def list_unprocessed_matches(limit, queue \\ 420) do
|
||||
query =
|
||||
from m in MatchSchema,
|
||||
where:
|
||||
(m.fact_champion_picked_item_status == 0 or
|
||||
m.fact_champion_picked_summoner_spell_status == 0 or
|
||||
m.fact_champion_played_game_status == 0) and
|
||||
m.queue_id == ^queue,
|
||||
order_by: [desc: m.updated_at],
|
||||
limit: ^limit
|
||||
|
||||
Repo.all(query)
|
||||
end
|
||||
|
||||
@type process_status :: :not_processed | :processed | :error
|
||||
defp process_status_atom_to_db(:not_processed), do: 0
|
||||
defp process_status_atom_to_db(:enqueued), do: 1
|
||||
defp process_status_atom_to_db(:processed), do: 2
|
||||
defp process_status_atom_to_db(:error), do: 3
|
||||
defp process_status_atom_to_db(:error_match_not_found), do: 4
|
||||
defp process_status_atom_to_db(_), do: raise("Invalid processing status")
|
||||
end
|
||||
|
||||
@@ -2,14 +2,28 @@ defmodule LolAnalytics.Dimensions.Match.MatchSchema do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
@casting_attrs [
|
||||
:match_id,
|
||||
:queue_id,
|
||||
:patch_number,
|
||||
:fact_champion_picked_item_status,
|
||||
:fact_champion_picked_summoner_spell_status,
|
||||
:fact_champion_played_game_status
|
||||
]
|
||||
|
||||
schema "dim_match" do
|
||||
field :match_id, :string
|
||||
field :patch_number, :string
|
||||
field :queue_id, :integer
|
||||
field :fact_champion_picked_item_status, :integer
|
||||
field :fact_champion_picked_summoner_spell_status, :integer
|
||||
field :fact_champion_played_game_status, :integer
|
||||
timestamps()
|
||||
end
|
||||
|
||||
def changeset(match = %__MODULE__{}, attrs \\ %{}) do
|
||||
match
|
||||
|> cast(attrs, [:match_id])
|
||||
|> cast(attrs, @casting_attrs)
|
||||
|> validate_required([:match_id])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -11,5 +11,6 @@ defmodule LolAnalytics.Dimensions.Patch.PatchSchema do
|
||||
patch
|
||||
|> cast(attrs, [:patch_number])
|
||||
|> validate_required([:patch_number])
|
||||
|> unique_constraint([:patch_number])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,15 +6,15 @@ defmodule LolAnalytics.Dimensions.Player.PlayerRepo do
|
||||
|
||||
def get_or_create(puuid) do
|
||||
query = from p in PlayerSchema, where: p.puuid == ^puuid
|
||||
player = Repo.one(query)
|
||||
|
||||
case player do
|
||||
case Repo.one(query) do
|
||||
nil ->
|
||||
player_changeset =
|
||||
PlayerSchema.changeset(
|
||||
%PlayerSchema{},
|
||||
%{puuid: puuid}
|
||||
)
|
||||
|
||||
Repo.insert(player_changeset)
|
||||
|
||||
player ->
|
||||
|
||||
@@ -2,14 +2,21 @@ defmodule LolAnalytics.Dimensions.Player.PlayerSchema do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
@attrs [:puuid, :last_processed_at]
|
||||
|
||||
schema "dim_player" do
|
||||
field :puuid, :string
|
||||
|
||||
field :last_processed_at, :utc_datetime,
|
||||
default: DateTime.utc_now() |> DateTime.truncate(:second)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
def changeset(player = %__MODULE__{}, attrs \\ %{}) do
|
||||
player
|
||||
|> cast(attrs, [:puuid])
|
||||
|> cast(attrs, @attrs)
|
||||
|> validate_required([:puuid])
|
||||
|> unique_constraint([:puuid])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
defmodule LolAnalytics.Facts.ChampionPickedItem.FactProcessor do
|
||||
require Logger
|
||||
|
||||
alias LolAnalytics.Dimensions.Match.MatchSchema
|
||||
alias LolAnalytics.Dimensions.Match.MatchRepo
|
||||
alias LolAnalytics.Facts.ChampionPickedItem.Repo
|
||||
|
||||
@doc """
|
||||
|
||||
iex> LolAnalytics.Facts.ChampionPickedItem.FactProcessor.process_game_at_url("http://192.168.1.55:9000/ranked/14.3.558.106/EUW1_6803789466.json")
|
||||
"""
|
||||
@impl true
|
||||
def process_game_at_url(url) do
|
||||
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
|
||||
HTTPoison.get(url),
|
||||
{:ok, decoded_match} <- Poison.decode(body, as: %LoLAPI.Model.MatchResponse{}) do
|
||||
process_game_data(decoded_match)
|
||||
else
|
||||
_ ->
|
||||
Logger.error("Could not process data from #{url} for ChampionPickedItem")
|
||||
{:error, "Could not process data from #{url}"}
|
||||
end
|
||||
end
|
||||
|
||||
@spec process_match(%MatchSchema{}) :: :ok | {:error, String.t()}
|
||||
def process_match(match) do
|
||||
match_url = "http://192.168.1.55:9000/ranked/#{match.patch_number}/#{match.match_id}.json"
|
||||
|
||||
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
|
||||
HTTPoison.get(match_url),
|
||||
{:ok, decoded_match} <- Poison.decode(body, as: %LoLAPI.Model.MatchResponse{}) do
|
||||
process_game_data(decoded_match)
|
||||
MatchRepo.update(match, %{fact_champion_picked_item_status: :processed})
|
||||
:ok
|
||||
else
|
||||
_ ->
|
||||
MatchRepo.update(match, fact_champion_picked_item_status: :error_match_not_found)
|
||||
Logger.error("Could not process data from #{match_url} for ChampionPickedItem")
|
||||
{:error, "Could not process data from #{match_url}"}
|
||||
end
|
||||
end
|
||||
|
||||
defp process_game_data(decoded_match) do
|
||||
participants = decoded_match.info.participants
|
||||
version = extract_game_version(decoded_match)
|
||||
|
||||
match =
|
||||
MatchRepo.get_or_create(%{
|
||||
match_id: decoded_match.metadata.matchId,
|
||||
patch_number: decoded_match.info.gameVersion,
|
||||
queue_id: decoded_match.info.queueId
|
||||
})
|
||||
|
||||
Logger.info("Processing ChampionPickedItem for match #{decoded_match.metadata.matchId}")
|
||||
|
||||
participants
|
||||
|> Enum.each(fn participant = %LoLAPI.Model.Participant{} ->
|
||||
if participant.teamPosition != "" do
|
||||
[:item0, :item1, :item2, :item3, :item4, :item5, :item6]
|
||||
|> Enum.with_index()
|
||||
|> Enum.each(fn {item_key, index} ->
|
||||
item_key = Map.get(participant, item_key)
|
||||
|
||||
Repo.insert(%{
|
||||
champion_id: participant.championId,
|
||||
match_id: decoded_match.metadata.matchId,
|
||||
is_win: participant.win,
|
||||
item_id: item_key,
|
||||
slot_number: index,
|
||||
game_length_seconds: decoded_match.info.gameDuration,
|
||||
queue_id: decoded_match.info.queueId,
|
||||
puuid: participant.puuid,
|
||||
team_position: participant.teamPosition,
|
||||
patch_number: version
|
||||
})
|
||||
end)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp extract_game_version(game_data) do
|
||||
game_data.info.gameVersion
|
||||
|> String.split(".")
|
||||
|> Enum.take(2)
|
||||
|> Enum.join(".")
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,89 @@
|
||||
defmodule LolAnalytics.Facts.ChampionPickedItem.Repo do
|
||||
import Ecto.Query
|
||||
|
||||
alias LolAnalytics.Dimensions.Patch.PatchRepo
|
||||
alias LolAnalytics.Dimensions.Item.ItemSchema
|
||||
alias LolAnalytics.Dimensions.Champion.ChampionSchema
|
||||
alias LolAnalytics.Facts.ChampionPickedItem.Schema
|
||||
alias LolAnalytics.Dimensions.Item.ItemRepo
|
||||
alias LolAnalytics.Dimensions.Player.PlayerRepo
|
||||
alias LolAnalytics.Dimensions.Champion.ChampionRepo
|
||||
alias LolAnalytics.Dimensions.Match.MatchRepo
|
||||
alias LoLAnalytics.Repo
|
||||
|
||||
@doc """
|
||||
Inserts a new entry for the campion_picked_item fact.
|
||||
|
||||
Example:
|
||||
|
||||
iex> item_picked = %{champion_id: 90, item_id: 1, match_id: "123", patch_number: "14.9", puuid: "1223", slot_number: 1, queue_id: 420, team_position: "JUNGLE", slot_number: 1, is_win: true}
|
||||
iex> LolAnalytics.Facts.ChampionPickedItem.Repo.insert(item_picked)
|
||||
"""
|
||||
@spec insert(%{
|
||||
:champion_id => integer(),
|
||||
:item_id => integer(),
|
||||
:match_id => binary(),
|
||||
:patch_number => String.t(),
|
||||
:puuid => String.t(),
|
||||
:is_win => boolean(),
|
||||
:queue_id => integer(),
|
||||
:team_position => String.t(),
|
||||
:slot_number => integer()
|
||||
}) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}
|
||||
def insert(attrs) do
|
||||
_champion = ChampionRepo.get_or_create(attrs.champion_id)
|
||||
_player = PlayerRepo.get_or_create(attrs.puuid)
|
||||
_patch = PatchRepo.get_or_create(attrs.patch_number)
|
||||
_item_id = ItemRepo.get_or_create(attrs.item_id)
|
||||
|
||||
prev =
|
||||
from(f in Schema,
|
||||
where:
|
||||
f.match_id == ^attrs.match_id and
|
||||
f.puuid == ^attrs.puuid and
|
||||
f.item_id == ^attrs.item_id and
|
||||
f.slot_number == ^attrs.slot_number
|
||||
)
|
||||
|> Repo.one()
|
||||
|
||||
Schema.changeset(prev || %Schema{}, attrs)
|
||||
|> Repo.insert_or_update()
|
||||
end
|
||||
|
||||
@spec get_champion_picked_items(String.t(), String.t(), String.t()) :: list()
|
||||
def get_champion_picked_items(champion_id, team_position, patch_number) do
|
||||
query =
|
||||
from f in Schema,
|
||||
where:
|
||||
f.champion_id == ^champion_id and
|
||||
f.team_position == ^team_position and
|
||||
f.item_id != 0 and f.patch_number == ^patch_number,
|
||||
join: c in ChampionSchema,
|
||||
on: c.champion_id == f.champion_id,
|
||||
join: i in ItemSchema,
|
||||
on: i.item_id == f.item_id,
|
||||
select: %{
|
||||
wins: fragment("count(CASE WHEN ? THEN 1 END)", f.is_win),
|
||||
win_rate:
|
||||
fragment(
|
||||
"
|
||||
((cast(count(CASE WHEN ? THEN 1 END) as float) / cast(count(*) as float)) * 100.0
|
||||
)",
|
||||
f.is_win
|
||||
),
|
||||
metadata: i.metadata,
|
||||
item_id: i.item_id,
|
||||
champion_id: c.champion_id,
|
||||
team_position: f.team_position,
|
||||
total_games: count("*")
|
||||
},
|
||||
group_by: [
|
||||
i.metadata,
|
||||
i.item_id,
|
||||
c.champion_id,
|
||||
f.team_position
|
||||
]
|
||||
|
||||
Repo.all(query)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,34 @@
|
||||
defmodule LolAnalytics.Facts.ChampionPickedItem.Schema do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
@casting_args [
|
||||
:champion_id,
|
||||
:item_id,
|
||||
:match_id,
|
||||
:is_win,
|
||||
:queue_id,
|
||||
:patch_number,
|
||||
:team_position,
|
||||
:puuid,
|
||||
:slot_number
|
||||
]
|
||||
|
||||
schema "fact_champion_picked_item" do
|
||||
field :champion_id, :integer
|
||||
field :item_id, :integer
|
||||
field :match_id, :string
|
||||
field :is_win, :boolean
|
||||
field :queue_id, :integer
|
||||
field :patch_number, :string
|
||||
field :team_position, :string
|
||||
field :puuid, :string
|
||||
field :slot_number, :integer
|
||||
end
|
||||
|
||||
def changeset(fact, attrs) do
|
||||
fact
|
||||
|> cast(attrs, @casting_args)
|
||||
|> validate_required(@casting_args)
|
||||
end
|
||||
end
|
||||
+26
-5
@@ -1,18 +1,39 @@
|
||||
defmodule LolAnalytics.Facts.ChampionPickedSummonerSpell.FactProcessor do
|
||||
require Logger
|
||||
|
||||
alias LolAnalytics.Dimensions.Match.MatchSchema
|
||||
alias LolAnalytics.Dimensions.Match.MatchRepo
|
||||
alias LolAnalytics.Facts.ChampionPickedSummonerSpell
|
||||
|
||||
def process_game_at_url(url) do
|
||||
data = HTTPoison.get!(url)
|
||||
process_game_data(data.body)
|
||||
@spec process_match(%MatchSchema{}) :: :ok | {:error, String.t()}
|
||||
def process_match(match) do
|
||||
match_url = "http://192.168.1.55:9000/ranked/#{match.patch_number}/#{match.match_id}.json"
|
||||
|
||||
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
|
||||
HTTPoison.get(match_url),
|
||||
{:ok, decoded_match} <- Poison.decode(body, as: %LoLAPI.Model.MatchResponse{}) do
|
||||
process_game_data(decoded_match)
|
||||
MatchRepo.update(match, %{fact_champion_picked_summoner_spell_status: :processed})
|
||||
:ok
|
||||
else
|
||||
_ ->
|
||||
MatchRepo.update(match, fact_champion_picked_summoner_spell_status: :error_match_not_found)
|
||||
Logger.error("Could not process data from #{match_url} for ChampionPickedItem")
|
||||
{:error, "Could not process data from #{match_url}"}
|
||||
end
|
||||
end
|
||||
|
||||
defp process_game_data(data) do
|
||||
decoded_match = Poison.decode!(data, as: %LoLAPI.Model.MatchResponse{})
|
||||
defp process_game_data(decoded_match) do
|
||||
participants = decoded_match.info.participants
|
||||
version = extract_game_version(decoded_match)
|
||||
|
||||
match =
|
||||
MatchRepo.get_or_create(%{
|
||||
match_id: decoded_match.metadata.matchId,
|
||||
patch_number: decoded_match.info.gameVersion,
|
||||
queue_id: decoded_match.info.queueId
|
||||
})
|
||||
|
||||
Logger.info("Processing ChampionPickedSummoner for match #{decoded_match.metadata.matchId}")
|
||||
|
||||
participants
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
defmodule LolAnalytics.Facts.ChampionPickedSummonerSpell.Repo do
|
||||
import Ecto.Query
|
||||
|
||||
alias LolAnalytics.Dimensions.Patch.PatchRepo
|
||||
alias LolAnalytics.Dimensions.SummonerSpell.SummonerSpellSchema
|
||||
alias LolAnalytics.Dimensions.SummonerSpell.SummonerSpellRepo
|
||||
alias LolAnalytics.Dimensions.Champion.ChampionSchema
|
||||
@@ -13,19 +14,18 @@ defmodule LolAnalytics.Facts.ChampionPickedSummonerSpell.Repo do
|
||||
|
||||
alias LoLAnalytics.Repo
|
||||
|
||||
@type insert_attrs :: %{
|
||||
match_id: String.t(),
|
||||
champion_id: String.t(),
|
||||
puuid: String.t(),
|
||||
summoner_spell_id: :integer
|
||||
}
|
||||
|
||||
@spec insert(insert_attrs()) :: any()
|
||||
@spec insert(%{
|
||||
:champion_id => String.t(),
|
||||
:match_id => String.t(),
|
||||
:patch_number => String.t(),
|
||||
:puuid => any(),
|
||||
:summoner_spell_id => String.t()
|
||||
}) :: any()
|
||||
def insert(attrs) do
|
||||
_match = MatchRepo.get_or_create(attrs.match_id)
|
||||
_champion = ChampionRepo.get_or_create(attrs.champion_id)
|
||||
_player = PlayerRepo.get_or_create(attrs.puuid)
|
||||
_spell = SummonerSpellRepo.get_or_create(attrs.summoner_spell_id)
|
||||
_patch = PatchRepo.get_or_create(attrs.patch_number)
|
||||
|
||||
prev =
|
||||
from(f in Schema,
|
||||
@@ -37,15 +37,18 @@ defmodule LolAnalytics.Facts.ChampionPickedSummonerSpell.Repo do
|
||||
)
|
||||
|> Repo.one()
|
||||
|
||||
changeset = Schema.changeset(prev || %Schema{}, attrs)
|
||||
|
||||
Repo.insert_or_update(changeset)
|
||||
Schema.changeset(prev || %Schema{}, attrs)
|
||||
|> Repo.insert_or_update()
|
||||
end
|
||||
|
||||
def get_champion_picked_summoners(championId, team_position \\ "MIDDLE") do
|
||||
@spec get_champion_picked_summoners(String.t(), String.t(), String.t()) :: list()
|
||||
def get_champion_picked_summoners(champion_id, team_position, patch_number) do
|
||||
query =
|
||||
from f in Schema,
|
||||
where: f.champion_id == ^championId and f.team_position == ^team_position,
|
||||
where:
|
||||
f.champion_id == ^champion_id and
|
||||
f.team_position == ^team_position and
|
||||
f.patch_number == ^patch_number,
|
||||
join: c in ChampionSchema,
|
||||
on: c.champion_id == f.champion_id,
|
||||
join: s in SummonerSpellSchema,
|
||||
@@ -59,10 +62,8 @@ defmodule LolAnalytics.Facts.ChampionPickedSummonerSpell.Repo do
|
||||
)",
|
||||
f.is_win
|
||||
),
|
||||
id: f.champion_id,
|
||||
spell_id: f.summoner_spell_id,
|
||||
metadata: s.metadata,
|
||||
champion_name: c.name,
|
||||
champion_id: c.champion_id,
|
||||
team_position: f.team_position,
|
||||
total_games: count("*")
|
||||
@@ -71,16 +72,10 @@ defmodule LolAnalytics.Facts.ChampionPickedSummonerSpell.Repo do
|
||||
f.champion_id,
|
||||
f.summoner_spell_id,
|
||||
s.metadata,
|
||||
c.image,
|
||||
c.name,
|
||||
c.champion_id,
|
||||
f.team_position
|
||||
]
|
||||
|
||||
Repo.all(query)
|
||||
end
|
||||
|
||||
def list_facts() do
|
||||
Repo.all(Schema)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,6 +7,7 @@ defmodule LolAnalytics.Facts.ChampionPickedSummonerSpell.Schema do
|
||||
:champion_id,
|
||||
:summoner_spell_id,
|
||||
:match_id,
|
||||
:patch_number,
|
||||
:is_win,
|
||||
:game_length_seconds,
|
||||
:queue_id,
|
||||
@@ -18,6 +19,7 @@ defmodule LolAnalytics.Facts.ChampionPickedSummonerSpell.Schema do
|
||||
field :champion_id, :integer
|
||||
field :summoner_spell_id, :integer
|
||||
field :match_id, :string
|
||||
field :patch_number, :string
|
||||
field :is_win, :boolean
|
||||
field :game_length_seconds, :integer
|
||||
field :queue_id, :integer
|
||||
|
||||
+76
-32
@@ -5,16 +5,13 @@ defmodule LolAnalytics.Facts.ChampionPlayedGame.Repo do
|
||||
alias LolAnalytics.Dimensions.Champion.ChampionSchema
|
||||
alias LolAnalytics.Dimensions.Player.PlayerRepo
|
||||
alias LolAnalytics.Dimensions.Champion.ChampionRepo
|
||||
alias LolAnalytics.Dimensions.Match.MatchRepo
|
||||
alias LolAnalytics.Facts.ChampionPlayedGame.Schema
|
||||
alias LoLAnalytics.Repo
|
||||
|
||||
def insert(attrs) do
|
||||
_match = MatchRepo.get_or_create(attrs.match_id)
|
||||
_champion = ChampionRepo.get_or_create(attrs.champion_id)
|
||||
_player = PlayerRepo.get_or_create(attrs.puuid)
|
||||
_patch = PatchRepo.get_or_create(attrs.patch_number)
|
||||
changeset = Schema.changeset(%Schema{}, attrs)
|
||||
|
||||
prev =
|
||||
from(f in Schema,
|
||||
@@ -26,40 +23,34 @@ defmodule LolAnalytics.Facts.ChampionPlayedGame.Repo do
|
||||
|
||||
changeset = Schema.changeset(prev || %Schema{}, attrs)
|
||||
|
||||
Repo.update(changeset)
|
||||
Repo.insert_or_update(changeset)
|
||||
end
|
||||
|
||||
def list_played_matches() do
|
||||
Repo.all(Schema)
|
||||
end
|
||||
|
||||
def get_win_rates do
|
||||
query =
|
||||
from m in Schema,
|
||||
join: c in ChampionSchema,
|
||||
on: c.champion_id == m.champion_id,
|
||||
select: %{
|
||||
wins: fragment("count(CASE WHEN ? THEN 1 END)", m.is_win),
|
||||
win_rate:
|
||||
fragment(
|
||||
"
|
||||
((cast(count(CASE WHEN ? THEN 1 END) as float) / cast(count(*) as float)) * 100.0
|
||||
)",
|
||||
m.is_win
|
||||
),
|
||||
id: m.champion_id,
|
||||
name: c.name,
|
||||
image: c.image,
|
||||
team_position: m.team_position,
|
||||
total_games: count("*")
|
||||
},
|
||||
group_by: [m.champion_id, c.image, c.name, m.team_position],
|
||||
having: count("*") > 100
|
||||
def get_win_rates(opts \\ []) do
|
||||
team_position = Keyword.get(opts, :team_position)
|
||||
patch = Keyword.get(opts, :patch_number, "14.12")
|
||||
|
||||
Repo.all(query)
|
||||
case {team_position, patch} do
|
||||
{nil, nil} ->
|
||||
get_win_rates()
|
||||
|
||||
{"all", patch} ->
|
||||
get_win_rates_for_patch(patch)
|
||||
|
||||
{nil, patch} ->
|
||||
get_win_rates_for_patch(patch)
|
||||
|
||||
{team_position, patch} ->
|
||||
get_win_rates_for_patch_and_team_position(patch, team_position)
|
||||
end
|
||||
end
|
||||
|
||||
def get_win_rates_by_patch(champion_id) do
|
||||
@spec champion_win_rates_by_patch(String.t(), String.t()) :: [map()]
|
||||
def champion_win_rates_by_patch(champion_id, team_position) do
|
||||
query =
|
||||
from m in Schema,
|
||||
join: c in ChampionSchema,
|
||||
@@ -80,13 +71,66 @@ defmodule LolAnalytics.Facts.ChampionPlayedGame.Repo do
|
||||
team_position: m.team_position,
|
||||
total_games: count("*")
|
||||
},
|
||||
where: c.champion_id == ^champion_id,
|
||||
group_by: [m.champion_id, c.image, c.name, m.team_position, m.patch_number],
|
||||
having: count("*") > 100
|
||||
group_by: [m.champion_id, c.image, c.name, m.patch_number, m.team_position],
|
||||
where: m.team_position == ^team_position and m.champion_id == ^champion_id,
|
||||
having: count("*") > 50
|
||||
|
||||
Repo.all(query)
|
||||
end
|
||||
|
||||
def get_win_rates_by_roles() do
|
||||
defp get_win_rates_for_patch(patch_number) do
|
||||
query =
|
||||
from m in Schema,
|
||||
join: c in ChampionSchema,
|
||||
on: c.champion_id == m.champion_id,
|
||||
select: %{
|
||||
wins: fragment("count(CASE WHEN ? THEN 1 END)", m.is_win),
|
||||
win_rate:
|
||||
fragment(
|
||||
"
|
||||
((cast(count(CASE WHEN ? THEN 1 END) as float) / cast(count(*) as float)) * 100.0
|
||||
)",
|
||||
m.is_win
|
||||
),
|
||||
id: m.champion_id,
|
||||
patch_number: m.patch_number,
|
||||
name: c.name,
|
||||
image: c.image,
|
||||
team_position: m.team_position,
|
||||
total_games: count("*")
|
||||
},
|
||||
group_by: [m.champion_id, c.image, c.name, m.team_position, m.patch_number],
|
||||
having: m.patch_number == ^patch_number and count("*") > 100
|
||||
|
||||
Repo.all(query)
|
||||
end
|
||||
|
||||
defp get_win_rates_for_patch_and_team_position(patch_number, team_position) do
|
||||
query =
|
||||
from m in Schema,
|
||||
join: c in ChampionSchema,
|
||||
on: c.champion_id == m.champion_id,
|
||||
select: %{
|
||||
wins: fragment("count(CASE WHEN ? THEN 1 END)", m.is_win),
|
||||
win_rate:
|
||||
fragment(
|
||||
"
|
||||
((cast(count(CASE WHEN ? THEN 1 END) as float) / cast(count(*) as float)) * 100.0
|
||||
)",
|
||||
m.is_win
|
||||
),
|
||||
id: m.champion_id,
|
||||
patch_number: m.patch_number,
|
||||
name: c.name,
|
||||
image: c.image,
|
||||
team_position: m.team_position,
|
||||
total_games: count("*")
|
||||
},
|
||||
group_by: [m.champion_id, c.image, c.name, m.team_position, m.patch_number],
|
||||
having:
|
||||
m.team_position == ^team_position and m.patch_number == ^patch_number and
|
||||
count("*") > 100
|
||||
|
||||
Repo.all(query)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,16 +1,38 @@
|
||||
defmodule LolAnalytics.Facts.ChampionPlayedGame.FactProcessor do
|
||||
require Logger
|
||||
|
||||
def process_game_at_url(path) do
|
||||
data = HTTPoison.get!(path)
|
||||
process_game_data(data.body)
|
||||
alias LolAnalytics.Dimensions.Match.MatchSchema
|
||||
alias LolAnalytics.Dimensions.Match.MatchRepo
|
||||
|
||||
@spec process_match(%MatchSchema{}) :: :ok | {:error, String.t()}
|
||||
def process_match(match) do
|
||||
match_url = "http://192.168.1.55:9000/ranked/#{match.patch_number}/#{match.match_id}.json"
|
||||
|
||||
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
|
||||
HTTPoison.get(match_url),
|
||||
{:ok, decoded_match} <- Poison.decode(body, as: %LoLAPI.Model.MatchResponse{}) do
|
||||
process_game_data(decoded_match)
|
||||
MatchRepo.update(match, %{fact_champion_played_game_status: :processed})
|
||||
:ok
|
||||
else
|
||||
_ ->
|
||||
MatchRepo.update(match, fact_champion_played_game_status: :error_match_not_found)
|
||||
Logger.error("Could not process data from #{match_url} for ChampionPickedItem")
|
||||
{:error, "Could not process data from #{match_url}"}
|
||||
end
|
||||
end
|
||||
|
||||
def process_game_data(data) do
|
||||
decoded_match = Poison.decode!(data, as: %LoLAPI.Model.MatchResponse{})
|
||||
def process_game_data(decoded_match) do
|
||||
participants = decoded_match.info.participants
|
||||
version = extract_game_version(decoded_match)
|
||||
|
||||
match =
|
||||
MatchRepo.get_or_create(%{
|
||||
match_id: decoded_match.metadata.matchId,
|
||||
patch_number: decoded_match.info.gameVersion,
|
||||
queue_id: decoded_match.info.queueId
|
||||
})
|
||||
|
||||
Logger.info("Processing ChampionPlayedMatch for #{decoded_match.metadata.matchId}")
|
||||
|
||||
participants
|
||||
|
||||
@@ -1,27 +1,22 @@
|
||||
defmodule LolAnalytics.Facts.FactsRunner do
|
||||
alias LolAnalytics.Facts
|
||||
|
||||
def analyze_all_matches do
|
||||
Storage.MatchStorage.S3MatchStorage.stream_files("ranked")
|
||||
|> peach(fn %{key: path} ->
|
||||
get_facts()
|
||||
|> Enum.each(fn fact_runner ->
|
||||
apply(fact_runner, ["http://192.168.1.55:9000/ranked/#{path}"])
|
||||
end)
|
||||
def analyze_match(match) do
|
||||
get_facts()
|
||||
|> Enum.each(fn fact_runner ->
|
||||
apply(fact_runner, [match])
|
||||
end)
|
||||
end
|
||||
|
||||
def analyze_match() do
|
||||
end
|
||||
|
||||
def get_facts() do
|
||||
[
|
||||
&Facts.ChampionPickedSummonerSpell.FactProcessor.process_game_at_url/1,
|
||||
&Facts.ChampionPlayedGame.FactProcessor.process_game_at_url/1
|
||||
&Facts.ChampionPickedSummonerSpell.FactProcessor.process_match/1,
|
||||
&Facts.ChampionPlayedGame.FactProcessor.process_match/1,
|
||||
&Facts.ChampionPickedItem.FactProcessor.process_match/1
|
||||
]
|
||||
end
|
||||
|
||||
def peach(enum, fun, concurrency \\ System.schedulers_online() * 2, timeout \\ :infinity) do
|
||||
def peach(enum, fun, concurrency \\ System.schedulers_online(), timeout \\ :infinity) do
|
||||
Task.async_stream(enum, &fun.(&1), max_concurrency: concurrency, timeout: timeout)
|
||||
|> Stream.each(fn {:ok, val} -> val end)
|
||||
|> Enum.to_list()
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
defmodule LolAnalytics.Match.MatchRepo do
|
||||
alias LolAnalytics.Match.MatchSchema
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
def list_matches do
|
||||
query = from m in MatchSchema, order_by: [desc: m.match_id]
|
||||
LoLAnalytics.Repo.all(query)
|
||||
end
|
||||
|
||||
def number_of_matches do
|
||||
query = from m in MatchSchema, select: count(m.match_id)
|
||||
LoLAnalytics.Repo.one(query)
|
||||
end
|
||||
|
||||
@spec get_match(String.t()) :: %LolAnalytics.Match.MatchSchema{} | nil
|
||||
def get_match(match_id) do
|
||||
query = from m in MatchSchema, where: m.match_id == ^match_id
|
||||
|
||||
LoLAnalytics.Repo.one(query)
|
||||
end
|
||||
|
||||
@spec insert_match(String.t()) :: %LolAnalytics.Match.MatchSchema{}
|
||||
def insert_match(match_id) do
|
||||
MatchSchema.changeset(%MatchSchema{}, %{:match_id => match_id, :processed => false})
|
||||
|> LoLAnalytics.Repo.insert()
|
||||
end
|
||||
|
||||
@spec update_match(%LolAnalytics.Match.MatchSchema{}, term()) ::
|
||||
%LolAnalytics.Match.MatchSchema{}
|
||||
def update_match(match, attrs) do
|
||||
match = MatchSchema.changeset(match, attrs)
|
||||
LoLAnalytics.Repo.update(match)
|
||||
end
|
||||
end
|
||||
@@ -1,19 +0,0 @@
|
||||
defmodule LolAnalytics.Match.MatchSchema do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
schema "match" do
|
||||
field :match_id, :string
|
||||
field :processed, :boolean, default: false
|
||||
field :match_url, :string
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
def changeset(%__MODULE__{} = match, params \\ %{}) do
|
||||
match
|
||||
|> cast(params, [:match_id, :processed, :match_url])
|
||||
|> validate_required([:match_id, :processed])
|
||||
end
|
||||
|
||||
end
|
||||
@@ -0,0 +1,34 @@
|
||||
defmodule LolAnalytics.MatchProcessor.MatchesBroadwayProcessor do
|
||||
alias LolAnalytics.Facts.FactsRunner
|
||||
use Broadway
|
||||
|
||||
def start_link(opts) do
|
||||
Broadway.start_link(__MODULE__,
|
||||
name: __MODULE__,
|
||||
processors: [default: []],
|
||||
producer: [
|
||||
module: {LolAnalytics.MatchProcessor.MatchesProducer, []},
|
||||
rate_limiting: [
|
||||
interval: 1000,
|
||||
allowed_messages: 40
|
||||
]
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
@impl Broadway
|
||||
def handle_message(_processor, message, _context) do
|
||||
message.data
|
||||
# build_match_url(message.data.queue_id, message.data.patch_number, message.data.match_id)
|
||||
|> FactsRunner.analyze_match()
|
||||
|
||||
message
|
||||
end
|
||||
|
||||
defp build_match_url(queue, patch_id, match_id) do
|
||||
"http://192.168.1.55:9000/#{queue_to_dir(queue)}/#{patch_id}/#{match_id}.json"
|
||||
end
|
||||
|
||||
defp queue_to_dir(420), do: "ranked"
|
||||
defp queue_to_dir(_), do: "ranked"
|
||||
end
|
||||
@@ -0,0 +1,33 @@
|
||||
defmodule LolAnalytics.MatchProcessor.MatchesProducer do
|
||||
use GenStage
|
||||
|
||||
@impl GenStage
|
||||
def init(opts) do
|
||||
{:producer, opts}
|
||||
end
|
||||
|
||||
def start_link(opts) do
|
||||
GenStage.start_link(__MODULE__, :ok)
|
||||
end
|
||||
|
||||
@impl GenStage
|
||||
def handle_demand(demand, state) do
|
||||
matches = query_unprocessed_matches(demand)
|
||||
|
||||
{:noreply, matches, state}
|
||||
end
|
||||
|
||||
defp query_unprocessed_matches(demand) when demand <= 0, do: []
|
||||
|
||||
defp query_unprocessed_matches(demand) do
|
||||
LolAnalytics.Dimensions.Match.MatchRepo.list_unprocessed_matches(demand)
|
||||
|> Enum.map(&broadway_transform/1)
|
||||
end
|
||||
|
||||
defp broadway_transform(match) do
|
||||
%Broadway.Message{
|
||||
data: match,
|
||||
acknowledger: Broadway.NoopAcknowledger.init()
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -44,7 +44,9 @@ defmodule LoLAnalytics.MixProject do
|
||||
{:lol_api, in_umbrella: true},
|
||||
{:storage, in_umbrella: true},
|
||||
{:httpoison, "~> 2.2"},
|
||||
{:poison, "~> 5.0"}
|
||||
{:poison, "~> 5.0"},
|
||||
{:gen_stage, "~> 1.2.1"},
|
||||
{:broadway, "~> 1.1"}
|
||||
]
|
||||
end
|
||||
|
||||
|
||||
@@ -16,13 +16,6 @@ defmodule LoLAnalytics.Repo.Migrations.AnalyticsTables do
|
||||
|
||||
create index("dim_item", [:item_id], unique: true)
|
||||
|
||||
create table("dim_match") do
|
||||
add :match_id, :string, primary_key: true, null: false
|
||||
timestamps()
|
||||
end
|
||||
|
||||
create index("dim_match", [:match_id], unique: true)
|
||||
|
||||
create table("dim_patch") do
|
||||
add :patch_number, :string, primary_key: true, null: false
|
||||
timestamps()
|
||||
@@ -30,6 +23,15 @@ defmodule LoLAnalytics.Repo.Migrations.AnalyticsTables do
|
||||
|
||||
create index("dim_patch", [:patch_number], unique: true)
|
||||
|
||||
create table("dim_match") do
|
||||
add :patch_number, references("dim_patch", column: :patch_number, type: :string)
|
||||
add :match_id, :string, primary_key: true, null: false
|
||||
timestamps()
|
||||
end
|
||||
|
||||
create index("dim_match", [:patch_number])
|
||||
create index("dim_match", [:match_id], unique: true)
|
||||
|
||||
create table("dim_player") do
|
||||
add :puuid, :string, primary_key: true, null: false
|
||||
timestamps()
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
defmodule LoLAnalytics.Repo.Migrations.ChampionPickedItem do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create table("fact_champion_picked_item") do
|
||||
add :champion_id, references("dim_champion", column: :champion_id, type: :integer)
|
||||
add :item_id, references("dim_item", column: :item_id, type: :integer)
|
||||
add :slot_number, :integer
|
||||
add :match_id, references("dim_match", column: :match_id, type: :string)
|
||||
add :is_win, :boolean
|
||||
add :game_length_seconds, :integer
|
||||
add :queue_id, :integer
|
||||
add :patch_number, references("dim_patch", column: :patch_number, type: :string)
|
||||
add :team_position, :string
|
||||
add :puuid, references("dim_player", column: :puuid, type: :string)
|
||||
end
|
||||
|
||||
create index("fact_champion_picked_item", [:champion_id])
|
||||
create index("fact_champion_picked_item", [:puuid, :match_id, :slot_number, :item_id], unique: true)
|
||||
create index("fact_champion_picked_item", [
|
||||
:item_id,
|
||||
:queue_id,
|
||||
:is_win,
|
||||
:patch_number,
|
||||
:team_position
|
||||
])
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,9 @@
|
||||
defmodule LoLAnalytics.Repo.Migrations.ItemMetadata do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
alter table("dim_item") do
|
||||
add :metadata, :map
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,11 @@
|
||||
defmodule LoLAnalytics.Repo.Migrations.ItemMetadataIndex do
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
execute("CREATE INDEX dim_item_metadata ON dim_item USING GIN(metadata)")
|
||||
end
|
||||
|
||||
def down do
|
||||
execute("DROP INDEX dim_item_metadata")
|
||||
end
|
||||
end
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
defmodule LoLAnalytics.Repo.Migrations.DimMatchFactProcessingStatus do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
alter table("dim_match") do
|
||||
add :fact_champion_played_game_status, :integer, default: 0
|
||||
add :fact_champion_picked_item_status, :integer, default: 0
|
||||
add :fact_champion_picked_summoner_spell_status, :integer, default: 0
|
||||
end
|
||||
|
||||
create index("dim_match", [:fact_champion_played_game_status])
|
||||
create index("dim_match", [:fact_champion_picked_item_status])
|
||||
create index("dim_match", [:fact_champion_picked_summoner_spell_status])
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,9 @@
|
||||
defmodule LoLAnalytics.Repo.Migrations.DimMatchQueueId do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
alter table "dim_match" do
|
||||
add :queue_id, :integer
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,9 @@
|
||||
defmodule LoLAnalytics.Repo.Migrations.DimPlayerLastProcessed do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
alter table "dim_player" do
|
||||
add :last_processed_at, :utc_datetime
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -3,3 +3,16 @@
|
||||
@import "tailwindcss/utilities";
|
||||
|
||||
/* This file is for your main application CSS */
|
||||
|
||||
.tooltip {
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.has-tooltip:hover .tooltip {
|
||||
visibility: visible;
|
||||
z-index: 100;
|
||||
border: 1px solid #333;
|
||||
background-color: whitesmoke;
|
||||
border-radius: 12px;
|
||||
}
|
||||
@@ -18,18 +18,22 @@
|
||||
// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
|
||||
import "phoenix_html"
|
||||
// Establish Phoenix Socket and LiveView configuration.
|
||||
import {Socket} from "phoenix"
|
||||
import {LiveSocket} from "phoenix_live_view"
|
||||
import { Socket } from "phoenix"
|
||||
import { LiveSocket } from "phoenix_live_view"
|
||||
import topbar from "../vendor/topbar"
|
||||
import { ChampionWinRate } from "./hooks/champion_win_rate_patch"
|
||||
|
||||
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
|
||||
let liveSocket = new LiveSocket("/live", Socket, {
|
||||
longPollFallbackMs: 2500,
|
||||
params: {_csrf_token: csrfToken}
|
||||
params: { _csrf_token: csrfToken },
|
||||
hooks: {
|
||||
ChampionWinRate,
|
||||
}
|
||||
})
|
||||
|
||||
// Show progress bar on live navigation and form submits
|
||||
topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
|
||||
topbar.config({ barColors: { 0: "#29d" }, shadowColor: "rgba(0, 0, 0, .3)" })
|
||||
window.addEventListener("phx:page-loading-start", _info => topbar.show(300))
|
||||
window.addEventListener("phx:page-loading-stop", _info => topbar.hide())
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import Chart from "chart.js/auto"
|
||||
|
||||
const ChampionWinRate = {
|
||||
mounted() {
|
||||
this.handleEvent("win-rate", ({ winRates }) => {
|
||||
this.sortedWinRates = winRates.sort((a, b) => {
|
||||
let [p1Major, p1Minor] = a.patch_number.split(".").map(Number);
|
||||
let [p2Major, p2Minor] = b.patch_number.split(".").map(Number);
|
||||
|
||||
|
||||
if (p1Major > p2Major || (p1Major == p2Major && p1Minor > p2Minor)) return 1;
|
||||
return -1
|
||||
})
|
||||
this.patches = this.sortedWinRates.map((winRate) => {
|
||||
return winRate.patch_number
|
||||
})
|
||||
this.winRateValues = this.sortedWinRates.map((winRate) => winRate.win_rate)
|
||||
// TODO: it breaks on liveview updates, should apply a better fix...
|
||||
setInterval(() => {
|
||||
const data = {
|
||||
labels: this.patches,
|
||||
datasets: [{
|
||||
data: this.winRateValues,
|
||||
fill: false,
|
||||
borderColor: 'rgb(75, 192, 192)',
|
||||
tension: 0.1
|
||||
}]
|
||||
};
|
||||
this.chart = new Chart(document.getElementById("win-rate"), {
|
||||
type: 'line',
|
||||
data: data,
|
||||
options: {
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
this.chart.canvas.parentNode.style.height = '250px';
|
||||
this.chart.canvas.parentNode.style.width = '400px';
|
||||
this.chart.labels.display = false;
|
||||
this.chart.options.legend.display = false
|
||||
this.chart.options.legend.display = false
|
||||
}, 1000)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export { ChampionWinRate };
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "assets",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"chart.js": "^4.4.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@kurkle/color": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz",
|
||||
"integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw=="
|
||||
},
|
||||
"node_modules/chart.js": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.3.tgz",
|
||||
"integrity": "sha512-qK1gkGSRYcJzqrrzdR6a+I0vQ4/R+SoODXyAjscQ/4mzuNzySaMCd+hyVxitSY1+L2fjPD1Gbn+ibNqRmwQeLw==",
|
||||
"dependencies": {
|
||||
"@kurkle/color": "^0.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"pnpm": ">=8"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"chart.js": "^4.4.3"
|
||||
}
|
||||
}
|
||||
+9
-2
@@ -4,12 +4,19 @@ defmodule LolAnalyticsWeb.ChampionComponents.ChampionAvatar do
|
||||
attr :id, :integer, required: true
|
||||
attr :image, :string, required: true
|
||||
attr :name, :string, required: true
|
||||
attr :width, :integer, default: 100
|
||||
attr :height, :integer, default: 100
|
||||
|
||||
def champion_avatar(assigns) do
|
||||
~H"""
|
||||
<style>
|
||||
.champion-avatar {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
</style>
|
||||
<div class="flex flex-col w-40">
|
||||
<img src={@image} alt="champion-icon" />
|
||||
<p class="w-full text-center"><%= @name %></p>
|
||||
<img src={@image} class="champion-avatar rounded-xl" alt="champion-icon" />
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
defmodule LolAnalyticsWeb.ChampionComponents.ChampionCard.Props do
|
||||
defstruct [:id, :win_rate, :image, :name, :team_position, :wins, :total_games]
|
||||
defstruct [:id, :win_rate, :image, :name, :team_position, :patch_number, :wins, :total_games]
|
||||
end
|
||||
|
||||
defmodule LolAnalyticsWeb.ChampionComponents.ChampionCard do
|
||||
@@ -11,7 +11,7 @@ defmodule LolAnalyticsWeb.ChampionComponents.ChampionCard do
|
||||
|
||||
def champion_card(assigns) do
|
||||
~H"""
|
||||
<.link patch={"/champions/#{@props.id}"}>
|
||||
<.link patch={"/champions/#{@props.id}?team-position=#{@props.team_position}&patch=#{@props.patch_number}"}>
|
||||
<div class="flex flex-col rounded-xl bg-clip-border overflow-hidden bg-gray-200">
|
||||
<div class="flex flex-col flex-col-reverse">
|
||||
<div class="flex w-auto px-4 py-1 opacity-80 gap-2 absolute z-10 align-bottom bg-black">
|
||||
@@ -23,7 +23,7 @@ defmodule LolAnalyticsWeb.ChampionComponents.ChampionCard do
|
||||
<div class="py-1" />
|
||||
<div class="pl-2">
|
||||
<h3>Win rate: <%= @props.win_rate %></h3>
|
||||
<h3>Wins: <%= @props.wins %> / <%= @props.total_games %></h3>
|
||||
<p class="text-xs">Wins: <%= @props.wins %> / <%= @props.total_games %></p>
|
||||
</div>
|
||||
<div class="py-1" />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
defmodule LolAnalyticsWeb.ChampionComponents.Items do
|
||||
use Phoenix.Component
|
||||
|
||||
def items(assigns) do
|
||||
image = ""
|
||||
|
||||
~H"""
|
||||
<div class="flex flex-wrap flex-wrap gap-4">
|
||||
<%= for item <- assigns.items do %>
|
||||
<div class="has-tooltip">
|
||||
<div clas="flex flex-col gap-1 p-4">
|
||||
<img class="rounded-xl" src={item.image} />
|
||||
<%!-- <p><%= item.name %></p> --%>
|
||||
<p><%= item.win_rate %>%</p>
|
||||
<p class="text-xs"><%= item.wins %>/<%= item.total_games %></p>
|
||||
</div>
|
||||
<span class="tooltip -mt-8 py-2 px-4 rounded-xl"><%= item.name %></span>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
defmodule LolAnalyticsWeb.ChampionComponents.SummonerSpells.Props.SummonerSpell do
|
||||
defstruct [:id, :name, :win_rate, :wins, :total_games, :image]
|
||||
|
||||
@type t :: %{
|
||||
id: integer(),
|
||||
win_rate: float(),
|
||||
wins: integer(),
|
||||
total_games: integer(),
|
||||
image: String.t(),
|
||||
name: String.t()
|
||||
}
|
||||
end
|
||||
|
||||
defmodule LolAnalyticsWeb.ChampionComponents.SummonerSpells.Props do
|
||||
alias LolAnalyticsWeb.ChampionComponents.SummonerSpells.Props.SummonerSpell
|
||||
|
||||
defstruct spell: %SummonerSpell{}
|
||||
|
||||
@type t :: %{spell: SummonerSpell.t()}
|
||||
end
|
||||
|
||||
defmodule LolAnalyticsWeb.ChampionComponents.SummonerSpell do
|
||||
alias LolAnalyticsWeb.ChampionComponents.SummonerSpells.Props
|
||||
use Phoenix.Component
|
||||
|
||||
attr :spells, Props, default: %Props{}
|
||||
|
||||
def summoner_spells(assigns) do
|
||||
~H"""
|
||||
<div class="flex flex-wrap flex-wrap gap-4">
|
||||
<%= for spell <- assigns.spells.summoner_spells do %>
|
||||
<div clas="flex flex-col gap-1">
|
||||
<img src={spell.spell.image} />
|
||||
<p><%= spell.spell.name %></p>
|
||||
<p><%= spell.spell.win_rate %>%</p>
|
||||
<p><%= spell.spell.wins %>/<%= spell.spell.total_games %></p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
defmodule LolAnalyticsWeb.ChampionComponents.SummonerSpells do
|
||||
alias LolAnalyticsWeb.ChampionComponents.SummonerSpells.Props
|
||||
use Phoenix.Component
|
||||
|
||||
def summoner_spells(assigns) do
|
||||
~H"""
|
||||
<div class="flex flex-wrap flex-wrap gap-4">
|
||||
<%= for spell <- assigns.spells do %>
|
||||
<div class="has-tooltip">
|
||||
<div clas="flex flex-col gap-1">
|
||||
<img class="rounded-xl" src={spell.image} />
|
||||
<p><%= spell.win_rate %>%</p>
|
||||
<p class="text-xs"><%= spell.wins %>/<%= spell.total_games %></p>
|
||||
</div>
|
||||
<div class="tooltip -my-8 px-4 py-2 rounded-xl">
|
||||
<p><%= spell.name %></p>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
||||
@@ -302,9 +302,6 @@ defmodule LoLAnalyticsWeb.CoreComponents do
|
||||
end
|
||||
|
||||
def input(%{type: "checkbox"} = assigns) do
|
||||
IO.puts(">>>>")
|
||||
IO.inspect(assigns)
|
||||
|
||||
assigns =
|
||||
assign_new(assigns, :checked, fn ->
|
||||
Phoenix.HTML.Form.normalize_value("checkbox", assigns[:value])
|
||||
|
||||
@@ -4,9 +4,6 @@
|
||||
<a href="/">
|
||||
LoL Analytics
|
||||
</a>
|
||||
<p class="bg-brand/5 text-brand rounded-full px-2 font-medium leading-6">
|
||||
v<%= Application.spec(:phoenix, :vsn) %>
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-4 font-semibold leading-6 text-zinc-900">
|
||||
<.link patch="/champions">
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
defmodule LolAnalyticsWeb.PatchSelector do
|
||||
use LoLAnalyticsWeb, :live_component
|
||||
|
||||
def mount(socket) do
|
||||
patches =
|
||||
LolAnalytics.Dimensions.Patch.PatchRepo.list_patches()
|
||||
|> Enum.map(fn patch ->
|
||||
%{patch_number: String.split(patch.patch_number, ".") |> Enum.take(2) |> Enum.join(".")}
|
||||
end)
|
||||
|> MapSet.new()
|
||||
|> Enum.to_list()
|
||||
|> Enum.sort(fn %{patch_number: p1}, %{patch_number: p2} ->
|
||||
[major_1, minor_1] = String.split(p1, ".") |> Enum.map(&String.to_integer/1)
|
||||
[major_2, minor_2] = String.split(p2, ".") |> Enum.map(&String.to_integer/1)
|
||||
|
||||
major_1 > major_2 || (major_1 == major_2 && minor_1 > minor_2)
|
||||
end)
|
||||
|
||||
patch_numbers = Enum.map(patches, & &1.patch_number)
|
||||
[last_patch | _] = patch_numbers
|
||||
|
||||
send(self(), %{patch: last_patch})
|
||||
|
||||
socket =
|
||||
assign(socket, :patch_numbers, patch_numbers)
|
||||
|> assign(:patch_form, to_form(%{"selected_patch" => last_patch}))
|
||||
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("selected_patch", %{"patch" => patch} = unsigned_params, socket) do
|
||||
send(self(), %{patch: patch})
|
||||
|
||||
{:noreply, assign(socket, :selected_patch, patch)}
|
||||
end
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div phx-feedback-for={@id}>
|
||||
<style>
|
||||
.patch-selector {
|
||||
width: 100px;
|
||||
}
|
||||
</style>
|
||||
<.form for={@patch_form} phx-change="validate" phx-target={@myself} phx-submit="save">
|
||||
<div class="flex gap-4">
|
||||
<p class="my-auto">Patch</p>
|
||||
<select
|
||||
phx-change="selected_patch"
|
||||
id="patch"
|
||||
name="patch"
|
||||
class="patch-selector cursor-pointer block w-full rounded-md border border-gray-300 bg-white shadow-sm focus:border-zinc-400 focus:ring-0 sm:text-sm"
|
||||
>
|
||||
<%= for patch <- @patch_numbers do %>
|
||||
<option key={patch} phx-click="select-patch" name={patch} value={patch}>
|
||||
<%= patch %>
|
||||
</option>
|
||||
<% end %>
|
||||
</select>
|
||||
</div>
|
||||
</.form>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
||||
-1
@@ -22,7 +22,6 @@ defmodule LolAnalyticsWeb.ChampionLive.Components.ChampionFilters do
|
||||
end
|
||||
|
||||
attr :selectedrole, :string, required: true
|
||||
attr :roles, :list, default: []
|
||||
|
||||
def render(assigns) do
|
||||
selected_class =
|
||||
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
defmodule LolAnalyticsWeb.ChampionLive.Components.ChampionsList do
|
||||
use Phoenix.Component
|
||||
|
||||
attr :champion, :map
|
||||
|
||||
defp render_champion(assigns) do
|
||||
detail_url =
|
||||
"/champions/#{assigns.champion.id}?team-position=#{assigns.champion.team_position}&patch=#{assigns.champion.patch_number}"
|
||||
|
||||
~H"""
|
||||
<tr>
|
||||
<td>
|
||||
<.link patch={detail_url}>
|
||||
<div class="flex cursor-pointer items-center gap-2">
|
||||
<img
|
||||
class="champion_image rounded-md"
|
||||
src={"https://ddragon.leagueoflegends.com/cdn/14.11.1/img/champion/#{assigns.champion.image}"}
|
||||
/>
|
||||
<%= @champion.name %>
|
||||
</div>
|
||||
</.link>
|
||||
</td>
|
||||
<td><%= @champion.win_rate %>%</td>
|
||||
<td><%= @champion.total_games %></td>
|
||||
<td>
|
||||
<img src={team_position_image(@champion.team_position)} class="w-5 h-5" />
|
||||
</td>
|
||||
</tr>
|
||||
"""
|
||||
end
|
||||
|
||||
attr :champions, :list
|
||||
attr :patch_number, :integer
|
||||
attr :position, :string
|
||||
|
||||
def champions_list(assigns) do
|
||||
~H"""
|
||||
<style>
|
||||
.champion_image {
|
||||
width: 50px;
|
||||
}
|
||||
</style>
|
||||
<table class="table w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="py-3 text-left text-gray-500 uppercase tracking-wider">
|
||||
Champion
|
||||
</th>
|
||||
<th scope="col" class="py-3 text-left text-gray-500 uppercase tracking-wider">
|
||||
Win rate
|
||||
</th>
|
||||
<th scope="col" class="py-3 text-left text-gray-500 uppercase tracking-wider">
|
||||
Total games
|
||||
</th>
|
||||
<th scope="col" class="py-3 text-left text-gray-500 uppercase tracking-wider">
|
||||
Position
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<%= for champion <- assigns.champions do %>
|
||||
<div class="cursor-pointer">
|
||||
<.link patch={"/champions/#{champion.id}?team-position=#{champion.team_position}&patch=#{champion.patch_number}"}>
|
||||
<.render_champion champion={champion} ) />
|
||||
</.link>
|
||||
</div>
|
||||
<% end %>
|
||||
<!-- table rows and cells go here -->
|
||||
</tbody>
|
||||
</table>
|
||||
"""
|
||||
end
|
||||
|
||||
defp team_position_image("BOTTOM"), do: "/images/lanes/bot.png"
|
||||
defp team_position_image("MIDDLE"), do: "/images/lanes/mid.png"
|
||||
defp team_position_image("TOP"), do: "/images/lanes/top.png"
|
||||
defp team_position_image("JUNGLE"), do: "/images/lanes/jungle.png"
|
||||
defp team_position_image("UTILITY"), do: "/images/lanes/utility.png"
|
||||
end
|
||||
@@ -2,55 +2,109 @@ defmodule LoLAnalyticsWeb.ChampionLive.Index do
|
||||
use LoLAnalyticsWeb, :live_view
|
||||
|
||||
import LolAnalyticsWeb.ChampionComponents.ChampionCard
|
||||
import LolAnalyticsWeb.ChampionLive.Components.ChampionsList
|
||||
import LolAnalyticsWeb.Loader
|
||||
import Phoenix.VerifiedRoutes
|
||||
|
||||
alias LolAnalyticsWeb.ChampionLive.Mapper
|
||||
alias LolAnalyticsWeb.ChampionLive.Components.ChampionFilters
|
||||
alias LolAnalyticsWeb.PatchSelector
|
||||
|
||||
@behaviour LolAnalyticsWeb.ChampionFilters.EventHandler
|
||||
|
||||
@impl true
|
||||
def mount(params, _session, socket) do
|
||||
role =
|
||||
case params["role"] do
|
||||
nil -> "all"
|
||||
role -> role
|
||||
end
|
||||
role = params["role"] || "all"
|
||||
patch = params["patch"] || "14.12"
|
||||
display_mode = params["display_mode"] || "grid"
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> assign(:selected_role, role)
|
||||
|> assign(:selected_patch, patch)
|
||||
|> assign(:champions, %{status: :loading})
|
||||
|> load_champs(role)
|
||||
|> assign(:display_mode, display_mode)
|
||||
|> load_champs(role, patch)
|
||||
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
def handle_params(params, _uri, socket) do
|
||||
case params["role"] do
|
||||
role -> {:noreply, assign(socket, selected_role: role)}
|
||||
_ -> {:noreply, socket}
|
||||
end
|
||||
role = params["role"] || "all"
|
||||
patch = params["patch"] || "14.10"
|
||||
display_mode = params["display_mode"] || "grid"
|
||||
|
||||
{
|
||||
:noreply,
|
||||
assign(socket, selected_role: role)
|
||||
|> assign(selected_patch: patch)
|
||||
|> assign(display_mode: display_mode)
|
||||
}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("filter", %{"role" => selected_role} = params, socket) do
|
||||
query =
|
||||
get_query_params(socket)
|
||||
|> Map.merge(%{role: selected_role})
|
||||
|
||||
{:reply, %{},
|
||||
socket
|
||||
|> push_navigate(to: ~p"/champions?#{params}")
|
||||
|> push_patch(to: ~p"/champions?#{query}")
|
||||
|> assign(:champions, %{status: :loading})
|
||||
|> load_champs(selected_role)
|
||||
|> load_champs(selected_role, socket.assigns.selected_patch)
|
||||
|> assign(:selected_role, selected_role)}
|
||||
end
|
||||
|
||||
defp load_champs(socket, selected_role) do
|
||||
def handle_event("set-display-mode", %{"mode" => mode} = params, socket) do
|
||||
query_params =
|
||||
get_query_params(socket)
|
||||
|> Map.merge(%{display_mode: mode})
|
||||
|
||||
{:noreply,
|
||||
assign(socket, :display_mode, mode)
|
||||
|> push_patch(to: ~p"/champions?#{query_params}")}
|
||||
end
|
||||
|
||||
def handle_info(%{patch: patch}, socket) do
|
||||
selected_role = socket.assigns.selected_role
|
||||
|
||||
query_params =
|
||||
get_query_params(socket)
|
||||
|> Map.merge(%{patch: patch})
|
||||
|
||||
socket =
|
||||
assign(socket, :champions, %{status: :loading})
|
||||
|> assign(:selected_patch, patch)
|
||||
|> push_patch(to: ~p"/champions?#{query_params}")
|
||||
|> load_champs(selected_role, patch)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
defp load_champs(socket, selected_role, "all") do
|
||||
socket
|
||||
|> start_async(
|
||||
:get_champs,
|
||||
fn ->
|
||||
LolAnalytics.Facts.ChampionPlayedGame.Repo.get_win_rates()
|
||||
|> filter_champs(selected_role)
|
||||
LolAnalytics.Facts.ChampionPlayedGame.Repo.get_win_rates(team_position: selected_role)
|
||||
|> Mapper.map_champs()
|
||||
|> Enum.sort(&(&1.win_rate >= &2.win_rate))
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
defp load_champs(socket, selected_role, patch) do
|
||||
socket
|
||||
|> start_async(
|
||||
:get_champs,
|
||||
fn ->
|
||||
LolAnalytics.Facts.ChampionPlayedGame.Repo.get_win_rates(
|
||||
team_position: selected_role,
|
||||
patch_number: patch
|
||||
)
|
||||
|> Mapper.map_champs()
|
||||
|> Enum.sort(&(&1.win_rate >= &2.win_rate))
|
||||
end
|
||||
)
|
||||
end
|
||||
@@ -59,13 +113,54 @@ defmodule LoLAnalyticsWeb.ChampionLive.Index do
|
||||
{:noreply, assign(socket, :champions, %{status: :data, data: champs})}
|
||||
end
|
||||
|
||||
def render_champions(assigns) do
|
||||
def render_display_mode_selector_selector(assigns) do
|
||||
~H"""
|
||||
<div class="flex">
|
||||
<div phx-click="set-display-mode" phx-value-mode="grid" class="cursor-pointer">
|
||||
<.icon name={grid_icon(assigns.display_mode)} alt="table" />
|
||||
</div>
|
||||
<div phx-click="set-display-mode" phx-value-mode="list" class="cursor-pointer">
|
||||
<.icon name={list_icon(assigns.display_mode)} alt="table" />
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp grid_icon(selected_display_mode) do
|
||||
case selected_display_mode do
|
||||
"grid" -> "hero-squares-2x2-solid"
|
||||
"list" -> "hero-squares-2x2"
|
||||
end
|
||||
end
|
||||
|
||||
def list_icon(selected_display_mode) do
|
||||
case selected_display_mode do
|
||||
"grid" -> "hero-table-cells"
|
||||
"list" -> "hero-table-cells-solid"
|
||||
end
|
||||
end
|
||||
|
||||
def render_champions_list(assigns) do
|
||||
case assigns.champions do
|
||||
%{status: :loading} ->
|
||||
~H"""
|
||||
<.loader />
|
||||
"""
|
||||
|
||||
%{status: :data, data: champions} ->
|
||||
~H"""
|
||||
<.champions_list champions={champions} />
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
||||
def render_champions_grid(%{:champions => champions_state} = assigns) do
|
||||
case champions_state do
|
||||
%{status: :loading} ->
|
||||
~H"""
|
||||
<.loader />
|
||||
"""
|
||||
|
||||
%{status: :data, data: champions} ->
|
||||
~H"""
|
||||
<div id="champions" class="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
||||
@@ -77,14 +172,6 @@ defmodule LoLAnalyticsWeb.ChampionLive.Index do
|
||||
end
|
||||
end
|
||||
|
||||
defp filter_champs(champs, selected_role) do
|
||||
champs =
|
||||
LolAnalytics.Facts.ChampionPlayedGame.Repo.get_win_rates()
|
||||
|> Enum.filter((&filter_role/1).(selected_role))
|
||||
|> Mapper.map_champs()
|
||||
|> Enum.sort(&(&1.win_rate >= &2.win_rate))
|
||||
end
|
||||
|
||||
defp filter_role(role) do
|
||||
fn champ ->
|
||||
champ.team_position == role || role == "all"
|
||||
@@ -100,4 +187,12 @@ defmodule LoLAnalyticsWeb.ChampionLive.Index do
|
||||
socket
|
||||
|> assign(:page_title, "Listing Champions")
|
||||
end
|
||||
|
||||
defp get_query_params(socket) do
|
||||
%{
|
||||
patch: socket.assigns.selected_patch,
|
||||
role: socket.assigns.selected_role,
|
||||
display_mode: socket.assigns.display_mode
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
<.header>
|
||||
Champions
|
||||
<p class="text-3xl">
|
||||
Champions
|
||||
</p>
|
||||
|
||||
</.header>
|
||||
|
||||
<div class="h-4" />
|
||||
|
||||
<div class="px-2">
|
||||
<h1 class="text-l font-semibold">Filters</h1>
|
||||
|
||||
@@ -10,7 +15,18 @@
|
||||
<.live_component module={ChampionFilters} id="role-filters" selectedrole={@selected_role || "all" } />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<.live_component module={PatchSelector} id="patch-selector" initial_patch={@selected_patch} />
|
||||
|
||||
<.render_display_mode_selector_selector display_mode={@display_mode} />
|
||||
</div>
|
||||
|
||||
|
||||
<div class="h-4"></div>
|
||||
|
||||
<.render_champions champions={@champions} />
|
||||
<%= if @display_mode=="grid" do %>
|
||||
<.render_champions_grid champions={@champions} />
|
||||
<% else %>
|
||||
<.render_champions_list id="champions-list" champions={@champions} />
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -1,8 +1,10 @@
|
||||
defmodule LoLAnalyticsWeb.ChampionLive.Show do
|
||||
use LoLAnalyticsWeb, :live_view
|
||||
|
||||
import LolAnalyticsWeb.ChampionComponents.SummonerSpell
|
||||
import LolAnalyticsWeb.ChampionComponents.SummonerSpells
|
||||
import LolAnalyticsWeb.ChampionComponents.ChampionAvatar
|
||||
import LolAnalyticsWeb.ChampionComponents.Items
|
||||
import LolAnalyticsWeb.Loader
|
||||
|
||||
alias LolAnalyticsWeb.ChampionComponents.SummonerSpells.ShowMapper
|
||||
|
||||
@@ -12,28 +14,154 @@ defmodule LoLAnalyticsWeb.ChampionLive.Show do
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_params(%{"id" => id}, _, socket) do
|
||||
def handle_params(%{"id" => id, "team-position" => team_position, "patch" => patch}, _, socket) do
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:page_title, page_title(socket.assigns.live_action))
|
||||
|> assign(:champion, load_champion_info(id) |> ShowMapper.map_champion())
|
||||
|> assign(:summoner_spells, %{
|
||||
summoner_spells: load_summoner_spells(id) |> ShowMapper.map_spells()
|
||||
})}
|
||||
|> assign(:champion, load_champion_info(id))
|
||||
|> load_win_rates(id, team_position)
|
||||
|> load_summoner_spells(id, team_position, patch)
|
||||
|> load_items(id, team_position, patch)}
|
||||
end
|
||||
|
||||
defp load_summoner_spells(champion_id) do
|
||||
LolAnalytics.Facts.ChampionPickedSummonerSpell.Repo.get_champion_picked_summoners(champion_id)
|
||||
def load_win_rates(socket, champion_id, team_position) do
|
||||
socket
|
||||
|> start_async(:get_win_rates, fn ->
|
||||
LolAnalytics.Facts.ChampionPlayedGame.Repo.champion_win_rates_by_patch(
|
||||
champion_id,
|
||||
team_position
|
||||
)
|
||||
|> Enum.sort(fn p1, p2 ->
|
||||
[_, minor_1] = String.split(p1.patch_number, ".") |> Enum.map(&String.to_integer/1)
|
||||
[_, minor_2] = String.split(p2.patch_number, ".") |> Enum.map(&String.to_integer/1)
|
||||
|
||||
p1 < p2 && minor_1 < minor_2
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
defp load_summoner_spells(socket, champion_id, team_position, patch_number) do
|
||||
socket
|
||||
|> assign(:summoner_spells, %{status: :loading})
|
||||
|> start_async(:get_summoners, fn ->
|
||||
LolAnalytics.Facts.ChampionPickedSummonerSpell.Repo.get_champion_picked_summoners(
|
||||
champion_id,
|
||||
team_position,
|
||||
patch_number
|
||||
)
|
||||
|> ShowMapper.map_spells()
|
||||
end)
|
||||
end
|
||||
|
||||
defp load_items(socket, champion_id, team_position, patch) do
|
||||
socket
|
||||
|> assign(:items, %{status: :loading})
|
||||
|> assign(:boots, %{status: :loading})
|
||||
|> start_async(
|
||||
:get_items,
|
||||
fn ->
|
||||
items =
|
||||
LolAnalytics.Facts.ChampionPickedItem.Repo.get_champion_picked_items(
|
||||
champion_id,
|
||||
team_position,
|
||||
patch
|
||||
)
|
||||
|
||||
popular_items = items |> ShowMapper.map_items() |> Enum.take(30)
|
||||
|
||||
boots = items |> ShowMapper.extract_boots()
|
||||
|
||||
%{boots: boots, popular: popular_items}
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
@impl true
|
||||
@spec handle_async(:get_items | :get_summoners | :get_win_rates, {:ok, any()}, map()) ::
|
||||
{:noreply, map()}
|
||||
def handle_async(:get_win_rates, {:ok, result}, socket) do
|
||||
{:noreply, push_event(socket, "win-rate", %{winRates: result})}
|
||||
end
|
||||
|
||||
def handle_async(:get_items, {:ok, %{popular: popular, boots: boots}} = result, socket) do
|
||||
socket =
|
||||
socket
|
||||
|> assign(:items, %{
|
||||
status: :data,
|
||||
data: %{
|
||||
popular: popular,
|
||||
boots: boots
|
||||
}
|
||||
})
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_async(:get_summoners, {:ok, summoner_spells}, socket) do
|
||||
socket =
|
||||
assign(socket, :summoner_spells, %{
|
||||
status: :data,
|
||||
data: summoner_spells
|
||||
})
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
defp load_champion_info(champion_id) do
|
||||
champion = LolAnalytics.Dimensions.Champion.ChampionRepo.get_or_create(champion_id)
|
||||
|
||||
IO.inspect(champion)
|
||||
|
||||
champion
|
||||
LolAnalytics.Dimensions.Champion.ChampionRepo.get_or_create(champion_id)
|
||||
|> ShowMapper.map_champion()
|
||||
end
|
||||
|
||||
defp page_title(:show), do: "Show Champion"
|
||||
defp page_title(:edit), do: "Edit Champion"
|
||||
|
||||
def render_items(assigns) do
|
||||
case assigns.items do
|
||||
%{status: :loading} ->
|
||||
~H"""
|
||||
<.loader />
|
||||
"""
|
||||
|
||||
%{status: :data, data: data} ->
|
||||
~H"""
|
||||
<%= if Enum.count(data.boots) > 0 do %>
|
||||
<h2 class="text-2xl">Items</h2>
|
||||
|
||||
<h2 class="text-xl">Boots</h2>
|
||||
|
||||
<div class="my-2" />
|
||||
|
||||
<.items items={data.boots} />
|
||||
|
||||
<div class="my-4" />
|
||||
<% end %>
|
||||
|
||||
<h2 class="text-xl">Popular items</h2>
|
||||
|
||||
<div class="my-2" />
|
||||
|
||||
<.items items={data.popular} />
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
||||
def render_summoners(assigns) do
|
||||
case assigns.summoner_pells do
|
||||
%{status: :loading} ->
|
||||
~H"""
|
||||
<h2 class="text-2xl">Summoner spells</h2>
|
||||
<div class="my-2" />
|
||||
<.loader />
|
||||
"""
|
||||
|
||||
%{status: :data, data: data} ->
|
||||
~H"""
|
||||
<h2 class="text-2xl">Summoner spells</h2>
|
||||
|
||||
<div class="my-2" />
|
||||
|
||||
<.summoner_spells spells={data} />
|
||||
"""
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,11 +1,36 @@
|
||||
<style>
|
||||
.win-rate {
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
.win-rate-container {
|
||||
width: 100%;
|
||||
height: 250px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<.header>
|
||||
Champion <%= @champion.id %>
|
||||
<:actions></:actions>
|
||||
<h1 class="text-4xl">
|
||||
<%= @champion.name %>
|
||||
</h1>
|
||||
</.header>
|
||||
|
||||
<.champion_avatar id={@champion.id} name={@champion.name}
|
||||
image={"https://ddragon.leagueoflegends.com/cdn/14.11.1/img/champion/#{@champion.image}"} />
|
||||
<div class="my-6" />
|
||||
|
||||
<div class="flex flex-row gap-4">
|
||||
<.champion_avatar class="w-20" id={@champion.id} name={@champion.name}
|
||||
image={"https://ddragon.leagueoflegends.com/cdn/14.11.1/img/champion/#{@champion.image}"} />
|
||||
<div>
|
||||
<p class="text-lg">Win rate by patch</p>
|
||||
<div class="win-rate-container" style="position: relative; height:250px; width:400px">
|
||||
<canvas class="w-full win-rate" height="250" class="win-rate" id="win-rate" phx-hook="ChampionWinRate" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="my-4" />
|
||||
|
||||
<.render_summoners summoner_pells={@summoner_spells} />
|
||||
|
||||
<div class="my-4" />
|
||||
|
||||
<.summoner_spells spells={@summoner_spells} />
|
||||
<.render_items items={@items} />
|
||||
@@ -13,21 +13,48 @@ defmodule LolAnalyticsWeb.ChampionComponents.SummonerSpells.ShowMapper do
|
||||
def map_spells(items) do
|
||||
items
|
||||
|> Enum.map(&map_spell/1)
|
||||
|> Enum.sort(&(&1.spell.total_games > &2.spell.total_games))
|
||||
|> Enum.sort(&(&1.total_games > &2.total_games))
|
||||
end
|
||||
|
||||
def map_spell(spell) do
|
||||
image = spell.metadata["image"]["full"]
|
||||
|
||||
%LolAnalyticsWeb.ChampionComponents.SummonerSpells.Props{
|
||||
spell: %LolAnalyticsWeb.ChampionComponents.SummonerSpells.Props.SummonerSpell{
|
||||
id: spell.id,
|
||||
win_rate: :erlang.float_to_binary(spell.win_rate, decimals: 2),
|
||||
total_games: spell.total_games,
|
||||
image: "https://ddragon.leagueoflegends.com/cdn/14.11.1/img/spell/#{image}",
|
||||
name: spell.metadata["name"],
|
||||
wins: spell.wins
|
||||
}
|
||||
%{
|
||||
id: spell["id"],
|
||||
win_rate: :erlang.float_to_binary(spell.win_rate, decimals: 2),
|
||||
total_games: spell.total_games,
|
||||
image: "https://ddragon.leagueoflegends.com/cdn/14.11.1/img/spell/#{image}",
|
||||
name: spell.metadata["name"],
|
||||
wins: spell.wins
|
||||
}
|
||||
end
|
||||
|
||||
def map_items(items) do
|
||||
items
|
||||
|> Enum.map(&map_item/1)
|
||||
|> Enum.sort(&(&1.total_games > &2.total_games))
|
||||
end
|
||||
|
||||
def map_item(item) do
|
||||
image = item.metadata["image"]["full"]
|
||||
|
||||
%{
|
||||
id: item["id"],
|
||||
win_rate: :erlang.float_to_binary(item.win_rate, decimals: 2),
|
||||
total_games: item.total_games,
|
||||
image: "https://ddragon.leagueoflegends.com/cdn/14.11.1/img/item/#{image}",
|
||||
name: item.metadata["name"],
|
||||
wins: item.wins
|
||||
}
|
||||
end
|
||||
|
||||
def extract_boots(items) do
|
||||
items
|
||||
|> Enum.filter(fn item ->
|
||||
tags = item.metadata["tags"]
|
||||
tags |> Enum.any?(&String.equivalent?(&1, "Boots"))
|
||||
end)
|
||||
|> Enum.map(&map_item/1)
|
||||
|> Enum.sort(&(&1.total_games > &2.total_games))
|
||||
end
|
||||
end
|
||||
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 7.9 KiB |
@@ -1,7 +1,7 @@
|
||||
defmodule LoLAPI.MatchApi do
|
||||
require Logger
|
||||
@match_base_endpoint "https://europe.api.riotgames.com/lol/match/v5/matches/%{matchid}"
|
||||
@puuid_matches_base_endpoint "https://europe.api.riotgames.com/lol/match/v5/matches/by-puuid/%{puuid}/ids"
|
||||
@puuid_matches_base_endpoint "https://europe.api.riotgames.com/lol/match/v5/matches/by-puuid/%{puuid}/ids?queue=420"
|
||||
|
||||
@doc """
|
||||
Get match by id
|
||||
|
||||
@@ -10,8 +10,8 @@ defmodule Scrapper.Application do
|
||||
children = [
|
||||
Scrapper.Queue.MatchQueue,
|
||||
Scrapper.Queue.PlayerQueue,
|
||||
{Scrapper.Processor.MatchProcessor, []},
|
||||
{Scrapper.Processor.PlayerProcessor, []}
|
||||
{Scrapper.Consumer.MatchConsumer, []},
|
||||
{Scrapper.Consumer.PlayerConsumer, []}
|
||||
# Starts a worker by calling: Scrapper.Worker.start_link(arg)
|
||||
# {Scrapper.Worker, arg}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
defmodule Scrapper.Consumer.MatchConsumer do
|
||||
require Logger
|
||||
use Broadway
|
||||
|
||||
def start_link(_opts) do
|
||||
Broadway.start_link(
|
||||
__MODULE__,
|
||||
name: __MODULE__,
|
||||
producer: [
|
||||
module:
|
||||
{BroadwayRabbitMQ.Producer,
|
||||
queue: "match",
|
||||
connection: [
|
||||
username: "guest",
|
||||
password: "guest",
|
||||
host: "localhost"
|
||||
],
|
||||
on_failure: :reject_and_requeue,
|
||||
qos: [
|
||||
prefetch_count: 1
|
||||
]},
|
||||
concurrency: 1,
|
||||
rate_limiting: [
|
||||
interval: 300,
|
||||
allowed_messages: 1
|
||||
]
|
||||
],
|
||||
processors: [
|
||||
default: [
|
||||
concurrency: 1
|
||||
]
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_message(_, message = %Broadway.Message{}, _) do
|
||||
match_id = message.data
|
||||
|
||||
resp = LoLAPI.MatchApi.get_match_by_id(match_id)
|
||||
process_resp(resp, match_id)
|
||||
|
||||
message
|
||||
end
|
||||
|
||||
def process_resp({:ok, raw_match}, match_id) do
|
||||
Task.start_link(fn ->
|
||||
decoded_match = Poison.decode!(raw_match, as: %LoLAPI.Model.MatchResponse{})
|
||||
|
||||
match_url =
|
||||
case decoded_match.info.queueId do
|
||||
420 ->
|
||||
Logger.info("#{match_id} #{decoded_match.info.gameVersion}")
|
||||
|
||||
Storage.MatchStorage.S3MatchStorage.store_match(
|
||||
match_id,
|
||||
raw_match,
|
||||
"ranked",
|
||||
"#{decoded_match.info.gameVersion}"
|
||||
)
|
||||
|
||||
LolAnalytics.Dimensions.Match.MatchRepo.get_or_create(%{
|
||||
match_id: decoded_match.metadata.matchId,
|
||||
patch_number: decoded_match.info.gameVersion,
|
||||
queue_id: 420
|
||||
})
|
||||
|
||||
_queue_id ->
|
||||
Storage.MatchStorage.S3MatchStorage.store_match(match_id, raw_match, "matches")
|
||||
end
|
||||
|
||||
decoded_match.metadata.participants
|
||||
# |> Enum.shuffle()
|
||||
# |> Enum.take(2)
|
||||
|> Enum.each(fn participant_puuid ->
|
||||
Scrapper.Queue.PlayerQueue.enqueue_puuid(participant_puuid)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
def process_resp({:err, _code}, _match_id) do
|
||||
end
|
||||
end
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
defmodule Scrapper.Processor.PlayerProcessor do
|
||||
defmodule Scrapper.Consumer.PlayerConsumer do
|
||||
use Broadway
|
||||
|
||||
def start_link(_opts) do
|
||||
@@ -20,7 +20,7 @@ defmodule Scrapper.Processor.PlayerProcessor do
|
||||
]},
|
||||
concurrency: 1,
|
||||
rate_limiting: [
|
||||
interval: 1000 * 10,
|
||||
interval: 6700,
|
||||
allowed_messages: 1
|
||||
]
|
||||
],
|
||||
@@ -52,7 +52,7 @@ defmodule Scrapper.Processor.PlayerProcessor do
|
||||
{
|
||||
matches
|
||||
|> Enum.each(fn match_id ->
|
||||
Scrapper.Queue.MatchQueue.queue_match(match_id)
|
||||
Scrapper.Queue.MatchQueue.enqueue_match(match_id)
|
||||
end)
|
||||
}
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
defmodule Scrapper.Processor.MatchProcessor do
|
||||
use Broadway
|
||||
|
||||
def start_link(_opts) do
|
||||
Broadway.start_link(
|
||||
__MODULE__,
|
||||
name: __MODULE__,
|
||||
producer: [
|
||||
module:
|
||||
{BroadwayRabbitMQ.Producer,
|
||||
queue: "match",
|
||||
connection: [
|
||||
username: "guest",
|
||||
password: "guest",
|
||||
host: "localhost"
|
||||
],
|
||||
on_failure: :reject_and_requeue,
|
||||
qos: [
|
||||
prefetch_count: 1
|
||||
]},
|
||||
concurrency: 1,
|
||||
rate_limiting: [
|
||||
interval: 333 * 1,
|
||||
allowed_messages: 1
|
||||
]
|
||||
],
|
||||
processors: [
|
||||
default: [
|
||||
concurrency: 1
|
||||
]
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_message(_, message = %Broadway.Message{}, _) do
|
||||
match_id = message.data
|
||||
|
||||
resp = LoLAPI.MatchApi.get_match_by_id(match_id)
|
||||
process_resp(resp, match_id)
|
||||
|
||||
message
|
||||
end
|
||||
|
||||
def process_resp({:ok, raw_match}, match_id) do
|
||||
decoded_match = Poison.decode!(raw_match, as: %LoLAPI.Model.MatchResponse{})
|
||||
match_url = Storage.MatchStorage.S3MatchStorage.store_match(match_id, raw_match, "matches")
|
||||
match = LolAnalytics.Match.MatchRepo.get_match(match_id)
|
||||
|
||||
case match do
|
||||
nil ->
|
||||
LolAnalytics.Match.MatchRepo.insert_match(match_id)
|
||||
|
||||
_ ->
|
||||
LolAnalytics.Match.MatchRepo.update_match(match, %{
|
||||
:processed => true,
|
||||
:match_url => match_url
|
||||
})
|
||||
end
|
||||
|
||||
decoded_match.metadata.participants
|
||||
|> Enum.shuffle()
|
||||
|> Enum.take(2)
|
||||
|> Enum.each(fn participant_puuid ->
|
||||
Scrapper.Queue.PlayerQueue.queue_puuid(participant_puuid)
|
||||
end)
|
||||
end
|
||||
|
||||
def process_resp({:err, _code}, _match_id) do
|
||||
end
|
||||
end
|
||||
@@ -14,16 +14,16 @@ defmodule Scrapper.Queue.MatchQueue do
|
||||
{:ok, %{:channel => channel, :connection => connection}}
|
||||
end
|
||||
|
||||
@spec queue_match(String.t()) :: any()
|
||||
def queue_match(match_id) do
|
||||
LolAnalytics.Match.MatchRepo.get_match(match_id)
|
||||
@spec enqueue_match(String.t()) :: any()
|
||||
def enqueue_match(match_id) do
|
||||
LolAnalytics.Dimensions.Match.MatchRepo.get(match_id)
|
||||
|> case do
|
||||
nil -> GenServer.call(__MODULE__, {:queue_match, match_id})
|
||||
nil -> GenServer.call(__MODULE__, {:enqueue_match, match_id})
|
||||
_match -> :already_processed
|
||||
end
|
||||
end
|
||||
|
||||
def handle_call({:queue_match, match_id}, _from, %{:channel => channel} = state) do
|
||||
def handle_call({:enqueue_match, match_id}, _from, %{:channel => channel} = state) do
|
||||
AMQP.Basic.publish(channel, "", "match", match_id)
|
||||
{:reply, nil, state}
|
||||
end
|
||||
|
||||
@@ -12,31 +12,36 @@ defmodule Scrapper.Queue.PlayerQueue do
|
||||
def init(_opts) do
|
||||
{:ok, connection} = AMQP.Connection.open()
|
||||
{:ok, channel} = AMQP.Channel.open(connection)
|
||||
AMQP.Queue.declare(channel, "player", durable: true)
|
||||
|
||||
AMQP.Queue.declare(channel, "player",
|
||||
durable: true,
|
||||
arguments: [{"x-max-length", :long, 1000}]
|
||||
)
|
||||
|
||||
{:ok, {channel, connection}}
|
||||
end
|
||||
|
||||
@spec queue_puuid(String.t()) :: nil
|
||||
def queue_puuid(puuid) do
|
||||
@spec enqueue_puuid(String.t()) :: nil
|
||||
def enqueue_puuid(puuid) do
|
||||
case LolAnalytics.Player.PlayerRepo.get_player(puuid) do
|
||||
nil -> GenServer.call(__MODULE__, {:queue_player, puuid})
|
||||
player_entry -> queue_if_not_updated?(player_entry)
|
||||
nil -> GenServer.call(__MODULE__, {:enqueue_player, puuid})
|
||||
player_entry -> enqueue_if_not_updated?(player_entry)
|
||||
end
|
||||
end
|
||||
|
||||
@spec queue_if_not_updated?(%LolAnalytics.Player.PlayerSchema{}) :: nil
|
||||
defp queue_if_not_updated?(player = %LolAnalytics.Player.PlayerSchema{}) do
|
||||
@spec enqueue_if_not_updated?(%LolAnalytics.Player.PlayerSchema{}) :: nil
|
||||
defp enqueue_if_not_updated?(player = %LolAnalytics.Player.PlayerSchema{}) do
|
||||
now = DateTime.utc_now() |> DateTime.truncate(:second)
|
||||
|
||||
case player.last_processed_at do
|
||||
nil ->
|
||||
GenServer.call(__MODULE__, {:queue_player, player.puuid})
|
||||
GenServer.call(__MODULE__, {:enqueue_player, player.puuid})
|
||||
|
||||
processed_at ->
|
||||
diff_in_hours = DateTime.diff(now, processed_at, :hour)
|
||||
|
||||
if diff_in_hours > @hours_to_update_player do
|
||||
GenServer.call(__MODULE__, {:queue_player, player.puuid})
|
||||
GenServer.call(__MODULE__, {:enqueue_player, player.puuid})
|
||||
else
|
||||
Logger.info(
|
||||
"Player #{player.puuid} already processed at #{player.last_processed_at}, diff was #{diff_in_hours}"
|
||||
@@ -45,7 +50,7 @@ defmodule Scrapper.Queue.PlayerQueue do
|
||||
end
|
||||
end
|
||||
|
||||
def handle_call({:queue_player, puuid}, _from, {channel, _} = state) do
|
||||
def handle_call({:enqueue_player, puuid}, _from, {channel, _} = state) do
|
||||
LolAnalytics.Player.PlayerRepo.insert_player(puuid)
|
||||
AMQP.Basic.publish(channel, "", "player", puuid)
|
||||
{:reply, nil, state}
|
||||
|
||||
@@ -7,8 +7,10 @@ defmodule Storage.MatchStorage.S3MatchStorage do
|
||||
end
|
||||
|
||||
@impl true
|
||||
def stream_files(path) do
|
||||
ExAws.S3.list_objects_v2(path)
|
||||
def stream_files(path, opts \\ []) do
|
||||
patch = Keyword.get(opts, :patch, "")
|
||||
|
||||
ExAws.S3.list_objects_v2(path, prefix: patch)
|
||||
|> ExAws.stream!()
|
||||
end
|
||||
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ import Config
|
||||
config :lol_analytics_web, LoLAnalyticsWeb.Endpoint,
|
||||
server: true,
|
||||
http: [ip: {0, 0, 0, 0}, port: 4000],
|
||||
url: [host: "localhost", port: 80],
|
||||
url: [host: "lol-analytics.kaizer.cloud", port: 443],
|
||||
cache_static_manifest: "priv/static/cache_manifest.json"
|
||||
|
||||
# Do not print debug messages in production
|
||||
|
||||
+18
-2
@@ -26,11 +26,27 @@ services:
|
||||
- ~/.docker-conf/rabbitmq/data/:/var/lib/rabbitmq/
|
||||
- ~/.docker-conf/rabbitmq/log/:/var/log/rabbitmq
|
||||
networks:
|
||||
- rabbitmq_go_net
|
||||
- rabbitmq_net
|
||||
minio:
|
||||
image: bitnami/minio:latest
|
||||
ports:
|
||||
- '9000:9000'
|
||||
- '9001:9001'
|
||||
networks:
|
||||
- minionetwork
|
||||
volumes:
|
||||
- 'minio_data:/data'
|
||||
environment:
|
||||
- MINIO_ROOT_USER=your_username
|
||||
- MINIO_ROOT_PASSWORD=your_pasword
|
||||
- MINIO_DEFAULT_BUCKETS=your_bucket_name
|
||||
volumes:
|
||||
postgres-data:
|
||||
minio_data:
|
||||
|
||||
|
||||
networks:
|
||||
rabbitmq_go_net:
|
||||
rabbitmq_net:
|
||||
driver: bridge
|
||||
minionetwork:
|
||||
driver: bridge
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"amqp": {:hex, :amqp, "3.3.0", "056d9f4bac96c3ab5a904b321e70e78b91ba594766a1fc2f32afd9c016d9f43b", [:mix], [{:amqp_client, "~> 3.9", [hex: :amqp_client, repo: "hexpm", optional: false]}], "hexpm", "8d3ae139d2646c630d674a1b8d68c7f85134f9e8b2a1c3dd5621616994b10a8b"},
|
||||
"amqp_client": {:hex, :amqp_client, "3.12.13", "6fc6a7c681e53fed4cbd3f5bcdda342a2b46976345e460ef85414c63698cfe70", [:make, :rebar3], [{:credentials_obfuscation, "3.4.0", [hex: :credentials_obfuscation, repo: "hexpm", optional: false]}, {:rabbit_common, "3.12.13", [hex: :rabbit_common, repo: "hexpm", optional: false]}], "hexpm", "76f41bff0792193f00e0062128db51eb68bcee0eb8236139247a7d1866438d03"},
|
||||
"bandit": {:hex, :bandit, "1.5.0", "3bc864a0da7f013ad3713a7f550c6a6ec0e19b8d8715ec678256a0dc197d5539", [:mix], [{:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "92d18d9a7228a597e0d4661ef69a874ea82d63ff49c7d801a5c68cb18ebbbd72"},
|
||||
"broadway": {:hex, :broadway, "1.0.7", "7808f9e3eb6f53ca6d060f0f9d61012dd8feb0d7a82e62d087dd517b9b66fa53", [:mix], [{:gen_stage, "~> 1.0", [hex: :gen_stage, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.3.7 or ~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e76cfb0a7d64176c387b8b1ddbfb023e2ee8a63e92f43664d78e6d5d0b1177c6"},
|
||||
"broadway": {:hex, :broadway, "1.1.0", "8ed3aea01fd6f5640b3e1515b90eca51c4fc1fac15fb954cdcf75dc054ae719c", [:mix], [{:gen_stage, "~> 1.0", [hex: :gen_stage, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.3.7 or ~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "25e315ef1afe823129485d981dcc6d9b221cea30e625fd5439e9b05f44fb60e4"},
|
||||
"broadway_rabbitmq": {:hex, :broadway_rabbitmq, "0.8.1", "6d68a480b2e49694e4f3836dcbbf8e621bb97b34e84787a2093d5cc3078a4d87", [:mix], [{:amqp, "~> 1.3 or ~> 2.0 or ~> 3.0", [hex: :amqp, repo: "hexpm", optional: false]}, {:broadway, "~> 1.0", [hex: :broadway, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.3.5 or ~> 0.4.0 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6eebe9b03c9673cbda790430389c47e4ca867f9904418cff1b71a74a59c2a986"},
|
||||
"castore": {:hex, :castore, "1.0.7", "b651241514e5f6956028147fe6637f7ac13802537e895a724f90bf3e36ddd1dd", [:mix], [], "hexpm", "da7785a4b0d2a021cd1292a60875a784b6caef71e76bf4917bdee1f390455cf5"},
|
||||
"certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"},
|
||||
@@ -21,7 +21,7 @@
|
||||
"gen_stage": {:hex, :gen_stage, "1.2.1", "19d8b5e9a5996d813b8245338a28246307fd8b9c99d1237de199d21efc4c76a1", [:mix], [], "hexpm", "83e8be657fa05b992ffa6ac1e3af6d57aa50aace8f691fcf696ff02f8335b001"},
|
||||
"gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"},
|
||||
"hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"},
|
||||
"heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized"]},
|
||||
"heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized", depth: 1]},
|
||||
"hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"},
|
||||
"httpoison": {:hex, :httpoison, "2.2.1", "87b7ed6d95db0389f7df02779644171d7319d319178f6680438167d7b69b1f3d", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "51364e6d2f429d80e14fe4b5f8e39719cacd03eb3f9a9286e61e216feac2d2df"},
|
||||
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
|
||||
@@ -29,7 +29,7 @@
|
||||
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
|
||||
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
|
||||
"mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"},
|
||||
"nimble_options": {:hex, :nimble_options, "1.1.0", "3b31a57ede9cb1502071fade751ab0c7b8dbe75a9a4c2b5bbb0943a690b63172", [:mix], [], "hexpm", "8bbbb3941af3ca9acc7835f5655ea062111c9c27bcac53e004460dfd19008a99"},
|
||||
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
|
||||
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
|
||||
"phoenix": {:hex, :phoenix, "1.7.12", "1cc589e0eab99f593a8aa38ec45f15d25297dd6187ee801c8de8947090b5a9d3", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "d646192fbade9f485b01bc9920c139bfdd19d0f8df3d73fd8eaf2dfbe0d2837c"},
|
||||
"phoenix_ecto": {:hex, :phoenix_ecto, "4.5.1", "6fdbc334ea53620e71655664df6f33f670747b3a7a6c4041cdda3e2c32df6257", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ebe43aa580db129e54408e719fb9659b7f9e0d52b965c5be26cdca416ecead28"},
|
||||
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
```
|
||||
SELECT
|
||||
(cast(count(CASE WHEN is_win THEN 1 END) as float) / cast(count(*) as float)) * 100.0 as win_rate,
|
||||
count(CASE WHEN is_win THEN 1 END) as games_won,
|
||||
count(*) as total_games,
|
||||
champion_id
|
||||
FROM fact_champion_played_game
|
||||
GROUP BY champion_id
|
||||
ORDER BY win_rate desc;
|
||||
```
|
||||
|
||||
```
|
||||
SELECT
|
||||
(cast(count(CASE WHEN is_win THEN 1 END) as float) / cast(count(*) as float)) * 100.0 as win_rate,
|
||||
count(CASE WHEN is_win THEN 1 END) as games_won,
|
||||
count(*) as total_games,
|
||||
champion_id,
|
||||
team_position
|
||||
FROM fact_champion_played_game
|
||||
GROUP BY champion_id, team_position
|
||||
ORDER BY win_rate desc;
|
||||
```
|
||||
Reference in New Issue
Block a user