Allow for non-standard ports in ToGo.
[pyTivo/wmcbrine/lucasnz.git] / beacon.py
blobba828fc8cc343bea58ef25b436a99a0d0c6cbf71
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 a second to respond
78 time.sleep(1)
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
89 config.tivo_ports[tsn] = info.getPort()
91 return names
93 def shutdown(self):
94 self.logger.info('Unregistering: %s' % ' '.join(self.share_names))
95 for info in self.share_info:
96 self.rz.unregisterService(info)
97 self.rz.close()
99 class Beacon:
100 def __init__(self):
101 self.UDPSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
102 self.UDPSock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
103 self.services = []
105 self.platform = PLATFORM_VIDEO
106 for section, settings in config.getShares():
107 try:
108 ct = GetPlugin(settings['type']).CONTENT_TYPE
109 except:
110 continue
111 if ct in ('x-container/tivo-music', 'x-container/tivo-photos'):
112 self.platform = PLATFORM_MAIN
113 break
115 if config.get_zc():
116 logger = logging.getLogger('pyTivo.beacon')
117 try:
118 self.bd = ZCBroadcast(logger)
119 except:
120 logger.error('Zeroconf failure')
121 self.bd = None
122 else:
123 self.bd = None
125 def add_service(self, service):
126 self.services.append(service)
127 self.send_beacon()
129 def format_services(self):
130 return ';'.join(self.services)
132 def format_beacon(self, conntype, services=True):
133 beacon = ['tivoconnect=1',
134 'method=%s' % conntype,
135 'identity={%s}' % config.getGUID(),
136 'machine=%s' % socket.gethostname(),
137 'platform=%s' % self.platform]
139 if services:
140 beacon.append('services=' + self.format_services())
141 else:
142 beacon.append('services=TiVoMediaServer:0/http')
144 return '\n'.join(beacon) + '\n'
146 def send_beacon(self):
147 beacon_ips = config.getBeaconAddresses()
148 beacon = self.format_beacon('broadcast')
149 for beacon_ip in beacon_ips.split():
150 if beacon_ip != 'listen':
151 try:
152 packet = beacon
153 while packet:
154 result = self.UDPSock.sendto(packet, (beacon_ip, 2190))
155 if result < 0:
156 break
157 packet = packet[result:]
158 except Exception, e:
159 print e
161 def start(self):
162 self.send_beacon()
163 self.timer = Timer(60, self.start)
164 self.timer.start()
166 def stop(self):
167 self.timer.cancel()
168 if self.bd:
169 self.bd.shutdown()
171 def recv_bytes(self, sock, length):
172 block = ''
173 while len(block) < length:
174 add = sock.recv(length - len(block))
175 if not add:
176 break
177 block += add
178 return block
180 def recv_packet(self, sock):
181 length = struct.unpack('!I', self.recv_bytes(sock, 4))[0]
182 return self.recv_bytes(sock, length)
184 def send_packet(self, sock, packet):
185 sock.sendall(struct.pack('!I', len(packet)) + packet)
187 def listen(self):
188 """ For the direct-connect, TCP-style beacon """
189 import thread
191 def server():
192 TCPSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
193 TCPSock.bind(('', 2190))
194 TCPSock.listen(5)
196 while True:
197 # Wait for a connection
198 client, address = TCPSock.accept()
200 # Accept (and discard) the client's beacon
201 self.recv_packet(client)
203 # Send ours
204 self.send_packet(client, self.format_beacon('connected'))
206 client.close()
208 thread.start_new_thread(server, ())
210 def get_name(self, address):
211 """ Exchange beacons, and extract the machine name. """
212 our_beacon = self.format_beacon('connected', False)
213 machine_name = re.compile('machine=(.*)\n').search
215 try:
216 tsock = socket.socket()
217 tsock.connect((address, 2190))
218 self.send_packet(tsock, our_beacon)
219 tivo_beacon = self.recv_packet(tsock)
220 tsock.close()
221 name = machine_name(tivo_beacon).groups()[0]
222 except:
223 name = address
225 return name