src/pyams_media/media.py
changeset 0 fd39db613f8b
child 6 b78dab193043
equal deleted inserted replaced
-1:000000000000 0:fd39db613f8b
       
     1 #
       
     2 # Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
       
     3 # All Rights Reserved.
       
     4 #
       
     5 # This software is subject to the provisions of the Zope Public License,
       
     6 # Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
       
     7 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
       
     8 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
       
     9 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
       
    10 # FOR A PARTICULAR PURPOSE.
       
    11 #
       
    12 
       
    13 __docformat__ = 'restructuredtext'
       
    14 
       
    15 
       
    16 # import standard library
       
    17 from mimetypes import guess_extension, guess_type
       
    18 
       
    19 # import interfaces
       
    20 from pyams_file.interfaces import IFile
       
    21 from pyams_media.interfaces import IMediaInfo, CUSTOM_AUDIO_TYPES, CUSTOM_VIDEO_TYPES, IMediaConversions, \
       
    22     IMediaConversion, IMediaConversionUtility
       
    23 from transaction.interfaces import ITransactionManager
       
    24 from zope.annotation.interfaces import IAnnotations
       
    25 from zope.lifecycleevent.interfaces import IObjectAddedEvent
       
    26 
       
    27 # import packages
       
    28 from pyams_file.file import FileFactory
       
    29 from pyams_media.ffbase import FFmpeg
       
    30 from pyams_media.ffdocument import FFDocument
       
    31 from pyams_utils.adapter import adapter_config, ContextAdapter
       
    32 from pyams_utils.registry import query_utility
       
    33 from pyramid.events import subscriber
       
    34 from pyramid.threadlocal import get_current_registry
       
    35 from zope.container.folder import Folder
       
    36 from zope.interface import implementer, alsoProvides
       
    37 from zope.lifecycleevent import ObjectCreatedEvent
       
    38 from zope.location import locate
       
    39 from zope.traversing.interfaces import ITraversable
       
    40 
       
    41 
       
    42 #
       
    43 # Media infos
       
    44 #
       
    45 
       
    46 MEDIA_INFO_KEY = 'pyams_media.media.info'
       
    47 
       
    48 
       
    49 @adapter_config(context=IFile, provides=IMediaInfo)
       
    50 def MediaInfoFactory(context):
       
    51     """Media info adapter"""
       
    52     if not (context.content_type.startswith(b'audio/') or
       
    53             context.content_type.startswith(b'video/') or
       
    54             context.content_type in (CUSTOM_AUDIO_TYPES + CUSTOM_VIDEO_TYPES)):
       
    55         return None
       
    56     annotations = IAnnotations(context)
       
    57     info = annotations.get(MEDIA_INFO_KEY)
       
    58     if info is None:
       
    59         info = annotations[MEDIA_INFO_KEY] = FFmpeg('ffprobe').info(context)
       
    60     return info
       
    61 
       
    62 
       
    63 #
       
    64 # Media conversions
       
    65 #
       
    66 
       
    67 MEDIA_CONVERSIONS_KEY = 'pyams_media.media.conversions'
       
    68 
       
    69 
       
    70 @implementer(IMediaConversions)
       
    71 class MediaConversions(Folder):
       
    72     """Media conversions"""
       
    73 
       
    74     def add_conversion(self, conversion, format, extension=None, width=None):
       
    75         target = FileFactory(conversion)
       
    76         registry = get_current_registry()
       
    77         registry.notify(ObjectCreatedEvent(target))
       
    78         alsoProvides(target, IMediaConversion)
       
    79         if extension is None:
       
    80             extension = guess_extension(format)
       
    81         target_name = '{name}{width}.{extension}'.format(name=target.content_type.decode().split('/', 1)[0]
       
    82                                                              if target.content_type else 'media',
       
    83                                                          width='-{0}'.format(width) if width else '',
       
    84                                                          extension=extension)
       
    85         self[target_name] = target
       
    86 
       
    87     def get_conversions(self):
       
    88         context = self.__parent__
       
    89         return [context] + list(self.values())
       
    90 
       
    91     def get_conversion(self, name):
       
    92         if '/' in name:
       
    93             for conversion in self.get_conversions():
       
    94                 if conversion.content_type == name:
       
    95                     return conversion
       
    96         return self.get(name)
       
    97 
       
    98     def has_conversion(self, formats):
       
    99         for conversion in self.get_conversions():
       
   100             if conversion.content_type in formats:
       
   101                 return True
       
   102         return False
       
   103 
       
   104 
       
   105 @adapter_config(context=IFile, provides=IMediaConversions)
       
   106 def MediaConversionsFactory(context):
       
   107     """Media conversions factory"""
       
   108     annotations = IAnnotations(context)
       
   109     conversions = annotations.get(MEDIA_CONVERSIONS_KEY)
       
   110     if conversions is None:
       
   111         conversions = annotations[MEDIA_CONVERSIONS_KEY] = MediaConversions()
       
   112         locate(conversions, context, '++conversions++')
       
   113     return conversions
       
   114 
       
   115 
       
   116 @adapter_config(name='conversions', context=IFile, provides=ITraversable)
       
   117 class MediaConversionsTraverser(ContextAdapter):
       
   118     """++conversions++ file traverser"""
       
   119 
       
   120     def traverse(self, name, furtherpath=None):
       
   121         return IMediaConversions(self.context)
       
   122 
       
   123 
       
   124 #
       
   125 # Media files events
       
   126 #
       
   127 
       
   128 def check_media_conversion(status, media):
       
   129     if not status:  # aborted transaction
       
   130         return
       
   131     converter = query_utility(IMediaConversionUtility)
       
   132     if converter is not None:
       
   133         converter.check_media_conversion(media)
       
   134 
       
   135 
       
   136 @subscriber(IObjectAddedEvent, context_selector=IFile)
       
   137 def handle_added_media(event):
       
   138     """Handle added media file"""
       
   139     media = event.object
       
   140     # Don't convert images or already converted files!
       
   141     if IMediaConversion.providedBy(media):
       
   142         return
       
   143     content_type = media.content_type.decode() if media.content_type else None
       
   144     if (not content_type) or content_type.startswith('image/'):
       
   145         return
       
   146     # Try to use FFMpeg if content type is unknown...
       
   147     media_type = content_type.startswith('audio/') or \
       
   148                  content_type.startswith('video/') or \
       
   149                  content_type in (CUSTOM_AUDIO_TYPES + CUSTOM_VIDEO_TYPES)
       
   150     if not media_type:
       
   151         document = FFDocument(media)
       
   152         metadata = document.__metadata__
       
   153         media_type = metadata.get('vtype')
       
   154         if media_type:
       
   155             ext = media_type.split('.')[0]
       
   156             content_type = guess_type('media.{0}'.format(ext))[0]
       
   157             if content_type is not None:
       
   158                 media.content_type = content_type
       
   159     if media_type:
       
   160         ITransactionManager(media).get().addAfterCommitHook(check_media_conversion, kws={'media': media})