Skip to content

Commit b808bef

Browse files
committed
Update TV to use new lichess feed endpoint
1 parent 9f674a9 commit b808bef

File tree

1 file changed

+46
-100
lines changed

1 file changed

+46
-100
lines changed

src/cli_chess/core/game/online_game/watch_tv/watch_tv_model.py

Lines changed: 46 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from cli_chess.menus.tv_channel_menu import TVChannelMenuOptions
33
from cli_chess.utils.event import Event, EventTopics
44
from cli_chess.utils.logging import log
5-
from chess import COLOR_NAMES, COLORS, Color, WHITE, BLACK
5+
from chess import COLOR_NAMES, COLORS, Color, WHITE
66
from berserk.exceptions import ResponseError
77
from time import sleep
88
from typing import Optional, Dict
@@ -25,40 +25,29 @@ def stop_watching(self):
2525
if self._tv_stream.is_alive():
2626
self._tv_stream.stop_watching()
2727

28-
def _handle_game_start(self, data: Dict):
29-
"""Parses and saves the game start data"""
30-
self.game_metadata.reset()
31-
32-
def _handle_game_end(self, data: Dict):
33-
"""Parses and saves the game end data"""
34-
3528
def _update_game_metadata(self, *args, data: Optional[Dict] = None) -> None:
3629
"""Parses and saves the data of the game being played"""
3730
if not data:
3831
return
3932
try:
40-
if EventTopics.GAME_START or EventTopics.GAME_END in args:
41-
if EventTopics.GAME_START in args:
42-
self.game_metadata.reset()
43-
33+
if EventTopics.GAME_START in args:
34+
self.game_metadata.reset()
4435
self.game_metadata.game_id = data.get('id')
45-
self.game_metadata.rated = data.get('rated')
46-
self.game_metadata.variant = data.get('variant')
47-
self.game_metadata.speed = data.get('speed')
48-
self.game_metadata.game_status.status = data.get('status')
49-
self.game_metadata.game_status.winner = data.get('winner') # Not included on draws or abort
36+
self.game_metadata.variant = self.channel
5037

51-
for color in COLOR_NAMES:
38+
for i, color in enumerate(COLOR_NAMES[::-1]):
5239
color_as_bool = Color(COLOR_NAMES.index(color))
53-
side_data = data.get('players', {}).get(color, {})
54-
player_data = side_data.get('user')
55-
ai_level = side_data.get('aiLevel')
56-
if side_data and player_data:
57-
self.game_metadata.players[color_as_bool].title = player_data.get('title')
58-
self.game_metadata.players[color_as_bool].name = player_data.get('name', "?")
59-
self.game_metadata.players[color_as_bool].rating = side_data.get('rating', "?")
60-
self.game_metadata.players[color_as_bool].is_provisional_rating = side_data.get('provisional', False)
61-
self.game_metadata.players[color_as_bool].rating_diff = side_data.get('ratingDiff', "")
40+
side_data = data.get('players', {})[i]
41+
player_data = side_data.get('user', {})
42+
ai_level = side_data.get('ai')
43+
if side_data and not ai_level:
44+
if player_data:
45+
self.game_metadata.players[color_as_bool].title = player_data.get('title')
46+
self.game_metadata.players[color_as_bool].name = player_data.get('name')
47+
self.game_metadata.players[color_as_bool].rating = side_data.get('rating', "?")
48+
self.game_metadata.players[color_as_bool].is_provisional_rating = side_data.get('provisional', False)
49+
else:
50+
self.game_metadata.players[color_as_bool].name = "Anonymous"
6251
elif ai_level:
6352
self.game_metadata.players[color_as_bool].name = f"Stockfish level {ai_level}"
6453

@@ -76,24 +65,14 @@ def stream_event_received(self, *args, data: Optional[Dict] = None, **kwargs):
7665
try:
7766
if data:
7867
if EventTopics.GAME_START in args:
79-
variant = data.get('variant', {}).get('key')
80-
white_rating = int(data.get('players', {}).get('white', {}).get('rating') or 0)
81-
black_rating = int(data.get('players', {}).get('black', {}).get('rating') or 0)
82-
orientation = WHITE if ((white_rating >= black_rating) or self.channel.key == "racingKings") else BLACK
83-
last_move = data.get('lastMove', "")
84-
if variant == "crazyhouse" and len(last_move) == 4 and last_move[:2] == last_move[2:]:
85-
# NOTE: This is a dirty fix. When streaming a crazyhouse game from lichess, if the
86-
# last move field in the initial stream output is a drop, lichess sends this as
87-
# e.g. e2e2 instead of N@e2. This causes issues parsing the UCI as e2e2 is invalid.
88-
# Considering we only use `lm` and `lastMove` for highlighting squares, this fix
89-
# changes this to a valid UCI string to still allow the square to be highlighted.
90-
# Without this, an exception will occur and we will call the API again, which is unnecessary.
91-
last_move = "k@" + last_move[2:]
92-
self.board_model.reinitialize_board(variant, orientation, data.get('fen'), last_move)
68+
orientation = Color(COLOR_NAMES.index(data.get('orientation', 'white')))
69+
self.board_model.reinitialize_board(self.channel.variant, orientation, data.get('fen'))
9370

9471
if EventTopics.MOVE_MADE in args:
95-
# NOTE: the `lm` field that lichess sends is not valid UCI. It should only be used
96-
# for highlighting move squares (invalid castle notation, missing promotion piece, etc).
72+
# NOTE: the `lm` field that lichess sends for TV feeds and 'lastMove' field sent
73+
# during game spectator streams is not valid UCI. It should only be used
74+
# for highlighting move squares (invalid castle notation, missing promotion piece,
75+
# crazyhouse drop notation, etc).
9776
self.board_model.set_board_position(data.get('fen'), uci_last_move=data.get('lm'))
9877

9978
self._update_game_metadata(*args, data=data)
@@ -103,11 +82,11 @@ def stream_event_received(self, *args, data: Optional[Dict] = None, **kwargs):
10382
raise
10483

10584

85+
# To restore old TV streaming logic see commit 23ca5cd
10686
class StreamTVChannel(threading.Thread):
10787
def __init__(self, channel: TVChannelMenuOptions):
10888
super().__init__(daemon=True)
10989
self.channel = channel
110-
self.current_game = ""
11190
self.running = False
11291
self.max_retries = 10
11392
self.retries = 0
@@ -119,68 +98,36 @@ def __init__(self, channel: TVChannelMenuOptions):
11998
except Exception as e:
12099
self.handle_exceptions(e)
121100

122-
# Current flow that has to be followed to watch the "variant" tv channels
123-
# as /api/tv/feed is only for the top-rated game, and doesn't allow channel specification
124-
# 1. Get current tv game (/api/tv/channels) -> Get the game ID for the game we're interested in
125-
# 2. Start streaming game, on initial input set board orientation, show player names, etc. On follow-up set pos.
126-
# 3. When the game completes, start this loop over.
127-
128-
def get_channel_game_id(self, channel: TVChannelMenuOptions) -> str:
129-
"""Returns the game ID of the ongoing TV game of the passed in channel"""
130-
channel_game_id = self.api_client.tv.get_current_games().get(channel.key, {}).get('gameId')
131-
if not channel_game_id:
132-
raise ValueError(f"TV Stream: Didn't receive game ID for current {channel.value} TV game")
133-
134-
return channel_game_id
135-
136101
def run(self):
137102
"""Main entrypoint for the thread"""
138103
log.info(f"Started watching {self.channel.value} TV")
139104
self.running = True
140105
while self.running:
141106
try:
142107
self.e_tv_stream_event.notify(EventTopics.GAME_SEARCH)
143-
game_id = self.get_channel_game_id(self.channel)
144-
145-
if game_id != self.current_game:
146-
self.current_game = game_id
147-
turns_behind = 0
148-
stream = self.api_client.games.stream_game_moves(game_id)
149-
150-
for event in stream:
151-
# TODO: This does close the stream, but not until the next event comes in (which can be a while
152-
# sometimes (especially in longer time format games like Classical). Ideally there's
153-
# a way to immediately kill the stream, without waiting for another event. This is certainly
154-
# something to watch for since if a user backs out of multiple TV streams and immediately
155-
# enters another streams/threads will start to compound streams/threads and quickly
156-
# bring us close to the 8 streams open per IP limit.
157-
if not self.running:
158-
stream.close()
159-
break
160-
161-
fen = event.get('fen')
162-
winner = event.get('winner')
163-
status = event.get('status', {}).get('name')
164-
165-
if winner or status != "started" and status:
166-
log.info(f"Game finished: {game_id}")
167-
self.e_tv_stream_event.notify(EventTopics.GAME_END, data=event)
168-
break
169-
170-
if status == "started":
171-
log.info(f"Started streaming TV game: {game_id}")
172-
self.e_tv_stream_event.notify(EventTopics.GAME_START, data=event)
173-
turns_behind = event.get('turns', 0)
174-
175-
if fen:
176-
if turns_behind <= 2:
177-
if event.get('wc') and event.get('bc'):
178-
self.e_tv_stream_event.notify(EventTopics.MOVE_MADE, data=event)
179-
else:
180-
# Keeping track of turns behind allows skipping this event until
181-
# we are caught up. This stops a quick game replay from happening.
182-
# We do however want to grab the last move event to pick up the clock data.
183-
turns_behind -= 1
108+
109+
# TODO: Update to use berserk TV specific method once implemented
110+
stream = self.api_client.tv._r.get(f"/api/tv/{self.channel.key}/feed", stream=True) # noqa
111+
112+
for event in stream:
113+
# TODO: This does close the stream, but not until the next event comes in (which can be a while
114+
# sometimes (especially in longer time format games like Classical). Ideally there's
115+
# a way to immediately kill the stream, without waiting for another event.
116+
if not self.running:
117+
stream.close()
118+
break
119+
120+
t = event.get('t')
121+
d = event.get('d')
122+
if not t or not d:
123+
raise ValueError(f"Unable to stream TV as the data is malformed: {event}")
124+
125+
if t == 'featured':
126+
log.info(f"Started streaming TV game: {d.get('id')}")
127+
self.e_tv_stream_event.notify(EventTopics.GAME_START, data=d)
128+
129+
if t == 'fen':
130+
self.e_tv_stream_event.notify(EventTopics.MOVE_MADE, data=d)
184131

185132
except Exception as e:
186133
self.handle_exceptions(e)
@@ -195,7 +142,6 @@ def handle_exceptions(self, e: Exception):
195142
"""Handles the passed in exception and responds appropriately"""
196143
log.error(e)
197144
if self.retries <= self.max_retries:
198-
self.current_game = ""
199145
delay = 2 * (self.retries + 1)
200146

201147
if isinstance(e, ResponseError):

0 commit comments

Comments
 (0)