|
1 | 1 | import os |
2 | | -import sys |
| 2 | +import pickle |
3 | 3 | from datetime import datetime |
4 | 4 |
|
5 | 5 | import googleapiclient.discovery |
6 | | -import httplib2 |
| 6 | +from google.auth.transport.requests import Request |
| 7 | +from google_auth_oauthlib.flow import InstalledAppFlow |
7 | 8 | from googleapiclient.errors import HttpError |
8 | 9 | from loguru import logger |
9 | | -from oauth2client.client import flow_from_clientsecrets |
10 | | -from oauth2client.file import Storage |
11 | | -from oauth2client.tools import argparser, run_flow |
12 | 10 |
|
13 | 11 | from app.config import settings |
14 | 12 | from app.db.base import Session |
|
19 | 17 |
|
20 | 18 | class YTApiClient: |
21 | 19 | def __init__(self): |
| 20 | + # Disable OAuthlib's HTTPS verification when running locally. |
| 21 | + # *DO NOT* leave this option enabled in production. |
| 22 | + os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1" |
22 | 23 | self.scopes = ["https://www.googleapis.com/auth/youtube.readonly"] |
23 | 24 | self.api_service_name = "youtube" |
24 | 25 | self.api_version = "v3" |
25 | | - self.client_secrets_file = settings.youtube_secret_json |
| 26 | + self._client_secrets_file = settings.youtube_secret_json |
26 | 27 | self._repository = YoutubeDataRepository(session=Session()) |
27 | | - |
28 | | - def get_video_info(self, video_id: list[str]) -> dict: |
29 | | - # Disable OAuthlib's HTTPS verification when running locally. |
30 | | - # *DO NOT* leave this option enabled in production. |
31 | | - os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1" |
32 | | - |
33 | | - # Get credentials and create an API client |
34 | | - flow = flow_from_clientsecrets(self.client_secrets_file, scope=self.scopes) |
35 | | - |
36 | | - storage = Storage("%s-oauth2.json" % sys.argv[0]) |
37 | | - credentials = storage.get() |
38 | | - if credentials is None or credentials.invalid: |
39 | | - flags = argparser.parse_args() |
40 | | - credentials = run_flow(flow, storage, flags) |
41 | | - |
| 28 | + self._credentials_file = f"google-oauth2.pickle" |
| 29 | + |
| 30 | + def _get_credentials(self): |
| 31 | + """Получить или обновить учетные данные.""" |
| 32 | + credentials = None |
| 33 | + if os.path.exists(self._credentials_file): |
| 34 | + with open(self._credentials_file, "rb") as token: |
| 35 | + credentials = pickle.load(token) |
| 36 | + if not credentials or not credentials.valid: |
| 37 | + if credentials and credentials.expired and credentials.refresh_token: |
| 38 | + credentials.refresh(Request()) |
| 39 | + else: |
| 40 | + flow = InstalledAppFlow.from_client_secrets_file(self._client_secrets_file, self.scopes) |
| 41 | + credentials = flow.run_local_server(port=0) |
| 42 | + # Сохраняем полученные учетные данные для последующего использования |
| 43 | + with open(self._credentials_file, "wb") as token: |
| 44 | + pickle.dump(credentials, token) |
| 45 | + return credentials |
| 46 | + |
| 47 | + def _make_request(self, func, *args, **kwargs): |
| 48 | + """Выполнить запрос к YouTube API с повторной попыткой в случае ошибки.""" |
| 49 | + try: |
| 50 | + return func(*args, **kwargs) |
| 51 | + except HttpError as e: |
| 52 | + if e.resp.status in [401, 403]: |
| 53 | + os.remove(self._credentials_file) # Удаляем просроченные учетные данные |
| 54 | + credentials = self._get_credentials() # Получаем новые учетные данные |
| 55 | + youtube = googleapiclient.discovery.build( |
| 56 | + self.api_service_name, self.api_version, credentials=credentials |
| 57 | + ) |
| 58 | + return func(*args, **kwargs) # Повторяем запрос с новыми учетными данными |
| 59 | + else: |
| 60 | + raise |
| 61 | + |
| 62 | + def get_video_info(self, video_ids: list[str]) -> dict: |
| 63 | + """Получить информацию о видео.""" |
42 | 64 | youtube = googleapiclient.discovery.build( |
43 | | - self.api_service_name, self.api_version, http=credentials.authorize(httplib2.Http()) |
| 65 | + self.api_service_name, self.api_version, credentials=self._get_credentials() |
44 | 66 | ) |
45 | | - |
46 | | - request = youtube.videos().list(part="snippet,statistics,status,contentDetails", id=",".join(video_id)) |
47 | | - response = request.execute() |
48 | | - |
| 67 | + request_func = ( |
| 68 | + lambda: youtube.videos() |
| 69 | + .list( |
| 70 | + part="snippet,statistics,status,contentDetails", |
| 71 | + id=",".join(video_ids), |
| 72 | + ) |
| 73 | + .execute() |
| 74 | + ) |
| 75 | + response = self._make_request(request_func) |
49 | 76 | return response |
50 | 77 |
|
51 | 78 | def update_video_info(self, video_ids: list[str]) -> None: |
@@ -90,34 +117,27 @@ def update_missing_video_info(self, videos_list: list[Video] = []): |
90 | 117 | self._repository.reset_all_invalid_videos() |
91 | 118 |
|
92 | 119 | def get_channel_info(self, channel_ids: list[str]) -> list[ChannelAPIInfoSchema]: |
93 | | - # Disable OAuthlib's HTTPS verification when running locally. |
94 | | - os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1" |
95 | | - |
96 | | - # Get credentials and create an API client |
97 | | - flow = flow_from_clientsecrets(self.client_secrets_file, scope=self.scopes) |
98 | | - storage = Storage("%s-oauth2.json" % sys.argv[0]) |
99 | | - credentials = storage.get() |
100 | | - if credentials is None or credentials.invalid: |
101 | | - flags = argparser.parse_args() |
102 | | - credentials = run_flow(flow, storage, flags) |
103 | | - |
| 120 | + credentials = self._get_credentials() |
104 | 121 | youtube = googleapiclient.discovery.build(self.api_service_name, self.api_version, credentials=credentials) |
105 | 122 | channels_info: list[ChannelAPIInfoSchema] = [] |
106 | 123 |
|
107 | | - try: |
108 | | - request = youtube.channels().list( |
| 124 | + request_func = ( |
| 125 | + lambda: youtube.channels() |
| 126 | + .list( |
109 | 127 | part="contentDetails,contentOwnerDetails,id,snippet,statistics,status,topicDetails", |
110 | 128 | id=",".join(channel_ids), |
111 | 129 | ) |
112 | | - response = request.execute() |
| 130 | + .execute() |
| 131 | + ) |
| 132 | + response = self._make_request(request_func) |
| 133 | + |
| 134 | + try: |
113 | 135 | # Преобразуем ответ в объект ChannelAPIInfoSchema |
114 | 136 | if "items" in response: |
115 | 137 | for item in response["items"]: |
116 | 138 | channels_info.append(ChannelAPIInfoSchema.from_api_response(item)) |
117 | 139 |
|
118 | 140 | return channels_info |
119 | | - except HttpError as e: |
120 | | - logger.error(f"An HTTP error {e.resp.status} occurred:\n{e.content}") |
121 | 141 | except KeyError: |
122 | 142 | logger.error("The response from the API did not contain the expected data.") |
123 | 143 | except Exception as e: |
|
0 commit comments