diff --git a/src/ytdl_sub/validators/url_validator.py b/src/ytdl_sub/validators/url_validator.py index fbf6a429..98dcd3fd 100644 --- a/src/ytdl_sub/validators/url_validator.py +++ b/src/ytdl_sub/validators/url_validator.py @@ -3,7 +3,6 @@ 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 @@ -20,9 +19,9 @@ 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 youtube, assume it is the video id + # If the url doesn't contain youtube or youtu.be, assume it is invalid if "youtube.com" not in url and "youtu.be" not in url: - return url + return None # If https:// is not present, urlparse will not work if not url.startswith("https://"): @@ -47,16 +46,16 @@ class YoutubeVideoUrlValidator(StringValidator): self._video_id = self._get_video_id(value) if not self._video_id: - raise self._validation_exception(f"'{value}' is not a valid Youtube video url or ID.") + raise self._validation_exception(f"'{value}' is not a valid Youtube video url.") @property - def video_id(self) -> str: + def video_url(self) -> str: """ Returns ------- - ID of the video + Full video URL """ - return self._video_id + return f"https://youtube.com/watch?v={self._video_id}" class YoutubePlaylistUrlValidator(StringValidator): @@ -69,9 +68,9 @@ class YoutubePlaylistUrlValidator(StringValidator): Examples: - https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc """ - # If the url doesn't contain youtube, assume it is the ID + # If the url doesn't contain youtube, assume it is invalid if "youtube.com" not in url: - return url + return None # If https:// is not present, urlparse will not work if not url.startswith("https://"): @@ -90,16 +89,16 @@ class YoutubePlaylistUrlValidator(StringValidator): 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.") + raise self._validation_exception(f"'{value}' is not a valid Youtube playlist url.") @property - def playlist_id(self) -> str: + def playlist_url(self) -> str: """ Returns ------- - ID of the channel + Full playlist URL """ - return self._playlist_id + return f"https://youtube.com/playlist?list={self._playlist_id}" class YoutubeChannelUrlValidator(StringValidator): @@ -107,23 +106,16 @@ class YoutubeChannelUrlValidator(StringValidator): _expected_value_type_name = "Youtube channel url" @classmethod - def _get_channel_id(cls, url: str) -> Optional[str]: + def _get_channel_url(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 + - https://www.youtube.com/user/username + - https://www.youtube.com/c/channel_name """ - # If the url doesn't contain youtube, assume it is the ID + # If the url doesn't contain youtube, assume it is invalid if "youtube.com" not in url: - return url + return None # If https:// is not present, urlparse will not work if not url.startswith("https://"): @@ -131,33 +123,23 @@ class YoutubeChannelUrlValidator(StringValidator): 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') + if any(query.path.startswith(qpath) for qpath in ("/channel/", "/user/", "/c/")): + return f"https://youtube.com{query.path}" 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.") + self._channel_url = self._get_channel_url(value) + if not self._channel_url: + raise self._validation_exception(f"'{value}' is not a valid Youtube channel url.") @property - def channel_id(self) -> str: + def channel_url(self) -> str: """ Returns ------- - ID of the channel + Full channel URL """ - return self._channel_id + return self._channel_url diff --git a/tests/unit/validators/test_url_validator.py b/tests/unit/validators/test_url_validator.py index 760b0def..26c6870b 100644 --- a/tests/unit/validators/test_url_validator.py +++ b/tests/unit/validators/test_url_validator.py @@ -1,14 +1,17 @@ +import re + import pytest from ytdl_sub.utils.exceptions import ValidationException -from ytdl_sub.validators.url_validator import YoutubeVideoUrlValidator, YoutubePlaylistUrlValidator +from ytdl_sub.validators.url_validator import YoutubeChannelUrlValidator +from ytdl_sub.validators.url_validator import YoutubePlaylistUrlValidator +from ytdl_sub.validators.url_validator import YoutubeVideoUrlValidator class TestYoutubeVideoUrlValidator: @pytest.mark.parametrize( "url", [ - "dQw4w9WgXcQ", "youtu.be/dQw4w9WgXcQ", "www.youtu.be/dQw4w9WgXcQ", "https://youtu.be/dQw4w9WgXcQ", @@ -18,15 +21,14 @@ class TestYoutubeVideoUrlValidator: "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_url_validator_success(self, url): + video_url = YoutubeVideoUrlValidator(name="unit test", value=url).video_url + assert video_url == "https://youtube.com/watch?v=dQw4w9WgXcQ" - - def test_youtube_video_validator_fail(self): + def test_youtube_video_url_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): + expected_error_msg = f"'{bad_url}' is not a valid Youtube video url." + with pytest.raises(ValidationException, match=re.escape(expected_error_msg)): YoutubeVideoUrlValidator(name="unit test", value=bad_url) @@ -34,40 +36,61 @@ 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_url_validator_success(self, url): + playlist_url = YoutubePlaylistUrlValidator(name="unit test", value=url).playlist_url + assert ( + playlist_url == "https://youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc" + ) - def test_youtube_playlist_validator_fail(self): + def test_youtube_playlist_url_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) + expected_error_msg = f"'{bad_url}' is not a valid Youtube playlist url." + with pytest.raises(ValidationException, match=re.escape(expected_error_msg)): + YoutubePlaylistUrlValidator(name="unit test", value=bad_url) + class TestYoutubeChannelUrlValidator: @pytest.mark.parametrize( - "url", + "url, expected_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", + ( + "https://www.youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw", + "https://youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw", + ), + ( + "https://youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw", + "https://youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw", + ), + ( + "youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw", + "https://youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw", + ), + ( + "www.youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw", + "https://youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw", + ), + ( + "https://www.youtube.com/c/RickastleyCoUkOfficial", + "https://youtube.com/c/RickastleyCoUkOfficial", + ), + ( + "https://www.youtube.com/user/videogamedunkey", + "https://youtube.com/user/videogamedunkey", + ), ], ) - 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_channel_url_validator_success(self, url, expected_url): + channel_url = YoutubeChannelUrlValidator(name="unit test", value=url).channel_url + assert channel_url == expected_url - 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 + def test_youtube_channel_url_validator_fail(self): + bad_url = "www.youtube.com/cha/UCuAXFkgsw1L7xaCfnd5JJOw" + expected_error_msg = f"'{bad_url}' is not a valid Youtube channel url." + with pytest.raises(ValidationException, match=re.escape(expected_error_msg)): + YoutubeChannelUrlValidator(name="unit test", value=bad_url)