diff options
| author | salmonik <raisersoft@proton.me> | 2026-05-20 15:40:18 +0200 |
|---|---|---|
| committer | salmonik <raisersoft@proton.me> | 2026-05-20 15:40:18 +0200 |
| commit | 4663f260b55de0975661fda1c00f60d2ca6e4506 (patch) | |
| tree | b649850859595542f8e7916a5513e0840e911318 | |
| parent | Fix TTS and AI voice playback bugs (diff) | |
| download | senator-sperma-4663f260b55de0975661fda1c00f60d2ca6e4506.tar.gz senator-sperma-4663f260b55de0975661fda1c00f60d2ca6e4506.zip | |
Clean up consumers: replace polling with VoiceManager.play_source()
- cron.py: replace while-is_playing polling with VoiceManager.play_source()
- vclogs.py: replace while-is_playing polling with VoiceManager.play_source()
- stop.py: use VoiceManager.disconnect() instead of raw voice_client
- join.py: use VoiceManager.connect() instead of raw channel.connect()
- voice.py: add back join_by_channel_id() convenience method
- Tests: update for new VoiceManager method names and signatures
| -rw-r--r-- | cogs/cron.py | 26 | ||||
| -rw-r--r-- | cogs/music/join.py | 4 | ||||
| -rw-r--r-- | cogs/music/stop.py | 5 | ||||
| -rw-r--r-- | cogs/vclogs.py | 7 | ||||
| -rw-r--r-- | cogs/voice.py | 11 | ||||
| -rw-r--r-- | tests/cogs/test_music_audio.py | 4 | ||||
| -rw-r--r-- | tests/cogs/test_music_join.py | 25 | ||||
| -rw-r--r-- | tests/cogs/test_voice.py | 4 |
8 files changed, 47 insertions, 39 deletions
diff --git a/cogs/cron.py b/cogs/cron.py index aad43d2..ee88085 100644 --- a/cogs/cron.py +++ b/cogs/cron.py | |||
| @@ -145,29 +145,23 @@ class Cron(commands.Cog): | |||
| 145 | 145 | ||
| 146 | if isinstance(channel, discord.VoiceChannel): | 146 | if isinstance(channel, discord.VoiceChannel): |
| 147 | voice_mgr = get_voice_manager(self.bot) | 147 | voice_mgr = get_voice_manager(self.bot) |
| 148 | voice_client = await voice_mgr.join_channel(channel) | ||
| 149 | if not voice_client: | ||
| 150 | if channel.guild.system_channel: | ||
| 151 | await channel.guild.system_channel.send( | ||
| 152 | "Nie udano się połączyć z kanałem głosowym" | ||
| 153 | ) | ||
| 154 | return | ||
| 155 | 148 | ||
| 156 | try: | 149 | try: |
| 157 | audio_source = discord.FFmpegPCMAudio(audio_url) | 150 | audio_source = discord.FFmpegPCMAudio(audio_url) |
| 158 | voice_client.play(audio_source) | 151 | success = await voice_mgr.play_source(channel, audio_source) |
| 159 | 152 | ||
| 160 | while voice_client.is_playing(): | 153 | if success: |
| 161 | await asyncio.sleep(1) | 154 | logger.info(f"Finished playing audio: {audio_url}") |
| 155 | else: | ||
| 156 | raise Exception("Playback failed") | ||
| 162 | 157 | ||
| 163 | logger.info("Audio playback finished normally - disconnecting") | 158 | await voice_mgr.disconnect(channel.guild) |
| 164 | await voice_client.disconnect() | ||
| 165 | logger.info(f"Finished playing audio: {audio_url}") | ||
| 166 | except Exception as e: | 159 | except Exception as e: |
| 167 | logger.error(f"Error playing audio: {e}") | 160 | logger.error(f"Error playing audio: {e}") |
| 168 | if voice_client.is_connected(): | 161 | try: |
| 169 | logger.info("Audio playback failed - disconnecting") | 162 | await voice_mgr.disconnect(channel.guild) |
| 170 | await voice_client.disconnect() | 163 | except Exception: |
| 164 | pass | ||
| 171 | if channel.guild.system_channel: | 165 | if channel.guild.system_channel: |
| 172 | await channel.guild.system_channel.send( | 166 | await channel.guild.system_channel.send( |
| 173 | f"Błąd podczas odgrywania audio: {audio_url}" | 167 | f"Błąd podczas odgrywania audio: {audio_url}" |
diff --git a/cogs/music/join.py b/cogs/music/join.py index c613b10..d68ca79 100644 --- a/cogs/music/join.py +++ b/cogs/music/join.py | |||
| @@ -6,6 +6,7 @@ | |||
| 6 | import logging | 6 | import logging |
| 7 | 7 | ||
| 8 | from discord.ext import commands | 8 | from discord.ext import commands |
| 9 | from cogs.voice import get_voice_manager | ||
| 9 | 10 | ||
| 10 | logger = logging.getLogger(__name__) | 11 | logger = logging.getLogger(__name__) |
| 11 | 12 | ||
| @@ -16,8 +17,9 @@ class Join(commands.Cog): | |||
| 16 | 17 | ||
| 17 | @commands.slash_command(name="join", description="dołącza do kanalu") | 18 | @commands.slash_command(name="join", description="dołącza do kanalu") |
| 18 | async def join(self, ctx): | 19 | async def join(self, ctx): |
| 20 | voice_mgr = get_voice_manager(self.bot) | ||
| 19 | channel = ctx.author.voice.channel | 21 | channel = ctx.author.voice.channel |
| 20 | await channel.connect() | 22 | await voice_mgr.connect(channel) |
| 21 | await ctx.respond(content="\u200b", ephemeral=True) | 23 | await ctx.respond(content="\u200b", ephemeral=True) |
| 22 | await ctx.interaction.delete_original_response() | 24 | await ctx.interaction.delete_original_response() |
| 23 | logger.info("Bot connected to voice channel") | 25 | logger.info("Bot connected to voice channel") |
diff --git a/cogs/music/stop.py b/cogs/music/stop.py index f06e7f5..9c8d5cf 100644 --- a/cogs/music/stop.py +++ b/cogs/music/stop.py | |||
| @@ -5,6 +5,8 @@ | |||
| 5 | 5 | ||
| 6 | from discord.ext import commands | 6 | from discord.ext import commands |
| 7 | 7 | ||
| 8 | from cogs.voice import get_voice_manager | ||
| 9 | |||
| 8 | import logging | 10 | import logging |
| 9 | 11 | ||
| 10 | logger = logging.getLogger(__name__) | 12 | logger = logging.getLogger(__name__) |
| @@ -18,7 +20,8 @@ class Stop(commands.Cog): | |||
| 18 | async def stop(self, ctx): | 20 | async def stop(self, ctx): |
| 19 | await ctx.respond(content="\u200b", ephemeral=True) | 21 | await ctx.respond(content="\u200b", ephemeral=True) |
| 20 | await ctx.interaction.delete_original_response() | 22 | await ctx.interaction.delete_original_response() |
| 21 | await ctx.voice_client.disconnect() | 23 | voice_mgr = get_voice_manager(self.bot) |
| 24 | await voice_mgr.disconnect(ctx.guild) | ||
| 22 | logger.info("Bot disconnected from voice channel") | 25 | logger.info("Bot disconnected from voice channel") |
| 23 | 26 | ||
| 24 | 27 | ||
diff --git a/cogs/vclogs.py b/cogs/vclogs.py index b538265..2e853ad 100644 --- a/cogs/vclogs.py +++ b/cogs/vclogs.py | |||
| @@ -160,12 +160,7 @@ class VcLogs(commands.Cog): | |||
| 160 | 160 | ||
| 161 | logger.info("preparing to play audio") | 161 | logger.info("preparing to play audio") |
| 162 | audio_source = discord.FFmpegPCMAudio(tts_file) | 162 | audio_source = discord.FFmpegPCMAudio(tts_file) |
| 163 | if volume != 1.0: | 163 | await voice_mgr.play_source(channel, audio_source, volume=volume) |
| 164 | audio_source = discord.PCMVolumeTransformer(audio_source) | ||
| 165 | audio_source.volume = volume | ||
| 166 | voice_client.play(audio_source) | ||
| 167 | while voice_client.is_playing(): | ||
| 168 | await asyncio.sleep(0.1) | ||
| 169 | except Exception as e: | 164 | except Exception as e: |
| 170 | logger.error(f"TTS playback error: {e}") | 165 | logger.error(f"TTS playback error: {e}") |
| 171 | finally: | 166 | finally: |
diff --git a/cogs/voice.py b/cogs/voice.py index ba1993e..86416ef 100644 --- a/cogs/voice.py +++ b/cogs/voice.py | |||
| @@ -151,6 +151,17 @@ class VoiceManager: | |||
| 151 | """Backward-compatible alias for connect().""" | 151 | """Backward-compatible alias for connect().""" |
| 152 | return await self.connect(channel) | 152 | return await self.connect(channel) |
| 153 | 153 | ||
| 154 | async def join_by_channel_id(self, channel_id): | ||
| 155 | """Join a voice channel by channel ID.""" | ||
| 156 | channel = self.bot.get_channel(channel_id) | ||
| 157 | if not channel: | ||
| 158 | logger.error(f"Channel {channel_id} not found") | ||
| 159 | return None | ||
| 160 | if not isinstance(channel, discord.VoiceChannel): | ||
| 161 | logger.error(f"Channel {channel_id} is not a voice channel") | ||
| 162 | return None | ||
| 163 | return await self.connect(channel) | ||
| 164 | |||
| 154 | async def get_client(self, guild): | 165 | async def get_client(self, guild): |
| 155 | """Get existing voice client for a guild.""" | 166 | """Get existing voice client for a guild.""" |
| 156 | return discord.utils.get(self.bot.voice_clients, guild=guild) | 167 | return discord.utils.get(self.bot.voice_clients, guild=guild) |
diff --git a/tests/cogs/test_music_audio.py b/tests/cogs/test_music_audio.py index 705f1b3..150ad5d 100644 --- a/tests/cogs/test_music_audio.py +++ b/tests/cogs/test_music_audio.py | |||
| @@ -59,7 +59,7 @@ async def test_prepare_file_source_absolute_path(): | |||
| 59 | async def test_prepare_source_url(): | 59 | async def test_prepare_source_url(): |
| 60 | from cogs.music.audio_playback import AudioPlaybackManager | 60 | from cogs.music.audio_playback import AudioPlaybackManager |
| 61 | 61 | ||
| 62 | source, _, is_url = await AudioPlaybackManager.prepare_source(MagicMock(), "https://example.com/stream") | 62 | source, _, is_url = await AudioPlaybackManager.prepare_source("https://example.com/stream") |
| 63 | 63 | ||
| 64 | assert is_url == True | 64 | assert is_url == True |
| 65 | assert source == "https://example.com/stream" | 65 | assert source == "https://example.com/stream" |
| @@ -70,7 +70,7 @@ async def test_prepare_source_local_file(): | |||
| 70 | from cogs.music.audio_playback import AudioPlaybackManager | 70 | from cogs.music.audio_playback import AudioPlaybackManager |
| 71 | 71 | ||
| 72 | with patch('os.path.exists', return_value=True): | 72 | with patch('os.path.exists', return_value=True): |
| 73 | source, display_name, is_url = await AudioPlaybackManager.prepare_source(MagicMock(), "/tmp/test.mp3") | 73 | source, display_name, is_url = await AudioPlaybackManager.prepare_source("/tmp/test.mp3") |
| 74 | 74 | ||
| 75 | assert is_url == False | 75 | assert is_url == False |
| 76 | assert display_name == "test" | 76 | assert display_name == "test" |
diff --git a/tests/cogs/test_music_join.py b/tests/cogs/test_music_join.py index 0176759..b40d93a 100644 --- a/tests/cogs/test_music_join.py +++ b/tests/cogs/test_music_join.py | |||
| @@ -24,16 +24,18 @@ async def test_join_command_success(join_cog, mock_channel, mock_guild, bot, moc | |||
| 24 | ctx.author = mock_user | 24 | ctx.author = mock_user |
| 25 | 25 | ||
| 26 | mock_voice_channel = MagicMock() | 26 | mock_voice_channel = MagicMock() |
| 27 | mock_voice = MagicMock() | ||
| 28 | mock_voice_channel.connect = AsyncMock(return_value=mock_voice) | ||
| 29 | |||
| 30 | ctx.author.voice = MagicMock() | 27 | ctx.author.voice = MagicMock() |
| 31 | ctx.author.voice.channel = mock_voice_channel | 28 | ctx.author.voice.channel = mock_voice_channel |
| 32 | 29 | ||
| 33 | with patch.object(mock_voice_channel, 'connect', new_callable=AsyncMock, return_value=mock_voice): | 30 | with patch('cogs.music.join.get_voice_manager') as mock_get_vm: |
| 31 | mock_vm = MagicMock() | ||
| 32 | mock_vm.connect = AsyncMock() | ||
| 33 | mock_get_vm.return_value = mock_vm | ||
| 34 | |||
| 34 | with patch.object(ctx, 'respond', new_callable=AsyncMock): | 35 | with patch.object(ctx, 'respond', new_callable=AsyncMock): |
| 35 | with patch.object(ctx.interaction, 'delete_original_response', new_callable=AsyncMock): | 36 | with patch.object(ctx.interaction, 'delete_original_response', new_callable=AsyncMock): |
| 36 | await join_cog.join.callback(join_cog, ctx) | 37 | await join_cog.join.callback(join_cog, ctx) |
| 38 | mock_vm.connect.assert_called_once_with(mock_voice_channel) | ||
| 37 | 39 | ||
| 38 | 40 | ||
| 39 | @pytest.mark.asyncio | 41 | @pytest.mark.asyncio |
| @@ -42,12 +44,13 @@ async def test_stop_command_success(stop_cog, mock_channel, mock_guild, bot, moc | |||
| 42 | ctx.user = mock_user | 44 | ctx.user = mock_user |
| 43 | ctx.author = mock_user | 45 | ctx.author = mock_user |
| 44 | 46 | ||
| 45 | mock_voice_client = MagicMock() | 47 | with patch('cogs.music.stop.get_voice_manager') as mock_get_vm: |
| 46 | mock_voice_client.disconnect = AsyncMock() | 48 | mock_vm = MagicMock() |
| 47 | ctx.voice_client = mock_voice_client | 49 | mock_vm.disconnect = AsyncMock() |
| 50 | mock_get_vm.return_value = mock_vm | ||
| 48 | 51 | ||
| 49 | with patch.object(ctx, 'respond', new_callable=AsyncMock): | 52 | with patch.object(ctx, 'respond', new_callable=AsyncMock): |
| 50 | with patch.object(ctx.interaction, 'delete_original_response', new_callable=AsyncMock): | 53 | with patch.object(ctx.interaction, 'delete_original_response', new_callable=AsyncMock): |
| 51 | await stop_cog.stop.callback(stop_cog, ctx) | 54 | await stop_cog.stop.callback(stop_cog, ctx) |
| 52 | 55 | ||
| 53 | mock_voice_client.disconnect.assert_called_once() | 56 | mock_vm.disconnect.assert_called_once_with(mock_guild) |
diff --git a/tests/cogs/test_voice.py b/tests/cogs/test_voice.py index 104eea6..6eb226b 100644 --- a/tests/cogs/test_voice.py +++ b/tests/cogs/test_voice.py | |||
| @@ -53,7 +53,7 @@ async def test_voice_manager_get_voice_status_not_connected(): | |||
| 53 | mock_bot = MagicMock() | 53 | mock_bot = MagicMock() |
| 54 | mock_guild = MagicMock() | 54 | mock_guild = MagicMock() |
| 55 | 55 | ||
| 56 | with patch('cogs.voice.VoiceManager.get_voice_client', return_value=None): | 56 | with patch('cogs.voice.VoiceManager.get_client', return_value=None): |
| 57 | manager = VoiceManager(mock_bot) | 57 | manager = VoiceManager(mock_bot) |
| 58 | voice_client, is_connected, is_playing = await manager.get_voice_status(mock_guild) | 58 | voice_client, is_connected, is_playing = await manager.get_voice_status(mock_guild) |
| 59 | 59 | ||
| @@ -71,7 +71,7 @@ async def test_voice_manager_stop(): | |||
| 71 | mock_voice_client = MagicMock() | 71 | mock_voice_client = MagicMock() |
| 72 | mock_voice_client.disconnect = AsyncMock() | 72 | mock_voice_client.disconnect = AsyncMock() |
| 73 | 73 | ||
| 74 | with patch('cogs.voice.VoiceManager.get_voice_client', return_value=mock_voice_client): | 74 | with patch('cogs.voice.VoiceManager.get_client', return_value=mock_voice_client): |
| 75 | manager = VoiceManager(mock_bot) | 75 | manager = VoiceManager(mock_bot) |
| 76 | await manager.stop(mock_guild) | 76 | await manager.stop(mock_guild) |
| 77 | 77 | ||
