Recreate the getboolean() method for the per-plugin option dicts. (When
[pyTivo/wmcbrine.git] / beacon.py
blob05af377e753f687aec5f48df393c4f6fafda2252
1 import logging
2 import re
3 import socket
4 import struct
5 import time
6 from threading import Timer
7 from urllib import quote
9 import Zeroconf
11 import config
12 from plugin import GetPlugin
14 SHARE_TEMPLATE = '/TiVoConnect?Command=QueryContainer&Container=%s'
15 PLATFORM_MAIN = 'pyTivo'
16 PLATFORM_VIDEO = 'pc/pyTivo' # For the nice icon
18 class ZCListener:
19 def __init__(self, names):
20 self.names = names
22 def removeService(self, server, type, name):
23 self.names.remove(name.replace('.' + type, ''))
25 def addService(self, server, type, name):
26 self.names.append(name.replace('.' + type, ''))
28 class ZCBroadcast:
29 def __init__(self, logger):
30 """ Announce our shares via Zeroconf. """
31 self.share_names = []
32 self.share_info = []
33 self.logger = logger
34 self.rz = Zeroconf.Zeroconf()
35 self.renamed = {}
36 old_titles = self.scan()
37 address = socket.inet_aton(config.get_ip())
38 port = int(config.getPort())
39 logger.info('Announcing shares...')
40 for section, settings in config.getShares():
41 try:
42 ct = GetPlugin(settings['type']).CONTENT_TYPE
43 except:
44 continue
45 if ct.startswith('x-container/'):
46 if 'video' in ct:
47 platform = PLATFORM_VIDEO
48 else:
49 platform = PLATFORM_MAIN
50 logger.info('Registering: %s' % section)
51 self.share_names.append(section)
52 desc = {'path': SHARE_TEMPLATE % quote(section),
53 'platform': platform, 'protocol': 'http'}
54 tt = ct.split('/')[1]
55 title = section
56 count = 1
57 while title in old_titles:
58 count += 1
59 title = '%s [%d]' % (section, count)
60 self.renamed[section] = title
61 info = Zeroconf.ServiceInfo('_%s._tcp.local.' % tt,
62 '%s._%s._tcp.local.' % (title, tt),
63 address, port, 0, 0, desc)
64 self.rz.registerService(info)
65 self.share_info.append(info)
67 def scan(self):
68 """ Look for TiVos using Zeroconf. """
69 VIDS = '_tivo-videos._tcp.local.'
70 names = []
72 self.logger.info('Scanning for TiVos...')
74 # Get the names of servers offering TiVo videos
75 browser = Zeroconf.ServiceBrowser(self.rz, VIDS, ZCListener(names))
77 # Give them half a second to respond
78 time.sleep(0.5)
80 # Now get the addresses -- this is the slow part
81 for name in names:
82 info = self.rz.getServiceInfo(VIDS, name + '.' + VIDS)
83 if info and 'TSN' in info.properties:
84 tsn = info.properties['TSN']
85 address = socket.inet_ntoa(info.getAddress())
86 config.tivos[tsn] = address
87 self.logger.info(name)
88 config.tivo_names[tsn] = name
90 return names
92 def shutdown(self):
93 self.logger.info('Unregistering: %s' % ' '.join(self.share_names))
94 for info in self.share_info:
95 self.rz.unregisterService(info)
96 self.rz.close()
98 class Beacon:
99 def __init__(self):
100 self.UDPSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
101 self.UDPSock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
102 self.services = []
104 self.platform = PLATFORM_VIDEO
105 for section, settings in config.getShares():
106 try:
107 ct = GetPlugin(settings['type']).CONTENT_TYPE
108 except:
109 continue
110 if ct in ('x-container/tivo-music', 'x-container/tivo-photos'):
111 self.platform = PLATFORM_MAIN
112 break
114 if config.get_zc():
115 logger = logging.getLogger('pyTivo.beacon')
116 try:
117 self.bd = ZCBroadcast(logger)
118 except:
119 logger.error('Zeroconf failure')
120 self.bd = None
121 else:
122 self.bd = None
124 def add_service(self, service):
125 self.services.append(service)
126 self.send_beacon()
128 def format_services(self):
129 return ';'.join(self.services)
131 def format_beacon(self, conntype, services=True):
132 beacon = ['tivoconnect=1',
133 'method=%s' % conntype,
134 'identity={%s}' % config.getGUID(),
135 'machine=%s' % socket.gethostname(),
136 'platform=%s' % self.platform]
138 if services:
139 beacon.append('services=' + self.format_services())
140 else:
141 beacon.append('services=TiVoMediaServer:0/http')
143 return '\n'.join(beacon) + '\n'
145 def send_beacon(self):
146 beacon_ips = config.getBeaconAddresses()
147 beacon = self.format_beacon('broadcast')
148 for beacon_ip in beacon_ips.split():
149 if beacon_ip != 'listen':
150 try:
151 packet = beacon
152 while packet:
153 result = self.UDPSock.sendto(packet, (beacon_ip, 2190))
154 if result < 0:
155 break
156 packet = packet[result:]
157 except error, e:
158 print e
160 def start(self):
161 self.send_beacon()
162 self.timer = Timer(60, self.start)
163 self.timer.start()
165 def stop(self):
166 self.timer.cancel()
167 if self.bd:
168 self.bd.shutdown()
170 def recv_bytes(self, sock, length):
171 block = ''
172 while len(block) < length:
173 add = sock.recv(length - len(block))
174 if not add:
175 break
176 block += add
177 return block
179 def recv_packet(self, sock):
180 length = struct.unpack('!I', self.recv_bytes(sock, 4))[0]
181 return self.recv_bytes(sock, length)
183 def send_packet(self, sock, packet):
184 sock.sendall(struct.pack('!I', len(packet)) + packet)
186 def listen(self):
187 """ For the direct-connect, TCP-style beacon """
188 import thread
190 def server():
191 TCPSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
192 TCPSock.bind(('', 2190))
193 TCPSock.listen(5)
195 while True:
196 # Wait for a connection
197 client, address = TCPSock.accept()
199 # Accept (and discard) the client's beacon
200 self.recv_packet(client)
202 # Send ours
203 self.send_packet(client, self.format_beacon('connected'))
205 client.close()
207 thread.start_new_thread(server, ())
209 def get_name(self, address):
210 """ Exchange beacons, and extract the machine name. """
211 our_beacon = self.format_beacon('connected', False)
212 machine_name = re.compile('machine=(.*)\n').search
214 try:
215 tsock = socket.socket()
216 tsock.connect((address, 2190))
217 self.send_packet(tsock, our_beacon)
218 tivo_beacon = self.recv_packet(tsock)
219 tsock.close()
220 name = machine_name(tivo_beacon).groups()[0]
221 except:
222 name = address
224 return name