diff -r 000000000000 -r fd39db613f8b src/pyams_media/media.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_media/media.py Wed Sep 02 15:31:55 2015 +0200 @@ -0,0 +1,160 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library +from mimetypes import guess_extension, guess_type + +# import interfaces +from pyams_file.interfaces import IFile +from pyams_media.interfaces import IMediaInfo, CUSTOM_AUDIO_TYPES, CUSTOM_VIDEO_TYPES, IMediaConversions, \ + IMediaConversion, IMediaConversionUtility +from transaction.interfaces import ITransactionManager +from zope.annotation.interfaces import IAnnotations +from zope.lifecycleevent.interfaces import IObjectAddedEvent + +# import packages +from pyams_file.file import FileFactory +from pyams_media.ffbase import FFmpeg +from pyams_media.ffdocument import FFDocument +from pyams_utils.adapter import adapter_config, ContextAdapter +from pyams_utils.registry import query_utility +from pyramid.events import subscriber +from pyramid.threadlocal import get_current_registry +from zope.container.folder import Folder +from zope.interface import implementer, alsoProvides +from zope.lifecycleevent import ObjectCreatedEvent +from zope.location import locate +from zope.traversing.interfaces import ITraversable + + +# +# Media infos +# + +MEDIA_INFO_KEY = 'pyams_media.media.info' + + +@adapter_config(context=IFile, provides=IMediaInfo) +def MediaInfoFactory(context): + """Media info adapter""" + if not (context.content_type.startswith(b'audio/') or + context.content_type.startswith(b'video/') or + context.content_type in (CUSTOM_AUDIO_TYPES + CUSTOM_VIDEO_TYPES)): + return None + annotations = IAnnotations(context) + info = annotations.get(MEDIA_INFO_KEY) + if info is None: + info = annotations[MEDIA_INFO_KEY] = FFmpeg('ffprobe').info(context) + return info + + +# +# Media conversions +# + +MEDIA_CONVERSIONS_KEY = 'pyams_media.media.conversions' + + +@implementer(IMediaConversions) +class MediaConversions(Folder): + """Media conversions""" + + def add_conversion(self, conversion, format, extension=None, width=None): + target = FileFactory(conversion) + registry = get_current_registry() + registry.notify(ObjectCreatedEvent(target)) + alsoProvides(target, IMediaConversion) + if extension is None: + extension = guess_extension(format) + target_name = '{name}{width}.{extension}'.format(name=target.content_type.decode().split('/', 1)[0] + if target.content_type else 'media', + width='-{0}'.format(width) if width else '', + extension=extension) + self[target_name] = target + + def get_conversions(self): + context = self.__parent__ + return [context] + list(self.values()) + + def get_conversion(self, name): + if '/' in name: + for conversion in self.get_conversions(): + if conversion.content_type == name: + return conversion + return self.get(name) + + def has_conversion(self, formats): + for conversion in self.get_conversions(): + if conversion.content_type in formats: + return True + return False + + +@adapter_config(context=IFile, provides=IMediaConversions) +def MediaConversionsFactory(context): + """Media conversions factory""" + annotations = IAnnotations(context) + conversions = annotations.get(MEDIA_CONVERSIONS_KEY) + if conversions is None: + conversions = annotations[MEDIA_CONVERSIONS_KEY] = MediaConversions() + locate(conversions, context, '++conversions++') + return conversions + + +@adapter_config(name='conversions', context=IFile, provides=ITraversable) +class MediaConversionsTraverser(ContextAdapter): + """++conversions++ file traverser""" + + def traverse(self, name, furtherpath=None): + return IMediaConversions(self.context) + + +# +# Media files events +# + +def check_media_conversion(status, media): + if not status: # aborted transaction + return + converter = query_utility(IMediaConversionUtility) + if converter is not None: + converter.check_media_conversion(media) + + +@subscriber(IObjectAddedEvent, context_selector=IFile) +def handle_added_media(event): + """Handle added media file""" + media = event.object + # Don't convert images or already converted files! + if IMediaConversion.providedBy(media): + return + content_type = media.content_type.decode() if media.content_type else None + if (not content_type) or content_type.startswith('image/'): + return + # Try to use FFMpeg if content type is unknown... + media_type = content_type.startswith('audio/') or \ + content_type.startswith('video/') or \ + content_type in (CUSTOM_AUDIO_TYPES + CUSTOM_VIDEO_TYPES) + if not media_type: + document = FFDocument(media) + metadata = document.__metadata__ + media_type = metadata.get('vtype') + if media_type: + ext = media_type.split('.')[0] + content_type = guess_type('media.{0}'.format(ext))[0] + if content_type is not None: + media.content_type = content_type + if media_type: + ITransactionManager(media).get().addAfterCommitHook(check_media_conversion, kws={'media': media})