Merge branch 'master' into merge-python3-port
[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
21from mediagoblin import mg_globals as mgg
5ac1fe80 22from mediagoblin.processing import (
440e33aa 23 BadMediaFail, FilenameBuilder,
5ac1fe80 24 ProgressCallback, MediaProcessor, ProcessingManager,
1cefccc7 25 request_from_args, get_process_filename,
5ac1fe80 26 store_public, copy_original)
5a34a80d 27
5ac1fe80 28from mediagoblin.media_types.audio.transcoders import (
440e33aa 29 AudioTranscoder, AudioThumbnailer)
5a34a80d 30
10085b77 31_log = logging.getLogger(__name__)
5a34a80d 32
df68438a
RE
33MEDIA_TYPE = 'mediagoblin.media_types.audio'
34
64712915 35
301da9ca 36def sniff_handler(media_file, filename):
df68438a 37 _log.info('Sniffing {0}'.format(MEDIA_TYPE))
196a5181 38 try:
4f4f2531 39 transcoder = AudioTranscoder()
ec4261a4 40 data = transcoder.discover(media_file.name)
4f4f2531
JW
41 except BadMediaFail:
42 _log.debug('Audio discovery raised BadMediaFail')
df68438a 43 return None
ec4261a4 44
440e33aa 45 if data.is_audio is True and data.is_video is False:
df68438a 46 return MEDIA_TYPE
10085b77 47
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
129 self.transcoder.discover(webm_audio_tmp)
130
0a8c0c70
RE
131 self._keep_best()
132
5ac1fe80 133 _log.debug('Saving medium...')
9448a98e
RE
134 store_public(self.entry, 'webm_audio', webm_audio_tmp,
135 self.name_builder.fill('{basename}.medium.webm'))
5ac1fe80 136
2e22cefd
RE
137 self.entry.set_file_metadata('webm_audio', **{'quality': quality})
138
ad80fc8a 139 def create_spectrogram(self, max_width=None, fft_size=None):
5ac1fe80
RE
140 if not max_width:
141 max_width = mgg.global_config['media:medium']['max_width']
142 if not fft_size:
143 fft_size = self.audio_config['spectrogram_fft_size']
144
2e22cefd
RE
145 if self._skip_processing('spectrogram', max_width=max_width,
146 fft_size=fft_size):
147 return
148
5ac1fe80
RE
149 wav_tmp = os.path.join(self.workbench.dir, self.name_builder.fill(
150 '{basename}.ogg'))
151
152 _log.info('Creating OGG source for spectrogram')
153 self.transcoder.transcode(
1cefccc7 154 self.process_filename,
5ac1fe80 155 wav_tmp,
ad80fc8a
RE
156 mux_string='vorbisenc quality={0} ! oggmux'.format(
157 self.audio_config['quality']))
5ac1fe80 158
d8f886dc 159 spectrogram_tmp = os.path.join(self.workbench.dir,
347ef583
RE
160 self.name_builder.fill(
161 '{basename}-spectrogram.jpg'))
d8f886dc 162
5ac1fe80
RE
163 self.thumbnailer.spectrogram(
164 wav_tmp,
d8f886dc 165 spectrogram_tmp,
5ac1fe80
RE
166 width=max_width,
167 fft_size=fft_size)
168
169 _log.debug('Saving spectrogram...')
d8f886dc 170 store_public(self.entry, 'spectrogram', spectrogram_tmp,
c6eaa555 171 self.name_builder.fill('{basename}.spectrogram.jpg'))
5ac1fe80 172
2e22cefd
RE
173 file_metadata = {'max_width': max_width,
174 'fft_size': fft_size}
175 self.entry.set_file_metadata('spectrogram', **file_metadata)
176
5ac1fe80
RE
177 def generate_thumb(self, size=None):
178 if not size:
776e4d7a
RE
179 max_width = mgg.global_config['media:thumb']['max_width']
180 max_height = mgg.global_config['media:thumb']['max_height']
5ac1fe80
RE
181 size = (max_width, max_height)
182
2e22cefd
RE
183 if self._skip_processing('thumb', size=size):
184 return
185
5ac1fe80
RE
186 thumb_tmp = os.path.join(self.workbench.dir, self.name_builder.fill(
187 '{basename}-thumbnail.jpg'))
188
d8f886dc
RE
189 # We need the spectrogram to create a thumbnail
190 spectrogram = self.entry.media_files.get('spectrogram')
191 if not spectrogram:
192 _log.info('No spectrogram found, we will create one.')
193 self.create_spectrogram()
194 spectrogram = self.entry.media_files['spectrogram']
195
196 spectrogram_filepath = mgg.public_store.get_local_path(spectrogram)
197
5ac1fe80 198 self.thumbnailer.thumbnail_spectrogram(
d8f886dc 199 spectrogram_filepath,
5ac1fe80 200 thumb_tmp,
b95cc59b 201 tuple(size))
5ac1fe80 202
c6eaa555
RE
203 store_public(self.entry, 'thumb', thumb_tmp,
204 self.name_builder.fill('{basename}.thumbnail.jpg'))
5ac1fe80 205
2e22cefd
RE
206 self.entry.set_file_metadata('thumb', **{'size': size})
207
5ac1fe80
RE
208
209class InitialProcessor(CommonAudioProcessor):
210 """
211 Initial processing steps for new audio
212 """
213 name = "initial"
214 description = "Initial processing"
215
216 @classmethod
217 def media_is_eligible(cls, entry=None, state=None):
218 """
219 Determine if this media type is eligible for processing
220 """
221 if not state:
222 state = entry.state
223 return state in (
224 "unprocessed", "failed")
225
226 @classmethod
227 def generate_parser(cls):
228 parser = argparse.ArgumentParser(
229 description=cls.description,
230 prog=cls.name)
231
232 parser.add_argument(
233 '--quality',
52e97704 234 type=float,
d8f886dc 235 help='vorbisenc quality. Range: -0.1..1')
5ac1fe80
RE
236
237 parser.add_argument(
238 '--fft_size',
239 type=int,
240 help='spectrogram fft size')
241
242 parser.add_argument(
243 '--thumb_size',
757376e3 244 nargs=2,
5ac1fe80 245 metavar=('max_width', 'max_height'),
7674b9c0
RE
246 type=int,
247 help='minimum size is 100 x 100')
5ac1fe80
RE
248
249 parser.add_argument(
250 '--medium_width',
251 type=int,
252 help='The width of the spectogram')
253
5ac1fe80
RE
254 return parser
255
256 @classmethod
257 def args_to_request(cls, args):
258 return request_from_args(
7e0c51af 259 args, ['quality', 'fft_size',
5ac1fe80
RE
260 'thumb_size', 'medium_width'])
261
262 def process(self, quality=None, fft_size=None, thumb_size=None,
7e0c51af 263 medium_width=None):
550af89f
RE
264 self.common_setup()
265
5ac1fe80
RE
266 self.transcode(quality=quality)
267 self.copy_original()
268
7e0c51af
CAW
269 self.create_spectrogram(max_width=medium_width, fft_size=fft_size)
270 self.generate_thumb(size=thumb_size)
271
5ac1fe80
RE
272 self.delete_queue_file()
273
274
2e50e4b5
RE
275class Resizer(CommonAudioProcessor):
276 """
277 Thumbnail and spectogram resizing process steps for processed audio
278 """
279 name = 'resize'
ad80fc8a 280 description = 'Resize thumbnail or spectogram'
3225008f 281 thumb_size = 'thumb_size'
2e50e4b5
RE
282
283 @classmethod
284 def media_is_eligible(cls, entry=None, state=None):
285 """
286 Determine if this media entry is eligible for processing
287 """
288 if not state:
289 state = entry.state
290 return state in 'processed'
291
292 @classmethod
293 def generate_parser(cls):
294 parser = argparse.ArgumentParser(
295 description=cls.description,
296 prog=cls.name)
297
2e50e4b5
RE
298 parser.add_argument(
299 '--fft_size',
300 type=int,
301 help='spectrogram fft size')
302
303 parser.add_argument(
304 '--thumb_size',
305 nargs=2,
306 metavar=('max_width', 'max_height'),
7674b9c0
RE
307 type=int,
308 help='minimum size is 100 x 100')
2e50e4b5
RE
309
310 parser.add_argument(
311 '--medium_width',
312 type=int,
313 help='The width of the spectogram')
314
315 parser.add_argument(
316 'file',
317 choices=['thumb', 'spectrogram'])
318
319 return parser
320
321 @classmethod
322 def args_to_request(cls, args):
323 return request_from_args(
ad80fc8a 324 args, ['thumb_size', 'file', 'fft_size', 'medium_width'])
2e50e4b5 325
0c509b1b 326 def process(self, file, thumb_size=None, fft_size=None,
2e50e4b5
RE
327 medium_width=None):
328 self.common_setup()
ad80fc8a 329
2e50e4b5
RE
330 if file == 'thumb':
331 self.generate_thumb(size=thumb_size)
332 elif file == 'spectrogram':
ad80fc8a 333 self.create_spectrogram(max_width=medium_width, fft_size=fft_size)
2e50e4b5
RE
334
335
0c509b1b
RE
336class Transcoder(CommonAudioProcessor):
337 """
338 Transcoding processing steps for processed audio
339 """
340 name = 'transcode'
341 description = 'Re-transcode audio'
342
343 @classmethod
344 def media_is_eligible(cls, entry=None, state=None):
345 if not state:
346 state = entry.state
347 return state in 'processed'
348
349 @classmethod
350 def generate_parser(cls):
351 parser = argparse.ArgumentParser(
352 description=cls.description,
353 prog=cls.name)
354
355 parser.add_argument(
356 '--quality',
357 help='vorbisenc quality. Range: -0.1..1')
358
359 return parser
360
361 @classmethod
362 def args_to_request(cls, args):
363 return request_from_args(
364 args, ['quality'])
365
366 def process(self, quality=None):
367 self.common_setup()
368 self.transcode(quality=quality)
369
370
5ac1fe80
RE
371class AudioProcessingManager(ProcessingManager):
372 def __init__(self):
373 super(self.__class__, self).__init__()
374 self.add_processor(InitialProcessor)
2e50e4b5 375 self.add_processor(Resizer)
0c509b1b 376 self.add_processor(Transcoder)