export game PGN with real player names and ratings through external file

curl 'l.org/game/export/FUPEWMpY?players=0ff573018f/gistfile1.txt'
pull/6962/head
Thibault Duplessis 2020-07-09 21:15:41 +02:00
parent 6e184110df
commit df655e9e23
5 changed files with 103 additions and 26 deletions

View File

@ -42,7 +42,8 @@ final class Game(
format = if (HTTPRequest acceptsJson req) GameApiV2.Format.JSON else GameApiV2.Format.PGN,
imported = getBool("imported", req),
flags = requestPgnFlags(req, extended = true),
noDelay = get("key", req).exists(env.noDelaySecretSetting.get().value.contains)
noDelay = get("key", req).exists(env.noDelaySecretSetting.get().value.contains),
playerFile = get("players", req)
)
env.api.gameApiV2.exportOne(game, config) flatMap { content =>
env.api.gameApiV2.filename(game, config.format) map { filename =>

View File

@ -40,6 +40,7 @@ final class Env(
onlineApiUsers: lila.bot.OnlineApiUsers,
challengeEnv: lila.challenge.Env,
msgEnv: lila.msg.Env,
cacheApi: lila.memo.CacheApi,
ws: WSClient,
val mode: Mode
)(implicit
@ -56,6 +57,8 @@ final class Env(
lazy val gameApi = wire[GameApi]
lazy val realPlayers = wire[RealPlayerApi]
lazy val gameApiV2 = wire[GameApiV2]
lazy val userGameApi = wire[UserGameApi]

View File

@ -27,7 +27,8 @@ final class GameApiV2(
playerRepo: lila.tournament.PlayerRepo,
swissApi: lila.swiss.SwissApi,
analysisRepo: lila.analyse.AnalysisRepo,
getLightUser: LightUser.Getter
getLightUser: LightUser.Getter,
realPlayerApi: RealPlayerApi
)(implicit
ec: scala.concurrent.ExecutionContext,
system: akka.actor.ActorSystem
@ -47,13 +48,21 @@ final class GameApiV2(
game.pgnImport ifTrue config.imported match {
case Some(imported) => fuccess(imported.pgn)
case None =>
enrich(config.flags)(game) flatMap {
case (game, initialFen, analysis) =>
config.format match {
case Format.JSON => toJson(game, initialFen, analysis, config.flags) dmap Json.stringify
case Format.PGN => pgnDump.toPgnString(game, initialFen, analysis, config.flags)
}
}
for {
realPlayers <- config.playerFile.??(realPlayerApi.apply)
(game, initialFen, analysis) <- enrich(config.flags)(game)
export <- config.format match {
case Format.JSON => toJson(game, initialFen, analysis, config.flags) dmap Json.stringify
case Format.PGN =>
pgnDump(
game,
initialFen,
analysis,
config.flags,
realPlayers = realPlayers
) dmap pgnDump.toPgnString
}
} yield export
}
}
@ -240,7 +249,8 @@ final class GameApiV2(
lightUsers <- gameLightUsers(g)
pgn <-
withFlags.pgnInJson ?? pgnDump
.toPgnString(g, initialFen, analysisOption, withFlags)
.apply(g, initialFen, analysisOption, withFlags)
.dmap(pgnDump.toPgnString)
.dmap(some)
} yield Json
.obj(
@ -307,7 +317,8 @@ object GameApiV2 {
format: Format,
imported: Boolean,
flags: WithFlags,
noDelay: Boolean
noDelay: Boolean,
playerFile: Option[String]
) extends Config
case class ByUserConfig(

View File

@ -22,7 +22,8 @@ final class PgnDump(
initialFen: Option[FEN],
analysis: Option[Analysis],
flags: WithFlags,
teams: Option[GameTeams] = None
teams: Option[GameTeams] = None,
realPlayers: Option[RealPlayers] = None
): Fu[Pgn] =
dumper(game, initialFen, flags, teams) flatMap { pgn =>
if (flags.tags) (game.simulId ?? simulApi.idToName) map { simulName =>
@ -36,6 +37,8 @@ final class PgnDump(
val evaled = analysis.ifTrue(flags.evals).fold(pgn)(addEvals(pgn, _))
if (flags.literate) annotator(evaled, analysis, game.opening, game.winnerColor, game.status)
else evaled
} map { pgn =>
realPlayers.fold(pgn)(_.update(game, pgn))
}
private def addEvals(p: Pgn, analysis: Analysis): Pgn =
@ -60,19 +63,12 @@ final class PgnDump(
def formatter(flags: WithFlags) =
(game: Game, initialFen: Option[FEN], analysis: Option[Analysis], teams: Option[GameTeams]) =>
toPgnString(game, initialFen, analysis, flags, teams)
apply(game, initialFen, analysis, flags, teams) dmap toPgnString
def toPgnString(
game: Game,
initialFen: Option[FEN],
analysis: Option[Analysis],
flags: WithFlags,
teams: Option[GameTeams] = None
) =
apply(game, initialFen, analysis, flags, teams) dmap { pgn =>
// merge analysis & eval comments
// 1. e4 { [%eval 0.17] } { [%clk 0:00:30] }
// 1. e4 { [%eval 0.17] [%clk 0:00:30] }
s"$pgn\n\n\n".replaceIf("] } { [", "] [")
}
def toPgnString(pgn: Pgn) = {
// merge analysis & eval comments
// 1. e4 { [%eval 0.17] } { [%clk 0:00:30] }
// 1. e4 { [%eval 0.17] [%clk 0:00:30] }
s"$pgn\n\n\n".replaceIf("] } { [", "] [")
}
}

View File

@ -0,0 +1,66 @@
package lila.api
import chess.format.pgn.{ Pgn, Tag, Tags }
import play.api.libs.ws.WSClient
import scala.concurrent.duration._
import lila.user.User
final class RealPlayerApi(
cacheApi: lila.memo.CacheApi,
ws: WSClient
)(implicit ec: scala.concurrent.ExecutionContext) {
def apply(url: String): Fu[Option[RealPlayers]] = cache get url
private val cache = cacheApi[String, Option[RealPlayers]](4, "api.realPlayer") {
_.expireAfterAccess(10 seconds)
.buildAsyncFuture { url =>
ws.url(url)
.withRequestTimeout(3.seconds)
.get()
.map { res =>
val valid =
res.status == 200 &&
res.headers
.get("Content-Type")
.exists(_.exists(_ startsWith "text/plain"))
valid ?? {
res.body.linesIterator
.take(9999)
.toList
.flatMap { line =>
line.split(';').map(_.trim) match {
case Array(id, name, rating) =>
Some(id -> RealPlayer(name.some.filter(_.nonEmpty), rating.toIntOption))
case Array(id, name) => Some(id -> RealPlayer(name.some.filter(_.nonEmpty), none))
case _ => none
}
}
.toMap
.some
.map(RealPlayers)
}
}
}
}
}
case class RealPlayers(players: Map[User.ID, RealPlayer]) {
def update(game: lila.game.Game, pgn: Pgn) =
pgn.copy(
tags = pgn.tags ++ Tags {
game.players.flatMap { player =>
player.userId.flatMap(players.get) ?? { rp =>
List(
rp.name.map { name => Tag(player.color.fold(Tag.White, Tag.Black), name) },
rp.rating.map { rating => Tag(player.color.fold(Tag.WhiteElo, Tag.BlackElo), rating.toString) }
).flatten
}
}
}
)
}
case class RealPlayer(name: Option[String], rating: Option[Int])