Initial Commit

This commit is contained in:
Bradley Bickford 2025-04-24 15:28:53 -04:00
commit 7bb45b12ca
9 changed files with 319 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
venv/

44
config.json Normal file
View File

@ -0,0 +1,44 @@
{
"instance_name": "Test",
"mpv_arguments": [
"--loop",
"--screen=0"
],
"buttons": [
{
"name": "Goofy",
"video_url": "videos/goofy.mp4",
"default": true
},
{
"name": "Neutron Stars",
"video_url": "videos/neutronstars.mp4",
"default": false
},
{
"name": "Gimme The Bleach Boys",
"video_url": "videos/bleach.mp4",
"default": false
},
{
"name": "The Cloak",
"video_url": "videos/cloak.mp4",
"default": false
},
{
"name": "Dog of Wisdom",
"video_url": "videos/dog.mp4",
"default": false
},
{
"name": "Nyan Cat",
"video_url": "videos/nyan.mp4",
"default": false
},
{
"name": "Smooth Jazz Nyan Cat",
"video_url": "videos/smoothjazznyan.mp4",
"default": false
}
]
}

51
index.html Normal file
View File

@ -0,0 +1,51 @@
<!DOCTYPE html>
<html>
<head>
<title>MPV Commander</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</head>
<script>
async function onclick_processor(name) {
try {
const response = await fetch(
window.location.origin + "/stream",
{
method: "POST",
"headers": {
"Content-Type": "application/json"
},
body: JSON.stringify({ "name": name })
}
)
if (!response.ok) {
throw new Error(`Response status: ${response.status}`)
}
const json = await response.json()
console.log(json)
} catch (error) {
console.log(error.message)
}
}
</script>
<body>
<div class="container">
<h1 class="text-center">MPV Commander Instance {{ instance_name }}</h1>
<h3 class="text-center">Which video stream do you want to show?</h3>
{% for index in range(buttons | length) %}
{% if index % 3 == 0 %}
<div class="row">
{% endif %}
{% set current_name = buttons[index]["name"] %}
<button class="col-sm-4 btn-default p-3 my-1" onclick="onclick_processor('{{ current_name }}')">{{ current_name }}</button>
{% if index + 1 == buttons | length or (index + 1) % 3 == 0 %}
</div>
{% endif %}
{% endfor %}
</div>
</body>
</html>

62
mpv_commander.py Normal file
View File

@ -0,0 +1,62 @@
import os
import signal
import subprocess
import sqlite3
import time
import json
get_active_video = r'''
SELECT video_url FROM buttons
WHERE is_active = 1
LIMIT 1
'''
def run_mpv(previous_process, mpv_arguments, video_url):
if not previous_process is None:
os.killpg(os.getpgid(previous_process.pid), signal.SIGTERM)
return subprocess.Popen(
"/usr/bin/mpv {arguments} {video_url}".format(
arguments = " ".join(mpv_arguments),
video_url = video_url
),
stdout = subprocess.PIPE,
stderr = subprocess.PIPE,
shell = True,
preexec_fn = os.setsid
)
def get_active_video_url(db):
cursor = db.execute(get_active_video)
return cursor.fetchall()[0][0]
with open("config.json", "r") as config_file:
config_json = json.loads(config_file.read())
current_process = None
current_video_url = None
db = sqlite3.connect("mpvcommander.db")
while True:
if current_process is None:
current_video_url = get_active_video_url(db)
current_process = run_mpv(
current_process,
config_json["mpv_arguments"],
current_video_url
)
active_video_url = get_active_video_url(db)
if current_video_url != active_video_url:
current_video_url = active_video_url
current_process = run_mpv(
current_process,
config_json["mpv_arguments"],
current_video_url
)
time.sleep(1)

71
mpv_dbbuilder.py Normal file
View File

@ -0,0 +1,71 @@
import sqlite3
import json
import sys
destroy_table = r'''
DROP TABLE IF EXISTS buttons
'''
make_table = r'''
CREATE TABLE IF NOT EXISTS buttons
(button_name TEXT PRIMARY KEY,
video_url TEXT NOT NULL,
is_default BOOLEAN NOT NULL CHECK (is_default IN (0, 1)),
is_active BOOLEAN NOT NULL CHECK (is_active IN (0, 1)))
'''
insert_row = r'''
INSERT INTO buttons VALUES
(?, ?, ?, ?)
'''
with open("config.json", "r") as config_file:
config_json = json.loads(config_file.read())
config_issues = ""
if not "instance_name" in config_json.keys():
config_issues = config_issues + "The config item instance_name is required\n"
for i in range(len(config_json["buttons"])):
if not "name" in config_json["buttons"][i].keys():
config_issues = config_issues + "Config index {index} is missing the required name parameter\n".format(index = (i + 1))
if not "video_url" in config_json["buttons"][i].keys():
config_issues = config_issues + "Config index {index} is missing the required video_url parameter\n".format(index = (i + 1))
if not "default" in config_json["buttons"][i].keys():
config_issues = config_issues + "Config index {index} is missing the required default parameter\n".format(index = (i + 1))
all_defaults = [element for element in config_json["buttons"] if element["default"]]
if len(all_defaults) > 1:
config_issues = config_issues + "More than one button config is set as default, only one can be the default, remove one of these: {button_list}\n".format(
button_list = ", ".join([element["name"] if "name" in element else "NAME MISSING" for element in all_defaults])
)
elif len(all_defaults) == 0:
config_issues = config_issues = "At least one button config must be set as the default\n"
if len(config_issues) != 0:
print("Config issues detected! Please correct these issues before trying again")
print(config_issues)
sys.exit(1)
db = sqlite3.connect("mpvcommander.db")
db.execute(destroy_table)
db.execute(make_table)
for button in config_json["buttons"]:
db.execute(insert_row, [
button["name"],
button["video_url"],
1 if button["default"] else 0,
1 if button["default"] else 0
])
db.commit()
db.close()

90
mpv_server.py Normal file
View File

@ -0,0 +1,90 @@
from flask import Flask, request, jsonify, g
from jinja2 import Template
import json
import sys
import sqlite3
disable_all_active = r'''
UPDATE buttons SET is_active = 0
'''
set_active_by_name_sql = r'''
UPDATE buttons SET is_active = 1
WHERE button_name = ?
'''
with open("config.json", "r") as config_file:
config_json = json.loads(config_file.read())
app = Flask(__name__)
def get_db():
db = getattr(g, "_database", None)
if db is None:
db = g._database = sqlite3.connect("mpvcommander.db")
return db
def set_active_by_name(name):
db = get_db()
db.execute(disable_all_active)
db.execute(set_active_by_name, name)
db.commit()
@app.route("/")
def root_route():
with open("index.html", "r") as html_file:
template = Template(html_file.read())
return template.render(config_json), 200
@app.route("/stream", methods=["POST"])
def stream_route():
if request.content_type == 'application/json':
body = request.get_json()
if not "name" in body.keys():
error_response = {
'status': "ERROR",
"reason": "A name must be specified in the body of the request"
}
return jsonify(error_response), 400
resource_list = [element for element in config_json["buttons"] if element["name"] == body["name"]]
if(len(resource_list) == 0):
error_response = {
'status': "ERROR",
"reason": "The name {name} does not exist in config['buttons'], check your config".format(name = body["name"])
}
return jsonify(error_response), 400
db = get_db()
db.execute(disable_all_active)
db.execute(set_active_by_name_sql, [body["name"]])
db.commit()
return jsonify(resource_list[0]), 200
else:
error_response = {
'status': 'ERROR',
'reason': 'Posted body must be of content type application/json'
}
return jsonify(error_response)
@app.teardown_appcontext
def close_connection(exception):
db = getattr(g, '_database', None)
if db is not None:
db.close()
if __name__ == '__main__':
app.run(debug=True, host="0.0.0.0", port=1801)

BIN
mpvcommander.db Normal file

Binary file not shown.

BIN
videos/goofy.mp4 Normal file

Binary file not shown.

BIN
videos/neutronstars.mp4 Normal file

Binary file not shown.