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
|
from baps_types.marker import Marker
|
||||||
|
|
||||||
|
|
||||||
def _time_ms():
|
def _time_ms():
|
||||||
return round(time() * 1000)
|
return round(time() * 1000)
|
||||||
|
|
||||||
|
|
||||||
class PlanItem:
|
class PlanItem:
|
||||||
_timeslotitemid: str = "0"
|
_timeslotitemid: str = "0"
|
||||||
_weight: int = 0
|
_weight: int = 0
|
||||||
|
@ -72,7 +75,7 @@ class PlanItem:
|
||||||
self._played_at = _time_ms()
|
self._played_at = _time_ms()
|
||||||
|
|
||||||
def play_count_decrement(self):
|
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:
|
if self._play_count == 0:
|
||||||
self._played_at = 0
|
self._played_at = 0
|
||||||
|
|
||||||
|
@ -118,7 +121,9 @@ class PlanItem:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def intro(self) -> float:
|
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?)
|
# TODO: Handle multiple (shouldn't happen?)
|
||||||
if len(markers) > 0:
|
if len(markers) > 0:
|
||||||
return markers[0].time
|
return markers[0].time
|
||||||
|
@ -126,7 +131,9 @@ class PlanItem:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cue(self) -> float:
|
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?)
|
# TODO: Handle multiple (shouldn't happen?)
|
||||||
if len(markers) > 0:
|
if len(markers) > 0:
|
||||||
return markers[0].time
|
return markers[0].time
|
||||||
|
@ -134,7 +141,9 @@ class PlanItem:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def outro(self) -> float:
|
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?)
|
# TODO: Handle multiple (shouldn't happen?)
|
||||||
if len(markers) > 0:
|
if len(markers) > 0:
|
||||||
return markers[0].time
|
return markers[0].time
|
||||||
|
@ -164,7 +173,7 @@ class PlanItem:
|
||||||
"played": self.play_count > 0,
|
"played": self.play_count > 0,
|
||||||
"played_at": self.played_at,
|
"played_at": self.played_at,
|
||||||
"play_count": self.play_count,
|
"play_count": self.play_count,
|
||||||
"clean": self.clean
|
"clean": self.clean,
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, new_item: Dict[str, Any]):
|
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._artist = new_item["artist"] if "artist" in new_item else None
|
||||||
self._length = new_item["length"]
|
self._length = new_item["length"]
|
||||||
self._markers = (
|
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._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._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
|
self._clean = new_item["clean"] if "clean" in new_item else True
|
||||||
|
|
||||||
# TODO: Edit this to handle markers when MyRadio supports them
|
# 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 = {
|
marker = {
|
||||||
"name": "Intro",
|
"name": "Intro",
|
||||||
"time": new_item["intro"],
|
"time": new_item["intro"],
|
||||||
"position": "start",
|
"position": "start",
|
||||||
"section": None
|
"section": None,
|
||||||
}
|
}
|
||||||
self.set_marker(Marker(json.dumps(marker)))
|
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 = {
|
marker = {
|
||||||
"name": "Cue",
|
"name": "Cue",
|
||||||
"time": new_item["cue"],
|
"time": new_item["cue"],
|
||||||
"position": "mid",
|
"position": "mid",
|
||||||
"section": None
|
"section": None,
|
||||||
}
|
}
|
||||||
self.set_marker(Marker(json.dumps(marker)))
|
self.set_marker(Marker(json.dumps(marker)))
|
||||||
# TODO: Convert / handle outro being from end of item.
|
# 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 = {
|
marker = {
|
||||||
"name": "Outro",
|
"name": "Outro",
|
||||||
"time": new_item["outro"],
|
"time": new_item["outro"],
|
||||||
"position": "end",
|
"position": "end",
|
||||||
"section": None
|
"section": None,
|
||||||
}
|
}
|
||||||
self.set_marker(Marker(json.dumps(marker)))
|
self.set_marker(Marker(json.dumps(marker)))
|
||||||
|
|
||||||
|
|
||||||
# Fix any OS specific / or \'s
|
# Fix any OS specific / or \'s
|
||||||
if self.filename:
|
if self.filename:
|
||||||
if os.path.sep == "/":
|
if os.path.sep == "/":
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
from helpers.the_terminator import Terminator
|
from helpers.the_terminator import Terminator
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
from multiprocessing import Queue, current_process
|
from multiprocessing import Queue, current_process
|
||||||
|
@ -95,12 +94,12 @@ class MattchBox(Controller):
|
||||||
self.logger.log.info("Received from controller: " + str(line))
|
self.logger.log.info("Received from controller: " + str(line))
|
||||||
if line == 255:
|
if line == 255:
|
||||||
self.ser.write(b"\xff") # Send 255 back, this is a keepalive.
|
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.
|
# We've received a status update about fader live status, fader is down.
|
||||||
self.sendToPlayer(line-51, "SETLIVE:False")
|
self.sendToPlayer(line - 51, "SETLIVE:False")
|
||||||
elif line in [61,62,63]:
|
elif line in [61, 62, 63]:
|
||||||
# We've received a status update about fader live status, fader is up.
|
# 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]:
|
elif line in [1, 3, 5]:
|
||||||
self.sendToPlayer(int(line / 2), "PLAYPAUSE")
|
self.sendToPlayer(int(line / 2), "PLAYPAUSE")
|
||||||
elif line in [2, 4, 6]:
|
elif line in [2, 4, 6]:
|
||||||
|
@ -136,5 +135,7 @@ class MattchBox(Controller):
|
||||||
self.connect(None)
|
self.connect(None)
|
||||||
|
|
||||||
def sendToPlayer(self, channel: int, msg: str):
|
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)
|
self.server_to_q[channel].put("CONTROLLER:" + msg)
|
||||||
|
|
108
file_manager.py
108
file_manager.py
|
@ -31,17 +31,20 @@ class FileManager:
|
||||||
terminator = Terminator()
|
terminator = Terminator()
|
||||||
self.channel_count = len(channel_from_q)
|
self.channel_count = len(channel_from_q)
|
||||||
self.channel_received = None
|
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.next_channel_preload = 0
|
||||||
self.known_channels_preloaded = [False]*self.channel_count
|
self.known_channels_preloaded = [False] * self.channel_count
|
||||||
self.known_channels_normalised = [False]*self.channel_count
|
self.known_channels_normalised = [False] * self.channel_count
|
||||||
self.last_known_item_ids = [[]]*self.channel_count
|
self.last_known_item_ids = [[]] * self.channel_count
|
||||||
try:
|
try:
|
||||||
|
|
||||||
while not terminator.terminate:
|
while not terminator.terminate:
|
||||||
# If all channels have received the delete command, reset for the next one.
|
# 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):
|
if (
|
||||||
self.channel_received = [False]*self.channel_count
|
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):
|
for channel in range(self.channel_count):
|
||||||
try:
|
try:
|
||||||
|
@ -50,13 +53,16 @@ class FileManager:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
#source = message.split(":")[0]
|
# source = message.split(":")[0]
|
||||||
command = message.split(":",2)[1]
|
command = message.split(":", 2)[1]
|
||||||
|
|
||||||
# If we have requested a new show plan, empty the music-tmp directory for the previous show.
|
# If we have requested a new show plan, empty the music-tmp directory for the previous show.
|
||||||
if command == "GETPLAN":
|
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.
|
# 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 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/")
|
path: str = resolve_external_file_path("/music-tmp/")
|
||||||
|
|
||||||
if not os.path.isdir(path):
|
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
|
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:
|
for file in files:
|
||||||
if isWindows():
|
if isWindows():
|
||||||
filepath = path+"\\"+file
|
filepath = path + "\\" + file
|
||||||
else:
|
else:
|
||||||
filepath = path+"/"+file
|
filepath = path + "/" + file
|
||||||
self.logger.log.info("Removing file {} on new show load.".format(filepath))
|
self.logger.log.info(
|
||||||
|
"Removing file {} on new show load.".format(
|
||||||
|
filepath
|
||||||
|
)
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
os.remove(filepath)
|
os.remove(filepath)
|
||||||
except Exception:
|
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
|
continue
|
||||||
self.channel_received[channel] = True
|
self.channel_received[channel] = True
|
||||||
self.known_channels_preloaded = [False]*self.channel_count
|
self.known_channels_preloaded = [False] * self.channel_count
|
||||||
self.known_channels_normalised = [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 we receive a new status message, let's check for files which have not been pre-loaded.
|
||||||
if command == "STATUS":
|
if command == "STATUS":
|
||||||
extra = message.split(":",3)
|
extra = message.split(":", 3)
|
||||||
if extra[2] != "OKAY":
|
if extra[2] != "OKAY":
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -107,28 +127,30 @@ class FileManager:
|
||||||
self.known_channels_preloaded[channel] = False
|
self.known_channels_preloaded[channel] = False
|
||||||
|
|
||||||
except Exception:
|
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.
|
# Let's try preload / normalise some files now we're free of messages.
|
||||||
preloaded = self.do_preload()
|
preloaded = self.do_preload()
|
||||||
normalised = self.do_normalise()
|
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.
|
# We didn't do any hard work, let's sleep.
|
||||||
sleep(0.2)
|
sleep(0.2)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.log.exception(
|
self.logger.log.exception("Received unexpected exception: {}".format(e))
|
||||||
"Received unexpected exception: {}".format(e))
|
|
||||||
del self.logger
|
del self.logger
|
||||||
|
|
||||||
|
|
||||||
# Attempt to preload a file onto disk.
|
# Attempt to preload a file onto disk.
|
||||||
def do_preload(self):
|
def do_preload(self):
|
||||||
channel = self.next_channel_preload
|
channel = self.next_channel_preload
|
||||||
|
|
||||||
# All channels have preloaded all files, do nothing.
|
# 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
|
return False # Didn't preload anything
|
||||||
|
|
||||||
# Right, let's have a quick check in the status for shows without filenames, to preload them.
|
# 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.
|
# We've not downloaded this file yet, let's do that.
|
||||||
if not item_obj.filename:
|
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.
|
# 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.
|
# 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
|
# Save back the resulting item back in regular dict form
|
||||||
|
@ -152,7 +180,9 @@ class FileManager:
|
||||||
|
|
||||||
if did_download:
|
if did_download:
|
||||||
downloaded_something = True
|
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
|
break
|
||||||
else:
|
else:
|
||||||
# We didn't download anything this time, file was already loaded.
|
# We didn't download anything this time, file was already loaded.
|
||||||
|
@ -168,15 +198,14 @@ class FileManager:
|
||||||
|
|
||||||
return downloaded_something
|
return downloaded_something
|
||||||
|
|
||||||
|
|
||||||
# If we've preloaded everything, get to work normalising tracks before playback.
|
# If we've preloaded everything, get to work normalising tracks before playback.
|
||||||
def do_normalise(self):
|
def do_normalise(self):
|
||||||
# Some channels still have files to preload, do nothing.
|
# 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
|
return False # Didn't normalise
|
||||||
|
|
||||||
# Quit early if all channels are normalised already.
|
# 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
|
return False
|
||||||
|
|
||||||
channel = self.next_channel_preload
|
channel = self.next_channel_preload
|
||||||
|
@ -189,16 +218,22 @@ class FileManager:
|
||||||
|
|
||||||
filename = item_obj.filename
|
filename = item_obj.filename
|
||||||
if not 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.
|
continue # Try next song.
|
||||||
elif (not os.path.isfile(filename)):
|
elif not os.path.isfile(filename):
|
||||||
self.logger.log.exception("Filename for normalisation does not exist. This is bad.")
|
self.logger.log.exception(
|
||||||
|
"Filename for normalisation does not exist. This is bad."
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
elif "normalised" in filename:
|
elif "normalised" in filename:
|
||||||
continue
|
continue
|
||||||
# Sweet, we now need to try generating a normalised version.
|
# Sweet, we now need to try generating a normalised version.
|
||||||
try:
|
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.
|
# This will return immediately if we already have a normalised file.
|
||||||
item_obj.filename = generate_normalised_file(filename)
|
item_obj.filename = generate_normalised_file(filename)
|
||||||
# TODO Hacky
|
# TODO Hacky
|
||||||
|
@ -216,8 +251,3 @@ class FileManager:
|
||||||
self.next_channel_preload = 0
|
self.next_channel_preload = 0
|
||||||
|
|
||||||
return normalised_something
|
return normalised_something
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ from typing import Any, Dict, List, Optional, Tuple
|
||||||
import sounddevice as sd
|
import sounddevice as sd
|
||||||
from helpers.os_environment import isLinux, isMacOS, isWindows
|
from helpers.os_environment import isLinux, isMacOS, isWindows
|
||||||
import glob
|
import glob
|
||||||
|
|
||||||
if isWindows():
|
if isWindows():
|
||||||
from serial.tools.list_ports_windows import comports
|
from serial.tools.list_ports_windows import comports
|
||||||
|
|
||||||
|
@ -39,7 +40,9 @@ class DeviceManager:
|
||||||
else:
|
else:
|
||||||
host_apis[host_api_id]["usable"] = True
|
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: List[Dict] = list(filter(cls._isOutput, host_api_devices))
|
||||||
outputs = sorted(outputs, key=lambda k: k["name"])
|
outputs = sorted(outputs, key=lambda k: k["name"])
|
||||||
|
|
|
@ -56,7 +56,8 @@ class MyRadioAPI:
|
||||||
async with func as response:
|
async with func as response:
|
||||||
if response.status != status_code:
|
if response.status != status_code:
|
||||||
self._logException(
|
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()))
|
self._logException(str(await response.text()))
|
||||||
return await response.read()
|
return await response.read()
|
||||||
|
@ -81,7 +82,9 @@ class MyRadioAPI:
|
||||||
self._logException(str(r.text))
|
self._logException(str(r.text))
|
||||||
return json.loads(r.text) if json_payload else 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":
|
if api_version == "v2":
|
||||||
url = "{}/v2{}".format(self.config.get()["myradio_api_url"], url)
|
url = "{}/v2{}".format(self.config.get()["myradio_api_url"], url)
|
||||||
elif api_version == "non":
|
elif api_version == "non":
|
||||||
|
@ -198,7 +201,6 @@ class MyRadioAPI:
|
||||||
self.logger.log.error("Show plan in unknown format.")
|
self.logger.log.error("Show plan in unknown format.")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
# Audio Library
|
# Audio Library
|
||||||
|
|
||||||
async def get_filename(self, item: PlanItem, did_download: bool = False):
|
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 something else (another channel, the preloader etc) is downloading the track, wait for it.
|
||||||
if os.path.isfile(filename + dl_suffix):
|
if os.path.isfile(filename + dl_suffix):
|
||||||
time_waiting_s = 0
|
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:
|
while time_waiting_s < 20:
|
||||||
# TODO: Make something better here.
|
# 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 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):
|
if os.path.isfile(filename):
|
||||||
# Now the file is downloaded successfully
|
# Now the file is downloaded successfully
|
||||||
return (filename, False) if did_download else filename
|
return (filename, False) if did_download else filename
|
||||||
time_waiting_s +=1
|
time_waiting_s += 1
|
||||||
self._log("Still waiting",DEBUG)
|
self._log("Still waiting", DEBUG)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
# File doesn't exist, download it.
|
# File doesn't exist, download it.
|
||||||
|
@ -300,7 +305,7 @@ class MyRadioAPI:
|
||||||
async def get_playlist_aux_items(self, library_id: str):
|
async def get_playlist_aux_items(self, library_id: str):
|
||||||
# Sometimes they have "aux-<ID>", we only need the index.
|
# Sometimes they have "aux-<ID>", we only need the index.
|
||||||
if library_id.index("-") > -1:
|
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)
|
url = "/nipswebPlaylist/{}/items".format(library_id)
|
||||||
request = await self.async_api_call(url)
|
request = await self.async_api_call(url)
|
||||||
|
@ -351,12 +356,14 @@ class MyRadioAPI:
|
||||||
source: str = self.config.get()["myradio_api_tracklist_source"]
|
source: str = self.config.get()["myradio_api_tracklist_source"]
|
||||||
data = {
|
data = {
|
||||||
"trackid": item.trackid,
|
"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.
|
# Starttime and timeslotid are default in the API to current time/show.
|
||||||
tracklist_id = None
|
tracklist_id = None
|
||||||
try:
|
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:
|
except Exception as e:
|
||||||
self._logException("Failed to get tracklistid. {}".format(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)
|
self._log("Tracklistitemid is None, can't end tracklist.", WARNING)
|
||||||
return False
|
return False
|
||||||
if not isinstance(tracklistitemid, int):
|
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
|
return False
|
||||||
|
|
||||||
self._log("Ending tracklistitemid {}".format(tracklistitemid))
|
self._log("Ending tracklistitemid {}".format(tracklistitemid))
|
||||||
|
|
|
@ -1,49 +1,44 @@
|
||||||
import os
|
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.
|
# 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.
|
# Takes filename in, normalialises it and returns a normalised file path.
|
||||||
def generate_normalised_file(filename: str):
|
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.")
|
raise ValueError("Invalid filename given.")
|
||||||
|
|
||||||
# Already normalised.
|
# Already normalised.
|
||||||
if filename.endswith("-normalised.mp3"):
|
if filename.endswith("-normalised.mp3"):
|
||||||
return filename
|
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.
|
# The file already exists, short circuit.
|
||||||
if (os.path.exists(normalised_filename)):
|
if os.path.exists(normalised_filename):
|
||||||
return normalised_filename
|
return normalised_filename
|
||||||
|
|
||||||
sound = AudioSegment.from_file(filename, "mp3")
|
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")
|
normalised_sound.export(normalised_filename, bitrate="320k", format="mp3")
|
||||||
return normalised_filename
|
return normalised_filename
|
||||||
|
|
||||||
|
|
||||||
# Returns either a normalised file path (based on filename), or the original if not available.
|
# Returns either a normalised file path (based on filename), or the original if not available.
|
||||||
def get_normalised_filename_if_available(filename:str):
|
def get_normalised_filename_if_available(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.")
|
raise ValueError("Invalid filename given.")
|
||||||
|
|
||||||
# Already normalised.
|
# Already normalised.
|
||||||
if filename.endswith("-normalised.mp3"):
|
if filename.endswith("-normalised.mp3"):
|
||||||
return filename
|
return filename
|
||||||
|
|
||||||
|
|
||||||
normalised_filename = "{}-normalised.mp3".format(filename.rstrip(".mp3"))
|
normalised_filename = "{}-normalised.mp3".format(filename.rstrip(".mp3"))
|
||||||
|
|
||||||
# normalised version exists
|
# normalised version exists
|
||||||
if (os.path.exists(normalised_filename)):
|
if os.path.exists(normalised_filename):
|
||||||
return normalised_filename
|
return normalised_filename
|
||||||
|
|
||||||
# Else we've not got a normalised verison, just take original.
|
# Else we've not got a normalised verison, just take original.
|
||||||
return filename
|
return filename
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from logging import CRITICAL, DEBUG, INFO
|
from logging import DEBUG, INFO
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from copy import copy
|
from copy import copy
|
||||||
|
@ -79,7 +79,7 @@ class StateManager:
|
||||||
# If there are any new config options in the default state, save them.
|
# If there are any new config options in the default state, save them.
|
||||||
# Uses update() to save them to file too.
|
# Uses update() to save them to file too.
|
||||||
for key in default_state.keys():
|
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])
|
self.update(key, default_state[key])
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
|
@ -114,7 +114,6 @@ class StateManager:
|
||||||
|
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
|
|
||||||
|
|
||||||
current_time = now.strftime("%H:%M:%S")
|
current_time = now.strftime("%H:%M:%S")
|
||||||
state_to_json["last_updated"] = current_time
|
state_to_json["last_updated"] = current_time
|
||||||
|
|
||||||
|
@ -154,20 +153,30 @@ class StateManager:
|
||||||
allow = False
|
allow = False
|
||||||
|
|
||||||
# It's hard to compare lists, especially of complex objects like show plans, just write it.
|
# 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
|
allow = True
|
||||||
|
|
||||||
# If the two objects have dict representations, and they don't match, allow writing.
|
# If the two objects have dict representations, and they don't match, allow writing.
|
||||||
# TODO: This should be easier.
|
# 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__:
|
if value.__dict__ != state_to_update[key].__dict__:
|
||||||
allow = True
|
allow = True
|
||||||
|
|
||||||
if not allow:
|
if not allow:
|
||||||
|
|
||||||
# Just some debug logging.
|
# Just some debug logging.
|
||||||
if update_file and (key not in ["playing", "loaded", "initialised", "remaining", "pos_true"]):
|
if update_file and (
|
||||||
self._log("Not updating state for key '{}' with value '{}' of type '{}'.".format(key, value, type(value)), DEBUG)
|
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.
|
# We're trying to update the state with the same value.
|
||||||
# In this case, ignore the update
|
# In this case, ignore the update
|
||||||
|
@ -176,11 +185,21 @@ class StateManager:
|
||||||
|
|
||||||
if index > -1 and key in state_to_update:
|
if index > -1 and key in state_to_update:
|
||||||
if not isinstance(state_to_update[key], list):
|
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
|
return
|
||||||
list_items = state_to_update[key]
|
list_items = state_to_update[key]
|
||||||
if index >= len(list_items):
|
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
|
return
|
||||||
list_items[index] = value
|
list_items[index] = value
|
||||||
state_to_update[key] = list_items
|
state_to_update[key] = list_items
|
||||||
|
@ -190,7 +209,12 @@ class StateManager:
|
||||||
self.state = state_to_update
|
self.state = state_to_update
|
||||||
|
|
||||||
if update_file:
|
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.
|
# Either a routine write, or state has changed.
|
||||||
# Update the file
|
# Update the file
|
||||||
self.write_to_file(state_to_update)
|
self.write_to_file(state_to_update)
|
||||||
|
|
|
@ -70,7 +70,11 @@ if __name__ == "__main__":
|
||||||
if sys.argv[1] == "Presenter":
|
if sys.argv[1] == "Presenter":
|
||||||
webbrowser.open("http://localhost:13500/presenter/")
|
webbrowser.open("http://localhost:13500/presenter/")
|
||||||
except Exception as e:
|
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(1)
|
||||||
|
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
|
@ -15,9 +15,10 @@ with open(resolve_local_file_path("package.json")) as file:
|
||||||
build_beta = True
|
build_beta = True
|
||||||
try:
|
try:
|
||||||
import build
|
import build
|
||||||
|
|
||||||
build_commit = build.BUILD
|
build_commit = build.BUILD
|
||||||
build_branch = build.BRANCH
|
build_branch = build.BRANCH
|
||||||
build_beta = (build_branch != "release")
|
build_beta = build_branch != "release"
|
||||||
except (ModuleNotFoundError, AttributeError):
|
except (ModuleNotFoundError, AttributeError):
|
||||||
pass
|
pass
|
||||||
BUILD: str = build_commit
|
BUILD: str = build_commit
|
||||||
|
|
242
player.py
242
player.py
|
@ -21,6 +21,7 @@
|
||||||
|
|
||||||
# Stop the Pygame Hello message.
|
# Stop the Pygame Hello message.
|
||||||
import os
|
import os
|
||||||
|
|
||||||
os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "hide"
|
os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "hide"
|
||||||
|
|
||||||
from queue import Empty
|
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.
|
# Don't mess with playback, we only care about if it's supposed to be loaded.
|
||||||
if not self._isLoaded(short_test=True):
|
if not self._isLoaded(short_test=True):
|
||||||
return False
|
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
|
@property
|
||||||
def status(self):
|
def status(self):
|
||||||
|
@ -251,7 +255,9 @@ class Player:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
else:
|
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.
|
self.stopped_manually = True # Don't trigger _ended() on seeking.
|
||||||
if pos > 0:
|
if pos > 0:
|
||||||
self.state.update("paused", True)
|
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.
|
# 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)
|
# 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.
|
# 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
|
return item
|
||||||
|
|
||||||
# TODO Allow just moving an item inside the channel instead of removing and adding.
|
# 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)
|
self._fix_and_update_weights(plan_copy)
|
||||||
|
|
||||||
|
|
||||||
loaded_item = self.state.get()["loaded_item"]
|
loaded_item = self.state.get()["loaded_item"]
|
||||||
if loaded_item:
|
if loaded_item:
|
||||||
|
|
||||||
|
@ -346,13 +353,15 @@ class Player:
|
||||||
|
|
||||||
def remove_from_plan(self, weight: int) -> bool:
|
def remove_from_plan(self, weight: int) -> bool:
|
||||||
plan_copy: List[PlanItem] = copy.copy(self.state.get()["show_plan"])
|
plan_copy: List[PlanItem] = copy.copy(self.state.get()["show_plan"])
|
||||||
found: Optional[PlanItem ] = None
|
found: Optional[PlanItem] = None
|
||||||
|
|
||||||
before = []
|
before = []
|
||||||
for item in plan_copy:
|
for item in plan_copy:
|
||||||
before += (item.weight, item.name)
|
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:
|
for i in plan_copy:
|
||||||
if i.weight == weight:
|
if i.weight == weight:
|
||||||
|
@ -372,18 +381,16 @@ class Player:
|
||||||
# So we'll want to update the weight.
|
# So we'll want to update the weight.
|
||||||
|
|
||||||
# We're removing the loaded item from the channel.
|
# We're removing the loaded item from the channel.
|
||||||
#if loaded_item.weight == weight:
|
# if loaded_item.weight == weight:
|
||||||
loaded_item.weight = -1
|
loaded_item.weight = -1
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# If loaded_item wasn't the same instance, we'd want to do the below.
|
# If loaded_item wasn't the same instance, we'd want to do the below.
|
||||||
|
|
||||||
# We removed an item above it. Shift it up.
|
# We removed an item above it. Shift it up.
|
||||||
#elif loaded_item.weight > weight:
|
# elif loaded_item.weight > weight:
|
||||||
# loaded_item.weight -= 1
|
# loaded_item.weight -= 1
|
||||||
# Else, new weight stays the same.
|
# Else, new weight stays the same.
|
||||||
#else:
|
# else:
|
||||||
# return True
|
# return True
|
||||||
|
|
||||||
self.state.update("loaded_item", loaded_item)
|
self.state.update("loaded_item", loaded_item)
|
||||||
|
@ -399,7 +406,10 @@ class Player:
|
||||||
loaded_state = self.state.get()
|
loaded_state = self.state.get()
|
||||||
self.unload()
|
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"])
|
self.output(loaded_state["output"])
|
||||||
|
|
||||||
showplan = loaded_state["show_plan"]
|
showplan = loaded_state["show_plan"]
|
||||||
|
@ -412,14 +422,12 @@ class Player:
|
||||||
break
|
break
|
||||||
|
|
||||||
if loaded_item is None:
|
if loaded_item is None:
|
||||||
self.logger.log.error(
|
self.logger.log.error("Failed to find weight: {}".format(weight))
|
||||||
"Failed to find weight: {}".format(weight))
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
reload = False
|
reload = False
|
||||||
if loaded_item.filename == "" or loaded_item.filename is None:
|
if loaded_item.filename == "" or loaded_item.filename is None:
|
||||||
self.logger.log.info(
|
self.logger.log.info("Filename is not specified, loading from API.")
|
||||||
"Filename is not specified, loading from API.")
|
|
||||||
reload = True
|
reload = True
|
||||||
elif not os.path.exists(loaded_item.filename):
|
elif not os.path.exists(loaded_item.filename):
|
||||||
self.logger.log.warn(
|
self.logger.log.warn(
|
||||||
|
@ -434,7 +442,9 @@ class Player:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Swap with a normalised version if it's ready, else returns original.
|
# 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)
|
self.state.update("loaded_item", loaded_item)
|
||||||
|
|
||||||
|
@ -452,8 +462,7 @@ class Player:
|
||||||
while load_attempt < 5:
|
while load_attempt < 5:
|
||||||
load_attempt += 1
|
load_attempt += 1
|
||||||
try:
|
try:
|
||||||
self.logger.log.info("Loading file: " +
|
self.logger.log.info("Loading file: " + str(loaded_item.filename))
|
||||||
str(loaded_item.filename))
|
|
||||||
mixer.music.load(loaded_item.filename)
|
mixer.music.load(loaded_item.filename)
|
||||||
except Exception:
|
except Exception:
|
||||||
# We couldn't load that file.
|
# We couldn't load that file.
|
||||||
|
@ -464,7 +473,9 @@ class Player:
|
||||||
continue # Try loading again.
|
continue # Try loading again.
|
||||||
|
|
||||||
if not self.isLoaded:
|
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)
|
time.sleep(1)
|
||||||
continue # Try loading again.
|
continue # Try loading again.
|
||||||
|
|
||||||
|
@ -475,12 +486,11 @@ class Player:
|
||||||
else:
|
else:
|
||||||
# WARNING! Pygame / SDL can't seek .wav files :/
|
# WARNING! Pygame / SDL can't seek .wav files :/
|
||||||
self.state.update(
|
self.state.update(
|
||||||
"length", mixer.Sound(
|
"length",
|
||||||
loaded_item.filename).get_length() / 1000
|
mixer.Sound(loaded_item.filename).get_length() / 1000,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
self.logger.log.exception(
|
self.logger.log.exception("Failed to update the length of item.")
|
||||||
"Failed to update the length of item.")
|
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
continue # Try loading again.
|
continue # Try loading again.
|
||||||
|
|
||||||
|
@ -561,7 +571,11 @@ class Player:
|
||||||
try:
|
try:
|
||||||
marker = Marker(marker_str)
|
marker = Marker(marker_str)
|
||||||
except Exception as e:
|
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
|
return False
|
||||||
|
|
||||||
if timeslotitemid == "-1":
|
if timeslotitemid == "-1":
|
||||||
|
@ -569,10 +583,12 @@ class Player:
|
||||||
if not self.isLoaded:
|
if not self.isLoaded:
|
||||||
return False
|
return False
|
||||||
timeslotitemid = self.state.get()["loaded_item"].timeslotitemid
|
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
|
set_loaded = True
|
||||||
|
|
||||||
|
|
||||||
plan_copy: List[PlanItem] = copy.copy(self.state.get()["show_plan"])
|
plan_copy: List[PlanItem] = copy.copy(self.state.get()["show_plan"])
|
||||||
for i in range(len(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:
|
except Exception as e:
|
||||||
self.logger.log.error(
|
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
|
success = False
|
||||||
|
|
||||||
if set_loaded:
|
if set_loaded:
|
||||||
try:
|
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:
|
except Exception as e:
|
||||||
self.logger.log.error(
|
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
|
success = False
|
||||||
|
|
||||||
return success
|
return success
|
||||||
|
@ -605,7 +629,9 @@ class Player:
|
||||||
item.play_count_increment() if played else item.play_count_reset()
|
item.play_count_increment() if played else item.play_count_reset()
|
||||||
self.state.update("show_plan", plan)
|
self.state.update("show_plan", plan)
|
||||||
elif len(plan) > weight:
|
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)
|
self.state.update("show_plan", plan[weight], weight)
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
@ -617,11 +643,10 @@ class Player:
|
||||||
self.state.update("live", live)
|
self.state.update("live", live)
|
||||||
|
|
||||||
# If we're going to live (potentially from not live/PFL), potentially tracklist if it's playing.
|
# If we're going to live (potentially from not live/PFL), potentially tracklist if it's playing.
|
||||||
if (live):
|
if live:
|
||||||
self._potentially_tracklist()
|
self._potentially_tracklist()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
# Helper functions
|
# Helper functions
|
||||||
|
|
||||||
# This essentially allows the tracklist end API call to happen in a separate thread, to avoid hanging playout/loading.
|
# 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"]
|
mode = self.state.get()["tracklist_mode"]
|
||||||
|
|
||||||
time: int = -1
|
time: int = -1
|
||||||
if mode in ["on","fader-live"]:
|
if mode in ["on", "fader-live"]:
|
||||||
time = 1 # Let's do it pretty quickly.
|
time = 1 # Let's do it pretty quickly.
|
||||||
elif mode == "delayed":
|
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.)
|
# 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
|
time = TRACKLISTING_DELAYED_S
|
||||||
|
|
||||||
if time >= 0 and not self.tracklist_start_timer:
|
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 = Timer(time, self._tracklist_start)
|
||||||
self.tracklist_start_timer.start()
|
self.tracklist_start_timer.start()
|
||||||
elif self.tracklist_start_timer:
|
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.
|
# This essentially allows the tracklist end API call to happen in a separate thread, to avoid hanging playout/loading.
|
||||||
def _potentially_end_tracklist(self):
|
def _potentially_end_tracklist(self):
|
||||||
|
@ -663,24 +694,34 @@ class Player:
|
||||||
self.logger.log.info("No tracklist to end.")
|
self.logger.log.info("No tracklist to end.")
|
||||||
return
|
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:
|
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:
|
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
|
return
|
||||||
self.state.update("tracklist_id", None)
|
self.state.update("tracklist_id", None)
|
||||||
# This threads it, so it won't hang track loading if it fails.
|
# 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 = Timer(1, self._tracklist_end, [tracklist_id])
|
||||||
self.tracklist_end_timer.start()
|
self.tracklist_end_timer.start()
|
||||||
else:
|
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):
|
def _tracklist_start(self):
|
||||||
state = self.state.get()
|
state = self.state.get()
|
||||||
loaded_item = state["loaded_item"]
|
loaded_item = state["loaded_item"]
|
||||||
if not 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:
|
elif not self.isPlaying:
|
||||||
self.logger.log.info("Not tracklisting since not playing.")
|
self.logger.log.info("Not tracklisting since not playing.")
|
||||||
|
@ -688,20 +729,27 @@ class Player:
|
||||||
else:
|
else:
|
||||||
|
|
||||||
tracklist_id = state["tracklist_id"]
|
tracklist_id = state["tracklist_id"]
|
||||||
if (not tracklist_id):
|
if not tracklist_id:
|
||||||
if (state["tracklist_mode"] == "fader-live" and not state["live"]):
|
if state["tracklist_mode"] == "fader-live" and not state["live"]:
|
||||||
self.logger.log.info("Not tracklisting since fader is not live.")
|
self.logger.log.info("Not tracklisting since fader is not live.")
|
||||||
else:
|
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)
|
tracklist_id = self.api.post_tracklist_start(loaded_item)
|
||||||
if not tracklist_id:
|
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:
|
else:
|
||||||
self.logger.log.info("Tracklist id: '{}'".format(tracklist_id))
|
self.logger.log.info("Tracklist id: '{}'".format(tracklist_id))
|
||||||
self.state.update("tracklist_id", tracklist_id)
|
self.state.update("tracklist_id", tracklist_id)
|
||||||
else:
|
else:
|
||||||
self.logger.log.info("Not tracklisting item '{}', already got tracklistid: '{}'".format(
|
self.logger.log.info(
|
||||||
loaded_item.name, tracklist_id))
|
"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.
|
# No matter what we end up doing, we need to kill this timer so future ones can run.
|
||||||
self.tracklist_start_timer = None
|
self.tracklist_start_timer = None
|
||||||
|
@ -709,10 +757,14 @@ class Player:
|
||||||
def _tracklist_end(self, tracklist_id):
|
def _tracklist_end(self, tracklist_id):
|
||||||
|
|
||||||
if 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)
|
self.api.post_tracklist_end(tracklist_id)
|
||||||
else:
|
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
|
self.tracklist_end_timer = None
|
||||||
|
|
||||||
|
@ -727,7 +779,11 @@ class Player:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Track has ended
|
# 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
|
# Repeat 1
|
||||||
# TODO ENUM
|
# TODO ENUM
|
||||||
|
@ -742,13 +798,19 @@ class Player:
|
||||||
# If it's been removed, weight will be -1.
|
# If it's been removed, weight will be -1.
|
||||||
# Just stop in this case.
|
# Just stop in this case.
|
||||||
if loaded_item.weight < 0:
|
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:
|
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 there's another item after this one, load that.
|
||||||
if len(state["show_plan"]) > loaded_item.weight+1:
|
if len(state["show_plan"]) > loaded_item.weight + 1:
|
||||||
self.load(loaded_item.weight+1)
|
self.load(loaded_item.weight + 1)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Repeat All (Jump to top again)
|
# Repeat All (Jump to top again)
|
||||||
|
@ -795,8 +857,7 @@ class Player:
|
||||||
|
|
||||||
self.state.update(
|
self.state.update(
|
||||||
"remaining",
|
"remaining",
|
||||||
max(0, (self.state.get()["length"] -
|
max(0, (self.state.get()["length"] - self.state.get()["pos_true"])),
|
||||||
self.state.get()["pos_true"])),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def _ping_times(self):
|
def _ping_times(self):
|
||||||
|
@ -832,17 +893,18 @@ class Player:
|
||||||
response += "FAIL"
|
response += "FAIL"
|
||||||
|
|
||||||
if self.out_q:
|
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.
|
# Don't fill logs with status pushes, it's a mess.
|
||||||
self.logger.log.debug(("Sending: {}".format(response)))
|
self.logger.log.debug(("Sending: {}".format(response)))
|
||||||
self.out_q.put(response)
|
self.out_q.put(response)
|
||||||
else:
|
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):
|
def _send_status(self):
|
||||||
# TODO This is hacky
|
# TODO This is hacky
|
||||||
self._retMsg(str(self.status), okay_str=True,
|
self._retMsg(str(self.status), okay_str=True, custom_prefix="ALL:STATUS:")
|
||||||
custom_prefix="ALL:STATUS:")
|
|
||||||
|
|
||||||
def _fix_and_update_weights(self, plan):
|
def _fix_and_update_weights(self, plan):
|
||||||
def _sort_weight(e: PlanItem):
|
def _sort_weight(e: PlanItem):
|
||||||
|
@ -854,7 +916,6 @@ class Player:
|
||||||
|
|
||||||
self.logger.log.debug("Weights before fixing:\n{}".format(before))
|
self.logger.log.debug("Weights before fixing:\n{}".format(before))
|
||||||
|
|
||||||
|
|
||||||
plan.sort(key=_sort_weight) # Sort into weighted order.
|
plan.sort(key=_sort_weight) # Sort into weighted order.
|
||||||
|
|
||||||
sorted = []
|
sorted = []
|
||||||
|
@ -874,7 +935,11 @@ class Player:
|
||||||
self.state.update("show_plan", plan)
|
self.state.update("show_plan", plan)
|
||||||
|
|
||||||
def __init__(
|
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)
|
process_title = "Player: Channel " + str(channel)
|
||||||
|
@ -899,7 +964,9 @@ class Player:
|
||||||
|
|
||||||
self.state.update("channel", channel)
|
self.state.update("channel", channel)
|
||||||
self.state.update("tracklist_mode", server_state.get()["tracklist_mode"])
|
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.
|
# 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"])
|
plan_copy: List[PlanItem] = copy.copy(self.state.get()["show_plan"])
|
||||||
|
@ -908,8 +975,7 @@ class Player:
|
||||||
loaded_state = copy.copy(self.state.state)
|
loaded_state = copy.copy(self.state.state)
|
||||||
|
|
||||||
if loaded_state["output"]:
|
if loaded_state["output"]:
|
||||||
self.logger.log.info("Setting output to: " +
|
self.logger.log.info("Setting output to: " + str(loaded_state["output"]))
|
||||||
str(loaded_state["output"]))
|
|
||||||
self.output(loaded_state["output"])
|
self.output(loaded_state["output"])
|
||||||
else:
|
else:
|
||||||
self.logger.log.info("Using default output device.")
|
self.logger.log.info("Using default output device.")
|
||||||
|
@ -918,7 +984,7 @@ class Player:
|
||||||
loaded_item = loaded_state["loaded_item"]
|
loaded_item = loaded_state["loaded_item"]
|
||||||
if loaded_item:
|
if loaded_item:
|
||||||
# No need to load on init, the output switch does this, as it would for regular output switching.
|
# 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.
|
# 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.
|
# 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_source = ""
|
||||||
self.last_msg = ""
|
self.last_msg = ""
|
||||||
self.logger.log.warn(
|
self.logger.log.warn(
|
||||||
"Message from unknown sender source: {}".format(
|
"Message from unknown sender source: {}".format(source)
|
||||||
source)
|
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -981,9 +1046,13 @@ class Player:
|
||||||
# Unpause, so we don't jump to 0, we play from the current pos.
|
# Unpause, so we don't jump to 0, we play from the current pos.
|
||||||
"PLAY": lambda: self._retMsg(self.unpause()),
|
"PLAY": lambda: self._retMsg(self.unpause()),
|
||||||
"PAUSE": lambda: self._retMsg(self.pause()),
|
"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()),
|
"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(
|
"SEEK": lambda: self._retMsg(
|
||||||
self.seek(float(self.last_msg.split(":")[1]))
|
self.seek(float(self.last_msg.split(":")[1]))
|
||||||
),
|
),
|
||||||
|
@ -1011,19 +1080,33 @@ class Player:
|
||||||
"UNLOAD": lambda: self._retMsg(self.unload()),
|
"UNLOAD": lambda: self._retMsg(self.unload()),
|
||||||
"ADD": lambda: self._retMsg(
|
"ADD": lambda: self._retMsg(
|
||||||
self.add_to_plan(
|
self.add_to_plan(
|
||||||
json.loads(
|
json.loads(":".join(self.last_msg.split(":")[1:]))
|
||||||
":".join(self.last_msg.split(":")[1:]))
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
"REMOVE": lambda: self._retMsg(
|
"REMOVE": lambda: self._retMsg(
|
||||||
self.remove_from_plan(
|
self.remove_from_plan(int(self.last_msg.split(":")[1]))
|
||||||
int(self.last_msg.split(":")[1]))
|
|
||||||
),
|
),
|
||||||
"CLEAR": lambda: self._retMsg(self.clear_channel_plan()),
|
"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])),
|
"SETMARKER": lambda: self._retMsg(
|
||||||
"RESETPLAYED": lambda: self._retMsg(self.set_played(weight=int(self.last_msg.split(":")[1]), played = False)),
|
self.set_marker(
|
||||||
"SETPLAYED": lambda: self._retMsg(self.set_played(weight=int(self.last_msg.split(":")[1]), played = True)),
|
self.last_msg.split(":")[1],
|
||||||
"SETLIVE": lambda: self._retMsg(self.set_live(self.last_msg.split(":")[1] == "True")),
|
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]
|
message_type: str = self.last_msg.split(":")[0]
|
||||||
|
@ -1051,8 +1134,7 @@ class Player:
|
||||||
except SystemExit:
|
except SystemExit:
|
||||||
self.logger.log.info("Received SystemExit")
|
self.logger.log.info("Received SystemExit")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.log.exception(
|
self.logger.log.exception("Received unexpected Exception: {}".format(e))
|
||||||
"Received unexpected Exception: {}".format(e))
|
|
||||||
|
|
||||||
self.logger.log.info("Quiting player " + str(channel))
|
self.logger.log.info("Quiting player " + str(channel))
|
||||||
self.quit()
|
self.quit()
|
||||||
|
|
|
@ -10,7 +10,9 @@ from helpers.the_terminator import Terminator
|
||||||
class PlayerHandler:
|
class PlayerHandler:
|
||||||
logger: LoggingManager
|
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")
|
self.logger = LoggingManager("PlayerHandler")
|
||||||
process_title = "Player Handler"
|
process_title = "Player Handler"
|
||||||
|
@ -31,7 +33,6 @@ class PlayerHandler:
|
||||||
if command == "GET_PLAN" or command == "STATUS":
|
if command == "GET_PLAN" or command == "STATUS":
|
||||||
file_to_q[channel].put(message)
|
file_to_q[channel].put(message)
|
||||||
|
|
||||||
|
|
||||||
# TODO ENUM
|
# TODO ENUM
|
||||||
if source in ["ALL", "WEBSOCKET"]:
|
if source in ["ALL", "WEBSOCKET"]:
|
||||||
websocket_to_q[channel].put(message)
|
websocket_to_q[channel].put(message)
|
||||||
|
@ -46,7 +47,6 @@ class PlayerHandler:
|
||||||
|
|
||||||
sleep(0.02)
|
sleep(0.02)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.log.exception(
|
self.logger.log.exception("Received unexpected exception: {}".format(e))
|
||||||
"Received unexpected exception: {}".format(e))
|
|
||||||
del self.logger
|
del self.logger
|
||||||
_exit(0)
|
_exit(0)
|
||||||
|
|
77
server.py
77
server.py
|
@ -110,28 +110,53 @@ class BAPSicleServer:
|
||||||
terminator = Terminator()
|
terminator = Terminator()
|
||||||
log_function = self.logger.log.info
|
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"]):
|
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.)
|
# 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
|
# 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))
|
log_function("Player {} not running, (re)starting.".format(channel))
|
||||||
self.player[channel] = multiprocessing.Process(
|
self.player[channel] = multiprocessing.Process(
|
||||||
target=player.Player,
|
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()
|
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.")
|
log_function("Player Handler not running, (re)starting.")
|
||||||
self.player_handler = multiprocessing.Process(
|
self.player_handler = multiprocessing.Process(
|
||||||
target=PlayerHandler,
|
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()
|
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.")
|
log_function("File Manager not running, (re)starting.")
|
||||||
self.file_manager = multiprocessing.Process(
|
self.file_manager = multiprocessing.Process(
|
||||||
target=FileManager,
|
target=FileManager,
|
||||||
|
@ -139,24 +164,38 @@ class BAPSicleServer:
|
||||||
)
|
)
|
||||||
self.file_manager.start()
|
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.")
|
log_function("Websocket Server not running, (re)starting.")
|
||||||
self.websockets_server = multiprocessing.Process(
|
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()
|
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.")
|
log_function("Webserver not running, (re)starting.")
|
||||||
self.webserver = multiprocessing.Process(
|
self.webserver = multiprocessing.Process(
|
||||||
target=WebServer, args=(self.player_to_q, self.ui_to_q, self.state)
|
target=WebServer, args=(self.player_to_q, self.ui_to_q, self.state)
|
||||||
)
|
)
|
||||||
self.webserver.start()
|
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.")
|
log_function("Controller Handler not running, (re)starting.")
|
||||||
self.controller_handler = multiprocessing.Process(
|
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()
|
self.controller_handler.start()
|
||||||
|
|
||||||
|
@ -179,7 +218,9 @@ class BAPSicleServer:
|
||||||
ProxyManager.register("StateManager", StateManager)
|
ProxyManager.register("StateManager", StateManager)
|
||||||
manager = ProxyManager()
|
manager = ProxyManager()
|
||||||
manager.start()
|
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")
|
self.state.update("running_state", "running")
|
||||||
|
|
||||||
|
@ -203,8 +244,16 @@ class BAPSicleServer:
|
||||||
self.controller_to_q.append(multiprocessing.Queue())
|
self.controller_to_q.append(multiprocessing.Queue())
|
||||||
self.file_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(
|
||||||
print("The Server UI is available at http://{}:{}".format(self.state.get()["host"], self.state.get()["port"]))
|
"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.
|
# TODO Move this to player or installer.
|
||||||
if False:
|
if False:
|
||||||
|
|
3
setup.py
3
setup.py
|
@ -7,4 +7,5 @@ setup(
|
||||||
description=package.DESCRIPTION,
|
description=package.DESCRIPTION,
|
||||||
author=package.AUTHOR,
|
author=package.AUTHOR,
|
||||||
license=package.LICENSE,
|
license=package.LICENSE,
|
||||||
packages=find_packages())
|
packages=find_packages(),
|
||||||
|
)
|
||||||
|
|
|
@ -68,7 +68,9 @@ class TestPlayer(unittest.TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
cls.logger = LoggingManager("Test_Player")
|
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
|
# clean up logic for the test suite declared in the test module
|
||||||
# code that is executed after all tests in one test run
|
# 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_from_q = multiprocessing.Queue()
|
||||||
self.player_to_q = multiprocessing.Queue()
|
self.player_to_q = multiprocessing.Queue()
|
||||||
self.player = multiprocessing.Process(
|
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.player.start()
|
||||||
self._send_msg_wait_OKAY("CLEAR") # Empty any previous track items.
|
self._send_msg_wait_OKAY("CLEAR") # Empty any previous track items.
|
||||||
|
@ -125,7 +128,7 @@ class TestPlayer(unittest.TestCase):
|
||||||
source = response[: response.index(":")]
|
source = response[: response.index(":")]
|
||||||
if source in sources_filter:
|
if source in sources_filter:
|
||||||
return response[
|
return response[
|
||||||
len(source + ":" + msg) + 1:
|
len(source + ":" + msg) + 1 :
|
||||||
] # +1 to remove trailing : on source.
|
] # +1 to remove trailing : on source.
|
||||||
except Empty:
|
except Empty:
|
||||||
pass
|
pass
|
||||||
|
@ -339,9 +342,13 @@ class TestPlayer(unittest.TestCase):
|
||||||
# Now test that all the markers we setup are present.
|
# Now test that all the markers we setup are present.
|
||||||
item = json_obj["show_plan"][0]
|
item = json_obj["show_plan"][0]
|
||||||
self.assertEqual(item["weight"], 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(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]
|
item = json_obj["show_plan"][1]
|
||||||
self.assertEqual(item["weight"], 1)
|
self.assertEqual(item["weight"], 1)
|
||||||
|
@ -355,7 +362,9 @@ class TestPlayer(unittest.TestCase):
|
||||||
self.assertEqual(item["intro"], 0.0)
|
self.assertEqual(item["intro"], 0.0)
|
||||||
self.assertEqual(item["outro"], 0.0)
|
self.assertEqual(item["outro"], 0.0)
|
||||||
self.assertEqual(item["cue"], 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
|
# TODO: Now test editing/deleting them
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,11 @@ from time import sleep
|
||||||
import json
|
import json
|
||||||
import os
|
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.logging_manager import LoggingManager
|
||||||
from helpers.device_manager import DeviceManager
|
from helpers.device_manager import DeviceManager
|
||||||
from helpers.state_manager import StateManager
|
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.normalisation import get_normalised_filename_if_available
|
||||||
from helpers.myradio_api import MyRadioAPI
|
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_FILEPATH = resolve_external_file_path("logs")
|
||||||
LOG_FILENAME = LOG_FILEPATH + "/WebServer.log"
|
LOG_FILENAME = LOG_FILEPATH + "/WebServer.log"
|
||||||
|
@ -52,17 +59,17 @@ LOGGING_CONFIG = dict(
|
||||||
"file": {
|
"file": {
|
||||||
"class": "logging.FileHandler",
|
"class": "logging.FileHandler",
|
||||||
"formatter": "generic",
|
"formatter": "generic",
|
||||||
"filename": LOG_FILENAME
|
"filename": LOG_FILENAME,
|
||||||
},
|
},
|
||||||
"error_file": {
|
"error_file": {
|
||||||
"class": "logging.FileHandler",
|
"class": "logging.FileHandler",
|
||||||
"formatter": "generic",
|
"formatter": "generic",
|
||||||
"filename": LOG_FILENAME
|
"filename": LOG_FILENAME,
|
||||||
},
|
},
|
||||||
"access_file": {
|
"access_file": {
|
||||||
"class": "logging.FileHandler",
|
"class": "logging.FileHandler",
|
||||||
"formatter": "access",
|
"formatter": "access",
|
||||||
"filename": LOG_FILENAME
|
"filename": LOG_FILENAME,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
formatters={
|
formatters={
|
||||||
|
@ -113,7 +120,7 @@ def ui_index(request):
|
||||||
"server_build": config["server_build"],
|
"server_build": config["server_build"],
|
||||||
"server_name": config["server_name"],
|
"server_name": config["server_name"],
|
||||||
"server_beta": config["server_beta"],
|
"server_beta": config["server_beta"],
|
||||||
"server_branch": config["server_branch"]
|
"server_branch": config["server_branch"],
|
||||||
}
|
}
|
||||||
return render_template("index.html", data=data)
|
return render_template("index.html", data=data)
|
||||||
|
|
||||||
|
@ -124,8 +131,7 @@ def ui_status(request):
|
||||||
for i in range(server_state.get()["num_channels"]):
|
for i in range(server_state.get()["num_channels"]):
|
||||||
channel_states.append(status(i))
|
channel_states.append(status(i))
|
||||||
|
|
||||||
data = {"channels": channel_states,
|
data = {"channels": channel_states, "ui_page": "status", "ui_title": "Status"}
|
||||||
"ui_page": "status", "ui_title": "Status"}
|
|
||||||
return render_template("status.html", data=data)
|
return render_template("status.html", data=data)
|
||||||
|
|
||||||
|
|
||||||
|
@ -153,7 +159,7 @@ def ui_config_server(request):
|
||||||
"ui_title": "Server Config",
|
"ui_title": "Server Config",
|
||||||
"state": server_state.get(),
|
"state": server_state.get(),
|
||||||
"ser_ports": DeviceManager.getSerialPorts(),
|
"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)
|
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_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_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"))
|
server_state.update("tracklist_mode", request.form.get("tracklist_mode"))
|
||||||
|
|
||||||
return redirect("/restart")
|
return redirect("/restart")
|
||||||
|
@ -192,11 +200,7 @@ def ui_logs_list(request):
|
||||||
log_files.append(file.rstrip(".log"))
|
log_files.append(file.rstrip(".log"))
|
||||||
|
|
||||||
log_files.sort()
|
log_files.sort()
|
||||||
data = {
|
data = {"ui_page": "logs", "ui_title": "Logs", "logs": log_files}
|
||||||
"ui_page": "logs",
|
|
||||||
"ui_title": "Logs",
|
|
||||||
"logs": log_files
|
|
||||||
}
|
|
||||||
return render_template("loglist.html", data=data)
|
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))
|
log_file = open(resolve_external_file_path("/logs/{}.log").format(path))
|
||||||
data = {
|
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_page": "logs",
|
||||||
"ui_title": "Logs - {}".format(path),
|
"ui_title": "Logs - {}".format(path),
|
||||||
"page": page
|
"page": page,
|
||||||
}
|
}
|
||||||
log_file.close()
|
log_file.close()
|
||||||
return render_template("log.html", data=data)
|
return render_template("log.html", data=data)
|
||||||
|
@ -296,6 +302,7 @@ def player_all_stop(request):
|
||||||
|
|
||||||
# Show Plan Functions
|
# Show Plan Functions
|
||||||
|
|
||||||
|
|
||||||
@app.route("/plan/load/<timeslotid:int>")
|
@app.route("/plan/load/<timeslotid:int>")
|
||||||
def plan_load(request, timeslotid: int):
|
def plan_load(request, timeslotid: int):
|
||||||
|
|
||||||
|
@ -314,6 +321,7 @@ def plan_clear(request):
|
||||||
|
|
||||||
# API Proxy Endpoints
|
# API Proxy Endpoints
|
||||||
|
|
||||||
|
|
||||||
@app.route("/plan/list")
|
@app.route("/plan/list")
|
||||||
async def api_list_showplans(request):
|
async def api_list_showplans(request):
|
||||||
|
|
||||||
|
@ -323,7 +331,11 @@ async def api_list_showplans(request):
|
||||||
@app.route("/library/search/track")
|
@app.route("/library/search/track")
|
||||||
async def api_search_library(request):
|
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>")
|
@app.route("/library/playlists/<type:string>")
|
||||||
|
@ -368,7 +380,7 @@ def json_status(request):
|
||||||
async def audio_file(request, type: str, id: int):
|
async def audio_file(request, type: str, id: int):
|
||||||
if type not in ["managed", "track"]:
|
if type not in ["managed", "track"]:
|
||||||
abort(404)
|
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.
|
# Swap with a normalised version if it's ready, else returns original.
|
||||||
filename = get_normalised_filename_if_available(filename)
|
filename = get_normalised_filename_if_available(filename)
|
||||||
|
@ -378,18 +390,25 @@ async def audio_file(request, type: str, id: int):
|
||||||
|
|
||||||
|
|
||||||
# Static Files
|
# 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")
|
app.static("/static", resolve_local_file_path("ui-static"), name="ui-static")
|
||||||
|
|
||||||
|
|
||||||
dist_directory = resolve_local_file_path("presenter-build")
|
dist_directory = resolve_local_file_path("presenter-build")
|
||||||
app.static('/presenter', dist_directory)
|
app.static("/presenter", dist_directory)
|
||||||
app.static("/presenter/", resolve_local_file_path("presenter-build/index.html"),
|
app.static(
|
||||||
strict_slashes=True, name="presenter-index")
|
"/presenter/",
|
||||||
|
resolve_local_file_path("presenter-build/index.html"),
|
||||||
|
strict_slashes=True,
|
||||||
|
name="presenter-index",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# Helper Functions
|
# Helper Functions
|
||||||
|
|
||||||
|
|
||||||
def status(channel: int):
|
def status(channel: int):
|
||||||
while not player_from_q[channel].empty():
|
while not player_from_q[channel].empty():
|
||||||
player_from_q[channel].get() # Just waste any previous status responses.
|
player_from_q[channel].get() # Just waste any previous status responses.
|
||||||
|
@ -402,7 +421,7 @@ def status(channel: int):
|
||||||
if response.startswith("UI:STATUS:"):
|
if response.startswith("UI:STATUS:"):
|
||||||
response = response.split(":", 2)[2]
|
response = response.split(":", 2)[2]
|
||||||
# TODO: Handle OKAY / FAIL
|
# TODO: Handle OKAY / FAIL
|
||||||
response = response[response.index(":") + 1:]
|
response = response[response.index(":") + 1 :]
|
||||||
try:
|
try:
|
||||||
response = json.loads(response)
|
response = json.loads(response)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -416,6 +435,7 @@ def status(channel: int):
|
||||||
|
|
||||||
sleep(0.02)
|
sleep(0.02)
|
||||||
|
|
||||||
|
|
||||||
# WebServer Start / Stop Functions
|
# WebServer Start / Stop Functions
|
||||||
|
|
||||||
|
|
||||||
|
@ -428,7 +448,7 @@ def quit(request):
|
||||||
"ui_title": "Quitting BAPSicle",
|
"ui_title": "Quitting BAPSicle",
|
||||||
"title": "See you later!",
|
"title": "See you later!",
|
||||||
"ui_menu": False,
|
"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)
|
return render_template("message.html", data)
|
||||||
|
|
||||||
|
@ -444,7 +464,7 @@ def restart(request):
|
||||||
"ui_menu": False,
|
"ui_menu": False,
|
||||||
"message": "Just putting BAPSicle back in the freezer for a moment!",
|
"message": "Just putting BAPSicle back in the freezer for a moment!",
|
||||||
"redirect_to": "/",
|
"redirect_to": "/",
|
||||||
"redirect_wait_ms": 10000
|
"redirect_wait_ms": 10000,
|
||||||
}
|
}
|
||||||
return render_template("message.html", data)
|
return render_template("message.html", data)
|
||||||
|
|
||||||
|
@ -467,13 +487,15 @@ def WebServer(player_to: List[Queue], player_from: List[Queue], state: StateMana
|
||||||
terminate = Terminator()
|
terminate = Terminator()
|
||||||
while not terminate.terminate:
|
while not terminate.terminate:
|
||||||
try:
|
try:
|
||||||
sync(app.run(
|
sync(
|
||||||
|
app.run(
|
||||||
host=server_state.get()["host"],
|
host=server_state.get()["host"],
|
||||||
port=server_state.get()["port"],
|
port=server_state.get()["port"],
|
||||||
debug=(not isBundelled()),
|
debug=(not isBundelled()),
|
||||||
auto_reload=False,
|
auto_reload=False,
|
||||||
access_log=(not isBundelled())
|
access_log=(not isBundelled()),
|
||||||
))
|
)
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
break
|
break
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
|
|
|
@ -74,9 +74,7 @@ class WebsocketServer:
|
||||||
self.from_webstudio = asyncio.create_task(self.handle_from_webstudio(websocket))
|
self.from_webstudio = asyncio.create_task(self.handle_from_webstudio(websocket))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.threads = await shield(
|
self.threads = await shield(asyncio.gather(self.from_webstudio))
|
||||||
asyncio.gather(self.from_webstudio)
|
|
||||||
)
|
|
||||||
finally:
|
finally:
|
||||||
self.from_webstudio.cancel()
|
self.from_webstudio.cancel()
|
||||||
|
|
||||||
|
@ -92,13 +90,10 @@ class WebsocketServer:
|
||||||
channel = int(data["channel"])
|
channel = int(data["channel"])
|
||||||
self.sendCommand(channel, data)
|
self.sendCommand(channel, data)
|
||||||
|
|
||||||
await asyncio.wait(
|
await asyncio.wait([conn.send(message) for conn in self.baps_clients])
|
||||||
[conn.send(message) for conn in self.baps_clients]
|
|
||||||
)
|
|
||||||
|
|
||||||
except websockets.exceptions.ConnectionClosedError as e:
|
except websockets.exceptions.ConnectionClosedError as e:
|
||||||
self.logger.log.error(
|
self.logger.log.error("Client Disconncted {}, {}".format(websocket, e))
|
||||||
"Client Disconncted {}, {}".format(websocket, e))
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.log.exception(
|
self.logger.log.exception(
|
||||||
|
@ -152,8 +147,7 @@ class WebsocketServer:
|
||||||
extra += str(data["timeslotId"])
|
extra += str(data["timeslotId"])
|
||||||
elif command == "SETMARKER":
|
elif command == "SETMARKER":
|
||||||
extra += "{}:{}".format(
|
extra += "{}:{}".format(
|
||||||
data["timeslotitemid"],
|
data["timeslotitemid"], json.dumps(data["marker"])
|
||||||
json.dumps(data["marker"])
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO: Move this to player handler.
|
# TODO: Move this to player handler.
|
||||||
|
@ -174,21 +168,22 @@ class WebsocketServer:
|
||||||
|
|
||||||
# Now send the special case.
|
# Now send the special case.
|
||||||
self.channel_to_q[new_channel].put(
|
self.channel_to_q[new_channel].put(
|
||||||
"WEBSOCKET:ADD:" + json.dumps(item))
|
"WEBSOCKET:ADD:" + json.dumps(item)
|
||||||
|
)
|
||||||
|
|
||||||
# Don't bother, we should be done.
|
# Don't bother, we should be done.
|
||||||
return
|
return
|
||||||
|
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
self.logger.log.exception(
|
self.logger.log.exception(
|
||||||
"Error decoding extra data {} for command {} ".format(
|
"Error decoding extra data {} for command {} ".format(e, command)
|
||||||
e, command
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Stick the message together and send!
|
# 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 != "":
|
if extra != "":
|
||||||
message += ":" + extra
|
message += ":" + extra
|
||||||
|
|
||||||
|
@ -202,9 +197,7 @@ class WebsocketServer:
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.logger.log.error(
|
self.logger.log.error("Command missing from message. Data: {}".format(data))
|
||||||
"Command missing from message. Data: {}".format(data)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def handle_to_webstudio(self):
|
async def handle_to_webstudio(self):
|
||||||
|
|
||||||
|
@ -244,9 +237,7 @@ class WebsocketServer:
|
||||||
data = json.dumps(
|
data = json.dumps(
|
||||||
{"command": command, "data": message, "channel": channel}
|
{"command": command, "data": message, "channel": channel}
|
||||||
)
|
)
|
||||||
await asyncio.wait(
|
await asyncio.wait([conn.send(data) for conn in self.baps_clients])
|
||||||
[conn.send(data) for conn in self.baps_clients]
|
|
||||||
)
|
|
||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
continue
|
continue
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
|
Loading…
Reference in a new issue