From 749a36b445bf30f26158738104ea0f5903c78a68 Mon Sep 17 00:00:00 2001
From: Heorhii Mykhailenko
Date: Tue, 6 Jun 2023 18:18:12 +0300
Subject: [PATCH] Initial commit
---
.gitignore | 131 +++++++++++++++++++++++++++++++++++
README.md | 14 ++++
config-example.ini | 6 ++
mail.py | 53 ++++++++++++++
main.py | 152 +++++++++++++++++++++++++++++++++++++++++
requirements.txt | 21 ++++++
templates/index.html | 17 +++++
templates/success.html | 15 ++++
templates/video.html | 45 ++++++++++++
9 files changed, 454 insertions(+)
create mode 100755 .gitignore
create mode 100755 README.md
create mode 100755 config-example.ini
create mode 100755 mail.py
create mode 100755 main.py
create mode 100755 requirements.txt
create mode 100755 templates/index.html
create mode 100755 templates/success.html
create mode 100755 templates/video.html
diff --git a/.gitignore b/.gitignore
new file mode 100755
index 0000000..e4b0aa2
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,131 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+pip-wheel-metadata/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+.python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+config.ini
diff --git a/README.md b/README.md
new file mode 100755
index 0000000..e64ea36
--- /dev/null
+++ b/README.md
@@ -0,0 +1,14 @@
+# Video processing service
+
+This program is a video processing service. It is a project for New Generation programming courses.
+
+## Features
+- uploading a video
+- selecting a part of a video to cut and work with
+- converting a video to another format
+- adding a watermark image to a video
+- sending the result to an email address
+- displaying a progress bar
+
+## Usage
+Before running the program, a file `config-example.ini` must be renamed to `config.ini` and be filled with valid SMTP server credentials.
diff --git a/config-example.ini b/config-example.ini
new file mode 100755
index 0000000..49e746f
--- /dev/null
+++ b/config-example.ini
@@ -0,0 +1,6 @@
+[Mail]
+Server = localhost
+Port = 465
+Username = sender@localhost
+Password = 123456
+SSL = True
diff --git a/mail.py b/mail.py
new file mode 100755
index 0000000..5f4ec2d
--- /dev/null
+++ b/mail.py
@@ -0,0 +1,53 @@
+import configparser, email, os, smtplib, ssl
+
+from email import encoders
+from email.mime.base import MIMEBase
+from email.mime.multipart import MIMEMultipart
+from email.mime.text import MIMEText
+
+config = configparser.ConfigParser()
+config.read('config.ini')
+
+def send_email(destination, attachment):
+ subject = 'Your modified video'
+ body = 'You have recently used our service to modify a video. The result is attached.'
+ sender_email = config.get('Mail', 'Username')
+ receiver_email = destination
+ password = config.get('Mail', 'Password')
+
+ # Create a multipart message and set headers
+ message = MIMEMultipart()
+ message['From'] = sender_email
+ message['To'] = receiver_email
+ message['Subject'] = subject
+
+ # Add body to email
+ message.attach(MIMEText(body, 'plain'))
+
+ with open(attachment, 'rb') as file:
+ part = MIMEBase('application', 'octet-stream')
+ part.set_payload(file.read())
+
+ # Encode file in ASCII characters to send by email
+ encoders.encode_base64(part)
+
+ # Add header as key/value pair to attachment part
+ part.add_header(
+ 'Content-Disposition',
+ f'attachment; filename= {os.path.normpath(attachment)}',
+ )
+
+ # Add attachment to message and convert message to string
+ message.attach(part)
+ text = message.as_string()
+
+ # Log in to server using secure context and send email
+ if config.getboolean('Mail', 'SSL'):
+ context = ssl.create_default_context()
+ with smtplib.SMTP_SSL(config.get('Mail', 'Server'), config.get('Mail', 'Port'), context=context) as server:
+ server.login(sender_email, password)
+ server.sendmail(sender_email, receiver_email, text)
+ else:
+ with smtplib.SMTP(config.get('Mail', 'Server'), config.get('Mail', 'Port')) as server:
+ server.login(sender_email, password)
+ server.sendmail(sender_email, receiver_email, text)
diff --git a/main.py b/main.py
new file mode 100755
index 0000000..a1fcda6
--- /dev/null
+++ b/main.py
@@ -0,0 +1,152 @@
+from mail import send_email
+from flask import Flask, abort, render_template, redirect, request, url_for
+from PIL import Image
+from proglog import ProgressBarLogger
+import os
+import uuid
+
+import moviepy.editor as moviepy
+
+app = Flask(__name__)
+
+ALLOWED_UPLOAD_EXTENSIONS = {'mp4', 'webm', 'mkv', 'avi', 'ogv'}
+ALLOWED_CONVERT_EXTENSIONS = {'mp4', 'webm', 'mkv', 'avi', 'ogv', 'ogg', 'mp3', 'flac'}
+AUDIO_EXTENSIONS = {'ogg', 'mp3', 'flac'}
+ALLOWED_WATERMARK_EXTENSIONS = {'bmp', 'png', 'jpg', 'jpeg', 'tiff', 'tga', 'svg'}
+
+percentages = {}
+
+class BarLogger(ProgressBarLogger):
+ global percentages
+ id = None
+
+ def bars_callback(self, bar, attr, value, old_value=None):
+ percentages[self.id] = (value / self.bars[bar]['total']) * 100
+
+ def __init__(self, id):
+ super(self.__class__, self).__init__()
+ self.id = id
+
+def is_integer(n):
+ try:
+ return float(n).is_integer()
+ except ValueError:
+ return False
+
+def getExtension(filename, isWatermark=False):
+ if not '.' in filename:
+ return False
+ extension = filename.rsplit('.', 1)[1].lower()
+ if extension in ALLOWED_UPLOAD_EXTENSIONS and not isWatermark:
+ return extension
+ elif extension in ALLOWED_WATERMARK_EXTENSIONS and isWatermark:
+ return extension
+ else:
+ return False
+
+@app.route('/', methods=['GET', 'POST'])
+def index():
+ if request.method == 'POST' and 'video' in request.files:
+ file = request.files['video']
+ if not file.filename == '':
+ extension = getExtension(file.filename)
+ if extension != False:
+ filename = str(uuid.uuid4()) + '.' + extension
+ file.save(os.path.join('videos', filename))
+ return redirect(url_for('video', id=filename))
+ else:
+ return render_template('index.html', error='Non-allowed file extension.')
+
+ return render_template('index.html')
+
+@app.route('/video', methods=['GET', 'POST'])
+def video():
+ error = ''
+ changed = False
+ audio = None
+ id = request.args.get('id')
+ path = os.path.join('videos', id)
+ if id != None and os.path.isfile(path):
+ clip = moviepy.VideoFileClip(path)
+ if request.method == 'POST':
+ start = request.form.get('start')
+ end = request.form.get('end')
+ extension = request.form.get('extension')
+ email = request.form.get('email')
+ if not email:
+ abort(400)
+ if start and end:
+ if is_integer(start) and is_integer(end) and int(start) < int(end):
+ if int(start) < 0 or int(end) > clip.duration:
+ error += 'Selected subclip out of bounds.
'
+ else:
+ clip = clip.subclip(start, end)
+ changed = True
+ if extension:
+ if extension in ALLOWED_CONVERT_EXTENSIONS:
+ if extension in AUDIO_EXTENSIONS:
+ audio = clip.audio
+ newId = id.rsplit('.', 1)[0] + '.' + extension
+ if id == newId:
+ error += 'A different extension than the currently used one is needed.
'
+ else:
+ changed = True
+ else:
+ error += 'Requested recode extension not allowed.
'
+ if 'watermark' in request.files:
+ file = request.files['watermark']
+ if not file.filename == '':
+ if audio:
+ error += 'Audio cannot be watermarked.
'
+ else:
+ watermarkExtension = getExtension(file.filename, True)
+ if watermarkExtension:
+ watermarkPath = os.path.join('videos', id + '-watermark' + '.' + watermarkExtension)
+ file.save(watermarkPath)
+
+ formatter = {'PNG': 'RGBA', 'JPEG': 'RGB'}
+ img = Image.open(watermarkPath)
+ rgbimg = Image.new(formatter.get(img.format, 'RGB'), img.size)
+ rgbimg.paste(img)
+ rgbimg.save(watermarkPath, format=img.format)
+
+ watermark = (moviepy.ImageClip(watermarkPath)
+ .set_duration(clip.duration)
+ .set_pos(('right', 'bottom')))
+
+ clip = moviepy.CompositeVideoClip([clip, watermark])
+ os.remove(watermarkPath)
+ changed = True
+ else:
+ error += 'Non-allowed watermark extension.
'
+ if changed:
+ logger = BarLogger(id)
+ if 'newId' in locals():
+ os.remove(path)
+ id = newId
+ path = os.path.join('videos', id)
+ if audio:
+ audio.write_audiofile(path, logger=logger)
+ else:
+ clip.write_videofile(path, logger=logger)
+ send_email(email, path)
+ os.remove(path)
+ return render_template('success.html', error=error)
+
+ return render_template('video.html', id=id, length=int(clip.duration), error=error)
+ else:
+ abort(404)
+
+@app.route('/progress', methods=['GET'])
+def progress():
+ id = request.args.get('id')
+ percentage = percentages.get(id)
+ if percentage:
+ return str(percentage)
+ else:
+ abort(404)
+
+if __name__ == '__main__':
+ if not os.path.isdir('videos'):
+ os.mkdir('videos')
+ app.run()
diff --git a/requirements.txt b/requirements.txt
new file mode 100755
index 0000000..a4e4c31
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,21 @@
+blinker==1.6.2
+certifi==2023.5.7
+charset-normalizer==3.1.0
+click==8.1.3
+configparser==5.3.0
+decorator==4.4.2
+Flask==2.3.2
+idna==3.4
+imageio==2.31.0
+imageio-ffmpeg==0.4.8
+itsdangerous==2.1.2
+Jinja2==3.1.2
+MarkupSafe==2.1.2
+moviepy==1.0.3
+numpy==1.24.3
+Pillow==9.5.0
+proglog==0.1.10
+requests==2.31.0
+tqdm==4.65.0
+urllib3==2.0.2
+Werkzeug==2.3.3
diff --git a/templates/index.html b/templates/index.html
new file mode 100755
index 0000000..555ccb8
--- /dev/null
+++ b/templates/index.html
@@ -0,0 +1,17 @@
+
+
+
+ Video upload
+
+
+ {% if error %}
+ {{ error | safe }}
+ {% endif %}
+
+
+
+
diff --git a/templates/success.html b/templates/success.html
new file mode 100755
index 0000000..9d2346f
--- /dev/null
+++ b/templates/success.html
@@ -0,0 +1,15 @@
+
+
+
+ Video upload
+
+
+ The modified video was sent to your email address. You can upload another one if you want to.
+ {% if error %}
+ The following errors occurred:
+ {{ error | safe }}
+ {% endif %}
+ Upload another video
+
+
+
diff --git a/templates/video.html b/templates/video.html
new file mode 100755
index 0000000..4fda2c6
--- /dev/null
+++ b/templates/video.html
@@ -0,0 +1,45 @@
+
+
+
+ Video editing
+
+
+ {% if error %}
+ {{ error | safe }}
+ {% endif %}
+ Video {{name}} ({{length}})
+
+
+
+
+
+