Chunked transfers for the music plugin. I _think_ everything is legit
[pyTivo/wmcbrine.git] / plugin.py
blob268a41c75dae11abada5cc673b027d4ee257aca9
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_response(200)
74 handler.end_headers()
75 f = open(unicode(path, 'utf-8'), 'rb')
76 shutil.copyfileobj(f, handler.wfile)
77 f.close()
79 def get_local_base_path(self, handler, query):
80 return os.path.normpath(handler.container['path'])
82 def get_local_path(self, handler, query):
84 subcname = query['Container'][0]
86 path = self.get_local_base_path(handler, query)
87 for folder in subcname.split('/')[1:]:
88 if folder == '..':
89 return False
90 path = os.path.join(path, folder)
91 return path
93 def item_count(self, handler, query, cname, files, last_start=0):
94 """Return only the desired portion of the list, as specified by
95 ItemCount, AnchorItem and AnchorOffset. 'files' is either a
96 list of strings, OR a list of objects with a 'name' attribute.
97 """
98 def no_anchor(handler, anchor):
99 handler.server.logger.warning('Anchor not found: ' + anchor)
101 totalFiles = len(files)
102 index = 0
104 if totalFiles and 'ItemCount' in query:
105 count = int(query['ItemCount'][0])
107 if 'AnchorItem' in query:
108 bs = '/TiVoConnect?Command=QueryContainer&Container='
109 local_base_path = self.get_local_base_path(handler, query)
111 anchor = query['AnchorItem'][0]
112 if anchor.startswith(bs):
113 anchor = anchor.replace(bs, '/', 1)
114 anchor = unquote(anchor)
115 anchor = anchor.replace(os.path.sep + cname, local_base_path, 1)
116 if not '://' in anchor:
117 anchor = os.path.normpath(anchor)
119 if type(files[0]) == str:
120 filenames = files
121 else:
122 filenames = [x.name for x in files]
123 try:
124 index = filenames.index(anchor, last_start)
125 except ValueError:
126 if last_start:
127 try:
128 index = filenames.index(anchor, 0, last_start)
129 except ValueError:
130 no_anchor(handler, anchor)
131 else:
132 no_anchor(handler, anchor) # just use index = 0
134 if count > 0:
135 index += 1
137 if 'AnchorOffset' in query:
138 index += int(query['AnchorOffset'][0])
140 if count < 0:
141 index = (index + count) % len(files)
142 count = -count
143 files = files[index:index + count]
145 return files, totalFiles, index
147 def get_files(self, handler, query, filterFunction=None, force_alpha=False):
149 class FileData:
150 def __init__(self, name, isdir):
151 self.name = name
152 self.isdir = isdir
153 st = os.stat(unicode(name, 'utf-8'))
154 self.mdate = int(st.st_mtime)
155 self.size = st.st_size
157 class SortList:
158 def __init__(self, files):
159 self.files = files
160 self.unsorted = True
161 self.sortby = None
162 self.last_start = 0
164 def build_recursive_list(path, recurse=True):
165 files = []
166 path = unicode(path, 'utf-8')
167 try:
168 for f in os.listdir(path):
169 if f.startswith('.'):
170 continue
171 f = os.path.join(path, f)
172 isdir = os.path.isdir(f)
173 if sys.platform == 'darwin':
174 f = unicodedata.normalize('NFC', f)
175 f = f.encode('utf-8')
176 if recurse and isdir:
177 files.extend(build_recursive_list(f))
178 else:
179 if not filterFunction or filterFunction(f, file_type):
180 files.append(FileData(f, isdir))
181 except:
182 pass
183 return files
185 subcname = query['Container'][0]
186 path = self.get_local_path(handler, query)
188 file_type = query.get('Filter', [''])[0]
190 recurse = query.get('Recurse', ['No'])[0] == 'Yes'
192 filelist = []
193 rc = self.recurse_cache
194 dc = self.dir_cache
195 if recurse:
196 if path in rc and rc.mtime(path) + 300 >= time.time():
197 filelist = rc[path]
198 else:
199 updated = os.stat(unicode(path, 'utf-8'))[8]
200 if path in dc and dc.mtime(path) >= updated:
201 filelist = dc[path]
202 for p in rc:
203 if path.startswith(p) and rc.mtime(p) < updated:
204 del rc[p]
206 if not filelist:
207 filelist = SortList(build_recursive_list(path, recurse))
209 if recurse:
210 rc[path] = filelist
211 else:
212 dc[path] = filelist
214 def dir_sort(x, y):
215 if x.isdir == y.isdir:
216 return name_sort(x, y)
217 else:
218 return y.isdir - x.isdir
220 def name_sort(x, y):
221 return cmp(x.name, y.name)
223 def date_sort(x, y):
224 return cmp(y.mdate, x.mdate)
226 sortby = query.get('SortOrder', ['Normal'])[0]
227 if filelist.unsorted or filelist.sortby != sortby:
228 if force_alpha:
229 filelist.files.sort(dir_sort)
230 elif sortby == '!CaptureDate':
231 filelist.files.sort(date_sort)
232 else:
233 filelist.files.sort(name_sort)
235 filelist.sortby = sortby
236 filelist.unsorted = False
238 files = filelist.files[:]
240 # Trim the list
241 files, total, start = self.item_count(handler, query, handler.cname,
242 files, filelist.last_start)
243 if len(files) > 1:
244 filelist.last_start = start
245 return files, total, start