11 from urlparse
import urlparse
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']}
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 FOLDER_TEMPLATE
= file(tfname
, 'rb').read()
54 PLAYLIST_TEMPLATE
= file(tpname
, 'rb').read()
57 # subprocess is broken for me on windows so super hack
58 def patchSubprocess():
59 o
= subprocess
.Popen
._make
_inheritable
61 def _make_inheritable(self
, handle
):
62 if not handle
: return subprocess
.GetCurrentProcess()
63 return o(self
, handle
)
65 subprocess
.Popen
._make
_inheritable
= _make_inheritable
67 mswindows
= (sys
.platform
== "win32")
72 def __init__(self
, name
, isdir
):
75 self
.isplay
= os
.path
.splitext(name
)[1].lower() in PLAYLISTS
81 CONTENT_TYPE
= 'x-container/tivo-music'
87 media_data_cache
= LRUCache(300)
88 recurse_cache
= LRUCache(5)
89 dir_cache
= LRUCache(10)
91 def send_file(self
, handler
, container
, name
):
95 path
, query
= handler
.path
.split('?')
99 opts
= cgi
.parse_qs(query
)
100 seek
= int(opts
.get('Seek', [0])[0])
101 duration
= int(opts
.get('Duration', [0])[0])
103 fname
= os
.path
.join(os
.path
.normpath(container
['path']),
104 unquote(path
)[len(name
) + 2:])
105 fname
= unicode(fname
, 'utf-8')
107 needs_transcode
= (os
.path
.splitext(fname
)[1].lower() in TRANSCODE
110 handler
.send_response(200)
111 handler
.send_header('Content-Type', 'audio/mpeg')
112 if not needs_transcode
:
113 fsize
= os
.path
.getsize(fname
)
114 handler
.send_header('Content-Length', fsize
)
115 handler
.send_header('Connection', 'close')
116 handler
.end_headers()
120 fname
= fname
.encode('iso8859-1')
121 cmd
= [config
.ffmpeg_path(), '-i', fname
, '-ab',
122 '320k', '-ar', '44100', '-f', 'mp3', '-']
124 cmd
[-1:] = ['-ss', '%.3f' % (seek
/ 1000.0), '-']
126 cmd
[-1:] = ['-t', '%.3f' % (duration
/ 1000.0), '-']
128 ffmpeg
= subprocess
.Popen(cmd
, bufsize
=(64 * 1024),
129 stdout
=subprocess
.PIPE
)
131 shutil
.copyfileobj(ffmpeg
.stdout
, handler
.wfile
)
135 f
= open(fname
, 'rb')
137 shutil
.copyfileobj(f
, handler
.wfile
)
142 def QueryContainer(self
, handler
, query
):
144 def AudioFileFilter(f
, filter_type
=None):
145 ext
= os
.path
.splitext(f
)[1].lower()
147 if ext
in ('.mp3', '.mp2') or ext
in TRANSCODE
:
152 if not filter_type
or filter_type
.split('/')[0] != self
.AUDIO
:
154 file_type
= self
.PLAYLIST
155 elif os
.path
.isdir(f
):
156 file_type
= self
.DIRECTORY
161 if f
.name
in self
.media_data_cache
:
162 return self
.media_data_cache
[f
.name
]
165 item
['path'] = f
.name
166 item
['part_path'] = f
.name
.replace(local_base_path
, '', 1)
167 item
['name'] = os
.path
.split(f
.name
)[1]
168 item
['is_dir'] = f
.isdir
169 item
['is_playlist'] = f
.isplay
170 item
['params'] = 'No'
173 item
['Title'] = f
.title
176 item
['Duration'] = f
.duration
178 if f
.isdir
or f
.isplay
or '://' in f
.name
:
179 self
.media_data_cache
[f
.name
] = item
182 # If the format is: (track #) Song name...
183 #artist, album, track = f.name.split(os.path.sep)[-3:]
184 #track = os.path.splitext(track)[0]
185 #if track[0].isdigit:
186 # track = ' '.join(track.split(' ')[1:])
188 #item['SongTitle'] = track
189 #item['AlbumTitle'] = album
190 #item['ArtistName'] = artist
192 ext
= os
.path
.splitext(f
.name
)[1].lower()
195 # If the file is an mp3, let's load the EasyID3 interface
197 audioFile
= MP3(f
.name
, ID3
=EasyID3
)
199 # Otherwise, let mutagen figure it out
200 audioFile
= mutagen
.File(f
.name
)
202 # Pull the length from the FileType, if present
203 if audioFile
.info
.length
> 0:
204 item
['Duration'] = int(audioFile
.info
.length
* 1000)
206 # Grab our other tags, if present
207 def get_tag(tagname
, d
):
208 for tag
in ([tagname
] + TAGNAMES
[tagname
]):
216 artist
= get_tag('artist', audioFile
)
217 title
= get_tag('title', audioFile
)
218 if artist
== 'Various Artists' and '/' in title
:
219 artist
, title
= [x
.strip() for x
in title
.split('/')]
220 item
['ArtistName'] = artist
221 item
['SongTitle'] = title
222 item
['AlbumTitle'] = get_tag('album', audioFile
)
223 item
['AlbumYear'] = get_tag('date', audioFile
)
224 item
['MusicGenre'] = get_tag('genre', audioFile
)
225 except Exception, msg
:
228 if 'Duration' not in item
:
229 fname
= unicode(f
.name
, 'utf-8')
231 fname
= fname
.encode('iso8859-1')
232 cmd
= [config
.ffmpeg_path(), '-i', fname
]
233 ffmpeg
= subprocess
.Popen(cmd
, stderr
=subprocess
.PIPE
,
234 stdout
=subprocess
.PIPE
,
235 stdin
=subprocess
.PIPE
)
237 # wait 10 sec if ffmpeg is not back give up
238 for i
in xrange(200):
240 if not ffmpeg
.poll() == None:
243 if ffmpeg
.poll() != None:
244 output
= ffmpeg
.stderr
.read()
247 millisecs
= ((int(d
.group(1)) * 3600 +
248 int(d
.group(2)) * 60 +
249 int(d
.group(3))) * 1000 +
251 (10 ** (3 - len(d
.group(4)))))
254 item
['Duration'] = millisecs
256 if 'Duration' in item
:
257 item
['params'] = 'Yes'
259 self
.media_data_cache
[f
.name
] = item
262 subcname
= query
['Container'][0]
263 cname
= subcname
.split('/')[0]
264 local_base_path
= self
.get_local_base_path(handler
, query
)
266 if (not cname
in handler
.server
.containers
or
267 not self
.get_local_path(handler
, query
)):
268 handler
.send_error(404)
271 if os
.path
.splitext(subcname
)[1].lower() in PLAYLISTS
:
272 t
= Template(PLAYLIST_TEMPLATE
, filter=EncodeUnicode
)
273 t
.files
, t
.total
, t
.start
= self
.get_playlist(handler
, query
)
275 t
= Template(FOLDER_TEMPLATE
, filter=EncodeUnicode
)
276 t
.files
, t
.total
, t
.start
= self
.get_files(handler
, query
,
278 t
.files
= map(media_data
, t
.files
)
285 handler
.send_response(200)
286 handler
.send_header('Content-Type', 'text/xml')
287 handler
.send_header('Content-Length', len(page
))
288 handler
.send_header('Connection', 'close')
289 handler
.end_headers()
290 handler
.wfile
.write(page
)
292 def parse_playlist(self
, list_name
, recurse
):
294 ext
= os
.path
.splitext(list_name
)[1].lower()
297 url
= list_name
.index('http://')
298 list_name
= list_name
[url
:]
299 list_file
= urllib
.urlopen(list_name
)
301 list_file
= open(unicode(list_name
, 'utf-8'))
302 local_path
= os
.path
.sep
.join(list_name
.split(os
.path
.sep
)[:-1])
304 if ext
in ('.m3u', '.pls'):
305 charset
= 'iso-8859-1'
309 if ext
in ('.wpl', '.asx', '.wax', '.wvx', '.b4s'):
311 for line
in list_file
:
312 line
= unicode(line
, charset
).encode('utf-8')
320 playlist
.append(FileData(s
.group(1), False))
323 names
, titles
, lengths
= {}, {}, {}
324 for line
in list_file
:
325 line
= unicode(line
, charset
).encode('utf-8')
328 names
[s
.group(1)] = s
.group(2)
332 titles
[s
.group(1)] = s
.group(2)
336 lengths
[s
.group(1)] = int(s
.group(2))
339 f
= FileData(names
[key
], False)
341 f
.title
= titles
[key
]
343 f
.duration
= lengths
[key
]
346 else: # ext == '.m3u' or '.m3u8' or '.ram'
347 duration
, title
= 0, ''
349 for line
in list_file
:
350 line
= unicode(line
.strip(), charset
).encode('utf-8')
352 if line
.startswith('#EXTINF:'):
354 duration
, title
= line
[8:].split(',')
355 duration
= int(duration
)
359 elif not line
.startswith('#'):
360 f
= FileData(line
, False)
361 f
.title
= title
.strip()
362 f
.duration
= duration
364 duration
, title
= 0, ''
368 # Expand relative paths
369 for i
in xrange(len(playlist
)):
370 if not '://' in playlist
[i
].name
:
371 name
= playlist
[i
].name
372 if not os
.path
.isabs(name
):
373 name
= os
.path
.join(local_path
, name
)
374 playlist
[i
].name
= os
.path
.normpath(name
)
380 newlist
.extend(self
.parse_playlist(i
.name
, recurse
))
388 def get_files(self
, handler
, query
, filterFunction
=None):
391 def __init__(self
, files
):
397 def build_recursive_list(path
, recurse
=True):
399 path
= unicode(path
, 'utf-8')
401 for f
in os
.listdir(path
):
402 if f
.startswith('.'):
404 f
= os
.path
.join(path
, f
)
405 isdir
= os
.path
.isdir(f
)
406 f
= f
.encode('utf-8')
407 if recurse
and isdir
:
408 files
.extend(build_recursive_list(f
))
410 fd
= FileData(f
, isdir
)
411 if recurse
and fd
.isplay
:
412 files
.extend(self
.parse_playlist(f
, recurse
))
413 elif isdir
or filterFunction(f
, file_type
):
420 if x
.isdir
== y
.isdir
:
421 if x
.isplay
== y
.isplay
:
422 return name_sort(x
, y
)
424 return y
.isplay
- x
.isplay
426 return y
.isdir
- x
.isdir
429 return cmp(x
.name
, y
.name
)
431 subcname
= query
['Container'][0]
432 cname
= subcname
.split('/')[0]
433 path
= self
.get_local_path(handler
, query
)
435 file_type
= query
.get('Filter', [''])[0]
437 recurse
= query
.get('Recurse', ['No'])[0] == 'Yes'
440 if recurse
and path
in self
.recurse_cache
:
441 if self
.recurse_cache
.mtime(path
) + 3600 >= time
.time():
442 filelist
= self
.recurse_cache
[path
]
443 elif not recurse
and path
in self
.dir_cache
:
444 if self
.dir_cache
.mtime(path
) >= os
.stat(path
)[8]:
445 filelist
= self
.dir_cache
[path
]
448 filelist
= SortList(build_recursive_list(path
, recurse
))
451 self
.recurse_cache
[path
] = filelist
453 self
.dir_cache
[path
] = filelist
458 sortby
= query
.get('SortOrder', ['Normal'])[0]
459 if 'Random' in sortby
:
460 if 'RandomSeed' in query
:
461 seed
= query
['RandomSeed'][0]
463 if 'RandomStart' in query
:
464 start
= query
['RandomStart'][0]
467 if filelist
.unsorted
or filelist
.sortby
!= sortby
:
468 if 'Random' in sortby
:
469 self
.random_lock
.acquire()
472 random
.shuffle(filelist
.files
)
473 self
.random_lock
.release()
475 local_base_path
= self
.get_local_base_path(handler
, query
)
476 start
= unquote(start
)
477 start
= start
.replace(os
.path
.sep
+ cname
,
479 filenames
= [x
.name
for x
in filelist
.files
]
481 index
= filenames
.index(start
)
482 i
= filelist
.files
.pop(index
)
483 filelist
.files
.insert(0, i
)
485 handler
.server
.logger
.warning('Start not found: ' +
488 filelist
.files
.sort(dir_sort
)
490 filelist
.sortby
= sortby
491 filelist
.unsorted
= False
493 files
= filelist
.files
[:]
496 files
, total
, start
= self
.item_count(handler
, query
, cname
, files
,
498 filelist
.last_start
= start
499 return files
, total
, start
501 def get_playlist(self
, handler
, query
):
502 subcname
= query
['Container'][0]
503 cname
= subcname
.split('/')[0]
506 url
= subcname
.index('http://')
507 list_name
= subcname
[url
:]
509 list_name
= self
.get_local_path(handler
, query
)
511 recurse
= query
.get('Recurse', ['No'])[0] == 'Yes'
512 playlist
= self
.parse_playlist(list_name
, recurse
)
515 if 'Random' in query
.get('SortOrder', ['Normal'])[0]:
516 seed
= query
.get('RandomSeed', [''])[0]
517 start
= query
.get('RandomStart', [''])[0]
519 self
.random_lock
.acquire()
522 random
.shuffle(playlist
)
523 self
.random_lock
.release()
525 local_base_path
= self
.get_local_base_path(handler
, query
)
526 start
= unquote(start
)
527 start
= start
.replace(os
.path
.sep
+ cname
,
529 filenames
= [x
.name
for x
in playlist
]
531 index
= filenames
.index(start
)
532 i
= playlist
.pop(index
)
533 playlist
.insert(0, i
)
535 handler
.server
.logger
.warning('Start not found: ' + start
)
538 return self
.item_count(handler
, query
, cname
, playlist
)