22from cli_chess .menus .tv_channel_menu import TVChannelMenuOptions
33from cli_chess .utils .event import Event , EventTopics
44from 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
66from berserk .exceptions import ResponseError
77from time import sleep
88from 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
10686class 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