Use audio icon when spectrogram fails. Add note to doc:media-types.
[mediagoblin.git] / mediagoblin / media_types / audio / processing.py
CommitLineData
5a34a80d
JW
1# GNU MediaGoblin -- federated, autonomous media hosting
2# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
3#
4# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU Affero General Public License as published by
6# the Free Software Foundation, either version 3 of the License, or
7# (at your option) any later version.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Affero General Public License for more details.
13#
14# You should have received a copy of the GNU Affero General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
5ac1fe80 17import argparse
5a34a80d 18import logging
5a34a80d
JW
19import os
20
896d00fb
BP
21import six
22
5a34a80d 23from mediagoblin import mg_globals as mgg
5ac1fe80 24from mediagoblin.processing import (
440e33aa 25 BadMediaFail, FilenameBuilder,
5ac1fe80 26 ProgressCallback, MediaProcessor, ProcessingManager,
1cefccc7 27 request_from_args, get_process_filename,
5ac1fe80 28 store_public, copy_original)
5a34a80d 29
5ac1fe80 30from mediagoblin.media_types.audio.transcoders import (
440e33aa 31 AudioTranscoder, AudioThumbnailer)
57d8212a 32from mediagoblin.media_types.tools import discover
5a34a80d 33
10085b77 34_log = logging.getLogger(__name__)
5a34a80d 35
df68438a
RE
36MEDIA_TYPE = 'mediagoblin.media_types.audio'
37
64712915 38
301da9ca 39def sniff_handler(media_file, filename):
df68438a 40 _log.info('Sniffing {0}'.format(MEDIA_TYPE))
2d1e8905
BB
41 try:
42 data = discover(media_file.name)
43 except Exception as e:
896d00fb 44 _log.info(six.text_type(e))
2d1e8905 45 return None
57d8212a 46 if data and data.get_audio_streams() and not data.get_video_streams():
df68438a 47 return MEDIA_TYPE
df68438a 48 return None
5a34a80d 49
64712915 50
5ac1fe80
RE
51class CommonAudioProcessor(MediaProcessor):
52 """
53 Provides a base for various audio processing steps
54 """
0a8c0c70 55 acceptable_files = ['original', 'best_quality', 'webm_audio']
5ac1fe80
RE
56
57 def common_setup(self):
58 """
440e33aa
RE
59 Setup the workbench directory and pull down the original file, add
60 the audio_config, transcoder, thumbnailer and spectrogram_tmp path
5ac1fe80
RE
61 """
62 self.audio_config = mgg \
9ff68c22 63 .global_config['plugins']['mediagoblin.media_types.audio']
5ac1fe80 64
1cefccc7
RE
65 # Pull down and set up the processing file
66 self.process_filename = get_process_filename(
67 self.entry, self.workbench, self.acceptable_files)
68 self.name_builder = FilenameBuilder(self.process_filename)
5ac1fe80 69
5ac1fe80
RE
70 self.transcoder = AudioTranscoder()
71 self.thumbnailer = AudioThumbnailer()
72
73 def copy_original(self):
74 if self.audio_config['keep_original']:
75 copy_original(
1cefccc7 76 self.entry, self.process_filename,
5ac1fe80
RE
77 self.name_builder.fill('{basename}{ext}'))
78
0a8c0c70
RE
79 def _keep_best(self):
80 """
81 If there is no original, keep the best file that we have
82 """
83 if not self.entry.media_files.get('best_quality'):
84 # Save the best quality file if no original?
85 if not self.entry.media_files.get('original') and \
86 self.entry.media_files.get('webm_audio'):
87 self.entry.media_files['best_quality'] = self.entry \
88 .media_files['webm_audio']
89
2e22cefd
RE
90 def _skip_processing(self, keyname, **kwargs):
91 file_metadata = self.entry.get_file_metadata(keyname)
92 skip = True
93
94 if not file_metadata:
95 return False
96
97 if keyname == 'webm_audio':
98 if kwargs.get('quality') != file_metadata.get('quality'):
99 skip = False
100 elif keyname == 'spectrogram':
101 if kwargs.get('max_width') != file_metadata.get('max_width'):
102 skip = False
103 elif kwargs.get('fft_size') != file_metadata.get('fft_size'):
104 skip = False
105 elif keyname == 'thumb':
106 if kwargs.get('size') != file_metadata.get('size'):
107 skip = False
108
109 return skip
110
5ac1fe80
RE
111 def transcode(self, quality=None):
112 if not quality:
113 quality = self.audio_config['quality']
114
2e22cefd
RE
115 if self._skip_processing('webm_audio', quality=quality):
116 return
117
5ac1fe80
RE
118 progress_callback = ProgressCallback(self.entry)
119 webm_audio_tmp = os.path.join(self.workbench.dir,
120 self.name_builder.fill(
121 '{basename}{ext}'))
122
5ac1fe80 123 self.transcoder.transcode(
1cefccc7 124 self.process_filename,
5ac1fe80
RE
125 webm_audio_tmp,
126 quality=quality,
127 progress_callback=progress_callback)
128
0a8c0c70
RE
129 self._keep_best()
130
5ac1fe80 131 _log.debug('Saving medium...')
9448a98e
RE
132 store_public(self.entry, 'webm_audio', webm_audio_tmp,
133 self.name_builder.fill('{basename}.medium.webm'))
5ac1fe80 134
2e22cefd
RE
135 self.entry.set_file_metadata('webm_audio', **{'quality': quality})
136
ad80fc8a 137 def create_spectrogram(self, max_width=None, fft_size=None):
5ac1fe80
RE
138 if not max_width:
139 max_width = mgg.global_config['media:medium']['max_width']
140 if not fft_size:
141 fft_size = self.audio_config['spectrogram_fft_size']
142
2e22cefd
RE
143 if self._skip_processing('spectrogram', max_width=max_width,
144 fft_size=fft_size):
145 return
5ac1fe80
RE
146 wav_tmp = os.path.join(self.workbench.dir, self.name_builder.fill(
147 '{basename}.ogg'))
5ac1fe80 148 _log.info('Creating OGG source for spectrogram')
57d8212a
BB
149 self.transcoder.transcode(self.process_filename, wav_tmp,
150 mux_name='oggmux')
2b487fc1 151
d8f886dc 152 spectrogram_tmp = os.path.join(self.workbench.dir,
347ef583
RE
153 self.name_builder.fill(
154 '{basename}-spectrogram.jpg'))
2b487fc1
J
155
156 try:
157 self.thumbnailer.spectrogram(
158 wav_tmp,
159 spectrogram_tmp,
160 width=max_width,
161 fft_size=fft_size)
162
163 _log.debug('Saving spectrogram...')
164 store_public(self.entry, 'spectrogram', thumbnail,
c6eaa555 165 self.name_builder.fill('{basename}.spectrogram.jpg'))
5ac1fe80 166
2b487fc1 167 file_metadata = {'max_width': max_width,
2e22cefd 168 'fft_size': fft_size}
2b487fc1
J
169 self.entry.set_file_metadata('spectrogram', **file_metadata)
170
171 except IndexError:
172 _log.warn(
173 'Your version of Numpy is too new to create the waveform thumbnail (#5457). '
174 "Try\n\t./bin/pip install numpy==1.9.1\n\t./bin/pip install scikits.audiolab==0.10.2")
175
176 except Exception as exc:
177 _log.warn('Failed to create spectrogram: '
178 + '{0}'.exc)
2e22cefd 179
5ac1fe80
RE
180 def generate_thumb(self, size=None):
181 if not size:
776e4d7a
RE
182 max_width = mgg.global_config['media:thumb']['max_width']
183 max_height = mgg.global_config['media:thumb']['max_height']
5ac1fe80
RE
184 size = (max_width, max_height)
185
2e22cefd
RE
186 if self._skip_processing('thumb', size=size):
187 return
188
5ac1fe80
RE
189 thumb_tmp = os.path.join(self.workbench.dir, self.name_builder.fill(
190 '{basename}-thumbnail.jpg'))
191
d8f886dc 192 # We need the spectrogram to create a thumbnail
2b487fc1
J
193 try:
194 spectrogram = self.entry.media_files.get('spectrogram')
195 if not spectrogram:
196 _log.info('No spectrogram found, we will create one.')
197 self.create_spectrogram()
198 spectrogram = self.entry.media_files['spectrogram']
199
200 spectrogram_filepath = mgg.public_store.get_local_path(spectrogram)
201
202 except:
203 _log.warn('Failed to create spectrogram, using default audio image instead.')
204 spectrogram_filepath = 'mediagoblin/static/images/media_thumbs/audio.png'
d8f886dc 205
5ac1fe80 206 self.thumbnailer.thumbnail_spectrogram(
d8f886dc 207 spectrogram_filepath,
5ac1fe80 208 thumb_tmp,
b95cc59b 209 tuple(size))
5ac1fe80 210
c6eaa555
RE
211 store_public(self.entry, 'thumb', thumb_tmp,
212 self.name_builder.fill('{basename}.thumbnail.jpg'))
5ac1fe80 213
2e22cefd
RE
214 self.entry.set_file_metadata('thumb', **{'size': size})
215
5ac1fe80
RE
216
217class InitialProcessor(CommonAudioProcessor):
218 """
219 Initial processing steps for new audio
220 """
221 name = "initial"
222 description = "Initial processing"
223
224 @classmethod
225 def media_is_eligible(cls, entry=None, state=None):
226 """
227 Determine if this media type is eligible for processing
228 """
229 if not state:
230 state = entry.state
231 return state in (
232 "unprocessed", "failed")
233
234 @classmethod
235 def generate_parser(cls):
236 parser = argparse.ArgumentParser(
237 description=cls.description,
238 prog=cls.name)
239
240 parser.add_argument(
241 '--quality',
52e97704 242 type=float,
d8f886dc 243 help='vorbisenc quality. Range: -0.1..1')
5ac1fe80
RE
244
245 parser.add_argument(
246 '--fft_size',
247 type=int,
248 help='spectrogram fft size')
249
250 parser.add_argument(
251 '--thumb_size',
757376e3 252 nargs=2,
5ac1fe80 253 metavar=('max_width', 'max_height'),
7674b9c0
RE
254 type=int,
255 help='minimum size is 100 x 100')
5ac1fe80
RE
256
257 parser.add_argument(
258 '--medium_width',
259 type=int,
260 help='The width of the spectogram')
261
5ac1fe80
RE
262 return parser
263
264 @classmethod
265 def args_to_request(cls, args):
266 return request_from_args(
7e0c51af 267 args, ['quality', 'fft_size',
5ac1fe80
RE
268 'thumb_size', 'medium_width'])
269
270 def process(self, quality=None, fft_size=None, thumb_size=None,
7e0c51af 271 medium_width=None):
550af89f
RE
272 self.common_setup()
273
5ac1fe80
RE
274 self.transcode(quality=quality)
275 self.copy_original()
276
7e0c51af
CAW
277 self.create_spectrogram(max_width=medium_width, fft_size=fft_size)
278 self.generate_thumb(size=thumb_size)
279
5ac1fe80
RE
280 self.delete_queue_file()
281
282
2e50e4b5
RE
283class Resizer(CommonAudioProcessor):
284 """
285 Thumbnail and spectogram resizing process steps for processed audio
286 """
287 name = 'resize'
ad80fc8a 288 description = 'Resize thumbnail or spectogram'
3225008f 289 thumb_size = 'thumb_size'
2e50e4b5
RE
290
291 @classmethod
292 def media_is_eligible(cls, entry=None, state=None):
293 """
294 Determine if this media entry is eligible for processing
295 """
296 if not state:
297 state = entry.state
298 return state in 'processed'
299
300 @classmethod
301 def generate_parser(cls):
302 parser = argparse.ArgumentParser(
303 description=cls.description,
304 prog=cls.name)
305
2e50e4b5
RE
306 parser.add_argument(
307 '--fft_size',
308 type=int,
309 help='spectrogram fft size')
310
311 parser.add_argument(
312 '--thumb_size',
313 nargs=2,
314 metavar=('max_width', 'max_height'),
7674b9c0
RE
315 type=int,
316 help='minimum size is 100 x 100')
2e50e4b5
RE
317
318 parser.add_argument(
319 '--medium_width',
320 type=int,
321 help='The width of the spectogram')
322
323 parser.add_argument(
324 'file',
325 choices=['thumb', 'spectrogram'])
326
327 return parser
328
329 @classmethod
330 def args_to_request(cls, args):
331 return request_from_args(
ad80fc8a 332 args, ['thumb_size', 'file', 'fft_size', 'medium_width'])
2e50e4b5 333
0c509b1b 334 def process(self, file, thumb_size=None, fft_size=None,
2e50e4b5
RE
335 medium_width=None):
336 self.common_setup()
ad80fc8a 337
2e50e4b5
RE
338 if file == 'thumb':
339 self.generate_thumb(size=thumb_size)
340 elif file == 'spectrogram':
ad80fc8a 341 self.create_spectrogram(max_width=medium_width, fft_size=fft_size)
2e50e4b5
RE
342
343
0c509b1b
RE
344class Transcoder(CommonAudioProcessor):
345 """
346 Transcoding processing steps for processed audio
347 """
348 name = 'transcode'
349 description = 'Re-transcode audio'
350
351 @classmethod
352 def media_is_eligible(cls, entry=None, state=None):
353 if not state:
354 state = entry.state
355 return state in 'processed'
356
357 @classmethod
358 def generate_parser(cls):
359 parser = argparse.ArgumentParser(
360 description=cls.description,
361 prog=cls.name)
362
363 parser.add_argument(
364 '--quality',
365 help='vorbisenc quality. Range: -0.1..1')
366
367 return parser
368
369 @classmethod
370 def args_to_request(cls, args):
371 return request_from_args(
372 args, ['quality'])
373
374 def process(self, quality=None):
375 self.common_setup()
376 self.transcode(quality=quality)
377
378
5ac1fe80
RE
379class AudioProcessingManager(ProcessingManager):
380 def __init__(self):
1a2982d6 381 super(AudioProcessingManager, self).__init__()
5ac1fe80 382 self.add_processor(InitialProcessor)
2e50e4b5 383 self.add_processor(Resizer)
0c509b1b 384 self.add_processor(Transcoder)