Merge branch 'dev' into michaelg

This commit is contained in:
Michael Grace 2020-11-03 22:52:58 +00:00 committed by GitHub
commit 2e84e4bebd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 5549 additions and 48 deletions

6
.gitignore vendored
View file

@ -22,8 +22,12 @@ build/build/BAPSicle/
build/output/
venv/
config.py
dev/welcome.mp3
dev/welcome.mp3
build/build-exe-pyinstaller-command.sh

View file

@ -1,7 +1,7 @@
# BAPSicle
### a.k.a. The Next-Gen BAPS server
!["BAPSicle logo, a pink melting ice lolly."](/dev/logo.png "BAPSicle Logo")
!["BAPSicle logo, a pink melting ice lolly."](docs/images/logo.png "BAPSicle Logo")
Welcome! This is BAPS. More acurately, this is yet another attempt at a BAPS3 server.
@ -16,32 +16,48 @@ Currently there's just a batch script. Simply run ``install.bat`` as administrat
This will:
* Copy BAPSicle into ``C:\Program Files\BAPSicle``
* Install BAPSicle.exe as a Windows Service with NSSM.
* If all goes well, open [http://localhost:5000](localhost:5000) for the server UI.
* If all goes well, open [http://localhost:13500](localhost:13500) for the server UI.
### Linux
Installed service for linux is comming soon. Testing is primarily on Ubuntu 20.04. Your milage with other distros will vary.
Installed service for linux is coming soon. Testing is primarily on Ubuntu 20.04. Your milage with other distros will vary.
### MacOS
Support for MacOS will be the last to come, sorry about that.
Currently there's no installer for MacOS, so you'll have to move the ``build/output/BAPSicle.app`` you've built and make it start automatically (if you want).
Starting and stopping the server, as well as UI links, are available in the System Menu once opening the app.
!["BAPSicle in the MacOS System Menu"](docs/images/system-menu.png "System Menu")
## Developing
### Requirements
On all platforms:
* Python 3.7 (3.8 may also work, 3.9 is unlikely to.)
* Git (Obviously)
On MacOS:
* Homebrew (To install command line Platypus)
### Running
To just run the server standaline without installing, run ``python ./launch_standalone.py``.
### Building
## Building
Currently mostly Windows focused.
### Windows
To build a BAPSicle.exe, run ``build\build-exe.py``. The resulting file will appear in ``build\output``. You can then use the install instructions above to install it, or just run it standalone.
To build a ``BAPSicle.exe``, run ``build\build-exe.bat``. The resulting file will appear in ``build\output``. You can then use the install instructions above to install it, or just run it standalone.
### Linux
To build a ``BAPSicle`` executable, run ``build/build-linux.sh``. The resulting file will appear in ``build/output``.
### MacOS
To build a ``BAPSicle.app``, run ``build/build-macos.sh``. The resulting file will appear in ``build/output``.
### Other bits
Provided is a VScode debug config to let you debug live, as well as ``dev\install-githook.bat`` that will help git to clean your code up as you're committing!
Provided is a VScode debug config to let you debug live, as well as ``dev\install-githook.{bat,sh}`` that will help git to clean your code up as you're committing!

5297
build/BAPSicle.platypus Normal file

File diff suppressed because it is too large Load diff

View file

@ -7,7 +7,7 @@
},
{
"optionDest": "filenames",
"value": "\\launch_standalone.py"
"value": "/launch_standalone.py"
},
{
"optionDest": "onefile",
@ -15,11 +15,15 @@
},
{
"optionDest": "console",
"value": true
"value": false
},
{
"optionDest": "exclude-module",
"value": "tkinter"
},
{
"optionDest": "icon_file",
"value": "\\build\\icon.ico"
"value": "/build/icon.ico"
},
{
"optionDest": "name",
@ -63,15 +67,15 @@
},
{
"optionDest": "datas",
"value": "\\templates;templates/"
"value": "/templates;templates/"
},
{
"optionDest": "datas",
"value": "\\ui-static;ui-static/"
"value": "/ui-static;ui-static/"
}
],
"nonPyinstallerOptions": {
"increaseRecursionLimit": false,
"manualArguments": ""
}
}
}

View file

@ -32,12 +32,13 @@ for option in config["pyinstallerOptions"]:
cmd_str += "--" + str(option_dest) + ' "' + str(option["value"]) + '" '
command = open('build-exe-pyinstaller-command.bat', 'w')
for format in [".bat", ".sh"]:
command = open('build-exe-pyinstaller-command'+format, 'w')
if filename == "":
print("No filename data was found in json file.")
command.write("")
else:
command.write(cmd_str + ' --distpath "output/" --workpath "build/" "' + filename + '"')
if filename == "":
print("No filename data was found in json file.")
command.write("")
else:
command.write(cmd_str + ' --distpath "output/" --workpath "build/" "' + filename + '"')
command.close()
command.close()

17
build/build-linux.sh Executable file
View file

@ -0,0 +1,17 @@
#!/bin/bash
cd "$(dirname "$0")"
apt install libportaudio2
pip3 install -r requirements.txt
pip3 install -r requirements-linux.txt
pip3 install -e ..\
python3 ./generate-build-exe-config.py
python3 ./build-exe.py
bash ./build-exe-pyinstaller-command.sh
rm ./*.spec

19
build/build-macos.sh Executable file
View file

@ -0,0 +1,19 @@
#!/bin/bash
cd "$(dirname "$0")"
pip3 install -r requirements.txt
pip3 install -r requirements-macos.txt
pip3 install -e ..\
python3 ./generate-build-exe-config.py
python3 ./build-exe.py
bash ./build-exe-pyinstaller-command.sh
rm ./*.spec
brew install platypus
platypus --load-profile ./BAPSicle.platypus --overwrite ./output/BAPSicle.app

View file

@ -1,5 +1,6 @@
import json
import os
from helpers.os_environment import isWindows
dir_path = os.path.dirname(os.path.realpath(__file__))
parent_path = os.path.dirname(dir_path)
@ -11,6 +12,8 @@ in_file.close()
for option in config["pyinstallerOptions"]:
if option["optionDest"] in ["datas", "filenames", "icon_file"]:
option["value"] = os.path.abspath(parent_path + option["value"])
if not isWindows():
option["value"] = option["value"].replace(";",":")
out_file = open('build-exe-config.json', 'w')
out_file.write(json.dumps(config, indent=2))

BIN
build/icon.icns Normal file

Binary file not shown.

BIN
build/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

27
build/macos-platypus.sh Normal file
View file

@ -0,0 +1,27 @@
#!/bin/bash
if [ "$1" == "" ]
then
echo "DISABLED|BAPSicle Server"
echo "----"
if curl --output /dev/null --silent --head --fail --max-time 1 "http://localhost:13500"
then
echo "Status"
echo "Config"
echo "Logs"
echo "----"
echo "Stop Server"
else
echo "DISABLED|Status"
echo "DISABLED|Config"
echo "DISABLED|Logs"
echo "----"
echo "Start Server"
fi
exit
fi
if [ "$1" == "Stop Server" ]
then
curl "http://localhost:13500/quit"
else
./BAPSicle "$1"
fi

View file

@ -0,0 +1 @@
pyinstaller

View file

@ -0,0 +1 @@
pyinstaller

4
dev/install-githook.sh Normal file
View file

@ -0,0 +1,4 @@
#!/bin/bash
cd "$(dirname "$0")"
cp "./pre-commit" "../.git/hooks/"

View file

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

BIN
docs/images/system-menu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

26
helpers/device_manager.py Normal file
View file

@ -0,0 +1,26 @@
import sounddevice as sd
import importlib
from helpers.os_environment import isMacOS
class DeviceManager():
@classmethod
def _isOutput(self, device):
return device["max_output_channels"] > 0
@classmethod
def _getDevices(self):
# To update the list of devices
# Sadly this doesn't work on MacOS.
if not isMacOS():
sd._terminate()
sd._initialize()
devices = sd.query_devices()
return devices
@classmethod
def getOutputs(self):
outputs = filter(self._isOutput, self._getDevices())
return outputs

View file

@ -28,7 +28,7 @@ nssm set %service_name% AppStopMethodConsole 5000
nssm set %service_name% AppStopMethodWindow 5000
nssm set %service_name% AppStopMethodThreads 5000
nssm set %service_name% DisplayName "BAPSicle Server"
nssm set %service_name% Description "The next gen Broadcast and Presenting Suite server! Access settings on port 5000."
nssm set %service_name% Description "The next gen Broadcast and Presenting Suite server! Access settings on port 13500."
nssm set %service_name% ObjectName LocalSystem
nssm set %service_name% Start SERVICE_AUTO_START
nssm set %service_name% Type SERVICE_INTERACTIVE_PROCESS
@ -41,4 +41,4 @@ nssm start %service_name%
timeout 4 /nobreak
explorer "http://localhost:5000/"
explorer "http://localhost:13500/"

View file

@ -1,13 +1,48 @@
import multiprocessing
import time
import sys
import webbrowser
from server import BAPSicleServer
if __name__ == '__main__':
# On Windows calling this function is necessary.
# Causes all kinds of loops if not present.
multiprocessing.freeze_support()
server = multiprocessing.Process(target=BAPSicleServer).start()
def startServer():
server = multiprocessing.Process(target=BAPSicleServer)
server.start()
while True:
time.sleep(1)
pass
time.sleep(5)
if server and server.is_alive():
pass
else:
print("Server dead. Exiting.")
sys.exit(0)
if __name__ == '__main__':
# On Windows, calling this function is necessary.
# Causes all kinds of loops if not present.
# IT HAS TO BE RIGHT HERE, AT THE TOP OF __MAIN__
# NOT INSIDE AN IF STATEMENT. RIGHT. HERE.
# If it's not here, multiprocessing just doesn't run in the package.
# Freeze support refers to being packaged with Pyinstaller.
multiprocessing.freeze_support()
if len(sys.argv) > 1:
# We got an argument! It's probably Platypus's UI.
try:
if (sys.argv[1]) == "Start Server":
print("NOTIFICATION:Welcome to BAPSicle!")
webbrowser.open("http://localhost:13500/")
startServer()
if (sys.argv[1] == "Status"):
webbrowser.open("http://localhost:13500/status")
if (sys.argv[1] == "Config"):
webbrowser.open("http://localhost:13500/config")
if (sys.argv[1] == "Logs"):
webbrowser.open("http://localhost:13500/logs")
except Exception as e:
print("ALERT:BAPSicle failed with exception:\n", e)
sys.exit(0)
else:
startServer()

View file

@ -25,13 +25,20 @@ import setproctitle
import copy
import json
import time
from typing import Callable, Dict, List
from pygame import mixer
from state_manager import StateManager
from plan import PlanObject
from mutagen.mp3 import MP3
import os
import sys
os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = "hide"
from pygame import mixer
from mutagen.mp3 import MP3
from state_manager import StateManager
from helpers.os_environment import isMacOS
class Player():
@ -225,6 +232,12 @@ class Player():
def load(self, timeslotitemid: int):
if not self.isPlaying:
self.unload()
# Fix any OS specific / or \'s
if os.path.sep == "/":
filename = filename.replace("\\", '/')
else:
filename = filename.replace("/", '\\')
updated: bool = False
@ -240,6 +253,7 @@ class Player():
filename: str = self.state.state["loaded_item"].filename
try:
mixer.music.load(filename)
except:
@ -446,6 +460,7 @@ class Player():
print("Quiting player ", channel)
self.quit()
self._retMsg("EXIT")
sys.exit(0)
def showOutput(in_q, out_q):
@ -457,6 +472,8 @@ def showOutput(in_q, out_q):
if __name__ == "__main__":
if isMacOS:
multiprocessing.set_start_method("spawn", True)
in_q = multiprocessing.Queue()
out_q = multiprocessing.Queue()
@ -474,7 +491,8 @@ if __name__ == "__main__":
# Do some testing
in_q.put("LOADED?")
in_q.put("PLAY")
in_q.put("LOAD:dev/test.mp3") # I mean, this won't work now, this can get sorted :)
in_q.put("LOAD:dev/test.mp3")
in_q.put("LOADED?")
in_q.put("PLAY")
print("Entering infinite loop.")

View file

@ -17,10 +17,10 @@ import multiprocessing
import player
from flask import Flask, render_template, send_from_directory, request
import json
import sounddevice as sd
import setproctitle
import config
import pyttsx3
import logging
from helpers.os_environment import isMacOS
from helpers.device_manager import DeviceManager
setproctitle.setproctitle("BAPSicle - Server")
@ -35,10 +35,17 @@ class BAPSicleServer():
app = Flask(__name__, static_url_path='')
log = logging.getLogger('werkzeug')
log.disabled = True
app.logger.disabled = True
channel_to_q = []
channel_from_q = []
channel_p = []
stopping = False
### General Endpoints
@app.errorhandler(404)
@ -65,12 +72,7 @@ def ui_config():
for i in range(3):
channel_states.append(status(i))
devices = sd.query_devices()
outputs = []
for device in devices:
if device["max_output_channels"] > 0:
outputs.append(device)
outputs = DeviceManager.getOutputs()
data = {
'channels': channel_states,
@ -223,6 +225,12 @@ def status(channel):
return response
@app.route("/quit")
def quit():
stopServer()
return "Shutting down..."
@app.route("/player/all/stop")
def all_stop():
for channel in channel_to_q:
@ -243,7 +251,10 @@ def send_static(path):
def startServer():
if isMacOS():
multiprocessing.set_start_method("spawn", True)
for channel in range(3):
channel_to_q.append(multiprocessing.Queue())
channel_from_q.append(multiprocessing.Queue())
channel_p.append(
@ -282,17 +293,29 @@ def startServer():
channel_to_q[0].put("PLAY")
# Don't use reloader, it causes Nested Processes!
app.run(host=config.HOST, port=config.PORT, debug=True, use_reloader=False)
app.run(host='0.0.0.0', port=13500, debug=True, use_reloader=False)
def stopServer():
print("Stopping server.py")
for q in channel_to_q:
q.put("QUIT")
for player in channel_p:
player.join()
global app
app = None
try:
player.join()
except:
pass
print("Stopped all players.")
global stopping
if stopping == False:
stopping = True
shutdown = request.environ.get('werkzeug.server.shutdown')
if shutdown is None:
print("Shutting down Server.")
else:
print("Shutting down Flask.")
shutdown()
if __name__ == "__main__":

View file

@ -10,6 +10,11 @@ class StateManager:
__state = {}
def __init__(self, name, default_state=None):
try:
os.mkdir(resolve_external_file_path("/state"))
except FileExistsError:
pass
self.filepath = resolve_external_file_path("/state/" + name + ".json")
if not os.path.isfile(self.filepath):
self.log("No file found for " + self.filepath)