Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 28 additions & 14 deletions app/controllers/Tv.scala
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package controllers

import play.api.http.ContentTypes

import scala.util.chaining.*
import play.api.mvc.Result
import play.api.libs.json.*
import views.*

import lila.app.{ given, * }
import lila.app.{ *, given }
import lila.game.Pov
import lila.tv.Tv.Channel
import lila.common.Json.given
Expand Down Expand Up @@ -90,22 +91,35 @@ final class Tv(env: Env, apiC: => Api, gameC: => Game) extends LilaController(en
}
}

def feed = Anon:
def feedDefault = Anon:
serveFeedFromChannel(Channel.Best)

def feed(chanKey: String) = Anon:
Channel.byKey.get(chanKey) so serveFeedFromChannel

private def serveFeedFromChannel(channel: Channel)(using Context): Fu[Result] =
import makeTimeout.short
import akka.pattern.ask
import lila.round.TvBroadcast
import play.api.libs.EventSource
import lila.tv.TvBroadcast
val bc = getBool("bc")
val ctag = summon[scala.reflect.ClassTag[TvBroadcast.SourceType]]
env.round.tvBroadcast ? TvBroadcast.Connect(bc) mapTo ctag map { source =>
if bc then
Ok.chunked(source via EventSource.flow log "Tv.feed")
.as(ContentTypes.EVENT_STREAM) pipe noProxyBuffer
else jsToNdJson(source)
}
env.tv.channelBroadcasts.get(channel) so: actor =>
actor ? TvBroadcast.Connect(bc) mapTo ctag map { source =>
if bc then
Ok.chunked(source via EventSource.flow log "Tv.feed")
.as(ContentTypes.EVENT_STREAM) pipe noProxyBuffer
else jsToNdJson(source)
}

def frameDefault = Anon:
serveFrameFromChannel(Channel.Best)

def frame(chanKey: String) = Anon:
Channel.byKey.get(chanKey) so serveFrameFromChannel

def frame = Anon:
env.tv.tv.getBestGame.flatMap:
_.fold(notFoundText()): game =>
private def serveFrameFromChannel(channel: Channel)(using Context) =
env.tv.tv.getGame(channel) flatMap:
_.fold(notFoundText()): g =>
InEmbedContext:
Ok(views.html.tv.embed(Pov naturalOrientation game))
Ok(views.html.tv.embed(Pov naturalOrientation g, channel.key.some))
5 changes: 3 additions & 2 deletions app/views/game/mini.scala
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ object mini:
showRatings = ctx.pref.showRatings
)

def noCtx(pov: Pov, tv: Boolean = false): Tag =
val link = if tv then routes.Tv.index else routes.Round.watcher(pov.gameId, pov.color.name)
def noCtx(pov: Pov, tv: Boolean = false, channelKey: Option[String] = None): Tag =
val link = if tv then channelKey.fold(routes.Tv.index) { routes.Tv.onChannel }
else routes.Round.watcher(pov.gameId, pov.color.name)
renderMini(pov, link.url.some)(using defaultLang, None)

private def renderMini(
Expand Down
9 changes: 5 additions & 4 deletions app/views/tv/embed.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,17 @@ import lila.app.ui.ScalatagsTemplate.*

object embed:

private val dataStreamUrl = attr("data-stream-url") := "/tv/feed?bc=1"
private val defaultDataStreamUrl = "/tv/feed?bc=1"

def apply(pov: lila.game.Pov)(using EmbedContext) =
def apply(pov: lila.game.Pov, channelKey: Option[String])(using EmbedContext) =
val dataStreamUrl = channelKey.fold(defaultDataStreamUrl)(key => s"/tv/${key}/feed?bc=1")
views.html.base.embed(
title = "lichess.org chess TV",
cssModule = "tv.embed"
)(
dataStreamUrl,
attr("data-stream-url") := dataStreamUrl,
div(id := "featured-game", cls := "embedded", title := "lichess.org TV")(
views.html.game.mini.noCtx(pov, tv = true)(targetBlank)
views.html.game.mini.noCtx(pov, tv = true, channelKey)(targetBlank)
),
cashTag,
chessgroundTag,
Expand Down
9 changes: 6 additions & 3 deletions conf/routes
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,19 @@ POST /bookmark/$gameId<\w{8}> controllers.Game.bookmark(gameId)

# TV
GET /$lang<\w\w\w?>/tv controllers.Tv.indexLang(lang)
GET /tv/frame controllers.Tv.frame
GET /tv/feed controllers.Tv.feed
GET /tv/frame controllers.Tv.frameDefault
GET /tv/feed controllers.Tv.feedDefault
GET /tv/channels controllers.Main.movedPermanently(to = "/api/tv/channels")
GET /tv/:chanKey controllers.Tv.onChannel(chanKey)
GET /tv/:chanKey/frame controllers.Tv.frame(chanKey)
GET /tv/:chanKey/feed controllers.Tv.feed(chanKey)
GET /tv/$gameId<\w{8}>/$color<white|black>/sides controllers.Tv.sides(gameId, color)
GET /games controllers.Tv.games
GET /games/:chanKey controllers.Tv.gamesChannel(chanKey)
GET /games/:chanKey/replacement/$gameId<\w{8}> controllers.Tv.gameChannelReplacement(chanKey, gameId, exclude: List[String])
GET /api/tv/channels controllers.Tv.channels
GET /api/tv/feed controllers.Tv.feed
GET /api/tv/feed controllers.Tv.feedDefault
GET /api/tv/:chanKey/feed controllers.Tv.feed(chanKey)
GET /api/tv/:chanKey controllers.Tv.apiGamesChannel(chanKey)

# Relation
Expand Down
3 changes: 0 additions & 3 deletions modules/hub/src/main/actorApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -182,9 +182,6 @@ package timeline:
def modsOnly(value: Boolean) = add(ModsOnly(value))
private def add(p: Propagation) = copy(propagations = p :: propagations)

package tv:
case class TvSelect(gameId: GameId, speed: chess.Speed, data: JsObject)

package notify:
case class NotifiedBatch(userIds: Iterable[UserId])

Expand Down
2 changes: 0 additions & 2 deletions modules/round/src/main/Env.scala
Original file line number Diff line number Diff line change
Expand Up @@ -191,8 +191,6 @@ final class Env(

val playing = wire[PlayingUsers]

val tvBroadcast = system.actorOf(Props(wire[TvBroadcast]))

val apiMoveStream = wire[ApiMoveStream]

def resign(pov: Pov): Unit =
Expand Down
3 changes: 1 addition & 2 deletions modules/round/src/main/RoundSocket.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import lila.game.{ Event, Game, Pov }
import lila.hub.actorApi.map.{ Exists, Tell, TellAll, TellIfExists, TellMany }
import lila.hub.actorApi.round.{ Abort, Berserk, Rematch, Resign, TourStanding }
import lila.hub.actorApi.socket.remote.{ TellSriIn, TellSriOut }
import lila.hub.actorApi.tv.TvSelect
import lila.hub.AsyncActorConcMap
import lila.room.RoomSocket.{ Protocol as RP, * }
import lila.socket.RemoteSocket.{ Protocol as P, * }
Expand Down Expand Up @@ -181,7 +180,7 @@ final class RoundSocket(
"finishGame",
"roundUnplayed"
):
case TvSelect(gameId, speed, json) =>
case actorApi.TvSelect(gameId, speed, _, json) =>
sendForGameId(gameId)(Protocol.Out.tvSelect(gameId, speed, json))
case Tell(id, e @ BotConnected(color, v)) =>
val gameId = GameId(id)
Expand Down
3 changes: 3 additions & 0 deletions modules/round/src/main/actorApi.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package lila.round
package actorApi

import play.api.libs.json.JsObject
import chess.format.Uci
import chess.{ Color, MoveMetrics }

Expand All @@ -26,6 +27,8 @@ case class GameAndSocketStatus(game: lila.game.Game, socket: SocketStatus)
case class RoomCrowd(white: Boolean, black: Boolean)
case class BotConnected(color: Color, v: Boolean)

case class TvSelect(gameId: GameId, speed: chess.Speed, channel: String, data: JsObject)

package round:

case class HumanPlay(
Expand Down
12 changes: 8 additions & 4 deletions modules/tv/src/main/Env.scala
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package lila.tv

import akka.actor.ActorSystem
import akka.actor.{ ActorSystem, Props }
import com.softwaremill.macwire.*
import akka.actor.ActorRef
import lila.tv.Tv.Channel

@Module
@annotation.nowarn("msg=unused")
final class Env(
gameRepo: lila.game.GameRepo,
renderer: lila.hub.actors.Renderer,
Expand All @@ -20,6 +21,9 @@ final class Env(

lazy val tv = wire[Tv]

system.scheduler.scheduleWithFixedDelay(12 seconds, 3 seconds) { () =>
val channelBroadcasts: Map[Channel, ActorRef] = Tv.Channel.values.map { c =>
c -> system.actorOf(Props(wire[TvBroadcast]))
}.toMap

system.scheduler.scheduleWithFixedDelay(12 seconds, 3 seconds): () =>
tvSyncActor ! TvSyncActor.Select
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
package lila.round
package lila.tv

import akka.actor.*
import akka.stream.scaladsl.*
import chess.format.Fen
import play.api.libs.json.*

import lila.common.{ Bus, LightUser }
import lila.Lila.{ GameId, none }
import lila.common.Json.given
import lila.common.{ Bus, LightUser }
import lila.game.Pov
import lila.game.actorApi.MoveGameEvent
import lila.round.actorApi.TvSelect
import lila.socket.Socket
import play.api.libs.json.*

final private class TvBroadcast(
lightUserSync: LightUser.GetterSync
lightUserSync: LightUser.GetterSync,
channel: Tv.Channel,
gameProxyRepo: lila.round.GameProxyRepo
) extends Actor:

import TvBroadcast.*
Expand All @@ -20,7 +24,7 @@ final private class TvBroadcast(

private var featured = none[Featured]

Bus.subscribe(self, "changeFeaturedGame")
Bus.subscribe(self, "tvSelect")

given Executor = context.system.dispatcher

Expand All @@ -47,33 +51,33 @@ final private class TvBroadcast(
case Add(client) => clients = clients + client
case Remove(client) => clients = clients - client

case ChangeFeatured(pov, msg) =>
unsubscribeFromFeaturedId()
Bus.subscribe(self, MoveGameEvent makeChan pov.gameId)
val feat = Featured(
pov.gameId,
Json.obj(
"id" -> pov.gameId,
"orientation" -> pov.color.name,
"players" -> pov.game.players.mapList { p =>
val user = p.userId.flatMap(lightUserSync)
Json
.obj("color" -> p.color.name)
.add("user" -> user.map(LightUser.write))
.add("ai" -> p.aiLevel)
.add("rating" -> p.rating)
.add("seconds" -> pov.game.clock.map(_.remainingTime(pov.color).roundSeconds))
}
),
fen = Fen write pov.game.situation
)
clients.foreach { client =>
client.queue offer {
if client.fromLichess then msg
else feat.socketMsg
}
case TvSelect(gameId, speed, chanKey, data) if chanKey == channel.key =>
gameProxyRepo game gameId map2 { game =>
unsubscribeFromFeaturedId()
Bus.subscribe(self, MoveGameEvent makeChan gameId)
val pov = Pov naturalOrientation game
val feat = Featured(
gameId,
Json.obj(
"id" -> gameId,
"orientation" -> pov.color.name,
"players" -> game.players.mapList: p =>
val user = p.userId.flatMap(lightUserSync)
Json
.obj("color" -> p.color.name)
.add("user" -> user.map(LightUser.write))
.add("ai" -> p.aiLevel)
.add("rating" -> p.rating)
.add("seconds" -> game.clock.map(_.remainingTime(pov.color).roundSeconds))
),
fen = Fen write game.situation
)
clients.foreach: client =>
client.queue.offer:
if client.fromLichess then data
else feat.socketMsg
featured = feat.some
}
featured = feat.some

case MoveGameEvent(game, fen, move) =>
val msg = Socket.makeMessage(
Expand Down
2 changes: 1 addition & 1 deletion modules/tv/src/main/TvSyncActor.scala
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ final private[tv] class TvSyncActor(
"rating" -> player.rating
)
)
Bus.publish(lila.hub.actorApi.tv.TvSelect(game.id, game.speed, data), "tvSelect")
Bus.publish(lila.round.actorApi.TvSelect(game.id, game.speed, channel.key, data), "tvSelect")
if channel == Tv.Channel.Best then
actorAsk(renderer.actor, RenderFeaturedJs(game))(makeTimeout(100 millis)) foreach {
case html: String =>
Expand Down