Merge branch 'dev' into michaelg
This commit is contained in:
commit
2e84e4bebd
22 changed files with 5549 additions and 48 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -22,8 +22,12 @@ build/build/BAPSicle/
|
|||
|
||||
build/output/
|
||||
|
||||
|
||||
venv/
|
||||
|
||||
config.py
|
||||
|
||||
dev/welcome.mp3
|
||||
|
||||
build/build-exe-pyinstaller-command.sh
|
||||
|
||||
|
|
32
README.md
32
README.md
|
@ -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
5297
build/BAPSicle.platypus
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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,11 +67,11 @@
|
|||
},
|
||||
{
|
||||
"optionDest": "datas",
|
||||
"value": "\\templates;templates/"
|
||||
"value": "/templates;templates/"
|
||||
},
|
||||
{
|
||||
"optionDest": "datas",
|
||||
"value": "\\ui-static;ui-static/"
|
||||
"value": "/ui-static;ui-static/"
|
||||
}
|
||||
],
|
||||
"nonPyinstallerOptions": {
|
||||
|
|
|
@ -32,7 +32,8 @@ 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.")
|
||||
|
|
17
build/build-linux.sh
Executable file
17
build/build-linux.sh
Executable 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
19
build/build-macos.sh
Executable 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
|
||||
|
|
@ -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
BIN
build/icon.icns
Normal file
Binary file not shown.
BIN
build/icon.png
Normal file
BIN
build/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
27
build/macos-platypus.sh
Normal file
27
build/macos-platypus.sh
Normal 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
|
1
build/requirements-linux.txt
Normal file
1
build/requirements-linux.txt
Normal file
|
@ -0,0 +1 @@
|
|||
pyinstaller
|
1
build/requirements-macos.txt
Normal file
1
build/requirements-macos.txt
Normal file
|
@ -0,0 +1 @@
|
|||
pyinstaller
|
4
dev/install-githook.sh
Normal file
4
dev/install-githook.sh
Normal file
|
@ -0,0 +1,4 @@
|
|||
#!/bin/bash
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
cp "./pre-commit" "../.git/hooks/"
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
BIN
docs/images/system-menu.png
Normal file
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
26
helpers/device_manager.py
Normal 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
|
|
@ -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/"
|
||||
|
|
|
@ -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)
|
||||
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()
|
||||
|
|
26
player.py
26
player.py
|
@ -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.")
|
||||
|
|
47
server.py
47
server.py
|
@ -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:
|
||||
try:
|
||||
player.join()
|
||||
global app
|
||||
app = None
|
||||
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__":
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue