Skip to content

Commit 86b0bb7

Browse files
author
holedaemon
authored
Feature: Bumps (#56)
1 parent d307415 commit 86b0bb7

File tree

9 files changed

+553
-12
lines changed

9 files changed

+553
-12
lines changed

cogs/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from typing import TYPE_CHECKING
77
from .debug import DebugCog
88
from .player import PlayerCog
9+
from .bumps import BumpCog
910
if TYPE_CHECKING:
1011
from utils.blanco import BlancoBot
1112

@@ -17,3 +18,4 @@ def setup(bot: 'BlancoBot'):
1718
# Add cogs
1819
bot.add_cog(DebugCog(bot))
1920
bot.add_cog(PlayerCog(bot))
21+
bot.add_cog(BumpCog(bot))

cogs/bumps.py

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
"""
2+
BumpCog: Cog for guild bumps.
3+
"""
4+
5+
from typing import TYPE_CHECKING
6+
7+
from nextcord import (Color, Permissions, Interaction, SlashOption, slash_command)
8+
from nextcord.ext.commands import Cog
9+
10+
from dataclass.bump import Bump
11+
12+
from utils.url import check_url
13+
from utils.embeds import CustomEmbed, create_error_embed, create_success_embed
14+
from utils.logger import create_logger
15+
from utils.paginator import Paginator, list_chunks
16+
17+
if TYPE_CHECKING:
18+
from utils.blanco import BlancoBot
19+
20+
21+
class BumpCog(Cog):
22+
"""
23+
Cog for guild bumps.
24+
"""
25+
def __init__(self, bot: 'BlancoBot'):
26+
"""
27+
Constructor for BumpCog.
28+
"""
29+
self._bot = bot
30+
self._logger = create_logger(self.__class__.__name__)
31+
self._logger.info('Loaded BumpCog')
32+
33+
@slash_command(
34+
name='bump',
35+
dm_permission=False,
36+
default_member_permissions=Permissions(manage_guild=True)
37+
)
38+
async def bump(self, itx: Interaction):
39+
"""
40+
Base slash command for bumps.
41+
"""
42+
43+
44+
@bump.subcommand(name='toggle', description='Toggle the playback of bumps.')
45+
async def bump_toggle(
46+
self,
47+
itx: Interaction,
48+
toggle: bool = SlashOption(
49+
name='toggle',
50+
description='Turn bumps on or off?',
51+
required=False
52+
)
53+
):
54+
"""
55+
Subcommand for toggling bumps.
56+
"""
57+
if itx.guild is None:
58+
raise RuntimeError('[bump::toggle] itx.guild is None')
59+
60+
if toggle is None:
61+
enabled = self._bot.database.get_bumps_enabled(itx.guild.id)
62+
status = "Bump playback is currently enabled." if enabled \
63+
else "Bump playback is currently disabled."
64+
return await itx.response.send_message(
65+
embed=create_success_embed(
66+
title="Bumps status",
67+
body=status,
68+
)
69+
)
70+
71+
self._bot.database.set_bumps_enabled(itx.guild.id, toggle)
72+
status = "Bump playback has been enabled." if toggle \
73+
else "Bump playback has been disabled."
74+
return await itx.response.send_message(
75+
embed=create_success_embed(
76+
title="Bumps toggled",
77+
body=status,
78+
)
79+
)
80+
81+
@bump.subcommand(name='add', description='Add a bump.')
82+
async def bump_add(
83+
self,
84+
itx: Interaction,
85+
title: str = SlashOption(name='title', description='Title of bump.', required=True),
86+
author: str = SlashOption(name='author', description='Author of bump.', required=True),
87+
url: str = SlashOption(name='url', description='URL to add.', required=True),
88+
):
89+
"""
90+
Subcommand for adding a bump.
91+
"""
92+
if itx.guild is None:
93+
raise RuntimeError('[bump::add] itx.guild is None')
94+
95+
if len(title) > 32 or len(author) > 32:
96+
return await itx.response.send_message(
97+
embed=create_error_embed(
98+
message='Titles/authors cannot exceed 32 characters in length.'
99+
)
100+
)
101+
102+
if not check_url(url):
103+
return await itx.response.send_message(
104+
embed=create_error_embed(
105+
message='The given URL is not valid.'
106+
)
107+
)
108+
109+
bump = self._bot.database.get_bump_by_url(itx.guild.id, url)
110+
if bump is not None:
111+
return await itx.response.send_message(
112+
embed=create_error_embed(
113+
message='A bump with the given URL already exists.'
114+
)
115+
)
116+
117+
self._bot.database.add_bump(itx.guild.id, url, title, author)
118+
return await itx.response.send_message(
119+
embed=create_success_embed(
120+
title='Bump added',
121+
body='Bump has been successfully added to the database.'
122+
)
123+
)
124+
125+
@bump.subcommand(name='remove', description='Remove a bump.')
126+
async def bump_remove(
127+
self,
128+
itx: Interaction,
129+
idx: int = SlashOption(name='index', description='Index of bump.', required=True)
130+
):
131+
"""
132+
Subcommand for removing a bump.
133+
"""
134+
if itx.guild is None:
135+
raise RuntimeError('[bump::remove] itx.guild is None')
136+
137+
bump = self._bot.database.get_bump(itx.guild.id, idx)
138+
if bump is None:
139+
return await itx.response.send_message(
140+
embed=create_error_embed(
141+
message='There is no bump at that index for this guild.'
142+
)
143+
)
144+
145+
self._bot.database.delete_bump(itx.guild.id, idx)
146+
return await itx.response.send_message(
147+
embed=create_success_embed(
148+
title='Bump removed',
149+
body='Bump has successfully been removed from the database.'
150+
)
151+
)
152+
153+
@bump.subcommand(name='list', description='List every bump.')
154+
async def bump_list(
155+
self,
156+
itx: Interaction,
157+
):
158+
"""
159+
Subcommand for listing bumps.
160+
"""
161+
if itx.guild is None:
162+
raise RuntimeError('[bump::list] itx.guild is None')
163+
await itx.response.defer()
164+
165+
bumps = self._bot.database.get_bumps(itx.guild.id)
166+
if bumps is None:
167+
return await itx.response.send_message(
168+
embed=create_error_embed(
169+
message='This guild has no bumps.'
170+
)
171+
)
172+
173+
pages = []
174+
count = 1
175+
for _, chunk in enumerate(list_chunks(bumps)):
176+
chunk_bumps = []
177+
178+
bump: Bump
179+
for bump in chunk:
180+
line = f'{bump.idx} :: [{bump.title}]({bump.url}) by {bump.author}'
181+
chunk_bumps.append(line)
182+
count += 1
183+
184+
embed = CustomEmbed(
185+
title=f'Bumps for {itx.guild.name}',
186+
description='\n'.join(chunk_bumps),
187+
color=Color.lighter_gray()
188+
)
189+
pages.append(embed.get())
190+
191+
paginator = Paginator(itx)
192+
return await paginator.run(pages)
193+
194+
@bump.subcommand(name='interval', description='Set or get the bump interval.')
195+
async def bump_interval(
196+
self,
197+
itx: Interaction,
198+
interval: int = SlashOption(
199+
name='interval',
200+
description='The new interval bumps will play at',
201+
required=False,
202+
min_value=1,
203+
max_value=60
204+
)
205+
):
206+
"""
207+
Subcommand for changing/checking the bump interval.
208+
"""
209+
210+
if itx.guild is None:
211+
raise RuntimeError('[bump::interval] itx.guild is None')
212+
213+
if interval is None:
214+
curr_interval = self._bot.database.get_bump_interval(itx.guild.id)
215+
return await itx.response.send_message(
216+
embed=create_success_embed(
217+
title='Current Interval',
218+
body=f'A bump will play once at least every {curr_interval} minute(s).'
219+
)
220+
)
221+
222+
self._bot.database.set_bump_interval(itx.guild.id, interval)
223+
return await itx.response.send_message(
224+
embed=create_success_embed(
225+
title='Interval Changed',
226+
body=f'The bump interval has been set to {interval} minute(s).'
227+
)
228+
)

cogs/player/__init__.py

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
"""
44

55
from asyncio import TimeoutError as AsyncioTimeoutError
6-
from itertools import islice
76
from typing import TYPE_CHECKING, Any, Generator, List, Optional
87

98
from mafic import PlayerNotConnected
@@ -20,7 +19,7 @@
2019
from utils.exceptions import (EmptyQueueError, EndOfQueueError, JockeyError,
2120
JockeyException, SpotifyNoResultsError)
2221
from utils.logger import create_logger
23-
from utils.paginator import Paginator
22+
from utils.paginator import Paginator, list_chunks
2423
from utils.player_checks import check_mutual_voice
2524
from views.spotify_dropdown import SpotifyDropdownView
2625

@@ -31,14 +30,6 @@
3130
from utils.blanco import BlancoBot
3231

3332

34-
def list_chunks(data: List[Any]) -> Generator[List[Any], Any, Any]:
35-
"""
36-
Yield 10-element chunks of a list. Used for pagination.
37-
"""
38-
for i in range(0, len(data), 10):
39-
yield list(islice(data, i, i + 10))
40-
41-
4233
class PlayerCog(Cog):
4334
"""
4435
Cog for creating, controlling, and destroying music players for guilds.

cogs/player/jockey.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
from utils.constants import UNPAUSE_THRESHOLD
1515
from utils.embeds import create_error_embed
1616
from utils.exceptions import (EndOfQueueError, JockeyError, JockeyException,
17-
LavalinkSearchError, SpotifyNoResultsError)
17+
LavalinkSearchError, SpotifyNoResultsError,
18+
BumpError, BumpNotReadyError, BumpNotEnabledError)
1819
from utils.musicbrainz import annotate_track
1920
from utils.time import human_readable_time
2021
from views.now_playing import NowPlayingView
@@ -547,6 +548,18 @@ async def skip(self, *, forward: bool = True, index: int = -1, auto: bool = True
547548
# to prevent the user from spamming them.
548549
await self._edit_np_controls(show_controls=False)
549550

551+
try:
552+
await self.play_bump()
553+
return
554+
except (JockeyException, SpotifyNoResultsError) as err:
555+
self._logger.error('Error parsing bump into track: %s', err)
556+
except BumpError as err:
557+
self._logger.error('Error playing bump: %s', err)
558+
except BumpNotEnabledError:
559+
self._logger.debug('Bumps are not enabled in this guild.')
560+
except BumpNotReadyError:
561+
self._logger.debug('Not ready to play a bump yet.')
562+
550563
# If index is specified, use that instead
551564
if index != -1:
552565
try:
@@ -632,3 +645,39 @@ async def update_now_playing(self):
632645
self.guild.name,
633646
exc
634647
)
648+
649+
async def play_bump(self):
650+
"""
651+
Check and attempt to play a bump if it's been long enough.
652+
"""
653+
654+
enabled = self._db.get_bumps_enabled(self.guild.id)
655+
if not enabled:
656+
raise BumpNotEnabledError
657+
658+
interval = self._db.get_bump_interval(self.guild.id) * 60
659+
last_bump = self._db.get_last_bump(self.guild.id)
660+
661+
if last_bump == 0:
662+
self._db.set_last_bump(self.guild.id)
663+
raise BumpNotReadyError
664+
665+
if int(time()) - last_bump < interval:
666+
raise BumpNotReadyError
667+
668+
bump = self._db.get_random_bump(self.guild.id)
669+
if bump is None:
670+
raise BumpError('Guild has no bumps.')
671+
672+
requester = self._bot.user.id if self._bot.user is not None else self.guild.me.id
673+
674+
try:
675+
tracks = await parse_query(self.node, self._bot.spotify, bump.url, requester)
676+
except (JockeyException, SpotifyNoResultsError):
677+
raise
678+
679+
if len(tracks) == 0:
680+
raise BumpError('Unable to parse bump URL into tracks.')
681+
682+
await self._play(tracks[0])
683+
self._db.set_last_bump(self.guild.id)

0 commit comments

Comments
 (0)