From 4c66c64041e2098e32637d1682def7bcbe892702 Mon Sep 17 00:00:00 2001 From: Rafael Vargas Date: Sat, 26 Mar 2022 17:42:49 -0400 Subject: [PATCH] Upgrading PlayController and Spotify Connection --- Controllers/PlayController.py | 49 +++++++++++++- Controllers/ShuffleController.py | 3 +- Exceptions/Exceptions.py | 10 +++ Music/Downloader.py | 9 ++- Music/Player.py | 66 +------------------ Music/Playlist.py | 3 +- Music/Searcher.py | 42 ++++++------ Music/Song.py | 6 +- Music/Spotify.py | 107 +++++++++++++------------------ Utils/Sender.py | 12 ++++ Utils/Utils.py | 27 ++++++++ Views/Embeds.py | 30 +++++++-- config/Messages.py | 10 +++ config/config.py | 1 + main.py | 1 - 15 files changed, 213 insertions(+), 163 deletions(-) create mode 100644 Utils/Sender.py diff --git a/Controllers/PlayController.py b/Controllers/PlayController.py index 6092f65..693bfea 100644 --- a/Controllers/PlayController.py +++ b/Controllers/PlayController.py @@ -1,13 +1,20 @@ +import asyncio +from Exceptions.Exceptions import DownloadingError, Error from discord.ext.commands import Context from discord import Client from Controllers.AbstractController import AbstractController from Exceptions.Exceptions import ImpossibleMove, UnknownError from Controllers.ControllerResponse import ControllerResponse +from Music.Downloader import Downloader +from Music.Searcher import Searcher +from Music.Song import Song class PlayController(AbstractController): def __init__(self, ctx: Context, bot: Client) -> None: super().__init__(ctx, bot) + self.__searcher = Searcher() + self.__down = Downloader() async def run(self, args: str) -> ControllerResponse: track = " ".join(args) @@ -25,7 +32,47 @@ class PlayController(AbstractController): embed = self.embeds.UNKNOWN_ERROR() return ControllerResponse(self.ctx, embed, error) - await self.player.play(self.ctx, track, requester) + try: + musics = await self.__searcher.search(track) + for music in musics: + song = Song(music, self.player.playlist, requester) + self.player.playlist.add_song(song) + quant = len(musics) + + songs_preload = self.player.playlist.songs_to_preload + await self.__down.preload(songs_preload) + + if quant == 1: + pos = len(self.player.playlist) + song = self.__down.finish_one_song(song) + if song.problematic: + embed = self.embeds.SONG_PROBLEMATIC() + error = DownloadingError() + response = ControllerResponse(self.ctx, embed, error) + + elif not self.player.playing: + embed = self.embeds.SONG_ADDED(song.title) + response = ControllerResponse(self.ctx, embed) + else: + embed = self.embeds.SONG_ADDED_TWO(song.info, pos) + response = ControllerResponse(self.ctx, embed) + else: + embed = self.embeds.SONGS_ADDED(quant) + response = ControllerResponse(self.ctx, embed) + + asyncio.create_task(self.player.play(self.ctx)) + return response + + except Exception as err: + if isinstance(err, Error): + print(f'DEVELOPER NOTE -> PlayController Error: {err.message}') + error = err + embed = self.embeds.CUSTOM_ERROR(error) + else: + error = UnknownError() + embed = self.embeds.UNKNOWN_ERROR() + + return ControllerResponse(self.ctx, embed, error) def __user_connected(self) -> bool: if self.ctx.author.voice: diff --git a/Controllers/ShuffleController.py b/Controllers/ShuffleController.py index c01195e..5ad3ac3 100644 --- a/Controllers/ShuffleController.py +++ b/Controllers/ShuffleController.py @@ -1,3 +1,4 @@ +import asyncio from discord.ext.commands import Context from discord import Client from Controllers.AbstractController import AbstractController @@ -16,7 +17,7 @@ class ShuffleController(AbstractController): self.player.playlist.shuffle() songs = self.player.playlist.songs_to_preload - await self.__down.preload(songs) + asyncio.create_task(self.__down.preload(songs)) embed = self.embeds.SONGS_SHUFFLED() return ControllerResponse(self.ctx, embed) except Exception as e: diff --git a/Exceptions/Exceptions.py b/Exceptions/Exceptions.py index c158cf8..80af102 100644 --- a/Exceptions/Exceptions.py +++ b/Exceptions/Exceptions.py @@ -34,6 +34,16 @@ class BadCommandUsage(Error): super().__init__(message, title, *args) +class DownloadingError(Error): + def __init__(self, message='', title='', *args: object) -> None: + super().__init__(message, title, *args) + + +class SpotifyError(Error): + def __init__(self, message='', title='', *args: object) -> None: + super().__init__(message, title, *args) + + class UnknownError(Error): def __init__(self, message='', title='', *args: object) -> None: super().__init__(message, title, *args) diff --git a/Music/Downloader.py b/Music/Downloader.py index 2ed871b..d96986d 100644 --- a/Music/Downloader.py +++ b/Music/Downloader.py @@ -1,5 +1,7 @@ import asyncio from typing import List + +from numpy import extract from Config.Config import Configs from yt_dlp import YoutubeDL from concurrent.futures import ThreadPoolExecutor @@ -25,7 +27,7 @@ class Downloader(): 'default_search': 'auto', 'playliststart': 0, 'extract_flat': False, - 'playlistend': config.MAX_PLAYLIST_LENGTH, + 'playlistend': config.MAX_PLAYLIST_FORCED_LENGTH, } __BASE_URL = 'https://www.youtube.com/watch?v={}' @@ -127,7 +129,10 @@ class Downloader(): extracted_info = ydl.extract_info(search, download=False) if self.__failed_to_extract(extracted_info): - self.__get_forced_extracted_info(extracted_info) + extracted_info = self.__get_forced_extracted_info(title) + + if extracted_info is None: + return {} if self.__is_multiple_musics(extracted_info): return extracted_info['entries'][0] diff --git a/Music/Player.py b/Music/Player.py index cd855f7..e2f2cc1 100644 --- a/Music/Player.py +++ b/Music/Player.py @@ -36,71 +36,7 @@ class Player(commands.Cog): def playlist(self) -> Playlist: return self.__playlist - async def play(self, ctx: Context, track: str, requester: str) -> str: - try: - links, provider = self.__searcher.search(track) - if provider == Provider.Unknown or links == None: - embed = Embed( - title=self.__config.ERROR_TITLE, - description=self.__config.INVALID_INPUT, - colours=self.__config.COLOURS['blue']) - await ctx.send(embed=embed) - return None - - if provider == Provider.YouTube: - links = await self.__down.extract_info(links[0]) - - if len(links) == 0: - embed = Embed( - title=self.__config.ERROR_TITLE, - description="This video is unavailable", - colours=self.__config.COLOURS['blue']) - await ctx.send(embed=embed) - return None - - songs_quant = 0 - for info in links: - song = self.__playlist.add_song(info, requester) - songs_quant += 1 - - songs_preload = self.__playlist.songs_to_preload - await self.__down.preload(songs_preload) - except Exception as e: - print(f'DEVELOPER NOTE -> Error while Downloading in Player: {e}') - embed = Embed( - title=self.__config.ERROR_TITLE, - description=self.__config.DOWNLOADING_ERROR, - colours=self.__config.COLOURS['blue']) - await ctx.send(embed=embed) - return - - if songs_quant == 1: - song = self.__down.finish_one_song(song) - pos = len(self.__playlist) - - if song.problematic: - embed = Embed( - title=self.__config.ERROR_TITLE, - description=self.__config.DOWNLOADING_ERROR, - colours=self.__config.COLOURS['blue']) - await ctx.send(embed=embed) - return None - elif not self.__playing: - embed = Embed( - title=self.__config.SONG_PLAYER, - description=self.__config.SONG_ADDED.format(song.title), - colour=self.__config.COLOURS['blue']) - await ctx.send(embed=embed) - else: - embed = self.__format_embed(song.info, self.__config.SONG_ADDED_TWO, pos) - await ctx.send(embed=embed) - else: - embed = Embed( - title=self.__config.SONG_PLAYER, - description=self.__config.SONGS_ADDED.format(songs_quant), - colour=self.__config.COLOURS['blue']) - await ctx.send(embed=embed) - + async def play(self, ctx: Context) -> str: if not self.__playing: first_song = self.__playlist.next_song() await self.__play_music(ctx, first_song) diff --git a/Music/Playlist.py b/Music/Playlist.py index 4d9b823..e8fca49 100644 --- a/Music/Playlist.py +++ b/Music/Playlist.py @@ -92,8 +92,7 @@ class Playlist(IPlaylist): self.__current = last_song return self.__current # return the song - def add_song(self, identifier: str, requester: str) -> Song: - song = Song(identifier=identifier, playlist=self, requester=requester) + def add_song(self, song: Song) -> Song: self.__queue.append(song) return song diff --git a/Music/Searcher.py b/Music/Searcher.py index c26214e..2482acb 100644 --- a/Music/Searcher.py +++ b/Music/Searcher.py @@ -1,40 +1,44 @@ +from Exceptions.Exceptions import InvalidInput, SpotifyError +from Music.Downloader import Downloader from Music.Types import Provider from Music.Spotify import SpotifySearch -from Utils.Utils import is_url +from Utils.Utils import Utils +from Config.Messages import SearchMessages class Searcher(): def __init__(self) -> None: self.__Spotify = SpotifySearch() + self.__messages = SearchMessages() + self.__down = Downloader() - def search(self, music: str) -> list: - provider = self.__identify_source(music) + async def search(self, track: str) -> list: + provider = self.__identify_source(track) + if provider == Provider.Unknown: + raise InvalidInput(self.__messages.UNKNOWN_INPUT, self.__messages.UNKNOWN_INPUT_TITLE) - if provider == Provider.YouTube: - return [music], Provider.YouTube + elif provider == Provider.YouTube: + musics = await self.__down.extract_info(track) + return musics elif provider == Provider.Spotify: - if self.__Spotify.connected == True: - musics = self.__Spotify.search(music) - return musics, Provider.Name - else: - print('DEVELOPER NOTE -> Spotify Not Connected') - return [], Provider.Unknown + try: + musics = self.__Spotify.search(track) + return musics + except: + raise SpotifyError(self.__messages.SPOTIFY_ERROR, self.__messages.GENERIC_TITLE) elif provider == Provider.Name: - return [music], Provider.Name + return [track] - elif provider == Provider.Unknown: - return None, Provider.Unknown - - def __identify_source(self, music) -> Provider: - if not is_url(music): + def __identify_source(self, track) -> Provider: + if not Utils.is_url(track): return Provider.Name - if "https://www.youtu" in music or "https://youtu.be" in music or "https://music.youtube" in music: + if "https://www.youtu" in track or "https://youtu.be" in track or "https://music.youtube" in track: return Provider.YouTube - if "https://open.spotify.com" in music: + if "https://open.spotify.com" in track: return Provider.Spotify return Provider.Unknown diff --git a/Music/Song.py b/Music/Song.py index 415fd0b..d8af059 100644 --- a/Music/Song.py +++ b/Music/Song.py @@ -17,17 +17,15 @@ class Song(ISong): self.__required_keys = ['url'] for key in self.__required_keys: - if key in info: + if key in info.keys(): self.__info[key] = info[key] else: print(f'DEVELOPER NOTE -> {key} not found in info of music: {self.identifier}') self.destroy() for key in self.__usefull_keys: - if key in info: + if key in info.keys(): self.__info[key] = info[key] - else: - print(f'DEVELOPER NOTE -> {key} not found in info of music: {self.identifier}') @property def source(self) -> str: diff --git a/Music/Spotify.py b/Music/Spotify.py index c1a65f5..4294184 100644 --- a/Music/Spotify.py +++ b/Music/Spotify.py @@ -1,4 +1,4 @@ -import spotipy +from spotipy import Spotify from spotipy.oauth2 import SpotifyClientCredentials from Config.Config import Configs @@ -9,86 +9,67 @@ class SpotifySearch(): self.__connected = False self.__connect() - @property - def connected(self): - return self.__connected - - def __connect(self) -> bool: + def __connect(self) -> None: try: - # Initialize the connection with Spotify API - self.__api = spotipy.Spotify(auth_manager=SpotifyClientCredentials( - client_id=self.__config.SPOTIFY_ID, client_secret=self.__config.SPOTIFY_SECRET)) + auth = SpotifyClientCredentials(self.__config.SPOTIFY_ID, self.__config.SPOTIFY_SECRET) + self.__api = Spotify(auth_manager=auth) self.__connected = True - return True except Exception as e: - print(f'DEVELOPER NOTE -> Spotify Connection {e}') - return False + print(f'DEVELOPER NOTE -> Spotify Connection Error {e}') - def search(self, music=str) -> list: + def search(self, music: str) -> list: type = music.split('/')[3].split('?')[0] code = music.split('/')[4].split('?')[0] - if type == 'album': - musics = self.__get_album(code) - elif type == 'playlist': - musics = self.__get_playlist(code) - elif type == 'track': - musics = self.__get_track(code) - elif type == 'artist': - musics = self.__get_artist(code) - else: - return None + musics = [] + + if self.__connected: + if type == 'album': + musics = self.__get_album(code) + elif type == 'playlist': + musics = self.__get_playlist(code) + elif type == 'track': + musics = self.__get_track(code) + elif type == 'artist': + musics = self.__get_artist(code) return musics - def __get_album(self, code=str) -> list: - if self.__connected == True: - try: - results = self.__api.album_tracks(code) - musics = results['items'] + def __get_album(self, code: str) -> list: + results = self.__api.album_tracks(code) + musics = results['items'] - while results['next']: # Get the next pages - results = self.__api.next(results) - musics.extend(results['items']) + while results['next']: # Get the next pages + results = self.__api.next(results) + musics.extend(results['items']) - musicsTitle = [] + musicsTitle = [] - for music in musics: - try: - title = self.__extract_title(music) - musicsTitle.append(title) - except: - pass - return musicsTitle - except Exception as e: - raise e + for music in musics: + title = self.__extract_title(music) + musicsTitle.append(title) - def __get_playlist(self, code=str) -> list: - try: - results = self.__api.playlist_items(code) - itens = results['items'] + return musicsTitle - while results['next']: # Load the next pages - results = self.__api.next(results) - itens.extend(results['items']) + def __get_playlist(self, code: str) -> list: + results = self.__api.playlist_items(code) + itens = results['items'] - musics = [] - for item in itens: - musics.append(item['track']) + while results['next']: # Load the next pages + results = self.__api.next(results) + itens.extend(results['items']) - titles = [] - for music in musics: - try: - title = self.__extract_title(music) - titles.append(title) - except Exception as e: - raise e + musics = [] + for item in itens: + musics.append(item['track']) - return titles + titles = [] + for music in musics: + title = self.__extract_title(music) + titles.append(title) - except Exception as e: - raise e + return titles - def __get_track(self, code=str) -> list: + def __get_track(self, code: str) -> list: results = self.__api.track(code) name = results['name'] artists = '' @@ -97,7 +78,7 @@ class SpotifySearch(): return [f'{name} {artists}'] - def __get_artist(self, code=str) -> list: + def __get_artist(self, code: str) -> list: results = self.__api.artist_top_tracks(code, country='BR') musics_titles = [] diff --git a/Utils/Sender.py b/Utils/Sender.py new file mode 100644 index 0000000..63d800c --- /dev/null +++ b/Utils/Sender.py @@ -0,0 +1,12 @@ +from discord.ext.commands import Context +from discord import Embed + + +class Sender: + @classmethod + async def send_embed(cls, ctx: Context, embed: Embed) -> None: + pass + + @classmethod + async def send_message(cls, ctx: Context, message: Embed) -> None: + pass diff --git a/Utils/Utils.py b/Utils/Utils.py index a500f77..f287cdc 100644 --- a/Utils/Utils.py +++ b/Utils/Utils.py @@ -5,6 +5,33 @@ from functools import wraps, partial config = Configs() +class Utils: + @classmethod + def format_time(cls, duration) -> str: + if not duration: + return "00:00" + + hours = duration // 60 // 60 + minutes = duration // 60 % 60 + seconds = duration % 60 + + return "{}{}{:02d}:{:02d}".format( + hours if hours else "", + ":" if hours else "", + minutes, + seconds) + + @classmethod + def is_url(cls, string) -> bool: + regex = re.compile( + "http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+") + + if re.search(regex, string): + return True + else: + return False + + def is_connected(ctx): try: voice_channel = ctx.guild.voice_client.channel diff --git a/Views/Embeds.py b/Views/Embeds.py index bf40bf9..5d84fc4 100644 --- a/Views/Embeds.py +++ b/Views/Embeds.py @@ -1,3 +1,4 @@ +from Exceptions.Exceptions import Error from discord import Embed from Config.Config import Configs from Config.Colors import Colors @@ -23,32 +24,36 @@ class Embeds: ) return embed + def SONG_ADDED_TWO(self, info: dict, pos: int) -> Embed: + embed = self.SONG_INFO(info, self.__config.SONG_ADDED_TWO, pos) + return embed + def INVALID_INPUT(self) -> Embed: embed = Embed( title=self.__config.ERROR_TITLE, description=self.__config.INVALID_INPUT, - colours=self.__colors.BLUE) + colour=self.__colors.BLACK) return embed def UNAVAILABLE_VIDEO(self) -> Embed: embed = Embed( title=self.__config.ERROR_TITLE, description=self.__config.VIDEO_UNAVAILABLE, - colours=self.__colors.BLUE) + colour=self.__colors.BLACK) return embed def DOWNLOADING_ERROR(self) -> Embed: embed = Embed( title=self.__config.ERROR_TITLE, description=self.__config.DOWNLOADING_ERROR, - colours=self.__colors.BLUE) + colour=self.__colors.BLACK) return embed def SONG_ADDED(self, title: str) -> Embed: embed = Embed( title=self.__config.SONG_PLAYER, description=self.__config.SONG_ADDED.format(title), - colours=self.__colors.BLUE) + colour=self.__colors.BLUE) return embed def SONGS_ADDED(self, quant: int) -> Embed: @@ -62,7 +67,7 @@ class Embeds: embedvc = Embed( title=title, description=f"[{info['title']}]({info['original_url']})", - color=self.__colors.BLUE + colour=self.__colors.BLUE ) embedvc.add_field(name=self.__config.SONGINFO_UPLOADER, @@ -115,6 +120,14 @@ class Embeds: ) return embed + def CUSTOM_ERROR(self, error: Error) -> Embed: + embed = Embed( + title=error.title, + description=error.message, + colour=self.__colors.BLACK + ) + return embed + def WRONG_LENGTH_INPUT(self) -> Embed: embed = Embed( title=self.__config.BAD_COMMAND_TITLE, @@ -201,6 +214,13 @@ class Embeds: ) return embed + def SONG_PROBLEMATIC(self) -> Embed: + embed = Embed( + title=self.__config.ERROR_TITLE, + description=self.__config.DOWNLOADING_ERROR, + colour=self.__colors.BLACK) + return embed + def NO_CHANNEL(self) -> Embed: embed = Embed( title=self.__config.IMPOSSIBLE_MOVE, diff --git a/config/Messages.py b/config/Messages.py index 061f830..d401fc6 100644 --- a/config/Messages.py +++ b/config/Messages.py @@ -69,3 +69,13 @@ class Messages(Singleton): self.BAD_COMMAND = f'❌ Bad usage of this command, type {configs.BOT_PREFIX}help "command" to understand the command better' self.INVITE_URL = 'https://discordapp.com/oauth2/authorize?client_id={}&scope=bot>' self.VIDEO_UNAVAILABLE = '❌ Sorry. This video is unavailable for download.' + + +class SearchMessages(Singleton): + def __init__(self) -> None: + if not super().created: + config = Configs() + self.UNKNOWN_INPUT = f'This type of input was too strange, try something else or type {config.BOT_PREFIX}help play' + self.UNKNOWN_INPUT_TITLE = 'Nothing Found' + self.SPOTIFY_ERROR = 'Spotify could not process any songs with this input, verify your link or try again later.' + self.GENERIC_TITLE = 'Input could not be processed' diff --git a/config/config.py b/config/config.py index e917004..79c9d55 100644 --- a/config/config.py +++ b/config/config.py @@ -17,6 +17,7 @@ class Configs(Singleton): self.STARTUP_COMPLETE_MESSAGE = 'Vulkan is now operating.' self.MAX_PLAYLIST_LENGTH = 50 + self.MAX_PLAYLIST_FORCED_LENGTH = 5 self.MAX_PRELOAD_SONGS = 10 self.MAX_SONGS_HISTORY = 15 diff --git a/main.py b/main.py index ed294ec..256c4d8 100644 --- a/main.py +++ b/main.py @@ -1,4 +1,3 @@ -from distutils.command.config import config from discord import Intents, Client from os import listdir from Config.Config import Configs