diff --git a/.gitignore b/.gitignore index 2c65419..072980d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ resources/data.* venv *.sw* .ropeproject +__pycache__ +.undodir diff --git a/app.py b/app.py new file mode 100755 index 0000000..1b3c2f5 --- /dev/null +++ b/app.py @@ -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() diff --git a/index.html b/index.html deleted file mode 100644 index e34cf2b..0000000 --- a/index.html +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - Example Title - - - - - - - - - -
-
-
-

Stream Name Stream Title

-
-
- -
-
-

Viewers

-

Current Viewers: 0

-

Total Viewers During Stream:

0

-
-
- -
-
-
-

Current Viewers: . Session Peak Viewers: .

-

Stream Online?

-
- - - - - - - - - - - \ No newline at end of file diff --git a/index.mustache b/index.mustache new file mode 100644 index 0000000..03ebae8 --- /dev/null +++ b/index.mustache @@ -0,0 +1,106 @@ + + + + + + {{name}} + + + + + + + + + +
+
+
+
+
+
+
+

Stream Title: {{streamTitle}}

+
+
+

Stream online? {{online}}

+
+
+
+
+
+

Current Viewers: {{viewerCount}}

+
+
+

Session Max Viewers: {{sessionMaxViewerCount}}

+
+
+

Overall Max Viewers: {{overallMaxViewerCount}}

+
+
+
+
+
+

Current tags:

+

{{tags}}

+
+
+
+
+ + +
+
+ + +
+
+
+
+
+ + + + + + + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..765d233 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +chevron==0.14.0 +Flask==2.1.3 +requests==2.28.1 diff --git a/static/script.js b/static/script.js new file mode 100644 index 0000000..c5c5a60 --- /dev/null +++ b/static/script.js @@ -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()