Although beacons _should_ fit within single packets, maybe we should
[pyTivo/wmcbrine.git] / beacon.py
blob3da19447ce28a34e62cd4689a8ede83009f7628b
1 import logging
2 import re
3 import struct
4 import time
5 from socket import *
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 = inet_aton(config.get_ip())
38 port = int(config.getPort())
39 logger.info('Announcing shares...')
40 for section, settings in config.getShares():
41 ct = GetPlugin(settings['type']).CONTENT_TYPE
42 if ct.startswith('x-container/'):
43 if 'video' in ct:
44 platform = PLATFORM_VIDEO
45 else:
46 platform = PLATFORM_MAIN
47 logger.info('Registering: %s' % section)
48 self.share_names.append(section)
49 desc = {'path': SHARE_TEMPLATE % quote(section),
50 'platform': platform, 'protocol': 'http'}
51 tt = ct.split('/')[1]
52 title = section
53 count = 1
54 while title in old_titles:
55 count += 1
56 title = '%s [%d]' % (section, count)
57 self.renamed[section] = title
58 info = Zeroconf.ServiceInfo('_%s._tcp.local.' % tt,
59 '%s._%s._tcp.local.' % (title, tt),
60 address, port, 0, 0, desc)
61 self.rz.registerService(info)
62 self.share_info.append(info)
64 def scan(self):
65 """ Look for TiVos using Zeroconf. """
66 VIDS = '_tivo-videos._tcp.local.'
67 names = []
69 self.logger.info('Scanning for TiVos...')
71 # Get the names of servers offering TiVo videos
72 browser = Zeroconf.ServiceBrowser(self.rz, VIDS, ZCListener(names))
74 # Give them half a second to respond
75 time.sleep(0.5)
77 # Now get the addresses -- this is the slow part
78 for name in names:
79 info = self.rz.getServiceInfo(VIDS, name + '.' + VIDS)
80 if info and 'TSN' in info.properties:
81 tsn = info.properties['TSN']
82 address = inet_ntoa(info.getAddress())
83 config.tivos[tsn] = address
84 self.logger.info(name)
85 config.tivo_names[tsn] = name
87 return names
89 def shutdown(self):
90 self.logger.info('Unregistering: %s' % ' '.join(self.share_names))
91 for info in self.share_info:
92 self.rz.unregisterService(info)
93 self.rz.close()
95 class Beacon:
96 def __init__(self):
97 self.UDPSock = socket(AF_INET, SOCK_DGRAM)
98 self.UDPSock.setsockopt(SOL_SOCKET, SO_BROADCAST, 1)
99 self.services = []
101 self.platform = PLATFORM_VIDEO
102 for section, settings in config.getShares():
103 ct = GetPlugin(settings['type']).CONTENT_TYPE
104 if ct in ('x-container/tivo-music', 'x-container/tivo-photos'):
105 self.platform = PLATFORM_MAIN
106 break
108 if config.get_zc():
109 logger = logging.getLogger('pyTivo.beacon')
110 try:
111 self.bd = ZCBroadcast(logger)
112 except:
113 logger.error('Zeroconf failure')
114 self.bd = None
115 else:
116 self.bd = None
118 def add_service(self, service):
119 self.services.append(service)
120 self.send_beacon()
122 def format_services(self):
123 return ';'.join(self.services)
125 def format_beacon(self, conntype, services=True):
126 beacon = ['tivoconnect=1',
127 'method=%s' % conntype,
128 'identity=%s' % config.getGUID(),
129 'machine=%s' % gethostname(),
130 'platform=%s' % self.platform]
132 if services:
133 beacon.append('services=' + self.format_services())
134 else:
135 beacon.append('services=TiVoMediaServer:0/http')
137 return '\n'.join(beacon) + '\n'
139 def send_beacon(self):
140 beacon_ips = config.getBeaconAddresses()
141 beacon = self.format_beacon('broadcast')
142 for beacon_ip in beacon_ips.split():
143 if beacon_ip != 'listen':
144 try:
145 packet = beacon
146 while packet:
147 result = self.UDPSock.sendto(packet, (beacon_ip, 2190))
148 if result < 0:
149 break
150 packet = packet[result:]
151 except error, e:
152 print e
154 def start(self):
155 self.send_beacon()
156 self.timer = Timer(60, self.start)
157 self.timer.start()
159 def stop(self):
160 self.timer.cancel()
161 if self.bd:
162 self.bd.shutdown()
164 def listen(self):
165 """ For the direct-connect, TCP-style beacon """
166 import thread
168 def server():
169 TCPSock = socket(AF_INET, SOCK_STREAM)
170 TCPSock.bind(('', 2190))
171 TCPSock.listen(5)
173 while True:
174 # Wait for a connection
175 client, address = TCPSock.accept()
177 # Accept the client's beacon
178 client_length = struct.unpack('!I', client.recv(4))[0]
179 client_message = client.recv(client_length)
181 # Send ours
182 message = self.format_beacon('connected')
183 client.send(struct.pack('!I', len(message)))
184 client.send(message)
185 client.close()
187 thread.start_new_thread(server, ())
189 def get_name(self, address):
190 """ Exchange beacons, and extract the machine name. """
191 our_beacon = self.format_beacon('connected', False)
192 machine_name = re.compile('machine=(.*)\n').search
194 try:
195 tsock = socket()
196 tsock.connect((address, 2190))
198 tsock.send(struct.pack('!I', len(our_beacon)))
199 tsock.send(our_beacon)
201 length = struct.unpack('!I', tsock.recv(4))[0]
202 tivo_beacon = tsock.recv(length)
204 tsock.close()
206 name = machine_name(tivo_beacon).groups()[0]
207 except:
208 name = address
210 return name