Skip to content

Commit a875a67

Browse files
authored
Add support for fetching subscriptions (#52)
1 parent 0f92d17 commit a875a67

File tree

4 files changed

+136
-1
lines changed

4 files changed

+136
-1
lines changed

src/youtubeaio/models.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,24 @@ class YouTubeChannel(BaseModel):
107107
None,
108108
alias="contentDetails",
109109
)
110+
111+
112+
class YouTubeSubscriptionSnippet(BaseModel):
113+
"""Model representing a YouTube subscription snippet."""
114+
115+
title: str = Field(...)
116+
description: str = Field(...)
117+
subscribed_at: datetime = Field(..., alias="publishedAt")
118+
channel_info: dict[str, str] = Field(..., alias="resourceId")
119+
120+
@property
121+
def channel_id(self) -> str:
122+
"""Return channel id."""
123+
return self.channel_info["channelId"]
124+
125+
126+
class YouTubeSubscription(BaseModel):
127+
"""Model representing a YouTube subscription."""
128+
129+
subscription_id: str = Field(..., alias="id")
130+
snippet: YouTubeSubscriptionSnippet | None = None

src/youtubeaio/youtube.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
build_url,
1313
first,
1414
)
15-
from youtubeaio.models import YouTubeChannel, YouTubeVideo
15+
from youtubeaio.models import YouTubeChannel, YouTubeSubscription, YouTubeVideo
1616
from youtubeaio.types import (
1717
AuthScope,
1818
MissingScopeError,
@@ -232,6 +232,22 @@ async def get_channels(
232232
async for item in self._get_channels(param):
233233
yield item
234234

235+
async def get_user_subscriptions(
236+
self,
237+
) -> AsyncGenerator[YouTubeSubscription, None]:
238+
"""Get subscriptions for authenticated user."""
239+
param = {
240+
"part": "snippet",
241+
"mine": "true",
242+
}
243+
async for item in self._build_generator(
244+
"GET",
245+
"subscriptions",
246+
param,
247+
YouTubeSubscription,
248+
):
249+
yield item # type: ignore[misc]
250+
235251
async def close(self) -> None:
236252
"""Close open client session."""
237253
if self.session and self._close_session:
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
{
2+
"kind": "youtube#SubscriptionListResponse",
3+
"etag": "ToySCAR2vCoWXFlWF3I2pOR9odY",
4+
"pageInfo": {
5+
"totalResults": 547,
6+
"resultsPerPage": 2
7+
},
8+
"items": [
9+
{
10+
"kind": "youtube#subscription",
11+
"etag": "qQ8Upr97jeB0ksUo2lFQXxXtcis",
12+
"id": "l6YW-siEBx2rtBlTJ_ip132nwdjtfEj_s_XrLrZyj3Y",
13+
"snippet": {
14+
"publishedAt": "2020-05-18T01:04:54.7417Z",
15+
"title": "DougDoug",
16+
"description": "I'm a pepper who teach how to videogame real good.\n\nI stream live on Twitch! https://www.twitch.tv/DougDoug\n\nFollow me on twitter! https://twitter.com/DougDougFood",
17+
"resourceId": {
18+
"kind": "youtube#channel",
19+
"channelId": "UClyGlKOhDUooPJFy4v_mqPg"
20+
},
21+
"channelId": "UCJA_MqaDMFv6Dh-E8bpszIw",
22+
"thumbnails": {
23+
"default": {
24+
"url": "https://yt3.ggpht.com/ytc/AOPolaQNqmde36_-rZt3loE_aZJ1iJKX936_5ebnJA2dVQ=s88-c-k-c0x00ffffff-no-rj"
25+
},
26+
"medium": {
27+
"url": "https://yt3.ggpht.com/ytc/AOPolaQNqmde36_-rZt3loE_aZJ1iJKX936_5ebnJA2dVQ=s240-c-k-c0x00ffffff-no-rj"
28+
},
29+
"high": {
30+
"url": "https://yt3.ggpht.com/ytc/AOPolaQNqmde36_-rZt3loE_aZJ1iJKX936_5ebnJA2dVQ=s800-c-k-c0x00ffffff-no-rj"
31+
}
32+
}
33+
}
34+
},
35+
{
36+
"kind": "youtube#subscription",
37+
"etag": "3Ma9gfFVk8R7tC6js2OPirKjleg",
38+
"id": "l6YW-siEBx0PYqtFJQsPPR3JU1r9dzJpv-pjRAZJ0iU",
39+
"snippet": {
40+
"publishedAt": "2023-01-04T20:35:54.095866Z",
41+
"title": "Home Assistant",
42+
"description": "Home Assistant is an open source home automation that puts local control and privacy first. Powered by a worldwide community of tinkerers and DIY enthusiasts. Perfect to run on a Raspberry Pi or a local server.",
43+
"resourceId": {
44+
"kind": "youtube#channel",
45+
"channelId": "UCbX3YkedQunLt7EQAdVxh7w"
46+
},
47+
"channelId": "UCJA_MqaDMFv6Dh-E8bpszIw",
48+
"thumbnails": {
49+
"default": {
50+
"url": "https://yt3.ggpht.com/ytc/AOPolaTRAmW34QPGjkXJ9QKJaSXXzbP3K_BlPY5IodnE0Q=s88-c-k-c0x00ffffff-no-rj"
51+
},
52+
"medium": {
53+
"url": "https://yt3.ggpht.com/ytc/AOPolaTRAmW34QPGjkXJ9QKJaSXXzbP3K_BlPY5IodnE0Q=s240-c-k-c0x00ffffff-no-rj"
54+
},
55+
"high": {
56+
"url": "https://yt3.ggpht.com/ytc/AOPolaTRAmW34QPGjkXJ9QKJaSXXzbP3K_BlPY5IodnE0Q=s800-c-k-c0x00ffffff-no-rj"
57+
}
58+
}
59+
}
60+
}
61+
]
62+
}

tests/test_subscription.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""Tests for the YouTube client."""
2+
3+
import aiohttp
4+
from aresponses import ResponsesMockServer
5+
6+
from youtubeaio.youtube import YouTube
7+
8+
from . import load_fixture
9+
from .const import YOUTUBE_URL
10+
11+
12+
async def test_fetch_user_subscriptions(
13+
aresponses: ResponsesMockServer,
14+
) -> None:
15+
"""Test retrieving a video."""
16+
aresponses.add(
17+
YOUTUBE_URL,
18+
"/youtube/v3/subscriptions?part=snippet&mine=true",
19+
"GET",
20+
aresponses.Response(
21+
status=200,
22+
headers={"Content-Type": "application/json"},
23+
text=load_fixture("subscription_response_snippet.json"),
24+
),
25+
match_querystring=True,
26+
)
27+
async with aiohttp.ClientSession() as session:
28+
youtube = YouTube(session=session)
29+
count = 0
30+
async for subscription in youtube.get_user_subscriptions():
31+
count += 1
32+
assert subscription
33+
assert subscription.snippet
34+
assert subscription.snippet.channel_id
35+
assert count == 2
36+
await youtube.close()

0 commit comments

Comments
 (0)