From 262a8b166ec991b13fad22b6c77feada4b5278ce Mon Sep 17 00:00:00 2001 From: jhezjkp Date: Sat, 16 Sep 2017 11:53:07 +0800 Subject: [PATCH 01/12] fix empty event parse exception --- airplay/airplay.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/airplay/airplay.py b/airplay/airplay.py index 72006cf..d437073 100644 --- a/airplay/airplay.py +++ b/airplay/airplay.py @@ -180,7 +180,7 @@ def _monitor_events(self, event_queue, control_queue): # pragma: no cover # parse it try: req = AirPlayEvent(FakeSocket(raw_request), event_socket.getpeername(), None) - except RuntimeError as exc: + except Exception as exc: raise RuntimeError( "Unexpected request from AirPlay while processing events\n" "Error: {0}\nReceived:\n{1}".format(exc, raw_request) From 533af62d2cb979cce2e34e7df53910f508c354f8 Mon Sep 17 00:00:00 2001 From: jhezjkp Date: Sat, 16 Sep 2017 12:43:30 +0800 Subject: [PATCH 02/12] fix content not complete issue --- airplay/airplay.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/airplay/airplay.py b/airplay/airplay.py index d437073..3401d11 100644 --- a/airplay/airplay.py +++ b/airplay/airplay.py @@ -4,6 +4,7 @@ import socket import time import warnings +import traceback from multiprocessing import Process, Queue @@ -84,7 +85,13 @@ def do_POST(self): raise RuntimeError('Received an event with a zero length body.') # parse XML plist - self.event = plist_loads(self.rfile.read(content_length)) + xml = self.rfile.read(content_length) + if len(xml) < content_length: + # sometimes the content is not complete... + self.event = plist_loads(xml+"") + else: + self.event = plist_loads(xml) + class AirPlay(object): @@ -190,7 +197,8 @@ def _monitor_events(self, event_queue, control_queue): # pragma: no cover event_socket.send(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") # skip non-video events - if req.event.get('category', None) != 'video': + if not hasattr(req, 'event') or req.event is None or \ + req.event.get('category', None) != 'video': continue # send the event back to the parent process @@ -199,6 +207,7 @@ def _monitor_events(self, event_queue, control_queue): # pragma: no cover except KeyboardInterrupt: return except Exception as exc: + traceback.print_exc() event_queue.put(exc) return From 2f1cd9f4c88265c2281d8e39ce8564516e5dad61 Mon Sep 17 00:00:00 2001 From: jhezjkp Date: Sat, 16 Sep 2017 14:45:33 +0800 Subject: [PATCH 03/12] replace argparse with click --- airplay/cli.py | 73 +++++++++++++++++++++++--------------------------- 1 file changed, 33 insertions(+), 40 deletions(-) diff --git a/airplay/cli.py b/airplay/cli.py index 9d66b2e..d5a41b9 100644 --- a/airplay/cli.py +++ b/airplay/cli.py @@ -1,6 +1,6 @@ -import argparse import os import time +import traceback from airplay import AirPlay @@ -41,55 +41,48 @@ def humanize_seconds(secs): return "%02d:%02d:%02d" % (h, m, s) -def main(): - parser = argparse.ArgumentParser( - description="Playback a local or remote video file via AirPlay. " - "This does not do any on-the-fly transcoding (yet), " - "so the file must already be suitable for the AirPlay device." - ) - - parser.add_argument( - 'path', - help='An absolute path or URL to a video file' - ) - - parser.add_argument( - '--position', - '--pos', - '-p', - default=0.0, - type=float, - help='Where to being playback [0.0-1.0]' - ) - - parser.add_argument( - '--device', - '--dev', - '-d', - default=None, - help='Playback video to a specific device [:()]' - ) - - args = parser.parse_args() - - # connect to the AirPlay device we want to control +@click.group() +def cli(): + pass + + +@cli.command() +def discover(path): + """Discover AirPlay devices in connected network""" + click.echo("hello, discover!") + + +@cli.command() +@click.argument('path', metavar='') +def photo(path): + """AirPlay a photo""" + click.echo("hello, photo!") + + +@cli.command() +@click.argument('path', metavar='') +@click.option('-p', '--pos', '--position', metavar="", default=0, type=float) +@click.option('-d', '--dev', '--device', metavar="[:]") +def video(path, position, device): + """AirPlay a video""" + # click.echo("yes") + # if device: + # click.echo("device:%s" % device) + #connect to the AirPlay device we want to control try: - ap = get_airplay_device(args.device) + ap = get_airplay_device(device) except (ValueError, RuntimeError) as exc: - parser.error(exc) + traceback.print_exc() duration = 0 - position = 0 state = 'loading' - path = args.path - # if the url is on our local disk, then we need to spin up a server to start it if os.path.exists(path): path = ap.serve(path) # play what they asked - ap.play(path, args.position) + ap.play(path, position) # stay in this loop until we exit with click.progressbar(length=100, show_eta=False) as bar: @@ -138,4 +131,4 @@ def main(): if __name__ == '__main__': - main() + cli() From 8e2fa2f682592c4033e7b8e7c3989e717e660fc4 Mon Sep 17 00:00:00 2001 From: jhezjkp Date: Sat, 16 Sep 2017 14:54:35 +0800 Subject: [PATCH 04/12] fix no device found issue --- airplay/cli.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/airplay/cli.py b/airplay/cli.py index d5a41b9..9283c98 100644 --- a/airplay/cli.py +++ b/airplay/cli.py @@ -1,5 +1,6 @@ import os import time +import sys import traceback from airplay import AirPlay @@ -21,7 +22,7 @@ def get_airplay_device(hostport): devices = AirPlay.find(fast=True) if len(devices) == 0: - raise RuntimeError('No AirPlay devices were found. Use --device to manually specify an device.') + return None elif len(devices) == 1: return devices[0] elif len(devices) > 1: @@ -65,15 +66,17 @@ def photo(path): @click.option('-d', '--dev', '--device', metavar="[:]") def video(path, position, device): """AirPlay a video""" - # click.echo("yes") - # if device: - # click.echo("device:%s" % device) #connect to the AirPlay device we want to control + ap = None try: ap = get_airplay_device(device) except (ValueError, RuntimeError) as exc: traceback.print_exc() + if not ap: + click.secho("No AirPlay devices found in your network!", fg="red", err=True) + sys.exit(-1) + duration = 0 state = 'loading' From 9bd7eec4ada34667ccba8cf94a5cc0e4c04652f8 Mon Sep 17 00:00:00 2001 From: jhezjkp Date: Sat, 16 Sep 2017 15:04:04 +0800 Subject: [PATCH 05/12] device discover --- airplay/cli.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/airplay/cli.py b/airplay/cli.py index 9283c98..538058e 100644 --- a/airplay/cli.py +++ b/airplay/cli.py @@ -48,9 +48,16 @@ def cli(): @cli.command() -def discover(path): +def discover(): """Discover AirPlay devices in connected network""" - click.echo("hello, discover!") + click.echo("AirPlay device discover start...") + devices = AirPlay.find(fast=True) + if not devices or len(devices) == 0: + click.echo("There's no AirPlay device in your network.") + else: + click.echo("AirPlay device discover complete!") + for dd in devices: + click.echo("\t %s\t %s:%s\n" % (dd.name, dd.host, dd.port)) @cli.command() From cc77374e0035876ba5e71c4471cb71b5d06a626c Mon Sep 17 00:00:00 2001 From: jhezjkp Date: Sat, 16 Sep 2017 16:54:01 +0800 Subject: [PATCH 06/12] add photo airplay feature --- airplay/airplay.py | 39 ++++++++++++++++++++++++++++++++++----- airplay/cli.py | 32 ++++++++++++++++++++++++++++++-- 2 files changed, 64 insertions(+), 7 deletions(-) diff --git a/airplay/airplay.py b/airplay/airplay.py index 3401d11..9ac8354 100644 --- a/airplay/airplay.py +++ b/airplay/airplay.py @@ -254,12 +254,13 @@ def events(self, block=True): except Empty: return - def _command(self, uri, method='GET', body='', **kwargs): + def _command(self, uri, method='GET', header={}, body='', **kwargs): """Makes an HTTP request through to an AirPlay server Args: uri(string): The URI to request method(string): The HTTP verb to use when requesting `uri`, defaults to GET + header(dict): The header to be send with the request body(string): If provided, will be sent witout alteration as the request body. Content-Length header will be set to len(`body`) **kwargs: If provided, Will be converted to a query string and appended to `uri` @@ -274,8 +275,12 @@ def _command(self, uri, method='GET', body='', **kwargs): # generate the request if len(kwargs): uri = uri + '?' + urlencode(kwargs) - - request = method + " " + uri + " HTTP/1.1\r\nContent-Length: " + str(len(body)) + "\r\n\r\n" + body + # request = method + " " + uri + " HTTP/1.1\r\nContent-Length: " + str(len(body)) + "\r\n\r\n" + body + header['Content-Length'] = str(len(body)) + sheader = "" + for key, value in header.items(): + sheader += key + ": " + value + "\r\n" + request = method + " " + uri + " HTTP/1.1\r\n" + sheader + "\r\n" + body try: request = bytes(request, 'UTF-8') @@ -283,7 +288,7 @@ def _command(self, uri, method='GET', body='', **kwargs): pass # send it - self.control_socket.send(request) + self.control_socket.sendall(request) # parse our response result = self.control_socket.recv(self.RECV_SIZE) @@ -351,7 +356,31 @@ def play(self, url, position=0.0): return self._command( '/play', 'POST', - "Content-Location: {0}\nStart-Position: {1}\n\n".format(url, float(position)) + body="Content-Location: {0}\nStart-Position: {1}\n\n".format(url, float(position)) + ) + + def photo(self, img): + """Start a photo display + + Args: + url(string): A URL to video content that the AirPlay server is capable of playing + pos(float): The position in the content to being playback. 0.0 = start, 1.0 = end. + + Returns: + bool: The request was accepted. + + """ + header = { + "Cache-Control": "no-cache", + "Pragma": "no-cache", + "Connection": "keep-alive", + "Content-Type": "application/octet-stream" + } + return self._command( + '/photo', + 'PUT', + header, + img ) def rate(self, rate): diff --git a/airplay/cli.py b/airplay/cli.py index 538058e..68d5a16 100644 --- a/airplay/cli.py +++ b/airplay/cli.py @@ -1,7 +1,9 @@ import os +import os.path import time import sys import traceback +import urllib from airplay import AirPlay @@ -61,10 +63,36 @@ def discover(): @cli.command() +@click.option('-t', '--time', 'duration', metavar='