From e0abfea76042e33c3a449a60aebb5531fb07b98c Mon Sep 17 00:00:00 2001 From: Gabriel Magno Date: Tue, 12 Oct 2021 19:34:18 -0300 Subject: [PATCH 01/18] Try removing the command to set the broadcast TTL to 4 --- nanodlna/devices.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nanodlna/devices.py b/nanodlna/devices.py index 0c2bc85..054eab1 100644 --- a/nanodlna/devices.py +++ b/nanodlna/devices.py @@ -74,7 +74,7 @@ def get_devices(timeout=3.0): logging.debug("Configuring broadcast message") s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) - s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 4) + #s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 4) s.bind(("", SSDP_BROADCAST_PORT + 10)) logging.debug("Sending broadcast message") From 450ba892e057aa940052e87810c805d21a1187ca Mon Sep 17 00:00:00 2001 From: Gabriel Magno Date: Tue, 19 Oct 2021 23:27:53 -0300 Subject: [PATCH 02/18] Try setting the ttl as unsigned char --- nanodlna/devices.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/nanodlna/devices.py b/nanodlna/devices.py index 054eab1..f45adcb 100644 --- a/nanodlna/devices.py +++ b/nanodlna/devices.py @@ -3,6 +3,7 @@ import re import socket +import struct import sys import xml.etree.ElementTree as ET @@ -74,7 +75,11 @@ def get_devices(timeout=3.0): logging.debug("Configuring broadcast message") s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) - #s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 4) + + # OpenBSD needs the ttl for the IP_MULTICAST_TTL as an unsigned char + ttl = struct.pack("B", 4) + s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl) + s.bind(("", SSDP_BROADCAST_PORT + 10)) logging.debug("Sending broadcast message") From 1dc4481053409361d640b4f8bf77e94add0209a1 Mon Sep 17 00:00:00 2001 From: s482dcaw Date: Wed, 26 Jan 2022 19:20:14 +0000 Subject: [PATCH 03/18] Modify device listing to facilitate players that expose devicelists, such as Sonos --- nanodlna/devices.py | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/nanodlna/devices.py b/nanodlna/devices.py index 0c2bc85..026a2d6 100644 --- a/nanodlna/devices.py +++ b/nanodlna/devices.py @@ -26,6 +26,7 @@ SSDP_BROADCAST_MSG = "\r\n".join(SSDP_BROADCAST_PARAMS) UPNP_DEFAULT_SERVICE_TYPE = "urn:schemas-upnp-org:service:AVTransport:1" +DEVICE_TYPE = "urn:schemas-upnp-org:device:MediaRenderer:1" def register_device(location_url): @@ -33,13 +34,44 @@ def register_device(location_url): xml = urllibreq.urlopen(location_url).read().decode("UTF-8") xml = re.sub(" xmlns=\"[^\"]+\"", "", xml, count=1) info = ET.fromstring(xml) + inList = False location = urllibparse.urlparse(location_url) hostname = location.hostname - friendly_name = info.find("./device/friendlyName").text - + # First try deviceLists try: + friendly_name = info.find( + "./device/deviceList/device/" + "[deviceType='{0}']/friendlyName".format( + DEVICE_TYPE + ) + ).text + manufacturer_name = info.find( + "./device/deviceList/device/" + "[deviceType='{0}']/manufacturer".format( + DEVICE_TYPE + ) + ).text + inList = True + except: + friendly_name = info.find("./device/friendlyName").text + manufacturer_name = info.find("./device/manufacturer").text + + if inList == True: + try: + path = info.find( + "./device/deviceList/device/" + "[deviceType='{0}']/serviceList/service/" + "[serviceType='{1}']/controlURL".format( + DEVICE_TYPE, UPNP_DEFAULT_SERVICE_TYPE + ) + ).text + action_url = urllibparse.urljoin(location_url, path) + except AttributeError: + action_url = None + else: + try: path = info.find( "./device/serviceList/service/" "[serviceType='{0}']/controlURL".format( @@ -47,12 +79,13 @@ def register_device(location_url): ) ).text action_url = urllibparse.urljoin(location_url, path) - except AttributeError: + except AttributeError: action_url = None device = { "location": location_url, "hostname": hostname, + "manufacturer_name": manufacturer_name, "friendly_name": friendly_name, "action_url": action_url, "st": UPNP_DEFAULT_SERVICE_TYPE From 1c75dac6d6aaf16d308d920ea939897b2ac49f4e Mon Sep 17 00:00:00 2001 From: s482dcaw Date: Wed, 26 Jan 2022 20:53:58 +0000 Subject: [PATCH 04/18] Adding actions to Pause and Stop the DLNA player --- nanodlna/cli.py | 66 +++++++++++++++++++++++++++++ nanodlna/dlna.py | 20 +++++++++ nanodlna/templates/action-Pause.xml | 8 ++++ nanodlna/templates/action-Stop.xml | 8 ++++ 4 files changed, 102 insertions(+) create mode 100644 nanodlna/templates/action-Pause.xml create mode 100644 nanodlna/templates/action-Stop.xml diff --git a/nanodlna/cli.py b/nanodlna/cli.py index 7667932..7f86382 100755 --- a/nanodlna/cli.py +++ b/nanodlna/cli.py @@ -58,6 +58,62 @@ def list_devices(args): for i, device in enumerate(my_devices, 1): print("Device {0}:\n{1}\n\n".format(i, json.dumps(device, indent=4))) +def find_device(args): + + device = None + + if not args.device_url and not args.device_query: + logging.info("No device url and no query string provided") + sys.exit("No device specified; exiting") + + if args.device_url: + logging.info("Select device by URL") + device = devices.register_device(args.device_url) + else: + my_devices = devices.get_devices(args.timeout) + + if len(my_devices) == 0: + sys.exit("No devices found; exiting") + elif len(my_devices) == 1: + logging.info("Only one device exists, selecting this") + device = my_devices[0] + else: + logging.info("Select device by query") + for listed in my_devices: + if args.device_query.lower() in str(listed).lower(): + device = listed + break + + if not device: + sys.exit("No devices found; exiting") + + logging.info("Device selected: {}".format(json.dumps(device))) + return device + +def pause(args): + + set_logs(args) + + logging.info("Selecting device to pause") + device=find_device(args) + + # Pause through DLNA protocol + logging.info("Sending pause command") + dlna.pause(device) + + +def stop(args): + + set_logs(args) + + logging.info("Selecting device to stop") + + device=find_device(args) + + # Stop through DLNA protocol + logging.info("Sending stop command") + dlna.stop(device) + def play(args): @@ -135,6 +191,16 @@ def run(): p_list = subparsers.add_parser('list') p_list.set_defaults(func=list_devices) + p_stop = subparsers.add_parser('stop') + p_stop.add_argument("-d", "--device", dest="device_url") + p_stop.add_argument("-q", "--query-device", dest="device_query") + p_stop.set_defaults(func=stop) + + p_pause = subparsers.add_parser('pause') + p_pause.add_argument("-d", "--device", dest="device_url") + p_pause.add_argument("-q", "--query-device", dest="device_query") + p_pause.set_defaults(func=pause) + p_play = subparsers.add_parser('play') p_play.add_argument("-d", "--device", dest="device_url") p_play.add_argument("-H", "--host", dest="local_host") diff --git a/nanodlna/dlna.py b/nanodlna/dlna.py index a6f2111..6e82904 100644 --- a/nanodlna/dlna.py +++ b/nanodlna/dlna.py @@ -95,3 +95,23 @@ def play(files_urls, device): send_dlna_action(device, video_data, "SetAVTransportURI") logging.debug("Playing video") send_dlna_action(device, video_data, "Play") + + +def stop(device): + + logging.debug("Stoping device: {}".format( + json.dumps({ + "device": device + }) + )) + send_dlna_action(device, {"":""}, "Stop") + + +def pause(device): + + logging.debug("Pausing device: {}".format( + json.dumps({ + "device": device + }) + )) + send_dlna_action(device, {"":""}, "Pause") diff --git a/nanodlna/templates/action-Pause.xml b/nanodlna/templates/action-Pause.xml new file mode 100644 index 0000000..838fca7 --- /dev/null +++ b/nanodlna/templates/action-Pause.xml @@ -0,0 +1,8 @@ + + + + + 0 + + + diff --git a/nanodlna/templates/action-Stop.xml b/nanodlna/templates/action-Stop.xml new file mode 100644 index 0000000..7026ce1 --- /dev/null +++ b/nanodlna/templates/action-Stop.xml @@ -0,0 +1,8 @@ + + + + + 0 + + + From 183c0d8e41cdd1bbd99f19c51971200ac32e119d Mon Sep 17 00:00:00 2001 From: Gabriel Magno Date: Tue, 10 May 2022 18:09:44 -0300 Subject: [PATCH 05/18] Refactoring to simplify code --- nanodlna/devices.py | 52 +++++++++++++++++---------------------------- 1 file changed, 19 insertions(+), 33 deletions(-) diff --git a/nanodlna/devices.py b/nanodlna/devices.py index 026a2d6..3c87657 100644 --- a/nanodlna/devices.py +++ b/nanodlna/devices.py @@ -34,52 +34,38 @@ def register_device(location_url): xml = urllibreq.urlopen(location_url).read().decode("UTF-8") xml = re.sub(" xmlns=\"[^\"]+\"", "", xml, count=1) info = ET.fromstring(xml) - inList = False location = urllibparse.urlparse(location_url) hostname = location.hostname - # First try deviceLists + device_root = info.find("./device") + if not device_root: + device_root = info.find( + "./device/deviceList/device/" + "[deviceType='{0}']".format( + DEVICE_TYPE + ) + ) + try: - friendly_name = info.find( - "./device/deviceList/device/" - "[deviceType='{0}']/friendlyName".format( - DEVICE_TYPE - ) - ).text - manufacturer_name = info.find( - "./device/deviceList/device/" - "[deviceType='{0}']/manufacturer".format( - DEVICE_TYPE - ) - ).text - inList = True + friendly_name = device_root.find("./friendlyName").text except: - friendly_name = info.find("./device/friendlyName").text - manufacturer_name = info.find("./device/manufacturer").text + friendly_name = None - if inList == True: - try: - path = info.find( - "./device/deviceList/device/" - "[deviceType='{0}']/serviceList/service/" - "[serviceType='{1}']/controlURL".format( - DEVICE_TYPE, UPNP_DEFAULT_SERVICE_TYPE - ) - ).text - action_url = urllibparse.urljoin(location_url, path) - except AttributeError: - action_url = None - else: - try: + try: + manufacturer_name = device_root.find("./manufacturer").text + except: + manufacturer_name = None + + try: path = info.find( - "./device/serviceList/service/" + "./serviceList/service/" "[serviceType='{0}']/controlURL".format( UPNP_DEFAULT_SERVICE_TYPE ) ).text action_url = urllibparse.urljoin(location_url, path) - except AttributeError: + except: action_url = None device = { From 84de196e1702e7ff5f85bd68da39e0bd1d192d97 Mon Sep 17 00:00:00 2001 From: Gabriel Magno Date: Tue, 10 May 2022 21:12:27 -0300 Subject: [PATCH 06/18] Bind socket to choosen local host, and defaults to 0.0.0.0 --- nanodlna/cli.py | 6 +++--- nanodlna/devices.py | 9 ++++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/nanodlna/cli.py b/nanodlna/cli.py index 7667932..e392fca 100755 --- a/nanodlna/cli.py +++ b/nanodlna/cli.py @@ -52,7 +52,7 @@ def list_devices(args): set_logs(args) logging.info("Scanning devices...") - my_devices = devices.get_devices(args.timeout) + my_devices = devices.get_devices(args.timeout, args.local_host) logging.info("Number of devices found: {}".format(len(my_devices))) for i, device in enumerate(my_devices, 1): @@ -88,7 +88,7 @@ def play(args): logging.info("Select device by URL") device = devices.register_device(args.device_url) else: - my_devices = devices.get_devices(args.timeout) + my_devices = devices.get_devices(args.timeout, args.local_host) if len(my_devices) > 0: if args.device_query: @@ -127,6 +127,7 @@ def run(): parser = argparse.ArgumentParser( description="A minimal UPnP/DLNA media streamer.") parser.set_defaults(func=lambda args: parser.print_help()) + parser.add_argument("-H", "--host", dest="local_host") parser.add_argument("-t", "--timeout", type=float, default=5) parser.add_argument("-b", "--debug", dest="debug_activated", action="store_true") @@ -137,7 +138,6 @@ def run(): p_play = subparsers.add_parser('play') p_play.add_argument("-d", "--device", dest="device_url") - p_play.add_argument("-H", "--host", dest="local_host") p_play.add_argument("-q", "--query-device", dest="device_query") p_play.add_argument("-s", "--subtitle", dest="file_subtitle") p_play.add_argument("-n", "--no-subtitle", diff --git a/nanodlna/devices.py b/nanodlna/devices.py index f45adcb..46658c7 100644 --- a/nanodlna/devices.py +++ b/nanodlna/devices.py @@ -71,7 +71,10 @@ def register_device(location_url): return device -def get_devices(timeout=3.0): +def get_devices(timeout=3.0, host=None): + + if not host: + host = "0.0.0.0" logging.debug("Configuring broadcast message") s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) @@ -80,7 +83,7 @@ def get_devices(timeout=3.0): ttl = struct.pack("B", 4) s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl) - s.bind(("", SSDP_BROADCAST_PORT + 10)) + s.bind((host, 0)) logging.debug("Sending broadcast message") s.sendto(SSDP_BROADCAST_MSG.encode("UTF-8"), (SSDP_BROADCAST_ADDR, @@ -125,7 +128,7 @@ def get_devices(timeout=3.0): timeout = int(sys.argv[1]) if len(sys.argv) >= 2 else 5 - devices = get_devices(timeout) + devices = get_devices(timeout, "0.0.0.0") for i, device in enumerate(devices, 1): print("Device {0}:\n{1}\n\n".format(i, json.dumps(device, indent=4))) From b1e409685fffaf945a436ba59973e6f111fc9aab Mon Sep 17 00:00:00 2001 From: Gabriel Magno Date: Tue, 10 May 2022 22:05:09 -0300 Subject: [PATCH 07/18] Properly handl actions with empty data --- nanodlna/dlna.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/nanodlna/dlna.py b/nanodlna/dlna.py index 6e82904..109db6d 100644 --- a/nanodlna/dlna.py +++ b/nanodlna/dlna.py @@ -28,7 +28,9 @@ def send_dlna_action(device, data, action): action_data = pkgutil.get_data( "nanodlna", "templates/action-{0}.xml".format(action)).decode("UTF-8") - action_data = action_data.format(**data).encode("UTF-8") + if data: + action_data = action_data.format(**data) + action_data = action_data.encode("UTF-8") headers = { "Content-Type": "text/xml; charset=\"utf-8\"", @@ -97,21 +99,19 @@ def play(files_urls, device): send_dlna_action(device, video_data, "Play") -def stop(device): - - logging.debug("Stoping device: {}".format( +def pause(device): + logging.debug("Pausing device: {}".format( json.dumps({ "device": device }) )) - send_dlna_action(device, {"":""}, "Stop") + send_dlna_action(device, None, "Pause") -def pause(device): - - logging.debug("Pausing device: {}".format( +def stop(device): + logging.debug("Stopping device: {}".format( json.dumps({ "device": device }) )) - send_dlna_action(device, {"":""}, "Pause") + send_dlna_action(device, None, "Stop") From 558b95ff50b47d113b052dafaa30163c88166ea2 Mon Sep 17 00:00:00 2001 From: Gabriel Magno Date: Tue, 10 May 2022 22:05:51 -0300 Subject: [PATCH 08/18] Use the find_device function inside the play function --- nanodlna/cli.py | 121 +++++++++++++++++++----------------------------- 1 file changed, 47 insertions(+), 74 deletions(-) diff --git a/nanodlna/cli.py b/nanodlna/cli.py index 7f86382..487e2e6 100755 --- a/nanodlna/cli.py +++ b/nanodlna/cli.py @@ -58,13 +58,12 @@ def list_devices(args): for i, device in enumerate(my_devices, 1): print("Device {0}:\n{1}\n\n".format(i, json.dumps(device, indent=4))) + def find_device(args): - device = None + logging.info("Selecting device to play") - if not args.device_url and not args.device_query: - logging.info("No device url and no query string provided") - sys.exit("No device specified; exiting") + device = None if args.device_url: logging.info("Select device by URL") @@ -72,48 +71,18 @@ def find_device(args): else: my_devices = devices.get_devices(args.timeout) - if len(my_devices) == 0: - sys.exit("No devices found; exiting") - elif len(my_devices) == 1: - logging.info("Only one device exists, selecting this") - device = my_devices[0] - else: - logging.info("Select device by query") - for listed in my_devices: - if args.device_query.lower() in str(listed).lower(): - device = listed - break - - if not device: - sys.exit("No devices found; exiting") + if len(my_devices) > 0: + if args.device_query: + logging.info("Select device by query") + device = [ + device for device in my_devices + if args.device_query.lower() in str(device).lower()][0] + else: + logging.info("Select first device") + device = my_devices[0] - logging.info("Device selected: {}".format(json.dumps(device))) return device -def pause(args): - - set_logs(args) - - logging.info("Selecting device to pause") - device=find_device(args) - - # Pause through DLNA protocol - logging.info("Sending pause command") - dlna.pause(device) - - -def stop(args): - - set_logs(args) - - logging.info("Selecting device to stop") - - device=find_device(args) - - # Stop through DLNA protocol - logging.info("Sending stop command") - dlna.stop(device) - def play(args): @@ -135,27 +104,7 @@ def play(args): logging.info("Media files: {}".format(json.dumps(files))) - # Select device to play - logging.info("Selecting device to play") - - device = None - - if args.device_url: - logging.info("Select device by URL") - device = devices.register_device(args.device_url) - else: - my_devices = devices.get_devices(args.timeout) - - if len(my_devices) > 0: - if args.device_query: - logging.info("Select device by query") - device = [ - device for device in my_devices - if args.device_query.lower() in str(device).lower()][0] - else: - logging.info("Select first device") - device = my_devices[0] - + device = find_device(args) if not device: sys.exit("No devices found.") @@ -178,6 +127,30 @@ def play(args): dlna.play(files_urls, device) +def pause(args): + + set_logs(args) + + logging.info("Selecting device to pause") + device = find_device(args) + + # Pause through DLNA protocol + logging.info("Sending pause command") + dlna.pause(device) + + +def stop(args): + + set_logs(args) + + logging.info("Selecting device to stop") + device = find_device(args) + + # Stop through DLNA protocol + logging.info("Sending stop command") + dlna.stop(device) + + def run(): parser = argparse.ArgumentParser( @@ -191,16 +164,6 @@ def run(): p_list = subparsers.add_parser('list') p_list.set_defaults(func=list_devices) - p_stop = subparsers.add_parser('stop') - p_stop.add_argument("-d", "--device", dest="device_url") - p_stop.add_argument("-q", "--query-device", dest="device_query") - p_stop.set_defaults(func=stop) - - p_pause = subparsers.add_parser('pause') - p_pause.add_argument("-d", "--device", dest="device_url") - p_pause.add_argument("-q", "--query-device", dest="device_query") - p_pause.set_defaults(func=pause) - p_play = subparsers.add_parser('play') p_play.add_argument("-d", "--device", dest="device_url") p_play.add_argument("-H", "--host", dest="local_host") @@ -211,6 +174,16 @@ def run(): p_play.add_argument("file_video") p_play.set_defaults(func=play) + p_pause = subparsers.add_parser('pause') + p_pause.add_argument("-d", "--device", dest="device_url") + p_pause.add_argument("-q", "--query-device", dest="device_query") + p_pause.set_defaults(func=pause) + + p_stop = subparsers.add_parser('stop') + p_stop.add_argument("-d", "--device", dest="device_url") + p_stop.add_argument("-q", "--query-device", dest="device_query") + p_stop.set_defaults(func=stop) + args = parser.parse_args() args.func(args) From 0e4e8c6b3060b1f020cacfb1000183ccb8309e4d Mon Sep 17 00:00:00 2001 From: Gabriel Magno Date: Wed, 11 May 2022 19:36:35 -0300 Subject: [PATCH 09/18] Ade debug information with the device XML --- nanodlna/devices.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/nanodlna/devices.py b/nanodlna/devices.py index 3c87657..3bb1802 100644 --- a/nanodlna/devices.py +++ b/nanodlna/devices.py @@ -35,6 +35,15 @@ def register_device(location_url): xml = re.sub(" xmlns=\"[^\"]+\"", "", xml, count=1) info = ET.fromstring(xml) + logging.debug( + "Device to be registered: {}".format( + json.dumps({ + "location_url": location_url, + "raw": xml + }) + ) + ) + location = urllibparse.urlparse(location_url) hostname = location.hostname From a3ea124a154cff55279f713d0e35ffe2f2d329f4 Mon Sep 17 00:00:00 2001 From: Gabriel Magno Date: Wed, 11 May 2022 21:44:52 -0300 Subject: [PATCH 10/18] Handle devices with XML namespace with single quotes --- nanodlna/devices.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/nanodlna/devices.py b/nanodlna/devices.py index 3bb1802..0107d8d 100644 --- a/nanodlna/devices.py +++ b/nanodlna/devices.py @@ -35,15 +35,21 @@ def register_device(location_url): xml = re.sub(" xmlns=\"[^\"]+\"", "", xml, count=1) info = ET.fromstring(xml) +def register_device(location_url): + + xml_raw = urllibreq.urlopen(location_url).read().decode("UTF-8") logging.debug( "Device to be registered: {}".format( json.dumps({ "location_url": location_url, - "raw": xml + "xml_raw": xml_raw }) ) ) + xml = re.sub(r"""\s(xmlns="[^"]+"|xmlns='[^']+')""", '', xml_raw, count=1) + info = ET.fromstring(xml) + location = urllibparse.urlparse(location_url) hostname = location.hostname From e8c80d36af6463beef7b497e49acf1530b4a6014 Mon Sep 17 00:00:00 2001 From: Gabriel Magno Date: Wed, 11 May 2022 21:45:51 -0300 Subject: [PATCH 11/18] Improve handling of XML parsing and device register --- nanodlna/devices.py | 52 +++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/nanodlna/devices.py b/nanodlna/devices.py index 0107d8d..e587968 100644 --- a/nanodlna/devices.py +++ b/nanodlna/devices.py @@ -25,15 +25,17 @@ "MAN: \"ssdp:discover\"", "MX: 10", "ST: ssdp:all", "", ""] SSDP_BROADCAST_MSG = "\r\n".join(SSDP_BROADCAST_PARAMS) -UPNP_DEFAULT_SERVICE_TYPE = "urn:schemas-upnp-org:service:AVTransport:1" -DEVICE_TYPE = "urn:schemas-upnp-org:device:MediaRenderer:1" +UPNP_DEVICE_TYPE = "urn:schemas-upnp-org:device:MediaRenderer:1" +UPNP_SERVICE_TYPE = "urn:schemas-upnp-org:service:AVTransport:1" -def register_device(location_url): +def get_xml_field_text(xml_root, query): + result = None + if xml_root: + node = xml_root.find(query) + result = node.text if node is not None else None + return result - xml = urllibreq.urlopen(location_url).read().decode("UTF-8") - xml = re.sub(" xmlns=\"[^\"]+\"", "", xml, count=1) - info = ET.fromstring(xml) def register_device(location_url): @@ -58,38 +60,32 @@ def register_device(location_url): device_root = info.find( "./device/deviceList/device/" "[deviceType='{0}']".format( - DEVICE_TYPE + UPNP_DEVICE_TYPE ) ) - try: - friendly_name = device_root.find("./friendlyName").text - except: - friendly_name = None - - try: - manufacturer_name = device_root.find("./manufacturer").text - except: - manufacturer_name = None - - try: - path = info.find( - "./serviceList/service/" - "[serviceType='{0}']/controlURL".format( - UPNP_DEFAULT_SERVICE_TYPE - ) - ).text - action_url = urllibparse.urljoin(location_url, path) - except: + friendly_name = get_xml_field_text(device_root, "./friendlyName") + manufacturer = get_xml_field_text(device_root, "./manufacturer") + action_url_path = get_xml_field_text( + device_root, + "./serviceList/service/" + "[serviceType='{0}']/controlURL".format( + UPNP_SERVICE_TYPE + ) + ) + + if action_url_path is not None: + action_url = urllibparse.urljoin(location_url, action_url_path) + else: action_url = None device = { "location": location_url, "hostname": hostname, - "manufacturer_name": manufacturer_name, + "manufacturer": manufacturer, "friendly_name": friendly_name, "action_url": action_url, - "st": UPNP_DEFAULT_SERVICE_TYPE + "st": UPNP_SERVICE_TYPE } logging.debug( From f937ae2423e281c8bde0d09c67137e63aece77da Mon Sep 17 00:00:00 2001 From: Gabriel Magno Date: Sat, 14 May 2022 18:55:55 -0300 Subject: [PATCH 12/18] Check if 'st' info exists in broadcast message --- nanodlna/devices.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/nanodlna/devices.py b/nanodlna/devices.py index e587968..85a4c07 100644 --- a/nanodlna/devices.py +++ b/nanodlna/devices.py @@ -139,9 +139,17 @@ def get_devices(timeout=3.0): except Exception: pass - devices_urls = [dev["location"] - for dev in devices if "AVTransport" in dev["st"]] - devices = [register_device(location_url) for location_url in devices_urls] + devices_urls = [ + dev["location"] + for dev in devices + if "st" in dev and + "AVTransport" in dev["st"] + ] + + devices = [ + register_device(location_url) + for location_url in devices_urls + ] return devices From c8c4ed77bef0651c619e9a6a27c2a48044b0a2ed Mon Sep 17 00:00:00 2001 From: Gabriel Magno Date: Sat, 14 May 2022 19:29:40 -0300 Subject: [PATCH 13/18] Handle SIGINT and Ctrl+C, so that it sends stop to device --- nanodlna/cli.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/nanodlna/cli.py b/nanodlna/cli.py index 487e2e6..761ef03 100755 --- a/nanodlna/cli.py +++ b/nanodlna/cli.py @@ -6,6 +6,7 @@ import json import os import sys +import signal import datetime import tempfile @@ -122,11 +123,22 @@ def play(args): logging.info("Streaming server ready") + # Register handler if interrupt signal is received + signal.signal(signal.SIGINT, build_handler_stop(device)) + # Play the video through DLNA protocol logging.info("Sending play command") dlna.play(files_urls, device) +def build_handler_stop(device): + def signal_handler(sig, frame): + logging.info("Interrupt signal detected, sending stop command to device") + dlna.stop(device) + sys.exit("Interrupt signal detected, sent stop command to device, exiting now") + return signal_handler + + def pause(args): set_logs(args) From 07a56eec3dae453b8c9e3767d62224081498b700 Mon Sep 17 00:00:00 2001 From: Gabriel Magno Date: Sat, 14 May 2022 19:31:55 -0300 Subject: [PATCH 14/18] Improve formatting --- nanodlna/cli.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/nanodlna/cli.py b/nanodlna/cli.py index 761ef03..63276f6 100755 --- a/nanodlna/cli.py +++ b/nanodlna/cli.py @@ -133,9 +133,15 @@ def play(args): def build_handler_stop(device): def signal_handler(sig, frame): - logging.info("Interrupt signal detected, sending stop command to device") + logging.info( + "Interrupt signal detected" + "sending stop command to device" + ) dlna.stop(device) - sys.exit("Interrupt signal detected, sent stop command to device, exiting now") + sys.exit( + "Interrupt signal detected" + "sent stop command to device, exiting now" + ) return signal_handler From 45c8f6142abd20874237a778a21c6ed620f92c96 Mon Sep 17 00:00:00 2001 From: Gabriel Magno Date: Sat, 14 May 2022 20:10:37 -0300 Subject: [PATCH 15/18] Properly stops the streaming server before exiting --- nanodlna/cli.py | 18 ++++++++++++------ nanodlna/streaming.py | 4 ++++ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/nanodlna/cli.py b/nanodlna/cli.py index 63276f6..069dbce 100755 --- a/nanodlna/cli.py +++ b/nanodlna/cli.py @@ -133,14 +133,20 @@ def play(args): def build_handler_stop(device): def signal_handler(sig, frame): - logging.info( - "Interrupt signal detected" - "sending stop command to device" - ) + + logging.info("Interrupt signal detected") + + logging.info("Sending stop command to render device") dlna.stop(device) + + logging.info("Stopping streaming server") + streaming.stop_server() + sys.exit( - "Interrupt signal detected" - "sent stop command to device, exiting now" + "Interrupt signal detected. " + "Sent stop command to render device and " + "stopped streaming. " + "nano-dlna will exit now!" ) return signal_handler diff --git a/nanodlna/streaming.py b/nanodlna/streaming.py index b9447c0..169c20b 100644 --- a/nanodlna/streaming.py +++ b/nanodlna/streaming.py @@ -88,6 +88,10 @@ def start_server(files, serve_ip, serve_port=9000): return files_urls +def stop_server(): + reactor.stop() + + def get_serve_ip(target_ip, target_port=80): logging.debug("Identifying server IP") s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) From 509d84eea707db1a51d0069f7daba822e7fea7bb Mon Sep 17 00:00:00 2001 From: Gabriel Magno Date: Sat, 14 May 2022 20:10:53 -0300 Subject: [PATCH 16/18] Remove duplicated devices --- nanodlna/devices.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/nanodlna/devices.py b/nanodlna/devices.py index 85a4c07..0416423 100644 --- a/nanodlna/devices.py +++ b/nanodlna/devices.py @@ -100,6 +100,17 @@ def register_device(location_url): return device +def remove_duplicates(devices): + seen = set() + result_devices = [] + for device in devices: + device_str = str(device) + if device_str not in seen: + result_devices.append(device) + seen.add(device_str) + return result_devices + + def get_devices(timeout=3.0): logging.debug("Configuring broadcast message") @@ -151,6 +162,8 @@ def get_devices(timeout=3.0): for location_url in devices_urls ] + devices = remove_duplicates(devices) + return devices From fa4b4e89e91dde19089b656eeb72a72b90f15f71 Mon Sep 17 00:00:00 2001 From: Gabriel Magno Date: Mon, 16 May 2022 18:39:59 -0300 Subject: [PATCH 17/18] Merge develop --- nanodlna/cli.py | 24 ++++++++++++++++++++++++ nanodlna/devices.py | 27 ++++++++++++++++++++++++--- nanodlna/streaming.py | 4 ++++ 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/nanodlna/cli.py b/nanodlna/cli.py index b3d8580..776534d 100755 --- a/nanodlna/cli.py +++ b/nanodlna/cli.py @@ -6,6 +6,7 @@ import json import os import sys +import signal import datetime import tempfile @@ -122,11 +123,34 @@ def play(args): logging.info("Streaming server ready") + # Register handler if interrupt signal is received + signal.signal(signal.SIGINT, build_handler_stop(device)) + # Play the video through DLNA protocol logging.info("Sending play command") dlna.play(files_urls, device) +def build_handler_stop(device): + def signal_handler(sig, frame): + + logging.info("Interrupt signal detected") + + logging.info("Sending stop command to render device") + dlna.stop(device) + + logging.info("Stopping streaming server") + streaming.stop_server() + + sys.exit( + "Interrupt signal detected. " + "Sent stop command to render device and " + "stopped streaming. " + "nano-dlna will exit now!" + ) + return signal_handler + + def pause(args): set_logs(args) diff --git a/nanodlna/devices.py b/nanodlna/devices.py index ea29e7b..403b6d4 100644 --- a/nanodlna/devices.py +++ b/nanodlna/devices.py @@ -101,6 +101,17 @@ def register_device(location_url): return device +def remove_duplicates(devices): + seen = set() + result_devices = [] + for device in devices: + device_str = str(device) + if device_str not in seen: + result_devices.append(device) + seen.add(device_str) + return result_devices + + def get_devices(timeout=3.0, host=None): if not host: @@ -147,9 +158,19 @@ def get_devices(timeout=3.0, host=None): except Exception: pass - devices_urls = [dev["location"] - for dev in devices if "AVTransport" in dev["st"]] - devices = [register_device(location_url) for location_url in devices_urls] + devices_urls = [ + dev["location"] + for dev in devices + if "st" in dev and + "AVTransport" in dev["st"] + ] + + devices = [ + register_device(location_url) + for location_url in devices_urls + ] + + devices = remove_duplicates(devices) return devices diff --git a/nanodlna/streaming.py b/nanodlna/streaming.py index b9447c0..169c20b 100644 --- a/nanodlna/streaming.py +++ b/nanodlna/streaming.py @@ -88,6 +88,10 @@ def start_server(files, serve_ip, serve_port=9000): return files_urls +def stop_server(): + reactor.stop() + + def get_serve_ip(target_ip, target_port=80): logging.debug("Identifying server IP") s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) From 037175e677f62112f78e95c0ce744c7f6d83b467 Mon Sep 17 00:00:00 2001 From: Gabriel Magno Date: Mon, 16 May 2022 18:45:04 -0300 Subject: [PATCH 18/18] Bumped version from v0.2.1 to v0.3.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 0f96f74..b36faa5 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ setup( name='nanodlna', - version='0.2.1', + version='0.3.0', description='A minimal UPnP/DLNA media streamer', long_description="""nano-dlna is a command line tool that allows you to play a local video file in your TV (or any other DLNA compatible device)""",