diff --git a/Config/Messages.py b/Config/Messages.py index afe26b0..dc09f71 100644 --- a/Config/Messages.py +++ b/Config/Messages.py @@ -75,10 +75,10 @@ class SearchMessages(Singleton): 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 = 'URL could not be processed' - self.YOUTUBE_ERROR = 'Youtube could not process any songs with this input, verify your link or try again later.' - self.INVALID_SPOTIFY_URL = 'Invalid Spotify URL, verify your link.' + self.SPOTIFY_NOT_FOUND = 'Spotify could not process any songs with this input, verify your link or try again later.' + self.YOUTUBE_NOT_FOUND = 'Youtube could not process any songs with this input, verify your link or try again later.' + self.DEEZER_NOT_FOUND = 'Deezer could not process any songs with this input, verify your link or try again later.' class SpotifyMessages(Singleton): @@ -86,3 +86,10 @@ class SpotifyMessages(Singleton): if not super().created: self.INVALID_SPOTIFY_URL = 'Invalid Spotify URL, verify your link.' self.GENERIC_TITLE = 'URL could not be processed' + + +class DeezerMessages(Singleton): + def __init__(self) -> None: + if not super().created: + self.INVALID_DEEZER_URL = 'Invalid Deezer URL, verify your link.' + self.GENERIC_TITLE = 'URL could not be processed' diff --git a/Exceptions/Exceptions.py b/Exceptions/Exceptions.py index 6c29478..dee5718 100644 --- a/Exceptions/Exceptions.py +++ b/Exceptions/Exceptions.py @@ -49,6 +49,11 @@ class SpotifyError(VulkanError): super().__init__(message, title, *args) +class DeezerError(VulkanError): + def __init__(self, message='', title='', *args: object) -> None: + super().__init__(message, title, *args) + + class UnknownError(VulkanError): def __init__(self, message='', title='', *args: object) -> None: super().__init__(message, title, *args) diff --git a/Music/DeezerSearcher.py b/Music/DeezerSearcher.py new file mode 100644 index 0000000..19048ef --- /dev/null +++ b/Music/DeezerSearcher.py @@ -0,0 +1,72 @@ +import deezer +from Exceptions.Exceptions import DeezerError +from Config.Messages import DeezerMessages + + +class DeezerSearcher: + def __init__(self) -> None: + self.__client = deezer.Client() + self.__messages = DeezerMessages() + self.__acceptedTypes = ['track', 'artist', 'playlist', 'album'] + + def search(self, url: str) -> None: + if not self.__verifyValidUrl(url): + raise DeezerError(self.__messages.INVALID_DEEZER_URL, self.__messages.GENERIC_TITLE) + + urlType = url.split('/')[4].split('?')[0] + code = int(url.split('/')[5].split('?')[0]) + + try: + musics = [] + if urlType == 'album': + musics = self.__get_album(code) + elif urlType == 'playlist': + musics = self.__get_playlist(code) + elif urlType == 'track': + musics = self.__get_track(code) + elif urlType == 'artist': + musics = self.__get_artist(code) + + return musics + except Exception as e: + print(f'[DEEZER ERROR] -> {e}') + raise DeezerError(self.__messages.INVALID_DEEZER_URL, self.__messages.GENERIC_TITLE) + + def __get_album(self, code: int) -> list: + album = self.__client.get_album(code) + + return [track.title for track in album.tracks] + + def __get_track(self, code: int) -> list: + track = self.__client.get_track(code) + + return [track.title] + + def __get_playlist(self, code: int) -> list: + playlist = self.__client.get_playlist(code) + + return [track.title for track in playlist.tracks] + + def __get_artist(self, code: int) -> list: + artist = self.__client.get_artist(code) + + topMusics = artist.get_top() + + return [track.title for track in topMusics] + + def __verifyValidUrl(self, url: str) -> bool: + try: + urlType = url.split('/')[4].split('?')[0] + code = url.split('/')[5].split('?')[0] + + code = int(code) + + if urlType == '' or code == '': + return False + + if urlType not in self.__acceptedTypes: + return False + + return True + except: + return False diff --git a/Music/Downloader.py b/Music/Downloader.py index 2c295f0..d5dcdbf 100644 --- a/Music/Downloader.py +++ b/Music/Downloader.py @@ -65,6 +65,7 @@ class Downloader(): with YoutubeDL(options) as ydl: try: extracted_info = ydl.extract_info(url, download=False) + # Some links doesn't extract unless extract_flat key is passed as False in options if self.__failed_to_extract(extracted_info): extracted_info = self.__get_forced_extracted_info(url) diff --git a/Music/Searcher.py b/Music/Searcher.py index 30624a9..4610588 100644 --- a/Music/Searcher.py +++ b/Music/Searcher.py @@ -1,7 +1,8 @@ -from Exceptions.Exceptions import InvalidInput, SpotifyError, YoutubeError +from Exceptions.Exceptions import DeezerError, InvalidInput, SpotifyError, YoutubeError from Music.Downloader import Downloader from Music.Types import Provider from Music.Spotify import SpotifySearch +from Music.DeezerSearcher import DeezerSearcher from Utils.Utils import Utils from Utils.UrlAnalyzer import URLAnalyzer from Config.Messages import SearchMessages @@ -9,7 +10,8 @@ from Config.Messages import SearchMessages class Searcher(): def __init__(self) -> None: - self.__Spotify = SpotifySearch() + self.__spotify = SpotifySearch() + self.__deezer = DeezerSearcher() self.__messages = SearchMessages() self.__down = Downloader() @@ -24,20 +26,35 @@ class Searcher(): musics = await self.__down.extract_info(track) return musics except: - raise YoutubeError(self.__messages.YOUTUBE_ERROR, self.__messages.GENERIC_TITLE) + raise YoutubeError(self.__messages.YOUTUBE_NOT_FOUND, self.__messages.GENERIC_TITLE) elif provider == Provider.Spotify: try: - musics = self.__Spotify.search(track) + musics = self.__spotify.search(track) if musics == None or len(musics) == 0: - raise SpotifyError(self.__messages.SPOTIFY_ERROR, self.__messages.GENERIC_TITLE) + raise SpotifyError(self.__messages.SPOTIFY_NOT_FOUND, + self.__messages.GENERIC_TITLE) return musics except SpotifyError as error: raise error # Redirect already processed error except Exception as e: print(f'[Spotify Error] -> {e}') - raise SpotifyError(self.__messages.SPOTIFY_ERROR, self.__messages.GENERIC_TITLE) + raise SpotifyError(self.__messages.SPOTIFY_NOT_FOUND, self.__messages.GENERIC_TITLE) + + elif provider == Provider.Deezer: + try: + musics = self.__deezer.search(track) + if musics == None or len(musics) == 0: + raise DeezerError(self.__messages.DEEZER_NOT_FOUND, + self.__messages.GENERIC_TITLE) + + return musics + except DeezerError as error: + raise error # Redirect already processed error + except Exception as e: + print(f'[Deezer Error] -> {e}') + raise DeezerError(self.__messages.DEEZER_NOT_FOUND, self.__messages.GENERIC_TITLE) elif provider == Provider.Name: return [track] @@ -52,7 +69,7 @@ class Searcher(): if 'start_radio' or 'index' in trackAnalyzer.queryParams.keys(): return trackAnalyzer.getCleanedUrl() - def __identify_source(self, track) -> Provider: + def __identify_source(self, track: str) -> Provider: if not Utils.is_url(track): return Provider.Name @@ -62,4 +79,7 @@ class Searcher(): if "https://open.spotify.com" in track: return Provider.Spotify + if "https://www.deezer.com" in track: + return Provider.Deezer + return Provider.Unknown diff --git a/Music/Types.py b/Music/Types.py index 7ec21de..61d09bc 100644 --- a/Music/Types.py +++ b/Music/Types.py @@ -1,8 +1,9 @@ from enum import Enum -class Provider(Enum): +class Provider(str, Enum): Spotify = 'Spotify' + Deezer = 'Deezer' YouTube = 'YouTube' Name = 'Track Name' Unknown = 'Unknown' diff --git a/README.md b/README.md index af86978..fbf8aa5 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,17 @@ # **Vulkan** -A Music Discord bot, written in Python, that supports Youtube and Spotify sources for playing. Vulkan was designed so that anyone can fork this project, follow the instructions and use it in their own way, Vulkan can also be configured in Heroku to work 24/7. +A Music Discord bot, written in Python, that plays *Youtube*, *Spotify* and *Deezer* links. Vulkan was designed so that anyone can fork this project, follow the instructions and use it in their own way, Vulkan can also be configured in Heroku to work 24/7. # **Music** -- Play musics from Youtube and Spotify Playlists +- Play musics from Youtube, Spotify and Deezer links (Albums, Artists, Playlists and Tracks) - Control loop of one or all musics - Allow moving and removing musics in the queue - Play musics in queue randomly - Store played songs and allow bidirectional flow ### Commands -```!play [title, spotify_url, youtube_url]``` - Start playing song +```!play [title, spotify_url, youtube_url, deezer_url]``` - Start playing song ```!resume``` - Resume the song player diff --git a/Tests/TestsHelper.py b/Tests/TestsHelper.py index a44326f..fb49c20 100644 --- a/Tests/TestsHelper.py +++ b/Tests/TestsHelper.py @@ -19,3 +19,10 @@ class TestsConstants(Singleton): self.SPOTIFY_ALBUM_URL = 'https://open.spotify.com/album/71O60S5gIJSIAhdnrDIh3N' self.SPOTIFY_WRONG1_URL = 'https://open.spotify.com/wrongUrl' self.SPOTIFY_WRONG2_URL = 'https://open.spotify.com/track/WrongID' + + self.DEEZER_TRACK_URL = 'https://www.deezer.com/br/track/33560861' + self.DEEZER_ARTIST_URL = 'https://www.deezer.com/br/artist/180' + self.DEEZER_PLAYLIST_URL = 'https://www.deezer.com/br/playlist/1001939451' + self.DEEZER_ALBUM_URL = 'https://www.deezer.com/en/album/236107012' + self.DEEZER_WRONG1_URL = 'xxxhttps://www.deezer.com/br/album/5' + self.DEEZER_WRONG2_URL = 'https://www.deezer.com/en/album/23610701252' diff --git a/Tests/VDeezerTests.py b/Tests/VDeezerTests.py new file mode 100644 index 0000000..063e790 --- /dev/null +++ b/Tests/VDeezerTests.py @@ -0,0 +1,66 @@ +from Tests.TestBase import VulkanTesterBase +from Exceptions.Exceptions import DeezerError + + +class VulkanDeezerTest(VulkanTesterBase): + def __init__(self) -> None: + super().__init__() + + def test_deezerTrack(self) -> bool: + musics = self._runner.run_coroutine( + self._searcher.search(self._constants.DEEZER_TRACK_URL)) + + if len(musics) > 0: + return True + else: + return False + + def test_deezerPlaylist(self) -> bool: + musics = self._runner.run_coroutine( + self._searcher.search(self._constants.DEEZER_PLAYLIST_URL)) + + if len(musics) > 0: + return True + else: + return False + + def test_deezerArtist(self) -> bool: + musics = self._runner.run_coroutine( + self._searcher.search(self._constants.DEEZER_ARTIST_URL)) + + if len(musics) > 0: + return True + else: + return False + + def test_deezerAlbum(self) -> bool: + musics = self._runner.run_coroutine( + self._searcher.search(self._constants.DEEZER_ALBUM_URL)) + + if len(musics) > 0: + return True + else: + return False + + def test_deezerWrongUrlShouldThrowException(self) -> bool: + try: + musics = self._runner.run_coroutine( + self._searcher.search(self._constants.DEEZER_WRONG1_URL)) + + except DeezerError as e: + print(f'Deezer Error -> {e.message}') + return True + except Exception as e: + print(e) + return False + + def test_deezerWrongUrlTwoShouldThrowException(self) -> bool: + try: + musics = self._runner.run_coroutine( + self._searcher.search(self._constants.DEEZER_WRONG2_URL)) + + except DeezerError as e: + print(f'Deezer Error -> {e.message}') + return True + except Exception as e: + return False diff --git a/Tests/VDownloaderTests.py b/Tests/VDownloaderTests.py index e64b2f1..65daf9c 100644 --- a/Tests/VDownloaderTests.py +++ b/Tests/VDownloaderTests.py @@ -58,15 +58,25 @@ class VulkanDownloaderTest(VulkanTesterBase): def test_YoutubeMixPlaylist(self) -> None: # Search the link to determine names - music = self._runner.run_coroutine( + musics = self._runner.run_coroutine( self._searcher.search(self._constants.YT_MIX_URL)) # Musics from Mix should download only the first music - if len(music) == 1: - return True - else: + if len(musics) != 1: return False + playlist = Playlist() + song = Song(musics[0], playlist, '') + playlist.add_song(song) + + self._runner.run_coroutine(self._downloader.download_song(song)) + + if song.problematic: + return False + else: + print(song.title) + return True + def test_musicTitle(self): playlist = Playlist() song = Song(self._constants.MUSIC_TITLE_STRING, playlist, '') @@ -81,11 +91,30 @@ class VulkanDownloaderTest(VulkanTesterBase): return True def test_YoutubePersonalPlaylist(self) -> None: - musics = self._runner.run_coroutine( + musicsList = self._runner.run_coroutine( self._searcher.search(self._constants.YT_PERSONAL_PLAYLIST_URL)) - if len(musics) > 0: - print(musics) - return True - else: + if len(musicsList) == 0: return False + + # Create and store songs in list + playlist = Playlist() + songsList: List[Song] = [] + for info in musicsList: + song = Song(identifier=info, playlist=playlist, requester='') + playlist.add_song(song) + songsList.append(song) + + # Create a list of coroutines without waiting for them + tasks: List[Task] = [] + for song in songsList: + tasks.append(self._downloader.download_song(song)) + + # Send for runner to execute them concurrently + self._runner.run_coroutines_list(tasks) + + for song in songsList: + if not song.problematic and song.title == None: + return False + + return True diff --git a/requirements.txt b/requirements.txt index 2fe4355..9519446 100644 Binary files a/requirements.txt and b/requirements.txt differ diff --git a/run_tests.py b/run_tests.py index 03417ba..2eadf89 100644 --- a/run_tests.py +++ b/run_tests.py @@ -1,8 +1,11 @@ from Tests.VDownloaderTests import VulkanDownloaderTest from Tests.VSpotifyTests import VulkanSpotifyTest +from Tests.VDeezerTests import VulkanDeezerTest tester = VulkanDownloaderTest() -# tester.run() +tester.run() tester = VulkanSpotifyTest() tester.run() +tester = VulkanDeezerTest() +tester.run()