diff --git a/piped_api/client.py b/piped_api/client.py index 27d5b77..8106aa8 100644 --- a/piped_api/client.py +++ b/piped_api/client.py @@ -4,9 +4,10 @@ from requests import Session from .models import BasePipedModel from .models.comments import Comments +from .models.videos import Video -_MDL = t.TypeVar('_MDL', bound=BasePipedModel) +_MDL = t.TypeVar('_MDL', bound=t.Type[BasePipedModel]) @@ -62,3 +63,15 @@ class PipedClient: else: return self._get_json(f"/comments/{video_id}", Comments) + + + + def get_video(self, video_id: str) -> Video: + """ + Gets information about a specific video. + + ### Parameters: + - `video_id` - The ID of the video to get information for + """ + + return self._get_json(f"/streams/{video_id}", Video) diff --git a/piped_api/models/videos.py b/piped_api/models/videos.py new file mode 100644 index 0000000..5c1b349 --- /dev/null +++ b/piped_api/models/videos.py @@ -0,0 +1,560 @@ +from re import S +import typing as t + +from datetime import datetime, date, timedelta + +from . import BasePipedModel + + +class Video(BasePipedModel): + @property + def title(self) -> str: + """ + The title/name of the video + """ + + return self.data['title'] + + + @property + def description(self) -> str: + """ + The description of the video + """ + + return self.data['description'] + + + @property + def upload_date(self) -> date: + """ + The date the video was uploaded at. + + ### Note: + Use `Video.data['uploadDate']` to get the raw string that was obtained from the API - this package + automatically converts the raw string to a `datetime.date` object. + """ + + raw = self.data['uploadDate'] + + return datetime.strptime(raw, r"%Y-%m-%d").date() + + + @property + def uploader(self) -> str: + """ + The channel name of the author of the video + """ + + return self.data['uploader'] + + + @property + def uploader_url(self) -> str: + """ + The URI to the author's channel + """ + + return self.data['uploaderUrl'] + + + @property + def uploader_avatar(self) -> str: + """ + The URL to the video author's avatar image + """ + + return self.data['uploaderAvatar'] + + + @property + def thumbnail_url(self) -> str: + """ + The URL to the video's thumbnail image + """ + + return self.data['thumbnail'] + + + @property + def hls(self) -> t.Optional[str]: + """ + The hls manifest URL, to be used for Livestreams + """ + + return self.data['hls'] + + + @property + def dash(self) -> t.Optional[str]: + """ + The dash manifest URL for OTF streams + """ + + return self.data['dash'] + + + @property + def lbry_id(self) -> str: + """ + The lbry id of the video, if available. I assume this has something to do with https://lbry.com/ + """ + + return self.data['lbryId'] + + + @property + def uploader_verified(self) -> str: + """ + Whether or not the channel that uploaded the video is verified by YouTube (badge) + """ + + return self.data['uploaderVerified'] + + + @property + def duration(self) -> timedelta: + """ + The duration of the video. + + ### Note: + The original value from the API was in seconds (`Video.data['duration']`), but this package + converts it to a `datetime.timedelta` object. + """ + + return timedelta(seconds=self.data['duration']) + + + @property + def views(self) -> int: + """ + The number of views the video has received + """ + + return self.data['views'] + + + @property + def likes(self) -> int: + """ + The amount of likes the video has received. `-1` if hidden + """ + + return self.data['likes'] + + + @property + def dislikes(self) -> int: + """ + The amount of dislikes the video has received. `-1` if hidden + + ### Note: + This is obsolete since YouTube did a tiny gigantical little big whoopsie with their like system and screwed it all up + You can use awesome user-made projects such as https://returnyoutubedislike.com to obtain the dislike count + """ + + return self.data['dislikes'] + + + + class Stream(BasePipedModel): + """ + Either an audio or video stream of a video + """ + + @property + def url(self) -> str: + """ + The URL of the stream + """ + + return self.data['url'] + + + @property + def format(self) -> str: + """ + The format of the stream (`'M4A' or 'WEBMA_OPUS' or 'MPEG_4' or 'WEBM' or 'v3GPP'` + + No, I don't know how many are there or what does each mean + """ + + return self.data['format'] + + + @property + def quality(self) -> str: + """ + The standard quality we all know and love (e. g.: `'240p'` for video or `'128k'` for audio) + """ + + return self.data['quality'] + + + @property + def mime_type(self) -> str: + """ + If you come from web development (or other invidious area that works with these French mimes), + then you already know what this is. If not, consider [checking the Mozilla documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types) + """ + + return self.data['mimeType'] + + + @property + def codec(self) -> str: + """ + What is this? I don't know. A codec? + """ + + return self.data['codec'] + + + @property + def video_only(self) -> bool: + """ + Whether or not the stream is video only (AKA. muted video) + """ + + return self.data['videoOnly'] + + + @property + def bitrate(self) -> int: + """ + The bitrate of the stream + """ + + return self.data['bitrate'] + + + @property + def init_start(self) -> int: + """ + Not sure what this does, but it seems to be useful for creating dash streams + """ + + return self.data['initStart'] + + + @property + def init_end(self) -> int: + """ + Not sure what this does, but it seems to be useful for creating dash streams + """ + + return self.data['initEnd'] + + + @property + def index_start(self) -> int: + """ + Not sure what this does, but it seems to be useful for creating dash streams + """ + + return self.data['indexStart'] + + + @property + def index_end(self) -> int: + """ + Not sure what this does, but it seems to be useful for creating dash streams + """ + + return self.data['indexEnd'] + + + @property + def width(self) -> int: + """ + The width of the stream. `'0'` for audio streams (makes sense) + """ + + return self.data['width'] + + + @property + def height(self) -> int: + """ + The height of the stream. `'0'` for audio streams (makes sense) + """ + + return self.data['width'] + + + @property + def fps(self) -> int: + """ + Frames Per Second. This is usually `'0'` for audio and `'30'` or `'60'` for video + """ + + return self.data['fps'] + + + def get_streams(self, type: t.Literal['video', 'audio']='video') -> t.List[Stream]: + """ + Get the streams of a video. + + ### Parameters: + - `type` - The type of stream to get. Either `'video'` or `'audio'` + """ + + if type == 'video' or type == 'audio': + return [self.Stream(stream_data) for stream_data in self.data[f"{type}Streams"]] + + raise ValueError('Invalid stream type. Must be either `video` or `audio`') + + + + class RelatedVideo(BasePipedModel): + """ + A related video to the current video (e. g.: from the right sidebar) + """ + + @property + def url(self) -> str: + """ + The URL to the related video + """ + + return self.data['url'] + + + @property + def title(self) -> str: + """ + The title of the related video + """ + + return self.data['title'] + + + @property + def thumbnail(self) -> str: + """ + The thumbnail URL of the related video + """ + + return self.data['thumbnail'] + + + @property + def uploader_name(self) -> str: + """ + The name of the channel that uploaded the related video + """ + + return self.data['uploaderName'] + + + @property + def uploader_url(self) -> str: + """ + The URL of the channel that uploaded the related video + """ + + return self.data['uploaderUrl'] + + + @property + def uploader_avatar(self) -> str: + """ + The URL of the channel's avatar + """ + + return self.data['uploaderAvatar'] + + + @property + def uploaded_date(self) -> str: + """ + The date the related video was uploaded (format: `'x y ago'`) + """ + + return self.data['uploadedDate'] + + + @property + def short_description(self) -> t.Optional[str]: + """ + The short description of the related video. As far as I know, this is always `None` - perhaps some unused YouTube feature? + """ + + return self.data['shortDescription'] + + + @property + def duration(self) -> timedelta: + """ + The duration of the related video. + + ### Note: + The original value from the API was in seconds (`Video.data['duration']`), but this package + converts it to a `datetime.timedelta` object. + """ + + return timedelta(seconds=self.data['duration']) + + + @property + def views(self) -> str: + """ + The amount of views the related video has received + """ + + return self.data['views'] + + + @property + def uploaded(self) -> datetime: + """ + The date the related video was uploaded (as a `datetime.datetime` object). + + ### Note: + The original value was in POSIX timestamp (`Video.data['uploaded']`), but this package converts it to a `datetime.datetime` object. + """ + + return datetime.fromtimestamp(self.data['uploaded']) + + + @property + def uploader_verified(self) -> bool: + """ + Whether or not the channel that uploaded the related video is verified by YouTube (e. g.: has badge) + """ + + return self.data['uploaderVerified'] + + + + @property + def related_videos(self) -> t.List[RelatedVideo]: + """ + List of related streams + """ + + return [self.RelatedVideo(video_data) for video_data in self.data['relatedVideos']] + + + + class Subtitle(BasePipedModel): + @property + def url(self) -> str: + """ + The URL to the subtitle + """ + + return self.data['url'] + + + @property + def mime_type(self) -> str: + """ + If you come from web development (or other invidious area that works with these French mimes), + then you already know what this is. If not, consider [checking the Mozilla documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types) + """ + + return self.data['mimeType'] + + + @property + def name(self) -> str: + """ + The name of the language the captions are in + """ + + return self.data['name'] + + + @property + def code(self) -> str: + """ + The country code for the captions + """ + + return self.data['code'] + + + @property + def auto_generated(self) -> bool: + """ + Whether or not the captions are auto-generated by YouTube + """ + + return self.data['autoGenerated'] + + + + @property + def subtitles(self) -> t.List[Subtitle]: + """ + A list of captions for the video + """ + + return [self.Subtitle(subtitle_data) for subtitle_data in self.data['subtitles']] + + + @property + def livestream(self) -> bool: + """ + Whether or not the video is a livestream + """ + + return self.data['livestream'] + + + @property + def proxy_url(self) -> str: + """ + The base URL for Piped proxy + """ + + return self.data['proxyUrl'] + + + + class Chapter(BasePipedModel): + """ + A video chapter (or "section"). + + YouTube displays a list of chapters, if there are timestamps in the description. + """ + + @property + def title(self) -> str: + """ + The title of the chapter + """ + + return self.data['title'] + + + @property + def image(self) -> str: + """ + The image URL for the chapter + """ + + return self.data['image'] + + + @property + def start(self) -> timedelta: + """ + The start time of the chapter + + ### Note: + The original value from the API was in seconds, this package automatically converts it to `datetime.timedelta` + """ + + return timedelta(seconds=self.data['start']) + + + + @property + def chapters(self) -> t.List[Chapter]: + """ + A list of chapters for the video + """ + + return [self.Chapter(chapter_data) for chapter_data in self.data['chapters']] diff --git a/tests/test_video.py b/tests/test_video.py new file mode 100644 index 0000000..4cdd6eb --- /dev/null +++ b/tests/test_video.py @@ -0,0 +1,30 @@ +from tests import CLIENT + +from datetime import datetime + + +def test_video(video_id: str='dQw4w9WgXcQ') -> None: + """ + Prints out information about a video. + """ + + video = CLIENT.get_video(video_id) + short_description = video.description[:100].replace('\n', '') + + print(f""" + Video ID: {video_id} + Title: {video.title} + Description: {short_description}... + Views: {video.views} + + Uploaded by: {video.uploader} + Uploaded on: {video.upload_date} ({datetime.now().year - video.upload_date.year} years ago) + + Duration: {video.duration} + FPS: {video.get_streams('video')[0].fps} + """) + + + +if __name__ == '__main__': + test_video()