Merge branch 'python-rewrite'

- rewrites the software to use a python and flask based local webserver,
  for initial simplicity.
This commit is contained in:
Ariana Giroux 2022-08-04 17:43:48 -06:00
commit 8cf956c2f0
6 changed files with 290 additions and 47 deletions

2
.gitignore vendored
View File

@ -4,3 +4,5 @@ resources/data.*
venv
*.sw*
.ropeproject
__pycache__
.undodir

133
app.py Executable file
View File

@ -0,0 +1,133 @@
#! /Users/arianagiroux/Documents/Coding/owncast-obs-streamer/venv/bin/python3
""" A simple flask app to provide a streamer UI for obs and owncast.
Usage:
$ ./app.py
> * Serving Flask app 'app' (lazy loading)
> * Environment: production
> WARNING: This is a development server. Do not use it in a production deployment.
> Use a production WSGI server instead.
> * Debug mode: off
> * Running on http://127.0.0.1:5000 (Press CTRL+C to quit)
> Open 127.0.0.1:5000 in your local browser
Note:
If port 5000 is already bound, use the following command to start the flask
app:
$ flask run -p INTEGER
The app will automatically fail if there is not file containing valid JSON data
located at ./resources/data.json. This json file should have the following data
structure:
{
"stream_url":"https://yourstream.url",
"user_name":"admin",
"stream_key":"your_stream_key"
}
"""
import chevron
import requests
import json
from os import path
from pprint import pprint
from flask import Flask, request, Response
# load json data, or raise exception.
if path.exists('resources/data.json'):
try:
stream_data = json.load(open('resources/data.json', 'r'))
except json.JSONDecodeError as e:
raise RuntimeError('JSON data did not pass validation.')
else:
raise RuntimeError('Could not find ./resources/data.json, required for '
'runtime!')
# initialize requests session
session = requests.Session()
session.auth = ('admin', stream_data['stream_key'])
# initialize flask app
app = Flask(__name__)
def render(data={}):
""" Template rendering function using mustache (via the pypi chevron
implementation.
:returns: A full HTML template with relevant data from the server.
"""
return chevron.render(template=open('index.mustache', 'r'), data=data)
@app.route("/api/serverstatus", methods=['GET'])
def getServerStatus():
""" Obtains useful information from the server from either /api/yp or
/api/status.
:returns: the collected data as a JSON formatted dict.
"""
response = session.get(stream_data['stream_url'] + '/api/yp')
response_data = response.json()
data = {
'name': response_data['name'],
'online': response_data['online'],
'overallMaxViewerCount': response_data['overallMaxViewerCount'],
'sessionMaxViewerCount': response_data['sessionMaxViewerCount'],
'viewerCount': response_data['viewerCount'],
'description': response_data['description'],
'tags': response_data['tags'],
'nsfw': response_data['nsfw'],
}
response = session.get(stream_data['stream_url'] + '/api/status')
response_data = response.json()
data['streamTitle'] = response_data['streamTitle']
return json.dumps(data)
@app.route("/api/update/streamtitle", methods=['POST'])
def updateStreamTitle():
""" An endpoint to allow the user to update the stream title.
:returns: the status code of the post request made to the server.
"""
response = session.post(
stream_data['stream_url'] + '/api/admin/config/streamtitle',
data=json.dumps({'value': request.get_json(force=True)})
)
return Response(status=response.status_code)
@app.route('/api/update/servertags', methods=['POST'])
def updateServerTags():
""" An endpoint to allow the user to update the stream title.
:returns: the status code of the post request made to the server.
"""
values = [i.strip() for i in request.get_json(force=True).split(',')]
response = session.post(
stream_data['stream_url'] + '/api/admin/config/tags',
data=json.dumps({
'value': values
})
)
return Response(status=response.status_code)
@app.route("/")
def index():
""" A simple initial endpoint that loads in relevant data from the server.
:returns: the rendered template (see app.render for more)
"""
return render(json.loads(getServerStatus()))
if __name__ == '__main__':
app.run()

View File

@ -1,47 +0,0 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<title>Example Title</title>
<meta name="author" content="Your Name">
<meta name="description" content="Example description">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="">
<link rel="icon" type="image/x-icon" href=""/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.0.0/dist/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
</head>
<body>
<header></header>
<main>
<div class="container" style="width: 100%">
<h1 id="stream-name" style="margin: 1rem">Stream Name <small id="stream-title">Stream Title</small></h1>
<div class="row" style="width: 100%">
<div class="col-1">
<!-- spacer -->
</div>
<div id="viewers" class="col">
<h2>Viewers</h2>
<p>Current Viewers: <span id="current-viewers">0</span></p>
<p>Total Viewers During Stream: <p id="session-viewers">0</p></p>
</div>
<div class="col-1">
<!-- spacer -->
</div>
</div>
</div>
<p>Current Viewers: <span id="currentViewers"></span>. Session Peak Viewers: <span id="sessionPeak"></span>.</p>
<p>Stream Online? <span id="online"></span></p>
</main>
<footer></footer>
</body>
<script type="text/javascript" src="resources/data.js"></script>
<script type="text/javascript" src="resources/script.js"></script>
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.12.9/dist/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.0.0/dist/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
</html>

106
index.mustache Normal file
View File

@ -0,0 +1,106 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<title id="streamName">{{name}}</title>
<meta name="author" content="Your Name">
<meta name="description" content="{{description}}">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="">
<link rel="icon" type="image/x-icon" href=""/>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
</head>
<body>
<header>
</header>
<main>
<div class="container" style="background-color:rgba(0,0,0,0.1);padding:1rem">
<div class="row" style="background-color:rgb(256,256,256);padding:1rem;">
<div id="titleSection" class="row">
<div class="col">
<h2 style="text-align:left">Stream Title: <em><span id="streamTitle">{{streamTitle}}</span></em></h2>
</div>
<div class="col">
<h3 style="text-align:right">Stream online? <em><span id="streamOnline">{{online}}</span></em></h3>
</div>
</div>
<hr>
<div id="viewerSection" class="row">
<div class="col">
<p>Current Viewers: <span id="currentViewers">{{viewerCount}}</span></p>
</div>
<div class="col">
<p>Session Max Viewers: <span id="sessionMaxViewerCount">{{sessionMaxViewerCount}}</span></p>
</div>
<div class="col">
<p>Overall Max Viewers: <span id="overallMaxViewerCount">{{overallMaxViewerCount}}</span></p>
</div>
</div>
<hr>
<div id="tagsSection" class="container" style="background-color:rgba(0,0,0,0.2)">
<div class="card" style="padding:1rem;margin:0.5rem 0">
<h4 class="card-title">Current tags:</h4>
<p id="tags" class="card-text" style="padding:1rem">{{tags}}</p>
</div>
</div>
<div id="updateButtonsSection" class="row" style="margin-top:1rem">
<div class="col">
<button type="button" class="btn btn-light mx-auto d-block" style="width:12em" data-bs-toggle="modal" data-bs-target="#streamTitleModal">Update Stream Title</button>
<div class="modal fade" id="streamTitleModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Update Stream Title</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form onsubmit="updateStreamTitle(document.getElementById('streamTitleInput').value); return false" style="width:100%" id="streamTitleForm">
<input type="text" name="streamTitle" id="streamTitleInput" style="width:100%" />
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button submit" form="streamTitleForm" class="btn btn-primary" data-bs-dismiss="modal">Save changes</button>
</div>
</div>
</div>
</div>
</div>
<div class="col">
<button type="button" class="btn btn-light mx-auto d-block" style="width:12em" data-bs-toggle="modal" data-bs-target="#tagsModal">Update Tags</button>
<div class="modal fade" id="tagsModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Update Tags</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form onsubmit="updateTags(document.getElementById('tagsInput').value); return false" style="width:100%" id="tagsForm">
<input type="text" name="tagsInput" id="tagsInput" style="width:100%" />
</form>
<p class="small text-center">Seperate tags by comma.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button submit" form="tagsForm" class="btn btn-primary" data-bs-dismiss="modal">Save changes</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<footer>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
<script src="/static/script.js"></script>
</body>
<!-- vim: foldmethod=manual nowrap
-->
</html>

3
requirements.txt Normal file
View File

@ -0,0 +1,3 @@
chevron==0.14.0
Flask==2.1.3
requests==2.28.1

46
static/script.js Normal file
View File

@ -0,0 +1,46 @@
async function getEndpoint(url = '') {
const response = await fetch(url, {method:'GET'});
const data = await response.json();
return data;
}
async function uiUpdateOnce() {
var data = await getEndpoint('http://127.0.0.1:5000/api/serverstatus');
document.getElementById("streamTitle").innerHTML = data.streamTitle;
document.getElementById("currentViewers").innerHTML = data.viewerCount;
document.getElementById("sessionMaxViewerCount").innerHTML = data.sessionMaxViewerCount;
document.getElementById("overallMaxViewerCount").innerHTML = data.overallMaxViewerCount;
document.getElementById("tags").innerHTML = data.tags;
}
async function continuousUiUpdate() {
window.setInterval(async () => {
uiUpdateOnce();
}, 1000);
}
async function updateStreamTitle(value) {
// this function simply passes data along to the python script, and allows
// python to handle data processing
response = await fetch('http://127.0.0.1:5000/api/update/streamtitle',
{
method:'POST',
body:JSON.stringify(value)
}
);
await uiUpdateOnce();
}
async function updateTags(value) {
// this function simply passes data along to the python script, and allows
// python to handle data processing
response = await fetch('http://127.0.0.1:5000/api/update/servertags',
{
method:'POST',
body:JSON.stringify(value)
}
);
await uiUpdateOnce();
}
continuousUiUpdate()