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