]> git.ekhem.eu.org Git - ydlpd.git/commitdiff
Switch from a python server to nginx + fastcgi.
authorJakub Czajka <jakub@ekhem.eu.org>
Thu, 23 Nov 2023 23:25:18 +0000 (00:25 +0100)
committerJakub Czajka <jakub@ekhem.eu.org>
Mon, 1 Jan 2024 20:45:00 +0000 (21:45 +0100)
README
download.sh [new file with mode: 0755]
requirements.txt [deleted file]
server.py [deleted file]
ydlpd.conf [new file with mode: 0644]
ydlpd.sh [new file with mode: 0755]

diff --git a/README b/README
index 30f3cb5797f606a60f0cfa3db53fb70674c6c37f..99183f82c9f1a689e73b5fa8e3bf2f8df980a7ff 100644 (file)
--- a/README
+++ b/README
@@ -3,17 +3,3 @@ yt-dlp-server
 
 HTTP interface for `yt-dlp`. Exposes a GET / endpoint with a HTML form for
 obtaining download parameters.
-
-Dependencies
-------------
-
-* ffmpeg
-
-Install
--------
-
-```
-python3 -m venv site-packages
-source site-packages/bin/activate
-pip install -r requirements.txt
-```
diff --git a/download.sh b/download.sh
new file mode 100755 (executable)
index 0000000..29c4f9b
--- /dev/null
@@ -0,0 +1,45 @@
+#!/bin/sh
+# Copyright (c) 2023 Jakub Czajka <jakub@ekhem.eu.org>
+# License: GPL-3.0 or later.
+
+FULL_URL=$(echo ${URL} | sed 's/%3A/:/g ; s/%2F/\//g ; s/%3F/\?/g ; s/%3D/=/g')
+
+LINK_ID=$(echo ${FULL_URL} | sed 's/^.*\=//')
+
+# Clear /tmp for download.
+if [ -e "/tmp/${LINK_ID}" ]
+then
+    rm -rf "/tmp/${LINK_ID}"
+fi
+
+case "${FORMAT}" in
+    "video")
+       EXT=".mp4"
+       ARGS="--format mp4"
+    ;;
+    "audio")
+       EXT=".ogg"
+       ARGS="--format 251 --extract-audio --postprocessor-args ar:16000\
+        --remux-video ogg --prefer-ffmpeg"
+    ;;
+esac
+
+echo "HTTP/1.1 200 OK"
+echo "Content-Type: text/html"
+echo "
+<!DOCTYPE html>
+<html>
+<head>
+  <meta http-equiv='Refresh' content='0;URL=/${LINK_ID}${EXT}' />
+  <style>
+    body {
+      white-space: pre-wrap;
+    }
+  </style>
+</head>
+<body>"
+echo "Downloading ${FULL_URL} as ${FORMAT}"
+yt-dlp ${ARGS} --output /tmp/"${LINK_ID}${EXT}" "${FULL_URL}"
+echo "\
+</body>
+</html>"
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644 (file)
index a90ebb6..0000000
+++ /dev/null
@@ -1 +0,0 @@
-yt-dlp==2023.9.24
diff --git a/server.py b/server.py
deleted file mode 100644 (file)
index bfc6b76..0000000
--- a/server.py
+++ /dev/null
@@ -1,169 +0,0 @@
-# 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()
diff --git a/ydlpd.conf b/ydlpd.conf
new file mode 100644 (file)
index 0000000..93ab4a6
--- /dev/null
@@ -0,0 +1,59 @@
+# 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;
+}
diff --git a/ydlpd.sh b/ydlpd.sh
new file mode 100755 (executable)
index 0000000..c2e93cd
--- /dev/null
+++ b/ydlpd.sh
@@ -0,0 +1,53 @@
+#!/bin/sh
+# Copyright (c) 2023 Jakub Czajka <jakub@ekhem.eu.org>
+# License: GPL-3.0 or later.
+
+YOUTUBE_URL_RE="(https:\/\/)?(www\.)?youtu(\.be|be\.com)\/watch\?v=([\w\-_]+)"
+
+echo "HTTP/1.1 200 OK"
+echo "Content-type: text/html"
+echo "
+<!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 action='/download'>
+      <label for='url'>URL:</label>
+      <input type='text' id='url' name='url' pattern='${YOUTUBE_URL_RE}'
+        required />
+
+      <label for='format'>Format:</label>
+      <input list='formats' id='format' name='format'>
+
+      <datalist id='formats'>
+        <option value='video' />
+        <option value='audio' />
+      </datalist>
+
+      <input type='submit'>
+    </form>
+  </div>
+</body>
+</html>"