Skip to content

Commit

Permalink
Add pagination to queue via reactions and invoke argument.
Browse files Browse the repository at this point in the history
  • Loading branch information
itsTheFae committed Feb 8, 2024
1 parent 74a3d40 commit 332fd9d
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 75 deletions.
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@

[![GitHub stars](https://img.shields.io/github/stars/Just-Some-Bots/MusicBot.svg)](https://github.com/Just-Some-Bots/MusicBot/stargazers)
[![GitHub forks](https://img.shields.io/github/forks/Just-Some-Bots/MusicBot.svg)](https://github.com/Just-Some-Bots/MusicBot/network)
[![Python version](https://img.shields.io/badge/python-3.8%2C%203.9%2C%203.10%2C%203.11-blue.svg)](https://python.org)
[![Python version](https://img.shields.io/badge/python-3.8%2C%203.9%2C%203.10%2C%203.11%2C%203.12-blue.svg)](https://python.org)
[![Discord](https://discordapp.com/api/guilds/129489631539494912/widget.png?style=shield)](https://discord.gg/bots)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/)
[![Checked with mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/)
![Static Badge](https://img.shields.io/badge/Lint-Pylint_and_Flake8-blue?style=flat)


MusicBot is the original Discord music bot written for [Python](https://www.python.org "Python homepage") 3.8+, using the [discord.py](https://github.com/Rapptz/discord.py) library. It plays requested songs from YouTube and other services into a Discord server (or multiple servers). If the queue is empty, MusicBot will play a list of existing songs that is configurable. The bot features a permission system, allowing owners to restrict commands to certain people. MusicBot is capable of streaming live media into a voice channel (experimental).

Expand All @@ -29,6 +34,9 @@ This fork contains changes that may or may not be merged into upstream.
Cherry-picking (or otherwise copying) is welcome should you feel inclined.
Here is a list of changes made so far, with most recent first:


- Update the `queue` command to add pagination by both command arg and reactions.
- Allow `listids` and `perms` commands to fall back to sending in public if DM fails.
- Add actual command-line arguments to control logging, show version, and skip startup checks.
- Supported CLI flags:
- `-V` to print version and exit.
Expand Down
4 changes: 2 additions & 2 deletions config/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,8 @@
"cmd-cache-clear-no-cache": "No cache found to clear.",
"cmd-queue-more": "\n... and %s more",
"cmd-queue-none": "There are no songs queued! Queue something with {}play.",
"cmd-queue-playing-author": "Currently playing: `{0}` added by `{1}` {2}\n",
"cmd-queue-playing-noauthor": "Currently playing: `{0}` {1}\n",
"cmd-queue-playing-author": "Currently playing: `{0}`\nAdded by: `{1}`\nProgress: {2}\n",
"cmd-queue-playing-noauthor": "Currently playing: `{0}`\nProgress: {1}\n",
"cmd-queue-entry-author": "{0} -- `{1}` by `{2}`",
"cmd-queue-entry-noauthor": "{0} -- `{1}`",
"cmd-clean-invalid": "Invalid parameter. Please provide a number of messages to search.",
Expand Down
210 changes: 138 additions & 72 deletions musicbot/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
EMOJI_CHECK_MARK_BUTTON,
EMOJI_CROSS_MARK_BUTTON,
EMOJI_IDLE_ICON,
EMOJI_NEXT_ICON,
EMOJI_PREV_ICON,
)
from .constants import VERSION as BOTVERSION
from .constructs import GuildSpecificData, Response
Expand Down Expand Up @@ -1837,6 +1839,7 @@ def _gen_embed(self) -> discord.Embed:
text=self.config.footer_text, icon_url="https://i.imgur.com/gFHBoZA.png"
)

# TODO: handle this part when EmbedResponse get handled.
author_name = "MusicBot"
avatar_url = None
if self.user:
Expand Down Expand Up @@ -2732,7 +2735,6 @@ async def _cmd_play_compound_link(
associated playlist to be queued as well.
"""
# TODO: maybe add config to auto yes or no and bypass this.
# TODO: this currently will queue the original video twice.

async def _prompt_for_playing(
prompt: str, next_url: str, ignore_vid: str = ""
Expand Down Expand Up @@ -2810,25 +2812,6 @@ async def _cmd_play(
"""
This function handles actually playing any given URL or song subject.
Tested against these URLs:
- https://www.youtube.com/watch?v=UBxIN7f1k30 # live stream that will be dead in the future.
- https://www.youtube.com/playlist?list=PLBcHt8htZXKVCzW_Mkn4NrByBxn53o3cA # 1373 videos, 8+ hours each, coffee house jazz.
- https://www.youtube.com/playlist?list=PL80gRr4GwcsznLYH-G_FXnzkP5_cHl-KR
- https://www.youtube.com/watch?v=bm48ncbhU10&list=PL80gRr4GwcsznLYH-G_FXnzkP5_cHl-KR
- https://www.youtube.com/watch?v=bm48ncbhU10
- https://youtu.be/L5uV3gmOH9g
- https://open.spotify.com/playlist/37i9dQZF1DXaImRpG7HXqp
- https://open.spotify.com/track/0YupMLYOYz6lZDbN3kRt7A?si=5b0eeb51b04c4af9
- https://open.spotify.com/album/1y8Yw0NDcP2qxbZufIXt7u # 1 item album
- https://open.spotify.com/album/5LbHbwejgZXRZAgzVAjkhj # multi-item album
- https://soundcloud.com/neilcic/cabinet-man
- https://soundcloud.com/grweston/sets/mashups
- https://lemondemon.bandcamp.com/album/spirit-phone
- slippery people talking heads live 84
- ytsearch4:talking heads stop making sense
- https://cdn.discordapp.com/attachments/741945274901200897/875075008723046410/cheesed.mp4
- https://playerservices.streamtheworld.com/api/livestream-redirect/KUPDFM.mp3?dist=hubbard&source=hubbard-web&ttag=web&gdpr=0
"""
player = _player if _player else None

Expand Down Expand Up @@ -4315,20 +4298,53 @@ async def cmd_cache(self, opt: str = "info") -> CommandResponse:
return None

async def cmd_queue(
self, guild: discord.Guild, player: MusicPlayer
self,
guild: discord.Guild,
channel: MessageableChannel,
player: MusicPlayer,
page: str = "0",
update_msg: Optional[discord.Message] = None,
) -> CommandResponse:
"""
Usage:
{command_prefix}queue
{command_prefix}queue [page_number]
Prints the current song queue.
Show later entries if available by giving optional page number.
"""

# TODO: find a way to paginate the results herein.
lines = []
unlisted = 0
andmoretext = f"* ... and {len(player.playlist.entries)} more*"
# handle the page argument.
page_number = 0
if page:
try:
page_number = abs(int(page))
except (ValueError, TypeError) as e:
raise exceptions.CommandError(
"Queue page argument must be a whole number.",
expire_in=30,
) from e

# check for no entries at all.
total_entry_count = len(player.playlist.entries)
if not total_entry_count:
raise exceptions.CommandError(
self.str.get(
"cmd-queue-none",
"There are no songs queued! Queue something with {}play.",
).format(self.server_data[guild.id].command_prefix)
)

# now check if page number is out of bounds.
limit_per_page = 10 # TODO: make this configurable, up to 25 fields per embed.
pages_total = math.ceil(total_entry_count / limit_per_page)
if page_number > pages_total:
raise exceptions.CommandError(
"Requested page number is out of bounds.\n"
f"There are **{pages_total}** pages."
)

# Get current entry info if any.
current_progress = ""
if player.is_playing and player.current_entry:
song_progress = format_song_duration(player.progress)
song_total = (
Expand All @@ -4338,65 +4354,115 @@ async def cmd_queue(
)
prog_str = f"`[{song_progress}/{song_total}]`"

if player.current_entry.meta.get(
"channel", False
) and player.current_entry.meta.get("author", False):
lines.append(
self.str.get(
"cmd-queue-playing-author",
"Currently playing: `{0}` added by `{1}` {2}\n",
).format(
player.current_entry.title,
player.current_entry.meta["author"].name,
prog_str,
)
# TODO: Honestly the meta info could use a typed interface too.
cur_entry_channel = player.current_entry.meta.get("channel", None)
cur_entry_author = player.current_entry.meta.get("author", None)
if cur_entry_channel and cur_entry_author:
current_progress = self.str.get(
"cmd-queue-playing-author",
"Currently playing: `{0}`\nAdded by: `{1}`\nProgress: {2}\n",
).format(
player.current_entry.title,
cur_entry_author.name,
prog_str,
)

else:
lines.append(
self.str.get(
"cmd-queue-playing-noauthor", "Currently playing: `{0}` {1}\n"
).format(player.current_entry.title, prog_str)
)
current_progress = self.str.get(
"cmd-queue-playing-noauthor",
"Currently playing: `{0}`\nProgress: {1}\n",
).format(player.current_entry.title, prog_str)

# calculate start and stop slice indices
start_index = limit_per_page * page_number
end_index = start_index + limit_per_page

# create an embed
starting_at = start_index + 1 # add 1 to index for display.
embed = self._gen_embed()
embed.title = "Songs in queue"
embed.description = (
f"{current_progress}There are `{total_entry_count}` entries in the queue.\n"
f"Here are the next {limit_per_page} songs, starting at song #{starting_at}"
)

for i, item in enumerate(player.playlist, 1):
if item.meta.get("channel", False) and item.meta.get("author", False):
nextline = (
self.str.get("cmd-queue-entry-author", "{0} -- `{1}` by `{2}`")
.format(i, item.title, item.meta["author"].name)
.strip()
# add the tracks to the embed fields
queue_segment = list(player.playlist.entries)[start_index:end_index]
for idx, item in enumerate(queue_segment, starting_at):
if item == player.current_entry:
# TODO: remove this debug later
log.debug("Skipped the current playlist entry.")
continue

item_channel = item.meta.get("channel", None)
item_author = item.meta.get("author", None)
if item_channel and item_author:
embed.add_field(
name=f"Entry #{idx}",
value=f"Title: `{item.title}`\nAdded by: `{item_author.name}`",
inline=False,
)
else:
nextline = (
self.str.get("cmd-queue-entry-noauthor", "{0} -- `{1}`")
.format(i, item.title)
.strip()
embed.add_field(
name=f"Entry #{idx}",
value=f"Title: `{item.title}`",
inline=False,
)

currentlinesum = sum(len(x) + 1 for x in lines) # +1 is for newline char
# handle sending or editing the queue message.
if update_msg:
q_msg = await self.safe_edit_message(update_msg, embed, send_if_fail=True)
else:
if pages_total <= 1:
q_msg = await self.safe_send_message(channel, embed, expire_in=30)
else:
q_msg = await self.safe_send_message(channel, embed)

if (
currentlinesum + len(nextline) + len(andmoretext)
> DISCORD_MSG_CHAR_LIMIT
) or (i > self.config.queue_length):
if currentlinesum + len(andmoretext):
unlisted += 1
continue
if pages_total <= 1:
log.debug("Not enough entries to paginate the queue.")
return None

if not q_msg:
log.warning("Could not post queue message, no message to add reactions to.")
raise exceptions.CommandError(
"Try that again. MusicBot couldn't make or get a reference to the queue message. If the issue persists, file a bug report."
)

lines.append(nextline)
# set up the page numbers to be used by reactions.
# this essentially make the pages wrap around.
prev_index = page_number - 1
next_index = page_number + 1
if prev_index < 0:
prev_index = pages_total
if next_index > pages_total:
next_index = 0

if unlisted:
lines.append(self.str.get("cmd-queue-more", "\n... and %s more") % unlisted)
for r in [EMOJI_PREV_ICON, EMOJI_NEXT_ICON, EMOJI_CROSS_MARK_BUTTON]:
await q_msg.add_reaction(r)

if not lines:
lines.append(
self.str.get(
"cmd-queue-none",
"There are no songs queued! Queue something with {}play.",
).format(self.server_data[guild.id].command_prefix)
def _check_react(reaction: discord.Reaction, user: discord.Member) -> bool:
# Do not check for the requesting author, any reaction is valid.
return q_msg.id == reaction.message.id and user.id != self.user.id

try:
reaction, _user = await self.wait_for(
"reaction_add", timeout=60, check=_check_react
)
if reaction.emoji == EMOJI_NEXT_ICON:
await q_msg.clear_reactions()
await self.cmd_queue(guild, channel, player, str(next_index), q_msg)

message = "\n".join(lines)
return Response(message, delete_after=30)
if reaction.emoji == EMOJI_PREV_ICON:
await q_msg.clear_reactions()
await self.cmd_queue(guild, channel, player, str(prev_index), q_msg)

if reaction.emoji == EMOJI_CROSS_MARK_BUTTON:
await self.safe_delete_message(q_msg)

except asyncio.TimeoutError:
await self.safe_delete_message(q_msg)

return None

async def cmd_clean(
self,
Expand Down
6 changes: 6 additions & 0 deletions musicbot/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

# constant string exempt from i18n
DEFAULT_FOOTER_TEXT: str = f"Just-Some-Bots/MusicBot ({VERSION})"
DEFAULT_BOT_NAME: str = "MusicBot"
DEFAULT_BOT_ICON: str = "https://i.imgur.com/gFHBoZA.png"


# File path constants
Expand Down Expand Up @@ -54,3 +56,7 @@
EMOJI_IDLE_ICON: str = "\U0001f634" # same as \N{SLEEPING FACE}
EMOJI_PLAY_ICON: str = "\u25B6" # add \uFE0F to make button
EMOJI_PAUSE_ICON: str = "\u23F8\uFE0F" # add \uFE0F to make button
EMOJI_LAST_ICON: str = "\u23ED\uFE0F" # next track button
EMOJI_FIRST_ICON: str = "\u23EE\uFE0F" # last track button
EMOJI_NEXT_ICON: str = "\u23E9" # fast-forward button
EMOJI_PREV_ICON: str = "\u23EA" # rewind button

0 comments on commit 332fd9d

Please sign in to comment.