11 from urlparse
import urlparse
12 from xml
.sax
.saxutils
import escape
15 from Cheetah
.Template
import Template
16 from Cheetah
.Filters
import Filter
17 from lrucache
import LRUCache
19 from plugin
import Plugin
, quote
, unquote
20 from plugins
.video
.transcode
import kill
22 SCRIPTDIR
= os
.path
.dirname(__file__
)
25 return config
.get('Server', 'ffmpeg')
29 PLAYLISTS
= ('.m3u', '.m3u8', '.ram', '.pls', '.b4s', '.wpl', '.asx',
32 TRANSCODE
= ('.mp4', '.m4a', '.flc', '.ogg', '.wma', '.aac', '.wav',
33 '.aif', '.aiff', '.au', '.flac')
35 # Search strings for different playlist types
36 asxfile
= re
.compile('ref +href *= *"(.+)"', re
.IGNORECASE
).search
37 wplfile
= re
.compile('media +src *= *"(.+)"', re
.IGNORECASE
).search
38 b4sfile
= re
.compile('Playstring="file:(.+)"').search
39 plsfile
= re
.compile('[Ff]ile(\d+)=(.+)').match
40 plstitle
= re
.compile('[Tt]itle(\d+)=(.+)').match
41 plslength
= re
.compile('[Ll]ength(\d+)=(\d+)').match
43 # Duration -- parse from ffmpeg output
44 durre
= re
.compile(r
'.*Duration: ([0-9]+):([0-9]+):([0-9]+)\.([0-9]+),').search
46 # Preload the templates
47 tfname
= os
.path
.join(SCRIPTDIR
, 'templates', 'container.tmpl')
48 tpname
= os
.path
.join(SCRIPTDIR
, 'templates', 'm3u.tmpl')
49 FOLDER_TEMPLATE
= file(tfname
, 'rb').read()
50 PLAYLIST_TEMPLATE
= file(tpname
, 'rb').read()
53 # subprocess is broken for me on windows so super hack
54 def patchSubprocess():
55 o
= subprocess
.Popen
._make
_inheritable
57 def _make_inheritable(self
, handle
):
58 if not handle
: return subprocess
.GetCurrentProcess()
59 return o(self
, handle
)
61 subprocess
.Popen
._make
_inheritable
= _make_inheritable
63 mswindows
= (sys
.platform
== "win32")
68 def __init__(self
, name
, isdir
):
71 self
.isplay
= os
.path
.splitext(name
)[1].lower() in PLAYLISTS
75 class EncodeUnicode(Filter
):
76 def filter(self
, val
, **kw
):
77 """Encode Unicode strings, by default in UTF-8"""
79 encoding
= kw
.get('encoding', 'utf8')
81 if type(val
) == type(u
''):
82 filtered
= val
.encode(encoding
)
89 CONTENT_TYPE
= 'x-container/tivo-music'
95 media_data_cache
= LRUCache(300)
96 recurse_cache
= LRUCache(5)
97 dir_cache
= LRUCache(10)
99 def send_file(self
, handler
, container
, name
):
100 seek
, duration
= 0, 0
103 path
, query
= handler
.path
.split('?')
107 opts
= cgi
.parse_qs(query
)
108 seek
= int(opts
.get('Seek', [0])[0])
109 duration
= int(opts
.get('Duration', [0])[0])
111 fname
= os
.path
.join(os
.path
.normpath(container
['path']),
112 unquote(path
)[len(name
) + 2:])
113 fname
= unicode(fname
, 'utf-8')
115 needs_transcode
= (os
.path
.splitext(fname
)[1].lower() in TRANSCODE
118 handler
.send_response(200)
119 handler
.send_header('Content-Type', 'audio/mpeg')
120 if not needs_transcode
:
121 fsize
= os
.path
.getsize(fname
)
122 handler
.send_header('Content-Length', fsize
)
123 handler
.send_header('Connection', 'close')
124 handler
.end_headers()
128 fname
= fname
.encode('iso8859-1')
129 cmd
= [ffmpeg_path(), '-i', fname
, '-ab',
130 '320k', '-ar', '44100', '-f', 'mp3', '-']
132 cmd
[-1:] = ['-ss', '%.3f' % (seek
/ 1000.0), '-']
134 cmd
[-1:] = ['-t', '%.3f' % (duration
/ 1000.0), '-']
136 ffmpeg
= subprocess
.Popen(cmd
, stdout
=subprocess
.PIPE
)
138 shutil
.copyfileobj(ffmpeg
.stdout
, handler
.wfile
)
142 f
= file(fname
, 'rb')
144 shutil
.copyfileobj(f
, handler
.wfile
)
148 def QueryContainer(self
, handler
, query
):
150 def AudioFileFilter(f
, filter_type
=None):
151 ext
= os
.path
.splitext(f
)[1].lower()
153 if ext
in ('.mp3', '.mp2') or ext
in TRANSCODE
:
158 if not filter_type
or filter_type
.split('/')[0] != self
.AUDIO
:
160 file_type
= self
.PLAYLIST
161 elif os
.path
.isdir(f
):
162 file_type
= self
.DIRECTORY
167 if f
.name
in self
.media_data_cache
:
168 return self
.media_data_cache
[f
.name
]
171 item
['path'] = f
.name
172 item
['part_path'] = f
.name
.replace(local_base_path
, '', 1)
173 item
['name'] = os
.path
.split(f
.name
)[1]
174 item
['is_dir'] = f
.isdir
175 item
['is_playlist'] = f
.isplay
176 item
['params'] = 'No'
179 item
['Title'] = f
.title
182 item
['Duration'] = f
.duration
184 if f
.isdir
or f
.isplay
or '://' in f
.name
:
185 self
.media_data_cache
[f
.name
] = item
188 if os
.path
.splitext(f
.name
)[1].lower() in TRANSCODE
:
189 # If the format is: (track #) Song name...
190 #artist, album, track = f.name.split(os.path.sep)[-3:]
191 #track = os.path.splitext(track)[0]
192 #if track[0].isdigit:
193 # track = ' '.join(track.split(' ')[1:])
195 #item['SongTitle'] = track
196 #item['AlbumTitle'] = album
197 #item['ArtistName'] = artist
198 fname
= unicode(f
.name
, 'utf-8')
200 fname
= fname
.encode('iso8859-1')
201 cmd
= [ffmpeg_path(), '-i', fname
]
202 ffmpeg
= subprocess
.Popen(cmd
, stderr
=subprocess
.PIPE
,
203 stdout
=subprocess
.PIPE
,
204 stdin
=subprocess
.PIPE
)
206 # wait 10 sec if ffmpeg is not back give up
207 for i
in xrange(200):
209 if not ffmpeg
.poll() == None:
212 if ffmpeg
.poll() != None:
213 output
= ffmpeg
.stderr
.read()
216 millisecs
= ((int(d
.group(1)) * 3600 +
217 int(d
.group(2)) * 60 +
218 int(d
.group(3))) * 1000 +
220 (10 ** (3 - len(d
.group(4)))))
223 item
['Duration'] = millisecs
226 audioFile
= eyeD3
.Mp3AudioFile(unicode(f
.name
, 'utf-8'))
227 item
['Duration'] = audioFile
.getPlayTime() * 1000
229 tag
= audioFile
.getTag()
230 artist
= tag
.getArtist()
231 title
= tag
.getTitle()
232 if artist
== 'Various Artists' and '/' in title
:
233 artist
, title
= title
.split('/')
234 item
['ArtistName'] = artist
.strip()
235 item
['SongTitle'] = title
.strip()
236 item
['AlbumTitle'] = tag
.getAlbum()
237 item
['AlbumYear'] = tag
.getYear()
238 item
['MusicGenre'] = tag
.getGenre().getName()
239 except Exception, msg
:
242 if 'Duration' in item
:
243 item
['params'] = 'Yes'
245 self
.media_data_cache
[f
.name
] = item
248 subcname
= query
['Container'][0]
249 cname
= subcname
.split('/')[0]
250 local_base_path
= self
.get_local_base_path(handler
, query
)
252 if (not handler
.server
.containers
.has_key(cname
) or
253 not self
.get_local_path(handler
, query
)):
254 handler
.send_response(404)
255 handler
.end_headers()
258 if os
.path
.splitext(subcname
)[1].lower() in PLAYLISTS
:
259 t
= Template(PLAYLIST_TEMPLATE
, filter=EncodeUnicode
)
260 t
.files
, t
.total
, t
.start
= self
.get_playlist(handler
, query
)
262 t
= Template(FOLDER_TEMPLATE
, filter=EncodeUnicode
)
263 t
.files
, t
.total
, t
.start
= self
.get_files(handler
, query
,
265 t
.files
= map(media_data
, t
.files
)
272 handler
.send_response(200)
273 handler
.send_header('Content-Type', 'text/xml')
274 handler
.send_header('Content-Length', len(page
))
275 handler
.send_header('Connection', 'close')
276 handler
.end_headers()
277 handler
.wfile
.write(page
)
279 def parse_playlist(self
, list_name
, recurse
):
281 ext
= os
.path
.splitext(list_name
)[1].lower()
284 url
= list_name
.index('http://')
285 list_name
= list_name
[url
:]
286 list_file
= urllib
.urlopen(list_name
)
288 list_file
= open(unicode(list_name
, 'utf-8'))
289 local_path
= os
.path
.sep
.join(list_name
.split(os
.path
.sep
)[:-1])
291 if ext
in ('.m3u', '.pls'):
292 charset
= 'iso-8859-1'
296 if ext
in ('.wpl', '.asx', '.wax', '.wvx', '.b4s'):
298 for line
in list_file
:
299 line
= unicode(line
, charset
).encode('utf-8')
307 playlist
.append(FileData(s
.group(1), False))
310 names
, titles
, lengths
= {}, {}, {}
311 for line
in list_file
:
312 line
= unicode(line
, charset
).encode('utf-8')
315 names
[s
.group(1)] = s
.group(2)
319 titles
[s
.group(1)] = s
.group(2)
323 lengths
[s
.group(1)] = int(s
.group(2))
326 f
= FileData(names
[key
], False)
328 f
.title
= titles
[key
]
330 f
.duration
= lengths
[key
]
333 else: # ext == '.m3u' or '.m3u8' or '.ram'
334 duration
, title
= 0, ''
336 for line
in list_file
:
337 line
= unicode(line
.strip(), charset
).encode('utf-8')
339 if line
.startswith('#EXTINF:'):
341 duration
, title
= line
[8:].split(',')
342 duration
= int(duration
)
346 elif not line
.startswith('#'):
347 f
= FileData(line
, False)
348 f
.title
= title
.strip()
349 f
.duration
= duration
351 duration
, title
= 0, ''
355 # Expand relative paths
356 for i
in xrange(len(playlist
)):
357 if not '://' in playlist
[i
].name
:
358 name
= playlist
[i
].name
359 if not os
.path
.isabs(name
):
360 name
= os
.path
.join(local_path
, name
)
361 playlist
[i
].name
= os
.path
.normpath(name
)
367 newlist
.extend(self
.parse_playlist(i
.name
, recurse
))
375 def get_files(self
, handler
, query
, filterFunction
=None):
378 def __init__(self
, files
):
384 def build_recursive_list(path
, recurse
=True):
386 path
= unicode(path
, 'utf-8')
388 for f
in os
.listdir(path
):
389 if f
.startswith('.'):
391 f
= os
.path
.join(path
, f
)
392 isdir
= os
.path
.isdir(f
)
393 f
= f
.encode('utf-8')
394 if recurse
and isdir
:
395 files
.extend(build_recursive_list(f
))
397 fd
= FileData(f
, isdir
)
398 if recurse
and fd
.isplay
:
399 files
.extend(self
.parse_playlist(f
, recurse
))
400 elif isdir
or filterFunction(f
, file_type
):
407 if x
.isdir
== y
.isdir
:
408 if x
.isplay
== y
.isplay
:
409 return name_sort(x
, y
)
411 return y
.isplay
- x
.isplay
413 return y
.isdir
- x
.isdir
416 return cmp(x
.name
, y
.name
)
418 subcname
= query
['Container'][0]
419 cname
= subcname
.split('/')[0]
420 path
= self
.get_local_path(handler
, query
)
422 file_type
= query
.get('Filter', [''])[0]
424 recurse
= query
.get('Recurse', ['No'])[0] == 'Yes'
427 if recurse
and path
in self
.recurse_cache
:
428 if self
.recurse_cache
.mtime(path
) + 3600 >= time
.time():
429 filelist
= self
.recurse_cache
[path
]
430 elif not recurse
and path
in self
.dir_cache
:
431 if self
.dir_cache
.mtime(path
) >= os
.stat(path
)[8]:
432 filelist
= self
.dir_cache
[path
]
435 filelist
= SortList(build_recursive_list(path
, recurse
))
438 self
.recurse_cache
[path
] = filelist
440 self
.dir_cache
[path
] = filelist
445 sortby
= query
.get('SortOrder', ['Normal'])[0]
446 if 'Random' in sortby
:
447 if 'RandomSeed' in query
:
448 seed
= query
['RandomSeed'][0]
450 if 'RandomStart' in query
:
451 start
= query
['RandomStart'][0]
454 if filelist
.unsorted
or filelist
.sortby
!= sortby
:
455 if 'Random' in sortby
:
456 self
.random_lock
.acquire()
459 random
.shuffle(filelist
.files
)
460 self
.random_lock
.release()
462 local_base_path
= self
.get_local_base_path(handler
, query
)
463 start
= unquote(start
)
464 start
= start
.replace(os
.path
.sep
+ cname
,
466 filenames
= [x
.name
for x
in filelist
.files
]
468 index
= filenames
.index(start
)
469 i
= filelist
.files
.pop(index
)
470 filelist
.files
.insert(0, i
)
472 print 'Start not found:', start
474 filelist
.files
.sort(dir_sort
)
476 filelist
.sortby
= sortby
477 filelist
.unsorted
= False
479 files
= filelist
.files
[:]
482 files
, total
, start
= self
.item_count(handler
, query
, cname
, files
,
484 filelist
.last_start
= start
485 return files
, total
, start
487 def get_playlist(self
, handler
, query
):
488 subcname
= query
['Container'][0]
489 cname
= subcname
.split('/')[0]
492 url
= subcname
.index('http://')
493 list_name
= subcname
[url
:]
495 list_name
= self
.get_local_path(handler
, query
)
497 recurse
= query
.get('Recurse', ['No'])[0] == 'Yes'
498 playlist
= self
.parse_playlist(list_name
, recurse
)
501 if 'Random' in query
.get('SortOrder', ['Normal'])[0]:
502 seed
= query
.get('RandomSeed', [''])[0]
503 start
= query
.get('RandomStart', [''])[0]
505 self
.random_lock
.acquire()
508 random
.shuffle(playlist
)
509 self
.random_lock
.release()
511 local_base_path
= self
.get_local_base_path(handler
, query
)
512 start
= unquote(start
)
513 start
= start
.replace(os
.path
.sep
+ cname
,
515 filenames
= [x
.name
for x
in playlist
]
517 index
= filenames
.index(start
)
518 i
= playlist
.pop(index
)
519 playlist
.insert(0, i
)
521 print 'Start not found:', start
524 return self
.item_count(handler
, query
, cname
, playlist
)