unify game exports
parent
f83fedfc2e
commit
3428119b3c
|
@ -10,36 +10,6 @@ object Export extends LilaController {
|
|||
|
||||
private def env = Env.game
|
||||
|
||||
def pgn(id: String) = Open { implicit ctx =>
|
||||
lila.mon.export.pgn.game()
|
||||
OptionFuResult(GameRepo game id) { game =>
|
||||
gameToPgn(
|
||||
game,
|
||||
asImported = get("as") contains "imported",
|
||||
asRaw = get("as").contains("raw")
|
||||
) map { content =>
|
||||
Ok(content).withHeaders(
|
||||
CONTENT_TYPE -> pgnContentType,
|
||||
CONTENT_DISPOSITION -> ("attachment; filename=" + (Env.api.pgnDump filename game))
|
||||
)
|
||||
} recover {
|
||||
case err => NotFound(err.getMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def gameToPgn(from: GameModel, asImported: Boolean, asRaw: Boolean): Fu[String] = from match {
|
||||
case game if game.playable => fufail("Can't export PGN of game in progress")
|
||||
case game => (game.pgnImport.ifTrue(asImported) match {
|
||||
case Some(i) => fuccess(i.pgn)
|
||||
case None => for {
|
||||
initialFen <- GameRepo initialFen game
|
||||
pgn <- Env.api.pgnDump(game, initialFen, analysis = none, PgnDump.WithFlags(clocks = !asRaw))
|
||||
analysis ← !asRaw ?? (Env.analyse.analyser get game)
|
||||
} yield Env.analyse.annotator(pgn, analysis, game.opening, game.winnerColor, game.status, game.clock).toString
|
||||
})
|
||||
}
|
||||
|
||||
private val PngRateLimitGlobal = new lila.memo.RateLimit[String](
|
||||
credits = 240,
|
||||
duration = 1 minute,
|
||||
|
|
|
@ -27,7 +27,29 @@ object Game extends LilaController {
|
|||
}
|
||||
}
|
||||
|
||||
def export(username: String) = OpenOrScoped()(
|
||||
def exportOne(id: String) = Open { implicit ctx =>
|
||||
OptionFuResult(GameRepo game id) { game =>
|
||||
if (game.playable) BadRequest("Can't export PGN of game in progress").fuccess
|
||||
else {
|
||||
val config = GameApiV2.OneConfig(
|
||||
format = if (HTTPRequest acceptsJson ctx.req) GameApiV2.Format.JSON else GameApiV2.Format.PGN,
|
||||
imported = getBool("imported"),
|
||||
flags = requestPgnFlags(ctx.req)
|
||||
)
|
||||
lila.mon.export.pgn.game()
|
||||
Env.api.gameApiV2.exportOne(game, config) flatMap { content =>
|
||||
Env.api.gameApiV2.filename(game, config.format) map { filename =>
|
||||
Ok(content).withHeaders(
|
||||
CONTENT_TYPE -> gameContentType(config),
|
||||
CONTENT_DISPOSITION -> s"attachment; filename=$filename"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def exportByUser(username: String) = OpenOrScoped()(
|
||||
open = ctx => handleExport(username, ctx.me, ctx.req, oauth = false),
|
||||
scoped = req => me => handleExport(username, me.some, req, oauth = true)
|
||||
)
|
||||
|
@ -39,7 +61,7 @@ object Game extends LilaController {
|
|||
Api.GlobalLinearLimitPerIP(HTTPRequest lastRemoteAddress req) {
|
||||
Api.GlobalLinearLimitPerUserOption(me) {
|
||||
val format = if (HTTPRequest acceptsNdJson req) GameApiV2.Format.JSON else GameApiV2.Format.PGN
|
||||
val config = GameApiV2.Config(
|
||||
val config = GameApiV2.ByUserConfig(
|
||||
user = user,
|
||||
format = format,
|
||||
since = getLong("since", req) map { ts => new DateTime(ts) },
|
||||
|
@ -49,13 +71,7 @@ object Game extends LilaController {
|
|||
perfType = ~get("perfType", req) split "," flatMap { lila.rating.PerfType(_) } toSet,
|
||||
color = get("color", req) flatMap chess.Color.apply,
|
||||
analysed = getBoolOpt("analysed", req),
|
||||
flags = lila.game.PgnDump.WithFlags(
|
||||
moves = getBoolOpt("moves", req) | true,
|
||||
tags = getBoolOpt("tags", req) | true,
|
||||
clocks = getBoolOpt("clocks", req) | false,
|
||||
evals = getBoolOpt("evals", req) | false,
|
||||
opening = getBoolOpt("opening", req) | false
|
||||
),
|
||||
flags = requestPgnFlags(req),
|
||||
perSecond = MaxPerSecond(me match {
|
||||
case Some(m) if m is user.id => 50
|
||||
case Some(_) if oauth => 20 // bonus for oauth logged in only (not for XSRF)
|
||||
|
@ -63,12 +79,9 @@ object Game extends LilaController {
|
|||
})
|
||||
)
|
||||
val date = (DateTimeFormat forPattern "yyyy-MM-dd") print new DateTime
|
||||
Ok.chunked(Env.api.gameApiV2.exportUserGames(config)).withHeaders(
|
||||
CONTENT_TYPE -> (format match {
|
||||
case GameApiV2.Format.PGN => pgnContentType
|
||||
case GameApiV2.Format.JSON => ndJsonContentType
|
||||
}),
|
||||
CONTENT_DISPOSITION -> ("attachment; filename=" + s"lichess_${user.username}_$date.${format.toString.toLowerCase}")
|
||||
Ok.chunked(Env.api.gameApiV2.exportByUser(config)).withHeaders(
|
||||
CONTENT_TYPE -> gameContentType(config),
|
||||
CONTENT_DISPOSITION -> s"attachment; filename=lichess_${user.username}_$date.${format.toString.toLowerCase}"
|
||||
).fuccess
|
||||
}
|
||||
}
|
||||
|
@ -76,6 +89,23 @@ object Game extends LilaController {
|
|||
}
|
||||
}
|
||||
|
||||
private def requestPgnFlags(req: RequestHeader) =
|
||||
lila.game.PgnDump.WithFlags(
|
||||
moves = getBoolOpt("moves", req) | true,
|
||||
tags = getBoolOpt("tags", req) | true,
|
||||
clocks = getBoolOpt("clocks", req) | false,
|
||||
evals = getBoolOpt("evals", req) | false,
|
||||
opening = getBoolOpt("opening", req) | false
|
||||
)
|
||||
|
||||
private def gameContentType(config: GameApiV2.Config) = config.format match {
|
||||
case GameApiV2.Format.PGN => pgnContentType
|
||||
case GameApiV2.Format.JSON => config match {
|
||||
case _: GameApiV2.ByUserConfig => ndJsonContentType
|
||||
case _: GameApiV2.OneConfig => JSON
|
||||
}
|
||||
}
|
||||
|
||||
private[controllers] def preloadUsers(game: GameModel): Funit =
|
||||
Env.user.lightUserApi preloadMany game.userIds
|
||||
}
|
||||
|
|
|
@ -69,12 +69,12 @@ atom = atom.some) {
|
|||
<div class="pgn_options">
|
||||
<strong>PGN</strong>
|
||||
<div>
|
||||
<a data-icon="x" class="text" rel="nofollow" href="@routes.Export.pgn(game.id)">@trans.downloadAnnotated()</a>
|
||||
<a data-icon="x" class="text" rel="nofollow" href="@routes.Game.exportOne(game.id)?evals=1&clocks=1&opening=1">@trans.downloadAnnotated()</a>
|
||||
@if(analysis.isDefined) {
|
||||
<a data-icon="x" class="text" rel="nofollow" href="@routes.Export.pgn(game.id)?as=raw">@trans.downloadRaw()</a>
|
||||
<a data-icon="x" class="text" rel="nofollow" href="@routes.Game.exportOne(game.id)">@trans.downloadRaw()</a>
|
||||
}
|
||||
@if(game.isPgnImport) {
|
||||
<a data-icon="x" class="text" rel="nofollow" href="@routes.Export.pgn(game.id)?as=imported">@trans.downloadImported()</a>
|
||||
<a data-icon="x" class="text" rel="nofollow" href="@routes.Game.exportOne(game.id)?imported=1">@trans.downloadImported()</a>
|
||||
}
|
||||
<a data-icon="=" class="text embed_howto" target="_blank">@trans.embedInYourWebsite()</a>
|
||||
</div>
|
||||
|
|
|
@ -54,14 +54,14 @@ chessground = false) {
|
|||
<div class="panel fen_pgn">
|
||||
<p><strong>FEN</strong><input readonly="true" spellcheck="false" class="copyable autoselect fen" /></p>
|
||||
<p><strong>PGN</strong>
|
||||
<a data-icon="x" rel="nofollow" href="@routes.Export.pgn(game.id)"> @trans.downloadAnnotated()</a>
|
||||
<a data-icon="x" rel="nofollow" href="@routes.Game.exportOne(game.id)?evals=1&clocks=1&opening=1"> @trans.downloadAnnotated()</a>
|
||||
@if(analysis.isDefined) {
|
||||
/
|
||||
<a data-icon="x" rel="nofollow" href="@routes.Export.pgn(game.id)?as=raw"> @trans.downloadRaw()</a>
|
||||
<a data-icon="x" rel="nofollow" href="@routes.Game.exportOne(game.id)"> @trans.downloadRaw()</a>
|
||||
}
|
||||
@if(game.isPgnImport) {
|
||||
/
|
||||
<a data-icon="x" rel="nofollow" href="@routes.Export.pgn(game.id)?as=imported"> @trans.downloadImported()</a>
|
||||
<a data-icon="x" rel="nofollow" href="@routes.Game.exportOne(game.id)?imported=1"> @trans.downloadImported()</a>
|
||||
}
|
||||
</p>
|
||||
<div class="pgn">@pgn</div>
|
||||
|
|
|
@ -61,7 +61,7 @@
|
|||
}
|
||||
}
|
||||
@if(ctx is u) {
|
||||
<a class="button hint--top-left" href="@routes.Game.export(u.username)" data-hint="@trans.exportGames()"><span data-icon="x"></span></a>
|
||||
<a class="button hint--top-left" href="@routes.Game.exportByUser(u.username)" data-hint="@trans.exportGames()"><span data-icon="x"></span></a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -14,7 +14,7 @@ POST /timeline/unsub/:channel controllers.Timeline.unsub(channel: Strin
|
|||
GET /games/search controllers.Search.index(page: Int ?= 1)
|
||||
|
||||
# Game export
|
||||
GET /games/export/:username controllers.Game.export(username: String)
|
||||
GET /games/export/:username controllers.Game.exportByUser(username: String)
|
||||
|
||||
# TV
|
||||
GET /tv controllers.Tv.index
|
||||
|
@ -283,7 +283,8 @@ GET /team/:id/users controllers.Team.users(id: String)
|
|||
# Analyse
|
||||
POST /$gameId<\w{8}>/request-analysis controllers.Analyse.requestAnalysis(gameId: String)
|
||||
|
||||
GET /game/export/$gameId<\w{8}>.pgn controllers.Export.pgn(gameId: String)
|
||||
GET /game/export/$gameId<\w{8}> controllers.Game.exportOne(gameId: String)
|
||||
GET /game/export/$gameId<\w{8}>.pgn controllers.Game.exportOne(gameId: String)
|
||||
GET /game/export/png/$gameId<\w{8}>.png controllers.Export.png(gameId: String)
|
||||
|
||||
# Fishnet
|
||||
|
|
|
@ -6,6 +6,7 @@ import play.api.libs.json._
|
|||
import scala.concurrent.duration._
|
||||
|
||||
import chess.format.FEN
|
||||
import chess.format.pgn.Tag
|
||||
import lila.analyse.{ AnalysisRepo, JsonView => analysisJson, Analysis }
|
||||
import lila.common.{ LightUser, MaxPerSecond }
|
||||
import lila.game.JsonView._
|
||||
|
@ -20,7 +21,31 @@ final class GameApiV2(
|
|||
|
||||
import GameApiV2._
|
||||
|
||||
def exportUserGames(config: Config): Enumerator[String] = {
|
||||
def exportOne(game: Game, config: OneConfig): Fu[String] =
|
||||
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) map Json.stringify
|
||||
case Format.PGN => pgnDump.toPgnString(game, initialFen, analysis, config.flags)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val fileR = """[\s,]""".r
|
||||
def filename(game: Game, format: Format): Fu[String] = gameLightUsers(game) map {
|
||||
case List(wu, bu) => fileR.replaceAllIn(
|
||||
"lichess_pgn_%s_%s_vs_%s.%s.%s".format(
|
||||
Tag.UTCDate.format.print(game.createdAt),
|
||||
pgnDump.dumper.player(game.whitePlayer, wu),
|
||||
pgnDump.dumper.player(game.blackPlayer, bu),
|
||||
game.id,
|
||||
format.toString.toLowerCase
|
||||
), "_"
|
||||
)
|
||||
}
|
||||
|
||||
def exportByUser(config: ByUserConfig): Enumerator[String] = {
|
||||
|
||||
import reactivemongo.play.iteratees.cursorProducer
|
||||
import lila.db.dsl._
|
||||
|
@ -48,27 +73,27 @@ final class GameApiV2(
|
|||
case Format.JSON => jsonFormatter(config.flags)
|
||||
}
|
||||
|
||||
games &>
|
||||
Enumeratee.mapM { game =>
|
||||
GameRepo initialFen game flatMap { initialFen =>
|
||||
(config.flags.evals ?? AnalysisRepo.byGame(game)) map { analysis =>
|
||||
(game, initialFen, analysis)
|
||||
}
|
||||
}
|
||||
} &> formatter
|
||||
games &> Enumeratee.mapM(enrich(config.flags)) &> formatter
|
||||
}
|
||||
|
||||
private def enrich(flags: WithFlags)(game: Game) =
|
||||
GameRepo initialFen game flatMap { initialFen =>
|
||||
(flags.evals ?? AnalysisRepo.byGame(game)) map { analysis =>
|
||||
(game, initialFen, analysis)
|
||||
}
|
||||
}
|
||||
|
||||
private def jsonFormatter(flags: WithFlags) =
|
||||
Enumeratee.mapM[(Game, Option[FEN], Option[Analysis])].apply[String] {
|
||||
case (game, initialFen, analysis) => toJson(game, analysis, initialFen, flags) map { json =>
|
||||
case (game, initialFen, analysis) => toJson(game, initialFen, analysis, flags) map { json =>
|
||||
s"${Json.stringify(json)}\n"
|
||||
}
|
||||
}
|
||||
|
||||
private def toJson(
|
||||
g: Game,
|
||||
analysisOption: Option[Analysis],
|
||||
initialFen: Option[FEN],
|
||||
analysisOption: Option[Analysis],
|
||||
withFlags: WithFlags
|
||||
): Fu[JsObject] = gameLightUsers(g) map { lightUsers =>
|
||||
Json.obj(
|
||||
|
@ -119,7 +144,17 @@ object GameApiV2 {
|
|||
case object JSON extends Format
|
||||
}
|
||||
|
||||
case class Config(
|
||||
sealed trait Config {
|
||||
val format: Format
|
||||
}
|
||||
|
||||
case class OneConfig(
|
||||
format: Format,
|
||||
imported: Boolean,
|
||||
flags: WithFlags
|
||||
) extends Config
|
||||
|
||||
case class ByUserConfig(
|
||||
user: lila.user.User,
|
||||
format: Format,
|
||||
since: Option[DateTime] = None,
|
||||
|
@ -131,7 +166,7 @@ object GameApiV2 {
|
|||
color: Option[chess.Color],
|
||||
flags: WithFlags,
|
||||
perSecond: MaxPerSecond
|
||||
) {
|
||||
) extends Config {
|
||||
def postFilter(g: Game) =
|
||||
rated.fold(true)(g.rated ==) && {
|
||||
perfType.isEmpty || g.perfType.exists(perfType.contains)
|
||||
|
|
|
@ -11,7 +11,7 @@ import lila.game.PgnDump.WithFlags
|
|||
import lila.game.{ Game, GameRepo, Query }
|
||||
|
||||
final class PgnDump(
|
||||
dumper: lila.game.PgnDump,
|
||||
val dumper: lila.game.PgnDump,
|
||||
getSimulName: String => Fu[Option[String]],
|
||||
getTournamentName: String => Option[String]
|
||||
) {
|
||||
|
@ -37,17 +37,17 @@ final class PgnDump(
|
|||
}))
|
||||
}
|
||||
|
||||
def filename(game: Game) = dumper filename game
|
||||
|
||||
def formatter(flags: WithFlags) =
|
||||
Enumeratee.mapM[(Game, Option[FEN], Option[Analysis])].apply[String] {
|
||||
case (game, initialFen, analysis) =>
|
||||
apply(game, initialFen, analysis, flags).map { 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".replace("] } { [", "] [")
|
||||
}
|
||||
case (game, initialFen, analysis) => toPgnString(game, initialFen, analysis, flags)
|
||||
}
|
||||
|
||||
def toPgnString(game: Game, initialFen: Option[FEN], analysis: Option[Analysis], flags: WithFlags) =
|
||||
apply(game, initialFen, analysis, flags).map { 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".replace("] } { [", "] [")
|
||||
}
|
||||
|
||||
// def exportGamesFromIds(ids: List[String]): Enumerator[String] =
|
||||
|
|
|
@ -74,6 +74,6 @@ object HTTPRequest {
|
|||
|
||||
def isHttp10(req: RequestHeader) = req.version == "HTTP/1.0"
|
||||
|
||||
def acceptsNdJson(req: RequestHeader) =
|
||||
req.headers get HeaderNames.ACCEPT contains "application/x-ndjson"
|
||||
def acceptsNdJson(req: RequestHeader) = req.headers get HeaderNames.ACCEPT contains "application/x-ndjson"
|
||||
def acceptsJson(req: RequestHeader) = req.headers get HeaderNames.ACCEPT contains "application/json"
|
||||
}
|
||||
|
|
|
@ -36,19 +36,6 @@ final class PgnDump(
|
|||
}
|
||||
}
|
||||
|
||||
private val fileR = """[\s,]""".r
|
||||
|
||||
def filename(game: Game): Fu[String] = gameLightUsers(game) map {
|
||||
case (wu, bu) => fileR.replaceAllIn(
|
||||
"lichess_pgn_%s_%s_vs_%s.%s.pgn".format(
|
||||
Tag.UTCDate.format.print(game.createdAt),
|
||||
player(game.whitePlayer, wu),
|
||||
player(game.blackPlayer, bu),
|
||||
game.id
|
||||
), "_"
|
||||
)
|
||||
}
|
||||
|
||||
private def gameUrl(id: String) = s"$netBaseUrl/$id"
|
||||
|
||||
private def gameLightUsers(game: Game): Fu[(Option[LightUser], Option[LightUser])] =
|
||||
|
@ -56,7 +43,7 @@ final class PgnDump(
|
|||
|
||||
private def rating(p: Player) = p.rating.fold("?")(_.toString)
|
||||
|
||||
private def player(p: Player, u: Option[LightUser]) =
|
||||
def player(p: Player, u: Option[LightUser]) =
|
||||
p.aiLevel.fold(u.fold(p.name | lila.user.User.anonymous)(_.name))("lichess AI level " + _)
|
||||
|
||||
private val customStartPosition: Set[chess.variant.Variant] =
|
||||
|
|
Loading…
Reference in New Issue