tiny-dlna 0.0.1__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/PKG-INFO +28 -0
- tiny-dlna-0.0.1/README.md +13 -0
- tiny-dlna-0.0.1/setup.cfg +4 -0
- tiny-dlna-0.0.1/setup.py +42 -0
- tiny-dlna-0.0.1/ssdp.py +36 -0
- tiny-dlna-0.0.1/tiny_dlna.egg-info/PKG-INFO +28 -0
- tiny-dlna-0.0.1/tiny_dlna.egg-info/SOURCES.txt +10 -0
- tiny-dlna-0.0.1/tiny_dlna.egg-info/dependency_links.txt +1 -0
- tiny-dlna-0.0.1/tiny_dlna.egg-info/entry_points.txt +2 -0
- tiny-dlna-0.0.1/tiny_dlna.egg-info/requires.txt +1 -0
- tiny-dlna-0.0.1/tiny_dlna.egg-info/top_level.txt +2 -0
- tiny-dlna-0.0.1/tiny_render.py +257 -0
tiny-dlna-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: tiny-dlna
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: a tiny DLNA receiver
|
|
5
|
+
Home-page: https://github.com/mitnk/tiny-dlna
|
|
6
|
+
Author: mitnk
|
|
7
|
+
License: MIT
|
|
8
|
+
Keywords: dlna
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
Home Page: https://github.com/mitnk/tiny-dlna
|
|
12
|
+
|
|
13
|
+
A tiny DLNA receiver.
|
|
14
|
+
|
|
15
|
+
Install
|
|
16
|
+
-------
|
|
17
|
+
|
|
18
|
+
::
|
|
19
|
+
|
|
20
|
+
$ pip install tiny-dlna
|
|
21
|
+
|
|
22
|
+
Usages
|
|
23
|
+
------
|
|
24
|
+
|
|
25
|
+
::
|
|
26
|
+
|
|
27
|
+
$ tiny-render
|
|
28
|
+
|
tiny-dlna-0.0.1/setup.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from setuptools import setup
|
|
2
|
+
|
|
3
|
+
DESC = """
|
|
4
|
+
Home Page: https://github.com/mitnk/tiny-dlna
|
|
5
|
+
|
|
6
|
+
A tiny DLNA receiver.
|
|
7
|
+
|
|
8
|
+
Install
|
|
9
|
+
-------
|
|
10
|
+
|
|
11
|
+
::
|
|
12
|
+
|
|
13
|
+
$ pip install tiny-dlna
|
|
14
|
+
|
|
15
|
+
Usages
|
|
16
|
+
------
|
|
17
|
+
|
|
18
|
+
::
|
|
19
|
+
|
|
20
|
+
$ tiny-render
|
|
21
|
+
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
setup(
|
|
25
|
+
name='tiny-dlna',
|
|
26
|
+
version='0.0.1',
|
|
27
|
+
description='a tiny DLNA receiver',
|
|
28
|
+
long_description=DESC,
|
|
29
|
+
url='https://github.com/mitnk/tiny-dlna',
|
|
30
|
+
author='mitnk',
|
|
31
|
+
license='MIT',
|
|
32
|
+
keywords='dlna',
|
|
33
|
+
py_modules=["tiny_render", "ssdp"],
|
|
34
|
+
entry_points={
|
|
35
|
+
'console_scripts': [
|
|
36
|
+
'tiny-render=tiny_render:main',
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
install_requires=[
|
|
40
|
+
'flask>=3.0.0',
|
|
41
|
+
],
|
|
42
|
+
)
|
tiny-dlna-0.0.1/ssdp.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import socket
|
|
2
|
+
|
|
3
|
+
SSDP_MULTICAST = '239.255.255.250'
|
|
4
|
+
SSDP_PORT = 1900
|
|
5
|
+
LOCATION = "http://192.168.1.228:5000/description.xml"
|
|
6
|
+
USN = 'uuid:dlna-tiny-render-t001::upnp:rootdevice'
|
|
7
|
+
|
|
8
|
+
ssdp_response = (
|
|
9
|
+
'HTTP/1.1 200 OK\r\n'
|
|
10
|
+
'CACHE-CONTROL: max-age=1800\r\n'
|
|
11
|
+
'EXT:\r\n'
|
|
12
|
+
f'LOCATION: {LOCATION}\r\n'
|
|
13
|
+
'HOSTNAME: 192.168.1.228\r\n'
|
|
14
|
+
'SERVER: Custom/1.0 UPnP/1.0 DLNADOC/1.50\r\n'
|
|
15
|
+
f'ST: urn:schemas-upnp-org:service:AVTransport:1\r\n'
|
|
16
|
+
f'USN: {USN}\r\n'
|
|
17
|
+
'\r\n'
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
def ssdp_listener():
|
|
21
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
|
|
22
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
23
|
+
sock.bind(('0.0.0.0', SSDP_PORT))
|
|
24
|
+
|
|
25
|
+
# Join the SSDP multicast group
|
|
26
|
+
mreq = socket.inet_aton(SSDP_MULTICAST) + socket.inet_aton('0.0.0.0')
|
|
27
|
+
sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
|
|
28
|
+
|
|
29
|
+
while True:
|
|
30
|
+
data, addr = sock.recvfrom(1024)
|
|
31
|
+
if b'M-SEARCH' in data and b'ssdp:discover' in data:
|
|
32
|
+
print(f'Received M-SEARCH from {addr}, sending response...')
|
|
33
|
+
sock.sendto(ssdp_response.encode('utf-8'), addr)
|
|
34
|
+
|
|
35
|
+
if __name__ == '__main__':
|
|
36
|
+
ssdp_listener()
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: tiny-dlna
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: a tiny DLNA receiver
|
|
5
|
+
Home-page: https://github.com/mitnk/tiny-dlna
|
|
6
|
+
Author: mitnk
|
|
7
|
+
License: MIT
|
|
8
|
+
Keywords: dlna
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
Home Page: https://github.com/mitnk/tiny-dlna
|
|
12
|
+
|
|
13
|
+
A tiny DLNA receiver.
|
|
14
|
+
|
|
15
|
+
Install
|
|
16
|
+
-------
|
|
17
|
+
|
|
18
|
+
::
|
|
19
|
+
|
|
20
|
+
$ pip install tiny-dlna
|
|
21
|
+
|
|
22
|
+
Usages
|
|
23
|
+
------
|
|
24
|
+
|
|
25
|
+
::
|
|
26
|
+
|
|
27
|
+
$ tiny-render
|
|
28
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
flask>=3.0.0
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import html
|
|
2
|
+
import re
|
|
3
|
+
import subprocess
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
import xml.etree.ElementTree as ET
|
|
7
|
+
|
|
8
|
+
from flask import Flask, request, Response
|
|
9
|
+
from ssdp import ssdp_listener
|
|
10
|
+
|
|
11
|
+
app = Flask(__name__)
|
|
12
|
+
|
|
13
|
+
XML_AVSET_DONE = """
|
|
14
|
+
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
|
|
15
|
+
<s:Body>
|
|
16
|
+
<u:SetAVTransportURIResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"/>
|
|
17
|
+
</s:Body>
|
|
18
|
+
</s:Envelope>
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
XML_PLAY_DONE = """
|
|
22
|
+
<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
|
|
23
|
+
<s:Body>
|
|
24
|
+
<u:PlayResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"/>
|
|
25
|
+
</s:Body>
|
|
26
|
+
</s:Envelope>
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
XML_POSINFO = """
|
|
30
|
+
<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
|
|
31
|
+
<s:Body>
|
|
32
|
+
<u:GetPositionInfoResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
|
|
33
|
+
<Track>0</Track>
|
|
34
|
+
<TrackDuration>00:00:00</TrackDuration>
|
|
35
|
+
<RelTime>{0}</RelTime>
|
|
36
|
+
<AbsTime>{0}</AbsTime>
|
|
37
|
+
<RelCount>2147483647</RelCount>
|
|
38
|
+
<AbsCount>2147483647</AbsCount>
|
|
39
|
+
</u:GetPositionInfoResponse>
|
|
40
|
+
</s:Body>
|
|
41
|
+
</s:Envelope>
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
XML_TRANSINFO = """
|
|
45
|
+
<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
|
|
46
|
+
<s:Body>
|
|
47
|
+
<u:GetTransportInfoResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
|
|
48
|
+
<CurrentTransportState>PLAYING</CurrentTransportState>
|
|
49
|
+
<CurrentTransportStatus>OK</CurrentTransportStatus>
|
|
50
|
+
<CurrentSpeed>1</CurrentSpeed>
|
|
51
|
+
</u:GetTransportInfoResponse>
|
|
52
|
+
</s:Body>
|
|
53
|
+
</s:Envelope>
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
XML_STOP_DONE = """
|
|
57
|
+
<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
|
|
58
|
+
<s:Body>
|
|
59
|
+
<u:StopResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"/>
|
|
60
|
+
</s:Body>
|
|
61
|
+
</s:Envelope>
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
class MPVRenderer:
|
|
65
|
+
def __init__(self):
|
|
66
|
+
self.process = None
|
|
67
|
+
|
|
68
|
+
def play_media(self, url, title=None, srt=None):
|
|
69
|
+
self.stop_media() # Stop any existing media
|
|
70
|
+
cmd = ['mpv', url]
|
|
71
|
+
if title:
|
|
72
|
+
cmd.append('--title={}'.format(title))
|
|
73
|
+
if srt:
|
|
74
|
+
cmd.append('--sub-file={}'.format(srt))
|
|
75
|
+
self.process = subprocess.Popen(cmd)
|
|
76
|
+
|
|
77
|
+
def stop_media(self):
|
|
78
|
+
if self.process:
|
|
79
|
+
self.process.terminate()
|
|
80
|
+
self.process = None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
renderer = MPVRenderer()
|
|
84
|
+
|
|
85
|
+
_DATA = {
|
|
86
|
+
'CURRENT_URI': '',
|
|
87
|
+
'CURRENT_SRT': '',
|
|
88
|
+
'VIDEO_TITLE': '',
|
|
89
|
+
'STARTED_AT': 0,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@app.route('/description.xml')
|
|
93
|
+
def description():
|
|
94
|
+
xml = """<?xml version="1.0"?>
|
|
95
|
+
<root xmlns="urn:schemas-upnp-org:device-1-0" xmlns:dlna="urn:schemas-dlna-org:device-1-0">
|
|
96
|
+
<specVersion>
|
|
97
|
+
<major>1</major>
|
|
98
|
+
<minor>0</minor>
|
|
99
|
+
</specVersion>
|
|
100
|
+
<device>
|
|
101
|
+
<deviceType>urn:schemas-upnp-org:device:MediaRenderer:1</deviceType>
|
|
102
|
+
<friendlyName>Tiny Render</friendlyName>
|
|
103
|
+
<manufacturer>mitnk</manufacturer>
|
|
104
|
+
<modelName>T001</modelName>
|
|
105
|
+
<UDN>uuid:dlna-tiny-render-t001</UDN>
|
|
106
|
+
<dlna:X_DLNADOC xmlns:dlna="urn:schemas-dlna-org:device-1-0">DMR-1.50</dlna:X_DLNADOC>
|
|
107
|
+
<serviceList>
|
|
108
|
+
<service>
|
|
109
|
+
<serviceType>urn:schemas-upnp-org:service:AVTransport:1</serviceType>
|
|
110
|
+
<serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
|
|
111
|
+
<SCPDURL>AVTransport/82d8-eb72-b097/scpd.xml</SCPDURL>
|
|
112
|
+
<controlURL>AVTransport/82d8-eb72-b097/control</controlURL>
|
|
113
|
+
<eventSubURL>AVTransport/82d8-eb72-b097/event</eventSubURL>
|
|
114
|
+
</service>
|
|
115
|
+
<service>
|
|
116
|
+
<serviceType>urn:schemas-upnp-org:service:ConnectionManager:1</serviceType>
|
|
117
|
+
<serviceId>urn:upnp-org:serviceId:ConnectionManager</serviceId>
|
|
118
|
+
<SCPDURL>ConnectionManager/82d8-eb72-b097/scpd.xml</SCPDURL>
|
|
119
|
+
<controlURL>ConnectionManager/82d8-eb72-b097/control</controlURL>
|
|
120
|
+
<eventSubURL>ConnectionManager/82d8-eb72-b097/event</eventSubURL>
|
|
121
|
+
</service>
|
|
122
|
+
<service>
|
|
123
|
+
<serviceType>urn:schemas-upnp-org:service:RenderingControl:1</serviceType>
|
|
124
|
+
<serviceId>urn:upnp-org:serviceId:RenderingControl</serviceId>
|
|
125
|
+
<SCPDURL>RenderingControl/82d8-eb72-b097/scpd.xml</SCPDURL>
|
|
126
|
+
<controlURL>RenderingControl/82d8-eb72-b097/control</controlURL>
|
|
127
|
+
<eventSubURL>RenderingControl/82d8-eb72-b097/event</eventSubURL>
|
|
128
|
+
</service>
|
|
129
|
+
</serviceList>
|
|
130
|
+
</device>
|
|
131
|
+
</root>"""
|
|
132
|
+
return Response(xml, mimetype="text/xml")
|
|
133
|
+
|
|
134
|
+
def to_track_time(seconds):
|
|
135
|
+
hours = seconds // 3600
|
|
136
|
+
minutes = (seconds % 3600) // 60
|
|
137
|
+
remaining_seconds = seconds % 60
|
|
138
|
+
return f"{hours}:{minutes:02}:{remaining_seconds:02}"
|
|
139
|
+
|
|
140
|
+
def is_stop(request):
|
|
141
|
+
return b'<u:Stop' in request.data
|
|
142
|
+
|
|
143
|
+
def is_play(request):
|
|
144
|
+
return b'<u:Play' in request.data
|
|
145
|
+
|
|
146
|
+
def is_setav(request):
|
|
147
|
+
return b'SetAVTransportURI' in request.data
|
|
148
|
+
|
|
149
|
+
def is_gettrans(request):
|
|
150
|
+
return b'u:GetTransportInfo' in request.data
|
|
151
|
+
|
|
152
|
+
def is_getpos(request):
|
|
153
|
+
return b'u:GetPositionInfo' in request.data
|
|
154
|
+
|
|
155
|
+
def get_title_re(xml_data):
|
|
156
|
+
pattern = re.compile(r'<dc:title>(.*?)</dc:title>', re.DOTALL)
|
|
157
|
+
match = pattern.search(xml_data)
|
|
158
|
+
if match:
|
|
159
|
+
return match.group(1)
|
|
160
|
+
|
|
161
|
+
def get_metadata(request):
|
|
162
|
+
root = ET.fromstring(request.data)
|
|
163
|
+
current_uri = root.find('.//CurrentURI').text
|
|
164
|
+
metadata = root.find('.//CurrentURIMetaData').text
|
|
165
|
+
|
|
166
|
+
current_srt = None
|
|
167
|
+
title = None
|
|
168
|
+
|
|
169
|
+
metadata = html.unescape(metadata)
|
|
170
|
+
try:
|
|
171
|
+
metadata = ET.fromstring(metadata)
|
|
172
|
+
except ET.ParseError:
|
|
173
|
+
# HACK: fall back to `re` to get title only (e.g. Huya)
|
|
174
|
+
print('** got xml.ParseError, fall back to re')
|
|
175
|
+
title = get_title_re(metadata)
|
|
176
|
+
return {'video': current_uri, 'title': title}
|
|
177
|
+
|
|
178
|
+
ns = {'dc': 'http://purl.org/dc/elements/1.1/'}
|
|
179
|
+
obj = metadata.find('.//dc:title', ns)
|
|
180
|
+
if obj:
|
|
181
|
+
title = obj.text
|
|
182
|
+
|
|
183
|
+
for res in metadata.findall('.//{urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/}res'):
|
|
184
|
+
if res.get('type') == 'text/subtitle':
|
|
185
|
+
current_srt = res.text.strip()
|
|
186
|
+
break
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
'video': current_uri,
|
|
190
|
+
'srt': current_srt,
|
|
191
|
+
'title': current_srt,
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@app.route('/AVTransport/82d8-eb72-b097/control', methods=['POST'])
|
|
196
|
+
def control():
|
|
197
|
+
if is_setav(request):
|
|
198
|
+
metadata = get_metadata(request)
|
|
199
|
+
|
|
200
|
+
current_uri = metadata['video']
|
|
201
|
+
current_srt = metadata.get('srt')
|
|
202
|
+
video_title = metadata.get('title')
|
|
203
|
+
|
|
204
|
+
print('++ SetAV:', current_uri)
|
|
205
|
+
_DATA['CURRENT_URI'] = current_uri
|
|
206
|
+
print('++ SRT:', current_srt)
|
|
207
|
+
if current_srt:
|
|
208
|
+
_DATA['CURRENT_SRT'] = current_srt
|
|
209
|
+
if video_title:
|
|
210
|
+
_DATA['VIDEO_TITLE'] = video_title
|
|
211
|
+
return Response(XML_AVSET_DONE, mimetype="text/xml")
|
|
212
|
+
|
|
213
|
+
elif is_play(request):
|
|
214
|
+
_DATA['STARTED_AT'] = time.time()
|
|
215
|
+
url = _DATA['CURRENT_URI']
|
|
216
|
+
srt = _DATA['CURRENT_SRT']
|
|
217
|
+
title = _DATA['VIDEO_TITLE']
|
|
218
|
+
print('++ Playing', url)
|
|
219
|
+
renderer.play_media(url, title, srt)
|
|
220
|
+
return Response(XML_PLAY_DONE, mimetype="text/xml")
|
|
221
|
+
|
|
222
|
+
elif is_getpos(request):
|
|
223
|
+
print('++ GetPositionInfo ++')
|
|
224
|
+
seconds = int(time.time() - _DATA['STARTED_AT'])
|
|
225
|
+
reltime = to_track_time(seconds)
|
|
226
|
+
return Response(XML_POSINFO.format(reltime), mimetype="text/xml")
|
|
227
|
+
|
|
228
|
+
elif is_gettrans(request):
|
|
229
|
+
print('++ GetTransportInfo ++')
|
|
230
|
+
return Response(XML_TRANSINFO, mimetype="text/xml")
|
|
231
|
+
|
|
232
|
+
elif is_stop(request):
|
|
233
|
+
print('\n+++ Stopping +++')
|
|
234
|
+
_DATA['CURRENT_URI'] = ''
|
|
235
|
+
_DATA['CURRENT_SRT'] = ''
|
|
236
|
+
_DATA['VIDEO_TITLE'] = ''
|
|
237
|
+
_DATA['STARTED_AT'] = 0
|
|
238
|
+
renderer.stop_media()
|
|
239
|
+
return Response(XML_STOP_DONE, mimetype="text/xml")
|
|
240
|
+
|
|
241
|
+
print(request.data)
|
|
242
|
+
return Response('Action Not Supported', status=500)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
class SSDPServer(threading.Thread):
|
|
246
|
+
def run(self):
|
|
247
|
+
ssdp_listener()
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def main():
|
|
251
|
+
ssdp = SSDPServer()
|
|
252
|
+
ssdp.start()
|
|
253
|
+
app.run(host="0.0.0.0", port=5000)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
if __name__ == "__main__":
|
|
257
|
+
main()
|