Leftover debug statements.
[pyTivo/wmcbrine.git] / plugins / music / music.py
blob1781842d936e341e464b049d9ec4099d00d6a38b
1 import os, random, re, shutil, socket, sys, urllib
2 from Cheetah.Template import Template
3 from Cheetah.Filters import Filter
4 from plugin import Plugin
5 from xml.sax.saxutils import escape
6 from lrucache import LRUCache
7 from urlparse import urlparse
8 import eyeD3
10 SCRIPTDIR = os.path.dirname(__file__)
12 CLASS_NAME = 'Music'
14 PLAYLISTS = ('.m3u', '.m3u8', '.ram', '.pls', '.b4s', '.wpl', '.asx',
15 '.wax', '.wvx')
17 # Search strings for different playlist types
18 asxfile = re.compile('ref +href *= *"(.+)"', re.IGNORECASE).search
19 wplfile = re.compile('media +src *= *"(.+)"', re.IGNORECASE).search
20 b4sfile = re.compile('Playstring="file:(.+)"').search
21 plsfile = re.compile('[Ff]ile(\d+)=(.+)').match
22 plstitle = re.compile('[Tt]itle(\d+)=(.+)').match
23 plslength = re.compile('[Ll]ength(\d+)=(\d+)').match
25 if os.path.sep == '/':
26 quote = urllib.quote
27 unquote = urllib.unquote_plus
28 else:
29 quote = lambda x: urllib.quote(x.replace(os.path.sep, '/'))
30 unquote = lambda x: urllib.unquote_plus(x).replace('/', os.path.sep)
32 # Preload the templates
33 tfname = os.path.join(SCRIPTDIR, 'templates', 'container.tmpl')
34 tpname = os.path.join(SCRIPTDIR, 'templates', 'm3u.tmpl')
35 folder_template = file(tfname, 'rb').read()
36 playlist_template = file(tpname, 'rb').read()
38 class FileData:
39 def __init__(self, name, isdir):
40 self.name = name
41 self.isdir = isdir
42 self.isplay = os.path.splitext(name)[1].lower() in PLAYLISTS
43 self.title = ''
44 self.duration = 0
46 class EncodeUnicode(Filter):
47 def filter(self, val, **kw):
48 """Encode Unicode strings, by default in UTF-8"""
50 if kw.has_key('encoding'):
51 encoding = kw['encoding']
52 else:
53 encoding='utf8'
55 if type(val) == type(u''):
56 filtered = val.encode(encoding)
57 else:
58 filtered = str(val)
59 return filtered
61 class Music(Plugin):
63 CONTENT_TYPE = 'x-container/tivo-music'
65 AUDIO = 'audio'
66 DIRECTORY = 'dir'
67 PLAYLIST = 'play'
69 media_data_cache = LRUCache(300)
70 recurse_cache = LRUCache(5)
71 dir_cache = LRUCache(10)
73 def send_file(self, handler, container, name):
74 o = urlparse("http://fake.host" + handler.path)
75 path = unquote(o[2])
76 fname = container['path'] + path[len(name) + 1:]
77 fname = unicode(fname, 'utf-8')
78 fsize = os.path.getsize(fname)
79 handler.send_response(200)
80 handler.send_header('Content-Type', 'audio/mpeg')
81 handler.send_header('Content-Length', fsize)
82 handler.send_header('Connection', 'close')
83 handler.end_headers()
84 f = file(fname, 'rb')
85 shutil.copyfileobj(f, handler.wfile)
87 def QueryContainer(self, handler, query):
89 def AudioFileFilter(f, filter_type=None):
91 if filter_type:
92 filter_start = filter_type.split('/')[0]
93 else:
94 filter_start = filter_type
96 if os.path.isdir(f):
97 ftype = self.DIRECTORY
99 elif eyeD3.isMp3File(f):
100 ftype = self.AUDIO
101 elif os.path.splitext(f)[1].lower() in PLAYLISTS:
102 ftype = self.PLAYLIST
103 else:
104 ftype = False
106 if filter_start == self.AUDIO:
107 if ftype == self.AUDIO:
108 return ftype
109 else:
110 return False
111 else:
112 return ftype
114 def media_data(f):
115 if f.name in self.media_data_cache:
116 return self.media_data_cache[f.name]
118 item = {}
119 item['path'] = f.name
120 item['part_path'] = f.name.replace(local_base_path, '', 1)
121 item['name'] = os.path.split(f.name)[1]
122 item['is_dir'] = f.isdir
123 item['is_playlist'] = f.isplay
125 if f.title:
126 item['Title'] = f.title
128 if f.duration > 0:
129 item['Duration'] = f.duration
131 if f.isdir or f.isplay or '://' in f.name:
132 self.media_data_cache[f.name] = item
133 return item
135 try:
136 audioFile = eyeD3.Mp3AudioFile(unicode(f.name, 'utf-8'))
137 item['Duration'] = audioFile.getPlayTime() * 1000
139 tag = audioFile.getTag()
140 artist = tag.getArtist()
141 title = tag.getTitle()
142 if artist == 'Various Artists' and '/' in title:
143 artist, title = title.split('/')
144 item['ArtistName'] = artist.strip()
145 item['SongTitle'] = title.strip()
146 item['AlbumTitle'] = tag.getAlbum()
147 item['AlbumYear'] = tag.getYear()
148 item['MusicGenre'] = tag.getGenre().getName()
149 except Exception, msg:
150 print msg
152 self.media_data_cache[f.name] = item
153 return item
155 subcname = query['Container'][0]
156 cname = subcname.split('/')[0]
157 local_base_path = self.get_local_base_path(handler, query)
159 if not handler.server.containers.has_key(cname) or \
160 not self.get_local_path(handler, query):
161 handler.send_response(404)
162 handler.end_headers()
163 return
165 if os.path.splitext(subcname)[1].lower() in PLAYLISTS:
166 t = Template(playlist_template, filter=EncodeUnicode)
167 t.files, t.total, t.start = self.get_playlist(handler, query)
168 else:
169 t = Template(folder_template, filter=EncodeUnicode)
170 t.files, t.total, t.start = self.get_files(handler, query,
171 AudioFileFilter)
172 t.files = map(media_data, t.files)
173 t.container = cname
174 t.name = subcname
175 t.quote = quote
176 t.escape = escape
177 page = str(t)
179 handler.send_response(200)
180 handler.send_header('Content-Type', 'text/xml')
181 handler.send_header('Content-Length', len(page))
182 handler.send_header('Connection', 'close')
183 handler.end_headers()
184 handler.wfile.write(page)
186 def parse_playlist(self, list_name, recurse):
188 ext = os.path.splitext(list_name)[1].lower()
190 try:
191 url = list_name.index('http://')
192 list_name = list_name[url:]
193 list_file = urllib.urlopen(list_name)
194 except:
195 list_file = open(unicode(list_name, 'utf-8'))
196 local_path = os.path.sep.join(list_name.split(os.path.sep)[:-1])
198 if ext in ('.m3u', '.pls'):
199 charset = 'iso-8859-1'
200 else:
201 charset = 'utf-8'
203 if ext in ('.wpl', '.asx', '.wax', '.wvx', '.b4s'):
204 playlist = []
205 for line in list_file:
206 line = unicode(line, charset).encode('utf-8')
207 if ext == '.wpl':
208 s = wplfile(line)
209 elif ext == '.b4s':
210 s = b4sfile(line)
211 else:
212 s = asxfile(line)
213 if s:
214 playlist.append(FileData(s.group(1), False))
216 elif ext == '.pls':
217 names, titles, lengths = {}, {}, {}
218 for line in list_file:
219 line = unicode(line, charset).encode('utf-8')
220 s = plsfile(line)
221 if s:
222 names[s.group(1)] = s.group(2)
223 else:
224 s = plstitle(line)
225 if s:
226 titles[s.group(1)] = s.group(2)
227 else:
228 s = plslength(line)
229 if s:
230 lengths[s.group(1)] = int(s.group(2))
231 playlist = []
232 for key in names:
233 f = FileData(names[key], False)
234 if key in titles:
235 f.title = titles[key]
236 if key in lengths:
237 f.duration = lengths[key]
238 playlist.append(f)
240 else: # ext == '.m3u' or '.m3u8' or '.ram'
241 duration, title = 0, ''
242 playlist = []
243 for line in list_file:
244 line = unicode(line.strip(), charset).encode('utf-8')
245 if line:
246 if line.startswith('#EXTINF:'):
247 try:
248 duration, title = line[8:].split(',')
249 duration = int(duration)
250 except ValueError:
251 duration = 0
253 elif not line.startswith('#'):
254 f = FileData(line, False)
255 f.title = title.strip()
256 f.duration = duration
257 playlist.append(f)
258 duration, title = 0, ''
260 list_file.close()
262 # Expand relative paths
263 for i in xrange(len(playlist)):
264 if not '://' in playlist[i].name:
265 name = playlist[i].name
266 if not os.path.isabs(name):
267 name = os.path.join(local_path, name)
268 playlist[i].name = os.path.normpath(name)
270 if recurse:
271 newlist = []
272 for i in playlist:
273 if i.isplay:
274 newlist.extend(self.parse_playlist(i.name, recurse))
275 else:
276 newlist.append(i)
278 playlist = newlist
280 return playlist
282 def get_files(self, handler, query, filterFunction=None):
284 class SortList:
285 def __init__(self, files):
286 self.files = files
287 self.unsorted = True
288 self.sortby = None
289 self.last_start = 0
291 def build_recursive_list(path, recurse=True):
292 files = []
293 path = unicode(path, 'utf-8')
294 for f in os.listdir(path):
295 f = os.path.join(path, f)
296 isdir = os.path.isdir(f)
297 f = f.encode('utf-8')
298 if recurse and isdir:
299 files.extend(build_recursive_list(f))
300 else:
301 fd = FileData(f, isdir)
302 if recurse and fd.isplay:
303 files.extend(self.parse_playlist(f, recurse))
304 elif isdir or filterFunction(f, file_type):
305 files.append(fd)
306 return files
308 def dir_sort(x, y):
309 if x.isdir == y.isdir:
310 if x.isplay == y.isplay:
311 return name_sort(x, y)
312 else:
313 return y.isplay - x.isplay
314 else:
315 return y.isdir - x.isdir
317 def name_sort(x, y):
318 return cmp(x.name, y.name)
320 subcname = query['Container'][0]
321 cname = subcname.split('/')[0]
322 path = self.get_local_path(handler, query)
324 file_type = query.get('Filter', [''])[0]
326 recurse = query.get('Recurse',['No'])[0] == 'Yes'
328 if recurse and path in self.recurse_cache:
329 filelist = self.recurse_cache[path]
330 elif not recurse and path in self.dir_cache:
331 filelist = self.dir_cache[path]
332 else:
333 filelist = SortList(build_recursive_list(path, recurse))
335 if recurse:
336 self.recurse_cache[path] = filelist
337 else:
338 self.dir_cache[path] = filelist
340 # Sort it
341 seed = ''
342 start = ''
343 sortby = query.get('SortOrder', ['Normal'])[0]
344 if 'Random' in sortby:
345 if 'RandomSeed' in query:
346 seed = query['RandomSeed'][0]
347 sortby += seed
348 if 'RandomStart' in query:
349 start = query['RandomStart'][0]
350 sortby += start
352 if filelist.unsorted or filelist.sortby != sortby:
353 if 'Random' in sortby:
354 self.random_lock.acquire()
355 if seed:
356 random.seed(seed)
357 random.shuffle(filelist.files)
358 self.random_lock.release()
359 if start:
360 local_base_path = self.get_local_base_path(handler, query)
361 start = unquote(start)
362 start = start.replace(os.path.sep + cname,
363 local_base_path, 1)
364 filenames = [x.name for x in filelist.files]
365 try:
366 index = filenames.index(start)
367 i = filelist.files.pop(index)
368 filelist.files.insert(0, i)
369 except ValueError:
370 print 'Start not found:', start
371 else:
372 filelist.files.sort(dir_sort)
374 filelist.sortby = sortby
375 filelist.unsorted = False
377 files = filelist.files[:]
379 # Trim the list
380 files, total, start = self.item_count(handler, query, cname, files,
381 filelist.last_start)
382 filelist.last_start = start
383 return files, total, start
385 def get_playlist(self, handler, query):
386 subcname = query['Container'][0]
387 cname = subcname.split('/')[0]
389 try:
390 url = subcname.index('http://')
391 list_name = subcname[url:]
392 except:
393 list_name = self.get_local_path(handler, query)
395 recurse = query.get('Recurse',['No'])[0] == 'Yes'
396 playlist = self.parse_playlist(list_name, recurse)
398 # Trim the list
399 return self.item_count(handler, query, cname, playlist)