Skip to content

Commit

Permalink
Merge pull request #12 from Krutyi-4el/develop
Browse files Browse the repository at this point in the history
Player buttons
  • Loading branch information
solaluset authored Jan 2, 2023
2 parents d2675e0 + 3d40bc5 commit d8bc462
Show file tree
Hide file tree
Showing 5 changed files with 282 additions and 79 deletions.
179 changes: 160 additions & 19 deletions musicbot/audiocontroller.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from enum import Enum
from inspect import isawaitable
from typing import TYPE_CHECKING, Coroutine, Optional, List, Tuple

import discord
Expand All @@ -8,13 +9,15 @@
from musicbot import linkutils, utils
from musicbot.playlist import Playlist
from musicbot.songinfo import Song
from musicbot.utils import compare_components

# avoiding circular import
if TYPE_CHECKING:
from musicbot.bot import MusicBot


_cached_downloaders: List[Tuple[dict, yt_dlp.YoutubeDL]] = []
_not_provided = object()


class PauseState(Enum):
Expand All @@ -23,6 +26,24 @@ class PauseState(Enum):
RESUMED = "Resumed playback :arrow_forward:"


class LoopState(Enum):
INVALID = "Invalid loop mode!"
ENABLED = "Loop enabled :arrows_counterclockwise:"
DISABLED = "Loop disabled :x:"


class MusicButton(discord.ui.Button):
def __init__(self, callback, **kwargs):
super().__init__(**kwargs)
self._callback = callback

async def callback(self, inter):
await inter.response.defer()
res = self._callback(inter)
if isawaitable(res):
await res


class AudioController(object):
"""Controls the playback of audio and the sequential playing of the songs.
Expand All @@ -37,6 +58,7 @@ def __init__(self, bot: "MusicBot", guild: discord.Guild):
self.bot = bot
self.playlist = Playlist()
self.current_song = None
self._next_song = None
self.guild = guild

sett = bot.settings[guild]
Expand All @@ -46,6 +68,9 @@ def __init__(self, bot: "MusicBot", guild: discord.Guild):

self.command_channel: Optional[discord.abc.Messageable] = None

self.last_message = None
self.last_view = None

# according to Python documentation, we need
# to keep strong references to all tasks
self._tasks = set()
Expand Down Expand Up @@ -102,6 +127,107 @@ async def fetch_song_info(self, song: Song):
)
song.update(info)

def make_view(self):
if not self.is_active():
return None

view = discord.ui.View(timeout=None)
is_empty = len(self.playlist) == 0

prev_button = MusicButton(
lambda _: self.prev_song(),
disabled=not self.playlist.has_prev(),
emoji="⏮️",
)
view.add_item(prev_button)

pause_button = MusicButton(
lambda _: self.pause(),
emoji="⏸️" if self.guild.voice_client.is_playing() else "▶️",
)
view.add_item(pause_button)

next_button = MusicButton(
lambda _: self.next_song(),
disabled=not self.playlist.has_next(),
emoji="⏭️",
)
view.add_item(next_button)

loop_button = MusicButton(
lambda _: self.loop(),
disabled=is_empty,
emoji="🔁",
label="Loop: " + self.playlist.loop,
)
view.add_item(loop_button)

np_button = MusicButton(
self.current_song_callback,
row=1,
disabled=self.current_song is None,
emoji="💿",
)
view.add_item(np_button)

shuffle_button = MusicButton(
lambda _: self.playlist.shuffle(),
row=1,
disabled=is_empty,
emoji="🔀",
)
view.add_item(shuffle_button)

queue_button = MusicButton(
self.queue_callback, row=1, disabled=is_empty, emoji="📜"
)
view.add_item(queue_button)

stop_button = MusicButton(
lambda _: self.stop_player(),
row=1,
emoji="⏹️",
style=discord.ButtonStyle.red,
)
view.add_item(stop_button)

self.last_view = view

return view

async def current_song_callback(self, inter):
await (await inter.client.get_application_context(inter)).send(
embed=self.current_song.info.format_output(config.SONGINFO_SONGINFO),
)

async def queue_callback(self, inter):
await (await inter.client.get_application_context(inter)).send(
embed=self.playlist.queue_embed(),
)

async def update_view(self, view=_not_provided):
msg = self.last_message
if not msg:
return
old_view = self.last_view
if view is _not_provided:
view = self.make_view()
if view is None:
self.last_message = None
elif compare_components(old_view.to_components(), view.to_components()):
return
try:
await msg.edit(view=view)
except discord.HTTPException as e:
if e.code == 50027: # Invalid Webhook Token
try:
self.last_message = await msg.channel.fetch_message(msg.id)
await self.update_view(view)
except discord.NotFound:
self.last_message = None
else:
print("Failed to update view:", e)

def is_active(self) -> bool:
client = self.guild.voice_client
return client is not None and (client.is_playing() or client.is_paused())
Expand All @@ -121,14 +247,36 @@ def pause(self):
elif client.is_paused():
client.resume()
return PauseState.RESUMED
else:
return PauseState.NOTHING_TO_PAUSE
return PauseState.NOTHING_TO_PAUSE

def loop(self, mode=None):
if mode is None:
if self.playlist.loop == "off":
mode = "all"
else:
mode = "off"

if mode not in ("all", "single", "off"):
return LoopState.INVALID

self.playlist.loop = mode

if mode == "off":
return LoopState.DISABLED
return LoopState.ENABLED

def next_song(self, error=None):
"""Invoked after a song is finished. Plays the next song if there is one."""

next_song = self.playlist.next(self.current_song)
if self.is_active():
self.guild.voice_client.stop()
return

if self._next_song:
next_song = self._next_song
self._next_song = None
else:
next_song = self.playlist.next()

self.current_song = None

Expand Down Expand Up @@ -158,8 +306,6 @@ async def play_song(self, song: Song):
self.playlist.add_name(song.info.title)
self.current_song = song

self.playlist.playhistory.append(self.current_song)

self.guild.voice_client.play(
discord.FFmpegPCMAudio(
song.base_url,
Expand All @@ -178,8 +324,6 @@ async def play_song(self, song: Song):
embed=song.info.format_output(config.SONGINFO_NOW_PLAYING)
)

self.playlist.playque.popleft()

for song in list(self.playlist.playque)[: config.MAX_SONG_PRELOAD]:
self.add_task(self.preload(song))

Expand Down Expand Up @@ -334,34 +478,30 @@ async def search_youtube(self, title: str) -> Optional[dict]:

return r["entries"][0]

async def stop_player(self):
def stop_player(self):
"""Stops the player and removes all songs from the queue"""
if not self.is_active():
return

self.playlist.loop = "off"
self.playlist.next(self.current_song)
self.playlist.next()
self.clear_queue()
self.guild.voice_client.stop()

async def prev_song(self) -> bool:
def prev_song(self) -> bool:
"""Loads the last song from the history into the queue and starts it"""

self.timer.cancel()
self.timer = utils.Timer(self.timeout_handler)

if len(self.playlist.playhistory) == 0:
prev_song = self.playlist.prev()
if not prev_song:
return False

prev_song = self.playlist.prev(self.current_song)

if not self.is_active():

if prev_song == "Dummy":
self.playlist.next(self.current_song)
return False
await self.play_song(prev_song)
self.add_task(self.play_song(prev_song))
else:
self._next_song = prev_song
self.guild.voice_client.stop()
return True

Expand Down Expand Up @@ -396,7 +536,8 @@ async def uconnect(self, ctx):
return False

async def udisconnect(self):
await self.stop_player()
self.stop_player()
await self.update_view(None)
if self.guild.voice_client is None:
return False
await self.guild.voice_client.disconnect(force=True)
Expand Down
34 changes: 32 additions & 2 deletions musicbot/bot.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from typing import Dict, Union

import discord
from discord.ext import bridge
from discord.ext import bridge, tasks
from discord.ext.commands import DefaultHelpCommand
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import sessionmaker
Expand Down Expand Up @@ -38,6 +38,11 @@ async def start(self, *args, **kwargs):
await extract_legacy_settings(self)
return await super().start(*args, **kwargs)

async def close(self):
for audiocontroller in self.audio_controllers.values():
await audiocontroller.udisconnect()
return await super().close()

async def on_ready(self):
self.settings.update(await GuildSettings.load_many(self, self.guilds))

Expand All @@ -47,10 +52,18 @@ async def on_ready(self):

print(config.STARTUP_COMPLETE_MESSAGE)

if not self.update_views.is_running():
self.update_views.start()

async def on_guild_join(self, guild):
print(guild.name)
await self.register(guild)

@tasks.loop(seconds=1)
async def update_views(self):
for audiocontroller in self.audio_controllers.values():
await audiocontroller.update_view()

def add_command(self, command):
# fix empty description
# https://github.com/Pycord-Development/pycord/issues/1619
Expand All @@ -74,6 +87,13 @@ async def get_prefix(
async def get_application_context(self, interaction):
return await super().get_application_context(interaction, ApplicationContext)

async def process_application_commands(self, inter):
if not inter.guild:
await inter.response.send_message(config.NO_GUILD_MESSAGE)
return

await super().process_application_commands(inter)

async def process_commands(self, message: discord.Message):
if message.author.bot:
return
Expand Down Expand Up @@ -124,8 +144,18 @@ class Context(bridge.BridgeContext):
guild: discord.Guild

async def send(self, *args, **kwargs):
audiocontroller = self.bot.audio_controllers[self.guild]
await audiocontroller.update_view(None)
view = audiocontroller.make_view()
if view:
kwargs["view"] = view
# use `respond` for compatibility
return await self.respond(*args, **kwargs)
res = await self.respond(*args, **kwargs)
if isinstance(res, discord.Interaction):
audiocontroller.last_message = await res.original_message()
else:
audiocontroller.last_message = res
return res


class ExtContext(bridge.BridgeExtContext, Context):
Expand Down
Loading

0 comments on commit d8bc462

Please sign in to comment.