Merge branch 'develop' into master
This commit is contained in:
86 files changed
+1667
-471
No files matched your search
@@ -45,7 +45,7 @@ body:
|
||||
|
||||
Python version : [e.g. Python 3.6]
|
||||
|
||||
App version / Branch [e.g. latest, V2.0, master, develop]
|
||||
App version / Branch : [e.g. latest, V2.0, master, develop]
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
blank_issues_enabled: true
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Ask a question
|
||||
about: Join our discord server to ask questions and discuss with maintainers and contributors.
|
||||
|
||||
@@ -243,3 +243,4 @@ video_creation/data/videos.json
|
||||
video_creation/data/envvars.txt
|
||||
|
||||
config.toml
|
||||
video_creation/data/videos.json
|
||||
+2
-2
@@ -28,7 +28,7 @@ All types of contributions are encouraged and valued. See the [Table of Contents
|
||||
|
||||
## I Have a Question
|
||||
|
||||
> If you want to ask a question, we assume that you have read the available [Documentation](https://luka-hietala.gitbook.io/documentation-for-the-reddit-bot/).
|
||||
> If you want to ask a question, we assume that you have read the available [Documentation](https://reddit-video-maker-bot.netlify.app/).
|
||||
|
||||
Before you ask a question, it is best to search for existing [Issues](https://github.com/elebumm/RedditVideoMakerBot/issues) that might help you. In case you have found a suitable issue and still need clarification, you can write your question in this issue. It is also advisable to search the internet for answers first.
|
||||
|
||||
@@ -144,4 +144,4 @@ When making your PR, follow these guidelines:
|
||||
|
||||
### Improving The Documentation
|
||||
|
||||
All updates to the documentation should be made in a pull request to [this repo](https://github.com/LukaHietala/reddit-bot-docs)
|
||||
All updates to the documentation should be made in a pull request to [this repo](https://github.com/LukaHietala/RedditVideoMakerBot-website)
|
||||
@@ -1,32 +1,118 @@
|
||||
# Import the server module
|
||||
import http.server
|
||||
import webbrowser
|
||||
from pathlib import Path
|
||||
|
||||
# Used "tomlkit" instead of "toml" because it doesn't change formatting on "dump"
|
||||
import tomlkit
|
||||
from flask import (
|
||||
Flask,
|
||||
redirect,
|
||||
render_template,
|
||||
request,
|
||||
send_from_directory,
|
||||
url_for,
|
||||
)
|
||||
|
||||
import utils.gui_utils as gui
|
||||
|
||||
# Set the hostname
|
||||
HOST = "localhost"
|
||||
# Set the port number
|
||||
PORT = 4000
|
||||
|
||||
# Define class to display the index page of the web server
|
||||
class PythonServer(http.server.SimpleHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
if self.path == "/GUI":
|
||||
self.path = "index.html"
|
||||
return http.server.SimpleHTTPRequestHandler.do_GET(self)
|
||||
# Configure application
|
||||
app = Flask(__name__, template_folder="GUI")
|
||||
|
||||
# Configure secret key only to use 'flash'
|
||||
app.secret_key = b'_5#y2L"F4Q8z\n\xec]/'
|
||||
|
||||
|
||||
# Declare object of the class
|
||||
webServer = http.server.HTTPServer((HOST, PORT), PythonServer)
|
||||
# Print the URL of the webserver, new =2 opens in a new tab
|
||||
print(f"Server started at http://{HOST}:{PORT}/GUI/")
|
||||
webbrowser.open(f"http://{HOST}:{PORT}/GUI/", new=2)
|
||||
print("Website opened in new tab")
|
||||
print("Press Ctrl+C to quit")
|
||||
try:
|
||||
# Run the web server
|
||||
webServer.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
# Stop the web server
|
||||
webServer.server_close()
|
||||
print("The server is stopped.")
|
||||
exit()
|
||||
# Ensure responses aren't cached
|
||||
@app.after_request
|
||||
def after_request(response):
|
||||
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
|
||||
response.headers["Expires"] = 0
|
||||
response.headers["Pragma"] = "no-cache"
|
||||
return response
|
||||
|
||||
|
||||
# Display index.html
|
||||
@app.route("/")
|
||||
def index():
|
||||
return render_template("index.html", file="videos.json")
|
||||
|
||||
|
||||
@app.route("/backgrounds", methods=["GET"])
|
||||
def backgrounds():
|
||||
return render_template("backgrounds.html", file="backgrounds.json")
|
||||
|
||||
|
||||
@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()
|
||||
|
||||
gui.add_background(youtube_uri, filename, citation, position)
|
||||
|
||||
return redirect(url_for("backgrounds"))
|
||||
|
||||
|
||||
@app.route("/background/delete", methods=["POST"])
|
||||
def background_delete():
|
||||
key = request.form.get("background-key")
|
||||
gui.delete_background(key)
|
||||
|
||||
return redirect(url_for("backgrounds"))
|
||||
|
||||
|
||||
@app.route("/settings", methods=["GET", "POST"])
|
||||
def settings():
|
||||
config_load = tomlkit.loads(Path("config.toml").read_text())
|
||||
config = gui.get_config(config_load)
|
||||
|
||||
# Get checks for all values
|
||||
checks = gui.get_checks()
|
||||
|
||||
if request.method == "POST":
|
||||
# Get data from form as dict
|
||||
data = request.form.to_dict()
|
||||
|
||||
# Change settings
|
||||
config = gui.modify_settings(data, config_load, checks)
|
||||
|
||||
return render_template(
|
||||
"settings.html", file="config.toml", data=config, checks=checks
|
||||
)
|
||||
|
||||
|
||||
# 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")
|
||||
|
||||
|
||||
# Make videos in results folder accessible
|
||||
@app.route("/results/<path:name>")
|
||||
def results(name):
|
||||
return send_from_directory("results", name, as_attachment=True)
|
||||
|
||||
|
||||
# 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
|
||||
if __name__ == "__main__":
|
||||
webbrowser.open(f"http://{HOST}:{PORT}", new=2)
|
||||
print("Website opened in new tab. Refresh if it didn't load.")
|
||||
app.run(port=PORT)
|
||||
@@ -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 %}
|
||||
+115
-214
@@ -1,229 +1,131 @@
|
||||
<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.8.3/font/bootstrap-icons.css">
|
||||
{% extends "layout.html" %}
|
||||
{% block main %}
|
||||
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="album py-2 bg-light">
|
||||
<main>
|
||||
<div class="album py-2 bg-light">
|
||||
<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 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>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</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 © 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>
|
||||
const intervals = [
|
||||
<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 }
|
||||
];
|
||||
];
|
||||
|
||||
function timeSince(date) {
|
||||
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`;
|
||||
}
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
$.getJSON("../video_creation/data/videos.json",
|
||||
$(document).ready(function () {
|
||||
$.getJSON("videos.json",
|
||||
function (data) {
|
||||
data.sort((b, a) => a['time'] - b['time'])
|
||||
var video = '';
|
||||
$.each(data, function (key, value) {
|
||||
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="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>';
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
$('#videos').append(video);
|
||||
});
|
||||
});
|
||||
|
||||
$(document).ready(function(){
|
||||
$(document).ready(function () {
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
$('[data-toggle="tooltip"]').on('click', function(){
|
||||
$(this).tooltip('hide');
|
||||
$('[data-toggle="tooltip"]').on('click', function () {
|
||||
$(this).tooltip('hide');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
$('#copy').tooltip({
|
||||
trigger: 'click',
|
||||
placement: 'bottom'
|
||||
});
|
||||
$('#copy').tooltip({
|
||||
trigger: 'click',
|
||||
placement: 'bottom'
|
||||
});
|
||||
|
||||
function setTooltip(btn, message) {
|
||||
function setTooltip(btn, message) {
|
||||
$(btn).tooltip('hide')
|
||||
.attr('data-original-title', message)
|
||||
.tooltip('show');
|
||||
}
|
||||
.attr('data-original-title', message)
|
||||
.tooltip('show');
|
||||
}
|
||||
|
||||
function hoverTooltip(btn, message) {
|
||||
function hoverTooltip(btn, message) {
|
||||
$(btn).tooltip('hide')
|
||||
.attr('data-original-title', message)
|
||||
.tooltip('show');
|
||||
}
|
||||
.attr('data-original-title', message)
|
||||
.tooltip('show');
|
||||
}
|
||||
|
||||
function hideTooltip(btn) {
|
||||
setTimeout(function() {
|
||||
$(btn).tooltip('hide');
|
||||
function hideTooltip(btn) {
|
||||
setTimeout(function () {
|
||||
$(btn).tooltip('hide');
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
function disposeTooltip(btn) {
|
||||
setTimeout(function() {
|
||||
$(btn).tooltip('dispose');
|
||||
function disposeTooltip(btn) {
|
||||
setTimeout(function () {
|
||||
$(btn).tooltip('dispose');
|
||||
}, 1500);
|
||||
}
|
||||
}
|
||||
|
||||
var clipboard = new ClipboardJS('#copy');
|
||||
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);
|
||||
});
|
||||
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);
|
||||
});
|
||||
|
||||
clipboard.on('error', function(e) {
|
||||
console.error('Action:', e.action);
|
||||
console.error('Trigger:', e.trigger);
|
||||
setTooltip(e.trigger, fallbackMessage(e.action));
|
||||
hideTooltip(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);
|
||||
});
|
||||
|
||||
function getCopyData(subreddit, reddit_title, filename, background_credit){
|
||||
function getCopyData(subreddit, reddit_title, filename, background_credit) {
|
||||
|
||||
if (subreddit == undefined) {
|
||||
subredditCopy = "";
|
||||
@@ -245,37 +147,36 @@
|
||||
return copyData;
|
||||
}
|
||||
|
||||
function getLink(subreddit, id, reddit_title) {
|
||||
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>";
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
function checkTitle(reddit_title, filename) {
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
+142
@@ -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 © 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.
@@ -39,12 +39,11 @@ The only original thing being done is the editing and gathering of all materials
|
||||
|
||||
1. Clone this repository
|
||||
2. Run `pip install -r requirements.txt`
|
||||
|
||||
3. Run `python -m playwright install` and `python -m playwright install-deps`
|
||||
|
||||
**EXPERIMENTAL!!!!**
|
||||
|
||||
On MacOS and Linux (debian, arch, fedora and centos, and based on those), you can run an install script that will automatically install steps 1 to 3. (requires bash)
|
||||
On macOS and Linux (debian, arch, fedora and centos, and based on those), you can run an install script that will automatically install steps 1 to 3. (requires bash)
|
||||
|
||||
`bash <(curl -sL https://raw.githubusercontent.com/elebumm/RedditVideoMakerBot/master/install.sh)`
|
||||
|
||||
@@ -58,7 +57,7 @@ This can also be used to update the installation
|
||||
|
||||
(Note if you got an error installing or running the bot try first rerunning the command with a three after the name e.g. python3 or pip3)
|
||||
|
||||
If you want to read more detailed guide about the bot, please refer to the [documentation](https://luka-hietala.gitbook.io/documentation-for-the-reddit-bot/)
|
||||
If you want to read more detailed guide about the bot, please refer to the [documentation](https://reddit-video-maker-bot.netlify.app/)
|
||||
|
||||
## Video
|
||||
|
||||
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
import random
|
||||
from utils import settings
|
||||
|
||||
from gtts import gTTS
|
||||
|
||||
max_chars = 0
|
||||
from utils import settings
|
||||
|
||||
|
||||
class GTTS:
|
||||
|
||||
+5
-8
@@ -1,9 +1,11 @@
|
||||
import base64
|
||||
from utils import settings
|
||||
import random
|
||||
|
||||
import requests
|
||||
from requests.adapters import HTTPAdapter, Retry
|
||||
|
||||
from utils import settings
|
||||
|
||||
# from profanity_filter import ProfanityFilter
|
||||
# pf = ProfanityFilter()
|
||||
# Code by @JasonLovesDoggo
|
||||
@@ -62,9 +64,7 @@ noneng = [
|
||||
|
||||
class TikTok: # TikTok Text-to-Speech Wrapper
|
||||
def __init__(self):
|
||||
self.URI_BASE = (
|
||||
"https://api16-normal-useast5.us.tiktokv.com/media/api/text/speech/invoke/?text_speaker="
|
||||
)
|
||||
self.URI_BASE = "https://api16-normal-useast5.us.tiktokv.com/media/api/text/speech/invoke/?text_speaker="
|
||||
self.max_chars = 300
|
||||
self.voices = {"human": human, "nonhuman": nonhuman, "noneng": noneng}
|
||||
|
||||
@@ -75,10 +75,7 @@ class TikTok: # TikTok Text-to-Speech Wrapper
|
||||
voice = (
|
||||
self.randomvoice()
|
||||
if random_voice
|
||||
else (
|
||||
settings.config["settings"]["tts"]["tiktok_voice"]
|
||||
or random.choice(self.voices["human"])
|
||||
)
|
||||
else (settings.config["settings"]["tts"]["tiktok_voice"] or random.choice(self.voices["human"]))
|
||||
)
|
||||
try:
|
||||
r = requests.post(f"{self.URI_BASE}{voice}&req_text={text}&speaker_map_type=0")
|
||||
|
||||
+6
-8
@@ -1,9 +1,11 @@
|
||||
#!/usr/bin/env python3
|
||||
import random
|
||||
import sys
|
||||
|
||||
from boto3 import Session
|
||||
from botocore.exceptions import BotoCoreError, ClientError, ProfileNotFound
|
||||
import sys
|
||||
|
||||
from utils import settings
|
||||
import random
|
||||
|
||||
voices = [
|
||||
"Brian",
|
||||
@@ -37,15 +39,11 @@ class AWSPolly:
|
||||
voice = self.randomvoice()
|
||||
else:
|
||||
if not settings.config["settings"]["tts"]["aws_polly_voice"]:
|
||||
raise ValueError(
|
||||
f"Please set the TOML variable AWS_VOICE to a valid voice. options are: {voices}"
|
||||
)
|
||||
raise ValueError(f"Please set the TOML variable AWS_VOICE to a valid voice. options are: {voices}")
|
||||
voice = str(settings.config["settings"]["tts"]["aws_polly_voice"]).capitalize()
|
||||
try:
|
||||
# Request speech synthesis
|
||||
response = polly.synthesize_speech(
|
||||
Text=text, OutputFormat="mp3", VoiceId=voice, Engine="neural"
|
||||
)
|
||||
response = polly.synthesize_speech(Text=text, OutputFormat="mp3", VoiceId=voice, Engine="neural")
|
||||
except (BotoCoreError, ClientError) as error:
|
||||
# The service returned an error, exit gracefully
|
||||
print(error)
|
||||
|
||||
+62
-35
@@ -1,17 +1,22 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Tuple
|
||||
import re
|
||||
|
||||
# import sox
|
||||
# from mutagen import MutagenError
|
||||
# from mutagen.mp3 import MP3, HeaderNotFoundError
|
||||
import numpy as np
|
||||
import translators as ts
|
||||
from moviepy.audio.AudioClip import AudioClip
|
||||
from moviepy.audio.fx.volumex import volumex
|
||||
from moviepy.editor import AudioFileClip
|
||||
from rich.progress import track
|
||||
from moviepy.editor import AudioFileClip, CompositeAudioClip, concatenate_audioclips
|
||||
|
||||
from utils import settings
|
||||
from utils.console import print_step, print_substep
|
||||
from utils.voice import sanitize_text
|
||||
from utils import settings
|
||||
|
||||
DEFAULT_MAX_LENGTH: int = 40 # video length variable
|
||||
|
||||
@@ -20,7 +25,7 @@ class TTSEngine:
|
||||
"""Calls the given TTS engine to reduce code duplication and allow multiple TTS engines.
|
||||
|
||||
Args:
|
||||
tts_module : The TTS module. Your module should handle the TTS itself and saving to the given path under the run method.
|
||||
tts_module : The TTS module. Your module should handle the TTS itself and saving to the given path under the run method.
|
||||
reddit_object : The reddit object that contains the posts to read.
|
||||
path (Optional) : The unix style path to save the mp3 files to. This must not have leading or trailing slashes.
|
||||
max_length (Optional) : The maximum length of the mp3 files in total.
|
||||
@@ -54,6 +59,7 @@ class TTSEngine:
|
||||
self.call_tts("title", process_text(self.reddit_object["thread_title"]))
|
||||
# processed_text = ##self.reddit_object["thread_post"] != ""
|
||||
idx = None
|
||||
|
||||
if settings.config["settings"]["storymode"] :
|
||||
if settings.config["settings"]["storymodemethode"] == 0:
|
||||
if (len(self.reddit_object["thread_post"]) > self.tts_module.max_chars):
|
||||
@@ -86,50 +92,71 @@ class TTSEngine:
|
||||
def split_post(self, text: str, idx):
|
||||
split_files = []
|
||||
split_text = [
|
||||
x.group().strip()
|
||||
for x in re.finditer(
|
||||
r" *(((.|\n){0," + str(self.tts_module.max_chars) + "})(\.|.$))", text
|
||||
)
|
||||
x.group().strip() for x in re.finditer(r" *(((.|\n){0," + str(self.tts_module.max_chars) + "})(\.|.$))", text)
|
||||
]
|
||||
offset = 0
|
||||
self.create_silence_mp3()
|
||||
|
||||
idy = None
|
||||
for idy, text_cut in enumerate(split_text):
|
||||
# print(f"{idx}-{idy}: {text_cut}\n")
|
||||
new_text = process_text(text_cut)
|
||||
if not new_text or new_text.isspace():
|
||||
offset += 1
|
||||
newtext = process_text(text_cut)
|
||||
# print(f"{idx}-{idy}: {newtext}\n")
|
||||
|
||||
if not newtext or newtext.isspace():
|
||||
print("newtext was blank because sanitized split text resulted in none")
|
||||
continue
|
||||
else:
|
||||
self.call_tts(f"{idx}-{idy}.part", newtext)
|
||||
with open(f"{self.path}/list.txt", 'w') as f:
|
||||
for idz in range(0, len(split_text)):
|
||||
f.write("file " + f"'{idx}-{idz}.part.mp3'" + "\n")
|
||||
split_files.append(str(f"{self.path}/{idx}-{idy}.part.mp3"))
|
||||
f.write("file " + f"'silence.mp3'" + "\n")
|
||||
|
||||
self.call_tts(f"{idx}-{idy - offset}.part", new_text)
|
||||
split_files.append(AudioFileClip(f"{self.path}/{idx}-{idy - offset}.part.mp3"))
|
||||
|
||||
CompositeAudioClip([concatenate_audioclips(split_files)]).write_audiofile(
|
||||
f"{self.path}/{idx}.mp3", fps=44100, verbose=False, logger=None
|
||||
)
|
||||
|
||||
for i in split_files:
|
||||
name = i.filename
|
||||
i.close()
|
||||
Path(name).unlink()
|
||||
|
||||
# for i in range(0, idy + 1):
|
||||
# print(f"Cleaning up {self.path}/{idx}-{i}.part.mp3")
|
||||
|
||||
# Path(f"{self.path}/{idx}-{i}.part.mp3").unlink()
|
||||
os.system("ffmpeg -f concat -y -hide_banner -loglevel panic -safe 0 " +
|
||||
"-i " + f"{self.path}/list.txt " +
|
||||
"-c copy " + f"{self.path}/{idx}.mp3")
|
||||
try:
|
||||
for i in range(0, len(split_files)):
|
||||
os.unlink(split_files[i])
|
||||
except FileNotFoundError as e:
|
||||
print("File not found: " + e.filename)
|
||||
except OSError:
|
||||
print("OSError")
|
||||
|
||||
def call_tts(self, filename: str, text: str):
|
||||
self.tts_module.run(text, filepath=f"{self.path}/{filename}.mp3")
|
||||
# try:
|
||||
# self.length += MP3(f"{self.path}/{filename}.mp3").info.length
|
||||
# except (MutagenError, HeaderNotFoundError):
|
||||
# self.length += sox.file_info.duration(f"{self.path}/{filename}.mp3")
|
||||
|
||||
try:
|
||||
self.tts_module.run(text, filepath=f"{self.path}/{filename}_no_silence.mp3")
|
||||
self.create_silence_mp3()
|
||||
|
||||
with open(f"{self.path}/{filename}.txt", 'w') as f:
|
||||
f.write("file " + f"'{filename}_no_silence.mp3'" + "\n")
|
||||
f.write("file " + f"'silence.mp3'" + "\n")
|
||||
f.close()
|
||||
os.system("ffmpeg -f concat -y -hide_banner -loglevel panic -safe 0 " +
|
||||
"-i " + f"{self.path}/{filename}.txt " +
|
||||
"-c copy " + f"{self.path}/{filename}.mp3")
|
||||
clip = AudioFileClip(f"{self.path}/{filename}.mp3")
|
||||
self.last_clip_length = clip.duration
|
||||
self.length += clip.duration
|
||||
self.last_clip_length = clip.duration
|
||||
clip.close()
|
||||
try:
|
||||
name = [f"{filename}_no_silence.mp3", "silence.mp3", f"{filename}.txt"]
|
||||
for i in range(0, len(name)):
|
||||
os.unlink(str(rf"{self.path}/" + name[i]))
|
||||
except FileNotFoundError as e:
|
||||
print("File not found: " + e.filename)
|
||||
except OSError:
|
||||
print("OSError")
|
||||
except:
|
||||
self.length = 0
|
||||
|
||||
def create_silence_mp3(self):
|
||||
silence_duration = settings.config["settings"]["tts"]["silence_duration"]
|
||||
silence = AudioClip(make_frame=lambda t: np.sin(440 * 2 * np.pi * t), duration=silence_duration, fps=44100)
|
||||
silence = volumex(silence, 0)
|
||||
silence.write_audiofile(f"{self.path}/silence.mp3", fps=44100, verbose=False, logger=None)
|
||||
|
||||
|
||||
def process_text(text: str):
|
||||
lang = settings.config["reddit"]["thread"]["post_lang"]
|
||||
|
||||
+3
-3
@@ -1,5 +1,7 @@
|
||||
import random
|
||||
|
||||
import pyttsx3
|
||||
|
||||
from utils import settings
|
||||
|
||||
|
||||
@@ -30,9 +32,7 @@ class pyttsx:
|
||||
voice_id = self.randomvoice()
|
||||
engine = pyttsx3.init()
|
||||
voices = engine.getProperty("voices")
|
||||
engine.setProperty(
|
||||
"voice", voices[voice_id].id
|
||||
) # changing index changes voices but ony 0 and 1 are working here
|
||||
engine.setProperty("voice", voices[voice_id].id) # changing index changes voices but ony 0 and 1 are working here
|
||||
engine.save_to_file(text, f"{filepath}")
|
||||
engine.runAndWait()
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import random
|
||||
|
||||
import requests
|
||||
from requests.exceptions import JSONDecodeError
|
||||
|
||||
from utils import settings
|
||||
from utils.voice import check_ratelimit
|
||||
|
||||
@@ -37,9 +39,7 @@ class StreamlabsPolly:
|
||||
voice = self.randomvoice()
|
||||
else:
|
||||
if not settings.config["settings"]["tts"]["streamlabs_polly_voice"]:
|
||||
raise ValueError(
|
||||
f"Please set the config variable STREAMLABS_POLLY_VOICE to a valid voice. options are: {voices}"
|
||||
)
|
||||
raise ValueError(f"Please set the config variable STREAMLABS_POLLY_VOICE to a valid voice. options are: {voices}")
|
||||
voice = str(settings.config["settings"]["tts"]["streamlabs_polly_voice"]).capitalize()
|
||||
body = {"voice": voice, "text": text, "service": "polly"}
|
||||
response = requests.post(self.url, data=body)
|
||||
|
||||
+7
-1
@@ -62,7 +62,13 @@ function install_macos(){
|
||||
fi
|
||||
# Install the required packages
|
||||
echo "Installing required Packages"
|
||||
brew install python@3.10 tcl-tk python-tk
|
||||
if [! command --version python3 &> /dev/null ]; then
|
||||
echo "Installing python3"
|
||||
brew install python@3.10
|
||||
else
|
||||
echo "python3 already installed."
|
||||
fi
|
||||
brew install tcl-tk python-tk
|
||||
}
|
||||
|
||||
# Function to install for arch (and other forks like manjaro)
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
#!/usr/bin/env python
|
||||
import math
|
||||
import re
|
||||
from subprocess import Popen
|
||||
import sys
|
||||
from os import name
|
||||
from subprocess import Popen
|
||||
|
||||
from pathlib import Path
|
||||
from prawcore import ResponseException
|
||||
|
||||
from reddit.subreddit import get_subreddit_threads
|
||||
from utils.cleanup import cleanup
|
||||
from utils.console import print_markdown, print_step, print_substep
|
||||
from utils import settings
|
||||
from utils.cleanup import cleanup
|
||||
from utils.console import print_markdown, print_step
|
||||
from utils.id import id
|
||||
from utils.version import checkversion
|
||||
|
||||
from video_creation.background import (
|
||||
download_background,
|
||||
chop_background_video,
|
||||
@@ -22,7 +22,7 @@ from video_creation.final_video import make_final_video
|
||||
from video_creation.screenshot_downloader import download_screenshots_of_reddit_posts
|
||||
from video_creation.voices import save_text_to_mp3
|
||||
|
||||
__VERSION__ = "2.4.1"
|
||||
__VERSION__ = "2.4.2"
|
||||
|
||||
print(
|
||||
"""
|
||||
@@ -36,7 +36,7 @@ print(
|
||||
)
|
||||
# Modified by JasonLovesDoggo
|
||||
print_markdown(
|
||||
"### Thanks for using this tool! [Feel free to contribute to this project on GitHub!](https://lewismenelaws.com) If you have any questions, feel free to reach out to me on Twitter or submit a GitHub issue. You can find solutions to many common problems in the [Documentation](https://luka-hietala.gitbook.io/documentation-for-the-reddit-bot/)"
|
||||
"### Thanks for using this tool! [Feel free to contribute to this project on GitHub!](https://lewismenelaws.com) If you have any questions, feel free to reach out to me on Twitter or submit a GitHub issue. You can find solutions to many common problems in the [Documentation](): https://reddit-video-maker-bot.netlify.app/"
|
||||
)
|
||||
checkversion(__VERSION__)
|
||||
|
||||
@@ -75,14 +75,14 @@ def shutdown():
|
||||
print("Exiting...")
|
||||
exit()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
config = settings.check_toml("utils/.config.template.toml", "config.toml")
|
||||
assert sys.version_info >= (3, 9), "Python 3.10 or higher is required"
|
||||
directory = Path().absolute()
|
||||
config = settings.check_toml(f"{directory}/utils/.config.template.toml", "config.toml")
|
||||
config is False and exit()
|
||||
try:
|
||||
if config["settings"]["times_to_run"]:
|
||||
run_many(config["settings"]["times_to_run"])
|
||||
|
||||
elif len(config["reddit"]["thread"]["post_id"].split("+")) > 1:
|
||||
if len(config["reddit"]["thread"]["post_id"].split("+")) > 1:
|
||||
for index, post_id in enumerate(config["reddit"]["thread"]["post_id"].split("+")):
|
||||
index += 1
|
||||
print_step(
|
||||
@@ -90,6 +90,8 @@ if __name__ == "__main__":
|
||||
)
|
||||
main(post_id)
|
||||
Popen("cls" if name == "nt" else "clear", shell=True).wait()
|
||||
elif config["settings"]["times_to_run"]:
|
||||
run_many(config["settings"]["times_to_run"])
|
||||
else:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
|
||||
+6
-6
@@ -1,11 +1,10 @@
|
||||
import re
|
||||
|
||||
import praw
|
||||
from praw.models import MoreComments
|
||||
from prawcore.exceptions import ResponseException
|
||||
|
||||
from utils import settings
|
||||
import praw
|
||||
from praw.models import MoreComments
|
||||
|
||||
from utils.console import print_step, print_substep
|
||||
from utils.subreddit import get_subreddit_undone
|
||||
from utils.videos import check_done
|
||||
@@ -42,9 +41,8 @@ def get_subreddit_threads(POST_ID: str):
|
||||
check_for_async=False,
|
||||
)
|
||||
except ResponseException as e:
|
||||
match e.response.status_code:
|
||||
case 401:
|
||||
print("Invalid credentials - please check them in config.toml")
|
||||
if e.response.status_code == 401:
|
||||
print("Invalid credentials - please check them in config.toml")
|
||||
except:
|
||||
print("Something went wrong...")
|
||||
|
||||
@@ -105,6 +103,7 @@ def get_subreddit_threads(POST_ID: str):
|
||||
for top_level_comment in submission.comments:
|
||||
if isinstance(top_level_comment, MoreComments):
|
||||
continue
|
||||
|
||||
if top_level_comment.body in ["[removed]", "[deleted]"]:
|
||||
continue # # see https://github.com/JasonLovesDoggo/RedditVideoMakerBot/issues/78
|
||||
if not top_level_comment.stickied:
|
||||
@@ -129,5 +128,6 @@ def get_subreddit_threads(POST_ID: str):
|
||||
"comment_id": top_level_comment.id,
|
||||
}
|
||||
)
|
||||
|
||||
print_substep("Received subreddit threads Successfully.", style="bold green")
|
||||
return content
|
||||
@@ -4,6 +4,7 @@ gTTS==2.2.4
|
||||
moviepy==1.0.3
|
||||
playwright==1.23.0
|
||||
praw==7.6.0
|
||||
prawcore~=2.3.0
|
||||
pytube==12.1.0
|
||||
requests==2.28.1
|
||||
rich==12.5.1
|
||||
@@ -11,3 +12,5 @@ toml==0.10.2
|
||||
translators==5.3.1
|
||||
pyttsx3==2.90
|
||||
Pillow~=9.1.1
|
||||
tomlkit==0.11.4
|
||||
Flask==2.2.2
|
||||
@@ -3,12 +3,12 @@ client_id = { optional = false, nmin = 12, nmax = 30, explanation = "The ID of y
|
||||
client_secret = { optional = false, nmin = 20, nmax = 40, explanation = "The SECRET of your Reddit app of SCRIPT type", example = "fFAGRNJru1FTz70BzhT3Zg", regex = "^[-a-zA-Z0-9._~+/]+=*$", input_error = "The client ID can only contain printable characters.", oob_error = "The secret should be over 20 and under 40 characters, double check your input." }
|
||||
username = { optional = false, nmin = 3, nmax = 20, explanation = "The username of your reddit account", example = "JasonLovesDoggo", regex = "^[-_0-9a-zA-Z]+$", oob_error = "A username HAS to be between 3 and 20 characters" }
|
||||
password = { optional = false, nmin = 8, explanation = "The password of your reddit account", example = "fFAGRNJru1FTz70BzhT3Zg", oob_error = "Password too short" }
|
||||
2fa = { optional = true, type = "bool", options = [true, false,], default = false, explanation = "Whether you have Reddit 2FA enabled, Valid options are True and False", example = true }
|
||||
2fa = { optional = true, type = "bool", options = [true, false, ], default = false, explanation = "Whether you have Reddit 2FA enabled, Valid options are True and False", example = true }
|
||||
|
||||
|
||||
[reddit.thread]
|
||||
random = { optional = true, options = [true, false,], default = false, type = "bool", explanation = "If set to no, it will ask you a thread link to extract the thread, if yes it will randomize it. Default: 'False'", example = "True" }
|
||||
subreddit = { optional = false, regex = "[_0-9a-zA-Z]+$", nmin = 3, explanation = "What subreddit to pull posts from, the name of the sub, not the URL. You can have multiple subreddits, add an + with no spaces.", example = "AskReddit+Redditdev", oob_error = "A subreddit name HAS to be between 3 and 20 characters" }
|
||||
random = { optional = true, options = [true, false, ], default = false, type = "bool", explanation = "If set to no, it will ask you a thread link to extract the thread, if yes it will randomize it. Default: 'False'", example = "True" }
|
||||
subreddit = { optional = false, regex = "[_0-9a-zA-Z\\+]+$", nmin = 3, explanation = "What subreddit to pull posts from, the name of the sub, not the URL. You can have multiple subreddits, add an + with no spaces.", example = "AskReddit+Redditdev", oob_error = "A subreddit name HAS to be between 3 and 20 characters" }
|
||||
post_id = { optional = true, default = "", regex = "^((?!://|://)[+a-zA-Z0-9])*$", explanation = "Used if you want to use a specific post.", example = "urdtfx" }
|
||||
max_comment_length = { default = 500, optional = false, nmin = 10, nmax = 10000, type = "int", explanation = "max number of characters a comment can have. default is 500", example = 500, oob_error = "the max comment length should be between 10 and 10000" }
|
||||
min_comment_length = { default = 1, optional = true, nmin = 0, nmax = 10000, type = "int", explanation = "min_comment_length number of characters a comment can have. default is 0", example = 50, oob_error = "the max comment length should be between 1 and 100" }
|
||||
@@ -17,24 +17,26 @@ min_comments = { default = 20, optional = false, nmin = 10, type = "int", explan
|
||||
|
||||
|
||||
[settings]
|
||||
allow_nsfw = { optional = false, type = "bool", default = false, example = false, options = [true, false,], explanation = "Whether to allow NSFW content, True or False" }
|
||||
theme = { optional = false, default = "dark", example = "light", options = ["dark", "light",], explanation = "Sets the Reddit theme, either LIGHT or DARK" }
|
||||
allow_nsfw = { optional = false, type = "bool", default = false, example = false, options = [true, false, ], explanation = "Whether to allow NSFW content, True or False" }
|
||||
theme = { optional = false, default = "dark", example = "light", options = ["dark", "light", ], explanation = "Sets the Reddit theme, either LIGHT or DARK" }
|
||||
times_to_run = { optional = false, default = 1, example = 2, explanation = "Used if you want to run multiple times. Set to an int e.g. 4 or 29 or 1", type = "int", nmin = 1, oob_error = "It's very hard to run something less than once." }
|
||||
opacity = { optional = false, default = 0.9, example = 0.8, explanation = "Sets the opacity of the comments when overlayed over the background", type = "float", nmin = 0, nmax = 1, oob_error = "The opacity HAS to be between 0 and 1", input_error = "The opacity HAS to be a decimal number between 0 and 1" }
|
||||
transition = { optional = true, default = 0.2, example = 0.2, explanation = "Sets the transition time (in seconds) between the comments. Set to 0 if you want to disable it.", type = "float", nmin = 0, nmax = 2, oob_error = "The transition HAS to be between 0 and 2", input_error = "The opacity HAS to be a decimal number between 0 and 2" }
|
||||
storymode = { optional = true, type = "bool", default = false, example = false, options = [true, false,], explanation = "Only read out title and post content, not yet implemented" }
|
||||
storymodemethode= { optional = true, default = 1, example = 1, explanation = "Method to for storymode. Set to an int e.g.0 or 1", type = "int", nmin = 0, oob_error = "It's very hard to run something less than once." }
|
||||
|
||||
|
||||
[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)"}
|
||||
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" }
|
||||
@@ -1,45 +0,0 @@
|
||||
# Supported Background. Can add/remove background video here....
|
||||
# <key>-<value> : key -> used as keyword for TOML file. value -> background configuration
|
||||
# Format (value):
|
||||
# 1. Youtube URI
|
||||
# 2. filename
|
||||
# 3. Citation (owner of the video)
|
||||
# 4. Position of image clips in the background. See moviepy reference for more information. (https://zulko.github.io/moviepy/ref/VideoClip/VideoClip.html#moviepy.video.VideoClip.VideoClip.set_position)
|
||||
background_options = {
|
||||
"motor-gta": ( # Motor-GTA Racing
|
||||
"https://www.youtube.com/watch?v=vw5L4xCPy9Q",
|
||||
"bike-parkour-gta.mp4",
|
||||
"Achy Gaming",
|
||||
lambda t: ("center", 480 + t),
|
||||
),
|
||||
"rocket-league": ( # Rocket League
|
||||
"https://www.youtube.com/watch?v=2X9QGY__0II",
|
||||
"rocket_league.mp4",
|
||||
"Orbital Gameplay",
|
||||
lambda t: ("center", 200 + t),
|
||||
),
|
||||
"minecraft": ( # Minecraft parkour
|
||||
"https://www.youtube.com/watch?v=n_Dv4JMiwK8",
|
||||
"parkour.mp4",
|
||||
"bbswitzer",
|
||||
"center",
|
||||
),
|
||||
"gta": ( # GTA Stunt Race
|
||||
"https://www.youtube.com/watch?v=qGa9kWREOnE",
|
||||
"gta-stunt-race.mp4",
|
||||
"Achy Gaming",
|
||||
lambda t: ("center", 480 + t),
|
||||
),
|
||||
"csgo-surf": ( # CSGO Surf
|
||||
"https://www.youtube.com/watch?v=E-8JlyO59Io",
|
||||
"csgo-surf.mp4",
|
||||
"Aki",
|
||||
"center",
|
||||
),
|
||||
"cluster-truck": ( # Cluster Truck Gameplay
|
||||
"https://www.youtube.com/watch?v=uVKxtdMgJVU",
|
||||
"cluster_truck.mp4",
|
||||
"No Copyright Gameplay",
|
||||
lambda t: ("center", 480 + t),
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"__comment": "Supported Backgrounds. Can add/remove background video here...",
|
||||
"motor-gta": [
|
||||
"https://www.youtube.com/watch?v=vw5L4xCPy9Q",
|
||||
"bike-parkour-gta.mp4",
|
||||
"Achy Gaming",
|
||||
480
|
||||
],
|
||||
"rocket-league": [
|
||||
"https://www.youtube.com/watch?v=2X9QGY__0II",
|
||||
"rocket_league.mp4",
|
||||
"Orbital Gameplay",
|
||||
200
|
||||
],
|
||||
"minecraft": [
|
||||
"https://www.youtube.com/watch?v=n_Dv4JMiwK8",
|
||||
"parkour.mp4",
|
||||
"bbswitzer",
|
||||
"center"
|
||||
],
|
||||
"gta": [
|
||||
"https://www.youtube.com/watch?v=qGa9kWREOnE",
|
||||
"gta-stunt-race.mp4",
|
||||
"Achy Gaming",
|
||||
480
|
||||
],
|
||||
"csgo-surf": [
|
||||
"https://www.youtube.com/watch?v=E-8JlyO59Io",
|
||||
"csgo-surf.mp4",
|
||||
"Aki",
|
||||
"center"
|
||||
],
|
||||
"cluster-truck": [
|
||||
"https://www.youtube.com/watch?v=uVKxtdMgJVU",
|
||||
"cluster_truck.mp4",
|
||||
"No Copyright Gameplay",
|
||||
480
|
||||
]
|
||||
}
|
||||
+8
-23
@@ -1,11 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
import re
|
||||
|
||||
from rich.columns import Columns
|
||||
from rich.console import Console
|
||||
from rich.markdown import Markdown
|
||||
from rich.padding import Padding
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
from rich.columns import Columns
|
||||
import re
|
||||
|
||||
console = Console()
|
||||
|
||||
@@ -54,11 +55,7 @@ def handle_input(
|
||||
return default if default is not NotImplemented else ""
|
||||
if default is not NotImplemented:
|
||||
console.print(
|
||||
"[green]"
|
||||
+ message
|
||||
+ '\n[blue bold]The default value is "'
|
||||
+ str(default)
|
||||
+ '"\nDo you want to use it?(y/n)'
|
||||
"[green]" + message + '\n[blue bold]The default value is "' + str(default) + '"\nDo you want to use it?(y/n)'
|
||||
)
|
||||
if input().casefold().startswith("y"):
|
||||
return default
|
||||
@@ -71,9 +68,7 @@ def handle_input(
|
||||
if check_type is not False:
|
||||
try:
|
||||
user_input = check_type(user_input)
|
||||
if (nmin is not None and user_input < nmin) or (
|
||||
nmax is not None and user_input > nmax
|
||||
):
|
||||
if (nmin is not None and user_input < nmin) or (nmax is not None and user_input > nmax):
|
||||
# FAILSTATE Input out of bounds
|
||||
console.print("[red]" + oob_error)
|
||||
continue
|
||||
@@ -89,9 +84,7 @@ def handle_input(
|
||||
continue
|
||||
else:
|
||||
# FAILSTATE Input STRING out of bounds
|
||||
if (nmin is not None and len(user_input) < nmin) or (
|
||||
nmax is not None and len(user_input) > nmax
|
||||
):
|
||||
if (nmin is not None and len(user_input) < nmin) or (nmax is not None and len(user_input) > nmax):
|
||||
console.print("[red bold]" + oob_error)
|
||||
continue
|
||||
break # SUCCESS Input STRING in bounds
|
||||
@@ -105,16 +98,8 @@ def handle_input(
|
||||
isinstance(eval(user_input), check_type)
|
||||
return check_type(user_input)
|
||||
except:
|
||||
console.print(
|
||||
"[red bold]"
|
||||
+ err_message
|
||||
+ "\nValid options are: "
|
||||
+ ", ".join(map(str, options))
|
||||
+ "."
|
||||
)
|
||||
console.print("[red bold]" + err_message + "\nValid options are: " + ", ".join(map(str, options)) + ".")
|
||||
continue
|
||||
if user_input in options:
|
||||
return user_input
|
||||
console.print(
|
||||
"[red bold]" + err_message + "\nValid options are: " + ", ".join(map(str, options)) + "."
|
||||
)
|
||||
console.print("[red bold]" + err_message + "\nValid options are: " + ", ".join(map(str, options)) + ".")
|
||||
@@ -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
|
||||
+3
-1
@@ -1,10 +1,12 @@
|
||||
import re
|
||||
|
||||
from utils.console import print_substep
|
||||
|
||||
|
||||
def id(reddit_obj: dict):
|
||||
"""
|
||||
This function takes a reddit object and returns the post id
|
||||
"""
|
||||
id = re.sub(r"[^\w\s-]", "", reddit_obj["thread_id"])
|
||||
print_substep(f"Thread ID is {id}", style="bold blue")
|
||||
return id
|
||||
return id
|
||||
+8
-15
@@ -1,13 +1,12 @@
|
||||
#!/usr/bin/env python
|
||||
import re
|
||||
from typing import Tuple, Dict
|
||||
from pathlib import Path
|
||||
import toml
|
||||
from rich.console import Console
|
||||
import re
|
||||
|
||||
from typing import Tuple, Dict
|
||||
|
||||
from utils.console import handle_input
|
||||
|
||||
|
||||
console = Console()
|
||||
config = dict # autocomplete
|
||||
|
||||
@@ -35,17 +34,12 @@ def check(value, checks, name):
|
||||
except:
|
||||
incorrect = True
|
||||
|
||||
if (
|
||||
not incorrect and "options" in checks and value not in checks["options"]
|
||||
): # FAILSTATE Value is not one of the options
|
||||
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)
|
||||
)
|
||||
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
|
||||
|
||||
@@ -85,9 +79,7 @@ def check(value, checks, name):
|
||||
err_message=get_check_value("input_error", "Incorrect input"),
|
||||
nmin=get_check_value("nmin", None),
|
||||
nmax=get_check_value("nmax", None),
|
||||
oob_error=get_check_value(
|
||||
"oob_error", "Input out of bounds(Value too high/low/long/short)"
|
||||
),
|
||||
oob_error=get_check_value("oob_error", "Input out of bounds(Value too high/low/long/short)"),
|
||||
options=get_check_value("options", None),
|
||||
optional=get_check_value("optional", False),
|
||||
)
|
||||
@@ -167,4 +159,5 @@ If you see any prompts, that means that you have unset/incorrectly set variables
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
check_toml("utils/.config.template.toml", "config.toml")
|
||||
directory = Path().absolute()
|
||||
check_toml(f"{directory}/utils/.config.template.toml", "config.toml")
|
||||
+1
-3
@@ -57,9 +57,7 @@ def get_subreddit_undone(submissions: list, subreddit, times_checked=0):
|
||||
print("all time filters have been checked you absolute madlad ")
|
||||
|
||||
return get_subreddit_undone(
|
||||
subreddit.top(
|
||||
time_filter=VALID_TIME_FILTERS[index], limit=(50 if int(index) == 0 else index + 1 * 50)
|
||||
),
|
||||
subreddit.top(time_filter=VALID_TIME_FILTERS[index], limit=(50 if int(index) == 0 else index + 1 * 50)),
|
||||
subreddit,
|
||||
times_checked=index,
|
||||
) # all the videos in hot have already been done
|
||||
|
||||
+2
-3
@@ -1,11 +1,10 @@
|
||||
import requests
|
||||
|
||||
from utils.console import print_step
|
||||
|
||||
|
||||
def checkversion(__VERSION__):
|
||||
response = requests.get(
|
||||
"https://api.github.com/repos/elebumm/RedditVideoMakerBot/releases/latest"
|
||||
)
|
||||
response = requests.get("https://api.github.com/repos/elebumm/RedditVideoMakerBot/releases/latest")
|
||||
latestversion = response.json()["tag_name"]
|
||||
if __VERSION__ == latestversion:
|
||||
print_step(f"You are using the newest version ({__VERSION__}) of the bot")
|
||||
|
||||
+2
-5
@@ -1,7 +1,6 @@
|
||||
from __future__ import annotations
|
||||
from ast import Str
|
||||
import re
|
||||
|
||||
import re
|
||||
from typing import Tuple
|
||||
|
||||
from PIL import ImageFont, Image, ImageDraw, ImageEnhance
|
||||
@@ -37,9 +36,7 @@ class Video:
|
||||
im.save(path)
|
||||
return ImageClip(path)
|
||||
|
||||
def add_watermark(
|
||||
self, text, redditid, opacity=0.5, duration: int | float = 5, position: Tuple = (0.7, 0.9), fontsize=15
|
||||
):
|
||||
def add_watermark(self, text, redditid, opacity=0.5, duration: int | float = 5, position: Tuple = (0.7, 0.9), fontsize=15):
|
||||
compensation = round(
|
||||
(position[0] / ((len(text) * (fontsize / 5) / 1.5) / 100 + position[0] * position[0])),
|
||||
ndigits=2,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import json
|
||||
import time
|
||||
from typing import Dict
|
||||
|
||||
from praw.models import Submission
|
||||
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import re
|
||||
import sys
|
||||
from datetime import datetime
|
||||
import time as pytime
|
||||
from datetime import datetime
|
||||
from time import sleep
|
||||
|
||||
from requests import Response
|
||||
@@ -81,7 +81,7 @@ def sanitize_text(text: str) -> str:
|
||||
result = re.sub(regex_urls, " ", text)
|
||||
|
||||
# note: not removing apostrophes
|
||||
regex_expr = r"\s['|’]|['|’]\s|[\^_~@!&;#:\-–—%“”‘\"%\*/{}\[\]\(\)\\|<>=+]"
|
||||
regex_expr = r"\s['|’]|['|’]\s|[\^_~@!&;#:\-%—“”‘\"%\*/{}\[\]\(\)\\|<>=+]"
|
||||
result = re.sub(regex_expr, " ", result)
|
||||
result = result.replace("+", "plus").replace("&", "and")
|
||||
# remove extra whitespace
|
||||
|
||||
@@ -1,19 +1,32 @@
|
||||
from pathlib import Path
|
||||
import json
|
||||
import random
|
||||
from random import randrange
|
||||
import re
|
||||
from pathlib import Path
|
||||
from random import randrange
|
||||
from typing import Any, Tuple
|
||||
|
||||
|
||||
from moviepy.editor import VideoFileClip
|
||||
from moviepy.video.io.ffmpeg_tools import ffmpeg_extract_subclip
|
||||
from pytube import YouTube
|
||||
from pytube.cli import on_progress
|
||||
|
||||
from utils import settings
|
||||
from utils.CONSTANTS import background_options
|
||||
from utils.console import print_step, print_substep
|
||||
|
||||
# Load background videos
|
||||
with open("utils/backgrounds.json") as json_file:
|
||||
background_options = json.load(json_file)
|
||||
|
||||
# Remove "__comment" from backgrounds
|
||||
background_options.pop("__comment", None)
|
||||
|
||||
# Add position lambda function
|
||||
# (https://zulko.github.io/moviepy/ref/VideoClip/VideoClip.html#moviepy.video.VideoClip.VideoClip.set_position)
|
||||
for name in list(background_options.keys()):
|
||||
pos = background_options[name][3]
|
||||
|
||||
if pos != "center":
|
||||
background_options[name][3] = lambda t: ("center", pos + t)
|
||||
|
||||
|
||||
def get_start_and_end_times(video_length: int, length_of_clip: int) -> Tuple[int, int]:
|
||||
"""Generates a random interval of time to be used as the background of the video.
|
||||
@@ -52,9 +65,7 @@ def download_background(background_config: Tuple[str, str, str, Any]):
|
||||
uri, filename, credit, _ = background_config
|
||||
if Path(f"assets/backgrounds/{credit}-{filename}").is_file():
|
||||
return
|
||||
print_step(
|
||||
"We need to download the backgrounds videos. they are fairly large but it's only done once. 😎"
|
||||
)
|
||||
print_step("We need to download the backgrounds videos. they are fairly large but it's only done once. 😎")
|
||||
print_substep("Downloading the backgrounds videos... please be patient 🙏 ")
|
||||
print_substep(f"Downloading {filename} from {uri}")
|
||||
YouTube(uri, on_progress_callback=on_progress).streams.filter(res="1080p").first().download(
|
||||
|
||||
@@ -1 +1 @@
|
||||
[]
|
||||
[]
|
||||
Executable → Regular
+7
-7
@@ -4,6 +4,7 @@ import os
|
||||
import re
|
||||
from os.path import exists
|
||||
from typing import Tuple, Any
|
||||
|
||||
from moviepy.audio.AudioClip import concatenate_audioclips, CompositeAudioClip
|
||||
from moviepy.audio.io.AudioFileClip import AudioFileClip
|
||||
from moviepy.video.VideoClip import ImageClip
|
||||
@@ -13,11 +14,11 @@ from moviepy.video.io.VideoFileClip import VideoFileClip
|
||||
from moviepy.video.io.ffmpeg_tools import ffmpeg_extract_subclip
|
||||
from rich.console import Console
|
||||
|
||||
from utils import settings
|
||||
from utils.cleanup import cleanup
|
||||
from utils.console import print_step, print_substep
|
||||
from utils.video import Video
|
||||
from utils.videos import save_data
|
||||
from utils import settings
|
||||
|
||||
console = Console()
|
||||
W, H = 1080, 1920
|
||||
@@ -141,9 +142,7 @@ def make_final_video(
|
||||
|
||||
|
||||
img_clip_pos = background_config[3]
|
||||
image_concat = concatenate_videoclips(image_clips).set_position(
|
||||
img_clip_pos
|
||||
) # note transition kwarg for delay in imgs
|
||||
image_concat = concatenate_videoclips(image_clips).set_position(img_clip_pos) # note transition kwarg for delay in imgs
|
||||
image_concat.audio = audio_composite
|
||||
final = CompositeVideoClip([background_clip, image_concat])
|
||||
title = re.sub(r"[^\w\s-]", "", reddit_obj["thread_title"])
|
||||
@@ -163,10 +162,13 @@ def make_final_video(
|
||||
# # lowered_audio = audio_background.multiply_volume( # todo get this to work
|
||||
# # VOLUME_MULTIPLIER) # lower volume by background_audio_volume, use with fx
|
||||
# final.set_audio(final_audio)
|
||||
|
||||
final = Video(final).add_watermark(
|
||||
text=f"Background credit: {background_config[2]}", opacity=0.4, redditid=reddit_obj
|
||||
)
|
||||
|
||||
|
||||
|
||||
final.write_videofile(
|
||||
f"assets/temp/{id}/temp.mp4",
|
||||
fps=30,
|
||||
@@ -187,6 +189,4 @@ def make_final_video(
|
||||
print_substep(f"Removed {cleanups} temporary files 🗑")
|
||||
print_substep("See result in the results folder!")
|
||||
|
||||
print_step(
|
||||
f'Reddit title: {reddit_obj["thread_title"]} \n Background Credit: {background_config[2]}'
|
||||
)
|
||||
print_step(f'Reddit title: {reddit_obj["thread_title"]} \n Background Credit: {background_config[2]}')
|
||||
@@ -1,22 +1,15 @@
|
||||
import json
|
||||
|
||||
from pathlib import Path
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
from utils import settings
|
||||
from playwright.async_api import async_playwright # pylint: disable=unused-import
|
||||
|
||||
# do not remove the above line
|
||||
|
||||
import translators as ts
|
||||
from playwright.sync_api import sync_playwright, ViewportSize
|
||||
from rich.progress import track
|
||||
import translators as ts
|
||||
from utils.imagenarator import imagemaker
|
||||
from utils.console import print_step, print_substep
|
||||
|
||||
|
||||
|
||||
|
||||
def download_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: int):
|
||||
"""Downloads screenshots of reddit posts as seen on the web. Downloads to assets/temp/png
|
||||
|
||||
@@ -32,7 +25,8 @@ def download_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: in
|
||||
with sync_playwright() as p:
|
||||
print_substep("Launching Headless Browser...")
|
||||
|
||||
browser = p.chromium.launch() #headless=False #to check for chrome view
|
||||
browser = p.chromium.launch(headless=True) # add headless=False for debug
|
||||
|
||||
context = browser.new_context()
|
||||
|
||||
if settings.config["settings"]["theme"] == "dark":
|
||||
@@ -53,9 +47,7 @@ def download_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: in
|
||||
page.wait_for_load_state() # Wait for page to fully load
|
||||
|
||||
if page.locator('[data-click-id="text"] button').is_visible():
|
||||
page.locator(
|
||||
'[data-click-id="text"] button'
|
||||
).click() # Remove "Click to see nsfw" Button in Screenshot
|
||||
page.locator('[data-click-id="text"] button').click() # Remove "Click to see nsfw" Button in Screenshot
|
||||
|
||||
# translate code
|
||||
|
||||
@@ -74,9 +66,9 @@ def download_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: in
|
||||
print_substep("Skipping translation...")
|
||||
|
||||
postcontentpath = f"assets/temp/{id}/png/title.png"
|
||||
page.locator('[data-test-id="post-content"]').screenshot(path= postcontentpath)
|
||||
page.locator('[data-test-id="post-content"]').screenshot(path=postcontentpath)
|
||||
|
||||
if reddit_object["thread_post"] != "" and settings.config["settings"]["storymode"] == True:
|
||||
if settings.config["settings"]["storymode"] == True:
|
||||
if settings.config["settings"]["storymodemethode"] == 0:
|
||||
try : #new change
|
||||
page.locator('[data-click-id="text"]').first.screenshot(
|
||||
@@ -88,10 +80,10 @@ def download_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: in
|
||||
for idx,item in enumerate(reddit_object["thread_post"]):
|
||||
imagemaker(item,idx=idx,reddit_obj=reddit_object)
|
||||
|
||||
|
||||
|
||||
else:
|
||||
for idx, comment in enumerate(
|
||||
track(reddit_object["comments"], "Downloading screenshots...")
|
||||
):
|
||||
for idx, comment in enumerate(track(reddit_object["comments"], "Downloading screenshots...")):
|
||||
# Stop if we have reached the screenshot_num
|
||||
if idx >= screenshot_num:
|
||||
break
|
||||
@@ -113,9 +105,7 @@ def download_screenshots_of_reddit_posts(reddit_object: dict, screenshot_num: in
|
||||
[comment_tl, comment["comment_id"]],
|
||||
)
|
||||
try:
|
||||
page.locator(f"#t1_{comment['comment_id']}").screenshot(
|
||||
path=f"assets/temp/{id}/png/comment_{idx}.png"
|
||||
)
|
||||
page.locator(f"#t1_{comment['comment_id']}").screenshot(path=f"assets/temp/{id}/png/comment_{idx}.png")
|
||||
except TimeoutError:
|
||||
del reddit_object["comments"]
|
||||
screenshot_num += 1
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from typing import Dict, Tuple
|
||||
from typing import Tuple
|
||||
|
||||
from rich.console import Console
|
||||
|
||||
from TTS.engine_wrapper import TTSEngine
|
||||
from TTS.GTTS import GTTS
|
||||
from TTS.streamlabs_polly import StreamlabsPolly
|
||||
from TTS.aws_polly import AWSPolly
|
||||
from TTS.TikTok import TikTok
|
||||
from TTS.aws_polly import AWSPolly
|
||||
from TTS.engine_wrapper import TTSEngine
|
||||
from TTS.pyttsx import pyttsx
|
||||
from TTS.streamlabs_polly import StreamlabsPolly
|
||||
from utils import settings
|
||||
from utils.console import print_table, print_step
|
||||
|
||||
|
||||
console = Console()
|
||||
|
||||
TTSProviders = {
|
||||
|
||||
Reference in new issue
Block a user