change get_queued_filename to get_orig_filename and modified function
[mediagoblin.git] / mediagoblin / processing / __init__.py
1 # GNU MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
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 logging
18 import os
19
20 from mediagoblin.db.util import atomic_update
21 from mediagoblin import mg_globals as mgg
22
23 from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
24
25 _log = logging.getLogger(__name__)
26
27
28 class ProgressCallback(object):
29 def __init__(self, entry):
30 self.entry = entry
31
32 def __call__(self, progress):
33 if progress:
34 self.entry.transcoding_progress = progress
35 self.entry.save()
36
37
38 def create_pub_filepath(entry, filename):
39 return mgg.public_store.get_unique_filepath(
40 ['media_entries',
41 unicode(entry.id),
42 filename])
43
44
45 class FilenameBuilder(object):
46 """Easily slice and dice filenames.
47
48 Initialize this class with an original file path, then use the fill()
49 method to create new filenames based on the original.
50
51 """
52 MAX_FILENAME_LENGTH = 255 # VFAT's maximum filename length
53
54 def __init__(self, path):
55 """Initialize a builder from an original file path."""
56 self.dirpath, self.basename = os.path.split(path)
57 self.basename, self.ext = os.path.splitext(self.basename)
58 self.ext = self.ext.lower()
59
60 def fill(self, fmtstr):
61 """Build a new filename based on the original.
62
63 The fmtstr argument can include the following:
64 {basename} -- the original basename, with the extension removed
65 {ext} -- the original extension, always lowercase
66
67 If necessary, {basename} will be truncated so the filename does not
68 exceed this class' MAX_FILENAME_LENGTH in length.
69
70 """
71 basename_len = (self.MAX_FILENAME_LENGTH -
72 len(fmtstr.format(basename='', ext=self.ext)))
73 return fmtstr.format(basename=self.basename[:basename_len],
74 ext=self.ext)
75
76
77 class ProcessingState(object):
78 """
79 The first and only argument to the "processor" of a media type
80
81 This could be thought of as a "request" to the processor
82 function. It has the main info for the request (media entry)
83 and a bunch of tools for the request on it.
84 It can get more fancy without impacting old media types.
85 """
86 def __init__(self, entry):
87 self.entry = entry
88 self.workbench = None
89 self.orig_filename = None
90
91 def set_workbench(self, wb):
92 self.workbench = wb
93
94 def get_orig_filename(self):
95 """
96 Get the a filename for the original, on local storage
97
98 If the media entry has a queued_media_file, use that, otherwise
99 use the original.
100
101 In the future, this will return the highest quality file available
102 if neither the original or queued file are available
103 """
104 if self.orig_filename is not None:
105 return self.orig_filename
106
107 if self.entry.queued_media_file:
108 orig_filepath = self.entry.queued_media_file
109 else:
110 orig_filepath = self.entry.media_files['original']
111
112 orig_filename = self.workbench.localized_file(
113 mgg.queue_store, orig_filepath,
114 'source')
115 self.orig_filename = orig_filename
116 return orig_filename
117
118 def copy_original(self, target_name, keyname=u"original"):
119 self.store_public(keyname, self.get_orig_filename(), target_name)
120
121 def store_public(self, keyname, local_file, target_name=None):
122 if target_name is None:
123 target_name = os.path.basename(local_file)
124 target_filepath = create_pub_filepath(self.entry, target_name)
125 if keyname in self.entry.media_files:
126 _log.warn("store_public: keyname %r already used for file %r, "
127 "replacing with %r", keyname,
128 self.entry.media_files[keyname], target_filepath)
129 mgg.public_store.copy_local_to_storage(local_file, target_filepath)
130 self.entry.media_files[keyname] = target_filepath
131
132 def delete_queue_file(self):
133 # Remove queued media file from storage and database.
134 # queued_filepath is in the task_id directory which should
135 # be removed too, but fail if the directory is not empty to be on
136 # the super-safe side.
137 queued_filepath = self.entry.queued_media_file
138 mgg.queue_store.delete_file(queued_filepath) # rm file
139 mgg.queue_store.delete_dir(queued_filepath[:-1]) # rm dir
140 self.entry.queued_media_file = []
141
142
143 def mark_entry_failed(entry_id, exc):
144 """
145 Mark a media entry as having failed in its conversion.
146
147 Uses the exception that was raised to mark more information. If
148 the exception is a derivative of BaseProcessingFail then we can
149 store extra information that can be useful for users telling them
150 why their media failed to process.
151
152 Args:
153 - entry_id: The id of the media entry
154
155 """
156 # Was this a BaseProcessingFail? In other words, was this a
157 # type of error that we know how to handle?
158 if isinstance(exc, BaseProcessingFail):
159 # Looks like yes, so record information about that failure and any
160 # metadata the user might have supplied.
161 atomic_update(mgg.database.MediaEntry,
162 {'id': entry_id},
163 {u'state': u'failed',
164 u'fail_error': unicode(exc.exception_path),
165 u'fail_metadata': exc.metadata})
166 else:
167 _log.warn("No idea what happened here, but it failed: %r", exc)
168 # Looks like no, so just mark it as failed and don't record a
169 # failure_error (we'll assume it wasn't handled) and don't record
170 # metadata (in fact overwrite it if somehow it had previous info
171 # here)
172 atomic_update(mgg.database.MediaEntry,
173 {'id': entry_id},
174 {u'state': u'failed',
175 u'fail_error': None,
176 u'fail_metadata': {}})
177
178
179 class BaseProcessingFail(Exception):
180 """
181 Base exception that all other processing failure messages should
182 subclass from.
183
184 You shouldn't call this itself; instead you should subclass it
185 and provid the exception_path and general_message applicable to
186 this error.
187 """
188 general_message = u''
189
190 @property
191 def exception_path(self):
192 return u"%s:%s" % (
193 self.__class__.__module__, self.__class__.__name__)
194
195 def __init__(self, **metadata):
196 self.metadata = metadata or {}
197
198
199 class BadMediaFail(BaseProcessingFail):
200 """
201 Error that should be raised when an inappropriate file was given
202 for the media type specified.
203 """
204 general_message = _(u'Invalid file given for media type.')