Skip to content

Commit b8aadd6

Browse files
authored
Add support for fetching playlist items (#54)
1 parent 44ae73c commit b8aadd6

File tree

4 files changed

+354
-1
lines changed

4 files changed

+354
-1
lines changed

src/youtubeaio/models.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,30 @@ class YouTubeSubscription(BaseModel):
133133

134134
subscription_id: str = Field(..., alias="id")
135135
snippet: YouTubeSubscriptionSnippet | None = None
136+
137+
138+
class YouTubePlaylistItemSnippet(BaseModel):
139+
"""Model representing a YouTube playlist item snippet."""
140+
141+
added_at: datetime = Field(..., alias="publishedAt")
142+
title: str = Field(...)
143+
description: str = Field(...)
144+
thumbnails: YouTubeVideoThumbnails = Field(...)
145+
playlist_id: str = Field(..., alias="playlistId")
146+
147+
148+
class YouTubePlaylistItemContentDetails(BaseModel):
149+
"""Model representing a YouTube playlist item content details."""
150+
151+
video_id: str = Field(..., alias="videoId")
152+
153+
154+
class YouTubePlaylistItem(BaseModel):
155+
"""Model representing a YouTube playlist item."""
156+
157+
playlist_item_id: str = Field(..., alias="id")
158+
snippet: YouTubePlaylistItemSnippet | None = Field(None)
159+
content_details: YouTubePlaylistItemContentDetails | None = Field(
160+
None,
161+
alias="contentDetails",
162+
)

src/youtubeaio/youtube.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@
1212
build_url,
1313
first,
1414
)
15-
from youtubeaio.models import YouTubeChannel, YouTubeSubscription, YouTubeVideo
15+
from youtubeaio.models import (
16+
YouTubeChannel,
17+
YouTubePlaylistItem,
18+
YouTubeSubscription,
19+
YouTubeVideo,
20+
)
1621
from youtubeaio.types import (
1722
AuthScope,
1823
MissingScopeError,
@@ -248,6 +253,25 @@ async def get_user_subscriptions(
248253
):
249254
yield item # type: ignore[misc]
250255

256+
async def get_playlist_items(
257+
self,
258+
playlist_id: str,
259+
max_results: int = 50,
260+
) -> AsyncGenerator[YouTubePlaylistItem, None]:
261+
"""Get playlist by id."""
262+
param = {
263+
"part": "snippet,contentDetails",
264+
"playlistId": playlist_id,
265+
"maxResults": max_results,
266+
}
267+
async for item in self._build_generator(
268+
"GET",
269+
"playlistItems",
270+
param,
271+
YouTubePlaylistItem,
272+
):
273+
yield item # type: ignore[misc]
274+
251275
async def close(self) -> None:
252276
"""Close open client session."""
253277
if self.session and self._close_session:
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
{
2+
"kind": "youtube#playlistItemListResponse",
3+
"etag": "ymk0VS6i1mVdlljgcX3cnxScdhU",
4+
"items": [
5+
{
6+
"kind": "youtube#playlistItem",
7+
"etag": "H3SdsdssTDmYJkvgC4rOsXsq_nY",
8+
"id": "VVVfeDVYRzFPVjJQNnVaWjVGU005VHR3LlVaNG4xbjEzY3JJ",
9+
"snippet": {
10+
"publishedAt": "2023-07-21T19:00:27Z",
11+
"channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw",
12+
"title": "Unleash Your Web Development Potential with dval.dev",
13+
"description": "Website → https://dval.dev\n\nDiscover dval.dev, a resource to unlock your web development potential. Join Dan Valinotti as he shares inspiring insights, expert tips, and showcases his captivating portfolio. Dive into the world of web components, JavaScript, and more to gain the inspiration you need for your own projects.\n\n#MyDomain is a series from Google Registry where website creators share the stories behind their domain names, as well as website-building tips. Stay tuned for more episodes.\n\nGet your .app website: https://get.dev\n#MyDomain playlist → https://goo.gle/MyDomain\n\nSubscribe → https://goo.gle/developers",
14+
"thumbnails": {
15+
"default": {
16+
"url": "https://i.ytimg.com/vi/UZ4n1n13crI/default.jpg",
17+
"width": 120,
18+
"height": 90
19+
},
20+
"medium": {
21+
"url": "https://i.ytimg.com/vi/UZ4n1n13crI/mqdefault.jpg",
22+
"width": 320,
23+
"height": 180
24+
},
25+
"high": {
26+
"url": "https://i.ytimg.com/vi/UZ4n1n13crI/hqdefault.jpg",
27+
"width": 480,
28+
"height": 360
29+
},
30+
"standard": {
31+
"url": "https://i.ytimg.com/vi/UZ4n1n13crI/sddefault.jpg",
32+
"width": 640,
33+
"height": 480
34+
},
35+
"maxres": {
36+
"url": "https://i.ytimg.com/vi/UZ4n1n13crI/maxresdefault.jpg",
37+
"width": 1280,
38+
"height": 720
39+
}
40+
},
41+
"channelTitle": "Google for Developers",
42+
"playlistId": "UU_x5XG1OV2P6uZZ5FSM9Ttw",
43+
"position": 0,
44+
"resourceId": {
45+
"kind": "youtube#video",
46+
"videoId": "UZ4n1n13crI"
47+
},
48+
"videoOwnerChannelTitle": "Google for Developers",
49+
"videoOwnerChannelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw"
50+
},
51+
"contentDetails": {
52+
"videoId": "UZ4n1n13crI",
53+
"videoPublishedAt": "2023-07-21T19:00:27Z"
54+
}
55+
},
56+
{
57+
"kind": "youtube#playlistItem",
58+
"etag": "uVfC4v2s8ojCfH8ei695bB_m8WU",
59+
"id": "VVVfeDVYRzFPVjJQNnVaWjVGU005VHR3Lmd3QTBUcVNONmxn",
60+
"snippet": {
61+
"publishedAt": "2023-07-21T19:00:15Z",
62+
"channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw",
63+
"title": "Learn How to Supercharge Your Last.fm Developer Experience with Finale.app",
64+
"description": "Website →https:/finale.app \n\nDiscover Finale.app, a Last.fm client developed by Noa Rubin. Track your music listening habits and unlock detailed statistics like never before. With exclusive features such as multi-source logging and the ability to create stunning collages of your top songs, artists, and albums, Finale takes your Last.fm experience to the next level. In this #MyDomain episode, uncover the story behind the name as Finale represents its musical roots and being the ultimate Last.fm client.\n\n\n#MyDomain is a series from Google Registry where website creators share the stories behind their domain names, as well as website-building tips. Stay tuned for more episodes.\n\nGet your .app website: https://get.app\n#MyDomain playlist → https://goo.gle/MyDomain\n\nSubscribe → https://goo.gle/developers",
65+
"thumbnails": {
66+
"default": {
67+
"url": "https://i.ytimg.com/vi/gwA0TqSN6lg/default.jpg",
68+
"width": 120,
69+
"height": 90
70+
},
71+
"medium": {
72+
"url": "https://i.ytimg.com/vi/gwA0TqSN6lg/mqdefault.jpg",
73+
"width": 320,
74+
"height": 180
75+
},
76+
"high": {
77+
"url": "https://i.ytimg.com/vi/gwA0TqSN6lg/hqdefault.jpg",
78+
"width": 480,
79+
"height": 360
80+
},
81+
"standard": {
82+
"url": "https://i.ytimg.com/vi/gwA0TqSN6lg/sddefault.jpg",
83+
"width": 640,
84+
"height": 480
85+
},
86+
"maxres": {
87+
"url": "https://i.ytimg.com/vi/gwA0TqSN6lg/maxresdefault.jpg",
88+
"width": 1280,
89+
"height": 720
90+
}
91+
},
92+
"channelTitle": "Google for Developers",
93+
"playlistId": "UU_x5XG1OV2P6uZZ5FSM9Ttw",
94+
"position": 1,
95+
"resourceId": {
96+
"kind": "youtube#video",
97+
"videoId": "gwA0TqSN6lg"
98+
},
99+
"videoOwnerChannelTitle": "Google for Developers",
100+
"videoOwnerChannelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw"
101+
},
102+
"contentDetails": {
103+
"videoId": "gwA0TqSN6lg",
104+
"videoPublishedAt": "2023-07-21T19:00:15Z"
105+
}
106+
},
107+
{
108+
"kind": "youtube#playlistItem",
109+
"etag": "BQMpEaUTP5kL9Wa_kTh9QJYcn4k",
110+
"id": "VVVfeDVYRzFPVjJQNnVaWjVGU005VHR3LmJiV1V3UEhuQlRj",
111+
"snippet": {
112+
"publishedAt": "2023-07-20T21:00:30Z",
113+
"channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw",
114+
"title": "What tools make it easier for devs to develop?",
115+
"description": "Watch this video for developer tips and tricks that can make your projects easier and fun!\n\nSubscribe to Google Developers → https://goo.gle/developers \n\n#streetinterviews #developers",
116+
"thumbnails": {
117+
"default": {
118+
"url": "https://i.ytimg.com/vi/bbWUwPHnBTc/default.jpg",
119+
"width": 120,
120+
"height": 90
121+
},
122+
"medium": {
123+
"url": "https://i.ytimg.com/vi/bbWUwPHnBTc/mqdefault.jpg",
124+
"width": 320,
125+
"height": 180
126+
},
127+
"high": {
128+
"url": "https://i.ytimg.com/vi/bbWUwPHnBTc/hqdefault.jpg",
129+
"width": 480,
130+
"height": 360
131+
},
132+
"standard": {
133+
"url": "https://i.ytimg.com/vi/bbWUwPHnBTc/sddefault.jpg",
134+
"width": 640,
135+
"height": 480
136+
},
137+
"maxres": {
138+
"url": "https://i.ytimg.com/vi/bbWUwPHnBTc/maxresdefault.jpg",
139+
"width": 1280,
140+
"height": 720
141+
}
142+
},
143+
"channelTitle": "Google for Developers",
144+
"playlistId": "UU_x5XG1OV2P6uZZ5FSM9Ttw",
145+
"position": 2,
146+
"resourceId": {
147+
"kind": "youtube#video",
148+
"videoId": "bbWUwPHnBTc"
149+
},
150+
"videoOwnerChannelTitle": "Google for Developers",
151+
"videoOwnerChannelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw"
152+
},
153+
"contentDetails": {
154+
"videoId": "bbWUwPHnBTc",
155+
"videoPublishedAt": "2023-07-20T21:00:30Z"
156+
}
157+
},
158+
{
159+
"kind": "youtube#playlistItem",
160+
"etag": "YNIS7l0t5fF1qcSbThewbKZI4Ac",
161+
"id": "VVVfeDVYRzFPVjJQNnVaWjVGU005VHR3LmRka0l0X3NXOGJv",
162+
"snippet": {
163+
"publishedAt": "2023-07-20T15:00:28Z",
164+
"channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw",
165+
"title": "Writing IOS and Android apps simultaneously?",
166+
"description": "We’ve all been there. You write one app for one operating system only to find out you need to write it for another. Watch this short to see how you can develop for Android and IOS simultaneously with Flutter. \n\nWatch more episodes of GDE Secrets → https://goo.gle/3Di2Olg \nSubscribe to Google Developers → https://goo.gle/developers \n\n#GDESecrets",
167+
"thumbnails": {
168+
"default": {
169+
"url": "https://i.ytimg.com/vi/ddkIt_sW8bo/default.jpg",
170+
"width": 120,
171+
"height": 90
172+
},
173+
"medium": {
174+
"url": "https://i.ytimg.com/vi/ddkIt_sW8bo/mqdefault.jpg",
175+
"width": 320,
176+
"height": 180
177+
},
178+
"high": {
179+
"url": "https://i.ytimg.com/vi/ddkIt_sW8bo/hqdefault.jpg",
180+
"width": 480,
181+
"height": 360
182+
},
183+
"standard": {
184+
"url": "https://i.ytimg.com/vi/ddkIt_sW8bo/sddefault.jpg",
185+
"width": 640,
186+
"height": 480
187+
},
188+
"maxres": {
189+
"url": "https://i.ytimg.com/vi/ddkIt_sW8bo/maxresdefault.jpg",
190+
"width": 1280,
191+
"height": 720
192+
}
193+
},
194+
"channelTitle": "Google for Developers",
195+
"playlistId": "UU_x5XG1OV2P6uZZ5FSM9Ttw",
196+
"position": 3,
197+
"resourceId": {
198+
"kind": "youtube#video",
199+
"videoId": "ddkIt_sW8bo"
200+
},
201+
"videoOwnerChannelTitle": "Google for Developers",
202+
"videoOwnerChannelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw"
203+
},
204+
"contentDetails": {
205+
"videoId": "ddkIt_sW8bo",
206+
"videoPublishedAt": "2023-07-20T15:00:28Z"
207+
}
208+
},
209+
{
210+
"kind": "youtube#playlistItem",
211+
"etag": "CQPbyZ8zzkvPaimscUGyLnI7XMA",
212+
"id": "VVVfeDVYRzFPVjJQNnVaWjVGU005VHR3LlNaLTROSFhTb0JJ",
213+
"snippet": {
214+
"publishedAt": "2023-07-18T21:00:17Z",
215+
"channelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw",
216+
"title": "What tips would you give to new dev?",
217+
"description": "So, you want to be a dev, huh? When you’re first starting out, it’s nice to hear what elder developers have to say and what advice they would give to a newcomer in our community. \n\nSubscribe to Google Developers → https://goo.gle/developers \n\n#streetInterviews #developers",
218+
"thumbnails": {
219+
"default": {
220+
"url": "https://i.ytimg.com/vi/SZ-4NHXSoBI/default.jpg",
221+
"width": 120,
222+
"height": 90
223+
},
224+
"medium": {
225+
"url": "https://i.ytimg.com/vi/SZ-4NHXSoBI/mqdefault.jpg",
226+
"width": 320,
227+
"height": 180
228+
},
229+
"high": {
230+
"url": "https://i.ytimg.com/vi/SZ-4NHXSoBI/hqdefault.jpg",
231+
"width": 480,
232+
"height": 360
233+
},
234+
"standard": {
235+
"url": "https://i.ytimg.com/vi/SZ-4NHXSoBI/sddefault.jpg",
236+
"width": 640,
237+
"height": 480
238+
},
239+
"maxres": {
240+
"url": "https://i.ytimg.com/vi/SZ-4NHXSoBI/maxresdefault.jpg",
241+
"width": 1280,
242+
"height": 720
243+
}
244+
},
245+
"channelTitle": "Google for Developers",
246+
"playlistId": "UU_x5XG1OV2P6uZZ5FSM9Ttw",
247+
"position": 4,
248+
"resourceId": {
249+
"kind": "youtube#video",
250+
"videoId": "SZ-4NHXSoBI"
251+
},
252+
"videoOwnerChannelTitle": "Google for Developers",
253+
"videoOwnerChannelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw"
254+
},
255+
"contentDetails": {
256+
"videoId": "SZ-4NHXSoBI",
257+
"videoPublishedAt": "2023-07-18T21:00:17Z"
258+
}
259+
}
260+
],
261+
"pageInfo": {
262+
"totalResults": 5783,
263+
"resultsPerPage": 5
264+
}
265+
}

tests/test_playlist_item.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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_playlist_items(
13+
aresponses: ResponsesMockServer,
14+
) -> None:
15+
"""Test retrieving playlist items."""
16+
aresponses.add(
17+
YOUTUBE_URL,
18+
"/youtube/v3/playlistItems",
19+
"GET",
20+
aresponses.Response(
21+
status=200,
22+
headers={"Content-Type": "application/json"},
23+
text=load_fixture("playlist_item_response_snippet_content_details.json"),
24+
),
25+
)
26+
async with aiohttp.ClientSession() as session:
27+
youtube = YouTube(session=session)
28+
count = 0
29+
async for playlist_item in youtube.get_playlist_items(
30+
"UU_x5XG1OV2P6uZZ5FSM9Ttw",
31+
):
32+
count += 1
33+
assert playlist_item
34+
assert playlist_item.snippet
35+
assert playlist_item.content_details
36+
assert count == 5
37+
await youtube.close()

0 commit comments

Comments
 (0)