Merge branch 'jwandborg-f298_create_envball'
[mediagoblin.git] / mediagoblin / process_media / __init__.py
1 # GNU MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2011 Free Software Foundation, Inc
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
17 import Image
18
19 from contextlib import contextmanager
20 from celery.task import Task
21 from celery import registry
22
23 from mediagoblin.db.util import ObjectId
24 from mediagoblin import mg_globals as mgg
25 from mediagoblin.process_media.errors import BaseProcessingFail, BadMediaFail
26
27
28 THUMB_SIZE = 180, 180
29 MEDIUM_SIZE = 640, 640
30
31
32 def create_pub_filepath(entry, filename):
33 return mgg.public_store.get_unique_filepath(
34 ['media_entries',
35 unicode(entry['_id']),
36 filename])
37
38
39 @contextmanager
40 def closing(callback):
41 try:
42 yield callback
43 finally:
44 pass
45
46
47 ################################
48 # Media processing initial steps
49 ################################
50
51 class ProcessMedia(Task):
52 """
53 Pass this entry off for processing.
54 """
55 def run(self, media_id):
56 """
57 Pass the media entry off to the appropriate processing function
58 (for now just process_image...)
59 """
60 entry = mgg.database.MediaEntry.one(
61 {'_id': ObjectId(media_id)})
62
63 # Try to process, and handle expected errors.
64 try:
65 process_image(entry)
66 except BaseProcessingFail, exc:
67 mark_entry_failed(entry[u'_id'], exc)
68 return
69
70 entry['state'] = u'processed'
71 entry.save()
72
73 def on_failure(self, exc, task_id, args, kwargs, einfo):
74 """
75 If the processing failed we should mark that in the database.
76
77 Assuming that the exception raised is a subclass of BaseProcessingFail,
78 we can use that to get more information about the failure and store that
79 for conveying information to users about the failure, etc.
80 """
81 entry_id = args[0]
82 mark_entry_failed(entry_id, exc)
83
84
85 process_media = registry.tasks[ProcessMedia.name]
86
87
88 def mark_entry_failed(entry_id, exc):
89 """
90 Mark a media entry as having failed in its conversion.
91
92 Uses the exception that was raised to mark more information. If the
93 exception is a derivative of BaseProcessingFail then we can store extra
94 information that can be useful for users telling them why their media failed
95 to process.
96
97 Args:
98 - entry_id: The id of the media entry
99
100 """
101 # Was this a BaseProcessingFail? In other words, was this a
102 # type of error that we know how to handle?
103 if isinstance(exc, BaseProcessingFail):
104 # Looks like yes, so record information about that failure and any
105 # metadata the user might have supplied.
106 mgg.database['media_entries'].update(
107 {'_id': entry_id},
108 {'$set': {u'state': u'failed',
109 u'fail_error': exc.exception_path,
110 u'fail_metadata': exc.metadata}})
111 else:
112 # Looks like no, so just mark it as failed and don't record a
113 # failure_error (we'll assume it wasn't handled) and don't record
114 # metadata (in fact overwrite it if somehow it had previous info
115 # here)
116 mgg.database['media_entries'].update(
117 {'_id': entry_id},
118 {'$set': {u'state': u'failed',
119 u'fail_error': None,
120 u'fail_metadata': {}}})
121
122
123 def process_image(entry):
124 """
125 Code to process an image
126 """
127 workbench = mgg.workbench_manager.create_workbench()
128
129 queued_filepath = entry['queued_media_file']
130 queued_filename = workbench.localized_file(
131 mgg.queue_store, queued_filepath,
132 'source')
133
134 try:
135 thumb = Image.open(queued_filename)
136 except IOError:
137 raise BadMediaFail()
138
139 thumb.thumbnail(THUMB_SIZE, Image.ANTIALIAS)
140 # ensure color mode is compatible with jpg
141 if thumb.mode != "RGB":
142 thumb = thumb.convert("RGB")
143
144 thumb_filepath = create_pub_filepath(entry, 'thumbnail.jpg')
145
146 thumb_file = mgg.public_store.get_file(thumb_filepath, 'w')
147 with closing(thumb_file):
148 thumb.save(thumb_file, "JPEG", quality=90)
149
150 # If the size of the original file exceeds the specified size of a `medium`
151 # file, a `medium.jpg` files is created and later associated with the media
152 # entry.
153 medium = Image.open(queued_filename)
154 medium_processed = False
155
156 if medium.size[0] > MEDIUM_SIZE[0] or medium.size[1] > MEDIUM_SIZE[1]:
157 medium.thumbnail(MEDIUM_SIZE, Image.ANTIALIAS)
158
159 if medium.mode != "RGB":
160 medium = medium.convert("RGB")
161
162 medium_filepath = create_pub_filepath(entry, 'medium.jpg')
163
164 medium_file = mgg.public_store.get_file(medium_filepath, 'w')
165 with closing(medium_file):
166 medium.save(medium_file, "JPEG", quality=90)
167 medium_processed = True
168
169 # we have to re-read because unlike PIL, not everything reads
170 # things in string representation :)
171 queued_file = file(queued_filename, 'rb')
172
173 with queued_file:
174 original_filepath = create_pub_filepath(entry, queued_filepath[-1])
175
176 with closing(mgg.public_store.get_file(original_filepath, 'wb')) as original_file:
177 original_file.write(queued_file.read())
178
179 mgg.queue_store.delete_file(queued_filepath)
180 entry['queued_media_file'] = []
181 media_files_dict = entry.setdefault('media_files', {})
182 media_files_dict['thumb'] = thumb_filepath
183 media_files_dict['original'] = original_filepath
184 if medium_processed:
185 media_files_dict['medium'] = medium_filepath
186
187 # clean up workbench
188 workbench.destroy_self()