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
10 SCRIPTDIR
= os
.path
.dirname(__file__
)
14 PLAYLISTS
= ('.m3u', '.ram', '.pls', '.b4s', '.wpl', '.asx', '.wax', '.wvx')
16 # Search strings for different playlist types
17 asxfile
= re
.compile('ref +href *= *"(.+)"', re
.IGNORECASE
).search
18 wplfile
= re
.compile('media +src *= *"(.+)"', re
.IGNORECASE
).search
19 b4sfile
= re
.compile('Playstring="file:(.+)"').search
20 plsfile
= re
.compile('[Ff]ile(\d+)=(.+)').match
21 plstitle
= re
.compile('[Tt]itle(\d+)=(.+)').match
22 plslength
= re
.compile('[Ll]ength(\d+)=(\d+)').match
24 if os
.path
.sep
== '/':
26 unquote
= urllib
.unquote_plus
28 quote
= lambda x
: urllib
.quote(x
.replace(os
.path
.sep
, '/'))
29 unquote
= lambda x
: urllib
.unquote_plus(x
).replace('/', os
.path
.sep
)
32 def __init__(self
, name
, isdir
):
35 self
.isplay
= os
.path
.splitext(name
)[1].lower() in PLAYLISTS
39 class EncodeUnicode(Filter
):
40 def filter(self
, val
, **kw
):
41 """Encode Unicode strings, by default in UTF-8"""
43 if kw
.has_key('encoding'):
44 encoding
= kw
['encoding']
48 if type(val
) == type(u
''):
49 filtered
= val
.encode(encoding
)
56 CONTENT_TYPE
= 'x-container/tivo-music'
62 media_data_cache
= LRUCache(300)
64 def send_file(self
, handler
, container
, name
):
65 o
= urlparse("http://fake.host" + handler
.path
)
67 fname
= container
['path'] + path
[len(name
) + 1:]
68 fsize
= os
.path
.getsize(fname
)
69 handler
.send_response(200)
70 handler
.send_header('Content-Type', 'audio/mpeg')
71 handler
.send_header('Content-Length', fsize
)
72 handler
.send_header('Connection', 'close')
75 shutil
.copyfileobj(f
, handler
.wfile
)
77 def QueryContainer(self
, handler
, query
):
79 def AudioFileFilter(f
, filter_type
=None):
82 filter_start
= filter_type
.split('/')[0]
84 filter_start
= filter_type
87 ftype
= self
.DIRECTORY
89 elif eyeD3
.isMp3File(f
):
91 elif os
.path
.splitext(f
)[1].lower() in PLAYLISTS
:
96 if filter_start
== self
.AUDIO
:
97 if ftype
== self
.AUDIO
:
105 if f
.name
in self
.media_data_cache
:
106 return self
.media_data_cache
[f
.name
]
109 item
['path'] = f
.name
110 item
['part_path'] = f
.name
.replace(local_base_path
, '')
111 item
['name'] = os
.path
.split(f
.name
)[1]
112 item
['is_dir'] = f
.isdir
113 item
['is_playlist'] = f
.isplay
116 item
['Title'] = f
.title
119 item
['Duration'] = f
.duration
121 if f
.isdir
or f
.isplay
or '://' in f
.name
:
122 self
.media_data_cache
[f
.name
] = item
126 audioFile
= eyeD3
.Mp3AudioFile(f
.name
)
127 item
['Duration'] = audioFile
.getPlayTime() * 1000
129 tag
= audioFile
.getTag()
130 item
['ArtistName'] = tag
.getArtist()
131 item
['AlbumTitle'] = tag
.getAlbum()
132 item
['SongTitle'] = tag
.getTitle()
133 item
['AlbumYear'] = tag
.getYear()
134 item
['MusicGenre'] = tag
.getGenre().getName()
135 except Exception, msg
:
138 self
.media_data_cache
[f
.name
] = item
141 subcname
= query
['Container'][0]
142 cname
= subcname
.split('/')[0]
143 local_base_path
= self
.get_local_base_path(handler
, query
)
145 if not handler
.server
.containers
.has_key(cname
) or \
146 not self
.get_local_path(handler
, query
):
147 handler
.send_response(404)
148 handler
.end_headers()
151 if os
.path
.splitext(subcname
)[1].lower() in PLAYLISTS
:
152 t
= Template(file=os
.path
.join(SCRIPTDIR
, 'templates', 'm3u.tmpl'),
153 filter=EncodeUnicode
)
154 t
.files
, t
.total
, t
.start
= self
.get_playlist(handler
, query
)
156 t
= Template(file=os
.path
.join(SCRIPTDIR
,'templates',
157 'container.tmpl'), filter=EncodeUnicode
)
158 t
.files
, t
.total
, t
.start
= self
.get_files(handler
, query
,
160 t
.files
= map(media_data
, t
.files
)
167 handler
.send_response(200)
168 handler
.send_header('Content-Type', 'text/xml')
169 handler
.send_header('Content-Length', len(page
))
170 handler
.send_header('Connection', 'close')
171 handler
.end_headers()
172 handler
.wfile
.write(page
)
174 def item_count(self
, handler
, query
, cname
, files
):
175 """Return only the desired portion of the list, as specified by
176 ItemCount, AnchorItem and AnchorOffset
178 totalFiles
= len(files
)
181 if query
.has_key('ItemCount'):
182 count
= int(query
['ItemCount'][0])
184 if query
.has_key('AnchorItem'):
185 bs
= '/TiVoConnect?Command=QueryContainer&Container='
186 local_base_path
= self
.get_local_base_path(handler
, query
)
188 anchor
= query
['AnchorItem'][0]
189 if anchor
.startswith(bs
):
190 anchor
= anchor
.replace(bs
, '/')
191 anchor
= unquote(anchor
)
192 anchor
= anchor
.replace(os
.path
.sep
+ cname
, local_base_path
)
193 anchor
= os
.path
.normpath(anchor
)
195 filenames
= [x
.name
for x
in files
]
197 index
= filenames
.index(anchor
)
199 print 'Anchor not found:', anchor
# just use index = 0
204 if query
.has_key('AnchorOffset'):
205 index
+= int(query
['AnchorOffset'][0])
209 files
= files
[index
:index
+ count
]
212 if index
+ count
< 0:
214 files
= files
[index
+ count
:index
]
217 else: # No AnchorItem
220 files
= files
[:count
]
222 index
= count
% len(files
)
223 files
= files
[count
:]
225 return files
, totalFiles
, index
227 def get_files(self
, handler
, query
, filterFunction
=None):
229 def build_recursive_list(path
, recurse
=True):
231 for f
in os
.listdir(path
):
232 f
= os
.path
.join(path
, f
)
233 isdir
= os
.path
.isdir(f
)
234 if recurse
and isdir
:
235 files
.extend(build_recursive_list(f
))
237 if isdir
or filterFunction(f
, file_type
):
238 files
.append(FileData(f
, isdir
))
242 if x
.isdir
== y
.isdir
:
243 if x
.isplay
== y
.isplay
:
244 return name_sort(x
, y
)
246 return y
.isplay
- x
.isplay
248 return y
.isdir
- x
.isdir
251 return cmp(x
.name
, y
.name
)
253 subcname
= query
['Container'][0]
254 cname
= subcname
.split('/')[0]
255 path
= self
.get_local_path(handler
, query
)
257 file_type
= query
.get('Filter', [''])[0]
259 recurse
= query
.get('Recurse',['No'])[0] == 'Yes'
260 filelist
= build_recursive_list(path
, recurse
)
263 if query
.get('SortOrder',['Normal'])[0] == 'Random':
264 seed
= query
.get('RandomSeed', ['1'])[0]
265 self
.random_lock
.acquire()
267 random
.shuffle(filelist
)
268 self
.random_lock
.release()
270 filelist
.sort(dir_sort
)
273 return self
.item_count(handler
, query
, cname
, filelist
)
275 def get_playlist(self
, handler
, query
):
276 subcname
= query
['Container'][0]
277 cname
= subcname
.split('/')[0]
279 list_name
= self
.get_local_path(handler
, query
)
280 local_path
= os
.path
.sep
.join(list_name
.split(os
.path
.sep
)[:-1])
281 ext
= os
.path
.splitext(list_name
)[1].lower()
283 if ext
in ('.wpl', '.asx', '.wax', '.wvx', '.b4s'):
285 for line
in file(list_name
):
293 playlist
.append(FileData(s
.group(1), False))
296 names
, titles
, lengths
= {}, {}, {}
297 for line
in file(list_name
):
300 names
[s
.group(1)] = s
.group(2)
304 titles
[s
.group(1)] = s
.group(2)
308 lengths
[s
.group(1)] = int(s
.group(2))
311 f
= FileData(names
[key
], False)
313 f
.title
= titles
[key
]
315 f
.duration
= lengths
[key
]
318 else: # ext == '.m3u' or '.ram'
319 duration
, title
= 0, ''
321 for x
in file(list_name
):
324 if x
.startswith('#EXTINF:'):
326 duration
, title
= x
[8:].split(',')
327 duration
= int(duration
)
331 elif not x
.startswith('#'):
332 f
= FileData(x
, False)
333 f
.title
= title
.strip()
334 f
.duration
= duration
336 duration
, title
= 0, ''
338 # Expand relative paths
339 for i
in xrange(len(playlist
)):
340 if not '://' in playlist
[i
].name
:
341 name
= playlist
[i
].name
342 if not os
.path
.isabs(name
):
343 name
= os
.path
.join(local_path
, name
)
344 playlist
[i
].name
= os
.path
.normpath(name
)
347 return self
.item_count(handler
, query
, cname
, playlist
)