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