├── .gitignore ├── LICENSE ├── README.md ├── setup.cfg ├── setup.py └── streamscrobbler ├── __init__.py └── streamscrobbler.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | __pycache__ 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | .tox 28 | nosetests.xml 29 | 30 | # Translations 31 | *.mo 32 | 33 | # Mr Developer 34 | .mr.developer.cfg 35 | .project 36 | .pydevproject 37 | .idea/* 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Håkan Nylén 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | streamscrobbler-python 2 | ====================== 3 | 4 | This python class gets the metadata and song played on a stream. The metadata is content-type (mpeg, ACC, ACC+), bitrate and played song. It has support to handling pls files directly for now. m3u support will come soon. 5 | 6 | 7 | ### dependencies 8 | Streamscrobbler is importing these packages: 9 | 10 | * httplib2 as http 11 | * httplib 12 | * re 13 | * urlparse 14 | * pprint 15 | * urllib2 16 | 17 | prepare by installing them before testing this class. 18 | 19 | ### Streams supported 20 | Supports the following streamtypes: 21 | 22 | * Shoutcast 23 | * Icecast 24 | * probably other custom made that use icy-metaint 25 | 26 | And also different stream services: 27 | 28 | * Radionomy 29 | * Streammachine 30 | * tunein 31 | 32 | ### How to use it 33 | You use one function to get a object of status and metadata. 34 | status is a integer of 0,1,2 - 0 is down, 1 is up, 2 is up with metadata 35 | metadata is a object of bitrate, content-type and songtitle. 36 | 37 | See how to call it under here: 38 | ```Python 39 | from streamscrobbler import streamscrobbler 40 | streamscrobbler = streamscrobbler() 41 | 42 | ##streamurl can be a url directly to the stream or to a pls file. Support for m3u is coming soon. 43 | streamurl = "http://217.198.148.101:80/" 44 | stationinfo = streamscrobbler.getServerInfo(streamurl) 45 | ##metadata is the bitrate and current song 46 | metadata = stationinfo.get("metadata") 47 | ## status is the integer to tell if the server is up or down, 0 means down, 1 up, 2 means up but also got metadata. 48 | status = stationinfo.get("status") 49 | ``` -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | from setuptools import setup, find_packages 5 | import sys 6 | import os 7 | 8 | 9 | setup( 10 | name = "streamscrobbler", 11 | packages = ["streamscrobbler"], 12 | version = "0.0.1", 13 | description = "A python class used on Dirble.com to get titles and bitrates on shoutcast and icecast streams", 14 | author = "Håkan Nylén", 15 | author_email = "confacted@gmail.com", 16 | url = "https://github.com/Dirble/streamscrobbler-python", 17 | keywords = ["shoutcast", "icecast", "stream", "ICY"], 18 | install_requires = ["httplib2"], 19 | classifiers = [], 20 | include_package_data = True 21 | ) 22 | -------------------------------------------------------------------------------- /streamscrobbler/__init__.py: -------------------------------------------------------------------------------- 1 | from streamscrobbler import * -------------------------------------------------------------------------------- /streamscrobbler/streamscrobbler.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | import httplib2 as http 4 | import httplib 5 | import re 6 | from urlparse import urlparse 7 | import pprint 8 | import urllib2 9 | 10 | 11 | class streamscrobbler: 12 | def parse_headers(self, response): 13 | headers = {} 14 | int = 0 15 | while True: 16 | line = response.readline() 17 | if line == '\r\n': 18 | break # end of headers 19 | if ':' in line: 20 | key, value = line.split(':', 1) 21 | headers[key] = value.rstrip() 22 | if int == 12: 23 | break; 24 | int = int + 1 25 | return headers 26 | 27 | 28 | # this is the fucntion you should call with the url to get all data sorted as a object in the return 29 | def getServerInfo(self, url): 30 | print 31 | "shoutcast check v.2" 32 | 33 | if url.endswith('.pls') or url.endswith('listen.pls?sid=1'): 34 | address = self.checkPLS(url) 35 | else: 36 | address = url 37 | if isinstance(address, str): 38 | meta_interval = self.getAllData(address) 39 | else: 40 | meta_interval = {"status": 0, "metadata": None} 41 | 42 | return meta_interval 43 | 44 | def getAllData(self, address): 45 | shoutcast = False 46 | status = 0 47 | 48 | request = urllib2.Request(address) 49 | user_agent = 'iTunes/9.1.1' 50 | request.add_header('User-Agent', user_agent) 51 | request.add_header('icy-metadata', 1) 52 | try: 53 | response = urllib2.urlopen(request, timeout=6) 54 | headers = self.getHeaders(response) 55 | pp = pprint.PrettyPrinter(indent=4) 56 | print "parse headers: " 57 | pp.pprint(headers) 58 | 59 | if "server" in headers: 60 | shoutcast = headers['server'] 61 | elif "X-Powered-By" in headers: 62 | shoutcast = headers['X-Powered-By'] 63 | elif "icy-notice1" in headers: 64 | shoutcast = headers['icy-notice2'] 65 | else: 66 | shoutcast = bool(1) 67 | 68 | if isinstance(shoutcast, bool): 69 | if shoutcast is True: 70 | status = 1 71 | else: 72 | status = 0 73 | metadata = False; 74 | elif "SHOUTcast" in shoutcast: 75 | status = 1 76 | metadata = self.shoutcastCheck(response, headers, False) 77 | elif "Icecast" or "137" in shoutcast: 78 | status = 1 79 | metadata = self.shoutcastCheck(response, headers, True) 80 | elif "StreamMachine" in shoutcast: 81 | status = 1 82 | metadata = self.shoutcastCheck(response, headers, True) 83 | elif shoutcast is not None: 84 | status = 1 85 | metadata = self.shoutcastCheck(response, headers, True) 86 | else: 87 | metadata = False 88 | response.close() 89 | return {"status": status, "metadata": metadata} 90 | 91 | except urllib2.HTTPError, e: 92 | print ' Error, HTTPError = ' + str(e.code) 93 | return {"status": status, "metadata": None} 94 | 95 | except urllib2.URLError, e: 96 | print " Error, URLError: " + str(e.reason) 97 | return {"status": status, "metadata": None} 98 | 99 | except Exception, err: 100 | print " Error: " + str(err) 101 | return {"status": status, "metadata": None} 102 | 103 | 104 | def checkPLS(self, address): 105 | try: 106 | response = urllib2.urlopen(address, timeout=2) 107 | for line in response: 108 | if line.startswith("File1="): 109 | stream = line; 110 | 111 | response.close() 112 | if 'stream' in locals(): 113 | return stream[6:] 114 | else: 115 | return bool(0) 116 | except Exception: 117 | return bool(0) 118 | 119 | 120 | def shoutcastCheck(self, response, headers, itsOld): 121 | if itsOld is not True: 122 | if 'icy-br' in headers: 123 | bitrate = headers['icy-br'] 124 | bitrate = bitrate.rstrip() 125 | else: 126 | bitrate = None 127 | 128 | if 'icy-metaint' in headers: 129 | icy_metaint_header = headers['icy-metaint'] 130 | else: 131 | icy_metaint_header = None 132 | 133 | if "Content-Type" in headers: 134 | contenttype = headers['Content-Type'] 135 | elif 'content-type' in headers: 136 | contenttype = headers['content-type'] 137 | 138 | else: 139 | if 'icy-br' in headers: 140 | bitrate = headers['icy-br'].split(",")[0] 141 | else: 142 | bitrate = None 143 | if 'icy-metaint' in headers: 144 | icy_metaint_header = headers['icy-metaint'] 145 | else: 146 | icy_metaint_header = None 147 | 148 | if headers.get('Content-Type') is not None: 149 | contenttype = headers.get('Content-Type') 150 | elif headers.get('content-type') is not None: 151 | contenttype = headers.get('content-type') 152 | 153 | if icy_metaint_header is not None: 154 | metaint = int(icy_metaint_header) 155 | print "icy metaint: " + str(metaint) 156 | read_buffer = metaint + 255 157 | content = response.read(read_buffer) 158 | 159 | start = "StreamTitle='" 160 | end = "';" 161 | 162 | try: 163 | title = re.search('%s(.*)%s' % (start, end), content[metaint:]).group(1) 164 | title = re.sub("StreamUrl='.*?';", "", title).replace("';", "").replace("StreamUrl='", "") 165 | title = re.sub("&artist=.*", "", title) 166 | title = re.sub("http://.*", "", title) 167 | title.rstrip() 168 | except Exception, err: 169 | print "songtitle error: " + str(err) 170 | title = content[metaint:].split("'")[1] 171 | 172 | return {'song': title, 'bitrate': bitrate, 'contenttype': contenttype.rstrip()} 173 | else: 174 | print 175 | "No metaint" 176 | return False 177 | 178 | def getHeaders(self, response): 179 | if self.is_empty(response.headers.dict) is False: 180 | headers = response.headers.dict 181 | elif hasattr(response.info(),"item") and self.is_empty(response.info().item()) is False: 182 | headers = response.info().item() 183 | else: 184 | headers = self.parse_headers(response) 185 | return headers 186 | 187 | def is_empty(self, any_structure): 188 | if any_structure: 189 | return False 190 | else: 191 | return True 192 | 193 | 194 | def stripTags(self, text): 195 | finished = 0 196 | while not finished: 197 | finished = 1 198 | start = text.find("<") 199 | if start >= 0: 200 | stop = text[start:].find(">") 201 | if stop >= 0: 202 | text = text[:start] + text[start + stop + 1:] 203 | finished = 0 204 | return text --------------------------------------------------------------------------------