Added more functions to GUI

- Access to settings (config.toml) through GUI
- Adding/removing backgrounds through GUI
- Testing TTS voices through GUI
pull/1211/head
RiveN000 2 years ago
parent 3a1c972a2a
commit 9cd94dac03

106
GUI.py

@ -1,5 +1,3 @@
import json
import re
import webbrowser
from pathlib import Path
@ -7,7 +5,6 @@ from pathlib import Path
import tomlkit
from flask import (
Flask,
flash,
redirect,
render_template,
request,
@ -15,6 +12,8 @@ from flask import (
url_for,
)
import utils.gui_utils as gui
# Set the hostname
HOST = "localhost"
# Set the port number
@ -39,86 +38,77 @@ def after_request(response):
# Display index.html
@app.route("/")
def index():
return render_template("index.html")
return render_template("index.html", file="videos.json")
# Make videos.json accessible
@app.route("/videos.json")
def videos_json():
return send_from_directory("video_creation/data", "videos.json")
# Make videos in results folder accessible
@app.route("/results/<path:name>")
def results(name):
return send_from_directory("results", name, as_attachment=True)
@app.route("/backgrounds", methods=["GET"])
def backgrounds():
return render_template("backgrounds.html", file="backgrounds.json")
@app.route("/add_background", methods=["POST"])
def add_background():
@app.route("/background/add", methods=["POST"])
def background_add():
# Get form values
youtube_uri = request.form.get("youtube_uri").strip()
filename = request.form.get("filename").strip()
citation = request.form.get("citation").strip()
position = request.form.get("position").strip()
# Validate YouTube URI
regex = re.compile(
r"(?:\/|%3D|v=|vi=)([0-9A-z-_]{11})(?:[%#?&]|$)"
).search(youtube_uri)
gui.add_background(youtube_uri, filename, citation, position)
if not regex:
flash("YouTube URI is invalid!", "error")
return redirect(url_for("index"))
return redirect(url_for("backgrounds"))
youtube_uri = f"https://www.youtube.com/watch?v={regex.group(1)}"
# Check if position is valid
if position == "" or position == "center":
position = "center"
@app.route("/background/delete", methods=["POST"])
def background_delete():
key = request.form.get("background-key")
gui.delete_background(key)
elif position.isdecimal():
position = int(position)
return redirect(url_for("backgrounds"))
else:
flash('Position is invalid! It can be "center" or decimal number.', "error")
return redirect(url_for("index"))
# Sanitize filename
filename = filename.replace(" ", "-").split(".")[0]
@app.route("/settings", methods=["GET", "POST"])
def settings():
config_load = tomlkit.loads(Path("config.toml").read_text())
config = gui.get_config(config_load)
# Check if background doesn't already exist
with open("utils/backgrounds.json", "r", encoding="utf-8") as backgrounds:
data = json.load(backgrounds)
# Get checks for all values
checks = gui.get_checks()
# Check if key isn't already taken
if filename in list(data.keys()):
flash("Background video with this name already exist!", "error")
return redirect(url_for("index"))
if request.method == "POST":
# Get data from form as dict
data = request.form.to_dict()
# Check if the YouTube URI isn't already used under different name
if youtube_uri in [data[i][0] for i in list(data.keys())]:
flash("Background video with this YouTube URI is already added!", "error")
return redirect(url_for("index"))
# Change settings
config = gui.modify_settings(data, config_load, checks)
# Add background video to json file
with open("utils/backgrounds.json", "r+", encoding="utf-8") as backgrounds:
data = json.load(backgrounds)
return render_template(
"settings.html", file="config.toml", data=config, checks=checks
)
data[filename] = [youtube_uri, filename + ".mp4", citation, position]
backgrounds.seek(0)
json.dump(data, backgrounds, ensure_ascii=False, indent=4)
# Add background video to ".config.template.toml" to make it accessible
config = tomlkit.loads(Path("utils/.config.template.toml").read_text())
config["settings"]["background"]["background_choice"]["options"].append(filename)
# Make videos.json accessible
@app.route("/videos.json")
def videos_json():
return send_from_directory("video_creation/data", "videos.json")
# Make backgrounds.json accessible
@app.route("/backgrounds.json")
def backgrounds_json():
return send_from_directory("utils", "backgrounds.json")
with Path("utils/.config.template.toml").open("w") as toml_file:
toml_file.write(tomlkit.dumps(config))
flash(f'Added "{citation}-{filename}.mp4" as a new background video!')
# Make videos in results folder accessible
@app.route("/results/<path:name>")
def results(name):
return send_from_directory("results", name, as_attachment=True)
return redirect(url_for("index"))
# Make voices samples in voices folder accessible
@app.route("/voices/<path:name>")
def voices(name):
return send_from_directory("GUI/voices", name, as_attachment=True)
# Run browser and start the app

@ -0,0 +1,263 @@
{% extends "layout.html" %}
{% block main %}
<!-- Delete Background Modal -->
<div class="modal fade" id="deleteBtnModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Delete background</h5>
</div>
<div class="modal-body">
Are you sure you want to delete this background?
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<form action="background/delete" method="post">
<input type="hidden" id="background-key" name="background-key" value="">
<button type="submit" class="btn btn-danger">Delete</button>
</form>
</div>
</div>
</div>
</div>
<!-- Add Background Modal -->
<div class="modal fade" id="backgroundAddModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add background video</h5>
</div>
<div class="modal-body">
<!-- Add video form -->
<form id="addBgForm" action="background/add" method="post" novalidate>
<div class="form-group row">
<label class="col-4 col-form-label" for="youtube_uri">YouTube URI</label>
<div class="col-8">
<div class="input-group">
<div class="input-group-text">
<i class="bi bi-youtube"></i>
</div>
<input name="youtube_uri" placeholder="https://www.youtube.com/watch?v=..." type="text"
class="form-control">
</div>
<span id="feedbackYT" class="form-text feedback-invalid"></span>
</div>
</div>
<div class="form-group row">
<label for="filename" class="col-4 col-form-label">Filename</label>
<div class="col-8">
<div class="input-group">
<div class="input-group-text">
<i class="bi bi-file-earmark"></i>
</div>
<input name="filename" placeholder="Example: cool-background" type="text"
class="form-control">
</div>
<span id="feedbackFilename" class="form-text feedback-invalid"></span>
</div>
</div>
<div class="form-group row">
<label for="citation" class="col-4 col-form-label">Credits (owner of the video)</label>
<div class="col-8">
<div class="input-group">
<div class="input-group-text">
<i class="bi bi-person-circle"></i>
</div>
<input name="citation" placeholder="YouTube Channel" type="text" class="form-control">
</div>
<span class="form-text text-muted">Include the channel name of the
owner of the background video you are adding.</span>
</div>
</div>
<div class="form-group row">
<label for="position" class="col-4 col-form-label">Position of screenshots</label>
<div class="col-8">
<div class="input-group">
<div class="input-group-text">
<i class="bi bi-arrows-fullscreen"></i>
</div>
<input name="position" placeholder="Example: center" type="text" class="form-control">
</div>
<span class="form-text text-muted">Advanced option (you can leave it
empty). Valid options are "center" and decimal numbers</span>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button name="submit" type="submit" class="btn btn-success">Add background</button>
</form>
</div>
</div>
</div>
</div>
<main>
<div class="album py-2 bg-light">
<div class="container">
<div class="row justify-content-between mt-2">
<div class="col-12 col-md-3 mb-3">
<input type="text" class="form-control searchFilter" placeholder="Search backgrounds"
onkeyup="searchFilter()">
</div>
<div class="col-12 col-md-2 mb-3">
<button type="button" class="btn btn-primary form-control" data-toggle="modal"
data-target="#backgroundAddModal">
Add background video
</button>
</div>
</div>
<div class="grid row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3" id="backgrounds">
</div>
</div>
</div>
</main>
<script>
var keys = [];
var youtube_urls = [];
// Show background videos
$(document).ready(function () {
$.getJSON("backgrounds.json",
function (data) {
delete data["__comment"];
var background = '';
$.each(data, function (key, value) {
// Add YT urls and keys (for validation)
keys.push(key);
youtube_urls.push(value[0]);
background += '<div class="col">';
background += '<div class="card shadow-sm">';
background += '<iframe class="bd-placeholder-img card-img-top" width="100%" height="225" src="https://www.youtube-nocookie.com/embed/' + value[0].split("?v=")[1] + '" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>';
background += '<div class="card-body">';
background += '<p class="card-text">' + value[2] + ' • ' + key + '</p>';
background += '<div class="d-flex justify-content-between align-items-center">';
background += '<div class="btn-group">';
background += '<button type="button" class="btn btn-outline-danger" data-toggle="modal" data-target="#deleteBtnModal" data-background-key="' + key + '">Delete</button>';
background += '</div>';
background += '</div>';
background += '</div>';
background += '</div>';
background += '</div>';
});
$('#backgrounds').append(background);
});
});
// Add background key when deleting
$('#deleteBtnModal').on('show.bs.modal', function (event) {
var button = $(event.relatedTarget);
var key = button.data('background-key');
$('#background-key').prop('value', key);
});
var searchFilter = () => {
const input = document.querySelector(".searchFilter");
const cards = document.getElementsByClassName("col");
console.log(cards[1])
let filter = input.value
for (let i = 0; i < cards.length; i++) {
let title = cards[i].querySelector(".card-text");
if (title.innerText.toLowerCase().indexOf(filter.toLowerCase()) > -1) {
cards[i].classList.remove("d-none")
} else {
cards[i].classList.add("d-none")
}
}
}
// Validate form
$("#addBgForm").submit(function (event) {
$("#addBgForm input").each(function () {
if (!(validate($(this)))) {
event.preventDefault();
event.stopPropagation();
}
});
});
$('#addBgForm input[type="text"]').on("keyup", function () {
validate($(this));
});
function validate(object) {
let bool = check(object.prop("name"), object.prop("value"));
// Change class
if (bool) {
object.removeClass("is-invalid");
object.addClass("is-valid");
}
else {
object.removeClass("is-valid");
object.addClass("is-invalid");
}
return bool;
// Check values (return true/false)
function check(name, value) {
if (name == "youtube_uri") {
// URI validation
let regex = /(?:\/|%3D|v=|vi=)([0-9A-z-_]{11})(?:[%#?&]|$)/;
if (!(regex.test(value))) {
$("#feedbackYT").html("Invalid URI");
$("#feedbackYT").show();
return false;
}
// Check if this background already exists
if (youtube_urls.includes(value)) {
$("#feedbackYT").html("This background is already added");
$("#feedbackYT").show();
return false;
}
$("#feedbackYT").hide();
return true;
}
if (name == "filename") {
// Check if key is already taken
if (keys.includes(value)) {
$("#feedbackFilename").html("This filename is already taken");
$("#feedbackFilename").show();
return false;
}
let regex = /^([a-zA-Z0-9\s_-]{1,100})$/;
if (!(regex.test(value))) {
return false;
}
return true;
}
if (name == "citation") {
if (value.trim()) {
return true;
}
}
if (name == "position") {
if (!(value == "center" || value.length == 0 || value % 1 == 0)) {
return false;
}
return true;
}
}
}
</script>
{% endblock %}

@ -1,404 +1,182 @@
<html lang="en">
{% extends "layout.html" %}
{% block main %}
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="cache-control" content="no-cache" />
<title>RedditVideoMakerBot</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.1.3/dist/css/bootstrap.min.css"
integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<link href="https://getbootstrap.com/docs/5.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.3/font/bootstrap-icons.css">
<style>
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
.b-example-divider {
height: 3rem;
background-color: rgba(0, 0, 0, .1);
border: solid rgba(0, 0, 0, .15);
border-width: 1px 0;
box-shadow: inset 0 .5em 1.5em rgba(0, 0, 0, .1), inset 0 .125em .5em rgba(0, 0, 0, .15);
}
.b-example-vr {
flex-shrink: 0;
width: 1.5rem;
height: 100vh;
}
.bi {
vertical-align: -.125em;
fill: currentColor;
}
.nav-scroller {
position: relative;
z-index: 2;
height: 2.75rem;
overflow-y: hidden;
}
.nav-scroller .nav {
display: flex;
flex-wrap: nowrap;
padding-bottom: 1rem;
margin-top: -1px;
overflow-x: auto;
text-align: center;
white-space: nowrap;
-webkit-overflow-scrolling: touch;
}
#tooltip {
background-color: #333;
color: white;
padding: 5px 10px;
border-radius: 4px;
font-size: 13px;
}
</style>
</head>
<body>
<header>
{% if get_flashed_messages() %}
{% for category, message in get_flashed_messages(with_categories=true) %}
{% if category == "error" %}
<div class="alert alert-danger mb-0 text-center" role="alert">
{{ message }}
</div>
{% else %}
<div class="alert alert-success mb-0 text-center" role="alert">
{{ message }}
</div>
{% endif %}
{% endfor %}
{% endif %}
<div class="navbar navbar-dark bg-dark shadow-sm">
<div class="container">
<a href="#" class="navbar-brand d-flex align-items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" stroke="currentColor"
stroke-linecap="round" stroke-linejoin="round" stroke-width="2" aria-hidden="true" class="me-2"
viewBox="0 0 24 24">
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z" />
<circle cx="12" cy="13" r="4" />
</svg>
<strong>RedditVideoMakerBot</strong>
</a>
<!-- Button trigger modal -->
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#backgroundModal">
Add background video
</button>
<!-- Modal -->
<div class="modal fade" id="backgroundModal" tabindex="-1" role="dialog" aria-labelledby="backgroundModalLabel"
aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="backgroundModalLabel">Add Background Video</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<!-- Add video form -->
<form action="/add_background" method="post">
<div class="form-group row">
<label class="col-4 col-form-label" for="youtube_uri">YouTube URI</label>
<div class="col-8">
<div class="input-group">
<div class="input-group-prepend">
<div class="input-group-text">
<i class="bi bi-youtube"></i>
</div>
</div>
<input id="youtube_uri" name="youtube_uri" placeholder="https://www.youtube.com/watch?v=..."
type="text" class="form-control" required="required">
</div>
</div>
</div>
<div class="form-group row">
<label for="filename" class="col-4 col-form-label">Filename</label>
<div class="col-8">
<div class="input-group">
<div class="input-group-prepend">
<div class="input-group-text">
<i class="bi bi-file-earmark"></i>
</div>
</div>
<input id="filename" name="filename" placeholder="example: cool-background" type="text"
aria-describedby="filenameHelpBlock" required="required" class="form-control">
</div>
</div>
</div>
<div class="form-group row">
<label for="citation" class="col-4 col-form-label">Credits (owner of the video)</label>
<div class="col-8">
<div class="input-group">
<div class="input-group-prepend">
<div class="input-group-text">
<i class="bi bi-person-circle"></i>
</div>
</div>
<input id="citation" name="citation" placeholder="YouTube Channel" type="text"
class="form-control" required="required" aria-describedby="citationHelpBlock">
</div>
<span id="citationHelpBlock" class="form-text text-muted">Include the channel name of the owner of
the background video you are adding.</span>
</div>
</div>
<div class="form-group row">
<label for="position" class="col-4 col-form-label">Position of screenshots</label>
<div class="col-8">
<div class="input-group">
<div class="input-group-prepend">
<div class="input-group-text">
<i class="bi bi-arrows-fullscreen"></i>
</div>
</div>
<input id="position" name="position" placeholder="Example: center" type="text"
class="form-control" aria-describedby="positionHelpBlock">
</div>
<span id="positionHelpBlock" class="form-text text-muted">Advanced option (you can leave it
empty). Valid options are "center" and decimal numbers</span>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button name="submit" type="submit" class="btn btn-primary">Add background</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</header>
<main>
<main>
<div class="album py-2 bg-light">
<div class="container">
<div class="container">
<div class="row mt-2">
<div class="col-12 col-md-3 mb-3">
<input type="text" class="form-control" placeholder="Search videos" aria-label="Search videos"
onkeyup="searchFilter()">
</div>
</div>
<div class="row mt-2">
<div class="col-12 col-md-3 mb-3">
<input type="text" class="form-control searchFilter" placeholder="Search videos"
aria-label="Search videos" onkeyup="searchFilter()">
</div>
</div>
<div class="grid row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3" id="videos">
<div class="grid row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3" id="videos">
</div>
</div>
</div>
</div>
</main>
</main>
<footer class="text-muted py-5">
<div class="container">
<p class="float-end mb-1">
<a href="#">Back to top</a>
</p>
<p class="mb-1"><a href="https://getbootstrap.com/docs/5.2/examples/album/" target="_blank">Album</a> Example
Theme by &copy; Bootstrap. <a
href="https://github.com/elebumm/RedditVideoMakerBot/blob/master/README.md#developers-and-maintainers"
target="_blank">Developers and Maintainers</a></p>
<p class="mb-0">If your data is not refreshing, try to hard reload(Ctrl + F5) and visit your local <a
href="../video_creation/data/videos.json" target="_blank">videos.json</a> file.</p>
</div>
</footer>
<script src="https://code.jquery.com/jquery-3.1.1.js" integrity="sha256-16cdPddA6VdVInumRGo6IbivbERE8p7CQR3HzTBuELA="
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.14.3/dist/umd/popper.min.js"
integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49"
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.1.3/dist/js/bootstrap.min.js"
integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy"
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.10/clipboard.min.js"></script>
<script src="https://unpkg.com/isotope-layout@3/dist/isotope.pkgd.js"></script>
<script>
<script>
const intervals = [
{ label: 'year', seconds: 31536000 },
{ label: 'month', seconds: 2592000 },
{ label: 'day', seconds: 86400 },
{ label: 'hour', seconds: 3600 },
{ label: 'minute', seconds: 60 },
{ label: 'second', seconds: 1 }
{ label: 'year', seconds: 31536000 },
{ label: 'month', seconds: 2592000 },
{ label: 'day', seconds: 86400 },
{ label: 'hour', seconds: 3600 },
{ label: 'minute', seconds: 60 },
{ label: 'second', seconds: 1 }
];
function timeSince(date) {
const seconds = Math.floor((Date.now() / 1000 - date));
const interval = intervals.find(i => i.seconds < seconds);
const count = Math.floor(seconds / interval.seconds);
return `${count} ${interval.label}${count !== 1 ? 's' : ''} ago`;
const seconds = Math.floor((Date.now() / 1000 - date));
const interval = intervals.find(i => i.seconds < seconds);
const count = Math.floor(seconds / interval.seconds);
return `${count} ${interval.label}${count !== 1 ? 's' : ''} ago`;
}
$(document).ready(function () {
$.getJSON("videos.json",
function (data) {
data.sort((b, a) => a['time'] - b['time'])
var video = '';
$.each(data, function (key, value) {
video += '<div class="col">';
video += '<div class="card shadow-sm">';
//keeping original themed image card for future thumbnail usage video += '<svg class="bd-placeholder-img card-img-top" width="100%" height="225" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Placeholder: Thumbnail" preserveAspectRatio="xMidYMid slice" focusable="false"><title>Placeholder</title><rect width="100%" height="100%" fill="#55595c"/><text x="50%" y="50%" fill="#eceeef" dy=".3em">r/'+value.subreddit+'</text></svg>';
video += '<div class="card-body">';
video += '<p class="card-text">r/' + value.subreddit + ' • ' + checkTitle(value.reddit_title, value.filename) + '</p>';
video += '<div class="d-flex justify-content-between align-items-center">';
video += '<div class="btn-group">';
video += '<a href="https://www.reddit.com/r/' + value.subreddit + '/comments/' + value.id + '/" class="btn btn-sm btn-outline-secondary" target="_blank">View</a>';
video += '<a href="http://localhost:4000/results/' + value.subreddit + '/' + value.filename + '" class="btn btn-sm btn-outline-secondary" download>Download</a>';
video += '</div>';
video += '<div class="btn-group">';
video += '<button type="button" data-toggle="tooltip" id="copy" data-original-title="Copy to clipboard" class="btn btn-sm btn-outline-secondary" data-clipboard-text="' + getCopyData(value.subreddit, value.reddit_title, value.filename, value.background_credit) + '"><i class="bi bi-card-text"></i></button>';
video += '<button type="button" data-toggle="tooltip" id="copy" data-original-title="Copy to clipboard" class="btn btn-sm btn-outline-secondary" data-clipboard-text="' + checkTitle(value.reddit_title, value.filename) + ' #Shorts #reddit"><i class="bi bi-youtube"></i></button>';
video += '<button type="button" data-toggle="tooltip" id="copy" data-original-title="Copy to clipboard" class="btn btn-sm btn-outline-secondary" data-clipboard-text="' + checkTitle(value.reddit_title, value.filename) + ' #reddit"><i class="bi bi-instagram"></i></button>';
video += '</div>';
video += '<small class="text-muted">' + timeSince(value.time) + '</small>';
video += '</div>';
video += '</div>';
video += '</div>';
video += '</div>';
});
$('#videos').append(video);
});
$.getJSON("videos.json",
function (data) {
data.sort((b, a) => a['time'] - b['time'])
var video = '';
$.each(data, function (key, value) {
video += '<div class="col">';
video += '<div class="card shadow-sm">';
//keeping original themed image card for future thumbnail usage video += '<svg class="bd-placeholder-img card-img-top" width="100%" height="225" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Placeholder: Thumbnail" preserveAspectRatio="xMidYMid slice" focusable="false"><title>Placeholder</title><rect width="100%" height="100%" fill="#55595c"/><text x="50%" y="50%" fill="#eceeef" dy=".3em">r/'+value.subreddit+'</text></svg>';
video += '<div class="card-body">';
video += '<p class="card-text">r/' + value.subreddit + ' • ' + checkTitle(value.reddit_title, value.filename) + '</p>';
video += '<div class="d-flex justify-content-between align-items-center">';
video += '<div class="btn-group">';
video += '<a href="https://www.reddit.com/r/' + value.subreddit + '/comments/' + value.id + '/" class="btn btn-sm btn-outline-secondary" target="_blank">View</a>';
video += '<a href="http://localhost:4000/results/' + value.subreddit + '/' + value.filename + '" class="btn btn-sm btn-outline-secondary" download>Download</a>';
video += '</div>';
video += '<div class="btn-group">';
video += '<button type="button" data-toggle="tooltip" id="copy" data-original-title="Copy to clipboard" class="btn btn-sm btn-outline-secondary" data-clipboard-text="' + getCopyData(value.subreddit, value.reddit_title, value.filename, value.background_credit) + '"><i class="bi bi-card-text"></i></button>';
video += '<button type="button" data-toggle="tooltip" id="copy" data-original-title="Copy to clipboard" class="btn btn-sm btn-outline-secondary" data-clipboard-text="' + checkTitle(value.reddit_title, value.filename) + ' #Shorts #reddit"><i class="bi bi-youtube"></i></button>';
video += '<button type="button" data-toggle="tooltip" id="copy" data-original-title="Copy to clipboard" class="btn btn-sm btn-outline-secondary" data-clipboard-text="' + checkTitle(value.reddit_title, value.filename) + ' #reddit"><i class="bi bi-instagram"></i></button>';
video += '</div>';
video += '<small class="text-muted">' + timeSince(value.time) + '</small>';
video += '</div>';
video += '</div>';
video += '</div>';
video += '</div>';
});
$('#videos').append(video);
});
});
$(document).ready(function () {
$('[data-toggle="tooltip"]').tooltip();
$('[data-toggle="tooltip"]').on('click', function () {
$(this).tooltip('hide');
});
$('[data-toggle="tooltip"]').tooltip();
$('[data-toggle="tooltip"]').on('click', function () {
$(this).tooltip('hide');
});
});
$('#copy').tooltip({
trigger: 'click',
placement: 'bottom'
trigger: 'click',
placement: 'bottom'
});
function setTooltip(btn, message) {
$(btn).tooltip('hide')
.attr('data-original-title', message)
.tooltip('show');
$(btn).tooltip('hide')
.attr('data-original-title', message)
.tooltip('show');
}
function hoverTooltip(btn, message) {
$(btn).tooltip('hide')
.attr('data-original-title', message)
.tooltip('show');
$(btn).tooltip('hide')
.attr('data-original-title', message)
.tooltip('show');
}
function hideTooltip(btn) {
setTimeout(function () {
$(btn).tooltip('hide');
}, 1000);
setTimeout(function () {
$(btn).tooltip('hide');
}, 1000);
}
function disposeTooltip(btn) {
setTimeout(function () {
$(btn).tooltip('dispose');
}, 1500);
setTimeout(function () {
$(btn).tooltip('dispose');
}, 1500);
}
var clipboard = new ClipboardJS('#copy');
clipboard.on('success', function (e) {
e.clearSelection();
console.info('Action:', e.action);
console.info('Text:', e.text);
console.info('Trigger:', e.trigger);
setTooltip(e.trigger, 'Copied!');
hideTooltip(e.trigger);
disposeTooltip(e.trigger);
e.clearSelection();
console.info('Action:', e.action);
console.info('Text:', e.text);
console.info('Trigger:', e.trigger);
setTooltip(e.trigger, 'Copied!');
hideTooltip(e.trigger);
disposeTooltip(e.trigger);
});
clipboard.on('error', function (e) {
console.error('Action:', e.action);
console.error('Trigger:', e.trigger);
setTooltip(e.trigger, fallbackMessage(e.action));
hideTooltip(e.trigger);
console.error('Action:', e.action);
console.error('Trigger:', e.trigger);
setTooltip(e.trigger, fallbackMessage(e.action));
hideTooltip(e.trigger);
});
function getCopyData(subreddit, reddit_title, filename, background_credit) {
if (subreddit == undefined) {
subredditCopy = "";
} else {
subredditCopy = "r/" + subreddit + "\n\n";
}
if (subreddit == undefined) {
subredditCopy = "";
} else {
subredditCopy = "r/" + subreddit + "\n\n";
}
const file = filename.slice(0, -4);
if (reddit_title == file) {
titleCopy = reddit_title;
} else {
titleCopy = file;
}
const file = filename.slice(0, -4);
if (reddit_title == file) {
titleCopy = reddit_title;
} else {
titleCopy = file;
}
var copyData = "";
copyData += subredditCopy;
copyData += titleCopy;
copyData += "\n\nBackground credit: " + background_credit;
return copyData;
var copyData = "";
copyData += subredditCopy;
copyData += titleCopy;
copyData += "\n\nBackground credit: " + background_credit;
return copyData;
}
function getLink(subreddit, id, reddit_title) {
if (subreddit == undefined) {
return reddit_title;
} else {
return "<a target='_blank' href='https://www.reddit.com/r/" + subreddit + "/comments/" + id + "/'>" + reddit_title + "</a>";
}
if (subreddit == undefined) {
return reddit_title;
} else {
return "<a target='_blank' href='https://www.reddit.com/r/" + subreddit + "/comments/" + id + "/'>" + reddit_title + "</a>";
}
}
function checkTitle(reddit_title, filename) {
const file = filename.slice(0, -4);
if (reddit_title == file) {
return reddit_title;
} else {
return file;
}
const file = filename.slice(0, -4);
if (reddit_title == file) {
return reddit_title;
} else {
return file;
}
}
var searchFilter = () => {
const input = document.querySelector(".form-control");
const cards = document.getElementsByClassName("col");
console.log(cards[1])
let filter = input.value
for (let i = 0; i < cards.length; i++) {
let title = cards[i].querySelector(".card-text");
if (title.innerText.toLowerCase().indexOf(filter.toLowerCase()) > -1) {
cards[i].classList.remove("d-none")
} else {
cards[i].classList.add("d-none")
const input = document.querySelector(".searchFilter");
const cards = document.getElementsByClassName("col");
console.log(cards[1])
let filter = input.value
for (let i = 0; i < cards.length; i++) {
let title = cards[i].querySelector(".card-text");
if (title.innerText.toLowerCase().indexOf(filter.toLowerCase()) > -1) {
cards[i].classList.remove("d-none")
} else {
cards[i].classList.add("d-none")
}
}
}
}
</script>
</body>
</html>
</script>
{% endblock %}

@ -0,0 +1,142 @@
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="cache-control" content="no-cache" />
<title>RedditVideoMakerBot</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.1.3/dist/css/bootstrap.min.css"
integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<link href="https://getbootstrap.com/docs/5.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.1/font/bootstrap-icons.css">
<style>
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
.feedback-invalid {
color: #dc3545;
}
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
.bi {
vertical-align: -.125em;
fill: currentColor;
}
.nav {
display: flex;
flex-wrap: nowrap;
padding-bottom: 1rem;
margin-top: -1px;
overflow-x: auto;
text-align: center;
white-space: nowrap;
-webkit-overflow-scrolling: touch;
}
#tooltip {
background-color: #333;
color: white;
padding: 5px 10px;
border-radius: 4px;
font-size: 13px;
}
.tooltip-inner {
max-width: 500px !important;
}
</style>
</head>
<script src="https://code.jquery.com/jquery-3.1.1.js" integrity="sha256-16cdPddA6VdVInumRGo6IbivbERE8p7CQR3HzTBuELA="
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.14.3/dist/umd/popper.min.js"
integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49"
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.1.3/dist/js/bootstrap.min.js"
integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy"
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.10/clipboard.min.js"></script>
<script src="https://unpkg.com/isotope-layout@3/dist/isotope.pkgd.js"></script>
<body>
<header>
{% if get_flashed_messages() %}
{% for category, message in get_flashed_messages(with_categories=true) %}
{% if category == "error" %}
<div class="alert alert-danger mb-0 text-center" role="alert">
{{ message }}
</div>
{% else %}
<div class="alert alert-success mb-0 text-center" role="alert">
{{ message }}
</div>
{% endif %}
{% endfor %}
{% endif %}
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a href="/" class="navbar-brand d-flex align-items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" stroke="currentColor"
stroke-linecap="round" stroke-linejoin="round" stroke-width="2" aria-hidden="true" class="me-2"
viewBox="0 0 24 24">
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z" />
<circle cx="12" cy="13" r="4" />
</svg>
<strong>RedditVideoMakerBot</strong>
</a>
<div class="collapse navbar-collapse">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link" href="backgrounds">Background Manager</a>
</li>
<li class="nav-item">
<a class="nav-link" href="settings">Settings</a>
</li>
</ul>
<!-- Future feature
<ul class="navbar-nav">
<li class="nav-item">
<button class="btn btn-outline-success mr-auto mt-2 mt-lg-0">Create new short</button>
</li>
</ul>
-->
</div>
</div>
</nav>
</header>
{% block main %}{% endblock %}
<footer class="text-muted py-5">
<div class="container">
<p class="float-end mb-1">
<a href="#">Back to top</a>
</p>
<p class="mb-1"><a href="https://getbootstrap.com/docs/5.2/examples/album/" target="_blank">Album</a>
Example
Theme by &copy; Bootstrap. <a
href="https://github.com/elebumm/RedditVideoMakerBot/blob/master/README.md#developers-and-maintainers"
target="_blank">Developers and Maintainers</a></p>
<p class="mb-0">If your data is not refreshing, try to hard reload(Ctrl + F5) and visit your local
<strong>{{ file }}</strong> file.
</p>
</div>
</footer>
</body>
</html>

@ -0,0 +1,576 @@
{% extends "layout.html" %}
{% block main %}
<main>
<br>
<div class="container">
<form id="settingsForm" action="/settings" method="post" novalidate>
<!-- Reddit Credentials -->
<p class="h4">Reddit Credentials</p>
<div class="row mb-2">
<label for="client_id" class="col-4">Client ID</label>
<div class="col-8">
<div class="input-group">
<div class="input-group-text">
<i class="bi bi-person"></i>
</div>
<input name="client_id" value="{{ data.client_id }}" placeholder="Your Reddit app's client ID"
type="text" class="form-control" data-toggle="tooltip"
data-original-title='Text under "personal use script" on www.reddit.com/prefs/apps'>
</div>
</div>
</div>
<div class="row mb-2">
<label for="client_secret" class="col-4">Client Secret</label>
<div class="col-8">
<div class="input-group">
<div class="input-group-text">
<i class="bi bi-key-fill"></i>
</div>
<input name="client_secret" value="{{ data.client_secret }}"
placeholder="Your Reddit app's client secret" type="text" class="form-control"
data-toggle="tooltip" data-original-title='"Secret" on www.reddit.com/prefs/apps'>
</div>
</div>
</div>
<div class="row mb-2">
<label for="username" class="col-4">Reddit Username</label>
<div class="col-8">
<div class="input-group">
<div class="input-group-text">
<i class="bi bi-person-fill"></i>
</div>
<input name="username" value="{{ data.username }}" placeholder="Your Reddit account's username"
type="text" class="form-control">
</div>
</div>
</div>
<div class="row mb-2">
<label for="password" class="col-4">Reddit Password</label>
<div class="col-8">
<div class="input-group">
<div class="input-group-text">
<i class="bi bi-lock-fill"></i>
</div>
<input name="password" value="{{ data.password }}" placeholder="Your Reddit account's password"
type="password" class="form-control">
</div>
</div>
</div>
<div class="row mb-2">
<label class="col-4">Do you have Reddit 2FA enabled?</label>
<div class="col-8">
<div class="form-check form-switch">
<input name="2fa" class="form-check-input" type="checkbox" value="True" data-toggle="tooltip"
data-original-title='Check it if you have enabled 2FA on your Reddit account'>
</div>
<span class="form-text text-muted"><a
href="https://reddit-video-maker-bot.netlify.app/docs/configuring#setting-up-the-api"
target="_blank">Need help? Click here to open step-by-step guide.</a></span>
</div>
</div>
<!-- Reddit Thread -->
<p class="h4">Reddit Thread</p>
<div class="row mb-2">
<label class="col-4">Random Thread</label>
<div class="col-8">
<div class="form-check form-switch">
<input name="random" class="form-check-input" type="checkbox" value="True" data-toggle="tooltip"
data-original-title='If disabled, it will ask you for a thread link, instead of picking random one'>
</div>
</div>
</div>
<div class="row mb-2">
<label for="subreddit" class="col-4">Subreddit</label>
<div class="col-8">
<div class="input-group">
<div class="input-group-text">
<i class="bi bi-reddit"></i>
</div>
<input value="{{ data.subreddit }}" name="subreddit" type="text" class="form-control"
placeholder="Subreddit to pull posts from (e.g. AskReddit)" data-toggle="tooltip"
data-original-title='You can have multiple subreddits,
add "+" between them (e.g. AskReddit+Redditdev)'>
</div>
</div>
</div>
<div class="row mb-2">
<label for="post_id" class="col-4">Post ID</label>
<div class="col-8">
<div class="input-group">
<div class="input-group-text">
<i class="bi bi-file-text"></i>
</div>
<input value="{{ data.post_id }}" name="post_id" type="text" class="form-control"
placeholder="Used if you want to use a specific post (e.g. urdtfx)">
</div>
</div>
</div>
<div class="row mb-2">
<label for="max_comment_length" class="col-4">Max Comment Length</label>
<div class="col-8">
<div class="input-group">
<input name="max_comment_length" type="range" class="form-range" min="10" max="10000" step="1"
value="{{ data.max_comment_length }}" data-toggle="tooltip"
data-original-title="{{ data.max_comment_length }}">
</div>
<span class="form-text text-muted">Max number of characters a comment can have.</span>
</div>
</div>
<div class="row mb-2">
<label for="post_lang" class="col-4">Post Language</label>
<div class="col-8">
<div class="input-group">
<div class="input-group-text">
<i class="bi bi-translate"></i>
</div>
<input value="{{ data.post_lang }}" name="post_lang" type="text" class="form-control"
placeholder="The language you would like to translate to">
</div>
</div>
</div>
<div class="row mb-2">
<label for="min_comments" class="col-4">Minimum Comments</label>
<div class="col-8">
<div class="input-group">
<input name="min_comments" type="range" class="form-range" min="15" max="1000" step="1"
value="{{ data.min_comments }}" data-toggle="tooltip"
data-original-title="{{ data.min_comments }}">
</div>
<span class="form-text text-muted">The minimum number of comments a post should have to be
included.</span>
</div>
</div>
<!-- General Settings -->
<p class="h4">General Settings</p>
<div class="row mb-2">
<label class="col-4">Allow NSFW</label>
<div class="col-8">
<div class="form-check form-switch">
<input name="allow_nsfw" class="form-check-input" type="checkbox" value="True"
data-toggle="tooltip" data-original-title='If checked NSFW posts will be allowed'>
</div>
</div>
</div>
<div class="row mb-2">
<label for="theme" class="col-4">Reddit Theme</label>
<div class="col-8">
<select name="theme" class="form-select" data-toggle="tooltip"
data-original-title='Sets the theme of Reddit screenshots'>
<option value="dark">Dark</option>
<option value="light">Light</option>
</select>
</div>
</div>
<div class="row mb-2">
<label for="times_to_run" class="col-4">Times To Run</label>
<div class="col-8">
<div class="input-group">
<input name="times_to_run" type="range" class="form-range" min="1" max="1000" step="1"
value="{{ data.times_to_run }}" data-toggle="tooltip"
data-original-title="{{ data.times_to_run }}">
</div>
<span class="form-text text-muted">Used if you want to create multiple videos.</span>
</div>
</div>
<div class="row mb-2">
<label for="opacity" class="col-4">Opacity Of Comments</label>
<div class="col-8">
<div class="input-group">
<input name="opacity" type="range" class="form-range" min="0" max="1" step="0.05"
value="{{ data.opacity }}" data-toggle="tooltip" data-original-title="{{ data.opacity }}">
</div>
<span class="form-text text-muted">Sets the opacity of the comments when overlayed over the
background.</span>
</div>
</div>
<div class="row mb-2">
<label for="transition" class="col-4">Transition</label>
<div class="col-8">
<div class="input-group">
<input name="transition" type="range" class="form-range" min="0" max="2" step="0.05"
value="{{ data.transition }}" data-toggle="tooltip"
data-original-title="{{ data.transition }}">
</div>
<span class="form-text text-muted">Sets the transition time (in seconds) between the
comments. Set to 0 if you want to disable it.</span>
</div>
</div>
<div class="row mb-2">
<label for="background_choice" class="col-4">Background Choice</label>
<div class="col-8">
<select name="background_choice" class="form-select" data-toggle="tooltip"
data-original-title='Sets the background of the video'>
<option value=" ">Random Video</option>
{% for background in checks["background_choice"]["options"][1:] %}
<option value="{{background}}">{{background}}</option>
{% endfor %}
</select>
<span class="form-text text-muted"><a href="/backgrounds" target="_blank">See all available
backgrounds</a></span>
</div>
</div>
<!-- TTS Settings -->
<p class="h4">TTS Settings</p>
<div class="row mb-2">
<label for="voice_choice" class="col-4">TTS Voice Choice</label>
<div class="col-8">
<select name="voice_choice" class="form-select" data-toggle="tooltip"
data-original-title='The voice platform used for TTS generation'>
<option value="streamlabspolly">Streamlabspolly</option>
<option value="tiktok">TikTok</option>
<option value="googletranslate">Google Translate</option>
<option value="awspolly">AWS Polly</option>
<option value="pyttsx">Python TTS (pyttsx)</option>
</select>
</div>
</div>
<div class="row mb-2">
<label for="aws_polly_voice" class="col-4">AWS Polly Voice</label>
<div class="col-8">
<div class="input-group voices">
<select name="aws_polly_voice" class="form-select" data-toggle="tooltip"
data-original-title='The voice used for AWS Polly'>
<option value="Brian">Brian</option>
<option value="Emma">Emma</option>
<option value="Russell">Russell</option>
<option value="Joey">Joey</option>
<option value="Matthew">Matthew</option>
<option value="Joanna">Joanna</option>
<option value="Kimberly">Kimberly</option>
<option value="Amy">Amy</option>
<option value="Geraint">Geraint</option>
<option value="Nicole">Nicole</option>
<option value="Justin">Justin</option>
<option value="Ivy">Ivy</option>
<option value="Kendra">Kendra</option>
<option value="Salli">Salli</option>
<option value="Raveena">Raveena</option>
</select>
<button type="button" class="btn btn-primary"><i id="awspolly_icon"
class="bi-volume-up-fill"></i></button>
</div>
</div>
</div>
<div class="row mb-2">
<label for="streamlabs_polly_voice" class="col-4">Streamlabs Polly Voice</label>
<div class="col-8">
<div class="input-group voices">
<select id="streamlabs_polly_voice" name="streamlabs_polly_voice" class="form-select"
data-toggle="tooltip" data-original-title='The voice used for Streamlabs Polly'>
<option value="Brian">Brian</option>
<option value="Emma">Emma</option>
<option value="Russell">Russell</option>
<option value="Joey">Joey</option>
<option value="Matthew">Matthew</option>
<option value="Joanna">Joanna</option>
<option value="Kimberly">Kimberly</option>
<option value="Amy">Amy</option>
<option value="Geraint">Geraint</option>
<option value="Nicole">Nicole</option>
<option value="Justin">Justin</option>
<option value="Ivy">Ivy</option>
<option value="Kendra">Kendra</option>
<option value="Salli">Salli</option>
<option value="Raveena">Raveena</option>
</select>
<button type="button" class="btn btn-primary"><i id="streamlabs_icon"
class="bi bi-volume-up-fill"></i></button>
</div>
</div>
</div>
<div class="row mb-2">
<label for="tiktok_voice" class="col-4">TikTok Voice</label>
<div class="col-8">
<div class="input-group voices">
<select name="tiktok_voice" class="form-select" data-toggle="tooltip"
data-original-title='The voice used for TikTok TTS'>
<option disabled value="">-----Disney Voices-----</option>
<option value="en_us_ghostface">Ghost Face</option>
<option value="en_us_chewbacca">Chewbacca</option>
<option value="en_us_c3po">C3PO</option>
<option value="en_us_stitch">Stitch</option>
<option value="en_us_stormtrooper">Stormtrooper</option>
<option value="en_us_rocket">Rocket</option>
<option disabled value="">-----English Voices-----</option>
<option value="en_au_001">English AU - Female</option>
<option value="en_au_002">English AU - Male</option>
<option value="en_uk_001">English UK - Male 1</option>
<option value="en_uk_003">English UK - Male 2</option>
<option value="en_us_001">English US - Female (Int. 1)</option>
<option value="en_us_002">English US - Female (Int. 2)</option>
<option value="en_us_006">English US - Male 1</option>
<option value="en_us_007">English US - Male 2</option>
<option value="en_us_009">English US - Male 3</option>
<option value="en_us_010">English US - Male 4</option>
<option disabled value="">-----European Voices-----</option>
<option value="fr_001">French - Male 1</option>
<option value="fr_002">French - Male 2</option>
<option value="de_001">German - Female</option>
<option value="de_002">German - Male</option>
<option value="es_002">Spanish - Male</option>
<option disabled value="">-----American Voices-----</option>
<option value="es_mx_002">Spanish MX - Male</option>
<option value="br_001">Portuguese BR - Female 1</option>
<option value="br_003">Portuguese BR - Female 2</option>
<option value="br_004">Portuguese BR - Female 3</option>
<option value="br_005">Portuguese BR - Male</option>
<option disabled value="">-----Asian Voices-----</option>
<option value="id_001">Indonesian - Female</option>
<option value="jp_001">Japanese - Female 1</option>
<option value="jp_003">Japanese - Female 2</option>
<option value="jp_005">Japanese - Female 3</option>
<option value="jp_006">Japanese - Male</option>
<option value="kr_002">Korean - Male 1</option>
<option value="kr_003">Korean - Female</option>
<option value="kr_004">Korean - Male 2</option>
</select>
<button type="button" class="btn btn-primary"><i id="tiktok_icon"
class="bi-volume-up-fill"></i></button>
</div>
</div>
</div>
<div class="row mb-2">
<label for="python_voice" class="col-4">Python Voice</label>
<div class="col-8">
<div class="input-group">
<div class="input-group-text">
<i class="bi bi-mic-fill"></i>
</div>
<input value="{{ data.python_voice }}" name="python_voice" type="text" class="form-control"
data-toggle="tooltip"
data-original-title='The index of the system TTS voices (can be downloaded externally, run ptt.py to find value, start from zero)'>
</div>
</div>
</div>
<div class="row mb-2">
<label for="py_voice_num" class="col-4">Py Voice Number</label>
<div class="col-8">
<div class="input-group">
<div class="input-group-text">
<i class="bi bi-headset"></i>
</div>
<input value="{{ data.py_voice_num }}" name="py_voice_num" type="text" class="form-control"
data-toggle="tooltip"
data-original-title='The number of system voices (2 are pre-installed in Windows)'>
</div>
</div>
</div>
<div class="row mb-2">
<label for="silence_duration" class="col-4">Silence Duration</label>
<div class="col-8">
<div class="input-group">
<input name="silence_duration" type="range" class="form-range" min="0" max="5" step="0.05"
value="{{ data.silence_duration }}" data-toggle="tooltip"
data-original-title="{{ data.silence_duration }}">
</div>
<span class="form-text text-muted">Time in seconds between TTS comments.</span>
</div>
</div>
<div class="col text-center">
<br>
<button id="defaultSettingsBtn" type="button" class="btn btn-secondary">Default
Settings</button>
<button id="submitButton" type="submit" class="btn btn-success">Save
Changes</button>
</div>
</form>
</div>
<audio src=""></audio>
</main>
<script>
// Test voices buttons
var playing = false;
$(".voices button").click(function () {
var icon = $(this).find("i");
var audio = $("audio");
if (playing) {
playing.toggleClass("bi-volume-up-fill bi-stop-fill");
// Clicked the same button - stop audio
if (playing.prop("id") == icon.prop("id")) {
audio[0].pause();
playing = false;
return;
}
}
icon.toggleClass("bi-volume-up-fill bi-stop-fill");
let path = "voices/" + $(this).closest(".voices").find("select").prop("value").toLowerCase() + ".mp3";
audio.prop("src", path);
audio[0].play();
playing = icon;
audio[0].onended = function () {
icon.toggleClass("bi-volume-up-fill bi-stop-fill");
playing = false;
}
});
// Wait for DOM to load
$(document).ready(function () {
// Add tooltips
$('[data-toggle="tooltip"]').tooltip();
$('[data-toggle="tooltip"]').on('click', function () {
$(this).tooltip('hide');
});
// Update slider tooltip
$(".form-range").on("input", function () {
$(this).attr("value", $(this).val());
$(this).attr("data-original-title", $(this).val());
$(this).tooltip("show");
});
// Get current config
var data = JSON.parse('{{data | tojson}}');
// Set current checkboxes
$('.form-check-input').each(function () {
$(this).prop("checked", data[$(this).prop("name")]);
});
// Set current select options
$('.form-select').each(function () {
$(this).prop("value", data[$(this).prop("name")]);
});
// Submit "False" when checkbox isn't ticked
$('#settingsForm').submit(function () {
$('.form-check-input').each(function () {
if (!($(this).is(':checked'))) {
$(this).prop("value", "False");
$(this).prop("checked", true);
}
});
});
// Get validation values
let validateChecks = JSON.parse('{{checks | tojson}}');
// Set default values
$("#defaultSettingsBtn").click(function (event) {
$("#settingsForm input, #settingsForm select").each(function () {
let check = validateChecks[$(this).prop("name")];
if (check["default"]) {
$(this).prop("value", check["default"]);
// Update tooltip value for input[type="range"]
if ($(this).prop("type") == "range") {
$(this).attr("data-original-title", check["default"]);
}
}
});
});
// Validate form
$('#settingsForm').submit(function (event) {
$("#settingsForm input, #settingsForm select").each(function () {
if (!(validate($(this)))) {
event.preventDefault();
event.stopPropagation();
$("html, body").animate({
scrollTop: $(this).offset().top
});
}
});
});
$("#settingsForm input").on("keyup", function () {
validate($(this));
});
$("#settingsForm select").on("change", function () {
validate($(this));
});
function validate(object) {
let bool = check(object.prop("name"), object.prop("value"));
// Change class
if (bool) {
object.removeClass("is-invalid");
object.addClass("is-valid");
}
else {
object.removeClass("is-valid");
object.addClass("is-invalid");
}
return bool;
// Check values (return true/false)
function check(name, value) {
let check = validateChecks[name];
// If value is empty - check if it's optional
if (value.length == 0) {
if (check["optional"] == false) {
return false;
}
else {
object.prop("value", check["default"]);
return true;
}
}
// Check if value is too short
if (check["nmin"]) {
if (check["type"] == "int" || check["type"] == "float") {
if (value < check["nmin"]) {
return false;
}
}
else {
if (value.length < check["nmin"]) {
return false;
}
}
}
// Check if value is too long
if (check["nmax"]) {
if (check["type"] == "int" || check["type"] == "float") {
if (value > check["nmax"]) {
return false;
}
}
else {
if (value.length > check["nmax"]) {
return false;
}
}
}
// Check if value matches regex
if (check["regex"]) {
let regex = new RegExp(check["regex"]);
if (!(regex.test(value))) {
return false;
}
}
return true;
}
}
});
</script>
{% endblock %}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

@ -25,16 +25,16 @@ storymode = { optional = true, type = "bool", default = false, example = false,
[settings.background]
background_choice = { optional = true, default = "minecraft", example = "rocket-league", options = ["minecraft", "gta", "rocket-league", "motor-gta", "csgo-surf", "cluster-truck", ""], explanation = "Sets the background for the video based on game name" }
background_choice = { optional = true, default = "minecraft", example = "rocket-league", options = ["", "minecraft", "gta", "rocket-league", "motor-gta", "csgo-surf", "cluster-truck"], explanation = "Sets the background for the video based on game name" }
#background_audio = { optional = true, type = "bool", default = false, example = false, options = [true, false,], explanation = "Sets a audio to play in the background (put a background.mp3 file in the assets/backgrounds directory for it to be used.)" }
#background_audio_volume = { optional = true, type = "float", default = 0.3, example = 0.1, explanation="Sets the volume of the background audio. only used if the background_audio is also set to true" }
[settings.tts]
voice_choice = { optional = false, default = "", options = ["streamlabspolly", "tiktok", "googletranslate", "awspolly", "pyttsx", ], example = "tiktok", explanation = "The voice platform used for TTS generation. This can be left blank and you will be prompted to choose at runtime." }
voice_choice = { optional = false, default = "tiktok", options = ["streamlabspolly", "tiktok", "googletranslate", "awspolly", "pyttsx", ], example = "tiktok", explanation = "The voice platform used for TTS generation. This can be left blank and you will be prompted to choose at runtime." }
aws_polly_voice = { optional = false, default = "Matthew", example = "Matthew", explanation = "The voice used for AWS Polly" }
streamlabs_polly_voice = { optional = false, default = "Matthew", example = "Matthew", explanation = "The voice used for Streamlabs Polly" }
tiktok_voice = { optional = false, default = "en_us_006", example = "en_us_006", explanation = "The voice used for TikTok TTS" }
python_voice = { optional = false, default = "1", example = "1", explanation = "The index of the system tts voices (can be downloaded externally, run ptt.py to find value, start from zero)" }
py_voice_num = { optional = false, default = "2", example = "2", explanation = "the number of system voices(2 are pre-installed in windows)" }
silence_duration = { optional = true, example = "0.1", explanation = "time in seconds between TTS comments", default = 0.3, type = "float" }
py_voice_num = { optional = false, default = "2", example = "2", explanation = "The number of system voices (2 are pre-installed in Windows)" }
silence_duration = { optional = true, example = "0.1", explanation = "Time in seconds between TTS comments", default = 0.3, type = "float" }

@ -0,0 +1,226 @@
import json
import re
from pathlib import Path
import toml
import tomlkit
from flask import flash
# Get validation checks from template
def get_checks():
template = toml.load("utils/.config.template.toml")
checks = {}
def unpack_checks(obj: dict):
for key in obj.keys():
if "optional" in obj[key].keys():
checks[key] = obj[key]
else:
unpack_checks(obj[key])
unpack_checks(template)
return checks
# Get current config (from config.toml) as dict
def get_config(obj: dict, done={}):
for key in obj.keys():
if not isinstance(obj[key], dict):
done[key] = obj[key]
else:
get_config(obj[key], done)
return done
# Checks if value is valid
def check(value, checks):
incorrect = False
if value == "False":
value = ""
if not incorrect and "type" in checks:
try:
value = eval(checks["type"])(value)
except Exception:
incorrect = True
if (
not incorrect and "options" in checks and value not in checks["options"]
): # FAILSTATE Value is not one of the options
incorrect = True
if (
not incorrect
and "regex" in checks
and (
(isinstance(value, str) and re.match(checks["regex"], value) is None)
or not isinstance(value, str)
)
): # FAILSTATE Value doesn't match regex, or has regex but is not a string.
incorrect = True
if (
not incorrect
and not hasattr(value, "__iter__")
and (
("nmin" in checks and checks["nmin"] is not None and value < checks["nmin"])
or (
"nmax" in checks
and checks["nmax"] is not None
and value > checks["nmax"]
)
)
):
incorrect = True
if (
not incorrect
and hasattr(value, "__iter__")
and (
(
"nmin" in checks
and checks["nmin"] is not None
and len(value) < checks["nmin"]
)
or (
"nmax" in checks
and checks["nmax"] is not None
and len(value) > checks["nmax"]
)
)
):
incorrect = True
if incorrect:
return "Error"
return value
# Modify settings (after form is submitted)
def modify_settings(data: dict, config_load, checks: dict):
# Modify config settings
def modify_config(obj: dict, name: str, value: any):
for key in obj.keys():
if name == key:
obj[key] = value
elif not isinstance(obj[key], dict):
continue
else:
modify_config(obj[key], name, value)
# Remove empty/incorrect key-value pairs
data = {key: value for key, value in data.items() if value and key in checks.keys()}
# Validate values
for name in data.keys():
value = check(data[name], checks[name])
# Value is invalid
if value == "Error":
flash("Some values were incorrect and didn't save!", "error")
else:
# Value is valid
modify_config(config_load, name, value)
# Save changes in config.toml
with Path("config.toml").open("w") as toml_file:
toml_file.write(tomlkit.dumps(config_load))
flash("Settings saved!")
return get_config(config_load)
# Delete background video
def delete_background(key):
# Read backgrounds.json
with open("utils/backgrounds.json", "r", encoding="utf-8") as backgrounds:
data = json.load(backgrounds)
# Remove background from backgrounds.json
with open("utils/backgrounds.json", "w", encoding="utf-8") as backgrounds:
if data.pop(key, None):
json.dump(data, backgrounds, ensure_ascii=False, indent=4)
else:
flash("Couldn't find this background. Try refreshing the page.", "error")
return
# Remove background video from ".config.template.toml"
config = tomlkit.loads(Path("utils/.config.template.toml").read_text())
config["settings"]["background"]["background_choice"]["options"].remove(key)
with Path("utils/.config.template.toml").open("w") as toml_file:
toml_file.write(tomlkit.dumps(config))
flash(f'Successfully removed "{key}" background!')
# Add background video
def add_background(youtube_uri, filename, citation, position):
# Validate YouTube URI
regex = re.compile(r"(?:\/|%3D|v=|vi=)([0-9A-z-_]{11})(?:[%#?&]|$)").search(
youtube_uri
)
if not regex:
flash("YouTube URI is invalid!", "error")
return
youtube_uri = f"https://www.youtube.com/watch?v={regex.group(1)}"
# Check if position is valid
if position == "" or position == "center":
position = "center"
elif position.isdecimal():
position = int(position)
else:
flash('Position is invalid! It can be "center" or decimal number.', "error")
return
# Sanitize filename
regex = re.compile(r"^([a-zA-Z0-9\s_-]{1,100})$").match(filename)
if not regex:
flash("Filename is invalid!", "error")
return
filename = filename.replace(" ", "_")
# Check if background doesn't already exist
with open("utils/backgrounds.json", "r", encoding="utf-8") as backgrounds:
data = json.load(backgrounds)
# Check if key isn't already taken
if filename in list(data.keys()):
flash("Background video with this name already exist!", "error")
return
# Check if the YouTube URI isn't already used under different name
if youtube_uri in [data[i][0] for i in list(data.keys())]:
flash("Background video with this YouTube URI is already added!", "error")
return
# Add background video to json file
with open("utils/backgrounds.json", "r+", encoding="utf-8") as backgrounds:
data = json.load(backgrounds)
data[filename] = [youtube_uri, filename + ".mp4", citation, position]
backgrounds.seek(0)
json.dump(data, backgrounds, ensure_ascii=False, indent=4)
# Add background video to ".config.template.toml"
config = tomlkit.loads(Path("utils/.config.template.toml").read_text())
config["settings"]["background"]["background_choice"]["options"].append(filename)
with Path("utils/.config.template.toml").open("w") as toml_file:
toml_file.write(tomlkit.dumps(config))
flash(f'Added "{citation}-{filename}.mp4" as a new background video!')
return
Loading…
Cancel
Save