Changeset 6c43706e0a9fbcf5d82c8f538bc8ab254234942d

Show
Ignore:
Timestamp:
12/29/07 21:24:43 (1 year ago)
Author:
William McBrine <wmcbrine@gmail.com>
git-committer:
William McBrine <wmcbrine@gmail.com> 1198985083 -0500
git-parent:

[052b7c82d665d02cbe40c7f338ba757614871a32]

git-author:
William McBrine <wmcbrine@gmail.com> 1198985083 -0500
Message:

Add playlist functionality to the music module -- version 0.9 (first git version). This also incorporates several other changes, intended to make it faster, more portable, and more conformant to the HMO spec.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • plugins/music/music.py

    rba90f24 r6c43706  
    1 import os, socket, re, sys 
     1import os, random, re, shutil, socket, sys, urllib 
    22from Cheetah.Template import Template 
     3from Cheetah.Filters import Filter 
    34from plugin import Plugin 
    4 from urllib import unquote_plus, quote, unquote 
    55from xml.sax.saxutils import escape 
    66from lrucache import LRUCache 
     7from urlparse import urlparse 
    78import eyeD3 
    89 
     
    1112CLASS_NAME = 'Music' 
    1213 
     14PLAYLISTS = ('.m3u', '.ram', '.pls', '.b4s', '.wpl', '.asx', '.wax', '.wvx') 
     15 
     16# Search strings for different playlist types 
     17asxfile = re.compile('ref +href *= *"(.+)"', re.IGNORECASE).search 
     18wplfile = re.compile('media +src *= *"(.+)"', re.IGNORECASE).search 
     19b4sfile = re.compile('Playstring="file:(.+)"').search 
     20plsfile = re.compile('[Ff]ile(\d+)=(.+)').match 
     21plstitle = re.compile('[Tt]itle(\d+)=(.+)').match 
     22plslength = re.compile('[Ll]ength(\d+)=(\d+)').match 
     23 
     24if os.path.sep == '/': 
     25    quote = urllib.quote 
     26    unquote = urllib.unquote_plus 
     27else: 
     28    quote = lambda x: urllib.quote(x.replace(os.path.sep, '/')) 
     29    unquote = lambda x: urllib.unquote_plus(x).replace('/', os.path.sep) 
     30 
     31class 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 
     39class 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 
    1354class Music(Plugin): 
    14      
     55 
    1556    CONTENT_TYPE = 'x-container/tivo-music' 
    1657 
    1758    AUDIO = 'audio' 
    1859    DIRECTORY = 'dir' 
    19  
    20     playable_cache = {} 
    21     playable_cache = LRUCache(1000) 
    22     media_data_cache = LRUCache(100) 
    23          
     60    PLAYLIST = 'play' 
     61 
     62    media_data_cache = LRUCache(300) 
     63 
     64    def send_file(self, handler, container, name): 
     65        o = urlparse("http://fake.host" + handler.path) 
     66        path = unquote(o[2]) 
     67        fname = container['path'] + path[len(name) + 1:] 
     68        fsize = os.path.getsize(fname) 
     69        handler.send_response(200) 
     70        handler.send_header('Content-Type', 'audio/mpeg') 
     71        handler.send_header('Content-Length', fsize) 
     72        handler.send_header('Connection', 'close') 
     73        handler.end_headers() 
     74        f = file(fname, 'rb') 
     75        shutil.copyfileobj(f, handler.wfile) 
     76 
    2477    def QueryContainer(self, handler, query): 
    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 
    26141        subcname = query['Container'][0] 
    27142        cname = subcname.split('/')[0] 
    28143        local_base_path = self.get_local_base_path(handler, query) 
    29           
    30         if not handler.server.containers.has_key(cname) or not self.get_local_path(handler, query): 
     144 
     145        if not handler.server.containers.has_key(cname) or \ 
     146           not self.get_local_path(handler, query): 
    31147            handler.send_response(404) 
    32148            handler.end_headers() 
    33149            return 
    34150         
    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 
    96162        t.name = subcname 
    97         t.container = cname 
    98         t.files, t.total, t.start = self.get_files(handler, query, AudioFileFilter) 
    99         t.files = map(media_data, t.files) 
    100163        t.quote = quote 
    101164        t.escape = escape 
    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) 
  • plugins/music/templates/container.tmpl

    rba90f24 r6c43706  
    1 <?xml version="1.0" encoding="ISO-8859-1" ?> 
     1<?xml version="1.0" encoding="UTF-8" ?> 
    22<TiVoContainer> 
    33    <ItemStart>$start</ItemStart> 
     
    1515            <Title>$escape($file.name)</Title> 
    1616            <ContentType>x-container/folder</ContentType> 
    17             <SourceFormat>x-container/folder</SourceFormat> 
    1817        </Details> 
    1918        <Links> 
    2019            <Content> 
    21                     <Url>/TiVoConnect?Command=QueryContainer&amp;Container=$quote($name)/$quote($file.name)</Url> 
    22                     <ContentType>x-tivo-container/folder</ContentType> 
     20                <ContentType>x-tivo-container/folder</ContentType> 
     21                <Url>/TiVoConnect?Command=QueryContainer&amp;Container=$quote($name)/$quote($file.name)</Url> 
     22            </Content> 
     23        </Links> 
     24    </Item> 
     25    #elif $file['is_playlist'] 
     26    <Item> 
     27        <Details> 
     28            <Title>#echo '.'.join(file['name'].split('.')[:-1]) #</Title> 
     29            <ContentType>x-tivo-container/playlist</ContentType> 
     30        </Details> 
     31        <Links> 
     32            <Content> 
     33                <ContentType>x-tivo-container/playlist</ContentType> 
     34                <Url>/TiVoConnect?Command=QueryContainer&amp;Container=$quote($name)/$quote($file.name)</Url> 
    2335            </Content> 
    2436        </Links> 
     
    2739    <Item> 
    2840        <Details> 
     41            #if not 'Title' in $file 
    2942            <Title>#echo '.'.join(file['name'].split('.')[:-1]) #</Title> 
    30             <ContentType>audio/*</ContentType> 
    31             <SourceFormat>audio/mpeg</SourceFormat> 
    32  
    33             #for $key in $file 
    34                 <$key>$file[$key]</$key> 
     43            #end if 
     44            <ContentType>audio/mpeg</ContentType> 
     45            #for $key in ('Title', 'Duration', 'ArtistName', 'SongTitle', 'AlbumTitle', 'AlbumYear', 'MusicGenre') 
     46            #if $key in $file and $file[$key] 
     47            <$key>$file[$key]</$key> 
     48            #end if 
    3549            #end for             
    36  
    3750        </Details> 
    3851        <Links> 
    3952            <Content> 
    40                 <ContentType>audio/*</ContentType> 
    41                     <AcceptsParams>No</AcceptsParams> 
    42                     <Url>/$quote($container)$quote($file.part_path)</Url> 
    43                 </Content> 
     53                <ContentType>audio/mpeg</ContentType> 
     54                <AcceptsParams>No</AcceptsParams> 
     55                <Url>/$quote($container)$quote($file.part_path)</Url> 
     56            </Content> 
    4457        </Links> 
    4558    </Item> 
     
    4760    #end for 
    4861</TiVoContainer> 
    49