Compare commits

..

1 Commits

Author SHA1 Message Date
alvaro 2e8417e09b wip deploy 2024-05-26 18:03:57 +02:00
116 changed files with 714 additions and 2694 deletions
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/env ruby
# A sample docker-setup hook
#
# Sets up a Docker network which can then be used by the applications containers
ssh user@example.com docker network create kamal
+14
View File
@@ -0,0 +1,14 @@
#!/bin/sh
# A sample post-deploy hook
#
# These environment variables are available:
# KAMAL_RECORDED_AT
# KAMAL_PERFORMER
# KAMAL_VERSION
# KAMAL_HOSTS
# KAMAL_ROLE (if set)
# KAMAL_DESTINATION (if set)
# KAMAL_RUNTIME
echo "$KAMAL_PERFORMER deployed $KAMAL_VERSION to $KAMAL_DESTINATION in $KAMAL_RUNTIME seconds"
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh
echo "Rebooted Traefik on $KAMAL_HOSTS"
+51
View File
@@ -0,0 +1,51 @@
#!/bin/sh
# A sample pre-build hook
#
# Checks:
# 1. We have a clean checkout
# 2. A remote is configured
# 3. The branch has been pushed to the remote
# 4. The version we are deploying matches the remote
#
# These environment variables are available:
# KAMAL_RECORDED_AT
# KAMAL_PERFORMER
# KAMAL_VERSION
# KAMAL_HOSTS
# KAMAL_ROLE (if set)
# KAMAL_DESTINATION (if set)
if [ -n "$(git status --porcelain)" ]; then
echo "Git checkout is not clean, aborting..." >&2
git status --porcelain >&2
exit 1
fi
first_remote=$(git remote)
if [ -z "$first_remote" ]; then
echo "No git remote set, aborting..." >&2
exit 1
fi
current_branch=$(git branch --show-current)
if [ -z "$current_branch" ]; then
echo "Not on a git branch, aborting..." >&2
exit 1
fi
remote_head=$(git ls-remote $first_remote --tags $current_branch | cut -f1)
if [ -z "$remote_head" ]; then
echo "Branch not pushed to remote, aborting..." >&2
exit 1
fi
if [ "$KAMAL_VERSION" != "$remote_head" ]; then
echo "Version ($KAMAL_VERSION) does not match remote HEAD ($remote_head), aborting..." >&2
exit 1
fi
exit 0
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env ruby
# A sample pre-connect check
#
# Warms DNS before connecting to hosts in parallel
#
# These environment variables are available:
# KAMAL_RECORDED_AT
# KAMAL_PERFORMER
# KAMAL_VERSION
# KAMAL_HOSTS
# KAMAL_ROLE (if set)
# KAMAL_DESTINATION (if set)
# KAMAL_RUNTIME
hosts = ENV["KAMAL_HOSTS"].split(",")
results = nil
max = 3
elapsed = Benchmark.realtime do
results = hosts.map do |host|
Thread.new do
tries = 1
begin
Socket.getaddrinfo(host, 0, Socket::AF_UNSPEC, Socket::SOCK_STREAM, nil, Socket::AI_CANONNAME)
rescue SocketError
if tries < max
puts "Retrying DNS warmup: #{host}"
tries += 1
sleep rand
retry
else
puts "DNS warmup failed: #{host}"
host
end
end
tries
end
end.map(&:value)
end
retries = results.sum - hosts.size
nopes = results.count { |r| r == max }
puts "Prewarmed %d DNS lookups in %.2f sec: %d retries, %d failures" % [ hosts.size, elapsed, retries, nopes ]
+109
View File
@@ -0,0 +1,109 @@
#!/usr/bin/env ruby
# A sample pre-deploy hook
#
# Checks the Github status of the build, waiting for a pending build to complete for up to 720 seconds.
#
# Fails unless the combined status is "success"
#
# These environment variables are available:
# KAMAL_RECORDED_AT
# KAMAL_PERFORMER
# KAMAL_VERSION
# KAMAL_HOSTS
# KAMAL_COMMAND
# KAMAL_SUBCOMMAND
# KAMAL_ROLE (if set)
# KAMAL_DESTINATION (if set)
# Only check the build status for production deployments
if ENV["KAMAL_COMMAND"] == "rollback" || ENV["KAMAL_DESTINATION"] != "production"
exit 0
end
require "bundler/inline"
# true = install gems so this is fast on repeat invocations
gemfile(true, quiet: true) do
source "https://rubygems.org"
gem "octokit"
gem "faraday-retry"
end
MAX_ATTEMPTS = 72
ATTEMPTS_GAP = 10
def exit_with_error(message)
$stderr.puts message
exit 1
end
class GithubStatusChecks
attr_reader :remote_url, :git_sha, :github_client, :combined_status
def initialize
@remote_url = `git config --get remote.origin.url`.strip.delete_prefix("https://github.com/")
@git_sha = `git rev-parse HEAD`.strip
@github_client = Octokit::Client.new(access_token: ENV["GITHUB_TOKEN"])
refresh!
end
def refresh!
@combined_status = github_client.combined_status(remote_url, git_sha)
end
def state
combined_status[:state]
end
def first_status_url
first_status = combined_status[:statuses].find { |status| status[:state] == state }
first_status && first_status[:target_url]
end
def complete_count
combined_status[:statuses].count { |status| status[:state] != "pending"}
end
def total_count
combined_status[:statuses].count
end
def current_status
if total_count > 0
"Completed #{complete_count}/#{total_count} checks, see #{first_status_url} ..."
else
"Build not started..."
end
end
end
$stdout.sync = true
puts "Checking build status..."
attempts = 0
checks = GithubStatusChecks.new
begin
loop do
case checks.state
when "success"
puts "Checks passed, see #{checks.first_status_url}"
exit 0
when "failure"
exit_with_error "Checks failed, see #{checks.first_status_url}"
when "pending"
attempts += 1
end
exit_with_error "Checks are still pending, gave up after #{MAX_ATTEMPTS * ATTEMPTS_GAP} seconds" if attempts == MAX_ATTEMPTS
puts checks.current_status
sleep(ATTEMPTS_GAP)
checks.refresh!
end
rescue Octokit::NotFound
exit_with_error "Build status could not be found"
end
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh
echo "Rebooting Traefik on $KAMAL_HOSTS..."
+1 -52
View File
@@ -1,52 +1 @@
# 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`.
# LoLAnalytics.Umbrella
@@ -0,0 +1,3 @@
defmodule LolAnalytics.Analyzer do
@callback analyze(:url, path :: String.t()) :: :ok
end
@@ -0,0 +1,50 @@
defmodule LolAnalytics.Analyzer.ChampionAnalyzer do
alias Hex.HTTP
@behaviour LolAnalytics.Analyzer
def analyze_all_matches do
Storage.MatchStorage.S3MatchStorage.list_files("ranked")
|> Enum.map(& &1.key)
|> Enum.each(fn path ->
LolAnalytics.Analyzer.ChampionAnalyzer.analyze(:url, "http://localhost:9000/ranked/#{path}")
end)
end
@doc """
iex> LolAnalytics.Analyzer.ChampionAnalyzer.analyze(:url, "http://localhost:9000/ranked/14.9.580.2108/EUW1_6923309745.json")
"""
@impl true
@spec analyze(atom(), String.t()) :: :ok
def analyze(:url, path) do
data = HTTPoison.get!(path)
analyze(:data, data.body)
:ok
end
@impl true
@spec analyze(atom(), any()) :: list(LoLAPI.Model.Participant.t())
def analyze(:data, data) do
decoded_match = Poison.decode!(data, as: %LoLAPI.Model.MatchResponse{})
participants = decoded_match.info.participants
version = extract_game_version(decoded_match)
participants
|> Enum.each(fn participant = %LoLAPI.Model.Participant{} ->
if participant.teamPosition != "" do
LolAnalytics.ChampionWinRate.ChampionWinRateRepo.add_champion_win_rate(
participant.championId,
version,
participant.teamPosition,
participant.win
)
end
end)
end
defp extract_game_version(game_data) do
game_data.info.gameVersion
|> String.split(".")
|> Enum.take(2)
|> Enum.join(".")
end
end
@@ -10,8 +10,7 @@ defmodule LoLAnalytics.Application do
children = [
LoLAnalytics.Repo,
{DNSCluster, query: Application.get_env(:lol_analytics, :dns_cluster_query) || :ignore},
{Phoenix.PubSub, name: LoLAnalytics.PubSub},
{Task.Supervisor, name: LoLAnalytics.TaskSupervisor}
{Phoenix.PubSub, name: LoLAnalytics.PubSub}
# Start a worker by calling: LoLAnalytics.Worker.start_link(arg)
# {LoLAnalytics.Worker, arg}
]
@@ -0,0 +1,56 @@
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
@@ -0,0 +1,20 @@
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
@@ -1,41 +0,0 @@
defmodule LolAnalytics.Dimensions.Champion.ChampionMetadata do
alias LolAnalytics.Dimensions.Champion.ChampionRepo
@champions_data_url "https://ddragon.leagueoflegends.com/cdn/14.11.1/data/en_US/champion.json"
def update_metadata() do
{:ok, %{"data" => data}} = get_champions()
data
|> Enum.each(&save_metadata/1)
end
defp get_champions() do
with {:ok, resp} <- HTTPoison.get(@champions_data_url),
data <- Poison.decode(resp.body) do
data
else
{:error, reason} -> {:error, reason}
end
end
defp save_metadata({_champion, info}) do
%{
"image" => %{
"full" => full_image
},
"name" => name,
"key" => key_string
} = info
attrs = %{
image: full_image,
name: name
}
{champion_id, _} = Integer.parse(key_string)
ChampionRepo.update(champion_id, attrs)
info
end
end
@@ -1,29 +0,0 @@
defmodule LolAnalytics.Dimensions.Champion.ChampionRepo do
alias LoLAnalytics.Repo
alias LolAnalytics.Dimensions.Champion.ChampionSchema
@spec get_or_create(String.t()) :: struct()
def get_or_create(champion_id) do
champion = Repo.get_by(ChampionSchema, champion_id: champion_id)
case champion do
nil ->
changeset = ChampionSchema.changeset(%ChampionSchema{}, %{champion_id: champion_id})
Repo.insert(changeset)
champion ->
champion
end
end
def update(champion_id, attrs) do
get_or_create(champion_id)
|> ChampionSchema.changeset(attrs)
|> Repo.update()
end
@spec list_champions() :: any()
def list_champions() do
Repo.all(ChampionSchema)
end
end
@@ -1,17 +0,0 @@
defmodule LolAnalytics.Dimensions.Champion.ChampionSchema do
use Ecto.Schema
import Ecto.Changeset
schema "dim_champion" do
field :champion_id, :integer
field :name, :string
field :image, :string
timestamps()
end
def changeset(champion = %__MODULE__{}, attrs \\ %{}) do
champion
|> cast(attrs, [:champion_id, :name, :image])
|> validate_required([:champion_id])
end
end
@@ -1,22 +0,0 @@
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
@@ -1,30 +0,0 @@
defmodule LolAnalytics.Dimensions.Item.ItemRepo do
alias LolAnalytics.Dimensions.Item.ItemSchema
alias LoLAnalytics.Repo
import Ecto.Query
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)
item ->
item
end
end
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
@@ -1,18 +0,0 @@
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, @args)
|> validate_required([:item_id])
end
end
@@ -1,70 +0,0 @@
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(%{
: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 ->
%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
@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(_), do: raise("Invalid processing status")
end
@@ -1,29 +0,0 @@
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, @casting_attrs)
|> validate_required([:match_id])
end
end
@@ -1,28 +0,0 @@
defmodule LolAnalytics.Dimensions.Patch.PatchRepo do
alias LolAnalytics.Dimensions.Patch.PatchSchema
alias LoLAnalytics.Repo
import Ecto.Query
def get_or_create(patch_number) do
query = from p in PatchSchema, where: p.patch_number == ^patch_number
case Repo.one(query) do
nil ->
patch_changeset =
PatchSchema.changeset(
%PatchSchema{},
%{patch_number: patch_number}
)
Repo.insert(patch_changeset)
patch ->
patch
end
end
def list_patches() do
Repo.all(PatchSchema)
end
end
@@ -1,16 +0,0 @@
defmodule LolAnalytics.Dimensions.Patch.PatchSchema do
use Ecto.Schema
import Ecto.Changeset
schema "dim_patch" do
field :patch_number, :string
timestamps()
end
def changeset(patch = %__MODULE__{}, attrs \\ %{}) do
patch
|> cast(attrs, [:patch_number])
|> validate_required([:patch_number])
|> unique_constraint([:patch_number])
end
end
@@ -1,28 +0,0 @@
defmodule LolAnalytics.Dimensions.Player.PlayerRepo do
import Ecto.Query
alias LolAnalytics.Dimensions.Player.PlayerSchema
alias LoLAnalytics.Repo
def get_or_create(puuid) do
query = from p in PlayerSchema, where: p.puuid == ^puuid
case Repo.one(query) do
nil ->
player_changeset =
PlayerSchema.changeset(
%PlayerSchema{},
%{puuid: puuid}
)
Repo.insert(player_changeset)
player ->
player
end
end
def list_players() do
Repo.all(PlayerSchema)
end
end
@@ -1,22 +0,0 @@
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, @attrs)
|> validate_required([:puuid])
|> unique_constraint([:puuid])
end
end
@@ -1,22 +0,0 @@
defmodule LolAnalytics.Dimensions.SummonerSpell.SummonerSpellMetadata do
alias LolAnalytics.Dimensions.SummonerSpell.SummonerSpellRepo
@spells_url "https://ddragon.leagueoflegends.com/cdn/14.11.1/data/en_US/summoner.json"
def update_metadata() do
get_spells()
|> Enum.each(&save_metadata/1)
end
defp get_spells() do
case HTTPoison.get(@spells_url) do
{:ok, resp} ->
Poison.decode!(resp.body)["data"]
end
end
defp save_metadata({name, metadata}) do
%{"key" => spell_id} = metadata
SummonerSpellRepo.update(spell_id, %{metadata: metadata})
end
end
@@ -1,37 +0,0 @@
defmodule LolAnalytics.Dimensions.SummonerSpell.SummonerSpellRepo do
import Ecto.Query
alias LolAnalytics.Dimensions.SummonerSpell.SummonerSpellSchema
alias LoLAnalytics.Repo
@spec get_or_create(String.t()) :: any()
def get_or_create(spell_id) do
query = from s in SummonerSpellSchema, where: s.spell_id == ^spell_id
spell = Repo.one(query)
case spell do
nil ->
spell_changeset =
SummonerSpellSchema.changeset(
%SummonerSpellSchema{},
%{spell_id: spell_id}
)
Repo.insert(spell_changeset)
spell ->
spell
end
end
@spec update(spell_id :: String.t(), attrs :: map()) :: any()
def update(spell_id, attrs) do
get_or_create(spell_id)
|> SummonerSpellSchema.changeset(attrs)
|> Repo.update()
end
def list_spells() do
Repo.all(SummonerSpellSchema)
end
end
@@ -1,16 +0,0 @@
defmodule LolAnalytics.Dimensions.SummonerSpell.SummonerSpellSchema do
use Ecto.Schema
import Ecto.Changeset
schema "dim_summoner_spell" do
field :spell_id, :integer
field :metadata, :map
timestamps()
end
def changeset(summoner_spell = %__MODULE__{}, attrs) do
summoner_spell
|> cast(attrs, [:spell_id, :metadata])
|> validate_required([:spell_id])
end
end
@@ -1,70 +0,0 @@
defmodule LolAnalytics.Facts.ChampionPickedItem.FactProcessor do
alias LolAnalytics.Dimensions.Match.MatchRepo
alias LolAnalytics.Facts.ChampionPickedItem.Repo
require Logger
@behaviour LolAnalytics.Facts.FactBehaviour
@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
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)
MatchRepo.update(match, %{fact_champion_picked_item_status: :processed})
end
defp extract_game_version(game_data) do
game_data.info.gameVersion
|> String.split(".")
|> Enum.take(2)
|> Enum.join(".")
end
end
@@ -1,89 +0,0 @@
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
@@ -1,34 +0,0 @@
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
@@ -1,77 +0,0 @@
defmodule LolAnalytics.Facts.ChampionPickedSummonerSpell.FactProcessor do
@behaviour LolAnalytics.Facts.FactBehaviour
require Logger
alias LolAnalytics.Dimensions.Match.MatchRepo
alias LolAnalytics.Facts.ChampionPickedSummonerSpell
@impl true
@spec process_game_at_url(String.t()) :: any()
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 ChampionPickedSummonerSpell")
{:error, "Could not process data from #{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 ChampionPickedSummoner for match #{decoded_match.metadata.matchId}")
participants
|> Enum.each(fn participant = %LoLAPI.Model.Participant{} ->
if participant.teamPosition != "" do
attrs_spell_1 = %{
champion_id: participant.championId,
match_id: decoded_match.metadata.matchId,
is_win: participant.win,
summoner_spell_id: participant.summoner1Id,
game_length_seconds: decoded_match.info.gameDuration,
queue_id: decoded_match.info.queueId,
puuid: participant.puuid,
team_position: participant.teamPosition,
patch_number: version
}
attrs_spell_2 = %{
champion_id: participant.championId,
match_id: decoded_match.metadata.matchId,
is_win: participant.win,
summoner_spell_id: participant.summoner2Id,
game_length_seconds: decoded_match.info.gameDuration,
queue_id: decoded_match.info.queueId,
puuid: participant.puuid,
team_position: participant.teamPosition,
patch_number: version
}
ChampionPickedSummonerSpell.Repo.insert(attrs_spell_1)
ChampionPickedSummonerSpell.Repo.insert(attrs_spell_2)
end
end)
MatchRepo.update(match, %{fact_champion_picked_summoner_spell_status: :processed})
end
defp extract_game_version(game_data) do
game_data.info.gameVersion
|> String.split(".")
|> Enum.take(2)
|> Enum.join(".")
end
end
@@ -1,81 +0,0 @@
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
alias LolAnalytics.Facts.ChampionPickedSummonerSpell.Schema
alias LolAnalytics.Dimensions.Player.PlayerRepo
alias LolAnalytics.Dimensions.Champion.ChampionRepo
alias LolAnalytics.Dimensions.Match.MatchRepo
alias LoLAnalytics.Repo
@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
_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,
where:
f.match_id == ^attrs.match_id and
f.champion_id == ^attrs.champion_id and
f.summoner_spell_id ==
^attrs.summoner_spell_id
)
|> Repo.one()
Schema.changeset(prev || %Schema{}, attrs)
|> Repo.insert_or_update()
end
@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 == ^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,
on: s.spell_id == f.summoner_spell_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
),
spell_id: f.summoner_spell_id,
metadata: s.metadata,
champion_id: c.champion_id,
team_position: f.team_position,
total_games: count("*")
},
group_by: [
f.champion_id,
f.summoner_spell_id,
s.metadata,
c.champion_id,
f.team_position
]
Repo.all(query)
end
end
@@ -1,36 +0,0 @@
defmodule LolAnalytics.Facts.ChampionPickedSummonerSpell.Schema do
use Ecto.Schema
import Ecto.Changeset
@params [
:champion_id,
:summoner_spell_id,
:match_id,
:patch_number,
:is_win,
:game_length_seconds,
:queue_id,
:team_position,
:puuid
]
schema "fact_champion_picked_summoner_spell" 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
field :team_position, :string
field :puuid, :string
end
def changeset(fact = %__MODULE__{}, attrs \\ %{}) do
fact
|> cast(attrs, @params)
|> validate_required(@params)
|> unique_constraint([:puuid, :match_id, :summoner_spell_id])
end
end
@@ -1,136 +0,0 @@
defmodule LolAnalytics.Facts.ChampionPlayedGame.Repo do
import Ecto.Query
alias LolAnalytics.Dimensions.Patch.PatchRepo
alias LolAnalytics.Dimensions.Champion.ChampionSchema
alias LolAnalytics.Dimensions.Player.PlayerRepo
alias LolAnalytics.Dimensions.Champion.ChampionRepo
alias LolAnalytics.Facts.ChampionPlayedGame.Schema
alias LoLAnalytics.Repo
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)
prev =
from(f in Schema,
where:
f.match_id == ^attrs.match_id and
f.champion_id == ^attrs.champion_id and f.queue_id == ^attrs.queue_id
)
|> Repo.one()
changeset = Schema.changeset(prev || %Schema{}, attrs)
Repo.insert_or_update(changeset)
end
def list_played_matches() do
Repo.all(Schema)
end
def get_win_rates(opts \\ []) do
team_position = Keyword.get(opts, :team_position)
patch = Keyword.get(opts, :patch_number, "14.12")
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
@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,
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.patch_number, m.team_position],
where: m.team_position == ^team_position and m.champion_id == ^champion_id,
having: count("*") > 50
Repo.all(query)
end
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,35 +0,0 @@
defmodule LolAnalytics.Facts.ChampionPlayedGame.Schema do
use Ecto.Schema
import Ecto.Changeset
@casting_attrs [
:champion_id,
:match_id,
:patch_number,
:is_win,
:game_length_seconds,
:team_position,
:puuid,
:queue_id
]
schema "fact_champion_played_game" do
field :champion_id, :integer
field :match_id, :string
field :patch_number, :string
field :is_win, :boolean
field :game_length_seconds, :integer
field :team_position, :string
field :puuid, :string
field :queue_id, :integer
timestamps()
end
def changeset(fact = %__MODULE__{}, attrs \\ %{}) do
fact
|> cast(attrs, @casting_attrs)
|> validate_required(@casting_attrs)
|> unique_constraint([:id, :champion_id, :queue_id])
end
end
@@ -1,61 +0,0 @@
defmodule LolAnalytics.Facts.ChampionPlayedGame.FactProcessor do
alias LolAnalytics.Dimensions.Match.MatchRepo
require Logger
@behaviour LolAnalytics.Facts.FactBehaviour
@impl true
@spec process_game_at_url(String.t()) :: none()
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 ChampionPlayedGame")
{:error, "Could not process data from #{url}"}
end
end
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
|> Enum.each(fn participant = %LoLAPI.Model.Participant{} ->
if participant.teamPosition != "" do
attrs = %{
champion_id: participant.championId,
match_id: decoded_match.metadata.matchId,
is_win: participant.win,
game_length_seconds: decoded_match.info.gameDuration,
queue_id: decoded_match.info.queueId,
puuid: participant.puuid,
team_position: participant.teamPosition,
patch_number: version
}
LolAnalytics.Facts.ChampionPlayedGame.Repo.insert(attrs)
end
end)
MatchRepo.update(match, %{fact_champion_played_game_status: :processed})
end
defp extract_game_version(game_data) do
game_data.info.gameVersion
|> String.split(".")
|> Enum.take(2)
|> Enum.join(".")
end
end
@@ -1,3 +0,0 @@
defmodule LolAnalytics.Facts.FactBehaviour do
@callback process_game_at_url(String.t()) :: any()
end
@@ -1,40 +0,0 @@
defmodule LolAnalytics.Facts.FactsRunner do
alias LolAnalytics.Facts
def analyze_by_patch(patch) do
Storage.MatchStorage.S3MatchStorage.stream_files("ranked", patch: patch)
|> peach(fn %{key: path} ->
get_facts()
|> Enum.each(fn fact_runner ->
apply(fact_runner, ["http://192.168.1.55:9000/ranked/#{path}"])
end)
end)
end
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)
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.ChampionPickedItem.FactProcessor.process_game_at_url/1
]
end
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()
end
end
@@ -0,0 +1,35 @@
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{}
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
@@ -0,0 +1,19 @@
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
@@ -1,26 +0,0 @@
defmodule LolAnalytics.MatchesProcessor do
use GenServer
def init(init_args) do
{:ok, init_args}
end
@doc """
iex> LolAnalytics.MatchesProcessor.process_for_patch "14.12.593.5894"
"""
def process_for_patch(patch) do
Task.Supervisor.async(LoLAnalytics.TaskSupervisor, fn ->
LolAnalytics.Facts.FactsRunner.analyze_by_patch(patch)
end)
end
def process_all_matches() do
Task.Supervisor.async(LoLAnalytics.TaskSupervisor, fn ->
LolAnalytics.Facts.FactsRunner.analyze_all_matches()
end)
end
def get_running_processes() do
Task.Supervisor.children(LoLAnalytics.TaskSupervisor)
end
end
-1
View File
@@ -42,7 +42,6 @@ defmodule LoLAnalytics.MixProject do
{:postgrex, ">= 0.0.0"},
{:jason, "~> 1.2"},
{:lol_api, in_umbrella: true},
{:storage, in_umbrella: true},
{:httpoison, "~> 2.2"},
{:poison, "~> 5.0"}
]
@@ -1,64 +0,0 @@
defmodule LoLAnalytics.Repo.Migrations.AnalyticsTables do
use Ecto.Migration
def change do
create table("dim_champion") do
add :champion_id, :integer, primary_key: true, null: false
timestamps()
end
create index("dim_champion", [:champion_id], unique: true)
create table("dim_item") do
add :item_id, :integer, primary_key: true, null: false
timestamps()
end
create index("dim_item", [:item_id], unique: true)
create table("dim_patch") do
add :patch_number, :string, primary_key: true, null: false
timestamps()
end
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()
end
create index("dim_player", [:puuid], unique: true)
create table("dim_summoner_spell") do
add :spell_id, :integer, primary_key: true, null: false
timestamps()
end
create index("dim_summoner_spell", [:spell_id], unique: true)
create table("fact_champion_played_game") do
add :champion_id, references("dim_champion", column: :champion_id, type: :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)
timestamps()
end
create index("fact_champion_played_game", [:id, :champion_id, :queue_id])
create index("fact_champion_played_game", [:puuid, :match_id], unique: true)
end
end
@@ -1,10 +0,0 @@
defmodule LoLAnalytics.Repo.Migrations.ChampionDimName do
use Ecto.Migration
def change do
alter table("dim_champion") do
add :name, :string
add :image, :string
end
end
end
@@ -1,31 +0,0 @@
defmodule LoLAnalytics.Repo.Migrations.SummonerSpellWinRate do
use Ecto.Migration
def change do
create table("fact_champion_picked_summoner_spell") do
add :champion_id, references("dim_champion", column: :champion_id, type: :integer)
add :summoner_spell_id, references("dim_summoner_spell", column: :spell_id, type: :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 :team_position, :string
add :puuid, references("dim_player", column: :puuid, type: :string)
add :patch_number, references("dim_patch", column: :patch_number, type: :string)
end
create index(
"fact_champion_picked_summoner_spell",
[:puuid, :match_id, :summoner_spell_id],
unique: true
)
create index("fact_champion_picked_summoner_spell", [
:is_win,
:team_position,
:queue_id,
:summoner_spell_id,
:patch_number
])
end
end
@@ -1,9 +0,0 @@
defmodule LoLAnalytics.Repo.Migrations.SummonerSpellMetadata do
use Ecto.Migration
def change do
alter table("dim_summoner_spell") do
add :metadata, :map
end
end
end
@@ -1,11 +0,0 @@
defmodule LoLAnalytics.Repo.Migrations.SummonerSpellMetadataIndex do
use Ecto.Migration
def up do
execute("CREATE INDEX dim_summoner_spell_metadata ON dim_summoner_spell USING GIN(metadata)")
end
def down do
execute("DROP INDEX dim_summoner_spell_metadata")
end
end
@@ -1,28 +0,0 @@
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
@@ -1,9 +0,0 @@
defmodule LoLAnalytics.Repo.Migrations.ItemMetadata do
use Ecto.Migration
def change do
alter table("dim_item") do
add :metadata, :map
end
end
end
@@ -1,11 +0,0 @@
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
@@ -1,15 +0,0 @@
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
@@ -1,9 +0,0 @@
defmodule LoLAnalytics.Repo.Migrations.DimMatchQueueId do
use Ecto.Migration
def change do
alter table "dim_match" do
add :queue_id, :integer
end
end
end
@@ -1,9 +0,0 @@
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
-13
View File
@@ -3,16 +3,3 @@
@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;
}
+4 -8
View File
@@ -18,22 +18,18 @@
// 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 },
hooks: {
ChampionWinRate,
}
params: {_csrf_token: csrfToken}
})
// 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())
@@ -1,50 +0,0 @@
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
View File
@@ -1,28 +0,0 @@
{
"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"
}
}
}
}
@@ -1,5 +0,0 @@
{
"dependencies": {
"chart.js": "^4.4.3"
}
}
@@ -1,23 +0,0 @@
defmodule LolAnalyticsWeb.ChampionComponents.ChampionAvatar do
use Phoenix.Component
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} class="champion-avatar rounded-xl" alt="champion-icon" />
</div>
"""
end
end
@@ -1,39 +0,0 @@
defmodule LolAnalyticsWeb.ChampionComponents.ChampionCard.Props do
defstruct [:id, :win_rate, :image, :name, :team_position, :patch_number, :wins, :total_games]
end
defmodule LolAnalyticsWeb.ChampionComponents.ChampionCard do
use Phoenix.Component
alias LolAnalyticsWeb.ChampionComponents.ChampionCard.Props
attr :props, Props, default: %Props{}
def champion_card(assigns) do
~H"""
<.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">
<img src={team_position_image(@props.team_position)} class="w-5 h-5" />
<h3 class="text-white"><%= @props.name %></h3>
</div>
<img src={"https://ddragon.leagueoflegends.com/cdn/14.11.1/img/champion/#{@props.image}"} />
</div>
<div class="py-1" />
<div class="pl-2">
<h3>Win rate: <%= @props.win_rate %></h3>
<p class="text-xs">Wins: <%= @props.wins %> / <%= @props.total_games %></p>
</div>
<div class="py-1" />
</div>
</.link>
"""
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
@@ -1,23 +0,0 @@
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
@@ -1,23 +0,0 @@
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
@@ -315,8 +315,8 @@ defmodule LoLAnalyticsWeb.CoreComponents do
type="checkbox"
id={@id}
name={@name}
value="false"
checked={false}
value="true"
checked={@checked}
class="rounded border-zinc-300 text-zinc-900 focus:ring-0"
{@rest}
/>
@@ -2,13 +2,25 @@
<div class="flex items-center justify-between border-b border-zinc-100 py-3 text-sm">
<div class="flex items-center gap-4">
<a href="/">
LoL Analytics
<img src={~p"/images/logo.svg"} width="36" />
</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">
Champions
</.link>
<a href="https://twitter.com/elixirphoenix" class="hover:text-zinc-700">
@elixirphoenix
</a>
<a href="https://github.com/phoenixframework/phoenix" class="hover:text-zinc-700">
GitHub
</a>
<a
href="https://hexdocs.pm/phoenix/overview.html"
class="rounded-lg bg-zinc-100 px-2 py-1 hover:bg-zinc-200/80"
>
Get Started <span aria-hidden="true">&rarr;</span>
</a>
</div>
</div>
</header>
@@ -1,7 +1,6 @@
<!DOCTYPE html>
<html lang="en" class="[scrollbar-gutter:stable]">
<head>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="csrf-token" content={get_csrf_token()} />
@@ -11,10 +10,8 @@
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
</script>
</head>
<body class="bg-white antialiased">
</head>
<body class="bg-white antialiased">
<%= @inner_content %>
</body>
</body>
</html>
@@ -1,28 +0,0 @@
defmodule LolAnalyticsWeb.Loader do
use Phoenix.Component
def loader(assigns) do
~H"""
<div role="status" class="flex flex-col mx-auto w-max justify-center items-center gap-4">
<span class="text-3xl">Loading</span>
<svg
aria-hidden="true"
class="w-16 h-16 text-gray-300 animate-spin dark:text-gray-300 fill-orange-600"
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill"
/>
</svg>
<span class="sr-only">Loading...</span>
</div>
"""
end
end
@@ -1,66 +0,0 @@
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 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,3 +0,0 @@
defmodule LolAnalyticsWeb.ChampionLive.ChampionSummary do
defstruct [:id, :win_rate, :image, :name, :team_position, :wins, :total_games]
end
@@ -1,57 +0,0 @@
defmodule LolAnalyticsWeb.ChampionLive.Components.ChampionFilters do
use LoLAnalyticsWeb, :live_component
@roles_definition [
%{title: "All", value: "all"},
%{title: "Top", value: "TOP"},
%{title: "Jungle", value: "JUNGLE"},
%{title: "Mid", value: "MIDDLE"},
%{title: "Bot", value: "BOTTOM"},
%{title: "Support", value: "UTILITY"}
]
def on_role_selected(role) do
send_update(LolAnalyticsWeb.ChampionLive, role)
end
def mount(socket) do
socket =
assign(socket, :roles, @roles_definition)
{:ok, socket}
end
attr :selectedrole, :string, required: true
attr :roles, :list, default: []
def render(assigns) do
selected_class =
"px-8 py-2 flex flex-row gap-2 align-middle rounded-full border-gray-200 border border-orange-600 cursor-pointer"
~H"""
<div>
<div class="flex flex-row overflow-x-scroll sm:overflow-x-auto justify-between">
<%= for role <- @roles do %>
<%= if (assigns.selectedrole == role.value) do %>
<div phx-click="filter" phx-value-role={role.title} class={selected_class}>
<p><%= role.title %></p>
</div>
<% else %>
<div
phx-click="filter"
phx-value-role={role.value}
class="px-8 py-2 flex flex-row gap-2 align-middle cursor-pointer"
>
<p><%= role.title %></p>
</div>
<% end %>
<% end %>
</div>
</div>
"""
end
end
defmodule LolAnalyticsWeb.ChampionLive.Components.ChampionFilters.EventHandler do
@callback role_selected() :: none()
end
@@ -1,129 +0,0 @@
defmodule LoLAnalyticsWeb.ChampionLive.Index do
use LoLAnalyticsWeb, :live_view
import LolAnalyticsWeb.ChampionComponents.ChampionCard
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 = params["role"] || "all"
patch = params["patch"] || "14.12"
socket =
socket
|> assign(:selected_role, role)
|> assign(:selected_patch, patch)
|> assign(:champions, %{status: :loading})
|> load_champs(role, patch)
{:ok, socket}
end
def handle_params(params, _uri, socket) do
role = params["role"] || "all"
patch = params["patch"] || "14.12"
{
:noreply,
assign(socket, selected_role: role)
|> assign(selected_patch: patch)
}
end
@impl true
def handle_event("filter", %{"role" => selected_role} = params, socket) do
query = Map.merge(params, %{patch: socket.assigns.selected_patch || "14.12"})
{:reply, %{},
socket
|> push_patch(to: ~p"/champions?#{query}")
|> assign(:champions, %{status: :loading})
|> load_champs(selected_role, socket.assigns.selected_patch)
|> assign(:selected_role, selected_role)}
end
def handle_info(%{patch: patch}, socket) do
selected_role = socket.assigns.selected_role
query_params = %{role: selected_role, 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(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
def handle_async(:get_champs, {:ok, champs}, socket) do
{:noreply, assign(socket, :champions, %{status: :data, data: champs})}
end
def render_champions(assigns) do
case assigns.champions do
%{status: :loading} ->
~H"""
<.loader />
"""
%{status: :data, data: champions} ->
~H"""
<div id="champions" class="grid grid-cols-2 sm:grid-cols-4 gap-4">
<%= for champion <- champions do %>
<.champion_card props={champion} />
<% end %>
</div>
"""
end
end
defp filter_role(role) do
fn champ ->
champ.team_position == role || role == "all"
end
end
@impl true
def handle_params(params, _url, socket) do
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
end
defp apply_action(socket, :index, _params) do
socket
|> assign(:page_title, "Listing Champions")
end
end
@@ -1,18 +0,0 @@
<.header>
Champions
</.header>
<div class="px-2">
<h1 class="text-l font-semibold">Filters</h1>
<div class="flex flex-col py-2 gap-2">
<p>Roles</p>
<.live_component module={ChampionFilters} id="role-filters" selectedrole={@selected_role || "all" } />
</div>
<.live_component module={PatchSelector} id="patch-selector" />
<div class="h-4"></div>
<.render_champions champions={@champions} />
</div>
@@ -1,13 +0,0 @@
defmodule LolAnalyticsWeb.ChampionLive.Mapper do
alias LolAnalyticsWeb.ChampionLive.ChampionSummary
def map_champs(champs) do
champs
|> Enum.map(fn champ ->
%{
champ
| win_rate: :erlang.float_to_binary(champ.win_rate, decimals: 2)
}
end)
end
end
@@ -1,167 +0,0 @@
defmodule LoLAnalyticsWeb.ChampionLive.Show do
use LoLAnalyticsWeb, :live_view
import LolAnalyticsWeb.ChampionComponents.SummonerSpells
import LolAnalyticsWeb.ChampionComponents.ChampionAvatar
import LolAnalyticsWeb.ChampionComponents.Items
import LolAnalyticsWeb.Loader
alias LolAnalyticsWeb.ChampionComponents.SummonerSpells.ShowMapper
@impl true
def mount(_params, _session, socket) do
{:ok, socket}
end
@impl true
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))
|> load_win_rates(id, team_position)
|> load_summoner_spells(id, team_position, patch)
|> load_items(id, team_position, patch)}
end
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
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,36 +0,0 @@
<style>
.win-rate {
height: 250px;
}
.win-rate-container {
width: 100%;
height: 250px;
}
</style>
<.header>
<h1 class="text-4xl">
<%= @champion.name %>
</h1>
</.header>
<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" />
<.render_items items={@items} />
@@ -1,60 +0,0 @@
defmodule LolAnalyticsWeb.ChampionComponents.SummonerSpells.ShowMapper do
alias LolAnalyticsWeb.ChampionComponents.SummonerSpells.SummonerSpell
@spec map_champion(%LolAnalytics.Dimensions.Champion.ChampionSchema{}) :: map()
def map_champion(champion) do
%{
id: champion.champion_id,
name: champion.name,
image: champion.image
}
end
def map_spells(items) do
items
|> Enum.map(&map_spell/1)
|> Enum.sort(&(&1.total_games > &2.total_games))
end
def map_spell(spell) do
image = spell.metadata["image"]["full"]
%{
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
@@ -1,27 +0,0 @@
defmodule LoLAnalyticsWeb.RoleLive.FormComponent do
use LoLAnalyticsWeb, :live_component
@impl true
def render(assigns) do
~H"""
<div>
<.header>
<%= @title %>
<:subtitle>Use this form to manage role records in your database.</:subtitle>
</.header>
<.simple_form
for={@form}
id="role-form"
phx-target={@myself}
phx-change="validate"
phx-submit="save"
>
<:actions>
<.button phx-disable-with="Saving...">Save Role</.button>
</:actions>
</.simple_form>
</div>
"""
end
end
@@ -1,34 +0,0 @@
defmodule LoLAnalyticsWeb.RoleLive.Index do
use LoLAnalyticsWeb, :live_view
@roles ["ALL", "TOP", "MIDDLE", "JUNGLE", "UTILITY", "BOTTOM"]
@impl true
def mount(_params, _session, socket) do
{:ok, stream(socket, :roles, @roles)}
end
@impl true
def handle_params(params, _url, socket) do
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
end
defp apply_action(socket, :index, _params) do
socket
|> assign(:page_title, "Listing Roles")
|> assign(:role, nil)
end
@impl true
def handle_info({LoLAnalyticsWeb.RoleLive.FormComponent, {:saved, role}}, socket) do
{:noreply, stream_insert(socket, :roles, role)}
end
# @impl true
# def handle_event("delete", %{"id" => id}, socket) do
# role = Accounts.get_role!(id)
# {:ok, _} = Accounts.delete_role(role)
# {:noreply, stream_delete(socket, :roles, role)}
# end
end
@@ -1,4 +0,0 @@
<.header>
Listing Roles
<:actions></:actions>
</.header>
@@ -1,18 +0,0 @@
defmodule LoLAnalyticsWeb.RoleLive.Show do
use LoLAnalyticsWeb, :live_view
@impl true
def mount(_params, _session, socket) do
{:ok, socket}
end
@impl true
def handle_params(%{"id" => _id}, _, socket) do
{:noreply,
socket
|> assign(:page_title, page_title(socket.assigns.live_action))}
end
defp page_title(:show), do: "Show Role"
defp page_title(:edit), do: "Edit Role"
end
@@ -1,5 +0,0 @@
<.header>
Role <%= @role.id %>
<:subtitle>This is a role record from your database.</:subtitle>
<:actions></:actions>
</.header>
@@ -17,13 +17,7 @@ defmodule LoLAnalyticsWeb.Router do
scope "/", LoLAnalyticsWeb do
pipe_through :browser
live "/", ChampionLive.Index, :index
live "/champions", ChampionLive.Index, :index
live "/champions/new", ChampionLive.Index, :new
live "/champions/:id/edit", ChampionLive.Index, :edit
live "/champions/:id", ChampionLive.Show, :show
live "/champions/:id/show/edit", ChampionLive.Show, :edit
get "/", PageController, :home
end
# Other scopes may use custom stacks.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

@@ -1,5 +0,0 @@
# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
#
# To ban all spiders from the entire site uncomment the next two lines:
# User-agent: *
# Disallow: /
Binary file not shown.
@@ -1,108 +0,0 @@
defmodule LoLAnalyticsWeb.ChampionLiveTest do
use LoLAnalyticsWeb.ConnCase
import Phoenix.LiveViewTest
import LoLAnalytics.AccountsFixtures
@create_attrs %{}
@update_attrs %{}
@invalid_attrs %{}
defp create_champion(_) do
champion = champion_fixture()
%{champion: champion}
end
describe "Index" do
setup [:create_champion]
test "lists all champions", %{conn: conn} do
{:ok, _index_live, html} = live(conn, ~p"/champions")
assert html =~ "Listing Champions"
end
test "saves new champion", %{conn: conn} do
{:ok, index_live, _html} = live(conn, ~p"/champions")
assert index_live |> element("a", "New Champion") |> render_click() =~
"New Champion"
assert_patch(index_live, ~p"/champions/new")
assert index_live
|> form("#champion-form", champion: @invalid_attrs)
|> render_change() =~ "can&#39;t be blank"
assert index_live
|> form("#champion-form", champion: @create_attrs)
|> render_submit()
assert_patch(index_live, ~p"/champions")
html = render(index_live)
assert html =~ "Champion created successfully"
end
test "updates champion in listing", %{conn: conn, champion: champion} do
{:ok, index_live, _html} = live(conn, ~p"/champions")
assert index_live |> element("#champions-#{champion.id} a", "Edit") |> render_click() =~
"Edit Champion"
assert_patch(index_live, ~p"/champions/#{champion}/edit")
assert index_live
|> form("#champion-form", champion: @invalid_attrs)
|> render_change() =~ "can&#39;t be blank"
assert index_live
|> form("#champion-form", champion: @update_attrs)
|> render_submit()
assert_patch(index_live, ~p"/champions")
html = render(index_live)
assert html =~ "Champion updated successfully"
end
test "deletes champion in listing", %{conn: conn, champion: champion} do
{:ok, index_live, _html} = live(conn, ~p"/champions")
assert index_live |> element("#champions-#{champion.id} a", "Delete") |> render_click()
refute has_element?(index_live, "#champions-#{champion.id}")
end
end
describe "Show" do
setup [:create_champion]
test "displays champion", %{conn: conn, champion: champion} do
{:ok, _show_live, html} = live(conn, ~p"/champions/#{champion}")
assert html =~ "Show Champion"
end
test "updates champion within modal", %{conn: conn, champion: champion} do
{:ok, show_live, _html} = live(conn, ~p"/champions/#{champion}")
assert show_live |> element("a", "Edit") |> render_click() =~
"Edit Champion"
assert_patch(show_live, ~p"/champions/#{champion}/show/edit")
assert show_live
|> form("#champion-form", champion: @invalid_attrs)
|> render_change() =~ "can&#39;t be blank"
assert show_live
|> form("#champion-form", champion: @update_attrs)
|> render_submit()
assert_patch(show_live, ~p"/champions/#{champion}")
html = render(show_live)
assert html =~ "Champion updated successfully"
end
end
end
@@ -1,108 +0,0 @@
defmodule LoLAnalyticsWeb.RoleLiveTest do
use LoLAnalyticsWeb.ConnCase
import Phoenix.LiveViewTest
import LoLAnalytics.AccountsFixtures
@create_attrs %{}
@update_attrs %{}
@invalid_attrs %{}
defp create_role(_) do
role = role_fixture()
%{role: role}
end
describe "Index" do
setup [:create_role]
test "lists all roles", %{conn: conn} do
{:ok, _index_live, html} = live(conn, ~p"/roles")
assert html =~ "Listing Roles"
end
test "saves new role", %{conn: conn} do
{:ok, index_live, _html} = live(conn, ~p"/roles")
assert index_live |> element("a", "New Role") |> render_click() =~
"New Role"
assert_patch(index_live, ~p"/roles/new")
assert index_live
|> form("#role-form", role: @invalid_attrs)
|> render_change() =~ "can&#39;t be blank"
assert index_live
|> form("#role-form", role: @create_attrs)
|> render_submit()
assert_patch(index_live, ~p"/roles")
html = render(index_live)
assert html =~ "Role created successfully"
end
test "updates role in listing", %{conn: conn, role: role} do
{:ok, index_live, _html} = live(conn, ~p"/roles")
assert index_live |> element("#roles-#{role.id} a", "Edit") |> render_click() =~
"Edit Role"
assert_patch(index_live, ~p"/roles/#{role}/edit")
assert index_live
|> form("#role-form", role: @invalid_attrs)
|> render_change() =~ "can&#39;t be blank"
assert index_live
|> form("#role-form", role: @update_attrs)
|> render_submit()
assert_patch(index_live, ~p"/roles")
html = render(index_live)
assert html =~ "Role updated successfully"
end
test "deletes role in listing", %{conn: conn, role: role} do
{:ok, index_live, _html} = live(conn, ~p"/roles")
assert index_live |> element("#roles-#{role.id} a", "Delete") |> render_click()
refute has_element?(index_live, "#roles-#{role.id}")
end
end
describe "Show" do
setup [:create_role]
test "displays role", %{conn: conn, role: role} do
{:ok, _show_live, html} = live(conn, ~p"/roles/#{role}")
assert html =~ "Show Role"
end
test "updates role within modal", %{conn: conn, role: role} do
{:ok, show_live, _html} = live(conn, ~p"/roles/#{role}")
assert show_live |> element("a", "Edit") |> render_click() =~
"Edit Role"
assert_patch(show_live, ~p"/roles/#{role}/show/edit")
assert show_live
|> form("#role-form", role: @invalid_attrs)
|> render_change() =~ "can&#39;t be blank"
assert show_live
|> form("#role-form", role: @update_attrs)
|> render_submit()
assert_patch(show_live, ~p"/roles/#{role}")
html = render(show_live)
assert html =~ "Role updated successfully"
end
end
end
+1 -1
View File
@@ -18,7 +18,7 @@ defmodule LoLAPI.AccountApi do
200 ->
{:ok, Poison.decode(response.body)}
_code ->
code ->
Logger.error("Error getting puuid from player #{name} \##{tag}")
{:err, response.status_code}
end

Some files were not shown because too many files have changed in this diff Show More