diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c2b6274 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,75 @@ +# We need gcc, build-essential and git to install our requirements but we +# don't need them when run the application so we can selectively copy artifacts +# from this stage (compile-image) to second one (runtime-image), leaving +# behind everything we don't need in the final build. +FROM python:3.9-slim AS compile-image + +# We don't want apt-get to interact with us, +# and we want the default answers to be used for all questions. +# Is it also completely silent and unobtrusive. +ARG DEBIAN_FRONTEND=noninteractive + +# Update packages and install needed packages to build our requirements. +RUN apt-get update && \ + apt-get install -y --no-install-recommends build-essential gcc git + +# Create new virtual environment in /opt/venv and change to it. +ENV VIRTUAL_ENV=/opt/venv +RUN python3 -m venv $VIRTUAL_ENV +ENV PATH="$VIRTUAL_ENV/bin:$PATH" + +# Copy and install requirements. +COPY requirements.txt . +RUN pip install --disable-pip-version-check --no-cache-dir --requirement requirements.txt + +# Change to our second stage. This is the one that will run the application. +FROM python:3.9-slim AS runtime-image +RUN apt-get update && \ + apt-get install -y --no-install-recommends ffmpeg +# Copy Python dependencies from our build image. +COPY --from=compile-image /opt/venv /opt/venv + + + +# Create user so we don't run as root. +RUN useradd --create-home botuser + +# Create directories we need +RUN mkdir -p /home/botuser/Uploads && mkdir -p /home/botuser/templates + +# Change ownership of directories +RUN chown -R botuser:botuser /home/botuser && chmod -R 755 /home/botuser + +# Change user +USER botuser + +# Change directory to where we will run the application. +WORKDIR /home/botuser + +# Copy our Python application to our home directory. +COPY main.py ./ +COPY Uploads/ ./Uploads +COPY templates/ ./templates + +# Don't generate byte code (.pyc-files). +# These are only needed if we run the python-files several times. +# Docker doesn't keep the data between runs so this adds nothing. +ENV PYTHONDONTWRITEBYTECODE 1 + +# Force the stdout and stderr streams to be unbuffered. +# Will allow log messages to be immediately dumped instead of being buffered. +# This is useful when the application crashes before writing messages stuck in the buffer. +# Has a minor performance loss. We don't have many log messages so probably makes zero difference. +ENV PYTHONUNBUFFERED 1 + +# Use our virtual environment that we created in the other stage. +ENV PATH="/opt/venv/bin:$PATH" + +# Make the website accessible outside localhost +ENV FLASK_RUN_HOST=0.0.0.0 + +# Expose the web port +EXPOSE 5000 + +# Run bot. +CMD [ "gunicorn", "--workers=2", "--threads=4", "--log-file=-", "--bind=0.0.0.0:5000", "main:app"] diff --git a/Uploads/.gitkeep b/Uploads/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d2e7796 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,12 @@ +version: "3" +services: + discord-nice-embed-maker-for-my-yoy: + image: thelovinator/discord-nice-embed-maker-for-my-yoy + container_name: discord-nice-embed-maker-for-my-yoy + ports: + - "5000:5000" + volumes: + - uploads:/home/botuser/static/tweets + restart: unless-stopped +volumes: + uploads: diff --git a/main.py b/main.py new file mode 100644 index 0000000..1914728 --- /dev/null +++ b/main.py @@ -0,0 +1,125 @@ +import json +import shlex +import subprocess +from datetime import datetime + +from flask import ( + Flask, + flash, + redirect, + render_template, + request, + send_from_directory, + url_for, +) +from werkzeug.utils import secure_filename + +app = Flask(__name__) +app.config["UPLOAD_FOLDER"] = "Uploads" + + +# function to find the resolution of the input video file +def find_video_resolution(path_to_video): + cmd = "ffprobe -v quiet -print_format json -show_streams " + args = shlex.split(cmd) + args.append(path_to_video) + # run the ffprobe process, decode stdout into utf-8 & convert to JSON + ffprobe_output = subprocess.check_output(args).decode("utf-8") + ffprobe_output = json.loads(ffprobe_output) + + # find height and width + height = ffprobe_output["streams"][0]["height"] + width = ffprobe_output["streams"][0]["width"] + + return height, width + + +def generate_html( + video_url, video_width, video_height, video_screenshot, video_filename +): + video_html = f""" + + + +
+ + + + + + + + + """ + + video_filename += ".html" + html_url = f"https://killyoy.lovinator.space/{video_filename}" + + with open(f"Uploads/{video_filename}", "w") as file: + file.write(video_html) + return html_url + + +def get_first_frame(path_video, file_filename): + cmd = f"ffmpeg -y -i {path_video} -vframes 1 Uploads/{file_filename}.jpg" + args = shlex.split(cmd) + + subprocess.check_output(args).decode("utf-8") + + return f"https://killyoy.lovinator.space/{file_filename}.jpg" + + +@app.route("/") +def index(): + return render_template("index.html") + + +@app.route("/", methods=["GET", "POST"]) +def upload_file(): + if request.method == "POST": + # check if the post request has the file part + if "file" not in request.files: + flash("No file part") + return redirect(request.url) + file = request.files["file"] + # if user does not select file, browser also + # submit an empty part without filename + if file.filename == "": + flash("No selected file") + return redirect(request.url) + if file: + filename = secure_filename(file.filename) + print(f"{filename=}") + filepath = f"Uploads/{file.filename}" + print(f"{filepath=}") + file.save(filepath) + + height, width = find_video_resolution(filepath) + print(f"{height=}") + print(f"{width=}") + + screenshot_url = get_first_frame(filepath, file.filename) + print(f"{screenshot_url=}") + + video_url = f"https://killyoy.lovinator.space/{file.filename}" + print(f"{video_url=}") + + html_url = generate_html( + video_url, + width, + height, + screenshot_url, + filename, + ) + print(f"{html_url=}") + return redirect(url_for("uploaded_file", filename=file.filename)) + return redirect(url_for("index")) + + +@app.route("/