shoutcast.py 3.5 KB

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