mainly channel plans
Mainly Channel Plans
This commit is contained in:
commit
9853118d6c
7 changed files with 345 additions and 47 deletions
11
.gitignore
vendored
11
.gitignore
vendored
|
@ -1,5 +1,5 @@
|
|||
|
||||
|
||||
.vscode/settings.json
|
||||
|
||||
__pycache__/
|
||||
|
||||
|
@ -10,6 +10,7 @@ state/
|
|||
build/build-exe-config.json
|
||||
|
||||
install/*.exe
|
||||
install/nssm
|
||||
|
||||
*.pyo
|
||||
|
||||
|
@ -21,4 +22,12 @@ build/build/BAPSicle/
|
|||
|
||||
build/output/
|
||||
|
||||
|
||||
venv/
|
||||
|
||||
config.py
|
||||
|
||||
dev/welcome.mp3
|
||||
|
||||
build/build-exe-pyinstaller-command.sh
|
||||
|
||||
|
|
|
@ -3,4 +3,5 @@ flask
|
|||
mutagen
|
||||
sounddevice
|
||||
autopep8
|
||||
setproctitle
|
||||
setproctitle
|
||||
pyttsx3
|
6
config.py.example
Normal file
6
config.py.example
Normal file
|
@ -0,0 +1,6 @@
|
|||
# Flask Details
|
||||
HOST: str = "localhost"
|
||||
PORT: int = 5000
|
||||
|
||||
# BAPSicle Details
|
||||
VERSION: float = 1.0
|
50
plan.py
Normal file
50
plan.py
Normal file
|
@ -0,0 +1,50 @@
|
|||
"""
|
||||
BAPSicle Server
|
||||
Next-gen audio playout server for University Radio York playout,
|
||||
based on WebStudio interface.
|
||||
|
||||
Show Plan Items
|
||||
|
||||
Authors:
|
||||
Michael Grace
|
||||
|
||||
Date:
|
||||
November 2020
|
||||
"""
|
||||
|
||||
from typing import Dict
|
||||
|
||||
class PlanObject:
|
||||
_timeslotitemid: int = 0
|
||||
_filename: str = ""
|
||||
_title: str = ""
|
||||
_artist: str = ""
|
||||
|
||||
@property
|
||||
def timeslotitemid(self) -> int:
|
||||
return self._timeslotitemid
|
||||
|
||||
@property
|
||||
def filename(self) -> str:
|
||||
return self._filename
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "{0} - {1}".format(self._title, self._artist) if self._artist else self._title
|
||||
|
||||
@property
|
||||
def __dict__(self) -> Dict[str, any]:
|
||||
return {
|
||||
"timeslotitemid": self.timeslotitemid,
|
||||
"title": self._title,
|
||||
"artist": self._artist,
|
||||
"name": self.name,
|
||||
"filename": self.filename
|
||||
}
|
||||
|
||||
def __init__(self, new_item: Dict[str, any]):
|
||||
self._timeslotitemid = new_item["timeslotitemid"]
|
||||
self._filename = new_item["filename"]
|
||||
self._title = new_item["title"]
|
||||
self._artist = new_item["artist"]
|
||||
|
196
player.py
196
player.py
|
@ -1,3 +1,18 @@
|
|||
"""
|
||||
BAPSicle Server
|
||||
Next-gen audio playout server for University Radio York playout,
|
||||
based on WebStudio interface.
|
||||
|
||||
Audio Player
|
||||
|
||||
Authors:
|
||||
Matthew Stratford
|
||||
Michael Grace
|
||||
|
||||
Date:
|
||||
October, November 2020
|
||||
"""
|
||||
|
||||
# This is the player. Reliability is critical here, so we're catching
|
||||
# literally every exception possible and handling it.
|
||||
|
||||
|
@ -10,6 +25,11 @@ import setproctitle
|
|||
import copy
|
||||
import json
|
||||
import time
|
||||
|
||||
from typing import Callable, Dict, List
|
||||
|
||||
from plan import PlanObject
|
||||
|
||||
import os
|
||||
import sys
|
||||
os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = "hide"
|
||||
|
@ -29,7 +49,7 @@ class Player():
|
|||
|
||||
__default_state = {
|
||||
"initialised": False,
|
||||
"filename": "",
|
||||
"loaded_item": None,
|
||||
"channel": -1,
|
||||
"playing": False,
|
||||
"paused": False,
|
||||
|
@ -39,8 +59,11 @@ class Player():
|
|||
"pos_true": 0,
|
||||
"remaining": 0,
|
||||
"length": 0,
|
||||
"loop": False,
|
||||
"output": None
|
||||
"auto_advance": True,
|
||||
"repeat": "NONE", #NONE, ONE or ALL
|
||||
"play_on_load": False,
|
||||
"output": None,
|
||||
"show_plan": []
|
||||
}
|
||||
|
||||
@property
|
||||
|
@ -64,7 +87,7 @@ class Player():
|
|||
|
||||
@property
|
||||
def isLoaded(self):
|
||||
if not self.state.state["filename"]:
|
||||
if not self.state.state["loaded_item"]:
|
||||
return False
|
||||
if self.isPlaying:
|
||||
return True
|
||||
|
@ -91,9 +114,17 @@ class Player():
|
|||
|
||||
@property
|
||||
def status(self):
|
||||
res = json.dumps(self.state.state)
|
||||
state = copy.copy(self.state.state)
|
||||
|
||||
# Not the biggest fan of this, but maybe I'll get a better solution for this later
|
||||
state["loaded_item"] = state["loaded_item"].__dict__ if state["loaded_item"] else None
|
||||
state["show_plan"] = [repr.__dict__ for repr in state["show_plan"]]
|
||||
|
||||
res = json.dumps(state)
|
||||
return res
|
||||
|
||||
### Audio Playout Related Methods
|
||||
|
||||
def play(self, pos=0):
|
||||
# if not self.isPlaying:
|
||||
try:
|
||||
|
@ -130,7 +161,8 @@ class Player():
|
|||
# if self.isPlaying or self.isPaused:
|
||||
try:
|
||||
mixer.music.stop()
|
||||
except:
|
||||
except Exception as e:
|
||||
print("Couldn't Stop Player:", e)
|
||||
return False
|
||||
self.state.update("pos", 0)
|
||||
self.state.update("pos_offset", 0)
|
||||
|
@ -151,7 +183,53 @@ class Player():
|
|||
self._updateState(pos=pos)
|
||||
return True
|
||||
|
||||
def load(self, filename):
|
||||
def set_auto_advance(self, message: int) -> bool:
|
||||
if message == 0:
|
||||
self.state.update("auto_advance", False)
|
||||
return True # It did it
|
||||
elif message == 1:
|
||||
self.state.update("auto_advance", True)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def set_repeat(self, message: str) -> bool:
|
||||
if message in ["ALL", "ONE", "NONE"]:
|
||||
self.state.update("repeat", message)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def set_play_on_load(self, message: int) -> bool:
|
||||
if message == 0:
|
||||
self.state.update("play_on_load", False)
|
||||
return True # It did it
|
||||
elif message == 1:
|
||||
self.state.update("play_on_load", True)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
### Show Plan Related Methods
|
||||
|
||||
def add_to_plan(self, new_item: Dict[str, any]) -> bool:
|
||||
self.state.update("show_plan", self.state.state["show_plan"] + [PlanObject(new_item)])
|
||||
return True
|
||||
|
||||
def remove_from_plan(self, timeslotitemid: int) -> bool:
|
||||
plan_copy = copy.copy(self.state.state["show_plan"])
|
||||
for i in range(len(plan_copy)):
|
||||
if plan_copy[i].timeslotitemid == timeslotitemid:
|
||||
plan_copy.remove(i)
|
||||
self.state.update("show_plan", plan_copy)
|
||||
return True
|
||||
return False
|
||||
|
||||
def clear_channel_plan(self) -> bool:
|
||||
self.state.update("show_plan", [])
|
||||
return True
|
||||
|
||||
def load(self, timeslotitemid: int):
|
||||
if not self.isPlaying:
|
||||
self.unload()
|
||||
# Fix any OS specific / or \'s
|
||||
|
@ -160,8 +238,21 @@ class Player():
|
|||
else:
|
||||
filename = filename.replace("/", '\\')
|
||||
|
||||
print(filename)
|
||||
self.state.update("filename", filename)
|
||||
|
||||
updated: bool = False
|
||||
|
||||
for i in range(len(self.state.state["show_plan"])):
|
||||
if self.state.state["show_plan"][i].timeslotitemid == timeslotitemid:
|
||||
self.state.update("loaded_item", self.state.state["show_plan"][i])
|
||||
updated = True
|
||||
break
|
||||
|
||||
if not updated:
|
||||
print("Failed to find timeslotitemid:", timeslotitemid)
|
||||
return False
|
||||
|
||||
filename: str = self.state.state["loaded_item"].filename
|
||||
|
||||
|
||||
try:
|
||||
mixer.music.load(filename)
|
||||
|
@ -185,7 +276,7 @@ class Player():
|
|||
try:
|
||||
mixer.music.unload()
|
||||
self.state.update("paused", False)
|
||||
self.state.update("filename", "")
|
||||
self.state.update("loaded_item", None)
|
||||
except:
|
||||
return False
|
||||
return not self.isLoaded
|
||||
|
@ -197,7 +288,7 @@ class Player():
|
|||
def output(self, name=None):
|
||||
self.quit()
|
||||
self.state.update("output", name)
|
||||
self.state.update("filename", "")
|
||||
self.state.update("loaded_item", None)
|
||||
try:
|
||||
if name:
|
||||
mixer.init(44100, -16, 1, 1024, devicename=name)
|
||||
|
@ -226,6 +317,31 @@ class Player():
|
|||
|
||||
self.state.update("remaining", self.state.state["length"] - self.state.state["pos_true"])
|
||||
|
||||
if self.state.state["remaining"] == 0:
|
||||
# Track has ended
|
||||
print("Finished", self.state.state["loaded_item"].name)
|
||||
|
||||
# Repeat 1
|
||||
if self.state.state["repeat"] == "ONE":
|
||||
self.play()
|
||||
|
||||
# Auto Advance
|
||||
elif self.state.state["auto_advance"]:
|
||||
for i in range(len(self.state.state["show_plan"])):
|
||||
if self.state.state["show_plan"][i].timeslotitemid == self.state.state["loaded_item"].timeslotitemid:
|
||||
if len(self.state.state["show_plan"]) > i+1:
|
||||
self.load(self.state.state["show_plan"][i+1].timeslotitemid)
|
||||
break
|
||||
|
||||
# Repeat All
|
||||
elif self.state.state["repeat"] == "ALL":
|
||||
self.load(self.state.state["show_plan"][0].timeslotitemid)
|
||||
|
||||
# Play on Load
|
||||
if self.state.state["play_on_load"]:
|
||||
self.play()
|
||||
|
||||
|
||||
def _retMsg(self, msg, okay_str=False):
|
||||
response = self.last_msg + ":"
|
||||
if msg == True:
|
||||
|
@ -259,9 +375,9 @@ class Player():
|
|||
print("Using default output device.")
|
||||
self.output()
|
||||
|
||||
if loaded_state["filename"]:
|
||||
print("Loading filename: " + loaded_state["filename"])
|
||||
self.load(loaded_state["filename"])
|
||||
if loaded_state["loaded_item"]:
|
||||
print("Loading filename: " + loaded_state["loaded_item"].filename)
|
||||
self.load(loaded_state["loaded_item"].timeslotitemid)
|
||||
|
||||
if loaded_state["pos_true"] != 0:
|
||||
print("Seeking to pos_true: " + str(loaded_state["pos_true"]))
|
||||
|
@ -294,40 +410,37 @@ class Player():
|
|||
|
||||
elif self.isInit:
|
||||
|
||||
if (self.last_msg == 'LOADED?'):
|
||||
self._retMsg(self.isLoaded)
|
||||
continue
|
||||
message_types: Dict[str, Callable[any, bool]] = { # TODO Check Types
|
||||
"STATUS": lambda: self._retMsg(self.status, True),
|
||||
|
||||
# Audio Playout
|
||||
"PLAY": lambda: self._retMsg(self.play()),
|
||||
"PAUSE": lambda: self._retMsg(self.pause()),
|
||||
"UNPAUSE": lambda: self._retMsg(self.unpause()),
|
||||
"STOP": lambda: self._retMsg(self.stop()),
|
||||
"SEEK": lambda: self._retMsg(self.seek(float(self.last_msg.split(":")[1]))),
|
||||
"AUTOADVANCE": lambda: self._retMsg(self.set_auto_advance(int(self.last_msg.split(":")[1]))),
|
||||
"REPEAT": lambda: self._retMsg(self.set_repeat(self.last_msg.split(":")[1])),
|
||||
"PLAYONLOAD": lambda: self._retMsg(self.set_play_on_load(int(self.last_msg.split(":")[1]))),
|
||||
|
||||
elif (self.last_msg == 'PLAY'):
|
||||
self._retMsg(self.play())
|
||||
# Show Plan Items
|
||||
"LOAD": lambda: self._retMsg(self.load(int(self.last_msg.split(":")[1]))),
|
||||
"LOADED?": lambda: self._retMsg(self.isLoaded),
|
||||
"UNLOAD": lambda: self._retMsg(self.unload()),
|
||||
"ADD": lambda: self._retMsg(self.add_to_plan(json.loads(":".join(self.last_msg.split(":")[1:])))),
|
||||
"REMOVE": lambda: self._retMsg(self.remove_from_plan(int(self.last_msg.split(":")[1]))),
|
||||
"CLEAR": lambda: self._retMsg(self.clear_channel_plan())
|
||||
}
|
||||
|
||||
elif (self.last_msg == 'PAUSE'):
|
||||
self._retMsg(self.pause())
|
||||
message_type: str = self.last_msg.split(":")[0]
|
||||
|
||||
elif (self.last_msg == 'UNPAUSE'):
|
||||
self._retMsg(self.unpause())
|
||||
|
||||
elif (self.last_msg == 'STOP'):
|
||||
self._retMsg(self.stop())
|
||||
if message_type in message_types.keys():
|
||||
message_types[message_type]()
|
||||
|
||||
elif (self.last_msg == 'QUIT'):
|
||||
self.running = False
|
||||
continue
|
||||
|
||||
elif (self.last_msg.startswith("SEEK")):
|
||||
split = self.last_msg.split(":")
|
||||
self._retMsg(self.seek(float(split[1])))
|
||||
|
||||
elif (self.last_msg.startswith("LOAD")):
|
||||
split = self.last_msg.split(":")
|
||||
self._retMsg(self.load(split[1]))
|
||||
|
||||
elif (self.last_msg == 'UNLOAD'):
|
||||
self._retMsg(self.unload())
|
||||
|
||||
elif (self.last_msg == 'STATUS'):
|
||||
self._retMsg(self.status, True)
|
||||
|
||||
else:
|
||||
self._retMsg("Unknown Command")
|
||||
else:
|
||||
|
@ -378,7 +491,8 @@ if __name__ == "__main__":
|
|||
# Do some testing
|
||||
in_q.put("LOADED?")
|
||||
in_q.put("PLAY")
|
||||
in_q.put("LOAD:\\Users\\mstratford\\Documents\\Dev\\GitHub\\bapsicle\\dev\\test.mp3")
|
||||
|
||||
in_q.put("LOAD:dev/test.mp3")
|
||||
in_q.put("LOADED?")
|
||||
in_q.put("PLAY")
|
||||
print("Entering infinite loop.")
|
||||
|
|
113
server.py
113
server.py
|
@ -1,3 +1,18 @@
|
|||
"""
|
||||
BAPSicle Server
|
||||
Next-gen audio playout server for University Radio York playout,
|
||||
based on WebStudio interface.
|
||||
|
||||
Flask Server
|
||||
|
||||
Authors:
|
||||
Matthew Stratford
|
||||
Michael Grace
|
||||
|
||||
Date:
|
||||
October, November 2020
|
||||
"""
|
||||
|
||||
import multiprocessing
|
||||
import player
|
||||
from flask import Flask, render_template, send_from_directory, request
|
||||
|
@ -31,6 +46,8 @@ channel_p = []
|
|||
stopping = False
|
||||
|
||||
|
||||
### General Endpoints
|
||||
|
||||
@app.errorhandler(404)
|
||||
def page_not_found(e):
|
||||
data = {
|
||||
|
@ -79,6 +96,7 @@ def ui_status():
|
|||
}
|
||||
return render_template('status.html', data=data)
|
||||
|
||||
### Channel Audio Options
|
||||
|
||||
@app.route("/player/<int:channel>/play")
|
||||
def play(channel):
|
||||
|
@ -125,6 +143,27 @@ def output(channel, name):
|
|||
channel_to_q[channel].put("OUTPUT:" + name)
|
||||
return ui_status()
|
||||
|
||||
@app.route("/player/<int:channel>/autoadvance/<int:state>")
|
||||
def autoadvance(channel: int, state: int):
|
||||
channel_to_q[channel].put("AUTOADVANCE:" + str(state))
|
||||
return ui_status()
|
||||
|
||||
@app.route("/player/<int:channel>/repeat/<state>")
|
||||
def autoadvance(channel: int, state):
|
||||
channel_to_q[channel].put("REPEAT:" + state.upper())
|
||||
return ui_status()
|
||||
|
||||
@app.route("/player/<int:channel>/playonload/<int:state>")
|
||||
def autoadvance(channel: int, state: int):
|
||||
channel_to_q[channel].put("PLAYONLOAD:" + str(state))
|
||||
return ui_status()
|
||||
|
||||
### Channel Items
|
||||
|
||||
@app.route("/player/<int:channel>/load/<int:timeslotitemid>")
|
||||
def load(channel:int, timeslotitemid: int):
|
||||
channel_to_q[channel].put("LOAD:" + str(timeslotitemid))
|
||||
return ui_status()
|
||||
|
||||
@app.route("/player/<int:channel>/unload")
|
||||
def unload(channel):
|
||||
|
@ -133,6 +172,41 @@ def unload(channel):
|
|||
|
||||
return ui_status()
|
||||
|
||||
@app.route("/player/<int:channel>/add", methods=["POST"])
|
||||
def add_to_plan(channel: int):
|
||||
new_item: Dict[str, any] = {
|
||||
"timeslotitemid": int(request.form["timeslotitemid"]),
|
||||
"filename": request.form["filename"],
|
||||
"title": request.form["title"],
|
||||
"artist": request.form["artist"],
|
||||
}
|
||||
|
||||
channel_to_q[channel].put("ADD:" + json.dumps(new_item))
|
||||
|
||||
return new_item
|
||||
|
||||
@app.route("/player/<int:channel>/move/<int:timeslotitemid>/<int:position>")
|
||||
def move_plan(channel: int, timeslotitemid: int, position: int):
|
||||
channel_to_q[channel].put("MOVE:" + json.dumps({"timeslotitemid": timeslotitemid, "position": position}))
|
||||
|
||||
#TODO Return
|
||||
return True
|
||||
|
||||
@app.route("/player/<int:channel>/remove/<int:timeslotitemid>")
|
||||
def remove_plan(channel: int, timeslotitemid: int):
|
||||
channel_to_q[channel].put("REMOVE:" + timeslotitemid)
|
||||
|
||||
#TODO Return
|
||||
return True
|
||||
|
||||
@app.route("/player/<int:channel>/clear")
|
||||
def clear_channel_plan(channel: int):
|
||||
channel_to_q[channel].put("CLEAR")
|
||||
|
||||
#TODO Return
|
||||
return True
|
||||
|
||||
### General Channel Endpoints
|
||||
|
||||
@app.route("/player/<int:channel>/status")
|
||||
def status(channel):
|
||||
|
@ -161,7 +235,14 @@ def quit():
|
|||
def all_stop():
|
||||
for channel in channel_to_q:
|
||||
channel.put("STOP")
|
||||
ui_status()
|
||||
return ui_status()
|
||||
|
||||
|
||||
@app.route("/player/all/clear")
|
||||
def clear_all_channels():
|
||||
for channel in channel_to_q:
|
||||
channel.put("CLEAR")
|
||||
return ui_status()
|
||||
|
||||
|
||||
@app.route('/static/<path:path>')
|
||||
|
@ -185,9 +266,35 @@ def startServer():
|
|||
)
|
||||
channel_p[channel].start()
|
||||
|
||||
# Don't use reloader, it causes Nested Processes!
|
||||
app.run(host='0.0.0.0', port=13500, debug=True, use_reloader=False)
|
||||
# Welcome Speech
|
||||
|
||||
text_to_speach = pyttsx3.init()
|
||||
text_to_speach.save_to_file(
|
||||
"""Thank-you for installing BAPSicle - the play-out server from the broadcasting and presenting suite.
|
||||
This server is accepting connections on port {0}
|
||||
The version of the server service is {1}
|
||||
Please refer to the documentation included with this application for further assistance.""".format(
|
||||
config.PORT,
|
||||
config.VERSION
|
||||
),
|
||||
"dev/welcome.mp3"
|
||||
)
|
||||
text_to_speach.runAndWait()
|
||||
|
||||
new_item: Dict[str, any] = {
|
||||
"timeslotitemid": 0,
|
||||
"filename": "dev/welcome.mp3",
|
||||
"title": "Welcome to BAPSicle",
|
||||
"artist": "University Radio York",
|
||||
}
|
||||
|
||||
channel_to_q[0].put("ADD:" + json.dumps(new_item))
|
||||
channel_to_q[0].put("LOAD:0")
|
||||
channel_to_q[0].put("PLAY")
|
||||
|
||||
# Don't use reloader, it causes Nested Processes!
|
||||
|
||||
app.run(host='0.0.0.0', port=13500, debug=True, use_reloader=False)
|
||||
|
||||
def stopServer():
|
||||
print("Stopping server.py")
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import copy
|
||||
import json
|
||||
import os
|
||||
from helpers.os_environment import resolve_external_file_path
|
||||
from plan import PlanObject
|
||||
|
||||
|
||||
class StateManager:
|
||||
|
@ -39,6 +41,10 @@ class StateManager:
|
|||
else:
|
||||
self.__state = json.loads(file_state)
|
||||
|
||||
# Turn from JSON -> PlanObject
|
||||
self.__state["loaded_item"] = PlanObject(self.__state["loaded_item"]) if self.__state["loaded_item"] else None
|
||||
self.__state["show_plan"] = [PlanObject(obj) for obj in self.__state["show_plan"]]
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
return self.__state
|
||||
|
@ -48,8 +54,13 @@ class StateManager:
|
|||
self.__state = state
|
||||
|
||||
file = open(self.filepath, "w")
|
||||
|
||||
# Not the biggest fan of this, but maybe I'll get a better solution for this later
|
||||
state_to_json = copy.copy(state)
|
||||
state_to_json["loaded_item"] = state_to_json["loaded_item"].__dict__ if state_to_json["loaded_item"] else None
|
||||
state_to_json["show_plan"] = [repr.__dict__ for repr in state_to_json["show_plan"]]
|
||||
|
||||
file.write(json.dumps(state, indent=2, sort_keys=True))
|
||||
file.write(json.dumps(state_to_json, indent=2, sort_keys=True))
|
||||
|
||||
file.close()
|
||||
|
||||
|
|
Loading…
Reference in a new issue