diff --git a/streamrip/rip/parse_url.py b/streamrip/rip/parse_url.py index 51e0fbc..bdfa0b0 100644 --- a/streamrip/rip/parse_url.py +++ b/streamrip/rip/parse_url.py @@ -18,7 +18,7 @@ from ..media import ( logger = logging.getLogger("streamrip") URL_REGEX = re.compile( - r"https?://(?:www|open|play|listen)?\.?(qobuz|tidal|deezer|dzr)\.?(com|page.link)(?:(?:/(album|artist|track|playlist|video|label))|(?:\/[-\w]+?))+\/([-\w]+)", + r"https?://(?:www|open|play|listen)?\.?(qobuz|tidal|deezer)\.com?(?:(?:/(album|artist|track|playlist|video|label))|(?:\/[-\w]+?))+\/([-\w]+)", ) SOUNDCLOUD_URL_REGEX = re.compile(r"https://soundcloud.com/[-\w:/]+") LASTFM_URL_REGEX = re.compile(r"https://www.last.fm/user/\w+/playlists/\w+") diff --git a/tests/test_parse_url.py b/tests/test_parse_url.py new file mode 100644 index 0000000..88bea94 --- /dev/null +++ b/tests/test_parse_url.py @@ -0,0 +1,155 @@ +import unittest +from unittest.mock import AsyncMock, patch + +from streamrip.rip.parse_url import ( + DeezerDynamicURL, + GenericURL, + SoundcloudURL, + parse_url, +) + + +class TestParseURL(unittest.TestCase): + def test_deezer_dynamic_url(self): + """Test that Deezer dynamic URLs are matched correctly.""" + url = "https://dzr.page.link/SnV6hCyHihkmCCwUA" + result = parse_url(url) + + self.assertIsNotNone(result) + self.assertIsInstance(result, DeezerDynamicURL) + self.assertEqual(result.source, "deezer") + + def test_qobuz_album_url(self): + """Test that Qobuz album URLs are matched correctly.""" + url = "https://www.qobuz.com/fr-fr/album/bizarre-ride-ii-the-pharcyde-the-pharcyde/0066991040005" + result = parse_url(url) + + self.assertIsNotNone(result) + self.assertIsInstance(result, GenericURL) + self.assertEqual(result.source, "qobuz") + + # Verify the regex match groups + groups = result.match.groups() + self.assertEqual(len(groups), 3) + self.assertEqual(groups[0], "qobuz") # source + self.assertEqual(groups[1], "album") # media_type + self.assertEqual(groups[2], "0066991040005") # item_id + + def test_tidal_track_url(self): + """Test that Tidal track URLs are matched correctly.""" + url = "https://tidal.com/browse/track/3083287" + result = parse_url(url) + + self.assertIsNotNone(result) + self.assertIsInstance(result, GenericURL) + self.assertEqual(result.source, "tidal") + + # Verify the regex match groups + groups = result.match.groups() + self.assertEqual(len(groups), 3) + self.assertEqual(groups[0], "tidal") # source + self.assertEqual(groups[1], "track") # media_type + self.assertEqual(groups[2], "3083287") # item_id + + def test_deezer_track_url(self): + """Test that Deezer track URLs are matched correctly.""" + url = "https://www.deezer.com/track/4195713" + result = parse_url(url) + + self.assertIsNotNone(result) + self.assertIsInstance(result, GenericURL) + self.assertEqual(result.source, "deezer") + + # Verify the regex match groups + groups = result.match.groups() + self.assertEqual(len(groups), 3) + self.assertEqual(groups[0], "deezer") # source + self.assertEqual(groups[1], "track") # media_type + self.assertEqual(groups[2], "4195713") # item_id + + def test_invalid_url(self): + """Test that invalid URLs return None.""" + urls = [ + "https://example.com", + "not a url", + "https://spotify.com/track/123456", # Unsupported source + "https://tidal.com/invalid/3083287", # Invalid media type + ] + + for url in urls: + result = parse_url(url) + self.assertIsNone(result, f"URL should not parse: {url}") + + def test_alternate_url_formats(self): + """Test various URL formats that should be valid.""" + # Test with different domain prefixes + url1 = "https://open.tidal.com/track/3083287" + url2 = "https://play.qobuz.com/album/0066991040005" + url3 = "https://listen.tidal.com/track/3083287" + + for url in [url1, url2, url3]: + result = parse_url(url) + self.assertIsNotNone(result, f"Should parse URL: {url}") + self.assertIsInstance(result, GenericURL) + + def test_url_with_language_code(self): + """Test URLs with different language codes.""" + urls = [ + "https://www.qobuz.com/us-en/album/name/id123456", + "https://www.qobuz.com/gb-en/album/name/id123456", + "https://www.deezer.com/en/track/4195713", + "https://www.deezer.com/fr/track/4195713", + ] + + for url in urls: + result = parse_url(url) + self.assertIsNotNone(result, f"Should parse URL: {url}") + self.assertIsInstance(result, GenericURL) + + def test_soundcloud_url(self): + """Test that Soundcloud URLs are matched correctly.""" + urls = [ + "https://soundcloud.com/artist-name/track-name", + "https://soundcloud.com/artist-name/sets/playlist-name", + ] + + for url in urls: + result = parse_url(url) + self.assertIsNotNone(result, f"Should parse URL: {url}") + self.assertIsInstance(result, SoundcloudURL) + self.assertEqual(result.source, "soundcloud") + + +class TestDeezerDynamicURL(unittest.TestCase): + @patch("streamrip.rip.parse_url.DeezerDynamicURL._extract_info_from_dynamic_link") + def test_into_pending_album(self, mock_extract): + """Test conversion of Deezer dynamic URL to a PendingAlbum.""" + import asyncio + + async def run_test(): + url = "https://dzr.page.link/SnV6hCyHihkmCCwUA" + result = parse_url(url) + + # Mock the extract method to return album type and ID + mock_extract.return_value = ("album", "12345") + + # Mock the client, config, db + mock_client = AsyncMock() + mock_client.source = "deezer" + mock_config = AsyncMock() + mock_db = AsyncMock() + + # Call into_pending + pending = await result.into_pending(mock_client, mock_config, mock_db) + + # Verify the correct pending type was created + self.assertEqual(pending.__class__.__name__, "PendingAlbum") + self.assertEqual(pending.id, "12345") + + # Run the coroutine + asyncio.run(run_test()) + + +if __name__ == "__main__": + unittest.main() +