not-lain commited on
Commit
e67568c
Β·
1 Parent(s): c0a28a9

update play command

Browse files
Files changed (1) hide show
  1. app.py +257 -95
app.py CHANGED
@@ -1,107 +1,202 @@
 
 
 
 
 
 
 
 
 
1
  import discord
 
2
  from discord.ext import commands
3
- from huggingface_hub import hf_hub_download
4
- import gradio as gr
5
  from dotenv import load_dotenv
6
- import os
7
- import threading
8
- import asyncio
9
 
10
- # Load environment variables
11
- load_dotenv()
12
- if os.path.exists("assets") is False:
13
- os.makedirs("assets", exist_ok=True)
14
- hf_hub_download(
15
- "not-lain/assets", "lofi.mp3", repo_type="dataset", local_dir="assets"
16
- )
17
 
18
- song = "assets/lofi.mp3"
19
 
20
- # Bot configuration
21
- intents = discord.Intents.default()
22
- intents.message_content = True
23
- bot = commands.Bot(command_prefix="!", intents=intents)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
 
26
  class MusicBot:
27
- def __init__(self):
28
- self.is_playing = False
29
- self.voice_client = None
30
- self.keepalive_task = None
31
- self.last_context = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
- async def voice_keepalive(self, voice_client):
34
- """Keeps the voice connection alive by periodically playing audio"""
35
- print("Starting voice keepalive task")
36
- while True:
37
- if voice_client.is_connected() and not self.is_playing:
38
- await self.play_next(self.last_context)
39
- await asyncio.sleep(15)
40
- else:
41
- await asyncio.sleep(1)
42
-
43
- async def join_voice(self, ctx):
44
- if ctx.author.voice:
45
- channel = ctx.author.voice.channel
46
- if self.voice_client is None:
47
- self.voice_client = await channel.connect()
48
- self.last_context = ctx
49
- if self.keepalive_task:
50
- self.keepalive_task.cancel()
51
- self.keepalive_task = asyncio.create_task(
52
- self.voice_keepalive(self.voice_client)
53
- )
54
- else:
55
- await self.voice_client.move_to(channel)
56
- else:
57
- await ctx.send("You need to be in a voice channel!")
58
 
59
- async def play_next(self, ctx):
60
- if not self.is_playing:
61
- self.is_playing = True
62
- try:
63
- audio_source = discord.FFmpegPCMAudio(song)
 
 
 
64
 
65
- def after_playing(e):
66
- self.is_playing = False
67
 
68
- self.voice_client.play(audio_source, after=after_playing)
69
- except Exception as e:
70
- print(f"Error playing file: {e}")
71
- await ctx.send("Error playing the song.")
72
  self.is_playing = False
 
73
 
74
- async def stop_playing(self, ctx):
75
- try:
76
- if self.keepalive_task:
77
- self.keepalive_task.cancel()
78
- self.keepalive_task = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
 
 
 
 
 
 
 
 
 
 
80
  if self.voice_client:
81
  if self.voice_client.is_playing():
82
  self.voice_client.stop()
83
-
84
- self.is_playing = False
85
- self.last_context = None
86
-
87
  if self.voice_client.is_connected():
88
  await self.voice_client.disconnect(force=False)
89
-
90
- self.voice_client = None
91
  return True
92
-
93
  return False
94
-
95
  except Exception as e:
96
- print(f"Error during cleanup: {e}")
97
- self.is_playing = False
98
- self.voice_client = None
99
- self.last_context = None
100
- self.keepalive_task = None
101
  return False
102
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
 
104
- music_bot = MusicBot()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
 
106
 
107
  @bot.event
@@ -117,17 +212,43 @@ async def on_ready():
117
  print(f"An error occurred while syncing commands: {e}")
118
 
119
 
120
- @bot.tree.command(name="play", description="Play the sample music")
121
- async def play(interaction: discord.Interaction):
122
- await interaction.response.defer()
123
- ctx = await bot.get_context(interaction)
124
- await music_bot.join_voice(ctx)
 
 
 
 
 
 
 
125
 
126
- if not music_bot.is_playing:
127
- await music_bot.play_next(ctx)
128
- await interaction.followup.send("Playing sample music!")
129
- else:
130
- await interaction.followup.send("Already playing!")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
 
132
 
133
  @bot.tree.command(name="skip", description="Skip the current song")
@@ -196,20 +317,61 @@ async def leave(interaction: discord.Interaction):
196
  await interaction.followup.send("An error occurred while trying to disconnect.")
197
 
198
 
199
- def run_discord_bot():
200
- bot.run(os.getenv("DISCORD_TOKEN"))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
 
 
202
 
203
- # Create the Gradio interface
204
- with gr.Blocks() as iface:
205
- gr.Markdown("# Discord Music Bot Control Panel")
206
- gr.Markdown("Bot is running in background")
 
 
 
 
 
 
 
 
 
 
 
 
207
 
208
 
209
  if __name__ == "__main__":
210
- # Start the Discord bot in a separate thread
211
  bot_thread = threading.Thread(target=run_discord_bot, daemon=True)
212
  bot_thread.start()
213
 
214
- # Run Gradio interface in the main thread
 
 
 
215
  iface.launch(debug=True)
 
1
+ import asyncio
2
+ import logging
3
+ import os
4
+ import time
5
+ import threading
6
+ from dataclasses import dataclass
7
+ from enum import Enum
8
+ from typing import Optional, Callable
9
+
10
  import discord
11
+ from discord import app_commands
12
  from discord.ext import commands
 
 
13
  from dotenv import load_dotenv
14
+ import gradio as gr
15
+ from huggingface_hub import hf_hub_download
16
+ from gradio_client import Client
17
 
18
+ # Configure logging
19
+ logging.basicConfig(
20
+ level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
21
+ )
22
+ logger = logging.getLogger(__name__)
 
 
23
 
 
24
 
25
+ class LoopMode(str, Enum):
26
+ NONE = "none"
27
+ SINGLE = "single"
28
+ QUEUE = "queue"
29
+
30
+
31
+ @dataclass
32
+ class BotConfig:
33
+ ASSETS_DIR: str = "assets"
34
+ SONG_FILE: str = "lofi.mp3"
35
+ HF_REPO: str = "not-lain/assets"
36
+ HF_SPACE: str = "https://not-lain-ytdlp.hf.space/"
37
+ MAX_RETRY_ATTEMPTS: int = 3
38
+
39
+ @property
40
+ def song_path(self) -> str:
41
+ return f"{self.ASSETS_DIR}/{self.SONG_FILE}"
42
+
43
+
44
+ @dataclass
45
+ class QueueItem:
46
+ url: str
47
+ title: Optional[str] = None
48
+ file_path: Optional[str] = None
49
+
50
+
51
+ class VoiceStateError(Exception):
52
+ """Custom exception for voice state errors"""
53
+
54
+ pass
55
 
56
 
57
  class MusicBot:
58
+ def __init__(self, config: BotConfig):
59
+ self.config = config
60
+ self.is_playing: bool = False
61
+ self.voice_client: Optional[discord.VoiceClient] = None
62
+ self.last_context: Optional[commands.Context] = None
63
+ self.loop_mode: LoopMode = LoopMode.NONE
64
+ self.current_song: Optional[QueueItem] = None
65
+ self.queue: list[QueueItem] = []
66
+ self.hf_client = Client(config.HF_SPACE, hf_token=None)
67
+
68
+ async def ensure_voice_state(self, ctx: commands.Context) -> None:
69
+ """Validate voice state and raise appropriate errors"""
70
+ if not ctx.author.voice:
71
+ raise VoiceStateError("You need to be in a voice channel!")
72
+
73
+ if self.voice_client and ctx.author.voice.channel != self.voice_client.channel:
74
+ raise VoiceStateError("You must be in the same voice channel as the bot!")
75
+
76
+ async def download_song(self, queue_item: QueueItem) -> None:
77
+ """Download song from URL and update queue item with file path"""
78
+ try:
79
+ job = self.hf_client.submit(url=queue_item.url, api_name="/predict")
80
+ while not job.done():
81
+ time.sleep(0.1)
82
+ print("job.outputs() = ", job.outputs())
83
+ title, file_path = job.outputs()[0]
84
+ queue_item.title = title
85
+ queue_item.file_path = file_path
86
+ except Exception as e:
87
+ logger.error(f"Error downloading song: {e}")
88
+ raise
89
 
90
+ async def play_next(self, ctx: commands.Context) -> None:
91
+ if self.is_playing:
92
+ return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
 
94
+ try:
95
+ if not self.current_song and self.queue:
96
+ self.current_song = self.queue.pop(0)
97
+ # Download song if it hasn't been downloaded yet
98
+ if not self.current_song.file_path:
99
+ await self.download_song(self.current_song)
100
+ elif not self.current_song:
101
+ return
102
 
103
+ self.is_playing = True
104
+ audio_source = discord.FFmpegPCMAudio(self.current_song.file_path)
105
 
106
+ def after_playing(error):
107
+ if error:
108
+ logger.error(f"Error in playback: {error}")
 
109
  self.is_playing = False
110
+ asyncio.run_coroutine_threadsafe(self.handle_song_end(ctx), bot.loop)
111
 
112
+ self.voice_client.play(audio_source, after=after_playing)
113
+
114
+ except Exception as e:
115
+ logger.error(f"Error playing file: {e}")
116
+ self.is_playing = False
117
+ raise
118
+
119
+ async def handle_song_end(self, ctx: commands.Context) -> None:
120
+ if self.loop_mode == LoopMode.NONE:
121
+ self.current_song = None
122
+ elif self.loop_mode == LoopMode.QUEUE and self.current_song:
123
+ self.queue.append(self.current_song)
124
+ self.current_song = None
125
+
126
+ if not self.is_playing:
127
+ await self.play_next(ctx)
128
+
129
+ async def join_voice(self, ctx: commands.Context) -> None:
130
+ if not ctx.author.voice:
131
+ await ctx.send("You need to be in a voice channel!")
132
+ return
133
 
134
+ channel = ctx.author.voice.channel
135
+ if self.voice_client is None:
136
+ self.voice_client = await channel.connect()
137
+ self.last_context = ctx
138
+ else:
139
+ await self.voice_client.move_to(channel)
140
+
141
+ async def stop_playing(self, ctx: commands.Context) -> bool:
142
+ try:
143
  if self.voice_client:
144
  if self.voice_client.is_playing():
145
  self.voice_client.stop()
 
 
 
 
146
  if self.voice_client.is_connected():
147
  await self.voice_client.disconnect(force=False)
148
+ self._reset_state()
 
149
  return True
 
150
  return False
 
151
  except Exception as e:
152
+ logger.error(f"Error during cleanup: {e}")
153
+ self._reset_state()
 
 
 
154
  return False
155
 
156
+ def add_to_queue(self, url: str) -> int:
157
+ """Add song to queue and return position"""
158
+ queue_item = QueueItem(url=url)
159
+ self.queue.append(queue_item)
160
+ return len(self.queue)
161
+
162
+ def _reset_state(self) -> None:
163
+ self.is_playing = False
164
+ self.voice_client = None
165
+ self.last_context = None
166
+ self.loop_mode = LoopMode.NONE
167
+ self.current_song = None
168
+ self.queue.clear()
169
+
170
 
171
+ async def handle_voice_command(
172
+ interaction: discord.Interaction, action: Callable, defer: bool = True
173
+ ) -> None:
174
+ """Generic handler for voice-related commands"""
175
+ try:
176
+ if defer:
177
+ await interaction.response.defer()
178
+ ctx = await bot.get_context(interaction)
179
+ await music_bot.ensure_voice_state(ctx)
180
+ await action(ctx, interaction)
181
+ except VoiceStateError as e:
182
+ if not interaction.response.is_done():
183
+ await interaction.response.send_message(str(e))
184
+ else:
185
+ await interaction.followup.send(str(e))
186
+ except Exception as e:
187
+ logger.error(f"Command error: {e}")
188
+ if not interaction.response.is_done():
189
+ await interaction.response.send_message("An error occurred!")
190
+ else:
191
+ await interaction.followup.send("An error occurred!")
192
+
193
+
194
+ # Initialize bot and music bot instance
195
+ config = BotConfig()
196
+ intents = discord.Intents.default()
197
+ intents.message_content = True
198
+ bot = commands.Bot(command_prefix="!", intents=intents)
199
+ music_bot = MusicBot(config)
200
 
201
 
202
  @bot.event
 
212
  print(f"An error occurred while syncing commands: {e}")
213
 
214
 
215
+ @bot.tree.command(name="lofi", description="Play lofi music")
216
+ async def lofi(interaction: discord.Interaction):
217
+ async def play_lofi(ctx, interaction: discord.Interaction):
218
+ await music_bot.join_voice(ctx)
219
+ music_bot.current_song = QueueItem(
220
+ url=config.song_path, title="Lofi Music", file_path=config.song_path
221
+ )
222
+ if not music_bot.is_playing:
223
+ await music_bot.play_next(ctx)
224
+ await interaction.followup.send("Playing lofi music! 🎡")
225
+ else:
226
+ await interaction.followup.send("Already playing!")
227
 
228
+ await handle_voice_command(interaction, play_lofi)
229
+
230
+
231
+ @bot.tree.command(name="play", description="Play a youtube song")
232
+ async def play(interaction: discord.Interaction, url: str):
233
+ async def play_song(ctx, interaction: discord.Interaction):
234
+ await music_bot.join_voice(ctx)
235
+
236
+ if music_bot.is_playing or music_bot.queue:
237
+ position = music_bot.add_to_queue(url)
238
+ await interaction.followup.send(
239
+ f"Added song to queue at position {position}! 🎡"
240
+ )
241
+ else:
242
+ music_bot.add_to_queue(url)
243
+ await music_bot.play_next(ctx)
244
+ if music_bot.current_song and music_bot.current_song.title:
245
+ await interaction.followup.send(
246
+ f"Playing {music_bot.current_song.title}! 🎡"
247
+ )
248
+ else:
249
+ await interaction.followup.send("Playing song! 🎡")
250
+
251
+ await handle_voice_command(interaction, play_song)
252
 
253
 
254
  @bot.tree.command(name="skip", description="Skip the current song")
 
317
  await interaction.followup.send("An error occurred while trying to disconnect.")
318
 
319
 
320
+ @bot.tree.command(name="loop", description="Set loop mode")
321
+ @app_commands.choices(
322
+ mode=[app_commands.Choice(name=mode.value, value=mode.value) for mode in LoopMode]
323
+ )
324
+ async def loop(interaction: discord.Interaction, mode: str):
325
+ try:
326
+ music_bot.loop_mode = LoopMode(mode)
327
+ await interaction.response.send_message(f"Loop mode set to: {mode}")
328
+ except ValueError:
329
+ await interaction.response.send_message("Invalid loop mode!")
330
+
331
+
332
+ @bot.tree.command(name="queue", description="Show current queue")
333
+ async def queue(interaction: discord.Interaction):
334
+ if not music_bot.queue and not music_bot.current_song:
335
+ await interaction.response.send_message("Queue is empty!")
336
+ return
337
+
338
+ queue_list = []
339
+ if music_bot.current_song:
340
+ status = "🎡 Now playing"
341
+ title = music_bot.current_song.title or "Loading..."
342
+ queue_list.append(f"{status}: {title}")
343
+
344
+ for i, item in enumerate(music_bot.queue, 1):
345
+ title = item.title or "Loading..."
346
+ queue_list.append(f"{i}. {title}")
347
 
348
+ await interaction.response.send_message("\n".join(queue_list))
349
 
350
+
351
+ def initialize_assets() -> None:
352
+ if not os.path.exists(config.ASSETS_DIR):
353
+ os.makedirs(config.ASSETS_DIR, exist_ok=True)
354
+ hf_hub_download(
355
+ config.HF_REPO,
356
+ config.SONG_FILE,
357
+ repo_type="dataset",
358
+ local_dir=config.ASSETS_DIR,
359
+ )
360
+
361
+
362
+ def run_discord_bot() -> None:
363
+ """Run the Discord bot with the token from environment variables."""
364
+ load_dotenv()
365
+ bot.run(os.getenv("DISCORD_TOKEN"))
366
 
367
 
368
  if __name__ == "__main__":
369
+ initialize_assets()
370
  bot_thread = threading.Thread(target=run_discord_bot, daemon=True)
371
  bot_thread.start()
372
 
373
+ with gr.Blocks() as iface:
374
+ gr.Markdown("# Discord Music Bot Control Panel")
375
+ gr.Markdown("Bot is running in background")
376
+
377
  iface.launch(debug=True)