diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 2afa35f..4e0e773 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -1,3 +1,10 @@ +# Version: 0.0.10 +Date: 2024.05.01 +- Fix: + - remove whitespace from the beginning and end of title and artist + - image conversion to jpeg for transparent or RGBA + - index out of range error in when list is empty from musicbrainz + # Version: 0.0.9 Date: 2024.02.06 - Fix: diff --git a/pytest/modules/Audio/test_youtube.py b/pytest/modules/Audio/test_youtube.py new file mode 100644 index 0000000..e2d9bcd --- /dev/null +++ b/pytest/modules/Audio/test_youtube.py @@ -0,0 +1,57 @@ +import unittest +from unittest.mock import patch, MagicMock +from src.modules.Audio.youtube import get_youtube_title +from src.modules.Audio.youtube import download_and_convert_thumbnail + +class TestGetYoutubeTitle(unittest.TestCase): + @patch("yt_dlp.YoutubeDL") + def test_get_youtube_title(self, mock_youtube_dl): + # Arrange + # Also test leading and trailing whitespaces + mock_youtube_dl.return_value.__enter__.return_value.extract_info.return_value = { + "artist": " Test Artist ", + "track": " Test Track ", + "title": " Test Artist - Test Track ", + "channel": " Test Channel " + } + url = " https://www.youtube.com/watch?v=dQw4w9WgXcQ " + + # Act + result = get_youtube_title(url) + + # Assert + self.assertEqual(result, ("Test Artist", "Test Track")) + mock_youtube_dl.assert_called_once() + + @patch("src.modules.Audio.youtube.yt_dlp.YoutubeDL") + @patch("src.modules.Audio.youtube.Image.open") + @patch("src.modules.Audio.youtube.os.path.join") + @patch("src.modules.Audio.youtube.crop_image_to_square") + def test_download_and_convert_thumbnail(self, mock_crop_image_to_square, mock_os_path_join, mock_image_open, mock_youtube_dl): + # Arrange + mock_youtube_dl.return_value.__enter__.return_value.extract_info.return_value = {"thumbnail": "test_thumbnail_url"} + mock_youtube_dl.return_value.__enter__.return_value.urlopen.return_value.read.return_value = b"test_image_data" + mock_image = MagicMock() + mock_image.convert.return_value = mock_image + mock_image_open.return_value = mock_image + mock_os_path_join.return_value = "/path/to/output/test.jpg" + + ydl_opts = {} + url = "https://www.youtube.com/watch?v=dQw4w9WgXcQ" + clear_filename = "test" + output_path = "/path/to/output" + + # Act + download_and_convert_thumbnail(ydl_opts, url, clear_filename, output_path) + + # Assert + mock_youtube_dl.assert_called_once_with(ydl_opts) + mock_youtube_dl.return_value.__enter__.return_value.extract_info.assert_called_once_with(url, download=False) + mock_youtube_dl.return_value.__enter__.return_value.urlopen.assert_called_once_with("test_thumbnail_url") + mock_image.convert.assert_called_once_with('RGB') + mock_os_path_join.assert_called_once_with(output_path, clear_filename + " [CO].jpg") + mock_image.save.assert_called_once_with("/path/to/output/test.jpg", "JPEG") + mock_crop_image_to_square.assert_called_once_with("/path/to/output/test.jpg") + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/pytest/modules/test_musicbrainz_client.py b/pytest/modules/test_musicbrainz_client.py index 7186ff0..e342024 100644 --- a/pytest/modules/test_musicbrainz_client.py +++ b/pytest/modules/test_musicbrainz_client.py @@ -88,6 +88,107 @@ def test_get_music_infos_when_title_and_artist_are_the_same(self, mock_search_re self.assertEqual(year, None) self.assertEqual(genre, None) + @patch('musicbrainzngs.search_artists') + @patch('musicbrainzngs.search_release_groups') + def test_get_music_infos(self, mock_search_release_groups, mock_search_artists): + # Arrange + artist = 'UltraSinger' + title = 'That\'s Rocking!' + search = f'{artist} - {title} (UltrStar 2023) FULL HD' + + # Set up mock return values for the MusicBrainz API calls + mock_search_artists.return_value = { + 'artist-list': [ + {'name': f' {artist} '} # Also test leading and trailing whitespaces + ] + } + + mock_search_release_groups.return_value = { + 'release-group-list': [ + { + 'title': f' {title} ', # Also test leading and trailing whitespaces + 'artist-credit-phrase': f' {artist} ', # Also test leading and trailing whitespaces + 'first-release-date': ' 2023-01-01 ', # Also test leading and trailing whitespaces + 'tag-list': [ + {'name': ' Genre 1 '}, # Also test leading and trailing whitespaces + {'name': ' Genre 2 '} # Also test leading and trailing whitespaces + ] + } + ] + } + + # Act + title, artist, year, genre = get_music_infos(search) + + # Assert + self.assertEqual(title, 'That\'s Rocking!') + self.assertEqual(artist, 'UltraSinger') + self.assertEqual(year, '2023-01-01') + self.assertEqual(genre, 'Genre 1,Genre 2,') + + @patch('musicbrainzngs.search_artists') + @patch('musicbrainzngs.search_release_groups') + def test_get_empty_artist_music_infos(self, mock_search_release_groups, mock_search_artists): + # Arrange + artist = 'UltraSinger' + title = 'That\'s Rocking!' + search = f'{artist} - {title} (UltrStar 2023) FULL HD' + + # Set up mock return values for the MusicBrainz API calls + mock_search_artists.return_value = { + 'artist-list': [] + } + + mock_search_release_groups.return_value = { + 'release-group-list': [ + { + 'title': f' {title} ', # Also test leading and trailing whitespaces + 'artist-credit-phrase': f' {artist} ', # Also test leading and trailing whitespaces + 'first-release-date': ' 2023-01-01 ', # Also test leading and trailing whitespaces + 'tag-list': [ + {'name': ' Genre 1 '}, # Also test leading and trailing whitespaces + {'name': ' Genre 2 '} # Also test leading and trailing whitespaces + ] + } + ] + } + + # Act + title, artist, year, genre = get_music_infos(search) + + # Assert + self.assertEqual(title, None) + self.assertEqual(artist, None) + self.assertEqual(year, None) + self.assertEqual(genre, None) + + @patch('musicbrainzngs.search_artists') + @patch('musicbrainzngs.search_release_groups') + def test_get_empty_release_music_infos(self, mock_search_release_groups, mock_search_artists): + # Arrange + artist = 'UltraSinger' + title = 'That\'s Rocking!' + search = f'{artist} - {title} (UltrStar 2023) FULL HD' + + # Set up mock return values for the MusicBrainz API calls + mock_search_artists.return_value = { + 'artist-list': [ + {'name': f' {artist} '} # Also test leading and trailing whitespaces + ] + } + + mock_search_release_groups.return_value = { + 'release-group-list': [] + } + + # Act + title, artist, year, genre = get_music_infos(search) + + # Assert + self.assertEqual(title, None) + self.assertEqual(artist, None) + self.assertEqual(year, None) + self.assertEqual(genre, None) if __name__ == '__main__': unittest.main() diff --git a/pytest/test_UltraSinger.py b/pytest/test_UltraSinger.py index 4bac9a1..b7275bf 100644 --- a/pytest/test_UltraSinger.py +++ b/pytest/test_UltraSinger.py @@ -1,8 +1,10 @@ """Tests for UltraSinger.py""" import unittest +from unittest.mock import patch, MagicMock from src.UltraSinger import format_separated_string from src.UltraSinger import extract_year +from src.UltraSinger import parse_ultrastar_txt class TestUltraSinger(unittest.TestCase): def test_format_separated_string(self): @@ -22,3 +24,36 @@ def test_extract_year(self): for year in years: self.assertEqual(year, "2023") + + @patch("src.UltraSinger.ultrastar_parser.parse_ultrastar_txt") + @patch("src.UltraSinger.ultrastar_converter.ultrastar_bpm_to_real_bpm") + @patch("src.UltraSinger.os.path.dirname") + @patch("src.UltraSinger.get_unused_song_output_dir") + @patch("src.UltraSinger.os_helper.create_folder") + def test_parse_ultrastar_txt(self, mock_create_folder, mock_get_unused_song_output_dir, + mock_dirname, mock_ultrastar_bpm_to_real_bpm, + mock_parse_ultrastar_txt): + # Arrange + mock_parse_ultrastar_txt.return_value = MagicMock(mp3="test.mp3", + artist=" Test Artist ", # Also test leading and trailing whitespaces + title=" Test Title ") # Also test leading and trailing whitespaces + mock_ultrastar_bpm_to_real_bpm.return_value = 120.0 + mock_dirname.return_value = "\\path\\to\\input" + mock_get_unused_song_output_dir.return_value = "\\path\\to\\output\\Test Artist - Test Title" + mock_create_folder.return_value = None + + # Act + result = parse_ultrastar_txt() + + # Assert + self.assertEqual(result, ("test", + 120.0, + "\\path\\to\\output\\Test Artist - Test Title", + "\\path\\to\\input\\test.mp3", + mock_parse_ultrastar_txt.return_value)) + + mock_parse_ultrastar_txt.assert_called_once() + mock_ultrastar_bpm_to_real_bpm.assert_called_once() + mock_dirname.assert_called_once() + mock_get_unused_song_output_dir.assert_called_once() + mock_create_folder.assert_called_once() diff --git a/src/UltraSinger.py b/src/UltraSinger.py index 942bf20..edb938a 100644 --- a/src/UltraSinger.py +++ b/src/UltraSinger.py @@ -787,15 +787,16 @@ def parse_ultrastar_txt() -> tuple[str, float, str, str, UltrastarTxtValue]: ultrastar_audio_input_path = os.path.join(dirname, ultrastar_mp3_name) song_output = os.path.join( settings.output_file_path, - ultrastar_class.artist + " - " + ultrastar_class.title, + ultrastar_class.artist.strip() + " - " + ultrastar_class.title.strip(), ) - song_output = get_unused_song_output_dir(song_output) + song_output = get_unused_song_output_dir(str(song_output)) os_helper.create_folder(song_output) + return ( - basename_without_ext, + str(basename_without_ext), real_bpm, song_output, - ultrastar_audio_input_path, + str(ultrastar_audio_input_path), ultrastar_class, ) diff --git a/src/modules/Audio/youtube.py b/src/modules/Audio/youtube.py index d03ee44..f0d9f77 100644 --- a/src/modules/Audio/youtube.py +++ b/src/modules/Audio/youtube.py @@ -9,6 +9,7 @@ from modules.console_colors import ULTRASINGER_HEAD from modules.Image.image_helper import crop_image_to_square + def get_youtube_title(url: str) -> tuple[str, str]: """Get the title of the YouTube video""" @@ -19,10 +20,10 @@ def get_youtube_title(url: str) -> tuple[str, str]: ) if "artist" in result: - return result["artist"], result["track"] + return result["artist"].strip(), result["track"].strip() if "-" in result["title"]: - return result["title"].split("-")[0], result["title"].split("-")[1] - return result["channel"], result["title"] + return result["title"].split("-")[0].strip(), result["title"].split("-")[1].strip() + return result["channel"].strip(), result["title"].strip() def download_youtube_audio(url: str, clear_filename: str, output_path: str): @@ -62,6 +63,7 @@ def download_and_convert_thumbnail(ydl_opts, url: str, clear_filename: str, outp response = ydl.urlopen(thumbnail_url) image_data = response.read() image = Image.open(io.BytesIO(image_data)) + image = image.convert('RGB') # Convert to RGB to avoid transparency or RGBA issues image_path = os.path.join(output_path, clear_filename + " [CO].jpg") image.save(image_path, "JPEG") crop_image_to_square(image_path) diff --git a/src/modules/musicbrainz_client.py b/src/modules/musicbrainz_client.py index 29eb781..0d07f73 100644 --- a/src/modules/musicbrainz_client.py +++ b/src/modules/musicbrainz_client.py @@ -10,27 +10,35 @@ def get_music_infos(search_string: str) -> tuple[str, str, str, str]: musicbrainzngs.set_useragent("UltraSinger", "0.1", "https://github.com/rakuri255/UltraSinger") # search for artist and titel to get release on the first place + artist = None artists = musicbrainzngs.search_artists(search_string) - artist = artists['artist-list'][0]['name'] + if len(artists['artist-list']) != 0: + artist = artists['artist-list'][0]['name'].strip() + else: + print(f"{ULTRASINGER_HEAD} {red_highlighted('No match found')}") + return None, None, None, None + + release = None release_groups = musicbrainzngs.search_release_groups(search_string, artist=artist) - release = release_groups['release-group-list'][0] + if len(release_groups['release-group-list']) != 0: + release = release_groups['release-group-list'][0] - if 'artist-credit-phrase' in release: - artist = release['artist-credit-phrase'] + if release is not None and 'artist-credit-phrase' in release: + artist = release['artist-credit-phrase'].strip() title = None - if 'title' in release: - clean_search_string = search_string.translate(str.maketrans('', '', string.punctuation)).lower() - clean_release_title = release['title'].translate(str.maketrans('', '', string.punctuation)).lower() - clean_artist = artist.translate(str.maketrans('', '', string.punctuation)).lower() + if release is not None and 'title' in release: + clean_search_string = search_string.translate(str.maketrans('', '', string.punctuation)).lower().strip() + clean_release_title = release['title'].translate(str.maketrans('', '', string.punctuation)).lower().strip() + clean_artist = artist.translate(str.maketrans('', '', string.punctuation)).lower().strip() # prepare search string when title and artist are the same if clean_release_title == clean_artist: - # remmove the first acurance of the artist + # remove the first appearance of the artist clean_search_string = clean_search_string.replace(clean_artist, "", 1) if clean_release_title in clean_search_string: - title = release['title'] + title = release['title'].strip() else: print( f"{ULTRASINGER_HEAD} cant find title {red_highlighted(clean_release_title)} in {red_highlighted(clean_search_string)}") @@ -43,14 +51,14 @@ def get_music_infos(search_string: str) -> tuple[str, str, str, str]: year = None if 'first-release-date' in release: - year = release['first-release-date'] + year = release['first-release-date'].strip() print(f"{ULTRASINGER_HEAD} Found release year: {blue_highlighted(year)}") genres = None if 'tag-list' in release: genres = "" for tag in release['tag-list']: - genres += f"{tag['name']}," + genres += f"{tag['name'].strip()}," print(f"{ULTRASINGER_HEAD} Found genres: {blue_highlighted(genres)}") return title, artist, year, genres