Lint with black for formatting.
This commit is contained in:
parent
398c0ac0ce
commit
2941d90f60
16 changed files with 623 additions and 374 deletions
|
@ -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 == "/":
|
||||
|
|
|
@ -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)
|
||||
|
|
296
file_manager.py
296
file_manager.py
|
@ -31,193 +31,223 @@ 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:
|
||||
message = channel_from_q[channel].get_nowait()
|
||||
except Exception:
|
||||
continue
|
||||
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:
|
||||
# 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.
|
||||
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.
|
||||
|
||||
self.channel_received[channel] = True
|
||||
continue
|
||||
|
||||
# Delete the previous show files!
|
||||
# Note: The players load into RAM. If something is playing over the load, the source file can still be deleted.
|
||||
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."
|
||||
)
|
||||
continue
|
||||
|
||||
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
|
||||
else:
|
||||
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."
|
||||
)
|
||||
continue
|
||||
self.channel_received[channel] = True
|
||||
continue
|
||||
|
||||
# Delete the previous show files!
|
||||
# Note: The players load into RAM. If something is playing over the load, the source file can still be deleted.
|
||||
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.")
|
||||
continue
|
||||
|
||||
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
|
||||
else:
|
||||
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.")
|
||||
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)
|
||||
if extra[2] != "OKAY":
|
||||
continue
|
||||
extra = message.split(":", 3)
|
||||
if extra[2] != "OKAY":
|
||||
continue
|
||||
|
||||
status = json.loads(extra[3])
|
||||
show_plan = status["show_plan"]
|
||||
item_ids = []
|
||||
for item in show_plan:
|
||||
item_ids += item["timeslotitemid"]
|
||||
status = json.loads(extra[3])
|
||||
show_plan = status["show_plan"]
|
||||
item_ids = []
|
||||
for item in show_plan:
|
||||
item_ids += item["timeslotitemid"]
|
||||
|
||||
# If the new status update has a different order / list of items, let's update the show plan we know about
|
||||
# This will trigger the chunk below to do the rounds again and preload any new files.
|
||||
if item_ids != self.last_known_item_ids[channel]:
|
||||
self.last_known_item_ids[channel] = item_ids
|
||||
self.last_known_show_plan[channel] = show_plan
|
||||
self.known_channels_preloaded[channel] = False
|
||||
# If the new status update has a different order / list of items, let's update the show plan we know about
|
||||
# This will trigger the chunk below to do the rounds again and preload any new files.
|
||||
if item_ids != self.last_known_item_ids[channel]:
|
||||
self.last_known_item_ids[channel] = item_ids
|
||||
self.last_known_show_plan[channel] = show_plan
|
||||
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):
|
||||
# We didn't do any hard work, let's sleep.
|
||||
sleep(0.2)
|
||||
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
|
||||
channel = self.next_channel_preload
|
||||
|
||||
# All channels have preloaded all files, do nothing.
|
||||
if (self.known_channels_preloaded == [True]*self.channel_count):
|
||||
return False # Didn't preload anything
|
||||
# All channels have preloaded all files, do nothing.
|
||||
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.
|
||||
# Keep an eye on if we downloaded anything.
|
||||
# If we didn't, we know that all items in this channel have been downloaded.
|
||||
downloaded_something = False
|
||||
for i in range(len(self.last_known_show_plan[channel])):
|
||||
# Right, let's have a quick check in the status for shows without filenames, to preload them.
|
||||
# Keep an eye on if we downloaded anything.
|
||||
# If we didn't, we know that all items in this channel have been downloaded.
|
||||
downloaded_something = False
|
||||
for i in range(len(self.last_known_show_plan[channel])):
|
||||
|
||||
item_obj = PlanItem(self.last_known_show_plan[channel][i])
|
||||
item_obj = PlanItem(self.last_known_show_plan[channel][i])
|
||||
|
||||
# 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))
|
||||
# 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
|
||||
)
|
||||
)
|
||||
|
||||
# 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))
|
||||
# Alright, we've done one, now let's give back control to process new statuses etc.
|
||||
# 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)
|
||||
)
|
||||
# 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
|
||||
self.last_known_show_plan[channel][i] = item_obj.__dict__
|
||||
# Save back the resulting item back in regular dict form
|
||||
self.last_known_show_plan[channel][i] = item_obj.__dict__
|
||||
|
||||
if did_download:
|
||||
downloaded_something = True
|
||||
self.logger.log.info("File successfully preloaded: {}".format(item_obj.filename))
|
||||
break
|
||||
else:
|
||||
# We didn't download anything this time, file was already loaded.
|
||||
# Let's try the next one.
|
||||
continue
|
||||
if did_download:
|
||||
downloaded_something = True
|
||||
self.logger.log.info(
|
||||
"File successfully preloaded: {}".format(item_obj.filename)
|
||||
)
|
||||
break
|
||||
else:
|
||||
# We didn't download anything this time, file was already loaded.
|
||||
# Let's try the next one.
|
||||
continue
|
||||
|
||||
# Tell the file manager that this channel is fully downloaded, this is so it can consider normalising once all channels have files.
|
||||
self.known_channels_preloaded[channel] = not downloaded_something
|
||||
# Tell the file manager that this channel is fully downloaded, this is so it can consider normalising once all channels have files.
|
||||
self.known_channels_preloaded[channel] = not downloaded_something
|
||||
|
||||
self.next_channel_preload += 1
|
||||
if self.next_channel_preload >= self.channel_count:
|
||||
self.next_channel_preload = 0
|
||||
|
||||
return downloaded_something
|
||||
self.next_channel_preload += 1
|
||||
if self.next_channel_preload >= self.channel_count:
|
||||
self.next_channel_preload = 0
|
||||
|
||||
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):
|
||||
return False # Didn't normalise
|
||||
# Some channels still have files to preload, do nothing.
|
||||
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):
|
||||
return False
|
||||
# Quit early if all channels are normalised already.
|
||||
if self.known_channels_normalised == [True] * self.channel_count:
|
||||
return False
|
||||
|
||||
channel = self.next_channel_preload
|
||||
channel = self.next_channel_preload
|
||||
|
||||
normalised_something = False
|
||||
# Look through all the show plan files
|
||||
for i in range(len(self.last_known_show_plan[channel])):
|
||||
|
||||
item_obj = PlanItem(self.last_known_show_plan[channel][i])
|
||||
|
||||
filename = item_obj.filename
|
||||
if not filename:
|
||||
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.")
|
||||
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))
|
||||
# This will return immediately if we already have a normalised file.
|
||||
item_obj.filename = generate_normalised_file(filename)
|
||||
# TODO Hacky
|
||||
self.last_known_show_plan[channel][i] = item_obj.__dict__
|
||||
normalised_something = True
|
||||
break # Now go let another channel have a go.
|
||||
except Exception as e:
|
||||
self.logger.log.exception("Failed to generate normalised file.", str(e))
|
||||
continue
|
||||
|
||||
self.known_channels_normalised[channel] = not normalised_something
|
||||
|
||||
self.next_channel_preload += 1
|
||||
if self.next_channel_preload >= self.channel_count:
|
||||
self.next_channel_preload = 0
|
||||
|
||||
return normalised_something
|
||||
normalised_something = False
|
||||
# Look through all the show plan files
|
||||
for i in range(len(self.last_known_show_plan[channel])):
|
||||
|
||||
item_obj = PlanItem(self.last_known_show_plan[channel][i])
|
||||
|
||||
filename = item_obj.filename
|
||||
if not filename:
|
||||
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."
|
||||
)
|
||||
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)
|
||||
)
|
||||
# This will return immediately if we already have a normalised file.
|
||||
item_obj.filename = generate_normalised_file(filename)
|
||||
# TODO Hacky
|
||||
self.last_known_show_plan[channel][i] = item_obj.__dict__
|
||||
normalised_something = True
|
||||
break # Now go let another channel have a go.
|
||||
except Exception as e:
|
||||
self.logger.log.exception("Failed to generate normalised file.", str(e))
|
||||
continue
|
||||
|
||||
self.known_channels_normalised[channel] = not normalised_something
|
||||
|
||||
self.next_channel_preload += 1
|
||||
if self.next_channel_preload >= self.channel_count:
|
||||
self.next_channel_preload = 0
|
||||
|
||||
return normalised_something
|
||||
|
|
|
@ -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"])
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -1,49 +1,44 @@
|
|||
import os
|
||||
from helpers.os_environment import resolve_external_file_path
|
||||
from pydub import AudioSegment, effects # Audio leveling!
|
||||
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"))):
|
||||
raise ValueError("Invalid filename given.")
|
||||
if not (isinstance(filename, str) and filename.endswith(".mp3")):
|
||||
raise ValueError("Invalid filename given.")
|
||||
|
||||
# Already normalised.
|
||||
if filename.endswith("-normalised.mp3"):
|
||||
return filename
|
||||
# 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)):
|
||||
# The file already exists, short circuit.
|
||||
if os.path.exists(normalised_filename):
|
||||
return normalised_filename
|
||||
|
||||
sound = AudioSegment.from_file(filename, "mp3")
|
||||
normalised_sound = effects.normalize(sound)
|
||||
|
||||
normalised_sound.export(normalised_filename, bitrate="320k", format="mp3")
|
||||
return normalised_filename
|
||||
|
||||
sound = AudioSegment.from_file(filename, "mp3")
|
||||
normalised_sound = effects.normalize(sound) #match_target_amplitude(sound, -10)
|
||||
|
||||
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"))):
|
||||
raise ValueError("Invalid filename given.")
|
||||
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"):
|
||||
# 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):
|
||||
return normalised_filename
|
||||
|
||||
# Else we've not got a normalised verison, just take original.
|
||||
return filename
|
||||
|
||||
|
||||
normalised_filename = "{}-normalised.mp3".format(filename.rstrip(".mp3"))
|
||||
|
||||
# normalised version exists
|
||||
if (os.path.exists(normalised_filename)):
|
||||
return normalised_filename
|
||||
|
||||
# Else we've not got a normalised verison, just take original.
|
||||
return filename
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
254
player.py
254
player.py
|
@ -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,21 +381,19 @@ class Player:
|
|||
# So we'll want to update the weight.
|
||||
|
||||
# We're removing the loaded item from the channel.
|
||||
#if loaded_item.weight == weight:
|
||||
loaded_item.weight = -1
|
||||
|
||||
|
||||
# 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)
|
||||
self.state.update("loaded_item", loaded_item)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
@ -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.
|
||||
|
@ -461,12 +470,14 @@ class Player:
|
|||
"Couldn't load file: " + str(loaded_item.filename)
|
||||
)
|
||||
time.sleep(1)
|
||||
continue # Try loading again.
|
||||
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.
|
||||
continue # Try loading again.
|
||||
|
||||
try:
|
||||
if loaded_item.filename.endswith(".mp3"):
|
||||
|
@ -475,14 +486,13 @@ 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.
|
||||
continue # Try loading again.
|
||||
|
||||
# Everything worked, we made it!
|
||||
if loaded_item.cue > 0:
|
||||
|
@ -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,19 +798,25 @@ 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)
|
||||
# TODO ENUM
|
||||
elif state["repeat"] == "all":
|
||||
self.load(0) # Jump to the top.
|
||||
self.load(0) # Jump to the top.
|
||||
return
|
||||
|
||||
# No automations, just stop playing.
|
||||
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
77
server.py
77
server.py
|
@ -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:
|
||||
|
|
3
setup.py
3
setup.py
|
@ -7,4 +7,5 @@ setup(
|
|||
description=package.DESCRIPTION,
|
||||
author=package.AUTHOR,
|
||||
license=package.LICENSE,
|
||||
packages=find_packages())
|
||||
packages=find_packages(),
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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(
|
||||
host=server_state.get()["host"],
|
||||
port=server_state.get()["port"],
|
||||
debug=(not isBundelled()),
|
||||
auto_reload=False,
|
||||
access_log=(not isBundelled())
|
||||
))
|
||||
sync(
|
||||
app.run(
|
||||
host=server_state.get()["host"],
|
||||
port=server_state.get()["port"],
|
||||
debug=(not isBundelled()),
|
||||
auto_reload=False,
|
||||
access_log=(not isBundelled()),
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
break
|
||||
loop = asyncio.get_event_loop()
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue