1
1
import re
2
+ from typing import Iterable , Mapping , Match , Optional , Union
2
3
3
4
import isodate
4
5
import requests
5
6
6
7
from cloudbot import hook
7
8
from cloudbot .bot import bot
8
9
from cloudbot .util import colors , timeformat
9
- from cloudbot .util .formatting import pluralize_auto
10
+ from cloudbot .util .formatting import pluralize_suffix
11
+
12
+ youtube_re = re .compile (
13
+ r'(?:youtube.*?(?:v=|/v/)|youtu\.be/|yooouuutuuube.*?id=)([-_a-zA-Z0-9]+)' , re .I
14
+ )
15
+ ytpl_re = re .compile (
16
+ r'(.*:)//(www.youtube.com/playlist|youtube.com/playlist)(:[0-9]+)?(.*)' , re .I
17
+ )
10
18
11
- youtube_re = re .compile (r'(?:youtube.*?(?:v=|/v/)|youtu\.be/|yooouuutuuube.*?id=)([-_a-zA-Z0-9]+)' , re .I )
12
19
13
20
base_url = 'https://www.googleapis.com/youtube/v3/'
14
- api_url = base_url + 'videos?part=contentDetails%2C+snippet%2C+statistics&id={}&key={}'
15
- search_api_url = base_url + 'search?part=id&maxResults=1'
16
- playlist_api_url = base_url + 'playlists?part=snippet%2CcontentDetails%2Cstatus'
17
- video_url = "http://youtu.be/%s"
18
- err_no_api = "The YouTube API is off in the Google Developers Console."
19
21
20
22
21
23
class APIError (Exception ):
22
- def __init__ (self , message , response = None ):
24
+ def __init__ (self , message : str , response : Optional [ str ] = None ) -> None :
23
25
super ().__init__ (message )
24
26
self .message = message
25
27
self .response = response
26
28
27
29
28
- def get_video_description (video_id ):
29
- dev_key = bot .config .get_api_key ("google_dev_key" )
30
- request = requests .get (api_url .format (video_id , dev_key ))
31
- json = request .json ()
30
+ class NoApiKeyError (APIError ):
31
+ def __init__ (self ) -> None :
32
+ super ().__init__ ("Missing API key" )
33
+
34
+
35
+ class NoResultsError (APIError ):
36
+ def __init__ (self ) -> None :
37
+ super ().__init__ ("No results" )
38
+
39
+
40
+ def raise_api_errors (response : requests .Response ) -> None :
41
+ try :
42
+ response .raise_for_status ()
43
+ except requests .RequestException as e :
44
+ try :
45
+ data = response .json ()
46
+ except ValueError :
47
+ raise e
48
+
49
+ errors = data .get ('errors' )
50
+ if not errors :
51
+ return
52
+
53
+ first_error = errors [0 ]
54
+ domain = first_error ['domain' ]
55
+ reason = first_error ['reason' ]
56
+ raise APIError ("API Error ({}/{})" .format (domain , reason ), data ) from e
57
+
58
+
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 })
85
+
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
+
32
90
33
- if json .get ('error' ):
34
- if json ['error' ]['code' ] == 403 :
35
- raise APIError (err_no_api , json )
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
+ )
36
95
37
- raise APIError ("Unknown error" , json )
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 ()
38
103
39
104
data = json ['items' ]
40
105
if not data :
41
- return None
106
+ raise NoResultsError ()
42
107
43
- snippet = data [0 ]['snippet' ]
44
- statistics = data [0 ]['statistics' ]
45
- content_details = data [0 ]['contentDetails' ]
108
+ item = data [0 ]
109
+ snippet = item ['snippet' ]
110
+ statistics = item ['statistics' ]
111
+ content_details = item ['contentDetails' ]
46
112
47
113
out = '\x02 {}\x02 ' .format (snippet ['title' ])
48
114
49
115
if not content_details .get ('duration' ):
50
116
return out
51
117
52
118
length = isodate .parse_duration (content_details ['duration' ])
53
- out += ' - length \x02 {}\x02 ' .format (timeformat .format_time (int (length .total_seconds ()), simple = True ))
119
+ out += ' - length \x02 {}\x02 ' .format (
120
+ timeformat .format_time (int (length .total_seconds ()), simple = True )
121
+ )
54
122
try :
55
123
total_votes = float (statistics ['likeCount' ]) + float (statistics ['dislikeCount' ])
56
124
except (LookupError , ValueError ):
57
125
total_votes = 0
58
126
59
127
if total_votes != 0 :
60
128
# format
61
- likes = pluralize_auto (int (statistics ['likeCount' ]), "like" )
62
- dislikes = pluralize_auto (int (statistics ['dislikeCount' ]), "dislike" )
129
+ likes = pluralize_suffix (int (statistics ['likeCount' ]), "like" )
130
+ dislikes = pluralize_suffix (int (statistics ['dislikeCount' ]), "dislike" )
63
131
64
132
percent = 100 * float (statistics ['likeCount' ]) / total_votes
65
- out += ' - {}, {} (\x02 {:.1f}\x02 %)' .format (likes ,
66
- dislikes , percent )
133
+ out += ' - {}, {} (\x02 {:.1f}\x02 %)' .format (likes , dislikes , percent )
67
134
68
135
if 'viewCount' in statistics :
69
136
views = int (statistics ['viewCount' ])
70
- out += ' - \x02 {:,}\x02 view{}' .format (views , "s" [views == 1 :])
137
+ out += ' - \x02 {:,}\x02 view{}' .format (views , "s" [views == 1 :])
71
138
72
139
uploader = snippet ['channelTitle' ]
73
140
74
141
upload_time = isodate .parse_datetime (snippet ['publishedAt' ])
75
- out += ' - \x02 {}\x02 on \x02 {}\x02 ' .format (uploader ,
76
- upload_time .strftime ("%Y.%m.%d" ))
142
+ out += ' - \x02 {}\x02 on \x02 {}\x02 ' .format (
143
+ uploader , upload_time .strftime ("%Y.%m.%d" )
144
+ )
77
145
78
146
try :
79
147
yt_rating = content_details ['contentRating' ]['ytRating' ]
@@ -86,119 +154,100 @@ def get_video_description(video_id):
86
154
return out
87
155
88
156
89
- def get_video_id (reply , text ):
90
- dev_key = bot .config .get_api_key ('google_dev_key' )
91
- if not dev_key :
92
- return None , "This command requires a Google Developers Console API key."
93
-
157
+ def get_video_id (text : str ) -> str :
94
158
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
159
+ request = do_search (text )
160
+ except requests .RequestException as e :
161
+ raise APIError ("Unable to connect to API" ) from e
100
162
163
+ raise_api_errors (request )
101
164
json = request .json ()
102
165
103
- if json .get ('error' ):
104
- if json ['error' ]['code' ] == 403 :
105
- return None , err_no_api
106
-
107
- return None , "Error performing search."
108
-
109
166
if not json .get ('items' ):
110
- return None , "No results found."
167
+ raise NoResultsError ()
111
168
112
- video_id = json ['items' ][0 ]['id' ]['videoId' ]
113
- return video_id , None
169
+ video_id = json ['items' ][0 ]['id' ]['videoId' ] # type: str
170
+ return video_id
114
171
115
172
116
173
@hook .regex (youtube_re )
117
- def youtube_url (match ) :
174
+ def youtube_url (match : Match [ str ]) -> str :
118
175
return get_video_description (match .group (1 ))
119
176
120
177
121
178
@hook .command ("youtube" , "you" , "yt" , "y" )
122
- def youtube (text , reply ):
179
+ def youtube (text : str , reply ) -> str :
123
180
"""<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
-
128
181
try :
129
- return get_video_description (video_id ) + " - " + video_url % video_id
182
+ video_id = get_video_id (text )
183
+ return get_video_description (video_id ) + " - " + make_short_url (video_id )
184
+ except NoResultsError as e :
185
+ return e .message
130
186
except APIError as e :
131
187
reply (e .message )
132
188
raise
133
189
134
190
135
191
@hook .command ("youtime" , "ytime" )
136
- def youtime (text , reply ):
192
+ def youtime (text : str , reply ) -> str :
137
193
"""<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
-
142
- 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 ()
194
+ parts = ['statistics' , 'contentDetails' , 'snippet' ]
195
+ try :
196
+ video_id = get_video_id (text )
197
+ request = get_video (video_id , parts )
198
+ raise_api_errors (request )
199
+ except NoResultsError as e :
200
+ return e .message
201
+ except APIError as e :
202
+ reply (e .message )
203
+ raise
145
204
146
205
json = request .json ()
147
206
148
- if json .get ('error' ):
149
- return
150
207
data = json ['items' ]
151
- snippet = data [0 ]['snippet' ]
152
- content_details = data [0 ]['contentDetails' ]
153
- statistics = data [0 ]['statistics' ]
208
+ item = data [0 ]
209
+ snippet = item ['snippet' ]
210
+ content_details = item ['contentDetails' ]
211
+ statistics = item ['statistics' ]
154
212
155
- if not content_details .get ('duration' ):
156
- return
213
+ duration = content_details .get ('duration' )
214
+ if not duration :
215
+ return "Missing duration in API response"
157
216
158
- length = isodate .parse_duration (content_details [ ' duration' ] )
217
+ length = isodate .parse_duration (duration )
159
218
l_sec = int (length .total_seconds ())
160
219
views = int (statistics ['viewCount' ])
161
220
total = int (l_sec * views )
162
221
163
222
length_text = timeformat .format_time (l_sec , simple = True )
164
223
total_text = timeformat .format_time (total , accuracy = 8 )
165
224
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 )
169
-
170
-
171
- ytpl_re = re . compile ( r'(.*:)//(www.youtube.com/playlist|youtube.com/playlist)(:[0-9]+)?(.*)' , re . I )
225
+ return (
226
+ 'The video \x02 {} \x02 has a length of {} and has been viewed {:,} times for '
227
+ 'a total run time of {}!' . format (
228
+ snippet [ 'title' ], length_text , views , total_text
229
+ )
230
+ )
172
231
173
232
174
233
@hook .regex (ytpl_re )
175
- def ytplaylist_url (match , reply ) :
234
+ def ytplaylist_url (match : Match [ str ]) -> str :
176
235
location = match .group (4 ).split ("=" )[- 1 ]
177
- 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
236
+ request = get_playlist (location , ['contentDetails' , 'snippet' ])
237
+ raise_api_errors (request )
184
238
185
239
json = request .json ()
186
240
187
- if json .get ('error' ):
188
- if json ['error' ]['code' ] == 403 :
189
- return err_no_api
190
-
191
- return 'Error looking up playlist.'
192
-
193
241
data = json ['items' ]
194
242
if not data :
195
- return "No results found."
243
+ raise NoResultsError ()
196
244
197
- snippet = data [0 ]['snippet' ]
198
- content_details = data [0 ]['contentDetails' ]
245
+ item = data [0 ]
246
+ snippet = item ['snippet' ]
247
+ content_details = item ['contentDetails' ]
199
248
200
249
title = snippet ['title' ]
201
250
author = snippet ['channelTitle' ]
202
251
num_videos = int (content_details ['itemCount' ])
203
- count_videos = ' - \x02 {:,}\x02 video{}' .format (num_videos , "s" [num_videos == 1 :])
252
+ count_videos = ' - \x02 {:,}\x02 video{}' .format (num_videos , "s" [num_videos == 1 :])
204
253
return "\x02 {}\x02 {} - \x02 {}\x02 " .format (title , count_videos , author )
0 commit comments