1 import subprocess
, os
, random
, re
, shutil
, socket
, sys
, urllib
, time
, cgi
3 from plugins
.video
.transcode
import kill
4 from Cheetah
.Template
import Template
5 from Cheetah
.Filters
import Filter
6 from plugin
import Plugin
, quote
, unquote
7 from xml
.sax
.saxutils
import escape
8 from lrucache
import LRUCache
9 from urlparse
import urlparse
12 SCRIPTDIR
= os
.path
.dirname(__file__
)
15 return config
.get('Server', 'ffmpeg')
19 PLAYLISTS
= ('.m3u', '.m3u8', '.ram', '.pls', '.b4s', '.wpl', '.asx',
22 TRANSCODE
= ('.mp4', '.m4a', '.flc', '.ogg', '.wma', '.aac', '.wav',
23 '.aif', '.aiff', '.au', '.flac')
25 # Search strings for different playlist types
26 asxfile
= re
.compile('ref +href *= *"(.+)"', re
.IGNORECASE
).search
27 wplfile
= re
.compile('media +src *= *"(.+)"', re
.IGNORECASE
).search
28 b4sfile
= re
.compile('Playstring="file:(.+)"').search
29 plsfile
= re
.compile('[Ff]ile(\d+)=(.+)').match
30 plstitle
= re
.compile('[Tt]itle(\d+)=(.+)').match
31 plslength
= re
.compile('[Ll]ength(\d+)=(\d+)').match
33 # Duration -- parse from ffmpeg output
34 durre
= re
.compile(r
'.*Duration: ([0-9]+):([0-9]+):([0-9]+)\.([0-9]+),').search
36 # Preload the templates
37 tfname
= os
.path
.join(SCRIPTDIR
, 'templates', 'container.tmpl')
38 tpname
= os
.path
.join(SCRIPTDIR
, 'templates', 'm3u.tmpl')
39 FOLDER_TEMPLATE
= file(tfname
, 'rb').read()
40 PLAYLIST_TEMPLATE
= file(tpname
, 'rb').read()
43 # subprocess is broken for me on windows so super hack
44 def patchSubprocess():
45 o
= subprocess
.Popen
._make
_inheritable
47 def _make_inheritable(self
, handle
):
48 if not handle
: return subprocess
.GetCurrentProcess()
49 return o(self
, handle
)
51 subprocess
.Popen
._make
_inheritable
= _make_inheritable
53 mswindows
= (sys
.platform
== "win32")
58 def __init__(self
, name
, isdir
):
61 self
.isplay
= os
.path
.splitext(name
)[1].lower() in PLAYLISTS
65 class EncodeUnicode(Filter
):
66 def filter(self
, val
, **kw
):
67 """Encode Unicode strings, by default in UTF-8"""
69 encoding
= kw
.get('encoding', 'utf8')
71 if type(val
) == type(u
''):
72 filtered
= val
.encode(encoding
)
79 CONTENT_TYPE
= 'x-container/tivo-music'
85 media_data_cache
= LRUCache(300)
86 recurse_cache
= LRUCache(5)
87 dir_cache
= LRUCache(10)
89 def send_file(self
, handler
, container
, name
):
93 path
, query
= handler
.path
.split('?')
97 opts
= cgi
.parse_qs(query
)
98 seek
= int(opts
.get('Seek', [0])[0])
99 duration
= int(opts
.get('Duration', [0])[0])
101 fname
= os
.path
.join(os
.path
.normpath(container
['path']),
102 unquote(path
)[len(name
) + 2:])
103 fname
= unicode(fname
, 'utf-8')
105 needs_transcode
= os
.path
.splitext(fname
)[1].lower() in TRANSCODE \
108 handler
.send_response(200)
109 handler
.send_header('Content-Type', 'audio/mpeg')
110 if not needs_transcode
:
111 fsize
= os
.path
.getsize(fname
)
112 handler
.send_header('Content-Length', fsize
)
113 handler
.send_header('Connection', 'close')
114 handler
.end_headers()
118 fname
= fname
.encode('iso8859-1')
119 cmd
= [ffmpeg_path(), '-i', fname
, '-ab',
120 '320k', '-ar', '44100', '-f', 'mp3', '-']
122 cmd
[-1:] = ['-ss', '%.3f' % (seek
/ 1000.0), '-']
124 cmd
[-1:] = ['-t', '%.3f' % (duration
/ 1000.0), '-']
126 ffmpeg
= subprocess
.Popen(cmd
, stdout
=subprocess
.PIPE
)
128 shutil
.copyfileobj(ffmpeg
.stdout
, handler
.wfile
)
132 f
= file(fname
, 'rb')
134 shutil
.copyfileobj(f
, handler
.wfile
)
138 def QueryContainer(self
, handler
, query
):
140 def AudioFileFilter(f
, filter_type
=None):
141 ext
= os
.path
.splitext(f
)[1].lower()
143 if ext
in ('.mp3', '.mp2') or ext
in TRANSCODE
:
148 if not filter_type
or filter_type
.split('/')[0] != self
.AUDIO
:
150 file_type
= self
.PLAYLIST
151 elif os
.path
.isdir(f
):
152 file_type
= self
.DIRECTORY
157 if f
.name
in self
.media_data_cache
:
158 return self
.media_data_cache
[f
.name
]
161 item
['path'] = f
.name
162 item
['part_path'] = f
.name
.replace(local_base_path
, '', 1)
163 item
['name'] = os
.path
.split(f
.name
)[1]
164 item
['is_dir'] = f
.isdir
165 item
['is_playlist'] = f
.isplay
166 item
['params'] = 'No'
169 item
['Title'] = f
.title
172 item
['Duration'] = f
.duration
174 if f
.isdir
or f
.isplay
or '://' in f
.name
:
175 self
.media_data_cache
[f
.name
] = item
178 if os
.path
.splitext(f
.name
)[1].lower() in TRANSCODE
:
179 # If the format is: (track #) Song name...
180 #artist, album, track = f.name.split(os.path.sep)[-3:]
181 #track = os.path.splitext(track)[0]
182 #if track[0].isdigit:
183 # track = ' '.join(track.split(' ')[1:])
185 #item['SongTitle'] = track
186 #item['AlbumTitle'] = album
187 #item['ArtistName'] = artist
188 fname
= unicode(f
.name
, 'utf-8')
190 fname
= fname
.encode('iso8859-1')
191 cmd
= [ffmpeg_path(), '-i', fname
]
192 ffmpeg
= subprocess
.Popen(cmd
, stderr
=subprocess
.PIPE
,
193 stdout
=subprocess
.PIPE
,
194 stdin
=subprocess
.PIPE
)
196 # wait 10 sec if ffmpeg is not back give up
197 for i
in xrange(200):
199 if not ffmpeg
.poll() == None:
202 if ffmpeg
.poll() != None:
203 output
= ffmpeg
.stderr
.read()
206 millisecs
= ((int(d
.group(1)) * 3600 +
207 int(d
.group(2)) * 60 +
208 int(d
.group(3))) * 1000 +
210 (10 ** (3 - len(d
.group(4)))))
213 item
['Duration'] = millisecs
216 audioFile
= eyeD3
.Mp3AudioFile(unicode(f
.name
, 'utf-8'))
217 item
['Duration'] = audioFile
.getPlayTime() * 1000
219 tag
= audioFile
.getTag()
220 artist
= tag
.getArtist()
221 title
= tag
.getTitle()
222 if artist
== 'Various Artists' and '/' in title
:
223 artist
, title
= title
.split('/')
224 item
['ArtistName'] = artist
.strip()
225 item
['SongTitle'] = title
.strip()
226 item
['AlbumTitle'] = tag
.getAlbum()
227 item
['AlbumYear'] = tag
.getYear()
228 item
['MusicGenre'] = tag
.getGenre().getName()
229 except Exception, msg
:
232 if 'Duration' in item
:
233 item
['params'] = 'Yes'
235 self
.media_data_cache
[f
.name
] = item
238 subcname
= query
['Container'][0]
239 cname
= subcname
.split('/')[0]
240 local_base_path
= self
.get_local_base_path(handler
, query
)
242 if not handler
.server
.containers
.has_key(cname
) or \
243 not self
.get_local_path(handler
, query
):
244 handler
.send_response(404)
245 handler
.end_headers()
248 if os
.path
.splitext(subcname
)[1].lower() in PLAYLISTS
:
249 t
= Template(PLAYLIST_TEMPLATE
, filter=EncodeUnicode
)
250 t
.files
, t
.total
, t
.start
= self
.get_playlist(handler
, query
)
252 t
= Template(FOLDER_TEMPLATE
, filter=EncodeUnicode
)
253 t
.files
, t
.total
, t
.start
= self
.get_files(handler
, query
,
255 t
.files
= map(media_data
, t
.files
)
262 handler
.send_response(200)
263 handler
.send_header('Content-Type', 'text/xml')
264 handler
.send_header('Content-Length', len(page
))
265 handler
.send_header('Connection', 'close')
266 handler
.end_headers()
267 handler
.wfile
.write(page
)
269 def parse_playlist(self
, list_name
, recurse
):
271 ext
= os
.path
.splitext(list_name
)[1].lower()
274 url
= list_name
.index('http://')
275 list_name
= list_name
[url
:]
276 list_file
= urllib
.urlopen(list_name
)
278 list_file
= open(unicode(list_name
, 'utf-8'))
279 local_path
= os
.path
.sep
.join(list_name
.split(os
.path
.sep
)[:-1])
281 if ext
in ('.m3u', '.pls'):
282 charset
= 'iso-8859-1'
286 if ext
in ('.wpl', '.asx', '.wax', '.wvx', '.b4s'):
288 for line
in list_file
:
289 line
= unicode(line
, charset
).encode('utf-8')
297 playlist
.append(FileData(s
.group(1), False))
300 names
, titles
, lengths
= {}, {}, {}
301 for line
in list_file
:
302 line
= unicode(line
, charset
).encode('utf-8')
305 names
[s
.group(1)] = s
.group(2)
309 titles
[s
.group(1)] = s
.group(2)
313 lengths
[s
.group(1)] = int(s
.group(2))
316 f
= FileData(names
[key
], False)
318 f
.title
= titles
[key
]
320 f
.duration
= lengths
[key
]
323 else: # ext == '.m3u' or '.m3u8' or '.ram'
324 duration
, title
= 0, ''
326 for line
in list_file
:
327 line
= unicode(line
.strip(), charset
).encode('utf-8')
329 if line
.startswith('#EXTINF:'):
331 duration
, title
= line
[8:].split(',')
332 duration
= int(duration
)
336 elif not line
.startswith('#'):
337 f
= FileData(line
, False)
338 f
.title
= title
.strip()
339 f
.duration
= duration
341 duration
, title
= 0, ''
345 # Expand relative paths
346 for i
in xrange(len(playlist
)):
347 if not '://' in playlist
[i
].name
:
348 name
= playlist
[i
].name
349 if not os
.path
.isabs(name
):
350 name
= os
.path
.join(local_path
, name
)
351 playlist
[i
].name
= os
.path
.normpath(name
)
357 newlist
.extend(self
.parse_playlist(i
.name
, recurse
))
365 def get_files(self
, handler
, query
, filterFunction
=None):
368 def __init__(self
, files
):
374 def build_recursive_list(path
, recurse
=True):
376 path
= unicode(path
, 'utf-8')
378 for f
in os
.listdir(path
):
379 if f
.startswith('.'):
381 f
= os
.path
.join(path
, f
)
382 isdir
= os
.path
.isdir(f
)
383 f
= f
.encode('utf-8')
384 if recurse
and isdir
:
385 files
.extend(build_recursive_list(f
))
387 fd
= FileData(f
, isdir
)
388 if recurse
and fd
.isplay
:
389 files
.extend(self
.parse_playlist(f
, recurse
))
390 elif isdir
or filterFunction(f
, file_type
):
397 if x
.isdir
== y
.isdir
:
398 if x
.isplay
== y
.isplay
:
399 return name_sort(x
, y
)
401 return y
.isplay
- x
.isplay
403 return y
.isdir
- x
.isdir
406 return cmp(x
.name
, y
.name
)
408 subcname
= query
['Container'][0]
409 cname
= subcname
.split('/')[0]
410 path
= self
.get_local_path(handler
, query
)
412 file_type
= query
.get('Filter', [''])[0]
414 recurse
= query
.get('Recurse', ['No'])[0] == 'Yes'
417 if recurse
and path
in self
.recurse_cache
:
418 if self
.recurse_cache
.mtime(path
) + 3600 >= time
.time():
419 filelist
= self
.recurse_cache
[path
]
420 elif not recurse
and path
in self
.dir_cache
:
421 if self
.dir_cache
.mtime(path
) >= os
.stat(path
)[8]:
422 filelist
= self
.dir_cache
[path
]
425 filelist
= SortList(build_recursive_list(path
, recurse
))
428 self
.recurse_cache
[path
] = filelist
430 self
.dir_cache
[path
] = filelist
435 sortby
= query
.get('SortOrder', ['Normal'])[0]
436 if 'Random' in sortby
:
437 if 'RandomSeed' in query
:
438 seed
= query
['RandomSeed'][0]
440 if 'RandomStart' in query
:
441 start
= query
['RandomStart'][0]
444 if filelist
.unsorted
or filelist
.sortby
!= sortby
:
445 if 'Random' in sortby
:
446 self
.random_lock
.acquire()
449 random
.shuffle(filelist
.files
)
450 self
.random_lock
.release()
452 local_base_path
= self
.get_local_base_path(handler
, query
)
453 start
= unquote(start
)
454 start
= start
.replace(os
.path
.sep
+ cname
,
456 filenames
= [x
.name
for x
in filelist
.files
]
458 index
= filenames
.index(start
)
459 i
= filelist
.files
.pop(index
)
460 filelist
.files
.insert(0, i
)
462 print 'Start not found:', start
464 filelist
.files
.sort(dir_sort
)
466 filelist
.sortby
= sortby
467 filelist
.unsorted
= False
469 files
= filelist
.files
[:]
472 files
, total
, start
= self
.item_count(handler
, query
, cname
, files
,
474 filelist
.last_start
= start
475 return files
, total
, start
477 def get_playlist(self
, handler
, query
):
478 subcname
= query
['Container'][0]
479 cname
= subcname
.split('/')[0]
482 url
= subcname
.index('http://')
483 list_name
= subcname
[url
:]
485 list_name
= self
.get_local_path(handler
, query
)
487 recurse
= query
.get('Recurse', ['No'])[0] == 'Yes'
488 playlist
= self
.parse_playlist(list_name
, recurse
)
491 if 'Random' in query
.get('SortOrder', ['Normal'])[0]:
492 seed
= query
.get('RandomSeed', [''])[0]
493 start
= query
.get('RandomStart', [''])[0]
495 self
.random_lock
.acquire()
498 random
.shuffle(playlist
)
499 self
.random_lock
.release()
501 local_base_path
= self
.get_local_base_path(handler
, query
)
502 start
= unquote(start
)
503 start
= start
.replace(os
.path
.sep
+ cname
,
505 filenames
= [x
.name
for x
in playlist
]
507 index
= filenames
.index(start
)
508 i
= playlist
.pop(index
)
509 playlist
.insert(0, i
)
511 print 'Start not found:', start
514 return self
.item_count(handler
, query
, cname
, playlist
)