Lint with black for formatting.

This commit is contained in:
Matthew Stratford 2021-09-11 16:49:08 +01:00
parent 398c0ac0ce
commit 2941d90f60
16 changed files with 623 additions and 374 deletions

View file

@ -20,8 +20,11 @@ from time import time
from baps_types.marker import Marker
def _time_ms():
return round(time() * 1000)
class PlanItem:
_timeslotitemid: str = "0"
_weight: int = 0
@ -72,7 +75,7 @@ class PlanItem:
self._played_at = _time_ms()
def play_count_decrement(self):
self._play_count = max(0,self._play_count - 1)
self._play_count = max(0, self._play_count - 1)
if self._play_count == 0:
self._played_at = 0
@ -118,7 +121,9 @@ class PlanItem:
@property
def intro(self) -> float:
markers = list(filter(lambda m: m.position == "start" and m.section is None, self._markers))
markers = list(
filter(lambda m: m.position == "start" and m.section is None, self._markers)
)
# TODO: Handle multiple (shouldn't happen?)
if len(markers) > 0:
return markers[0].time
@ -126,7 +131,9 @@ class PlanItem:
@property
def cue(self) -> float:
markers = list(filter(lambda m: m.position == "mid" and m.section is None, self._markers))
markers = list(
filter(lambda m: m.position == "mid" and m.section is None, self._markers)
)
# TODO: Handle multiple (shouldn't happen?)
if len(markers) > 0:
return markers[0].time
@ -134,7 +141,9 @@ class PlanItem:
@property
def outro(self) -> float:
markers = list(filter(lambda m: m.position == "end" and m.section is None, self._markers))
markers = list(
filter(lambda m: m.position == "end" and m.section is None, self._markers)
)
# TODO: Handle multiple (shouldn't happen?)
if len(markers) > 0:
return markers[0].time
@ -164,7 +173,7 @@ class PlanItem:
"played": self.play_count > 0,
"played_at": self.played_at,
"play_count": self.play_count,
"clean": self.clean
"clean": self.clean,
}
def __init__(self, new_item: Dict[str, Any]):
@ -183,40 +192,59 @@ class PlanItem:
self._artist = new_item["artist"] if "artist" in new_item else None
self._length = new_item["length"]
self._markers = (
[Marker(marker) for marker in new_item["markers"]] if "markers" in new_item else []
[Marker(marker) for marker in new_item["markers"]]
if "markers" in new_item
else []
)
self._play_count = new_item["play_count"] if "play_count" in new_item else 0
self._played_at = new_item["played_at"] if "played_at" in new_item else 0
self._clean = new_item["clean"] if "clean" in new_item else True
# TODO: Edit this to handle markers when MyRadio supports them
if "intro" in new_item and (isinstance(new_item["intro"], int) or isinstance(new_item["intro"], float)) and new_item["intro"] > 0:
if (
"intro" in new_item
and (
isinstance(new_item["intro"], int)
or isinstance(new_item["intro"], float)
)
and new_item["intro"] > 0
):
marker = {
"name": "Intro",
"time": new_item["intro"],
"position": "start",
"section": None
"section": None,
}
self.set_marker(Marker(json.dumps(marker)))
if "cue" in new_item and (isinstance(new_item["cue"], int) or isinstance(new_item["cue"], float)) and new_item["cue"] > 0:
if (
"cue" in new_item
and (isinstance(new_item["cue"], int) or isinstance(new_item["cue"], float))
and new_item["cue"] > 0
):
marker = {
"name": "Cue",
"time": new_item["cue"],
"position": "mid",
"section": None
"section": None,
}
self.set_marker(Marker(json.dumps(marker)))
# TODO: Convert / handle outro being from end of item.
if "outro" in new_item and (isinstance(new_item["outro"], int) or isinstance(new_item["outro"], float)) and new_item["outro"] > 0:
if (
"outro" in new_item
and (
isinstance(new_item["outro"], int)
or isinstance(new_item["outro"], float)
)
and new_item["outro"] > 0
):
marker = {
"name": "Outro",
"time": new_item["outro"],
"position": "end",
"section": None
"section": None,
}
self.set_marker(Marker(json.dumps(marker)))
# Fix any OS specific / or \'s
if self.filename:
if os.path.sep == "/":

View file

@ -1,4 +1,3 @@
from helpers.the_terminator import Terminator
from typing import List, Optional
from multiprocessing import Queue, current_process
@ -95,12 +94,12 @@ class MattchBox(Controller):
self.logger.log.info("Received from controller: " + str(line))
if line == 255:
self.ser.write(b"\xff") # Send 255 back, this is a keepalive.
elif line in [51,52,53]:
elif line in [51, 52, 53]:
# We've received a status update about fader live status, fader is down.
self.sendToPlayer(line-51, "SETLIVE:False")
elif line in [61,62,63]:
self.sendToPlayer(line - 51, "SETLIVE:False")
elif line in [61, 62, 63]:
# We've received a status update about fader live status, fader is up.
self.sendToPlayer(line-61, "SETLIVE:True")
self.sendToPlayer(line - 61, "SETLIVE:True")
elif line in [1, 3, 5]:
self.sendToPlayer(int(line / 2), "PLAYPAUSE")
elif line in [2, 4, 6]:
@ -136,5 +135,7 @@ class MattchBox(Controller):
self.connect(None)
def sendToPlayer(self, channel: int, msg: str):
self.logger.log.info("Sending message to player channel {}: {}".format(channel, msg))
self.logger.log.info(
"Sending message to player channel {}: {}".format(channel, msg)
)
self.server_to_q[channel].put("CONTROLLER:" + msg)

View file

@ -31,17 +31,20 @@ class FileManager:
terminator = Terminator()
self.channel_count = len(channel_from_q)
self.channel_received = None
self.last_known_show_plan = [[]]*self.channel_count
self.last_known_show_plan = [[]] * self.channel_count
self.next_channel_preload = 0
self.known_channels_preloaded = [False]*self.channel_count
self.known_channels_normalised = [False]*self.channel_count
self.last_known_item_ids = [[]]*self.channel_count
self.known_channels_preloaded = [False] * self.channel_count
self.known_channels_normalised = [False] * self.channel_count
self.last_known_item_ids = [[]] * self.channel_count
try:
while not terminator.terminate:
# If all channels have received the delete command, reset for the next one.
if (self.channel_received == None or self.channel_received == [True]*self.channel_count):
self.channel_received = [False]*self.channel_count
if (
self.channel_received == None
or self.channel_received == [True] * self.channel_count
):
self.channel_received = [False] * self.channel_count
for channel in range(self.channel_count):
try:
@ -50,13 +53,16 @@ class FileManager:
continue
try:
#source = message.split(":")[0]
command = message.split(":",2)[1]
# source = message.split(":")[0]
command = message.split(":", 2)[1]
# If we have requested a new show plan, empty the music-tmp directory for the previous show.
if command == "GETPLAN":
if self.channel_received != [False]*self.channel_count and self.channel_received[channel] != True:
if (
self.channel_received != [False] * self.channel_count
and self.channel_received[channel] != True
):
# We've already received a delete trigger on a channel, let's not delete the folder more than once.
# If the channel was already in the process of being deleted, the user has requested it again, so allow it.
@ -68,28 +74,42 @@ class FileManager:
path: str = resolve_external_file_path("/music-tmp/")
if not os.path.isdir(path):
self.logger.log.warning("Music-tmp folder is missing, not handling.")
self.logger.log.warning(
"Music-tmp folder is missing, not handling."
)
continue
files = [f for f in os.listdir(path) if os.path.isfile(os.path.join(path, f))]
files = [
f
for f in os.listdir(path)
if os.path.isfile(os.path.join(path, f))
]
for file in files:
if isWindows():
filepath = path+"\\"+file
filepath = path + "\\" + file
else:
filepath = path+"/"+file
self.logger.log.info("Removing file {} on new show load.".format(filepath))
filepath = path + "/" + file
self.logger.log.info(
"Removing file {} on new show load.".format(
filepath
)
)
try:
os.remove(filepath)
except Exception:
self.logger.log.warning("Failed to remove, skipping. Likely file is still in use.")
self.logger.log.warning(
"Failed to remove, skipping. Likely file is still in use."
)
continue
self.channel_received[channel] = True
self.known_channels_preloaded = [False]*self.channel_count
self.known_channels_normalised = [False]*self.channel_count
self.known_channels_preloaded = [False] * self.channel_count
self.known_channels_normalised = [
False
] * self.channel_count
# If we receive a new status message, let's check for files which have not been pre-loaded.
if command == "STATUS":
extra = message.split(":",3)
extra = message.split(":", 3)
if extra[2] != "OKAY":
continue
@ -107,28 +127,30 @@ class FileManager:
self.known_channels_preloaded[channel] = False
except Exception:
self.logger.log.exception("Failed to handle message {} on channel {}.".format(message, channel))
self.logger.log.exception(
"Failed to handle message {} on channel {}.".format(
message, channel
)
)
# Let's try preload / normalise some files now we're free of messages.
preloaded = self.do_preload()
normalised = self.do_normalise()
if (not preloaded and not normalised):
if not preloaded and not normalised:
# We didn't do any hard work, let's sleep.
sleep(0.2)
except Exception as e:
self.logger.log.exception(
"Received unexpected exception: {}".format(e))
self.logger.log.exception("Received unexpected exception: {}".format(e))
del self.logger
# Attempt to preload a file onto disk.
def do_preload(self):
channel = self.next_channel_preload
# All channels have preloaded all files, do nothing.
if (self.known_channels_preloaded == [True]*self.channel_count):
if self.known_channels_preloaded == [True] * self.channel_count:
return False # Didn't preload anything
# Right, let's have a quick check in the status for shows without filenames, to preload them.
@ -141,10 +163,16 @@ class FileManager:
# We've not downloaded this file yet, let's do that.
if not item_obj.filename:
self.logger.log.info("Checking pre-load on channel {}, weight {}: {}".format(channel, item_obj.weight, item_obj.name))
self.logger.log.info(
"Checking pre-load on channel {}, weight {}: {}".format(
channel, item_obj.weight, item_obj.name
)
)
# Getting the file name will only pull the new file if the file doesn't already exist, so this is not too inefficient.
item_obj.filename,did_download = sync(self.api.get_filename(item_obj, True))
item_obj.filename, did_download = sync(
self.api.get_filename(item_obj, True)
)
# Alright, we've done one, now let's give back control to process new statuses etc.
# Save back the resulting item back in regular dict form
@ -152,7 +180,9 @@ class FileManager:
if did_download:
downloaded_something = True
self.logger.log.info("File successfully preloaded: {}".format(item_obj.filename))
self.logger.log.info(
"File successfully preloaded: {}".format(item_obj.filename)
)
break
else:
# We didn't download anything this time, file was already loaded.
@ -168,15 +198,14 @@ class FileManager:
return downloaded_something
# If we've preloaded everything, get to work normalising tracks before playback.
def do_normalise(self):
# Some channels still have files to preload, do nothing.
if (self.known_channels_preloaded != [True]*self.channel_count):
if self.known_channels_preloaded != [True] * self.channel_count:
return False # Didn't normalise
# Quit early if all channels are normalised already.
if (self.known_channels_normalised == [True]*self.channel_count):
if self.known_channels_normalised == [True] * self.channel_count:
return False
channel = self.next_channel_preload
@ -189,16 +218,22 @@ class FileManager:
filename = item_obj.filename
if not filename:
self.logger.log.exception("Somehow got empty filename when all channels are preloaded.")
self.logger.log.exception(
"Somehow got empty filename when all channels are preloaded."
)
continue # Try next song.
elif (not os.path.isfile(filename)):
self.logger.log.exception("Filename for normalisation does not exist. This is bad.")
elif not os.path.isfile(filename):
self.logger.log.exception(
"Filename for normalisation does not exist. This is bad."
)
continue
elif "normalised" in filename:
continue
# Sweet, we now need to try generating a normalised version.
try:
self.logger.log.info("Normalising on channel {}: {}".format(channel,filename))
self.logger.log.info(
"Normalising on channel {}: {}".format(channel, filename)
)
# This will return immediately if we already have a normalised file.
item_obj.filename = generate_normalised_file(filename)
# TODO Hacky
@ -216,8 +251,3 @@ class FileManager:
self.next_channel_preload = 0
return normalised_something

View file

@ -2,6 +2,7 @@ from typing import Any, Dict, List, Optional, Tuple
import sounddevice as sd
from helpers.os_environment import isLinux, isMacOS, isWindows
import glob
if isWindows():
from serial.tools.list_ports_windows import comports
@ -39,7 +40,9 @@ class DeviceManager:
else:
host_apis[host_api_id]["usable"] = True
host_api_devices = (device for device in devices if device["hostapi"] == host_api_id)
host_api_devices = (
device for device in devices if device["hostapi"] == host_api_id
)
outputs: List[Dict] = list(filter(cls._isOutput, host_api_devices))
outputs = sorted(outputs, key=lambda k: k["name"])

View file

@ -56,7 +56,8 @@ class MyRadioAPI:
async with func as response:
if response.status != status_code:
self._logException(
"Failed to get API request. Status code: " + str(response.status)
"Failed to get API request. Status code: "
+ str(response.status)
)
self._logException(str(await response.text()))
return await response.read()
@ -81,7 +82,9 @@ class MyRadioAPI:
self._logException(str(r.text))
return json.loads(r.text) if json_payload else r.text
async def async_api_call(self, url, api_version="v2", method="GET", data=None, timeout=10):
async def async_api_call(
self, url, api_version="v2", method="GET", data=None, timeout=10
):
if api_version == "v2":
url = "{}/v2{}".format(self.config.get()["myradio_api_url"], url)
elif api_version == "non":
@ -198,7 +201,6 @@ class MyRadioAPI:
self.logger.log.error("Show plan in unknown format.")
return None
# Audio Library
async def get_filename(self, item: PlanItem, did_download: bool = False):
@ -240,15 +242,18 @@ class MyRadioAPI:
# If something else (another channel, the preloader etc) is downloading the track, wait for it.
if os.path.isfile(filename + dl_suffix):
time_waiting_s = 0
self._log("Waiting for download to complete from another worker. " + filename, DEBUG)
self._log(
"Waiting for download to complete from another worker. " + filename,
DEBUG,
)
while time_waiting_s < 20:
# TODO: Make something better here.
# If the connectivity is super poor or we're loading reeaaaalllly long files, this may be annoying, but this is just in case somehow the other api download gives up.
if os.path.isfile(filename):
# Now the file is downloaded successfully
return (filename, False) if did_download else filename
time_waiting_s +=1
self._log("Still waiting",DEBUG)
time_waiting_s += 1
self._log("Still waiting", DEBUG)
time.sleep(1)
# File doesn't exist, download it.
@ -300,7 +305,7 @@ class MyRadioAPI:
async def get_playlist_aux_items(self, library_id: str):
# Sometimes they have "aux-<ID>", we only need the index.
if library_id.index("-") > -1:
library_id = library_id[library_id.index("-") + 1:]
library_id = library_id[library_id.index("-") + 1 :]
url = "/nipswebPlaylist/{}/items".format(library_id)
request = await self.async_api_call(url)
@ -351,12 +356,14 @@ class MyRadioAPI:
source: str = self.config.get()["myradio_api_tracklist_source"]
data = {
"trackid": item.trackid,
"sourceid": int(source) if source.isnumeric() else source
"sourceid": int(source) if source.isnumeric() else source,
}
# Starttime and timeslotid are default in the API to current time/show.
tracklist_id = None
try:
tracklist_id = self.api_call("/tracklistItem/", method="POST", data=data)["payload"]["audiologid"]
tracklist_id = self.api_call("/tracklistItem/", method="POST", data=data)[
"payload"
]["audiologid"]
except Exception as e:
self._logException("Failed to get tracklistid. {}".format(e))
@ -370,7 +377,9 @@ class MyRadioAPI:
self._log("Tracklistitemid is None, can't end tracklist.", WARNING)
return False
if not isinstance(tracklistitemid, int):
self._logException("Tracklistitemid '{}' is not an integer!".format(tracklistitemid))
self._logException(
"Tracklistitemid '{}' is not an integer!".format(tracklistitemid)
)
return False
self._log("Ending tracklistitemid {}".format(tracklistitemid))

View file

@ -1,49 +1,44 @@
import os
from helpers.os_environment import resolve_external_file_path
from pydub import AudioSegment, effects # Audio leveling!
# Stuff to help make BAPSicle play out leveled audio.
def match_target_amplitude(sound, target_dBFS):
change_in_dBFS = target_dBFS - sound.dBFS
return sound.apply_gain(change_in_dBFS)
# Takes filename in, normalialises it and returns a normalised file path.
def generate_normalised_file(filename: str):
if (not (isinstance(filename, str) and filename.endswith(".mp3"))):
if not (isinstance(filename, str) and filename.endswith(".mp3")):
raise ValueError("Invalid filename given.")
# Already normalised.
if filename.endswith("-normalised.mp3"):
return filename
normalised_filename = "{}-normalised.mp3".format(filename.rsplit(".",1)[0])
normalised_filename = "{}-normalised.mp3".format(filename.rsplit(".", 1)[0])
# The file already exists, short circuit.
if (os.path.exists(normalised_filename)):
if os.path.exists(normalised_filename):
return normalised_filename
sound = AudioSegment.from_file(filename, "mp3")
normalised_sound = effects.normalize(sound) #match_target_amplitude(sound, -10)
normalised_sound = effects.normalize(sound)
normalised_sound.export(normalised_filename, bitrate="320k", format="mp3")
return normalised_filename
# Returns either a normalised file path (based on filename), or the original if not available.
def get_normalised_filename_if_available(filename:str):
if (not (isinstance(filename, str) and filename.endswith(".mp3"))):
def get_normalised_filename_if_available(filename: str):
if not (isinstance(filename, str) and filename.endswith(".mp3")):
raise ValueError("Invalid filename given.")
# Already normalised.
if filename.endswith("-normalised.mp3"):
return filename
normalised_filename = "{}-normalised.mp3".format(filename.rstrip(".mp3"))
# normalised version exists
if (os.path.exists(normalised_filename)):
if os.path.exists(normalised_filename):
return normalised_filename
# Else we've not got a normalised verison, just take original.
return filename

View file

@ -1,6 +1,6 @@
import json
import os
from logging import CRITICAL, DEBUG, INFO
from logging import DEBUG, INFO
import time
from datetime import datetime
from copy import copy
@ -79,7 +79,7 @@ class StateManager:
# If there are any new config options in the default state, save them.
# Uses update() to save them to file too.
for key in default_state.keys():
if not key in file_state.keys():
if key not in file_state.keys():
self.update(key, default_state[key])
except Exception:
@ -114,7 +114,6 @@ class StateManager:
now = datetime.now()
current_time = now.strftime("%H:%M:%S")
state_to_json["last_updated"] = current_time
@ -154,20 +153,30 @@ class StateManager:
allow = False
# It's hard to compare lists, especially of complex objects like show plans, just write it.
if (isinstance(value, list)):
if isinstance(value, list):
allow = True
# If the two objects have dict representations, and they don't match, allow writing.
# TODO: This should be easier.
if (getattr(value, "__dict__", None) and getattr(state_to_update[key], "__dict__", None)):
if getattr(value, "__dict__", None) and getattr(
state_to_update[key], "__dict__", None
):
if value.__dict__ != state_to_update[key].__dict__:
allow = True
if not allow:
# Just some debug logging.
if update_file and (key not in ["playing", "loaded", "initialised", "remaining", "pos_true"]):
self._log("Not updating state for key '{}' with value '{}' of type '{}'.".format(key, value, type(value)), DEBUG)
if update_file and (
key
not in ["playing", "loaded", "initialised", "remaining", "pos_true"]
):
self._log(
"Not updating state for key '{}' with value '{}' of type '{}'.".format(
key, value, type(value)
),
DEBUG,
)
# We're trying to update the state with the same value.
# In this case, ignore the update
@ -176,11 +185,21 @@ class StateManager:
if index > -1 and key in state_to_update:
if not isinstance(state_to_update[key], list):
self._log("Not updating state for key '{}' with value '{}' of type '{}' since index is set and key is not a list.".format(key, value, type(value)), DEBUG)
self._log(
"Not updating state for key '{}' with value '{}' of type '{}' since index is set and key is not a list.".format(
key, value, type(value)
),
DEBUG,
)
return
list_items = state_to_update[key]
if index >= len(list_items):
self._log("Not updating state for key '{}' with value '{}' of type '{}' because index '{}' is too large..".format(key, value, type(value), index), DEBUG)
self._log(
"Not updating state for key '{}' with value '{}' of type '{}' because index '{}' is too large..".format(
key, value, type(value), index
),
DEBUG,
)
return
list_items[index] = value
state_to_update[key] = list_items
@ -190,7 +209,12 @@ class StateManager:
self.state = state_to_update
if update_file:
self._log("Writing change to key '{}' with value '{}' of type '{}' to disk.".format(key, value, type(value)), DEBUG)
self._log(
"Writing change to key '{}' with value '{}' of type '{}' to disk.".format(
key, value, type(value)
),
DEBUG,
)
# Either a routine write, or state has changed.
# Update the file
self.write_to_file(state_to_update)

View file

@ -70,7 +70,11 @@ if __name__ == "__main__":
if sys.argv[1] == "Presenter":
webbrowser.open("http://localhost:13500/presenter/")
except Exception as e:
print("ALERT:BAPSicle failed with exception of type {}:{}".format(type(e).__name__, e))
print(
"ALERT:BAPSicle failed with exception of type {}:{}".format(
type(e).__name__, e
)
)
sys.exit(1)
sys.exit(0)

View file

@ -15,9 +15,10 @@ with open(resolve_local_file_path("package.json")) as file:
build_beta = True
try:
import build
build_commit = build.BUILD
build_branch = build.BRANCH
build_beta = (build_branch != "release")
build_beta = build_branch != "release"
except (ModuleNotFoundError, AttributeError):
pass
BUILD: str = build_commit

242
player.py
View file

@ -21,6 +21,7 @@
# Stop the Pygame Hello message.
import os
os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "hide"
from queue import Empty
@ -148,7 +149,10 @@ class Player:
# Don't mess with playback, we only care about if it's supposed to be loaded.
if not self._isLoaded(short_test=True):
return False
return (self.state.get()["pos_true"] == self.state.get()["loaded_item"].cue and not self.isPlaying)
return (
self.state.get()["pos_true"] == self.state.get()["loaded_item"].cue
and not self.isPlaying
)
@property
def status(self):
@ -251,7 +255,9 @@ class Player:
return False
return True
else:
self.logger.log.debug("Not playing during seek, setting pos state for next play.")
self.logger.log.debug(
"Not playing during seek, setting pos state for next play."
)
self.stopped_manually = True # Don't trigger _ended() on seeking.
if pos > 0:
self.state.update("paused", True)
@ -298,7 +304,9 @@ class Player:
# Kinda a bodge for the moment, each "Ghost" (item which is not saved in the database showplan yet) needs to have a unique temporary item.
# To do this, we'll start with the channel number the item was originally added to (to stop items somehow simultaneously added to different channels from having the same id)
# And chuck in the unix epoch in ns for good measure.
item.timeslotitemid = "GHOST-{}-{}".format(self.state.get()["channel"], time.time_ns())
item.timeslotitemid = "GHOST-{}-{}".format(
self.state.get()["channel"], time.time_ns()
)
return item
# TODO Allow just moving an item inside the channel instead of removing and adding.
@ -315,7 +323,6 @@ class Player:
self._fix_and_update_weights(plan_copy)
loaded_item = self.state.get()["loaded_item"]
if loaded_item:
@ -346,13 +353,15 @@ class Player:
def remove_from_plan(self, weight: int) -> bool:
plan_copy: List[PlanItem] = copy.copy(self.state.get()["show_plan"])
found: Optional[PlanItem ] = None
found: Optional[PlanItem] = None
before = []
for item in plan_copy:
before += (item.weight, item.name)
self.logger.log.debug("Weights before removing weight {}:\n{}".format(weight, before))
self.logger.log.debug(
"Weights before removing weight {}:\n{}".format(weight, before)
)
for i in plan_copy:
if i.weight == weight:
@ -372,18 +381,16 @@ class Player:
# So we'll want to update the weight.
# We're removing the loaded item from the channel.
#if loaded_item.weight == weight:
# if loaded_item.weight == weight:
loaded_item.weight = -1
# If loaded_item wasn't the same instance, we'd want to do the below.
# We removed an item above it. Shift it up.
#elif loaded_item.weight > weight:
# elif loaded_item.weight > weight:
# loaded_item.weight -= 1
# Else, new weight stays the same.
#else:
# else:
# return True
self.state.update("loaded_item", loaded_item)
@ -399,7 +406,10 @@ class Player:
loaded_state = self.state.get()
self.unload()
self.logger.log.info("Resetting output (in case of sound output gone silent somehow) to " + str(loaded_state["output"]))
self.logger.log.info(
"Resetting output (in case of sound output gone silent somehow) to "
+ str(loaded_state["output"])
)
self.output(loaded_state["output"])
showplan = loaded_state["show_plan"]
@ -412,14 +422,12 @@ class Player:
break
if loaded_item is None:
self.logger.log.error(
"Failed to find weight: {}".format(weight))
self.logger.log.error("Failed to find weight: {}".format(weight))
return False
reload = False
if loaded_item.filename == "" or loaded_item.filename is None:
self.logger.log.info(
"Filename is not specified, loading from API.")
self.logger.log.info("Filename is not specified, loading from API.")
reload = True
elif not os.path.exists(loaded_item.filename):
self.logger.log.warn(
@ -434,7 +442,9 @@ class Player:
return False
# Swap with a normalised version if it's ready, else returns original.
loaded_item.filename = get_normalised_filename_if_available(loaded_item.filename)
loaded_item.filename = get_normalised_filename_if_available(
loaded_item.filename
)
self.state.update("loaded_item", loaded_item)
@ -452,8 +462,7 @@ class Player:
while load_attempt < 5:
load_attempt += 1
try:
self.logger.log.info("Loading file: " +
str(loaded_item.filename))
self.logger.log.info("Loading file: " + str(loaded_item.filename))
mixer.music.load(loaded_item.filename)
except Exception:
# We couldn't load that file.
@ -464,7 +473,9 @@ class Player:
continue # Try loading again.
if not self.isLoaded:
self.logger.log.error("Pygame loaded file without error, but never actually loaded.")
self.logger.log.error(
"Pygame loaded file without error, but never actually loaded."
)
time.sleep(1)
continue # Try loading again.
@ -475,12 +486,11 @@ class Player:
else:
# WARNING! Pygame / SDL can't seek .wav files :/
self.state.update(
"length", mixer.Sound(
loaded_item.filename).get_length() / 1000
"length",
mixer.Sound(loaded_item.filename).get_length() / 1000,
)
except Exception:
self.logger.log.exception(
"Failed to update the length of item.")
self.logger.log.exception("Failed to update the length of item.")
time.sleep(1)
continue # Try loading again.
@ -561,7 +571,11 @@ class Player:
try:
marker = Marker(marker_str)
except Exception as e:
self.logger.log.error("Failed to create Marker instance with {} {}: {}".format(timeslotitemid, marker_str, e))
self.logger.log.error(
"Failed to create Marker instance with {} {}: {}".format(
timeslotitemid, marker_str, e
)
)
return False
if timeslotitemid == "-1":
@ -569,10 +583,12 @@ class Player:
if not self.isLoaded:
return False
timeslotitemid = self.state.get()["loaded_item"].timeslotitemid
elif self.isLoaded and self.state.get()["loaded_item"].timeslotitemid == timeslotitemid:
elif (
self.isLoaded
and self.state.get()["loaded_item"].timeslotitemid == timeslotitemid
):
set_loaded = True
plan_copy: List[PlanItem] = copy.copy(self.state.get()["show_plan"])
for i in range(len(self.state.get()["show_plan"])):
@ -585,15 +601,23 @@ class Player:
except Exception as e:
self.logger.log.error(
"Failed to set marker on item {}: {} with marker \n{}".format(timeslotitemid, e, marker))
"Failed to set marker on item {}: {} with marker \n{}".format(
timeslotitemid, e, marker
)
)
success = False
if set_loaded:
try:
self.state.update("loaded_item", self.state.get()["loaded_item"].set_marker(marker))
self.state.update(
"loaded_item", self.state.get()["loaded_item"].set_marker(marker)
)
except Exception as e:
self.logger.log.error(
"Failed to set marker on loaded_item {}: {} with marker \n{}".format(timeslotitemid, e, marker))
"Failed to set marker on loaded_item {}: {} with marker \n{}".format(
timeslotitemid, e, marker
)
)
success = False
return success
@ -605,7 +629,9 @@ class Player:
item.play_count_increment() if played else item.play_count_reset()
self.state.update("show_plan", plan)
elif len(plan) > weight:
plan[weight].play_count_increment() if played else plan[weight].play_count_reset()
plan[weight].play_count_increment() if played else plan[
weight
].play_count_reset()
self.state.update("show_plan", plan[weight], weight)
else:
return False
@ -617,11 +643,10 @@ class Player:
self.state.update("live", live)
# If we're going to live (potentially from not live/PFL), potentially tracklist if it's playing.
if (live):
if live:
self._potentially_tracklist()
return True
# Helper functions
# This essentially allows the tracklist end API call to happen in a separate thread, to avoid hanging playout/loading.
@ -629,18 +654,24 @@ class Player:
mode = self.state.get()["tracklist_mode"]
time: int = -1
if mode in ["on","fader-live"]:
if mode in ["on", "fader-live"]:
time = 1 # Let's do it pretty quickly.
elif mode == "delayed":
# Let's do it in a bit, once we're sure it's been playing. (Useful if we've got no idea if it's live or cueing.)
time = TRACKLISTING_DELAYED_S
if time >= 0 and not self.tracklist_start_timer:
self.logger.log.info("Setting timer for tracklisting in {} secs due to Mode: {}".format(time, mode))
self.logger.log.info(
"Setting timer for tracklisting in {} secs due to Mode: {}".format(
time, mode
)
)
self.tracklist_start_timer = Timer(time, self._tracklist_start)
self.tracklist_start_timer.start()
elif self.tracklist_start_timer:
self.logger.log.error("Failed to potentially tracklist, timer already busy.")
self.logger.log.error(
"Failed to potentially tracklist, timer already busy."
)
# This essentially allows the tracklist end API call to happen in a separate thread, to avoid hanging playout/loading.
def _potentially_end_tracklist(self):
@ -663,24 +694,34 @@ class Player:
self.logger.log.info("No tracklist to end.")
return
self.logger.log.info("Setting timer for ending tracklist_id '{}'".format(tracklist_id))
self.logger.log.info(
"Setting timer for ending tracklist_id '{}'".format(tracklist_id)
)
if tracklist_id:
self.logger.log.info("Attempting to end tracklist_id '{}'".format(tracklist_id))
self.logger.log.info(
"Attempting to end tracklist_id '{}'".format(tracklist_id)
)
if self.tracklist_end_timer:
self.logger.log.error("Failed to potentially end tracklist, timer already busy.")
self.logger.log.error(
"Failed to potentially end tracklist, timer already busy."
)
return
self.state.update("tracklist_id", None)
# This threads it, so it won't hang track loading if it fails.
self.tracklist_end_timer = Timer(1, self._tracklist_end, [tracklist_id])
self.tracklist_end_timer.start()
else:
self.logger.log.warning("Failed to potentially end tracklist, no tracklist started.")
self.logger.log.warning(
"Failed to potentially end tracklist, no tracklist started."
)
def _tracklist_start(self):
state = self.state.get()
loaded_item = state["loaded_item"]
if not loaded_item:
self.logger.log.error("Tried to call _tracklist_start() with no loaded item!")
self.logger.log.error(
"Tried to call _tracklist_start() with no loaded item!"
)
elif not self.isPlaying:
self.logger.log.info("Not tracklisting since not playing.")
@ -688,20 +729,27 @@ class Player:
else:
tracklist_id = state["tracklist_id"]
if (not tracklist_id):
if (state["tracklist_mode"] == "fader-live" and not state["live"]):
if not tracklist_id:
if state["tracklist_mode"] == "fader-live" and not state["live"]:
self.logger.log.info("Not tracklisting since fader is not live.")
else:
self.logger.log.info("Tracklisting item: '{}'".format(loaded_item.name))
self.logger.log.info(
"Tracklisting item: '{}'".format(loaded_item.name)
)
tracklist_id = self.api.post_tracklist_start(loaded_item)
if not tracklist_id:
self.logger.log.warning("Failed to tracklist '{}'".format(loaded_item.name))
self.logger.log.warning(
"Failed to tracklist '{}'".format(loaded_item.name)
)
else:
self.logger.log.info("Tracklist id: '{}'".format(tracklist_id))
self.state.update("tracklist_id", tracklist_id)
else:
self.logger.log.info("Not tracklisting item '{}', already got tracklistid: '{}'".format(
loaded_item.name, tracklist_id))
self.logger.log.info(
"Not tracklisting item '{}', already got tracklistid: '{}'".format(
loaded_item.name, tracklist_id
)
)
# No matter what we end up doing, we need to kill this timer so future ones can run.
self.tracklist_start_timer = None
@ -709,10 +757,14 @@ class Player:
def _tracklist_end(self, tracklist_id):
if tracklist_id:
self.logger.log.info("Attempting to end tracklist_id '{}'".format(tracklist_id))
self.logger.log.info(
"Attempting to end tracklist_id '{}'".format(tracklist_id)
)
self.api.post_tracklist_end(tracklist_id)
else:
self.logger.log.error("Tracklist_id to _tracklist_end() missing. Failed to end tracklist.")
self.logger.log.error(
"Tracklist_id to _tracklist_end() missing. Failed to end tracklist."
)
self.tracklist_end_timer = None
@ -727,7 +779,11 @@ class Player:
return
# Track has ended
self.logger.log.info("Playback ended of {}, weight {}:".format(loaded_item.name, loaded_item.weight))
self.logger.log.info(
"Playback ended of {}, weight {}:".format(
loaded_item.name, loaded_item.weight
)
)
# Repeat 1
# TODO ENUM
@ -742,13 +798,19 @@ class Player:
# If it's been removed, weight will be -1.
# Just stop in this case.
if loaded_item.weight < 0:
self.logger.log.debug("Loaded item is no longer in channel (weight {}), not auto advancing.".format(loaded_item.weight))
self.logger.log.debug(
"Loaded item is no longer in channel (weight {}), not auto advancing.".format(
loaded_item.weight
)
)
else:
self.logger.log.debug("Found current loaded item in this channel show plan. Auto Advancing.")
self.logger.log.debug(
"Found current loaded item in this channel show plan. Auto Advancing."
)
# If there's another item after this one, load that.
if len(state["show_plan"]) > loaded_item.weight+1:
self.load(loaded_item.weight+1)
if len(state["show_plan"]) > loaded_item.weight + 1:
self.load(loaded_item.weight + 1)
return
# Repeat All (Jump to top again)
@ -795,8 +857,7 @@ class Player:
self.state.update(
"remaining",
max(0, (self.state.get()["length"] -
self.state.get()["pos_true"])),
max(0, (self.state.get()["length"] - self.state.get()["pos_true"])),
)
def _ping_times(self):
@ -832,17 +893,18 @@ class Player:
response += "FAIL"
if self.out_q:
if ("STATUS:" not in response):
if "STATUS:" not in response:
# Don't fill logs with status pushes, it's a mess.
self.logger.log.debug(("Sending: {}".format(response)))
self.out_q.put(response)
else:
self.logger.log.exception("Message return Queue is missing!!!! Can't send message.")
self.logger.log.exception(
"Message return Queue is missing!!!! Can't send message."
)
def _send_status(self):
# TODO This is hacky
self._retMsg(str(self.status), okay_str=True,
custom_prefix="ALL:STATUS:")
self._retMsg(str(self.status), okay_str=True, custom_prefix="ALL:STATUS:")
def _fix_and_update_weights(self, plan):
def _sort_weight(e: PlanItem):
@ -854,7 +916,6 @@ class Player:
self.logger.log.debug("Weights before fixing:\n{}".format(before))
plan.sort(key=_sort_weight) # Sort into weighted order.
sorted = []
@ -874,7 +935,11 @@ class Player:
self.state.update("show_plan", plan)
def __init__(
self, channel: int, in_q: multiprocessing.Queue, out_q: multiprocessing.Queue, server_state: StateManager
self,
channel: int,
in_q: multiprocessing.Queue,
out_q: multiprocessing.Queue,
server_state: StateManager,
):
process_title = "Player: Channel " + str(channel)
@ -899,7 +964,9 @@ class Player:
self.state.update("channel", channel)
self.state.update("tracklist_mode", server_state.get()["tracklist_mode"])
self.state.update("live", True) # Channel is live until controller says it isn't.
self.state.update(
"live", True
) # Channel is live until controller says it isn't.
# Just in case there's any weights somehow messed up, let's fix them.
plan_copy: List[PlanItem] = copy.copy(self.state.get()["show_plan"])
@ -908,8 +975,7 @@ class Player:
loaded_state = copy.copy(self.state.state)
if loaded_state["output"]:
self.logger.log.info("Setting output to: " +
str(loaded_state["output"]))
self.logger.log.info("Setting output to: " + str(loaded_state["output"]))
self.output(loaded_state["output"])
else:
self.logger.log.info("Using default output device.")
@ -918,7 +984,7 @@ class Player:
loaded_item = loaded_state["loaded_item"]
if loaded_item:
# No need to load on init, the output switch does this, as it would for regular output switching.
#self.load(loaded_item.weight)
# self.load(loaded_item.weight)
# Load may jump to the cue point, as it would do on a regular load.
# If we were at a different state before, we have to override it now.
@ -946,8 +1012,7 @@ class Player:
self.last_msg_source = ""
self.last_msg = ""
self.logger.log.warn(
"Message from unknown sender source: {}".format(
source)
"Message from unknown sender source: {}".format(source)
)
continue
@ -981,9 +1046,13 @@ class Player:
# Unpause, so we don't jump to 0, we play from the current pos.
"PLAY": lambda: self._retMsg(self.unpause()),
"PAUSE": lambda: self._retMsg(self.pause()),
"PLAYPAUSE": lambda: self._retMsg(self.unpause() if not self.isPlaying else self.pause()), # For the hardware controller.
"PLAYPAUSE": lambda: self._retMsg(
self.unpause() if not self.isPlaying else self.pause()
), # For the hardware controller.
"UNPAUSE": lambda: self._retMsg(self.unpause()),
"STOP": lambda: self._retMsg(self.stop(user_initiated=True)),
"STOP": lambda: self._retMsg(
self.stop(user_initiated=True)
),
"SEEK": lambda: self._retMsg(
self.seek(float(self.last_msg.split(":")[1]))
),
@ -1011,19 +1080,33 @@ class Player:
"UNLOAD": lambda: self._retMsg(self.unload()),
"ADD": lambda: self._retMsg(
self.add_to_plan(
json.loads(
":".join(self.last_msg.split(":")[1:]))
json.loads(":".join(self.last_msg.split(":")[1:]))
)
),
"REMOVE": lambda: self._retMsg(
self.remove_from_plan(
int(self.last_msg.split(":")[1]))
self.remove_from_plan(int(self.last_msg.split(":")[1]))
),
"CLEAR": lambda: self._retMsg(self.clear_channel_plan()),
"SETMARKER": lambda: self._retMsg(self.set_marker(self.last_msg.split(":")[1], self.last_msg.split(":", 2)[2])),
"RESETPLAYED": lambda: self._retMsg(self.set_played(weight=int(self.last_msg.split(":")[1]), played = False)),
"SETPLAYED": lambda: self._retMsg(self.set_played(weight=int(self.last_msg.split(":")[1]), played = True)),
"SETLIVE": lambda: self._retMsg(self.set_live(self.last_msg.split(":")[1] == "True")),
"SETMARKER": lambda: self._retMsg(
self.set_marker(
self.last_msg.split(":")[1],
self.last_msg.split(":", 2)[2],
)
),
"RESETPLAYED": lambda: self._retMsg(
self.set_played(
weight=int(self.last_msg.split(":")[1]),
played=False,
)
),
"SETPLAYED": lambda: self._retMsg(
self.set_played(
weight=int(self.last_msg.split(":")[1]), played=True
)
),
"SETLIVE": lambda: self._retMsg(
self.set_live(self.last_msg.split(":")[1] == "True")
),
}
message_type: str = self.last_msg.split(":")[0]
@ -1051,8 +1134,7 @@ class Player:
except SystemExit:
self.logger.log.info("Received SystemExit")
except Exception as e:
self.logger.log.exception(
"Received unexpected Exception: {}".format(e))
self.logger.log.exception("Received unexpected Exception: {}".format(e))
self.logger.log.info("Quiting player " + str(channel))
self.quit()

View file

@ -10,7 +10,9 @@ from helpers.the_terminator import Terminator
class PlayerHandler:
logger: LoggingManager
def __init__(self, channel_from_q, websocket_to_q, ui_to_q, controller_to_q, file_to_q):
def __init__(
self, channel_from_q, websocket_to_q, ui_to_q, controller_to_q, file_to_q
):
self.logger = LoggingManager("PlayerHandler")
process_title = "Player Handler"
@ -31,7 +33,6 @@ class PlayerHandler:
if command == "GET_PLAN" or command == "STATUS":
file_to_q[channel].put(message)
# TODO ENUM
if source in ["ALL", "WEBSOCKET"]:
websocket_to_q[channel].put(message)
@ -46,7 +47,6 @@ class PlayerHandler:
sleep(0.02)
except Exception as e:
self.logger.log.exception(
"Received unexpected exception: {}".format(e))
self.logger.log.exception("Received unexpected exception: {}".format(e))
del self.logger
_exit(0)

View file

@ -110,28 +110,53 @@ class BAPSicleServer:
terminator = Terminator()
log_function = self.logger.log.info
while not terminator.terminate and self.state.get()["running_state"] == "running":
while (
not terminator.terminate and self.state.get()["running_state"] == "running"
):
for channel in range(self.state.get()["num_channels"]):
# Use pid_exists to confirm process is actually still running. Python may not report is_alive() correctly (especially over system sleeps etc.)
# https://medium.com/pipedrive-engineering/encountering-some-python-trickery-683bd5f66750
if not self.player[channel] or not self.player[channel].is_alive() or not psutil.pid_exists(self.player[channel].pid):
if (
not self.player[channel]
or not self.player[channel].is_alive()
or not psutil.pid_exists(self.player[channel].pid)
):
log_function("Player {} not running, (re)starting.".format(channel))
self.player[channel] = multiprocessing.Process(
target=player.Player,
args=(channel, self.player_to_q[channel], self.player_from_q[channel], self.state)
args=(
channel,
self.player_to_q[channel],
self.player_from_q[channel],
self.state,
),
)
self.player[channel].start()
if not self.player_handler or not self.player_handler.is_alive() or not psutil.pid_exists(self.player_handler.pid):
if (
not self.player_handler
or not self.player_handler.is_alive()
or not psutil.pid_exists(self.player_handler.pid)
):
log_function("Player Handler not running, (re)starting.")
self.player_handler = multiprocessing.Process(
target=PlayerHandler,
args=(self.player_from_q, self.websocket_to_q, self.ui_to_q, self.controller_to_q, self.file_to_q),
args=(
self.player_from_q,
self.websocket_to_q,
self.ui_to_q,
self.controller_to_q,
self.file_to_q,
),
)
self.player_handler.start()
if not self.file_manager or not self.file_manager.is_alive() or not psutil.pid_exists(self.file_manager.pid):
if (
not self.file_manager
or not self.file_manager.is_alive()
or not psutil.pid_exists(self.file_manager.pid)
):
log_function("File Manager not running, (re)starting.")
self.file_manager = multiprocessing.Process(
target=FileManager,
@ -139,24 +164,38 @@ class BAPSicleServer:
)
self.file_manager.start()
if not self.websockets_server or not self.websockets_server.is_alive() or not psutil.pid_exists(self.websockets_server.pid):
if (
not self.websockets_server
or not self.websockets_server.is_alive()
or not psutil.pid_exists(self.websockets_server.pid)
):
log_function("Websocket Server not running, (re)starting.")
self.websockets_server = multiprocessing.Process(
target=WebsocketServer, args=(self.player_to_q, self.websocket_to_q, self.state)
target=WebsocketServer,
args=(self.player_to_q, self.websocket_to_q, self.state),
)
self.websockets_server.start()
if not self.webserver or not self.webserver.is_alive() or not psutil.pid_exists(self.webserver.pid):
if (
not self.webserver
or not self.webserver.is_alive()
or not psutil.pid_exists(self.webserver.pid)
):
log_function("Webserver not running, (re)starting.")
self.webserver = multiprocessing.Process(
target=WebServer, args=(self.player_to_q, self.ui_to_q, self.state)
)
self.webserver.start()
if not self.controller_handler or not self.controller_handler.is_alive() or not psutil.pid_exists(self.controller_handler.pid):
if (
not self.controller_handler
or not self.controller_handler.is_alive()
or not psutil.pid_exists(self.controller_handler.pid)
):
log_function("Controller Handler not running, (re)starting.")
self.controller_handler = multiprocessing.Process(
target=MattchBox, args=(self.player_to_q, self.controller_to_q, self.state)
target=MattchBox,
args=(self.player_to_q, self.controller_to_q, self.state),
)
self.controller_handler.start()
@ -179,7 +218,9 @@ class BAPSicleServer:
ProxyManager.register("StateManager", StateManager)
manager = ProxyManager()
manager.start()
self.state: StateManager = manager.StateManager("BAPSicleServer", self.logger, self.default_state)
self.state: StateManager = manager.StateManager(
"BAPSicleServer", self.logger, self.default_state
)
self.state.update("running_state", "running")
@ -203,8 +244,16 @@ class BAPSicleServer:
self.controller_to_q.append(multiprocessing.Queue())
self.file_to_q.append(multiprocessing.Queue())
print("Welcome to BAPSicle Server version: {}, build: {}.".format(package.VERSION, package.BUILD))
print("The Server UI is available at http://{}:{}".format(self.state.get()["host"], self.state.get()["port"]))
print(
"Welcome to BAPSicle Server version: {}, build: {}.".format(
package.VERSION, package.BUILD
)
)
print(
"The Server UI is available at http://{}:{}".format(
self.state.get()["host"], self.state.get()["port"]
)
)
# TODO Move this to player or installer.
if False:

View file

@ -7,4 +7,5 @@ setup(
description=package.DESCRIPTION,
author=package.AUTHOR,
license=package.LICENSE,
packages=find_packages())
packages=find_packages(),
)

View file

@ -68,7 +68,9 @@ class TestPlayer(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.logger = LoggingManager("Test_Player")
cls.server_state = StateManager("BAPSicleServer", cls.logger, default_state={"tracklist_mode": "off"}) # Mostly dummy here.
cls.server_state = StateManager(
"BAPSicleServer", cls.logger, default_state={"tracklist_mode": "off"}
) # Mostly dummy here.
# clean up logic for the test suite declared in the test module
# code that is executed after all tests in one test run
@ -82,7 +84,8 @@ class TestPlayer(unittest.TestCase):
self.player_from_q = multiprocessing.Queue()
self.player_to_q = multiprocessing.Queue()
self.player = multiprocessing.Process(
target=Player, args=(-1, self.player_to_q, self.player_from_q, self.server_state)
target=Player,
args=(-1, self.player_to_q, self.player_from_q, self.server_state),
)
self.player.start()
self._send_msg_wait_OKAY("CLEAR") # Empty any previous track items.
@ -125,7 +128,7 @@ class TestPlayer(unittest.TestCase):
source = response[: response.index(":")]
if source in sources_filter:
return response[
len(source + ":" + msg) + 1:
len(source + ":" + msg) + 1 :
] # +1 to remove trailing : on source.
except Empty:
pass
@ -339,9 +342,13 @@ class TestPlayer(unittest.TestCase):
# Now test that all the markers we setup are present.
item = json_obj["show_plan"][0]
self.assertEqual(item["weight"], 0)
self.assertEqual(item["intro"], 2.0) # Backwards compat with basic Webstudio intro/cue/outro
self.assertEqual(
item["intro"], 2.0
) # Backwards compat with basic Webstudio intro/cue/outro
self.assertEqual(item["cue"], 3.14)
self.assertEqual([json.dumps(item) for item in item["markers"]], markers[0:2]) # Check the full marker configs match
self.assertEqual(
[json.dumps(item) for item in item["markers"]], markers[0:2]
) # Check the full marker configs match
item = json_obj["show_plan"][1]
self.assertEqual(item["weight"], 1)
@ -355,7 +362,9 @@ class TestPlayer(unittest.TestCase):
self.assertEqual(item["intro"], 0.0)
self.assertEqual(item["outro"], 0.0)
self.assertEqual(item["cue"], 0.0)
self.assertEqual([json.dumps(item) for item in item["markers"]], markers[3:])
self.assertEqual(
[json.dumps(item) for item in item["markers"]], markers[3:]
)
# TODO: Now test editing/deleting them

View file

@ -16,7 +16,11 @@ from time import sleep
import json
import os
from helpers.os_environment import isBundelled, resolve_external_file_path, resolve_local_file_path
from helpers.os_environment import (
isBundelled,
resolve_external_file_path,
resolve_local_file_path,
)
from helpers.logging_manager import LoggingManager
from helpers.device_manager import DeviceManager
from helpers.state_manager import StateManager
@ -24,7 +28,10 @@ from helpers.the_terminator import Terminator
from helpers.normalisation import get_normalised_filename_if_available
from helpers.myradio_api import MyRadioAPI
env = Environment(loader=FileSystemLoader('%s/ui-templates/' % os.path.dirname(__file__)), autoescape=select_autoescape())
env = Environment(
loader=FileSystemLoader("%s/ui-templates/" % os.path.dirname(__file__)),
autoescape=select_autoescape(),
)
LOG_FILEPATH = resolve_external_file_path("logs")
LOG_FILENAME = LOG_FILEPATH + "/WebServer.log"
@ -52,17 +59,17 @@ LOGGING_CONFIG = dict(
"file": {
"class": "logging.FileHandler",
"formatter": "generic",
"filename": LOG_FILENAME
"filename": LOG_FILENAME,
},
"error_file": {
"class": "logging.FileHandler",
"formatter": "generic",
"filename": LOG_FILENAME
"filename": LOG_FILENAME,
},
"access_file": {
"class": "logging.FileHandler",
"formatter": "access",
"filename": LOG_FILENAME
"filename": LOG_FILENAME,
},
},
formatters={
@ -113,7 +120,7 @@ def ui_index(request):
"server_build": config["server_build"],
"server_name": config["server_name"],
"server_beta": config["server_beta"],
"server_branch": config["server_branch"]
"server_branch": config["server_branch"],
}
return render_template("index.html", data=data)
@ -124,8 +131,7 @@ def ui_status(request):
for i in range(server_state.get()["num_channels"]):
channel_states.append(status(i))
data = {"channels": channel_states,
"ui_page": "status", "ui_title": "Status"}
data = {"channels": channel_states, "ui_page": "status", "ui_title": "Status"}
return render_template("status.html", data=data)
@ -153,7 +159,7 @@ def ui_config_server(request):
"ui_title": "Server Config",
"state": server_state.get(),
"ser_ports": DeviceManager.getSerialPorts(),
"tracklist_modes": ["off", "on", "delayed", "fader-live"]
"tracklist_modes": ["off", "on", "delayed", "fader-live"],
}
return render_template("config_server.html", data=data)
@ -177,7 +183,9 @@ def ui_config_server_update(request):
server_state.update("myradio_base_url", request.form.get("myradio_base_url"))
server_state.update("myradio_api_url", request.form.get("myradio_api_url"))
server_state.update("myradio_api_tracklist_source", request.form.get("myradio_api_tracklist_source"))
server_state.update(
"myradio_api_tracklist_source", request.form.get("myradio_api_tracklist_source")
)
server_state.update("tracklist_mode", request.form.get("tracklist_mode"))
return redirect("/restart")
@ -192,11 +200,7 @@ def ui_logs_list(request):
log_files.append(file.rstrip(".log"))
log_files.sort()
data = {
"ui_page": "logs",
"ui_title": "Logs",
"logs": log_files
}
data = {"ui_page": "logs", "ui_title": "Logs", "logs": log_files}
return render_template("loglist.html", data=data)
@ -210,10 +214,12 @@ def ui_logs_render(request, path):
log_file = open(resolve_external_file_path("/logs/{}.log").format(path))
data = {
"logs": log_file.read().splitlines()[-300*page:(-300*(page-1) if page > 1 else None)][::-1],
"logs": log_file.read().splitlines()[
-300 * page : (-300 * (page - 1) if page > 1 else None)
][::-1],
"ui_page": "logs",
"ui_title": "Logs - {}".format(path),
"page": page
"page": page,
}
log_file.close()
return render_template("log.html", data=data)
@ -296,6 +302,7 @@ def player_all_stop(request):
# Show Plan Functions
@app.route("/plan/load/<timeslotid:int>")
def plan_load(request, timeslotid: int):
@ -314,6 +321,7 @@ def plan_clear(request):
# API Proxy Endpoints
@app.route("/plan/list")
async def api_list_showplans(request):
@ -323,7 +331,11 @@ async def api_list_showplans(request):
@app.route("/library/search/track")
async def api_search_library(request):
return resp_json(await api.get_track_search(request.args.get("title"), request.args.get("artist")))
return resp_json(
await api.get_track_search(
request.args.get("title"), request.args.get("artist")
)
)
@app.route("/library/playlists/<type:string>")
@ -368,7 +380,7 @@ def json_status(request):
async def audio_file(request, type: str, id: int):
if type not in ["managed", "track"]:
abort(404)
filename = resolve_external_file_path("music-tmp/{}-{}.mp3".format(type,id))
filename = resolve_external_file_path("music-tmp/{}-{}.mp3".format(type, id))
# Swap with a normalised version if it's ready, else returns original.
filename = get_normalised_filename_if_available(filename)
@ -378,18 +390,25 @@ async def audio_file(request, type: str, id: int):
# Static Files
app.static("/favicon.ico", resolve_local_file_path("ui-static/favicon.ico"), name="ui-favicon")
app.static(
"/favicon.ico", resolve_local_file_path("ui-static/favicon.ico"), name="ui-favicon"
)
app.static("/static", resolve_local_file_path("ui-static"), name="ui-static")
dist_directory = resolve_local_file_path("presenter-build")
app.static('/presenter', dist_directory)
app.static("/presenter/", resolve_local_file_path("presenter-build/index.html"),
strict_slashes=True, name="presenter-index")
app.static("/presenter", dist_directory)
app.static(
"/presenter/",
resolve_local_file_path("presenter-build/index.html"),
strict_slashes=True,
name="presenter-index",
)
# Helper Functions
def status(channel: int):
while not player_from_q[channel].empty():
player_from_q[channel].get() # Just waste any previous status responses.
@ -402,7 +421,7 @@ def status(channel: int):
if response.startswith("UI:STATUS:"):
response = response.split(":", 2)[2]
# TODO: Handle OKAY / FAIL
response = response[response.index(":") + 1:]
response = response[response.index(":") + 1 :]
try:
response = json.loads(response)
except Exception as e:
@ -416,6 +435,7 @@ def status(channel: int):
sleep(0.02)
# WebServer Start / Stop Functions
@ -428,7 +448,7 @@ def quit(request):
"ui_title": "Quitting BAPSicle",
"title": "See you later!",
"ui_menu": False,
"message": "BAPSicle is going back into winter hibernation, see you again soon!"
"message": "BAPSicle is going back into winter hibernation, see you again soon!",
}
return render_template("message.html", data)
@ -444,7 +464,7 @@ def restart(request):
"ui_menu": False,
"message": "Just putting BAPSicle back in the freezer for a moment!",
"redirect_to": "/",
"redirect_wait_ms": 10000
"redirect_wait_ms": 10000,
}
return render_template("message.html", data)
@ -467,13 +487,15 @@ def WebServer(player_to: List[Queue], player_from: List[Queue], state: StateMana
terminate = Terminator()
while not terminate.terminate:
try:
sync(app.run(
sync(
app.run(
host=server_state.get()["host"],
port=server_state.get()["port"],
debug=(not isBundelled()),
auto_reload=False,
access_log=(not isBundelled())
))
access_log=(not isBundelled()),
)
)
except Exception:
break
loop = asyncio.get_event_loop()

View file

@ -74,9 +74,7 @@ class WebsocketServer:
self.from_webstudio = asyncio.create_task(self.handle_from_webstudio(websocket))
try:
self.threads = await shield(
asyncio.gather(self.from_webstudio)
)
self.threads = await shield(asyncio.gather(self.from_webstudio))
finally:
self.from_webstudio.cancel()
@ -92,13 +90,10 @@ class WebsocketServer:
channel = int(data["channel"])
self.sendCommand(channel, data)
await asyncio.wait(
[conn.send(message) for conn in self.baps_clients]
)
await asyncio.wait([conn.send(message) for conn in self.baps_clients])
except websockets.exceptions.ConnectionClosedError as e:
self.logger.log.error(
"Client Disconncted {}, {}".format(websocket, e))
self.logger.log.error("Client Disconncted {}, {}".format(websocket, e))
except Exception as e:
self.logger.log.exception(
@ -152,8 +147,7 @@ class WebsocketServer:
extra += str(data["timeslotId"])
elif command == "SETMARKER":
extra += "{}:{}".format(
data["timeslotitemid"],
json.dumps(data["marker"])
data["timeslotitemid"], json.dumps(data["marker"])
)
# TODO: Move this to player handler.
@ -174,21 +168,22 @@ class WebsocketServer:
# Now send the special case.
self.channel_to_q[new_channel].put(
"WEBSOCKET:ADD:" + json.dumps(item))
"WEBSOCKET:ADD:" + json.dumps(item)
)
# Don't bother, we should be done.
return
except ValueError as e:
self.logger.log.exception(
"Error decoding extra data {} for command {} ".format(
e, command
)
"Error decoding extra data {} for command {} ".format(e, command)
)
pass
# Stick the message together and send!
message += command # Put the command in at the end, in case MOVE etc changed it.
message += (
command # Put the command in at the end, in case MOVE etc changed it.
)
if extra != "":
message += ":" + extra
@ -202,9 +197,7 @@ class WebsocketServer:
)
else:
self.logger.log.error(
"Command missing from message. Data: {}".format(data)
)
self.logger.log.error("Command missing from message. Data: {}".format(data))
async def handle_to_webstudio(self):
@ -244,9 +237,7 @@ class WebsocketServer:
data = json.dumps(
{"command": command, "data": message, "channel": channel}
)
await asyncio.wait(
[conn.send(data) for conn in self.baps_clients]
)
await asyncio.wait([conn.send(data) for conn in self.baps_clients])
except queue.Empty:
continue
except ValueError: