Commit | Line | Data |
---|---|---|
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 | 17 | import argparse |
5a34a80d | 18 | import logging |
25e39842 | 19 | from tempfile import NamedTemporaryFile |
5a34a80d JW |
20 | import os |
21 | ||
22 | from mediagoblin import mg_globals as mgg | |
5ac1fe80 RE |
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) | |
5a34a80d | 28 | |
5ac1fe80 RE |
29 | from mediagoblin.media_types.audio.transcoders import ( |
30 | AudioTranscoder,AudioThumbnailer) | |
5a34a80d | 31 | |
10085b77 | 32 | _log = logging.getLogger(__name__) |
5a34a80d | 33 | |
df68438a RE |
34 | MEDIA_TYPE = 'mediagoblin.media_types.audio' |
35 | ||
64712915 | 36 | |
ec4261a4 | 37 | def sniff_handler(media_file, **kw): |
df68438a | 38 | _log.info('Sniffing {0}'.format(MEDIA_TYPE)) |
196a5181 | 39 | try: |
4f4f2531 | 40 | transcoder = AudioTranscoder() |
ec4261a4 | 41 | data = transcoder.discover(media_file.name) |
4f4f2531 JW |
42 | except BadMediaFail: |
43 | _log.debug('Audio discovery raised BadMediaFail') | |
df68438a | 44 | return None |
ec4261a4 | 45 | |
4f4f2531 | 46 | if data.is_audio == True and data.is_video == False: |
df68438a | 47 | return MEDIA_TYPE |
10085b77 | 48 | |
df68438a | 49 | return None |
5a34a80d | 50 | |
64712915 | 51 | |
fb46fa66 | 52 | def process_audio(proc_state): |
45ab3e07 | 53 | """Code to process uploaded audio. Will be run by celery. |
5a34a80d | 54 | |
45ab3e07 SS |
55 | A Workbench() represents a local tempory dir. It is automatically |
56 | cleaned up when this function exits. | |
57 | """ | |
fb46fa66 E |
58 | entry = proc_state.entry |
59 | workbench = proc_state.workbench | |
45ab3e07 | 60 | audio_config = mgg.global_config['media_type:mediagoblin.media_types.audio'] |
5a34a80d JW |
61 | |
62 | queued_filepath = entry.queued_media_file | |
63 | queued_filename = workbench.localized_file( | |
64 | mgg.queue_store, queued_filepath, | |
65 | 'source') | |
b781c3c9 | 66 | name_builder = FilenameBuilder(queued_filename) |
5a34a80d | 67 | |
b781c3c9 | 68 | webm_audio_filepath = create_pub_filepath( |
5a34a80d JW |
69 | entry, |
70 | '{original}.webm'.format( | |
71 | original=os.path.splitext( | |
72 | queued_filepath[-1])[0])) | |
73 | ||
b781c3c9 JK |
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 | ||
10085b77 | 86 | transcoder = AudioTranscoder() |
5a34a80d | 87 | |
25e39842 | 88 | with NamedTemporaryFile(dir=workbench.dir) as webm_audio_tmp: |
64712915 | 89 | progress_callback = ProgressCallback(entry) |
5a34a80d JW |
90 | |
91 | transcoder.transcode( | |
92 | queued_filename, | |
b781c3c9 | 93 | webm_audio_tmp.name, |
64712915 JW |
94 | quality=audio_config['quality'], |
95 | progress_callback=progress_callback) | |
5a34a80d | 96 | |
a855e92a | 97 | transcoder.discover(webm_audio_tmp.name) |
5a34a80d JW |
98 | |
99 | _log.debug('Saving medium...') | |
b781c3c9 JK |
100 | mgg.public_store.get_file(webm_audio_filepath, 'wb').write( |
101 | webm_audio_tmp.read()) | |
5a34a80d | 102 | |
b781c3c9 | 103 | entry.media_files['webm_audio'] = webm_audio_filepath |
5a34a80d | 104 | |
c7cf6235 | 105 | # entry.media_data_init(length=int(data.audiolength)) |
5a34a80d | 106 | |
10085b77 JW |
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 | ||
25e39842 | 114 | with NamedTemporaryFile(dir=workbench.dir, suffix='.ogg') as wav_tmp: |
549000d9 | 115 | _log.info('Creating OGG source for spectrogram') |
10085b77 JW |
116 | transcoder.transcode( |
117 | queued_filename, | |
118 | wav_tmp.name, | |
549000d9 JW |
119 | mux_string='vorbisenc quality={0} ! oggmux'.format( |
120 | audio_config['quality'])) | |
10085b77 JW |
121 | |
122 | thumbnailer = AudioThumbnailer() | |
123 | ||
25e39842 | 124 | with NamedTemporaryFile(dir=workbench.dir, suffix='.jpg') as spectrogram_tmp: |
10085b77 JW |
125 | thumbnailer.spectrogram( |
126 | wav_tmp.name, | |
127 | spectrogram_tmp.name, | |
196a5181 JW |
128 | width=mgg.global_config['media:medium']['max_width'], |
129 | fft_size=audio_config['spectrogram_fft_size']) | |
10085b77 JW |
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 | ||
25e39842 | 137 | with NamedTemporaryFile(dir=workbench.dir, suffix='.jpg') as thumb_tmp: |
10085b77 JW |
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: | |
5a34a80d | 155 | entry.media_files['thumb'] = ['fake', 'thumb', 'path.jpg'] |
196a5181 | 156 | |
36ae6bcb SS |
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 = [] | |
5ac1fe80 RE |
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 | webm_audio_filepath) | |
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 | spectrogram_filepath) | |
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, thumb_filepath) | |
276 | ||
277 | ||
278 | class InitialProcessor(CommonAudioProcessor): | |
279 | """ | |
280 | Initial processing steps for new audio | |
281 | """ | |
282 | name = "initial" | |
283 | description = "Initial processing" | |
284 | ||
285 | @classmethod | |
286 | def media_is_eligible(cls, entry=None, state=None): | |
287 | """ | |
288 | Determine if this media type is eligible for processing | |
289 | """ | |
290 | if not state: | |
291 | state = entry.state | |
292 | return state in ( | |
293 | "unprocessed", "failed") | |
294 | ||
295 | @classmethod | |
296 | def generate_parser(cls): | |
297 | parser = argparse.ArgumentParser( | |
298 | description=cls.description, | |
299 | prog=cls.name) | |
300 | ||
301 | parser.add_argument( | |
302 | '--quality', | |
303 | help='vorbisenc quality') | |
304 | ||
305 | parser.add_argument( | |
306 | '--fft_size', | |
307 | type=int, | |
308 | help='spectrogram fft size') | |
309 | ||
310 | parser.add_argument( | |
311 | '--thumb_size', | |
312 | metavar=('max_width', 'max_height'), | |
313 | type=int) | |
314 | ||
315 | parser.add_argument( | |
316 | '--medium_width', | |
317 | type=int, | |
318 | help='The width of the spectogram') | |
319 | ||
320 | parser.add_argument( | |
321 | '--create_spectrogram', | |
322 | action='store_true', | |
323 | help='Create spectogram and thumbnail') | |
324 | ||
325 | return parser | |
326 | ||
327 | @classmethod | |
328 | def args_to_request(cls, args): | |
329 | return request_from_args( | |
330 | args, ['create_spectrogram', 'quality', 'fft_size', | |
331 | 'thumb_size', 'medium_width']) | |
332 | ||
333 | def process(self, quality=None, fft_size=None, thumb_size=None, | |
334 | create_spectrogram=None, medium_width=None): | |
335 | if not create_spectrogram: | |
336 | create_spectrogram = self.audio_config['create_spectrogram'] | |
337 | ||
338 | self.common_setup() | |
339 | self.transcode(quality=quality) | |
340 | self.copy_original() | |
341 | ||
342 | if create_spectrogram: | |
343 | self.create_spectrogram(quality=quality, max_width=medium_width, | |
344 | fft_size=fft_size) | |
345 | self.generate_thumb(size=thumb_size) | |
346 | self.delete_queue_file() | |
347 | ||
348 | ||
349 | class AudioProcessingManager(ProcessingManager): | |
350 | def __init__(self): | |
351 | super(self.__class__, self).__init__() | |
352 | self.add_processor(InitialProcessor) |