tiny-dlna 0.5.2__tar.gz → 0.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tiny-dlna
3
- Version: 0.5.2
3
+ Version: 0.7.0
4
4
  Summary: a tiny DLNA receiver
5
5
  Home-page: https://github.com/mitnk/tiny-dlna
6
6
  Author: mitnk
@@ -31,3 +31,15 @@ from apps like 虎牙直播, Bilibili, and other video platforms. Additionally,
31
31
  can also use `nano-dlna` to play local videos (like in your RaspberryPi) on it.
32
32
 
33
33
  Note: mpv needs to be installed on your system.
34
+
35
+ ::
36
+
37
+ $ tiny-cli list
38
+
39
+ List available DLNA devices.
40
+
41
+ ::
42
+
43
+ $ tiny-cli play ~/Movies/foo/bar.mp4 -q 'TV'
44
+
45
+ Play video on the DLNA device having "TV" in its name.
@@ -19,7 +19,7 @@ from apps like 虎牙直播, Bilibili, and other video platforms. Additionally,
19
19
  can also use `nano-dlna` to play local videos (like in your RaspberryPi) on it.
20
20
 
21
21
  Note that [mpv](https://mpv.io/) needs to be installed on your system. On Mac,
22
- do following:
22
+ do following (for Windows, add the mpv's root into PATH):
23
23
 
24
24
  ```
25
25
  $ ln -sf /Applications/mpv.app/Contents/MacOS/mpv /usr/local/bin/
@@ -32,6 +32,26 @@ List available DLNA devices:
32
32
  $ tiny-cli list
33
33
  ```
34
34
 
35
+ Play a local video file on a DLNA device having "TV" in its name:
36
+ ```
37
+ $ tiny-cli play ~/Movies/foo/bar.mp4 -q TV
38
+ ```
39
+
40
+ If there is a `bar.srt` in the same directory, it will be served as long as
41
+ the DLNA device supports subtitles.
42
+
43
+ ## Dev
44
+
45
+ ```
46
+ $ python -m tiny_dlna.tiny_cli -h
47
+ $ python -m tiny_dlna.tiny_render -h
48
+ ```
49
+
50
+ ### More DLNA Actions, like Seek/Pause?
51
+
52
+ This repository will be kept minimal. For additional DLNA actions, consider
53
+ forking it.
54
+
35
55
  ## Related projects
36
56
 
37
57
  - https://github.com/xfangfang/Macast
@@ -24,22 +24,35 @@ from apps like 虎牙直播, Bilibili, and other video platforms. Additionally,
24
24
  can also use `nano-dlna` to play local videos (like in your RaspberryPi) on it.
25
25
 
26
26
  Note: mpv needs to be installed on your system.
27
+
28
+ ::
29
+
30
+ $ tiny-cli list
31
+
32
+ List available DLNA devices.
33
+
34
+ ::
35
+
36
+ $ tiny-cli play ~/Movies/foo/bar.mp4 -q 'TV'
37
+
38
+ Play video on the DLNA device having "TV" in its name.
27
39
  """
28
40
 
29
41
  setup(
30
42
  name='tiny-dlna',
31
- version='0.5.2',
43
+ version='0.7.0',
32
44
  description='a tiny DLNA receiver',
33
45
  long_description=DESC,
34
46
  url='https://github.com/mitnk/tiny-dlna',
35
47
  author='mitnk',
36
48
  license='MIT',
37
49
  keywords='dlna',
38
- py_modules=["tiny_cli", "tiny_render", "tiny_ssdp"],
50
+ packages=['tiny_dlna'],
51
+ package_dir={'tiny_dlna': 'tiny_dlna'},
39
52
  entry_points={
40
53
  'console_scripts': [
41
- 'tiny-render=tiny_render:main',
42
- 'tiny-cli=tiny_cli:main',
54
+ 'tiny-render=tiny_dlna.tiny_render:main',
55
+ 'tiny-cli=tiny_dlna.tiny_cli:main',
43
56
  ],
44
57
  },
45
58
  install_requires=[
File without changes
@@ -0,0 +1,288 @@
1
+ import argparse
2
+ import json
3
+ import logging
4
+ import os.path
5
+ import random
6
+ import shutil
7
+ import signal
8
+ import socket
9
+ import threading
10
+ import time
11
+ import urllib.parse
12
+ import urllib.request as urlreq
13
+ import xml.etree.ElementTree as ET
14
+
15
+ from flask import Flask, send_from_directory
16
+ from xml.sax.saxutils import escape as xmlescape
17
+ from .tiny_ssdp import SSDP_MULTICAST_IP, SSDP_PORT, get_host_ip
18
+ from .tiny_xmls import * # NOQA
19
+
20
+ logger = logging.getLogger('tiny_cli')
21
+
22
+ MSEARCH_MSG = (
23
+ 'M-SEARCH * HTTP/1.1\r\n'
24
+ f'HOST: {SSDP_MULTICAST_IP}:{SSDP_PORT}\r\n'
25
+ 'MAN: "ssdp:discover"\r\n'
26
+ 'MX: 1\r\n'
27
+ 'ST: urn:schemas-upnp-org:service:AVTransport:1\r\n'
28
+ '\r\n'
29
+ )
30
+ DIR_LINKS = '~/.config/tiny-dlna/symlinks'
31
+
32
+
33
+ def _get_device_info(location):
34
+ p = urllib.parse.urlparse(location)
35
+
36
+ attrs = {}
37
+ namespace = {'ns': 'urn:schemas-upnp-org:device-1-0'}
38
+ namespaces = {
39
+ '': 'urn:schemas-upnp-org:device-1-0',
40
+ 'dlna': 'urn:schemas-dlna-org:device-1-0'
41
+ }
42
+
43
+ with urlreq.urlopen(location, timeout=1.0) as r:
44
+ xml = r.read()
45
+ root = ET.fromstring(xml.strip())
46
+
47
+ friendly_name = root.find('.//ns:friendlyName', namespace).text
48
+ if friendly_name:
49
+ attrs['friendly_name'] = friendly_name
50
+
51
+ elem = root.find(
52
+ ".//service[serviceId='urn:upnp-org:serviceId:AVTransport']",
53
+ namespaces=namespaces,
54
+ )
55
+ if elem:
56
+ control_url = elem.find('controlURL', namespaces).text
57
+ if control_url:
58
+ attrs['control_url'] = f'http://{p.hostname}:{p.port}/{control_url}'
59
+
60
+ return attrs
61
+
62
+
63
+ def _parse_ssdp_response(resp):
64
+ lines = resp.split('\r\n')
65
+ device = {}
66
+
67
+ if not lines[0].endswith('200 OK'):
68
+ return None
69
+
70
+ for line in lines:
71
+ try:
72
+ k, v = [x.strip() for x in line.split(':', 1)]
73
+ except ValueError:
74
+ continue
75
+
76
+ k = k.lower()
77
+ if k == 'location':
78
+ device['location'] = v
79
+ elif k == 'usn':
80
+ device['usn'] = v
81
+ elif k == 'st':
82
+ device['st'] = v
83
+
84
+ if ':service:AVTransport:' not in device.get('st', ''):
85
+ return None
86
+
87
+ attrs = _get_device_info(device['location'])
88
+ device.update(attrs)
89
+ return device
90
+
91
+
92
+ def get_dlna_devices():
93
+ # Create the UDP socket
94
+ sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
95
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
96
+ sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2)
97
+ sock.settimeout(1.2)
98
+
99
+ # Send the M-SEARCH message to the SSDP multicast address
100
+ logger.debug("Sending M-SEARCH...")
101
+ sock.sendto(MSEARCH_MSG.encode('utf-8'), (SSDP_MULTICAST_IP, SSDP_PORT))
102
+
103
+ devices = []
104
+ known_locations = set()
105
+ while True:
106
+ try:
107
+ data, addr = sock.recvfrom(1024)
108
+ device = _parse_ssdp_response(data.decode('utf-8'))
109
+ location = device.get('location')
110
+ logger.debug(f'got reply from {addr}, Location: {location}')
111
+ if device and location not in known_locations:
112
+ devices.append(device)
113
+ known_locations.add(location)
114
+ except socket.timeout:
115
+ break
116
+
117
+ return devices
118
+
119
+
120
+ def list_dlna_devices():
121
+ devices = get_dlna_devices()
122
+ print(json.dumps({'devices': devices}, sort_keys=True, indent=2))
123
+
124
+
125
+ def post(url, action_data, headers):
126
+ action_data = action_data.encode("utf-8")
127
+ r = urlreq.Request(url, action_data, headers)
128
+ urlreq.urlopen(r)
129
+ logging.debug("Request sent")
130
+
131
+
132
+ app = Flask(__name__)
133
+
134
+
135
+ @app.route('/videos/<name_video>')
136
+ def serve_video(name_video):
137
+ dir_base = os.path.expanduser(DIR_LINKS)
138
+ return send_from_directory(dir_base, name_video)
139
+
140
+
141
+ def run_flask_server(port):
142
+ app.run(host='0.0.0.0', port=port)
143
+
144
+
145
+ def send_dlna_command(url_control, action_body, action_name):
146
+ st = "urn:schemas-upnp-org:service:AVTransport:1"
147
+ headers = {
148
+ 'Content-Type': 'text/xml; charset="utf-8"',
149
+ 'SOAPACTION': f'"{st}#{action_name}"',
150
+ "Connection": "close",
151
+ }
152
+ post(url_control, action_body, headers)
153
+
154
+
155
+ def send_set_av_transport(url_control, url_video, url_srt=None):
156
+ title = url_video.split('/')[-1]
157
+ meta_items = XML_VIDEO.format(title=title)
158
+ if url_srt:
159
+ meta_items += XML_SUBTITLE.format(url_srt=url_srt)
160
+ metadata = XML_META.format(items=meta_items)
161
+ xml = XML_SETAV.format(
162
+ url_video=url_video,
163
+ metadata=xmlescape(metadata),
164
+ )
165
+ send_dlna_command(url_control, xml, 'SetAVTransportURI')
166
+
167
+
168
+ def send_play(url_control):
169
+ send_dlna_command(url_control, XML_PLAY, 'Play')
170
+
171
+
172
+ def send_stop(url_control):
173
+ dir_links = os.path.expanduser(DIR_LINKS)
174
+ try:
175
+ shutil.rmtree(dir_links)
176
+ except: # NOQA
177
+ pass
178
+ send_dlna_command(url_control, XML_STOP, 'Stop')
179
+
180
+
181
+ def create_link(path_file):
182
+ name_file = os.path.basename(path_file)
183
+ dir_links = os.path.expanduser(DIR_LINKS)
184
+ if not os.path.exists(dir_links):
185
+ os.makedirs(dir_links, exist_ok=True)
186
+
187
+ path_link = os.path.join(dir_links, name_file)
188
+ if os.path.exists(path_link):
189
+ if os.path.islink(path_link):
190
+ os.remove(path_link)
191
+ else:
192
+ logger.error('failed to create softlink')
193
+ exit(1)
194
+
195
+ os.symlink(path_file, path_link)
196
+ logger.info(f'created softlink: {path_link}')
197
+
198
+
199
+ def get_control_url(args):
200
+ devices = get_dlna_devices()
201
+ for d in devices:
202
+ if args.query.lower() in d['friendly_name'].lower():
203
+ return d.get('control_url')
204
+
205
+
206
+ def play_video(args):
207
+ path_video = args.video_file
208
+ if not os.path.isfile(path_video):
209
+ print(f'no such file: {path_video}')
210
+ exit(0)
211
+
212
+ url_control = get_control_url(args)
213
+ if not url_control:
214
+ print('no such DLNA device found')
215
+ exit(0)
216
+
217
+ ip = get_host_ip()
218
+ port = random.randint(50000, 58999)
219
+
220
+ logger.info(f'play video file: {path_video}')
221
+ create_link(path_video)
222
+ name_video = os.path.basename(path_video)
223
+ url_video = f"http://{ip}:{port}/videos/{name_video}"
224
+
225
+ path_srt = '.'.join(path_video.split('.')[:-1]) + '.srt'
226
+ if os.path.exists(path_srt):
227
+ create_link(path_srt)
228
+ name_srt = os.path.basename(path_srt)
229
+ url_srt = f"http://{ip}:{port}/videos/{name_srt}"
230
+ else:
231
+ path_srt = None
232
+ url_srt = None
233
+
234
+ server_thread = threading.Thread(target=run_flask_server, args=(port,))
235
+ server_thread.daemon = True
236
+ server_thread.start()
237
+
238
+ # Give some time for the server to start
239
+ time.sleep(1.6)
240
+
241
+ send_set_av_transport(url_control, url_video, url_srt)
242
+ send_play(url_control)
243
+
244
+ def signal_handler(sig, frame):
245
+ send_stop(url_control)
246
+ exit(0)
247
+ signal.signal(signal.SIGINT, signal_handler)
248
+
249
+ # Keep main thread running so the server stays up and we can catch signal
250
+ while True:
251
+ time.sleep(1)
252
+
253
+
254
+ def main():
255
+ logging.basicConfig(
256
+ level=logging.ERROR,
257
+ format='[%(asctime)s][%(levelname)s] %(message)s',
258
+ )
259
+
260
+ parser = argparse.ArgumentParser(prog='tiny-cli')
261
+ subparsers = parser.add_subparsers(dest='command', required=True,
262
+ help='Choose a command')
263
+
264
+ greet_parser = subparsers.add_parser('list', help='List available DLNA devices')
265
+ greet_parser.add_argument('-v', dest='verbose', action='store_true',
266
+ help='Enable verbose logs')
267
+
268
+ play_parser = subparsers.add_parser('play', help='Play via via DLNA device')
269
+ play_parser.add_argument('video_file')
270
+ play_parser.add_argument('-v', dest='verbose', action='store_true',
271
+ help='Enable verbose logs')
272
+ play_parser.add_argument('-q', dest='query', type=str, required=True,
273
+ help='Specify Device by Friendly Name')
274
+
275
+ args = parser.parse_args()
276
+ if args.verbose:
277
+ logger.setLevel(logging.DEBUG)
278
+
279
+ if args.command == 'list':
280
+ list_dlna_devices()
281
+ elif args.command == 'play':
282
+ if not args.verbose:
283
+ logging.getLogger('werkzeug').setLevel(logging.ERROR)
284
+ play_video(args)
285
+
286
+
287
+ if __name__ == '__main__':
288
+ main()
@@ -1,3 +1,4 @@
1
+ import argparse
1
2
  import html
2
3
  import logging
3
4
  import re
@@ -7,89 +8,13 @@ import time
7
8
  import xml.etree.ElementTree as ET
8
9
 
9
10
  from flask import Flask, request, Response
10
- from tiny_ssdp import ssdp_listener, RENDER_PORT, UUID
11
+ from .tiny_ssdp import get_uuid, ssdp_listener
12
+ from .tiny_xmls import * # NOQA
11
13
 
12
14
  app = Flask(__name__)
13
15
  logger = logging.getLogger('tiny_render')
14
16
 
15
17
 
16
- XML_DESC_PTN = """<?xml version="1.0" encoding="UTF-8"?>
17
- <root xmlns:dlna="urn:schemas-dlna-org:device-1-0" xmlns="urn:schemas-upnp-org:device-1-0">
18
- <specVersion>
19
- <major>1</major>
20
- <minor>0</minor>
21
- </specVersion>
22
- <device>
23
- <deviceType>urn:schemas-upnp-org:device:MediaRenderer:1</deviceType>
24
- <friendlyName>{}</friendlyName>
25
- <manufacturer>mitnk</manufacturer>
26
- <modelName>Tiny-Render</modelName>
27
- <modelDescription>AVTransport Media Renderer</modelDescription>
28
- <UDN>uuid:{}</UDN>
29
- <dlna:X_DLNADOC xmlns:dlna="urn:schemas-dlna-org:device-1-0">DMR-1.50</dlna:X_DLNADOC>
30
- <serviceList>
31
- <service>
32
- <serviceType>urn:schemas-upnp-org:service:AVTransport:1</serviceType>
33
- <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
34
- <SCPDURL>AVTransport/scpd.xml</SCPDURL>
35
- <controlURL>AVTransport/control</controlURL>
36
- <eventSubURL>AVTransport/event</eventSubURL>
37
- </service>
38
- </serviceList>
39
- </device>
40
- </root>"""
41
-
42
- XML_AVSET_DONE = """
43
- <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
44
- <s:Body>
45
- <u:SetAVTransportURIResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"/>
46
- </s:Body>
47
- </s:Envelope>
48
- """
49
-
50
- XML_PLAY_DONE = """
51
- <s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
52
- <s:Body>
53
- <u:PlayResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"/>
54
- </s:Body>
55
- </s:Envelope>
56
- """
57
-
58
- XML_POSINFO = """
59
- <s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
60
- <s:Body>
61
- <u:GetPositionInfoResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
62
- <Track>0</Track>
63
- <TrackDuration>00:00:00</TrackDuration>
64
- <RelTime>{0}</RelTime>
65
- <AbsTime>{0}</AbsTime>
66
- <RelCount>2147483647</RelCount>
67
- <AbsCount>2147483647</AbsCount>
68
- </u:GetPositionInfoResponse>
69
- </s:Body>
70
- </s:Envelope>
71
- """
72
-
73
- XML_TRANSINFO = """
74
- <s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
75
- <s:Body>
76
- <u:GetTransportInfoResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
77
- <CurrentTransportState>PLAYING</CurrentTransportState>
78
- <CurrentTransportStatus>OK</CurrentTransportStatus>
79
- <CurrentSpeed>1</CurrentSpeed>
80
- </u:GetTransportInfoResponse>
81
- </s:Body>
82
- </s:Envelope>
83
- """
84
-
85
- XML_STOP_DONE = """
86
- <s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
87
- <s:Body>
88
- <u:StopResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"/>
89
- </s:Body>
90
- </s:Envelope>
91
- """
92
-
93
18
  class MPVRenderer:
94
19
  def __init__(self):
95
20
  self.process = None
@@ -120,11 +45,33 @@ _DATA = {
120
45
  'STARTED_AT': 0,
121
46
  }
122
47
 
48
+
123
49
  @app.route('/description.xml')
124
50
  def description():
125
51
  friendly_name = app.config['FRIENDLY_NAME']
126
- xml = XML_DESC_PTN.format(friendly_name, UUID)
127
- return Response(xml, mimetype="text/xml")
52
+ uuid_str = get_uuid()
53
+ xml = XML_DESC_PTN.format(friendly_name, uuid_str)
54
+ resp = Response(xml, mimetype="text/xml")
55
+ resp.headers['Server'] = 'UPnP/1.0 Werkzeug/3.0 TinyRender/0.7'
56
+ return resp
57
+
58
+ # these 3 dlna_ routings below are only here so that some dummy app
59
+ # could treat our "tiny render" as a proper dlna device. (Mouyu ..)
60
+ @app.route('/dlna/AVTransport.xml')
61
+ def dlna_avtransport():
62
+ resp = Response(XML_DLNA_AVT, mimetype="text/xml")
63
+ return resp
64
+
65
+ @app.route('/dlna/RenderingControl.xml')
66
+ def dlna_render_control():
67
+ resp = Response(XML_DLNA_RENDER_CTRL, mimetype="text/xml")
68
+ return resp
69
+
70
+ @app.route('/dlna/ConnectionManager.xml')
71
+ def dlna_conn_manager():
72
+ resp = Response(XML_DLNA_CONN_MANAGER, mimetype="text/xml")
73
+ return resp
74
+
128
75
 
129
76
  def to_track_time(seconds):
130
77
  hours = seconds // 3600
@@ -132,12 +79,12 @@ def to_track_time(seconds):
132
79
  remaining_seconds = seconds % 60
133
80
  return f"{hours}:{minutes:02}:{remaining_seconds:02}"
134
81
 
135
- def is_stop(request):
136
- return b'<u:Stop' in request.data
137
-
138
82
  def is_play(request):
139
83
  return b'<u:Play' in request.data
140
84
 
85
+ def is_stop(request):
86
+ return b'<u:Stop' in request.data
87
+
141
88
  def is_setav(request):
142
89
  return b'SetAVTransportURI' in request.data
143
90
 
@@ -157,27 +104,35 @@ def get_title_re(xml_data):
157
104
  return match.group(1)
158
105
 
159
106
  def get_metadata(request):
160
- root = ET.fromstring(request.data)
107
+ root = ET.fromstring(request.data.strip())
161
108
  current_uri = root.find('.//CurrentURI').text
162
109
  metadata = root.find('.//CurrentURIMetaData').text
110
+ if not metadata:
111
+ logger.debug('no metadata')
112
+ return {'video': current_uri, 'title': ''}
163
113
 
164
- current_srt = ''
165
114
  title = ''
166
-
167
115
  metadata = html.unescape(metadata)
168
116
  try:
169
- metadata = ET.fromstring(metadata)
117
+ # HACK: replace all `&` to `&amp;`
118
+ metadata = metadata.replace('&', '&amp;')
119
+ metadata = ET.fromstring(metadata.strip())
170
120
  except ET.ParseError:
171
121
  # HACK: fall back to `re` to get title only (e.g. Huya)
172
122
  logger.debug('** got xml.ParseError, fall back to re')
173
123
  title = get_title_re(metadata)
174
124
  return {'video': current_uri, 'title': title}
175
125
 
176
- ns = {'dc': 'http://purl.org/dc/elements/1.1/'}
177
- obj = metadata.find('.//dc:title', ns)
178
- if obj:
126
+ ns = {
127
+ 'dc': 'http://purl.org/dc/elements/1.1/',
128
+ '': 'urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/'
129
+ }
130
+ item = metadata.find('.//item', namespaces=ns)
131
+ obj = item.find('.//dc:title', namespaces=ns)
132
+ if obj is not None:
179
133
  title = obj.text
180
134
 
135
+ current_srt = ''
181
136
  for res in metadata.findall('.//{urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/}res'):
182
137
  if res.get('type') == 'text/subtitle':
183
138
  current_srt = res.text.strip()
@@ -186,7 +141,7 @@ def get_metadata(request):
186
141
  return {
187
142
  'video': current_uri,
188
143
  'srt': current_srt,
189
- 'title': current_srt,
144
+ 'title': title,
190
145
  }
191
146
 
192
147
 
@@ -199,7 +154,7 @@ def control():
199
154
  video_title = metadata.get('title', '')
200
155
 
201
156
  logger.debug(f'Action: SetAV: {current_uri}')
202
- logger.debug(f'SRT: {current_srt}')
157
+ logger.debug(f'Title: {video_title} SRT: {current_srt}')
203
158
  _DATA['CURRENT_URI'] = current_uri
204
159
  _DATA['CURRENT_SRT'] = current_srt
205
160
  _DATA['VIDEO_TITLE'] = video_title
@@ -211,11 +166,11 @@ def control():
211
166
  srt = _DATA['CURRENT_SRT']
212
167
  title = _DATA['VIDEO_TITLE']
213
168
  logger.debug(f'action: Play: {url}')
214
- logger.debug(f'title: {title} srt: {srt}')
215
169
  renderer.play_media(url, title, srt)
216
170
  return Response(XML_PLAY_DONE, mimetype="text/xml")
217
171
 
218
172
  elif is_getpos(request):
173
+ # this is only a dummy impl; we need rpc to mpv process for real.
219
174
  logger.debug('action: GetPositionInfo')
220
175
  seconds = int(time.time() - _DATA['STARTED_AT'])
221
176
  reltime = to_track_time(seconds)
@@ -236,47 +191,50 @@ def control():
236
191
  return Response(XML_STOP_DONE, mimetype="text/xml")
237
192
 
238
193
  elif is_seek(request):
239
- logger.debug('seek')
240
- return Response('To Seek', status=500)
194
+ logger.debug('action:seek')
195
+ return Response(XML_SEEK_DONE, mimetype="text/xml")
241
196
 
242
197
  logger.error(f'action not support: {request.data}')
243
198
  return Response('Action Not Supported', status=500)
244
199
 
245
200
 
246
201
  class SSDPServer(threading.Thread):
202
+ def __init__(self, render_port):
203
+ super().__init__()
204
+ self.render_port = render_port
205
+
247
206
  def run(self):
248
- ssdp_listener()
207
+ ssdp_listener(self.render_port)
249
208
 
250
209
 
251
210
  def main():
252
- import argparse
253
-
254
211
  parser = argparse.ArgumentParser(prog='tiny-render')
255
- parser.add_argument('--http-log', action='store_true', help='Enable server logs')
212
+ parser.add_argument('--http-logs', action='store_true', help='Enable server logs')
256
213
  parser.add_argument('--verbose', '-v', action='store_true', help='Enable debug logs')
257
214
  parser.add_argument('--name', type=str, default='Tiny Render', help='Change render name')
258
215
 
259
216
  args = parser.parse_args()
260
217
 
261
218
  logging.basicConfig(
262
- level=logging.INFO,
219
+ level=logging.ERROR,
263
220
  format='[%(asctime)s][%(levelname)s] %(message)s',
264
221
  )
265
222
 
266
- if not args.http_log:
223
+ if not args.http_logs:
267
224
  logging.getLogger('werkzeug').setLevel(logging.ERROR)
268
- logging.getLogger('ssdp').setLevel(logging.ERROR)
225
+ logging.getLogger('tiny_ssdp').setLevel(logging.ERROR)
269
226
 
270
227
  if args.verbose:
271
- logging.basicConfig(level=logging.DEBUG)
228
+ logger.setLevel(logging.DEBUG)
272
229
 
273
- ssdp = SSDPServer()
230
+ port = 59876
231
+ ssdp = SSDPServer(port)
274
232
  ssdp.start()
275
233
 
276
234
  friendly_name = args.name
277
235
  app.config['FRIENDLY_NAME'] = friendly_name
278
236
  logging.info(f'Starting DLNA Receiver: {friendly_name}')
279
- app.run(host="0.0.0.0", port=RENDER_PORT, debug=False)
237
+ app.run(host="0.0.0.0", port=port, debug=False)
280
238
 
281
239
 
282
240
  if __name__ == "__main__":