from __future__ import annotations from typing import Any, Iterable, Mapping import requests from src import config from src.data_info import PlaylistInfo, TokenInfo, UserInfo, YoutubeVideoInfo, YoutubeVideoInPlaylistInfo from ..errors import YoutubeApiError from .base import BaseDSPApiClient __all__ = ["YoutubeApiClient"] class YoutubeApiClient(BaseDSPApiClient): default_error_cls = YoutubeApiError root_url = "https://www.googleapis.com" base_url = f"{root_url}/youtube/v3" base_oauth_url = "https://accounts.google.com/o/oauth2/auth" base_auth_url = "https://oauth2.googleapis.com/token" batch_size: int = 50 def __init__( self, *, client_id: str = config.GOOGLE_CLIENT_ID, client_secret: str = config.GOOGLE_CLIENT_SECRET, **kwargs ): super().__init__(**kwargs) self._client_id = client_id self._client_secret = client_secret def _get_token_from_code(self, code: str, redirect_uri: str, **extra: Any) -> TokenInfo: request = requests.Request( method="post", url=f"{self.base_auth_url}", data={ "grant_type": "authorization_code", "client_id": self._client_id, "client_secret": self._client_secret, "code": code, "redirect_uri": redirect_uri, }, ) result = self.send_request_with_retry(request, authorize=False).json() return TokenInfo( refresh_token=result["refresh_token"], access_token=result["access_token"], expires_in=result["expires_in"] ) def _refresh_access_token(self) -> tuple[str, int]: request = requests.Request( method="post", url=self.base_auth_url, data={ "grant_type": "refresh_token", "refresh_token": self._refresh_token, "client_id": self._client_id, "client_secret": self._client_secret, }, ) result = self.send_request_with_retry(request, authorize=False).json() return result["access_token"], result["expires_in"] def search(self, query: str, max_results: int = 10) -> Iterable[YoutubeVideoInfo]: request = requests.Request( method="get", url=f"{self.base_url}/search", params={"q": query, "part": "id,snippet", "maxResults": max_results, "type": "video"}, ) result = self.send_request_with_retry(request).json() for item in result.get("items", []): yield YoutubeVideoInfo( id=item["id"]["videoId"], title=item["snippet"]["title"], channel_id=item["snippet"]["channelId"], channel_title=item["snippet"]["channelTitle"], ) def get_playlist(self, playlist_id: str) -> Iterable[YoutubeVideoInPlaylistInfo]: page_token = None while True: request = requests.Request( method="get", url=f"{self.base_url}/playlistItems", params={ "part": "id,snippet", "maxResults": self.batch_size, "playlistId": playlist_id, "pageToken": page_token, }, ) result = self.send_request_with_retry(request).json() for item in result.get("items", []): yield YoutubeVideoInPlaylistInfo( id=item["snippet"]["resourceId"]["videoId"], playlist_item_id=item["id"] ) page_token = result.get("nextPageToken") if page_token is None: break def get_playlist_info(self, playlist_id: str) -> PlaylistInfo: request = requests.Request( method="get", url=f"{self.base_url}/playlists", params={"part": "snippet,contentDetails", "id": playlist_id} ) result = self.send_request_with_retry(request).json() items = result.get("items") if not items: raise YoutubeApiError(f"No items found for playlist {playlist_id}") playlist = items[0] return PlaylistInfo( title=playlist["snippet"]["title"], description=playlist["snippet"]["description"], image_url=playlist["snippet"]["thumbnails"]["default"]["url"], total_tracks=playlist["contentDetails"]["itemCount"], user_id=playlist["snippet"]["channelId"], user_name=None, ) def insert_video(self, playlist_id: str, video_id: str, position: int): request = requests.Request( method="post", url=f"{self.base_url}/playlistItems", params={"part": "snippet"}, json={ "snippet": { "playlistId": playlist_id, "position": position, "resourceId": {"kind": "youtube#video", "videoId": video_id}, } }, ) self.send_request_with_retry(request) self._logger.debug(f"Inserted video id {video_id} at position {position}") def delete_playlist_item(self, playlist_item_id: str): request = requests.Request( method="delete", url=f"{self.base_url}/playlistItems", params={"id": playlist_item_id} ) self.send_request_with_retry(request) self._logger.debug(f"Playlist item {playlist_item_id} deleted.") def update_playlist_info(self, playlist_id: str, snippet_info: Mapping[str, Any]): request = requests.Request( method="put", url=f"{self.base_url}/playlists", params={"part": "snippet"}, json={"id": playlist_id, "snippet": snippet_info}, ) self.send_request_with_retry(request) self._logger.debug(f"Updated playlist info {playlist_id}") def get_user_info(self, **extra) -> UserInfo: self._logger.debug("Getting user info") request = requests.Request(method="get", url=f"{self.root_url}/oauth2/v1/userinfo", params={"alt": "json"}) result = self.send_request_with_retry(request).json() self._logger.debug("Got user info") return UserInfo(user_identifier=result["id"], display_name=result["name"], image_url=result["picture"])