-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathyoutube_manager.py
More file actions
158 lines (132 loc) · 5.89 KB
/
youtube_manager.py
File metadata and controls
158 lines (132 loc) · 5.89 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
import os
import logging
import time
from pathlib import Path
from typing import Optional
try:
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
except ImportError:
pass
# Same scope as responder to share token
SCOPES = ['https://www.googleapis.com/auth/youtube.force-ssl']
class YouTubeManager:
"""
Manages YouTube Broadcast Metadata (Title, Description, Tags).
Uses the same token.json as YouTubeResponder.
"""
def __init__(self, token_file='token.json', client_secret='client_secret.json', mock=False):
self.logger = logging.getLogger(__name__)
self.base_dir = Path(__file__).parent
self.token_path = self.base_dir / token_file
self.client_secrets_path = self.base_dir / client_secret
self.youtube = None
self.credentials = None
self.mock = mock
# Rate Limiting
self.last_update_time = 0
self.update_cooldown = 300 # 5 minutes
def authenticate(self) -> bool:
"""Load credentials or start OAuth flow"""
if self.mock:
self.logger.info("🔐 MOCK: YouTube Auth successful")
return True
try:
if self.token_path.exists():
self.credentials = Credentials.from_authorized_user_file(
str(self.token_path), SCOPES
)
if not self.credentials or not self.credentials.valid:
if self.credentials and self.credentials.expired and self.credentials.refresh_token:
self.logger.info("🔄 Refreshing access token...")
self.credentials.refresh(Request())
else:
self.logger.info("🔐 Starting new OAuth flow for YouTube Manager...")
flow = InstalledAppFlow.from_client_secrets_file(
str(self.client_secrets_path), SCOPES
)
self.credentials = flow.run_local_server(port=0)
# Save
with open(self.token_path, 'w') as token:
token.write(self.credentials.to_json())
self.youtube = build('youtube', 'v3', credentials=self.credentials)
return True
except Exception as e:
self.logger.error(f"❌ Auth Failed: {e}")
return False
def update_broadcast(self, video_id: str, title: str, description: str = None) -> bool:
"""
Update the title/description of a live broadcast.
Costs ~50 units. Use sparingly.
"""
if not self.youtube:
if not self.authenticate(): return False
# Rate Limit Check
# For mock, we might want to bypass cooldown for testing?
# But let's keep it to verify logic, unless forced.
# Actually, for dress rehearsal, we want to see it happen fast.
# So if mock, override cooldown? No, dress rehearsal waits 10s+.
if not self.mock and time.time() - self.last_update_time < self.update_cooldown:
self.logger.warning(f"⏳ Update skipped: Cooldown active ({self.update_cooldown}s)")
return False
try:
self.logger.info(f"📡 Updating Broadcast Metadata for {video_id}...")
if self.mock:
self.logger.info(f"✅ YouTube Updated (MOCK): '{title}'")
self.last_update_time = time.time()
return True
# 1. Get current broadcast details (to get snippet)
# We need the 'snippet' to update it.
# Actually, liveBroadcasts.update requires 'id' and 'snippet'.
# Note: video_id is often same as broadcast_id, need to verifying.
# Usually we need to look up broadcast ID from video ID?
# Or assume video_id == broadcast_id for simple cases?
# Let's try to find the broadcast ID associated with this video ID.
request = self.youtube.liveBroadcasts().list(
part="id,snippet",
id=video_id
)
response = request.execute()
if not response.get("items"):
# Maybe video_id != broadcast_id?
# Try searching by status=active? No, that returns all.
self.logger.error(f"❌ Broadcast not found for ID: {video_id}")
return False
broadcast = response["items"][0]
snippet = broadcast["snippet"]
# Update fields
snippet["title"] = title
if description:
snippet["description"] = description
# specific call to update
update_request = self.youtube.liveBroadcasts().update(
part="snippet",
body={
"id": video_id,
"snippet": snippet
}
)
update_response = update_request.execute()
self.logger.info(f"✅ YouTube Updated: '{title}'")
self.last_update_time = time.time()
return True
except HttpError as e:
self.logger.error(f"❌ YouTube API Error: {e}")
return False
except Exception as e:
self.logger.error(f"❌ Update Failed: {e}")
return False
if __name__ == "__main__":
# Test CLI
import sys
logging.basicConfig(level=logging.INFO)
mgr = YouTubeManager()
if len(sys.argv) > 2:
vid = sys.argv[1]
new_title = sys.argv[2]
mgr.update_broadcast(vid, new_title)
else:
print("Usage: python3 youtube_manager.py <video_id> <new_title>")