├── .gitignore ├── MANIFEST.in ├── DiscordUtils ├── __init__.py ├── InviteTracker.py ├── Music.py └── Pagination.py ├── setup.py ├── LICENSE.txt └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include DiscordUtils/Pagination.py 2 | include DiscordUtils/InviteTracker.py 3 | include DiscordUtils/Music.py 4 | -------------------------------------------------------------------------------- /DiscordUtils/__init__.py: -------------------------------------------------------------------------------- 1 | from DiscordUtils import Pagination 2 | from DiscordUtils.InviteTracker import InviteTracker 3 | from DiscordUtils.Music import Music 4 | 5 | __title__ = "DiscordUtils" 6 | __version__ = "1.3.4" 7 | __author__ = "toxicrecker" 8 | __license__ = "MIT" 9 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r", encoding="utf-8", errors="ignore") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="DiscordUtils", 8 | version="1.3.4", 9 | author="toxicrecker", 10 | description="DiscordUtils is a very useful library made to be used with discord.py", 11 | long_description=long_description, 12 | long_description_content_type="text/markdown", 13 | url="https://www.github.com/toxicrecker/DiscordUtils", 14 | packages=setuptools.find_packages(), 15 | classifiers=[ 16 | "Programming Language :: Python :: 3", 17 | "License :: OSI Approved :: MIT License", 18 | "Operating System :: OS Independent", 19 | ], 20 | python_requires=">= 3.6", 21 | include_package_data=True, 22 | install_requires=["discord.py"], 23 | extras_require={"voice": ["discord.py[voice]", "youtube-dl"]} 24 | ) 25 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | Copyright (c) 2018 toxicrecker 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | The above copyright notice and this permission notice shall be included in all 10 | copies or substantial portions of the Software. 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 15 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 17 | SOFTWARE. -------------------------------------------------------------------------------- /DiscordUtils/InviteTracker.py: -------------------------------------------------------------------------------- 1 | from discord.errors import Forbidden 2 | from discord import AuditLogAction 3 | from datetime import datetime 4 | from asyncio import sleep 5 | 6 | class InviteTracker(): 7 | def __init__(self, bot): 8 | self.bot = bot 9 | self._cache = {} 10 | self.add_listeners() 11 | 12 | def add_listeners(self): 13 | self.bot.add_listener(self.cache_invites, "on_ready") 14 | self.bot.add_listener(self.update_invite_cache, "on_invite_create") 15 | self.bot.add_listener(self.remove_invite_cache, "on_invite_delete") 16 | self.bot.add_listener(self.add_guild_cache, "on_guild_join") 17 | self.bot.add_listener(self.remove_guild_cache, "on_guild_remove") 18 | 19 | async def cache_invites(self): 20 | for guild in self.bot.guilds: 21 | try: 22 | self._cache[guild.id] = {} 23 | for invite in await guild.invites(): 24 | self._cache[guild.id][invite.code] = invite 25 | except Forbidden: 26 | continue 27 | 28 | async def update_invite_cache(self, invite): 29 | if invite.guild.id not in self._cache.keys(): 30 | self._cache[invite.guild.id] = {} 31 | self._cache[invite.guild.id][invite.code] = invite 32 | 33 | async def remove_invite_cache(self, invite): 34 | if invite.guild.id not in self._cache.keys(): 35 | return 36 | ref_invite = self._cache[invite.guild.id][invite.code] 37 | if (ref_invite.created_at.timestamp()+ref_invite.max_age > datetime.utcnow().timestamp() or ref_invite.max_age == 0) and ref_invite.max_uses > 0 and ref_invite.uses == ref_invite.max_uses-1: 38 | try: 39 | async for entry in invite.guild.audit_logs(limit=1, action=AuditLogAction.invite_delete): 40 | if entry.target.code != invite.code: 41 | self._cache[invite.guild.id][ref_invite.code].revoked = True 42 | return 43 | else: 44 | self._cache[invite.guild.id][ref_invite.code].revoked = True 45 | return 46 | except Forbidden: 47 | self._cache[invite.guild.id][ref_invite.code].revoked = True 48 | return 49 | else: 50 | self._cache[invite.guild.id].pop(invite.code) 51 | 52 | async def add_guild_cache(self, guild): 53 | self._cache[guild.id] = {} 54 | for invite in await guild.invites(): 55 | self._cache[guild.id][invite.code] = invite 56 | 57 | async def remove_guild_cache(self, guild): 58 | try: 59 | self._cache.pop(guild.id) 60 | except KeyError: 61 | return 62 | 63 | async def fetch_inviter(self, member): 64 | await sleep(self.bot.latency) 65 | for new_invite in await member.guild.invites(): 66 | for cached_invite in self._cache[member.guild.id].values(): 67 | if new_invite.code == cached_invite.code and new_invite.uses - cached_invite.uses == 1 or cached_invite.revoked: 68 | if cached_invite.revoked: 69 | self._cache[member.guild.id].pop(cached_invite.code) 70 | elif new_invite.inviter == cached_invite.inviter: 71 | self._cache[member.guild.id][cached_invite.code] = new_invite 72 | else: 73 | self._cache[member.guild.id][cached_invite.code].uses += 1 74 | return cached_invite.inviter -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This package is not maintained anymore 2 | 3 | # DiscordUtils 4 | A very useful library made to be used in with [discord.py](https://pypi.org/project/discord.py/) 5 | 6 | # Installation 7 | For access to Pagination and InviteTracker use: 8 | ``` 9 | pip install DiscordUtils 10 | ``` 11 | 12 | or, instead use the following for access to Music functions aswell 13 | ``` 14 | pip install DiscordUtils[voice] 15 | ``` 16 | Requires discord.py[voice] so make sure you have all dependencies of it installed. 17 | 18 | # Example code 19 | 20 | ### DiscordUtils.Pagination.AutoEmbedPaginator 21 | ```python 22 | @bot.command() 23 | async def paginate(ctx): 24 | embed1 = discord.Embed(color=ctx.author.color).add_field(name="Example", value="Page 1") 25 | embed2 = discord.Embed(color=ctx.author.color).add_field(name="Example", value="Page 2") 26 | embed3 = discord.Embed(color=ctx.author.color).add_field(name="Example", value="Page 3") 27 | paginator = DiscordUtils.Pagination.AutoEmbedPaginator(ctx) 28 | embeds = [embed1, embed2, embed3] 29 | await paginator.run(embeds) 30 | ``` 31 | 32 | ### DiscordUtils.Pagination.CustomEmbedPaginator 33 | ```python 34 | @bot.command() 35 | async def paginate(ctx): 36 | embed1 = discord.Embed(color=ctx.author.color).add_field(name="Example", value="Page 1") 37 | embed2 = discord.Embed(color=ctx.author.color).add_field(name="Example", value="Page 2") 38 | embed3 = discord.Embed(color=ctx.author.color).add_field(name="Example", value="Page 3") 39 | paginator = DiscordUtils.Pagination.CustomEmbedPaginator(ctx) 40 | paginator.add_reaction('⏮️', "first") 41 | paginator.add_reaction('⏪', "back") 42 | paginator.add_reaction('🔐', "lock") 43 | paginator.add_reaction('⏩', "next") 44 | paginator.add_reaction('⏭️', "last") 45 | embeds = [embed1, embed2, embed3] 46 | await paginator.run(embeds) 47 | ``` 48 | 49 | ### DiscordUtils.InviteTracker 50 | ```python 51 | import discord 52 | from discord.ext import commands 53 | import DiscordUtils 54 | 55 | intents = discord.intents.default() 56 | intents.members = True 57 | bot = commands.AutoShardedBot(command_prefix=">", intents=intents) 58 | tracker = DiscordUtils.InviteTracker(bot) 59 | 60 | @bot.event 61 | async def on_member_join(member): 62 | inviter = await tracker.fetch_inviter(member) # inviter is the member who invited 63 | ``` 64 | 65 | ### DiscordUtils.Music 66 | ```python 67 | import discord 68 | from discord.ext import commands 69 | import DiscordUtils 70 | 71 | bot = commands.AutoShardedBot(command_prefix=">") 72 | music = DiscordUtils.Music() 73 | 74 | @bot.command() 75 | async def join(ctx): 76 | await ctx.author.voice.channel.connect() #Joins author's voice channel 77 | 78 | @bot.command() 79 | async def leave(ctx): 80 | await ctx.voice_client.disconnect() 81 | 82 | @bot.command() 83 | async def play(ctx, *, url): 84 | player = music.get_player(guild_id=ctx.guild.id) 85 | if not player: 86 | player = music.create_player(ctx, ffmpeg_error_betterfix=True) 87 | if not ctx.voice_client.is_playing(): 88 | await player.queue(url, search=True) 89 | song = await player.play() 90 | await ctx.send(f"Playing {song.name}") 91 | else: 92 | song = await player.queue(url, search=True) 93 | await ctx.send(f"Queued {song.name}") 94 | 95 | @bot.command() 96 | async def pause(ctx): 97 | player = music.get_player(guild_id=ctx.guild.id) 98 | song = await player.pause() 99 | await ctx.send(f"Paused {song.name}") 100 | 101 | @bot.command() 102 | async def resume(ctx): 103 | player = music.get_player(guild_id=ctx.guild.id) 104 | song = await player.resume() 105 | await ctx.send(f"Resumed {song.name}") 106 | 107 | @bot.command() 108 | async def stop(ctx): 109 | player = music.get_player(guild_id=ctx.guild.id) 110 | await player.stop() 111 | await ctx.send("Stopped") 112 | 113 | @bot.command() 114 | async def loop(ctx): 115 | player = music.get_player(guild_id=ctx.guild.id) 116 | song = await player.toggle_song_loop() 117 | if song.is_looping: 118 | await ctx.send(f"Enabled loop for {song.name}") 119 | else: 120 | await ctx.send(f"Disabled loop for {song.name}") 121 | 122 | @bot.command() 123 | async def queue(ctx): 124 | player = music.get_player(guild_id=ctx.guild.id) 125 | await ctx.send(f"{', '.join([song.name for song in player.current_queue()])}") 126 | 127 | @bot.command() 128 | async def np(ctx): 129 | player = music.get_player(guild_id=ctx.guild.id) 130 | song = player.now_playing() 131 | await ctx.send(song.name) 132 | 133 | @bot.command() 134 | async def skip(ctx): 135 | player = music.get_player(guild_id=ctx.guild.id) 136 | data = await player.skip(force=True) 137 | if len(data) == 2: 138 | await ctx.send(f"Skipped from {data[0].name} to {data[1].name}") 139 | else: 140 | await ctx.send(f"Skipped {data[0].name}") 141 | 142 | @bot.command() 143 | async def volume(ctx, vol): 144 | player = music.get_player(guild_id=ctx.guild.id) 145 | song, volume = await player.change_volume(float(vol) / 100) # volume should be a float between 0 to 1 146 | await ctx.send(f"Changed volume for {song.name} to {volume*100}%") 147 | 148 | @bot.command() 149 | async def remove(ctx, index): 150 | player = music.get_player(guild_id=ctx.guild.id) 151 | song = await player.remove_from_queue(int(index)) 152 | await ctx.send(f"Removed {song.name} from queue") 153 | ``` 154 | 155 | For further information please read the docs 156 | 157 | # Links 158 | **[Documentation](https://docs.discordutils.gq)** 159 | 160 | **[Github](https://github.discordutils.gq)** 161 | 162 | # Support 163 | **__Please make sure that you are on the latest version of [DiscordUtils](https://pypi.org/project/DiscordUtils) and [youtube_dl](https://pypi.org/project/youtube_dl) before contacting for support__** 164 | 165 | DM/PM `toxic_recker#2795` on Discord for support 166 | -------------------------------------------------------------------------------- /DiscordUtils/Music.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import aiohttp 3 | import re 4 | try: 5 | import youtube_dl 6 | import discord 7 | has_voice = True 8 | except ImportError: 9 | has_voice = False 10 | 11 | if has_voice: 12 | youtube_dl.utils.bug_reports_message = lambda: '' 13 | ydl = youtube_dl.YoutubeDL({"format": "bestaudio/best", "restrictfilenames": True, "noplaylist": True, "nocheckcertificate": True, "ignoreerrors": True, "logtostderr": False, "quiet": True, "no_warnings": True, "source_address": "0.0.0.0"}) 14 | 15 | class EmptyQueue(Exception): 16 | """Cannot skip because queue is empty""" 17 | 18 | class NotConnectedToVoice(Exception): 19 | """Cannot create the player because bot is not connected to voice""" 20 | 21 | class NotPlaying(Exception): 22 | """Cannot because nothing is being played""" 23 | 24 | async def ytbettersearch(query): 25 | url = f"https://www.youtube.com/results?search_query={query}" 26 | async with aiohttp.ClientSession() as session: 27 | async with session.get(url) as resp: 28 | html = await resp.text() 29 | index = html.find('watch?v') 30 | url = "" 31 | while True: 32 | char = html[index] 33 | if char == '"': 34 | break 35 | url += char 36 | index += 1 37 | url = f"https://www.youtube.com/{url}" 38 | return url 39 | 40 | async def get_video_data(url, search, bettersearch, loop): 41 | if not has_voice: 42 | raise RuntimeError("DiscordUtils[voice] install needed in order to use voice") 43 | 44 | if not search and not bettersearch: 45 | data = await loop.run_in_executor(None, lambda: ydl.extract_info(url, download=False)) 46 | source = data["url"] 47 | url = "https://www.youtube.com/watch?v="+data["id"] 48 | title = data["title"] 49 | description = data["description"] 50 | likes = data["like_count"] 51 | dislikes = data["dislike_count"] 52 | views = data["view_count"] 53 | duration = data["duration"] 54 | thumbnail = data["thumbnail"] 55 | channel = data["uploader"] 56 | channel_url = data["uploader_url"] 57 | return Song(source, url, title, description, views, duration, thumbnail, channel, channel_url, False) 58 | else: 59 | if bettersearch: 60 | url = await ytbettersearch(url) 61 | data = await loop.run_in_executor(None, lambda: ydl.extract_info(url, download=False)) 62 | source = data["url"] 63 | url = "https://www.youtube.com/watch?v="+data["id"] 64 | title = data["title"] 65 | description = data["description"] 66 | likes = data["like_count"] 67 | dislikes = data["dislike_count"] 68 | views = data["view_count"] 69 | duration = data["duration"] 70 | thumbnail = data["thumbnail"] 71 | channel = data["uploader"] 72 | channel_url = data["uploader_url"] 73 | return Song(source, url, title, description, views, duration, thumbnail, channel, channel_url, False) 74 | elif search: 75 | ytdl = youtube_dl.YoutubeDL({"format": "bestaudio/best", "restrictfilenames": True, "noplaylist": True, "nocheckcertificate": True, "ignoreerrors": True, "logtostderr": False, "quiet": True, "no_warnings": True, "default_search": "auto", "source_address": "0.0.0.0"}) 76 | data = await loop.run_in_executor(None, lambda: ytdl.extract_info(url, download=False)) 77 | try: 78 | data = data["entries"][0] 79 | except KeyError or TypeError: 80 | pass 81 | del ytdl 82 | source = data["url"] 83 | url = "https://www.youtube.com/watch?v="+data["id"] 84 | title = data["title"] 85 | description = data["description"] 86 | likes = data["like_count"] 87 | dislikes = data["dislike_count"] 88 | views = data["view_count"] 89 | duration = data["duration"] 90 | thumbnail = data["thumbnail"] 91 | channel = data["uploader"] 92 | channel_url = data["uploader_url"] 93 | return Song(source, url, title, description, views, duration, thumbnail, channel, channel_url, False) 94 | 95 | def check_queue(ctx, opts, music, after, on_play, loop): 96 | if not has_voice: 97 | raise RuntimeError("DiscordUtils[voice] install needed in order to use voice") 98 | 99 | try: 100 | song = music.queue[ctx.guild.id][0] 101 | except IndexError: 102 | return 103 | if not song.is_looping: 104 | try: 105 | music.queue[ctx.guild.id].pop(0) 106 | except IndexError: 107 | return 108 | if len(music.queue[ctx.guild.id]) > 0: 109 | source = discord.PCMVolumeTransformer(discord.FFmpegPCMAudio(music.queue[ctx.guild.id][0].source, **opts)) 110 | ctx.voice_client.play(source, after=lambda error: after(ctx, opts, music, after, on_play, loop)) 111 | song = music.queue[ctx.guild.id][0] 112 | if on_play: 113 | loop.create_task(on_play(ctx, song)) 114 | else: 115 | source = discord.PCMVolumeTransformer(discord.FFmpegPCMAudio(music.queue[ctx.guild.id][0].source, **opts)) 116 | ctx.voice_client.play(source, after=lambda error: after(ctx, opts, music, after, on_play, loop)) 117 | song = music.queue[ctx.guild.id][0] 118 | if on_play: 119 | loop.create_task(on_play(ctx, song)) 120 | 121 | class Music(object): 122 | def __init__(self): 123 | if not has_voice: 124 | raise RuntimeError("DiscordUtils[voice] install needed in order to use voice") 125 | 126 | self.queue = {} 127 | self.players = [] 128 | 129 | def create_player(self, ctx, **kwargs): 130 | if not ctx.voice_client: 131 | raise NotConnectedToVoice("Cannot create the player because bot is not connected to voice") 132 | player = MusicPlayer(ctx, self, **kwargs) 133 | self.players.append(player) 134 | return player 135 | 136 | def get_player(self, **kwargs): 137 | guild = kwargs.get("guild_id") 138 | channel = kwargs.get("channel_id") 139 | for player in self.players: 140 | if guild and channel and player.ctx.guild.id == guild and player.voice.channel.id == channel: 141 | return player 142 | elif not guild and channel and player.voice.channel.id == channel: 143 | return player 144 | elif not channel and guild and player.ctx.guild.id == guild: 145 | return player 146 | else: 147 | return None 148 | 149 | class MusicPlayer(object): 150 | def __init__(self, ctx, music, **kwargs): 151 | if not has_voice: 152 | raise RuntimeError("DiscordUtils[voice] install needed in order to use voice") 153 | 154 | self.ctx = ctx 155 | self.voice = ctx.voice_client 156 | self.loop = ctx.bot.loop 157 | self.music = music 158 | if self.ctx.guild.id not in self.music.queue.keys(): 159 | self.music.queue[self.ctx.guild.id] = [] 160 | self.after_func = check_queue 161 | self.on_play_func = self.on_queue_func = self.on_skip_func = self.on_stop_func = self.on_pause_func = self.on_resume_func = self.on_loop_toggle_func = self.on_volume_change_func = self.on_remove_from_queue_func = None 162 | ffmpeg_error = kwargs.get("ffmpeg_error_betterfix", kwargs.get("ffmpeg_error_fix")) 163 | if ffmpeg_error and "ffmpeg_error_betterfix" in kwargs.keys(): 164 | self.ffmpeg_opts = {"options": "-vn -loglevel quiet -hide_banner -nostats", "before_options": "-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 0 -nostdin"} 165 | elif ffmpeg_error: 166 | self.ffmpeg_opts = {"options": "-vn", "before_options": "-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 0 -nostdin"} 167 | else: 168 | self.ffmpeg_opts = {"options": "-vn", "before_options": "-nostdin"} 169 | def disable(self): 170 | self.music.players.remove(self) 171 | def on_queue(self, func): 172 | self.on_queue_func = func 173 | def on_play(self, func): 174 | self.on_play_func = func 175 | def on_skip(self, func): 176 | self.on_skip_func = func 177 | def on_stop(self, func): 178 | self.on_stop_func = func 179 | def on_pause(self, func): 180 | self.on_pause_func = func 181 | def on_resume(self, func): 182 | self.on_resume_func = func 183 | def on_loop_toggle(self, func): 184 | self.on_loop_toggle_func = func 185 | def on_volume_change(self, func): 186 | self.on_volume_change_func = func 187 | def on_remove_from_queue(self, func): 188 | self.on_remove_from_queue_func = func 189 | async def queue(self, url, search=False, bettersearch=False): 190 | song = await get_video_data(url, search, bettersearch, self.loop) 191 | self.music.queue[self.ctx.guild.id].append(song) 192 | if self.on_queue_func: 193 | await self.on_queue_func(self.ctx, song) 194 | return song 195 | async def play(self): 196 | source = discord.PCMVolumeTransformer(discord.FFmpegPCMAudio(self.music.queue[self.ctx.guild.id][0].source, **self.ffmpeg_opts)) 197 | self.voice.play(source, after=lambda error: self.after_func(self.ctx, self.ffmpeg_opts, self.music, self.after_func, self.on_play_func, self.loop)) 198 | song = self.music.queue[self.ctx.guild.id][0] 199 | if self.on_play_func: 200 | await self.on_play_func(self.ctx, song) 201 | return song 202 | async def skip(self, force=False): 203 | if len(self.music.queue[self.ctx.guild.id]) == 0: 204 | raise NotPlaying("Cannot loop because nothing is being played") 205 | elif not len(self.music.queue[self.ctx.guild.id]) > 1 and not force: 206 | raise EmptyQueue("Cannot skip because queue is empty") 207 | else: 208 | old = self.music.queue[self.ctx.guild.id][0] 209 | old.is_looping = False if old.is_looping else False 210 | self.voice.stop() 211 | try: 212 | new = self.music.queue[self.ctx.guild.id][0] 213 | if self.on_skip_func: 214 | await self.on_skip_func(self.ctx, old, new) 215 | return (old, new) 216 | except IndexError: 217 | if self.on_skip_func: 218 | await self.on_skip_func(self.ctx, old) 219 | return old 220 | async def stop(self): 221 | try: 222 | self.music.queue[self.ctx.guild.id] = [] 223 | self.voice.stop() 224 | self.music.players.remove(self) 225 | except: 226 | raise NotPlaying("Cannot loop because nothing is being played") 227 | if self.on_stop_func: 228 | await self.on_stop_func(self.ctx) 229 | async def pause(self): 230 | try: 231 | self.voice.pause() 232 | song = self.music.queue[self.ctx.guild.id][0] 233 | except: 234 | raise NotPlaying("Cannot pause because nothing is being played") 235 | if self.on_pause_func: 236 | await self.on_pause_func(self.ctx, song) 237 | return song 238 | async def resume(self): 239 | try: 240 | self.voice.resume() 241 | song = self.music.queue[self.ctx.guild.id][0] 242 | except: 243 | raise NotPlaying("Cannot resume because nothing is being played") 244 | if self.on_resume_func: 245 | await self.on_resume_func(self.ctx, song) 246 | return song 247 | def current_queue(self): 248 | try: 249 | return self.music.queue[self.ctx.guild.id] 250 | except KeyError: 251 | raise EmptyQueue("Queue is empty") 252 | def now_playing(self): 253 | try: 254 | return self.music.queue[self.ctx.guild.id][0] 255 | except: 256 | return None 257 | async def toggle_song_loop(self): 258 | try: 259 | song = self.music.queue[self.ctx.guild.id][0] 260 | except: 261 | raise NotPlaying("Cannot loop because nothing is being played") 262 | if not song.is_looping: 263 | song.is_looping = True 264 | else: 265 | song.is_looping = False 266 | if self.on_loop_toggle_func: 267 | await self.on_loop_toggle_func(self.ctx, song) 268 | return song 269 | async def change_volume(self, vol): 270 | self.voice.source.volume = vol 271 | try: 272 | song = self.music.queue[self.ctx.guild.id][0] 273 | except: 274 | raise NotPlaying("Cannot loop because nothing is being played") 275 | if self.on_volume_change_func: 276 | await self.on_volume_change_func(self.ctx, song, vol) 277 | return (song, vol) 278 | async def remove_from_queue(self, index): 279 | if index == 0: 280 | try: 281 | song = self.music.queue[self.ctx.guild.id][0] 282 | except: 283 | raise NotPlaying("Cannot loop because nothing is being played") 284 | await self.skip(force=True) 285 | return song 286 | song = self.music.queue[self.ctx.guild.id][index] 287 | self.music.queue[self.ctx.guild.id].pop(index) 288 | if self.on_remove_from_queue_func: 289 | await self.on_remove_from_queue_func(self.ctx, song) 290 | return song 291 | def delete(self): 292 | self.music.players.remove(self) 293 | 294 | class Song(object): 295 | def __init__(self, source, url, title, description, views, duration, thumbnail, channel, channel_url, loop): 296 | self.source = source 297 | self.url = url 298 | self.title = title 299 | self.description = description 300 | self.views = views 301 | self.name = title 302 | self.duration = duration 303 | self.thumbnail = thumbnail 304 | self.channel = channel 305 | self.channel_url = channel_url 306 | self.is_looping = loop 307 | -------------------------------------------------------------------------------- /DiscordUtils/Pagination.py: -------------------------------------------------------------------------------- 1 | import discord 2 | import asyncio 3 | from discord.ext import commands 4 | 5 | class AutoEmbedPaginator(object): 6 | def __init__(self, ctx, **kwargs): 7 | self.embeds = None 8 | self.ctx = ctx 9 | self.bot = ctx.bot 10 | self.current_page = 0 11 | self.auto_footer = kwargs.get("auto_footer", False) 12 | self.remove_reactions = kwargs.get("remove_reactions", False) 13 | self.control_emojis = ('⏮️', '⏪', '🔐', '⏩', '⏭️') 14 | self.timeout = int(kwargs.get("timeout", 60)) 15 | async def run(self, embeds, send_to=None): 16 | if not send_to: 17 | send_to = self.ctx 18 | wait_for = self.ctx.author if send_to == self.ctx else send_to 19 | if not self.embeds: 20 | self.embeds = embeds 21 | if self.auto_footer: 22 | self.embeds[0].set_footer(text=f'({self.current_page+1}/{len(self.embeds)})') 23 | msg = await send_to.send(embed=self.embeds[0]) 24 | for emoji in self.control_emojis: 25 | try: 26 | await msg.add_reaction(emoji) 27 | except: 28 | pass 29 | msg = await msg.channel.fetch_message(msg.id) 30 | def check(reaction, user): 31 | return user == wait_for and reaction.message.id == msg.id and str(reaction.emoji) in self.control_emojis 32 | while True: 33 | if self.timeout > 0: 34 | try: 35 | reaction, user = await self.bot.wait_for("reaction_add",check=check, timeout=self.timeout) 36 | except asyncio.TimeoutError: 37 | self.current_page = 0 38 | for reaction in msg.reactions: 39 | if reaction.message.author.id == self.bot.user.id: 40 | try: 41 | await msg.remove_reaction(str(reaction.emoji), reaction.message.author) 42 | except: 43 | pass 44 | return msg 45 | break 46 | else: 47 | reaction, user = await self.bot.wait_for("reaction_add",check=check) 48 | if str(reaction.emoji) == self.control_emojis[0]: 49 | self.current_page = 0 50 | if self.remove_reactions: 51 | try: 52 | await msg.remove_reaction(str(reaction.emoji), user) 53 | except: 54 | pass 55 | if self.auto_footer: 56 | self.embeds[0].set_footer(text=f'({self.current_page+1}/{len(self.embeds)})') 57 | await msg.edit(embed=self.embeds[0]) 58 | elif str(reaction.emoji) == self.control_emojis[1]: 59 | self.current_page = self.current_page-1 60 | self.current_page = 0 if self.current_page<0 else self.current_page 61 | if self.remove_reactions: 62 | try: 63 | await msg.remove_reaction(str(reaction.emoji), user) 64 | except: 65 | pass 66 | if self.auto_footer: 67 | self.embeds[self.current_page].set_footer(text=f'({self.current_page+1}/{len(self.embeds)})') 68 | await msg.edit(embed=self.embeds[self.current_page]) 69 | elif str(reaction.emoji) == self.control_emojis[2]: 70 | self.current_page = 0 71 | for reaction in msg.reactions: 72 | try: 73 | if reaction.message.author.id == self.bot.user.id: 74 | await msg.remove_reaction(str(reaction.emoji), reaction.message.author) 75 | except: 76 | pass 77 | return msg 78 | break 79 | elif str(reaction.emoji) == self.control_emojis[3]: 80 | self.current_page = self.current_page + 1 81 | self.current_page = len(self.embeds)-1 if self.current_page > len(self.embeds)-1 else self.current_page 82 | if self.remove_reactions: 83 | try: 84 | await msg.remove_reaction(str(reaction.emoji), user) 85 | except: 86 | pass 87 | if self.auto_footer: 88 | self.embeds[self.current_page].set_footer(text=f'({self.current_page+1}/{len(self.embeds)})') 89 | await msg.edit(embed=self.embeds[self.current_page]) 90 | elif str(reaction.emoji) == self.control_emojis[4]: 91 | self.current_page = len(self.embeds)-1 92 | if self.remove_reactions: 93 | try: 94 | await msg.remove_reaction(str(reaction.emoji), user) 95 | except: 96 | pass 97 | if self.auto_footer: 98 | self.embeds[len(self.embeds)-1].set_footer(text=f'({self.current_page+1}/{len(self.embeds)})') 99 | await msg.edit(embed=self.embeds[len(self.embeds)-1]) 100 | 101 | class CustomEmbedPaginator(object): 102 | def __init__(self, ctx, **kwargs): 103 | self.embeds = None 104 | self.ctx = ctx 105 | self.bot = ctx.bot 106 | self.timeout = int(kwargs.get("timeout",60)) 107 | self.current_page = 0 108 | self.control_emojis = [] 109 | self.control_commands = [] 110 | self.auto_footer = kwargs.get("auto_footer", False) 111 | self.remove_reactions = kwargs.get("remove_reactions", False) 112 | def add_reaction(self, emoji, command): 113 | self.control_emojis.append(emoji) 114 | self.control_commands.append(command) 115 | def insert_reaction(self, index, emoji, command): 116 | self.control_emojis.insert(index, emoji) 117 | self.control_commands.insert(index, command) 118 | def remove_reaction(self, emoji): 119 | if emoji in self.control_emojis: 120 | index = self.control_emojis.index(emoji) 121 | self.control_emojis.remove(emoji) 122 | self.control_commands.pop(index) 123 | def remove_reaction_at(self, index): 124 | if index > len(self.control_emojis)-1: 125 | index = len(self.control_emojis)-1 126 | elif index < 0: 127 | index = 0 128 | try: 129 | self.control_emojis.pop(index) 130 | self.control_commands.pop(index) 131 | except: 132 | pass 133 | def clear_reactions(self): 134 | self.control_emojis = [] 135 | self.control_commands = [] 136 | async def run(self, embeds, send_to=None): 137 | self.embeds = embeds 138 | if not send_to: 139 | send_to = self.ctx 140 | wait_for = self.ctx.author if send_to == self.ctx else send_to 141 | if self.auto_footer: 142 | self.embeds[0].set_footer(text=f'({self.current_page+1}/{len(self.embeds)})') 143 | msg = await send_to.send(embed=self.embeds[0]) 144 | for emoji in self.control_emojis: 145 | try: 146 | await msg.add_reaction(emoji) 147 | except: 148 | pass 149 | msg = await msg.channel.fetch_message(msg.id) 150 | def check(reaction, user): 151 | return user == wait_for and reaction.message.id == msg.id and str(reaction.emoji) in self.control_emojis 152 | while True: 153 | if self.timeout > 0: 154 | try: 155 | reaction, user = await self.bot.wait_for("reaction_add",check=check, timeout=self.timeout) 156 | except asyncio.TimeoutError: 157 | for reaction in msg.reactions: 158 | if reaction.message.author.id == self.bot.user.id and self.remove_reactions: 159 | try: 160 | await msg.remove_reaction(str(reaction.emoji), reaction.message.author) 161 | except: 162 | pass 163 | self.current_page = 0 164 | return msg 165 | break 166 | else: 167 | reaction, user = await self.bot.wait_for("reaction_add",check=check) 168 | for emoji in self.control_emojis: 169 | if emoji == str(reaction.emoji): 170 | index = self.control_emojis.index(emoji) 171 | cmd = self.control_commands[index] 172 | if cmd.lower() == "first": 173 | self.current_page = 0 174 | if self.remove_reactions: 175 | try: 176 | await msg.remove_reaction(str(reaction.emoji), user) 177 | except: 178 | pass 179 | if self.auto_footer: 180 | self.embeds[0].set_footer(text=f'({self.current_page+1}/{len(self.embeds)})') 181 | await msg.edit(embed=self.embeds[0]) 182 | elif cmd.lower() == "last": 183 | self.current_page = len(self.embeds)-1 184 | if self.remove_reactions: 185 | try: 186 | await msg.remove_reaction(str(reaction.emoji), user) 187 | except: 188 | pass 189 | if self.auto_footer: 190 | self.embeds[len(self.embeds)-1].set_footer(text=f'({self.current_page+1}/{len(self.embeds)})') 191 | await msg.edit(embed=self.embeds[len(self.embeds)-1]) 192 | elif cmd.lower() == "next": 193 | self.current_page += 1 194 | self.current_page = len(self.embeds)-1 if self.current_page > len(self.embeds)-1 else self.current_page 195 | if self.remove_reactions: 196 | try: 197 | await msg.remove_reaction(str(reaction.emoji), user) 198 | except: 199 | pass 200 | if self.auto_footer: 201 | self.embeds[self.current_page].set_footer(text=f'({self.current_page+1}/{len(self.embeds)})') 202 | await msg.edit(embed=self.embeds[self.current_page]) 203 | elif cmd.lower() == "back": 204 | self.current_page = self.current_page-1 205 | self.current_page = 0 if self.current_page<0 else self.current_page 206 | if self.remove_reactions: 207 | try: 208 | await msg.remove_reaction(str(reaction.emoji), user) 209 | except: 210 | pass 211 | if self.auto_footer: 212 | self.embeds[self.current_page].set_footer(text=f'({self.current_page+1}/{len(self.embeds)})') 213 | await msg.edit(embed=self.embeds[self.current_page]) 214 | elif cmd.lower() == "delete": 215 | self.current_page = 0 216 | await msg.delete() 217 | return msg 218 | break 219 | elif cmd.lower() == "clear" or cmd.lower() == "lock": 220 | self.current_page = 0 221 | for reaction in msg.reactions: 222 | try: 223 | await msg.clear_reactions() 224 | break 225 | except discord.Forbidden or discord.HTTPException: 226 | if reaction.message.author.id == self.bot.user.id: 227 | try: 228 | await msg.remove_reaction(str(reaction.emoji), reaction.message.author) 229 | except: 230 | pass 231 | return msg 232 | break 233 | elif cmd.startswith("page"): 234 | shit = cmd.split() 235 | pg = int(shit[1]) 236 | self.current_page = pg 237 | if pg > len(embeds)-1: 238 | pg = len(embeds)-1 239 | if pg < 0: 240 | pg = 0 241 | if self.remove_reactions: 242 | try: 243 | await msg.remove_reaction(str(reaction.emoji), user) 244 | except: 245 | pass 246 | if self.auto_footer: 247 | self.embeds[self.current_page].set_footer(text=f'({pg+1}/{len(self.embeds)})') 248 | await msg.edit(embed=self.embeds[pg]) 249 | elif cmd.startswith("remove"): 250 | things = cmd.split() 251 | things.pop(0) 252 | something = things[0] 253 | if something.isdigit(): 254 | index = int(something) 255 | index = len(self.control_emojis)-1 if index > len(self.control_emojis)-1 else index 256 | index = 0 if index < 0 else index 257 | emoji = self.control_emojis[index] 258 | if self.remove_reactions: 259 | try: 260 | await msg.remove_reaction(str(reaction.emoji), user) 261 | except: 262 | pass 263 | try: 264 | await msg.remove_reaction(emoji, self.bot.user) 265 | except: 266 | pass 267 | self.control_emojis.pop(index) 268 | self.control_commands.pop(index) 269 | else: 270 | emoji = something 271 | if emoji in self.control_emojis: 272 | if self.remove_reactions: 273 | try: 274 | await msg.remove_reaction(str(reaction.emoji), user) 275 | except: 276 | pass 277 | try: 278 | await msg.remove_reaction(emoji, self.bot.user) 279 | except: 280 | pass 281 | index = self.control_emojis.index(emoji) 282 | self.control_emojis.remove(emoji) 283 | self.control_commands.pop(index) 284 | --------------------------------------------------------------------------------