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 |