Skip to content

Commit

Permalink
Merge pull request gabrielmagno#21 from gabrielmagno/develop
Browse files Browse the repository at this point in the history
Develop
  • Loading branch information
gabrielmagno authored May 16, 2022
2 parents ea5b166 + 037175e commit 3df3241
Show file tree
Hide file tree
Showing 7 changed files with 209 additions and 46 deletions.
109 changes: 86 additions & 23 deletions nanodlna/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import json
import os
import sys
import signal
import datetime
import tempfile

Expand Down Expand Up @@ -52,34 +53,15 @@ 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):
print("Device {0}:\n{1}\n\n".format(i, json.dumps(device, indent=4)))


def play(args):

set_logs(args)

logging.info("Starting to play")

# Get video and subtitle file names

files = {"file_video": args.file_video}

if args.use_subtitle:

if not args.file_subtitle:
args.file_subtitle = get_subtitle(args.file_video)

if args.file_subtitle:
files["file_subtitle"] = args.file_subtitle

logging.info("Media files: {}".format(json.dumps(files)))
def find_device(args):

# Select device to play
logging.info("Selecting device to play")

device = None
Expand All @@ -88,7 +70,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:
Expand All @@ -100,6 +82,30 @@ def play(args):
logging.info("Select first device")
device = my_devices[0]

return device


def play(args):

set_logs(args)

logging.info("Starting to play")

# Get video and subtitle file names

files = {"file_video": args.file_video}

if args.use_subtitle:

if not args.file_subtitle:
args.file_subtitle = get_subtitle(args.file_video)

if args.file_subtitle:
files["file_subtitle"] = args.file_subtitle

logging.info("Media files: {}".format(json.dumps(files)))

device = find_device(args)
if not device:
sys.exit("No devices found.")

Expand All @@ -117,16 +123,64 @@ 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)

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(
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")
Expand All @@ -137,14 +191,23 @@ 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",
dest="use_subtitle", action="store_false")
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)
Expand Down
102 changes: 81 additions & 21 deletions nanodlna/devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import re
import socket
import struct
import sys
import xml.etree.ElementTree as ET

Expand All @@ -25,37 +26,67 @@
"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"
UPNP_DEVICE_TYPE = "urn:schemas-upnp-org:device:MediaRenderer:1"
UPNP_SERVICE_TYPE = "urn:schemas-upnp-org:service:AVTransport:1"


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


def register_device(location_url):

xml = urllibreq.urlopen(location_url).read().decode("UTF-8")
xml = re.sub(" xmlns=\"[^\"]+\"", "", xml, count=1)
xml_raw = urllibreq.urlopen(location_url).read().decode("UTF-8")
logging.debug(
"Device to be registered: {}".format(
json.dumps({
"location_url": location_url,
"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

friendly_name = info.find("./device/friendlyName").text

try:
path = info.find(
"./device/serviceList/service/"
"[serviceType='{0}']/controlURL".format(
UPNP_DEFAULT_SERVICE_TYPE
device_root = info.find("./device")
if not device_root:
device_root = info.find(
"./device/deviceList/device/"
"[deviceType='{0}']".format(
UPNP_DEVICE_TYPE
)
).text
action_url = urllibparse.urljoin(location_url, path)
except AttributeError:
)

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": manufacturer,
"friendly_name": friendly_name,
"action_url": action_url,
"st": UPNP_DEFAULT_SERVICE_TYPE
"st": UPNP_SERVICE_TYPE
}

logging.debug(
Expand All @@ -70,12 +101,31 @@ def register_device(location_url):
return device


def get_devices(timeout=3.0):
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:
host = "0.0.0.0"
logging.debug("Searching for devices on {}".format(host))

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.bind(("", SSDP_BROADCAST_PORT + 10))

# 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((host, 0))

logging.debug("Sending broadcast message")
s.sendto(SSDP_BROADCAST_MSG.encode("UTF-8"), (SSDP_BROADCAST_ADDR,
Expand Down Expand Up @@ -109,9 +159,19 @@ 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
]

devices = remove_duplicates(devices)

return devices

Expand All @@ -120,7 +180,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)))
22 changes: 21 additions & 1 deletion nanodlna/dlna.py
Original file line number Diff line number Diff line change
Expand Up @@ -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\"",
Expand Down Expand Up @@ -95,3 +97,21 @@ def play(files_urls, device):
send_dlna_action(device, video_data, "SetAVTransportURI")
logging.debug("Playing video")
send_dlna_action(device, video_data, "Play")


def pause(device):
logging.debug("Pausing device: {}".format(
json.dumps({
"device": device
})
))
send_dlna_action(device, None, "Pause")


def stop(device):
logging.debug("Stopping device: {}".format(
json.dumps({
"device": device
})
))
send_dlna_action(device, None, "Stop")
4 changes: 4 additions & 0 deletions nanodlna/streaming.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 8 additions & 0 deletions nanodlna/templates/action-Pause.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version='1.0' encoding='utf-8'?>
<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<u:Pause xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
<InstanceID>0</InstanceID>
</u:Pause>
</s:Body>
</s:Envelope>
8 changes: 8 additions & 0 deletions nanodlna/templates/action-Stop.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version='1.0' encoding='utf-8'?>
<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<u:Stop xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
<InstanceID>0</InstanceID>
</u:Stop>
</s:Body>
</s:Envelope>
Loading

0 comments on commit 3df3241

Please sign in to comment.