-
Notifications
You must be signed in to change notification settings - Fork 11
/
icecast.py
155 lines (130 loc) · 5.31 KB
/
icecast.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
import threading
import time
import pylibshout
import logging
logger = logging.getLogger('audio.icecast')
class Icecast(object):
connecting_timeout = 5.0
def __init__(self, source, config):
super(Icecast, self).__init__()
self.config = (config if isinstance(config, IcecastConfig)
else IcecastConfig(config))
self.source = source
self._shout = self.setup_libshout()
def connect(self):
"""Connect the libshout object to the configured server."""
try:
self._shout.open()
except (pylibshout.ShoutException) as err:
logger.exception("Failed to connect to Icecast server.")
raise IcecastError("Failed to connect to icecast server.")
def connected(self):
"""Returns True if the libshout object is currently connected to
an icecast server."""
try:
return True if self._shout.connected() == -7 else False
except AttributeError:
return False
def read(self, size, timeout=None):
raise NotImplementedError("Icecast does not support reading.")
def nonblocking(self, state):
pass
def close(self):
"""Closes the libshout object and tries to join the thread if we are
not calling this from our own thread."""
self._should_run.set()
try:
self._shout.close()
except (pylibshout.ShoutException) as err:
if err[0] == pylibshout.SHOUTERR_UNCONNECTED:
pass
else:
logger.exception("Exception in pylibshout close call.")
raise IcecastError("Exception in pylibshout close.")
try:
self._thread.join(5.0)
except (RuntimeError) as err:
pass
def run(self):
while not self._should_run.is_set():
while self.connected():
if hasattr(self, '_saved_meta'):
self.set_metadata(self._saved_meta)
del self._saved_meta
buff = self.source.read(4096)
if not buff:
# EOF
self.close()
logger.exception("Source EOF, closing ourself.")
break
try:
self._shout.send(buff)
self._shout.sync()
except (pylibshout.ShoutException) as err:
logger.exception("Failed sending stream data.")
self.reboot_libshout()
if not self._should_run.is_set():
time.sleep(self.connecting_timeout)
self.reboot_libshout()
def start(self):
"""Starts the thread that reads from source and feeds it to icecast."""
if not self.connected():
self.connect()
self._should_run = threading.Event()
self._thread = threading.Thread(target=self.run)
self._thread.name = "Icecast"
self._thread.daemon = True
self._thread.start()
def switch_source(self, new_source):
"""Tries to change the source without disconnect from icecast."""
self._should_run.set() # Gracefully try to get rid of the thread
try:
self._thread.join(5.0)
except RuntimeError as err:
logger.exception("Got called from my own thread.")
self.source = new_source # Swap out our source
self.start() # Start a new thread (so roundabout)
def set_metadata(self, metadata):
try:
self._shout.metadata = {'song': metadata} # Stupid library
except (pylibshout.ShoutException) as err:
logger.exception("Failed sending metadata. No action taken.")
self._saved_meta = metadata
def setup_libshout(self):
"""Internal method
Creates a libshout object and puts the configuration to use.
"""
shout = pylibshout.Shout(tag_fix=False)
self.config.setup(shout)
return shout
def reboot_libshout(self):
"""Internal method
Tries to recreate the libshout object.
"""
try:
self._shout = self.setup_libshout()
except (IcecastError) as err:
logger.exception("Configuration failed.")
self.close()
try:
self.connect()
except (IcecastError) as err:
logger.exception("Connection failure.")
class IcecastConfig(dict):
"""Simple dict subclass that knows how to apply the keys to a
libshout object.
"""
def __init__(self, attributes=None):
super(IcecastConfig, self).__init__(attributes or {})
def setup(self, shout):
"""Setup 'shout' configuration by setting attributes on the object.
'shout' is a pylibshout.Shout object.
"""
for key, value in self.iteritems():
try:
setattr(shout, key, value)
except pylibshout.ShoutException as err:
raise IcecastError(("Incorrect configuration option '{:s}' or "
" value '{:s}' used.").format(key, value))
class IcecastError(Exception):
pass