Skip to content

Commit

Permalink
Add support for video content details (#64)
Browse files Browse the repository at this point in the history
  • Loading branch information
joostlek authored Jul 25, 2023
1 parent 7462867 commit 69af75c
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 6 deletions.
21 changes: 21 additions & 0 deletions src/youtubeaio/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,27 @@ class VideoPart(str, Enum):
TOPIC_DETAILS = "topicDetails"


class VideoDimension(str, Enum):
"""Enum holding the possible video dimensions."""

D3 = "3d"
D2 = "2d"


class VideoDefinition(str, Enum):
"""Enum holding the possible video definitions."""

HD = "hd"
SD = "sd"


class VideoProjection(str, Enum):
"""Enum holding the possible video projections."""

THREE_SIXTY = "360"
RECTANGULAR = "rectangular"


class LiveBroadcastContent(str, Enum):
"""Enum holding the liveBroadcastContent values."""

Expand Down
22 changes: 22 additions & 0 deletions src/youtubeaio/helper.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""Helper functions for the YouTube API."""
import re
import urllib.parse
from collections.abc import AsyncGenerator, Generator
from datetime import timedelta
from enum import Enum
from typing import Any, TypeVar

Expand Down Expand Up @@ -106,3 +108,23 @@ async def limit(
if count > total:
break
yield item


def get_duration(duration: str) -> timedelta:
"""Return timedelta for ISO8601 duration string."""
attributes = {
"S": 0,
"M": 0,
"H": 0,
"D": 0,
}
for match in re.compile(r"(\d+[DHMS])").finditer(duration):
part = match.group(1)
time_value = int(part[:-1])
attributes[part[len(part) - 1]] = time_value
return timedelta(
days=attributes["D"],
hours=attributes["H"],
minutes=attributes["M"],
seconds=attributes["S"],
)
42 changes: 40 additions & 2 deletions src/youtubeaio/models.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
"""Models for YouTube API."""
from datetime import datetime
from datetime import datetime, timedelta
from typing import TypeVar

from pydantic import BaseModel, Field

from youtubeaio.const import LiveBroadcastContent
from youtubeaio.const import (
LiveBroadcastContent,
VideoDefinition,
VideoDimension,
VideoProjection,
)

__all__ = [
"YouTubeThumbnail",
Expand All @@ -18,6 +23,7 @@
"YouTubeChannel",
]

from youtubeaio.helper import get_duration
from youtubeaio.types import PartMissingError

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


class YouTubeVideoContentDetails(BaseModel):
"""Model representing video content details."""

raw_duration: str = Field(..., alias="duration")
dimension: VideoDimension = Field(...)
definition: VideoDefinition = Field(...)
raw_caption: str = Field(..., alias="caption")
licensed_content: bool = Field(..., alias="licensedContent")
projection: VideoProjection = Field(...)

@property
def caption(self) -> bool:
"""Return if video has caption."""
return self.raw_caption == "true"

@property
def duration(self) -> timedelta:
"""Return length of the video."""
return get_duration(self.raw_duration)


class YouTubeVideo(BaseModel):
"""Model representing a video."""

video_id: str = Field(..., alias="id")
nullable_snippet: YouTubeVideoSnippet | None = Field(None, alias="snippet")
nullable_content_details: YouTubeVideoContentDetails | None = Field(
None,
alias="contentDetails",
)

@property
def snippet(self) -> YouTubeVideoSnippet:
Expand All @@ -79,6 +110,13 @@ def snippet(self) -> YouTubeVideoSnippet:
raise PartMissingError
return self.nullable_snippet

@property
def content_details(self) -> YouTubeVideoContentDetails:
"""Return content details."""
if self.nullable_content_details is None:
raise PartMissingError
return self.nullable_content_details


class YouTubeChannelThumbnails(BaseModel):
"""Model representing channel thumbnails."""
Expand Down
2 changes: 1 addition & 1 deletion tests/__snapshots__/test_video.ambr
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# serializer version: 1
# name: test_fetch_video
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'))
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'>))
# ---
32 changes: 31 additions & 1 deletion tests/test_helper.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
"""Tests for the helper module."""
from collections.abc import AsyncGenerator
from datetime import timedelta
from typing import Any

import pytest

from youtubeaio.helper import build_scope, build_url, chunk, first, limit
from youtubeaio.helper import build_scope, build_url, chunk, first, get_duration, limit
from youtubeaio.types import AuthScope


Expand Down Expand Up @@ -137,3 +138,32 @@ async def test_build_url(
) -> None:
"""Test build url."""
assert build_url("asd.com", params, remove_none, split_lists, enum_value) == result


@pytest.mark.parametrize(
("duration", "result"),
[
(
"PT5S",
timedelta(seconds=5),
),
(
"PT10M5S",
timedelta(minutes=10, seconds=5),
),
(
"PT2H10M5S",
timedelta(hours=2, minutes=10, seconds=5),
),
(
"P4DT2H10M5S",
timedelta(days=4, hours=2, minutes=10, seconds=5),
),
],
)
def test_duration(
duration: str,
result: timedelta,
) -> None:
"""Test duration,."""
assert get_duration(duration) == result
13 changes: 11 additions & 2 deletions tests/test_video.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Tests for the YouTube client."""
import json
from datetime import timedelta

import aiohttp
import pytest
Expand Down Expand Up @@ -29,7 +30,9 @@ async def test_fetch_video(
aresponses.Response(
status=200,
headers={"Content-Type": "application/json"},
text=json.dumps(construct_fixture("video", ["snippet"], 1)),
text=json.dumps(
construct_fixture("video", ["snippet", "contentDetails"], 1),
),
),
)
async with aiohttp.ClientSession() as session, YouTube(session=session) as youtube:
Expand Down Expand Up @@ -184,14 +187,18 @@ async def test_nullable_fields(
aresponses.Response(
status=200,
headers={"Content-Type": "application/json"},
text=json.dumps(construct_fixture("video", ["snippet"], 1)),
text=json.dumps(
construct_fixture("video", ["snippet", "contentDetails"], 1),
),
),
)
async with aiohttp.ClientSession() as session:
youtube = YouTube(session=session)
video = await youtube.get_video(video_id="V4DDt30Aat4")
assert video
assert video.snippet.channel_id == "UCAuUUnT6oDeKwE6v1NGQxug"
assert video.content_details.duration == timedelta(minutes=21, seconds=3)
assert video.content_details.caption is True


async def test_nullable_fields_null(
Expand All @@ -214,3 +221,5 @@ async def test_nullable_fields_null(
assert video
with pytest.raises(PartMissingError):
assert video.snippet.thumbnails
with pytest.raises(PartMissingError):
assert video.content_details

0 comments on commit 69af75c

Please sign in to comment.