Merge remote-tracking branch 'joar/audio+sniffing'
Conflicts: mediagoblin/media_types/image/processing.py mediagoblin/media_types/video/__init__.py mediagoblin/media_types/video/processing.py mediagoblin/tests/test_submission.py
This commit is contained in:
@@ -16,10 +16,13 @@
|
||||
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
import tempfile
|
||||
|
||||
from mediagoblin import mg_globals
|
||||
from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
class FileTypeNotSupported(Exception):
|
||||
pass
|
||||
@@ -28,6 +31,35 @@ class InvalidFileType(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def sniff_media(media):
|
||||
'''
|
||||
Iterate through the enabled media types and find those suited
|
||||
for a certain file.
|
||||
'''
|
||||
|
||||
try:
|
||||
return get_media_type_and_manager(media.filename)
|
||||
except FileTypeNotSupported:
|
||||
_log.info('No media handler found by file extension. Doing it the expensive way...')
|
||||
# Create a temporary file for sniffers suchs as GStreamer-based
|
||||
# Audio video
|
||||
media_file = tempfile.NamedTemporaryFile()
|
||||
media_file.write(media.file.read())
|
||||
media.file.seek(0)
|
||||
|
||||
for media_type, manager in get_media_managers():
|
||||
_log.info('Sniffing {0}'.format(media_type))
|
||||
if manager['sniff_handler'](media_file, media=media):
|
||||
_log.info('{0} accepts the file'.format(media_type))
|
||||
return media_type, manager
|
||||
else:
|
||||
_log.debug('{0} did not accept the file'.format(media_type))
|
||||
|
||||
raise FileTypeNotSupported(
|
||||
# TODO: Provide information on which file types are supported
|
||||
_(u'Sorry, I don\'t support that file type :('))
|
||||
|
||||
|
||||
def get_media_types():
|
||||
"""
|
||||
Generator, yields the available media types
|
||||
@@ -42,7 +74,7 @@ def get_media_managers():
|
||||
'''
|
||||
for media_type in get_media_types():
|
||||
__import__(media_type)
|
||||
|
||||
|
||||
yield media_type, sys.modules[media_type].MEDIA_MANAGER
|
||||
|
||||
|
||||
@@ -67,22 +99,22 @@ def get_media_manager(_media_type):
|
||||
|
||||
def get_media_type_and_manager(filename):
|
||||
'''
|
||||
Get the media type and manager based on a filename
|
||||
Try to find the media type based on the file name, extension
|
||||
specifically. This is used as a speedup, the sniffing functionality
|
||||
then falls back on more in-depth bitsniffing of the source file.
|
||||
'''
|
||||
if filename.find('.') > 0:
|
||||
# Get the file extension
|
||||
ext = os.path.splitext(filename)[1].lower()
|
||||
else:
|
||||
raise InvalidFileType(
|
||||
_(u'Could not extract any file extension from "{filename}"').format(
|
||||
filename=filename))
|
||||
|
||||
for media_type, manager in get_media_managers():
|
||||
# Omit the dot from the extension and match it against
|
||||
# the media manager
|
||||
if ext[1:] in manager['accepted_extensions']:
|
||||
return media_type, manager
|
||||
for media_type, manager in get_media_managers():
|
||||
# Omit the dot from the extension and match it against
|
||||
# the media manager
|
||||
if ext[1:] in manager['accepted_extensions']:
|
||||
return media_type, manager
|
||||
else:
|
||||
raise FileTypeNotSupported(
|
||||
# TODO: Provide information on which file types are supported
|
||||
_(u'Sorry, I don\'t support that file type :('))
|
||||
_log.info('File {0} has no file extension, let\'s hope the sniffers get it.'.format(
|
||||
filename))
|
||||
|
||||
raise FileTypeNotSupported(
|
||||
_(u'Sorry, I don\'t support that file type :('))
|
||||
|
||||
@@ -14,13 +14,15 @@
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from mediagoblin.media_types.ascii.processing import process_ascii
|
||||
from mediagoblin.media_types.ascii.processing import process_ascii, \
|
||||
sniff_handler
|
||||
|
||||
|
||||
MEDIA_MANAGER = {
|
||||
"human_readable": "ASCII",
|
||||
"processor": process_ascii, # alternately a string,
|
||||
# 'mediagoblin.media_types.image.processing'?
|
||||
"sniff_handler": sniff_handler,
|
||||
"display_template": "mediagoblin/media_displays/ascii.html",
|
||||
"default_thumb": "images/media_thumbs/ascii.jpg",
|
||||
"accepted_extensions": [
|
||||
|
||||
@@ -23,6 +23,7 @@ import os
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AsciiToImage(object):
|
||||
'''
|
||||
Converter of ASCII art into image files, preserving whitespace
|
||||
|
||||
@@ -19,11 +19,25 @@ import Image
|
||||
import logging
|
||||
|
||||
from mediagoblin import mg_globals as mgg
|
||||
from mediagoblin.processing import create_pub_filepath, THUMB_SIZE
|
||||
from mediagoblin.processing import create_pub_filepath
|
||||
from mediagoblin.media_types.ascii import asciitoimage
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
SUPPORTED_EXTENSIONS = ['txt', 'asc', 'nfo']
|
||||
|
||||
|
||||
def sniff_handler(media_file, **kw):
|
||||
if kw.get('media') is not None:
|
||||
name, ext = os.path.splitext(kw['media'].filename)
|
||||
clean_ext = ext[1:].lower()
|
||||
|
||||
if clean_ext in SUPPORTED_EXTENSIONS:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def process_ascii(entry):
|
||||
'''
|
||||
Code to process a txt file
|
||||
@@ -69,7 +83,10 @@ def process_ascii(entry):
|
||||
queued_file.read())
|
||||
|
||||
with file(tmp_thumb_filename, 'w') as thumb_file:
|
||||
thumb.thumbnail(THUMB_SIZE, Image.ANTIALIAS)
|
||||
thumb.thumbnail(
|
||||
(mgg.global_config['media:thumb']['max_width'],
|
||||
mgg.global_config['media:thumb']['max_height']),
|
||||
Image.ANTIALIAS)
|
||||
thumb.save(thumb_file)
|
||||
|
||||
_log.debug('Copying local file to public storage')
|
||||
@@ -84,7 +101,6 @@ def process_ascii(entry):
|
||||
as original_file:
|
||||
original_file.write(queued_file.read())
|
||||
|
||||
|
||||
queued_file.seek(0) # Rewind *again*
|
||||
|
||||
unicode_filepath = create_pub_filepath(entry, 'ascii-portable.txt')
|
||||
|
||||
25
mediagoblin/media_types/audio/__init__.py
Normal file
25
mediagoblin/media_types/audio/__init__.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# GNU MediaGoblin -- federated, autonomous media hosting
|
||||
# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from mediagoblin.media_types.audio.processing import process_audio, \
|
||||
sniff_handler
|
||||
|
||||
MEDIA_MANAGER = {
|
||||
'human_readable': 'Audio',
|
||||
'processor': process_audio,
|
||||
'sniff_handler': sniff_handler,
|
||||
'display_template': 'mediagoblin/media_displays/audio.html',
|
||||
'accepted_extensions': ['mp3', 'flac', 'ogg', 'wav', 'm4a']}
|
||||
1
mediagoblin/media_types/audio/audioprocessing.py
Symbolic link
1
mediagoblin/media_types/audio/audioprocessing.py
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../extlib/freesound/audioprocessing.py
|
||||
131
mediagoblin/media_types/audio/processing.py
Normal file
131
mediagoblin/media_types/audio/processing.py
Normal file
@@ -0,0 +1,131 @@
|
||||
# GNU MediaGoblin -- federated, autonomous media hosting
|
||||
# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
from mediagoblin import mg_globals as mgg
|
||||
from mediagoblin.processing import create_pub_filepath, BadMediaFail
|
||||
|
||||
from mediagoblin.media_types.audio.transcoders import AudioTranscoder, \
|
||||
AudioThumbnailer
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
def sniff_handler(media_file, **kw):
|
||||
try:
|
||||
transcoder = AudioTranscoder()
|
||||
data = transcoder.discover(media_file.name)
|
||||
except BadMediaFail:
|
||||
_log.debug('Audio discovery raised BadMediaFail')
|
||||
return False
|
||||
|
||||
if data.is_audio == True and data.is_video == False:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def process_audio(entry):
|
||||
audio_config = mgg.global_config['media_type:mediagoblin.media_types.audio']
|
||||
|
||||
workbench = mgg.workbench_manager.create_workbench()
|
||||
|
||||
queued_filepath = entry.queued_media_file
|
||||
queued_filename = workbench.localized_file(
|
||||
mgg.queue_store, queued_filepath,
|
||||
'source')
|
||||
|
||||
ogg_filepath = create_pub_filepath(
|
||||
entry,
|
||||
'{original}.webm'.format(
|
||||
original=os.path.splitext(
|
||||
queued_filepath[-1])[0]))
|
||||
|
||||
transcoder = AudioTranscoder()
|
||||
|
||||
with tempfile.NamedTemporaryFile() as ogg_tmp:
|
||||
|
||||
transcoder.transcode(
|
||||
queued_filename,
|
||||
ogg_tmp.name,
|
||||
quality=audio_config['quality'])
|
||||
|
||||
data = transcoder.discover(ogg_tmp.name)
|
||||
|
||||
_log.debug('Saving medium...')
|
||||
mgg.public_store.get_file(ogg_filepath, 'wb').write(
|
||||
ogg_tmp.read())
|
||||
|
||||
entry.media_files['ogg'] = ogg_filepath
|
||||
|
||||
entry.media_data['audio'] = {
|
||||
u'length': int(data.audiolength)}
|
||||
|
||||
if audio_config['create_spectrogram']:
|
||||
spectrogram_filepath = create_pub_filepath(
|
||||
entry,
|
||||
'{original}-spectrogram.jpg'.format(
|
||||
original=os.path.splitext(
|
||||
queued_filepath[-1])[0]))
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix='.wav') as wav_tmp:
|
||||
_log.info('Creating WAV source for spectrogram')
|
||||
transcoder.transcode(
|
||||
queued_filename,
|
||||
wav_tmp.name,
|
||||
mux_string='wavenc')
|
||||
|
||||
thumbnailer = AudioThumbnailer()
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix='.jpg') as spectrogram_tmp:
|
||||
thumbnailer.spectrogram(
|
||||
wav_tmp.name,
|
||||
spectrogram_tmp.name,
|
||||
width=mgg.global_config['media:medium']['max_width'])
|
||||
|
||||
_log.debug('Saving spectrogram...')
|
||||
mgg.public_store.get_file(spectrogram_filepath, 'wb').write(
|
||||
spectrogram_tmp.read())
|
||||
|
||||
entry.media_files['spectrogram'] = spectrogram_filepath
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix='.jpg') as thumb_tmp:
|
||||
thumbnailer.thumbnail_spectrogram(
|
||||
spectrogram_tmp.name,
|
||||
thumb_tmp.name,
|
||||
(mgg.global_config['media:thumb']['max_width'],
|
||||
mgg.global_config['media:thumb']['max_height']))
|
||||
|
||||
thumb_filepath = create_pub_filepath(
|
||||
entry,
|
||||
'{original}-thumbnail.jpg'.format(
|
||||
original=os.path.splitext(
|
||||
queued_filepath[-1])[0]))
|
||||
|
||||
mgg.public_store.get_file(thumb_filepath, 'wb').write(
|
||||
thumb_tmp.read())
|
||||
|
||||
entry.media_files['thumb'] = thumb_filepath
|
||||
else:
|
||||
entry.media_files['thumb'] = ['fake', 'thumb', 'path.jpg']
|
||||
|
||||
mgg.queue_store.delete_file(queued_filepath)
|
||||
|
||||
entry.save()
|
||||
|
||||
# clean up workbench
|
||||
workbench.destroy_self()
|
||||
237
mediagoblin/media_types/audio/transcoders.py
Normal file
237
mediagoblin/media_types/audio/transcoders.py
Normal file
@@ -0,0 +1,237 @@
|
||||
# GNU MediaGoblin -- federated, autonomous media hosting
|
||||
# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import pdb
|
||||
import logging
|
||||
from PIL import Image
|
||||
|
||||
from mediagoblin.processing import BadMediaFail
|
||||
from mediagoblin.media_types.audio import audioprocessing
|
||||
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
CPU_COUNT = 2 # Just assuming for now
|
||||
|
||||
# IMPORT MULTIPROCESSING
|
||||
try:
|
||||
import multiprocessing
|
||||
try:
|
||||
CPU_COUNT = multiprocessing.cpu_count()
|
||||
except NotImplementedError:
|
||||
_log.warning('multiprocessing.cpu_count not implemented!\n'
|
||||
'Assuming 2 CPU cores')
|
||||
except ImportError:
|
||||
_log.warning('Could not import multiprocessing, assuming 2 CPU cores')
|
||||
|
||||
# IMPORT GOBJECT
|
||||
try:
|
||||
import gobject
|
||||
gobject.threads_init()
|
||||
except ImportError:
|
||||
raise Exception('gobject could not be found')
|
||||
|
||||
# IMPORT PYGST
|
||||
try:
|
||||
import pygst
|
||||
|
||||
# We won't settle for less. For now, this is an arbitrary limit
|
||||
# as we have not tested with > 0.10
|
||||
pygst.require('0.10')
|
||||
|
||||
import gst
|
||||
|
||||
import gst.extend.discoverer
|
||||
except ImportError:
|
||||
raise Exception('gst/pygst >= 0.10 could not be imported')
|
||||
|
||||
import numpy
|
||||
|
||||
|
||||
class AudioThumbnailer(object):
|
||||
def __init__(self):
|
||||
_log.info('Initializing {0}'.format(self.__class__.__name__))
|
||||
|
||||
def spectrogram(self, src, dst, **kw):
|
||||
width = kw['width']
|
||||
height = int(kw.get('height', float(width) * 0.3))
|
||||
fft_size = kw.get('fft_size', 2048)
|
||||
callback = kw.get('progress_callback')
|
||||
|
||||
processor = audioprocessing.AudioProcessor(
|
||||
src,
|
||||
fft_size,
|
||||
numpy.hanning)
|
||||
|
||||
samples_per_pixel = processor.audio_file.nframes / float(width)
|
||||
|
||||
spectrogram = audioprocessing.SpectrogramImage(width, height, fft_size)
|
||||
|
||||
for x in range(width):
|
||||
if callback and x % (width / 10) == 0:
|
||||
callback((x * 100) / width)
|
||||
|
||||
seek_point = int(x * samples_per_pixel)
|
||||
|
||||
(spectral_centroid, db_spectrum) = processor.spectral_centroid(
|
||||
seek_point)
|
||||
|
||||
spectrogram.draw_spectrum(x, db_spectrum)
|
||||
|
||||
if callback:
|
||||
callback(100)
|
||||
|
||||
spectrogram.save(dst)
|
||||
|
||||
def thumbnail_spectrogram(self, src, dst, thumb_size):
|
||||
'''
|
||||
Takes a spectrogram and creates a thumbnail from it
|
||||
'''
|
||||
if not (type(thumb_size) == tuple and len(thumb_size) == 2):
|
||||
raise Exception('thumb_size argument should be a tuple(width, height)')
|
||||
|
||||
im = Image.open(src)
|
||||
|
||||
im_w, im_h = [float(i) for i in im.size]
|
||||
th_w, th_h = [float(i) for i in thumb_size]
|
||||
|
||||
wadsworth_position = im_w * 0.3
|
||||
|
||||
start_x = max((
|
||||
wadsworth_position - ((im_h * (th_w / th_h)) / 2.0),
|
||||
0.0))
|
||||
|
||||
stop_x = start_x + (im_h * (th_w / th_h))
|
||||
|
||||
th = im.crop((
|
||||
int(start_x), 0,
|
||||
int(stop_x), int(im_h)))
|
||||
|
||||
if th.size[0] > th_w or th.size[1] > th_h:
|
||||
th.thumbnail(thumb_size, Image.ANTIALIAS)
|
||||
|
||||
th.save(dst)
|
||||
|
||||
|
||||
class AudioTranscoder(object):
|
||||
def __init__(self):
|
||||
_log.info('Initializing {0}'.format(self.__class__.__name__))
|
||||
|
||||
# Instantiate MainLoop
|
||||
self._loop = gobject.MainLoop()
|
||||
self._failed = None
|
||||
|
||||
def discover(self, src):
|
||||
self._src_path = src
|
||||
_log.info('Discovering {0}'.format(src))
|
||||
self._discovery_path = src
|
||||
|
||||
self._discoverer = gst.extend.discoverer.Discoverer(
|
||||
self._discovery_path)
|
||||
self._discoverer.connect('discovered', self.__on_discovered)
|
||||
self._discoverer.discover()
|
||||
|
||||
self._loop.run() # Run MainLoop
|
||||
|
||||
if self._failed:
|
||||
raise self._failed
|
||||
|
||||
# Once MainLoop has returned, return discovery data
|
||||
return getattr(self, '_discovery_data', False)
|
||||
|
||||
def __on_discovered(self, data, is_media):
|
||||
if not is_media:
|
||||
self._failed = BadMediaFail()
|
||||
_log.error('Could not discover {0}'.format(self._src_path))
|
||||
self.halt()
|
||||
|
||||
_log.debug('Discovered: {0}'.format(data.__dict__))
|
||||
|
||||
self._discovery_data = data
|
||||
|
||||
# Gracefully shut down MainLoop
|
||||
self.halt()
|
||||
|
||||
def transcode(self, src, dst, **kw):
|
||||
_log.info('Transcoding {0} into {1}'.format(src, dst))
|
||||
self._discovery_data = kw.get('data', self.discover(src))
|
||||
|
||||
self.__on_progress = kw.get('progress_callback')
|
||||
|
||||
quality = kw.get('quality', 0.3)
|
||||
|
||||
mux_string = kw.get(
|
||||
'mux_string',
|
||||
'vorbisenc quality={0} ! webmmux'.format(quality))
|
||||
|
||||
# Set up pipeline
|
||||
self.pipeline = gst.parse_launch(
|
||||
'filesrc location="{src}" ! '
|
||||
'decodebin2 ! queue ! audiorate tolerance={tolerance} ! '
|
||||
'audioconvert ! audio/x-raw-float,channels=2 ! '
|
||||
'{mux_string} ! '
|
||||
'progressreport silent=true ! '
|
||||
'filesink location="{dst}"'.format(
|
||||
src=src,
|
||||
tolerance=80000000,
|
||||
mux_string=mux_string,
|
||||
dst=dst))
|
||||
|
||||
self.bus = self.pipeline.get_bus()
|
||||
self.bus.add_signal_watch()
|
||||
self.bus.connect('message', self.__on_bus_message)
|
||||
|
||||
self.pipeline.set_state(gst.STATE_PLAYING)
|
||||
|
||||
self._loop.run()
|
||||
|
||||
def __on_bus_message(self, bus, message):
|
||||
_log.debug(message)
|
||||
|
||||
if (message.type == gst.MESSAGE_ELEMENT
|
||||
and message.structure.get_name() == 'progress'):
|
||||
data = dict(message.structure)
|
||||
|
||||
if self.__on_progress:
|
||||
self.__on_progress(data)
|
||||
|
||||
_log.info('{0}% done...'.format(
|
||||
data.get('percent')))
|
||||
elif message.type == gst.MESSAGE_EOS:
|
||||
_log.info('Done')
|
||||
self.halt()
|
||||
|
||||
def halt(self):
|
||||
if getattr(self, 'pipeline', False):
|
||||
self.pipeline.set_state(gst.STATE_NULL)
|
||||
del self.pipeline
|
||||
_log.info('Quitting MainLoop gracefully...')
|
||||
gobject.idle_add(self._loop.quit)
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
logging.basicConfig()
|
||||
_log.setLevel(logging.INFO)
|
||||
|
||||
#transcoder = AudioTranscoder()
|
||||
#data = transcoder.discover(sys.argv[1])
|
||||
#res = transcoder.transcode(*sys.argv[1:3])
|
||||
|
||||
thumbnailer = AudioThumbnailer()
|
||||
|
||||
thumbnailer.spectrogram(*sys.argv[1:], width=640)
|
||||
|
||||
pdb.set_trace()
|
||||
@@ -14,13 +14,15 @@
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from mediagoblin.media_types.image.processing import process_image
|
||||
from mediagoblin.media_types.image.processing import process_image, \
|
||||
sniff_handler
|
||||
|
||||
|
||||
MEDIA_MANAGER = {
|
||||
"human_readable": "Image",
|
||||
"processor": process_image, # alternately a string,
|
||||
# 'mediagoblin.media_types.image.processing'?
|
||||
"sniff_handler": sniff_handler,
|
||||
"display_template": "mediagoblin/media_displays/image.html",
|
||||
"default_thumb": "images/media_thumbs/image.jpg",
|
||||
"accepted_extensions": ["jpg", "jpeg", "png", "gif", "tiff"]}
|
||||
|
||||
@@ -16,14 +16,18 @@
|
||||
|
||||
import Image
|
||||
import os
|
||||
import logging
|
||||
|
||||
from mediagoblin import mg_globals as mgg
|
||||
from mediagoblin.processing import BadMediaFail, \
|
||||
create_pub_filepath, THUMB_SIZE, MEDIUM_SIZE, FilenameBuilder
|
||||
create_pub_filepath, FilenameBuilder
|
||||
from mediagoblin.tools.exif import exif_fix_image_orientation, \
|
||||
extract_exif, clean_exif, get_gps_data, get_useful, \
|
||||
exif_image_needs_rotation
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def resize_image(entry, filename, new_path, exif_tags, workdir, new_size,
|
||||
size_limits=(0, 0)):
|
||||
"""Store a resized version of an image and return its pathname.
|
||||
@@ -35,18 +39,13 @@ def resize_image(entry, filename, new_path, exif_tags, workdir, new_size,
|
||||
exif_tags -- EXIF data for the original image
|
||||
workdir -- directory path for storing converted image files
|
||||
new_size -- 2-tuple size for the resized image
|
||||
size_limits (optional) -- image is only resized if it exceeds this size
|
||||
|
||||
"""
|
||||
try:
|
||||
resized = Image.open(filename)
|
||||
except IOError:
|
||||
raise BadMediaFail()
|
||||
resized = exif_fix_image_orientation(resized, exif_tags) # Fix orientation
|
||||
|
||||
if ((resized.size[0] > size_limits[0]) or
|
||||
(resized.size[1] > size_limits[1])):
|
||||
resized.thumbnail(new_size, Image.ANTIALIAS)
|
||||
resized.thumbnail(new_size, Image.ANTIALIAS)
|
||||
|
||||
# Copy the new file to the conversion subdir, then remotely.
|
||||
tmp_resized_filename = os.path.join(workdir, new_path[-1])
|
||||
@@ -54,6 +53,33 @@ def resize_image(entry, filename, new_path, exif_tags, workdir, new_size,
|
||||
resized.save(resized_file)
|
||||
mgg.public_store.copy_local_to_storage(tmp_resized_filename, new_path)
|
||||
|
||||
|
||||
SUPPORTED_FILETYPES = ['png', 'gif', 'jpg', 'jpeg']
|
||||
|
||||
|
||||
def sniff_handler(media_file, **kw):
|
||||
if kw.get('media') is not None: # That's a double negative!
|
||||
name, ext = os.path.splitext(kw['media'].filename)
|
||||
clean_ext = ext[1:].lower() # Strip the . from ext and make lowercase
|
||||
|
||||
_log.debug('name: {0}\next: {1}\nlower_ext: {2}'.format(
|
||||
name,
|
||||
ext,
|
||||
clean_ext))
|
||||
|
||||
if clean_ext in SUPPORTED_FILETYPES:
|
||||
_log.info('Found file extension in supported filetypes')
|
||||
return True
|
||||
else:
|
||||
_log.debug('Media present, extension not found in {0}'.format(
|
||||
SUPPORTED_FILETYPES))
|
||||
else:
|
||||
_log.warning('Need additional information (keyword argument \'media\')'
|
||||
' to be able to handle sniffing')
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def process_image(entry):
|
||||
"""
|
||||
Code to process an image
|
||||
@@ -77,19 +103,24 @@ def process_image(entry):
|
||||
thumb_filepath = create_pub_filepath(
|
||||
entry, name_builder.fill('{basename}.thumbnail{ext}'))
|
||||
resize_image(entry, queued_filename, thumb_filepath,
|
||||
exif_tags, conversions_subdir, THUMB_SIZE)
|
||||
exif_tags, conversions_subdir,
|
||||
(mgg.global_config['media:thumb']['max_width'],
|
||||
mgg.global_config['media:thumb']['max_height']))
|
||||
|
||||
# If the size of the original file exceeds the specified size of a `medium`
|
||||
# file, a `.medium.jpg` files is created and later associated with the media
|
||||
# entry.
|
||||
medium = Image.open(queued_filename)
|
||||
if medium.size[0] > MEDIUM_SIZE[0] or medium.size[1] > MEDIUM_SIZE[1] \
|
||||
or exif_image_needs_rotation(exif_tags):
|
||||
if medium.size[0] > mgg.global_config['media:medium']['max_width'] \
|
||||
or medium.size[1] > mgg.global_config['media:medium']['max_height'] \
|
||||
or exif_image_needs_rotation(exif_tags):
|
||||
medium_filepath = create_pub_filepath(
|
||||
entry, name_builder.fill('{basename}.medium{ext}'))
|
||||
resize_image(
|
||||
entry, queued_filename, medium_filepath,
|
||||
exif_tags, conversions_subdir, MEDIUM_SIZE, MEDIUM_SIZE)
|
||||
exif_tags, conversions_subdir,
|
||||
(mgg.global_config['media:medium']['max_width'],
|
||||
mgg.global_config['media:medium']['max_height']))
|
||||
else:
|
||||
medium_filepath = None
|
||||
|
||||
@@ -99,7 +130,7 @@ def process_image(entry):
|
||||
|
||||
with queued_file:
|
||||
original_filepath = create_pub_filepath(
|
||||
entry, name_builder.fill('{basename}{ext}') )
|
||||
entry, name_builder.fill('{basename}{ext}'))
|
||||
|
||||
with mgg.public_store.get_file(original_filepath, 'wb') \
|
||||
as original_file:
|
||||
|
||||
@@ -14,16 +14,16 @@
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from mediagoblin.media_types.video.processing import process_video
|
||||
from mediagoblin.media_types.video.processing import process_video, \
|
||||
sniff_handler
|
||||
|
||||
|
||||
MEDIA_MANAGER = {
|
||||
"human_readable": "Video",
|
||||
"processor": process_video, # alternately a string,
|
||||
# 'mediagoblin.media_types.image.processing'?
|
||||
"processor": process_video, # alternately a string,
|
||||
# 'mediagoblin.media_types.image.processing'?
|
||||
"sniff_handler": sniff_handler,
|
||||
"display_template": "mediagoblin/media_displays/video.html",
|
||||
"default_thumb": "images/media_thumbs/video.jpg",
|
||||
# TODO: This list should be autogenerated based on gst plugins
|
||||
"accepted_extensions": [
|
||||
"mp4", "mov", "webm", "avi", "3gp", "3gpp", "mkv", "ogv", "ogg",
|
||||
"m4v"]}
|
||||
"mp4", "mov", "webm", "avi", "3gp", "3gpp", "mkv", "ogv", "m4v"]}
|
||||
|
||||
@@ -20,7 +20,7 @@ import os
|
||||
|
||||
from mediagoblin import mg_globals as mgg
|
||||
from mediagoblin.processing import mark_entry_failed, \
|
||||
THUMB_SIZE, MEDIUM_SIZE, create_pub_filepath, FilenameBuilder
|
||||
create_pub_filepath, FilenameBuilder
|
||||
from . import transcoders
|
||||
|
||||
logging.basicConfig()
|
||||
@@ -29,17 +29,27 @@ _log = logging.getLogger(__name__)
|
||||
_log.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
def sniff_handler(media_file, **kw):
|
||||
transcoder = transcoders.VideoTranscoder()
|
||||
data = transcoder.discover(media_file.name)
|
||||
|
||||
_log.debug('Discovered: {0}'.format(data))
|
||||
|
||||
if not data:
|
||||
_log.error('Could not discover {0}'.format(
|
||||
kw.get('media')))
|
||||
return False
|
||||
|
||||
if data['is_video'] == True:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def process_video(entry):
|
||||
"""
|
||||
Code to process a video
|
||||
|
||||
Much of this code is derived from the arista-transcoder script in
|
||||
the arista PyPI package and changed to match the needs of
|
||||
MediaGoblin
|
||||
|
||||
This function sets up the arista video encoder in some kind of new thread
|
||||
and attaches callbacks to that child process, hopefully, the
|
||||
entry-complete callback will be called when the video is done.
|
||||
Process a video entry, transcode the queued media files (originals) and
|
||||
create a thumbnail for the entry.
|
||||
"""
|
||||
video_config = mgg.global_config['media_type:mediagoblin.media_types.video']
|
||||
|
||||
@@ -62,7 +72,8 @@ def process_video(entry):
|
||||
|
||||
with tmp_dst:
|
||||
# Transcode queued file to a VP8/vorbis file that fits in a 640x640 square
|
||||
transcoder = transcoders.VideoTranscoder(queued_filename, tmp_dst.name)
|
||||
transcoder = transcoders.VideoTranscoder()
|
||||
transcoder.transcode(queued_filename, tmp_dst.name)
|
||||
|
||||
# Push transcoded video to public storage
|
||||
_log.debug('Saving medium...')
|
||||
|
||||
@@ -21,12 +21,9 @@ os.putenv('GST_DEBUG_DUMP_DOT_DIR', '/tmp')
|
||||
|
||||
import sys
|
||||
import logging
|
||||
import pdb
|
||||
import urllib
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
logging.basicConfig()
|
||||
_log.setLevel(logging.DEBUG)
|
||||
|
||||
CPU_COUNT = 2
|
||||
try:
|
||||
@@ -38,17 +35,16 @@ try:
|
||||
pass
|
||||
except ImportError:
|
||||
_log.warning('Could not import multiprocessing, defaulting to 2 CPU cores')
|
||||
pass
|
||||
|
||||
try:
|
||||
import gtk
|
||||
except:
|
||||
except ImportError:
|
||||
raise Exception('Could not find pygtk')
|
||||
|
||||
try:
|
||||
import gobject
|
||||
gobject.threads_init()
|
||||
except:
|
||||
except ImportError:
|
||||
raise Exception('gobject could not be found')
|
||||
|
||||
try:
|
||||
@@ -56,7 +52,7 @@ try:
|
||||
pygst.require('0.10')
|
||||
import gst
|
||||
from gst.extend import discoverer
|
||||
except:
|
||||
except ImportError:
|
||||
raise Exception('gst/pygst 0.10 could not be found')
|
||||
|
||||
|
||||
@@ -270,7 +266,7 @@ class VideoThumbnailer:
|
||||
return 0
|
||||
|
||||
try:
|
||||
return pipeline.query_duration(gst.FORMAT_TIME)[0]
|
||||
return pipeline.query_duration(gst.FORMAT_TIME)[0]
|
||||
except gst.QueryError:
|
||||
return self._get_duration(pipeline, retries + 1)
|
||||
|
||||
@@ -320,12 +316,11 @@ class VideoThumbnailer:
|
||||
self.bus.disconnect(self.watch_id)
|
||||
self.bus = None
|
||||
|
||||
|
||||
def __halt_final(self):
|
||||
_log.info('Done')
|
||||
if self.errors:
|
||||
_log.error(','.join(self.errors))
|
||||
|
||||
|
||||
self.loop.quit()
|
||||
|
||||
|
||||
@@ -341,10 +336,15 @@ class VideoTranscoder:
|
||||
that it was refined afterwards and therefore is done more
|
||||
correctly.
|
||||
'''
|
||||
def __init__(self, src, dst, **kwargs):
|
||||
def __init__(self):
|
||||
_log.info('Initializing VideoTranscoder...')
|
||||
|
||||
self.loop = gobject.MainLoop()
|
||||
|
||||
def transcode(self, src, dst, **kwargs):
|
||||
'''
|
||||
Transcode a video file into a 'medium'-sized version.
|
||||
'''
|
||||
self.source_path = src
|
||||
self.destination_path = dst
|
||||
|
||||
@@ -358,6 +358,34 @@ class VideoTranscoder:
|
||||
self._setup()
|
||||
self._run()
|
||||
|
||||
def discover(self, src):
|
||||
'''
|
||||
Discover properties about a media file
|
||||
'''
|
||||
_log.info('Discovering {0}'.format(src))
|
||||
|
||||
self.source_path = src
|
||||
self._setup_discover(discovered_callback=self.__on_discovered)
|
||||
|
||||
self.discoverer.discover()
|
||||
|
||||
self.loop.run()
|
||||
|
||||
if hasattr(self, '_discovered_data'):
|
||||
return self._discovered_data.__dict__
|
||||
else:
|
||||
return None
|
||||
|
||||
def __on_discovered(self, data, is_media):
|
||||
_log.debug('Discovered: {0}'.format(data))
|
||||
if not is_media:
|
||||
self.__stop()
|
||||
raise Exception('Could not discover {0}'.format(self.source_path))
|
||||
|
||||
self._discovered_data = data
|
||||
|
||||
self.__stop_mainloop()
|
||||
|
||||
def _setup(self):
|
||||
self._setup_discover()
|
||||
self._setup_pipeline()
|
||||
@@ -370,12 +398,14 @@ class VideoTranscoder:
|
||||
_log.debug('Initializing MainLoop()')
|
||||
self.loop.run()
|
||||
|
||||
def _setup_discover(self):
|
||||
def _setup_discover(self, **kw):
|
||||
_log.debug('Setting up discoverer')
|
||||
self.discoverer = discoverer.Discoverer(self.source_path)
|
||||
|
||||
# Connect self.__discovered to the 'discovered' event
|
||||
self.discoverer.connect('discovered', self.__discovered)
|
||||
self.discoverer.connect(
|
||||
'discovered',
|
||||
kw.get('discovered_callback', self.__discovered))
|
||||
|
||||
def __discovered(self, data, is_media):
|
||||
'''
|
||||
@@ -422,7 +452,7 @@ class VideoTranscoder:
|
||||
self.ffmpegcolorspace = gst.element_factory_make(
|
||||
'ffmpegcolorspace', 'ffmpegcolorspace')
|
||||
self.pipeline.add(self.ffmpegcolorspace)
|
||||
|
||||
|
||||
self.videoscale = gst.element_factory_make('ffvideoscale', 'videoscale')
|
||||
#self.videoscale.set_property('method', 2) # I'm not sure this works
|
||||
#self.videoscale.set_property('add-borders', 0)
|
||||
@@ -516,7 +546,6 @@ class VideoTranscoder:
|
||||
# Setup the message bus and connect _on_message to the pipeline
|
||||
self._setup_bus()
|
||||
|
||||
|
||||
def _on_dynamic_pad(self, dbin, pad, islast):
|
||||
'''
|
||||
Callback called when ``decodebin2`` has a pad that we can connect to
|
||||
@@ -561,11 +590,11 @@ class VideoTranscoder:
|
||||
|
||||
t = message.type
|
||||
|
||||
if t == gst.MESSAGE_EOS:
|
||||
if message.type == gst.MESSAGE_EOS:
|
||||
self._discover_dst_and_stop()
|
||||
_log.info('Done')
|
||||
|
||||
elif t == gst.MESSAGE_ELEMENT:
|
||||
elif message.type == gst.MESSAGE_ELEMENT:
|
||||
if message.structure.get_name() == 'progress':
|
||||
data = dict(message.structure)
|
||||
|
||||
@@ -587,7 +616,6 @@ class VideoTranscoder:
|
||||
|
||||
self.dst_discoverer.discover()
|
||||
|
||||
|
||||
def __dst_discovered(self, data, is_media):
|
||||
self.dst_data = data
|
||||
|
||||
@@ -596,8 +624,9 @@ class VideoTranscoder:
|
||||
def __stop(self):
|
||||
_log.debug(self.loop)
|
||||
|
||||
# Stop executing the pipeline
|
||||
self.pipeline.set_state(gst.STATE_NULL)
|
||||
if hasattr(self, 'pipeline'):
|
||||
# Stop executing the pipeline
|
||||
self.pipeline.set_state(gst.STATE_NULL)
|
||||
|
||||
# This kills the loop, mercifully
|
||||
gobject.idle_add(self.__stop_mainloop)
|
||||
@@ -615,14 +644,15 @@ class VideoTranscoder:
|
||||
|
||||
if __name__ == '__main__':
|
||||
os.nice(19)
|
||||
logging.basicConfig()
|
||||
from optparse import OptionParser
|
||||
|
||||
parser = OptionParser(
|
||||
usage='%prog [-v] -a [ video | thumbnail ] SRC DEST')
|
||||
usage='%prog [-v] -a [ video | thumbnail | discover ] SRC [ DEST ]')
|
||||
|
||||
parser.add_option('-a', '--action',
|
||||
dest='action',
|
||||
help='One of "video" or "thumbnail"')
|
||||
help='One of "video", "discover" or "thumbnail"')
|
||||
|
||||
parser.add_option('-v',
|
||||
dest='verbose',
|
||||
@@ -646,13 +676,17 @@ if __name__ == '__main__':
|
||||
|
||||
_log.debug(args)
|
||||
|
||||
if not len(args) == 2:
|
||||
if not len(args) == 2 and not options.action == 'discover':
|
||||
parser.print_help()
|
||||
sys.exit()
|
||||
|
||||
transcoder = VideoTranscoder()
|
||||
|
||||
if options.action == 'thumbnail':
|
||||
VideoThumbnailer(*args)
|
||||
elif options.action == 'video':
|
||||
def cb(data):
|
||||
print('I\'m a callback!')
|
||||
transcoder = VideoTranscoder(*args, progress_callback=cb)
|
||||
transcoder.transcode(*args, progress_callback=cb)
|
||||
elif options.action == 'discover':
|
||||
print transcoder.discover(*args).__dict__
|
||||
|
||||
Reference in New Issue
Block a user