| | 14 | PLAYLISTS = ('.m3u', '.ram', '.pls', '.b4s', '.wpl', '.asx', '.wax', '.wvx') |
|---|
| | 15 | |
|---|
| | 16 | # Search strings for different playlist types |
|---|
| | 17 | asxfile = re.compile('ref +href *= *"(.+)"', re.IGNORECASE).search |
|---|
| | 18 | wplfile = re.compile('media +src *= *"(.+)"', re.IGNORECASE).search |
|---|
| | 19 | b4sfile = re.compile('Playstring="file:(.+)"').search |
|---|
| | 20 | plsfile = re.compile('[Ff]ile(\d+)=(.+)').match |
|---|
| | 21 | plstitle = re.compile('[Tt]itle(\d+)=(.+)').match |
|---|
| | 22 | plslength = re.compile('[Ll]ength(\d+)=(\d+)').match |
|---|
| | 23 | |
|---|
| | 24 | if os.path.sep == '/': |
|---|
| | 25 | quote = urllib.quote |
|---|
| | 26 | unquote = urllib.unquote_plus |
|---|
| | 27 | else: |
|---|
| | 28 | quote = lambda x: urllib.quote(x.replace(os.path.sep, '/')) |
|---|
| | 29 | unquote = lambda x: urllib.unquote_plus(x).replace('/', os.path.sep) |
|---|
| | 30 | |
|---|
| | 31 | class FileData: |
|---|
| | 32 | def __init__(self, name, isdir): |
|---|
| | 33 | self.name = name |
|---|
| | 34 | self.isdir = isdir |
|---|
| | 35 | self.isplay = os.path.splitext(name)[1].lower() in PLAYLISTS |
|---|
| | 36 | self.title = '' |
|---|
| | 37 | self.duration = 0 |
|---|
| | 38 | |
|---|
| | 39 | class EncodeUnicode(Filter): |
|---|
| | 40 | def filter(self, val, **kw): |
|---|
| | 41 | """Encode Unicode strings, by default in UTF-8""" |
|---|
| | 42 | |
|---|
| | 43 | if kw.has_key('encoding'): |
|---|
| | 44 | encoding = kw['encoding'] |
|---|
| | 45 | else: |
|---|
| | 46 | encoding='utf8' |
|---|
| | 47 | |
|---|
| | 48 | if type(val) == type(u''): |
|---|
| | 49 | filtered = val.encode(encoding) |
|---|
| | 50 | else: |
|---|
| | 51 | filtered = str(val) |
|---|
| | 52 | return filtered |
|---|
| | 53 | |
|---|
| 25 | | |
|---|
| | 78 | |
|---|
| | 79 | def AudioFileFilter(f, filter_type=None): |
|---|
| | 80 | |
|---|
| | 81 | if filter_type: |
|---|
| | 82 | filter_start = filter_type.split('/')[0] |
|---|
| | 83 | else: |
|---|
| | 84 | filter_start = filter_type |
|---|
| | 85 | |
|---|
| | 86 | if os.path.isdir(f): |
|---|
| | 87 | ftype = self.DIRECTORY |
|---|
| | 88 | |
|---|
| | 89 | elif eyeD3.isMp3File(f): |
|---|
| | 90 | ftype = self.AUDIO |
|---|
| | 91 | elif os.path.splitext(f)[1].lower() in PLAYLISTS: |
|---|
| | 92 | ftype = self.PLAYLIST |
|---|
| | 93 | else: |
|---|
| | 94 | ftype = False |
|---|
| | 95 | |
|---|
| | 96 | if filter_start == self.AUDIO: |
|---|
| | 97 | if ftype == self.AUDIO: |
|---|
| | 98 | return ftype |
|---|
| | 99 | else: |
|---|
| | 100 | return False |
|---|
| | 101 | else: |
|---|
| | 102 | return ftype |
|---|
| | 103 | |
|---|
| | 104 | def media_data(f): |
|---|
| | 105 | if f.name in self.media_data_cache: |
|---|
| | 106 | return self.media_data_cache[f.name] |
|---|
| | 107 | |
|---|
| | 108 | item = {} |
|---|
| | 109 | item['path'] = f.name |
|---|
| | 110 | item['part_path'] = f.name.replace(local_base_path, '') |
|---|
| | 111 | item['name'] = os.path.split(f.name)[1] |
|---|
| | 112 | item['is_dir'] = f.isdir |
|---|
| | 113 | item['is_playlist'] = f.isplay |
|---|
| | 114 | |
|---|
| | 115 | if f.title: |
|---|
| | 116 | item['Title'] = f.title |
|---|
| | 117 | |
|---|
| | 118 | if f.duration > 0: |
|---|
| | 119 | item['Duration'] = f.duration |
|---|
| | 120 | |
|---|
| | 121 | if f.isdir or f.isplay or '://' in f.name: |
|---|
| | 122 | self.media_data_cache[f.name] = item |
|---|
| | 123 | return item |
|---|
| | 124 | |
|---|
| | 125 | try: |
|---|
| | 126 | audioFile = eyeD3.Mp3AudioFile(f.name) |
|---|
| | 127 | item['Duration'] = audioFile.getPlayTime() * 1000 |
|---|
| | 128 | |
|---|
| | 129 | tag = audioFile.getTag() |
|---|
| | 130 | item['ArtistName'] = tag.getArtist() |
|---|
| | 131 | item['AlbumTitle'] = tag.getAlbum() |
|---|
| | 132 | item['SongTitle'] = tag.getTitle() |
|---|
| | 133 | item['AlbumYear'] = tag.getYear() |
|---|
| | 134 | item['MusicGenre'] = tag.getGenre().getName() |
|---|
| | 135 | except Exception, msg: |
|---|
| | 136 | print msg |
|---|
| | 137 | |
|---|
| | 138 | self.media_data_cache[f.name] = item |
|---|
| | 139 | return item |
|---|
| | 140 | |
|---|
| 35 | | def AudioFileFilter(file, filter_type = None): |
|---|
| 36 | | |
|---|
| 37 | | if filter_type: |
|---|
| 38 | | filter_start = filter_type.split('/')[0] |
|---|
| 39 | | else: |
|---|
| 40 | | filter_start = filter_type |
|---|
| 41 | | |
|---|
| 42 | | if file not in self.playable_cache: |
|---|
| 43 | | if os.path.isdir(file): |
|---|
| 44 | | self.playable_cache[file] = self.DIRECTORY |
|---|
| 45 | | |
|---|
| 46 | | elif eyeD3.isMp3File(file): |
|---|
| 47 | | self.playable_cache[file] = self.AUDIO |
|---|
| 48 | | else: |
|---|
| 49 | | self.playable_cache[file] = False |
|---|
| 50 | | |
|---|
| 51 | | if filter_start == self.AUDIO: |
|---|
| 52 | | if self.playable_cache[file] == self.AUDIO: |
|---|
| 53 | | return self.playable_cache[file] |
|---|
| 54 | | else: |
|---|
| 55 | | return False |
|---|
| 56 | | else: |
|---|
| 57 | | return self.playable_cache[file] |
|---|
| 58 | | |
|---|
| 59 | | |
|---|
| 60 | | def media_data(file): |
|---|
| 61 | | dict = {} |
|---|
| 62 | | dict['path'] = file |
|---|
| 63 | | dict['part_path'] = file.replace(local_base_path, '') |
|---|
| 64 | | dict['name'] = os.path.split(file)[1] |
|---|
| 65 | | dict['is_dir'] = os.path.isdir(file) |
|---|
| 66 | | |
|---|
| 67 | | if file in self.media_data_cache: |
|---|
| 68 | | return self.media_data_cache[file] |
|---|
| 69 | | |
|---|
| 70 | | if os.path.isdir(file) or not eyeD3.isMp3File(file): |
|---|
| 71 | | self.media_data_cache[file] = dict |
|---|
| 72 | | return dict |
|---|
| 73 | | |
|---|
| 74 | | try: |
|---|
| 75 | | audioFile = eyeD3.Mp3AudioFile(file) |
|---|
| 76 | | dict['Duration'] = audioFile.getPlayTime() * 1000 |
|---|
| 77 | | dict['SourceBitRate'] = audioFile.getBitRate()[1] |
|---|
| 78 | | dict['SourceSampleRate'] = audioFile.getSampleFreq() |
|---|
| 79 | | |
|---|
| 80 | | tag = audioFile.getTag() |
|---|
| 81 | | dict['ArtistName'] = str(tag.getArtist()) |
|---|
| 82 | | dict['AlbumTitle'] = str(tag.getAlbum()) |
|---|
| 83 | | dict['SongTitle'] = str(tag.getTitle()) |
|---|
| 84 | | dict['AlbumYear'] = tag.getYear() |
|---|
| 85 | | |
|---|
| 86 | | dict['MusicGenre'] = tag.getGenre().getName() |
|---|
| 87 | | except: |
|---|
| 88 | | pass |
|---|
| 89 | | |
|---|
| 90 | | self.media_data_cache[file] = dict |
|---|
| 91 | | return dict |
|---|
| 92 | | |
|---|
| 93 | | handler.send_response(200) |
|---|
| 94 | | handler.end_headers() |
|---|
| 95 | | t = Template(file=os.path.join(SCRIPTDIR,'templates', 'container.tmpl')) |
|---|
| | 151 | if os.path.splitext(subcname)[1].lower() in PLAYLISTS: |
|---|
| | 152 | t = Template(file=os.path.join(SCRIPTDIR, 'templates', 'm3u.tmpl'), |
|---|
| | 153 | filter=EncodeUnicode) |
|---|
| | 154 | t.files, t.total, t.start = self.get_playlist(handler, query) |
|---|
| | 155 | else: |
|---|
| | 156 | t = Template(file=os.path.join(SCRIPTDIR,'templates', |
|---|
| | 157 | 'container.tmpl'), filter=EncodeUnicode) |
|---|
| | 158 | t.files, t.total, t.start = self.get_files(handler, query, |
|---|
| | 159 | AudioFileFilter) |
|---|
| | 160 | t.files = map(media_data, t.files) |
|---|
| | 161 | t.container = cname |
|---|
| 102 | | handler.wfile.write(t) |
|---|
| 103 | | |
|---|
| 104 | | |
|---|
| 105 | | |
|---|
| | 165 | page = str(t) |
|---|
| | 166 | |
|---|
| | 167 | handler.send_response(200) |
|---|
| | 168 | handler.send_header('Content-Type', 'text/xml') |
|---|
| | 169 | handler.send_header('Content-Length', len(page)) |
|---|
| | 170 | handler.send_header('Connection', 'close') |
|---|
| | 171 | handler.end_headers() |
|---|
| | 172 | handler.wfile.write(page) |
|---|
| | 173 | |
|---|
| | 174 | def item_count(self, handler, query, cname, files): |
|---|
| | 175 | """Return only the desired portion of the list, as specified by |
|---|
| | 176 | ItemCount, AnchorItem and AnchorOffset |
|---|
| | 177 | """ |
|---|
| | 178 | totalFiles = len(files) |
|---|
| | 179 | index = 0 |
|---|
| | 180 | |
|---|
| | 181 | if query.has_key('ItemCount'): |
|---|
| | 182 | count = int(query['ItemCount'][0]) |
|---|
| | 183 | |
|---|
| | 184 | if query.has_key('AnchorItem'): |
|---|
| | 185 | bs = '/TiVoConnect?Command=QueryContainer&Container=' |
|---|
| | 186 | local_base_path = self.get_local_base_path(handler, query) |
|---|
| | 187 | |
|---|
| | 188 | anchor = query['AnchorItem'][0] |
|---|
| | 189 | if anchor.startswith(bs): |
|---|
| | 190 | anchor = anchor.replace(bs, '/') |
|---|
| | 191 | anchor = unquote(anchor) |
|---|
| | 192 | anchor = anchor.replace(os.path.sep + cname, local_base_path) |
|---|
| | 193 | anchor = os.path.normpath(anchor) |
|---|
| | 194 | |
|---|
| | 195 | filenames = [x.name for x in files] |
|---|
| | 196 | try: |
|---|
| | 197 | index = filenames.index(anchor) |
|---|
| | 198 | except ValueError: |
|---|
| | 199 | print 'Anchor not found:', anchor # just use index = 0 |
|---|
| | 200 | |
|---|
| | 201 | if count > 0: |
|---|
| | 202 | index += 1 |
|---|
| | 203 | |
|---|
| | 204 | if query.has_key('AnchorOffset'): |
|---|
| | 205 | index += int(query['AnchorOffset'][0]) |
|---|
| | 206 | |
|---|
| | 207 | #foward count |
|---|
| | 208 | if count > 0: |
|---|
| | 209 | files = files[index:index + count] |
|---|
| | 210 | #backwards count |
|---|
| | 211 | elif count < 0: |
|---|
| | 212 | if index + count < 0: |
|---|
| | 213 | count = -index |
|---|
| | 214 | files = files[index + count:index] |
|---|
| | 215 | index += count |
|---|
| | 216 | |
|---|
| | 217 | else: # No AnchorItem |
|---|
| | 218 | |
|---|
| | 219 | if count >= 0: |
|---|
| | 220 | files = files[:count] |
|---|
| | 221 | elif count < 0: |
|---|
| | 222 | index = count % len(files) |
|---|
| | 223 | files = files[count:] |
|---|
| | 224 | |
|---|
| | 225 | return files, totalFiles, index |
|---|
| | 226 | |
|---|
| | 227 | def get_files(self, handler, query, filterFunction=None): |
|---|
| | 228 | |
|---|
| | 229 | def build_recursive_list(path, recurse=True): |
|---|
| | 230 | files = [] |
|---|
| | 231 | for f in os.listdir(path): |
|---|
| | 232 | f = os.path.join(path, f) |
|---|
| | 233 | isdir = os.path.isdir(f) |
|---|
| | 234 | if recurse and isdir: |
|---|
| | 235 | files.extend(build_recursive_list(f)) |
|---|
| | 236 | else: |
|---|
| | 237 | if isdir or filterFunction(f, file_type): |
|---|
| | 238 | files.append(FileData(f, isdir)) |
|---|
| | 239 | return files |
|---|
| | 240 | |
|---|
| | 241 | def dir_sort(x, y): |
|---|
| | 242 | if x.isdir == y.isdir: |
|---|
| | 243 | if x.isplay == y.isplay: |
|---|
| | 244 | return name_sort(x, y) |
|---|
| | 245 | else: |
|---|
| | 246 | return y.isplay - x.isplay |
|---|
| | 247 | else: |
|---|
| | 248 | return y.isdir - x.isdir |
|---|
| | 249 | |
|---|
| | 250 | def name_sort(x, y): |
|---|
| | 251 | return cmp(x.name, y.name) |
|---|
| | 252 | |
|---|
| | 253 | subcname = query['Container'][0] |
|---|
| | 254 | cname = subcname.split('/')[0] |
|---|
| | 255 | path = self.get_local_path(handler, query) |
|---|
| | 256 | |
|---|
| | 257 | file_type = query.get('Filter', [''])[0] |
|---|
| | 258 | |
|---|
| | 259 | recurse = query.get('Recurse',['No'])[0] == 'Yes' |
|---|
| | 260 | filelist = build_recursive_list(path, recurse) |
|---|
| | 261 | |
|---|
| | 262 | # Sort |
|---|
| | 263 | if query.get('SortOrder',['Normal'])[0] == 'Random': |
|---|
| | 264 | seed = query.get('RandomSeed', ['1'])[0] |
|---|
| | 265 | self.random_lock.acquire() |
|---|
| | 266 | random.seed(seed) |
|---|
| | 267 | random.shuffle(filelist) |
|---|
| | 268 | self.random_lock.release() |
|---|
| | 269 | else: |
|---|
| | 270 | filelist.sort(dir_sort) |
|---|
| | 271 | |
|---|
| | 272 | # Trim the list |
|---|
| | 273 | return self.item_count(handler, query, cname, filelist) |
|---|
| | 274 | |
|---|
| | 275 | def get_playlist(self, handler, query): |
|---|
| | 276 | subcname = query['Container'][0] |
|---|
| | 277 | cname = subcname.split('/')[0] |
|---|
| | 278 | |
|---|
| | 279 | list_name = self.get_local_path(handler, query) |
|---|
| | 280 | local_path = os.path.sep.join(list_name.split(os.path.sep)[:-1]) |
|---|
| | 281 | ext = os.path.splitext(list_name)[1].lower() |
|---|
| | 282 | |
|---|
| | 283 | if ext in ('.wpl', '.asx', '.wax', '.wvx', '.b4s'): |
|---|
| | 284 | playlist = [] |
|---|
| | 285 | for line in file(list_name): |
|---|
| | 286 | if ext == '.wpl': |
|---|
| | 287 | s = wplfile(line) |
|---|
| | 288 | elif ext == '.b4s': |
|---|
| | 289 | s = b4sfile(line) |
|---|
| | 290 | else: |
|---|
| | 291 | s = asxfile(line) |
|---|
| | 292 | if s: |
|---|
| | 293 | playlist.append(FileData(s.group(1), False)) |
|---|
| | 294 | |
|---|
| | 295 | elif ext == '.pls': |
|---|
| | 296 | names, titles, lengths = {}, {}, {} |
|---|
| | 297 | for line in file(list_name): |
|---|
| | 298 | s = plsfile(line) |
|---|
| | 299 | if s: |
|---|
| | 300 | names[s.group(1)] = s.group(2) |
|---|
| | 301 | else: |
|---|
| | 302 | s = plstitle(line) |
|---|
| | 303 | if s: |
|---|
| | 304 | titles[s.group(1)] = s.group(2) |
|---|
| | 305 | else: |
|---|
| | 306 | s = plslength(line) |
|---|
| | 307 | if s: |
|---|
| | 308 | lengths[s.group(1)] = int(s.group(2)) |
|---|
| | 309 | playlist = [] |
|---|
| | 310 | for key in names: |
|---|
| | 311 | f = FileData(names[key], False) |
|---|
| | 312 | if key in titles: |
|---|
| | 313 | f.title = titles[key] |
|---|
| | 314 | if key in lengths: |
|---|
| | 315 | f.duration = lengths[key] |
|---|
| | 316 | playlist.append(f) |
|---|
| | 317 | |
|---|
| | 318 | else: # ext == '.m3u' or '.ram' |
|---|
| | 319 | duration, title = 0, '' |
|---|
| | 320 | playlist = [] |
|---|
| | 321 | for x in file(list_name): |
|---|
| | 322 | x = x.strip() |
|---|
| | 323 | if x: |
|---|
| | 324 | if x.startswith('#EXTINF:'): |
|---|
| | 325 | try: |
|---|
| | 326 | duration, title = x[8:].split(',') |
|---|
| | 327 | duration = int(duration) |
|---|
| | 328 | except ValueError: |
|---|
| | 329 | duration = 0 |
|---|
| | 330 | |
|---|
| | 331 | elif not x.startswith('#'): |
|---|
| | 332 | f = FileData(x, False) |
|---|
| | 333 | f.title = title.strip() |
|---|
| | 334 | f.duration = duration |
|---|
| | 335 | playlist.append(f) |
|---|
| | 336 | duration, title = 0, '' |
|---|
| | 337 | |
|---|
| | 338 | # Expand relative paths |
|---|
| | 339 | for i in xrange(len(playlist)): |
|---|
| | 340 | if not '://' in playlist[i].name: |
|---|
| | 341 | name = playlist[i].name |
|---|
| | 342 | if not os.path.isabs(name): |
|---|
| | 343 | name = os.path.join(local_path, name) |
|---|
| | 344 | playlist[i].name = os.path.normpath(name) |
|---|
| | 345 | |
|---|
| | 346 | # Trim the list |
|---|
| | 347 | return self.item_count(handler, query, cname, playlist) |
|---|