diff --git a/src/ytdl_sub/validators/url_validator.py b/src/ytdl_sub/validators/url_validator.py index 6cdac524..fbf6a429 100644 --- a/src/ytdl_sub/validators/url_validator.py +++ b/src/ytdl_sub/validators/url_validator.py @@ -3,12 +3,13 @@ from typing import Optional from urllib.parse import parse_qs from urllib.parse import urlparse +from ytdl_sub.utils.exceptions import ValidationException from ytdl_sub.validators.validators import StringValidator class YoutubeVideoUrlValidator(StringValidator): - _expected_value_type_name = "youtube video url" + _expected_value_type_name = "Youtube video url" @classmethod def _get_video_id(cls, url: str) -> Optional[str]: @@ -19,8 +20,8 @@ class YoutubeVideoUrlValidator(StringValidator): - https://www.youtube.com/embed/SA2iWivDJiE - https://www.youtube.com/v/SA2iWivDJiE?version=3&hl=en_US """ - # If the url doesn't contain the 'youtu' substring, assume it is the video id - if "youtu" not in url: + # If the url doesn't contain youtube, assume it is the video id + if "youtube.com" not in url and "youtu.be" not in url: return url # If https:// is not present, urlparse will not work @@ -56,3 +57,107 @@ class YoutubeVideoUrlValidator(StringValidator): ID of the video """ return self._video_id + + +class YoutubePlaylistUrlValidator(StringValidator): + + _expected_value_type_name = "Youtube playlist url" + + @classmethod + def _get_playlist_id(cls, url: str) -> Optional[str]: + """ + Examples: + - https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc + """ + # If the url doesn't contain youtube, assume it is the ID + if "youtube.com" not in url: + return url + + # If https:// is not present, urlparse will not work + if not url.startswith("https://"): + url = f"https://{url}" + + query = urlparse(url) + if query.hostname in ("youtube.com", "www.youtube.com"): + if query.path == "/playlist": + parsed_q = parse_qs(query.query) + return parsed_q["list"][0] + + return None + + def __init__(self, name: str, value: Any): + super().__init__(name, value) + + self._playlist_id = self._get_playlist_id(value) + if not self._playlist_id: + raise self._validation_exception(f"'{value}' is not a valid Youtube playlist url or ID.") + + @property + def playlist_id(self) -> str: + """ + Returns + ------- + ID of the channel + """ + return self._playlist_id + + +class YoutubeChannelUrlValidator(StringValidator): + + _expected_value_type_name = "Youtube channel url" + + @classmethod + def _get_channel_id(cls, url: str) -> Optional[str]: + """ + Examples: + - https://www.youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw + + NOT: + - https://www.youtube.com/user/a_username_that_can_change + - https://www.youtube.com/c/a_name_that_can_change + + Raises + ------ + ValidationException + If the url is for the user name + """ + # If the url doesn't contain youtube, assume it is the ID + if "youtube.com" not in url: + return url + + # If https:// is not present, urlparse will not work + if not url.startswith("https://"): + url = f"https://{url}" + + query = urlparse(url) + if query.hostname in ("youtube.com", "www.youtube.com"): + if query.path == "/channel": + parsed_q = parse_qs(query.query) + return parsed_q["v"][0] + if query.path in ("/user", "/c"): + raise ValidationException('user or c not allowed since it can change') + + return None + + def __init__(self, name: str, value: Any): + super().__init__(name, value) + + try: + self._channel_id = self._get_channel_id(value) + except ValidationException: + raise self._validation_exception( + f"{value} uses a deprecated Youtube url that which can change. " + f"Use the /channel/ url instead." + ) + + if not self._channel_id: + raise self._validation_exception(f"'{value}' is not a valid Youtube channel url or ID.") + + @property + def channel_id(self) -> str: + """ + Returns + ------- + ID of the channel + """ + return self._channel_id diff --git a/tests/unit/validators/test_url_validator.py b/tests/unit/validators/test_url_validator.py index 1a2d1342..760b0def 100644 --- a/tests/unit/validators/test_url_validator.py +++ b/tests/unit/validators/test_url_validator.py @@ -1,29 +1,73 @@ import pytest from ytdl_sub.utils.exceptions import ValidationException -from ytdl_sub.validators.url_validator import YoutubeVideoUrlValidator +from ytdl_sub.validators.url_validator import YoutubeVideoUrlValidator, YoutubePlaylistUrlValidator -@pytest.mark.parametrize( - "url", - [ - "dQw4w9WgXcQ", - "youtu.be/dQw4w9WgXcQ", - "www.youtu.be/dQw4w9WgXcQ", - "https://youtu.be/dQw4w9WgXcQ", - "https://www.youtu.be/dQw4w9WgXcQ", - "https://www.youtube.com/watch?v=dQw4w9WgXcQ&feature=feedu", - "https://www.youtube.com/embed/dQw4w9WgXcQ", - "https://www.youtube.com/v/dQw4w9WgXcQ?version=3&hl=en_US", - ], -) -def test_youtube_video_validator_success(url): - video_id = YoutubeVideoUrlValidator(name="unit test", value=url).video_id - assert video_id == "dQw4w9WgXcQ" +class TestYoutubeVideoUrlValidator: + @pytest.mark.parametrize( + "url", + [ + "dQw4w9WgXcQ", + "youtu.be/dQw4w9WgXcQ", + "www.youtu.be/dQw4w9WgXcQ", + "https://youtu.be/dQw4w9WgXcQ", + "https://www.youtu.be/dQw4w9WgXcQ", + "https://www.youtube.com/watch?v=dQw4w9WgXcQ&feature=feedu", + "https://www.youtube.com/embed/dQw4w9WgXcQ", + "https://www.youtube.com/v/dQw4w9WgXcQ?version=3&hl=en_US", + ], + ) + def test_youtube_video_validator_success(self, url): + video_id = YoutubeVideoUrlValidator(name="unit test", value=url).video_id + assert video_id == "dQw4w9WgXcQ" -def test_youtube_video_validator_fail(): - bad_url = "youtube.cum/asdfd" - expected_error_msg = f"'{bad_url}' is not a valid Youtube video url or ID." - with pytest.raises(ValidationException, match=expected_error_msg): - YoutubeVideoUrlValidator(name="unit test", value=bad_url) + def test_youtube_video_validator_fail(self): + bad_url = "youtube.cum/asdfd" + expected_error_msg = f"'{bad_url}' is not a valid Youtube video url or ID." + with pytest.raises(ValidationException, match=expected_error_msg): + YoutubeVideoUrlValidator(name="unit test", value=bad_url) + + +class TestYoutubePlaylistUrlValidator: + @pytest.mark.parametrize( + "url", + [ + "PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", + "youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", + "www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", + "https://youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", + "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", + ], + ) + def test_youtube_video_validator_success(self, url): + playlist_id = YoutubePlaylistUrlValidator(name="unit test", value=url).playlist_id + assert playlist_id == "PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc" + + def test_youtube_playlist_validator_fail(self): + bad_url = "youpoop.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc" + expected_error_msg = f"'{bad_url}' is not a valid Youtube playlist url or ID." + with pytest.raises(ValidationException, match=expected_error_msg): + YoutubeVideoUrlValidator(name="unit test", value=bad_url) + +class TestYoutubeChannelUrlValidator: + @pytest.mark.parametrize( + "url", + [ + "PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", + "youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", + "www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", + "https://youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", + "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", + ], + ) + def test_youtube_video_validator_success(self, url): + playlist_id = YoutubePlaylistUrlValidator(name="unit test", value=url).playlist_id + assert playlist_id == "PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc" + + def test_youtube_playlist_validator_fail(self): + bad_url = "youpoop.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc" + expected_error_msg = f"'{bad_url}' is not a valid Youtube playlist url or ID." + with pytest.raises(ValidationException, match=expected_error_msg): + YoutubeVideoUrlValidator(name="unit test", value=bad_url) \ No newline at end of file