Commit | Line | Data |
---|---|---|
41f446f4 CAW |
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 | |
41f446f4 | 18 | |
851c51a3 | 19 | from contextlib import contextmanager |
2e5ea6b9 | 20 | from celery.task import Task |
4a477e24 | 21 | from celery import registry |
41f446f4 | 22 | |
4a477e24 CAW |
23 | from mediagoblin.db.util import ObjectId |
24 | from mediagoblin import mg_globals as mgg | |
25 | from mediagoblin.process_media.errors import BaseProcessingFail, BadMediaFail | |
4b860cb8 | 26 | |
41f446f4 | 27 | |
24eaf0fd | 28 | THUMB_SIZE = 180, 180 |
93214d8e | 29 | MEDIUM_SIZE = 640, 640 |
41f446f4 CAW |
30 | |
31 | ||
180bdbde | 32 | def create_pub_filepath(entry, filename): |
48a7ba1e | 33 | return mgg.public_store.get_unique_filepath( |
180bdbde E |
34 | ['media_entries', |
35 | unicode(entry['_id']), | |
36 | filename]) | |
37 | ||
4a477e24 | 38 | |
851c51a3 JW |
39 | @contextmanager |
40 | def closing(callback): | |
41 | try: | |
42 | yield callback | |
43 | finally: | |
44 | pass | |
180bdbde | 45 | |
ca030ab6 | 46 | |
4a477e24 CAW |
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)}) | |
6788b412 CAW |
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 | ||
4a477e24 CAW |
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 | """ | |
6788b412 CAW |
81 | entry_id = args[0] |
82 | mark_entry_failed(entry_id, exc) | |
4a477e24 | 83 | |
4a477e24 | 84 | |
6788b412 | 85 | process_media = registry.tasks[ProcessMedia.name] |
4a477e24 CAW |
86 | |
87 | ||
6788b412 | 88 | def mark_entry_failed(entry_id, exc): |
2e5ea6b9 CAW |
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 | """ | |
6788b412 CAW |
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': {}}}) | |
4a477e24 CAW |
121 | |
122 | ||
123 | def process_image(entry): | |
124 | """ | |
125 | Code to process an image | |
126 | """ | |
127 | workbench = mgg.workbench_manager.create_workbench() | |
41f446f4 | 128 | |
fa7f9c61 | 129 | queued_filepath = entry['queued_media_file'] |
52426ae0 E |
130 | queued_filename = workbench.localized_file( |
131 | mgg.queue_store, queued_filepath, | |
ca030ab6 CAW |
132 | 'source') |
133 | ||
4b860cb8 CAW |
134 | try: |
135 | thumb = Image.open(queued_filename) | |
136 | except IOError: | |
137 | raise BadMediaFail() | |
138 | ||
93214d8e JW |
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") | |
41f446f4 | 143 | |
93214d8e | 144 | thumb_filepath = create_pub_filepath(entry, 'thumbnail.jpg') |
93214d8e | 145 | thumb_file = mgg.public_store.get_file(thumb_filepath, 'w') |
b5e7b967 | 146 | |
851c51a3 | 147 | with closing(thumb_file): |
899d8916 | 148 | thumb.save(thumb_file, "JPEG", quality=90) |
93214d8e | 149 | |
ff520ff5 CAW |
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. | |
93214d8e | 153 | medium = Image.open(queued_filename) |
2c9e635a | 154 | medium_processed = False |
93214d8e | 155 | |
2c9e635a JW |
156 | if medium.size[0] > MEDIUM_SIZE[0] or medium.size[1] > MEDIUM_SIZE[1]: |
157 | medium.thumbnail(MEDIUM_SIZE, Image.ANTIALIAS) | |
41f446f4 | 158 | |
2c9e635a JW |
159 | if medium.mode != "RGB": |
160 | medium = medium.convert("RGB") | |
41f446f4 | 161 | |
2c9e635a | 162 | medium_filepath = create_pub_filepath(entry, 'medium.jpg') |
2c9e635a | 163 | medium_file = mgg.public_store.get_file(medium_filepath, 'w') |
b5e7b967 | 164 | |
851c51a3 | 165 | with closing(medium_file): |
5f72a4c3 | 166 | medium.save(medium_file, "JPEG", quality=90) |
2c9e635a | 167 | medium_processed = True |
41f446f4 | 168 | |
fa7f9c61 CAW |
169 | # we have to re-read because unlike PIL, not everything reads |
170 | # things in string representation :) | |
ca030ab6 | 171 | queued_file = file(queued_filename, 'rb') |
fa7f9c61 CAW |
172 | |
173 | with queued_file: | |
2c9e635a | 174 | original_filepath = create_pub_filepath(entry, queued_filepath[-1]) |
fa7f9c61 | 175 | |
851c51a3 | 176 | with closing(mgg.public_store.get_file(original_filepath, 'wb')) as original_file: |
2c9e635a | 177 | original_file.write(queued_file.read()) |
fa7f9c61 | 178 | |
300c34e8 | 179 | mgg.queue_store.delete_file(queued_filepath) |
180bdbde | 180 | entry['queued_media_file'] = [] |
fa7f9c61 CAW |
181 | media_files_dict = entry.setdefault('media_files', {}) |
182 | media_files_dict['thumb'] = thumb_filepath | |
2c9e635a JW |
183 | media_files_dict['original'] = original_filepath |
184 | if medium_processed: | |
185 | media_files_dict['medium'] = medium_filepath | |
ca030ab6 CAW |
186 | |
187 | # clean up workbench | |
b67a983a | 188 | workbench.destroy_self() |