From 4fb9d8d1ba5500a3c4c67b92d2220f76cc9749bb Mon Sep 17 00:00:00 2001 From: Rafael Vargas Date: Sat, 9 Jul 2022 23:22:55 -0300 Subject: [PATCH 1/6] Adding new tests using unit test module --- Music/Downloader.py | 3 +++ Music/Song.py | 10 +++---- Tests/TestsDownload.py | 59 ++++++++++++++++++++++++++++++++++++++++++ Tests/TestsHelper.py | 10 +++++++ Tests/__init__.py | 0 5 files changed, 77 insertions(+), 5 deletions(-) create mode 100644 Tests/TestsDownload.py create mode 100644 Tests/TestsHelper.py create mode 100644 Tests/__init__.py diff --git a/Music/Downloader.py b/Music/Downloader.py index 0c4a8af..f36fa1d 100644 --- a/Music/Downloader.py +++ b/Music/Downloader.py @@ -57,6 +57,9 @@ class Downloader(): @run_async def extract_info(self, url: str) -> List[dict]: + if url == '': + return [] + if Utils.is_url(url): # If Url options = Downloader.__YDL_OPTIONS_EXTRACT with YoutubeDL(options) as ydl: diff --git a/Music/Song.py b/Music/Song.py index b28f65c..6c407a0 100644 --- a/Music/Song.py +++ b/Music/Song.py @@ -11,10 +11,10 @@ class Song: self.destroy() return None - self.__usefull_keys = ['duration', - 'title', 'webpage_url', - 'channel', 'id', 'uploader', - 'thumbnail', 'original_url'] + self.__useful_keys = ['duration', + 'title', 'webpage_url', + 'channel', 'id', 'uploader', + 'thumbnail', 'original_url'] self.__required_keys = ['url'] for key in self.__required_keys: @@ -24,7 +24,7 @@ class Song: print(f'DEVELOPER NOTE -> {key} not found in info of music: {self.identifier}') self.destroy() - for key in self.__usefull_keys: + for key in self.__useful_keys: if key in info.keys(): self.__info[key] = info[key] diff --git a/Tests/TestsDownload.py b/Tests/TestsDownload.py new file mode 100644 index 0000000..1f93c42 --- /dev/null +++ b/Tests/TestsDownload.py @@ -0,0 +1,59 @@ +import asyncio +from typing import List +import unittest +from Music.Downloader import Downloader +from Music.Playlist import Playlist +from Music.Song import Song +from Tests.TestsHelper import TestsConstants + + +def myAsyncTest(coro): + def wrapper(*args, **kwargs): + loop = asyncio.new_event_loop() + asyncio.set_event_loop(None) + try: + return loop.run_until_complete(coro(*args, **kwargs)) + finally: + loop.close() + return wrapper + + +class TestDownloader(unittest.IsolatedAsyncioTestCase): + def __init__(self, methodName: str = ...) -> None: + self.downloader = Downloader() + self.constants = TestsConstants() + super().__init__(methodName) + + @myAsyncTest + async def test_emptyString(self): + musicsList = await self.downloader.extract_info('') + + self.assertEqual(musicsList, [], self.constants.EMPTY_STRING_ERROR_MSG) + + @myAsyncTest + async def test_YoutubeMusicUrl(self) -> None: + musicInfo = await self.downloader.extract_info(self.constants.YOUTUBE_MUSIC_URL) + + self.assertTrue(self.__infoExtractedSuccessfully(musicInfo)) + + def test_musicTitle(self): + playlist = Playlist() + song = Song(self.constants.MUSIC_TITLE_STRING, playlist, '') + playlist.add_song(song) + + self.downloader.finish_one_song(song) + self.assertFalse(song.problematic) + + def __downloadSucceeded(self, downloadReturn: List[dict]) -> bool: + # print(downloadReturn) + return True + + def __infoExtractedSuccessfully(self, info: List[dict]) -> bool: + if len(info) > 0: + return True + else: + return False + + +if __name__ == 'main': + unittest.main() diff --git a/Tests/TestsHelper.py b/Tests/TestsHelper.py new file mode 100644 index 0000000..c237842 --- /dev/null +++ b/Tests/TestsHelper.py @@ -0,0 +1,10 @@ +from Config.Configs import Singleton + + +class TestsConstants(Singleton): + def __init__(self) -> None: + if not super().created: + self.EMPTY_STRING_ERROR_MSG = 'Downloader with Empty String should be empty list.' + self.SPOTIFY_TRACK_URL = 'https://open.spotify.com/track/7wpnz7hje4FbnjZuWQtJHP' + self.MUSIC_TITLE_STRING = 'Experience || AMV || Anime Mix' + self.YOUTUBE_MUSIC_URL = 'https://www.youtube.com/watch?v=MvJoiv842mk' diff --git a/Tests/__init__.py b/Tests/__init__.py new file mode 100644 index 0000000..e69de29 From cd5f4567be0ecc1e6ce35fdcf04ba2775bef2aea Mon Sep 17 00:00:00 2001 From: Rafael Vargas Date: Sun, 10 Jul 2022 01:23:59 -0300 Subject: [PATCH 2/6] Upgrading tests in Downloader --- Music/Downloader.py | 6 ++-- Music/Searcher.py | 12 ++++++++ Tests/TestsDownload.py | 68 ++++++++++++++++++++++++++++++++++-------- Tests/TestsHelper.py | 6 +++- Utils/UrlAnalyzer.py | 34 +++++++++++++++++++++ 5 files changed, 110 insertions(+), 16 deletions(-) create mode 100644 Utils/UrlAnalyzer.py diff --git a/Music/Downloader.py b/Music/Downloader.py index f36fa1d..2c295f0 100644 --- a/Music/Downloader.py +++ b/Music/Downloader.py @@ -27,7 +27,7 @@ class Downloader(): 'default_search': 'auto', 'playliststart': 0, 'extract_flat': False, - 'playlistend': config.MAX_PLAYLIST_FORCED_LENGTH, + 'playlistend': config.MAX_PLAYLIST_LENGTH, 'quiet': True } __BASE_URL = 'https://www.youtube.com/watch?v={}' @@ -53,7 +53,7 @@ class Downloader(): async def preload(self, songs: List[Song]) -> None: for song in songs: - asyncio.ensure_future(self.__download_song(song)) + asyncio.ensure_future(self.download_song(song)) @run_async def extract_info(self, url: str) -> List[dict]: @@ -108,7 +108,7 @@ class Downloader(): print(f'DEVELOPER NOTE -> Error Downloading URL {e}') return None - async def __download_song(self, song: Song) -> None: + async def download_song(self, song: Song) -> None: if song.source is not None: # If Music already preloaded return None diff --git a/Music/Searcher.py b/Music/Searcher.py index f9dd92e..42a7855 100644 --- a/Music/Searcher.py +++ b/Music/Searcher.py @@ -3,6 +3,7 @@ from Music.Downloader import Downloader from Music.Types import Provider from Music.Spotify import SpotifySearch from Utils.Utils import Utils +from Utils.UrlAnalyzer import URLAnalyzer from Config.Messages import SearchMessages @@ -19,6 +20,7 @@ class Searcher(): elif provider == Provider.YouTube: try: + track = self.__cleanYoutubeInput(track) musics = await self.__down.extract_info(track) return musics except: @@ -34,6 +36,16 @@ class Searcher(): elif provider == Provider.Name: return [track] + def __cleanYoutubeInput(self, track: str) -> str: + trackAnalyzer = URLAnalyzer(track) + # Just ID and List arguments probably + if trackAnalyzer.queryParamsQuant <= 2: + return track + + # Arguments used in Mix Youtube Playlists + if 'start_radio' or 'index' in trackAnalyzer.queryParams.keys(): + return trackAnalyzer.getCleanedUrl() + def __identify_source(self, track) -> Provider: if not Utils.is_url(track): return Provider.Name diff --git a/Tests/TestsDownload.py b/Tests/TestsDownload.py index 1f93c42..598a156 100644 --- a/Tests/TestsDownload.py +++ b/Tests/TestsDownload.py @@ -2,6 +2,7 @@ import asyncio from typing import List import unittest from Music.Downloader import Downloader +from Music.Searcher import Searcher from Music.Playlist import Playlist from Music.Song import Song from Tests.TestsHelper import TestsConstants @@ -21,6 +22,7 @@ def myAsyncTest(coro): class TestDownloader(unittest.IsolatedAsyncioTestCase): def __init__(self, methodName: str = ...) -> None: self.downloader = Downloader() + self.searcher = Searcher() self.constants = TestsConstants() super().__init__(methodName) @@ -32,27 +34,69 @@ class TestDownloader(unittest.IsolatedAsyncioTestCase): @myAsyncTest async def test_YoutubeMusicUrl(self) -> None: - musicInfo = await self.downloader.extract_info(self.constants.YOUTUBE_MUSIC_URL) + musics = await self.searcher.search(self.constants.YT_MUSIC_URL) - self.assertTrue(self.__infoExtractedSuccessfully(musicInfo)) + self.assertTrue(len(musics) > 0) - def test_musicTitle(self): + @myAsyncTest + async def test_YoutubePersonalPlaylist(self) -> None: + musics = await self.searcher.search(self.constants.YT_PERSONAL_PLAYLIST_URL) + + self.assertTrue(len(musics) > 0) + + @myAsyncTest + async def test_YoutubeChannelPlaylist(self) -> None: + # Search the link to determine names + musicsInfo = await self.searcher.search(self.constants.YT_CHANNEL_PLAYLIST_URL) + + self.assertTrue(len(musicsInfo) > 0) + + # Create and store songs in list + playlist = Playlist() + songsList = [] + for info in musicsInfo: + song = Song(identifier=info, playlist=playlist, requester='') + playlist.add_song(song) + songsList.append(song) + + # We need to trigger and wait multiple tasks, so we create multiple tasks with asyncio + # and then we await for each one of them. We use this because download_song is a Coroutine + tasks: List[asyncio.Task] = [] + for song in songsList: + task = asyncio.create_task(self.downloader.download_song(song)) + tasks.append(task) + + # Await for each task to finish + for task in tasks: + await task + + self.assertTrue(self.__verifySuccessfullyPreload(songsList)) + + @myAsyncTest + async def test_YoutubeMixPlaylist(self) -> None: + music = await self.searcher.search(self.constants.YT_MIX_URL) + + # Musics from Mix should download only the first music + self.assertTrue(len(music) == 1) + + @myAsyncTest + async def test_musicTitle(self): playlist = Playlist() song = Song(self.constants.MUSIC_TITLE_STRING, playlist, '') playlist.add_song(song) - self.downloader.finish_one_song(song) + task = asyncio.create_task(self.downloader.download_song(song)) + await task + self.assertFalse(song.problematic) - def __downloadSucceeded(self, downloadReturn: List[dict]) -> bool: - # print(downloadReturn) - return True + def __verifySuccessfullyPreload(self, songs: List[Song]) -> bool: + for song in songs: + if song.title == None: + print('Song failed to download') + return False - def __infoExtractedSuccessfully(self, info: List[dict]) -> bool: - if len(info) > 0: - return True - else: - return False + return True if __name__ == 'main': diff --git a/Tests/TestsHelper.py b/Tests/TestsHelper.py index c237842..9521a61 100644 --- a/Tests/TestsHelper.py +++ b/Tests/TestsHelper.py @@ -7,4 +7,8 @@ class TestsConstants(Singleton): self.EMPTY_STRING_ERROR_MSG = 'Downloader with Empty String should be empty list.' self.SPOTIFY_TRACK_URL = 'https://open.spotify.com/track/7wpnz7hje4FbnjZuWQtJHP' self.MUSIC_TITLE_STRING = 'Experience || AMV || Anime Mix' - self.YOUTUBE_MUSIC_URL = 'https://www.youtube.com/watch?v=MvJoiv842mk' + self.YT_MUSIC_URL = 'https://www.youtube.com/watch?v=MvJoiv842mk' + self.YT_MIX_URL = 'https://www.youtube.com/watch?v=ePjtnSPFWK8&list=RDMMePjtnSPFWK8&start_radio=1' + self.YT_PERSONAL_PLAYLIST_URL = 'https://www.youtube.com/playlist?list=PLbbKJHHZR9ShYuKAr71cLJCFbYE-83vhS' + # Links from playlists in channels some times must be extracted with force by Downloader + self.YT_CHANNEL_PLAYLIST_URL = 'https://www.youtube.com/watch?v=MvJoiv842mk&list=PLAI1099Tvk0zWU8X4dwc4vv4MpePQ4DLl' diff --git a/Utils/UrlAnalyzer.py b/Utils/UrlAnalyzer.py new file mode 100644 index 0000000..78666dd --- /dev/null +++ b/Utils/UrlAnalyzer.py @@ -0,0 +1,34 @@ +from typing import Dict + + +class URLAnalyzer: + def __init__(self, url: str) -> None: + self.__url = url + self.__queryParamsQuant = self.__url.count('&') + self.__url.count('?') + self.__queryParams: Dict[str, str] = self.__getAllQueryParams() + + @property + def queryParams(self) -> dict: + return self.__queryParams + + @property + def queryParamsQuant(self) -> int: + return self.__queryParamsQuant + + def getCleanedUrl(self) -> str: + firstE = self.__url.index('&') + return self.__url[:firstE] + + def __getAllQueryParams(self) -> dict: + if self.__queryParamsQuant <= 1: + return {} + + params = {} + arguments = self.__url.split('&') + arguments.pop(0) + + for queryParam in arguments: + queryName, queryValue = queryParam.split('=') + params[queryName] = queryValue + + return params From 0938dd37e2ebd2e946d341eb2af423887981cef2 Mon Sep 17 00:00:00 2001 From: Rafael Vargas Date: Sun, 10 Jul 2022 12:09:32 -0300 Subject: [PATCH 3/6] Now using own test module to test vulkan downloader methods --- README.md | 6 ++- Tests/Colors.py | 10 ++++ Tests/LoopRunner.py | 50 ++++++++++++++++++ Tests/TestBase.py | 86 +++++++++++++++++++++++++++++++ Tests/TestsDownload.py | 103 -------------------------------------- Tests/VDownloaderTests.py | 91 +++++++++++++++++++++++++++++++++ Tests/__init__.py | 0 requirements.txt | Bin 116 -> 244 bytes run_tests.py | 5 ++ 9 files changed, 247 insertions(+), 104 deletions(-) create mode 100644 Tests/Colors.py create mode 100644 Tests/LoopRunner.py create mode 100644 Tests/TestBase.py delete mode 100644 Tests/TestsDownload.py create mode 100644 Tests/VDownloaderTests.py delete mode 100644 Tests/__init__.py create mode 100644 run_tests.py diff --git a/README.md b/README.md index 1eec390..af86978 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,10 @@ To run your Bot in Heroku 24/7, you will need the Procfile located in root, then - https://github.com/xrisk/heroku-opus.git +## Testing +The tests were written manually with no package due to problems with async function in other packages, to execute them type in root:
+`python run_tests.py`
+ ## License - This program is free software: you can redistribute it and/or modify it under the terms of the [MIT License](https://github.com/RafaelSolVargas/Vulkan/blob/master/LICENSE). @@ -101,4 +105,4 @@ To run your Bot in Heroku 24/7, you will need the Procfile located in root, then ## Acknowledgment - - See the DingoLingo [project](https://github.com/Raptor123471/DingoLingo) from Raptor123471, it helped me a lot to build Vulkan. + - See the DingoLingo [project](https://github.com/Raptor123471/DingoLingo) from Raptor123471, it helped me a lot to build Vulkan. \ No newline at end of file diff --git a/Tests/Colors.py b/Tests/Colors.py new file mode 100644 index 0000000..d3d2a62 --- /dev/null +++ b/Tests/Colors.py @@ -0,0 +1,10 @@ +class Colors: + HEADER = '\033[95m' + OKBLUE = '\033[94m' + OKCYAN = '\033[96m' + OKGREEN = '\033[92m' + WARNING = '\033[93m' + FAIL = '\033[91m' + ENDC = '\033[0m' + BOLD = '\033[1m' + UNDERLINE = '\033[4m' diff --git a/Tests/LoopRunner.py b/Tests/LoopRunner.py new file mode 100644 index 0000000..660604a --- /dev/null +++ b/Tests/LoopRunner.py @@ -0,0 +1,50 @@ +import asyncio +from asyncio import AbstractEventLoop +from threading import Thread +from typing import Any, Coroutine, List + + +class LoopRunner(Thread): + """ + Class to help deal with asyncio coroutines and loops + Copyright: https://agariinc.medium.com/advanced-strategies-for-testing-async-code-in-python-6196a032d8d7 + """ + + def __init__(self, loop: AbstractEventLoop) -> None: + # We ensure to always use the same loop + self.loop = loop + Thread.__init__(self, name='runner') + + def run(self) -> None: + asyncio.set_event_loop(self.loop) + try: + self.loop.run_forever() + finally: + if self.loop.is_running(): + self.loop.close() + + def run_coroutine(self, coroutine: Coroutine) -> Any: + """Run a coroutine inside the loop and return the result, doesn't allow concurrency""" + result = asyncio.run_coroutine_threadsafe(coroutine, self.loop) + return result.result() + + def _stop(self): + self.loop.stop() + + def run_in_thread(self, callback, *args): + return self.loop.call_soon_threadsafe(callback, *args) + + def stop(self): + return self.run_in_thread(self._stop) + + def run_coroutines_list(self, coroutineList: List[Coroutine]) -> None: + """Create multiple tasks in the loop and wait for them, use concurrency""" + tasks = [] + for coroutine in coroutineList: + tasks.append(self.loop.create_task(coroutine)) + + self.run_coroutine(self.__waitForMultipleTasks(tasks)) + + async def __waitForMultipleTasks(self, coroutines: List[Coroutine]) -> None: + """Function to trigger the await for asyncio.wait coroutines""" + await asyncio.wait(coroutines) diff --git a/Tests/TestBase.py b/Tests/TestBase.py new file mode 100644 index 0000000..358b42c --- /dev/null +++ b/Tests/TestBase.py @@ -0,0 +1,86 @@ +import asyncio +from time import time +from typing import Callable, List, Tuple +from Tests.Colors import Colors +from Music.Downloader import Downloader +from Music.Searcher import Searcher +from Tests.TestsHelper import TestsConstants +from Tests.LoopRunner import LoopRunner + + +class VulkanTesterBase: + """My own module to execute asyncio tests""" + + def __init__(self) -> None: + self._downloader = Downloader() + self._searcher = Searcher() + self._constants = TestsConstants() + # Get the list of methods objects of this class if start with test + self._methodsList: List[Callable] = [getattr(self, func) for func in dir(self) if callable( + getattr(self, func)) and func.startswith("test")] + + def run(self) -> None: + self.__printSeparator() + methodsSummary: List[Tuple[Callable, bool]] = [] + testsSuccessQuant = 0 + testsStartTime = time() + + for method in self._methodsList: + currentTestStartTime = time() + self.__printTestStart(method) + success = False + try: + self._setUp() + success = method() + except Exception as e: + success = False + print(f'ERROR -> {e}') + finally: + self._tearDown() + + methodsSummary.append((method, success)) + runTime = time() - currentTestStartTime # Get the run time of the current test + if success: + testsSuccessQuant += 1 + self.__printTestSuccess(method, runTime) + else: + self.__printTestFailure(method, runTime) + + self.__printSeparator() + + testsRunTime = time() - testsStartTime + self.__printTestsSummary(methodsSummary, testsSuccessQuant, testsRunTime) + + def _setUp(self) -> None: + self._runner = LoopRunner(asyncio.new_event_loop()) + self._runner.start() + + def _tearDown(self) -> None: + self._runner.stop() + self._runner.join() + + def __printTestsSummary(self, methods: List[Tuple[Callable, bool]], totalSuccess: int, runTime: int) -> None: + for index, methodResult in enumerate(methods): + method = methodResult[0] + success = methodResult[1] + + if success: + print(f'{Colors.OKGREEN} {index} -> {method.__name__} = Success {Colors.ENDC}') + else: + print(f'{Colors.FAIL} {index} -> {method.__name__} = Failed {Colors.ENDC}') + + print() + print( + f'TESTS EXECUTED: {len(methods)} | SUCCESS: {totalSuccess} | FAILED: {len(methods) - totalSuccess} | TIME: {runTime:.2f}sec') + + def __printTestStart(self, method: Callable) -> None: + print(f'🧪 - Starting {method.__name__}') + + def __printTestSuccess(self, method: Callable, runTime: int) -> None: + print(f'{method.__name__} -> {Colors.OKGREEN} Success {Colors.ENDC} | ⏰ - {runTime:.2f}sec') + + def __printTestFailure(self, method: Callable, runTime: int) -> None: + print(f'{method.__name__} -> {Colors.FAIL} Test Failed {Colors.ENDC} | ⏰ - {runTime:.2f}sec') + + def __printSeparator(self) -> None: + print('=-=' * 15) diff --git a/Tests/TestsDownload.py b/Tests/TestsDownload.py deleted file mode 100644 index 598a156..0000000 --- a/Tests/TestsDownload.py +++ /dev/null @@ -1,103 +0,0 @@ -import asyncio -from typing import List -import unittest -from Music.Downloader import Downloader -from Music.Searcher import Searcher -from Music.Playlist import Playlist -from Music.Song import Song -from Tests.TestsHelper import TestsConstants - - -def myAsyncTest(coro): - def wrapper(*args, **kwargs): - loop = asyncio.new_event_loop() - asyncio.set_event_loop(None) - try: - return loop.run_until_complete(coro(*args, **kwargs)) - finally: - loop.close() - return wrapper - - -class TestDownloader(unittest.IsolatedAsyncioTestCase): - def __init__(self, methodName: str = ...) -> None: - self.downloader = Downloader() - self.searcher = Searcher() - self.constants = TestsConstants() - super().__init__(methodName) - - @myAsyncTest - async def test_emptyString(self): - musicsList = await self.downloader.extract_info('') - - self.assertEqual(musicsList, [], self.constants.EMPTY_STRING_ERROR_MSG) - - @myAsyncTest - async def test_YoutubeMusicUrl(self) -> None: - musics = await self.searcher.search(self.constants.YT_MUSIC_URL) - - self.assertTrue(len(musics) > 0) - - @myAsyncTest - async def test_YoutubePersonalPlaylist(self) -> None: - musics = await self.searcher.search(self.constants.YT_PERSONAL_PLAYLIST_URL) - - self.assertTrue(len(musics) > 0) - - @myAsyncTest - async def test_YoutubeChannelPlaylist(self) -> None: - # Search the link to determine names - musicsInfo = await self.searcher.search(self.constants.YT_CHANNEL_PLAYLIST_URL) - - self.assertTrue(len(musicsInfo) > 0) - - # Create and store songs in list - playlist = Playlist() - songsList = [] - for info in musicsInfo: - song = Song(identifier=info, playlist=playlist, requester='') - playlist.add_song(song) - songsList.append(song) - - # We need to trigger and wait multiple tasks, so we create multiple tasks with asyncio - # and then we await for each one of them. We use this because download_song is a Coroutine - tasks: List[asyncio.Task] = [] - for song in songsList: - task = asyncio.create_task(self.downloader.download_song(song)) - tasks.append(task) - - # Await for each task to finish - for task in tasks: - await task - - self.assertTrue(self.__verifySuccessfullyPreload(songsList)) - - @myAsyncTest - async def test_YoutubeMixPlaylist(self) -> None: - music = await self.searcher.search(self.constants.YT_MIX_URL) - - # Musics from Mix should download only the first music - self.assertTrue(len(music) == 1) - - @myAsyncTest - async def test_musicTitle(self): - playlist = Playlist() - song = Song(self.constants.MUSIC_TITLE_STRING, playlist, '') - playlist.add_song(song) - - task = asyncio.create_task(self.downloader.download_song(song)) - await task - - self.assertFalse(song.problematic) - - def __verifySuccessfullyPreload(self, songs: List[Song]) -> bool: - for song in songs: - if song.title == None: - print('Song failed to download') - return False - - return True - - -if __name__ == 'main': - unittest.main() diff --git a/Tests/VDownloaderTests.py b/Tests/VDownloaderTests.py new file mode 100644 index 0000000..e64b2f1 --- /dev/null +++ b/Tests/VDownloaderTests.py @@ -0,0 +1,91 @@ +from typing import List +from Tests.TestBase import VulkanTesterBase +from Music.Playlist import Playlist +from Music.Song import Song +from asyncio import Task + + +class VulkanDownloaderTest(VulkanTesterBase): + def __init__(self) -> None: + super().__init__() + + def test_emptyString(self) -> bool: + musicsList = self._runner.run_coroutine(self._downloader.extract_info('')) + + if musicsList == []: + return True + else: + return False + + def test_YoutubeMusicUrl(self) -> bool: + musicsList = self._runner.run_coroutine(self._searcher.search(self._constants.YT_MUSIC_URL)) + + if len(musicsList) > 0: + print(musicsList[0]) + return True + else: + return False + + def test_YoutubeChannelPlaylist(self) -> None: + # Search the link to determine names + musicsList = self._runner.run_coroutine( + self._searcher.search(self._constants.YT_CHANNEL_PLAYLIST_URL)) + + 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 song.problematic or song.title == None: + return False + + return True + + def test_YoutubeMixPlaylist(self) -> None: + # Search the link to determine names + music = 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: + return False + + def test_musicTitle(self): + playlist = Playlist() + song = Song(self._constants.MUSIC_TITLE_STRING, 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_YoutubePersonalPlaylist(self) -> None: + musics = self._runner.run_coroutine( + self._searcher.search(self._constants.YT_PERSONAL_PLAYLIST_URL)) + + if len(musics) > 0: + print(musics) + return True + else: + return False diff --git a/Tests/__init__.py b/Tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/requirements.txt b/requirements.txt index cc7481cdd922ab182e1b75cf75d91937f7b5cc16..2fe43553aa8cb40a26f5998b5c5366ec73a13ae3 100644 GIT binary patch literal 244 zcmZXOO$x$5429oX@Fi)_pDE3yh8^4gdfE literal 116 zcmX}j%L)Q93`XI7-erDzaZnd|5)l_|(8ACJ8^yl8x|!wq;M6vXJ}Yc8M;wu{9{?NWuDf+Tam@`%yrr!QnLOdhT^rde0%a9T>PDtd_TEE}q diff --git a/run_tests.py b/run_tests.py new file mode 100644 index 0000000..430eeb5 --- /dev/null +++ b/run_tests.py @@ -0,0 +1,5 @@ +from Tests.VDownloaderTests import VulkanDownloaderTest + + +tester = VulkanDownloaderTest() +tester.run() From 8336a95edaf4e890f7fa04a8eb3f2b95ee39bc2c Mon Sep 17 00:00:00 2001 From: Rafael Vargas Date: Sun, 10 Jul 2022 13:47:08 -0300 Subject: [PATCH 4/6] Adding Spotify Tests --- Tests/TestsHelper.py | 9 +++++- Tests/VSpotifyTests.py | 62 ++++++++++++++++++++++++++++++++++++++++++ run_tests.py | 3 ++ 3 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 Tests/VSpotifyTests.py diff --git a/Tests/TestsHelper.py b/Tests/TestsHelper.py index 9521a61..a44326f 100644 --- a/Tests/TestsHelper.py +++ b/Tests/TestsHelper.py @@ -5,10 +5,17 @@ class TestsConstants(Singleton): def __init__(self) -> None: if not super().created: self.EMPTY_STRING_ERROR_MSG = 'Downloader with Empty String should be empty list.' - self.SPOTIFY_TRACK_URL = 'https://open.spotify.com/track/7wpnz7hje4FbnjZuWQtJHP' self.MUSIC_TITLE_STRING = 'Experience || AMV || Anime Mix' + self.YT_MUSIC_URL = 'https://www.youtube.com/watch?v=MvJoiv842mk' self.YT_MIX_URL = 'https://www.youtube.com/watch?v=ePjtnSPFWK8&list=RDMMePjtnSPFWK8&start_radio=1' self.YT_PERSONAL_PLAYLIST_URL = 'https://www.youtube.com/playlist?list=PLbbKJHHZR9ShYuKAr71cLJCFbYE-83vhS' # Links from playlists in channels some times must be extracted with force by Downloader self.YT_CHANNEL_PLAYLIST_URL = 'https://www.youtube.com/watch?v=MvJoiv842mk&list=PLAI1099Tvk0zWU8X4dwc4vv4MpePQ4DLl' + + self.SPOTIFY_TRACK_URL = 'https://open.spotify.com/track/7wpnz7hje4FbnjZuWQtJHP' + self.SPOTIFY_PLAYLIST_URL = 'https://open.spotify.com/playlist/37i9dQZF1EIV9u4LtkBkSF' + self.SPOTIFY_ARTIST_URL = 'https://open.spotify.com/artist/4HF14RSTZQcEafvfPCFEpI' + 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' diff --git a/Tests/VSpotifyTests.py b/Tests/VSpotifyTests.py new file mode 100644 index 0000000..cd58cb3 --- /dev/null +++ b/Tests/VSpotifyTests.py @@ -0,0 +1,62 @@ +from Tests.TestBase import VulkanTesterBase + + +class VulkanSpotifyTest(VulkanTesterBase): + def __init__(self) -> None: + super().__init__() + + def test_spotifyTrack(self) -> bool: + musics = self._runner.run_coroutine( + self._searcher.search(self._constants.SPOTIFY_TRACK_URL)) + + if len(musics) > 0: + return True + else: + return False + + def test_spotifyPlaylist(self) -> bool: + musics = self._runner.run_coroutine( + self._searcher.search(self._constants.SPOTIFY_PLAYLIST_URL)) + + if len(musics) > 0: + return True + else: + return False + + def test_spotifyArtist(self) -> bool: + musics = self._runner.run_coroutine( + self._searcher.search(self._constants.SPOTIFY_ARTIST_URL)) + + if len(musics) > 0: + return True + else: + return False + + def test_spotifyAlbum(self) -> bool: + musics = self._runner.run_coroutine( + self._searcher.search(self._constants.SPOTIFY_ARTIST_URL)) + + if len(musics) > 0: + return True + else: + return False + + def test_spotifyWrongUrlOne(self) -> bool: + musics = self._runner.run_coroutine( + self._searcher.search(self._constants.SPOTIFY_WRONG1_URL)) + + print(musics) + if len(musics) == 0: + return True + else: + return False + + def test_spotifyWrongUrlTwo(self) -> bool: + musics = self._runner.run_coroutine( + self._searcher.search(self._constants.SPOTIFY_WRONG2_URL)) + + print(musics) + if len(musics) == 0: + return True + else: + return False diff --git a/run_tests.py b/run_tests.py index 430eeb5..03417ba 100644 --- a/run_tests.py +++ b/run_tests.py @@ -1,5 +1,8 @@ from Tests.VDownloaderTests import VulkanDownloaderTest +from Tests.VSpotifyTests import VulkanSpotifyTest tester = VulkanDownloaderTest() +# tester.run() +tester = VulkanSpotifyTest() tester.run() From 7e9a6d45c05c37efe56ecf79dbf59fd9906fec7c Mon Sep 17 00:00:00 2001 From: Rafael Vargas Date: Sun, 10 Jul 2022 14:18:07 -0300 Subject: [PATCH 5/6] Upgrading Spotify Invalid Input Dealing --- Config/Messages.py | 10 ++++++++- Music/Searcher.py | 8 ++++++- Music/Spotify.py | 48 ++++++++++++++++++++++++++++++------------ Tests/VSpotifyTests.py | 30 +++++++++++++++----------- 4 files changed, 69 insertions(+), 27 deletions(-) diff --git a/Config/Messages.py b/Config/Messages.py index 3f31f57..afe26b0 100644 --- a/Config/Messages.py +++ b/Config/Messages.py @@ -76,5 +76,13 @@ class SearchMessages(Singleton): 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' + 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.' + + +class SpotifyMessages(Singleton): + def __init__(self) -> None: + if not super().created: + self.INVALID_SPOTIFY_URL = 'Invalid Spotify URL, verify your link.' + self.GENERIC_TITLE = 'URL could not be processed' diff --git a/Music/Searcher.py b/Music/Searcher.py index 42a7855..30624a9 100644 --- a/Music/Searcher.py +++ b/Music/Searcher.py @@ -29,8 +29,14 @@ class Searcher(): elif provider == Provider.Spotify: try: musics = self.__Spotify.search(track) + if musics == None or len(musics) == 0: + raise SpotifyError(self.__messages.SPOTIFY_ERROR, self.__messages.GENERIC_TITLE) + return musics - except: + 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) elif provider == Provider.Name: diff --git a/Music/Spotify.py b/Music/Spotify.py index 758060a..5e575f4 100644 --- a/Music/Spotify.py +++ b/Music/Spotify.py @@ -1,10 +1,14 @@ from spotipy import Spotify from spotipy.oauth2 import SpotifyClientCredentials +from spotipy.exceptions import SpotifyException +from Exceptions.Exceptions import SpotifyError from Config.Configs import Configs +from Config.Messages import SpotifyMessages class SpotifySearch(): def __init__(self) -> None: + self.__messages = SpotifyMessages() self.__config = Configs() self.__connected = False self.__connect() @@ -17,22 +21,28 @@ class SpotifySearch(): except Exception as e: print(f'DEVELOPER NOTE -> Spotify Connection Error {e}') - def search(self, music: str) -> list: - type = music.split('/')[3].split('?')[0] - code = music.split('/')[4].split('?')[0] + def search(self, url: str) -> list: + if not self.__checkUrlValid(url): + raise SpotifyError(self.__messages.INVALID_SPOTIFY_URL, self.__messages.GENERIC_TITLE) + + type = url.split('/')[3].split('?')[0] + code = url.split('/')[4].split('?')[0] 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) + try: + 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 + return musics + except SpotifyException: + raise SpotifyError(self.__messages.INVALID_SPOTIFY_URL, self.__messages.GENERIC_TITLE) def __get_album(self, code: str) -> list: results = self.__api.album_tracks(code) @@ -94,3 +104,15 @@ class SpotifySearch(): title += f'{artist["name"]} ' return title + + def __checkUrlValid(self, url: str) -> bool: + try: + type = url.split('/')[3].split('?')[0] + code = url.split('/')[4].split('?')[0] + + if type == '' or code == '': + return False + + return True + except: + return False diff --git a/Tests/VSpotifyTests.py b/Tests/VSpotifyTests.py index cd58cb3..ad5235c 100644 --- a/Tests/VSpotifyTests.py +++ b/Tests/VSpotifyTests.py @@ -1,4 +1,7 @@ +from requests import HTTPError +from Music.Spotify import SpotifySearch from Tests.TestBase import VulkanTesterBase +from Exceptions.Exceptions import SpotifyError class VulkanSpotifyTest(VulkanTesterBase): @@ -41,22 +44,25 @@ class VulkanSpotifyTest(VulkanTesterBase): else: return False - def test_spotifyWrongUrlOne(self) -> bool: - musics = self._runner.run_coroutine( - self._searcher.search(self._constants.SPOTIFY_WRONG1_URL)) + def test_spotifyWrongUrlShouldThrowException(self) -> bool: + try: + musics = self._runner.run_coroutine( + self._searcher.search(self._constants.SPOTIFY_WRONG1_URL)) - print(musics) - if len(musics) == 0: + except SpotifyError as e: + print(f'Spotify Error -> {e.message}') return True - else: + except Exception as e: + print(e) return False - def test_spotifyWrongUrlTwo(self) -> bool: - musics = self._runner.run_coroutine( - self._searcher.search(self._constants.SPOTIFY_WRONG2_URL)) + def test_spotifyWrongUrlTwoShouldThrowException(self) -> bool: + try: + musics = self._runner.run_coroutine( + self._searcher.search(self._constants.SPOTIFY_WRONG2_URL)) - print(musics) - if len(musics) == 0: + except SpotifyError as e: + print(f'Spotify Error -> {e.message}') return True - else: + except Exception as e: return False From c826af229c62a7e52e09755861e093808fc83196 Mon Sep 17 00:00:00 2001 From: Rafael Vargas Date: Sun, 10 Jul 2022 14:27:40 -0300 Subject: [PATCH 6/6] Upgrading clean code --- Controllers/ControllerResponse.py | 8 ++++---- Controllers/MoveController.py | 4 ++-- Controllers/PlayController.py | 4 ++-- Controllers/RemoveController.py | 4 ++-- Exceptions/Exceptions.py | 27 +++++++++++++-------------- Tests/VSpotifyTests.py | 2 -- Views/Embeds.py | 4 ++-- 7 files changed, 25 insertions(+), 28 deletions(-) diff --git a/Controllers/ControllerResponse.py b/Controllers/ControllerResponse.py index 9aa6da1..09cf7f3 100644 --- a/Controllers/ControllerResponse.py +++ b/Controllers/ControllerResponse.py @@ -1,13 +1,13 @@ from typing import Union from discord.ext.commands import Context -from Exceptions.Exceptions import Error +from Exceptions.Exceptions import VulkanError from discord import Embed class ControllerResponse: - def __init__(self, ctx: Context, embed: Embed = None, error: Error = None) -> None: + def __init__(self, ctx: Context, embed: Embed = None, error: VulkanError = None) -> None: self.__ctx: Context = ctx - self.__error: Error = error + self.__error: VulkanError = error self.__embed: Embed = embed self.__success = False if error else True @@ -19,7 +19,7 @@ class ControllerResponse: def embed(self) -> Union[Embed, None]: return self.__embed - def error(self) -> Union[Error, None]: + def error(self) -> Union[VulkanError, None]: return self.__error @property diff --git a/Controllers/MoveController.py b/Controllers/MoveController.py index c4c06eb..e5292f0 100644 --- a/Controllers/MoveController.py +++ b/Controllers/MoveController.py @@ -3,7 +3,7 @@ from discord.ext.commands import Context from discord import Client from Controllers.AbstractController import AbstractController from Controllers.ControllerResponse import ControllerResponse -from Exceptions.Exceptions import BadCommandUsage, Error, InvalidInput, NumberRequired, UnknownError, WrongLength +from Exceptions.Exceptions import BadCommandUsage, VulkanError, InvalidInput, NumberRequired, UnknownError from Music.Downloader import Downloader @@ -44,7 +44,7 @@ class MoveController(AbstractController): error = UnknownError() return ControllerResponse(self.ctx, embed, error) - def __validate_input(self, pos1: str, pos2: str) -> Union[Error, None]: + def __validate_input(self, pos1: str, pos2: str) -> Union[VulkanError, None]: try: pos1 = int(pos1) pos2 = int(pos2) diff --git a/Controllers/PlayController.py b/Controllers/PlayController.py index 693bfea..66356ed 100644 --- a/Controllers/PlayController.py +++ b/Controllers/PlayController.py @@ -1,5 +1,5 @@ import asyncio -from Exceptions.Exceptions import DownloadingError, Error +from Exceptions.Exceptions import DownloadingError, VulkanError from discord.ext.commands import Context from discord import Client from Controllers.AbstractController import AbstractController @@ -64,7 +64,7 @@ class PlayController(AbstractController): return response except Exception as err: - if isinstance(err, Error): + if isinstance(err, VulkanError): # If error was already processed print(f'DEVELOPER NOTE -> PlayController Error: {err.message}') error = err embed = self.embeds.CUSTOM_ERROR(error) diff --git a/Controllers/RemoveController.py b/Controllers/RemoveController.py index 731ba6d..97bf73d 100644 --- a/Controllers/RemoveController.py +++ b/Controllers/RemoveController.py @@ -3,7 +3,7 @@ from discord.ext.commands import Context from discord import Client from Controllers.AbstractController import AbstractController from Controllers.ControllerResponse import ControllerResponse -from Exceptions.Exceptions import BadCommandUsage, Error, ErrorRemoving, InvalidInput, NumberRequired, UnknownError +from Exceptions.Exceptions import BadCommandUsage, VulkanError, ErrorRemoving, InvalidInput, NumberRequired class RemoveController(AbstractController): @@ -38,7 +38,7 @@ class RemoveController(AbstractController): embed = self.embeds.ERROR_REMOVING() return ControllerResponse(self.ctx, embed, error) - def __validate_input(self, position: str) -> Union[Error, None]: + def __validate_input(self, position: str) -> Union[VulkanError, None]: try: position = int(position) except: diff --git a/Exceptions/Exceptions.py b/Exceptions/Exceptions.py index 2a80acd..6c29478 100644 --- a/Exceptions/Exceptions.py +++ b/Exceptions/Exceptions.py @@ -1,8 +1,7 @@ -from Config.Configs import Configs from Config.Messages import Messages -class Error(Exception): +class VulkanError(Exception): def __init__(self, message='', title='', *args: object) -> None: self.__message = message self.__title = title @@ -17,7 +16,7 @@ class Error(Exception): return self.__title -class ImpossibleMove(Error): +class ImpossibleMove(VulkanError): def __init__(self, message='', title='', *args: object) -> None: message = Messages() if title == '': @@ -25,56 +24,56 @@ class ImpossibleMove(Error): super().__init__(message, title, *args) -class MusicUnavailable(Error): +class MusicUnavailable(VulkanError): def __init__(self, message='', title='', *args: object) -> None: super().__init__(message, title, *args) -class YoutubeError(Error): +class YoutubeError(VulkanError): def __init__(self, message='', title='', *args: object) -> None: super().__init__(message, title, *args) -class BadCommandUsage(Error): +class BadCommandUsage(VulkanError): def __init__(self, message='', title='', *args: object) -> None: super().__init__(message, title, *args) -class DownloadingError(Error): +class DownloadingError(VulkanError): def __init__(self, message='', title='', *args: object) -> None: super().__init__(message, title, *args) -class SpotifyError(Error): +class SpotifyError(VulkanError): def __init__(self, message='', title='', *args: object) -> None: super().__init__(message, title, *args) -class UnknownError(Error): +class UnknownError(VulkanError): def __init__(self, message='', title='', *args: object) -> None: super().__init__(message, title, *args) -class InvalidInput(Error): +class InvalidInput(VulkanError): def __init__(self, message='', title='', *args: object) -> None: super().__init__(message, title, *args) -class WrongLength(Error): +class WrongLength(VulkanError): def __init__(self, message='', title='', *args: object) -> None: super().__init__(message, title, *args) -class ErrorMoving(Error): +class ErrorMoving(VulkanError): def __init__(self, message='', title='', *args: object) -> None: super().__init__(message, title, *args) -class ErrorRemoving(Error): +class ErrorRemoving(VulkanError): def __init__(self, message='', title='', *args: object) -> None: super().__init__(message, title, *args) -class NumberRequired(Error): +class NumberRequired(VulkanError): def __init__(self, message='', title='', *args: object) -> None: super().__init__(message, title, *args) diff --git a/Tests/VSpotifyTests.py b/Tests/VSpotifyTests.py index ad5235c..7220319 100644 --- a/Tests/VSpotifyTests.py +++ b/Tests/VSpotifyTests.py @@ -1,5 +1,3 @@ -from requests import HTTPError -from Music.Spotify import SpotifySearch from Tests.TestBase import VulkanTesterBase from Exceptions.Exceptions import SpotifyError diff --git a/Views/Embeds.py b/Views/Embeds.py index 0fea62a..23571ee 100644 --- a/Views/Embeds.py +++ b/Views/Embeds.py @@ -1,5 +1,5 @@ from Config.Messages import Messages -from Exceptions.Exceptions import Error +from Exceptions.Exceptions import VulkanError from discord import Embed from Config.Configs import Configs from Config.Colors import Colors @@ -130,7 +130,7 @@ class Embeds: ) return embed - def CUSTOM_ERROR(self, error: Error) -> Embed: + def CUSTOM_ERROR(self, error: VulkanError) -> Embed: embed = Embed( title=error.title, description=error.message,