diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 83cf1ef..dff7fc9 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -23,6 +23,9 @@ jobs: run: | python -m pip install --upgrade pip pip install -r requirements.txt + - name: Lint with black + run: | + black . --check - name: Test with pytest run: | pytest diff --git a/README.md b/README.md index ac671f8..4c21953 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ [![Python package](https://github.com/patkub/console-songs/actions/workflows/python-app.yml/badge.svg)](https://github.com/patkub/console-songs/actions/workflows/python-app.yml) ![Python 3.11+ Required](https://img.shields.io/badge/python-3.11+-brightgreen.svg) +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) ### Idea 1. Fetch song lyrics @@ -37,6 +38,11 @@ pip3 install -r requirements.txt You should now be able to run `python3 songs.py --help` and see usage. +When making code changes, remember to format code with black: +``` +black . +``` + ### Usage Provide the song and optionally the artist's name diff --git a/display_lyrics/DisplayLyrics.py b/display_lyrics/DisplayLyrics.py index 2a58358..a55d290 100644 --- a/display_lyrics/DisplayLyrics.py +++ b/display_lyrics/DisplayLyrics.py @@ -41,17 +41,22 @@ def display_lyrics(song_info, original_lyrics, english_lyrics): # Display portion that has both original and english translation for i in range(min_stanzas): - side_by_side.print_side_by_side(split_original_lyrics[i], split_english_lyrics[i]) + side_by_side.print_side_by_side( + split_original_lyrics[i], split_english_lyrics[i] + ) print() if len_original_lyrics > len_english_lyrics: # original lyrics have more than english translation for i in range(len_original_lyrics - min_stanzas): - side_by_side.print_side_by_side(split_original_lyrics[min_stanzas - 1 + i], "") + side_by_side.print_side_by_side( + split_original_lyrics[min_stanzas - 1 + i], "" + ) print() elif len_english_lyrics > len_original_lyrics: # english translated lyrics have more than original for i in range(len_english_lyrics - min_stanzas): - side_by_side.print_side_by_side("", split_english_lyrics[min_stanzas - 1 + i]) + side_by_side.print_side_by_side( + "", split_english_lyrics[min_stanzas - 1 + i] + ) print() - diff --git a/display_lyrics/Lyrics.py b/display_lyrics/Lyrics.py index 53759ee..193ee5d 100644 --- a/display_lyrics/Lyrics.py +++ b/display_lyrics/Lyrics.py @@ -9,7 +9,7 @@ def __init__(self, lyrics): def normalize_lyrics(self, lyrics): # normalize line endings - self.lyrics = re.sub(r'[\r\n][\r\n]{2,}', '\n\n', lyrics) + self.lyrics = re.sub(r"[\r\n][\r\n]{2,}", "\n\n", lyrics) # split into stanzas self.stanzas = self.lyrics.split("\n\n") @@ -44,4 +44,3 @@ def get_min_stanzas(self, other): Get the minimum number of stanzas between two Lyrics """ return min(len(self.stanzas), len(other.stanzas)) - diff --git a/display_lyrics/__init__.py b/display_lyrics/__init__.py index a83bb6e..97bb107 100644 --- a/display_lyrics/__init__.py +++ b/display_lyrics/__init__.py @@ -1,2 +1,2 @@ from .DisplayLyrics import DisplayLyrics -from .Lyrics import Lyrics \ No newline at end of file +from .Lyrics import Lyrics diff --git a/display_lyrics/test_display_lyrics.py b/display_lyrics/test_display_lyrics.py index 3fea392..b1e4d2f 100644 --- a/display_lyrics/test_display_lyrics.py +++ b/display_lyrics/test_display_lyrics.py @@ -24,13 +24,13 @@ def get_expected_print_calls(song_info): call("\n{}".format(song_info.full_title)), call("{}\n".format(song_info.url)), call(), - call() + call(), ] return expected_print_calls -@patch('builtins.print') -@patch('side_by_side.print_side_by_side', fake_print_side_by_side) +@patch("builtins.print") +@patch("side_by_side.print_side_by_side", fake_print_side_by_side) def test_display_lyrics_method(mocked_print): """ Displays original and English translated lyrics side-by-side @@ -55,8 +55,8 @@ class FakeSongInfo: mocked_print.assert_has_calls(expected_print_calls) -@patch('builtins.print') -@patch('side_by_side.print_side_by_side', fake_print_side_by_side) +@patch("builtins.print") +@patch("side_by_side.print_side_by_side", fake_print_side_by_side) def test_display_lyrics_method_original_longer(mocked_print): """ Displays original and English translated lyrics side-by-side @@ -82,8 +82,8 @@ class FakeSongInfo: mocked_print.assert_has_calls(expected_print_calls) -@patch('builtins.print') -@patch('side_by_side.print_side_by_side', fake_print_side_by_side) +@patch("builtins.print") +@patch("side_by_side.print_side_by_side", fake_print_side_by_side) def test_display_lyrics_method_original_shorter(mocked_print): """ Displays original and English translated lyrics side-by-side @@ -107,4 +107,3 @@ class FakeSongInfo: # expect to print: title, url, and two blank lines expected_print_calls = get_expected_print_calls(song_info) mocked_print.assert_has_calls(expected_print_calls) - diff --git a/fetch_lyrics/FetchLyrics.py b/fetch_lyrics/FetchLyrics.py index 9985773..a6554a5 100644 --- a/fetch_lyrics/FetchLyrics.py +++ b/fetch_lyrics/FetchLyrics.py @@ -22,7 +22,7 @@ def fetch_lyrics(self, song, artist): @return: object: song info """ - #print("Looking for song {} by artist {}".format(song, artist)) + # print("Looking for song {} by artist {}".format(song, artist)) # https://genius.com/Mihail-ma-ucide-ea-lyrics song = self.genius.search_song(song, artist) return song diff --git a/fetch_lyrics/PatchedGenius.py b/fetch_lyrics/PatchedGenius.py index 8115c29..dc30467 100644 --- a/fetch_lyrics/PatchedGenius.py +++ b/fetch_lyrics/PatchedGenius.py @@ -8,6 +8,7 @@ class PatchedGenius(Genius): # pragma: no cover A patched version of Genius with @xanthon's fixes to remove ads and unwanted content in lyrics Credit: @xanthon from https://github.com/johnwmillr/LyricsGenius/pull/272 . Thanks! """ + def __init__(self, *args, **kwargs): super(PatchedGenius, self).__init__(*args, **kwargs) @@ -37,35 +38,36 @@ def lyrics(self, song_id=None, song_url=None, remove_section_headers=False): if song_url: path = song_url.replace("https://genius.com/", "") else: - path = self.song(song_id)['song']['path'][1:] + path = self.song(song_id)["song"]["path"][1:] # Scrape the song lyrics from the HTML html = BeautifulSoup( - self._make_request(path, web=True).replace('
', '\n'), - "html.parser" + self._make_request(path, web=True).replace("
", "\n"), "html.parser" ) # Determine the class of the div divs = html.find_all("div", class_=re.compile("^lyrics$|Lyrics__Container")) if divs is None or len(divs) <= 0: if self.verbose: - print("Couldn't find the lyrics section. " - "Please report this if the song has lyrics.\n" - "Song URL: https://genius.com/{}".format(path)) + print( + "Couldn't find the lyrics section. " + "Please report this if the song has lyrics.\n" + "Song URL: https://genius.com/{}".format(path) + ) return None # remove ads from div - ads = html.find("div", {'class': re.compile("RightSidebar__Container")}) + ads = html.find("div", {"class": re.compile("RightSidebar__Container")}) ads.decompose() # remove header - header = html.find("div", {'class': re.compile("LyricsHeader__Container")}) + header = html.find("div", {"class": re.compile("LyricsHeader__Container")}) header.decompose() # remove embed note / footer - footer = html.find("div", {'class': re.compile("LyricsFooter__Container")}) + footer = html.find("div", {"class": re.compile("LyricsFooter__Container")}) footer.decompose() lyrics = "\n".join([div.get_text() for div in divs]) # Remove [Verse], [Bridge], etc. if self.remove_section_headers or remove_section_headers: - lyrics = re.sub(r'(\[.*?\])*', '', lyrics) - lyrics = re.sub('\n{2}', '\n', lyrics) # Gaps between verses + lyrics = re.sub(r"(\[.*?\])*", "", lyrics) + lyrics = re.sub("\n{2}", "\n", lyrics) # Gaps between verses return lyrics.strip("\n") diff --git a/fetch_lyrics/__init__.py b/fetch_lyrics/__init__.py index 1744e4b..07c8c91 100644 --- a/fetch_lyrics/__init__.py +++ b/fetch_lyrics/__init__.py @@ -1,2 +1,2 @@ from .FetchLyrics import FetchLyrics -from .PatchedGenius import PatchedGenius \ No newline at end of file +from .PatchedGenius import PatchedGenius diff --git a/fetch_lyrics/test_fetch_lyrics.py b/fetch_lyrics/test_fetch_lyrics.py index e95f284..9b0d090 100644 --- a/fetch_lyrics/test_fetch_lyrics.py +++ b/fetch_lyrics/test_fetch_lyrics.py @@ -2,7 +2,7 @@ from unittest.mock import patch -@patch('lyricsgenius.Genius.search_song') +@patch("lyricsgenius.Genius.search_song") def test_fetch_lyrics_calls_genius_api(mock_search_song): """ Fetches lyrics for a song given its name and artist by calling Genius API @@ -19,4 +19,3 @@ def test_fetch_lyrics_calls_genius_api(mock_search_song): # Assert underlying mocked lyricsgenius.Genius.search_song was called mock_search_song.assert_called_with("fake_song", "fake_artist") - diff --git a/requirements.txt b/requirements.txt index 1e8521d..7c971c5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,6 +7,7 @@ side-by-side # Unit Tests pytest pytest-cov +black # PatchedGenius beautifulsoup4 diff --git a/songs.py b/songs.py index f74213e..d44108d 100644 --- a/songs.py +++ b/songs.py @@ -12,8 +12,10 @@ # Class to get song lyrics from Genius from fetch_lyrics import FetchLyrics, PatchedGenius + # Class to translate lyrics using Microsoft Azure AI Translator from translate_lyrics import TranslateLyrics + # Display output from display_lyrics import DisplayLyrics @@ -44,7 +46,9 @@ def process_song(song, artist, access_keys, genius_patch): # # Translate lyrics to English using Microsoft Azure AI Translator # - lyrics_translator = TranslateLyrics(access_keys["MS_TRANSLATOR_KEY"], access_keys["MS_TRANSLATOR_REGION"]) + lyrics_translator = TranslateLyrics( + access_keys["MS_TRANSLATOR_KEY"], access_keys["MS_TRANSLATOR_REGION"] + ) english_translation = lyrics_translator.translate_lyrics(song_lyrics) # @@ -53,7 +57,7 @@ def process_song(song, artist, access_keys, genius_patch): DisplayLyrics.display_lyrics(song_info, song_lyrics, english_translation) -if __name__ == '__main__': # pragma: no cover +if __name__ == "__main__": # pragma: no cover # # Parse arguments # @@ -62,19 +66,21 @@ def process_song(song, artist, access_keys, genius_patch): formatter_class=argparse.ArgumentDefaultsHelpFormatter ) parser.add_argument("song", nargs="+") - parser.add_argument('--genius-patch', - action=argparse.BooleanOptionalAction, - default=True, - help='Use patched version of Genius API') + parser.add_argument( + "--genius-patch", + action=argparse.BooleanOptionalAction, + default=True, + help="Use patched version of Genius API", + ) args = parser.parse_args() song = args.song[0] artist = args.song[1] if len(args.song) > 1 else None access_keys = { - 'GENIUS_ACCESS_TOKEN': os.getenv("GENIUS_ACCESS_TOKEN"), - 'MS_TRANSLATOR_KEY': os.getenv("MS_TRANSLATOR_KEY"), - 'MS_TRANSLATOR_REGION': os.getenv("MS_TRANSLATOR_REGION") + "GENIUS_ACCESS_TOKEN": os.getenv("GENIUS_ACCESS_TOKEN"), + "MS_TRANSLATOR_KEY": os.getenv("MS_TRANSLATOR_KEY"), + "MS_TRANSLATOR_REGION": os.getenv("MS_TRANSLATOR_REGION"), } # diff --git a/test_songs.py b/test_songs.py index 96660eb..f2fb2b4 100644 --- a/test_songs.py +++ b/test_songs.py @@ -6,10 +6,17 @@ mockedFetchLyrics.lyrics = "mocked_lyrics" -@patch('fetch_lyrics.FetchLyrics.FetchLyrics.fetch_lyrics', return_value=mockedFetchLyrics) -@patch('translate_lyrics.TranslateLyrics.TranslateLyrics.translate_lyrics', return_value="english translation") -@patch('display_lyrics.DisplayLyrics.DisplayLyrics.display_lyrics') -def test_process_song(mocked_display_lyrics, mocked_translate_lyrics, mocked_fetch_lyrics): +@patch( + "fetch_lyrics.FetchLyrics.FetchLyrics.fetch_lyrics", return_value=mockedFetchLyrics +) +@patch( + "translate_lyrics.TranslateLyrics.TranslateLyrics.translate_lyrics", + return_value="english translation", +) +@patch("display_lyrics.DisplayLyrics.DisplayLyrics.display_lyrics") +def test_process_song( + mocked_display_lyrics, mocked_translate_lyrics, mocked_fetch_lyrics +): """ Fetch song lyrics, translate to English, and display original and English side-by-side lyrics. """ @@ -18,9 +25,9 @@ def test_process_song(mocked_display_lyrics, mocked_translate_lyrics, mocked_fet song = "test_song_name" artist = "test_artist_name" access_keys = { - 'GENIUS_ACCESS_TOKEN': "fake_token_1", - 'MS_TRANSLATOR_KEY': "fake_token_2", - 'MS_TRANSLATOR_REGION': "fake_token_3" + "GENIUS_ACCESS_TOKEN": "fake_token_1", + "MS_TRANSLATOR_KEY": "fake_token_2", + "MS_TRANSLATOR_REGION": "fake_token_3", } experimental = False @@ -33,13 +40,20 @@ def test_process_song(mocked_display_lyrics, mocked_translate_lyrics, mocked_fet # mocked lyrics get translated mocked_translate_lyrics.assert_called_with("mocked_lyrics") # display is called with song info, original and translated lyrics - mocked_display_lyrics.assert_called_with(ANY, "mocked_lyrics", "english translation") + mocked_display_lyrics.assert_called_with( + ANY, "mocked_lyrics", "english translation" + ) -@patch('fetch_lyrics.FetchLyrics.FetchLyrics.fetch_lyrics', return_value=None) -@patch('translate_lyrics.TranslateLyrics.TranslateLyrics.translate_lyrics', return_value="english translation") -@patch('display_lyrics.DisplayLyrics.DisplayLyrics.display_lyrics') -def test_process_song_null(mocked_display_lyrics, mocked_translate_lyrics, mocked_fetch_lyrics): +@patch("fetch_lyrics.FetchLyrics.FetchLyrics.fetch_lyrics", return_value=None) +@patch( + "translate_lyrics.TranslateLyrics.TranslateLyrics.translate_lyrics", + return_value="english translation", +) +@patch("display_lyrics.DisplayLyrics.DisplayLyrics.display_lyrics") +def test_process_song_null( + mocked_display_lyrics, mocked_translate_lyrics, mocked_fetch_lyrics +): """ Null song fetched, translation and display are not called """ @@ -48,9 +62,9 @@ def test_process_song_null(mocked_display_lyrics, mocked_translate_lyrics, mocke song = "test_song_name" artist = "test_artist_name" access_keys = { - 'GENIUS_ACCESS_TOKEN': "fake_token_1", - 'MS_TRANSLATOR_KEY': "fake_token_2", - 'MS_TRANSLATOR_REGION': "fake_token_3" + "GENIUS_ACCESS_TOKEN": "fake_token_1", + "MS_TRANSLATOR_KEY": "fake_token_2", + "MS_TRANSLATOR_REGION": "fake_token_3", } experimental = False diff --git a/translate_lyrics/TranslateLyrics.py b/translate_lyrics/TranslateLyrics.py index 8572fb4..d5740c5 100644 --- a/translate_lyrics/TranslateLyrics.py +++ b/translate_lyrics/TranslateLyrics.py @@ -1,6 +1,7 @@ import requests, uuid from requests.exceptions import RequestException + class TranslateLyrics: def __init__(self, translator_key, region): """ @@ -20,24 +21,22 @@ def translate_lyrics(self, lyrics): # If you encounter any issues with the base_url or path, make sure # that you are using the latest endpoint: https://docs.microsoft.com/azure/cognitive-services/translator/reference/v3-0-translate endpoint = "https://api.cognitive.microsofttranslator.com/" - path = '/translate?api-version=3.0' + path = "/translate?api-version=3.0" # from romanian to english # params = '&from=ro&to=en' # or detect original language, and translate to english - params = '&to=en' + params = "&to=en" constructed_url = endpoint + path + params headers = { - 'Ocp-Apim-Subscription-Key': self.subscription_key, - 'Ocp-Apim-Subscription-Region': self.region, - 'Content-type': 'application/json', - 'X-ClientTraceId': str(uuid.uuid4()) + "Ocp-Apim-Subscription-Key": self.subscription_key, + "Ocp-Apim-Subscription-Region": self.region, + "Content-type": "application/json", + "X-ClientTraceId": str(uuid.uuid4()), } # You can pass more than one object in body. - body = [{ - 'text': lyrics - }] + body = [{"text": lyrics}] english_translation = "" try: diff --git a/translate_lyrics/__init__.py b/translate_lyrics/__init__.py index 8493be2..5277168 100644 --- a/translate_lyrics/__init__.py +++ b/translate_lyrics/__init__.py @@ -1 +1 @@ -from .TranslateLyrics import TranslateLyrics \ No newline at end of file +from .TranslateLyrics import TranslateLyrics diff --git a/translate_lyrics/test_translate_lyrics.py b/translate_lyrics/test_translate_lyrics.py index 1c25707..201a4e9 100644 --- a/translate_lyrics/test_translate_lyrics.py +++ b/translate_lyrics/test_translate_lyrics.py @@ -22,15 +22,9 @@ def mock_requests_post(*args, **kwargs): # TODO: improve this url detection if args[0].startswith("https://api.cognitive.microsofttranslator.com/"): - return MockResponse([ - { - "translations": [ - { - "text": "mocked translated lyrics" - } - ] - } - ], 200) + return MockResponse( + [{"translations": [{"text": "mocked translated lyrics"}]}], 200 + ) return MockResponse(None, 404) @@ -42,11 +36,7 @@ def mock_requests_post_invalid(*args, **kwargs): @param kwargs: @return: """ - return MockResponse([ - { - "fake_bad_data": "bad" - } - ], 200) + return MockResponse([{"fake_bad_data": "bad"}], 200) def test_translate_lyrics_sets_translator_key(): @@ -59,7 +49,7 @@ def test_translate_lyrics_sets_translator_key(): assert translate_lyrics.subscription_key == translator_key -@mock.patch('requests.post', side_effect=mock_requests_post) +@mock.patch("requests.post", side_effect=mock_requests_post) def test_translate_lyrics(mock_req): """ Translates lyrics @@ -78,7 +68,7 @@ def test_translate_lyrics(mock_req): assert english_translation == "mocked translated lyrics" -@mock.patch('requests.post', side_effect=mock_requests_post_invalid) +@mock.patch("requests.post", side_effect=mock_requests_post_invalid) def test_translate_lyrics_invalid(mock_req): """ Translates lyrics @@ -97,7 +87,7 @@ def test_translate_lyrics_invalid(mock_req): assert english_translation == "" -@mock.patch('requests.post', side_effect=mock_requests_post) +@mock.patch("requests.post", side_effect=mock_requests_post) def test_mock_requests_post_404(mock_req): """ Test 404 @@ -107,7 +97,7 @@ def test_mock_requests_post_404(mock_req): assert resp.status_code == 404 -@mock.patch('requests.post', side_effect=mock_requests_post_invalid) +@mock.patch("requests.post", side_effect=mock_requests_post_invalid) def test_mock_requests_post_invalid(mock_req): """ Test 404