Merge branch 'python-rewrite'
- rewrites the software to use a python and flask based local webserver, for initial simplicity.
This commit is contained in:
commit
8cf956c2f0
6 changed files with 290 additions and 47 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -4,3 +4,5 @@ resources/data.*
|
||||||
venv
|
venv
|
||||||
*.sw*
|
*.sw*
|
||||||
.ropeproject
|
.ropeproject
|
||||||
|
__pycache__
|
||||||
|
.undodir
|
||||||
|
|
133
app.py
Executable file
133
app.py
Executable 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()
|
47
index.html
47
index.html
|
@ -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
106
index.mustache
Normal 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
3
requirements.txt
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
chevron==0.14.0
|
||||||
|
Flask==2.1.3
|
||||||
|
requests==2.28.1
|
46
static/script.js
Normal file
46
static/script.js
Normal 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()
|
Loading…
Reference in a new issue