start of channel plans
This commit is contained in:
parent
6044cbb271
commit
330cc32be2
4 changed files with 173 additions and 21 deletions
46
plan.py
Normal file
46
plan.py
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
|
||||||
|
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"]
|
||||||
|
|
||||||
|
def __dict__(self) -> Dict[str, any]:
|
||||||
|
return {
|
||||||
|
"timeslotitemid": self.timeslotitemid,
|
||||||
|
"name": self.name,
|
||||||
|
"filename": self.filename
|
||||||
|
}
|
85
player.py
85
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
|
# This is the player. Reliability is critical here, so we're catching
|
||||||
# literally every exception possible and handling it.
|
# literally every exception possible and handling it.
|
||||||
|
|
||||||
|
@ -10,8 +25,10 @@ import setproctitle
|
||||||
import copy
|
import copy
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
|
from typing import Callable, Dict, List
|
||||||
from pygame import mixer
|
from pygame import mixer
|
||||||
from state_manager import StateManager
|
from state_manager import StateManager
|
||||||
|
from plan import PlanObject
|
||||||
from mutagen.mp3 import MP3
|
from mutagen.mp3 import MP3
|
||||||
import os
|
import os
|
||||||
os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = "hide"
|
os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = "hide"
|
||||||
|
@ -25,7 +42,7 @@ class Player():
|
||||||
|
|
||||||
__default_state = {
|
__default_state = {
|
||||||
"initialised": False,
|
"initialised": False,
|
||||||
"filename": "",
|
"loaded_item": None,
|
||||||
"channel": -1,
|
"channel": -1,
|
||||||
"playing": False,
|
"playing": False,
|
||||||
"paused": False,
|
"paused": False,
|
||||||
|
@ -36,7 +53,8 @@ class Player():
|
||||||
"remaining": 0,
|
"remaining": 0,
|
||||||
"length": 0,
|
"length": 0,
|
||||||
"loop": False,
|
"loop": False,
|
||||||
"output": None
|
"output": None,
|
||||||
|
"show_plan": []
|
||||||
}
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -60,7 +78,7 @@ class Player():
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def isLoaded(self):
|
def isLoaded(self):
|
||||||
if not self.state.state["filename"]:
|
if not self.state.state["loaded_item"]:
|
||||||
return False
|
return False
|
||||||
if self.isPlaying:
|
if self.isPlaying:
|
||||||
return True
|
return True
|
||||||
|
@ -87,7 +105,13 @@ class Player():
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def status(self):
|
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
|
return res
|
||||||
|
|
||||||
def play(self, pos=0):
|
def play(self, pos=0):
|
||||||
|
@ -126,7 +150,8 @@ class Player():
|
||||||
# if self.isPlaying or self.isPaused:
|
# if self.isPlaying or self.isPaused:
|
||||||
try:
|
try:
|
||||||
mixer.music.stop()
|
mixer.music.stop()
|
||||||
except:
|
except Exception as e:
|
||||||
|
print("Couldn't Stop Player:", e)
|
||||||
return False
|
return False
|
||||||
self.state.update("pos", 0)
|
self.state.update("pos", 0)
|
||||||
self.state.update("pos_offset", 0)
|
self.state.update("pos_offset", 0)
|
||||||
|
@ -147,11 +172,27 @@ class Player():
|
||||||
self._updateState(pos=pos)
|
self._updateState(pos=pos)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def load(self, filename):
|
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 load(self, timeslotitemid: int):
|
||||||
if not self.isPlaying:
|
if not self.isPlaying:
|
||||||
self.unload()
|
self.unload()
|
||||||
|
|
||||||
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:
|
try:
|
||||||
mixer.music.load(filename)
|
mixer.music.load(filename)
|
||||||
|
@ -175,7 +216,7 @@ class Player():
|
||||||
try:
|
try:
|
||||||
mixer.music.unload()
|
mixer.music.unload()
|
||||||
self.state.update("paused", False)
|
self.state.update("paused", False)
|
||||||
self.state.update("filename", "")
|
self.state.update("loaded_item", None)
|
||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
return not self.isLoaded
|
return not self.isLoaded
|
||||||
|
@ -187,7 +228,7 @@ class Player():
|
||||||
def output(self, name=None):
|
def output(self, name=None):
|
||||||
self.quit()
|
self.quit()
|
||||||
self.state.update("output", name)
|
self.state.update("output", name)
|
||||||
self.state.update("filename", "")
|
self.state.update("loaded_item", None)
|
||||||
try:
|
try:
|
||||||
if name:
|
if name:
|
||||||
mixer.init(44100, -16, 1, 1024, devicename=name)
|
mixer.init(44100, -16, 1, 1024, devicename=name)
|
||||||
|
@ -249,9 +290,9 @@ class Player():
|
||||||
print("Using default output device.")
|
print("Using default output device.")
|
||||||
self.output()
|
self.output()
|
||||||
|
|
||||||
if loaded_state["filename"]:
|
if loaded_state["loaded_item"]:
|
||||||
print("Loading filename: " + loaded_state["filename"])
|
print("Loading filename: " + loaded_state["loaded_item"["filename"]])
|
||||||
self.load(loaded_state["filename"])
|
self.load(loaded_state["loaded_item"["timeslotitemid"]])
|
||||||
|
|
||||||
if loaded_state["pos_true"] != 0:
|
if loaded_state["pos_true"] != 0:
|
||||||
print("Seeking to pos_true: " + str(loaded_state["pos_true"]))
|
print("Seeking to pos_true: " + str(loaded_state["pos_true"]))
|
||||||
|
@ -288,17 +329,21 @@ class Player():
|
||||||
self._retMsg(self.isLoaded)
|
self._retMsg(self.isLoaded)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
elif (self.last_msg.startswith("ADD")):
|
||||||
|
split = self.last_msg.split(":")
|
||||||
|
self._retMsg(self.add_to_plan(json.loads(":".join(split[1:]))))
|
||||||
|
|
||||||
elif (self.last_msg == 'PLAY'):
|
elif (self.last_msg == 'PLAY'):
|
||||||
self._retMsg(self.play())
|
self._retMsg(self.play())
|
||||||
|
|
||||||
elif (self.last_msg == 'PAUSE'):
|
elif (self.last_msg == 'PAUSE'):
|
||||||
self._retMsg(self.pause())
|
self._retMsg(self.pause())
|
||||||
|
|
||||||
elif (self.last_msg == 'UNPAUSE'):
|
elif (self.last_msg == 'UNPAUSE'):
|
||||||
self._retMsg(self.unpause())
|
self._retMsg(self.unpause())
|
||||||
|
|
||||||
elif (self.last_msg == 'STOP'):
|
elif (self.last_msg == 'STOP'):
|
||||||
self._retMsg(self.stop())
|
self._retMsg(self.stop())
|
||||||
|
|
||||||
elif (self.last_msg == 'QUIT'):
|
elif (self.last_msg == 'QUIT'):
|
||||||
self.running = False
|
self.running = False
|
||||||
|
@ -310,13 +355,13 @@ class Player():
|
||||||
|
|
||||||
elif (self.last_msg.startswith("LOAD")):
|
elif (self.last_msg.startswith("LOAD")):
|
||||||
split = self.last_msg.split(":")
|
split = self.last_msg.split(":")
|
||||||
self._retMsg(self.load(split[1]))
|
self._retMsg(self.load(int(split[1])))
|
||||||
|
|
||||||
elif (self.last_msg == 'UNLOAD'):
|
elif (self.last_msg == 'UNLOAD'):
|
||||||
self._retMsg(self.unload())
|
self._retMsg(self.unload())
|
||||||
|
|
||||||
elif (self.last_msg == 'STATUS'):
|
elif (self.last_msg == 'STATUS'):
|
||||||
self._retMsg(self.status, True)
|
self._retMsg(self.status, True)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self._retMsg("Unknown Command")
|
self._retMsg("Unknown Command")
|
||||||
|
@ -365,7 +410,7 @@ if __name__ == "__main__":
|
||||||
# Do some testing
|
# Do some testing
|
||||||
in_q.put("LOADED?")
|
in_q.put("LOADED?")
|
||||||
in_q.put("PLAY")
|
in_q.put("PLAY")
|
||||||
in_q.put("LOAD:\\Users\\matth\\Documents\\GitHub\\bapsicle\\dev\\test.mp3")
|
in_q.put("LOAD:dev/test.mp3") # I mean, this won't work now, this can get sorted :)
|
||||||
in_q.put("LOADED?")
|
in_q.put("LOADED?")
|
||||||
in_q.put("PLAY")
|
in_q.put("PLAY")
|
||||||
print("Entering infinite loop.")
|
print("Entering infinite loop.")
|
||||||
|
|
55
server.py
55
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 multiprocessing
|
||||||
import player
|
import player
|
||||||
from flask import Flask, render_template, send_from_directory, request
|
from flask import Flask, render_template, send_from_directory, request
|
||||||
|
@ -123,6 +138,10 @@ def output(channel, name):
|
||||||
channel_to_q[channel].put("OUTPUT:" + name)
|
channel_to_q[channel].put("OUTPUT:" + name)
|
||||||
return ui_status()
|
return ui_status()
|
||||||
|
|
||||||
|
@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")
|
@app.route("/player/<int:channel>/unload")
|
||||||
def unload(channel):
|
def unload(channel):
|
||||||
|
@ -131,6 +150,32 @@ def unload(channel):
|
||||||
|
|
||||||
return ui_status()
|
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>/status")
|
@app.route("/player/<int:channel>/status")
|
||||||
def status(channel):
|
def status(channel):
|
||||||
|
@ -174,6 +219,16 @@ def startServer():
|
||||||
)
|
)
|
||||||
channel_p[channel].start()
|
channel_p[channel].start()
|
||||||
|
|
||||||
|
# There is a plan for this, but I'm going to leave this here until i sort it
|
||||||
|
new_item: Dict[str, any] = {
|
||||||
|
"timeslotitemid": 0,
|
||||||
|
"filename": "dev/test.mp3",
|
||||||
|
"title": "Test File",
|
||||||
|
"artist": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
channel_to_q[0].put("ADD:" + json.dumps(new_item))
|
||||||
|
|
||||||
# Don't use reloader, it causes Nested Processes!
|
# Don't use reloader, it causes Nested Processes!
|
||||||
app.run(host=config.HOST, port=config.PORT, debug=True, use_reloader=False)
|
app.run(host=config.HOST, port=config.PORT, debug=True, use_reloader=False)
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import copy
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from helpers.os_environment import resolve_external_file_path
|
from helpers.os_environment import resolve_external_file_path
|
||||||
|
@ -43,8 +44,13 @@ class StateManager:
|
||||||
self.__state = state
|
self.__state = state
|
||||||
|
|
||||||
file = open(self.filepath, "w")
|
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()
|
file.close()
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue