From 59274aa5bbdc6676d076079c7db6f7974bce298f Mon Sep 17 00:00:00 2001 From: electricsteve <96793824+electricsteve@users.noreply.github.com> Date: Thu, 13 Nov 2025 21:46:10 +0100 Subject: [PATCH 1/7] feat: Add is_short method --- src/youtubeaio/youtube.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/youtubeaio/youtube.py b/src/youtubeaio/youtube.py index 61f01377..ba4f9274 100644 --- a/src/youtubeaio/youtube.py +++ b/src/youtubeaio/youtube.py @@ -77,6 +77,7 @@ def __init__( ], ] = { "get": self._api_get_request, + "head": self._api_head_request, } async def _check_request_return(self, response: ClientResponse) -> ClientResponse: @@ -115,6 +116,18 @@ async def _api_get_request( response = await session.get(url, headers=headers, json=data) return await self._check_request_return(response) + async def _api_head_request( + self, + session: ClientSession, + url: str, + data: dict[str, Any] | None = None, + ) -> ClientResponse: + """Make HEAD request with authorization.""" + headers = {"Authorization": f"Bearer {self._user_auth_token}"} + self.logger.debug("making GET request to %s", url) + response = await session.head(url, headers=headers, json=data) + return await self._check_request_return(response) + async def _build_generator( self, req: str, @@ -284,6 +297,25 @@ async def get_playlist_items( ): yield item # type: ignore[misc] + async def is_short( + self, + video_id: str + ) -> bool | None: + """Return True if the video ID corresponds to a YouTube Short.""" + _url = "https://www.youtube.com/shorts/" + video_id + async with asyncio.timeout(self.session_timeout): + response = await self._api_head_request( + self.session, + _url + ) + if response.status == 200: + return True + elif response.status == 303: + return False + else: + return None + + async def close(self) -> None: """Close open client session.""" if self.session and self._close_session: From 6f599ca88ce29de607597f30c6e4e6b73bb1e93d Mon Sep 17 00:00:00 2001 From: electricsteve <96793824+electricsteve@users.noreply.github.com> Date: Thu, 13 Nov 2025 21:55:48 +0100 Subject: [PATCH 2/7] raise error when not 200 or 303 --- src/youtubeaio/youtube.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/youtubeaio/youtube.py b/src/youtubeaio/youtube.py index ba4f9274..433dd8a3 100644 --- a/src/youtubeaio/youtube.py +++ b/src/youtubeaio/youtube.py @@ -313,7 +313,7 @@ async def is_short( elif response.status == 303: return False else: - return None + raise YouTubeAPIError async def close(self) -> None: From cfe1c5417045f57f90d50b955bcf07bbf351be41 Mon Sep 17 00:00:00 2001 From: electricsteve <96793824+electricsteve@users.noreply.github.com> Date: Thu, 13 Nov 2025 22:28:17 +0100 Subject: [PATCH 3/7] add test + fix linting --- src/youtubeaio/youtube.py | 19 ++++++--------- tests/test_short.py | 49 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 12 deletions(-) create mode 100644 tests/test_short.py diff --git a/src/youtubeaio/youtube.py b/src/youtubeaio/youtube.py index 433dd8a3..5a786495 100644 --- a/src/youtubeaio/youtube.py +++ b/src/youtubeaio/youtube.py @@ -297,24 +297,19 @@ async def get_playlist_items( ): yield item # type: ignore[misc] - async def is_short( - self, - video_id: str - ) -> bool | None: + async def is_short(self, video_id: str) -> bool: """Return True if the video ID corresponds to a YouTube Short.""" _url = "https://www.youtube.com/shorts/" + video_id + if not self.session: + self.session = ClientSession() + self._close_session = True async with asyncio.timeout(self.session_timeout): - response = await self._api_head_request( - self.session, - _url - ) + response = await self._api_head_request(self.session, _url) if response.status == 200: return True - elif response.status == 303: + if response.status == 303: return False - else: - raise YouTubeAPIError - + raise YouTubeAPIError async def close(self) -> None: """Close open client session.""" diff --git a/tests/test_short.py b/tests/test_short.py new file mode 100644 index 00000000..c20e5c9e --- /dev/null +++ b/tests/test_short.py @@ -0,0 +1,49 @@ +"""Tests for the YouTube client.""" + +import aiohttp +from aresponses import ResponsesMockServer + +from youtubeaio.youtube import YouTube + + +async def test_is_short( + aresponses: ResponsesMockServer, +) -> None: + """Test YouTube video id that is a short.""" + aresponses.add( + "www.youtube.com", + "/shorts/2YUPfsi8PF4", + "HEAD", + aresponses.Response( + status=200, + headers={"Content-Type": "text/html"}, + ), + ) + async with aiohttp.ClientSession() as session: + youtube = YouTube(session=session) + response = await youtube.is_short("2YUPfsi8PF4") + assert response + await youtube.close() + + +async def test_is_not_short( + aresponses: ResponsesMockServer, +) -> None: + """Test YouTube video id that is not a short.""" + aresponses.add( + "www.youtube.com", + "/shorts/KXaMtA6kWXU", + "HEAD", + aresponses.Response( + status=303, + headers={ + "Content-Type": "application/binary", + "Location": "https://www.youtube.com/watch?v=KXaMtA6kWXU", + }, + ), + ) + async with aiohttp.ClientSession() as session: + youtube = YouTube(session=session) + response = await youtube.is_short("KXaMtA6kWXU") + assert not response + await youtube.close() From b2c34e84d6b010a2e0ccf9f2c325ead4f4ffc872 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 13 Nov 2025 23:15:36 +0100 Subject: [PATCH 4/7] Update src/youtubeaio/youtube.py --- src/youtubeaio/youtube.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/youtubeaio/youtube.py b/src/youtubeaio/youtube.py index 5a786495..83ff6df4 100644 --- a/src/youtubeaio/youtube.py +++ b/src/youtubeaio/youtube.py @@ -124,7 +124,7 @@ async def _api_head_request( ) -> ClientResponse: """Make HEAD request with authorization.""" headers = {"Authorization": f"Bearer {self._user_auth_token}"} - self.logger.debug("making GET request to %s", url) + self.logger.debug("making HEAD request to %s", url) response = await session.head(url, headers=headers, json=data) return await self._check_request_return(response) From 0d9a3d6dca8774fc8664f692abf87e0d1f2dc297 Mon Sep 17 00:00:00 2001 From: Joostlek Date: Thu, 13 Nov 2025 23:18:04 +0100 Subject: [PATCH 5/7] Fix stuff --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 14b2eae4..958fb5b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,6 +53,7 @@ Changelog = "https://github.com/joostlek/python-youtube/releases" [tool.coverage.report] show_missing = true +fail_under = 80 [tool.coverage.run] plugins = ["covdefaults"] From f56f7ac50c8a4c2d45ab171a197194010aacd196 Mon Sep 17 00:00:00 2001 From: electricsteve <96793824+electricsteve@users.noreply.github.com> Date: Fri, 14 Nov 2025 17:41:31 +0100 Subject: [PATCH 6/7] fix: defferent response aiohttp and curl --- src/youtubeaio/youtube.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/youtubeaio/youtube.py b/src/youtubeaio/youtube.py index 83ff6df4..dea43add 100644 --- a/src/youtubeaio/youtube.py +++ b/src/youtubeaio/youtube.py @@ -307,7 +307,7 @@ async def is_short(self, video_id: str) -> bool: response = await self._api_head_request(self.session, _url) if response.status == 200: return True - if response.status == 303: + if response.status == 303 or response.status == 302 or response.status == 301: return False raise YouTubeAPIError From 48d3fe44442d0cbe1154da57d476ea3329742bb8 Mon Sep 17 00:00:00 2001 From: electricsteve <96793824+electricsteve@users.noreply.github.com> Date: Fri, 14 Nov 2025 18:30:25 +0100 Subject: [PATCH 7/7] ruff suggestion --- src/youtubeaio/youtube.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/youtubeaio/youtube.py b/src/youtubeaio/youtube.py index dea43add..0870725c 100644 --- a/src/youtubeaio/youtube.py +++ b/src/youtubeaio/youtube.py @@ -307,7 +307,7 @@ async def is_short(self, video_id: str) -> bool: response = await self._api_head_request(self.session, _url) if response.status == 200: return True - if response.status == 303 or response.status == 302 or response.status == 301: + if response.status in {303, 302, 301}: return False raise YouTubeAPIError