From 11e4a34a96012f15e5e14ac4e20d1c2da4f5621f Mon Sep 17 00:00:00 2001 From: cloudwithax Date: Thu, 11 Nov 2021 17:13:34 -0500 Subject: [PATCH] Added original spotify track to pomice.Track as an attribute --- examples/advanced.py | 212 +++++++++++++++++++++++++++++++++++++ pomice/objects.py | 23 ++-- pomice/pool.py | 5 +- pomice/spotify/playlist.py | 5 +- 4 files changed, 232 insertions(+), 13 deletions(-) create mode 100644 examples/advanced.py diff --git a/examples/advanced.py b/examples/advanced.py new file mode 100644 index 0000000..a312ef3 --- /dev/null +++ b/examples/advanced.py @@ -0,0 +1,212 @@ +""" +This example aims to show the full capabilities of the library. +This is in the form of a drop-in cog you can use and modify to your liking. +This example aims to include everything you would need to make a fully functioning music bot, +from a queue system, advanced queue control and more. +""" + +import discord +import pomice +import asyncio + +from discord.ext import commands +from contextlib import suppress + + +class Player(pomice.Player): + """Custom pomice Player class.""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.queue = asyncio.Queue() + self.controller: discord.Message = None + self.context: commands.Context = None + self.dj: discord.Member = None + + self.pause_votes = set() + self.resume_votes = set() + self.skip_votes = set() + self.shuffle_votes = set() + self.stop_votes = set() + + async def do_next(self) -> None: + # Clear the votes for a new song + self.pause_votes.clear() + self.resume_votes.clear() + self.skip_votes.clear() + self.shuffle_votes.clear() + self.stop_votes.clear() + + # Check if theres a controller still active and deletes it + if self.controller: + with suppress(discord.HTTPException): + await self.controller.delete() + + + # Queue up the next track, else teardown the player + try: + track: pomice.Track = self.queue.get_nowait() + except asyncio.queues.QueueEmpty: + return await self.teardown() + + await self.play(track) + + # Call the controller (a.k.a: The "Now Playing" embed) and check if one exists + + if track.is_stream: + embed = discord.Embed(title="Now playing", description=f":red_circle: **LIVE** [{track.title}]({track.uri}) [{track.requester.mention}]") + embed.set_footer(text=f"Use {track.ctx.prefix}help for more commands!", icon_url=f"{self.client.user.avatar.url}") + self.controller = await self.context.send(embed=embed) + else: + embed = discord.Embed(title=f"Now playing", description=f"[{track.title}]({track.uri}) [{track.requester.mention}]") + embed.set_footer(text=f"Use {track.ctx.prefix}help for more commands!", icon_url=f"{self.client.user.avatar.url}") + self.controller = await self.context.send(embed=embed) + + + async def teardown(self): + """Clear internal states, remove player controller and disconnect.""" + with suppress((discord.HTTPException), (KeyError)): + await self.destroy() + if self.controller: + await self.controller.delete() + + async def set_context(self, ctx: commands.Context): + """Set context for the player""" + self.context = ctx + self.dj = ctx.author + + + + +class Music(commands.Cog): + def __init__(self, bot: commands.Bot) -> None: + self.bot = bot + + # In order to initialize a node, or really do anything in this library, + # you need to make a node pool + self.pomice = pomice.NodePool() + + async def start_nodes(self): + # You can pass in Spotify credentials to enable Spotify querying. + # If you do not pass in valid Spotify credentials, Spotify querying will not work + await self.pomice.create_node( + bot=self.bot, + host="127.0.0.1", + port="3030", + password="youshallnotpass", + identifier="MAIN" + ) + print(f"Node is ready!") + + @commands.Cog.listener() + async def on_pomice_track_end(self, player: Player, track, _): + await player.do_next() + + @commands.Cog.listener() + async def on_pomice_track_stuck(self, player: Player, track, _): + await player.do_next() + + @commands.Cog.listener() + async def on_pomice_track_exception(self, player: Player, track, _): + await player.do_next() + + @commands.command(aliases=["connect"]) + async def join(self, ctx: commands.Context, *, channel: discord.VoiceChannel = None) -> None: + if not channel: + channel = getattr(ctx.author.voice, "channel", None) + if not channel: + return await ctx.send("You must be in a voice channel in order to use this command!") + + # With the release of discord.py 1.7, you can now add a compatible + # VoiceProtocol class as an argument in VoiceChannel.connect(). + # This library takes advantage of that and is how you initialize a player. + await ctx.author.voice.channel.connect(cls=Player) + player: Player = ctx.voice_client + # Set the player context so we can use it so send messages + player.set_context(ctx=ctx) + await ctx.send(f"Joined the voice channel `{channel.name}`") + + @commands.command(aliases=["dc", "disconnect"]) + async def leave(self, ctx: commands.Context): + if not ctx.voice_client: + return await ctx.send("You must be in a voice channel in order to use this command!") + + player: pomice.Player = ctx.voice_client + + await player.destroy() + await ctx.send("Player has left the channel.") + + @commands.command(aliases=["p"]) + async def play(self, ctx: commands.Context, *, search: str) -> None: + # Checks if the player is in the channel before we play anything + if not ctx.voice_client: + await ctx.invoke(self.join) + + player: Player = ctx.voice_client + + # If you search a keyword, Pomice will automagically search the result using YouTube + # You can pass in "search_type=" as an argument to change the search type + # i.e: player.get_tracks("query", search_type=SearchType.ytmsearch) + # will search up any keyword results on YouTube Music + results = await player.get_tracks(search) + + if not results: + raise commands.CommandError("No results were found for that search term.") + + if isinstance(results, pomice.Playlist): + for track in results.tracks: + await player.queue.put(track) + else: + track = results[0] + await player.queue.put(track) + + if not player.is_playing: + await player.play + + + + @commands.command() + async def pause(self, ctx: commands.Context): + if not ctx.voice_client: + raise commands.CommandError("No player detected") + + player: pomice.Player = ctx.voice_client + + if player.is_paused: + return await ctx.send("Player is already paused!") + + await player.set_pause(pause=True) + await ctx.send("Player has been paused") + + @commands.command() + async def resume(self, ctx: commands.Context): + if not ctx.voice_client: + raise commands.CommandError("No player detected") + + player: pomice.Player = ctx.voice_client + + if not player.is_paused: + return await ctx.send("Player is already playing!") + + await player.set_pause(pause=False) + await ctx.send("Player has been resumed") + + @commands.command() + async def stop(self, ctx: commands.Context): + if not ctx.voice_client: + raise commands.CommandError("No player detected") + + player: pomice.Player = ctx.voice_client + + if not player.is_playing: + return await ctx.send("Player is already stopped!") + + await player.stop() + await ctx.send("Player has been stopped") + + +def setup(bot: commands.Bot): + bot.add_cog(Music(bot)) + + diff --git a/pomice/objects.py b/pomice/objects.py index e7a3cf9..d07a7fd 100644 --- a/pomice/objects.py +++ b/pomice/objects.py @@ -17,17 +17,22 @@ class Track: info: dict, ctx: Optional[commands.Context] = None, spotify: bool = False, - search_type: SearchType = SearchType.ytsearch + search_type: SearchType = SearchType.ytsearch, + spotify_track = None, ): self.track_id = track_id self.info = info self.spotify = spotify - self.original: Optional[Track] = None if self.spotify else self - self._search_type = search_type + if self.spotify: + self.original: Optional[Track] = None + self._search_type = search_type + self.spotify_track = spotify_track + self.title = info.get("title") self.author = info.get("author") + self.thumbnail = info.get("thumbnail") self.length = info.get("length") self.ctx = ctx self.requester = self.ctx.author if ctx else None @@ -66,20 +71,18 @@ class Playlist: tracks: list, ctx: Optional[commands.Context] = None, spotify: bool = False, - thumbnail: Optional[str] = None, - uri: Optional[str] = None, + spotify_playlist = None ): self.playlist_info = playlist_info self.tracks_raw = tracks self.spotify = spotify - self.name = playlist_info.get("name") - - self._thumbnail = thumbnail - self._uri = uri - + if self.spotify: self.tracks = tracks + self.spotify_playlist = spotify_playlist + self._thumbnail = self.spotify_playlist.image + self._uri = self.spotify_playlist.uri else: self.tracks = [ Track(track_id=track["track"], info=track["info"], ctx=ctx) diff --git a/pomice/pool.py b/pomice/pool.py index 9a15048..7680435 100644 --- a/pomice/pool.py +++ b/pomice/pool.py @@ -300,6 +300,7 @@ class Node: ctx=ctx, search_type=search_type, spotify=True, + spotify_track=spotify_results, info={ "title": spotify_results.name, "author": spotify_results.artists, @@ -320,6 +321,7 @@ class Node: ctx=ctx, search_type=search_type, spotify=True, + spotify_track=track, info={ "title": track.name, "author": track.artists, @@ -339,8 +341,7 @@ class Node: tracks=tracks, ctx=ctx, spotify=True, - thumbnail=spotify_results.image, - uri=spotify_results.uri + spotify_playlist=spotify_results ) elif discord_url := DISCORD_MP3_URL_REGEX.match(query): diff --git a/pomice/spotify/playlist.py b/pomice/spotify/playlist.py index 6f59806..edff867 100644 --- a/pomice/spotify/playlist.py +++ b/pomice/spotify/playlist.py @@ -11,7 +11,10 @@ class Playlist: self.owner = data["owner"]["display_name"] self.total_tracks = data["tracks"]["total"] self.id = data["id"] - self.image = data["images"][0]["url"] + if data.get("images"): + self.image = data["images"][0]["url"] + else: + self.image = None self.uri = data["external_urls"]["spotify"] def __repr__(self) -> str: