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.
@@ -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,13 @@
1
+ # Tiny DLNA Render
2
+
3
+ Just to support subtitles.
4
+
5
+ ```
6
+ $ pip install tiny-dlna
7
+ $ tiny-render
8
+ ```
9
+
10
+ ## Related projects
11
+
12
+ - https://github.com/xfangfang/Macast
13
+ - https://github.com/gabrielmagno/nano-dlna
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ )
@@ -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,10 @@
1
+ README.md
2
+ setup.py
3
+ ssdp.py
4
+ tiny_render.py
5
+ tiny_dlna.egg-info/PKG-INFO
6
+ tiny_dlna.egg-info/SOURCES.txt
7
+ tiny_dlna.egg-info/dependency_links.txt
8
+ tiny_dlna.egg-info/entry_points.txt
9
+ tiny_dlna.egg-info/requires.txt
10
+ tiny_dlna.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ tiny-render = tiny_render:main
@@ -0,0 +1 @@
1
+ flask>=3.0.0
@@ -0,0 +1,2 @@
1
+ ssdp
2
+ tiny_render
@@ -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()