Compare commits

..

4 Commits

Author SHA1 Message Date
e9064f9b61 Update README.md 2025-07-15 19:41:28 +02:00
33410e68ed Reestrcture and new graph library 2025-07-15 19:17:51 +02:00
9cf756ffbd Update README.md
Added idea for charts
2025-07-08 15:46:23 +02:00
28720bfcd5 graph tests 2025-07-07 21:55:07 +02:00
12 changed files with 879 additions and 689 deletions

View File

@@ -1,59 +0,0 @@
name: Compile and test using leiningen
run-name: ${{ gitea.actor }} testing the code
on: [push]
jobs:
clojure:
name: Run tests
runs-on: ubuntu-latest
steps:
# Checkouts code
- name: Checkout
uses: actions/checkout@v3
# Install Java
- name: Install java
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '21'
# Install Leiningen
- name: Install Leiningen
run: |
curl https://raw.githubusercontent.com/technomancy/leiningen/stable/bin/lein > lein
chmod +x lein
sudo mv lein /usr/local/bin/lein
# Install dependencies
- name: Install dependencies
run: lein deps
# Optional: cache dependencies
- name: Cache dependencias
uses: actions/cache@v4
with:
path: ~/.m2/repository
key: ${{ runner.os }}-m2-${{ hashFiles('**/project.clj') }}
restore-keys: |
${{ runner.os }}-m2-
# Get leiningen's version
- name: Get leiningen version
run: lein -v
# Test the code
- name: Run tests
env:
TFT_API: ${{ secrets.DEV_API }}
run: lein test
# Send jar to repository
- name: Deploy on Gitea Maven
if: github.ref == 'refs/heads/main'
env:
GITEA_USER: ${{ secrets.DEPLOY_USER }}
GITEA_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
run: |
lein deploy gitea

2
.gitignore vendored
View File

@@ -14,5 +14,3 @@ pom.xml.asc
.clj-kondo
.lsp
.calva
*.svg
/logs

130
README.md
View File

@@ -1,53 +1,22 @@
# riot-clojure
This is a simple tool for using Riot Games public API. Those APIs are HUGE and
provide a lot of information, but this tool is focused in the time data of matches.
privide a lot of information, but this tool is focused in the time data of matches.
The objetive of this tool is extract the playing time of one player. Taking the
starting and ending timestamps we can create a chronogram with the time spent
starting and ending timestams we can create a chronogram with the time spent
playing.
This simple program needs two API keys: one for LOL and another one for TFT. You
can obtain a developer key from https://developer.riotgames.com/. Developer keys
are valid for 24 hours only, but you can refresh the key easily from the developer's
web site.
This simple program needs a developer API. You can obtain a key from https://developer.riotgames.com/
Developer keys are valid for 24 hours only, but you can refresh the key easily
from the developer's web site.
By default, the application searchs for the API key in the environment variables
`LOL_API` and `TFT_API`. If you prefer to provide the key via parameter, you must use `--lol-api-key` or
`--tft-api-key` parameters.
## Features
Those features are implemented in v1.1
* Call the API with development or production keys
* Get a list of matches
* Get matches between two dates
* Check if player is currently playing
* Show the match's data in several formats:
* Fancy ASCII table (by default)
* Simple ASCII table
* Clojure native EDN format
* JSON
* CSV
* Select data fields to show
* Format dates and durations
* Show the result of the match (win/loss)
* Bulk request for hundred of matches
* Generate native executables
By default, the application searchs for the API key in the environment variable
`RIOT_API`. If you prefer to provide the key via parameter, you must use `-k KEY` or
`--key=KEY`.
## Goals
Goals for version 1.1:
* [x] Simplify CLI options and parameters
* [x] Calculate simple statistics: win/loss rate, hours played every day, etc.
* [x] Get more data and delete some unused fields
* [x] Clean and refactor some code
* [x] Logging
* [x] Get only LOL or TFT data
* [x] Use Gitea Actions
Goals of the project, ordered from easiest to hardest:
* [x] Take API key from environment
@@ -70,7 +39,7 @@ Goals of the project, ordered from easiest to hardest:
* [x] Win / Loss match
* [x] Distribution as uberjar
* [x] Distribution as native Linux executable
* [x] Unit tests: move "comment" manual tests to real unit tests
* [ ] Unit tests: move "comment" manual tests to real unit tests
* [ ] Output as a graphical chronogram or calendar.
* [ ] Simple web server
* [ ] Universal player search
@@ -80,39 +49,88 @@ Goals of the project, ordered from easiest to hardest:
## Installation
Download release from https://git.rcorral.es/ruben/riot-clojure/releases.
Download from http://example.com/FIXME.
## Usage
Using the Java uberjar
FIXME: explanation
$ java -jar riot-clojure-0.1.0-standalone.jar [args]
Using the linux native image:
$ ./riot [args]
Both are equivalent. In the examples we will use the native image, because is shorter.
## Options
Run the application without params or wich `-?` param to show all options:
$ ./riot -?
FIXME: listing of options this app accepts.
## Examples
Show all matches from a march the 1st (in ISO format):
...
$ ./riot t <username> <tag> -s "2025-03-01"
### Bugs
Get te same data in CSV and store it in a file:
...
$ ./riot t <username> <tag> -s "2025-03-01" -o csv > results.csv
### Graphs
Don't format durations, show them in seconds
Posible ASCII art charts
$ ./riot t <username> <tag> -s "2025-03-01" --no-format-durations
#### Simple day
A one dimension diagram. Draw a tick each day the player has played
```
####### ### ###### ###########
------------------------------------------------------------------------------------------
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
june
```
#### Day and hour
The x axis represents days of month, while y axis represents hours of each day. A tick is drawed in the hour
when the player was playing.
```
23 |
22 |
21 |
20 |
19 | # #
18 | # # #
17 | # # # #
16 | #
15 | # # # #
14 | # # # #
13 | #
12 | # # # #
11 | # # # # #
10 | # # # #
09 | # #
08 |
07 |
06 |
05 |
04 |
03 |
02 |
01 |
00 |
------------------------------------------------------------------------------------------
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
june
```
## Interesting graphic and UI libraries
- Raster SVG: https://github.com/soulspace-org/cmp.batik
- Modify SVG graphics: https://github.com/stathissideris/dali
- Create Vega-lite SVG graphics: https://github.com/techascent/tech.viz
- Enhance CLI text: https://github.com/clj-commons/pretty?tab=readme-ov-file
- Universal UI: https://github.com/phronmophobic/membrane?tab=readme-ov-file
- Interesting libraries: https://www.clojure-toolbox.com/
## License

View File

@@ -1,26 +1,33 @@
(defproject riot-clojure "1.1.0"
(defproject riot-clojure "1.0.0"
:description "Utility for getting for Riot APIs in Clojure"
:url "https://git.rcorral.es/ruben/riot-clojure"
:license {:name "MIT"
:url "https://mit-license.org/"}
:dependencies [[org.clojure/clojure "1.11.1"]
[clj-http/clj-http "2.0.0"]
[cheshire/cheshire "6.0.0"]
[slingshot/slingshot "0.12.2"]
[org.clojure/tools.cli "1.1.230"]
[cli-matic/cli-matic "0.5.4"] ;; https://github.com/l3nz/cli-matic
[buddy/buddy-core "1.12.0-430"]
[org.clojure/tools.namespace "1.5.0"]
[lt.tokenmill/timewords "0.5.0"] ;; https://github.com/tokenmill/timewords
[org.clj-commons/pretty "3.5.0"] ;; https://github.com/clj-commons/pretty
[org.clojure/tools.logging "1.3.0"] ;; https://github.com/clojure/tools.logging
[org.slf4j/slf4j-api "2.0.17"] ;; https://www.slf4j.org/
[ch.qos.logback/logback-classic "1.5.18"]]
:jvm-opts ["-Dclojure.tools.logging.factory=clojure.tools.logging.impl/slf4j-factory"]
:dependencies [[org.clojure/clojure "1.11.1"] ;; Clojure itself
[clj-http/clj-http "2.0.0"] ;; HTTP client
[cheshire/cheshire "6.0.0"] ;; Convert to/from JSON
[slingshot/slingshot "0.12.2"] ;; Better exceptions for clj-http
;; [org.clojure/tools.cli "1.1.230"] ;; Parse params. NOT USED
[cli-matic/cli-matic "0.5.4"] ;; Parse params and make a nice CLI app
[buddy/buddy-core "1.12.0-430"] ;; Cipher data
[org.clojure/tools.namespace "1.5.0"] ;; Hot reload from REPL
[lt.tokenmill/timewords "0.5.0"] ;; Parse dates and times in natural languaje
:plugins [[io.taylorwood/lein-native-image "0.3.1"]]
;;; LIBS FOR GRAPHS, SOME OF THEM WILL BE REMOVED IN THE FUTURE
;[com.hypirion/clj-xchart "0.2.0"] ;; Graphs and charts
;[aerial.hanami "0.15.1"] ;; Parse vega-lite data and generate graphics
;[folcon/oz "1.6.0-alpha6.2"] ;; Parse vega and vega-lite
[metasoarous/oz "1.6.0-alpha36"] ;; Other version of Oz
;[metasoarous/oz "2.0.0-alpha5"] ;; Newer version of Oz
[incanter/incanter-charts "1.9.3"] ;; graphics with Incanter
;[hswick/jutsu "0.1.1"] ;; Wrapper para plotly
[techascent/tech.viz "6.00-beta-16-4"] ;; Vega-lite parser (https://github.com/techascent/tech.viz)
]
:plugins [[io.taylorwood/lein-native-image "0.3.1"]] ;; Compile to native using graal
:main ^:skip-aot riot.app
:target-path "target/%s"
@@ -32,22 +39,4 @@
:profiles {:uberjar {:aot :all
:jvm-opts ["-Dclojure.compiler.direct-linking=true"]}
:native-image {:jvm-opts ["-Dclojure.compiler.direct-linking=true"]}
:dev {:dependencies [[org.clojure/test.check "1.1.1"]]
:plugins [[io.taylorwood/lein-native-image "0.3.1"]
[lein-binplus "0.6.8"]]}}
;; lein bin configuration
:bin {:name "riot_clj"
:bin-path "~/bin"
:jvm-opts ["-server" "-Dfile.encoding=utf-8" "$JVM_OPTS"]}
;; Deploy to repository
:repositories {"gitea"
{:url "https://git.rcorral.es/api/packages/ruben/maven"
:username :env/DEPLOY_USER
:password :env/DEPLOY_TOKEN}}
;; Test selectors
:test-selectors {:default (complement (some-fn :tft :timezone))
:tft :tft
:timezone :timezone})
:dev {:dependencies [[org.clojure/test.check "1.1.1"]]}})

View File

@@ -1,36 +0,0 @@
<configuration>
<!-- Appender a consola -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<encoder>
<!--<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>-->
<pattern>[%-5level] %msg%n</pattern>
</encoder>
</appender>
<!-- Appender a archivo con rotación -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/riot-app.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- rota cada día, conserva 7 días -->
<fileNamePattern>logs/riot-app.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>7</maxHistory>
</rollingPolicy>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>DEBUG</level>
</filter>
<encoder>
<pattern>%d{ISO8601} %-5level %logger{36}:%L - %msg%n</pattern>
</encoder>
</appender>
<!-- Nivel de log raíz -->
<root level="DEBUG">
<appender-ref ref="STDOUT" />
<appender-ref ref="FILE" />
</root>
</configuration>

View File

@@ -6,9 +6,7 @@
[cli-matic.utils :as U]
[clojure.pprint :as pp]
[clojure.string :as str]
[timewords.core :refer [parse]]
[clojure.tools.logging :as log]
)
[timewords.core :refer [parse]])
(:use [riot.core]
[riot.data]
[slingshot.slingshot :only [try+]])
@@ -24,141 +22,179 @@
(some? puuid) puuid
(and (some? name) (some? tag)) (get-puuid-from-name name tag :api-key api-key)))
(comment
(get-puuid-from-params (get-lol-api-key) [] :puuid "annlAfxhJnTwlwRgQZYbGeQpD3jWb-ju7vVKEW_g-EIJf6xQT0eeb-0obARVekrksf8n9XCjcxyHHQ")
(get-puuid-from-params (get-lol-api-key) [] :name "Errepunto" :tag "4595")
(get-puuid-from-params (get-tft-api-key) [] :puuid "annlAfxhJnTwlwRgQZYbGeQpD3jWb-ju7vVKEW_g-EIJf6xQT0eeb-0obARVekrksf8n9XCjcxyHHQ")
(get-puuid-from-params (get-tft-api-key) [] :name "Errepunto" :tag "4595")
)
#_{:clj-kondo/ignore [:unresolved-symbol]}
(defn cmd-active
"Checks if a player is online at the moment"
[& {:keys [lol-api-key tft-api-key debug-http lol tft _arguments]
[& {:keys [lol-api-key tft-api-key debug-http _arguments]
:as opts}]
(try+
;(println "Check online" opts)
(let [lol-key (when lol (get-lol-api-key lol-api-key))
tft-key (when tft (get-tft-api-key tft-api-key))
lol-id (when lol (get-puuid-from-params lol-key _arguments opts))
tft-id (when tft (get-puuid-from-params tft-key _arguments opts))]
(if (and (nil? lol-id) (nil? tft-id))
(println "No games to check")
(let [lol-key (get-lol-api-key lol-api-key)
tft-key (get-tft-api-key tft-api-key)
lol-id (get-puuid-from-params lol-key _arguments opts)
tft-id (get-puuid-from-params tft-key _arguments opts)]
;(println "LOL API key" lol-key "TFT API key" tft-key)
(if (or (nil? lol-id) (nil? tft-id))
(U/exit! "Invalid params" 2)
(if (is-playing? lol-id tft-id :lol-api-key lol-key :tft-api-key tft-key :debug debug-http)
(do (println "Yes, it's playing") true)
(do (println "No, it's not playing right now") false))))
(println "Yes, it's playing")
(println "No, it's not playing right now"))))
(catch [:status 401] _
(U/exit! "Invalid API key" 3))
(catch [:status 404] _
(U/exit! "Unknown user or tag" 5))))
(comment
(cmd-active :_arguments ["annlAfxhJnTwlwRgQZYbGeQpD3jWb-ju7vVKEW_g-EIJf6xQT0eeb-0obARVekrksf8n9XCjcxyHHQ"])
(cmd-active :_arguments ["Errepunto" "4595"])
)
(defn format-result
(defn format-map
"Format result"
[output x]
(case output
"table" (as-ascii-table x)
"ptable" (as-pretty-table x)
"edn" (pp/pprint x)
"json" (println (as-json x))
"table" (as-ascii-table x)
"csv" (println (as-csv x))
(pp/pprint x))) ;edn as default
(comment
(format-map "json" riot.test-examples/matches-example)
(format-map "csv" riot.test-examples/matches-example2))
(defn format-dates-cond
"Format dates and durations if specified"
[format-dates pattern x]
[format-dates format-durations pattern x]
(let [dur (if format-durations
(with-parsed-durations x)
x)]
(if format-dates
(if (some? pattern)
(with-parsed-dates x :datetime-format pattern)
(with-parsed-dates x))
x))
(with-parsed-dates dur :datetime-format pattern)
(with-parsed-dates dur))
dur)))
(comment
(format-dates-cond false false nil riot.test-examples/matches-example2)
(format-dates-cond false true nil riot.test-examples/matches-example2)
(format-dates-cond true false nil riot.test-examples/matches-example2)
(format-dates-cond true true nil riot.test-examples/matches-example2))
(defn format-duration-cond
"Format durations if specified"
[format-durations x]
(if format-durations
(with-parsed-durations x)
x))
(defn calculate-statistics-cond
"If true, calculate statistics and print them to console"
[calculate x]
(when calculate
(log/info "Calculating statistics")
(pp/pprint (calculate-statistics x)))
x)
(defn reverse-cond
"If true, data is ordered from nearest to fartest"
[rev x] (if rev x (reverse x)))
(defn filter-today-cond
[only-today x] (if only-today (filter match-today? x) x))
(defn filter-columns
"Filter columns to show. With 'all' shows all columns"
[columns x] (if (= "all" columns)
x
(map #(select-keys % (map keyword (str/split columns #","))) x)))
(comment
(filter-columns "all" riot.test-examples/matches-example2)
(filter-columns "start,end" riot.test-examples/matches-example2)
(map #(select-keys % (map keyword (str/split "start,end" #","))) riot.test-examples/matches-example2))
(defn get-date
"Converts an string into a java.util.Date"
[x]
(if (nil? x) nil (parse x)))
#_{:clj-kondo/ignore [:unresolved-symbol]}
(defn cmd-timeline
"Get all matches"
[& {:keys [lol-api-key tft-api-key lol tft output format-dates format-durations date-format reverse order-by show-columns debug-http since until statistics _arguments]
"Get match "
[& {:keys [lol-api-key tft-api-key output count max-total format-dates format-durations date-format reverse order-by show-columns show-active debug-http only-today since until _arguments]
:as opts}]
(try+
;(println "Get timeline" opts)
(let [lol-key (when lol (get-lol-api-key lol-api-key))
tft-key (when tft (get-tft-api-key tft-api-key))
lol-id (when lol (get-puuid-from-params lol-key _arguments opts))
tft-id (when tft (get-puuid-from-params tft-key _arguments opts))
since-date (get-date since)
until-date (get-date until)]
(log/info (str "Fetching data between " since-date " and " (if (some? until-date) until-date (java.util.Date.))))
(if (and (nil? lol-id) (nil? tft-id))
(U/exit! "No games selected" 2)
(->> (get-matches-info-batch lol-id tft-id :lol-api-key lol-key :tft-api-key tft-key :debug debug-http :order-by (keyword order-by) :since since-date :until until-date)
(calculate-statistics-cond statistics)
(let [lol-key (get-lol-api-key lol-api-key)
tft-key (get-tft-api-key tft-api-key)
lol-id (get-puuid-from-params lol-key _arguments opts)
tft-id (get-puuid-from-params tft-key _arguments opts)]
;(println "LOL API key" lol-key "TFT API key" tft-key)
(if (or (nil? lol-id) (nil? tft-id))
(U/exit! "Invalid params" 2)
(->> (get-last-matches lol-id tft-id :lol-api-key lol-key :tft-api-key tft-key :count count :include-current show-active :debug debug-http :order-by (keyword order-by) :since (get-date since) :until (get-date until))
(filter-today-cond only-today)
(reverse-cond reverse)
(format-dates-cond format-dates date-format)
(format-duration-cond format-durations)
(take count)
(format-dates-cond format-dates format-durations date-format)
(filter-columns show-columns)
(format-result output))))
(map )
(format-map output))))
(catch [:status 401] _
(U/exit! "Invalid API key" 3))
(catch [:status 404] _
(U/exit! "Unknown user or tag" 5))
(catch [:status 429] _
(U/exit! "Rate limit exceeded, please wait some minutes and try again" 4))))
(comment
(cmd-timeline :_arguments ["dMNR4Aj5OW9jrGj0RUVBRuzfu77p3iO5y1W16ASNp1PI7pxuJWLz14b2pJiUn16DCPlyeREoi0MJ7Q"])
(cmd-timeline :_arguments ["Sarinailo" "EUW"]))
(defn cmd-bulk
"Get all matches"
[& {:keys [lol-api-key tft-api-key output format-dates format-durations date-format reverse order-by show-columns debug-http since until _arguments]
:as opts}]
(try+
;(println "Get timeline" opts)
(let [lol-key (get-lol-api-key lol-api-key)
tft-key (get-tft-api-key tft-api-key)
lol-id (get-puuid-from-params lol-key _arguments opts)
tft-id (get-puuid-from-params tft-key _arguments opts)]
;(println "LOL API key" lol-key "TFT API key" tft-key)
(if (or (nil? lol-id) (nil? tft-id))
(U/exit! "Invalid params" 2)
(->> (get-matches-info-batch lol-id tft-id :lol-api-key lol-key :tft-api-key tft-key :debug debug-http :order-by (keyword order-by) :since (get-date since) :until (get-date until))
(reverse-cond reverse)
(format-dates-cond format-dates format-durations date-format)
(filter-columns show-columns)
(format-map output))))
(catch [:status 401] _
(U/exit! "Invalid API key" 3))
(catch [:status 404] _
(U/exit! "Unknown user or tag" 5))
(catch [:status 429] _
(U/exit! "Rate limit exceeded, please wait some minutes and try again" 4))
(catch [:status 502] _
(U/exit! "Server timeout, please wait some minutes and try again" 6))
(catch [:status 503] _
(U/exit! "Server timeout, please wait some minutes and try again" 6))))
(U/exit! "Server timeout, please wait some minutes and try again" 6))
))
;; More info: https://github.com/l3nz/cli-matic/blob/master/README.md
(def CONFIGURATION
{:app {:command "riot"
:version "1.1.0"
:description ["Get how much you play your favourite games"
:description ["Get how much time have you spent on LoL or FTF"
""
"EXAMPLE USAGES:"
" Get all matches since March 1st 2025:"
" riot timeline <your_player_name> <your_tag> -s 2025-03-01"
""
" Get all matches as CSV, don't format durations:"
" riot timeline <your_player_name> <your_tag> -s 2025-03-01 -o csv --no-format-durations > my_matches.csv"
""
"ERROR CODES:"
" 0: OK, no error"
"Posible error codes:"
" 0: OK"
" 1: player not playing"
" 2: Invalid params"
" 3: Invalid API key, please, get and configure an updated one"
" 3: Invalid API key, please, get an updated one"
" 4: Rate limit exceeded, please wait some minutes and try again"
" 5: Unknown user and tag"
" 6: Server error. Please, try again"]}
" 6: Server error. Please, try again"]
:version "1.0.0"}
:global-opts [{:option "lol-api-key"
:as "API key for LOL data"
:type :string :default nil
@@ -170,49 +206,114 @@
{:option "debug-http"
:as "Show debug for HTTP connections. I'ts very verbose!"
:type :flag
:default false}
;; {:option "log-level"
;; :as "Log level (debug, info, warn, etc)"
;; :type :keyword
;; :default :info}
]
:default false}]
:commands [{:command "active" :short "a"
:description ["Shows if the player is currently playing"
"By default only shows LOL matches"]
:opts [{:option "lol"
:type :with-flag :default true
:as "Include LOL matches"}
{:option "tft"
:type :with-flag :default false
:as "Include TFT matches. Experimental"}]
:runs cmd-active}
{:command "timeline" :short "t"
:description ["Get info from LOL and TFT (experimental, disabled by default) matches in a time range."
:description ["Shows if the player currently playing"
"You can search by PUUID or name and tagline, but not both."
""
"Warning: it can take several minutes to obtain all data, due to API restrictions."]
:opts [{:option "lol"
:type :with-flag :default true
:as "Include LOL matches"}
{:option "tft"
"You can set the PUUID passing only one argument or using -p param."
"You can set the name and tag passing two arguments, or using -n and -t."]
:opts [{:option "puuid" :short "p"
:type :string
:as "Summonner's unique PUUID."}
{:option "name" :short "n"
:type :string :default nil
:as "Summoner name (Riot ID)"}
{:option "tag" :short "t"
:type :string :default nil
:as "Summoner tagline"}]
:runs cmd-active}
{:command "last" :short "l"
:description ["Gets time info from last matches."
"You can search by PUUID or name and tagline, but not both."
""
"You can set the PUUID passing only one argument or using -p param."
"You can set the name and tag passing two arguments, or using -n and -t."]
:opts [{:option "puuid" :short "p"
:type :string
:as "Summonner's unique PUUID."}
{:option "name" :short "n"
:type :string :default nil
:as "Summoner name (Riot ID)"}
{:option "tag" :short "t"
:type :string :default nil
:as "Summoner tagline"}
{:option "output" :short "o"
:type #{"edn" "json" "table" "csv"} :default "table"
:as ["Output type. It can be one of:"
" - edn: Clojure's internal EDN format"
" - json: Classic JSON"
" - table: ASCII table (by default)"
" - csv: CSV data, using ',' as field separator"]}
{:option "count" :short "c"
:type :int :default 10
:as "Max number of matches retrieved for each type of game (max value: 100)"}
;{:option "max-total" :short "m"
; :type :int :default 10
; :as "Max number of matches of all types, including current match"}
{:option "order-by"
:type #{"start" "end" "duration"} :default "start"
:as "Order by field"}
{:option "reverse" :short "r"
:type :with-flag :default false
:as "Include TFT matches. Experimental"}
:as "If true, newer matches are shown last in list"}
{:option "show-active" :short "a"
:type :with-flag :default true
:as "Include current playing match"}
{:option "format-dates"
:type :with-flag :default true
:as "Show formatted dates"}
{:option "format-durations"
:type :with-flag :default true
:as "Show formatted durations"}
{:option "date-format"
:type :string :default nil
:as "If format-dates is enabled, specifies the formatter's string"}
{:option "show-columns"
:type :string :default "all"
:as ["Show only selected columns, separated by comma. Supported columns:"
" - all: All columns"
" - start: Start timestamp"
" - end: End timestamp"
" - duration: Duration in seconds"
" - active: Match is in course"
" - game-type: lol or tft"
" - id: Game unique id"
" - winner: Winner of the match"]}
{:option "only-today"
:type :flag :default false
:as "If true, only shows today played matches"}
{:option "since" :short "s"
:type :string :default "today"
:type :string :default nil
:as ["Starting date (included) in 'yyyy-MM-dd' format."
"Other valid values: 'today', 'yesterday', 'last monday', etc"]}
"Other valid values: today, yesterday"]}
{:option "until" :short "u"
:type :string :default nil
:as ["Ending date (not included) in 'yyyy-MM-dd' format."
"Other valid values: today, yesterday, tomorrow, etc"]}
"Other valid values: today, yesterday, tomorrow"]}]
:runs cmd-timeline}
{:command "timeline" :short "t"
:description ["Get info from LOL and TFT matches in a time range."
"Warning: it can take several minutes to obtain all data, due to api restrictions."
"You can search by PUUID or name and tagline, but not both."
""
"You can set the PUUID passing only one argument or using -p param."
"You can set the name and tag passing two arguments, or using -n and -t."]
:opts [{:option "puuid" :short "p"
:type :string
:as "Summonner's unique PUUID."}
{:option "name" :short "n"
:type :string :default nil
:as "Summoner name (Riot ID)"}
{:option "tag" :short "t"
:type :string :default nil
:as "Summoner tagline"}
{:option "output" :short "o"
:type #{"table" "ptable" "edn" "json" "csv"} :default "ptable"
:as ["Output data format. It can be one of:"
" - table: ASCII table"
" - ptable: Pretty ASCII table (default)"
:type #{"edn" "json" "table" "csv"} :default "table"
:as ["Output type. It can be one of:"
" - edn: Clojure's internal EDN format"
" - json: Classic JSON"
" - table: ASCII table (by default)"
" - csv: CSV data, using ',' as field separator"]}
{:option "order-by"
:type #{"start" "end" "duration"} :default "start"
@@ -239,22 +340,51 @@
" - game-type: lol or tft"
" - id: Game unique id"
" - winner: Winner of the match"]}
{:option "statistics"
:type :with-flag :default true
:as "Show simple statistics"}]
:runs cmd-timeline}]})
{:option "only-today"
:type :flag :default false
:as "If true, only shows today played matches"}
{:option "since" :short "s"
:type :string :default nil
:as ["Starting date (included) in 'yyyy-MM-dd' format."
"Other valid values: today, yesterday, etc"]}
{:option "until" :short "u"
:type :string :default nil
:as ["Ending date (not included) in 'yyyy-MM-dd' format."
"Other valid values: today, yesterday, tomorrow, etc"]}]
:runs cmd-bulk}]})
(defn -main
"Main entry point"
[& args]
(log/debug "")
(log/debug "** Starting application **")
(log/debug "")
(run-cmd args CONFIGURATION))
(defn old-main
"Testing some basic things"
[& args]
(let [player "RdNVioNKYzvFuXI00zurL_8QvaU0E8P61NU0SzwIfbbHHk9HFvxLtSWiDHKuJ9iXb4UC0UUZ3ltLxw"]
;(println "API Key: " (get-lol-api-key))
(println "Last matches:")
(println "Player PUUID: " player)
;(as-json
;(as-ascii-table
; (with-parsed-dates
; (take 10
; (reverse
; (get-last-matches player :count 10 :print-not-active true)))))
(as-ascii-table
(with-parsed-dates-durations
; (filter match-today? ; Only today
(reverse (get-last-matches player :count 10 :print-not-active true))))
;)
))
(comment
; Launch manually
(-main)
(old-main)
)

View File

@@ -4,9 +4,7 @@
(ns riot.core
(:require [clj-http.client :as client]
[buddy.core.crypto :as crypto]
[buddy.core.codecs :as codecs]
[clojure.tools.logging :as log]
)
[buddy.core.codecs :as codecs])
(:use [slingshot.slingshot :only [try+]])
(:gen-class))
@@ -35,7 +33,7 @@
;;;; API KEYS
(def DEV_KEY "RGAPI-ec3779d1-bc61-4a2e-a071-34addcc6bd56")
(def DEV_KEY "RGAPI-e068aea3-5828-48e2-9500-259ef96c8c4f")
(def LOL_KEY "jXL+gA3LIeBPBvrOhLOYSZCiURC7eOtwMXahkxtwpdj6JDtT5NMu25zMz+UY2+9MuHBADjUJh46jSanrV5OBag==")
(def TFT_KEY (encrypt-data DEV_KEY secret-key iv))
@@ -104,54 +102,63 @@
[data response puuid]
(assoc data :winner (winner? response puuid)))
(comment
(get-in riot.test-examples/response-lol-match [:metadata :participants])
(get-in riot.test-examples/response-lol-match [:info :participants 3])
(get-player-info riot.test-examples/response-lol-match riot.test-examples/example-lol-puuid)
(winner? riot.test-examples/response-lol-match riot.test-examples/example-lol-puuid)
)
(defn with-winner-status
"Takes the original list of matches and adds winner info"
"Takes the original list of matches and parses durations"
[matches puuid]
(map #(assoc % :winner (winner? % puuid)) matches))
(comment
(with-winner-status riot.test-examples/matches-example riot.test-examples/example-lol-puuid)
)
;; json adapters
(def player-info-parser {:parser [(make-json-data [:puuid] :puuid)]})
(def lol-match-parser {:parser [(make-json-data [:info :gameCreation] :start)
(make-json-data [:info :gameEndTimestamp] :end)
(make-json-data [:info :gameDuration] :duration)
;; (make-json-data [:none] :active (constantly false))
(make-json-data [:none] :active (constantly false))
(make-json-data [:none] :game-type (constantly "lol"))
(make-json-data [:metadata :matchId] :id)
(make-json-data [:none] :winner (constantly false))
(make-json-data [:info :endOfGameResult] :result)]
(make-json-data [:none] :winner (constantly false))]
:post post-calculate-win})
(def tft-match-parser {:parser [(make-json-data [:info :gameCreation] :start)
(make-json-data [:none] :end (constantly -1))
(make-json-data [:info :game_length] :duration #(int %)) ; rounds to integer
;; (make-json-data [:none] :active (constantly false))
(make-json-data [:none] :game-type (constantly "tft"))
(make-json-data [:none] :active (constantly false))
(make-json-data [:none] :game-type (constantly "ftf"))
(make-json-data [:metadata :match_id] :id)
(make-json-data [:none] :winner (constantly false))
(make-json-data [:info :endOfGameResult] :result)]
(make-json-data [:none] :winner (constantly false))]
:post #(post-calculate-win
(post-calculate-end %1 %2 %3) %2 %3)}) ; end = start + (duration * 1000)
(def lol-current-parser {:parser [(make-json-data [:gameStartTime] :start)
(make-json-data [:none] :end (constantly nil))
(make-json-data [:gameLength] :duration)
;; (make-json-data [:none] :active (constantly true))
(make-json-data [:none] :active (constantly true))
(make-json-data [:none] :game-type (constantly "lol"))
(make-json-data [:gameId] :id)
(make-json-data [:none] :winner (constantly nil))
(make-json-data [:none] :result (constantly nil))]})
(make-json-data [:none] :winner (constantly nil))]})
(def tft-current-parser {:parser [(make-json-data [:gameStartTime] :start)
(make-json-data [:none] :end (constantly nil))
(make-json-data [:gameLength] :duration)
;; (make-json-data [:none] :active (constantly true))
(make-json-data [:none] :game-type (constantly "tft"))
(make-json-data [:none] :active (constantly true))
(make-json-data [:none] :game-type (constantly "ftf"))
(make-json-data [:gameId] :id)
(make-json-data [:none] :winner (constantly nil))
(make-json-data [:none] :result (constantly nil))]})
(make-json-data [:none] :winner (constantly nil))]})
(defn parse-response
@@ -181,7 +188,6 @@
:or {params nil
debug false}}]
(when debug (println "** SENDING REQUEST **"))
(log/trace "Sending request. Params: " params)
;; (println "Params: " params)
(:body (client/get url
{:debug debug
@@ -238,10 +244,8 @@
debug false
server "europe"
count 20
start 0}
:as params}]
start 0}}]
;; (println "get lol start: " start)
(log/trace "Getting list of LOL matches. Params: " params)
(raw-get-query
(create-endpoint server "/lol/match/v5/matches/by-puuid/" puuid "/ids")
:api-key api-key
@@ -256,9 +260,7 @@
debug false
server "europe"
count 20
start 0}
:as params}]
(log/trace "Getting list of TFT matches. Params: " params)
start 0}}]
(raw-get-query
(create-endpoint server "/tft/match/v1/matches/by-puuid/" puuid "/ids")
:api-key api-key
@@ -274,7 +276,6 @@
debug false
server "europe"}}]
;; (println "get-lol-match-info match:" match-id)
(log/debug "Getting data for LOL match " match-id)
(query
(create-endpoint server "/lol/match/v5/matches/" match-id)
lol-match-parser
@@ -291,7 +292,6 @@
debug false
server "europe"}}]
;; (println "get-tft-match-info match:" match-id)
(log/debug "Getting data for TFT match " match-id)
(query
(create-endpoint server "/tft/match/v1/matches/" match-id)
tft-match-parser
@@ -300,7 +300,7 @@
:puuid puuid))
#_{:clj-kondo/ignore [:unresolved-symbol]}
(defn get-lol-current-info
"Get current LoL match, if any"
[puuid & {:keys [api-key debug print-not-active server]
@@ -308,8 +308,6 @@
debug false
print-not-active false
server "euw1"}}]
(log/debug "Getting data for current LOL match")
(when (some? api-key)
(try+
(query
(create-endpoint server "/lol/spectator/v5/active-games/by-summoner/" puuid)
@@ -317,10 +315,9 @@
:api-key api-key
:debug debug)
(catch [:status 404] _
(when print-not-active (println "No active LoL match"))))))
(when print-not-active (println "No active LoL match")))))
#_{:clj-kondo/ignore [:unresolved-symbol]}
(defn get-tft-current-info
"Get current TFT match, if any"
[puuid & {:keys [api-key debug print-not-active server]
@@ -328,8 +325,6 @@
debug false
print-not-active false
server "euw1"}}]
(log/debug "Getting data for current TFT match")
(when (some? api-key)
(try+
(query
(create-endpoint server "/lol/spectator/tft/v5/active-games/by-puuid/" puuid)
@@ -337,7 +332,7 @@
:api-key api-key
:debug debug)
(catch [:status 404] _
(when print-not-active (println "No active tft match"))))))
(when print-not-active (println "No active FTF match")))))
@@ -354,7 +349,7 @@
(defn get-last-matches
"Get info for last LoL or tft matches"
"Get info for last LoL or FTF matches"
[lol-puuid tft-puuid & {:keys [lol-api-key tft-api-key include-current print-not-active debug server1 server2 count start order-by since until]
:or {lol-api-key (get-lol-api-key)
tft-api-key (get-tft-api-key)
@@ -381,14 +376,14 @@
;;;; Get data bulk
#_{:clj-kondo/ignore [:unresolved-symbol]}
(defn get-with-wait
"Calls a getter and waits for response. If rate limit has been exceeded, waits for some
seconds and retries."
[getter & {:keys [wait-limit-exceeded]
:or {wait-limit-exceeded 15}}]
(try+
(getter) ;; Executes getter function
(getter)
(catch [:status 429] _
(println "Rate limit exceeded, waiting for" wait-limit-exceeded "seconds")
(Thread/sleep (* 1000 wait-limit-exceeded))
@@ -402,7 +397,6 @@
first-batch 0
max-batches 10}}]
;; (println "Obtaining bulk data - range")
(log/debug "Getting data in " max-batches " batches")
(doall (flatten (concat
(map
#(get-with-wait (partial getter %) :wait-limit-exceeded wait-limit-exceeded)
@@ -479,23 +473,18 @@
(defn get-matches-info-batch
"Get a lot of LOL and TFT matches info"
[lol-puuid tft-puuid & {:keys [lol-api-key tft-api-key wait-limit-exceeded debug server server2 count order-by since until include-current]
[lol-puuid tft-puuid & {:keys [lol-api-key tft-api-key wait-limit-exceeded debug server count order-by since until]
:or {lol-api-key (get-lol-api-key)
tft-api-key (get-tft-api-key)
wait-limit-exceeded 15
debug false
server "europe"
server2 "euw1"
count 100
order-by :start
include-current true}}]
(log/debug "Fetching data in batches")
order-by :start}}]
(sort-by order-by
(filter some?
(concat
(get-matches-info-batch-lol (get-matches-batch-lol lol-puuid lol-api-key :wait-limit-exceeded wait-limit-exceeded :debug debug :server server :count count :since since :until until)
lol-puuid lol-api-key :debug debug :server server :puuid lol-puuid)
(get-matches-info-batch-tft (get-matches-batch-tft tft-puuid tft-api-key :wait-limit-exceeded wait-limit-exceeded :debug debug :server server :count count :since since :until until)
tft-puuid tft-api-key :debug debug :server server :puuid lol-puuid)
(when include-current [(get-lol-current-info lol-puuid :api-key lol-api-key :server server2 :debug debug)])
(when include-current [(get-tft-current-info tft-puuid :api-key tft-api-key :server server2 :debug debug)])))))
tft-puuid tft-api-key :debug debug :server server :puuid lol-puuid)))))

View File

@@ -4,12 +4,10 @@
(ns riot.data
(:import [java.time Instant LocalDate ZoneId Duration]
[java.time.format DateTimeFormatter])
#_{:clj-kondo/ignore [:refer-all]}
(:require [clojure.pprint :as pp]
[clojure.string :as str]
[cheshire.core :refer :all]
[riot.core :refer :all]
[clj-commons.format.table :refer [print-table] :as table])
[cheshire.core :refer :all])
(:use [riot.core :refer :all])
(:gen-class))
@@ -19,37 +17,33 @@
;; Parse UNIX time
(defn unix->ZonedDateTime
"Convert from UNIX time in millis, to a ZonedDateTime"
(defn unix-to-datetime
"Conver from UNIX time in millis, to a ZonedDateTime"
[millis]
(when (some? millis)
(.atZone (Instant/ofEpochMilli millis) (ZoneId/systemDefault))))
(defn unix->LocalDate
"Convert from UNIX time in millis to LocalDate"
[millis]
(when (some? millis)
(.toLocalDate (unix->ZonedDateTime millis))))
(defn LocalDate->Date
(defn localdate-to-date
"Converts a java.time.LocalDate to an ancient java.util.Date"
[localdate]
(when (some? localdate)
(java.util.Date/from (.toInstant (.atZone (.atStartOfDay localdate) (ZoneId/systemDefault))))))
(defn str->LocalDate
(defn parse-localdate
"Parse a yyyy-MM-dd date to java.time.LocalDate"
[s]
(java.time.LocalDate/parse s (DateTimeFormatter/ofPattern DEFAULT_DATE_FORMAT)))
(java.time.LocalDate/parse s (DateTimeFormatter/ofPattern DEFAULT_DATE_FORMAT))
)
(defn str->Date
(defn parse-date-str
[s]
(LocalDate->Date (str->LocalDate s)))
(localdate-to-date(parse-localdate s)))
(defn unix->Date
(defn parse-date-epoch
[millis]
(LocalDate->Date (unix->ZonedDateTime millis)))
(localdate-to-date (unix-to-datetime millis)))
(defn format-datetime
@@ -66,21 +60,18 @@
([millis] (format-datetime-millis millis DEFAULT_DATE_TIME_FORMAT))
([millis pattern]
(when (and (some? millis) (some? pattern))
(format-datetime (unix->ZonedDateTime millis) pattern))))
(format-datetime (unix-to-datetime millis) pattern))))
;; Parse durations
(defn format-duration-seconds
"Converts a duration in seconds to a string with hours, minutes and seconds"
"Converts a duration in seconds to a string with hours and seconds"
[seconds]
(when (some? seconds)
(if (< seconds 3600)
(format "%02d:%02d" (quot seconds 60) (rem seconds 60))
(format "%02d:%02d:%02d"
(quot seconds 3600)
(- (quot seconds 60) (* 60 (quot seconds 3600)))
(rem seconds 60)))))
(format "%02d:%02d:%02d" (quot seconds 3600) ( - ( quot seconds 60) 60) (rem seconds 60)))))
@@ -111,6 +102,10 @@
(->> matches
(map #(update % :start (fn [x] (format-datetime-millis x datetime-format))))
(map #(update % :end (fn [x] (format-datetime-millis x datetime-format))))))
(comment
(with-parsed-dates riot.test-examples/matches-example)
)
(defn with-parsed-durations
@@ -119,13 +114,15 @@
(map #(update % :duration format-duration-seconds) matches))
(defn with-parsed-dates-durations
"Takes the original list of matches and parses dates and durations"
[matches & {:keys [datetime-format]
:or {datetime-format DEFAULT_DATE_TIME_FORMAT}}]
(-> matches
(with-parsed-durations)
(with-parsed-dates :datetime-format datetime-format)))
(with-parsed-dates :datetime-format datetime-format)
))
;;; Export to other formats
@@ -145,21 +142,23 @@
(generate-string matches {:pretty pretty}))
(comment
(println (as-json (with-parsed-dates-durations riot.test-examples/matches-example
:pretty false)))
)
(defn as-ascii-table
"Export as ascii table"
[matches]
(pp/print-table matches))
(comment
(as-ascii-table riot.test-examples/matches-example)
(as-ascii-table (with-parsed-dates-durations riot.test-examples/matches-example))
)
(defn as-pretty-table
"Export as an ANSI coloured text table"
[matches]
(if (and (some? matches) (< 0 (count matches)))
(let [columns {:columns (-> (first matches) keys vec)
:style table/default-style}]
(print-table columns matches))
(println "No data available")))
(defn as-csv
@@ -172,79 +171,9 @@
(map #(clojure.string/join separator %)
(map vals matches))))))
(defn calculate-num-days-played
[matches]
(count (partition-by #(unix->LocalDate (:start %)) matches))
)
(defn calculate-days-played
[matches]
(map #(.toString %) (sort (keys (group-by #(unix->LocalDate (:start %)) matches)))))
(defn calculate-total-seconds-played
[matches]
(reduce + (filter some? (map :duration matches))))
(defn calculate-seconds-played-per-day
[matches]
)
(defn calculate-statistics
"Calculate several statistics about the matches"
[matches]
(when (and (some? matches) (seq matches))
(let [matches-lol (filter #(= (:game-type %) "lol") matches)
matches-tft (filter #(= (:game-type %) "tft") matches)
total (count matches)
total-lol (count matches-lol)
total-tft (count matches-tft)
win (count (filter :winner matches))
win-lol (count (filter :winner matches-lol))
win-tft (count (filter :winner matches-tft))
loss (- total win)
loss-lol (- total-lol win-lol)
loss-tft (- total-tft win-tft)
win-percent (if (zero? total) 0 (* (/ win total) 100.0))
win-percent-lol (if (zero? total-lol) 0 (* (/ win-lol total-lol) 100.0))
win-percent-tft (if (zero? total-tft) 0 (* (/ win-tft total-tft) 100.0))
num-days-played (calculate-num-days-played matches)
num-days-played-lol (calculate-num-days-played matches-lol)
num-days-played-tft (calculate-num-days-played matches-tft)
days-played (calculate-days-played matches)
days-played-lol (calculate-days-played matches-lol)
days-played-tft (calculate-days-played matches-tft)
seconds-played (calculate-total-seconds-played matches)
seconds-played-lol (calculate-total-seconds-played matches-lol)
seconds-played-tft (calculate-total-seconds-played matches-tft)
seconds-per-day (if (zero? num-days-played) 0 (/ seconds-played num-days-played))
seconds-per-day-lol (if (zero? num-days-played-lol) 0 (/ seconds-played-lol num-days-played-lol))
seconds-per-day-tft (if (zero? num-days-played-tft) 0 (/ seconds-played-tft num-days-played-tft))]
{:total {:all total
:lol total-lol
:tft total-tft}
:win {:all win
:lol win-lol
:tft win-tft}
:loss {:all loss
:lol loss-lol
:tft loss-tft}
:win-percent {:all win-percent
:lol win-percent-lol
:tft win-percent-tft}
:num-days-played {:all num-days-played
:lol num-days-played-lol
:tft num-days-played-tft}
:days-played {:all days-played
:lol days-played-lol
:tft days-played-tft}
:time-played {:all (format-duration-seconds seconds-played)
:lol (format-duration-seconds seconds-played-lol)
:tft (format-duration-seconds seconds-played-tft)}
:played-per-day {:all (format-duration-seconds (int seconds-per-day))
:lol (format-duration-seconds (int seconds-per-day-lol))
:tft (format-duration-seconds (int seconds-per-day-tft))}})))
(comment
(calculate-statistics '({:winner true} {:winner false} {:winner true}))
(as-csv riot.test-examples/matches-example)
(as-csv riot.test-examples/matches-example2)
(as-csv (with-parsed-dates-durations riot.test-examples/matches-example))
(as-csv (with-parsed-dates-durations riot.test-examples/matches-example2))
)

View File

@@ -0,0 +1,323 @@
(ns riot.graph_experiments
(:use riot.data)
(:require ;[com.hypirion.clj-xchart :as c]
[clojure.pprint :as pprint]
[clojure.math :as m]
[clojure.core.matrix :as mtx])
(:require [incanter.core :as incanter]
[incanter.stats :as stats]
[incanter.charts :as charts]
[incanter.datasets :as datasets])
(:require [oz.core :as oz])
(:require [tech.viz.vega :as vega]))
;; ;;;; XCHART
;; ;; XY chart
;; (def xchart_xy
;; (c/xy-chart {"Expected rate" [(range 10) (range 10)]
;; "Actual rate" [(range 10) (map #(+ % (rand-int 5) -2) (range 10))]}))
;; (comment
;; (c/view xchart_xy))
;; ;;;; HANAMI
;; (comment
;; (hc/xform ht/point-chart
;; :UDATA "data/cars.json"
;; :X "Horsepower" :Y "Miles_per_Gallon" :COLOR "Origin"))
;;;; OZ
(def test-plot
{:data {:values
[{:time "18:00" :volume 10}
{:time "18:02" :volume 41}
{:time "18:07" :volume 192}
{:time "18:30" :volume 257}
{:time "19:00" :volume 300}]
:format {:parse {:time "date:'%H:%M'"}}}
:encoding {:x {:field "time" :type "temporal" :timeUnit "hoursminutes"}
:y {:field "volume" :type "quantitative"}}
:mark "point"})
;;; to compile and view in Clojure - Oz:
(comment
(do
(println "calling (oz/start-server!)")
(oz/start-server!)
(println "calling (oz/view!)")
(oz/view! test-plot)
(println "calling (Thread/sleep)")
(Thread/sleep 5000))
)
;;;; INCANTER
;; Simple histogram
(comment
(incanter/view (charts/histogram (stats/sample-normal 1000))))
;; Boxplot demo
(defn create-box-plot [data]
(let [box-plot (charts/box-plot data
:title "Box Plot Example"
:y-label "Values"
:x-label "Dataset")]
(incanter/view box-plot)))
(def sample-data [5, 7, 8, 9, 10, 14, 15, 21, 23, 23, 24, 26, 28, 30, 37])
(comment
(create-box-plot sample-data))
;; Scatter
(comment
(incanter/view (charts/scatter-plot :Sepal.Length :Sepal.Width
:data (datasets/get-dataset :iris)))
(clojure.pprint/pprint (datasets/get-dataset :iris)))
;; Scatter grouped
(comment
(incanter/view (charts/scatter-plot :Sepal.Length :Sepal.Width
:group-by :Species
:data (datasets/get-dataset :iris)
:legend true)))
;; Scatter matrix (too complicated)
(comment
(incanter/view (charts/scatter-plot-matrix
(datasets/get-dataset :iris)
:nbins 20
:group-by :Species))
(incanter/with-data (datasets/get-dataset :iris) (incanter/view (charts/scatter-plot-matrix :nbins 20 :group-by :Species)))
(incanter/view (charts/scatter-plot-matrix
(datasets/get-dataset :chick-weight)
:group-by :Diet
:nbins 20)))
;; Heatmaps
(comment
(defn f [x y] (incanter/sin (incanter/sqrt (mtx/add (incanter/sq x) (incanter/sq y)))))
(incanter/view (charts/heat-map f -10 10 -15 15))
(incanter/view (charts/heat-map f -10 10 -10 10 :color? false))
(incanter/view (charts/heat-map f 5 10 5 10 :include-zero? false))
)
;; Personalize a scatter
(def my-data '({:start 1751537581404,
:end 1751539178158,
:duration 1583,
:active false,
:game-type "lol",
:id "EUW1_7450441192",
:winner true}
{:start 1751489335800,
:end 1751491099813,
:duration 1748,
:active false,
:game-type "lol",
:id "EUW1_7450117155",
:winner true}
{:start 1751486177961,
:end 1751487764068,
:duration 1568,
:active false,
:game-type "lol",
:id "EUW1_7450048068",
:winner true}
{:start 1751484450321,
:end 1751485680510,
:duration 1201,
:active false,
:game-type "lol",
:id "EUW1_7450013510",
:winner true}
{:start 1751400380588,
:end 1751401545922,
:duration 1133,
:active false,
:game-type "lol",
:id "EUW1_7449129548",
:winner true}
{:start 1751398221584,
:end 1751399322722,
:duration 1020,
:active false,
:game-type "lol",
:id "EUW1_7449082553",
:winner false}
{:start 1751394541765,
:end 1751396611534,
:duration 2040,
:active false,
:game-type "lol",
:id "EUW1_7449005657",
:winner false}
{:start 1751388187077,
:end 1751390143357,
:duration 1937,
:active false,
:game-type "lol",
:id "EUW1_7448887620",
:winner true}
{:start 1751384103153,
:end 1751385903519,
:duration 1767,
:active false,
:game-type "lol",
:id "EUW1_7448818616",
:winner false}
{:start 1751363056183,
:end 1751364555063,
:duration 1462,
:active false,
:game-type "lol",
:id "EUW1_7448564681",
:winner false}))
(defn extract-date-millis
"Extract date from a long epoch timestamp in milliseconds"
[x]
;(java.util.Date. x)
(let [date (new java.util.Date x)]
(. (java.util.Date. (. date getYear) (. date getMonth) (. date getDay)) getTime)))
(defn extract-date
"Extract date from a long epoch timestamp in milliseconds"
[x]
;(java.util.Date. x)
(let [date (new java.util.Date x)]
(java.util.Date. (. date getYear) (. date getMonth) (. date getDay))))
(comment
(extract-date-millis 1751363056183)
(. (extract-date-millis 1751363056183) getTime)
)
(defn extract-hour
"Extract hour from a long epoch timestamp in milliseconds"
[x]
;(java.util.Date. x)
(let [date (new java.util.Date x)]
(. date getHours)))
(comment
(java.util.Date. 1751537581404)
(java.util.Date. 1751539178158)
(extract-hour 1751537581404)
(extract-hour 1751539178158)
)
(defn extract-active-hours-millis
[match]
(when (every? some? [ (:start match) (:end match)])
(let [d-ini (extract-date-millis (:start match))
d-end (extract-date-millis (:end match))
h-ini (extract-hour (:start match))
h-end (extract-hour (:end match))
game-type (:game-type match)
winner (:winner match)]
(if (= d-ini d-end)
(for [day [d-ini]
hour (range h-ini (inc h-end))]
[day hour game-type winner])
(concat
(for [day [d-ini]
hour (range h-ini 24)]
[day hour game-type winner])
(for [day [d-ini]
hour (range 0 (inc h-end))]
[day hour game-type winner]))))))
(defn extract-active-hours
[match]
(when (every? some? [(:start match) (:end match)])
(let [d-ini (extract-date (:start match))
d-end (extract-date (:end match))
h-ini (extract-hour (:start match))
h-end (extract-hour (:end match))
game-type (:game-type match)
winner (:winner match)]
(if (= d-ini d-end)
(for [day [d-ini]
hour (range h-ini (inc h-end))]
[day hour game-type winner])
(concat
(for [day [d-ini]
hour (range h-ini 24)]
[day hour game-type winner])
(for [day [d-ini]
hour (range 0 (inc h-end))]
[day hour game-type winner]))))))
(comment
(extract-active-hours-millis (first my-data))
(partition 4 (flatten (map extract-active-hours-millis my-data)))
)
(defn extract-dataset-millis
[matches]
(incanter/dataset
[:days :hours :game-type :winner]
(partition 4 (flatten (map extract-active-hours-millis matches)))))
(defn extract-dataset
[matches]
(incanter/dataset
[:days :hours :game-type :winner]
(partition 4 (flatten (map extract-active-hours matches)))))
(comment
(extract-dataset-millis my-data)
(extract-dataset my-data)
(incanter/view (charts/scatter-plot :days :hours
:data (extract-dataset-millis my-data)
:group-by :game-type
:x-label "Day"
:y-label "Hour"
:legend false))
)
(comment
(defn process-my-data [x y] (+ x y (m/random)))
(incanter/view (charts/heat-map process-my-data
-10 10 -15 15
:x-label "Day"
:y-label "Hour"
:z-label "Count"
:color? true
:include-zero? false))
)
;;;; tech.viz
(comment
(let [ my-graph (vega/scatterplot [{:a 1 :b 2} {:a 2 :b 3}] :a :b)]
(vega/vega->svg-file my-graph "timeseries.svg"))
)

View File

@@ -1,12 +1,10 @@
(ns riot.core-test
{:clj-kondo/ignore [:unresolved-symbol]}
#_{:clj-kondo/ignore [:refer-all]}
(:require [clojure.test :refer :all]
[riot.test-examples :refer :all]
[riot.core :refer :all]
[timewords.core :refer [parse]]
[clojure.pprint :as pp]
[slingshot.slingshot :refer [try+]]))
[clojure.pprint :as pp])
(:use [slingshot.slingshot :only [try+]]))
@@ -32,8 +30,8 @@
(is (= (decrypt-data LOL_KEY) (get-lol-api-key nil)))
(is (= "abc" (get-lol-api-key "abc"))))
(testing "Get TFT API key"
;; (is (= (decrypt-data TFT_KEY) (get-tft-api-key)))
;; (is (= (decrypt-data TFT_KEY) (get-tft-api-key nil)))
(is (= (decrypt-data TFT_KEY) (get-tft-api-key)))
(is (= (decrypt-data TFT_KEY) (get-tft-api-key nil)))
(is (= "abc" (get-tft-api-key "abc")))))
@@ -123,16 +121,17 @@
(comment (get-puuid-from-name example-name example-tag :api-key (get-tft-api-key)))
;; From java.util.Date to seconds since 1970
(deftest ^:timezone test-date-to-seconds
(deftest test-date-to-seconds
(println "* Date to epoch seconds *")
(testing "Some dates"
(is (nil? (date-to-seconds nil)))
(is (some? (date-to-seconds)))
(is (= 1738364400 (date-to-seconds (java.util.Date. 125 1 1))))))
(is (= 1738364400 (date-to-seconds (java.util.Date. 125 1 1)))))
)
;; Get parameter for querying data
(deftest ^:timezone test-query-params
(deftest test-query-params
(println "* Generate query params *")
(testing "None params"
(is (= {"count" 100} (query-params))))
@@ -231,7 +230,8 @@
(is (not res) (str "Oh, well, the player is playing: " res))))
(testing "Include current"
(let [res (is-playing? example-lol-puuid example-tft-puuid)]
(is (not res) (str "Oh, well, the player is playing: " res))))))
(is (not res) (str "Oh, well, the player is playing: " res)))))
)
(deftest test-get-matches-batch-lol
@@ -243,7 +243,7 @@
(testing "Get LOL matches"
(is (= 10 (count (get-matches-batch-lol example-lol-puuid (get-lol-api-key):count 5 :max-batches 2))))))
(deftest ^:tft test-get-matches-batch-tft
(deftest test-get-matches-batch-tft
(println "* Get tft matches batch *")
(testing "Invalid data"
(is (= nil (get-matches-batch-tft nil nil)))
@@ -266,7 +266,7 @@
(get-lol-api-key)))))))
(deftest ^:tft test-get-matches-info-batch-tft
(deftest test-get-matches-info-batch-tft
(println "* Get tft matches info batch *")
(testing "Invalid data"
(is (= nil (get-matches-info-batch-tft nil nil nil)))
@@ -298,4 +298,5 @@
(get-matches-info-batch example-lol-puuid example-tft-puuid)
(catch [:status 404] {:keys [request-time headers body] :as excp}
(println "NOT Found 404")
(pp/pprint excp))))
(pp/pprint excp)))
)

View File

@@ -1,6 +1,5 @@
(ns riot.data-test
(:require [clojure.test :refer :all]
[clojure.pprint :as pp]
[riot.test-examples :refer :all]
[riot.data :refer :all]
[riot.core :refer :all])
@@ -10,38 +9,30 @@
;; Convert from epoch in millis (1970-01-01 00:00:00) to ZonedDateTime
(deftest ^:timezone test-unix->ZonedDateTime
(deftest test-unix-to-datetime
(println "* Epoch millis to java.util.Datetime *")
(testing "Synthetic data"
(testing "Get LOL API key"
(is (= (ZonedDateTime/of 2025 4 3 0 0 0 0 (ZoneId/systemDefault))
(unix->ZonedDateTime 1743631200000)))))
(deftest ^:timezone test-unix->LocalDate
(println "* Epoch millis to java.time.LocalDate *")
(testing "Synthetic data"
(is (= (LocalDate/of 2025 4 3) (unix->LocalDate 1743631200000)))
)
)
(unix-to-datetime 1743631200000)))))
;; Format a LocalDateTime o ZonedDateTime as a String
(deftest ^:timezone test-unix->ZonedDateTime
(deftest test-format-datetime
(println "* DateTime to String *")
(testing "Null"
(is (nil? (format-datetime-millis nil))))
(testing "Default format"
(is (= "2025-04-03 00:00:00"
(format-datetime (unix->ZonedDateTime 1743631200000)))))
(format-datetime (unix-to-datetime 1743631200000)))))
(testing "Personalized time formats"
(is (= "2025-04-03"
(format-datetime (unix->ZonedDateTime 1743631200000) "yyyy-MM-dd")))
(format-datetime (unix-to-datetime 1743631200000) "yyyy-MM-dd")))
(is (= "00:00"
(format-datetime (unix->ZonedDateTime 1743631200000) "HH:00")))))
(format-datetime (unix-to-datetime 1743631200000) "HH:00")))))
;; Format a epoch in millis as a String
(deftest ^:timezone test-datetime-millis
(deftest test-datetime-millis
(println "* DateTime to String *")
(testing "Null"
(is (nil? (format-datetime-millis nil))))
@@ -67,14 +58,12 @@
(testing "Long durations"
(is (= "01:00:00" (format-duration-seconds 3600)))
(is (= "01:00:01" (format-duration-seconds 3601)))
(is (= "01:01:01" (format-duration-seconds 3661)))
(is (= "05:53:09" (format-duration-seconds 21189)))
)
(is (= "01:01:01" (format-duration-seconds 3661))))
)
;; Epoch in millis for today at 00:00:00 hours. Difficult to test
(deftest ^:timezone test-today-millis
(deftest test-today-millis
(println "* Epoch for today *")
(testing "Epoch for today"
(is (some? (today-millis)))))
@@ -88,11 +77,12 @@
(is (match-today? {:start (+ 5000 (today-millis))})))
(testing "End date today"
(is (match-today? {:end (today-millis)}))
(is (match-today? {:end (+ 5000 (today-millis))}))))
(is (match-today? {:end (+ 5000 (today-millis))})))
)
;; Parse dates inside a lis of matches
(deftest ^:timezone test-with-parsed-dates
(deftest test-with-parsed-dates
(println "* Parse dates in a match list*")
(testing "Empty or invalid lists"
(is (= '() (with-parsed-dates '())))
@@ -100,11 +90,12 @@
(is (= '({:a "AAA" :b 2 :start nil, :end nil}) (with-parsed-dates '({:a "AAA" :b 2})))))
(testing "Valid list"
(is (= '({:start "2025-06-11 19:00:54", :end nil, :duration 1288, :active true, :game-type "lol", :id "EUW1_111111"})
(with-parsed-dates (take 1 matches-example))))))
(with-parsed-dates (take 1 matches-example))))
))
;; Parse durations
(deftest ^:timezone test-with-parsed-durations
(deftest test-with-parsed-dates
(println "* Parse durations in a match list *")
(testing "Empty or invalid lists"
(is (= '() (with-parsed-durations '())))
@@ -112,11 +103,12 @@
(is (= '({:a "AAA" :b 2 :duration nil}) (with-parsed-durations '({:a "AAA" :b 2})))))
(testing "Valid list"
(is (= '({:start 1749661254854, :end nil, :duration "21:28", :active true, :game-type "lol", :id "EUW1_111111"})
(with-parsed-durations (take 1 matches-example))))))
(with-parsed-durations (take 1 matches-example)))))
)
;; Parse dates and durations
(deftest ^:timezone test-with-parsed-dates-durations
(deftest test-with-parsed-dates
(println "* Parse dates and durations in a match list *")
(testing "Empty or invalid lists"
(is (= '() (with-parsed-dates-durations '())))
@@ -125,55 +117,8 @@
(is (= '({:a "AAA" :b 2 :duration nil, :start nil, :end nil})
(with-parsed-dates-durations '({:a "AAA" :b 2})))))
(testing "Valid list"
(is (= '({:start "2025-06-11 19:00:54", :end nil, :duration "21:28", :winner true, :game-type "lol", :id "EUW1_111111", :result "GameComplete"})
(with-parsed-dates-durations (take 1 matches-example))))))
;; Pretty table
(deftest test-as-pretty-table
(println "* Generates a pretty table *")
(testing "Some examples, printed to System.out"
(is (= nil (as-pretty-table [])))
(is (= nil (as-pretty-table nil)))
(is (= nil (as-pretty-table matches-example)))
(is (= nil (as-pretty-table matches-example2)))))
(deftest test-calculate-statistics
(println "* Calculate statistics *")
(testing "Simple check"
(is (nil? (calculate-statistics nil)))
(is (nil? (calculate-statistics '())))
(is (map? (calculate-statistics matches-example)))
(is (map? (calculate-statistics matches-example2))))
(let [data (calculate-statistics matches-example)]
(testing "With manual generated data"
(is (= 5 (get-in data [:total :all])))
(is (= 3 (get-in data [:total :lol])))
(is (= 2 (get-in data [:total :tft])))
(is (= 3 (get-in data [:win :all])))
(is (= 2 (get-in data [:loss :all])))
(is (= 60.0 (get-in data [:win-percent :all])))
(is (= 3 (get-in data [:num-days-played :all]))))))
(comment
matches-example
(.toLocalDate (unix->ZonedDateTime 1749661254854))
(partition-by #(unix->LocalDate (:start %)) matches-example)
(group-by #(unix->LocalDate (:start %)) matches-example)
(keys (group-by #(unix->LocalDate (:start %)) matches-example2))
(calculate-days-played matches-example2)
(reduce + (filter some? (map :duration matches-example)))
(calculate-statistics matches-example)
(calculate-statistics matches-example2)
(pp/pprint (calculate-statistics matches-example2))
(is (= '({:start "2025-06-11 19:00:54", :end nil, :duration "21:28", :active true, :game-type "lol", :id "EUW1_111111"})
(with-parsed-dates-durations (take 1 matches-example)))))
)

View File

@@ -8,7 +8,7 @@
(def example-tft-puuid "yJawqnx9nzvj9ZJdW_7ok0wmB2WqSqafKy1sDrA48Zefbx0Iuco9jfGyVdUhldjDt8IxLvMcs4r8MA")
(def example-lol-match {:start 1750106168280, :end 1750108132077, :duration 1882, :game-type "lol", :id "EUW1_7434497430" :winner false :result "GameComplete"})
(def example-lol-match {:start 1750106168280, :end 1750108132077, :duration 1882, :active false, :game-type "lol", :id "EUW1_7434497430" :winner false})
(def example-lol-never-played "annlAfxhJnTwlwRgQZYbGeQpD3jWb-ju7vVKEW_g-EIJf6xQT0eeb-0obARVekrksf8n9XCjcxyHHQ")
@@ -17,120 +17,83 @@
'({:start 1749661254854,
:end nil,
:duration 1288,
:active true,
:game-type "lol"
:id "EUW1_111111"
:winner true,
:result "GameComplete"}
:id "EUW1_111111"}
{:start 1749659482211,
:end 1749661133145,
:duration 1622,
:active false,
:game-type "lol"
:id "EUW1_222222"
:winner false,
:result "GameComplete"}
:id "EUW1_222222"}
{:start 1749655924238,
:end 1749658146178,
:duration nil,
:active false,
:game-type "lol"
:id "EUW1_3333333"
:winner true,
:result "GameComplete"}
{:start 1749755924238,
:end 1749758146178,
:duration nil,
:game-type "tft"
:id "EUW1_4444444"
:winner true
:result "GameComplete"}
{:start 1749855924238,
:end 1749858146178,
:duration nil,
:game-type "tft"
:id "EUW1_5555555"
:winner false
:result "GameComplete"}))
;; Real example from:
;; riot-clojur t "Walid Georgey" euw --tft -o edn -s "24 may" -u "30 may" --no-format-dates --no-format-durations
:id "EUW1_3333333"}))
(def matches-example2
'({:start 1748470702886,
:end 1748472666457,
:duration 1947,
'({:start 1749893615000,
:end 1749896091000,
:duration 2476,
:active false,
:game-type "ftf",
:id "EUW1_7431741880"}
{:start 1749886475803,
:end 1749887505751,
:duration 999,
:active false,
:game-type "lol",
:id "EUW1_7415410708",
:winner true
:result "GameComplete"}
{:start 1748466492126,
:end 1748468105264,
:duration 1585,
:id "EUW1_7431686898"}
{:start 1749830764708,
:end 1749831241808,
:duration 232,
:active false,
:game-type "lol",
:id "EUW1_7415317950",
:winner false
:result "GameComplete"}
{:start 1748180985396,
:end 1748182761075,
:duration 1754,
:id "EUW1_7431025296"}
{:start 1749740947765,
:end 1749743270521,
:duration 2214,
:active false,
:game-type "lol",
:id "EUW1_7411793848",
:winner true}
:result "GameComplete"
{:start 1748178332000,
:end 1748180377000,
:duration 2045,
:game-type "tft",
:id "EUW1_7411747119",
:winner true
:result "GameComplete"}
{:start 1748119701678,
:end 1748121421926,
:duration 1578,
:id "EUW1_7429951366"}
{:start 1749739258737,
:end 1749740607134,
:duration 1311,
:active false,
:game-type "lol",
:id "EUW1_7411183430",
:winner true
:result "GameComplete"}
{:start 1748117211000,
:end 1748119477000,
:duration 2266,
:game-type "tft",
:id "EUW1_7411136542",
:winner true
:result "GameComplete"}
{:start 1748110984521,
:end 1748112698320,
:duration 1694,
:id "EUW1_7429932844"}
{:start 1749737844367,
:end 1749739185680,
:duration 1144,
:active false,
:game-type "lol",
:id "EUW1_7410995061",
:winner true
:result "GameComplete"}
{:start 1748103234000,
:end 1748105725000,
:duration 2491,
:game-type "tft",
:id "EUW1_7410863506",
:winner true
:result "GameComplete"}
{:start 1748097496984,
:end 1748099470873,
:duration 1933,
:id "EUW1_7429905240"}
{:start 1749736215680,
:end 1749737698464,
:duration 1425,
:active false,
:game-type "lol",
:id "EUW1_7410750999",
:winner false
:result "GameComplete"}
{:start 1748094441000,
:end 1748096636000,
:duration 2195,
:game-type "tft",
:id "EUW1_7410701935",
:winner true
:result "GameComplete"}
{:start 1748088640068,
:end 1748090386650,
:duration 1701,
:id "EUW1_7429884230"}
{:start 1749733140474,
:end 1749734774103,
:duration 1535,
:active false,
:game-type "lol",
:id "EUW1_7410601082",
:winner true
:result "GameComplete"}))
:id "EUW1_7429836245"}
{:start 1749727474332,
:end 1749729494360,
:duration 1899,
:active false,
:game-type "lol",
:id "EUW1_7429774673"}
{:start 1749717297978,
:end 1749719815603,
:duration 2281,
:active false,
:game-type "lol",
:id "EUW1_7429678096"}))
(def response-player-info {:puuid "annlAfxhJnTwlwRgQZYbGeQpD3jWb-ju7vVKEW_g-EIJf6xQT0eeb-0obARVekrksf8n9XCjcxyHHQ",