Initial commit
This commit is contained in:
commit
749a36b445
9 changed files with 454 additions and 0 deletions
131
.gitignore
vendored
Executable file
131
.gitignore
vendored
Executable 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
14
README.md
Executable 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
6
config-example.ini
Executable file
|
@ -0,0 +1,6 @@
|
||||||
|
[Mail]
|
||||||
|
Server = localhost
|
||||||
|
Port = 465
|
||||||
|
Username = sender@localhost
|
||||||
|
Password = 123456
|
||||||
|
SSL = True
|
53
mail.py
Executable file
53
mail.py
Executable 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
152
main.py
Executable 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
21
requirements.txt
Executable 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
17
templates/index.html
Executable 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
15
templates/success.html
Executable 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
45
templates/video.html
Executable 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>
|
Reference in a new issue