start of channel plans

This commit is contained in:
michael-grace 2020-11-01 02:35:14 +00:00
parent 6044cbb271
commit 330cc32be2
4 changed files with 173 additions and 21 deletions

46
plan.py Normal file
View 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
}

View file

@ -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.")

View file

@ -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)

View file

@ -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()