Allow for missing SourceSize and CaptureDate in ToGo.
[pyTivo/wmcbrine.git] / beacon.py
blobae604fac366a0fb7649182e43f9e5b5f7c67a030
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 port = info.getPort()
87 config.tivos[tsn] = {'name': name, 'address': address,
88 'port': port}
89 config.tivos[tsn].update(info.properties)
90 self.logger.info(name)
92 return names
94 def shutdown(self):
95 self.logger.info('Unregistering: %s' % ' '.join(self.share_names))
96 for info in self.share_info:
97 self.rz.unregisterService(info)
98 self.rz.close()
100 class Beacon:
101 def __init__(self):
102 self.UDPSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
103 self.UDPSock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
104 self.services = []
106 self.platform = PLATFORM_VIDEO
107 for section, settings in config.getShares():
108 try:
109 ct = GetPlugin(settings['type']).CONTENT_TYPE
110 except:
111 continue
112 if ct in ('x-container/tivo-music', 'x-container/tivo-photos'):
113 self.platform = PLATFORM_MAIN
114 break
116 if config.get_zc():
117 logger = logging.getLogger('pyTivo.beacon')
118 try:
119 self.bd = ZCBroadcast(logger)
120 except:
121 logger.error('Zeroconf failure')
122 self.bd = None
123 else:
124 self.bd = None
126 def add_service(self, service):
127 self.services.append(service)
128 self.send_beacon()
130 def format_services(self):
131 return ';'.join(self.services)
133 def format_beacon(self, conntype, services=True):
134 beacon = ['tivoconnect=1',
135 'method=%s' % conntype,
136 'identity={%s}' % config.getGUID(),
137 'machine=%s' % socket.gethostname(),
138 'platform=%s' % self.platform]
140 if services:
141 beacon.append('services=' + self.format_services())
142 else:
143 beacon.append('services=TiVoMediaServer:0/http')
145 return '\n'.join(beacon) + '\n'
147 def send_beacon(self):
148 beacon_ips = config.getBeaconAddresses()
149 beacon = self.format_beacon('broadcast')
150 for beacon_ip in beacon_ips.split():
151 if beacon_ip != 'listen':
152 try:
153 packet = beacon
154 while packet:
155 result = self.UDPSock.sendto(packet, (beacon_ip, 2190))
156 if result < 0:
157 break
158 packet = packet[result:]
159 except Exception, e:
160 print e
162 def start(self):
163 self.send_beacon()
164 self.timer = Timer(60, self.start)
165 self.timer.start()
167 def stop(self):
168 self.timer.cancel()
169 if self.bd:
170 self.bd.shutdown()
172 def recv_bytes(self, sock, length):
173 block = ''
174 while len(block) < length:
175 add = sock.recv(length - len(block))
176 if not add:
177 break
178 block += add
179 return block
181 def recv_packet(self, sock):
182 length = struct.unpack('!I', self.recv_bytes(sock, 4))[0]
183 return self.recv_bytes(sock, length)
185 def send_packet(self, sock, packet):
186 sock.sendall(struct.pack('!I', len(packet)) + packet)
188 def listen(self):
189 """ For the direct-connect, TCP-style beacon """
190 import thread
192 def server():
193 TCPSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
194 TCPSock.bind(('', 2190))
195 TCPSock.listen(5)
197 while True:
198 # Wait for a connection
199 client, address = TCPSock.accept()
201 # Accept (and discard) the client's beacon
202 self.recv_packet(client)
204 # Send ours
205 self.send_packet(client, self.format_beacon('connected'))
207 client.close()
209 thread.start_new_thread(server, ())
211 def get_name(self, address):
212 """ Exchange beacons, and extract the machine name. """
213 our_beacon = self.format_beacon('connected', False)
214 machine_name = re.compile('machine=(.*)\n').search
216 try:
217 tsock = socket.socket()
218 tsock.connect((address, 2190))
219 self.send_packet(tsock, our_beacon)
220 tivo_beacon = self.recv_packet(tsock)
221 tsock.close()
222 name = machine_name(tivo_beacon).groups()[0]
223 except:
224 name = address
226 return name