use name_builder with store_public, not create_pub_filepath
[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 from tempfile import NamedTemporaryFile
20 import os
21
22 from mediagoblin import mg_globals as mgg
23 from mediagoblin.processing import (
24 create_pub_filepath, BadMediaFail, FilenameBuilder,
25 ProgressCallback, MediaProcessor, ProcessingManager,
26 request_from_args, get_orig_filename,
27 store_public, copy_original)
28
29 from mediagoblin.media_types.audio.transcoders import (
30 AudioTranscoder,AudioThumbnailer)
31
32 _log = logging.getLogger(__name__)
33
34 MEDIA_TYPE = 'mediagoblin.media_types.audio'
35
36
37 def sniff_handler(media_file, **kw):
38 _log.info('Sniffing {0}'.format(MEDIA_TYPE))
39 try:
40 transcoder = AudioTranscoder()
41 data = transcoder.discover(media_file.name)
42 except BadMediaFail:
43 _log.debug('Audio discovery raised BadMediaFail')
44 return None
45
46 if data.is_audio == True and data.is_video == False:
47 return MEDIA_TYPE
48
49 return None
50
51
52 def process_audio(proc_state):
53 """Code to process uploaded audio. Will be run by celery.
54
55 A Workbench() represents a local tempory dir. It is automatically
56 cleaned up when this function exits.
57 """
58 entry = proc_state.entry
59 workbench = proc_state.workbench
60 audio_config = mgg.global_config['media_type:mediagoblin.media_types.audio']
61
62 queued_filepath = entry.queued_media_file
63 queued_filename = workbench.localized_file(
64 mgg.queue_store, queued_filepath,
65 'source')
66 name_builder = FilenameBuilder(queued_filename)
67
68 webm_audio_filepath = create_pub_filepath(
69 entry,
70 '{original}.webm'.format(
71 original=os.path.splitext(
72 queued_filepath[-1])[0]))
73
74 if audio_config['keep_original']:
75 with open(queued_filename, 'rb') as queued_file:
76 original_filepath = create_pub_filepath(
77 entry, name_builder.fill('{basename}{ext}'))
78
79 with mgg.public_store.get_file(original_filepath, 'wb') as \
80 original_file:
81 _log.debug('Saving original...')
82 original_file.write(queued_file.read())
83
84 entry.media_files['original'] = original_filepath
85
86 transcoder = AudioTranscoder()
87
88 with NamedTemporaryFile(dir=workbench.dir) as webm_audio_tmp:
89 progress_callback = ProgressCallback(entry)
90
91 transcoder.transcode(
92 queued_filename,
93 webm_audio_tmp.name,
94 quality=audio_config['quality'],
95 progress_callback=progress_callback)
96
97 transcoder.discover(webm_audio_tmp.name)
98
99 _log.debug('Saving medium...')
100 mgg.public_store.get_file(webm_audio_filepath, 'wb').write(
101 webm_audio_tmp.read())
102
103 entry.media_files['webm_audio'] = webm_audio_filepath
104
105 # entry.media_data_init(length=int(data.audiolength))
106
107 if audio_config['create_spectrogram']:
108 spectrogram_filepath = create_pub_filepath(
109 entry,
110 '{original}-spectrogram.jpg'.format(
111 original=os.path.splitext(
112 queued_filepath[-1])[0]))
113
114 with NamedTemporaryFile(dir=workbench.dir, suffix='.ogg') as wav_tmp:
115 _log.info('Creating OGG source for spectrogram')
116 transcoder.transcode(
117 queued_filename,
118 wav_tmp.name,
119 mux_string='vorbisenc quality={0} ! oggmux'.format(
120 audio_config['quality']))
121
122 thumbnailer = AudioThumbnailer()
123
124 with NamedTemporaryFile(dir=workbench.dir, suffix='.jpg') as spectrogram_tmp:
125 thumbnailer.spectrogram(
126 wav_tmp.name,
127 spectrogram_tmp.name,
128 width=mgg.global_config['media:medium']['max_width'],
129 fft_size=audio_config['spectrogram_fft_size'])
130
131 _log.debug('Saving spectrogram...')
132 mgg.public_store.get_file(spectrogram_filepath, 'wb').write(
133 spectrogram_tmp.read())
134
135 entry.media_files['spectrogram'] = spectrogram_filepath
136
137 with NamedTemporaryFile(dir=workbench.dir, suffix='.jpg') as thumb_tmp:
138 thumbnailer.thumbnail_spectrogram(
139 spectrogram_tmp.name,
140 thumb_tmp.name,
141 (mgg.global_config['media:thumb']['max_width'],
142 mgg.global_config['media:thumb']['max_height']))
143
144 thumb_filepath = create_pub_filepath(
145 entry,
146 '{original}-thumbnail.jpg'.format(
147 original=os.path.splitext(
148 queued_filepath[-1])[0]))
149
150 mgg.public_store.get_file(thumb_filepath, 'wb').write(
151 thumb_tmp.read())
152
153 entry.media_files['thumb'] = thumb_filepath
154 else:
155 entry.media_files['thumb'] = ['fake', 'thumb', 'path.jpg']
156
157 # Remove queued media file from storage and database.
158 # queued_filepath is in the task_id directory which should
159 # be removed too, but fail if the directory is not empty to be on
160 # the super-safe side.
161 mgg.queue_store.delete_file(queued_filepath) # rm file
162 mgg.queue_store.delete_dir(queued_filepath[:-1]) # rm dir
163 entry.queued_media_file = []
164
165
166 class CommonAudioProcessor(MediaProcessor):
167 """
168 Provides a base for various audio processing steps
169 """
170
171 def common_setup(self):
172 """
173 """
174 self.audio_config = mgg \
175 .global_config['media_type:mediagoblin.media_types.audio']
176
177 # Pull down and set up the original file
178 self.orig_filename = get_orig_filename(
179 self.entry, self.workbench)
180 self.name_builder = FilenameBuilder(self.orig_filename)
181
182 self.spectrogram_tmp = os.path.join(self.workbench.dir,
183 self.name_builder.fill(
184 '{basename}-spectrogram.jpg'))
185
186 self.transcoder = AudioTranscoder()
187 self.thumbnailer = AudioThumbnailer()
188
189 def copy_original(self):
190 if self.audio_config['keep_original']:
191 copy_original(
192 self.entry, self.orig_filename,
193 self.name_builder.fill('{basename}{ext}'))
194
195 def transcode(self, quality=None):
196 if not quality:
197 quality = self.audio_config['quality']
198
199 progress_callback = ProgressCallback(self.entry)
200 webm_audio_tmp = os.path.join(self.workbench.dir,
201 self.name_builder.fill(
202 '{basename}{ext}'))
203
204 #webm_audio_filepath = create_pub_filepath(
205 # self.entry,
206 # '{original}.webm'.format(
207 # original=os.path.splitext(
208 # self.orig_filename[-1])[0]))
209
210 self.transcoder.transcode(
211 self.orig_filename,
212 webm_audio_tmp,
213 quality=quality,
214 progress_callback=progress_callback)
215
216 self.transcoder.discover(webm_audio_tmp)
217
218 _log.debug('Saving medium...')
219 store_public(self.entry, 'medium', webm_audio_tmp,
220 self.name_builder.fill('{basename}.medium{ext}'))
221
222 def create_spectrogram(self, quality=None, max_width=None, fft_size=None):
223 if not quality:
224 quality = self.audio_config['quality']
225 if not max_width:
226 max_width = mgg.global_config['media:medium']['max_width']
227 if not fft_size:
228 fft_size = self.audio_config['spectrogram_fft_size']
229
230 #spectrogram_filepath = create_pub_filepath(
231 # self.entry,
232 # '{original}-spectrogram.jpg'.format(
233 # original=os.path.splitext(
234 # self.orig_filename[-1])[0]))
235
236 wav_tmp = os.path.join(self.workbench.dir, self.name_builder.fill(
237 '{basename}.ogg'))
238
239 _log.info('Creating OGG source for spectrogram')
240 self.transcoder.transcode(
241 self.orig_filename,
242 wav_tmp,
243 mux_string='vorbisenc quality={0} ! oggmux'.format(quality))
244
245 self.thumbnailer.spectrogram(
246 wav_tmp,
247 self.spectrogram_tmp,
248 width=max_width,
249 fft_size=fft_size)
250
251 _log.debug('Saving spectrogram...')
252 store_public(self.entry, 'spectrogram', self.spectrogram_tmp,
253 self.name_builder.fill('{basename}.spectrogram.jpg'))
254
255 def generate_thumb(self, size=None):
256 if not size:
257 max_width = mgg.global_config['medium:thumb']['max_width']
258 max_height = mgg.global_config['medium:thumb']['max_height']
259 size = (max_width, max_height)
260
261 thumb_tmp = os.path.join(self.workbench.dir, self.name_builder.fill(
262 '{basename}-thumbnail.jpg'))
263
264 self.thumbnailer.thumbnail_spectrogram(
265 self.spectrogram_tmp,
266 thumb_tmp,
267 size)
268
269 #thumb_filepath = create_pub_filepath(
270 # self.entry,
271 # '{original}-thumbnail.jpg'.format(
272 # original=os.path.splitext(
273 # self.orig_filename[-1])[0]))
274
275 store_public(self.entry, 'thumb', thumb_tmp,
276 self.name_builder.fill('{basename}.thumbnail.jpg'))
277
278
279 class InitialProcessor(CommonAudioProcessor):
280 """
281 Initial processing steps for new audio
282 """
283 name = "initial"
284 description = "Initial processing"
285
286 @classmethod
287 def media_is_eligible(cls, entry=None, state=None):
288 """
289 Determine if this media type is eligible for processing
290 """
291 if not state:
292 state = entry.state
293 return state in (
294 "unprocessed", "failed")
295
296 @classmethod
297 def generate_parser(cls):
298 parser = argparse.ArgumentParser(
299 description=cls.description,
300 prog=cls.name)
301
302 parser.add_argument(
303 '--quality',
304 help='vorbisenc quality')
305
306 parser.add_argument(
307 '--fft_size',
308 type=int,
309 help='spectrogram fft size')
310
311 parser.add_argument(
312 '--thumb_size',
313 metavar=('max_width', 'max_height'),
314 type=int)
315
316 parser.add_argument(
317 '--medium_width',
318 type=int,
319 help='The width of the spectogram')
320
321 parser.add_argument(
322 '--create_spectrogram',
323 action='store_true',
324 help='Create spectogram and thumbnail')
325
326 return parser
327
328 @classmethod
329 def args_to_request(cls, args):
330 return request_from_args(
331 args, ['create_spectrogram', 'quality', 'fft_size',
332 'thumb_size', 'medium_width'])
333
334 def process(self, quality=None, fft_size=None, thumb_size=None,
335 create_spectrogram=None, medium_width=None):
336 self.common_setup()
337
338 if not create_spectrogram:
339 create_spectrogram = self.audio_config['create_spectrogram']
340
341 self.transcode(quality=quality)
342 self.copy_original()
343
344 if create_spectrogram:
345 self.create_spectrogram(quality=quality, max_width=medium_width,
346 fft_size=fft_size)
347 self.generate_thumb(size=thumb_size)
348 self.delete_queue_file()
349
350
351 class AudioProcessingManager(ProcessingManager):
352 def __init__(self):
353 super(self.__class__, self).__init__()
354 self.add_processor(InitialProcessor)