Skip to content

Commit 5eadabe

Browse files
author
vivanov
committed
YTApiClient + _combine_channel_info update
1 parent 531e5c2 commit 5eadabe

5 files changed

Lines changed: 99 additions & 57 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ __pycache__/
1616
.env
1717
*oauth2*.json
1818
*secret*.json
19+
*.pickle
1920
*.pem
2021
*.key
2122

app/__main__.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,29 @@
33
from loguru import logger
44

55
from app.const import channels_list
6+
from app.db.data_table import Channel
67
from app.integrations.ytapi import YTApiClient
78
from app.integrations.ytdlp import YTChannelDownloader
8-
from app.schema import ChannelAPIInfoSchema, ChannelInfoSchema, VideoSchema
9+
from app.schema import ChannelAPIInfoSchema, ChannelInfoSchema
910
from app.service.yt_monitor import YTMonitorService
1011

1112
# 1
12-
# downloader = YTChannelDownloader("https://www.youtube.com/@BorisYulin")
13+
downloader = YTChannelDownloader("https://www.youtube.com/@BorisYulin")
1314
# 1.1
1415
# with open("yt-dlp_response.json", 'w', encoding='utf-8') as f:
1516
# json.dump(downloader._get_channel_data(channels_list["channels"][0]), fp=f, ensure_ascii=False, indent=4)
1617
# 1.2
17-
# ytdlp_channel_info: ChannelInfoSchema = downloader.get_channel_info()
18+
ytdlp_channel_info: ChannelInfoSchema = downloader.get_channel_info()
19+
logger.debug(f"ytdlp_channel_info: {ytdlp_channel_info}")
1820
# 1.2
1921
# video_list, channel_id = downloader.get_video_list()
2022
# new_videos, old_videos = downloader.filter_new_old(video_list, channel_id)
2123

2224
# 2
23-
# downloader = YTApiClient()
25+
downloader = YTApiClient()
2426
# 2.1
25-
# ytapi_channel_info: ChannelAPIInfoSchema = downloader.get_channel_info([ytdlp_channel_info.channel_id])[0]
27+
ytapi_channel_info: ChannelAPIInfoSchema = downloader.get_channel_info([ytdlp_channel_info.channel_id])[0]
28+
logger.debug(f"ytapi_channel_info: {ytapi_channel_info}")
2629
# 2.2
2730
# print(downloader.get_video_info(["QpwJEYGCngI"]))
2831
# downloader.update_video_info(["QpwJEYGCngI"])
@@ -36,5 +39,7 @@
3639

3740
# 4
3841
monitor = YTMonitorService(channels_list["channels"])
39-
new_videos = monitor.monitor_channels_for_new_videos()
40-
logger.debug(f"new_videos: {len(new_videos)}")
42+
channel_info: Channel = monitor._combine_channel_info(ytdlp_channel_info, ytapi_channel_info)
43+
logger.debug(f"channel_info: {channel_info}")
44+
# new_videos = monitor.monitor_channels_for_new_videos()
45+
# logger.debug(f"new_videos: {len(new_videos)}")

app/config.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66
class Settings(BaseSettings):
77
app_host: str = "localhost"
88
app_port: int = 9091
9-
storage_path: str = "/mnt/volume"
109

11-
your_username: str = "name"
12-
your_password: str = "password"
10+
storage_path: str = "/mnt/volume"
11+
video_download_path: str = "/videos"
12+
thumbnail_download_path: str = "/videos/thumbnail"
1313

1414
db_host: str = "localhost"
1515
db_port: int = 5432
@@ -18,9 +18,6 @@ class Settings(BaseSettings):
1818
db_username: str = "postgres"
1919
db_password: str = "postgres"
2020

21-
video_download_path: str = "/video/download"
22-
thumbnail_download_path: str = "/video/download/thumbnail"
23-
2421
youtube_api_key: str = "youtube_key"
2522
youtube_secret_json: str = ""
2623

app/integrations/ytapi.py

Lines changed: 62 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import os
2-
import sys
2+
import pickle
33
from datetime import datetime
44

55
import googleapiclient.discovery
6-
import httplib2
6+
from google.auth.transport.requests import Request
7+
from google_auth_oauthlib.flow import InstalledAppFlow
78
from googleapiclient.errors import HttpError
89
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
1210

1311
from app.config import settings
1412
from app.db.base import Session
@@ -19,33 +17,62 @@
1917

2018
class YTApiClient:
2119
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"
2223
self.scopes = ["https://www.googleapis.com/auth/youtube.readonly"]
2324
self.api_service_name = "youtube"
2425
self.api_version = "v3"
25-
self.client_secrets_file = settings.youtube_secret_json
26+
self._client_secrets_file = settings.youtube_secret_json
2627
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+
"""Получить информацию о видео."""
4264
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()
4466
)
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)
4976
return response
5077

5178
def update_video_info(self, video_ids: list[str]) -> None:
@@ -90,34 +117,27 @@ def update_missing_video_info(self, videos_list: list[Video] = []):
90117
self._repository.reset_all_invalid_videos()
91118

92119
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()
104121
youtube = googleapiclient.discovery.build(self.api_service_name, self.api_version, credentials=credentials)
105122
channels_info: list[ChannelAPIInfoSchema] = []
106123

107-
try:
108-
request = youtube.channels().list(
124+
request_func = (
125+
lambda: youtube.channels()
126+
.list(
109127
part="contentDetails,contentOwnerDetails,id,snippet,statistics,status,topicDetails",
110128
id=",".join(channel_ids),
111129
)
112-
response = request.execute()
130+
.execute()
131+
)
132+
response = self._make_request(request_func)
133+
134+
try:
113135
# Преобразуем ответ в объект ChannelAPIInfoSchema
114136
if "items" in response:
115137
for item in response["items"]:
116138
channels_info.append(ChannelAPIInfoSchema.from_api_response(item))
117139

118140
return channels_info
119-
except HttpError as e:
120-
logger.error(f"An HTTP error {e.resp.status} occurred:\n{e.content}")
121141
except KeyError:
122142
logger.error("The response from the API did not contain the expected data.")
123143
except Exception as e:

app/service/yt_monitor.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
from loguru import logger
22

3-
from app.db.data_table import Video
3+
from app.db.data_table import Channel, Thumbnail, Video
44
from app.integrations.ytapi import YTApiClient
55
from app.integrations.ytdlp import YTChannelDownloader
6-
from app.schema import ChannelInfoSchema, VideoSchema
6+
from app.schema import ChannelAPIInfoSchema, ChannelInfoSchema
77

88

99
class YTMonitorService:
@@ -25,3 +25,22 @@ def monitor_channels_for_new_videos(self) -> list[Video]:
2525
channels_new_videos.extend(new_videos)
2626

2727
return channels_new_videos
28+
29+
def _combine_channel_info(
30+
self, ytdlp_channel_info: ChannelInfoSchema, ytapi_channel_info: ChannelAPIInfoSchema
31+
) -> Channel:
32+
combined_channel = Channel(
33+
channel_id=ytdlp_channel_info.channel_id or ytapi_channel_info.id,
34+
customUrl=ytapi_channel_info.customUrl,
35+
title=ytdlp_channel_info.title or ytapi_channel_info.title,
36+
description=ytdlp_channel_info.description or ytapi_channel_info.description,
37+
channel_url=ytdlp_channel_info.channel_url,
38+
channel_follower_count=ytdlp_channel_info.channel_follower_count or ytapi_channel_info.subscriberCount,
39+
viewCount=ytapi_channel_info.viewCount,
40+
videoCount=ytapi_channel_info.videoCount,
41+
published_at=ytapi_channel_info.published_at,
42+
country=ytapi_channel_info.country,
43+
tags=ytdlp_channel_info.tags,
44+
thumbnails=[Thumbnail(**thumb.model_dump()) for thumb in ytdlp_channel_info.thumbnails],
45+
)
46+
return combined_channel

0 commit comments

Comments
 (0)