root/plugins/photo/photo.py

Revision 3f7f64be861c65be6c8895e3f8b76e960e02976c, 14.8 kB (checked in by KRKeegan <-NOSPAM-kevin@krkeegan.com>, 10 months ago)

Add exception catch to all os.listdir commands

Otherwise we risk choking on folders that we don't have permission to view.

  • Property mode set to 100644
Line 
1 # Photo module for pyTivo by William McBrine <wmcbrine@users.sf.net>
2 # based partly on music.py and plugin.py
3 #
4 # After version 0.15, see git for the history
5 #
6 # Version 0.15, Dec. 29 -- allow Unicode; better error messages
7 # Version 0.14, Dec. 26 -- fix Random sort; handle ItemCount == 0
8 # Version 0.13, Dec. 19 -- more thread-safe; use draft mode always
9 # Version 0.12, Dec. 18 -- get date and orientation from Exif
10 # Version 0.11, Dec. 16 -- handle ItemCount, AnchorItem etc. correctly
11 # Version 0.10, Dec. 14 -- give full list if no ItemCount; use antialias
12 #                          mode always; allow larger thumbnails
13 # Version 0.9,  Dec. 13 -- different sort types
14 # Version 0.8,  Dec. 12 -- faster thumbnails, better quality full views
15 # Version 0.7,  Dec. 11 -- fix missing item on thumbnail scroll up,
16 #                          better anchor and path handling
17 # Version 0.6,  Dec. 10 -- cache recursive lookups for faster slide shows
18 # Version 0.5,  Dec. 10 -- fix reboot problem by keeping directory names
19 #                          (vs. contents) out of "Recurse=Yes" lists
20 # Version 0.4,  Dec. 10 -- drop the use of playable_cache, add path
21 #                          separator kludges for Windows
22 # Version 0.3,  Dec. 8  -- revert to using PixelShape, workaround for
23 #                          Image.save() under Windows
24 # Version 0.2,  Dec. 8  -- thumbnail caching, faster thumbnails
25 # Version 0.1,  Dec. 7, 2007
26
27 import os, re, random, urllib, threading, time, cgi
28 try:
29     import Image
30 except ImportError:
31     print 'Photo Plugin Error: The Python Imaging Library is not installed'
32 from cStringIO import StringIO
33 from Cheetah.Template import Template
34 from Cheetah.Filters import Filter
35 from plugin import Plugin, quote, unquote
36 from xml.sax.saxutils import escape
37 from lrucache import LRUCache
38
39 SCRIPTDIR = os.path.dirname(__file__)
40
41 CLASS_NAME = 'Photo'
42
43 # Match Exif date -- YYYY:MM:DD HH:MM:SS
44 exif_date = re.compile(r'(\d{4}):(\d\d):(\d\d) (\d\d):(\d\d):(\d\d)').search
45
46 # Match Exif orientation, Intel and Motorola versions
47 exif_orient_i = \
48     re.compile('\x12\x01\x03\x00\x01\x00\x00\x00(.)\x00\x00\x00').search
49 exif_orient_m = \
50     re.compile('\x01\x12\x00\x03\x00\x00\x00\x01\x00(.)\x00\x00').search
51
52 # Preload the template
53 tname = os.path.join(SCRIPTDIR, 'templates', 'container.tmpl')
54 photo_template = file(tname, 'rb').read()
55
56 class EncodeUnicode(Filter):
57     def filter(self, val, **kw):
58         """Encode Unicode strings, by default in UTF-8"""
59
60         if kw.has_key('encoding'):
61             encoding = kw['encoding']
62         else:
63             encoding='utf8'
64                            
65         if type(val) == type(u''):
66             filtered = val.encode(encoding)
67         else:
68             filtered = str(val)
69         return filtered
70
71 class Photo(Plugin):
72    
73     CONTENT_TYPE = 'x-container/tivo-photos'
74
75     class LockedLRUCache(LRUCache):
76         def __init__(self, num):
77             LRUCache.__init__(self, num)
78             self.lock = threading.RLock()
79
80         def acquire(self, blocking=1):
81             return self.lock.acquire(blocking)
82
83         def release(self):
84             self.lock.release()
85
86         def __setitem__(self, key, obj):
87             self.acquire()
88             LRUCache.__setitem__(self, key, obj)
89             self.release()
90
91     media_data_cache = LockedLRUCache(300)  # info and thumbnails
92     recurse_cache = LockedLRUCache(5)       # recursive directory lists
93     dir_cache = LockedLRUCache(10)          # non-recursive lists
94
95     def send_file(self, handler, container, name):
96
97         def send_jpeg(data):
98             handler.send_response(200)
99             handler.send_header('Content-Type', 'image/jpeg')
100             handler.send_header('Content-Length', len(data))
101             handler.send_header('Connection', 'close')
102             handler.end_headers()
103             handler.wfile.write(data)
104
105         path, query = handler.path.split('?')
106         infile = os.path.join(os.path.normpath(container['path']),
107                               unquote(path)[len(name) + 2:])
108         opts = cgi.parse_qs(query)
109
110         if 'Format' in opts and opts['Format'][0] != 'image/jpeg':
111             handler.send_error(415)
112             return
113
114         try:
115             attrs = self.media_data_cache[infile]
116         except:
117             attrs = None
118
119         # Set rotation
120         if attrs:
121             rot = attrs['rotation']
122         else:
123             rot = 0
124
125         if 'Rotation' in opts:
126             rot = (rot - int(opts['Rotation'][0])) % 360
127             if attrs:
128                 attrs['rotation'] = rot
129                 if 'thumb' in attrs:
130                     del attrs['thumb']
131
132         # Requested size
133         width = int(opts.get('Width', ['0'])[0])
134         height = int(opts.get('Height', ['0'])[0])
135
136         # Return saved thumbnail?
137         if attrs and 'thumb' in attrs and 0 < width < 100 and 0 < height < 100:
138             send_jpeg(attrs['thumb'])
139             return
140
141         # Load
142         try:
143             pic = Image.open(unicode(infile, 'utf-8'))
144         except Exception, msg:
145             print 'Could not open', infile, '--', msg
146             handler.send_error(404)
147             return
148
149         # Set draft mode
150         try:
151             pic.draft('RGB', (width, height))
152         except Exception, msg:
153             print 'Failed to set draft mode for', infile, '--', msg
154             handler.send_error(404)
155             return
156
157         # Read Exif data if possible
158         if 'exif' in pic.info:
159             exif = pic.info['exif']
160
161             # Capture date
162             if attrs and not 'odate' in attrs:
163                 date = exif_date(exif)
164                 if date:
165                     year, month, day, hour, minute, second = \
166                         (int(x) for x in date.groups())
167                     if year:
168                         odate = time.mktime((year, month, day, hour,
169                                              minute, second, -1, -1, -1))
170                         attrs['odate'] = '%#x' % int(odate)
171
172             # Orientation
173             if attrs and 'exifrot' in attrs:
174                 rot = (rot + attrs['exifrot']) % 360
175             else:
176                 if exif[6] == 'I':
177                     orient = exif_orient_i(exif)
178                 else:
179                     orient = exif_orient_m(exif)
180
181                 if orient:
182                     exifrot = ((ord(orient.group(1)) - 1) * -90) % 360
183                     rot = (rot + exifrot) % 360
184                     if attrs:
185                         attrs['exifrot'] = exifrot
186
187         # Rotate
188         try:
189             if rot:
190                 pic = pic.rotate(rot)
191         except Exception, msg:
192             print 'Rotate failed on', infile, '--', msg
193             handler.send_error(404)
194             return
195
196         # De-palletize
197         try:
198             if pic.mode == 'P':
199                 pic = pic.convert()
200         except Exception, msg:
201             print 'Palette conversion failed on', infile, '--', msg
202             handler.send_error(404)
203             return
204
205         # Old size
206         oldw, oldh = pic.size
207
208         if not width: width = oldw
209         if not height: height = oldh
210
211         # Correct aspect ratio
212         if 'PixelShape' in opts:
213             pixw, pixh = opts['PixelShape'][0].split(':')
214             oldw *= int(pixh)
215             oldh *= int(pixw)
216
217         # Resize
218         ratio = float(oldw) / oldh
219
220         if float(width) / height < ratio:
221             height = int(width / ratio)
222         else:
223             width = int(height * ratio)
224
225         try:
226             pic = pic.resize((width, height), Image.ANTIALIAS)
227         except Exception, msg:
228             print 'Resize failed on', infile, '--', msg
229             handler.send_error(404)
230             return
231
232         # Re-encode
233         try:
234             out = StringIO()
235             pic.save(out, 'JPEG')
236             encoded = out.getvalue()
237             out.close()
238         except Exception, msg:
239             print 'Encode failed on', infile, '--', msg
240             handler.send_error(404)
241             return
242
243         # Save thumbnails
244         if attrs and width < 100 and height < 100:
245             attrs['thumb'] = encoded
246
247         # Send it
248         send_jpeg(encoded)
249        
250     def QueryContainer(self, handler, query):
251
252         # Reject a malformed request -- these attributes should only
253         # appear in requests to send_file, but sometimes appear here
254         badattrs = ('Rotation', 'Width', 'Height', 'PixelShape')
255         for i in badattrs:
256             if i in query:
257                 handler.send_error(404)
258                 return
259
260         subcname = query['Container'][0]
261         cname = subcname.split('/')[0]
262         local_base_path = self.get_local_base_path(handler, query)
263         if not handler.server.containers.has_key(cname) or \
264            not self.get_local_path(handler, query):
265             handler.send_error(404)
266             return
267
268         def ImageFileFilter(f):
269             goodexts = ('.jpg', '.gif', '.png', '.bmp', '.tif', '.xbm',
270                         '.xpm', '.pgm', '.pbm', '.ppm', '.pcx', '.tga',
271                         '.fpx', '.ico', '.pcd', '.jpeg', '.tiff')
272             return os.path.splitext(f)[1].lower() in goodexts
273
274         def media_data(f):
275             if f.name in self.media_data_cache:
276                 return self.media_data_cache[f.name]
277
278             item = {}
279             item['path'] = f.name
280             item['part_path'] = f.name.replace(local_base_path, '', 1)
281             item['name'] = os.path.split(f.name)[1]
282             item['is_dir'] = f.isdir
283             item['rotation'] = 0
284             item['cdate'] = '%#x' % f.cdate
285             item['mdate'] = '%#x' % f.mdate
286
287             self.media_data_cache[f.name] = item
288             return item
289
290         t = Template(photo_template, filter=EncodeUnicode)
291         t.name = subcname
292         t.container = cname
293         t.files, t.total, t.start = self.get_files(handler, query,
294             ImageFileFilter)
295         t.files = map(media_data, t.files)
296         t.quote = quote
297         t.escape = escape
298         page = str(t)
299
300         handler.send_response(200)
301         handler.send_header('Content-Type', 'text/xml')
302         handler.send_header('Content-Length', len(page))
303         handler.send_header('Connection', 'close')
304         handler.end_headers()
305         handler.wfile.write(page)
306
307     def get_files(self, handler, query, filterFunction):
308
309         class FileData:
310             def __init__(self, name, isdir):
311                 self.name = name
312                 self.isdir = isdir
313                 st = os.stat(name)
314                 self.cdate = int(st.st_ctime)
315                 self.mdate = int(st.st_mtime)
316
317         class SortList:
318             def __init__(self, files):
319                 self.files = files
320                 self.unsorted = True
321                 self.sortby = None
322                 self.last_start = 0
323                 self.lock = threading.RLock()
324
325             def acquire(self, blocking=1):
326                 return self.lock.acquire(blocking)
327
328             def release(self):
329                 self.lock.release()
330
331         def build_recursive_list(path, recurse=True):
332             files = []
333             path = unicode(path, 'utf-8')
334             try:
335                 for f in os.listdir(path):
336                     f = os.path.join(path, f)
337                     isdir = os.path.isdir(f)
338                     f = f.encode('utf-8')
339                     if recurse and isdir:
340                         files.extend(build_recursive_list(f))
341                     else:
342                        if isdir or filterFunction(f):
343                            files.append(FileData(f, isdir))
344             except:
345                 pass
346
347             return files
348
349         def name_sort(x, y):
350             return cmp(x.name, y.name)
351
352         def cdate_sort(x, y):
353             return cmp(x.cdate, y.cdate)
354
355         def mdate_sort(x, y):
356             return cmp(x.mdate, y.mdate)
357
358         def dir_sort(x, y):
359             if x.isdir == y.isdir:
360                 return sortfunc(x, y)
361             else:
362                 return y.isdir - x.isdir
363
364         subcname = query['Container'][0]
365         cname = subcname.split('/')[0]
366         path = self.get_local_path(handler, query)
367
368         # Build the list
369         recurse = query.get('Recurse', ['No'])[0] == 'Yes'
370
371         if recurse and path in self.recurse_cache:
372             filelist = self.recurse_cache[path]
373         elif not recurse and path in self.dir_cache:
374             filelist = self.dir_cache[path]
375         else:
376             filelist = SortList(build_recursive_list(path, recurse))
377
378             if recurse:
379                 self.recurse_cache[path] = filelist
380             else:
381                 self.dir_cache[path] = filelist
382
383         filelist.acquire()
384
385         # Sort it
386         seed = ''
387         start = ''
388         sortby = query.get('SortOrder', ['Normal'])[0]
389         if 'Random' in sortby:
390             if 'RandomSeed' in query:
391                 seed = query['RandomSeed'][0]
392                 sortby += seed
393             if 'RandomStart' in query:
394                 start = query['RandomStart'][0]
395                 sortby += start
396
397         if filelist.unsorted or filelist.sortby != sortby:
398             if 'Random' in sortby:
399                 self.random_lock.acquire()
400                 if seed:
401                     random.seed(seed)
402                 random.shuffle(filelist.files)
403                 self.random_lock.release()
404                 if start:
405                     local_base_path = self.get_local_base_path(handler, query)
406                     start = unquote(start)
407                     start = start.replace(os.path.sep + cname,
408                                           local_base_path, 1)
409                     filenames = [x.name for x in filelist.files]
410                     try:
411                         index = filenames.index(start)
412                         i = filelist.files.pop(index)
413                         filelist.files.insert(0, i)
414                     except ValueError:
415                         print 'Start not found:', start
416             else:
417                 if 'CaptureDate' in sortby:
418                     sortfunc = cdate_sort
419                 elif 'LastChangeDate' in sortby:
420                     sortfunc = mdate_sort
421                 else:
422                     sortfunc = name_sort
423
424                 if 'Type' in sortby:
425                     filelist.files.sort(dir_sort)
426                 else:
427                     filelist.files.sort(sortfunc)
428
429             filelist.sortby = sortby
430             filelist.unsorted = False
431
432         files = filelist.files[:]
433
434         # Filter it -- this section needs work
435         if 'Filter' in query:
436             usedir = 'folder' in query['Filter'][0]
437             useimg = 'image' in query['Filter'][0]
438             if not usedir:
439                 files = [x for x in files if not x.isdir]
440             elif usedir and not useimg:
441                 files = [x for x in files if x.isdir]
442
443         files, total, start = self.item_count(handler, query, cname, files,
444                                               filelist.last_start)
445         filelist.last_start = start
446         filelist.release()
447         return files, total, start
Note: See TracBrowser for help on using the browser.