b74364bca36e8d7a5d44a0577a4049aac67506fe
[mediagoblin.git] / mediagoblin / media_types / audio / processing.py
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
17 import argparse
18 import logging
19 import os
20
21 import six
22
23 from mediagoblin import mg_globals as mgg
24 from mediagoblin.processing import (
25 BadMediaFail, FilenameBuilder,
26 ProgressCallback, MediaProcessor, ProcessingManager,
27 request_from_args, get_process_filename,
28 store_public, copy_original)
29
30 from mediagoblin.media_types.audio.transcoders import (
31 AudioTranscoder, AudioThumbnailer)
32 from mediagoblin.media_types.tools import discover
33
34 _log = logging.getLogger(__name__)
35
36 MEDIA_TYPE = 'mediagoblin.media_types.audio'
37
38
39 def sniff_handler(media_file, filename):
40 _log.info('Sniffing {0}'.format(MEDIA_TYPE))
41 try:
42 data = discover(media_file.name)
43 except Exception as e:
44 _log.info(six.text_type(e))
45 return None
46 if data and data.get_audio_streams() and not data.get_video_streams():
47 return MEDIA_TYPE
48 return None
49
50
51 class CommonAudioProcessor(MediaProcessor):
52 """
53 Provides a base for various audio processing steps
54 """
55 acceptable_files = ['original', 'best_quality', 'webm_audio']
56
57 def common_setup(self):
58 """
59 Setup the workbench directory and pull down the original file, add
60 the audio_config, transcoder, thumbnailer and spectrogram_tmp path
61 """
62 self.audio_config = mgg \
63 .global_config['plugins']['mediagoblin.media_types.audio']
64
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)
69
70 self.transcoder = AudioTranscoder()
71 self.thumbnailer = AudioThumbnailer()
72
73 def copy_original(self):
74 if self.audio_config['keep_original']:
75 copy_original(
76 self.entry, self.process_filename,
77 self.name_builder.fill('{basename}{ext}'))
78
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
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
111 def transcode(self, quality=None):
112 if not quality:
113 quality = self.audio_config['quality']
114
115 if self._skip_processing('webm_audio', quality=quality):
116 return
117
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
123 self.transcoder.transcode(
124 self.process_filename,
125 webm_audio_tmp,
126 quality=quality,
127 progress_callback=progress_callback)
128
129 self._keep_best()
130
131 _log.debug('Saving medium...')
132 store_public(self.entry, 'webm_audio', webm_audio_tmp,
133 self.name_builder.fill('{basename}.medium.webm'))
134
135 self.entry.set_file_metadata('webm_audio', **{'quality': quality})
136
137 def create_spectrogram(self, max_width=None, fft_size=None):
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
143 if self._skip_processing('spectrogram', max_width=max_width,
144 fft_size=fft_size):
145 return
146 wav_tmp = os.path.join(self.workbench.dir, self.name_builder.fill(
147 '{basename}.ogg'))
148 _log.info('Creating OGG source for spectrogram')
149 self.transcoder.transcode(self.process_filename, wav_tmp,
150 mux_name='oggmux')
151 spectrogram_tmp = os.path.join(self.workbench.dir,
152 self.name_builder.fill(
153 '{basename}-spectrogram.jpg'))
154 self.thumbnailer.spectrogram(
155 wav_tmp,
156 spectrogram_tmp,
157 width=max_width,
158 fft_size=fft_size)
159
160 _log.debug('Saving spectrogram...')
161 store_public(self.entry, 'spectrogram', spectrogram_tmp,
162 self.name_builder.fill('{basename}.spectrogram.jpg'))
163
164 file_metadata = {'max_width': max_width,
165 'fft_size': fft_size}
166 self.entry.set_file_metadata('spectrogram', **file_metadata)
167
168 def generate_thumb(self, size=None):
169 if not size:
170 max_width = mgg.global_config['media:thumb']['max_width']
171 max_height = mgg.global_config['media:thumb']['max_height']
172 size = (max_width, max_height)
173
174 if self._skip_processing('thumb', size=size):
175 return
176
177 thumb_tmp = os.path.join(self.workbench.dir, self.name_builder.fill(
178 '{basename}-thumbnail.jpg'))
179
180 # We need the spectrogram to create a thumbnail
181 spectrogram = self.entry.media_files.get('spectrogram')
182 if not spectrogram:
183 _log.info('No spectrogram found, we will create one.')
184 self.create_spectrogram()
185 spectrogram = self.entry.media_files['spectrogram']
186
187 spectrogram_filepath = mgg.public_store.get_local_path(spectrogram)
188
189 self.thumbnailer.thumbnail_spectrogram(
190 spectrogram_filepath,
191 thumb_tmp,
192 tuple(size))
193
194 store_public(self.entry, 'thumb', thumb_tmp,
195 self.name_builder.fill('{basename}.thumbnail.jpg'))
196
197 self.entry.set_file_metadata('thumb', **{'size': size})
198
199
200 class InitialProcessor(CommonAudioProcessor):
201 """
202 Initial processing steps for new audio
203 """
204 name = "initial"
205 description = "Initial processing"
206
207 @classmethod
208 def media_is_eligible(cls, entry=None, state=None):
209 """
210 Determine if this media type is eligible for processing
211 """
212 if not state:
213 state = entry.state
214 return state in (
215 "unprocessed", "failed")
216
217 @classmethod
218 def generate_parser(cls):
219 parser = argparse.ArgumentParser(
220 description=cls.description,
221 prog=cls.name)
222
223 parser.add_argument(
224 '--quality',
225 type=float,
226 help='vorbisenc quality. Range: -0.1..1')
227
228 parser.add_argument(
229 '--fft_size',
230 type=int,
231 help='spectrogram fft size')
232
233 parser.add_argument(
234 '--thumb_size',
235 nargs=2,
236 metavar=('max_width', 'max_height'),
237 type=int,
238 help='minimum size is 100 x 100')
239
240 parser.add_argument(
241 '--medium_width',
242 type=int,
243 help='The width of the spectogram')
244
245 return parser
246
247 @classmethod
248 def args_to_request(cls, args):
249 return request_from_args(
250 args, ['quality', 'fft_size',
251 'thumb_size', 'medium_width'])
252
253 def process(self, quality=None, fft_size=None, thumb_size=None,
254 medium_width=None):
255 self.common_setup()
256
257 self.transcode(quality=quality)
258 self.copy_original()
259
260 self.create_spectrogram(max_width=medium_width, fft_size=fft_size)
261 self.generate_thumb(size=thumb_size)
262
263 self.delete_queue_file()
264
265
266 class Resizer(CommonAudioProcessor):
267 """
268 Thumbnail and spectogram resizing process steps for processed audio
269 """
270 name = 'resize'
271 description = 'Resize thumbnail or spectogram'
272 thumb_size = 'thumb_size'
273
274 @classmethod
275 def media_is_eligible(cls, entry=None, state=None):
276 """
277 Determine if this media entry is eligible for processing
278 """
279 if not state:
280 state = entry.state
281 return state in 'processed'
282
283 @classmethod
284 def generate_parser(cls):
285 parser = argparse.ArgumentParser(
286 description=cls.description,
287 prog=cls.name)
288
289 parser.add_argument(
290 '--fft_size',
291 type=int,
292 help='spectrogram fft size')
293
294 parser.add_argument(
295 '--thumb_size',
296 nargs=2,
297 metavar=('max_width', 'max_height'),
298 type=int,
299 help='minimum size is 100 x 100')
300
301 parser.add_argument(
302 '--medium_width',
303 type=int,
304 help='The width of the spectogram')
305
306 parser.add_argument(
307 'file',
308 choices=['thumb', 'spectrogram'])
309
310 return parser
311
312 @classmethod
313 def args_to_request(cls, args):
314 return request_from_args(
315 args, ['thumb_size', 'file', 'fft_size', 'medium_width'])
316
317 def process(self, file, thumb_size=None, fft_size=None,
318 medium_width=None):
319 self.common_setup()
320
321 if file == 'thumb':
322 self.generate_thumb(size=thumb_size)
323 elif file == 'spectrogram':
324 self.create_spectrogram(max_width=medium_width, fft_size=fft_size)
325
326
327 class Transcoder(CommonAudioProcessor):
328 """
329 Transcoding processing steps for processed audio
330 """
331 name = 'transcode'
332 description = 'Re-transcode audio'
333
334 @classmethod
335 def media_is_eligible(cls, entry=None, state=None):
336 if not state:
337 state = entry.state
338 return state in 'processed'
339
340 @classmethod
341 def generate_parser(cls):
342 parser = argparse.ArgumentParser(
343 description=cls.description,
344 prog=cls.name)
345
346 parser.add_argument(
347 '--quality',
348 help='vorbisenc quality. Range: -0.1..1')
349
350 return parser
351
352 @classmethod
353 def args_to_request(cls, args):
354 return request_from_args(
355 args, ['quality'])
356
357 def process(self, quality=None):
358 self.common_setup()
359 self.transcode(quality=quality)
360
361
362 class AudioProcessingManager(ProcessingManager):
363 def __init__(self):
364 super(AudioProcessingManager, self).__init__()
365 self.add_processor(InitialProcessor)
366 self.add_processor(Resizer)
367 self.add_processor(Transcoder)
368
369 def workflow(self, entry, manager, feed_url, reprocess_action,
370 reprocess_info=None):
371 ProcessMedia().apply_async(
372 [entry.id, feed_url, reprocess_action, reprocess_info], {},
373 task_id=entry.queued_task_id)