lila/modules/relay/src/main/RelayFormat.scala

136 lines
4.2 KiB
Scala

package lila.relay
import io.lemonlabs.uri._
import play.api.libs.json._
import play.api.libs.ws.StandaloneWSClient
import scala.concurrent.duration._
import lila.study.MultiPgn
import lila.memo.CacheApi
import lila.memo.CacheApi._
final private class RelayFormatApi(ws: StandaloneWSClient, cacheApi: CacheApi)(implicit
ec: scala.concurrent.ExecutionContext
) {
import RelayFormat._
import RelayRound.Sync.UpstreamUrl
private val cache = cacheApi[UpstreamUrl.WithRound, RelayFormat](8, "relay.format") {
_.refreshAfterWrite(10 minutes)
.expireAfterAccess(20 minutes)
.buildAsyncFuture(guessFormat)
}
def get(upstream: UpstreamUrl.WithRound): Fu[RelayFormat] = cache get upstream
def refresh(upstream: UpstreamUrl.WithRound): Unit = cache invalidate upstream
private def guessFormat(upstream: UpstreamUrl.WithRound): Fu[RelayFormat] = {
val originalUrl = Url parse upstream.url
// http://view.livechesscloud.com/ed5fb586-f549-4029-a470-d590f8e30c76
def guessLcc(url: Url): Fu[Option[RelayFormat]] =
url.toString match {
case UpstreamUrl.LccRegex(id) =>
guessManyFiles(
Url.parse(
s"http://1.pool.livechesscloud.com/get/$id/round-${upstream.round | 1}/index.json"
)
)
case _ => fuccess(none)
}
def guessSingleFile(url: Url): Fu[Option[RelayFormat]] =
lila.common.Future.find(
List(
url.some,
!url.path.parts.contains(mostCommonSingleFileName) option addPart(url, mostCommonSingleFileName)
).flatten.distinct
)(looksLikePgn) dmap2 { (u: Url) =>
SingleFile(pgnDoc(u))
}
def guessManyFiles(url: Url): Fu[Option[RelayFormat]] =
lila.common.Future.find(
List(url) ::: mostCommonIndexNames.filterNot(url.path.parts.contains).map(addPart(url, _))
)(looksLikeJson) flatMap {
_ ?? { index =>
val jsonUrl = (n: Int) => jsonDoc(replaceLastPart(index, s"game-$n.json"))
val pgnUrl = (n: Int) => pgnDoc(replaceLastPart(index, s"game-$n.pgn"))
looksLikeJson(jsonUrl(1).url).map(_ option jsonUrl) orElse
looksLikePgn(pgnUrl(1).url).map(_ option pgnUrl) dmap2 {
ManyFiles(index, _)
}
}
}
guessLcc(originalUrl) orElse
guessSingleFile(originalUrl) orElse
guessManyFiles(originalUrl) orFail "No games found, check your source URL"
} addEffect { format =>
logger.info(s"guessed format of $upstream: $format")
}
private def httpGet(url: Url): Fu[Option[String]] =
ws.url(url.toString)
.withRequestTimeout(4.seconds)
.get()
.map {
case res if res.status == 200 => res.body.some
case _ => none
}
private def looksLikePgn(body: String): Boolean =
MultiPgn.split(body, 1).value.headOption ?? { pgn =>
lila.study.PgnImport(pgn, Nil).isValid
}
private def looksLikePgn(url: Url): Fu[Boolean] = httpGet(url).map { _ exists looksLikePgn }
private def looksLikeJson(body: String): Boolean =
try {
Json.parse(body) != JsNull
} catch {
case _: Exception => false
}
private def looksLikeJson(url: Url): Fu[Boolean] = httpGet(url).map { _ exists looksLikeJson }
}
sealed private trait RelayFormat
private object RelayFormat {
sealed trait DocFormat
object DocFormat {
case object Json extends DocFormat
case object Pgn extends DocFormat
}
case class Doc(url: Url, format: DocFormat)
def jsonDoc(url: Url) = Doc(url, DocFormat.Json)
def pgnDoc(url: Url) = Doc(url, DocFormat.Pgn)
case class SingleFile(doc: Doc) extends RelayFormat
type GameNumberToDoc = Int => Doc
case class ManyFiles(jsonIndex: Url, game: GameNumberToDoc) extends RelayFormat {
override def toString = s"Manyfiles($jsonIndex, ${game(0)})"
}
def addPart(url: Url, part: String) = url.withPath(url.path addPart part)
def replaceLastPart(url: Url, withPart: String) =
if (url.path.isEmpty) addPart(url, withPart)
else
url.withPath {
url.path.withParts {
url.path.parts.init :+ withPart
}
}
val mostCommonSingleFileName = "games.pgn"
val mostCommonIndexNames = List("round.json", "index.json")
}