+++ /dev/null
-# Copyright (c) 2023 Jakub Czajka <jakub@ekhem.eu.org>
-# License: GPL-3.0 or later.
-
-import argparse
-import os
-import re
-import tempfile
-import urllib.parse
-import uuid
-import yt_dlp
-
-from wsgiref.simple_server import make_server
-
-FORMAT_OPTS = {
- 'video': {
- 'mime': 'video/mp4',
- 'extension': 'mp4',
- 'ydl_opts': {
- 'format': 'mp4',
- }
- },
- 'audio': {
- 'mime': 'audio/ogg',
- 'extension': 'ogg',
- 'ydl_opts': {
- 'format': '251',
- 'postprocessors': [{
- 'key': 'FFmpegExtractAudio',
- }],
- 'postprocessor_args': [
- '-ar', '16000'
- ],
- 'prefer_ffmpeg': True
- }
- }
-}
-
-VIDEO_PARAMETERS_FORM = '''
-<!DOCTYPE html>
-<html>
-<head>
- <title>YT Download</title>
- <style>
- body {{
- border: ridge 5px;
- max-width: max-content;
- }}
- .container {{
- background-color: PowderBlue;
- padding: 7px;
- }}
- form {{
- display: grid;
- grid-gap: 5px;
- grid-template-columns: 1fr 4fr;
- }}
- #format {{
- justify-self: end;
- width: 50%;
- }}
- </style>
-</head>
-<body>
- <div class="container">
- <form>
- <label for="url">URL:</label>
- <input type="text" id="url" name="url">
-
- <label for="format">Format:</label>
- <input list="formats" id="format" name="format">
-
- <datalist id="formats">
- {0}
- </datalist>
-
- <input type="submit">
- </form>
- </div>
-</body>
-</html>
-'''
-
-YOUTUBE_URL_RE = '(https:\/\/)?(www\.)?youtu(\.be|be\.com)\/watch\?v=([\w\-_]+)'
-
-def build_form_for_video_parameters():
- formats_as_html = ['<option value="{}">'.format(f) for f in FORMAT_OPTS]
- return VIDEO_PARAMETERS_FORM.format(''.join(formats_as_html))
-
-def parse(query_string):
- parameters = urllib.parse.parse_qs(query_string)
- if not 'url' in parameters or not 'format' in parameters:
- return None, None
- return parameters['url'][0], parameters['format'][0]
-
-def set_temporary_output_file(ydl_opts, extension):
- without_ext = tempfile.gettempdir() + '/' + str(uuid.uuid4())
- output_file = without_ext + '.' + extension
- if 'postprocessor_args' in ydl_opts:
- ydl_opts['outtmpl'] = { 'default': without_ext }
- ydl_opts['postprocessor_args'].append(output_file)
- else:
- ydl_opts['outtmpl'] = { 'default': output_file }
- return output_file
-
-def wrap_file_for_serving(environ, file_path):
- UPLOAD_BLOCK_SIZE = 1024
- f = open(file_path, 'rb')
- if 'wsgi.file_wrapper' in environ:
- return environ['wsgi.file_wrapper'](f, UPLOAD_BLOCK_SIZE)
- return iter(lambda: f.read(UPLOAD_BLOCK_SIZE), '')
-
-def handle_request(environ, start_response):
- if not environ['QUERY_STRING']:
- start_response('200 OK', [('Content-type', 'text/html')])
- return [str.encode(build_form_for_video_parameters())]
-
- video_url, output_format = parse(environ['QUERY_STRING'])
- if not video_url:
- print('Error: One or more empty parameters.')
- start_response('200 OK', [('Content-type', 'text/plain')])
- return [b'Error: One or more empty parameters.']
-
- if not re.fullmatch(YOUTUBE_URL_RE, video_url):
- print(f'{video_url} is not a valid YouTube URL.')
- start_response('200 OK', [('Content-type', 'text/plain')])
- return [str.encode(f'\'{video_url}\' is not a valid YouTube URL.')]
-
- if not output_format in FORMAT_OPTS:
- print(f'{output_format} is not a valid output format.')
- start_response('200 OK', [('Content-type', 'text/plain')])
- return [str.encode(f'\'{output_format}\' is not a valid output format.')]
-
- ydl_opts = FORMAT_OPTS[output_format]['ydl_opts']
- tmp_file = set_temporary_output_file(ydl_opts,
- FORMAT_OPTS[output_format]['extension'])
-
- print(f'Downloading {video_url}...')
- with yt_dlp.YoutubeDL(ydl_opts) as ydl:
- error_code = ydl.download([video_url])
- if error_code:
- print(f'Download failed with code {error_code}')
- start_response('200 OK', [('Content-type', 'text/plain')])
- return [str.encode(f'Download failed with code {error_code}')]
- print('Done')
-
- print(f'Sending {video_url}...')
- start_response('200 OK', [
- ('Content-type', FORMAT_OPTS[output_format]['mime']),
- ('Content-Length', str(os.path.getsize(tmp_file))),
- ('Content-Disposition', f'attachment; filename="{tmp_file}"')])
- return wrap_file_for_serving(environ, tmp_file)
-
-if __name__ == '__main__':
- parser = argparse.ArgumentParser(prog='yt-dlp-server', description='HTTP '
- 'server for downloading from YouTube. Exposes a GET / endpoint with a '
- 'HTML form for obtaining download parameters.')
- parser.add_argument('-a', dest='host', default='localhost',
- help='default=localhost')
- parser.add_argument('-p', dest='port', type=int, required=True)
-
- args = parser.parse_args()
-
- with make_server(args.host, args.port, handle_request) as httpd:
- print('Serving on {0}:{1}...'.format(args.host, args.port))
- try:
- httpd.serve_forever()
- except KeyboardInterrupt:
- print('Shutting down.')
- httpd.server_close()
--- /dev/null
+# Copyright (c) 2023 Jakub Czajka <jakub@ekhem.eu.org>
+# License: GPL-3.0 or later.
+
+server {
+ server_name yt.${private_domain};
+
+ listen [::]:443 ssl http2;
+ listen 443 ssl http2;
+
+ ssl_certificate ${private_ssl_cert_dir}/fullchain.pem;
+ ssl_certificate_key ${private_ssl_cert_dir}/privkey.pem;
+
+ ssl_client_certificate ${ca_dir}/ca.pem;
+ ssl_verify_client on;
+
+ root ${prod_dir}/ydlpd;
+
+ location ~ /download(.*) {
+ include fastcgi_params;
+ fastcgi_pass unix:/var/run/fcgiwrap.socket;
+
+ # 30min
+ fastcgi_read_timeout 900;
+
+ fastcgi_buffering off;
+ fastcgi_param NO_BUFFERING "";
+
+ fastcgi_param URL ${dollar}arg_url;
+ fastcgi_param FORMAT ${dollar}arg_format;
+ fastcgi_param SCRIPT_FILENAME ${dollar}document_root/download.sh;
+ }
+
+ location ~ ^/([\w\-_]+\.(ogg|mp4))$ {
+ add_header Content-Disposition "attachment; filename=/tmp/${dollar}1";
+
+ alias /tmp/;
+ try_files ${dollar}1 =404;
+ }
+
+ location = / {
+ include fastcgi_params;
+ fastcgi_pass unix:/var/run/fcgiwrap.socket;
+
+ fastcgi_param SCRIPT_FILENAME ${dollar}document_root/ydlpd.sh;
+ }
+}
+
+server {
+ server_name yt.${private_domain};
+
+ listen [::]:80;
+ listen 80;
+
+ if (${dollar}host = yt.${private_domain}) {
+ return 301 https://${dollar}host${dollar}request_uri;
+ }
+
+ return 404;
+}