Skip to content

Commit 45e04b0

Browse files
authored
Merge pull request #213 from practical-python-org/music-queue
minor: working queue
2 parents a7483a7 + 29d833a commit 45e04b0

File tree

3 files changed

+140
-24
lines changed

3 files changed

+140
-24
lines changed

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"ffmpeg-python==0.2.0",
3131
"yt-dlp==2023.7.6",
3232
"googletrans==3.1.0a0",
33-
"dnspython==2.4.2"
33+
"dnspython==2.3.0"
3434
],
3535
classifiers=[
3636
# see https://pypi.org/classifiers/

src/zorak/__main__.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@
99

1010
import discord
1111
from discord.ext import commands
12-
from zorak.utilities.core.mongo import initialise_bot_db
1312

1413
from zorak.utilities.core.args_utils import parse_args
1514
from zorak.utilities.core.logging_utils import setup_logger
15+
from zorak.utilities.core.mongo import initialise_bot_db
1616
from zorak.utilities.core.server_settings import Settings
1717

1818
logger = logging.getLogger(__name__)
@@ -29,6 +29,7 @@ def load_cogs(bot):
2929
then digs through those directories and loads the cogs.
3030
"""
3131
logger.info("Loading Cogs...")
32+
failed_to_load = []
3233
for directory in os.listdir(COGS_ROOT_PATH):
3334
if directory.startswith("_"):
3435
logger.debug(f"Skipping {directory} as it is a hidden directory.")
@@ -45,12 +46,14 @@ def load_cogs(bot):
4546
logger.info(f"Loading Cog: {cog_path}")
4647
try:
4748
bot.load_extension(f"zorak.cogs.{directory}.{file[:-3]}")
49+
logger.debug(f"Loaded Cog: {cog_path}")
4850
except Exception as e:
4951
logger.warning("Failed to load: {%s}.{%s}, {%s}", directory, file, e)
50-
# logger.debug(f"Loaded Cog: {cog_path}")
51-
# except Exception as e:
52-
# logger.warning("Failed to load: {%s}.{%s}, {%s}", directory, file, e)
53-
logger.info("Loaded all cogs successfully.")
52+
failed_to_load.append(f"{file[:-3]}")
53+
if failed_to_load:
54+
logger.warning(f"Cog loading finished. Failed to load the following cogs: {', '.join(failed_to_load)}")
55+
else:
56+
logger.info("Loaded all cogs successfully.")
5457

5558

5659
def init_bot(token, bot):

src/zorak/cogs/general/general_music.py

Lines changed: 131 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
1+
import asyncio
2+
3+
from discord import FFmpegPCMAudio
14
from discord.ext import commands
25
from discord.utils import get
3-
from discord import FFmpegPCMAudio
46
from yt_dlp import YoutubeDL
57

8+
PREV_QUEUE_LIMIT = 10
9+
610

711
class Music(commands.Cog):
812
def __init__(self, bot):
913
self.bot = bot
14+
self.queue = {} # A dictionary to hold queues for different guilds
15+
self.prev_songs = {} # A dictionary to hold the last PREV_QUEUE_LIMIT 10 songs played in a guild
1016

1117
# command for bot to join the channel of the user,
1218
# if the bot has already joined and is in a different channel, it will move to the channel the user is in
@@ -20,27 +26,78 @@ async def join(self, ctx):
2026
else:
2127
voice = await channel.connect()
2228

23-
# command to play sound from a youtube URL
29+
@commands.command()
30+
async def skip(self, ctx):
31+
"""Skip the current song and play the next song in the queue."""
32+
voice = get(self.bot.voice_clients, guild=ctx.guild)
33+
voice.stop()
34+
if ctx.guild.id in self.queue and self.queue[ctx.guild.id]:
35+
last_played = self.queue[ctx.guild.id].pop(0)
36+
next_url = self.queue[ctx.guild.id][0]
37+
await self.play_song(ctx, next_url)
38+
else:
39+
voice.stop()
40+
41+
async def play_song(self, ctx, url):
42+
"""Play a song given its URL."""
43+
YDL_OPTIONS = {"format": "bestaudio/best[height<=480]", "noplaylist": "True"}
44+
voice = get(self.bot.voice_clients, guild=ctx.guild)
45+
46+
with YoutubeDL(YDL_OPTIONS) as ydl:
47+
info = ydl.extract_info(url, download=False)
48+
URL = info["url"]
49+
FFMPEG_OPTIONS = {"before_options": "-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5", "options": "-vn"}
50+
51+
# Going for a callback approach, could also try state-machine and backround queue manager approaches
52+
def after_callback(error):
53+
if error:
54+
print(f"Player error: {error}")
55+
if ctx.guild.id in self.queue and self.queue[ctx.guild.id]:
56+
last_played = self.queue[ctx.guild.id].pop(0)
57+
next_url = self.queue[ctx.guild.id][0]
58+
# enqueue the last played song to the front of the played songs stack
59+
if ctx.guild.id not in self.prev_songs:
60+
self.prev_songs[ctx.guild.id] = []
61+
if len(self.prev_songs[ctx.guild.id]) >= PREV_QUEUE_LIMIT:
62+
self.prev_songs[ctx.guild.id].pop(0) # Remove the oldest song if the limit is reached
63+
self.prev_songs[ctx.guild.id].append(url)
64+
asyncio.run_coroutine_threadsafe(self.play_song(ctx, next_url), self.bot.loop)
65+
66+
await ctx.send(f"Playing: {url}")
67+
voice.play(FFmpegPCMAudio(URL, **FFMPEG_OPTIONS), after=after_callback)
68+
2469
@commands.command()
2570
async def play(self, ctx, url):
26-
YDL_OPTIONS = {'format': 'bestaudio/best[height<=480]', 'noplaylist': 'True'}
2771
voice = get(self.bot.voice_clients, guild=ctx.guild)
72+
# Join the voice channel if the bot isn't already in one
73+
if not voice or (voice and not voice.is_connected()):
74+
await self.join(ctx)
75+
voice = get(self.bot.voice_clients, guild=ctx.guild) # Re-fetch the voice client after joining
76+
77+
# if len(self.queue.get(ctx.guild.id, [])) >= QUEUE_LIMIT:
78+
# await ctx.send(f"Queue limit reached: ({QUEUE_LIMIT}), please wait for the queue to clear before adding more songs.")
79+
# else:
80+
if ctx.guild.id not in self.queue:
81+
self.queue[ctx.guild.id] = []
82+
self.queue[ctx.guild.id].append(url)
2883

2984
if not voice.is_playing():
30-
with YoutubeDL(YDL_OPTIONS) as ydl:
31-
info = ydl.extract_info(url, download=False)
32-
URL = info['url']
33-
FFMPEG_OPTIONS = {'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5',
34-
'options': '-vn'}
85+
await self.play_song(ctx, self.queue[ctx.guild.id][0])
3586

36-
voice.play(FFmpegPCMAudio(URL, **FFMPEG_OPTIONS))
37-
voice.is_playing()
38-
await ctx.send(f'Playing: {url}')
39-
40-
# check if the bot is already playing
87+
@commands.command()
88+
async def prev(self, ctx):
89+
"""Play the previous song."""
90+
voice = get(self.bot.voice_clients, guild=ctx.guild)
91+
if ctx.guild.id in self.prev_songs and self.prev_songs[ctx.guild.id]:
92+
voice.stop()
93+
prev_url = self.prev_songs[ctx.guild.id].pop() # Get the last played song
94+
if ctx.guild.id in self.queue:
95+
self.queue[ctx.guild.id].insert(0, prev_url) # Add the previous song to the start of the queue
96+
else:
97+
self.queue[ctx.guild.id] = [prev_url]
98+
await self.play_song(ctx, prev_url)
4199
else:
42-
await ctx.send("Bot is already playing. Please Stop it first with /stop")
43-
return
100+
await ctx.send("No previous song to play!")
44101

45102
# command to resume voice if it is paused
46103
@commands.command()
@@ -49,7 +106,7 @@ async def resume(self, ctx):
49106

50107
if not voice.is_playing():
51108
voice.resume()
52-
await ctx.send('Resuming stream')
109+
await ctx.send("Resuming stream")
53110

54111
# command to pause voice if it is playing
55112
@commands.command()
@@ -58,7 +115,7 @@ async def pause(self, ctx):
58115

59116
if voice.is_playing():
60117
voice.pause()
61-
await ctx.send('Paused stream')
118+
await ctx.send("Paused stream")
62119

63120
# command to stop voice
64121
@commands.command()
@@ -67,7 +124,63 @@ async def stop(self, ctx):
67124

68125
if voice.is_playing():
69126
voice.stop()
70-
await ctx.send('Stopping stream...')
127+
await ctx.send("Stopping stream...")
128+
129+
@commands.command()
130+
async def clear_queue(self, ctx):
131+
self.queue[ctx.guild.id] = []
132+
await ctx.send("Queue cleared!")
133+
134+
# async def youtube_video_autocompletion(self, ctx: AutocompleteContext):
135+
# current = ctx.options["video"]
136+
# data = []
137+
138+
# # Check if the current input is a link
139+
# is_link = is_youtube_link(current)
140+
# if is_link:
141+
# return [current] # Return the link as the only option
142+
143+
# # Otherwise, search YouTube for videos with the current string
144+
# videos_search = VideosSearch(current, limit=5)
145+
# results = videos_search.result()
146+
147+
# for video in results["result"]:
148+
# video_link = video["link"]
149+
# data.append(video_link)
150+
151+
# return data
152+
153+
# @commands.slash_command()
154+
# async def play(
155+
# self,
156+
# ctx: ApplicationContext,
157+
# video_name: Option(str, autocomplete=youtube_video_autocompletion),
158+
# ):
159+
# YDL_OPTIONS = {"format": "bestaudio/best[height<=480]", "noplaylist": "True"}
160+
# voice = get(self.bot.voice_clients, guild=ctx.guild)
161+
162+
# if is_youtube_link(video_name):
163+
# url = video_name
164+
# else:
165+
# videos_search = VideosSearch(video_name, limit=5)
166+
# results = videos_search.result()
167+
# url = results["result"][0]["link"]
168+
169+
# if not voice.is_playing():
170+
# with YoutubeDL(YDL_OPTIONS) as ydl:
171+
# info = ydl.extract_info(url, download=False)
172+
# voice.play(
173+
# FFmpegPCMAudio(
174+
# info["url"], **{"before_options": "-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5", "options": "-vn"}
175+
# )
176+
# )
177+
# voice.is_playing()
178+
# await ctx.send(f"Playing! {url}")
179+
180+
# # check if the bot is already playing
181+
# else:
182+
# await ctx.send("Bot is already playing! Please Stop it first with /stop")
183+
# return
71184

72185

73186
def setup(bot):

0 commit comments

Comments
 (0)