11 from xml
.sax
.saxutils
import escape
14 from mutagen
.easyid3
import EasyID3
15 from mutagen
.mp3
import MP3
16 from Cheetah
.Template
import Template
17 from lrucache
import LRUCache
19 from plugin
import EncodeUnicode
, Plugin
, quote
, unquote
20 from plugins
.video
.transcode
import kill
22 SCRIPTDIR
= os
.path
.dirname(__file__
)
26 PLAYLISTS
= ('.m3u', '.m3u8', '.ram', '.pls', '.b4s', '.wpl', '.asx',
29 TRANSCODE
= ('.mp4', '.m4a', '.flc', '.ogg', '.wma', '.aac', '.wav',
30 '.aif', '.aiff', '.au', '.flac')
32 TAGNAMES
= {'artist': ['\xa9ART', 'Author'],
33 'title': ['\xa9nam', 'Title'],
34 'album': ['\xa9alb', u
'WM/AlbumTitle'],
35 'date': ['\xa9day', u
'WM/Year'],
36 'genre': ['\xa9gen', u
'WM/Genre']}
38 # Search strings for different playlist types
39 asxfile
= re
.compile('ref +href *= *"(.+)"', re
.IGNORECASE
).search
40 wplfile
= re
.compile('media +src *= *"(.+)"', re
.IGNORECASE
).search
41 b4sfile
= re
.compile('Playstring="file:(.+)"').search
42 plsfile
= re
.compile('[Ff]ile(\d+)=(.+)').match
43 plstitle
= re
.compile('[Tt]itle(\d+)=(.+)').match
44 plslength
= re
.compile('[Ll]ength(\d+)=(\d+)').match
46 # Duration -- parse from ffmpeg output
47 durre
= re
.compile(r
'.*Duration: ([0-9]+):([0-9]+):([0-9]+)\.([0-9]+),').search
49 # Preload the templates
50 tfname
= os
.path
.join(SCRIPTDIR
, 'templates', 'container.tmpl')
51 tpname
= os
.path
.join(SCRIPTDIR
, 'templates', 'm3u.tmpl')
52 FOLDER_TEMPLATE
= file(tfname
, 'rb').read()
53 PLAYLIST_TEMPLATE
= file(tpname
, 'rb').read()
56 # subprocess is broken for me on windows so super hack
57 def patchSubprocess():
58 o
= subprocess
.Popen
._make
_inheritable
60 def _make_inheritable(self
, handle
):
61 if not handle
: return subprocess
.GetCurrentProcess()
62 return o(self
, handle
)
64 subprocess
.Popen
._make
_inheritable
= _make_inheritable
66 mswindows
= (sys
.platform
== "win32")
71 def __init__(self
, name
, isdir
):
74 self
.isplay
= os
.path
.splitext(name
)[1].lower() in PLAYLISTS
80 CONTENT_TYPE
= 'x-container/tivo-music'
86 media_data_cache
= LRUCache(300)
87 recurse_cache
= LRUCache(5)
88 dir_cache
= LRUCache(10)
90 def send_file(self
, handler
, path
, query
):
91 seek
= int(query
.get('Seek', [0])[0])
92 duration
= int(query
.get('Duration', [0])[0])
94 fname
= unicode(path
, 'utf-8')
96 ext
= os
.path
.splitext(fname
)[1].lower()
97 needs_transcode
= ext
in TRANSCODE
or seek
or duration
99 handler
.send_response(200)
100 handler
.send_header('Content-Type', 'audio/mpeg')
101 if not needs_transcode
:
102 fsize
= os
.path
.getsize(fname
)
103 handler
.send_header('Content-Length', fsize
)
104 handler
.send_header('Connection', 'close')
105 handler
.end_headers()
109 fname
= fname
.encode('iso8859-1')
111 cmd
= [config
.get_bin('ffmpeg'), '-i', fname
]
112 if ext
in ['.mp3', '.mp2']:
113 cmd
+= ['-acodec', 'copy']
115 cmd
+= ['-ab', '320k', '-ar', '44100']
116 cmd
+= ['-f', 'mp3', '-']
118 cmd
[-1:] = ['-ss', '%.3f' % (seek
/ 1000.0), '-']
120 cmd
[-1:] = ['-t', '%.3f' % (duration
/ 1000.0), '-']
122 ffmpeg
= subprocess
.Popen(cmd
, bufsize
=(64 * 1024),
123 stdout
=subprocess
.PIPE
)
125 shutil
.copyfileobj(ffmpeg
.stdout
, handler
.wfile
)
129 f
= open(fname
, 'rb')
131 shutil
.copyfileobj(f
, handler
.wfile
)
136 def QueryContainer(self
, handler
, query
):
138 def AudioFileFilter(f
, filter_type
=None):
139 ext
= os
.path
.splitext(f
)[1].lower()
141 if ext
in ('.mp3', '.mp2') or (ext
in TRANSCODE
and
142 config
.get_bin('ffmpeg')):
147 if not filter_type
or filter_type
.split('/')[0] != self
.AUDIO
:
149 file_type
= self
.PLAYLIST
150 elif os
.path
.isdir(f
):
151 file_type
= self
.DIRECTORY
156 if f
.name
in self
.media_data_cache
:
157 return self
.media_data_cache
[f
.name
]
160 item
['path'] = f
.name
161 item
['part_path'] = f
.name
.replace(local_base_path
, '', 1)
162 item
['name'] = os
.path
.split(f
.name
)[1]
163 item
['is_dir'] = f
.isdir
164 item
['is_playlist'] = f
.isplay
165 item
['params'] = 'No'
168 item
['Title'] = f
.title
171 item
['Duration'] = f
.duration
173 if f
.isdir
or f
.isplay
or '://' in f
.name
:
174 self
.media_data_cache
[f
.name
] = item
177 # If the format is: (track #) Song name...
178 #artist, album, track = f.name.split(os.path.sep)[-3:]
179 #track = os.path.splitext(track)[0]
180 #if track[0].isdigit:
181 # track = ' '.join(track.split(' ')[1:])
183 #item['SongTitle'] = track
184 #item['AlbumTitle'] = album
185 #item['ArtistName'] = artist
187 ext
= os
.path
.splitext(f
.name
)[1].lower()
188 fname
= unicode(f
.name
, 'utf-8')
191 # If the file is an mp3, let's load the EasyID3 interface
193 audioFile
= MP3(fname
, ID3
=EasyID3
)
195 # Otherwise, let mutagen figure it out
196 audioFile
= mutagen
.File(fname
)
198 # Pull the length from the FileType, if present
199 if audioFile
.info
.length
> 0:
200 item
['Duration'] = int(audioFile
.info
.length
* 1000)
202 # Grab our other tags, if present
203 def get_tag(tagname
, d
):
204 for tag
in ([tagname
] + TAGNAMES
[tagname
]):
212 artist
= get_tag('artist', audioFile
)
213 title
= get_tag('title', audioFile
)
214 if artist
== 'Various Artists' and '/' in title
:
215 artist
, title
= [x
.strip() for x
in title
.split('/')]
216 item
['ArtistName'] = artist
217 item
['SongTitle'] = title
218 item
['AlbumTitle'] = get_tag('album', audioFile
)
219 item
['AlbumYear'] = get_tag('date', audioFile
)
220 item
['MusicGenre'] = get_tag('genre', audioFile
)
221 except Exception, msg
:
224 ffmpeg_path
= config
.get_bin('ffmpeg')
225 if 'Duration' not in item
and ffmpeg_path
:
227 fname
= fname
.encode('iso8859-1')
228 cmd
= [ffmpeg_path
, '-i', fname
]
229 ffmpeg
= subprocess
.Popen(cmd
, stderr
=subprocess
.PIPE
,
230 stdout
=subprocess
.PIPE
,
231 stdin
=subprocess
.PIPE
)
233 # wait 10 sec if ffmpeg is not back give up
234 for i
in xrange(200):
236 if not ffmpeg
.poll() == None:
239 if ffmpeg
.poll() != None:
240 output
= ffmpeg
.stderr
.read()
243 millisecs
= ((int(d
.group(1)) * 3600 +
244 int(d
.group(2)) * 60 +
245 int(d
.group(3))) * 1000 +
247 (10 ** (3 - len(d
.group(4)))))
250 item
['Duration'] = millisecs
252 if 'Duration' in item
and ffmpeg_path
:
253 item
['params'] = 'Yes'
255 self
.media_data_cache
[f
.name
] = item
258 subcname
= query
['Container'][0]
259 cname
= subcname
.split('/')[0]
260 local_base_path
= self
.get_local_base_path(handler
, query
)
262 if (not cname
in handler
.server
.containers
or
263 not self
.get_local_path(handler
, query
)):
264 handler
.send_error(404)
267 if os
.path
.splitext(subcname
)[1].lower() in PLAYLISTS
:
268 t
= Template(PLAYLIST_TEMPLATE
, filter=EncodeUnicode
)
269 t
.files
, t
.total
, t
.start
= self
.get_playlist(handler
, query
)
271 t
= Template(FOLDER_TEMPLATE
, filter=EncodeUnicode
)
272 t
.files
, t
.total
, t
.start
= self
.get_files(handler
, query
,
274 t
.files
= map(media_data
, t
.files
)
281 handler
.send_response(200)
282 handler
.send_header('Content-Type', 'text/xml')
283 handler
.send_header('Content-Length', len(page
))
284 handler
.send_header('Connection', 'close')
285 handler
.end_headers()
286 handler
.wfile
.write(page
)
288 def parse_playlist(self
, list_name
, recurse
):
290 ext
= os
.path
.splitext(list_name
)[1].lower()
293 url
= list_name
.index('http://')
294 list_name
= list_name
[url
:]
295 list_file
= urllib
.urlopen(list_name
)
297 list_file
= open(unicode(list_name
, 'utf-8'))
298 local_path
= os
.path
.sep
.join(list_name
.split(os
.path
.sep
)[:-1])
300 if ext
in ('.m3u', '.pls'):
301 charset
= 'iso-8859-1'
305 if ext
in ('.wpl', '.asx', '.wax', '.wvx', '.b4s'):
307 for line
in list_file
:
308 line
= unicode(line
, charset
).encode('utf-8')
316 playlist
.append(FileData(s
.group(1), False))
319 names
, titles
, lengths
= {}, {}, {}
320 for line
in list_file
:
321 line
= unicode(line
, charset
).encode('utf-8')
324 names
[s
.group(1)] = s
.group(2)
328 titles
[s
.group(1)] = s
.group(2)
332 lengths
[s
.group(1)] = int(s
.group(2))
335 f
= FileData(names
[key
], False)
337 f
.title
= titles
[key
]
339 f
.duration
= lengths
[key
]
342 else: # ext == '.m3u' or '.m3u8' or '.ram'
343 duration
, title
= 0, ''
345 for line
in list_file
:
346 line
= unicode(line
.strip(), charset
).encode('utf-8')
348 if line
.startswith('#EXTINF:'):
350 duration
, title
= line
[8:].split(',')
351 duration
= int(duration
)
355 elif not line
.startswith('#'):
356 f
= FileData(line
, False)
357 f
.title
= title
.strip()
358 f
.duration
= duration
360 duration
, title
= 0, ''
364 # Expand relative paths
365 for i
in xrange(len(playlist
)):
366 if not '://' in playlist
[i
].name
:
367 name
= playlist
[i
].name
368 if not os
.path
.isabs(name
):
369 name
= os
.path
.join(local_path
, name
)
370 playlist
[i
].name
= os
.path
.normpath(name
)
376 newlist
.extend(self
.parse_playlist(i
.name
, recurse
))
384 def get_files(self
, handler
, query
, filterFunction
=None):
387 def __init__(self
, files
):
393 def build_recursive_list(path
, recurse
=True):
395 path
= unicode(path
, 'utf-8')
397 for f
in os
.listdir(path
):
398 if f
.startswith('.'):
400 f
= os
.path
.join(path
, f
)
401 isdir
= os
.path
.isdir(f
)
402 f
= f
.encode('utf-8')
403 if recurse
and isdir
:
404 files
.extend(build_recursive_list(f
))
406 fd
= FileData(f
, isdir
)
407 if recurse
and fd
.isplay
:
408 files
.extend(self
.parse_playlist(f
, recurse
))
409 elif isdir
or filterFunction(f
, file_type
):
416 if x
.isdir
== y
.isdir
:
417 if x
.isplay
== y
.isplay
:
418 return name_sort(x
, y
)
420 return y
.isplay
- x
.isplay
422 return y
.isdir
- x
.isdir
425 return cmp(x
.name
, y
.name
)
427 subcname
= query
['Container'][0]
428 cname
= subcname
.split('/')[0]
429 path
= self
.get_local_path(handler
, query
)
431 file_type
= query
.get('Filter', [''])[0]
433 recurse
= query
.get('Recurse', ['No'])[0] == 'Yes'
436 rc
= self
.recurse_cache
442 updated
= os
.stat(unicode(path
, 'utf-8'))[8]
443 if path
in dc
and dc
.mtime(path
) >= updated
:
446 if path
.startswith(p
) and rc
.mtime(p
) < updated
:
450 filelist
= SortList(build_recursive_list(path
, recurse
))
460 sortby
= query
.get('SortOrder', ['Normal'])[0]
461 if 'Random' in sortby
:
462 if 'RandomSeed' in query
:
463 seed
= query
['RandomSeed'][0]
465 if 'RandomStart' in query
:
466 start
= query
['RandomStart'][0]
469 if filelist
.unsorted
or filelist
.sortby
!= sortby
:
470 if 'Random' in sortby
:
471 self
.random_lock
.acquire()
474 random
.shuffle(filelist
.files
)
475 self
.random_lock
.release()
477 local_base_path
= self
.get_local_base_path(handler
, query
)
478 start
= unquote(start
)
479 start
= start
.replace(os
.path
.sep
+ cname
,
481 filenames
= [x
.name
for x
in filelist
.files
]
483 index
= filenames
.index(start
)
484 i
= filelist
.files
.pop(index
)
485 filelist
.files
.insert(0, i
)
487 handler
.server
.logger
.warning('Start not found: ' +
490 filelist
.files
.sort(dir_sort
)
492 filelist
.sortby
= sortby
493 filelist
.unsorted
= False
495 files
= filelist
.files
[:]
498 files
, total
, start
= self
.item_count(handler
, query
, cname
, files
,
500 filelist
.last_start
= start
501 return files
, total
, start
503 def get_playlist(self
, handler
, query
):
504 subcname
= query
['Container'][0]
505 cname
= subcname
.split('/')[0]
508 url
= subcname
.index('http://')
509 list_name
= subcname
[url
:]
511 list_name
= self
.get_local_path(handler
, query
)
513 recurse
= query
.get('Recurse', ['No'])[0] == 'Yes'
514 playlist
= self
.parse_playlist(list_name
, recurse
)
517 if 'Random' in query
.get('SortOrder', ['Normal'])[0]:
518 seed
= query
.get('RandomSeed', [''])[0]
519 start
= query
.get('RandomStart', [''])[0]
521 self
.random_lock
.acquire()
524 random
.shuffle(playlist
)
525 self
.random_lock
.release()
527 local_base_path
= self
.get_local_base_path(handler
, query
)
528 start
= unquote(start
)
529 start
= start
.replace(os
.path
.sep
+ cname
,
531 filenames
= [x
.name
for x
in playlist
]
533 index
= filenames
.index(start
)
534 i
= playlist
.pop(index
)
535 playlist
.insert(0, i
)
537 handler
.server
.logger
.warning('Start not found: ' + start
)
540 return self
.item_count(handler
, query
, cname
, playlist
)