-
-
Save Lenart12/024222b63db38c65f68b57ae7e623d56 to your computer and use it in GitHub Desktop.
# -*- coding: utf-8 -*- | |
""" | |
Copyright (c) 2019 Valentin B. | |
A simple music bot written in discord.py using youtube-dl. | |
Though it's a simple example, music bots are complex and require much time and knowledge until they work perfectly. | |
Use this as an example or a base for your own bot and extend it as you want. If there are any bugs, please let me know. | |
Requirements: | |
Python 3.5+ | |
pip install -U discord.py pynacl youtube-dl | |
You also need FFmpeg in your PATH environment variable or the FFmpeg.exe binary in your bot's directory on Windows. | |
""" | |
import asyncio | |
import functools | |
import itertools | |
import math | |
import random | |
import discord | |
import youtube_dl | |
from async_timeout import timeout | |
from discord.ext import commands | |
import httpx | |
from bs4 import BeautifulSoup | |
# Silence useless bug reports messages | |
youtube_dl.utils.bug_reports_message = lambda: '' | |
token = open("token.txt", "r").read() | |
class VoiceError(Exception): | |
pass | |
class YTDLError(Exception): | |
pass | |
class YTDLSource(discord.PCMVolumeTransformer): | |
YTDL_OPTIONS = { | |
'format': 'bestaudio/best', | |
'extractaudio': True, | |
'audioformat': 'mp3', | |
'outtmpl': '%(extractor)s-%(id)s-%(title)s.%(ext)s', | |
'restrictfilenames': True, | |
'noplaylist': True, | |
'nocheckcertificate': True, | |
'ignoreerrors': False, | |
'logtostderr': False, | |
'quiet': True, | |
'no_warnings': True, | |
'default_search': 'ytsearch', | |
'source_address': '0.0.0.0', | |
} | |
FFMPEG_OPTIONS = { | |
'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5', | |
'options': '-vn', | |
} | |
ytdl = youtube_dl.YoutubeDL(YTDL_OPTIONS) | |
def __init__(self, ctx: commands.Context, source: discord.FFmpegPCMAudio, *, data: dict, volume: float = 0.5): | |
super().__init__(source, volume) | |
self.requester = ctx.author | |
self.channel = ctx.channel | |
self.data = data | |
self.uploader = data.get('uploader') | |
self.uploader_url = data.get('uploader_url') | |
date = data.get('upload_date') | |
self.upload_date = date[6:8] + '.' + date[4:6] + '.' + date[0:4] | |
self.title = data.get('title') | |
self.thumbnail = data.get('thumbnail') | |
self.description = data.get('description') | |
self.duration = self.parse_duration(int(data.get('duration'))) | |
self.tags = data.get('tags') | |
self.url = data.get('webpage_url') | |
self.views = data.get('view_count') | |
self.likes = data.get('like_count') | |
self.dislikes = data.get('dislike_count') | |
self.stream_url = data.get('url') | |
def __str__(self): | |
return '**{0.title}** by **{0.uploader}**'.format(self) | |
@classmethod | |
async def create_source(cls, ctx: commands.Context, search: str, *, loop: asyncio.BaseEventLoop = None): | |
loop = loop or asyncio.get_event_loop() | |
partial = functools.partial(cls.ytdl.extract_info, search, download=False, process=False) | |
data = await loop.run_in_executor(None, partial) | |
if data is None: | |
raise YTDLError('Couldn\'t find anything that matches `{}`'.format(search)) | |
if 'entries' not in data: | |
process_info = data | |
else: | |
process_info = None | |
for entry in data['entries']: | |
if entry: | |
process_info = entry | |
break | |
if process_info is None: | |
raise YTDLError('Couldn\'t find anything that matches `{}`'.format(search)) | |
webpage_url = process_info['webpage_url'] | |
partial = functools.partial(cls.ytdl.extract_info, webpage_url, download=False) | |
processed_info = await loop.run_in_executor(None, partial) | |
if processed_info is None: | |
raise YTDLError('Couldn\'t fetch `{}`'.format(webpage_url)) | |
if 'entries' not in processed_info: | |
info = processed_info | |
else: | |
info = None | |
while info is None: | |
try: | |
info = processed_info['entries'].pop(0) | |
except IndexError: | |
raise YTDLError('Couldn\'t retrieve any matches for `{}`'.format(webpage_url)) | |
return cls(ctx, discord.FFmpegPCMAudio(info['url'], **cls.FFMPEG_OPTIONS), data=info) | |
@classmethod | |
async def search_source(cls, ctx: commands.Context, search: str, *, loop: asyncio.BaseEventLoop = None): | |
channel = ctx.channel | |
loop = loop or asyncio.get_event_loop() | |
cls.search_query = '%s%s:%s' % ('ytsearch', 10, ''.join(search)) | |
partial = functools.partial(cls.ytdl.extract_info, cls.search_query, download=False, process=False) | |
info = await loop.run_in_executor(None, partial) | |
cls.search = {} | |
cls.search["title"] = f'Search results for:\n**{search}**' | |
cls.search["type"] = 'rich' | |
cls.search["color"] = 7506394 | |
cls.search["author"] = {'name': f'{ctx.author.name}', 'url': f'{ctx.author.avatar_url}', 'icon_url': f'{ctx.author.avatar_url}'} | |
lst = [] | |
for e in info['entries']: | |
#lst.append(f'`{info["entries"].index(e) + 1}.` {e.get("title")} **[{YTDLSource.parse_duration(int(e.get("duration")))}]**\n') | |
VId = e.get('id') | |
VUrl = 'https://www.youtube.com/watch?v=%s' % (VId) | |
lst.append(f'`{info["entries"].index(e) + 1}.` [{e.get("title")}]({VUrl})\n') | |
lst.append('\n**Type a number to make a choice, Type `cancel` to exit**') | |
cls.search["description"] = "\n".join(lst) | |
em = discord.Embed.from_dict(cls.search) | |
await ctx.send(embed=em, delete_after=45.0) | |
def check(msg): | |
return msg.content.isdigit() == True and msg.channel == channel or msg.content == 'cancel' or msg.content == 'Cancel' | |
try: | |
m = await bot.wait_for('message', check=check, timeout=45.0) | |
except asyncio.TimeoutError: | |
rtrn = 'timeout' | |
else: | |
if m.content.isdigit() == True: | |
sel = int(m.content) | |
if 0 < sel <= 10: | |
for key, value in info.items(): | |
if key == 'entries': | |
"""data = value[sel - 1]""" | |
VId = value[sel - 1]['id'] | |
VUrl = 'https://www.youtube.com/watch?v=%s' % (VId) | |
partial = functools.partial(cls.ytdl.extract_info, VUrl, download=False) | |
data = await loop.run_in_executor(None, partial) | |
rtrn = cls(ctx, discord.FFmpegPCMAudio(data['url'], **cls.FFMPEG_OPTIONS), data=data) | |
else: | |
rtrn = 'sel_invalid' | |
elif m.content == 'cancel': | |
rtrn = 'cancel' | |
else: | |
rtrn = 'sel_invalid' | |
return rtrn | |
@staticmethod | |
def parse_duration(duration: int): | |
if duration > 0: | |
minutes, seconds = divmod(duration, 60) | |
hours, minutes = divmod(minutes, 60) | |
days, hours = divmod(hours, 24) | |
duration = [] | |
if days > 0: | |
duration.append('{}'.format(days)) | |
if hours > 0: | |
duration.append('{}'.format(hours)) | |
if minutes > 0: | |
duration.append('{}'.format(minutes)) | |
if seconds > 0: | |
duration.append('{}'.format(seconds)) | |
value = ':'.join(duration) | |
elif duration == 0: | |
value = "LIVE" | |
return value | |
class Song: | |
__slots__ = ('source', 'requester') | |
def __init__(self, source: YTDLSource): | |
self.source = source | |
self.requester = source.requester | |
def create_embed(self): | |
embed = (discord.Embed(title='Now playing', description='```css\n{0.source.title}\n```'.format(self), color=discord.Color.blurple()) | |
.add_field(name='Duration', value=self.source.duration) | |
.add_field(name='Requested by', value=self.requester.mention) | |
.add_field(name='Uploader', value='[{0.source.uploader}]({0.source.uploader_url})'.format(self)) | |
.add_field(name='URL', value='[Click]({0.source.url})'.format(self)) | |
.set_thumbnail(url=self.source.thumbnail) | |
.set_author(name=self.requester.name, icon_url=self.requester.avatar_url)) | |
return embed | |
class SongQueue(asyncio.Queue): | |
def __getitem__(self, item): | |
if isinstance(item, slice): | |
return list(itertools.islice(self._queue, item.start, item.stop, item.step)) | |
else: | |
return self._queue[item] | |
def __iter__(self): | |
return self._queue.__iter__() | |
def __len__(self): | |
return self.qsize() | |
def clear(self): | |
self._queue.clear() | |
def shuffle(self): | |
random.shuffle(self._queue) | |
def remove(self, index: int): | |
del self._queue[index] | |
class VoiceState: | |
def __init__(self, bot: commands.Bot, ctx: commands.Context): | |
self.bot = bot | |
self._ctx = ctx | |
self.current = None | |
self.voice = None | |
self.next = asyncio.Event() | |
self.songs = SongQueue() | |
self.exists = True | |
self._loop = False | |
self._autoplay = True | |
self._volume = 0.5 | |
self.skip_votes = set() | |
self.audio_player = bot.loop.create_task(self.audio_player_task()) | |
def __del__(self): | |
self.audio_player.cancel() | |
@property | |
def loop(self): | |
return self._loop | |
@loop.setter | |
def loop(self, value: bool): | |
self._loop = value | |
@property | |
def autoplay(self): | |
return self._autoplay | |
@autoplay.setter | |
def autoplay(self, value: bool): | |
self._autoplay = value | |
@property | |
def volume(self): | |
return self._volume | |
@volume.setter | |
def volume(self, value: float): | |
self._volume = value | |
@property | |
def is_playing(self): | |
return self.voice and self.current | |
async def audio_player_task(self): | |
while True: | |
self.next.clear() | |
self.now = None | |
if self.loop == False: | |
# If autoplay is turned on wait 3 seconds for a new song. | |
# If no song is found find a new one, | |
# else if autoplay is turned off try to get the | |
# next song within 3 minutes. | |
# If no song will be added to the queue in time, | |
# the player will disconnect due to performance | |
# reasons. | |
if self.autoplay: | |
try: | |
async with timeout(3): | |
self.current = await self.songs.get() | |
except asyncio.TimeoutError: | |
# Spoof user agent to show whole page. | |
headers = {'User-Agent' : 'Mozilla/5.0 (compatible; Bingbot/2.0; +http://www.bing.com/bingbot.htm)'} | |
song_url = self.current.source.url | |
# Get the page | |
async with httpx.AsyncClient() as client: | |
response = await client.get(song_url, headers=headers) | |
soup = BeautifulSoup(response.text, features='lxml') | |
# Parse all the recommended videos out of the response and store them in a list | |
recommended_urls = [] | |
for li in soup.find_all('li', class_='related-list-item'): | |
a = li.find('a') | |
# Only videos (no mixes or playlists) | |
if 'content-link' in a.attrs['class']: | |
recommended_urls.append(f'https://www.youtube.com{a.get("href")}') | |
ctx = self._ctx | |
async with ctx.typing(): | |
try: | |
source = await YTDLSource.create_source(ctx, recommended_urls[0], loop=self.bot.loop) | |
except YTDLError as e: | |
await ctx.send('An error occurred while processing this request: {}'.format(str(e))) | |
self.bot.loop.create_task(self.stop()) | |
self.exists = False | |
return | |
else: | |
song = Song(source) | |
self.current = song | |
await ctx.send('Enqueued {}'.format(str(source))) | |
else: | |
try: | |
async with timeout(180): # 3 minutes | |
self.current = await self.songs.get() | |
except asyncio.TimeoutError: | |
self.bot.loop.create_task(self.stop()) | |
self.exists = False | |
return | |
self.current.source.volume = self._volume | |
self.voice.play(self.current.source, after=self.play_next_song) | |
await self.current.source.channel.send(embed=self.current.create_embed()) | |
#If the song is looped | |
elif self.loop == True: | |
self.now = discord.FFmpegPCMAudio(self.current.source.stream_url, **YTDLSource.FFMPEG_OPTIONS) | |
self.voice.play(self.now, after=self.play_next_song) | |
await self.next.wait() | |
def play_next_song(self, error=None): | |
if error: | |
raise VoiceError(str(error)) | |
self.next.set() | |
def skip(self): | |
self.skip_votes.clear() | |
if self.is_playing: | |
self.voice.stop() | |
async def stop(self): | |
self.songs.clear() | |
if self.voice: | |
await self.voice.disconnect() | |
self.voice = None | |
class Music(commands.Cog): | |
def __init__(self, bot: commands.Bot): | |
self.bot = bot | |
self.voice_states = {} | |
def get_voice_state(self, ctx: commands.Context): | |
state = self.voice_states.get(ctx.guild.id) | |
if not state or not state.exists: | |
state = VoiceState(self.bot, ctx) | |
self.voice_states[ctx.guild.id] = state | |
return state | |
def cog_unload(self): | |
for state in self.voice_states.values(): | |
self.bot.loop.create_task(state.stop()) | |
def cog_check(self, ctx: commands.Context): | |
if not ctx.guild: | |
raise commands.NoPrivateMessage('This command can\'t be used in DM channels.') | |
return True | |
async def cog_before_invoke(self, ctx: commands.Context): | |
ctx.voice_state = self.get_voice_state(ctx) | |
async def cog_command_error(self, ctx: commands.Context, error: commands.CommandError): | |
await ctx.send('An error occurred: {}'.format(str(error))) | |
@commands.Cog.listener() | |
async def on_message(self, message): | |
if message.author.id != bot.user.id: | |
print(f"{message.guild}/{message.channel}/{message.author.name}>{message.content}") | |
if message.embeds: | |
print(message.embeds[0].to_dict()) | |
@commands.command(name='join', invoke_without_subcommand=True) | |
async def _join(self, ctx: commands.Context): | |
"""Joins a voice channel.""" | |
destination = ctx.author.voice.channel | |
if ctx.voice_state.voice: | |
await ctx.voice_state.voice.move_to(destination) | |
return | |
ctx.voice_state.voice = await destination.connect() | |
@commands.command(name='summon') | |
@commands.has_permissions(manage_guild=True) | |
async def _summon(self, ctx: commands.Context, *, channel: discord.VoiceChannel = None): | |
"""Summons the bot to a voice channel. | |
If no channel was specified, it joins your channel. | |
""" | |
if not channel and not ctx.author.voice: | |
raise VoiceError('You are neither connected to a voice channel nor specified a channel to join.') | |
destination = channel or ctx.author.voice.channel | |
if ctx.voice_state.voice: | |
await ctx.voice_state.voice.move_to(destination) | |
return | |
ctx.voice_state.voice = await destination.connect() | |
@commands.command(name='leave', aliases=['disconnect']) | |
@commands.has_permissions(manage_guild=True) | |
async def _leave(self, ctx: commands.Context): | |
"""Clears the queue and leaves the voice channel.""" | |
if not ctx.voice_state.voice: | |
return await ctx.send('Not connected to any voice channel.') | |
await ctx.voice_state.stop() | |
del self.voice_states[ctx.guild.id] | |
@commands.command(name='volume') | |
@commands.is_owner() | |
async def _volume(self, ctx: commands.Context, *, volume: int): | |
"""Sets the volume of the player.""" | |
if not ctx.voice_state.is_playing: | |
return await ctx.send('Nothing being played at the moment.') | |
if 0 > volume > 100: | |
return await ctx.send('Volume must be between 0 and 100') | |
ctx.voice_state.volume = volume / 100 | |
await ctx.send('Volume of the player set to {}%'.format(volume)) | |
@commands.command(name='now', aliases=['current', 'playing']) | |
async def _now(self, ctx: commands.Context): | |
"""Displays the currently playing song.""" | |
embed = ctx.voice_state.current.create_embed() | |
await ctx.send(embed=embed) | |
@commands.command(name='pause', aliases=['pa']) | |
@commands.has_permissions(manage_guild=True) | |
async def _pause(self, ctx: commands.Context): | |
"""Pauses the currently playing song.""" | |
print(">>>Pause Command:") | |
if ctx.voice_state.is_playing and ctx.voice_state.voice.is_playing(): | |
ctx.voice_state.voice.pause() | |
await ctx.message.add_reaction('⏯') | |
@commands.command(name='resume', aliases=['re', 'res']) | |
@commands.has_permissions(manage_guild=True) | |
async def _resume(self, ctx: commands.Context): | |
"""Resumes a currently paused song.""" | |
if ctx.voice_state.is_playing and ctx.voice_state.voice.is_paused(): | |
ctx.voice_state.voice.resume() | |
await ctx.message.add_reaction('⏯') | |
@commands.command(name='stop') | |
@commands.has_permissions(manage_guild=True) | |
async def _stop(self, ctx: commands.Context): | |
"""Stops playing song and clears the queue.""" | |
ctx.voice_state.songs.clear() | |
if ctx.voice_state.is_playing: | |
ctx.voice_state.voice.stop() | |
await ctx.message.add_reaction('⏹') | |
@commands.command(name='skip', aliases=['s']) | |
async def _skip(self, ctx: commands.Context): | |
"""Vote to skip a song. The requester can automatically skip. | |
3 skip votes are needed for the song to be skipped. | |
""" | |
if not ctx.voice_state.is_playing: | |
return await ctx.send('Not playing any music right now...') | |
voter = ctx.message.author | |
if voter == ctx.voice_state.current.requester: | |
await ctx.message.add_reaction('⏭') | |
ctx.voice_state.skip() | |
elif voter.id not in ctx.voice_state.skip_votes: | |
ctx.voice_state.skip_votes.add(voter.id) | |
total_votes = len(ctx.voice_state.skip_votes) | |
if total_votes >= 3: | |
await ctx.message.add_reaction('⏭') | |
ctx.voice_state.skip() | |
else: | |
await ctx.send('Skip vote added, currently at **{}/3**'.format(total_votes)) | |
else: | |
await ctx.send('You have already voted to skip this song.') | |
@commands.command(name='queue') | |
async def _queue(self, ctx: commands.Context, *, page: int = 1): | |
"""Shows the player's queue. | |
You can optionally specify the page to show. Each page contains 10 elements. | |
""" | |
if len(ctx.voice_state.songs) == 0: | |
return await ctx.send('Empty queue.') | |
items_per_page = 10 | |
pages = math.ceil(len(ctx.voice_state.songs) / items_per_page) | |
start = (page - 1) * items_per_page | |
end = start + items_per_page | |
queue = '' | |
for i, song in enumerate(ctx.voice_state.songs[start:end], start=start): | |
queue += '`{0}.` [**{1.source.title}**]({1.source.url})\n'.format(i + 1, song) | |
embed = (discord.Embed(description='**{} tracks:**\n\n{}'.format(len(ctx.voice_state.songs), queue)) | |
.set_footer(text='Viewing page {}/{}'.format(page, pages))) | |
await ctx.send(embed=embed) | |
@commands.command(name='shuffle') | |
async def _shuffle(self, ctx: commands.Context): | |
"""Shuffles the queue.""" | |
if len(ctx.voice_state.songs) == 0: | |
return await ctx.send('Empty queue.') | |
ctx.voice_state.songs.shuffle() | |
await ctx.message.add_reaction('✅') | |
@commands.command(name='remove') | |
async def _remove(self, ctx: commands.Context, index: int): | |
"""Removes a song from the queue at a given index.""" | |
if len(ctx.voice_state.songs) == 0: | |
return await ctx.send('Empty queue.') | |
ctx.voice_state.songs.remove(index - 1) | |
await ctx.message.add_reaction('✅') | |
@commands.command(name='loop') | |
async def _loop(self, ctx: commands.Context): | |
"""Loops the currently playing song. | |
Invoke this command again to unloop the song. | |
""" | |
if not ctx.voice_state.is_playing: | |
return await ctx.send('Nothing being played at the moment.') | |
# Inverse boolean value to loop and unloop. | |
ctx.voice_state.loop = not ctx.voice_state.loop | |
await ctx.message.add_reaction('✅') | |
await ctx.send('Looping a song is now turned ' + ('on' if ctx.voice_state.loop else 'off') ) | |
@commands.command(name='autoplay') | |
async def _autoplay(self, ctx: commands.Context): | |
"""Automatically queue a new song that is related to the song at the end of the queue. | |
Invoke this command again to toggle autoplay the song. | |
""" | |
if not ctx.voice_state.is_playing: | |
return await ctx.send('Nothing being played at the moment.') | |
# Inverse boolean value to loop and unloop. | |
ctx.voice_state.autoplay = not ctx.voice_state.autoplay | |
await ctx.message.add_reaction('✅') | |
await ctx.send('Autoplay after end of queue is now ' + ('on' if ctx.voice_state.autoplay else 'off') ) | |
@commands.command(name='play', aliases=['p']) | |
async def _play(self, ctx: commands.Context, *, search: str): | |
"""Plays a song. | |
If there are songs in the queue, this will be queued until the | |
other songs finished playing. | |
This command automatically searches from various sites if no URL is provided. | |
A list of these sites can be found here: https://rg3.github.io/youtube-dl/supportedsites.html | |
""" | |
async with ctx.typing(): | |
try: | |
source = await YTDLSource.create_source(ctx, search, loop=self.bot.loop) | |
except YTDLError as e: | |
await ctx.send('An error occurred while processing this request: {}'.format(str(e))) | |
else: | |
if not ctx.voice_state.voice: | |
await ctx.invoke(self._join) | |
song = Song(source) | |
await ctx.voice_state.songs.put(song) | |
await ctx.send('Enqueued {}'.format(str(source))) | |
@commands.command(name='search') | |
async def _search(self, ctx: commands.Context, *, search: str): | |
"""Searches youtube. | |
It returns an imbed of the first 10 results collected from youtube. | |
Then the user can choose one of the titles by typing a number | |
in chat or they can cancel by typing "cancel" in chat. | |
Each title in the list can be clicked as a link. | |
""" | |
async with ctx.typing(): | |
try: | |
source = await YTDLSource.search_source(ctx, search, loop=self.bot.loop) | |
except YTDLError as e: | |
await ctx.send('An error occurred while processing this request: {}'.format(str(e))) | |
else: | |
if source == 'sel_invalid': | |
await ctx.send('Invalid selection') | |
elif source == 'cancel': | |
await ctx.send(':white_check_mark:') | |
elif source == 'timeout': | |
await ctx.send(':alarm_clock: **Time\'s up bud**') | |
else: | |
if not ctx.voice_state.voice: | |
await ctx.invoke(self._join) | |
song = Song(source) | |
await ctx.voice_state.songs.put(song) | |
await ctx.send('Enqueued {}'.format(str(source))) | |
@_join.before_invoke | |
@_play.before_invoke | |
async def ensure_voice_state(self, ctx: commands.Context): | |
if not ctx.author.voice or not ctx.author.voice.channel: | |
raise commands.CommandError('You are not connected to any voice channel.') | |
if ctx.voice_client: | |
if ctx.voice_client.channel != ctx.author.voice.channel: | |
raise commands.CommandError('Bot is already in a voice channel.') | |
bot = commands.Bot(command_prefix='|', case_insensitive=True, description="The Superior Bot") | |
bot.add_cog(Music(bot)) | |
@bot.event | |
async def on_ready(): | |
print('Logged in as:\n{0.user.name}\n{0.user.id}'.format(bot)) | |
@bot.command(name='botstop', aliases=['bstop']) | |
@commands.is_owner() | |
async def botstop(ctx): | |
print('Goodbye') | |
await ctx.send('Goodbye') | |
await bot.logout() | |
return | |
bot.run(token) |
on line 164, im getting bot is not defined
@justanothernoob4648 If you modify where you're defining the search_source
function to look like this async def search_source(self, ctx: commands.Context, search: str, *, loop: asyncio.BaseEventLoop = None, bot):
and then in the _search
function in the Music
class you modify where it calls the search_source
function to look like this source = await YTDLSource.search_source(ctx, search, loop=self.bot.loop, bot=self.bot)
This makes it so that you're passing in the bot
object into the search_source
function, resolving the problem
def init(self, ctx: commands.Context, source: discord.FFmpegPCMAudio, *, data: dict, volume: float = 0.5):
^
SyntaxError: invalid syntax
[Finished in 0.045s]So how to fix this?
@MalGamerz I think you're problem might be that you've put init(self,
instead of __init__(self,
It should look something like this def __init__(self, ctx: commands.Context, source: discord.FFmpegPCMAudio, *, data: dict, volume: float = 0.5):
i have got a error called raceback (most recent call last):
File "d:\Discord Bots\Mika_Bot\index.py", line 13, in
import httpx
ModuleNotFoundError: No module named 'httpx'
PS D:\Discord Bots\Mika_Bot>
- You'll have to install it:
pip install httpx
Also u have to do th above command in the cmd...
How to get to cmd???
1. [WIN] + [R]
2. Type cmd
3. Hit [ENTER]
4. Execute the pip command
Everything works fine but the bot does not play music even after printing await ctx.send('Enqueued {}'.format(str(source)))
despite adding search_source
Command raised an exception: AttributeError: type object 'YTDLSource' has no attribute 'search_source'
An error occurs.
i am getting error when use play command Unable to extract uploader id
New code is here https://github.com/Lenart12/GodecBot