From 5153f50b4b88411a0e7ad8db5626c846f62a0adb Mon Sep 17 00:00:00 2001 From: 0xcodepunk <1329474@gmail.com> Date: Sun, 5 Jul 2026 22:04:01 +0300 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=D1=80=D0=B0=D0=B4=D0=B8=D0=BE=20?= =?UTF-8?q?=D0=BD=D0=B5=20=D1=81=D1=82=D0=B0=D1=80=D1=82=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D0=BB=D0=BE=20=D0=B8=D0=B7-=D0=B7=D0=B0=20=D1=88=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BF=D1=80=D0=B8?= =?UTF-8?q?=D0=B2=D1=8F=D0=B7=D0=BE=D0=BA=20=D0=BF=D0=BE=D1=82=D0=BE=D0=BA?= =?UTF-8?q?=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - добавлен грейс-период SILENCE_RESEND_GRACE в _resume_radio_if_silent: пересылка радио не чаще 15с после последней отправки, HLS успевает буферизоваться - запрещено «возобновление» радио до первой привязки потока (last_stream_sent_at == 0) - убран sleep(1) как единственный ограничитель частоты пересылок - пересылка из _restore_ruark_after_speech теперь взводит last_stream_sent_at - добавлены тесты: молчащий Ruark на радио не бомбардируется перепривязками, пересылка после грейса работает Примечание: регрессия проявилась после перехода на событийный цикл (PR #20) — сообщения станции будят цикл ежесекундно, и страховка перепривязывала поток быстрее, чем Ruark успевал стартовать. --- .../main_stream_manager.py | 39 +++++++++++------- tests/test_streaming_loop.py | 41 +++++++++++++++++++ 2 files changed, 66 insertions(+), 14 deletions(-) diff --git a/src/main_stream_service/main_stream_manager.py b/src/main_stream_service/main_stream_manager.py index 9410641..50f0316 100644 --- a/src/main_stream_service/main_stream_manager.py +++ b/src/main_stream_service/main_stream_manager.py @@ -304,25 +304,35 @@ async def _stop_or_recover_paused_track( async def _resume_radio_if_silent( self, track: Track, ctx: "_CycleContext" ) -> None: - """Возобновляет радио, если станция играет, а Ruark молчит.""" - if not ( - ctx.last_track_progress != track.progress - and track.type == "FmRadio" - and not await self._ruark_controls.is_playing() - ): + """Возобновляет радио, если станция играет, а Ruark молчит. + + Пересылка не чаще SILENCE_RESEND_GRACE после последней отправки: + свежепривязанному потоку нужно время на буферизацию HLS, иначе + ежесекундные перепривязки сами не дают радио стартовать. + """ + if track.type != "FmRadio": + return + if ctx.last_track_progress == track.progress: + return + # До первой привязки потока возобновлять нечего + if not ctx.last_stream_sent_at: + return + if time.monotonic() - ctx.last_stream_sent_at < SILENCE_RESEND_GRACE: + return + if await self._ruark_controls.is_playing(): return logger.info("🔁 Возобновляем воспроизведение радио") radio_url = await self._station_controls.get_radio_url() - if radio_url: - await self._send_track_to_stream_server( - track_url=radio_url, - radio=True, - title=track.title, - ) - await asyncio.sleep(1) - else: + if not radio_url: logger.warning("⚠️ Не удалось получить URL радиостанции") + return + ctx.last_stream_sent_at = time.monotonic() + await self._send_track_to_stream_server( + track_url=radio_url, + radio=True, + title=track.title, + ) async def _refresh_track_if_unchanged( self, track: Track, ctx: "_CycleContext" @@ -592,6 +602,7 @@ async def _restore_ruark_after_speech( if ctx.track_url: # Для трека продолжаем с текущей позиции станции, # а не с начала — иначе рассинхрон + ctx.last_stream_sent_at = time.monotonic() await self._send_track_to_stream_server( ctx.track_url, radio=track.type == "FmRadio", diff --git a/tests/test_streaming_loop.py b/tests/test_streaming_loop.py index 949ee68..b963573 100644 --- a/tests/test_streaming_loop.py +++ b/tests/test_streaming_loop.py @@ -179,6 +179,47 @@ async def test_radio_track_sent_with_radio_flag(fast_sleep): assert call.kwargs["radio"] is True +async def test_silent_radio_not_respammed_within_grace(fast_sleep): + """Молчащий Ruark на радио не бомбардируется перепривязками.""" + # Прогресс радио тикает каждую итерацию — как на живой станции + tracks = [ + make_track( + id="fm_jazz", type="FmRadio", duration=0.0, progress=float(p) + ) + for p in range(1, 60) + ] + station = make_station_controls(tracks=tracks) + manager = make_manager(station, make_ruark(is_playing=False)) + send = manager._send_track_to_stream_server + + await drive_streaming(manager, until=lambda: False, timeout=0.3) + + # Только первоначальный свитч: грейс не даёт слать каждую итерацию + assert send.call_count == 1 + + +async def test_silent_radio_resent_after_grace(fast_sleep, monkeypatch): + """После грейса радио пересылается, если Ruark так и молчит.""" + monkeypatch.setattr( + "main_stream_service.main_stream_manager.SILENCE_RESEND_GRACE", 0.0 + ) + tracks = [ + make_track( + id="fm_jazz", type="FmRadio", duration=0.0, progress=float(p) + ) + for p in range(1, 60) + ] + station = make_station_controls(tracks=tracks) + manager = make_manager(station, make_ruark(is_playing=False)) + send = manager._send_track_to_stream_server + + await drive_streaming(manager, until=lambda: send.call_count >= 2) + + assert send.call_count >= 2 + resume_call = send.call_args_list[1] + assert resume_call.kwargs["radio"] is True + + async def test_alice_speech_ducks_ruark_volume(fast_sleep): track = make_track() # Первый вызов — инициализация last_alice_state, затем смена состояния From 81ad7d1263fc27b83b7aabf06fdb2faa00ce9199 Mon Sep 17 00:00:00 2001 From: 0xcodepunk <1329474@gmail.com> Date: Sun, 5 Jul 2026 22:06:42 +0300 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=D1=80=D0=B0=D0=B4=D0=B8=D0=BE=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=BB=D1=83=D1=87=D0=B0=D0=B5=D1=82=20=D0=B1=D0=BE?= =?UTF-8?q?=D0=BB=D1=8C=D1=88=D0=B5=20=D0=B2=D1=80=D0=B5=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=20=D0=BD=D0=B0=20=D1=81=D1=82=D0=B0=D1=80=D1=82=20=D0=BF?= =?UTF-8?q?=D0=BE=D1=81=D0=BB=D0=B5=20=D1=80=D0=B5=D1=87=D0=B8=20=D0=90?= =?UTF-8?q?=D0=BB=D0=B8=D1=81=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - увеличено ожидание PLAYING в _restore_ruark_after_speech для радио: 80 попыток вместо 30, HLS буферизуется дольше прямой ссылки трека - для треков поведение не изменилось: цикл выходит раньше по факту воспроизведения --- src/main_stream_service/main_stream_manager.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main_stream_service/main_stream_manager.py b/src/main_stream_service/main_stream_manager.py index 50f0316..9587dfc 100644 --- a/src/main_stream_service/main_stream_manager.py +++ b/src/main_stream_service/main_stream_manager.py @@ -587,7 +587,10 @@ async def _restore_ruark_after_speech( logger.info("🔁 Возвращаем громкость Ruark") await self._ruark_controls.set_volume(self._ruark_volume) - for _ in range(30): + # Радио буферизует HLS дольше, чем стартует трек, — + # ждём дольше, чтобы не перепривязывать поток зря + attempts = 80 if track.type == "FmRadio" else 30 + for _ in range(attempts): if await self._ruark_controls.is_playing(): logger.info("▶️ Ruark начал играть") await self._station_controls.fade_out_alice_volume()