sport/app/football/feed/Competitions.scala (427 lines of code) (raw):

package feed import com.github.nscala_time.time.Imports import com.github.nscala_time.time.Imports._ import common._ import conf.FootballClient import football.controllers.Interval import model.{Competition, Table, TeamFixture, TeamNameBuilder} import org.joda.time.DateTimeComparator import pa.{FootballMatch, _} import java.time.{Clock, LocalDate, ZonedDateTime} import java.util.Comparator import scala.collection.immutable import scala.concurrent.{ExecutionContext, Future} import scala.math.Ordering.Implicits._ trait Competitions extends implicits.Football { implicit val localDateOrdering: Ordering[LocalDate] = Ordering.by(_.toEpochDay) def competitions: Seq[Competition] def competitionsWithCompetitionFilter(path: String): Competitions = Competitions( competitions.filter(_.url == path), ) def competitionsWithTag(tag: String): Option[Competition] = competitions.find(_.url.endsWith(s"/$tag")) def competitionsWithId(compId: String): Option[Competition] = competitions.find(_.id == compId) lazy val competitionsWithTodaysMatchesAndFutureFixtures = Competitions( competitions .map(c => c.copy(matches = c.matches.filter(m => m.isFixture || m.isOn(LocalDate.now())))) .filter(_.hasMatches), ) lazy val competitionsWithTodaysMatchesAndPastResults = Competitions( competitions .map(c => c.copy(matches = c.matches.filter(m => m.isResult || m.isOn(LocalDate.now())))) .filter(_.hasMatches), ) lazy val withTodaysMatches = Competitions( competitions.map(c => c.copy(matches = c.matches.filter(_.isOn(LocalDate.now())))).filter(_.hasMatches), ) def withTeam(team: String): Competitions = Competitions( competitions.filter(_.hasLeagueTable).filter(_.leagueTable.exists(_.team.id == team)), ) def mostPertinentCompetitionForTeam(teamId: String): Option[Competition] = withTeam(teamId).competitions .sortBy({ competition => val table = Table(competition) val group = table.groups.find(_.entries.exists(_.team.id == teamId)) -(group.map(_.entries.length) getOrElse competition.leagueTable.length) }) .headOption lazy val matchDates = competitions.flatMap(_.matchDates).distinct.sorted def nextMatchDates(startDate: LocalDate, numDays: Int): Seq[LocalDate] = matchDates.filter(_ >= startDate).take(numDays) def previousMatchDates(date: LocalDate, numDays: Int): Seq[LocalDate] = matchDates.reverse.filter(_ <= date).take(numDays) def findMatch(id: String): Option[FootballMatch] = matches.find(_.id == id) def competitionForMatch(matchId: String): Option[Competition] = competitions.find(_.matches.exists(_.id == matchId)) def withTeamMatches(teamId: String): Seq[TeamFixture] = competitions .filter(_.hasMatches) .flatMap(c => c.matches.filter(m => m.homeTeam.id == teamId || m.awayTeam.id == teamId).sortByDate.map { m => TeamFixture(c, m) }, ) def findTeam(teamId: String): Option[FootballTeam] = competitions.flatMap(_.teams).find(_.id == teamId).map { unclean => MatchDayTeam(teamId, unclean.name, None, None, None, None) } def matchFor(date: LocalDate, homeTeamId: String, awayTeamId: String): Option[FootballMatch] = matches.find(m => m.homeTeam.id == homeTeamId && m.awayTeam.id == awayTeamId && m.date.toLocalDate == date) // note team1 & team2 are the home and away team, but we do NOT know their order def matchFor(interval: Interval, team1: String, team2: String): Option[FootballMatch] = { matches .filter(m => interval.contains(m.date)) .find(m => m.hasTeam(team1) && m.hasTeam(team2)) } def sortedMatches: Seq[FootballMatch] = matches.sortByDate def matches: Seq[FootballMatch] = competitions.flatMap(_.matches) def isMatchLiveOrAboutToStart(matches: Seq[FootballMatch], clock: Clock): Boolean = matches.exists(game => { val currentTime = ZonedDateTime.now(clock) game.isLive || (game.date.minusMinutes(5).isBefore(currentTime) && game.date.plusMinutes(15).isAfter(currentTime)) }) } object Competitions { def apply(comps: Seq[Competition]): Competitions = new Competitions { val competitions = comps } } // when updating this code, think about whether the mobile apps api needs to be updated too // https://github.com/guardian/mobile-apps-api // common-pa-feeds/src/main/scala/com/gu/mobile/football/data/pa/PaCompetitions.scala // Ordering is important! Competitions at the top of this list will be shown before competitions on the bottom // on pages such as /football/fixtures object CompetitionsProvider { val allCompetitions: Seq[Competition] = Seq( Competition( "700", "/football/world-cup-2022", "World Cup 2022", "World Cup 2022", "Internationals", showInTeamsList = true, tableDividers = List(2), startDate = Some(LocalDate.of(2022, 11, 1)), finalMatchSVG = Some("world_cup_2022_badge"), ), Competition( "423", "/football/women-s-euro-2022", "Women's Euro 2022", "Women's Euro 2022", "Internationals", showInTeamsList = true, tableDividers = List(2), startDate = Some(LocalDate.of(2022, 7, 1)), finalMatchSVG = Some("womens_euros_2022_badge"), ), Competition( "870", "/football/womens-world-cup", "Women's World Cup 2023", "Women's World Cup 2023", "Internationals", showInTeamsList = true, tableDividers = List(2), startDate = Some(LocalDate.of(2023, 7, 20)), finalMatchSVG = Some("womens_world_cup_2023_badge"), ), Competition( "100", "/football/premierleague", "Premier League", "Premier League", "English", showInTeamsList = true, tableDividers = List(4, 5, 17), ), Competition( "625", "/football/bundesligafootball", "Bundesliga", "Bundesliga", "European", showInTeamsList = true, tableDividers = List(3, 4, 6, 15, 16), ), Competition( "635", "/football/serieafootball", "Serie A", "Serie A", "European", showInTeamsList = true, tableDividers = List(4, 6, 17), ), Competition( "650", "/football/laligafootball", "La Liga", "La Liga", "European", showInTeamsList = true, tableDividers = List(4, 6, 17), ), Competition( "620", "/football/ligue1football", "Ligue 1", "Ligue 1", "European", showInTeamsList = true, tableDividers = List(3, 4, 16), ), Competition("961", "/football/womens-super-league", "Women's Super League", "Women's Super League", "English"), Competition( "500", "/football/championsleague", "Champions League", "Champions League", "European", tableDividers = List(8, 24), ), Competition( "750", // This ID was also used for the 2020 Euros "/football/euro-2024", "Euro 2024", "Euro 2024", "Internationals", showInTeamsList = true, tableDividers = List(2), startDate = Some(LocalDate.of(2024, 6, 14)), ), Competition( "751", "/football/euro-2024", "Euro 2024 qualifying", "Euro 2024 qualifying", "Internationals", showInTeamsList = true, startDate = Some(LocalDate.of(2023, 3, 23)), ), Competition( "701", "/football/world-cup-2026-qualifiers", "World Cup 2026 qualifying", "World Cup 2026 qual.", "Internationals", ), Competition( "510", "/football/uefa-europa-league", "Europa League", "Europa League", "European", showInTeamsList = true, tableDividers = List(8, 24), ), Competition( "995", "/football/women-s-nations-league", "Women's Nations League", "Women's Nations League", "European", showInTeamsList = true, tableDividers = List(2), ), Competition("301", "/football/carabao-cup", "Carabao Cup", "Carabao Cup", "English"), Competition("721", "/football/friendlies", "International friendlies", "Friendlies", "Internationals"), Competition("300", "/football/fa-cup", "FA Cup", "FA Cup", "English"), Competition( "101", "/football/championship", "Championship", "Championship", "English", showInTeamsList = true, tableDividers = List(2, 6, 21), ), Competition( "120", "/football/scottish-premiership", "Scottish Premiership", "Scottish Premiership", "Scottish", showInTeamsList = true, tableDividers = List(1, 3, 6, 11), ), Competition( "102", "/football/leagueonefootball", "League One", "League One", "English", showInTeamsList = true, tableDividers = List(2, 6, 20), ), Competition( "103", "/football/leaguetwofootball", "League Two", "League Two", "English", showInTeamsList = true, tableDividers = List(3, 7, 22), ), Competition( "121", "/football/scottish-championship", "Scottish Championship", "Scottish Championship", "Scottish", showInTeamsList = true, tableDividers = List(1, 8, 9), ), Competition( "122", "/football/scottish-league-one", "Scottish League One", "Scottish League One", "Scottish", showInTeamsList = true, tableDividers = List(1, 4, 8, 9), ), Competition( "123", "/football/scottish-league-two", "Scottish League Two", "Scottish League Two", "Scottish", showInTeamsList = true, tableDividers = List(1, 4), ), Competition( "501", "/football/champions-league-qualifying", "Champions League qualifying", "Champions League qual.", "European", ), Competition( "400", "/football/community-shield", "Community Shield", "Community Shield", "English", showInTeamsList = true, ), Competition("320", "/football/scottishcup", "Scottish Cup", "Scottish Cup", "Scottish"), Competition("321", "/football/cis-insurance-cup", "Scottish League Cup", "Scottish League Cup", "Scottish"), Competition( "994", "/football/nations-league", "Nations League", "Nations League", "Internationals", showInTeamsList = true, ), Competition( "713", "/football/africannationscup", "Africa Cup of Nations", "Africa Cup of Nations", "Internationals", showInTeamsList = true, tableDividers = Seq(2, 3), ), Competition( "714", "/football/copa-america", "Copa America", "Copa America", "Internationals", showInTeamsList = true, tableDividers = Seq(2, 3), ), Competition( "970", "/football/women-s-champions-league", "Women's Champions League", "Women's Champions League", "European", ), Competition("333", "/football/womens-fa-cup", "Women's FA Cup", "Women's FA Cup", "English"), Competition( "516", "/football/europa-conference-league", "Europa Conference League", "Europa Conference League", "European", showInTeamsList = true, tableDividers = List(2), ), ) } class CompetitionsService(val footballClient: FootballClient, competitionDefinitions: Seq[Competition]) extends Competitions with LiveMatches with Lineups with GuLogging with implicits.Football { private implicit val dateOrdering: Ordering[Imports.DateTime] = Ordering.comparatorToOrdering( DateTimeComparator.getInstance.asInstanceOf[Comparator[DateTime]], ) // Avoid fetching very old results from PA by restricting to most recent season private def oldestRelevantCompetitionSeasons(competitions: List[Season]): List[Season] = competitionDefinitions.flatMap { compDef => competitions .filter(_.competitionId == compDef.id) .sortBy(_.startDate.atStartOfDay().toLocalDate) .reverse .headOption // get most recent season only } override val teamNameBuilder = new TeamNameBuilder(this) val competitionAgents = competitionDefinitions map { new CompetitionAgent(footballClient, teamNameBuilder, _) } val competitionIds: Seq[String] = competitionDefinitions map { _.id } override def competitions: Seq[Competition] = competitionAgents.map(_.competition) def refreshCompetitionAgent(id: String, clock: Clock)(implicit executionContext: ExecutionContext): Unit = competitionAgents .find { _.competition.id == id } .foreach { c => c.refresh(clock) log.info( s"Completed refresh of competition '${c.competition.fullName}': currently ${c.competition.matches.length} matches", ) } def refreshCompetitionData()(implicit executionContext: ExecutionContext): Future[Unit] = { log.info("Refreshing competition data") footballClient.competitions .map { allComps => oldestRelevantCompetitionSeasons(allComps).foreach { season => competitionAgents.find(_.competition.id == season.id).foreach { agent => agent.competition.startDate match { case Some(existingStartDate) if season.startDate.isAfter(existingStartDate.atStartOfDay().toLocalDate) => log.info( s"updating competition: ${season.id} season: ${season.seasonId} startDate was: ${existingStartDate.toString} now: ${season.startDate.toString}", ) agent.update(agent.competition.copy(startDate = Some(season.startDate))) case None => log.info( s"setting competition: ${season.id} season: ${season.seasonId} startDate was: None now: ${season.startDate.toString}", ) agent.update(agent.competition.copy(startDate = Some(season.startDate))) case _ => } } } } .recover(footballClient.logErrorsWithMessage("Failed refreshing competitions data")) } def refreshMatchDay( clock: Clock, )(implicit executionContext: ExecutionContext): Future[immutable.Iterable[Competition]] = { log.info("Refreshing match day data") val result = getLiveMatches(clock).map(_.map { case (compId, newMatches) => competitionAgents.find(_.competition.id == compId).map { agent => agent.addMatches(newMatches) } }) result.map(_.flatten).flatMap(Future.sequence(_)) } def maybeRefreshLiveMatches( clock: Clock, )(implicit executionContext: ExecutionContext): Future[immutable.Iterable[Competition]] = { // matches is the list of all matches from all competitions if (isMatchLiveOrAboutToStart(matches, clock)) { log.info("Match is in Progress - refreshing match day data") refreshMatchDay(clock) } else { Future.successful(immutable.Iterable[Competition]()) } } }