Allow for pyTivo-style URLs; more Unicode in ToGo.
[pyTivo/wmcbrine.git] / plugin.py
blobcf384c005e600d9acf3f64e0591ac05bd8960cd1
1 import os
2 import random
3 import shutil
4 import sys
5 import threading
6 import time
7 import unicodedata
8 import urllib
10 from Cheetah.Filters import Filter
11 from lrucache import LRUCache
13 if os.path.sep == '/':
14 quote = urllib.quote
15 unquote = urllib.unquote_plus
16 else:
17 quote = lambda x: urllib.quote(x.replace(os.path.sep, '/'))
18 unquote = lambda x: os.path.normpath(urllib.unquote_plus(x))
20 class Error:
21 CONTENT_TYPE = 'text/html'
23 def GetPlugin(name):
24 try:
25 module_name = '.'.join(['plugins', name, name])
26 module = __import__(module_name, globals(), locals(), name)
27 plugin = getattr(module, module.CLASS_NAME)()
28 return plugin
29 except ImportError:
30 print 'Error no', name, 'plugin exists. Check the type ' \
31 'setting for your share.'
32 return Error
34 class EncodeUnicode(Filter):
35 def filter(self, val, **kw):
36 """Encode Unicode strings, by default in UTF-8"""
38 encoding = kw.get('encoding', 'utf8')
40 if type(val) == str:
41 try:
42 val = val.decode('utf8')
43 except:
44 if sys.platform == 'darwin':
45 val = val.decode('macroman')
46 else:
47 val = val.decode('iso8859-1')
48 elif type(val) != unicode:
49 val = str(val)
50 return val.encode(encoding)
52 class Plugin(object):
54 random_lock = threading.Lock()
56 CONTENT_TYPE = ''
58 recurse_cache = LRUCache(5)
59 dir_cache = LRUCache(10)
61 def __new__(cls, *args, **kwds):
62 it = cls.__dict__.get('__it__')
63 if it is not None:
64 return it
65 cls.__it__ = it = object.__new__(cls)
66 it.init(*args, **kwds)
67 return it
69 def init(self):
70 pass
72 def send_file(self, handler, path, query):
73 handler.send_content_file(unicode(path, 'utf-8'))
75 def get_local_base_path(self, handler, query):
76 return os.path.normpath(handler.container['path'])
78 def get_local_path(self, handler, query):
80 subcname = query['Container'][0]
82 path = self.get_local_base_path(handler, query)
83 for folder in subcname.split('/')[1:]:
84 if folder == '..':
85 return False
86 path = os.path.join(path, folder)
87 return path
89 def item_count(self, handler, query, cname, files, last_start=0):
90 """Return only the desired portion of the list, as specified by
91 ItemCount, AnchorItem and AnchorOffset. 'files' is either a
92 list of strings, OR a list of objects with a 'name' attribute.
93 """
94 def no_anchor(handler, anchor):
95 handler.server.logger.warning('Anchor not found: ' + anchor)
97 totalFiles = len(files)
98 index = 0
100 if totalFiles and 'ItemCount' in query:
101 count = int(query['ItemCount'][0])
103 if 'AnchorItem' in query:
104 bs = '/TiVoConnect?Command=QueryContainer&Container='
105 local_base_path = self.get_local_base_path(handler, query)
107 anchor = query['AnchorItem'][0]
108 if anchor.startswith(bs):
109 anchor = anchor.replace(bs, '/', 1)
110 anchor = unquote(anchor)
111 anchor = anchor.replace(os.path.sep + cname, local_base_path, 1)
112 if not '://' in anchor:
113 anchor = os.path.normpath(anchor)
115 if type(files[0]) == str:
116 filenames = files
117 else:
118 filenames = [x.name for x in files]
119 try:
120 index = filenames.index(anchor, last_start)
121 except ValueError:
122 if last_start:
123 try:
124 index = filenames.index(anchor, 0, last_start)
125 except ValueError:
126 no_anchor(handler, anchor)
127 else:
128 no_anchor(handler, anchor) # just use index = 0
130 if count > 0:
131 index += 1
133 if 'AnchorOffset' in query:
134 index += int(query['AnchorOffset'][0])
136 if count < 0:
137 index = (index + count) % len(files)
138 count = -count
139 files = files[index:index + count]
141 return files, totalFiles, index
143 def get_files(self, handler, query, filterFunction=None, force_alpha=False):
145 class FileData:
146 def __init__(self, name, isdir):
147 self.name = name
148 self.isdir = isdir
149 st = os.stat(unicode(name, 'utf-8'))
150 self.mdate = st.st_mtime
151 self.size = st.st_size
153 class SortList:
154 def __init__(self, files):
155 self.files = files
156 self.unsorted = True
157 self.sortby = None
158 self.last_start = 0
160 def build_recursive_list(path, recurse=True):
161 files = []
162 path = unicode(path, 'utf-8')
163 try:
164 for f in os.listdir(path):
165 if f.startswith('.'):
166 continue
167 f = os.path.join(path, f)
168 isdir = os.path.isdir(f)
169 if sys.platform == 'darwin':
170 f = unicodedata.normalize('NFC', f)
171 f = f.encode('utf-8')
172 if recurse and isdir:
173 files.extend(build_recursive_list(f))
174 else:
175 if not filterFunction or filterFunction(f, file_type):
176 files.append(FileData(f, isdir))
177 except:
178 pass
179 return files
181 subcname = query['Container'][0]
182 path = self.get_local_path(handler, query)
184 file_type = query.get('Filter', [''])[0]
186 recurse = query.get('Recurse', ['No'])[0] == 'Yes'
188 filelist = []
189 rc = self.recurse_cache
190 dc = self.dir_cache
191 if recurse:
192 if path in rc and rc.mtime(path) + 300 >= time.time():
193 filelist = rc[path]
194 else:
195 updated = os.path.getmtime(unicode(path, 'utf-8'))
196 if path in dc and dc.mtime(path) >= updated:
197 filelist = dc[path]
198 for p in rc:
199 if path.startswith(p) and rc.mtime(p) < updated:
200 del rc[p]
202 if not filelist:
203 filelist = SortList(build_recursive_list(path, recurse))
205 if recurse:
206 rc[path] = filelist
207 else:
208 dc[path] = filelist
210 def dir_sort(x, y):
211 if x.isdir == y.isdir:
212 return name_sort(x, y)
213 else:
214 return y.isdir - x.isdir
216 def name_sort(x, y):
217 return cmp(x.name, y.name)
219 def date_sort(x, y):
220 return cmp(y.mdate, x.mdate)
222 sortby = query.get('SortOrder', ['Normal'])[0]
223 if filelist.unsorted or filelist.sortby != sortby:
224 if force_alpha:
225 filelist.files.sort(dir_sort)
226 elif sortby == '!CaptureDate':
227 filelist.files.sort(date_sort)
228 else:
229 filelist.files.sort(name_sort)
231 filelist.sortby = sortby
232 filelist.unsorted = False
234 files = filelist.files[:]
236 # Trim the list
237 files, total, start = self.item_count(handler, query, handler.cname,
238 files, filelist.last_start)
239 if len(files) > 1:
240 filelist.last_start = start
241 return files, total, start