Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Player buttons #12

Merged
merged 7 commits into from
Jan 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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