Commit 498e3731c8cc2a4d9c000c5050c68465560ed39d
Rename to dillo-ytdl
ytdl:// is consistent with mpvcel committed on 4/28/2020, 2:46:30 PM
Parent: 29bace3450fcbaee33956eb93d25774d5485e71b
Files changed
README.md | changed |
ydl.dpi | deleted |
ytdl.dpi | added |
README.md | ||
---|---|---|
@@ -1,20 +1,20 @@ | ||
1 | -# dillo-ydl | |
1 … | +# dillo-ytdl | |
2 | 2 … | |
3 | 3 … | [youtube-dl][] plugin for [Dillo][]. |
4 | 4 … | |
5 | -It lets you visit URLs of the form "ydl:<url>" where `<url>` is the URL or video ID you would pass to `youtube-dl`, and you will get a page showing some info about the video, including a table of formats (as you would get from `youtube-dl -F`) with a link to download each one. | |
5 … | +It lets you visit URLs of the form "ytdl:<url>" where `<url>` is the URL or video ID you would pass to `youtube-dl`, and you will get a page showing some info about the video, including a table of formats (as you would get from `youtube-dl -F`) with a link to download each one. | |
6 | 6 … | |
7 | 7 … | ## Install |
8 | 8 … | |
9 | 9 … | ```sh |
10 | -git clone ssb://%oEzgqM849P03lR3tR7qwbjzw/c+R+v0oIoHyDgEDrbw=.sha256 dillo-ydl | |
11 | -cd dillo-ydl | |
10 … | +git clone ssb://%oEzgqM849P03lR3tR7qwbjzw/c+R+v0oIoHyDgEDrbw=.sha256 dillo-ytdl | |
11 … | +cd dillo-ytdl | |
12 | 12 … | pip install -r requirements.txt # or sudo apt-get install youtube-dl |
13 | -mkdir -p ~/.dillo/dpi/ydl | |
14 | -cp ydl.dpi ~/.dillo/dpi/ydl/ # or ln -rs ydl.dpi ~/.dillo/dpi/ydl/ | |
13 … | +mkdir -p ~/.dillo/dpi/ytdl | |
14 … | +cp ytdl.dpi ~/.dillo/dpi/ytdl/ # or ln -rs ytdl.dpi ~/.dillo/dpi/ytdl/ | |
15 | 15 … | test -f ~/.dillo/dpidrc || cp /etc/dillo/dpidrc ~/.dillo/dpidrc |
16 | -echo 'proto.ydl=ydl/ydl.dpi' >> ~/.dillo/dpidrc | |
16 … | +echo 'proto.ytdl=ytdl/ytdl.dpi' >> ~/.dillo/dpidrc | |
17 | 17 … | dpidc stop |
18 | 18 … | ``` |
19 | 19 … | |
20 | 20 … | [youtube-dl]: https://rg3.github.io/youtube-dl/ |
ydl.dpi | ||
---|---|---|
@@ -1,197 +1,0 @@ | ||
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("[ydl]: 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("[ydl]: 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>ydl</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.path == "" or uri.path == "/": return serveHome(conn, url, uri) | |
140 | - if uri.scheme == "dpi": return serveEcho(conn, url, uri) | |
141 | - serveResource(conn, url, url[4:]) | |
142 | - | |
143 | -def serveResource(conn, origUrl, url): | |
144 | - try: | |
145 | - res = ydl.extract_info(url, download=False) | |
146 | - except: | |
147 | - etype, value, tb = sys.exc_info() | |
148 | - writeHeader(conn, origUrl, "text/plain") | |
149 | - lines = traceback.format_exception(etype, value, tb) | |
150 | - conn.send((''.join(lines)).encode('utf-8')) | |
151 | - conn.shutdown(socket.SHUT_WR) | |
152 | - else: | |
153 | - serveResult(conn, origUrl, res) | |
154 | - | |
155 | -def handleConn(conn, addr): | |
156 | - buf = "" | |
157 | - while True: | |
158 | - data = conn.recv(1024) | |
159 | - if not data: break | |
160 | - buf += data.decode('utf-8') | |
161 | - | |
162 | - while len(buf) > 0: | |
163 | - if buf[0] != '<': | |
164 | - conn.close() | |
165 | - return | |
166 | - | |
167 | - m = authRe.match(buf) | |
168 | - if m: | |
169 | - buf = buf[m.end():] | |
170 | - continue | |
171 | - | |
172 | - m = openRe.match(buf) | |
173 | - if m: | |
174 | - buf = buf[m.end():] | |
175 | - url = m.group(1) | |
176 | - if url == "ydl:/quit": return serveQuit(conn, url) | |
177 | - threading.Thread(target=serveOpen, args=(conn, url)).start() | |
178 | - return | |
179 | - | |
180 | - m = byeRe.match(buf) | |
181 | - if m: | |
182 | - print("[ydl]: bye") | |
183 | - exit() | |
184 | - | |
185 | -stdin = socket.fromfd(sys.stdin.fileno(), socket.AF_INET, socket.SOCK_STREAM) | |
186 | - | |
187 | -ydl = youtube_dl.YoutubeDL({ | |
188 | - 'no_color': True | |
189 | -}) | |
190 | - | |
191 | -while True: | |
192 | - try: | |
193 | - conn, addr = stdin.accept() | |
194 | - except KeyboardInterrupt: | |
195 | - exit() | |
196 | - else: | |
197 | - handleConn(conn, addr) |
ytdl.dpi | |||
---|---|---|---|
@@ -1,0 +1,209 @@ | |||
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) |
Built with git-ssb-web