diff --git a/.gitignore b/.gitignore index b8ef619..84fdfe3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ -bot-env -__pycache__ \ No newline at end of file +*-env +__pycache__ +*test.py +*.json +*.txt +*.mp4 \ No newline at end of file diff --git a/NeodiumDownload/__init__.py b/NeodiumDownload/__init__.py deleted file mode 100644 index 6fed0eb..0000000 --- a/NeodiumDownload/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .download import Downloader, YTdownload, INSdownload \ No newline at end of file diff --git a/NeodiumDownload/download.py b/NeodiumDownload/download.py deleted file mode 100644 index 37d4e6e..0000000 --- a/NeodiumDownload/download.py +++ /dev/null @@ -1,168 +0,0 @@ -import tempfile -import discord -import requests -import random -from discord.ext import commands -from string import ascii_letters -from discord import SelectMenu, SelectOption -from yt_dlp import YoutubeDL, utils - -class FileBin(): - def upload(filepath: str, filename: str): - api = 'https://filebin.net/' - bin = ''.join(random.choice(ascii_letters) for i in range(18)) - r = requests.post(f'{api}{bin}/{filename}', data=open(filepath, 'rb')).json() - dl_url = api+r['bin']['id']+'/'+r['file']['filename'] - return dl_url - -class Downloader(): - def __init__(self, client: discord.Client, cookie_file: str): - self.client = client - self.dl_ytops = { - 'cookiefile': cookie_file, - 'noplaylist': True - } - - def getUrlInfo(self, url: str): - video_resolutions = [] - try: - with YoutubeDL({'noplaylist': True}) as ydl: - info = ydl.extract_info(url, download=False) - except Exception as e: - raise e - - for format in info['formats']: - if format['format_note'][-1] == 'p' and format['format_note'] not in video_resolutions: - video_resolutions.append(format['format_note']) - - return info, video_resolutions - - async def getUserChoice(self, ctx: commands.Context, url: str): - try: - info, video_resolutions = self.getUrlInfo(url) - except utils.DownloadError as e: - embed=discord.Embed(title='The link is broken, can\'t fetch data', color=0xfe4b81) - await ctx.send(embed=embed, delete_after=15) - raise e - video_title = info['title'] - title = 'Available formats for' - - options = [] - options.append(SelectOption(emoji='🔊', label='Audio Only', value='1', description='.webm')) - for i, res in enumerate(video_resolutions): - options.append(SelectOption(emoji='🎥', label=res, value=f'{i+2}', description='.mp4')) - - embed=discord.Embed(title=title, description=f'[{video_title}]({url})', color=0xfe4b81) - emb = await ctx.send(embed=embed, components=[ - [ - SelectMenu( - custom_id='_select_it', - options=options, - placeholder='Select a format', - max_values=1, - min_values=1 - ) - ] - ]) - - def check_selection(i: discord.Interaction, select_menu): - return i.author == ctx.author and i.message == emb - - interaction, select_menu = await self.client.wait_for('selection_select', check=check_selection) - if int(select_menu.values[0]) == 1: - format = 'bestaudio' - ext = 'webm' - embed=discord.Embed(title='Preparing your file please bear with us...', color=0xfe4b81) - await interaction.respond(embed=embed, hidden=True) - await self.downloadAndSendFile(ctx, url, format, ext) - else: - resolution = video_resolutions[int(select_menu.values[0])-2][:-1] - format = f'bestvideo[height<={resolution}]+bestaudio/best[height<={resolution}]' - ext = 'mp4' - embed=discord.Embed(title='Preparing your file please bear with us...', color=0xfe4b81) - await interaction.respond(embed=embed, hidden=True) - await self.downloadAndSendFile(ctx, url, format, ext) - - async def downloadAndSendFile(self, ctx: commands.Context, url: str, format: str, ext: str): - ytops = self.dl_ytops - ytops['format'] = format - ytops['merge_output_format'] = ext - - with tempfile.TemporaryDirectory(prefix='neodium_dl_') as tempdirname: - ytops['outtmpl'] = f'{tempdirname}/%(title)s_[%(resolution)s].%(ext)s' - with YoutubeDL(ytops) as ydl: - info = ydl.extract_info(url, download=True) - filepath = ydl.prepare_filename(info) - filename = filepath.split('/')[-1] - - try: - embed=discord.Embed(title='Your file is ready to download', color=0xfe4b81) - await ctx.send(embed=embed, file=discord.File(filepath)) - except Exception as e: - embed=discord.Embed(title='Its taking too long', description='Probably due to file exceeding server upload limit. Don\'t worry we are shiping it to you through filebin, please bear with us.', color=0xfe4b81) - await ctx.send(embed=embed, delete_after=10) - dl_url = FileBin.upload(filepath, filename) - embed=discord.Embed(title='Your file is ready to download', description=f'[{filename}]({dl_url})\n\n**Powered by [filebin.net](https://filebin.net/)**', color=0xfe4b81) - await ctx.send(embed=embed) - raise e - -class YTdownload(Downloader): - def __init__(self, client: discord.Client): - cookie_file = 'yt_cookies.txt' - super().__init__(client, cookie_file) - - async def downloadVideo(self, ctx, url): - await self.getUserChoice(ctx, url) - -class INSdownload(Downloader): - def __init__(self, client: discord.Client): - self.cookie_file = 'insta_cookies.txt' - super().__init__(client, self.cookie_file) - - async def downloadVideo(self, ctx, url): - try: - with YoutubeDL({'cookiefile': self.cookie_file}) as ydl: - info = ydl.extract_info(url, download=False) - except utils.DownloadError as e: # try to revive the file through requests, also a private system is to be made - embed=discord.Embed(title='The link might not be AV or the account is private', color=0xfe4b81) - await ctx.send(embed=embed, delete_after=15) - raise e - except Exception as e: - raise e - video_title = info['title'] - title = 'Available formats for' - - options = [] - options.append(SelectOption(emoji='🔊', label='Audio Only', value='1', description='.webm')) - options.append(SelectOption(emoji='🎥', label='Audio and Video', value='2', description='.mp4')) - - embed=discord.Embed(title=title, description=f'[{video_title}]({url})', color=0xfe4b81) - emb = await ctx.send(embed=embed, components=[ - [ - SelectMenu( - custom_id='_select_it', - options=options, - placeholder='Select a format', - max_values=1, - min_values=1 - ) - ] - ]) - - def check_selection(i: discord.Interaction, select_menu): - return i.author == ctx.author and i.message == emb - - interaction, select_menu = await self.client.wait_for('selection_select', check=check_selection) - if int(select_menu.values[0]) == 1: - format = 'bestaudio' - ext = 'webm' - embed=discord.Embed(title='Preparing your file please bear with us...', color=0xfe4b81) - await interaction.respond(embed=embed, hidden=True) - await self.downloadAndSendFile(ctx, url, format, ext) - - else: - format = 'bestvideo+bestaudio/best' - ext = 'mp4' - embed=discord.Embed(title='Preparing your file please bear with us...', color=0xfe4b81) - await interaction.respond(embed=embed, hidden=True) - await self.downloadAndSendFile(ctx, url, format, ext) \ No newline at end of file diff --git a/NeodiumUtils/__init__.py b/NeodiumUtils/__init__.py new file mode 100644 index 0000000..f324b74 --- /dev/null +++ b/NeodiumUtils/__init__.py @@ -0,0 +1,19 @@ +""" +copyright (c) 2021 pritam20ps05(Pritam Das) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. +""" +from .download import Downloader, YTdownload, INSdownload, private_login, ydl_async, YDL_OPTIONS +from .helpcommand import NeodiumHelpCommand +from .paginator import Paginator +from .prefetch import getCookieFile +from .spotify import SpotifyClient +from .vars import * \ No newline at end of file diff --git a/NeodiumUtils/download.py b/NeodiumUtils/download.py new file mode 100644 index 0000000..76729e6 --- /dev/null +++ b/NeodiumUtils/download.py @@ -0,0 +1,387 @@ +""" +copyright (c) 2021 pritam20ps05(Pritam Das) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. +""" +import tempfile +import discord +import random +import asyncio +import aiohttp +import concurrent.futures +from os import remove +from shlex import quote +from discord.ext import commands +from json import load, dumps +from string import ascii_letters +from discord import SelectMenu, SelectOption +from yt_dlp import YoutubeDL, utils +from yt_dlp.extractor.instagram import InstagramBaseIE +from .vars import * + +async def ffmpegPostProcessor(inputfile, vc, ac, ext): + outfilename = inputfile.split('/')[-1] + outfiledir = '/'.join(inputfile.split('/')[:-1])+'/output/' + outfile_name = outfilename.split('.') + outfile_name[-1] = ext + outfile = outfiledir+'.'.join(outfile_name) + mkprocess = await asyncio.create_subprocess_shell(f'mkdir {outfiledir}') + await mkprocess.wait() + inputfile_sh = quote(inputfile) + outfile_sh = quote(outfile) + ffprocess = await asyncio.create_subprocess_shell(f'ffmpeg -i {inputfile_sh} -c:v {vc} -c:a {ac} {outfile_sh}') + await ffprocess.wait() + return outfile + +async def ydl_async(url, ytops, d): + def y_dl(url, ytops, d): + with YoutubeDL(ytops) as ydl: + info = ydl.extract_info(url, download=d) + return info + loop = asyncio.get_running_loop() + with concurrent.futures.ThreadPoolExecutor() as pool: + result = await loop.run_in_executor(pool, y_dl, url, ytops, d) + return result + +def sanitizeYDLReturnable(info: dict): + if info.get('_type') == 'playlist': + return False + return True + +class GoFileError(Exception): + def __init__(self, msg, resp) -> None: + self.status = resp.get('status') + self.data = resp.get('data') + self.payload = resp + super().__init__(f'{msg}. api responded with status {self.status}.') + +class GoFile(): + async def getServer(): + api = 'https://api.gofile.io/getServer' + async with aiohttp.ClientSession() as session: + async with session.get(api) as resp: + r = await resp.json() + if r.get('status') == 'ok': + return r['data'].get('server') + raise GoFileError('[get_server] can\'t fetch servername', r) + + async def upload(filepath: str, filename: str): + server = await GoFile.getServer() + api = f'https://{server}.gofile.io' + async with aiohttp.ClientSession() as session: + formdata = aiohttp.FormData() + with open(filepath, 'rb') as f: + formdata.add_field('file', f, filename=filename) + async with session.post(f'{api}/uploadFile', data=formdata) as resp: + r = await resp.json() + if r.get('status') == 'ok': + return r['data'].get('downloadPage') + raise GoFileError('[upload] can\'t upload file', r) + +class Downloader(): + def __init__(self, client: discord.Client, cookie_file: str): + self.client = client + self.dl_ops = { + 'playlist_items': '1', + 'restrictfilenames': True, + 'cookiefile': cookie_file + } + self.vcodecs = ['h264', 'copy'] + + async def downloadAndSendFile(self, ctx: commands.Context, url: str, format: str, ext: str, copt: int, usrcreds=None): + ytops = self.dl_ops.copy() + if usrcreds: + ytops.update(usrcreds) + ytops['format'] = format + ytops['merge_output_format'] = ext + InstagramBaseIE._IS_LOGGED_IN = False + + with tempfile.TemporaryDirectory(prefix='neodium_dl_') as tempdirname: + ytops['outtmpl'] = f'{tempdirname}/%(title)s_[%(resolution)s].%(ext)s' + with YoutubeDL(ytops) as ydl: + info = await ydl_async(url, ytops, True) + filepath = ydl.prepare_filename(info) + filepath = await ffmpegPostProcessor(filepath, self.vcodecs[copt], 'aac', ext) + filename = filepath.split('/')[-1] + + try: + embed=discord.Embed(title='Your file is ready to download', description=f'File requested by {ctx.author.mention}', color=0xfe4b81) + if usrcreds: + await ctx.author.send(embed=embed, file=discord.File(filepath)) + await ctx.author.send(f'{ctx.author.mention} your file is ready please download it.', delete_after=60) + else: + await ctx.send(embed=embed, file=discord.File(filepath)) + await ctx.send(f'{ctx.author.mention} your file is ready please download it.', delete_after=60) + except discord.errors.HTTPException as e: + embed=discord.Embed(title='Its taking too long', description='Probably due to file exceeding server upload limit. Don\'t worry we are shipping it to you through a file sharing vendor, please bear with us.', color=0xfe4b81) + await ctx.send(embed=embed, delete_after=20) + dl_url = await GoFile.upload(filepath, filename) + embed=discord.Embed(title='Your file is ready to download', description=f'[{filename}]({dl_url})\nFile requested by {ctx.author.mention}\n\n**Powered by [Gofile.io](https://gofile.io/)**', color=0xfe4b81) + if usrcreds: + await ctx.author.send(embed=embed) + await ctx.author.send(f'{ctx.author.mention} your file is ready please download it.', delete_after=60) + else: + await ctx.send(embed=embed) + await ctx.send(f'{ctx.author.mention} your file is ready please download it.', delete_after=60) + if 'Payload Too Large' in e.__str__(): + print(e) + return + raise e + + async def EHdownload(self, ctx: commands.Context, url: str, format: str, ext: str, copt: int, usrcreds=None): + try: + return await self.downloadAndSendFile(ctx, url, format, ext, copt, usrcreds) + except utils.DownloadError as e: + if 'Requested format is not available' in e.msg: + embed=discord.Embed(title='Cannot fetch the requested format', color=0xfe4b81) + await ctx.send(embed=embed, delete_after=15) + return + raise e + +class YTdownload(Downloader): + def __init__(self, client: discord.Client): + cookie_file = 'yt_cookies.txt' + super().__init__(client, cookie_file) + + async def getUrlInfo(self, url: str, ctx): + video_resolutions = [] + try: + info = await ydl_async(url, self.dl_ops, False) + if not sanitizeYDLReturnable(info): + embed=discord.Embed(title='Multi-video support is currently not available', color=0xfe4b81) + await ctx.send(embed=embed, delete_after=15) + return (None, None) + except Exception as e: + raise e + + for format in info['formats']: + if 'p' in format['format_note'] and format['format_note'] not in video_resolutions: + video_resolutions.append(format['format_note']) + + return info, video_resolutions + + async def downloadVideo(self, ctx, url, copt): + try: + info, video_resolutions = await self.getUrlInfo(url, ctx) + if not info: + return + except utils.DownloadError as e: + embed=discord.Embed(title='The link is broken, can\'t fetch data', color=0xfe4b81) + await ctx.send(embed=embed, delete_after=15) + raise e + video_title = info['title'] + video_page = info['webpage_url'] + title = 'Available formats for' + + options = [] + options.append(SelectOption(emoji='🔊', label='Audio Only', value='1', description='.m4a')) + for i, res in enumerate(video_resolutions): + options.append(SelectOption(emoji='🎥', label=res, value=res, description='.mp4')) + + embed=discord.Embed(title=title, description=f'[{video_title}]({video_page})', color=0xfe4b81) + select_menu_context = SelectMenu( + custom_id='_select_it', + options=options, + placeholder='Select a format', + max_values=1, + min_values=1 + ) + emb = await ctx.send(embed=embed, components=[[select_menu_context]]) + + def check_selection(i: discord.Interaction, select_menu): + return i.author == ctx.author and i.message == emb + + async def disable_menu(ctx): + select_menu_context.disabled = True + await ctx.edit(embed=embed, components=[[select_menu_context]]) + + try: + interaction, select_menu = await self.client.wait_for('selection_select', check=check_selection, timeout=30.0) + except asyncio.TimeoutError: + print('timeout on selection_select') + await disable_menu(emb) + return + finally: + await disable_menu(emb) + if str(select_menu.values[0]) == '1': + format = 'bestaudio' + ext = 'm4a' + embed=discord.Embed(title='Preparing your file please bear with us...', description='This might take some time due to recent codec convertion update. We will let you know when your file gets ready', color=0xfe4b81) + await interaction.respond(embed=embed, hidden=True) + await self.EHdownload(ctx, video_page, format, ext, copt) + else: + format_note = select_menu.values[0] + format = f'bestvideo[format_note={format_note}]+bestaudio/best' + ext = 'mp4' + embed=discord.Embed(title='Preparing your file please bear with us...', description='This might take some time due to recent codec convertion update. We will let you know when your file gets ready', color=0xfe4b81) + await interaction.respond(embed=embed, hidden=True) + await self.EHdownload(ctx, video_page, format, ext, copt) + +class INSdownload(Downloader): + def __init__(self, client: discord.Client): + self.cookie_file = 'insta_cookies.txt' + super().__init__(client, self.cookie_file) + + async def downloadVideo(self, ctx, url, copt, usrcreds): + ig_ops = self.dl_ops.copy() + if usrcreds: + ig_ops.update(usrcreds) + + InstagramBaseIE._IS_LOGGED_IN = False + try: + info = await ydl_async(url, ig_ops, False) + if not sanitizeYDLReturnable(info): + embed=discord.Embed(title='Multi-video support is currently not available', color=0xfe4b81) + await ctx.send(embed=embed, delete_after=15) + return + except utils.DownloadError as e: # try to revive the file through requests, also a private system is to be made + if usrcreds: + embed=discord.Embed(title='The link might not be AV or the account is private or try relogging', color=0xfe4b81) + await ctx.send(embed=embed, delete_after=15) + else: + embed=discord.Embed(title='The link might not be AV or the account is private', color=0xfe4b81) + await ctx.send(embed=embed, delete_after=15) + raise e + except Exception as e: + raise e + video_title = info['title'] + video_page = info['webpage_url'] + title = 'Available formats for' + + options = [] + options.append(SelectOption(emoji='🔊', label='Audio Only', value='1', description='.m4a')) + options.append(SelectOption(emoji='🎥', label='Audio and Video', value='2', description='.mp4')) + + embed=discord.Embed(title=title, description=f'[{video_title}]({video_page})', color=0xfe4b81) + select_menu_context = SelectMenu( + custom_id='_select_it', + options=options, + placeholder='Select a format', + max_values=1, + min_values=1 + ) + emb = await ctx.send(embed=embed, components=[[select_menu_context]]) + + def check_selection(i: discord.Interaction, select_menu): + return i.author == ctx.author and i.message == emb + + async def disable_menu(ctx): + select_menu_context.disabled = True + await ctx.edit(embed=embed, components=[[select_menu_context]]) + + try: + interaction, select_menu = await self.client.wait_for('selection_select', check=check_selection, timeout=30.0) + except asyncio.TimeoutError: + print('timeout on selection_select') + await disable_menu(emb) + return + finally: + await disable_menu(emb) + if str(select_menu.values[0]) == '1': + format = 'bestaudio' + ext = 'm4a' + embed=discord.Embed(title='Preparing your file please bear with us...', description='This might take some time due to recent codec convertion update. We will let you know when your file gets ready', color=0xfe4b81) + await interaction.respond(embed=embed, hidden=True) + await self.EHdownload(ctx, video_page, format, ext, copt, usrcreds) + + else: + format = 'bestvideo+bestaudio/best' + ext = 'mp4' + embed=discord.Embed(title='Preparing your file please bear with us...', description='This might take some time due to recent codec convertion update. We will let you know when your file gets ready', color=0xfe4b81) + await interaction.respond(embed=embed, hidden=True) + await self.EHdownload(ctx, video_page, format, ext, copt, usrcreds) + +class private_login(): + def __init__(self, cred_path): + self.cred_path = cred_path + try: + with open(cred_path) as f: + self.creds = load(f) + except: + with open(cred_path, 'w') as f: + f.write('{}') + with open(cred_path) as f: + self.creds = load(f) + + def flush_data(self): + dump_data = dumps(self.creds, indent=4) + + with open(self.cred_path, "w") as outfile: + outfile.write(dump_data) + + def unique_keygen(self, chs=6): + return ''.join(random.choice(ascii_letters) for _ in range(chs)) + + async def login(self, ctx: commands.Context, usrn, passw): + is_username_valid = True + is_password_valid = True + has_process_failed = False + ukey = self.unique_keygen() + + if not self.is_user_authenticated(ctx.author.id): + ops = { + 'username': usrn, + 'password': passw, + 'extract_flat': True, + 'cookiefile': f'userdata/{ukey}_cookie.txt' + } + InstagramBaseIE._IS_LOGGED_IN = False + + try: + _ = await ydl_async('https://www.instagram.com/p/Cbj-9Tglk_i/', ops, False) + except utils.DownloadError as e: + if 'The username you entered doesn\'t belong to an account' in e.msg: + is_username_valid = False + elif 'your password was incorrect' in e.msg: + is_password_valid = False + else: + has_process_failed = True + print(e) + except Exception as e: + has_process_failed = True + print(e) + + if is_username_valid and is_password_valid and not has_process_failed: + self.creds[str(ctx.author.id)] = { + 'cookiefile': f'userdata/{ukey}_cookie.txt' + } + self.flush_data() + embed=discord.Embed(title='You have been successfully authenticated', color=0xfe4b81) + await ctx.send(embed=embed) + elif has_process_failed: + embed=discord.Embed(title='The authentication was not successfull', description='The authentication was not possible due to some reason. Make sure you have 2 factor auth disabled on your account. If the problem continues report it to the dev', color=0xfe4b81) + await ctx.send(embed=embed) + elif not is_username_valid: + embed=discord.Embed(title='Invalid username', color=0xfe4b81) + await ctx.send(embed=embed) + elif not is_password_valid: + embed=discord.Embed(title='Invalid password', color=0xfe4b81) + await ctx.send(embed=embed) + + def is_user_authenticated(self, uid: str): + if str(uid) in self.creds.keys(): + return True + else: + return False + + def get_usercreds(self, uid: str): + if self.is_user_authenticated(str(uid)): + return self.creds[str(uid)] + else: + return None + + async def logout(self, ctx: commands.Context): + if self.is_user_authenticated(ctx.author.id): + data = self.creds.pop(str(ctx.author.id)) + remove(data['cookiefile']) + self.flush_data() + embed=discord.Embed(title='You have been successfully logged out of your account', color=0xfe4b81) + await ctx.send(embed=embed) diff --git a/NeodiumUtils/helpcommand.py b/NeodiumUtils/helpcommand.py new file mode 100644 index 0000000..9b8f4f6 --- /dev/null +++ b/NeodiumUtils/helpcommand.py @@ -0,0 +1,133 @@ +""" +copyright (c) 2021 pritam20ps05(Pritam Das) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. +""" +import asyncio +import discord +from discord.ext import commands +from discord import SelectMenu, SelectOption +from .vars import * + +class NeodiumHelpCommand(commands.HelpCommand): + def __init__(self): + super().__init__() + self.verify_checks = False + + async def send_bot_help(self, mapping): + cog_embeds = {} + embed=discord.Embed(title='Neodium', description='A bot built by [pritam20ps05](https://github.com/pritam20ps05) made to enhance the experience of music by integrating it into discord. This bot was initially made to replace groovy after its discontinuation but it became something much more than that. This is a vanilla(original) version of the open source project neodium, checkout on github for extended documentation.', url='https://github.com/pritam20ps05/neodium', color=0xfe4b81) + embed.set_footer(text='Built on neodium core v1.2 by pritam20ps05', icon_url='https://user-images.githubusercontent.com/49360491/170466737-afafd7aa-f067-4503-9a1d-7d74de1b474b.png') + options = [] + for cog in mapping: + if cog: + cog_embeds[cog.qualified_name] = discord.Embed(title=cog.qualified_name, description=cog.description, color=0xfe4b81) + command_names = [] + for command in mapping[cog]: + command_names.append(command.name) + alias = '' + if command.aliases != []: + a = f', {self.context.prefix}'.join(command.aliases) + alias = f', {self.context.prefix}{a}' + cog_embeds[cog.qualified_name].add_field(name=f'{self.context.prefix}{command.name}{alias}', value=command.help) + cog_embeds[cog.qualified_name].set_footer(text=f'Use {self.context.prefix}help {cog.qualified_name} or {self.context.prefix}help to see more details') + options.append(SelectOption(label=cog.qualified_name, value=cog.qualified_name, description=', '.join(command_names[:4]))) + + select_menu_context = SelectMenu( + custom_id='_help_select_it', + options=options, + placeholder='Select a command type for its help', + max_values=1, + min_values=1 + ) + + msg = await self.get_destination().send(embed=embed, components=[ + [select_menu_context] + ]) + + def check_selection(i: discord.Interaction, select_menu): + return i.author == self.context.author and i.message == msg + + async def selection_callback(msg, interaction: discord.Interaction, select_menu): + await interaction.defer() + await msg.edit(embed=cog_embeds[select_menu.values[0]]) + + select_menu: SelectMenu = None + + try: + while True: + interaction, select_menu = await self.context.bot.wait_for('selection_select', check=check_selection, timeout=30.0) + await selection_callback(msg, interaction, select_menu) + except asyncio.TimeoutError: + select_menu_context.disabled = True + if not select_menu: + await msg.edit(embed=embed, components=[[select_menu_context]]) + else: + await msg.edit(embed=cog_embeds[select_menu.values[0]], components=[[select_menu_context]]) + + + async def send_cog_help(self, cog): + command_embeds = {} + embed=discord.Embed(title=cog.qualified_name, description=cog.description, color=0xfe4b81) + embed.set_footer(text='Built on neodium core v1.2 by pritam20ps05', icon_url='https://user-images.githubusercontent.com/49360491/170466737-afafd7aa-f067-4503-9a1d-7d74de1b474b.png') + commands = cog.get_commands() + options = [] + for command in commands: + alias = '' + if command.aliases != []: + a = f', {self.context.prefix}'.join(command.aliases) + alias = f', {self.context.prefix}{a}' + embed.add_field(name=f'{self.context.prefix}{command.name}{alias}', value=command.help) + command_embeds[command.name] = discord.Embed(title=f'{self.context.prefix}{command.name}{alias} {command.signature}', description=command.help, color=0xfe4b81) + command_embeds[command.name].set_footer(text=f'Use {self.context.prefix}help {command.name} to see only this message') + options.append(SelectOption(label=command.name, value=command.name)) + + select_menu_context = SelectMenu( + custom_id='_help_command_select_it', + options=options, + placeholder='Select a command for its help', + max_values=1, + min_values=1 + ) + + msg = await self.get_destination().send(embed=embed, components=[ + [select_menu_context] + ]) + + def check_selection(i: discord.Interaction, select_menu): + return i.author == self.context.author and i.message == msg + + async def selection_callback(msg, interaction: discord.Interaction, select_menu): + await interaction.defer() + await msg.edit(embed=command_embeds[select_menu.values[0]]) + + select_menu: SelectMenu = None + + try: + while True: + interaction, select_menu = await self.context.bot.wait_for('selection_select', check=check_selection, timeout=30.0) + await selection_callback(msg, interaction, select_menu) + except asyncio.TimeoutError: + select_menu_context.disabled = True + if not select_menu: + await msg.edit(embed=embed, components=[[select_menu_context]]) + else: + await msg.edit(embed=command_embeds[select_menu.values[0]], components=[[select_menu_context]]) + + + async def send_command_help(self, command): + alias = '' + if command.aliases != []: + a = f', {self.context.prefix}'.join(command.aliases) + alias = f', {self.context.prefix}{a}' + embed = discord.Embed(title=f'{self.context.prefix}{command.name}{alias} {command.signature}', description=command.help, color=0xfe4b81) + embed.set_footer(text='Built on neodium core v1.2 by pritam20ps05', icon_url='https://user-images.githubusercontent.com/49360491/170466737-afafd7aa-f067-4503-9a1d-7d74de1b474b.png') + await self.get_destination().send(embed=embed) \ No newline at end of file diff --git a/NeodiumUtils/paginator.py b/NeodiumUtils/paginator.py new file mode 100644 index 0000000..806e3c2 --- /dev/null +++ b/NeodiumUtils/paginator.py @@ -0,0 +1,140 @@ +""" +copyright (c) 2021 pritam20ps05(Pritam Das) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. +""" +import discord +import asyncio +from discord.ext import commands +from discord import ActionRow, Button, ButtonStyle +from .vars import * + +class Paginator(): + def __init__(self, bot: commands.Bot) -> None: + self.bot = bot + + async def flushPage(self, embed: discord.Embed, components: list[ActionRow], msg: discord.Message, disable_btns=False, delete=False): + for component in components: + component.disable_all_buttons_if(check=disable_btns) + if delete: + await msg.edit(embed=embed, components=components, delete_after=15) + else: + await msg.edit(embed=embed, components=components) + + async def handlePages(self, embeds: list[discord.Embed], ctx: commands.Context): + button_component = [ActionRow( + Button( + custom_id='first', + label='❮❮', + style=ButtonStyle.Primary + ), + Button( + custom_id='back', + label='❮', + style=ButtonStyle.Primary + ), + Button( + custom_id='next', + label='❯', + style=ButtonStyle.Primary + ), + Button( + custom_id='last', + label='❯❯', + style=ButtonStyle.Primary + ) + )] + + page_tracker = 0 + msg = await ctx.send(embed=embeds[page_tracker], components=button_component) + + def check(i: discord.Interaction, b): + return i.message == msg and i.member == ctx.author + + try: + while True: + interaction, button = await self.bot.wait_for('button_click', check=check, timeout=30.0) + await interaction.defer() + if button.custom_id == 'first': + page_tracker = 0 + elif button.custom_id == 'back': + if page_tracker > 0: + page_tracker = page_tracker - 1 + elif button.custom_id == 'next': + if page_tracker < len(embeds) - 1: + page_tracker = page_tracker + 1 + elif button.custom_id == 'last': + page_tracker = len(embeds) - 1 + + await self.flushPage(embed=embeds[page_tracker], components=button_component, msg=msg) + except asyncio.TimeoutError: + await self.flushPage(embed=embeds[page_tracker], components=button_component, msg=msg, disable_btns=True) + + async def handleOptions(self, embed: discord.Embed, nops: int, ctx: commands.Context): + button_row = [] + for i in range(nops): + button_row.append( + Button( + custom_id=f'opt:{i}', + label=f'{i+1}', + style=ButtonStyle.Primary + ) + ) + button_component = [ActionRow(*button_row)] + msg = await ctx.send(embed=embed, components=button_component) + + def check(i: discord.Interaction, b): + return i.message == msg and i.member == ctx.author + + try: + interaction, button = await self.bot.wait_for('button_click', check=check, timeout=30.0) + await interaction.defer() + await self.flushPage(embed=embed, components=button_component, msg=msg, disable_btns=True) + return int(button.custom_id.split(':')[1]) + except asyncio.TimeoutError as e: + await self.flushPage(embed=embed, components=button_component, msg=msg, disable_btns=True) + raise e + + async def handleDecision(self, embed: discord.Embed, resp_embed: discord.Embed, ctx: commands.Context, default=False): + button_component = [ActionRow( + Button( + custom_id='yes', + emoji='👍', + style=ButtonStyle.Success + ), + Button( + custom_id='no', + emoji='👎', + style=ButtonStyle.Danger + ) + )] + + msg = await ctx.send(embed=embed, components=button_component) + + def check(i: discord.Interaction, b): + return i.message == msg and i.member == ctx.author + + try: + interaction, button = await self.bot.wait_for('button_click', check=check, timeout=30.0) + await interaction.defer() + if button.custom_id == 'yes': + await self.flushPage(embed=resp_embed, components=button_component, msg=msg, disable_btns=True) + return True + else: + await self.flushPage(embed=embed, components=button_component, msg=msg, disable_btns=True, delete=True) + return False + except asyncio.TimeoutError: + if default: + await self.flushPage(embed=resp_embed, components=button_component, msg=msg, disable_btns=True) + return True + else: + await self.flushPage(embed=embed, components=button_component, msg=msg, disable_btns=True, delete=True) + return False \ No newline at end of file diff --git a/NeodiumUtils/prefetch.py b/NeodiumUtils/prefetch.py new file mode 100644 index 0000000..12e9ceb --- /dev/null +++ b/NeodiumUtils/prefetch.py @@ -0,0 +1,36 @@ +""" +copyright (c) 2021 pritam20ps05(Pritam Das) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. +""" +import gdown +from .vars import * + +class GdriveDownloadError(Exception): + def __init__(self, id): + super().__init__(f'Error downloading file -> {id} from google drive') + +def getCookieFile(id_insta=None, id_yt=None): + """ + CookieFile Fetcher + ------------------- + A function that downloads the credential files from G-drive whenever invoked + """ + if not id_insta: + id_insta = ig_file_id + if not id_yt: + id_yt = yt_file_id + output_insta = 'insta_cookies.txt' + output_yt = 'yt_cookies.txt' + f1 = gdown.download(output=output_insta, id=id_insta, quiet=False) + if not f1: raise GdriveDownloadError(id_insta) + f2 = gdown.download(output=output_yt, id=id_yt, quiet=False) + if not f2: raise GdriveDownloadError(id_yt) \ No newline at end of file diff --git a/NeodiumUtils/spotify.py b/NeodiumUtils/spotify.py new file mode 100644 index 0000000..43c5e89 --- /dev/null +++ b/NeodiumUtils/spotify.py @@ -0,0 +1,70 @@ +""" +copyright (c) 2021 pritam20ps05(Pritam Das) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. +""" +import discord +import tekore as tk +from discord.ext import commands +from yt_dlp import YoutubeDL +from .download import ydl_async +from .vars import * + +token = tk.request_client_token(client_id, client_secret) +spotify = tk.Spotify(token, asynchronous=True) + +class SpotifyClient(): + def getID(self, url: str, rtype: str): + return url.split(f'{rtype}/')[1].split('?')[0] + + def getURL(self, URI: str): + rtype, rid = URI.split(':')[1:] + return f'https://open.spotify.com/{rtype}/{rid}' + + async def addSongs(self, playlist_tracks: list[tk.model.PlaylistTrack], queue, ctx: commands.Context): + for i, track in enumerate(playlist_tracks): + track = track.track + if track.track: + query = f'{track.artists[0].name} - {track.name} official audio'.replace(":", "").replace("\"", "") + if i == 0: + with YoutubeDL(YDL_OPTIONS) as ydl: + info = ydl.extract_info(f'ytsearch:{query}', download=False) + else: + info = await ydl_async(f'ytsearch:{query}', YDL_OPTIONS, False) + info = info['entries'][0] + queue.append({ + "link": info['url'], + "url": self.getURL(track.uri), + "title": f'{track.name} - {track.artists[0].name}', + "thumbnails": [track.album.images[0].__dict__] + }) + embed=discord.Embed(title="Playlist items were added to queue", color=0xfe4b81) + await ctx.send(embed=embed) + + async def getPlaylist(self, playlist_url: str, ctx: commands.Context, sp: int, ep: int): + playlist_info = await spotify.playlist(self.getID(playlist_url, 'playlist')) + playlist_thumbnail = playlist_info.images[0].url + playlist_title = playlist_info.name + playlist_description = playlist_info.description + playlist_url = self.getURL(playlist_info.uri) + playlist_tracks = playlist_info.tracks.items[sp:ep] + + if sp or ep: + if ep: + embed=discord.Embed(title="Adding Playlist", description=f'[{playlist_title}]({playlist_url})\n{playlist_description}\n\n**From {sp+1} to {ep}**', color=0xfe4b81) + else: + embed=discord.Embed(title="Adding Playlist", description=f'[{playlist_title}]({playlist_url})\n{playlist_description}\n\n**From {sp+1} to {len(playlist_tracks)+sp}**', color=0xfe4b81) + else: + embed=discord.Embed(title="Adding Playlist", description=f'[{playlist_title}]({playlist_url})\n{playlist_description}', color=0xfe4b81) + embed.set_thumbnail(url=playlist_thumbnail) + embed.set_author(name='Spotify', icon_url='https://user-images.githubusercontent.com/49360491/177480503-96f98632-33a3-4884-a20e-572c13580bc9.png') + await ctx.send(embed=embed) + return playlist_tracks \ No newline at end of file diff --git a/NeodiumUtils/vars.py b/NeodiumUtils/vars.py new file mode 100644 index 0000000..4d3551e --- /dev/null +++ b/NeodiumUtils/vars.py @@ -0,0 +1,34 @@ +""" +copyright (c) 2021 pritam20ps05(Pritam Das) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. +""" +from os import environ + +token = environ["TOKEN"] +search_engine = environ["SEARCH_ENGINE"] +search_token = environ["SEARCH_TOKEN"] +yt_file_id = environ['YT_COOKIEFILE_ID'] +ig_file_id = environ['INSTA_COOKIEFILE_ID'] +client_id = environ['SPOTIFY_CLIENT_ID'] +client_secret = environ['SPOTIFY_CLIENT_SECRET'] + +YDL_OPTIONS = { + 'format': 'bestaudio', + 'noplaylist': 'True', + 'source_address': '0.0.0.0', + "cookiefile": "yt_cookies.txt" +} + +FFMPEG_OPTIONS = { + 'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5', + 'options': '-vn' +} \ No newline at end of file diff --git a/README.md b/README.md index b24a6dd..7f4c5a0 100644 --- a/README.md +++ b/README.md @@ -1,42 +1,118 @@ # Neodium -Neodium is a discord music bot with many advanced features. This bot has been made to fill the place of groovy and make it open-source so that anyone can use it. With a First come First serve and task clearing queueing system that is when a queued audio is played it gets removed from the queue so the next audio in the queue can be played. +Neodium is a discord music bot with many advanced features. This bot has been made to fill the place of groovy and make it open-source so that anyone can use it. With a First come First serve and task clearing queueing system that is when a queued audio is played it gets removed from the queue so the next audio in the queue can be played. Supports playing audio from youtube and spotify. ## Key Features 1. No disturb feature: If anyone is serving someone then the bot can't be made to leave the VC. 2. Advanced No disturb: You can lock the player by using -lock command and anyone will not be able to clear the queue and pause, resume, skip or stop the player except who initiated the lock. In future planning to add a voting system. -3. Live Feature: The bot can play live youtube videos using -live or -l +3. Live Feature: The bot can play live youtube videos using -live or -l . +4. Download Feature: Download any public YT or instagram audio video file. Additional support has also been given for instagram private only. **NOTE: These are the only the exclusive features that other bots generally don't have. For full usage check usage section** -## Usage/Commands - -**-add-playlist** [playlist url] [starting index] [ending index]: To add a playlist to queue -**-clear-queue** or **-clear** : Can only be done if there is no lock initiated. Clears the queue. -**-help** : Shows help message. -**-join** : Makes the bot join a VC. -**-leave** : Makes the bot leave the VC. -**-live** or **-l** [video url] : Plays the live youtube video. -**-lock** : Locks the player and the queue. -**-lyrics** : Displays lyrics of the current song. -**-pause** : Pauses player. -**-play** or **-p** [keyword] : Searches the keyword on youtube and plays the first result. -**-queue** [delimeter] : Displays the songs currently in queue. -**-remove** [index no] : Removes a mentioned song from queue. -**-resume** : Resumes the player. -**-search** or **-s** [keyword] : Searches the keyword and displays first 5 results. Choosing one of them will queue the song. -**-skip** : Skips the current song. -**-stop** : Stops the player from playing anything else. -**-download** or **-d** [video url]: Downloads the YT video, Instagram video or reel in the url. +# Usage/Commands +## Basic +This category of commands contains the basic functionalities of the bot such as joining a VC. +### commands + -join Makes the bot join the channel + of the user, if the bot has already + joined and is in a different channel + it will move to the channel the user + is in. Only if you are not trying to + disturb someone. + + -leave Makes the bot leave the voice channel. + Only if you are not trying to disturb + someone. +## Player +This category of commands contains the playable functionalities of the bot. All of them can make bot join vc, play audio and queue audio. +### commands + -search, -s [query] Searches the query on YT and gives 5 + results to choose from. Choosen one + will be queued or played. + + -play, -p [query] Searches the query on YT and plays or + queues the most relevant result. + + -live, -l [url] Plays a YT live from the URL provided + as input. + + -add-playlist [playlist_url] Adds a whole YT or Spotify playlist to queue + [starting_index] [ending_index] and starts playing it. Input is taken as + the URL to the public or unlisted playlist. + You can also mention a starting point or an + ending point of the playlist or both. +## Visualizer +This category of commands contains the visualizers which enables you to monitor some states of the bot or get some kind of info about something +### commands + -queue [limit=10] Displays the current queue. Limit is the + number of entries shown per page, default + is 10. + + -lyrics Displays the lyrics of the current song if + available. + + -current, -c Displays information about the current song + in the player. +## Queue +This category of commands contains the commands related to queues. Also there is a concept of queue lock which will dissable any user from using these commands except the user initiating the lock with some more exceptions. Queue lock effective commands will be marked with "Q". +### commands + -remove Q Removes an mentioned entry from the queue. + + -pause Q Pauses the current player. + + -resume Q Resumes the paused player. + + -skip Q Skips current audio. + + -stop Q Just like skip but also clears the queue. + + -clear-queue, -clear Q Clears the queue. + + -shuffle Q Randomly shuffles the whole queue. + + -lock Locks the queue and prevents anyone from + damaging anyone\'s experience. +## Download +This category of commands contains recently added download feature which can download YT and instagram audio video files with private support for instagram only. +### commands + -download, -d [url] Downloads YT or instagram audio video files + [codec_option=0] from the url. If the bot is already playing + something then passing no input will result in + selecting that video. Codec_option is for + choosing vcodec, default is 0 for h264 but can + be set to 1 for codec provided by vendor. + + -login [username] Supports the instagram private feature. + [password] This command Logs in to your instagram + account and uses it to access files through + your account. Once logged in use the download + command normally. This command can only be + used in DMs in order to protect your privacy. + + -logout Logout of your account only if you are already + logged in. +## Special +This category of commands contains the special commands which can only be accessed by the owner of the bot. These commands enables the owner to remotely invoke methods for temporary fixes or other debugging stuff. +### commands + -refetch [insta_cookie_fileid] Refetches the default cookie files from the + [yt_cookie_fileid] google dive file ids' of the cookie files. + If not passed it gets the file ids' from + environmental variables. + + -add-cog [cog_name] Adds a predefined cog to the bot. + + -remove-cog [cog_name] Removes a already existing cog. Generally + used to disable a functionality of the bot. ## Installation/Setup -First make sure you have ffmpeg installed install it by +First make sure you have ffmpeg installed, install it by for debian: ```bash -apt install ffmpeg +sudo apt install ffmpeg ``` Use the package manager [pip](https://pip.pypa.io/en/stable/) to install the requirements. @@ -45,38 +121,43 @@ Use the package manager [pip](https://pip.pypa.io/en/stable/) to install the req pip install -r requirements.txt ``` -This bot fetches lyrics from [genius.com](https://genius.com) searched using google search api. All this things are done by the lyrics_extractor module which requires SEARCH ENGINE CODE and GOOGLE SEARCH API TOKEN. How to setup lyrics_extractor is given [here](https://www.geeksforgeeks.org/create-a-gui-to-extract-lyrics-from-song-using-python/) you just need the two values and put it in credentials.json. +This bot fetches lyrics from [genius.com](https://genius.com) searched using google search api. All this things are done by the lyrics_extractor module which requires SEARCH ENGINE CODE and GOOGLE SEARCH API TOKEN. How to setup lyrics_extractor is given [here](https://www.geeksforgeeks.org/create-a-gui-to-extract-lyrics-from-song-using-python/) you just need the two values. -Now time for credentials if you are using the code from master branch or raw_dev then create a credentials.jsob file. +Now get the [bot token](https://github.com/reactiflux/discord-irc/wiki/Creating-a-discord-bot-&-getting-a-token), after that create 2 cookie files one for youtube and the other for instagram using [cookies.txt](https://chrome.google.com/webstore/detail/get-cookiestxt/bgaddhkoddajcdgocldbbfleckgcbcid?hl=en) chrome extension. Once done upload them to google drive, make it public and copy their file ids'. -### credentials.json -```json -{ - "token": "BOT TOKEN", - "search_engine": "SEARCH ENGINE CODE", - "search_token": "GOOGLE SEARCH API TOKEN" -} -``` -But if you using the code from deploy then there is no credentials.json file it takes them from the environment. Now setting them up. +You will also need spotify client id and client secret which you can get them after creating an app from [here](https://developer.spotify.com/dashboard/applications). + +The following are all the environmental variables required for starting the bot. ```bash -export TOKEN= -export SEARCH_ENGINE= -export SEARCH_TOKEN= +TOKEN +SEARCH_ENGINE +SEARCH_TOKEN +INSTA_COOKIEFILE_ID +YT_COOKIEFILE_ID +SPOTIFY_CLIENT_ID +SPOTIFY_CLIENT_SECRET ``` -The deploy branch requires an extra file named cookies.txt its just to remove age restriction. So to make it log into your not age restricted google account in your browser and then export cookies.txt file of that account using the cookies.txt extension. See more about it in setup of youtube-dl. - -Now running it is pretty easy as you can make a service, a background task etc. but the very general way is - -```bash -python bot.py -``` - -## Cautions - -1. Never try to make money from this project, not because I will sue you but Youtube can sue you and thats why this project is open-sourced. -2. Don't get confused between the branches and their code. master is the general stable code base, raw_dev is for me to write code and deploy is for servers or particularly for platform as a service. Also don't use any code outside these branches as they can be outdated. Deploy branch is always recomended. +In case of deploying it to heroku chekout the [deploy branch](https://github.com/pritam20ps05/neodium/tree/deploy) which will require the following buildpacks and the above mentioned variables. +### Buildpacks + heroku/python + https://github.com/jonathanong/heroku-buildpack-ffmpeg-latest.git + https://github.com/xrisk/heroku-opus.git + +## Branch Info + master The general and the most recent stable + version of the bot. + + raw_dev The most updated version of the bot + and the one with raw development. + + deploy The code that is ready to be deployed + to a heroku server. + + proj-info A accessory branch which gets the most + updates regarding documentation and + license. ## Contributing Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. diff --git a/bot.py b/bot.py index fb48846..1424812 100644 --- a/bot.py +++ b/bot.py @@ -1,38 +1,37 @@ +""" +copyright (c) 2021 pritam20ps05(Pritam Das) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. +""" import discord -import urllib.request -import re import asyncio -from time import time +import re +from random import shuffle from discord.ext import commands from discord.utils import get from discord import FFmpegPCMAudio -from DiscordUtils.Pagination import CustomEmbedPaginator as EmbedPaginator from yt_dlp import YoutubeDL, utils from lyrics_extractor import SongLyrics, LyricScraperException -from NeodiumDownload import YTdownload, INSdownload -from json import load - -YDL_OPTIONS = { - 'format': 'bestaudio', - 'noplaylist': 'True', - 'source_address': '0.0.0.0' - } - -FFMPEG_OPTIONS = { - 'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5', - 'options': '-vn' - } - -with open("credentials.json", "r") as creds: - cred = load(creds) - token = cred["token"] - search_engine = cred["search_engine"] - search_token = cred["search_token"] - -client = commands.Bot(command_prefix='-') # prefix our commands with '-' +from NeodiumUtils import * + +getCookieFile() + +activity = discord.Activity(type=discord.ActivityType.listening, name="-help") +client = commands.Bot(command_prefix='-', activity=activity, help_command=NeodiumHelpCommand()) # prefix our commands with '-' lyrics_api = SongLyrics(search_token, search_engine) +spotify_api = SpotifyClient() yt_dl_instance = YTdownload(client) in_dl_instance = INSdownload(client) +private_instance = private_login('login.json') +paginator = Paginator(client) player = {} masters = {} @@ -40,58 +39,88 @@ queuelocks = {} +def initGuilds(): + for guild in client.guilds: + player[guild.id] = {} + queues[guild.id] = [] + queuelocks[guild.id] = {} + queuelocks[guild.id]["lock"] = False + + + +class QueueLockCheckFailure(commands.CheckFailure): + def __init__(self, message=None): + super().__init__(message) + +def checkQueueLock(hard=False, check_if_bot_connected=False): + async def predicate(ctx): + voice = get(client.voice_clients, guild=ctx.guild) + if voice or not check_if_bot_connected: + if ctx.guild.id in queuelocks.keys() and queuelocks[ctx.guild.id]["lock"] and queuelocks[ctx.guild.id]["author"].voice and queuelocks[ctx.guild.id]["author"].voice.channel == voice.channel and not (not (voice.is_playing() or voice.is_paused()) and queues[ctx.guild.id] == []): + if queuelocks[ctx.guild.id]["author"] == ctx.message.author and not hard: + return True + else: + raise QueueLockCheckFailure("The queue is currently locked") + else: + queuelocks[ctx.guild.id] = {} + queuelocks[ctx.guild.id]["lock"] = False + return True + else: + raise QueueLockCheckFailure("I am currently not connected to any voice channel") + return commands.check(predicate) + # main queue manager function to play music in queues async def check_queue(id, voice, ctx, msg=None): - while voice.is_playing() or voice.is_paused(): - await asyncio.sleep(5) - if msg: - await msg.delete() - if queues[id] != [] and not (voice.is_playing() or voice.is_paused()): - current = queues[id].pop(0) - player[ctx.guild.id] = current - - embed=discord.Embed(title="Currently Playing", description=f'[{player[id]["title"]}]({player[id]["url"]})', color=0xfe4b81) - embed.set_thumbnail(url=player[id]["thumbnails"][len(player[id]["thumbnails"])-1]["url"]) - - voice.play(FFmpegPCMAudio(player[id]["link"], **FFMPEG_OPTIONS)) - msg = await ctx.send(embed=embed) - await asyncio.sleep(1) - - # if anyhow system fails to play the audio it tries to play it again - while not(voice.is_playing() or voice.is_paused()): - with YoutubeDL(YDL_OPTIONS) as ydl: - info = ydl.extract_info(player[id]["url"], download=False) - player[id]["link"] = info['url'] - player[id]["raw"] = info + while True: + while voice.is_playing() or voice.is_paused(): + await asyncio.sleep(5) + if msg: + await msg.delete() + if queues[id] != [] and not (voice.is_playing() or voice.is_paused()): + current = queues[id].pop(0) + player[ctx.guild.id] = current + + embed=discord.Embed(title="Currently Playing", description=f'[{player[id]["title"]}]({player[id]["url"]})', color=0xfe4b81) + embed.set_thumbnail(url=player[id]["thumbnails"][len(player[id]["thumbnails"])-1]["url"]) + voice.play(FFmpegPCMAudio(player[id]["link"], **FFMPEG_OPTIONS)) + msg = await ctx.send(embed=embed) await asyncio.sleep(1) - - await check_queue(id, voice, ctx, msg) - else: - player[ctx.guild.id] = {} + + # if anyhow system fails to play the audio it tries to play it again + while not(voice.is_playing() or voice.is_paused()): + info = await ydl_async(player[id]["url"], YDL_OPTIONS, False) + player[id]["link"] = info['url'] + voice.play(FFmpegPCMAudio(player[id]["link"], **FFMPEG_OPTIONS)) + await asyncio.sleep(1) + + else: + player[ctx.guild.id] = {} + break # a asyncronus function to get video details because normally some timeout issue occurs but this is slower async def addsongs(entries, ctx): - for song in entries: + for i, song in enumerate(entries): url = song["url"] try: - with YoutubeDL(YDL_OPTIONS) as ydl: - info = ydl.extract_info(url, download=False) + if i == 0: + with YoutubeDL(YDL_OPTIONS) as ydl: + info = ydl.extract_info(url, download=False) + else: + info = await ydl_async(url, YDL_OPTIONS, False) data = { "link": info['url'], "url": url, "title": info['title'], - "thumbnails": info["thumbnails"], - "raw": info + "thumbnails": info["thumbnails"] } queues[ctx.guild.id].append(data) except Exception as e: print(e) - await asyncio.sleep(2) embed=discord.Embed(title="Playlist items were added to queue", color=0xfe4b81) await ctx.send(embed=embed) @@ -101,205 +130,220 @@ async def addsongs(entries, ctx): @client.event async def on_ready(): print('Bot online') + initGuilds() -# command for bot to join the channel of the user, if the bot has already joined and is in a different channel, it will move to the channel the user is in -@client.command() -async def join(ctx): - if ctx.message.author.voice: - channel = ctx.message.author.voice.channel - voice = get(client.voice_clients, guild=ctx.guild) - if voice and voice.is_connected(): - if not masters[ctx.guild.id].voice or masters[ctx.guild.id].voice.channel != voice.channel or (not (voice.is_playing() or voice.is_paused()) and queues[ctx.guild.id] == []): - await voice.move_to(channel) - masters[ctx.guild.id] = ctx.message.author - else: - embed=discord.Embed(title="I am currently under use in your server", color=0xfe4b81) - await ctx.send(embed=embed, delete_after=10) +class BasicCommands(commands.Cog, name="Basic", description="This category of commands contains the basic functionalities of the bot such as joining a VC."): + def __init__(self, bot: commands.Bot): + self.bot = bot - else: - voice = await channel.connect() - queues[ctx.guild.id] = [] - masters[ctx.guild.id] = ctx.message.author - player[ctx.guild.id] = {} - else: - embed=discord.Embed(title="You are currently not connected to any voice channel", color=0xfe4b81) - await ctx.send(embed=embed, delete_after=10) + # command for bot to join the channel of the user, if the bot has already joined and is in a different channel, it will move to the channel the user is in + @commands.command(help='Makes the bot join the channel of the user, if the bot has already joined and is in a different channel it will move to the channel the user is in. Only if you are not trying to disturb someone') + async def join(self, ctx): + if ctx.message.author.voice: + channel = ctx.message.author.voice.channel + voice = get(self.bot.voice_clients, guild=ctx.guild) + if voice and voice.is_connected(): + if not masters[ctx.guild.id].voice or masters[ctx.guild.id].voice.channel != voice.channel or (not (voice.is_playing() or voice.is_paused()) and queues[ctx.guild.id] == []): + await voice.move_to(channel) + masters[ctx.guild.id] = ctx.message.author + else: + embed=discord.Embed(title="I am currently under use in your server", color=0xfe4b81) + await ctx.send(embed=embed, delete_after=10) + else: + voice = await channel.connect() + queues[ctx.guild.id] = [] + masters[ctx.guild.id] = ctx.message.author + player[ctx.guild.id] = {} + else: + embed=discord.Embed(title="You are currently not connected to any voice channel", color=0xfe4b81) + await ctx.send(embed=embed, delete_after=10) -@client.command(aliases=['s']) -async def search(ctx, *,keyw): - voice = get(client.voice_clients, guild=ctx.guild) - opts = { - "format": "bestaudio", - "quiet": True, - "noplaylist": True, - "skip_download": True, - 'forcetitle': True, - 'forceurl': True, - 'source_address': '0.0.0.0' - } + # leaves the vc on demand + @commands.command(help='Makes the bot leave the voice channel') + async def leave(self, ctx): + voice_client = get(self.bot.voice_clients, guild=ctx.guild) - with YoutubeDL(opts) as ydl: - songs = ydl.extract_info(f'ytsearch5:{keyw}') + if voice_client: + if not masters[ctx.guild.id].voice or masters[ctx.guild.id].voice.channel != voice_client.channel or (not (voice_client.is_playing() or voice_client.is_paused()) and queues[ctx.guild.id] == []): + if voice_client.is_playing(): + voice_client.stop() + player[ctx.guild.id] = {} + await voice_client.disconnect() + else: + embed=discord.Embed(title="You can't disturb anyone listening to a song", color=0xfe4b81) + await ctx.send(embed=embed, delete_after=10) + else: + embed=discord.Embed(title="I am currently not connected to a voice channel.", color=0xfe4b81) + await ctx.send(embed=embed, delete_after=7) - videos = songs["entries"] - try: - options = {'1️⃣': 0, '2️⃣': 1, '3️⃣': 2, '4️⃣': 3, '5️⃣': 4} - out = '' - for i, song in enumerate(videos): - out = f'{out}{i+1}. [{song["title"]}]({song["webpage_url"]})\n' +class PlayerCommands(commands.Cog, name="Player", description="This category of commands contains the playable functionalities of the bot. All of them can make bot join vc, play audio and queue audio."): + def __init__(self, bot: commands.Bot): + self.bot = bot - embed=discord.Embed(title="Search results", description=out, color=0xfe4b81) - emb = await ctx.send(embed=embed) - for option in options: - await emb.add_reaction(option) + @commands.command(aliases=['s'], help='Searches a query on YT and gives 5 results to choose from. Choosen one will be queued or played') + async def search(self, ctx, *, query): + voice = get(self.bot.voice_clients, guild=ctx.guild) - def check(reaction, user): - return reaction.message == emb and reaction.message.channel == ctx.channel and user == ctx.author - - react, user = await client.wait_for('reaction_add', check=check, timeout=30.0) - info = videos[options[react.emoji]] + opts = { + "format": "bestaudio", + "quiet": True, + "noplaylist": True, + "skip_download": True, + 'forcetitle': True, + 'forceurl': True, + 'source_address': '0.0.0.0', + "cookiefile": "yt_cookies.txt" + } - if voice: - if not masters[ctx.guild.id].voice or masters[ctx.guild.id].voice.channel != voice.channel or (not (voice.is_playing() or voice.is_paused()) and queues[ctx.guild.id] == []): - masters[ctx.guild.id] = ctx.message.author - # check if the bot is already playing - if not (voice.is_playing() or voice.is_paused()) and queues[ctx.guild.id] == []: - data = { - "link": info['url'], - "url": info['webpage_url'], - "title": info['title'], - "thumbnails": info["thumbnails"], - "raw": info - } - queues[ctx.guild.id].append(data) - await check_queue(ctx.guild.id, voice, ctx) + songs = await ydl_async(f'ytsearch5:{query}', opts, False) - else: - data = { - "link": info['url'], - "url": info['webpage_url'], - "title": info['title'], - "thumbnails": info["thumbnails"], - "raw": info - } - queues[ctx.guild.id].append(data) - embed=discord.Embed(title="Item queued", description=f'[{info["title"]}]({data["url"]})', color=0xfe4b81) - embed.set_thumbnail(url=info["thumbnails"][len(info["thumbnails"])-1]["url"]) - await ctx.send(embed=embed) - else: - if ctx.message.author.voice: - channel = ctx.message.author.voice.channel - voice = await channel.connect() - masters[ctx.guild.id] = ctx.message.author - queues[ctx.guild.id] = [] - player[ctx.guild.id] = {} - data = { - "link": info['url'], - "url": info['webpage_url'], - "title": info['title'], - "thumbnails": info["thumbnails"], - "raw": info - } - queues[ctx.guild.id].append(data) - await check_queue(ctx.guild.id, voice, ctx) - else: - embed=discord.Embed(title="You are currently not connected to any voice channel", color=0xfe4b81) - await ctx.send(embed=embed, delete_after=10) - except asyncio.TimeoutError: - # await emb.delete() - pass - except Exception as e: - embed=discord.Embed(title="can't play the requested audio", color=0xfe4b81) - await ctx.send(embed=embed, delete_after=10) - raise e + videos = songs["entries"] + try: + out = '' + for i, song in enumerate(videos): + out = f'{out}{i+1}. [{song["title"]}]({song["webpage_url"]})\n' + + embed=discord.Embed(title="Search results", description=out, color=0xfe4b81) + user_choice = await paginator.handleOptions(embed=embed, nops=len(videos), ctx=ctx) + info = videos[user_choice] + + if voice: + if not masters[ctx.guild.id].voice or masters[ctx.guild.id].voice.channel != voice.channel or (not (voice.is_playing() or voice.is_paused()) and queues[ctx.guild.id] == []): + masters[ctx.guild.id] = ctx.message.author + # check if the bot is already playing + if not (voice.is_playing() or voice.is_paused()) and queues[ctx.guild.id] == []: + data = { + "link": info['url'], + "url": info['webpage_url'], + "title": info['title'], + "thumbnails": info["thumbnails"] + } + queues[ctx.guild.id].append(data) + await check_queue(ctx.guild.id, voice, ctx) + else: + data = { + "link": info['url'], + "url": info['webpage_url'], + "title": info['title'], + "thumbnails": info["thumbnails"] + } + queues[ctx.guild.id].append(data) + embed=discord.Embed(title="Item queued", description=f'[{info["title"]}]({data["url"]})', color=0xfe4b81) + embed.set_thumbnail(url=info["thumbnails"][len(info["thumbnails"])-1]["url"]) + await ctx.send(embed=embed) + else: + if ctx.message.author.voice: + channel = ctx.message.author.voice.channel + voice = await channel.connect() + masters[ctx.guild.id] = ctx.message.author + queues[ctx.guild.id] = [] + player[ctx.guild.id] = {} + data = { + "link": info['url'], + "url": info['webpage_url'], + "title": info['title'], + "thumbnails": info["thumbnails"] + } + queues[ctx.guild.id].append(data) + await check_queue(ctx.guild.id, voice, ctx) + else: + embed=discord.Embed(title="You are currently not connected to any voice channel", color=0xfe4b81) + await ctx.send(embed=embed, delete_after=10) + except asyncio.TimeoutError: + # await emb.delete() + pass + except Exception as e: + embed=discord.Embed(title="can't play the requested audio", color=0xfe4b81) + await ctx.send(embed=embed, delete_after=10) + raise e -# command to play sound from a keyword and queue a song if request is made during playing an audio -@client.command(aliases=['p']) -async def play(ctx, *,keyw): - html = urllib.request.urlopen("https://www.youtube.com/results?search_query=" + keyw.replace(" ", "+")) - video_ids = re.findall(r"watch\?v=(\S{11})", html.read().decode()) - url = "https://www.youtube.com/watch?v=" + video_ids[0] - voice = get(client.voice_clients, guild=ctx.guild) - try: - with YoutubeDL(YDL_OPTIONS) as ydl: - info = ydl.extract_info(url, download=False) + # command to play sound from a keyword and queue a song if request is made during playing an audio + @commands.command(aliases=['p'], help='Searches a query on YT and plays or queues the most relevant result') + async def play(self, ctx, *, query): + voice = get(self.bot.voice_clients, guild=ctx.guild) - if voice: - if not masters[ctx.guild.id].voice or masters[ctx.guild.id].voice.channel != voice.channel or (not (voice.is_playing() or voice.is_paused()) and queues[ctx.guild.id] == []): - masters[ctx.guild.id] = ctx.message.author - # check if the bot is already playing - if not (voice.is_playing() or voice.is_paused()) and queues[ctx.guild.id] == []: - data = { - "link": info['url'], - "url": url, - "title": info['title'], - "thumbnails": info["thumbnails"], - "raw": info - } - queues[ctx.guild.id].append(data) - await check_queue(ctx.guild.id, voice, ctx) + try: + info = await ydl_async(f'ytsearch:{query}', YDL_OPTIONS, False) + info = info['entries'][0] + url = info['webpage_url'] + + if voice: + if not masters[ctx.guild.id].voice or masters[ctx.guild.id].voice.channel != voice.channel or (not (voice.is_playing() or voice.is_paused()) and queues[ctx.guild.id] == []): + masters[ctx.guild.id] = ctx.message.author + # check if the bot is already playing + if not (voice.is_playing() or voice.is_paused()) and queues[ctx.guild.id] == []: + data = { + "link": info['url'], + "url": url, + "title": info['title'], + "thumbnails": info["thumbnails"] + } + queues[ctx.guild.id].append(data) + await check_queue(ctx.guild.id, voice, ctx) - else: - data = { - "link": info['url'], - "url": url, - "title": info['title'], - "thumbnails": info["thumbnails"], - "raw": info - } - queues[ctx.guild.id].append(data) - embed=discord.Embed(title="Item queued", description=f'[{info["title"]}]({url})', color=0xfe4b81) - embed.set_thumbnail(url=info["thumbnails"][len(info["thumbnails"])-1]["url"]) - await ctx.send(embed=embed) - else: - if ctx.message.author.voice: - channel = ctx.message.author.voice.channel - voice = await channel.connect() - masters[ctx.guild.id] = ctx.message.author - queues[ctx.guild.id] = [] - player[ctx.guild.id] = {} - data = { - "link": info['url'], - "url": url, - "title": info['title'], - "thumbnails": info["thumbnails"], - "raw": info - } - queues[ctx.guild.id].append(data) - await check_queue(ctx.guild.id, voice, ctx) - else: - embed=discord.Embed(title="You are currently not connected to any voice channel", color=0xfe4b81) - await ctx.send(embed=embed, delete_after=10) - except Exception as e: - embed=discord.Embed(title="can't play the requested audio", color=0xfe4b81) - await ctx.send(embed=embed, delete_after=10) - raise e + else: + data = { + "link": info['url'], + "url": url, + "title": info['title'], + "thumbnails": info["thumbnails"] + } + queues[ctx.guild.id].append(data) + embed=discord.Embed(title="Item queued", description=f'[{info["title"]}]({url})', color=0xfe4b81) + embed.set_thumbnail(url=info["thumbnails"][len(info["thumbnails"])-1]["url"]) + await ctx.send(embed=embed) + else: + if ctx.message.author.voice: + channel = ctx.message.author.voice.channel + voice = await channel.connect() + masters[ctx.guild.id] = ctx.message.author + queues[ctx.guild.id] = [] + player[ctx.guild.id] = {} + data = { + "link": info['url'], + "url": url, + "title": info['title'], + "thumbnails": info["thumbnails"] + } + queues[ctx.guild.id].append(data) + await check_queue(ctx.guild.id, voice, ctx) + else: + embed=discord.Embed(title="You are currently not connected to any voice channel", color=0xfe4b81) + await ctx.send(embed=embed, delete_after=10) + except Exception as e: + embed=discord.Embed(title="can't play the requested audio", color=0xfe4b81) + await ctx.send(embed=embed, delete_after=10) + raise e -@client.command(aliases=['l']) -async def live(ctx, url=None): - opts = { - 'format': 'bestaudio/best', - 'noplaylist': True, - 'source_address': '0.0.0.0' - } - voice = get(client.voice_clients, guild=ctx.guild) + @commands.command(aliases=['l'], help='Plays a YT live from the URL provided as input') + async def live(self, ctx, url=None): + opts = { + 'format': 'bestaudio/best', + 'noplaylist': True, + 'source_address': '0.0.0.0', + "cookiefile": "yt_cookies.txt" + } + voice = get(self.bot.voice_clients, guild=ctx.guild) + + info = await ydl_async(url, opts, False) + if not info.get('is_live'): + embed=discord.Embed(title="The link is not of a live video", color=0xfe4b81) + await ctx.send(embed=embed, delete_after=10) + return - if url: - with YoutubeDL(opts) as ydl: - info = ydl.extract_info(url, download=False) if voice: if not masters[ctx.guild.id].voice or masters[ctx.guild.id].voice.channel != voice.channel or (not (voice.is_playing() or voice.is_paused()) and queues[ctx.guild.id] == []): masters[ctx.guild.id] = ctx.message.author @@ -336,415 +380,412 @@ async def live(ctx, url=None): -# shows the queued songs of the ctx guild -@client.command(name="queue") -async def listQueue(ctx, limit=10): - out = "" - pages = [] - npages = 1 - voice = get(client.voice_clients, guild=ctx.guild) - - - if voice and not queues[ctx.guild.id] == []: - if len(queues[ctx.guild.id])%limit == 0 and len(queues[ctx.guild.id]) != 0: - npages = int(len(queues[ctx.guild.id])/limit) - else: - npages = int(len(queues[ctx.guild.id])/limit) + 1 - paginator = EmbedPaginator(ctx) - paginator.add_reaction('⏮️', "first") - paginator.add_reaction('⏪', "back") - paginator.add_reaction('⏩', "next") - paginator.add_reaction('⏭️', "last") - - i = 0 - p = 1 - for j, song in enumerate(queues[ctx.guild.id]): - if i < limit: - out = out + str(j+1) + f'. [{song["title"]}]({song["url"]})\n' - i = i + 1 - else: - out = out + f'\n**Page {p}/{npages}**' - embed=discord.Embed(title="Currently in queue", description=out, color=0xfe4b81) - pages.append(embed) - out = str(j+1) + f'. [{song["title"]}]({song["url"]})\n' - i = 1 - p = p + 1 - out = out + f'\n**Page {p}/{npages}**' - embed=discord.Embed(title="Currently in queue", description=out, color=0xfe4b81) - pages.append(embed) - - await paginator.run(pages) - - else: - out = "None" - embed=discord.Embed(title="Currently in queue", description=out, color=0xfe4b81) - await ctx.send(embed=embed) - - + @commands.command(name="add-playlist", help='Adds a whole YT or Spotify playlist to queue and starts playing it. Input is taken as the URL to the public or unlisted playlist. You can also mention a starting point or an ending point of the playlist or both') + async def addPlaylist(self, ctx, url: str, sp: int = None, ep: int = None): + voice = get(self.bot.voice_clients, guild=ctx.guild) -@client.command() -async def lyrics(ctx, index=0): - out = "" + source = None + if 'youtube' in url or 'youtu.be' in url: + p_id = re.search(r'^.*?(?:list)=(.*?)(?:&|$)', url).groups() + if p_id: + link = "https://www.youtube.com/playlist?list=" + p_id[0] + source = 'youtube' + elif 'spotify' in url: + source = 'spotify' + spotify_tracks = await spotify_api.getPlaylist(url, ctx, sp, ep) + + if not source: + embed=discord.Embed(title="Invalid link", color=0xfe4b81) + await ctx.send(embed=embed) + return - if player[ctx.guild.id]: try: - lyric = lyrics_api.get_lyrics(player[ctx.guild.id]['title'])['lyrics'] - except LyricScraperException as e: - try: - if int(e.args[0]["error"]["code"]) == 429: - lyric = "Daily quota exceeded" + if source == 'youtube': + opts = { + "extract_flat": True, + "source_address": "0.0.0.0", + "cookiefile": "yt_cookies.txt" + } + info = await ydl_async(link, opts, False) + info["entries"] = info["entries"][sp:ep] + + if sp or ep: + if ep: + embed=discord.Embed(title="Adding Playlist", description=f'[{info["title"]}]({link})\n\n**From {sp+1} to {ep}**', color=0xfe4b81) + else: + embed=discord.Embed(title="Adding Playlist", description=f'[{info["title"]}]({link})\n\n**From {sp+1} to {len(info["entries"])+sp}**', color=0xfe4b81) else: - lyric = "Something went wrong" - print(e.args[0]["error"]) - except: - lyric = "Something went wrong" - print(e) - - out = f'**{player[ctx.guild.id]["title"]}**\n\n{lyric}' - if len(lyric) > 50: - out = f'{out}\n\n**Lyrics provided by [genius.com](https://genius.com/)**' - embed=discord.Embed(title="Lyrics", description=out, color=0xfe4b81) - else: - embed=discord.Embed(title="Nothing currently in the player", color=0xfe4b81) - await ctx.send(embed=embed) - - - -# removes a mentioned song from queue and displays it -@client.command(name="remove") -async def removeQueueSong(ctx, index: int): - voice = get(client.voice_clients, guild=ctx.guild) - - if voice and (index<=len(queues[ctx.guild.id]) and index>0): - if ctx.guild.id in queuelocks.keys() and queuelocks[ctx.guild.id]["lock"] and queuelocks[ctx.guild.id]["author"].voice and queuelocks[ctx.guild.id]["author"].voice.channel == voice.channel and not (not (voice.is_playing() or voice.is_paused()) and queues[ctx.guild.id] == []): - if queuelocks[ctx.guild.id]["author"] == ctx.message.author: - rem = queues[ctx.guild.id].pop(index-1) - embed=discord.Embed(title="Removed from queue", description=f'[{rem["title"]}]({rem["url"]})', color=0xfe4b81) + embed=discord.Embed(title="Adding Playlist", description=f'[{info["title"]}]({link})', color=0xfe4b81) + + if voice: + if not masters[ctx.guild.id].voice or masters[ctx.guild.id].voice.channel != voice.channel or (not (voice.is_playing() or voice.is_paused()) and queues[ctx.guild.id] == []): + masters[ctx.guild.id] = ctx.message.author + # check if the bot is already playing + if not (voice.is_playing() or voice.is_paused()) and queues[ctx.guild.id] == []: + if source == 'youtube': + await ctx.send(embed=embed) + coros = [] + coros.append(addsongs(info["entries"], ctx)) + coros.append(check_queue(ctx.guild.id, voice, ctx)) + asyncio.gather(*coros) + elif source == 'spotify': + coros = [] + coros.append(spotify_api.addSongs(spotify_tracks, queues[ctx.guild.id], ctx)) + coros.append(check_queue(ctx.guild.id, voice, ctx)) + asyncio.gather(*coros) + else: + if source == 'youtube': + await ctx.send(embed=embed) + await addsongs(info["entries"], ctx) + elif source == 'spotify': + await spotify_api.addSongs(spotify_tracks, queues[ctx.guild.id], ctx) + else: + if ctx.message.author.voice: + channel = ctx.message.author.voice.channel + voice = await channel.connect() + masters[ctx.guild.id] = ctx.message.author + queues[ctx.guild.id] = [] + player[ctx.guild.id] = {} + if source == 'youtube': + await ctx.send(embed=embed) + coros = [] + coros.append(addsongs(info["entries"], ctx)) + coros.append(check_queue(ctx.guild.id, voice, ctx)) + asyncio.gather(*coros) + elif source == 'spotify': + coros = [] + coros.append(spotify_api.addSongs(spotify_tracks, queues[ctx.guild.id], ctx)) + coros.append(check_queue(ctx.guild.id, voice, ctx)) + asyncio.gather(*coros) + else: + embed=discord.Embed(title="You are currently not connected to any voice channel", color=0xfe4b81) + await ctx.send(embed=embed, delete_after=10) + except utils.ExtractorError as e: + if "ERROR: The playlist does not exist." in e: + embed=discord.Embed(title="Such a playlist does not exist", color=0xfe4b81) else: - embed=discord.Embed(title="The queue is currently locked", color=0xfe4b81) - else: - queuelocks[ctx.guild.id] = {} - queuelocks[ctx.guild.id]["lock"] = False - rem = queues[ctx.guild.id].pop(index-1) - embed=discord.Embed(title="Removed from queue", description=f'[{rem["title"]}]({rem["url"]})', color=0xfe4b81) - else: - embed=discord.Embed(title="Invalid request", color=0xfe4b81) - await ctx.send(embed=embed) + embed=discord.Embed(title="can't queue the requested playlist", color=0xfe4b81) + await ctx.send(embed=embed, delete_after=10) + except RuntimeError as e: + print(e) + except Exception as e: + embed=discord.Embed(title="can't queue the requested playlist", color=0xfe4b81) + await ctx.send(embed=embed, delete_after=10) + raise e -@client.command(name="add-playlist") -async def addPlaylist(ctx, link: str, sp: int = None, ep: int = None): - voice = get(client.voice_clients, guild=ctx.guild) +class VisualizerCommands(commands.Cog, name="Visualizer", description="This category of commands contains the visualizers which enables you to monitor some states of the bot or get some kind of info about something."): + def __init__(self, bot: commands.Bot): + self.bot = bot - # user link formatting - if link.split("?")[0] == "https://www.youtube.com/watch": - id_frt = link.split("?")[1].split("&")[1] # list=PL9bw4S5ePsEEqCMJSiYZ-KTtEjzVy0YvK - link = "https://www.youtube.com/playlist?" + id_frt - elif link.split("?")[0] == "https://www.youtube.com/playlist": - pass - else: - # promt with invalid link - embed=discord.Embed(title="Invalid link", color=0xfe4b81) - await ctx.send(embed=embed) - return - try: - opts = { - "extract_flat": True, - "source_address": "0.0.0.0" - } - with YoutubeDL(opts) as ydl: - info = ydl.extract_info(link, download=False) - - # Entry slicing - info["entries"] = info["entries"][sp:ep] + # shows the queued songs of the ctx guild + @commands.command(name="queue", help='Displays the current queue. Limit is the number of entries shown per page, default is 10') + async def listQueue(self, ctx, limit=10): + out = "" + pages = [] + npages = 1 + voice = get(self.bot.voice_clients, guild=ctx.guild) - if sp or ep: - if ep: - embed=discord.Embed(title="Adding Playlist", description=f'[{info["title"]}]({link})\n\n**From {sp+1} to {ep}**', color=0xfe4b81) + if voice and not queues[ctx.guild.id] == []: + if len(queues[ctx.guild.id])%limit == 0 and len(queues[ctx.guild.id]) != 0: + npages = int(len(queues[ctx.guild.id])/limit) else: - embed=discord.Embed(title="Adding Playlist", description=f'[{info["title"]}]({link})\n\n**From {sp+1} to {len(info["entries"])+sp}**', color=0xfe4b81) - else: - embed=discord.Embed(title="Adding Playlist", description=f'[{info["title"]}]({link})', color=0xfe4b81) + npages = int(len(queues[ctx.guild.id])/limit) + 1 + i = 0 + p = 1 + for j, song in enumerate(queues[ctx.guild.id]): + if i < limit: + out = out + str(j+1) + f'. [{song["title"]}]({song["url"]})\n' + i = i + 1 + else: + out = out + f'\n**Page {p}/{npages}**' + embed=discord.Embed(title="Currently in queue", description=out, color=0xfe4b81) + pages.append(embed) + out = str(j+1) + f'. [{song["title"]}]({song["url"]})\n' + i = 1 + p = p + 1 + out = out + f'\n**Page {p}/{npages}**' + embed=discord.Embed(title="Currently in queue", description=out, color=0xfe4b81) + pages.append(embed) + + await paginator.handlePages(pages, ctx) - if voice: - if not masters[ctx.guild.id].voice or masters[ctx.guild.id].voice.channel != voice.channel or (not (voice.is_playing() or voice.is_paused()) and queues[ctx.guild.id] == []): - masters[ctx.guild.id] = ctx.message.author - # check if the bot is already playing - if not (voice.is_playing() or voice.is_paused()) and queues[ctx.guild.id] == []: - await ctx.send(embed=embed) - loop = asyncio.get_event_loop() - coros = [] - coros.append(addsongs(info["entries"], ctx)) - coros.append(check_queue(ctx.guild.id, voice, ctx)) - loop.run_until_complete(asyncio.gather(*coros)) - else: - await ctx.send(embed=embed) - await addsongs(info["entries"], ctx) - else: - if ctx.message.author.voice: - channel = ctx.message.author.voice.channel - voice = await channel.connect() - masters[ctx.guild.id] = ctx.message.author - queues[ctx.guild.id] = [] - player[ctx.guild.id] = {} - await ctx.send(embed=embed) - loop = asyncio.get_event_loop() - coros = [] - coros.append(addsongs(info["entries"], ctx)) - coros.append(check_queue(ctx.guild.id, voice, ctx)) - loop.run_until_complete(asyncio.gather(*coros)) - else: - embed=discord.Embed(title="You are currently not connected to any voice channel", color=0xfe4b81) - await ctx.send(embed=embed, delete_after=10) - except utils.ExtractorError as e: - if "ERROR: The playlist does not exist." in e: - embed=discord.Embed(title="Such a playlist does not exist", color=0xfe4b81) else: - embed=discord.Embed(title="can't queue the requested playlist", color=0xfe4b81) - await ctx.send(embed=embed, delete_after=10) - except RuntimeError as e: - print(e) - except Exception as e: - embed=discord.Embed(title="can't queue the requested playlist", color=0xfe4b81) - await ctx.send(embed=embed, delete_after=10) - raise e - - - -# command to resume voice if it is paused -@client.command() -async def resume(ctx): - voice = get(client.voice_clients, guild=ctx.guild) - embed=discord.Embed(title="Resuming...", color=0xfe4b81) + out = "None" + embed=discord.Embed(title="Currently in queue", description=out, color=0xfe4b81) + await ctx.send(embed=embed) - if voice: - if ctx.guild.id in queuelocks.keys() and queuelocks[ctx.guild.id]["lock"] and queuelocks[ctx.guild.id]["author"].voice and queuelocks[ctx.guild.id]["author"].voice.channel == voice.channel and not (not (voice.is_playing() or voice.is_paused()) and queues[ctx.guild.id] == []): - if queuelocks[ctx.guild.id]["author"] == ctx.message.author: - if not voice.is_playing(): - voice.resume() - await ctx.send(embed=embed) - else: - embed=discord.Embed(title="The queue is currently locked", color=0xfe4b81) - await ctx.send(embed=embed) - else: - queuelocks[ctx.guild.id] = {} - queuelocks[ctx.guild.id]["lock"] = False - if not voice.is_playing(): - voice.resume() - await ctx.send(embed=embed) - else: - embed=discord.Embed(title="I am currently not connected to any voice channel", color=0xfe4b81) - await ctx.send(embed=embed, delete_after=7) + @commands.command(help='Displays the lyrics of the current song if available') + async def lyrics(self, ctx, index=0): + out = "" -# command to pause voice if it is playing -@client.command() -async def pause(ctx): - voice = get(client.voice_clients, guild=ctx.guild) - embed=discord.Embed(title="Pausing...", color=0xfe4b81) + if player[ctx.guild.id]: + try: + lyric = lyrics_api.get_lyrics(player[ctx.guild.id]['title'])['lyrics'] + except LyricScraperException as e: + try: + if int(e.args[0]["error"]["code"]) == 429: + lyric = "Daily quota exceeded" + else: + lyric = "Something went wrong" + print(e.args[0]["error"]) + except: + lyric = "Something went wrong" + print(e) - if voice: - if ctx.guild.id in queuelocks.keys() and queuelocks[ctx.guild.id]["lock"] and queuelocks[ctx.guild.id]["author"].voice and queuelocks[ctx.guild.id]["author"].voice.channel == voice.channel and not (not (voice.is_playing() or voice.is_paused()) and queues[ctx.guild.id] == []): - if queuelocks[ctx.guild.id]["author"] == ctx.message.author: - if voice.is_playing(): - voice.pause() - await ctx.send(embed=embed) - else: - embed=discord.Embed(title="The queue is currently locked", color=0xfe4b81) - await ctx.send(embed=embed) + out = f'**{player[ctx.guild.id]["title"]}**\n\n{lyric}' + if len(lyric) > 50: + out = f'{out}\n\n**Lyrics provided by [genius.com](https://genius.com/)**' + embed=discord.Embed(title="Lyrics", description=out, color=0xfe4b81) else: - queuelocks[ctx.guild.id] = {} - queuelocks[ctx.guild.id]["lock"] = False - if voice.is_playing(): - voice.pause() - await ctx.send(embed=embed) - else: - embed=discord.Embed(title="I am currently not connected to any voice channel", color=0xfe4b81) - await ctx.send(embed=embed, delete_after=7) + embed=discord.Embed(title="Nothing currently in the player", color=0xfe4b81) + await ctx.send(embed=embed) -# command to skip voice -@client.command() -async def skip(ctx): - voice = get(client.voice_clients, guild=ctx.guild) - embed=discord.Embed(title="Skipping...", color=0xfe4b81) - if voice: - if ctx.guild.id in queuelocks.keys() and queuelocks[ctx.guild.id]["lock"] and queuelocks[ctx.guild.id]["author"].voice and queuelocks[ctx.guild.id]["author"].voice.channel == voice.channel and not (not (voice.is_playing() or voice.is_paused()) and queues[ctx.guild.id] == []): - if queuelocks[ctx.guild.id]["author"] == ctx.message.author: - if voice.is_playing(): - voice.stop() - await ctx.send(embed=embed) - else: - embed=discord.Embed(title="The queue is currently locked", color=0xfe4b81) - await ctx.send(embed=embed) + @commands.command(name='current', aliases=['c'], help='Displays information about the current song in the player') + async def currentlyPlaying(self, ctx): + if player[ctx.guild.id]: + embed=discord.Embed(title="Currently in the Player", description=f'[{player[ctx.guild.id]["title"]}]({player[ctx.guild.id]["url"]})', color=0xfe4b81) + embed.set_thumbnail(url=player[ctx.guild.id]["thumbnails"][len(player[ctx.guild.id]["thumbnails"])-1]["url"]) else: - queuelocks[ctx.guild.id] = {} - queuelocks[ctx.guild.id]["lock"] = False - if voice.is_playing(): - voice.stop() - await ctx.send(embed=embed) - else: - embed=discord.Embed(title="I am currently not connected to any voice channel", color=0xfe4b81) - await ctx.send(embed=embed, delete_after=7) - - - -# stops the bot player by clearing the current queue and skipping the current audio -@client.command() -async def stop(ctx): - voice = get(client.voice_clients, guild=ctx.guild) - embed=discord.Embed(title="Stopping...", color=0xfe4b81) + embed=discord.Embed(title="Nothing currently in the player", color=0xfe4b81) + await ctx.send(embed=embed, delete_after=10) - if voice: - if ctx.guild.id in queuelocks.keys() and queuelocks[ctx.guild.id]["lock"] and queuelocks[ctx.guild.id]["author"].voice and queuelocks[ctx.guild.id]["author"].voice.channel == voice.channel and not (not (voice.is_playing() or voice.is_paused()) and queues[ctx.guild.id] == []): - embed=discord.Embed(title="The queue is currently locked", color=0xfe4b81) - await ctx.send(embed=embed) - else: - queues[ctx.guild.id] = [] - queuelocks[ctx.guild.id] = {} - queuelocks[ctx.guild.id]["lock"] = False - if voice.is_playing(): - voice.stop() - await ctx.send(embed=embed) - else: - embed=discord.Embed(title="I am currently not connected to any voice channel", color=0xfe4b81) - await ctx.send(embed=embed, delete_after=7) +class QueueCommands(commands.Cog, name="Queue", description="This category of commands contains the commands related to queues. Also there is a concept of queue lock which will dissable any user from using these commands except the user initiating the lock with some more exceptions."): + def __init__(self, bot: commands.Bot): + self.bot = bot -# leaves the vc on demand -@client.command(name='leave', help='To make the bot leave the voice channel') -async def leave(ctx): - voice_client = get(client.voice_clients, guild=ctx.guild) - if voice_client: - if not masters[ctx.guild.id].voice or masters[ctx.guild.id].voice.channel != voice_client.channel or (not (voice_client.is_playing() or voice_client.is_paused()) and queues[ctx.guild.id] == []): - if voice_client.is_playing(): - voice_client.stop() - player[ctx.guild.id] = {} - await voice_client.disconnect() + # removes a mentioned song from queue and displays it + @commands.command(name="remove", help='Removes an mentioned entry from the queue') + @checkQueueLock(check_if_bot_connected=True) + async def removeQueueSong(self, ctx, index: int): + if (index<=len(queues[ctx.guild.id]) and index>0): + rem = queues[ctx.guild.id].pop(index-1) + embed=discord.Embed(title="Removed from queue", description=f'[{rem["title"]}]({rem["url"]})', color=0xfe4b81) else: - embed=discord.Embed(title="You can't disturb anyone listening to a song", color=0xfe4b81) - await ctx.send(embed=embed, delete_after=10) - else: - embed=discord.Embed(title="I am currently not connected to a voice channel.", color=0xfe4b81) - await ctx.send(embed=embed, delete_after=7) - - + embed=discord.Embed(title="Invalid request", color=0xfe4b81) + await ctx.send(embed=embed) -# command to clear queue -@client.command(name="clear-queue", aliases=["clear"]) -async def clearQueue(ctx): - voice = get(client.voice_clients, guild=ctx.guild) - # embed=discord.Embed(title="Stopping...", color=0xfe4b81) - options = ["👍", "🚫"] - if voice: - if ctx.guild.id in queuelocks.keys() and queuelocks[ctx.guild.id]["lock"] and queuelocks[ctx.guild.id]["author"].voice and queuelocks[ctx.guild.id]["author"].voice.channel == voice.channel and not (not (voice.is_playing() or voice.is_paused()) and queues[ctx.guild.id] == []): - embed=discord.Embed(title="The queue is currently locked", color=0xfe4b81) - await ctx.send(embed=embed) - else: - queuelocks[ctx.guild.id] = {} - queuelocks[ctx.guild.id]["lock"] = False - embed=discord.Embed(title="Do you really want to clear the queue", color=0xfe4b81) - emb = await ctx.send(embed=embed) + # command to pause voice if it is playing + @commands.command(help='Pauses the current player') + @checkQueueLock(check_if_bot_connected=True) + async def pause(self, ctx): + voice = get(self.bot.voice_clients, guild=ctx.guild) + embed=discord.Embed(title="Pausing...", color=0xfe4b81) + if voice.is_playing(): + voice.pause() + await ctx.send(embed=embed, delete_after=7) + + + # command to resume voice if it is paused + @commands.command(help='Resumes the paused player') + @checkQueueLock(check_if_bot_connected=True) + async def resume(self, ctx): + voice = get(self.bot.voice_clients, guild=ctx.guild) + embed=discord.Embed(title="Resuming...", color=0xfe4b81) + if not voice.is_playing(): + voice.resume() + await ctx.send(embed=embed, delete_after=7) + + + # command to skip voice + @commands.command(help='Skips current audio') + @checkQueueLock(check_if_bot_connected=True) + async def skip(self, ctx): + voice = get(self.bot.voice_clients, guild=ctx.guild) + embed=discord.Embed(title="Skipping...", color=0xfe4b81) + if voice.is_playing(): + voice.stop() + await ctx.send(embed=embed, delete_after=7) + + + # stops the bot player by clearing the current queue and skipping the current audio + @commands.command(help='Just like skip but also clears the queue') + @checkQueueLock(hard=True, check_if_bot_connected=True) + async def stop(self, ctx): + voice = get(self.bot.voice_clients, guild=ctx.guild) + embed=discord.Embed(title="Stopping...", color=0xfe4b81) + queues[ctx.guild.id] = [] + if voice.is_playing(): + voice.stop() + await ctx.send(embed=embed, delete_after=7) + + + # command to clear queue + @commands.command(name="clear-queue", aliases=["clear"], help='Clears the queue') + @checkQueueLock(hard=True, check_if_bot_connected=True) + async def clearQueue(self, ctx): + embed=discord.Embed(title="Do you really want to clear the queue", color=0xfe4b81) + success_embed=discord.Embed(title="The queue has been cleared", color=0xfe4b81) + user_decision = await paginator.handleDecision(embed=embed, resp_embed=success_embed, ctx=ctx) + if user_decision: + queues[ctx.guild.id] = [] - try: - for option in options: - await emb.add_reaction(option) - - def chk(reaction, user): - return reaction.message == emb and reaction.message.channel == ctx.channel and user == ctx.author - - react, user = await client.wait_for('reaction_add', check=chk, timeout=30.0) - if react.emoji == "👍": - queues[ctx.guild.id] = [] - await emb.delete() - embed=discord.Embed(title="The queue has been cleared", color=0xfe4b81) - await ctx.send(embed=embed) - else: - await emb.delete() - except asyncio.TimeoutError: - await emb.delete() - else: - embed=discord.Embed(title="I am currently not connected to any voice channel", color=0xfe4b81) - await ctx.send(embed=embed, delete_after=7) + @commands.command(name="shuffle", help='Shuffles the whole queue') + @checkQueueLock(check_if_bot_connected=True) + async def shuffleQueue(self, ctx): + embed=discord.Embed(title="Do you really want to shuffle the queue", color=0xfe4b81) + success_embed=discord.Embed(title="The queue has been shuffled", color=0xfe4b81) + user_decision = await paginator.handleDecision(embed=embed, resp_embed=success_embed, ctx=ctx) + if user_decision: + shuffle(queues[ctx.guild.id]) -@client.command() -async def lock(ctx): - voice_client = get(client.voice_clients, guild=ctx.guild) - - if ctx.message.author.voice: - if voice_client: - if ctx.guild.id in queuelocks.keys() and queuelocks[ctx.guild.id]["lock"] and queuelocks[ctx.guild.id]["author"].voice : - if queuelocks[ctx.guild.id]["author"] == ctx.message.author or (not (voice_client.is_playing() or voice_client.is_paused()) and queues[ctx.guild.id] == []): - queuelocks[ctx.guild.id]["lock"] = False - embed=discord.Embed(title="Queue lock has been removed", color=0xfe4b81) - await ctx.send(embed=embed) + @commands.command(help='Locks the queue and prevents anyone from damaging anyone\'s experience') + async def lock(self, ctx): + voice_client = get(self.bot.voice_clients, guild=ctx.guild) + + if ctx.message.author.voice: + if voice_client: + if ctx.guild.id in queuelocks.keys() and queuelocks[ctx.guild.id]["lock"] and queuelocks[ctx.guild.id]["author"].voice : + if queuelocks[ctx.guild.id]["author"] == ctx.message.author or (not (voice_client.is_playing() or voice_client.is_paused()) and queues[ctx.guild.id] == []): + queuelocks[ctx.guild.id]["lock"] = False + embed=discord.Embed(title="Queue lock has been removed", color=0xfe4b81) + await ctx.send(embed=embed) + else: + embed=discord.Embed(title=f'{queuelocks[ctx.guild.id]["author"].display_name} has already locked the queue', color=0xfe4b81) + await ctx.send(embed=embed) else: - embed=discord.Embed(title=f'{queuelocks[ctx.guild.id]["author"].display_name} has already locked the queue', color=0xfe4b81) + queuelocks[ctx.guild.id] = {} + queuelocks[ctx.guild.id]["lock"] = True + queuelocks[ctx.guild.id]["author"] = ctx.message.author + embed=discord.Embed(title=f'{queuelocks[ctx.guild.id]["author"].display_name} has initiated queuelock', color=0xfe4b81) await ctx.send(embed=embed) else: - queuelocks[ctx.guild.id] = {} - queuelocks[ctx.guild.id]["lock"] = True - queuelocks[ctx.guild.id]["author"] = ctx.message.author - embed=discord.Embed(title=f'{queuelocks[ctx.guild.id]["author"].display_name} has initiated queuelock', color=0xfe4b81) - await ctx.send(embed=embed) + embed=discord.Embed(title="I am currently not connected to any voice channel", color=0xfe4b81) + await ctx.send(embed=embed, delete_after=10) else: - embed=discord.Embed(title="I am currently not connected to any voice channel", color=0xfe4b81) + embed=discord.Embed(title="You are currently not connected to any voice channel", color=0xfe4b81) await ctx.send(embed=embed, delete_after=10) - else: - embed=discord.Embed(title="You are currently not connected to any voice channel", color=0xfe4b81) - await ctx.send(embed=embed, delete_after=10) -@client.command(name='download', aliases=['d']) -async def dl_yt(ctx, url: str): - def check_url(url: str): - uw = url.split("://") - if uw[0] == 'https' or uw[0] == 'http': - uweb = uw[1].split('/')[0] - if 'youtube' in uweb or 'youtu.be' in uweb: - return 1 - elif 'instagram' in uweb: - return 2 +class DownloadCommands(commands.Cog, name="Download", description="This category of commands contains recently added download feature which can download YT and instagram audio video files with private support for instagram only."): + def __init__(self, bot: commands.Bot): + self.bot = bot + + + @commands.command(name='download', aliases=['d'], help='Downloads YT or instagram audio video files from the url. If the bot is already playing something then passing no input will result in selecting that video. Copt can be passed for choosing vcodec, default is 0 for h264 but can be set to 1 for codec provided by vendor') + @commands.max_concurrency(number=1, per=commands.BucketType.default, wait=False) + async def dl_yt(self, ctx, url: str = None, copt: int = 0): + def check_url(url: str): + if url: + uw = url.split("://") + if uw[0] == 'https' or uw[0] == 'http': + uweb = uw[1].split('/')[0] + if 'youtube' in uweb or 'youtu.be' in uweb: + return 1 + elif 'instagram' in uweb: + return 2 + else: + return 0 + else: + return 0 else: return 0 - else: - return 0 - - url_type: int = check_url(url) - voice = get(client.voice_clients, guild=ctx.guild) - - if voice: - # check if the bot is already playing - if not (voice.is_playing() or voice.is_paused()) and queues[ctx.guild.id] == []: - if url_type == 1: - await yt_dl_instance.downloadVideo(ctx, url) - elif url_type == 2: - await in_dl_instance.downloadVideo(ctx, url) - else: - embed=discord.Embed(title='The link is broken, can\'t fetch data', color=0xfe4b81) - await ctx.send(embed=embed, delete_after=15) - else: - embed=discord.Embed(title="Can\'t download while playing something", description="This restriction has been implemented in order to avoid throttling. If any problem still arises then kindly report it to the dev.", color=0xfe4b81) - await ctx.send(embed=embed, delete_after=15) - else: + + if not url and player[ctx.guild.id] != {}: + url = player[ctx.guild.id]['url'] + + url_type: int = check_url(url) if url_type == 1: - await yt_dl_instance.downloadVideo(ctx, url) + await yt_dl_instance.downloadVideo(ctx, url, copt) elif url_type == 2: - await in_dl_instance.downloadVideo(ctx, url) + usrcreds = private_instance.get_usercreds(ctx.author.id) + await in_dl_instance.downloadVideo(ctx, url, copt, usrcreds) else: embed=discord.Embed(title='The link is broken, can\'t fetch data', color=0xfe4b81) await ctx.send(embed=embed, delete_after=15) + @commands.command(help='Supports the instagram private feature. This command Logs in to your inastagram account and uses it to access files through your account. Once logged in use the download command normally. This command can only be used in DMs in order to protect your privacy') + async def login(self, ctx, usrn=None, passw=None): + if isinstance(ctx.channel, discord.DMChannel): + if usrn and passw: + await private_instance.login(ctx, usrn, passw) + else: + embed=discord.Embed(title='Hey use this command here', description='Login command can only be used from the DM. This helps us keep your credentials private.', color=0xfe4b81) + await ctx.author.send(embed=embed, delete_after=30) + + @commands.command(help='Logout of your account only if you are already logged in') + async def logout(self, ctx): + if private_instance.is_user_authenticated(ctx.author.id): + embed=discord.Embed(title="Do you really want to logout", color=0xfe4b81) + user_decision = await paginator.handleDecision(embed=embed, resp_embed=embed, ctx=ctx) + if user_decision: + await private_instance.logout(ctx) + + +class SpecialCommands(commands.Cog, name="Special", description="This category of commands contains the special commands which can only be accessed by the owner of the bot. These commands enables the owner to remotely invoke methods for temporary fixes or other debugging stuff."): + def __init__(self, bot: commands.Bot): + self.bot = bot + + async def cog_check(self, ctx): + if not await ctx.bot.is_owner(ctx.author): + raise commands.NotOwner('You do not own this bot.') + return True + + + @commands.command(help='Refetches the default cookie files') + async def refetch(self, ctx, id_insta=None, id_yt=None): + getCookieFile(id_insta, id_yt) + embed=discord.Embed(title="Default cookies were refetched and refreshed successfully", color=0xfe4b81) + await ctx.send(embed=embed, delete_after=20) + + @commands.command(name="add-cog", help='Adds a predefined cog to the bot') + async def addCog(self, ctx, cog_name): + if cog_name != 'Special': + for cog in cog_list: + if cog.qualified_name == cog_name and not self.bot.get_cog(cog_name): + self.bot.add_cog(cog) + embed=discord.Embed(title=f"{cog_name} cog was added successfully", color=0xfe4b81) + await ctx.send(embed=embed, delete_after=20) + + @commands.command(name="remove-cog", help='Removes a already existing cog. Generally used to disable a functionality of the bot') + async def removeCog(self, ctx, cog_name): + if cog_name != 'Special': + self.bot.remove_cog(cog_name) + embed=discord.Embed(title=f"{cog_name} cog was removed successfully", color=0xfe4b81) + await ctx.send(embed=embed, delete_after=20) + +cog_list = [] + +cog_list.append(BasicCommands(client)) +cog_list.append(PlayerCommands(client)) +cog_list.append(VisualizerCommands(client)) +cog_list.append(QueueCommands(client)) +cog_list.append(DownloadCommands(client)) +cog_list.append(SpecialCommands(client)) + +for cog in cog_list: + client.add_cog(cog) + +@client.event +async def on_command_error(ctx, error): + if isinstance(error, commands.MaxConcurrencyReached): + embed=discord.Embed(title="Please wait..", description="Someone else is currently using this feature please wait before trying again. This restriction has been implemented to prevent throttling as the bot is currently running on a free server.", color=0xfe4b81) + await ctx.send(embed=embed, delete_after=20) + elif isinstance(error, commands.NotOwner): + embed=discord.Embed(title="Access Denied", description=f"It is a special command and is reserved to the owner of the bot only. This types of commands enables the owner to remotely triggure some functions for ease of use. Read more about them from `{ctx.prefix}help Special`.", color=0xfe4b81) + await ctx.reply(embed=embed, delete_after=20) + elif isinstance(error, QueueLockCheckFailure): + embed=discord.Embed(title=error, color=0xfe4b81) + await ctx.send(embed=embed, delete_after=10) + elif isinstance(error, commands.CommandNotFound): + print(error) + elif isinstance(error, commands.errors.MissingRequiredArgument): + print(error) + else: + raise error -client.run(token) \ No newline at end of file +client.run(token) diff --git a/credentials.json b/credentials.json deleted file mode 100644 index c0847f9..0000000 --- a/credentials.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "token": "ODkwMTIyNDQ0NDE1ODI4MDA4.YUrNIA.fv-0c0NGnLVttSo0QtX2rPnak4E", - "search_engine": "17b579f4a7b2df68d", - "search_token": "AIzaSyC-tKzYSVDN-ZxqaArvEtS0NuRNhisIrVY" -} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 2f1ca25..ccfd8e0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,32 +1,6 @@ -aiohttp==3.7.4.post0 -async-timeout==3.0.1 -attrs==21.2.0 -beautifulsoup4==4.10.0 -Brotli==1.0.9 -certifi==2021.5.30 -cffi==1.14.6 -chardet==4.0.0 -charset-normalizer==2.0.6 -colorama==0.4.4 -discord.py==1.7.3 -discord.py-message-components==1.7.5.4 -DiscordUtils==1.3.4 -docopt==0.6.2 -idna==3.2 -lxml==4.6.3 +discord.py-message-components>=1.7.5.4 +gdown==4.4.0 lyrics-extractor==3.0.1 -multidict==5.1.0 -mutagen==1.45.1 -pycparser==2.20 -pycryptodomex==3.12.0 PyNaCl==1.4.0 -requests==2.26.0 -six==1.16.0 -soupsieve==2.2.1 -tqdm==4.63.0 -typing-extensions==3.10.0.2 -urllib3==1.26.7 -websockets==10.1 -yarg==0.1.9 -yarl==1.6.3 -yt-dlp==2022.3.8.2 +tekore==4.4.0 +yt-dlp>=2022.7.18 \ No newline at end of file diff --git a/userdata/aaaUserData.txt b/userdata/aaaUserData.txt new file mode 100644 index 0000000..47fd28a --- /dev/null +++ b/userdata/aaaUserData.txt @@ -0,0 +1 @@ +This folder consists of all the user coookie files for the new instagram private feature. Please don't upload their data on the next commit \ No newline at end of file