Merge branch 'master' of git://gitorious.org/mediagoblin/mediagoblin
[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 from mediagoblin import mg_globals as mgg
22 from mediagoblin.processing import (
23 BadMediaFail, FilenameBuilder,
24 ProgressCallback, MediaProcessor, ProcessingManager,
25 request_from_args, get_process_filename,
26 store_public, copy_original)
27
28 from mediagoblin.media_types.audio.transcoders import (
29 AudioTranscoder, AudioThumbnailer)
30
31 _log = logging.getLogger(__name__)
32
33 MEDIA_TYPE = 'mediagoblin.media_types.audio'
34
35
36 def sniff_handler(media_file, **kw):
37 _log.info('Sniffing {0}'.format(MEDIA_TYPE))
38 try:
39 transcoder = AudioTranscoder()
40 data = transcoder.discover(media_file.name)
41 except BadMediaFail:
42 _log.debug('Audio discovery raised BadMediaFail')
43 return None
44
45 if data.is_audio is True and data.is_video is False:
46 return MEDIA_TYPE
47
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['media_type: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 transcode(self, quality=None):
91 if not quality:
92 quality = self.audio_config['quality']
93
94 progress_callback = ProgressCallback(self.entry)
95 webm_audio_tmp = os.path.join(self.workbench.dir,
96 self.name_builder.fill(
97 '{basename}{ext}'))
98
99 self.transcoder.transcode(
100 self.process_filename,
101 webm_audio_tmp,
102 quality=quality,
103 progress_callback=progress_callback)
104
105 self.transcoder.discover(webm_audio_tmp)
106
107 self._keep_best()
108
109 _log.debug('Saving medium...')
110 store_public(self.entry, 'webm_audio', webm_audio_tmp,
111 self.name_builder.fill('{basename}.medium.webm'))
112
113 def create_spectrogram(self, max_width=None, fft_size=None):
114 if not max_width:
115 max_width = mgg.global_config['media:medium']['max_width']
116 if not fft_size:
117 fft_size = self.audio_config['spectrogram_fft_size']
118
119 wav_tmp = os.path.join(self.workbench.dir, self.name_builder.fill(
120 '{basename}.ogg'))
121
122 _log.info('Creating OGG source for spectrogram')
123 self.transcoder.transcode(
124 self.process_filename,
125 wav_tmp,
126 mux_string='vorbisenc quality={0} ! oggmux'.format(
127 self.audio_config['quality']))
128
129 spectrogram_tmp = os.path.join(self.workbench.dir,
130 self.name_builder.fill(
131 '{basename}-spectrogram.jpg'))
132
133 self.thumbnailer.spectrogram(
134 wav_tmp,
135 spectrogram_tmp,
136 width=max_width,
137 fft_size=fft_size)
138
139 _log.debug('Saving spectrogram...')
140 store_public(self.entry, 'spectrogram', spectrogram_tmp,
141 self.name_builder.fill('{basename}.spectrogram.jpg'))
142
143 def generate_thumb(self, size=None):
144 if not size:
145 max_width = mgg.global_config['media:thumb']['max_width']
146 max_height = mgg.global_config['media:thumb']['max_height']
147 size = (max_width, max_height)
148
149 thumb_tmp = os.path.join(self.workbench.dir, self.name_builder.fill(
150 '{basename}-thumbnail.jpg'))
151
152 # We need the spectrogram to create a thumbnail
153 spectrogram = self.entry.media_files.get('spectrogram')
154 if not spectrogram:
155 _log.info('No spectrogram found, we will create one.')
156 self.create_spectrogram()
157 spectrogram = self.entry.media_files['spectrogram']
158
159 spectrogram_filepath = mgg.public_store.get_local_path(spectrogram)
160
161 self.thumbnailer.thumbnail_spectrogram(
162 spectrogram_filepath,
163 thumb_tmp,
164 tuple(size))
165
166 store_public(self.entry, 'thumb', thumb_tmp,
167 self.name_builder.fill('{basename}.thumbnail.jpg'))
168
169
170 class InitialProcessor(CommonAudioProcessor):
171 """
172 Initial processing steps for new audio
173 """
174 name = "initial"
175 description = "Initial processing"
176
177 @classmethod
178 def media_is_eligible(cls, entry=None, state=None):
179 """
180 Determine if this media type is eligible for processing
181 """
182 if not state:
183 state = entry.state
184 return state in (
185 "unprocessed", "failed")
186
187 @classmethod
188 def generate_parser(cls):
189 parser = argparse.ArgumentParser(
190 description=cls.description,
191 prog=cls.name)
192
193 parser.add_argument(
194 '--quality',
195 type=float,
196 help='vorbisenc quality. Range: -0.1..1')
197
198 parser.add_argument(
199 '--fft_size',
200 type=int,
201 help='spectrogram fft size')
202
203 parser.add_argument(
204 '--thumb_size',
205 nargs=2,
206 metavar=('max_width', 'max_height'),
207 type=int,
208 help='minimum size is 100 x 100')
209
210 parser.add_argument(
211 '--medium_width',
212 type=int,
213 help='The width of the spectogram')
214
215 parser.add_argument(
216 '--create_spectrogram',
217 action='store_true',
218 help='Create spectogram and thumbnail, will default to config')
219
220 return parser
221
222 @classmethod
223 def args_to_request(cls, args):
224 return request_from_args(
225 args, ['create_spectrogram', 'quality', 'fft_size',
226 'thumb_size', 'medium_width'])
227
228 def process(self, quality=None, fft_size=None, thumb_size=None,
229 create_spectrogram=None, medium_width=None):
230 self.common_setup()
231
232 if not create_spectrogram:
233 create_spectrogram = self.audio_config['create_spectrogram']
234
235 self.transcode(quality=quality)
236 self.copy_original()
237
238 if create_spectrogram:
239 self.create_spectrogram(max_width=medium_width, fft_size=fft_size)
240 self.generate_thumb(size=thumb_size)
241 self.delete_queue_file()
242
243
244 class Resizer(CommonAudioProcessor):
245 """
246 Thumbnail and spectogram resizing process steps for processed audio
247 """
248 name = 'resize'
249 description = 'Resize thumbnail or spectogram'
250 thumb_size = 'thumb_size'
251
252 @classmethod
253 def media_is_eligible(cls, entry=None, state=None):
254 """
255 Determine if this media entry is eligible for processing
256 """
257 if not state:
258 state = entry.state
259 return state in 'processed'
260
261 @classmethod
262 def generate_parser(cls):
263 parser = argparse.ArgumentParser(
264 description=cls.description,
265 prog=cls.name)
266
267 parser.add_argument(
268 '--fft_size',
269 type=int,
270 help='spectrogram fft size')
271
272 parser.add_argument(
273 '--thumb_size',
274 nargs=2,
275 metavar=('max_width', 'max_height'),
276 type=int,
277 help='minimum size is 100 x 100')
278
279 parser.add_argument(
280 '--medium_width',
281 type=int,
282 help='The width of the spectogram')
283
284 parser.add_argument(
285 'file',
286 choices=['thumb', 'spectrogram'])
287
288 return parser
289
290 @classmethod
291 def args_to_request(cls, args):
292 return request_from_args(
293 args, ['thumb_size', 'file', 'fft_size', 'medium_width'])
294
295 def process(self, file, thumb_size=None, fft_size=None,
296 medium_width=None):
297 self.common_setup()
298
299 if file == 'thumb':
300 self.generate_thumb(size=thumb_size)
301 elif file == 'spectrogram':
302 self.create_spectrogram(max_width=medium_width, fft_size=fft_size)
303
304
305 class Transcoder(CommonAudioProcessor):
306 """
307 Transcoding processing steps for processed audio
308 """
309 name = 'transcode'
310 description = 'Re-transcode audio'
311
312 @classmethod
313 def media_is_eligible(cls, entry=None, state=None):
314 if not state:
315 state = entry.state
316 return state in 'processed'
317
318 @classmethod
319 def generate_parser(cls):
320 parser = argparse.ArgumentParser(
321 description=cls.description,
322 prog=cls.name)
323
324 parser.add_argument(
325 '--quality',
326 help='vorbisenc quality. Range: -0.1..1')
327
328 return parser
329
330 @classmethod
331 def args_to_request(cls, args):
332 return request_from_args(
333 args, ['quality'])
334
335 def process(self, quality=None):
336 self.common_setup()
337 self.transcode(quality=quality)
338
339
340 class AudioProcessingManager(ProcessingManager):
341 def __init__(self):
342 super(self.__class__, self).__init__()
343 self.add_processor(InitialProcessor)
344 self.add_processor(Resizer)
345 self.add_processor(Transcoder)