Initial commit

This commit is contained in:
Heorhii Mykhailenko 2023-06-06 18:18:12 +03:00
commit 749a36b445
9 changed files with 454 additions and 0 deletions

131
.gitignore vendored Executable file
View file

@ -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

14
README.md Executable file
View file

@ -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.

6
config-example.ini Executable file
View file

@ -0,0 +1,6 @@
[Mail]
Server = localhost
Port = 465
Username = sender@localhost
Password = 123456
SSL = True

53
mail.py Executable file
View file

@ -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)

152
main.py Executable file
View file

@ -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.<br>'
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.<br>'
else:
changed = True
else:
error += 'Requested recode extension not allowed.<br>'
if 'watermark' in request.files:
file = request.files['watermark']
if not file.filename == '':
if audio:
error += 'Audio cannot be watermarked.<br>'
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.<br>'
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()

21
requirements.txt Executable file
View file

@ -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

17
templates/index.html Executable file
View file

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Video upload</title>
</head>
<body>
{% if error %}
<p>{{ error | safe }}</p>
{% endif %}
<form method="POST" enctype="multipart/form-data">
<label for="video">Video: </label><br>
<input type="file" id="video" name="video" required><br>
<button id="submit" type="submit">Submit</button>
</form>
</body>
</p>
</html>

15
templates/success.html Executable file
View file

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Video upload</title>
</head>
<body>
<p>The modified video was sent to your email address. You can upload another one if you want to.</p>
{% if error %}
<p>The following errors occurred:<br>
{{ error | safe }}</p>
{% endif %}
<p><a href="index.html">Upload another video</a></p>
</body>
</p>
</html>

45
templates/video.html Executable file
View file

@ -0,0 +1,45 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Video editing</title>
</head>
<body>
{% if error %}
<p>{{ error | safe }}</p>
{% endif %}
<p>Video {{name}} ({{length}})</p>
<form method="POST" enctype="multipart/form-data" onsubmit="trackProgress()">
<label for="start">Start (s): </label><br>
<input type="text" id="start" name="start"><br>
<label for="end">End (s): </label><br>
<input type="text" id="end" name="end"><br>
<label for="extension">Recode to (extension): </label><br>
<input type="text" id="extension" name="extension"><br>
<label for="watermark">Watermark: </label><br>
<input type="file" id="watermark" name="watermark"><br>
<label for="email">Send result to email: </label><br>
<input type="email" id="email" name="email" required><br>
<button id="submit" type="submit">Submit</button>
</form>
<progress id="bar" value="0" max="100">0 %</progress>
<script>
function getProgress() {
const urlParams = new URLSearchParams(window.location.search);
const id = urlParams.get('id');
const xhttp = new XMLHttpRequest();
xhttp.onload = function() {
document.getElementById("bar").value = this.responseText;
}
xhttp.open("GET", "/progress?id=" + id, true);
xhttp.send();
}
function trackProgress() {
window.setInterval(() => {
getProgress();
}, 1000);
}
</script>
</body>
</html>