Files: 498e3731c8cc2a4d9c000c5050c68465560ed39d / ytdl.dpi
6346 bytesRaw
1 | #!/usr/bin/env python3 |
2 | |
3 | # © 2018 cel @f/6sQ6d2CMxRUhLpspgGIulDxDCwYD7DzFzPNr7u5AU=.ed25519 |
4 | # Copying and distribution of this file, with or without modification, |
5 | # are permitted in any medium without royalty provided the copyright |
6 | # notice and this notice are preserved. This file is offered as-is, |
7 | # without any warranty. |
8 | |
9 | import socket |
10 | import sys |
11 | import re |
12 | import threading |
13 | import youtube_dl |
14 | import urllib.parse |
15 | import html |
16 | import traceback |
17 | import json |
18 | |
19 | authRe = re.compile("<cmd='auth' msg='([^']*)' '>") |
20 | openRe = re.compile("<cmd='open_url' url='(.*?)' '>") |
21 | byeRe = re.compile("<cmd='DpiBye' '>") |
22 | |
23 | print("[ytdl]: init", file=sys.stderr) |
24 | |
25 | def writeHeader(conn, url, contentType): |
26 | conn.send(("<cmd='start_send_page' url='" + url + "' '>").encode('utf-8')) |
27 | conn.send(('Content-type: ' + contentType + '\r\n\r\n').encode('utf-8')) |
28 | |
29 | def serveQuit(conn, url): |
30 | writeHeader(conn, url, "text/plain") |
31 | conn.send(b"quit") |
32 | conn.shutdown(socket.SHUT_WR) |
33 | print("[ytdl]: quit") |
34 | exit() |
35 | |
36 | def serveHome(conn, url, uri): |
37 | writeHeader(conn, url, "text/html") |
38 | conn.send(("<!doctype html>" + |
39 | "<html>" + |
40 | "<head>" + |
41 | "<title>ytdl</title></head>" + |
42 | "</head><body>\n" + |
43 | "<div>youtube_dl " + youtube_dl.version.__version__ + "</div>" + |
44 | "<div><a href=/quit>quit</a></div>" + |
45 | "</body></html>").encode('utf-8')) |
46 | conn.shutdown(socket.SHUT_WR) |
47 | |
48 | def serveEcho(conn, url, uri): |
49 | writeHeader(conn, url, "text/plain") |
50 | conn.send(str(uri).encode('utf-8')) |
51 | conn.shutdown(socket.SHUT_WR) |
52 | |
53 | def renderThumbnail(thumb): |
54 | url = thumb.get('url') |
55 | href = re.sub(r"default", "hqdefault", url) |
56 | return ("<a href='" + html.escape(href) + "'>" + |
57 | "<img src='" + html.escape(url) + "'" + |
58 | " alt='" + thumb.get('id', '') + "'></a>") |
59 | |
60 | def formatSize(size): |
61 | size = int(size) |
62 | if size < 1024: return size + ' B' |
63 | size /= 1024 |
64 | if size < 1024: return '%.2f KB' % size |
65 | size /= 1024 |
66 | if size < 1024: return '%.2f MB' % size |
67 | return '%.2f GB' % size |
68 | |
69 | def renderFormat(form): |
70 | id = form.get('format_id') |
71 | ext = form.get('ext') |
72 | name = form.get('format') |
73 | url = form.get('url') |
74 | resolution = form.get('resolution') |
75 | vcodec = form.get('vcodec') |
76 | acodec = form.get('acodec') |
77 | filesize = form.get('filesize') |
78 | return ("<tr><td>" + |
79 | ('<a href="' + html.escape(url) + '">' if url else "") + |
80 | html.escape(id) + |
81 | ('</a>' if url else "") + |
82 | "</td>" + |
83 | "<td>" + (html.escape(str(ext)) if ext else "") + "</td>" + |
84 | "<td align=right>" + (str(formatSize(filesize)) if filesize else "") + "</td>" + |
85 | "<td>" + (html.escape(str(name)) if name else "") + "</td>" + |
86 | "<td>" + (html.escape(str(resolution)) if resolution else "") + "</td>" + |
87 | "<td>" + (html.escape(str(acodec)) if acodec else "") + "</td>" + |
88 | "<td>" + (html.escape(str(vcodec)) if vcodec else "") + "</td>" + |
89 | "<!--<td><pre>" + json.dumps(form) + "</pre></td>-->") |
90 | |
91 | def renderEntry(entry): |
92 | thumbnails = entry.get('thumbnails', []) |
93 | formats = entry.get('formats', []) |
94 | uploader = entry.get('uploader') |
95 | description = entry.get('description') |
96 | duration = entry.get('duration') |
97 | view_count = entry.get('view_count') |
98 | |
99 | if formats: del entry['formats'] |
100 | tags = entry.get('tags') |
101 | return ( |
102 | ("<div>" + |
103 | " ".join([renderThumbnail(thumb) + "\n" for thumb in thumbnails]) + |
104 | "</div>\n" if len(thumbnails) > 0 else "") + |
105 | ("<div>" + html.escape(uploader) + "</div>" if uploader else "") + |
106 | ("<p>" + html.escape(description) + "</p>" if description else "") + |
107 | ("<p>Duration: " + html.escape(str(duration)) + "</p>" if duration else "") + |
108 | ("<p>View count: " + html.escape(str(view_count)) + "</p>" if view_count else "") + |
109 | ("<p>Tags: " + ", ".join([html.escape(str(tag)) for tag in tags]) + "</p>" if tags else "") + |
110 | ("<table>" + |
111 | "<thead>" + |
112 | "<tr><th>code</th><th>ext</th><th>size</th><th>format</th><th>resolution</th><th>audio</th><th>video</th></tr>" + |
113 | "</thead>\n<tbody>" + |
114 | "\n".join([renderFormat(form) + "\n" for form in formats]) + |
115 | "</tbody></table>\n" if len(thumbnails) > 0 else "")) |
116 | |
117 | def serveResult(conn, origUrl, res): |
118 | writeHeader(conn, origUrl, "text/html") |
119 | title = res.get('title') |
120 | webpage_url = res.get('webpage_url') |
121 | entries = res.get('entries', []) if res.get('_type', None) == 'playlist' else [res] |
122 | conn.send(("<!doctype html>" + |
123 | "<html>" + |
124 | "<head>" + |
125 | "<title>" + html.escape(title) + "</title></head>" + |
126 | "</head><body>\n" + |
127 | "<h1>" + |
128 | ("<a href='" + html.escape(webpage_url) + "'>" if webpage_url else "") + |
129 | html.escape(title) + |
130 | ("</a>" if webpage_url else "") + |
131 | "</h1>\n" + |
132 | "\n".join([renderEntry(entry) for entry in entries]) + |
133 | "<!--<pre>" + json.dumps(res, indent=True) + "</pre>-->\n" + |
134 | "</body></html>").encode('utf-8')) |
135 | conn.shutdown(socket.SHUT_WR) |
136 | |
137 | def serveOpen(conn, url): |
138 | uri = urllib.parse.urlparse(url) |
139 | if uri.scheme == "dpi": return serveEcho(conn, url, uri) |
140 | if uri.netloc == "" and (uri.path == "" or uri.path == "/"): |
141 | return serveHome(conn, url, uri) |
142 | if uri.scheme == "ytdl" or uri.scheme == "ydl": |
143 | if '.' in uri.netloc: |
144 | return serveResource(conn, url, urllib.parse.urlunparse(( |
145 | 'https', |
146 | uri.netloc, |
147 | uri.path, |
148 | uri.params, |
149 | uri.query, |
150 | uri.fragment |
151 | ))) |
152 | return serveResource(conn, url, re.sub('yt?dl:(//)?', '', url)) |
153 | return serveEcho(conn, url, uri) |
154 | |
155 | def serveResource(conn, origUrl, url): |
156 | try: |
157 | res = ytdl.extract_info(url, download=False) |
158 | except: |
159 | etype, value, tb = sys.exc_info() |
160 | writeHeader(conn, origUrl, "text/plain") |
161 | lines = traceback.format_exception(etype, value, tb) |
162 | conn.send((''.join(lines)).encode('utf-8')) |
163 | conn.shutdown(socket.SHUT_WR) |
164 | else: |
165 | serveResult(conn, origUrl, res) |
166 | |
167 | def handleConn(conn, addr): |
168 | buf = "" |
169 | while True: |
170 | data = conn.recv(1024) |
171 | if not data: break |
172 | buf += data.decode('utf-8') |
173 | |
174 | while len(buf) > 0: |
175 | if buf[0] != '<': |
176 | conn.close() |
177 | return |
178 | |
179 | m = authRe.match(buf) |
180 | if m: |
181 | buf = buf[m.end():] |
182 | continue |
183 | |
184 | m = openRe.match(buf) |
185 | if m: |
186 | buf = buf[m.end():] |
187 | url = m.group(1) |
188 | if url == "ytdl:/quit": return serveQuit(conn, url) |
189 | threading.Thread(target=serveOpen, args=(conn, url)).start() |
190 | return |
191 | |
192 | m = byeRe.match(buf) |
193 | if m: |
194 | print("[ytdl]: bye") |
195 | exit() |
196 | |
197 | stdin = socket.fromfd(sys.stdin.fileno(), socket.AF_INET, socket.SOCK_STREAM) |
198 | |
199 | ytdl = youtube_dl.YoutubeDL({ |
200 | 'no_color': True |
201 | }) |
202 | |
203 | while True: |
204 | try: |
205 | conn, addr = stdin.accept() |
206 | except KeyboardInterrupt: |
207 | exit() |
208 | else: |
209 | handleConn(conn, addr) |
210 |
Built with git-ssb-web