Skip to content

Commit 459ccd3

Browse files
committed
Clean up youtube requests
1 parent 1884baa commit 459ccd3

File tree

2 files changed

+97
-70
lines changed

2 files changed

+97
-70
lines changed

plugins/youtube.py

Lines changed: 90 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,43 @@
11
import re
2+
from typing import Iterable, Mapping, Match, Optional, Union
23

34
import isodate
45
import requests
56

67
from cloudbot import hook
78
from cloudbot.bot import bot
89
from cloudbot.util import colors, timeformat
9-
from cloudbot.util.formatting import pluralize_auto
10+
from cloudbot.util.formatting import pluralize_suffix
1011

1112
youtube_re = re.compile(
1213
r'(?:youtube.*?(?:v=|/v/)|youtu\.be/|yooouuutuuube.*?id=)([-_a-zA-Z0-9]+)', re.I
1314
)
15+
ytpl_re = re.compile(
16+
r'(.*:)//(www.youtube.com/playlist|youtube.com/playlist)(:[0-9]+)?(.*)', re.I
17+
)
18+
1419

1520
base_url = 'https://www.googleapis.com/youtube/v3/'
16-
api_url = base_url + 'videos?part=contentDetails%2C+snippet%2C+statistics&id={}&key={}'
17-
search_api_url = base_url + 'search?part=id&maxResults=1'
18-
playlist_api_url = base_url + 'playlists?part=snippet%2CcontentDetails%2Cstatus'
19-
video_url = "http://youtu.be/%s"
2021

2122

2223
class APIError(Exception):
23-
def __init__(self, message, response=None):
24+
def __init__(self, message: str, response: Optional[str] = None) -> None:
2425
super().__init__(message)
2526
self.message = message
2627
self.response = response
2728

2829

2930
class NoApiKeyError(APIError):
30-
def __init__(self):
31+
def __init__(self) -> None:
3132
super().__init__("Missing API key")
3233

3334

3435
class NoResultsError(APIError):
35-
def __init__(self):
36+
def __init__(self) -> None:
3637
super().__init__("No results")
3738

3839

39-
def handle_api_errors(response):
40+
def raise_api_errors(response: requests.Response) -> None:
4041
try:
4142
response.raise_for_status()
4243
except requests.RequestException as e:
@@ -55,20 +56,59 @@ def handle_api_errors(response):
5556
raise APIError("API Error ({}/{})".format(domain, reason), data) from e
5657

5758

58-
def get_video_description(video_id):
59-
dev_key = bot.config.get_api_key("google_dev_key")
60-
request = requests.get(api_url.format(video_id, dev_key))
61-
json = request.json()
59+
def make_short_url(video_id: str) -> str:
60+
return "http://youtu.be/{}".format(video_id)
61+
62+
63+
ParamValues = Union[int, str]
64+
ParamMap = Mapping[str, ParamValues]
65+
Parts = Iterable[str]
66+
67+
68+
def do_request(
69+
method: str, parts: Parts, params: Optional[ParamMap] = None, **kwargs: ParamValues
70+
) -> requests.Response:
71+
api_key = bot.config.get_api_key("google_dev_key")
72+
if not api_key:
73+
raise NoApiKeyError()
74+
75+
if params:
76+
kwargs.update(params)
77+
78+
kwargs['part'] = ','.join(parts)
79+
kwargs['key'] = api_key
80+
return requests.get(base_url + method, kwargs)
81+
82+
83+
def get_video(video_id: str, parts: Parts) -> requests.Response:
84+
return do_request('videos', parts, params={'maxResults': 1, 'id': video_id})
6285

63-
handle_api_errors(request)
86+
87+
def get_playlist(playlist_id: str, parts: Parts) -> requests.Response:
88+
return do_request('playlists', parts, params={'maxResults': 1, 'id': playlist_id})
89+
90+
91+
def do_search(term: str, result_type: str = 'video') -> requests.Response:
92+
return do_request(
93+
'search', ['snippet'], params={'maxResults': 1, 'q': term, 'type': result_type}
94+
)
95+
96+
97+
def get_video_description(video_id: str) -> str:
98+
parts = ['statistics', 'contentDetails', 'snippet']
99+
request = get_video(video_id, parts)
100+
raise_api_errors(request)
101+
102+
json = request.json()
64103

65104
data = json['items']
66105
if not data:
67-
return None
106+
raise NoResultsError()
68107

69-
snippet = data[0]['snippet']
70-
statistics = data[0]['statistics']
71-
content_details = data[0]['contentDetails']
108+
item = data[0]
109+
snippet = item['snippet']
110+
statistics = item['statistics']
111+
content_details = item['contentDetails']
72112

73113
out = '\x02{}\x02'.format(snippet['title'])
74114

@@ -86,8 +126,8 @@ def get_video_description(video_id):
86126

87127
if total_votes != 0:
88128
# format
89-
likes = pluralize_auto(int(statistics['likeCount']), "like")
90-
dislikes = pluralize_auto(int(statistics['dislikeCount']), "dislike")
129+
likes = pluralize_suffix(int(statistics['likeCount']), "like")
130+
dislikes = pluralize_suffix(int(statistics['dislikeCount']), "dislike")
91131

92132
percent = 100 * float(statistics['likeCount']) / total_votes
93133
out += ' - {}, {} (\x02{:.1f}\x02%)'.format(likes, dislikes, percent)
@@ -114,68 +154,67 @@ def get_video_description(video_id):
114154
return out
115155

116156

117-
def get_video_id(text):
118-
dev_key = bot.config.get_api_key('google_dev_key')
119-
if not dev_key:
120-
raise NoApiKeyError()
121-
157+
def get_video_id(text: str) -> str:
122158
try:
123-
request = requests.get(
124-
search_api_url, params={'q': text, 'key': dev_key, 'type': 'video'}
125-
)
159+
request = do_search(text)
126160
except requests.RequestException as e:
127161
raise APIError("Unable to connect to API") from e
128162

163+
raise_api_errors(request)
129164
json = request.json()
130165

131-
handle_api_errors(request)
132-
133166
if not json.get('items'):
134167
raise NoResultsError()
135168

136-
video_id = json['items'][0]['id']['videoId']
169+
video_id = json['items'][0]['id']['videoId'] # type: str
137170
return video_id
138171

139172

140173
@hook.regex(youtube_re)
141-
def youtube_url(match):
174+
def youtube_url(match: Match[str]) -> str:
142175
return get_video_description(match.group(1))
143176

144177

145178
@hook.command("youtube", "you", "yt", "y")
146-
def youtube(text, reply):
179+
def youtube(text: str, reply) -> str:
147180
"""<query> - Returns the first YouTube search result for <query>."""
148181
try:
149182
video_id = get_video_id(text)
150-
return get_video_description(video_id) + " - " + video_url % video_id
183+
return get_video_description(video_id) + " - " + make_short_url(video_id)
184+
except NoResultsError as e:
185+
return e.message
151186
except APIError as e:
152187
reply(e.message)
153188
raise
154189

155190

156191
@hook.command("youtime", "ytime")
157-
def youtime(text, reply):
192+
def youtime(text: str, reply) -> str:
158193
"""<query> - Gets the total run time of the first YouTube search result for <query>."""
159-
dev_key = bot.config.get_api_key('google_dev_key')
194+
parts = ['statistics', 'contentDetails', 'snippet']
160195
try:
161196
video_id = get_video_id(text)
162-
request = requests.get(api_url.format(video_id, dev_key))
163-
handle_api_errors(request)
197+
request = get_video(video_id, parts)
198+
raise_api_errors(request)
199+
except NoResultsError as e:
200+
return e.message
164201
except APIError as e:
165202
reply(e.message)
166203
raise
167204

168205
json = request.json()
169206

170207
data = json['items']
171-
snippet = data[0]['snippet']
172-
content_details = data[0]['contentDetails']
173-
statistics = data[0]['statistics']
208+
item = data[0]
209+
snippet = item['snippet']
210+
content_details = item['contentDetails']
211+
statistics = item['statistics']
174212

175-
if not content_details.get('duration'):
176-
return
213+
duration = content_details.get('duration')
214+
if not duration:
215+
return "Missing duration in API response"
177216

178-
length = isodate.parse_duration(content_details['duration'])
217+
length = isodate.parse_duration(duration)
179218
l_sec = int(length.total_seconds())
180219
views = int(statistics['viewCount'])
181220
total = int(l_sec * views)
@@ -191,26 +230,21 @@ def youtime(text, reply):
191230
)
192231

193232

194-
ytpl_re = re.compile(
195-
r'(.*:)//(www.youtube.com/playlist|youtube.com/playlist)(:[0-9]+)?(.*)', re.I
196-
)
197-
198-
199233
@hook.regex(ytpl_re)
200-
def ytplaylist_url(match):
234+
def ytplaylist_url(match: Match[str]) -> str:
201235
location = match.group(4).split("=")[-1]
202-
dev_key = bot.config.get_api_key("google_dev_key")
203-
request = requests.get(playlist_api_url, params={"id": location, "key": dev_key})
204-
handle_api_errors(request)
236+
request = get_playlist(location, ['contentDetails', 'snippet'])
237+
raise_api_errors(request)
205238

206239
json = request.json()
207240

208241
data = json['items']
209242
if not data:
210-
return
243+
raise NoResultsError()
211244

212-
snippet = data[0]['snippet']
213-
content_details = data[0]['contentDetails']
245+
item = data[0]
246+
snippet = item['snippet']
247+
content_details = item['contentDetails']
214248

215249
title = snippet['title']
216250
author = snippet['channelTitle']

tests/plugin_tests/test_youtube.py

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -67,26 +67,18 @@ def mock_api_keys():
6767

6868
class TestGetVideoDescription:
6969
base_url = 'https://www.googleapis.com/youtube/v3/'
70+
"videos?maxResults=1&id=phL7P6gtZRM&parts=statistics%2CcontentDetails%2Csnippet&key=APIKEY"
7071
api_url = base_url + (
71-
'videos?part=contentDetails%2C+snippet%2C+statistics&id={id}&key={key}'
72+
'videos?maxResults=1&id={id}&part=statistics%2CcontentDetails%2Csnippet&key={key}'
7273
)
7374
search_api_url = base_url + 'search?part=id&maxResults=1'
7475

7576
def test_no_key(self, mock_requests, mock_api_keys):
7677
from plugins import youtube
7778

78-
mock_requests.add(
79-
'GET',
80-
self.api_url.format(id='foobar', key='APIKEY'),
81-
match_querystring=True,
82-
json={
83-
'error': {'code': 500},
84-
'errors': [{'domain': 'foo', 'reason': 'bar'}],
85-
},
86-
status=403,
87-
)
79+
bot.config.get_api_key.return_value = None
8880

89-
with pytest.raises(youtube.APIError, match="API Error (foo/bar)"):
81+
with pytest.raises(youtube.NoApiKeyError):
9082
youtube.get_video_description('foobar')
9183

9284
def test_http_error(self, mock_requests, mock_api_keys):
@@ -103,7 +95,7 @@ def test_http_error(self, mock_requests, mock_api_keys):
10395
status=500,
10496
)
10597

106-
with pytest.raises(youtube.APIError, match="Unknown error"):
98+
with pytest.raises(youtube.APIError, match=r'API Error \(foo/bar\)'):
10799
youtube.get_video_description('foobar')
108100

109101
def test_success(self, mock_requests, mock_api_keys):
@@ -198,7 +190,8 @@ def test_no_results(self, mock_requests, mock_api_keys):
198190
json=data,
199191
)
200192

201-
assert youtube.get_video_description('phL7P6gtZRM') is None
193+
with pytest.raises(youtube.NoResultsError):
194+
youtube.get_video_description('phL7P6gtZRM')
202195

203196
def test_command_error_reply(self, mock_requests, mock_api_keys):
204197
from plugins import youtube

0 commit comments

Comments
 (0)