Skip to content

Commit 69af75c

Browse files
authored
Add support for video content details (#64)
1 parent 7462867 commit 69af75c

File tree

6 files changed

+126
-6
lines changed

6 files changed

+126
-6
lines changed

src/youtubeaio/const.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,27 @@ class VideoPart(str, Enum):
2626
TOPIC_DETAILS = "topicDetails"
2727

2828

29+
class VideoDimension(str, Enum):
30+
"""Enum holding the possible video dimensions."""
31+
32+
D3 = "3d"
33+
D2 = "2d"
34+
35+
36+
class VideoDefinition(str, Enum):
37+
"""Enum holding the possible video definitions."""
38+
39+
HD = "hd"
40+
SD = "sd"
41+
42+
43+
class VideoProjection(str, Enum):
44+
"""Enum holding the possible video projections."""
45+
46+
THREE_SIXTY = "360"
47+
RECTANGULAR = "rectangular"
48+
49+
2950
class LiveBroadcastContent(str, Enum):
3051
"""Enum holding the liveBroadcastContent values."""
3152

src/youtubeaio/helper.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"""Helper functions for the YouTube API."""
2+
import re
23
import urllib.parse
34
from collections.abc import AsyncGenerator, Generator
5+
from datetime import timedelta
46
from enum import Enum
57
from typing import Any, TypeVar
68

@@ -106,3 +108,23 @@ async def limit(
106108
if count > total:
107109
break
108110
yield item
111+
112+
113+
def get_duration(duration: str) -> timedelta:
114+
"""Return timedelta for ISO8601 duration string."""
115+
attributes = {
116+
"S": 0,
117+
"M": 0,
118+
"H": 0,
119+
"D": 0,
120+
}
121+
for match in re.compile(r"(\d+[DHMS])").finditer(duration):
122+
part = match.group(1)
123+
time_value = int(part[:-1])
124+
attributes[part[len(part) - 1]] = time_value
125+
return timedelta(
126+
days=attributes["D"],
127+
hours=attributes["H"],
128+
minutes=attributes["M"],
129+
seconds=attributes["S"],
130+
)

src/youtubeaio/models.py

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
"""Models for YouTube API."""
2-
from datetime import datetime
2+
from datetime import datetime, timedelta
33
from typing import TypeVar
44

55
from pydantic import BaseModel, Field
66

7-
from youtubeaio.const import LiveBroadcastContent
7+
from youtubeaio.const import (
8+
LiveBroadcastContent,
9+
VideoDefinition,
10+
VideoDimension,
11+
VideoProjection,
12+
)
813

914
__all__ = [
1015
"YouTubeThumbnail",
@@ -18,6 +23,7 @@
1823
"YouTubeChannel",
1924
]
2025

26+
from youtubeaio.helper import get_duration
2127
from youtubeaio.types import PartMissingError
2228

2329
T = TypeVar("T")
@@ -66,11 +72,36 @@ class YouTubeVideoSnippet(BaseModel):
6672
default_audio_language: str | None = Field(None, alias="defaultAudioLanguage")
6773

6874

75+
class YouTubeVideoContentDetails(BaseModel):
76+
"""Model representing video content details."""
77+
78+
raw_duration: str = Field(..., alias="duration")
79+
dimension: VideoDimension = Field(...)
80+
definition: VideoDefinition = Field(...)
81+
raw_caption: str = Field(..., alias="caption")
82+
licensed_content: bool = Field(..., alias="licensedContent")
83+
projection: VideoProjection = Field(...)
84+
85+
@property
86+
def caption(self) -> bool:
87+
"""Return if video has caption."""
88+
return self.raw_caption == "true"
89+
90+
@property
91+
def duration(self) -> timedelta:
92+
"""Return length of the video."""
93+
return get_duration(self.raw_duration)
94+
95+
6996
class YouTubeVideo(BaseModel):
7097
"""Model representing a video."""
7198

7299
video_id: str = Field(..., alias="id")
73100
nullable_snippet: YouTubeVideoSnippet | None = Field(None, alias="snippet")
101+
nullable_content_details: YouTubeVideoContentDetails | None = Field(
102+
None,
103+
alias="contentDetails",
104+
)
74105

75106
@property
76107
def snippet(self) -> YouTubeVideoSnippet:
@@ -79,6 +110,13 @@ def snippet(self) -> YouTubeVideoSnippet:
79110
raise PartMissingError
80111
return self.nullable_snippet
81112

113+
@property
114+
def content_details(self) -> YouTubeVideoContentDetails:
115+
"""Return content details."""
116+
if self.nullable_content_details is None:
117+
raise PartMissingError
118+
return self.nullable_content_details
119+
82120

83121
class YouTubeChannelThumbnails(BaseModel):
84122
"""Model representing channel thumbnails."""
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# serializer version: 1
22
# name: test_fetch_video
3-
YouTubeVideo(video_id='Ks-_Mh1QhMc', nullable_snippet=YouTubeVideoSnippet(published_at=datetime.datetime(2012, 10, 1, 15, 27, 35, tzinfo=TzInfo(UTC)), channel_id='UCAuUUnT6oDeKwE6v1NGQxug', title='Your body language may shape who you are | Amy Cuddy', description='Body language affects how others see us, but it may also change how we see ourselves. Social psychologist Amy Cuddy argues that "power posing" -- standing in a posture of confidence, even when we don\'t feel confident -- can boost feelings of confidence, and might have an impact on our chances for success. (Note: Some of the findings presented in this talk have been referenced in an ongoing debate among social scientists about robustness and reproducibility. Read Amy Cuddy\'s response here: http://ideas.ted.com/inside-the-debate-about-power-posing-a-q-a-with-amy-cuddy/)\n\nGet TED Talks recommended just for you! Learn more at https://www.ted.com/signup.\n\nThe TED Talks channel features the best talks and performances from the TED Conference, where the world\'s leading thinkers and doers give the talk of their lives in 18 minutes (or less). Look for talks on Technology, Entertainment and Design -- plus science, business, global issues, the arts and more.\n\nFollow TED on Twitter: http://www.twitter.com/TEDTalks\nLike TED on Facebook: https://www.facebook.com/TED\n\nSubscribe to our channel: https://www.youtube.com/TED', thumbnails=YouTubeVideoThumbnails(default=YouTubeThumbnail(url='https://i.ytimg.com/vi/Ks-_Mh1QhMc/default.jpg', width=120, height=90), medium=YouTubeThumbnail(url='https://i.ytimg.com/vi/Ks-_Mh1QhMc/mqdefault.jpg', width=320, height=180), high=YouTubeThumbnail(url='https://i.ytimg.com/vi/Ks-_Mh1QhMc/hqdefault.jpg', width=480, height=360), standard=YouTubeThumbnail(url='https://i.ytimg.com/vi/Ks-_Mh1QhMc/sddefault.jpg', width=640, height=480), maxres=YouTubeThumbnail(url='https://i.ytimg.com/vi/Ks-_Mh1QhMc/maxresdefault.jpg', width=1280, height=720)), channel_title='TED', tags=['Amy Cuddy', 'TED', 'TEDTalk', 'TEDTalks', 'TED Talk', 'TED Talks', 'TEDGlobal', 'brain', 'business', 'psychology', 'self', 'success'], live_broadcast_content=<LiveBroadcastContent.NONE: 'none'>, default_language='en', default_audio_language='en'))
3+
YouTubeVideo(video_id='Ks-_Mh1QhMc', nullable_snippet=YouTubeVideoSnippet(published_at=datetime.datetime(2012, 10, 1, 15, 27, 35, tzinfo=TzInfo(UTC)), channel_id='UCAuUUnT6oDeKwE6v1NGQxug', title='Your body language may shape who you are | Amy Cuddy', description='Body language affects how others see us, but it may also change how we see ourselves. Social psychologist Amy Cuddy argues that "power posing" -- standing in a posture of confidence, even when we don\'t feel confident -- can boost feelings of confidence, and might have an impact on our chances for success. (Note: Some of the findings presented in this talk have been referenced in an ongoing debate among social scientists about robustness and reproducibility. Read Amy Cuddy\'s response here: http://ideas.ted.com/inside-the-debate-about-power-posing-a-q-a-with-amy-cuddy/)\n\nGet TED Talks recommended just for you! Learn more at https://www.ted.com/signup.\n\nThe TED Talks channel features the best talks and performances from the TED Conference, where the world\'s leading thinkers and doers give the talk of their lives in 18 minutes (or less). Look for talks on Technology, Entertainment and Design -- plus science, business, global issues, the arts and more.\n\nFollow TED on Twitter: http://www.twitter.com/TEDTalks\nLike TED on Facebook: https://www.facebook.com/TED\n\nSubscribe to our channel: https://www.youtube.com/TED', thumbnails=YouTubeVideoThumbnails(default=YouTubeThumbnail(url='https://i.ytimg.com/vi/Ks-_Mh1QhMc/default.jpg', width=120, height=90), medium=YouTubeThumbnail(url='https://i.ytimg.com/vi/Ks-_Mh1QhMc/mqdefault.jpg', width=320, height=180), high=YouTubeThumbnail(url='https://i.ytimg.com/vi/Ks-_Mh1QhMc/hqdefault.jpg', width=480, height=360), standard=YouTubeThumbnail(url='https://i.ytimg.com/vi/Ks-_Mh1QhMc/sddefault.jpg', width=640, height=480), maxres=YouTubeThumbnail(url='https://i.ytimg.com/vi/Ks-_Mh1QhMc/maxresdefault.jpg', width=1280, height=720)), channel_title='TED', tags=['Amy Cuddy', 'TED', 'TEDTalk', 'TEDTalks', 'TED Talk', 'TED Talks', 'TEDGlobal', 'brain', 'business', 'psychology', 'self', 'success'], live_broadcast_content=<LiveBroadcastContent.NONE: 'none'>, default_language='en', default_audio_language='en'), nullable_content_details=YouTubeVideoContentDetails(raw_duration='PT21M3S', dimension=<VideoDimension.D2: '2d'>, definition=<VideoDefinition.HD: 'hd'>, raw_caption='true', licensed_content=True, projection=<VideoProjection.RECTANGULAR: 'rectangular'>))
44
# ---

tests/test_helper.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
"""Tests for the helper module."""
22
from collections.abc import AsyncGenerator
3+
from datetime import timedelta
34
from typing import Any
45

56
import pytest
67

7-
from youtubeaio.helper import build_scope, build_url, chunk, first, limit
8+
from youtubeaio.helper import build_scope, build_url, chunk, first, get_duration, limit
89
from youtubeaio.types import AuthScope
910

1011

@@ -137,3 +138,32 @@ async def test_build_url(
137138
) -> None:
138139
"""Test build url."""
139140
assert build_url("asd.com", params, remove_none, split_lists, enum_value) == result
141+
142+
143+
@pytest.mark.parametrize(
144+
("duration", "result"),
145+
[
146+
(
147+
"PT5S",
148+
timedelta(seconds=5),
149+
),
150+
(
151+
"PT10M5S",
152+
timedelta(minutes=10, seconds=5),
153+
),
154+
(
155+
"PT2H10M5S",
156+
timedelta(hours=2, minutes=10, seconds=5),
157+
),
158+
(
159+
"P4DT2H10M5S",
160+
timedelta(days=4, hours=2, minutes=10, seconds=5),
161+
),
162+
],
163+
)
164+
def test_duration(
165+
duration: str,
166+
result: timedelta,
167+
) -> None:
168+
"""Test duration,."""
169+
assert get_duration(duration) == result

tests/test_video.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Tests for the YouTube client."""
22
import json
3+
from datetime import timedelta
34

45
import aiohttp
56
import pytest
@@ -29,7 +30,9 @@ async def test_fetch_video(
2930
aresponses.Response(
3031
status=200,
3132
headers={"Content-Type": "application/json"},
32-
text=json.dumps(construct_fixture("video", ["snippet"], 1)),
33+
text=json.dumps(
34+
construct_fixture("video", ["snippet", "contentDetails"], 1),
35+
),
3336
),
3437
)
3538
async with aiohttp.ClientSession() as session, YouTube(session=session) as youtube:
@@ -184,14 +187,18 @@ async def test_nullable_fields(
184187
aresponses.Response(
185188
status=200,
186189
headers={"Content-Type": "application/json"},
187-
text=json.dumps(construct_fixture("video", ["snippet"], 1)),
190+
text=json.dumps(
191+
construct_fixture("video", ["snippet", "contentDetails"], 1),
192+
),
188193
),
189194
)
190195
async with aiohttp.ClientSession() as session:
191196
youtube = YouTube(session=session)
192197
video = await youtube.get_video(video_id="V4DDt30Aat4")
193198
assert video
194199
assert video.snippet.channel_id == "UCAuUUnT6oDeKwE6v1NGQxug"
200+
assert video.content_details.duration == timedelta(minutes=21, seconds=3)
201+
assert video.content_details.caption is True
195202

196203

197204
async def test_nullable_fields_null(
@@ -214,3 +221,5 @@ async def test_nullable_fields_null(
214221
assert video
215222
with pytest.raises(PartMissingError):
216223
assert video.snippet.thumbnails
224+
with pytest.raises(PartMissingError):
225+
assert video.content_details

0 commit comments

Comments
 (0)