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