sport/app/football/controllers/MatchController.scala (196 lines of code) (raw):
package football.controllers
import common._
import feed.CompetitionsService
import implicits.{Football, HtmlFormat, JsonFormat, Requests}
import model.Cached.{RevalidatableResult, WithoutRevalidationResult}
import model.TeamMap.findTeamIdByUrlName
import football.datetime.DateHelpers
import model._
import pa.{FootballMatch, LineUp, LineUpTeam, MatchDayTeam}
import play.api.libs.json._
import play.api.mvc.{Action, AnyContent, BaseController, ControllerComponents}
import conf.Configuration
import football.model.{DotcomRenderingFootballMatchSummaryDataModel, GuTeamCodes}
import play.api.libs.ws.WSClient
import renderers.DotcomRenderingService
import services.dotcomrendering.{FootballSummaryPagePicker, RemoteRender}
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import scala.concurrent.Future
case class MatchPage(theMatch: FootballMatch, lineUp: LineUp) extends StandalonePage with Football {
lazy val matchStarted = theMatch.isLive || theMatch.isResult
lazy val hasLineUp = lineUp.awayTeam.players.nonEmpty && lineUp.homeTeam.players.nonEmpty
def teamHasStats(team: LineUpTeam): Boolean =
(team.offsides, team.shotsOn, team.shotsOff, team.fouls) match {
case (0, 0, 0, 0) => false
case _ => true
}
lazy val hasPaStats: Boolean = teamHasStats(lineUp.homeTeam) && teamHasStats(lineUp.awayTeam)
private val id = MatchUrl(theMatch)
private val javascriptConfig: Map[String, JsValue] = Map(
"footballMatch" -> JsObject(
Seq(
"id" -> JsString(theMatch.id),
"dateInMillis" -> JsNumber(theMatch.date.toInstant.toEpochMilli),
"homeTeam" -> JsString(theMatch.homeTeam.id),
"awayTeam" -> JsString(theMatch.awayTeam.id),
"isLive" -> JsBoolean(theMatch.isLive),
),
),
)
override val metadata = MetaData.make(
id = id,
section = Some(SectionId.fromId("football")),
webTitle = s"${theMatch.homeTeam.name} ${theMatch.homeTeam.score.getOrElse("")} - ${theMatch.awayTeam.score
.getOrElse("")} ${theMatch.awayTeam.name}",
javascriptConfigOverrides = javascriptConfig,
)
}
sealed trait NsAnswer
case class EventAnswer(eventTime: String, eventType: String) extends NsAnswer
case class PlayerAnswer(
id: String,
name: String,
position: String,
lastName: String,
substitute: Boolean,
timeOnPitch: String,
shirtNumber: String,
events: Seq[EventAnswer],
) extends NsAnswer
case class TeamAnswer(
id: String,
name: String,
players: Seq[PlayerAnswer],
score: Option[Int],
scorers: List[String],
possession: Int,
shotsOn: Int,
shotsOff: Int,
corners: Int,
fouls: Int,
colours: String,
crest: String,
codename: String,
) extends NsAnswer
case class MatchDataAnswer(id: String, homeTeam: TeamAnswer, awayTeam: TeamAnswer, comments: Option[String])
extends NsAnswer
object NsAnswer {
val reportedEventTypes = List("booking", "dismissal", "substitution")
def makePlayers(team: LineUpTeam): Seq[PlayerAnswer] = {
team.players.map { player =>
val events = player.events.filter(event => NsAnswer.reportedEventTypes.contains(event.eventType)).map { event =>
EventAnswer(event.eventTime, event.eventType)
}
PlayerAnswer(
player.id,
player.name,
player.position,
player.lastName,
player.substitute,
player.timeOnPitch,
player.shirtNumber,
events,
)
}
}
def makeTeamAnswer(teamV1: MatchDayTeam, teamV2: LineUpTeam, teamPossession: Int, teamColour: String): TeamAnswer = {
val players = makePlayers(teamV2)
TeamAnswer(
teamV1.id,
teamV1.name,
players = players,
score = teamV1.score,
scorers = teamV1.scorers.fold(Nil: List[String])(_.split(",").toList),
possession = teamPossession,
shotsOn = teamV2.shotsOn,
shotsOff = teamV2.shotsOff,
corners = teamV2.corners,
fouls = teamV2.fouls,
colours = teamColour,
crest = s"${Configuration.staticSport.path}/football/crests/120/${teamV1.id}.png",
codename = GuTeamCodes.codeFor(teamV1),
)
}
def makeFromFootballMatch(theMatch: FootballMatch, lineUp: LineUp): MatchDataAnswer = {
val teamColours = TeamColours(lineUp.homeTeam, lineUp.awayTeam)
MatchDataAnswer(
theMatch.id,
makeTeamAnswer(theMatch.homeTeam, lineUp.homeTeam, lineUp.homeTeamPossession, teamColours.home),
makeTeamAnswer(theMatch.awayTeam, lineUp.awayTeam, lineUp.awayTeamPossession, teamColours.away),
theMatch.comments,
)
}
implicit val EventAnswerWrites: Writes[EventAnswer] = Json.writes[EventAnswer]
implicit val PlayerAnswerWrites: Writes[PlayerAnswer] = Json.writes[PlayerAnswer]
implicit val TeamAnswerWrites: Writes[TeamAnswer] = Json.writes[TeamAnswer]
implicit val MatchDataAnswerWrites: Writes[MatchDataAnswer] = Json.writes[MatchDataAnswer]
}
// --------------------------------------------------------------
class MatchController(
competitionsService: CompetitionsService,
val wsClient: WSClient,
val controllerComponents: ControllerComponents,
)(implicit context: ApplicationContext)
extends BaseController
with Football
with Requests
with GuLogging
with ImplicitControllerExecutionContext {
val remoteRenderer: DotcomRenderingService = DotcomRenderingService()
def renderMatchIdJson(matchId: String): Action[AnyContent] = renderMatchId(matchId)
def renderMatchId(matchId: String): Action[AnyContent] = render(competitionsService.findMatch(matchId))
def renderMatchJson(year: String, month: String, day: String, home: String, away: String): Action[AnyContent] =
renderMatch(year, month, day, home, away)
def renderMatch(year: String, month: String, day: String, home: String, away: String): Action[AnyContent] =
(findTeamIdByUrlName(home), findTeamIdByUrlName(away)) match {
case (Some(homeId), Some(awayId)) =>
val formatter = DateTimeFormatter.ofPattern("yyyyMMMdd").withZone(DateHelpers.defaultFootballZoneId)
val date = LocalDate.parse(s"$year${month.capitalize}$day", formatter)
val startOfDay = date.atStartOfDay(DateHelpers.defaultFootballZoneId)
val startOfTomorrow = startOfDay.plusDays(1)
render(competitionsService.matchFor(Interval(startOfDay, startOfTomorrow), homeId, awayId))
case _ => render(None)
}
private def render(maybeMatch: Option[FootballMatch]): Action[AnyContent] =
Action.async { implicit request =>
maybeMatch match {
case Some(theMatch) =>
val lineup: Future[LineUp] = competitionsService.getLineup(theMatch)
val page: Future[MatchPage] = lineup.map(MatchPage(theMatch, _))
val tier = FootballSummaryPagePicker.getTier()
page.flatMap { page =>
val footballMatch = NsAnswer.makeFromFootballMatch(theMatch, page.lineUp)
request.getRequestFormat match {
case JsonFormat if request.forceDCR =>
val model = DotcomRenderingFootballMatchSummaryDataModel(
page = page,
footballMatch = footballMatch,
)
Future.successful(Cached(CacheTime.FootballMatch)(JsonComponent.fromWritable(model)))
case JsonFormat =>
Future.successful(Cached(CacheTime.FootballMatch) {
JsonComponent(football.views.html.matchStats.matchStatsComponent(page))
})
case HtmlFormat if tier == RemoteRender =>
val model = DotcomRenderingFootballMatchSummaryDataModel(
page = page,
footballMatch = footballMatch,
)
remoteRenderer.getFootballMatchSummaryPage(
wsClient,
DotcomRenderingFootballMatchSummaryDataModel.toJson(model),
)
case _ =>
Future.successful(Cached(CacheTime.FootballMatch) {
RevalidatableResult.Ok(
football.views.html.matchStats
.matchStatsPage(page, competitionsService.competitionForMatch(theMatch.id)),
)
})
}
}
case None =>
Future.successful(Cached(CacheTime.FootballMatch)(WithoutRevalidationResult(Found("/football/results"))))
}
}
}