12 from xml
.sax
.saxutils
import escape
15 from mutagen
.easyid3
import EasyID3
16 from mutagen
.mp3
import MP3
17 from Cheetah
.Template
import Template
18 from lrucache
import LRUCache
20 from plugin
import EncodeUnicode
, Plugin
, quote
, unquote
21 from plugins
.video
.transcode
import kill
23 SCRIPTDIR
= os
.path
.dirname(__file__
)
27 PLAYLISTS
= ('.m3u', '.m3u8', '.ram', '.pls', '.b4s', '.wpl', '.asx',
30 TRANSCODE
= ('.mp4', '.m4a', '.flc', '.ogg', '.wma', '.aac', '.wav',
31 '.aif', '.aiff', '.au', '.flac')
33 TAGNAMES
= {'artist': ['\xa9ART', 'Author'],
34 'title': ['\xa9nam', 'Title'],
35 'album': ['\xa9alb', u
'WM/AlbumTitle'],
36 'date': ['\xa9day', u
'WM/Year'],
37 'genre': ['\xa9gen', u
'WM/Genre']}
41 # Search strings for different playlist types
42 asxfile
= re
.compile('ref +href *= *"([^"]*)"', re
.IGNORECASE
).search
43 wplfile
= re
.compile('media +src *= *"([^"]*)"', re
.IGNORECASE
).search
44 b4sfile
= re
.compile('Playstring="file:([^"]*)"').search
45 plsfile
= re
.compile('[Ff]ile(\d+)=(.+)').match
46 plstitle
= re
.compile('[Tt]itle(\d+)=(.+)').match
47 plslength
= re
.compile('[Ll]ength(\d+)=(\d+)').match
49 # Duration -- parse from ffmpeg output
50 durre
= re
.compile(r
'.*Duration: ([0-9]+):([0-9]+):([0-9]+)\.([0-9]+),').search
52 # Preload the templates
53 tfname
= os
.path
.join(SCRIPTDIR
, 'templates', 'container.tmpl')
54 tpname
= os
.path
.join(SCRIPTDIR
, 'templates', 'm3u.tmpl')
55 iname
= os
.path
.join(SCRIPTDIR
, 'templates', 'item.tmpl')
56 FOLDER_TEMPLATE
= file(tfname
, 'rb').read()
57 PLAYLIST_TEMPLATE
= file(tpname
, 'rb').read()
58 ITEM_TEMPLATE
= file(iname
, 'rb').read()
61 # subprocess is broken for me on windows so super hack
62 def patchSubprocess():
63 o
= subprocess
.Popen
._make
_inheritable
65 def _make_inheritable(self
, handle
):
66 if not handle
: return subprocess
.GetCurrentProcess()
67 return o(self
, handle
)
69 subprocess
.Popen
._make
_inheritable
= _make_inheritable
71 mswindows
= (sys
.platform
== "win32")
76 def __init__(self
, name
, isdir
):
79 self
.isplay
= os
.path
.splitext(name
)[1].lower() in PLAYLISTS
85 CONTENT_TYPE
= 'x-container/tivo-music'
91 media_data_cache
= LRUCache(300)
92 recurse_cache
= LRUCache(5)
93 dir_cache
= LRUCache(10)
95 def send_file(self
, handler
, path
, query
):
96 seek
= int(query
.get('Seek', [0])[0])
97 duration
= int(query
.get('Duration', [0])[0])
98 always
= (handler
.container
.get('force_ffmpeg',
99 'False').lower() == 'true' and config
.get_bin('ffmpeg'))
100 fname
= unicode(path
, 'utf-8')
102 ext
= os
.path
.splitext(fname
)[1].lower()
103 needs_transcode
= ext
in TRANSCODE
or seek
or duration
or always
105 if not needs_transcode
:
106 fsize
= os
.path
.getsize(fname
)
107 handler
.send_response(200)
108 handler
.send_header('Content-Length', fsize
)
110 handler
.send_response(206)
111 handler
.send_header('Transfer-Encoding', 'chunked')
112 handler
.send_header('Content-Type', 'audio/mpeg')
113 handler
.end_headers()
117 fname
= fname
.encode('iso8859-1')
119 cmd
= [config
.get_bin('ffmpeg'), '-i', fname
, '-vn']
120 if ext
in ['.mp3', '.mp2']:
121 cmd
+= ['-acodec', 'copy']
123 cmd
+= ['-ab', '320k', '-ar', '44100']
124 cmd
+= ['-f', 'mp3', '-']
126 cmd
[-1:] = ['-ss', '%.3f' % (seek
/ 1000.0), '-']
128 cmd
[-1:] = ['-t', '%.3f' % (duration
/ 1000.0), '-']
130 ffmpeg
= subprocess
.Popen(cmd
, bufsize
=BLOCKSIZE
,
131 stdout
=subprocess
.PIPE
)
134 block
= ffmpeg
.stdout
.read(BLOCKSIZE
)
135 handler
.wfile
.write('%x\r\n' % len(block
))
136 handler
.wfile
.write(block
)
137 handler
.wfile
.write('\r\n')
139 handler
.wfile
.flush()
140 except Exception, msg
:
141 handler
.server
.logger
.info(msg
)
148 f
= open(fname
, 'rb')
150 shutil
.copyfileobj(f
, handler
.wfile
)
155 def QueryContainer(self
, handler
, query
):
157 def AudioFileFilter(f
, filter_type
=None):
158 ext
= os
.path
.splitext(f
)[1].lower()
160 if ext
in ('.mp3', '.mp2') or (ext
in TRANSCODE
and
161 config
.get_bin('ffmpeg')):
166 if not filter_type
or filter_type
.split('/')[0] != self
.AUDIO
:
168 file_type
= self
.PLAYLIST
169 elif os
.path
.isdir(f
):
170 file_type
= self
.DIRECTORY
175 if f
.name
in self
.media_data_cache
:
176 return self
.media_data_cache
[f
.name
]
179 item
['path'] = f
.name
180 item
['part_path'] = f
.name
.replace(local_base_path
, '', 1)
181 item
['name'] = os
.path
.basename(f
.name
)
182 item
['is_dir'] = f
.isdir
183 item
['is_playlist'] = f
.isplay
184 item
['params'] = 'No'
187 item
['Title'] = f
.title
190 item
['Duration'] = f
.duration
192 if f
.isdir
or f
.isplay
or '://' in f
.name
:
193 self
.media_data_cache
[f
.name
] = item
196 # If the format is: (track #) Song name...
197 #artist, album, track = f.name.split(os.path.sep)[-3:]
198 #track = os.path.splitext(track)[0]
199 #if track[0].isdigit:
200 # track = ' '.join(track.split(' ')[1:])
202 #item['SongTitle'] = track
203 #item['AlbumTitle'] = album
204 #item['ArtistName'] = artist
206 ext
= os
.path
.splitext(f
.name
)[1].lower()
207 fname
= unicode(f
.name
, 'utf-8')
210 # If the file is an mp3, let's load the EasyID3 interface
212 audioFile
= MP3(fname
, ID3
=EasyID3
)
214 # Otherwise, let mutagen figure it out
215 audioFile
= mutagen
.File(fname
)
218 # Pull the length from the FileType, if present
219 if audioFile
.info
.length
> 0:
220 item
['Duration'] = int(audioFile
.info
.length
* 1000)
222 # Grab our other tags, if present
223 def get_tag(tagname
, d
):
224 for tag
in ([tagname
] + TAGNAMES
[tagname
]):
228 if type(value
) not in [str, unicode]:
235 artist
= get_tag('artist', audioFile
)
236 title
= get_tag('title', audioFile
)
237 if artist
== 'Various Artists' and '/' in title
:
238 artist
, title
= [x
.strip() for x
in title
.split('/')]
239 item
['ArtistName'] = artist
240 item
['SongTitle'] = title
241 item
['AlbumTitle'] = get_tag('album', audioFile
)
242 item
['AlbumYear'] = get_tag('date', audioFile
)[:4]
243 item
['MusicGenre'] = get_tag('genre', audioFile
)
244 except Exception, msg
:
247 ffmpeg_path
= config
.get_bin('ffmpeg')
248 if 'Duration' not in item
and ffmpeg_path
:
250 fname
= fname
.encode('iso8859-1')
251 cmd
= [ffmpeg_path
, '-i', fname
]
252 ffmpeg
= subprocess
.Popen(cmd
, stderr
=subprocess
.PIPE
,
253 stdout
=subprocess
.PIPE
,
254 stdin
=subprocess
.PIPE
)
256 # wait 10 sec if ffmpeg is not back give up
257 for i
in xrange(200):
259 if not ffmpeg
.poll() == None:
262 if ffmpeg
.poll() != None:
263 output
= ffmpeg
.stderr
.read()
266 millisecs
= ((int(d
.group(1)) * 3600 +
267 int(d
.group(2)) * 60 +
268 int(d
.group(3))) * 1000 +
270 (10 ** (3 - len(d
.group(4)))))
273 item
['Duration'] = millisecs
275 if 'Duration' in item
and ffmpeg_path
:
276 item
['params'] = 'Yes'
278 self
.media_data_cache
[f
.name
] = item
281 subcname
= query
['Container'][0]
282 local_base_path
= self
.get_local_base_path(handler
, query
)
284 if not self
.get_local_path(handler
, query
):
285 handler
.send_error(404)
288 if os
.path
.splitext(subcname
)[1].lower() in PLAYLISTS
:
289 t
= Template(PLAYLIST_TEMPLATE
, filter=EncodeUnicode
)
290 t
.files
, t
.total
, t
.start
= self
.get_playlist(handler
, query
)
292 t
= Template(FOLDER_TEMPLATE
, filter=EncodeUnicode
)
293 t
.files
, t
.total
, t
.start
= self
.get_files(handler
, query
,
295 t
.files
= map(media_data
, t
.files
)
296 t
.container
= handler
.cname
301 handler
.send_xml(str(t
))
303 def QueryItem(self
, handler
, query
):
304 uq
= urllib
.unquote_plus
305 splitpath
= [x
for x
in uq(query
['Url'][0]).split('/') if x
]
306 path
= os
.path
.join(handler
.container
['path'], *splitpath
[1:])
308 if path
in self
.media_data_cache
:
309 t
= Template(ITEM_TEMPLATE
, filter=EncodeUnicode
)
310 t
.file = self
.media_data_cache
[path
]
312 handler
.send_xml(str(t
))
314 handler
.send_error(404)
316 def parse_playlist(self
, list_name
, recurse
):
318 ext
= os
.path
.splitext(list_name
)[1].lower()
321 url
= list_name
.index('http://')
322 list_name
= list_name
[url
:]
323 list_file
= urllib
.urlopen(list_name
)
325 list_file
= open(unicode(list_name
, 'utf-8'))
326 local_path
= os
.path
.sep
.join(list_name
.split(os
.path
.sep
)[:-1])
328 if ext
in ('.m3u', '.pls'):
329 charset
= 'iso-8859-1'
333 if ext
in ('.wpl', '.asx', '.wax', '.wvx', '.b4s'):
335 for line
in list_file
:
336 line
= unicode(line
, charset
).encode('utf-8')
344 playlist
.append(FileData(s
.group(1), False))
347 names
, titles
, lengths
= {}, {}, {}
348 for line
in list_file
:
349 line
= unicode(line
, charset
).encode('utf-8')
352 names
[s
.group(1)] = s
.group(2)
356 titles
[s
.group(1)] = s
.group(2)
360 lengths
[s
.group(1)] = int(s
.group(2))
363 f
= FileData(names
[key
], False)
365 f
.title
= titles
[key
]
367 f
.duration
= lengths
[key
]
370 else: # ext == '.m3u' or '.m3u8' or '.ram'
371 duration
, title
= 0, ''
373 for line
in list_file
:
374 line
= unicode(line
.strip(), charset
).encode('utf-8')
376 if line
.startswith('#EXTINF:'):
378 duration
, title
= line
[8:].split(',', 1)
379 duration
= int(duration
)
383 elif not line
.startswith('#'):
384 f
= FileData(line
, False)
385 f
.title
= title
.strip()
386 f
.duration
= duration
388 duration
, title
= 0, ''
392 # Expand relative paths
393 for i
in xrange(len(playlist
)):
394 if not '://' in playlist
[i
].name
:
395 name
= playlist
[i
].name
396 if not os
.path
.isabs(name
):
397 name
= os
.path
.join(local_path
, name
)
398 playlist
[i
].name
= os
.path
.normpath(name
)
404 newlist
.extend(self
.parse_playlist(i
.name
, recurse
))
412 def get_files(self
, handler
, query
, filterFunction
=None):
415 def __init__(self
, files
):
421 def build_recursive_list(path
, recurse
=True):
423 path
= unicode(path
, 'utf-8')
425 for f
in os
.listdir(path
):
426 if f
.startswith('.'):
428 f
= os
.path
.join(path
, f
)
429 isdir
= os
.path
.isdir(f
)
430 if sys
.platform
== 'darwin':
431 f
= unicodedata
.normalize('NFC', f
)
432 f
= f
.encode('utf-8')
433 if recurse
and isdir
:
434 files
.extend(build_recursive_list(f
))
436 fd
= FileData(f
, isdir
)
437 if recurse
and fd
.isplay
:
438 files
.extend(self
.parse_playlist(f
, recurse
))
439 elif isdir
or filterFunction(f
, file_type
):
446 if x
.isdir
== y
.isdir
:
447 if x
.isplay
== y
.isplay
:
448 return name_sort(x
, y
)
450 return y
.isplay
- x
.isplay
452 return y
.isdir
- x
.isdir
455 return cmp(x
.name
, y
.name
)
457 path
= self
.get_local_path(handler
, query
)
459 file_type
= query
.get('Filter', [''])[0]
461 recurse
= query
.get('Recurse', ['No'])[0] == 'Yes'
464 rc
= self
.recurse_cache
470 updated
= os
.stat(unicode(path
, 'utf-8'))[8]
471 if path
in dc
and dc
.mtime(path
) >= updated
:
474 if path
.startswith(p
) and rc
.mtime(p
) < updated
:
478 filelist
= SortList(build_recursive_list(path
, recurse
))
488 sortby
= query
.get('SortOrder', ['Normal'])[0]
489 if 'Random' in sortby
:
490 if 'RandomSeed' in query
:
491 seed
= query
['RandomSeed'][0]
493 if 'RandomStart' in query
:
494 start
= query
['RandomStart'][0]
497 if filelist
.unsorted
or filelist
.sortby
!= sortby
:
498 if 'Random' in sortby
:
499 self
.random_lock
.acquire()
502 random
.shuffle(filelist
.files
)
503 self
.random_lock
.release()
505 local_base_path
= self
.get_local_base_path(handler
, query
)
506 start
= unquote(start
)
507 start
= start
.replace(os
.path
.sep
+ handler
.cname
,
509 filenames
= [x
.name
for x
in filelist
.files
]
511 index
= filenames
.index(start
)
512 i
= filelist
.files
.pop(index
)
513 filelist
.files
.insert(0, i
)
515 handler
.server
.logger
.warning('Start not found: ' +
518 filelist
.files
.sort(dir_sort
)
520 filelist
.sortby
= sortby
521 filelist
.unsorted
= False
523 files
= filelist
.files
[:]
526 files
, total
, start
= self
.item_count(handler
, query
, handler
.cname
,
527 files
, filelist
.last_start
)
528 filelist
.last_start
= start
529 return files
, total
, start
531 def get_playlist(self
, handler
, query
):
532 subcname
= query
['Container'][0]
535 url
= subcname
.index('http://')
536 list_name
= subcname
[url
:]
538 list_name
= self
.get_local_path(handler
, query
)
540 recurse
= query
.get('Recurse', ['No'])[0] == 'Yes'
541 playlist
= self
.parse_playlist(list_name
, recurse
)
544 if 'Random' in query
.get('SortOrder', ['Normal'])[0]:
545 seed
= query
.get('RandomSeed', [''])[0]
546 start
= query
.get('RandomStart', [''])[0]
548 self
.random_lock
.acquire()
551 random
.shuffle(playlist
)
552 self
.random_lock
.release()
554 local_base_path
= self
.get_local_base_path(handler
, query
)
555 start
= unquote(start
)
556 start
= start
.replace(os
.path
.sep
+ handler
.cname
,
558 filenames
= [x
.name
for x
in playlist
]
560 index
= filenames
.index(start
)
561 i
= playlist
.pop(index
)
562 playlist
.insert(0, i
)
564 handler
.server
.logger
.warning('Start not found: ' + start
)
567 return self
.item_count(handler
, query
, handler
.cname
, playlist
)