diff options
| author | 2010-12-27 16:07:43 +0100 | |
|---|---|---|
| committer | 2010-12-27 16:07:43 +0100 | |
| commit | eb9d750f59161f2bf0a55b4e75b56e65e6f7b432 (patch) | |
| tree | 975a0e589eb2a1e2e1bcc7b253ffaa115b193ae3 /AirPlayService.py | |
| download | totem-plugin-airplay-eb9d750f59161f2bf0a55b4e75b56e65e6f7b432.tar.gz totem-plugin-airplay-eb9d750f59161f2bf0a55b4e75b56e65e6f7b432.tar.bz2 | |
Initial commit of the sources
Diffstat (limited to 'AirPlayService.py')
| -rw-r--r-- | AirPlayService.py | 232 |
1 files changed, 232 insertions, 0 deletions
diff --git a/AirPlayService.py b/AirPlayService.py new file mode 100644 index 0000000..dee6dde --- /dev/null +++ b/AirPlayService.py | |||
| @@ -0,0 +1,232 @@ | |||
| 1 | # -*- coding: utf-8 -*- | ||
| 2 | |||
| 3 | import asyncore | ||
| 4 | import platform | ||
| 5 | import socket | ||
| 6 | import threading | ||
| 7 | import time | ||
| 8 | from datetime import datetime, date | ||
| 9 | from urlparse import urlparse | ||
| 10 | from ZeroconfService import ZeroconfService | ||
| 11 | |||
| 12 | __all__ = ["BaseAirPlayRequest", "AirPlayService", "AirPlayProtocolHandler"] | ||
| 13 | |||
| 14 | class BaseAirPlayRequest(object): | ||
| 15 | def read_from_socket(self, socket): | ||
| 16 | data = socket.recv(1024) | ||
| 17 | if not data: | ||
| 18 | return False | ||
| 19 | |||
| 20 | # we split the message into HTTP headers and content body | ||
| 21 | message = data.split("\r\n\r\n", 1) | ||
| 22 | headers = message[0] | ||
| 23 | headerlines = headers.splitlines() | ||
| 24 | |||
| 25 | # parse request headers | ||
| 26 | command = headerlines[0].split() | ||
| 27 | self.type = command[0] | ||
| 28 | self.uri = command[1] | ||
| 29 | self.version = command[2] | ||
| 30 | del headerlines[0] | ||
| 31 | self.headers = self.parse_headers(headerlines) | ||
| 32 | |||
| 33 | # parse any uri query parameters | ||
| 34 | self.params = None | ||
| 35 | if (self.uri.find('?')): | ||
| 36 | url = urlparse(self.uri) | ||
| 37 | if (url[4] is not ""): | ||
| 38 | self.params = dict([part.split('=') for part in url[4].split('&')]) | ||
| 39 | self.uri = url[2] | ||
| 40 | |||
| 41 | # parse message body | ||
| 42 | if (int(self.headers['Content-Length']) > 0): | ||
| 43 | self.body = message[1] | ||
| 44 | # read more data if we have to | ||
| 45 | if len(self.body) < int(self.headers['Content-Length']): | ||
| 46 | while 1: | ||
| 47 | data = socket.recv(8192) | ||
| 48 | if not data: | ||
| 49 | break | ||
| 50 | self.body = self.body + data | ||
| 51 | |||
| 52 | return True | ||
| 53 | |||
| 54 | def parse_headers(self, lines): | ||
| 55 | headers = {} | ||
| 56 | for line in lines: | ||
| 57 | if line: | ||
| 58 | name, value = line.split(": ", 1) | ||
| 59 | headers[name.strip()] = value.strip() | ||
| 60 | return headers | ||
| 61 | |||
| 62 | class AirPlayProtocolHandler(asyncore.dispatcher_with_send): | ||
| 63 | def __init__(self, socket, service): | ||
| 64 | asyncore.dispatcher_with_send.__init__(self, socket) | ||
| 65 | self.service = service | ||
| 66 | |||
| 67 | def handle_read(self): | ||
| 68 | # read from the socket and parse a HTTP request | ||
| 69 | request = BaseAirPlayRequest() | ||
| 70 | if (not request.read_from_socket(self)): | ||
| 71 | return | ||
| 72 | |||
| 73 | answer = "" | ||
| 74 | |||
| 75 | # process the request and run the appropriate callback | ||
| 76 | if (request.uri.find('/play')>-1): | ||
| 77 | parsedbody = request.parse_headers(request.body.splitlines()) | ||
| 78 | self.service.play(parsedbody['Content-Location'], float(parsedbody['Start-Position'])) | ||
| 79 | answer = self.create_request() | ||
| 80 | elif (request.uri.find('/stop')>-1): | ||
| 81 | self.service.stop(request.headers) | ||
| 82 | answer = self.create_request() | ||
| 83 | elif (request.uri.find('/scrub')>-1): | ||
| 84 | if request.type == 'GET': | ||
| 85 | d, p = self.service.get_scrub() | ||
| 86 | content = "duration: " + str(float(d)) | ||
| 87 | content += "\nposition: " + str(float(p)) | ||
| 88 | answer = self.create_request(200, content) | ||
| 89 | elif request.type == 'POST': | ||
| 90 | self.service.set_scrub(float(request.params['position'])) | ||
| 91 | answer = self.create_request() | ||
| 92 | elif (request.uri.find('/reverse')>-1): | ||
| 93 | self.service.reverse(request.headers) | ||
| 94 | answer = self.create_request(101) | ||
| 95 | elif (request.type == 'POST' and request.uri.find('/rate')>-1): | ||
| 96 | self.service.rate(float(request.params['value'])) | ||
| 97 | answer = self.create_request() | ||
| 98 | elif (request.type == 'PUT' and request.uri.find('/photo')>-1): | ||
| 99 | self.photo(request.body, request.headers['X-Apple-Transition']) | ||
| 100 | answer = self.create_request() | ||
| 101 | else: | ||
| 102 | print "ERROR: AirPlay - Unable to handle request \"%s\"" % (request.uri) | ||
| 103 | answer = self.create_request(404) | ||
| 104 | |||
| 105 | if(answer is not ""): | ||
| 106 | self.send(answer) | ||
| 107 | |||
| 108 | def getDateTime(self): | ||
| 109 | today = datetime.now() | ||
| 110 | datestr = today.strftime("%a, %d %b %Y %H:%M:%S") | ||
| 111 | return datestr+" GMT" | ||
| 112 | |||
| 113 | def create_request(self, status = 200, body = ""): | ||
| 114 | clength = len(bytes(body)) | ||
| 115 | if (status == 200): | ||
| 116 | answer = "HTTP/1.1 200 OK" | ||
| 117 | elif (status == 404): | ||
| 118 | answer = "HTTP/1.1 404 Not Found" | ||
| 119 | elif (status == 101): | ||
| 120 | answer = "HTTP/1.1 101 Switching Protocols" | ||
| 121 | answer += "\nUpgrade: PTTH/1.0" | ||
| 122 | answer += "\nConnection: Upgrade" | ||
| 123 | answer += "\nDate: " + self.getDateTime() | ||
| 124 | answer += "\nContent-Length: " + str(clength) | ||
| 125 | answer +="\n\n" | ||
| 126 | answer += body | ||
| 127 | return answer | ||
| 128 | |||
| 129 | def get_scrub(self): | ||
| 130 | return False | ||
| 131 | |||
| 132 | def set_scrub(self, position): | ||
| 133 | return False | ||
| 134 | |||
| 135 | def play(self, location, position): | ||
| 136 | return False | ||
| 137 | |||
| 138 | def stop(self, info): | ||
| 139 | return False | ||
| 140 | |||
| 141 | def reverse(self, info): | ||
| 142 | return True | ||
| 143 | |||
| 144 | def photo(self, data, transition): | ||
| 145 | return False | ||
| 146 | |||
| 147 | def rate(self, speed): | ||
| 148 | return False | ||
| 149 | |||
| 150 | def volume(self, info): | ||
| 151 | return False | ||
| 152 | |||
| 153 | def authorize(self, info): | ||
| 154 | return False | ||
| 155 | |||
| 156 | def event(self, info): | ||
| 157 | return False | ||
| 158 | |||
| 159 | class AsyncoreThread(threading.Thread): | ||
| 160 | def __init__(self, timeout=30.0, use_poll=0,map=None): | ||
| 161 | self.flag = True | ||
| 162 | self.timeout = 30.0 | ||
| 163 | self.use_poll = use_poll | ||
| 164 | self.map = map | ||
| 165 | threading.Thread.__init__(self, None, None, 'asyncore thread') | ||
| 166 | |||
| 167 | def run(self): | ||
| 168 | self.loop() | ||
| 169 | |||
| 170 | def loop(self): | ||
| 171 | if self.map is None: | ||
| 172 | self.map = asyncore.socket_map | ||
| 173 | |||
| 174 | if self.use_poll: | ||
| 175 | if hasattr(select, 'poll'): | ||
| 176 | poll_fun = asyncore.poll3 | ||
| 177 | else: | ||
| 178 | poll_fun = asyncore.poll2 | ||
| 179 | else: | ||
| 180 | poll_fun = asyncore.poll | ||
| 181 | |||
| 182 | while self.map and self.flag: | ||
| 183 | poll_fun(self.timeout,self.map) | ||
| 184 | |||
| 185 | def end(self): | ||
| 186 | self.flag=False | ||
| 187 | self.map=None | ||
| 188 | |||
| 189 | class AirPlayService(asyncore.dispatcher): | ||
| 190 | def __init__(self, name=None, host="0.0.0.0", port=22555): | ||
| 191 | print "AirPlayService running" | ||
| 192 | |||
| 193 | # create socket server | ||
| 194 | asyncore.dispatcher.__init__(self) | ||
| 195 | self.create_socket(socket.AF_INET, socket.SOCK_STREAM) | ||
| 196 | self.set_reuse_addr() | ||
| 197 | self.bind((host, port)) | ||
| 198 | self.listen(5) | ||
| 199 | self.remote_clients = [] | ||
| 200 | |||
| 201 | # create avahi service | ||
| 202 | if (name is None): | ||
| 203 | name = "Airplay Service on " + platform.node() | ||
| 204 | self.zeroconf_service = ZeroconfService(name, port=port, stype="_airplay._tcp", text=["Name="+name]) | ||
| 205 | |||
| 206 | # publish avahi service | ||
| 207 | self.zeroconf_service.publish() | ||
| 208 | |||
| 209 | # do this so we do not block the main thread | ||
| 210 | self.thread = AsyncoreThread(timeout=30) | ||
| 211 | self.thread.is_finished = False | ||
| 212 | self.thread.start() | ||
| 213 | |||
| 214 | def handle_accept(self): | ||
| 215 | pair = self.accept() | ||
| 216 | if pair is None: | ||
| 217 | pass | ||
| 218 | else: | ||
| 219 | sock, addr = pair | ||
| 220 | self.remote_clients.append(AirPlayProtocolHandler(sock, self)) | ||
| 221 | |||
| 222 | def handle_close(self): | ||
| 223 | self.close() | ||
| 224 | |||
| 225 | def __del__(self): | ||
| 226 | self.thread.end() | ||
| 227 | self.close() | ||
| 228 | del self.thread | ||
| 229 | |||
| 230 | # unpublish avahi service | ||
| 231 | self.zeroconf_service.unpublish() | ||
| 232 | |||
