Skip to content

Commit 1884baa

Browse files
committed
Expand youtube.py error info
1 parent e703f8c commit 1884baa

File tree

3 files changed

+91
-64
lines changed

3 files changed

+91
-64
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
- Change link_announcer.py to only warn on connection errors
1414
- Change user lookup logic in last.fm plugin
1515
- Refactor minecraft_ping plugin for updated mcstatus library
16+
- Expand youtube.py error information
1617
### Fixed
1718
- Fix matching exception in horoscope test
1819
- Fix youtube.py ISO time parse

plugins/youtube.py

Lines changed: 73 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@
88
from cloudbot.util import colors, timeformat
99
from cloudbot.util.formatting import pluralize_auto
1010

11-
youtube_re = re.compile(r'(?:youtube.*?(?:v=|/v/)|youtu\.be/|yooouuutuuube.*?id=)([-_a-zA-Z0-9]+)', re.I)
11+
youtube_re = re.compile(
12+
r'(?:youtube.*?(?:v=|/v/)|youtu\.be/|yooouuutuuube.*?id=)([-_a-zA-Z0-9]+)', re.I
13+
)
1214

1315
base_url = 'https://www.googleapis.com/youtube/v3/'
1416
api_url = base_url + 'videos?part=contentDetails%2C+snippet%2C+statistics&id={}&key={}'
1517
search_api_url = base_url + 'search?part=id&maxResults=1'
1618
playlist_api_url = base_url + 'playlists?part=snippet%2CcontentDetails%2Cstatus'
1719
video_url = "http://youtu.be/%s"
18-
err_no_api = "The YouTube API is off in the Google Developers Console."
1920

2021

2122
class APIError(Exception):
@@ -25,16 +26,41 @@ def __init__(self, message, response=None):
2526
self.response = response
2627

2728

29+
class NoApiKeyError(APIError):
30+
def __init__(self):
31+
super().__init__("Missing API key")
32+
33+
34+
class NoResultsError(APIError):
35+
def __init__(self):
36+
super().__init__("No results")
37+
38+
39+
def handle_api_errors(response):
40+
try:
41+
response.raise_for_status()
42+
except requests.RequestException as e:
43+
try:
44+
data = response.json()
45+
except ValueError:
46+
raise e
47+
48+
errors = data.get('errors')
49+
if not errors:
50+
return
51+
52+
first_error = errors[0]
53+
domain = first_error['domain']
54+
reason = first_error['reason']
55+
raise APIError("API Error ({}/{})".format(domain, reason), data) from e
56+
57+
2858
def get_video_description(video_id):
2959
dev_key = bot.config.get_api_key("google_dev_key")
3060
request = requests.get(api_url.format(video_id, dev_key))
3161
json = request.json()
3262

33-
if json.get('error'):
34-
if json['error']['code'] == 403:
35-
raise APIError(err_no_api, json)
36-
37-
raise APIError("Unknown error", json)
63+
handle_api_errors(request)
3864

3965
data = json['items']
4066
if not data:
@@ -50,7 +76,9 @@ def get_video_description(video_id):
5076
return out
5177

5278
length = isodate.parse_duration(content_details['duration'])
53-
out += ' - length \x02{}\x02'.format(timeformat.format_time(int(length.total_seconds()), simple=True))
79+
out += ' - length \x02{}\x02'.format(
80+
timeformat.format_time(int(length.total_seconds()), simple=True)
81+
)
5482
try:
5583
total_votes = float(statistics['likeCount']) + float(statistics['dislikeCount'])
5684
except (LookupError, ValueError):
@@ -62,18 +90,18 @@ def get_video_description(video_id):
6290
dislikes = pluralize_auto(int(statistics['dislikeCount']), "dislike")
6391

6492
percent = 100 * float(statistics['likeCount']) / total_votes
65-
out += ' - {}, {} (\x02{:.1f}\x02%)'.format(likes,
66-
dislikes, percent)
93+
out += ' - {}, {} (\x02{:.1f}\x02%)'.format(likes, dislikes, percent)
6794

6895
if 'viewCount' in statistics:
6996
views = int(statistics['viewCount'])
70-
out += ' - \x02{:,}\x02 view{}'.format(views, "s"[views == 1:])
97+
out += ' - \x02{:,}\x02 view{}'.format(views, "s"[views == 1 :])
7198

7299
uploader = snippet['channelTitle']
73100

74101
upload_time = isodate.parse_datetime(snippet['publishedAt'])
75-
out += ' - \x02{}\x02 on \x02{}\x02'.format(uploader,
76-
upload_time.strftime("%Y.%m.%d"))
102+
out += ' - \x02{}\x02 on \x02{}\x02'.format(
103+
uploader, upload_time.strftime("%Y.%m.%d")
104+
)
77105

78106
try:
79107
yt_rating = content_details['contentRating']['ytRating']
@@ -86,31 +114,27 @@ def get_video_description(video_id):
86114
return out
87115

88116

89-
def get_video_id(reply, text):
117+
def get_video_id(text):
90118
dev_key = bot.config.get_api_key('google_dev_key')
91119
if not dev_key:
92-
return None, "This command requires a Google Developers Console API key."
120+
raise NoApiKeyError()
93121

94122
try:
95-
request = requests.get(search_api_url, params={'q': text, 'key': dev_key, 'type': 'video'})
96-
request.raise_for_status()
97-
except Exception:
98-
reply("Error performing search.")
99-
raise
123+
request = requests.get(
124+
search_api_url, params={'q': text, 'key': dev_key, 'type': 'video'}
125+
)
126+
except requests.RequestException as e:
127+
raise APIError("Unable to connect to API") from e
100128

101129
json = request.json()
102130

103-
if json.get('error'):
104-
if json['error']['code'] == 403:
105-
return None, err_no_api
106-
107-
return None, "Error performing search."
131+
handle_api_errors(request)
108132

109133
if not json.get('items'):
110-
return None, "No results found."
134+
raise NoResultsError()
111135

112136
video_id = json['items'][0]['id']['videoId']
113-
return video_id, None
137+
return video_id
114138

115139

116140
@hook.regex(youtube_re)
@@ -121,11 +145,8 @@ def youtube_url(match):
121145
@hook.command("youtube", "you", "yt", "y")
122146
def youtube(text, reply):
123147
"""<query> - Returns the first YouTube search result for <query>."""
124-
video_id, err = get_video_id(reply, text)
125-
if err:
126-
return err
127-
128148
try:
149+
video_id = get_video_id(text)
129150
return get_video_description(video_id) + " - " + video_url % video_id
130151
except APIError as e:
131152
reply(e.message)
@@ -135,18 +156,17 @@ def youtube(text, reply):
135156
@hook.command("youtime", "ytime")
136157
def youtime(text, reply):
137158
"""<query> - Gets the total run time of the first YouTube search result for <query>."""
138-
video_id, err = get_video_id(reply, text)
139-
if err:
140-
return err
141-
142159
dev_key = bot.config.get_api_key('google_dev_key')
143-
request = requests.get(api_url.format(video_id, dev_key))
144-
request.raise_for_status()
160+
try:
161+
video_id = get_video_id(text)
162+
request = requests.get(api_url.format(video_id, dev_key))
163+
handle_api_errors(request)
164+
except APIError as e:
165+
reply(e.message)
166+
raise
145167

146168
json = request.json()
147169

148-
if json.get('error'):
149-
return
150170
data = json['items']
151171
snippet = data[0]['snippet']
152172
content_details = data[0]['contentDetails']
@@ -163,42 +183,37 @@ def youtime(text, reply):
163183
length_text = timeformat.format_time(l_sec, simple=True)
164184
total_text = timeformat.format_time(total, accuracy=8)
165185

166-
return 'The video \x02{}\x02 has a length of {} and has been viewed {:,} times for ' \
167-
'a total run time of {}!'.format(snippet['title'], length_text, views,
168-
total_text)
186+
return (
187+
'The video \x02{}\x02 has a length of {} and has been viewed {:,} times for '
188+
'a total run time of {}!'.format(
189+
snippet['title'], length_text, views, total_text
190+
)
191+
)
169192

170193

171-
ytpl_re = re.compile(r'(.*:)//(www.youtube.com/playlist|youtube.com/playlist)(:[0-9]+)?(.*)', re.I)
194+
ytpl_re = re.compile(
195+
r'(.*:)//(www.youtube.com/playlist|youtube.com/playlist)(:[0-9]+)?(.*)', re.I
196+
)
172197

173198

174199
@hook.regex(ytpl_re)
175-
def ytplaylist_url(match, reply):
200+
def ytplaylist_url(match):
176201
location = match.group(4).split("=")[-1]
177202
dev_key = bot.config.get_api_key("google_dev_key")
178-
try:
179-
request = requests.get(playlist_api_url, params={"id": location, "key": dev_key})
180-
request.raise_for_status()
181-
except Exception:
182-
reply("Error looking up playlist.")
183-
raise
203+
request = requests.get(playlist_api_url, params={"id": location, "key": dev_key})
204+
handle_api_errors(request)
184205

185206
json = request.json()
186207

187-
if json.get('error'):
188-
if json['error']['code'] == 403:
189-
return err_no_api
190-
191-
return 'Error looking up playlist.'
192-
193208
data = json['items']
194209
if not data:
195-
return "No results found."
210+
return
196211

197212
snippet = data[0]['snippet']
198213
content_details = data[0]['contentDetails']
199214

200215
title = snippet['title']
201216
author = snippet['channelTitle']
202217
num_videos = int(content_details['itemCount'])
203-
count_videos = ' - \x02{:,}\x02 video{}'.format(num_videos, "s"[num_videos == 1:])
218+
count_videos = ' - \x02{:,}\x02 video{}'.format(num_videos, "s"[num_videos == 1 :])
204219
return "\x02{}\x02 {} - \x02{}\x02".format(title, count_videos, author)

tests/plugin_tests/test_youtube.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,14 @@ def test_no_key(self, mock_requests, mock_api_keys):
7979
'GET',
8080
self.api_url.format(id='foobar', key='APIKEY'),
8181
match_querystring=True,
82-
json={'error': {'code': 403}},
82+
json={
83+
'error': {'code': 500},
84+
'errors': [{'domain': 'foo', 'reason': 'bar'}],
85+
},
8386
status=403,
8487
)
8588

86-
with pytest.raises(youtube.APIError, match="YouTube API is off"):
89+
with pytest.raises(youtube.APIError, match="API Error (foo/bar)"):
8790
youtube.get_video_description('foobar')
8891

8992
def test_http_error(self, mock_requests, mock_api_keys):
@@ -93,7 +96,10 @@ def test_http_error(self, mock_requests, mock_api_keys):
9396
'GET',
9497
self.api_url.format(id='foobar', key='APIKEY'),
9598
match_querystring=True,
96-
json={'error': {'code': 500}},
99+
json={
100+
'error': {'code': 500},
101+
'errors': [{'domain': 'foo', 'reason': 'bar'}],
102+
},
97103
status=500,
98104
)
99105

@@ -159,7 +165,9 @@ def test_success_nsfw(self, mock_requests, mock_api_keys):
159165
from plugins import youtube
160166

161167
data = deepcopy(video_data)
162-
data['items'][0]['contentDetails']['contentRating'] = {"ytRating": "ytAgeRestricted"}
168+
data['items'][0]['contentDetails']['contentRating'] = {
169+
"ytRating": "ytAgeRestricted"
170+
}
163171

164172
mock_requests.add(
165173
'GET',
@@ -208,7 +216,10 @@ def test_command_error_reply(self, mock_requests, mock_api_keys):
208216
'GET',
209217
self.api_url.format(id='foobar', key='APIKEY'),
210218
match_querystring=True,
211-
json={'error': {'code': 500}},
219+
json={
220+
'error': {'code': 500},
221+
'errors': [{'domain': 'foo', 'reason': 'bar'}],
222+
},
212223
status=500,
213224
)
214225

@@ -217,4 +228,4 @@ def test_command_error_reply(self, mock_requests, mock_api_keys):
217228
with pytest.raises(youtube.APIError):
218229
youtube.youtube('test video', reply)
219230

220-
reply.assert_called_with("Unknown error")
231+
reply.assert_called_with('API Error (foo/bar)')

0 commit comments

Comments
 (0)