| OLD | NEW | 
 | (Empty) | 
|    1 # Copyright (c) 2001-2004 Twisted Matrix Laboratories. |  | 
|    2 # See LICENSE for details. |  | 
|    3  |  | 
|    4  |  | 
|    5 """Chop up shoutcast stream into MP3s and metadata, if available.""" |  | 
|    6  |  | 
|    7 from twisted.web import http |  | 
|    8 from twisted import copyright |  | 
|    9  |  | 
|   10  |  | 
|   11 class ShoutcastClient(http.HTTPClient): |  | 
|   12     """Shoutcast HTTP stream. |  | 
|   13  |  | 
|   14     Modes can be 'length', 'meta' and 'mp3'. |  | 
|   15  |  | 
|   16     See http://www.smackfu.com/stuff/programming/shoutcast.html |  | 
|   17     for details on the protocol. |  | 
|   18     """ |  | 
|   19  |  | 
|   20     userAgent = "Twisted Shoutcast client " + copyright.version |  | 
|   21  |  | 
|   22     def __init__(self, path="/"): |  | 
|   23         self.path = path |  | 
|   24         self.got_metadata = False |  | 
|   25         self.metaint = None |  | 
|   26         self.metamode = "mp3" |  | 
|   27         self.databuffer = "" |  | 
|   28          |  | 
|   29     def connectionMade(self): |  | 
|   30         self.sendCommand("GET", self.path) |  | 
|   31         self.sendHeader("User-Agent", self.userAgent) |  | 
|   32         self.sendHeader("Icy-MetaData", "1") |  | 
|   33         self.endHeaders() |  | 
|   34          |  | 
|   35     def lineReceived(self, line): |  | 
|   36         # fix shoutcast crappiness |  | 
|   37         if not self.firstLine and line: |  | 
|   38             if len(line.split(": ", 1)) == 1: |  | 
|   39                 line = line.replace(":", ": ", 1) |  | 
|   40         http.HTTPClient.lineReceived(self, line) |  | 
|   41      |  | 
|   42     def handleHeader(self, key, value): |  | 
|   43         if key.lower() == 'icy-metaint': |  | 
|   44             self.metaint = int(value) |  | 
|   45             self.got_metadata = True |  | 
|   46  |  | 
|   47     def handleEndHeaders(self): |  | 
|   48         # Lets check if we got metadata, and set the |  | 
|   49         # appropriate handleResponsePart method. |  | 
|   50         if self.got_metadata: |  | 
|   51             # if we have metadata, then it has to be parsed out of the data stre
     am |  | 
|   52             self.handleResponsePart = self.handleResponsePart_with_metadata |  | 
|   53         else: |  | 
|   54             # otherwise, all the data is MP3 data |  | 
|   55             self.handleResponsePart = self.gotMP3Data |  | 
|   56  |  | 
|   57     def handleResponsePart_with_metadata(self, data): |  | 
|   58         self.databuffer += data |  | 
|   59         while self.databuffer: |  | 
|   60             stop = getattr(self, "handle_%s" % self.metamode)() |  | 
|   61             if stop: |  | 
|   62                 return |  | 
|   63  |  | 
|   64     def handle_length(self): |  | 
|   65         self.remaining = ord(self.databuffer[0]) * 16 |  | 
|   66         self.databuffer = self.databuffer[1:] |  | 
|   67         self.metamode = "meta" |  | 
|   68      |  | 
|   69     def handle_mp3(self): |  | 
|   70         if len(self.databuffer) > self.metaint: |  | 
|   71             self.gotMP3Data(self.databuffer[:self.metaint]) |  | 
|   72             self.databuffer = self.databuffer[self.metaint:] |  | 
|   73             self.metamode = "length" |  | 
|   74         else: |  | 
|   75             return 1 |  | 
|   76      |  | 
|   77     def handle_meta(self): |  | 
|   78         if len(self.databuffer) >= self.remaining: |  | 
|   79             if self.remaining: |  | 
|   80                 data = self.databuffer[:self.remaining] |  | 
|   81                 self.gotMetaData(self.parseMetadata(data)) |  | 
|   82             self.databuffer = self.databuffer[self.remaining:] |  | 
|   83             self.metamode = "mp3" |  | 
|   84         else: |  | 
|   85             return 1 |  | 
|   86  |  | 
|   87     def parseMetadata(self, data): |  | 
|   88         meta = [] |  | 
|   89         for chunk in data.split(';'): |  | 
|   90             chunk = chunk.strip().replace("\x00", "") |  | 
|   91             if not chunk: |  | 
|   92                 continue |  | 
|   93             key, value = chunk.split('=', 1) |  | 
|   94             if value.startswith("'") and value.endswith("'"): |  | 
|   95                 value = value[1:-1] |  | 
|   96             meta.append((key, value)) |  | 
|   97         return meta |  | 
|   98      |  | 
|   99     def gotMetaData(self, metadata): |  | 
|  100         """Called with a list of (key, value) pairs of metadata, |  | 
|  101         if metadata is available on the server. |  | 
|  102  |  | 
|  103         Will only be called on non-empty metadata. |  | 
|  104         """ |  | 
|  105         raise NotImplementedError, "implement in subclass" |  | 
|  106      |  | 
|  107     def gotMP3Data(self, data): |  | 
|  108         """Called with chunk of MP3 data.""" |  | 
|  109         raise NotImplementedError, "implement in subclass" |  | 
|  110  |  | 
|  111  |  | 
|  112 if __name__ == '__main__': |  | 
|  113     class Test(ShoutcastClient): |  | 
|  114         def gotMetaData(self, data): print "meta:", data |  | 
|  115         def gotMP3Data(self, data): pass |  | 
|  116      |  | 
|  117     from twisted.internet import protocol, reactor |  | 
|  118     import sys |  | 
|  119     protocol.ClientCreator(reactor, Test).connectTCP(sys.argv[1], int(sys.argv[2
     ])) |  | 
|  120     reactor.run() |  | 
| OLD | NEW |