|
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}) |