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', '.m3u8', '.ram', '.pls', '.b4s', '.wpl', '.asx',
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
== '/':
27 unquote
= urllib
.unquote_plus
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()
39 def __init__(self
, name
, isdir
):
42 self
.isplay
= os
.path
.splitext(name
)[1].lower() in PLAYLISTS
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']
55 if type(val
) == type(u
''):
56 filtered
= val
.encode(encoding
)
63 CONTENT_TYPE
= 'x-container/tivo-music'
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
)
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')
85 shutil
.copyfileobj(f
, handler
.wfile
)
87 def QueryContainer(self
, handler
, query
):
89 def AudioFileFilter(f
, filter_type
=None):
92 filter_start
= filter_type
.split('/')[0]
94 filter_start
= filter_type
97 ftype
= self
.DIRECTORY
99 elif eyeD3
.isMp3File(f
):
101 elif os
.path
.splitext(f
)[1].lower() in PLAYLISTS
:
102 ftype
= self
.PLAYLIST
106 if filter_start
== self
.AUDIO
:
107 if ftype
== self
.AUDIO
:
115 if f
.name
in self
.media_data_cache
:
116 return self
.media_data_cache
[f
.name
]
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
126 item
['Title'] = f
.title
129 item
['Duration'] = f
.duration
131 if f
.isdir
or f
.isplay
or '://' in f
.name
:
132 self
.media_data_cache
[f
.name
] = item
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
:
152 self
.media_data_cache
[f
.name
] = 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()
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
)
169 t
= Template(folder_template
, filter=EncodeUnicode
)
170 t
.files
, t
.total
, t
.start
= self
.get_files(handler
, query
,
172 t
.files
= map(media_data
, t
.files
)
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()
191 url
= list_name
.index('http://')
192 list_name
= list_name
[url
:]
193 list_file
= urllib
.urlopen(list_name
)
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'
203 if ext
in ('.wpl', '.asx', '.wax', '.wvx', '.b4s'):
205 for line
in list_file
:
206 line
= unicode(line
, charset
).encode('utf-8')
214 playlist
.append(FileData(s
.group(1), False))
217 names
, titles
, lengths
= {}, {}, {}
218 for line
in list_file
:
219 line
= unicode(line
, charset
).encode('utf-8')
222 names
[s
.group(1)] = s
.group(2)
226 titles
[s
.group(1)] = s
.group(2)
230 lengths
[s
.group(1)] = int(s
.group(2))
233 f
= FileData(names
[key
], False)
235 f
.title
= titles
[key
]
237 f
.duration
= lengths
[key
]
240 else: # ext == '.m3u' or '.m3u8' or '.ram'
241 duration
, title
= 0, ''
243 for line
in list_file
:
244 line
= unicode(line
.strip(), charset
).encode('utf-8')
246 if line
.startswith('#EXTINF:'):
248 duration
, title
= line
[8:].split(',')
249 duration
= int(duration
)
253 elif not line
.startswith('#'):
254 f
= FileData(line
, False)
255 f
.title
= title
.strip()
256 f
.duration
= duration
258 duration
, title
= 0, ''
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
)
274 newlist
.extend(self
.parse_playlist(i
.name
, recurse
))
282 def get_files(self
, handler
, query
, filterFunction
=None):
285 def __init__(self
, files
):
291 def build_recursive_list(path
, recurse
=True):
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
))
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
):
309 if x
.isdir
== y
.isdir
:
310 if x
.isplay
== y
.isplay
:
311 return name_sort(x
, y
)
313 return y
.isplay
- x
.isplay
315 return y
.isdir
- x
.isdir
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
]
333 filelist
= SortList(build_recursive_list(path
, recurse
))
336 self
.recurse_cache
[path
] = filelist
338 self
.dir_cache
[path
] = filelist
343 sortby
= query
.get('SortOrder', ['Normal'])[0]
344 if 'Random' in sortby
:
345 if 'RandomSeed' in query
:
346 seed
= query
['RandomSeed'][0]
348 if 'RandomStart' in query
:
349 start
= query
['RandomStart'][0]
352 if filelist
.unsorted
or filelist
.sortby
!= sortby
:
353 if 'Random' in sortby
:
354 self
.random_lock
.acquire()
357 random
.shuffle(filelist
.files
)
358 self
.random_lock
.release()
360 local_base_path
= self
.get_local_base_path(handler
, query
)
361 start
= unquote(start
)
362 start
= start
.replace(os
.path
.sep
+ cname
,
364 filenames
= [x
.name
for x
in filelist
.files
]
366 index
= filenames
.index(start
)
367 i
= filelist
.files
.pop(index
)
368 filelist
.files
.insert(0, i
)
370 print 'Start not found:', start
372 filelist
.files
.sort(dir_sort
)
374 filelist
.sortby
= sortby
375 filelist
.unsorted
= False
377 files
= filelist
.files
[:]
380 files
, total
, start
= self
.item_count(handler
, query
, cname
, files
,
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]
390 url
= subcname
.index('http://')
391 list_name
= subcname
[url
:]
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
)
399 return self
.item_count(handler
, query
, cname
, playlist
)