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
10
11
11
12
youtube_re = re .compile (
12
13
r'(?:youtube.*?(?:v=|/v/)|youtu\.be/|yooouuutuuube.*?id=)([-_a-zA-Z0-9]+)' , re .I
13
14
)
15
+ ytpl_re = re .compile (
16
+ r'(.*:)//(www.youtube.com/playlist|youtube.com/playlist)(:[0-9]+)?(.*)' , re .I
17
+ )
18
+
14
19
15
20
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"
20
21
21
22
22
23
class APIError (Exception ):
23
- def __init__ (self , message , response = None ):
24
+ def __init__ (self , message : str , response : Optional [ str ] = None ) -> None :
24
25
super ().__init__ (message )
25
26
self .message = message
26
27
self .response = response
27
28
28
29
29
30
class NoApiKeyError (APIError ):
30
- def __init__ (self ):
31
+ def __init__ (self ) -> None :
31
32
super ().__init__ ("Missing API key" )
32
33
33
34
34
35
class NoResultsError (APIError ):
35
- def __init__ (self ):
36
+ def __init__ (self ) -> None :
36
37
super ().__init__ ("No results" )
37
38
38
39
39
- def handle_api_errors (response ) :
40
+ def raise_api_errors (response : requests . Response ) -> None :
40
41
try :
41
42
response .raise_for_status ()
42
43
except requests .RequestException as e :
@@ -55,20 +56,59 @@ def handle_api_errors(response):
55
56
raise APIError ("API Error ({}/{})" .format (domain , reason ), data ) from e
56
57
57
58
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 })
62
85
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 ()
64
103
65
104
data = json ['items' ]
66
105
if not data :
67
- return None
106
+ raise NoResultsError ()
68
107
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' ]
72
112
73
113
out = '\x02 {}\x02 ' .format (snippet ['title' ])
74
114
@@ -86,8 +126,8 @@ def get_video_description(video_id):
86
126
87
127
if total_votes != 0 :
88
128
# 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" )
91
131
92
132
percent = 100 * float (statistics ['likeCount' ]) / total_votes
93
133
out += ' - {}, {} (\x02 {:.1f}\x02 %)' .format (likes , dislikes , percent )
@@ -114,68 +154,67 @@ def get_video_description(video_id):
114
154
return out
115
155
116
156
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 :
122
158
try :
123
- request = requests .get (
124
- search_api_url , params = {'q' : text , 'key' : dev_key , 'type' : 'video' }
125
- )
159
+ request = do_search (text )
126
160
except requests .RequestException as e :
127
161
raise APIError ("Unable to connect to API" ) from e
128
162
163
+ raise_api_errors (request )
129
164
json = request .json ()
130
165
131
- handle_api_errors (request )
132
-
133
166
if not json .get ('items' ):
134
167
raise NoResultsError ()
135
168
136
- video_id = json ['items' ][0 ]['id' ]['videoId' ]
169
+ video_id = json ['items' ][0 ]['id' ]['videoId' ] # type: str
137
170
return video_id
138
171
139
172
140
173
@hook .regex (youtube_re )
141
- def youtube_url (match ) :
174
+ def youtube_url (match : Match [ str ]) -> str :
142
175
return get_video_description (match .group (1 ))
143
176
144
177
145
178
@hook .command ("youtube" , "you" , "yt" , "y" )
146
- def youtube (text , reply ):
179
+ def youtube (text : str , reply ) -> str :
147
180
"""<query> - Returns the first YouTube search result for <query>."""
148
181
try :
149
182
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
151
186
except APIError as e :
152
187
reply (e .message )
153
188
raise
154
189
155
190
156
191
@hook .command ("youtime" , "ytime" )
157
- def youtime (text , reply ):
192
+ def youtime (text : str , reply ) -> str :
158
193
"""<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' ]
160
195
try :
161
196
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
164
201
except APIError as e :
165
202
reply (e .message )
166
203
raise
167
204
168
205
json = request .json ()
169
206
170
207
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' ]
174
212
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"
177
216
178
- length = isodate .parse_duration (content_details [ ' duration' ] )
217
+ length = isodate .parse_duration (duration )
179
218
l_sec = int (length .total_seconds ())
180
219
views = int (statistics ['viewCount' ])
181
220
total = int (l_sec * views )
@@ -191,26 +230,21 @@ def youtime(text, reply):
191
230
)
192
231
193
232
194
- ytpl_re = re .compile (
195
- r'(.*:)//(www.youtube.com/playlist|youtube.com/playlist)(:[0-9]+)?(.*)' , re .I
196
- )
197
-
198
-
199
233
@hook .regex (ytpl_re )
200
- def ytplaylist_url (match ) :
234
+ def ytplaylist_url (match : Match [ str ]) -> str :
201
235
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 )
205
238
206
239
json = request .json ()
207
240
208
241
data = json ['items' ]
209
242
if not data :
210
- return
243
+ raise NoResultsError ()
211
244
212
- snippet = data [0 ]['snippet' ]
213
- content_details = data [0 ]['contentDetails' ]
245
+ item = data [0 ]
246
+ snippet = item ['snippet' ]
247
+ content_details = item ['contentDetails' ]
214
248
215
249
title = snippet ['title' ]
216
250
author = snippet ['channelTitle' ]
0 commit comments