1 # GNU MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
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.
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.
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/>.
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
)
30 from mediagoblin
.media_types
.audio
.transcoders
import (
31 AudioTranscoder
, AudioThumbnailer
)
32 from mediagoblin
.media_types
.tools
import discover
34 _log
= logging
.getLogger(__name__
)
36 MEDIA_TYPE
= 'mediagoblin.media_types.audio'
39 def sniff_handler(media_file
, filename
):
40 _log
.info('Sniffing {0}'.format(MEDIA_TYPE
))
42 data
= discover(media_file
.name
)
43 except Exception as e
:
44 _log
.info(six
.text_type(e
))
46 if data
and data
.get_audio_streams() and not data
.get_video_streams():
51 class CommonAudioProcessor(MediaProcessor
):
53 Provides a base for various audio processing steps
55 acceptable_files
= ['original', 'best_quality', 'webm_audio']
57 def common_setup(self
):
59 Setup the workbench directory and pull down the original file, add
60 the audio_config, transcoder, thumbnailer and spectrogram_tmp path
62 self
.audio_config
= mgg \
63 .global_config
['plugins']['mediagoblin.media_types.audio']
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
)
70 self
.transcoder
= AudioTranscoder()
71 self
.thumbnailer
= AudioThumbnailer()
73 def copy_original(self
):
74 if self
.audio_config
['keep_original']:
76 self
.entry
, self
.process_filename
,
77 self
.name_builder
.fill('{basename}{ext}'))
81 If there is no original, keep the best file that we have
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']
90 def _skip_processing(self
, keyname
, **kwargs
):
91 file_metadata
= self
.entry
.get_file_metadata(keyname
)
97 if keyname
== 'webm_audio':
98 if kwargs
.get('quality') != file_metadata
.get('quality'):
100 elif keyname
== 'spectrogram':
101 if kwargs
.get('max_width') != file_metadata
.get('max_width'):
103 elif kwargs
.get('fft_size') != file_metadata
.get('fft_size'):
105 elif keyname
== 'thumb':
106 if kwargs
.get('size') != file_metadata
.get('size'):
111 def transcode(self
, quality
=None):
113 quality
= self
.audio_config
['quality']
115 if self
._skip
_processing
('webm_audio', quality
=quality
):
118 progress_callback
= ProgressCallback(self
.entry
)
119 webm_audio_tmp
= os
.path
.join(self
.workbench
.dir,
120 self
.name_builder
.fill(
123 self
.transcoder
.transcode(
124 self
.process_filename
,
127 progress_callback
=progress_callback
)
131 _log
.debug('Saving medium...')
132 store_public(self
.entry
, 'webm_audio', webm_audio_tmp
,
133 self
.name_builder
.fill('{basename}.medium.webm'))
135 self
.entry
.set_file_metadata('webm_audio', **{'quality': quality
})
137 def create_spectrogram(self
, max_width
=None, fft_size
=None):
139 max_width
= mgg
.global_config
['media:medium']['max_width']
141 fft_size
= self
.audio_config
['spectrogram_fft_size']
143 if self
._skip
_processing
('spectrogram', max_width
=max_width
,
146 wav_tmp
= os
.path
.join(self
.workbench
.dir, self
.name_builder
.fill(
148 _log
.info('Creating OGG source for spectrogram')
149 self
.transcoder
.transcode(self
.process_filename
, wav_tmp
,
152 spectrogram_tmp
= os
.path
.join(self
.workbench
.dir,
153 self
.name_builder
.fill(
154 '{basename}-spectrogram.jpg'))
157 self
.thumbnailer
.spectrogram(
163 _log
.debug('Saving spectrogram...')
164 store_public(self
.entry
, 'spectrogram', thumbnail
,
165 self
.name_builder
.fill('{basename}.spectrogram.jpg'))
167 file_metadata
= {'max_width': max_width
,
168 'fft_size': fft_size
}
169 self
.entry
.set_file_metadata('spectrogram', **file_metadata
)
173 'Your version of Numpy is too new to create the waveform thumbnail (#5457). '
174 "Try\n\t./bin/pip install numpy==1.9.1\n\t./bin/pip install scikits.audiolab==0.10.2")
176 except Exception as exc
:
177 _log
.warn('Failed to create spectrogram: '
180 def generate_thumb(self
, size
=None):
182 max_width
= mgg
.global_config
['media:thumb']['max_width']
183 max_height
= mgg
.global_config
['media:thumb']['max_height']
184 size
= (max_width
, max_height
)
186 if self
._skip
_processing
('thumb', size
=size
):
189 thumb_tmp
= os
.path
.join(self
.workbench
.dir, self
.name_builder
.fill(
190 '{basename}-thumbnail.jpg'))
192 # We need the spectrogram to create a thumbnail
194 spectrogram
= self
.entry
.media_files
.get('spectrogram')
196 _log
.info('No spectrogram found, we will create one.')
197 self
.create_spectrogram()
198 spectrogram
= self
.entry
.media_files
['spectrogram']
200 spectrogram_filepath
= mgg
.public_store
.get_local_path(spectrogram
)
203 _log
.warn('Failed to create spectrogram, using default audio image instead.')
204 spectrogram_filepath
= 'mediagoblin/static/images/media_thumbs/audio.png'
206 self
.thumbnailer
.thumbnail_spectrogram(
207 spectrogram_filepath
,
211 store_public(self
.entry
, 'thumb', thumb_tmp
,
212 self
.name_builder
.fill('{basename}.thumbnail.jpg'))
214 self
.entry
.set_file_metadata('thumb', **{'size': size
})
217 class InitialProcessor(CommonAudioProcessor
):
219 Initial processing steps for new audio
222 description
= "Initial processing"
225 def media_is_eligible(cls
, entry
=None, state
=None):
227 Determine if this media type is eligible for processing
232 "unprocessed", "failed")
235 def generate_parser(cls
):
236 parser
= argparse
.ArgumentParser(
237 description
=cls
.description
,
243 help='vorbisenc quality. Range: -0.1..1')
248 help='spectrogram fft size')
253 metavar
=('max_width', 'max_height'),
255 help='minimum size is 100 x 100')
260 help='The width of the spectogram')
265 def args_to_request(cls
, args
):
266 return request_from_args(
267 args
, ['quality', 'fft_size',
268 'thumb_size', 'medium_width'])
270 def process(self
, quality
=None, fft_size
=None, thumb_size
=None,
274 self
.transcode(quality
=quality
)
277 self
.create_spectrogram(max_width
=medium_width
, fft_size
=fft_size
)
278 self
.generate_thumb(size
=thumb_size
)
280 self
.delete_queue_file()
283 class Resizer(CommonAudioProcessor
):
285 Thumbnail and spectogram resizing process steps for processed audio
288 description
= 'Resize thumbnail or spectogram'
289 thumb_size
= 'thumb_size'
292 def media_is_eligible(cls
, entry
=None, state
=None):
294 Determine if this media entry is eligible for processing
298 return state
in 'processed'
301 def generate_parser(cls
):
302 parser
= argparse
.ArgumentParser(
303 description
=cls
.description
,
309 help='spectrogram fft size')
314 metavar
=('max_width', 'max_height'),
316 help='minimum size is 100 x 100')
321 help='The width of the spectogram')
325 choices
=['thumb', 'spectrogram'])
330 def args_to_request(cls
, args
):
331 return request_from_args(
332 args
, ['thumb_size', 'file', 'fft_size', 'medium_width'])
334 def process(self
, file, thumb_size
=None, fft_size
=None,
339 self
.generate_thumb(size
=thumb_size
)
340 elif file == 'spectrogram':
341 self
.create_spectrogram(max_width
=medium_width
, fft_size
=fft_size
)
344 class Transcoder(CommonAudioProcessor
):
346 Transcoding processing steps for processed audio
349 description
= 'Re-transcode audio'
352 def media_is_eligible(cls
, entry
=None, state
=None):
355 return state
in 'processed'
358 def generate_parser(cls
):
359 parser
= argparse
.ArgumentParser(
360 description
=cls
.description
,
365 help='vorbisenc quality. Range: -0.1..1')
370 def args_to_request(cls
, args
):
371 return request_from_args(
374 def process(self
, quality
=None):
376 self
.transcode(quality
=quality
)
379 class AudioProcessingManager(ProcessingManager
):
381 super(AudioProcessingManager
, self
).__init
__()
382 self
.add_processor(InitialProcessor
)
383 self
.add_processor(Resizer
)
384 self
.add_processor(Transcoder
)