10 from xml
.sax
.saxutils
import escape
13 from mutagen
.easyid3
import EasyID3
14 from mutagen
.mp3
import MP3
15 from Cheetah
.Template
import Template
16 from lrucache
import LRUCache
18 from plugin
import EncodeUnicode
, Plugin
, quote
, unquote
19 from plugins
.video
.transcode
import kill
21 SCRIPTDIR
= os
.path
.dirname(__file__
)
25 PLAYLISTS
= ('.m3u', '.m3u8', '.ram', '.pls', '.b4s', '.wpl', '.asx',
28 TRANSCODE
= ('.mp4', '.m4a', '.flc', '.ogg', '.wma', '.aac', '.wav',
29 '.aif', '.aiff', '.au', '.flac')
31 TAGNAMES
= {'artist': ['\xa9ART', 'Author'],
32 'title': ['\xa9nam', 'Title'],
33 'album': ['\xa9alb', u
'WM/AlbumTitle'],
34 'date': ['\xa9day', u
'WM/Year'],
35 'genre': ['\xa9gen', u
'WM/Genre']}
39 # Search strings for different playlist types
40 asxfile
= re
.compile('ref +href *= *"([^"]*)"', re
.IGNORECASE
).search
41 wplfile
= re
.compile('media +src *= *"([^"]*)"', re
.IGNORECASE
).search
42 b4sfile
= re
.compile('Playstring="file:([^"]*)"').search
43 plsfile
= re
.compile('[Ff]ile(\d+)=(.+)').match
44 plstitle
= re
.compile('[Tt]itle(\d+)=(.+)').match
45 plslength
= re
.compile('[Ll]ength(\d+)=(\d+)').match
47 # Duration -- parse from ffmpeg output
48 durre
= re
.compile(r
'.*Duration: ([0-9]+):([0-9]+):([0-9]+)\.([0-9]+),').search
50 # Preload the templates
51 tfname
= os
.path
.join(SCRIPTDIR
, 'templates', 'container.tmpl')
52 tpname
= os
.path
.join(SCRIPTDIR
, 'templates', 'm3u.tmpl')
53 iname
= os
.path
.join(SCRIPTDIR
, 'templates', 'item.tmpl')
54 FOLDER_TEMPLATE
= file(tfname
, 'rb').read()
55 PLAYLIST_TEMPLATE
= file(tpname
, 'rb').read()
56 ITEM_TEMPLATE
= file(iname
, 'rb').read()
59 # subprocess is broken for me on windows so super hack
60 def patchSubprocess():
61 o
= subprocess
.Popen
._make
_inheritable
63 def _make_inheritable(self
, handle
):
64 if not handle
: return subprocess
.GetCurrentProcess()
65 return o(self
, handle
)
67 subprocess
.Popen
._make
_inheritable
= _make_inheritable
69 mswindows
= (sys
.platform
== "win32")
74 def __init__(self
, name
, isdir
):
77 self
.isplay
= os
.path
.splitext(name
)[1].lower() in PLAYLISTS
83 CONTENT_TYPE
= 'x-container/tivo-music'
89 media_data_cache
= LRUCache(300)
90 recurse_cache
= LRUCache(5)
91 dir_cache
= LRUCache(10)
93 def send_file(self
, handler
, path
, query
):
94 seek
= int(query
.get('Seek', [0])[0])
95 duration
= int(query
.get('Duration', [0])[0])
96 always
= (handler
.container
.getboolean('force_ffmpeg') and
97 config
.get_bin('ffmpeg'))
98 fname
= unicode(path
, 'utf-8')
100 ext
= os
.path
.splitext(fname
)[1].lower()
101 needs_transcode
= ext
in TRANSCODE
or seek
or duration
or always
103 if not needs_transcode
:
104 fsize
= os
.path
.getsize(fname
)
105 handler
.send_response(200)
106 handler
.send_header('Content-Length', fsize
)
108 handler
.send_response(206)
109 handler
.send_header('Transfer-Encoding', 'chunked')
110 handler
.send_header('Content-Type', 'audio/mpeg')
111 handler
.end_headers()
115 fname
= fname
.encode('cp1252')
117 cmd
= [config
.get_bin('ffmpeg'), '-i', fname
, '-vn']
118 if ext
in ['.mp3', '.mp2']:
119 cmd
+= ['-acodec', 'copy']
121 cmd
+= ['-ab', '320k', '-ar', '44100']
122 cmd
+= ['-f', 'mp3', '-']
124 cmd
[-1:] = ['-ss', '%.3f' % (seek
/ 1000.0), '-']
126 cmd
[-1:] = ['-t', '%.3f' % (duration
/ 1000.0), '-']
128 ffmpeg
= subprocess
.Popen(cmd
, bufsize
=BLOCKSIZE
,
129 stdout
=subprocess
.PIPE
)
132 block
= ffmpeg
.stdout
.read(BLOCKSIZE
)
133 handler
.wfile
.write('%x\r\n' % len(block
))
134 handler
.wfile
.write(block
)
135 handler
.wfile
.write('\r\n')
136 except Exception, msg
:
137 handler
.server
.logger
.info(msg
)
144 f
= open(fname
, 'rb')
146 shutil
.copyfileobj(f
, handler
.wfile
)
152 handler
.wfile
.flush()
153 except Exception, msg
:
154 handler
.server
.logger
.info(msg
)
156 def QueryContainer(self
, handler
, query
):
158 def AudioFileFilter(f
, filter_type
=None):
159 ext
= os
.path
.splitext(f
)[1].lower()
161 if ext
in ('.mp3', '.mp2') or (ext
in TRANSCODE
and
162 config
.get_bin('ffmpeg')):
167 if not filter_type
or filter_type
.split('/')[0] != self
.AUDIO
:
169 file_type
= self
.PLAYLIST
170 elif os
.path
.isdir(f
):
171 file_type
= self
.DIRECTORY
176 if f
.name
in self
.media_data_cache
:
177 return self
.media_data_cache
[f
.name
]
180 item
['path'] = f
.name
181 item
['part_path'] = f
.name
.replace(local_base_path
, '', 1)
182 item
['name'] = os
.path
.basename(f
.name
)
183 item
['is_dir'] = f
.isdir
184 item
['is_playlist'] = f
.isplay
185 item
['params'] = 'No'
188 item
['Title'] = f
.title
191 item
['Duration'] = f
.duration
193 if f
.isdir
or f
.isplay
or '://' in f
.name
:
194 self
.media_data_cache
[f
.name
] = item
197 # If the format is: (track #) Song name...
198 #artist, album, track = f.name.split(os.path.sep)[-3:]
199 #track = os.path.splitext(track)[0]
200 #if track[0].isdigit:
201 # track = ' '.join(track.split(' ')[1:])
203 #item['SongTitle'] = track
204 #item['AlbumTitle'] = album
205 #item['ArtistName'] = artist
207 ext
= os
.path
.splitext(f
.name
)[1].lower()
208 fname
= unicode(f
.name
, 'utf-8')
211 # If the file is an mp3, let's load the EasyID3 interface
213 audioFile
= MP3(fname
, ID3
=EasyID3
)
215 # Otherwise, let mutagen figure it out
216 audioFile
= mutagen
.File(fname
)
219 # Pull the length from the FileType, if present
220 if audioFile
.info
.length
> 0:
221 item
['Duration'] = int(audioFile
.info
.length
* 1000)
223 # Grab our other tags, if present
224 def get_tag(tagname
, d
):
225 for tag
in ([tagname
] + TAGNAMES
[tagname
]):
229 if type(value
) not in [str, unicode]:
236 artist
= get_tag('artist', audioFile
)
237 title
= get_tag('title', audioFile
)
238 if artist
== 'Various Artists' and '/' in title
:
239 artist
, title
= [x
.strip() for x
in title
.split('/')]
240 item
['ArtistName'] = artist
241 item
['SongTitle'] = title
242 item
['AlbumTitle'] = get_tag('album', audioFile
)
243 item
['AlbumYear'] = get_tag('date', audioFile
)[:4]
244 item
['MusicGenre'] = get_tag('genre', audioFile
)
245 except Exception, msg
:
248 ffmpeg_path
= config
.get_bin('ffmpeg')
249 if 'Duration' not in item
and ffmpeg_path
:
251 fname
= fname
.encode('cp1252')
252 cmd
= [ffmpeg_path
, '-i', fname
]
253 ffmpeg
= subprocess
.Popen(cmd
, stderr
=subprocess
.PIPE
,
254 stdout
=subprocess
.PIPE
,
255 stdin
=subprocess
.PIPE
)
257 # wait 10 sec if ffmpeg is not back give up
258 for i
in xrange(200):
260 if not ffmpeg
.poll() == None:
263 if ffmpeg
.poll() != None:
264 output
= ffmpeg
.stderr
.read()
267 millisecs
= ((int(d
.group(1)) * 3600 +
268 int(d
.group(2)) * 60 +
269 int(d
.group(3))) * 1000 +
271 (10 ** (3 - len(d
.group(4)))))
274 item
['Duration'] = millisecs
276 if 'Duration' in item
and ffmpeg_path
:
277 item
['params'] = 'Yes'
279 self
.media_data_cache
[f
.name
] = item
282 subcname
= query
['Container'][0]
283 local_base_path
= self
.get_local_base_path(handler
, query
)
285 if not self
.get_local_path(handler
, query
):
286 handler
.send_error(404)
289 if os
.path
.splitext(subcname
)[1].lower() in PLAYLISTS
:
290 t
= Template(PLAYLIST_TEMPLATE
, filter=EncodeUnicode
)
291 t
.files
, t
.total
, t
.start
= self
.get_playlist(handler
, query
)
293 t
= Template(FOLDER_TEMPLATE
, filter=EncodeUnicode
)
294 t
.files
, t
.total
, t
.start
= self
.get_files(handler
, query
,
296 t
.files
= map(media_data
, t
.files
)
297 t
.container
= handler
.cname
302 handler
.send_xml(str(t
))
304 def QueryItem(self
, handler
, query
):
305 uq
= urllib
.unquote_plus
306 splitpath
= [x
for x
in uq(query
['Url'][0]).split('/') if x
]
307 path
= os
.path
.join(handler
.container
['path'], *splitpath
[1:])
309 if path
in self
.media_data_cache
:
310 t
= Template(ITEM_TEMPLATE
, filter=EncodeUnicode
)
311 t
.file = self
.media_data_cache
[path
]
313 handler
.send_xml(str(t
))
315 handler
.send_error(404)
317 def parse_playlist(self
, list_name
, recurse
):
319 ext
= os
.path
.splitext(list_name
)[1].lower()
322 url
= list_name
.index('http://')
323 list_name
= list_name
[url
:]
324 list_file
= urllib
.urlopen(list_name
)
326 list_file
= open(unicode(list_name
, 'utf-8'))
327 local_path
= os
.path
.sep
.join(list_name
.split(os
.path
.sep
)[:-1])
329 if ext
in ('.m3u', '.pls'):
334 if ext
in ('.wpl', '.asx', '.wax', '.wvx', '.b4s'):
336 for line
in list_file
:
337 line
= unicode(line
, charset
).encode('utf-8')
345 playlist
.append(FileData(s
.group(1), False))
348 names
, titles
, lengths
= {}, {}, {}
349 for line
in list_file
:
350 line
= unicode(line
, charset
).encode('utf-8')
353 names
[s
.group(1)] = s
.group(2)
357 titles
[s
.group(1)] = s
.group(2)
361 lengths
[s
.group(1)] = int(s
.group(2))
364 f
= FileData(names
[key
], False)
366 f
.title
= titles
[key
]
368 f
.duration
= lengths
[key
]
371 else: # ext == '.m3u' or '.m3u8' or '.ram'
372 duration
, title
= 0, ''
374 for line
in list_file
:
375 line
= unicode(line
.strip(), charset
).encode('utf-8')
377 if line
.startswith('#EXTINF:'):
379 duration
, title
= line
[8:].split(',', 1)
380 duration
= int(duration
)
384 elif not line
.startswith('#'):
385 f
= FileData(line
, False)
386 f
.title
= title
.strip()
387 f
.duration
= duration
389 duration
, title
= 0, ''
393 # Expand relative paths
394 for i
in xrange(len(playlist
)):
395 if not '://' in playlist
[i
].name
:
396 name
= playlist
[i
].name
397 if not os
.path
.isabs(name
):
398 name
= os
.path
.join(local_path
, name
)
399 playlist
[i
].name
= os
.path
.normpath(name
)
405 newlist
.extend(self
.parse_playlist(i
.name
, recurse
))
413 def get_files(self
, handler
, query
, filterFunction
=None):
416 def __init__(self
, files
):
422 def build_recursive_list(path
, recurse
=True):
424 path
= unicode(path
, 'utf-8')
426 for f
in os
.listdir(path
):
427 if f
.startswith('.'):
429 f
= os
.path
.join(path
, f
)
430 isdir
= os
.path
.isdir(f
)
431 if sys
.platform
== 'darwin':
432 f
= unicodedata
.normalize('NFC', f
)
433 f
= f
.encode('utf-8')
434 if recurse
and isdir
:
435 files
.extend(build_recursive_list(f
))
437 fd
= FileData(f
, isdir
)
438 if isdir
or filterFunction(f
, file_type
):
445 if x
.isdir
== y
.isdir
:
446 if x
.isplay
== y
.isplay
:
447 return name_sort(x
, y
)
449 return y
.isplay
- x
.isplay
451 return y
.isdir
- x
.isdir
454 return cmp(x
.name
, y
.name
)
456 path
= self
.get_local_path(handler
, query
)
458 file_type
= query
.get('Filter', [''])[0]
460 recurse
= query
.get('Recurse', ['No'])[0] == 'Yes'
463 rc
= self
.recurse_cache
469 updated
= os
.path
.getmtime(unicode(path
, 'utf-8'))
470 if path
in dc
and dc
.mtime(path
) >= updated
:
473 if path
.startswith(p
) and rc
.mtime(p
) < updated
:
477 filelist
= SortList(build_recursive_list(path
, recurse
))
487 sortby
= query
.get('SortOrder', ['Normal'])[0]
488 if 'Random' in sortby
:
489 if 'RandomSeed' in query
:
490 seed
= query
['RandomSeed'][0]
492 if 'RandomStart' in query
:
493 start
= query
['RandomStart'][0]
496 if filelist
.unsorted
or filelist
.sortby
!= sortby
:
497 if 'Random' in sortby
:
498 self
.random_lock
.acquire()
501 random
.shuffle(filelist
.files
)
502 self
.random_lock
.release()
504 local_base_path
= self
.get_local_base_path(handler
, query
)
505 start
= unquote(start
)
506 start
= start
.replace(os
.path
.sep
+ handler
.cname
,
508 filenames
= [x
.name
for x
in filelist
.files
]
510 index
= filenames
.index(start
)
511 i
= filelist
.files
.pop(index
)
512 filelist
.files
.insert(0, i
)
514 handler
.server
.logger
.warning('Start not found: ' +
517 filelist
.files
.sort(dir_sort
)
519 filelist
.sortby
= sortby
520 filelist
.unsorted
= False
522 files
= filelist
.files
[:]
525 files
, total
, start
= self
.item_count(handler
, query
, handler
.cname
,
526 files
, filelist
.last_start
)
527 filelist
.last_start
= start
528 return files
, total
, start
530 def get_playlist(self
, handler
, query
):
531 subcname
= query
['Container'][0]
534 url
= subcname
.index('http://')
535 list_name
= subcname
[url
:]
537 list_name
= self
.get_local_path(handler
, query
)
539 recurse
= query
.get('Recurse', ['No'])[0] == 'Yes'
540 playlist
= self
.parse_playlist(list_name
, recurse
)
543 if 'Random' in query
.get('SortOrder', ['Normal'])[0]:
544 seed
= query
.get('RandomSeed', [''])[0]
545 start
= query
.get('RandomStart', [''])[0]
547 self
.random_lock
.acquire()
550 random
.shuffle(playlist
)
551 self
.random_lock
.release()
553 local_base_path
= self
.get_local_base_path(handler
, query
)
554 start
= unquote(start
)
555 start
= start
.replace(os
.path
.sep
+ handler
.cname
,
557 filenames
= [x
.name
for x
in playlist
]
559 index
= filenames
.index(start
)
560 i
= playlist
.pop(index
)
561 playlist
.insert(0, i
)
563 handler
.server
.logger
.warning('Start not found: ' + start
)
566 return self
.item_count(handler
, query
, handler
.cname
, playlist
)