##// END OF EJS Templates
Download webm videos from youtube
neko259 -
r1328:3352da82 default
parent child Browse files
Show More
@@ -0,0 +1,83 b''
1 import os
2 import re
3
4 from django.core.files.uploadedfile import SimpleUploadedFile
5 from pytube import YouTube
6 import requests
7
8 from boards.utils import validate_file_size
9
10 YOUTUBE_VIDEO_FORMAT = 'webm'
11
12 HTTP_RESULT_OK = 200
13
14 HEADER_CONTENT_LENGTH = 'content-length'
15 HEADER_CONTENT_TYPE = 'content-type'
16
17 FILE_DOWNLOAD_CHUNK_BYTES = 100000
18
19 YOUTUBE_URL = re.compile(r'https?://www\.youtube\.com/watch\?v=\w+')
20
21
22 class Downloader:
23 @staticmethod
24 def handles(url: str) -> bool:
25 return False
26
27 @staticmethod
28 def download(url: str):
29 # Verify content headers
30 response_head = requests.head(url, verify=False)
31 content_type = response_head.headers[HEADER_CONTENT_TYPE].split(';')[0]
32 length_header = response_head.headers.get(HEADER_CONTENT_LENGTH)
33 if length_header:
34 length = int(length_header)
35 validate_file_size(length)
36 # Get the actual content into memory
37 response = requests.get(url, verify=False, stream=True)
38
39 # Download file, stop if the size exceeds limit
40 size = 0
41 content = b''
42 for chunk in response.iter_content(FILE_DOWNLOAD_CHUNK_BYTES):
43 size += len(chunk)
44 validate_file_size(size)
45 content += chunk
46
47 if response.status_code == HTTP_RESULT_OK and content:
48 # Set a dummy file name that will be replaced
49 # anyway, just keep the valid extension
50 filename = 'file.' + content_type.split('/')[1]
51 return SimpleUploadedFile(filename, content, content_type)
52
53
54 class YouTubeDownloader(Downloader):
55 @staticmethod
56 def download(url: str):
57 yt = YouTube()
58 yt.from_url(url)
59 videos = yt.filter(YOUTUBE_VIDEO_FORMAT)
60 if len(videos) > 0:
61 video = videos[0]
62 filename = '{}.{}'.format(video.filename, video.extension)
63 try:
64 video.download(on_progress=YouTubeDownloader.on_progress)
65
66 file = open(filename, 'rb')
67 content = file.read()
68 file.close()
69
70 os.remove(filename)
71 return SimpleUploadedFile(filename, content, video.extension)
72 except Exception as e:
73 if os.path.isfile(filename):
74 os.remove(filename)
75 raise e
76
77 @staticmethod
78 def handles(url: str) -> bool:
79 return YOUTUBE_URL.match(url)
80
81 @staticmethod
82 def on_progress(bytes, file_size, start):
83 validate_file_size(file_size)
@@ -1,395 +1,364 b''
1 1 import hashlib
2 2 import re
3 3 import time
4 4
5 5 import pytz
6 6 from django import forms
7 7 from django.core.files.uploadedfile import SimpleUploadedFile
8 8 from django.core.exceptions import ObjectDoesNotExist
9 9 from django.forms.util import ErrorList
10 10 from django.utils.translation import ugettext_lazy as _
11 import requests
12 11
13 12 from boards.mdx_neboard import formatters
13 from boards.models.attachment.downloaders import Downloader
14 14 from boards.models.post import TITLE_MAX_LENGTH
15 15 from boards.models import Tag, Post
16 from boards.utils import validate_file_size
16 17 from neboard import settings
17 18 import boards.settings as board_settings
18 19 import neboard
19 20
20 HEADER_CONTENT_LENGTH = 'content-length'
21 HEADER_CONTENT_TYPE = 'content-type'
22
23 21 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
24 22
25 23 VETERAN_POSTING_DELAY = 5
26 24
27 25 ATTRIBUTE_PLACEHOLDER = 'placeholder'
28 26 ATTRIBUTE_ROWS = 'rows'
29 27
30 28 LAST_POST_TIME = 'last_post_time'
31 29 LAST_LOGIN_TIME = 'last_login_time'
32 30 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
33 31 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
34 32
35 33 LABEL_TITLE = _('Title')
36 34 LABEL_TEXT = _('Text')
37 35 LABEL_TAG = _('Tag')
38 36 LABEL_SEARCH = _('Search')
39 37
40 38 ERROR_SPEED = _('Please wait %s seconds before sending message')
41 39
42 40 TAG_MAX_LENGTH = 20
43 41
44 FILE_DOWNLOAD_CHUNK_BYTES = 100000
45
46 HTTP_RESULT_OK = 200
47
48 42 TEXTAREA_ROWS = 4
49 43
50 44
51 45 def get_timezones():
52 46 timezones = []
53 47 for tz in pytz.common_timezones:
54 48 timezones.append((tz, tz),)
55 49 return timezones
56 50
57 51
58 52 class FormatPanel(forms.Textarea):
59 53 """
60 54 Panel for text formatting. Consists of buttons to add different tags to the
61 55 form text area.
62 56 """
63 57
64 58 def render(self, name, value, attrs=None):
65 59 output = '<div id="mark-panel">'
66 60 for formatter in formatters:
67 61 output += '<span class="mark_btn"' + \
68 62 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
69 63 '\', \'' + formatter.format_right + '\')">' + \
70 64 formatter.preview_left + formatter.name + \
71 65 formatter.preview_right + '</span>'
72 66
73 67 output += '</div>'
74 68 output += super(FormatPanel, self).render(name, value, attrs=None)
75 69
76 70 return output
77 71
78 72
79 73 class PlainErrorList(ErrorList):
80 74 def __unicode__(self):
81 75 return self.as_text()
82 76
83 77 def as_text(self):
84 78 return ''.join(['(!) %s ' % e for e in self])
85 79
86 80
87 81 class NeboardForm(forms.Form):
88 82 """
89 83 Form with neboard-specific formatting.
90 84 """
91 85
92 86 def as_div(self):
93 87 """
94 88 Returns this form rendered as HTML <as_div>s.
95 89 """
96 90
97 91 return self._html_output(
98 92 # TODO Do not show hidden rows in the list here
99 93 normal_row='<div class="form-row">'
100 94 '<div class="form-label">'
101 95 '%(label)s'
102 96 '</div>'
103 97 '<div class="form-input">'
104 98 '%(field)s'
105 99 '</div>'
106 100 '</div>'
107 101 '<div class="form-row">'
108 102 '%(help_text)s'
109 103 '</div>',
110 104 error_row='<div class="form-row">'
111 105 '<div class="form-label"></div>'
112 106 '<div class="form-errors">%s</div>'
113 107 '</div>',
114 108 row_ender='</div>',
115 109 help_text_html='%s',
116 110 errors_on_separate_row=True)
117 111
118 112 def as_json_errors(self):
119 113 errors = []
120 114
121 115 for name, field in list(self.fields.items()):
122 116 if self[name].errors:
123 117 errors.append({
124 118 'field': name,
125 119 'errors': self[name].errors.as_text(),
126 120 })
127 121
128 122 return errors
129 123
130 124
131 125 class PostForm(NeboardForm):
132 126
133 127 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
134 128 label=LABEL_TITLE,
135 129 widget=forms.TextInput(
136 130 attrs={ATTRIBUTE_PLACEHOLDER:
137 131 'test#tripcode'}))
138 132 text = forms.CharField(
139 133 widget=FormatPanel(attrs={
140 134 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
141 135 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
142 136 }),
143 137 required=False, label=LABEL_TEXT)
144 138 file = forms.FileField(required=False, label=_('File'),
145 139 widget=forms.ClearableFileInput(
146 140 attrs={'accept': 'file/*'}))
147 141 file_url = forms.CharField(required=False, label=_('File URL'),
148 142 widget=forms.TextInput(
149 143 attrs={ATTRIBUTE_PLACEHOLDER:
150 144 'http://example.com/image.png'}))
151 145
152 146 # This field is for spam prevention only
153 147 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
154 148 widget=forms.TextInput(attrs={
155 149 'class': 'form-email'}))
156 150 threads = forms.CharField(required=False, label=_('Additional threads'),
157 151 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER:
158 152 '123 456 789'}))
159 153
160 154 session = None
161 155 need_to_ban = False
162 156
163 157 def clean_title(self):
164 158 title = self.cleaned_data['title']
165 159 if title:
166 160 if len(title) > TITLE_MAX_LENGTH:
167 161 raise forms.ValidationError(_('Title must have less than %s '
168 162 'characters') %
169 163 str(TITLE_MAX_LENGTH))
170 164 return title
171 165
172 166 def clean_text(self):
173 167 text = self.cleaned_data['text'].strip()
174 168 if text:
175 169 max_length = board_settings.get_int('Forms', 'MaxTextLength')
176 170 if len(text) > max_length:
177 171 raise forms.ValidationError(_('Text must have less than %s '
178 172 'characters') % str(max_length))
179 173 return text
180 174
181 175 def clean_file(self):
182 176 file = self.cleaned_data['file']
183 177
184 178 if file:
185 self.validate_file_size(file.size)
179 validate_file_size(file.size)
186 180
187 181 return file
188 182
189 183 def clean_file_url(self):
190 184 url = self.cleaned_data['file_url']
191 185
192 186 file = None
193 187 if url:
194 188 file = self._get_file_from_url(url)
195 189
196 190 if not file:
197 191 raise forms.ValidationError(_('Invalid URL'))
198 192 else:
199 self.validate_file_size(file.size)
193 validate_file_size(file.size)
200 194
201 195 return file
202 196
203 197 def clean_threads(self):
204 198 threads_str = self.cleaned_data['threads']
205 199
206 200 if len(threads_str) > 0:
207 201 threads_id_list = threads_str.split(' ')
208 202
209 203 threads = list()
210 204
211 205 for thread_id in threads_id_list:
212 206 try:
213 207 thread = Post.objects.get(id=int(thread_id))
214 208 if not thread.is_opening() or thread.get_thread().archived:
215 209 raise ObjectDoesNotExist()
216 210 threads.append(thread)
217 211 except (ObjectDoesNotExist, ValueError):
218 212 raise forms.ValidationError(_('Invalid additional thread list'))
219 213
220 214 return threads
221 215
222 216 def clean(self):
223 217 cleaned_data = super(PostForm, self).clean()
224 218
225 219 if cleaned_data['email']:
226 220 self.need_to_ban = True
227 221 raise forms.ValidationError('A human cannot enter a hidden field')
228 222
229 223 if not self.errors:
230 224 self._clean_text_file()
231 225
232 226 if not self.errors and self.session:
233 227 self._validate_posting_speed()
234 228
235 229 return cleaned_data
236 230
237 231 def get_file(self):
238 232 """
239 233 Gets file from form or URL.
240 234 """
241 235
242 236 file = self.cleaned_data['file']
243 237 return file or self.cleaned_data['file_url']
244 238
245 239 def get_tripcode(self):
246 240 title = self.cleaned_data['title']
247 241 if title is not None and '#' in title:
248 242 code = title.split('#', maxsplit=1)[1] + neboard.settings.SECRET_KEY
249 243 return hashlib.md5(code.encode()).hexdigest()
250 244
251 245 def get_title(self):
252 246 title = self.cleaned_data['title']
253 247 if title is not None and '#' in title:
254 248 return title.split('#', maxsplit=1)[0]
255 249 else:
256 250 return title
257 251
258 252 def _clean_text_file(self):
259 253 text = self.cleaned_data.get('text')
260 254 file = self.get_file()
261 255
262 256 if (not text) and (not file):
263 257 error_message = _('Either text or file must be entered.')
264 258 self._errors['text'] = self.error_class([error_message])
265 259
266 260 def _validate_posting_speed(self):
267 261 can_post = True
268 262
269 263 posting_delay = settings.POSTING_DELAY
270 264
271 265 if board_settings.get_bool('Forms', 'LimitPostingSpeed'):
272 266 now = time.time()
273 267
274 268 current_delay = 0
275 269 need_delay = False
276 270
277 271 if not LAST_POST_TIME in self.session:
278 272 self.session[LAST_POST_TIME] = now
279 273
280 274 need_delay = True
281 275 else:
282 276 last_post_time = self.session.get(LAST_POST_TIME)
283 277 current_delay = int(now - last_post_time)
284 278
285 279 need_delay = current_delay < posting_delay
286 280
287 281 if need_delay:
288 282 error_message = ERROR_SPEED % str(posting_delay
289 283 - current_delay)
290 284 self._errors['text'] = self.error_class([error_message])
291 285
292 286 can_post = False
293 287
294 288 if can_post:
295 289 self.session[LAST_POST_TIME] = now
296 290
297 def validate_file_size(self, size: int):
298 max_size = board_settings.get_int('Forms', 'MaxFileSize')
299 if size > max_size:
300 raise forms.ValidationError(
301 _('File must be less than %s bytes')
302 % str(max_size))
303
304 291 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
305 292 """
306 293 Gets an file file from URL.
307 294 """
308 295
309 296 img_temp = None
310 297
311 298 try:
312 # Verify content headers
313 response_head = requests.head(url, verify=False)
314 content_type = response_head.headers[HEADER_CONTENT_TYPE].split(';')[0]
315 length_header = response_head.headers.get(HEADER_CONTENT_LENGTH)
316 if length_header:
317 length = int(length_header)
318 self.validate_file_size(length)
319 # Get the actual content into memory
320 response = requests.get(url, verify=False, stream=True)
321
322 # Download file, stop if the size exceeds limit
323 size = 0
324 content = b''
325 for chunk in response.iter_content(FILE_DOWNLOAD_CHUNK_BYTES):
326 size += len(chunk)
327 self.validate_file_size(size)
328 content += chunk
329
330 if response.status_code == HTTP_RESULT_OK and content:
331 # Set a dummy file name that will be replaced
332 # anyway, just keep the valid extension
333 filename = 'file.' + content_type.split('/')[1]
334 img_temp = SimpleUploadedFile(filename, content,
335 content_type)
299 for downloader in Downloader.__subclasses__():
300 if downloader.handles(url):
301 return downloader.download(url)
302 # If nobody of the specific downloaders handles this, use generic
303 # one
304 return Downloader.download(url)
305 except forms.ValidationError as e:
306 raise e
336 307 except Exception as e:
337 308 # Just return no file
338 309 pass
339 310
340 return img_temp
341
342 311
343 312 class ThreadForm(PostForm):
344 313
345 314 tags = forms.CharField(
346 315 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
347 316 max_length=100, label=_('Tags'), required=True)
348 317
349 318 def clean_tags(self):
350 319 tags = self.cleaned_data['tags'].strip()
351 320
352 321 if not tags or not REGEX_TAGS.match(tags):
353 322 raise forms.ValidationError(
354 323 _('Inappropriate characters in tags.'))
355 324
356 325 required_tag_exists = False
357 326 for tag in tags.split():
358 327 try:
359 328 Tag.objects.get(name=tag.strip().lower(), required=True)
360 329 required_tag_exists = True
361 330 break
362 331 except ObjectDoesNotExist:
363 332 pass
364 333
365 334 if not required_tag_exists:
366 335 all_tags = Tag.objects.filter(required=True)
367 336 raise forms.ValidationError(
368 337 _('Need at least one section.'))
369 338
370 339 return tags
371 340
372 341 def clean(self):
373 342 cleaned_data = super(ThreadForm, self).clean()
374 343
375 344 return cleaned_data
376 345
377 346
378 347 class SettingsForm(NeboardForm):
379 348
380 349 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
381 350 image_viewer = forms.ChoiceField(choices=settings.IMAGE_VIEWERS, label=_('Image view mode'))
382 351 username = forms.CharField(label=_('User name'), required=False)
383 352 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
384 353
385 354 def clean_username(self):
386 355 username = self.cleaned_data['username']
387 356
388 357 if username and not REGEX_TAGS.match(username):
389 358 raise forms.ValidationError(_('Inappropriate characters.'))
390 359
391 360 return username
392 361
393 362
394 363 class SearchForm(NeboardForm):
395 364 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
@@ -1,92 +1,103 b''
1 1 """
2 2 This module contains helper functions and helper classes.
3 3 """
4 4 import hashlib
5 5 import time
6 6 import hmac
7 7
8 8 from django.core.cache import cache
9 9 from django.db.models import Model
10 from django import forms
10 11
11 12 from django.utils import timezone
13 from django.utils.translation import ugettext_lazy as _
14 import boards
12 15
13 16 from neboard import settings
14 17
15 18
16 19 CACHE_KEY_DELIMITER = '_'
17 20 PERMISSION_MODERATE = 'moderation'
18 21
19 22 def get_client_ip(request):
20 23 x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
21 24 if x_forwarded_for:
22 25 ip = x_forwarded_for.split(',')[-1].strip()
23 26 else:
24 27 ip = request.META.get('REMOTE_ADDR')
25 28 return ip
26 29
27 30
28 31 # TODO The output format is not epoch because it includes microseconds
29 32 def datetime_to_epoch(datetime):
30 33 return int(time.mktime(timezone.localtime(
31 34 datetime,timezone.get_current_timezone()).timetuple())
32 35 * 1000000 + datetime.microsecond)
33 36
34 37
35 38 def get_websocket_token(user_id='', timestamp=''):
36 39 """
37 40 Create token to validate information provided by new connection.
38 41 """
39 42
40 43 sign = hmac.new(settings.CENTRIFUGE_PROJECT_SECRET.encode())
41 44 sign.update(settings.CENTRIFUGE_PROJECT_ID.encode())
42 45 sign.update(user_id.encode())
43 46 sign.update(timestamp.encode())
44 47 token = sign.hexdigest()
45 48
46 49 return token
47 50
48 51
49 52 def cached_result(key_method=None):
50 53 """
51 54 Caches method result in the Django's cache system, persisted by object name,
52 55 object name and model id if object is a Django model.
53 56 """
54 57 def _cached_result(function):
55 58 def inner_func(obj, *args, **kwargs):
56 59 # TODO Include method arguments to the cache key
57 60 cache_key_params = [obj.__class__.__name__, function.__name__]
58 61 if isinstance(obj, Model):
59 62 cache_key_params.append(str(obj.id))
60 63
61 64 if key_method is not None:
62 65 cache_key_params += [str(arg) for arg in key_method(obj)]
63 66
64 67 cache_key = CACHE_KEY_DELIMITER.join(cache_key_params)
65 68
66 69 persisted_result = cache.get(cache_key)
67 70 if persisted_result is not None:
68 71 result = persisted_result
69 72 else:
70 73 result = function(obj, *args, **kwargs)
71 74 cache.set(cache_key, result)
72 75
73 76 return result
74 77
75 78 return inner_func
76 79 return _cached_result
77 80
78 81
79 82 def is_moderator(request):
80 83 try:
81 84 moderate = request.user.has_perm(PERMISSION_MODERATE)
82 85 except AttributeError:
83 86 moderate = False
84 87
85 88 return moderate
86 89
87 90
88 91 def get_file_hash(file) -> str:
89 92 md5 = hashlib.md5()
90 93 for chunk in file.chunks():
91 94 md5.update(chunk)
92 95 return md5.hexdigest()
96
97
98 def validate_file_size(size: int):
99 max_size = boards.settings.get_int('Forms', 'MaxFileSize')
100 if size > max_size:
101 raise forms.ValidationError(
102 _('File must be less than %s bytes')
103 % str(max_size))
@@ -1,8 +1,9 b''
1 pytube
1 2 requests
2 3 adjacent
3 4 django-haystack
4 5 pillow
5 6 django>=1.8
6 7 bbcode
7 8 django-debug-toolbar
8 9 pytz
General Comments 0
You need to be logged in to leave comments. Login now