tiny-dlna 0.0.1__tar.gz → 0.3.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.
- {tiny-dlna-0.0.1 → tiny-dlna-0.3.0}/PKG-INFO +1 -1
- tiny-dlna-0.3.0/README.md +30 -0
- {tiny-dlna-0.0.1 → tiny-dlna-0.3.0}/setup.py +1 -1
- {tiny-dlna-0.0.1 → tiny-dlna-0.3.0}/ssdp.py +4 -1
- {tiny-dlna-0.0.1 → tiny-dlna-0.3.0}/tiny_dlna.egg-info/PKG-INFO +1 -1
- {tiny-dlna-0.0.1 → tiny-dlna-0.3.0}/tiny_render.py +95 -57
- tiny-dlna-0.0.1/README.md +0 -13
- {tiny-dlna-0.0.1 → tiny-dlna-0.3.0}/setup.cfg +0 -0
- {tiny-dlna-0.0.1 → tiny-dlna-0.3.0}/tiny_dlna.egg-info/SOURCES.txt +0 -0
- {tiny-dlna-0.0.1 → tiny-dlna-0.3.0}/tiny_dlna.egg-info/dependency_links.txt +0 -0
- {tiny-dlna-0.0.1 → tiny-dlna-0.3.0}/tiny_dlna.egg-info/entry_points.txt +0 -0
- {tiny-dlna-0.0.1 → tiny-dlna-0.3.0}/tiny_dlna.egg-info/requires.txt +0 -0
- {tiny-dlna-0.0.1 → tiny-dlna-0.3.0}/tiny_dlna.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Tiny DLNA
|
|
2
|
+
|
|
3
|
+
## Install
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
$ pip install tiny-dlna
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Tiny DLNA Render
|
|
10
|
+
|
|
11
|
+
Just to support subtitles.
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
$ tiny-render
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
This will activate a DLNA receiver named "Tiny Render", which can stream videos
|
|
18
|
+
from apps like 虎牙直播, Bilibili, and other video platforms. Additionally, you
|
|
19
|
+
can also use `nano-dlna` to play local videos (like in your RaspberryPi) on it.
|
|
20
|
+
|
|
21
|
+
> Note: [mpv](https://mpv.io/) needs to be installed on your system.
|
|
22
|
+
|
|
23
|
+
## Tiny DLNA Cli
|
|
24
|
+
|
|
25
|
+
To do.
|
|
26
|
+
|
|
27
|
+
## Related projects
|
|
28
|
+
|
|
29
|
+
- https://github.com/xfangfang/Macast
|
|
30
|
+
- https://github.com/gabrielmagno/nano-dlna
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
import socket
|
|
2
3
|
|
|
3
4
|
SSDP_MULTICAST = '239.255.255.250'
|
|
@@ -17,6 +18,8 @@ ssdp_response = (
|
|
|
17
18
|
'\r\n'
|
|
18
19
|
)
|
|
19
20
|
|
|
21
|
+
logger = logging.getLogger('ssdp')
|
|
22
|
+
|
|
20
23
|
def ssdp_listener():
|
|
21
24
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
|
|
22
25
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
@@ -29,7 +32,7 @@ def ssdp_listener():
|
|
|
29
32
|
while True:
|
|
30
33
|
data, addr = sock.recvfrom(1024)
|
|
31
34
|
if b'M-SEARCH' in data and b'ssdp:discover' in data:
|
|
32
|
-
|
|
35
|
+
logger.info(f'Received M-SEARCH from {addr}, sending response...')
|
|
33
36
|
sock.sendto(ssdp_response.encode('utf-8'), addr)
|
|
34
37
|
|
|
35
38
|
if __name__ == '__main__':
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import html
|
|
2
|
+
import logging
|
|
2
3
|
import re
|
|
3
4
|
import subprocess
|
|
4
5
|
import threading
|
|
@@ -9,6 +10,7 @@ from flask import Flask, request, Response
|
|
|
9
10
|
from ssdp import ssdp_listener
|
|
10
11
|
|
|
11
12
|
app = Flask(__name__)
|
|
13
|
+
logger = logging.getLogger('tiny_render')
|
|
12
14
|
|
|
13
15
|
XML_AVSET_DONE = """
|
|
14
16
|
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
|
|
@@ -72,7 +74,9 @@ class MPVRenderer:
|
|
|
72
74
|
cmd.append('--title={}'.format(title))
|
|
73
75
|
if srt:
|
|
74
76
|
cmd.append('--sub-file={}'.format(srt))
|
|
75
|
-
|
|
77
|
+
|
|
78
|
+
logger.debug(f'running mpv: {cmd}')
|
|
79
|
+
self.process = subprocess.Popen(cmd, stderr=subprocess.DEVNULL)
|
|
76
80
|
|
|
77
81
|
def stop_media(self):
|
|
78
82
|
if self.process:
|
|
@@ -91,44 +95,47 @@ _DATA = {
|
|
|
91
95
|
|
|
92
96
|
@app.route('/description.xml')
|
|
93
97
|
def description():
|
|
94
|
-
|
|
95
|
-
<root xmlns="urn:schemas-upnp-org:device-1-0" xmlns:dlna="urn:schemas-dlna-org:device-1-0">
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
</root>"""
|
|
98
|
+
desc_ptn = """<?xml version="1.0"?>
|
|
99
|
+
<root xmlns="urn:schemas-upnp-org:device-1-0" xmlns:dlna="urn:schemas-dlna-org:device-1-0">
|
|
100
|
+
<specVersion>
|
|
101
|
+
<major>1</major>
|
|
102
|
+
<minor>0</minor>
|
|
103
|
+
</specVersion>
|
|
104
|
+
<device>
|
|
105
|
+
<deviceType>urn:schemas-upnp-org:device:MediaRenderer:1</deviceType>
|
|
106
|
+
<friendlyName>{}</friendlyName>
|
|
107
|
+
<manufacturer>mitnk</manufacturer>
|
|
108
|
+
<modelName>T001</modelName>
|
|
109
|
+
<UDN>uuid:dlna-tiny-render-t001</UDN>
|
|
110
|
+
<dlna:X_DLNADOC xmlns:dlna="urn:schemas-dlna-org:device-1-0">DMR-1.50</dlna:X_DLNADOC>
|
|
111
|
+
<serviceList>
|
|
112
|
+
<service>
|
|
113
|
+
<serviceType>urn:schemas-upnp-org:service:AVTransport:1</serviceType>
|
|
114
|
+
<serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
|
|
115
|
+
<SCPDURL>AVTransport/82d8-eb72-b097/scpd.xml</SCPDURL>
|
|
116
|
+
<controlURL>AVTransport/82d8-eb72-b097/control</controlURL>
|
|
117
|
+
<eventSubURL>AVTransport/82d8-eb72-b097/event</eventSubURL>
|
|
118
|
+
</service>
|
|
119
|
+
<service>
|
|
120
|
+
<serviceType>urn:schemas-upnp-org:service:ConnectionManager:1</serviceType>
|
|
121
|
+
<serviceId>urn:upnp-org:serviceId:ConnectionManager</serviceId>
|
|
122
|
+
<SCPDURL>ConnectionManager/82d8-eb72-b097/scpd.xml</SCPDURL>
|
|
123
|
+
<controlURL>ConnectionManager/82d8-eb72-b097/control</controlURL>
|
|
124
|
+
<eventSubURL>ConnectionManager/82d8-eb72-b097/event</eventSubURL>
|
|
125
|
+
</service>
|
|
126
|
+
<service>
|
|
127
|
+
<serviceType>urn:schemas-upnp-org:service:RenderingControl:1</serviceType>
|
|
128
|
+
<serviceId>urn:upnp-org:serviceId:RenderingControl</serviceId>
|
|
129
|
+
<SCPDURL>RenderingControl/82d8-eb72-b097/scpd.xml</SCPDURL>
|
|
130
|
+
<controlURL>RenderingControl/82d8-eb72-b097/control</controlURL>
|
|
131
|
+
<eventSubURL>RenderingControl/82d8-eb72-b097/event</eventSubURL>
|
|
132
|
+
</service>
|
|
133
|
+
</serviceList>
|
|
134
|
+
</device>
|
|
135
|
+
</root>"""
|
|
136
|
+
|
|
137
|
+
friendly_name = app.config['FRIENDLY_NAME']
|
|
138
|
+
xml = desc_ptn.format(friendly_name)
|
|
132
139
|
return Response(xml, mimetype="text/xml")
|
|
133
140
|
|
|
134
141
|
def to_track_time(seconds):
|
|
@@ -152,6 +159,9 @@ def is_gettrans(request):
|
|
|
152
159
|
def is_getpos(request):
|
|
153
160
|
return b'u:GetPositionInfo' in request.data
|
|
154
161
|
|
|
162
|
+
def is_seek(request):
|
|
163
|
+
return b'u:Seek' in request.data
|
|
164
|
+
|
|
155
165
|
def get_title_re(xml_data):
|
|
156
166
|
pattern = re.compile(r'<dc:title>(.*?)</dc:title>', re.DOTALL)
|
|
157
167
|
match = pattern.search(xml_data)
|
|
@@ -163,15 +173,15 @@ def get_metadata(request):
|
|
|
163
173
|
current_uri = root.find('.//CurrentURI').text
|
|
164
174
|
metadata = root.find('.//CurrentURIMetaData').text
|
|
165
175
|
|
|
166
|
-
current_srt =
|
|
167
|
-
title =
|
|
176
|
+
current_srt = ''
|
|
177
|
+
title = ''
|
|
168
178
|
|
|
169
179
|
metadata = html.unescape(metadata)
|
|
170
180
|
try:
|
|
171
181
|
metadata = ET.fromstring(metadata)
|
|
172
182
|
except ET.ParseError:
|
|
173
183
|
# HACK: fall back to `re` to get title only (e.g. Huya)
|
|
174
|
-
|
|
184
|
+
logger.debug('** got xml.ParseError, fall back to re')
|
|
175
185
|
title = get_title_re(metadata)
|
|
176
186
|
return {'video': current_uri, 'title': title}
|
|
177
187
|
|
|
@@ -196,18 +206,15 @@ def get_metadata(request):
|
|
|
196
206
|
def control():
|
|
197
207
|
if is_setav(request):
|
|
198
208
|
metadata = get_metadata(request)
|
|
199
|
-
|
|
200
209
|
current_uri = metadata['video']
|
|
201
|
-
current_srt = metadata.get('srt')
|
|
202
|
-
video_title = metadata.get('title')
|
|
210
|
+
current_srt = metadata.get('srt', '')
|
|
211
|
+
video_title = metadata.get('title', '')
|
|
203
212
|
|
|
204
|
-
|
|
213
|
+
logger.debug(f'Action: SetAV: {current_uri}')
|
|
214
|
+
logger.debug(f'SRT: {current_srt}')
|
|
205
215
|
_DATA['CURRENT_URI'] = current_uri
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
_DATA['CURRENT_SRT'] = current_srt
|
|
209
|
-
if video_title:
|
|
210
|
-
_DATA['VIDEO_TITLE'] = video_title
|
|
216
|
+
_DATA['CURRENT_SRT'] = current_srt
|
|
217
|
+
_DATA['VIDEO_TITLE'] = video_title
|
|
211
218
|
return Response(XML_AVSET_DONE, mimetype="text/xml")
|
|
212
219
|
|
|
213
220
|
elif is_play(request):
|
|
@@ -215,22 +222,24 @@ def control():
|
|
|
215
222
|
url = _DATA['CURRENT_URI']
|
|
216
223
|
srt = _DATA['CURRENT_SRT']
|
|
217
224
|
title = _DATA['VIDEO_TITLE']
|
|
218
|
-
|
|
225
|
+
logger.debug(f'action: Play: {url}')
|
|
226
|
+
logger.debug(f'title: {title} srt: {srt}')
|
|
219
227
|
renderer.play_media(url, title, srt)
|
|
220
228
|
return Response(XML_PLAY_DONE, mimetype="text/xml")
|
|
221
229
|
|
|
222
230
|
elif is_getpos(request):
|
|
223
|
-
|
|
231
|
+
logger.debug('action: GetPositionInfo')
|
|
224
232
|
seconds = int(time.time() - _DATA['STARTED_AT'])
|
|
225
233
|
reltime = to_track_time(seconds)
|
|
226
234
|
return Response(XML_POSINFO.format(reltime), mimetype="text/xml")
|
|
227
235
|
|
|
228
236
|
elif is_gettrans(request):
|
|
229
|
-
|
|
237
|
+
logger.debug('action: GetTransportInfo')
|
|
230
238
|
return Response(XML_TRANSINFO, mimetype="text/xml")
|
|
231
239
|
|
|
232
240
|
elif is_stop(request):
|
|
233
|
-
|
|
241
|
+
logger.debug('stopping')
|
|
242
|
+
|
|
234
243
|
_DATA['CURRENT_URI'] = ''
|
|
235
244
|
_DATA['CURRENT_SRT'] = ''
|
|
236
245
|
_DATA['VIDEO_TITLE'] = ''
|
|
@@ -238,7 +247,11 @@ def control():
|
|
|
238
247
|
renderer.stop_media()
|
|
239
248
|
return Response(XML_STOP_DONE, mimetype="text/xml")
|
|
240
249
|
|
|
241
|
-
|
|
250
|
+
elif is_seek(request):
|
|
251
|
+
logger.debug('seek')
|
|
252
|
+
return Response('To Seek', status=500)
|
|
253
|
+
|
|
254
|
+
logger.error(f'action not support: {request.data}')
|
|
242
255
|
return Response('Action Not Supported', status=500)
|
|
243
256
|
|
|
244
257
|
|
|
@@ -248,9 +261,34 @@ class SSDPServer(threading.Thread):
|
|
|
248
261
|
|
|
249
262
|
|
|
250
263
|
def main():
|
|
264
|
+
import argparse
|
|
265
|
+
|
|
266
|
+
parser = argparse.ArgumentParser(prog='tiny-render')
|
|
267
|
+
parser.add_argument('--http-log', action='store_true', help='Enable server logs')
|
|
268
|
+
parser.add_argument('--verbose', '-v', action='store_true', help='Enable debug logs')
|
|
269
|
+
parser.add_argument('--name', type=str, default='Tiny Render', help='Change render name')
|
|
270
|
+
|
|
271
|
+
args = parser.parse_args()
|
|
272
|
+
|
|
273
|
+
logging.basicConfig(
|
|
274
|
+
level=logging.INFO,
|
|
275
|
+
format='[%(asctime)s][%(levelname)s] %(message)s',
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
if not args.http_log:
|
|
279
|
+
logging.getLogger('werkzeug').setLevel(logging.ERROR)
|
|
280
|
+
logging.getLogger('ssdp').setLevel(logging.ERROR)
|
|
281
|
+
|
|
282
|
+
if args.verbose:
|
|
283
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
284
|
+
|
|
251
285
|
ssdp = SSDPServer()
|
|
252
286
|
ssdp.start()
|
|
253
|
-
|
|
287
|
+
|
|
288
|
+
friendly_name = args.name
|
|
289
|
+
app.config['FRIENDLY_NAME'] = friendly_name
|
|
290
|
+
logging.info(f'Starting DLNA Receiver: {friendly_name}')
|
|
291
|
+
app.run(host="0.0.0.0", port=5000, debug=False)
|
|
254
292
|
|
|
255
293
|
|
|
256
294
|
if __name__ == "__main__":
|
tiny-dlna-0.0.1/README.md
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|